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
@@ -206,5 +206,10 @@ Examples:
206
206
  await handleRethink(trimmed, ctx, pi);
207
207
  return true;
208
208
  }
209
+ if (trimmed === "codebase" || trimmed.startsWith("codebase ")) {
210
+ const { handleCodebase } = await import("../../commands-codebase.js");
211
+ await handleCodebase(trimmed.replace(/^codebase\s*/, "").trim(), ctx, pi);
212
+ return true;
213
+ }
209
214
  return false;
210
215
  }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * GSD Command — /gsd codebase
3
+ *
4
+ * Generate and manage the codebase map (.gsd/CODEBASE.md).
5
+ * Subcommands: generate, update, stats, help
6
+ */
7
+
8
+ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
9
+
10
+ import {
11
+ generateCodebaseMap,
12
+ updateCodebaseMap,
13
+ writeCodebaseMap,
14
+ getCodebaseMapStats,
15
+ readCodebaseMap,
16
+ } from "./codebase-generator.js";
17
+
18
+ const USAGE =
19
+ "Usage: /gsd codebase [generate|update|stats]\n\n" +
20
+ " generate [--max-files N] — Generate or regenerate CODEBASE.md\n" +
21
+ " update — Incremental update (preserves descriptions)\n" +
22
+ " stats — Show file count, coverage, and generation time\n" +
23
+ " help — Show this help\n\n" +
24
+ "With no subcommand, shows stats if a map exists or help if not.";
25
+
26
+ export async function handleCodebase(
27
+ args: string,
28
+ ctx: ExtensionCommandContext,
29
+ _pi: ExtensionAPI,
30
+ ): Promise<void> {
31
+ const basePath = process.cwd();
32
+ const parts = args.trim().split(/\s+/);
33
+ const sub = parts[0] ?? "";
34
+
35
+ switch (sub) {
36
+ case "generate": {
37
+ const maxFiles = parseMaxFiles(args, ctx);
38
+ if (maxFiles === false) return; // validation failed, message already shown
39
+
40
+ const existing = readCodebaseMap(basePath);
41
+ const existingDescriptions = existing
42
+ ? (await import("./codebase-generator.js")).parseCodebaseMap(existing)
43
+ : undefined;
44
+
45
+ const result = generateCodebaseMap(basePath, { maxFiles: maxFiles ?? undefined }, existingDescriptions);
46
+
47
+ if (result.fileCount === 0) {
48
+ ctx.ui.notify(
49
+ "Codebase map generated with 0 files.\n" +
50
+ "Is this a git repository? Run 'git ls-files' to verify.",
51
+ "warning",
52
+ );
53
+ return;
54
+ }
55
+
56
+ const outPath = writeCodebaseMap(basePath, result.content);
57
+ ctx.ui.notify(
58
+ `Codebase map generated: ${result.fileCount} files\n` +
59
+ `Written to: ${outPath}` +
60
+ (result.truncated ? `\n⚠ Truncated — increase --max-files to include all files` : ""),
61
+ "success",
62
+ );
63
+ return;
64
+ }
65
+
66
+ case "update": {
67
+ const existing = readCodebaseMap(basePath);
68
+ if (!existing) {
69
+ ctx.ui.notify(
70
+ "No codebase map found. Run /gsd codebase generate to create one.",
71
+ "warning",
72
+ );
73
+ return;
74
+ }
75
+
76
+ const maxFiles = parseMaxFiles(args, ctx);
77
+ if (maxFiles === false) return;
78
+
79
+ const result = updateCodebaseMap(basePath, { maxFiles: maxFiles ?? undefined });
80
+ writeCodebaseMap(basePath, result.content);
81
+
82
+ ctx.ui.notify(
83
+ `Codebase map updated: ${result.fileCount} files\n` +
84
+ ` Added: ${result.added} | Removed: ${result.removed} | Unchanged: ${result.unchanged}` +
85
+ (result.truncated ? `\n⚠ Truncated — increase --max-files to include all files` : ""),
86
+ "success",
87
+ );
88
+ return;
89
+ }
90
+
91
+ case "stats": {
92
+ showStats(basePath, ctx);
93
+ return;
94
+ }
95
+
96
+ case "help":
97
+ ctx.ui.notify(USAGE, "info");
98
+ return;
99
+
100
+ case "": {
101
+ // Safe default: show stats if map exists, help if not
102
+ const existing = readCodebaseMap(basePath);
103
+ if (existing) {
104
+ showStats(basePath, ctx);
105
+ } else {
106
+ ctx.ui.notify(USAGE, "info");
107
+ }
108
+ return;
109
+ }
110
+
111
+ default:
112
+ ctx.ui.notify(
113
+ `Unknown subcommand "${sub}".\n\n${USAGE}`,
114
+ "warning",
115
+ );
116
+ }
117
+ }
118
+
119
+ function showStats(basePath: string, ctx: ExtensionCommandContext): void {
120
+ const stats = getCodebaseMapStats(basePath);
121
+ if (!stats.exists) {
122
+ ctx.ui.notify("No codebase map found. Run /gsd codebase generate to create one.", "info");
123
+ return;
124
+ }
125
+
126
+ const coverage = stats.fileCount > 0
127
+ ? Math.round((stats.describedCount / stats.fileCount) * 100)
128
+ : 0;
129
+
130
+ ctx.ui.notify(
131
+ `Codebase Map Stats:\n` +
132
+ ` Files: ${stats.fileCount}\n` +
133
+ ` Described: ${stats.describedCount} (${coverage}%)\n` +
134
+ ` Undescribed: ${stats.undescribedCount}\n` +
135
+ ` Generated: ${stats.generatedAt ?? "unknown"}\n\n` +
136
+ (stats.undescribedCount > 0
137
+ ? `Tip: Run /gsd codebase update to refresh after file changes.`
138
+ : `Coverage is complete.`),
139
+ "info",
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Parse and validate --max-files flag.
145
+ * Returns the parsed number, undefined if flag not present, or false if invalid.
146
+ */
147
+ function parseMaxFiles(args: string, ctx: ExtensionCommandContext): number | undefined | false {
148
+ const maxFilesStr = extractFlag(args, "--max-files");
149
+ if (!maxFilesStr) return undefined;
150
+
151
+ const maxFiles = parseInt(maxFilesStr, 10);
152
+ if (isNaN(maxFiles) || maxFiles < 1) {
153
+ ctx.ui.notify("--max-files must be a positive integer (e.g. --max-files 200).", "warning");
154
+ return false;
155
+ }
156
+ return maxFiles;
157
+ }
158
+
159
+ function extractFlag(args: string, flag: string): string | undefined {
160
+ const escaped = flag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
161
+ const regex = new RegExp(`${escaped}[=\\s]+(\\S+)`);
162
+ const match = args.match(regex);
163
+ return match?.[1];
164
+ }
@@ -184,11 +184,23 @@ export function buildCategorySummaries(prefs: Record<string, unknown>): Record<s
184
184
 
185
185
  // Git
186
186
  const git = prefs.git as Record<string, unknown> | undefined;
187
+ const staleThreshold = prefs.stale_commit_threshold_minutes;
188
+ const absorbSnapshots = git?.absorb_snapshot_commits;
187
189
  let gitSummary = "(defaults)";
188
- if (git && Object.keys(git).length > 0) {
189
- const branch = git.main_branch ?? "main";
190
- const push = git.auto_push ? "on" : "off";
191
- gitSummary = `main: ${branch}, push: ${push}`;
190
+ {
191
+ const parts: string[] = [];
192
+ if (git && Object.keys(git).length > 0) {
193
+ const branch = git.main_branch ?? "main";
194
+ const push = git.auto_push ? "on" : "off";
195
+ parts.push(`main: ${branch}, push: ${push}`);
196
+ }
197
+ if (staleThreshold !== undefined) {
198
+ parts.push(`stale: ${staleThreshold === 0 ? "off" : `${staleThreshold}m`}`);
199
+ }
200
+ if (absorbSnapshots !== undefined) {
201
+ parts.push(`absorb: ${absorbSnapshots ? "on" : "off"}`);
202
+ }
203
+ if (parts.length > 0) gitSummary = parts.join(", ");
192
204
  }
193
205
 
194
206
  // Skills
@@ -469,9 +481,39 @@ async function configureGit(ctx: ExtensionCommandContext, prefs: Record<string,
469
481
  git.isolation = isolationChoice;
470
482
  }
471
483
 
484
+ // absorb_snapshot_commits (git sub-key)
485
+ const currentAbsorb = git.absorb_snapshot_commits;
486
+ const absorbStr = currentAbsorb !== undefined ? String(currentAbsorb) : "";
487
+ const absorbChoice = await ctx.ui.select(
488
+ `Absorb snapshot commits into real commits${absorbStr ? ` (current: ${absorbStr})` : " (default: true)"}:`,
489
+ ["true", "false", "(keep current)"],
490
+ );
491
+ if (absorbChoice && absorbChoice !== "(keep current)") {
492
+ git.absorb_snapshot_commits = absorbChoice === "true";
493
+ }
494
+
472
495
  if (Object.keys(git).length > 0) {
473
496
  prefs.git = git;
474
497
  }
498
+
499
+ // stale_commit_threshold_minutes (top-level pref, shown in Git section)
500
+ const currentThreshold = prefs.stale_commit_threshold_minutes;
501
+ const thresholdStr = currentThreshold !== undefined ? String(currentThreshold) : "";
502
+ const thresholdInput = await ctx.ui.input(
503
+ `Stale commit threshold (minutes, 0 to disable)${thresholdStr ? ` (current: ${thresholdStr})` : " (default: 30)"}:`,
504
+ thresholdStr || "30",
505
+ );
506
+ if (thresholdInput !== null && thresholdInput !== undefined) {
507
+ const val = thresholdInput.trim();
508
+ const parsed = tryParseInteger(val);
509
+ if (val && parsed !== null && parsed >= 0) {
510
+ prefs.stale_commit_threshold_minutes = parsed;
511
+ } else if (val && parsed === null) {
512
+ ctx.ui.notify(`Invalid value "${val}" — must be a whole number. Keeping previous value.`, "warning");
513
+ } else if (!val && currentThreshold !== undefined) {
514
+ delete prefs.stale_commit_threshold_minutes;
515
+ }
516
+ }
475
517
  }
476
518
 
477
519
  async function configureSkills(ctx: ExtensionCommandContext, prefs: Record<string, unknown>): Promise<void> {
@@ -35,15 +35,17 @@ const UNIT_TYPE_TIERS: Record<string, ComplexityTier> = {
35
35
  "complete-slice": "light",
36
36
  "run-uat": "light",
37
37
 
38
- // Tier 2 — Standard: research, routine planning, discussion
38
+ // Tier 2 — Standard: research, routine discussion
39
39
  "discuss-milestone": "standard",
40
40
  "discuss-slice": "standard",
41
41
  "research-milestone": "standard",
42
42
  "research-slice": "standard",
43
- "plan-milestone": "standard",
44
- "plan-slice": "standard",
45
43
 
46
- // Tier 3 — Heavy: execution, replanning (requires deep reasoning)
44
+ // Tier 3 — Heavy: planning, execution, replanning (requires deep reasoning)
45
+ // Planning is heavy so it uses the best configured model (e.g. Opus) and is
46
+ // not downgraded by dynamic routing when a capable model is configured.
47
+ "plan-milestone": "heavy",
48
+ "plan-slice": "heavy",
47
49
  "execute-task": "standard", // default standard, upgraded by metadata
48
50
  "replan-slice": "heavy",
49
51
  "reassess-roadmap": "heavy",
@@ -185,8 +187,8 @@ function analyzePlanComplexity(
185
187
  // Check if this is a milestone-level plan (more complex) vs single slice
186
188
  const { milestone: mid, slice: sid } = parseUnitId(unitId);
187
189
  if (!sid) {
188
- // Milestone-level planning is always at least standard
189
- return { tier: "standard", reason: "milestone-level planning" };
190
+ // Milestone-level planning is always heavy requires full context and best model
191
+ return { tier: "heavy", reason: "milestone-level planning" };
190
192
  }
191
193
 
192
194
  // For slice planning, try to read the context/research to gauge complexity
@@ -10,7 +10,7 @@ import { deriveState, isMilestoneComplete } from "./state.js";
10
10
  import { listWorktrees, resolveGitDir, worktreesDir } from "./worktree-manager.js";
11
11
  import { abortAndReset } from "./git-self-heal.js";
12
12
  import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
13
- import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached } from "./native-git-bridge.js";
13
+ import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
14
14
  import { getAllWorktreeHealth } from "./worktree-health.js";
15
15
  import { loadEffectiveGSDPreferences } from "./preferences.js";
16
16
 
@@ -363,6 +363,54 @@ export async function checkGitHealth(
363
363
  // Non-fatal — orphaned worktree directory check failed
364
364
  }
365
365
 
366
+ // ── Stale uncommitted changes ────────────────────────────────────────────
367
+ // If the working tree has uncommitted changes and the last commit was
368
+ // longer ago than the configured threshold, flag it and optionally
369
+ // auto-commit a safety snapshot so work isn't lost.
370
+ try {
371
+ const prefs = loadEffectiveGSDPreferences()?.preferences ?? {};
372
+ const thresholdMinutes = prefs.stale_commit_threshold_minutes ?? 30;
373
+
374
+ if (thresholdMinutes > 0) {
375
+ const dirty = nativeHasChanges(basePath);
376
+ if (dirty) {
377
+ const branch = nativeGetCurrentBranch(basePath);
378
+ const lastEpoch = nativeLastCommitEpoch(basePath, branch || "HEAD");
379
+ const nowEpoch = Math.floor(Date.now() / 1000);
380
+ const minutesSinceCommit = lastEpoch > 0 ? (nowEpoch - lastEpoch) / 60 : Infinity;
381
+
382
+ if (minutesSinceCommit >= thresholdMinutes) {
383
+ const mins = Math.floor(minutesSinceCommit);
384
+ issues.push({
385
+ severity: "warning",
386
+ code: "stale_uncommitted_changes",
387
+ scope: "project",
388
+ unitId: "project",
389
+ message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting tracked files.`,
390
+ fixable: true,
391
+ });
392
+
393
+ if (shouldFix("stale_uncommitted_changes")) {
394
+ try {
395
+ nativeAddTracked(basePath);
396
+ const commitMsg = `gsd snapshot: uncommitted changes after ${mins}m inactivity`;
397
+ const result = nativeCommit(basePath, commitMsg);
398
+ if (result) {
399
+ fixesApplied.push(`created gsd snapshot after ${mins}m of uncommitted changes`);
400
+ } else {
401
+ fixesApplied.push("gsd snapshot skipped — nothing to commit after staging tracked files");
402
+ }
403
+ } catch {
404
+ fixesApplied.push("failed to create gsd snapshot commit");
405
+ }
406
+ }
407
+ }
408
+ }
409
+ }
410
+ } catch {
411
+ // Non-fatal — stale commit check failed
412
+ }
413
+
366
414
  // ── Worktree lifecycle checks ──────────────────────────────────────────
367
415
  // Check GSD-managed worktrees for: merged branches, stale work, dirty
368
416
  // state, and unpushed commits. Only worktrees under .gsd/worktrees/.
@@ -22,7 +22,7 @@ import { abortAndReset } from "./git-self-heal.js";
22
22
  import { rebuildState } from "./doctor.js";
23
23
  import { deriveState } from "./state.js";
24
24
  import { resolveMilestoneIntegrationBranch } from "./git-service.js";
25
- import { nativeIsRepo } from "./native-git-bridge.js";
25
+ import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
26
26
  import { loadEffectiveGSDPreferences } from "./preferences.js";
27
27
  import { runEnvironmentChecks } from "./doctor-environment.js";
28
28
 
@@ -295,6 +295,40 @@ export async function preDispatchHealthGate(basePath: string): Promise<PreDispat
295
295
  // Non-fatal — dispatch continues if state/branch check fails
296
296
  }
297
297
 
298
+ // ── Stale uncommitted changes — auto-snapshot before dispatch ──
299
+ // If the working tree is dirty and no commit has happened recently,
300
+ // create a safety snapshot so work isn't lost if the next unit crashes.
301
+ try {
302
+ if (nativeIsRepo(basePath)) {
303
+ const prefs = loadEffectiveGSDPreferences()?.preferences ?? {};
304
+ const thresholdMinutes = prefs.stale_commit_threshold_minutes ?? 30;
305
+
306
+ if (thresholdMinutes > 0 && nativeHasChanges(basePath)) {
307
+ const branch = nativeGetCurrentBranch(basePath);
308
+ const lastEpoch = nativeLastCommitEpoch(basePath, branch || "HEAD");
309
+ const nowEpoch = Math.floor(Date.now() / 1000);
310
+ const minutesSinceCommit = lastEpoch > 0 ? (nowEpoch - lastEpoch) / 60 : Infinity;
311
+
312
+ if (minutesSinceCommit >= thresholdMinutes) {
313
+ const mins = Math.floor(minutesSinceCommit);
314
+ try {
315
+ nativeAddTracked(basePath);
316
+ const commitMsg = `gsd snapshot: pre-dispatch, uncommitted changes after ${mins}m inactivity`;
317
+ const result = nativeCommit(basePath, commitMsg);
318
+ if (result) {
319
+ fixesApplied.push(`pre-dispatch: created gsd snapshot after ${mins}m of uncommitted changes`);
320
+ }
321
+ } catch {
322
+ // Non-blocking — snapshot failed but dispatch can continue
323
+ fixesApplied.push("pre-dispatch: gsd snapshot failed");
324
+ }
325
+ }
326
+ }
327
+ }
328
+ } catch {
329
+ // Non-fatal
330
+ }
331
+
298
332
  // ── Disk space check ──
299
333
  // Catches low-disk conditions before dispatch rather than letting the unit
300
334
  // fail mid-execution with ENOSPC (which wastes a full LLM turn).
@@ -61,6 +61,8 @@ export type DoctorIssueCode =
61
61
  | "worktree_stale"
62
62
  | "worktree_dirty"
63
63
  | "worktree_unpushed"
64
+ // Stale commit safety check
65
+ | "stale_uncommitted_changes"
64
66
  // Snapshot ref bloat
65
67
  | "snapshot_ref_bloat"
66
68
  // Runtime data integrity
@@ -48,7 +48,9 @@ const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fet
48
48
  const SERVER_RE = /internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable/i;
49
49
  // ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
50
50
  const CONNECTION_RE = /terminated|connection.?refused|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
51
- const STREAM_RE = /Unexpected end of JSON|Unexpected token.*JSON|Expected.*in JSON|Unterminated.*in JSON|SyntaxError.*JSON/i;
51
+ // Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
52
+ // This eliminates the need to enumerate every error message variant individually.
53
+ const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
52
54
  const RESET_DELAY_RE = /reset in (\d+)s/i;
53
55
 
54
56
  /**
@@ -91,9 +93,6 @@ export function classifyError(errorMsg: string, retryAfterMs?: number): ErrorCla
91
93
  }
92
94
 
93
95
  // 4. Stream truncation — downstream symptom of connection drop
94
- // Checked before server/connection because JSON parse errors can contain
95
- // substrings like "position 500" (matches SERVER_RE) or "Unterminated"
96
- // (matches CONNECTION_RE's "terminated" pattern).
97
96
  if (STREAM_RE.test(errorMsg)) {
98
97
  return { kind: "stream", retryAfterMs: retryAfterMs ?? 15_000 };
99
98
  }
@@ -32,6 +32,8 @@ import {
32
32
  nativeRmCached,
33
33
  nativeUpdateRef,
34
34
  nativeAddPaths,
35
+ nativeResetSoft,
36
+ nativeCommitSubject,
35
37
  } from "./native-git-bridge.js";
36
38
  import { GSDError, GSD_MERGE_CONFLICT, GSD_GIT_ERROR } from "./errors.js";
37
39
  import { getErrorMessage } from "./error-utils.js";
@@ -77,6 +79,11 @@ export interface GitPreferences {
77
79
  * Default: the main branch (from `main_branch` or auto-detected).
78
80
  */
79
81
  pr_target_branch?: string;
82
+ /** Whether to squash `gsd snapshot:` commits into the next real autoCommit.
83
+ * Enabled by default. Set to false to keep snapshot commits in history
84
+ * for forensic inspection.
85
+ */
86
+ absorb_snapshot_commits?: boolean;
80
87
  }
81
88
 
82
89
  export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/;
@@ -563,9 +570,95 @@ export class GitServiceImpl {
563
570
  ? buildTaskCommitMessage(taskContext)
564
571
  : `chore: auto-commit after ${unitType}\n\nGSD-Unit: ${unitId}`;
565
572
  nativeCommit(this.basePath, message, { allowEmpty: false });
573
+
574
+ // Absorb any preceding gsd snapshot commits into this real commit.
575
+ // Walk backwards from HEAD~1 counting consecutive snapshot subjects,
576
+ // then soft-reset to before them and re-commit with the same message.
577
+ this.absorbSnapshotCommits(message);
578
+
566
579
  return message;
567
580
  }
568
581
 
582
+ /**
583
+ * Squash consecutive `gsd snapshot:` commits that sit immediately below
584
+ * HEAD into the current HEAD commit. This keeps the git history clean
585
+ * after automated snapshot commits are superseded by real work.
586
+ *
587
+ * Guards:
588
+ * - Opt-in via `absorb_snapshot_commits` preference (default: true).
589
+ * - Refuses to rewrite commits that have been pushed to the remote
590
+ * tracking branch (checks merge-base ancestry).
591
+ * - Saves HEAD SHA before reset; restores it if the re-commit fails.
592
+ *
593
+ * Does nothing if there are no snapshot commits to absorb.
594
+ */
595
+ private absorbSnapshotCommits(headMessage: string): void {
596
+ try {
597
+ // Opt-in guard — users can disable to keep snapshot commits for forensics
598
+ if (this.prefs.absorb_snapshot_commits === false) return;
599
+
600
+ const GSD_SNAPSHOT_PREFIX = "gsd snapshot:";
601
+ let count = 0;
602
+
603
+ // Walk back from HEAD~1 counting consecutive snapshot commits (cap at 10)
604
+ for (let i = 1; i <= 10; i++) {
605
+ const subject = nativeCommitSubject(this.basePath, `HEAD~${i}`);
606
+ if (!subject.startsWith(GSD_SNAPSHOT_PREFIX)) break;
607
+ count = i;
608
+ }
609
+
610
+ if (count === 0) return;
611
+
612
+ // Guard: don't rewrite history that has been pushed to the remote.
613
+ // Check whether the newest snapshot commit (HEAD~1) is already
614
+ // reachable from the remote tracking branch. If it is, the snapshots
615
+ // have been pushed and must not be squashed via local history rewrite.
616
+ // (Checking resetTarget instead would false-positive when the remote
617
+ // is at the pre-snapshot base but the snapshots themselves are local.)
618
+ const resetTarget = `HEAD~${count + 1}`;
619
+ try {
620
+ const branch = nativeGetCurrentBranch(this.basePath);
621
+ if (branch) {
622
+ const remoteBranch = `origin/${branch}`;
623
+ // merge-base --is-ancestor exits 0 if HEAD~1 is ancestor of remote
624
+ execFileSync("git", ["merge-base", "--is-ancestor", "HEAD~1", remoteBranch], {
625
+ cwd: this.basePath,
626
+ stdio: ["ignore", "pipe", "pipe"],
627
+ });
628
+ // If we get here, newest snapshot IS reachable from remote — already pushed
629
+ return;
630
+ }
631
+ } catch {
632
+ // Not an ancestor or remote doesn't exist — safe to proceed
633
+ }
634
+
635
+ // Save HEAD SHA so we can restore if the re-commit fails
636
+ const savedHead = execFileSync("git", ["rev-parse", "HEAD"], {
637
+ cwd: this.basePath,
638
+ stdio: ["ignore", "pipe", "pipe"],
639
+ encoding: "utf-8",
640
+ }).trim();
641
+
642
+ nativeResetSoft(this.basePath, resetTarget);
643
+
644
+ // Re-run smartStage so the same RUNTIME_EXCLUSION_PATHS apply.
645
+ // Snapshot commits used nativeAddTracked (git add -u) which stages
646
+ // ALL tracked modifications including .gsd/ state files. Without
647
+ // re-staging, those .gsd/ changes leak into the absorbed commit.
648
+ this.smartStage();
649
+
650
+ try {
651
+ nativeCommit(this.basePath, headMessage, { allowEmpty: false });
652
+ } catch {
653
+ // Re-commit failed — restore original HEAD to avoid leaving the
654
+ // repo in a partially-reset state with no commit
655
+ nativeResetSoft(this.basePath, savedHead);
656
+ }
657
+ } catch {
658
+ // Non-fatal — if squash fails, the commits remain unsquashed
659
+ }
660
+ }
661
+
569
662
  // ─── Branch Queries ────────────────────────────────────────────────────
570
663
 
571
664
  /**
@@ -680,6 +680,16 @@ export function nativeAddAll(basePath: string): void {
680
680
  gitFileExec(basePath, ["add", "-A"]);
681
681
  }
682
682
 
683
+ /**
684
+ * Stage only already-tracked files (git add -u).
685
+ * Does NOT add new untracked files — only updates modifications and deletions
686
+ * for files git already knows about. Safe for automated snapshots where
687
+ * pulling in unknown untracked files (secrets, binaries) would be dangerous.
688
+ */
689
+ export function nativeAddTracked(basePath: string): void {
690
+ gitFileExec(basePath, ["add", "-u"]);
691
+ }
692
+
683
693
  /**
684
694
  * Stage all files with pathspec exclusions (git add -A -- ':!pattern' ...).
685
695
  * Excluded paths are never hashed by git, preventing hangs on large
@@ -931,6 +941,20 @@ export function nativeResetHard(basePath: string): void {
931
941
  execSync("git reset --hard HEAD", { cwd: basePath, stdio: "pipe" });
932
942
  }
933
943
 
944
+ /**
945
+ * Soft reset to a target ref (git reset --soft <ref>).
946
+ * Moves HEAD to `target` while keeping all changes staged in the index.
947
+ * Used to squash snapshot commits back into a single real commit.
948
+ */
949
+ export function nativeResetSoft(basePath: string, target: string): void {
950
+ execFileSync("git", ["reset", "--soft", target], {
951
+ cwd: basePath,
952
+ stdio: ["ignore", "pipe", "pipe"],
953
+ encoding: "utf-8",
954
+ env: GIT_NO_PROMPT_ENV,
955
+ });
956
+ }
957
+
934
958
  /**
935
959
  * Get the subject line of a commit (git log -1 --format=%s <ref>).
936
960
  * Returns empty string if the ref doesn't exist.
@@ -264,6 +264,7 @@ export const GSD_ROOT_FILES = {
264
264
  REQUIREMENTS: "REQUIREMENTS.md",
265
265
  OVERRIDES: "OVERRIDES.md",
266
266
  KNOWLEDGE: "KNOWLEDGE.md",
267
+ CODEBASE: "CODEBASE.md",
267
268
  } as const;
268
269
 
269
270
  export type GSDRootFileKey = keyof typeof GSD_ROOT_FILES;
@@ -276,6 +277,7 @@ const LEGACY_GSD_ROOT_FILES: Record<GSDRootFileKey, string> = {
276
277
  REQUIREMENTS: "requirements.md",
277
278
  OVERRIDES: "overrides.md",
278
279
  KNOWLEDGE: "knowledge.md",
280
+ CODEBASE: "codebase.md",
279
281
  };
280
282
 
281
283
  // ─── GSD Root Discovery ───────────────────────────────────────────────────────
@@ -93,6 +93,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
93
93
  "service_tier",
94
94
  "forensics_dedup",
95
95
  "show_token_cost",
96
+ "stale_commit_threshold_minutes",
96
97
  "experimental",
97
98
  ]);
98
99
 
@@ -253,6 +254,13 @@ export interface GSDPreferences {
253
254
  forensics_dedup?: boolean;
254
255
  /** Opt-in: show per-prompt and cumulative session token cost in the footer. Default: false. */
255
256
  show_token_cost?: boolean;
257
+ /**
258
+ * Minutes without a commit before flagging uncommitted changes as stale.
259
+ * When the threshold is exceeded and the working tree is dirty, doctor will
260
+ * auto-commit a safety snapshot tagged with `[gsd safety]`. Default: 30.
261
+ * Set to 0 to disable.
262
+ */
263
+ stale_commit_threshold_minutes?: number;
256
264
  /**
257
265
  * Opt-in experimental features. All features here are disabled by default.
258
266
  * See the preferences reference for details on each feature.