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
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ import { shouldRunOnboarding, runOnboarding } from './onboarding.js';
11
11
  import chalk from 'chalk';
12
12
  import { checkForUpdates } from './update-check.js';
13
13
  import { printHelp, printSubcommandHelp } from './help-text.js';
14
+ import { applySecurityOverrides } from './security-overrides.js';
14
15
  import { parseCliArgs as parseWebCliArgs, runWebCliBranch, migrateLegacyFlatSessions, } from './cli-web-branch.js';
15
16
  import { stopWebMode } from './web-mode.js';
16
17
  import { getProjectSessionsDir } from './project-sessions.js';
@@ -281,6 +282,7 @@ const modelsJsonPath = resolveModelsJsonPath();
281
282
  const modelRegistry = new ModelRegistry(authStorage, modelsJsonPath);
282
283
  markStartup('ModelRegistry');
283
284
  const settingsManager = SettingsManager.create(agentDir);
285
+ applySecurityOverrides(settingsManager);
284
286
  markStartup('SettingsManager.create');
285
287
  // Run onboarding wizard on first launch (no LLM provider configured)
286
288
  if (!isPrintMode && shouldRunOnboarding(authStorage, settingsManager.getDefaultProvider())) {
@@ -600,10 +602,19 @@ if (!process.stdin.isTTY || !process.stdout.isTTY) {
600
602
  // Skip when the first-run banner was already printed in loader.ts (prevents double banner).
601
603
  if (!process.env.GSD_FIRST_RUN_BANNER) {
602
604
  const { printWelcomeScreen } = await import('./welcome-screen.js');
605
+ let remoteChannel;
606
+ try {
607
+ const { resolveRemoteConfig } = await import('./resources/extensions/remote-questions/config.js');
608
+ const rc = resolveRemoteConfig();
609
+ if (rc)
610
+ remoteChannel = rc.channel;
611
+ }
612
+ catch { /* non-fatal */ }
603
613
  printWelcomeScreen({
604
614
  version: process.env.GSD_VERSION || '0.0.0',
605
615
  modelName: settingsManager.getDefaultModel() || undefined,
606
616
  provider: settingsManager.getDefaultProvider() || undefined,
617
+ remoteChannel,
607
618
  });
608
619
  }
609
620
  const interactiveMode = new InteractiveMode(session);
@@ -1341,15 +1341,18 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
1341
1341
  catch {
1342
1342
  // Non-fatal — proceed with merge; untracked files may block it
1343
1343
  }
1344
- // 7c. Clean stale MERGE_HEAD before the squash merge (#2912).
1345
- // The native (libgit2) merge path or a prior interrupted merge may leave
1346
- // MERGE_HEAD in the git dir. `git merge --squash` refuses to run when
1347
- // MERGE_HEAD exists, so remove it preemptively.
1344
+ // 7b. Clean up stale merge state before attempting squash merge (#2912).
1345
+ // A leftover MERGE_HEAD (from a previous failed merge, libgit2 native path,
1346
+ // or interrupted operation) causes `git merge --squash` to refuse with
1347
+ // "fatal: You have not concluded your merge (MERGE_HEAD exists)".
1348
+ // Defensively remove merge artifacts before starting.
1348
1349
  try {
1349
- const gitDirPre = resolveGitDir(originalBasePath_);
1350
- const mergeHeadPre = join(gitDirPre, "MERGE_HEAD");
1351
- if (existsSync(mergeHeadPre))
1352
- unlinkSync(mergeHeadPre);
1350
+ const gitDir_ = resolveGitDir(originalBasePath_);
1351
+ for (const f of ["SQUASH_MSG", "MERGE_MSG", "MERGE_HEAD"]) {
1352
+ const p = join(gitDir_, f);
1353
+ if (existsSync(p))
1354
+ unlinkSync(p);
1355
+ }
1353
1356
  }
1354
1357
  catch { /* best-effort */ }
1355
1358
  // 8. Squash merge — auto-resolve .gsd/ state file conflicts (#530)
@@ -42,27 +42,20 @@ export function registerHooks(pi) {
42
42
  if (gsdBinPath) {
43
43
  const { dirname } = await import("node:path");
44
44
  const { printWelcomeScreen } = await import(join(dirname(gsdBinPath), "welcome-screen.js"));
45
- printWelcomeScreen({ version: process.env.GSD_VERSION || "0.0.0" });
45
+ let remoteChannel;
46
+ try {
47
+ const { resolveRemoteConfig } = await import("../../remote-questions/config.js");
48
+ const rc = resolveRemoteConfig();
49
+ if (rc)
50
+ remoteChannel = rc.channel;
51
+ }
52
+ catch { /* non-fatal */ }
53
+ printWelcomeScreen({ version: process.env.GSD_VERSION || "0.0.0", remoteChannel });
46
54
  }
47
55
  }
48
56
  catch { /* non-fatal */ }
49
57
  }
50
58
  loadToolApiKeys();
51
- try {
52
- const [{ getRemoteConfigStatus }, { getLatestPromptSummary }] = await Promise.all([
53
- import("../../remote-questions/config.js"),
54
- import("../../remote-questions/status.js"),
55
- ]);
56
- const status = getRemoteConfigStatus();
57
- const latest = getLatestPromptSummary();
58
- if (!status.includes("not configured")) {
59
- const suffix = latest ? `\nLast remote prompt: ${latest.id} (${latest.status})` : "";
60
- ctx.ui.notify(`${status}${suffix}`, status.includes("disabled") ? "warning" : "info");
61
- }
62
- }
63
- catch {
64
- // ignore
65
- }
66
59
  });
67
60
  pi.on("session_switch", async (_event, ctx) => {
68
61
  resetWriteGateState();
@@ -71,12 +71,33 @@ export async function buildBeforeAgentStartResult(event, ctx) {
71
71
  newSkillsBlock = formatSkillsXml(newSkills);
72
72
  }
73
73
  }
74
+ let codebaseBlock = "";
75
+ const codebasePath = resolveGsdRootFile(process.cwd(), "CODEBASE");
76
+ if (existsSync(codebasePath)) {
77
+ try {
78
+ const rawContent = readFileSync(codebasePath, "utf-8").trim();
79
+ if (rawContent) {
80
+ // Cap injection size to ~2 000 tokens to avoid bloating every request.
81
+ // Full map is always available at .gsd/CODEBASE.md.
82
+ const MAX_CODEBASE_CHARS = 8_000;
83
+ const generatedMatch = rawContent.match(/Generated: (\S+)/);
84
+ const generatedAt = generatedMatch?.[1] ?? "unknown";
85
+ const content = rawContent.length > MAX_CODEBASE_CHARS
86
+ ? rawContent.slice(0, MAX_CODEBASE_CHARS) + "\n\n*(truncated — see .gsd/CODEBASE.md for full map)*"
87
+ : rawContent;
88
+ codebaseBlock = `\n\n[PROJECT CODEBASE — File structure and descriptions (generated ${generatedAt}, may be stale — run /gsd codebase update to refresh)]\n\n${content}`;
89
+ }
90
+ }
91
+ catch {
92
+ // skip
93
+ }
94
+ }
74
95
  warnDeprecatedAgentInstructions();
75
96
  const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd());
76
97
  // Re-inject forensics context on follow-up turns (#2941)
77
98
  const forensicsInjection = !injection ? buildForensicsContextInjection(process.cwd()) : null;
78
99
  const worktreeBlock = buildWorktreeContextBlock();
79
- const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`;
100
+ const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${codebaseBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`;
80
101
  stopContextTimer({
81
102
  systemPromptSize: fullSystem.length,
82
103
  injectionSize: injection?.length ?? forensicsInjection?.length ?? 0,
@@ -0,0 +1,279 @@
1
+ /**
2
+ * GSD Codebase Map Generator
3
+ *
4
+ * Produces .gsd/CODEBASE.md — a structural table of contents for the project.
5
+ * Gives fresh agent contexts instant orientation without filesystem exploration.
6
+ *
7
+ * Generation: walk `git ls-files`, group by directory, output with descriptions.
8
+ * Maintenance: agent updates descriptions as it works; incremental update preserves them.
9
+ */
10
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
11
+ import { join, dirname, extname } from "node:path";
12
+ import { execSync } from "node:child_process";
13
+ import { gsdRoot } from "./paths.js";
14
+ // ─── Defaults ────────────────────────────────────────────────────────────────
15
+ const DEFAULT_EXCLUDES = [
16
+ ".gsd/",
17
+ ".planning/",
18
+ ".git/",
19
+ "node_modules/",
20
+ "dist/",
21
+ "build/",
22
+ ".next/",
23
+ "coverage/",
24
+ "__pycache__/",
25
+ ".venv/",
26
+ "vendor/",
27
+ ];
28
+ const DEFAULT_MAX_FILES = 500;
29
+ const DEFAULT_COLLAPSE_THRESHOLD = 20;
30
+ // ─── Parsing ─────────────────────────────────────────────────────────────────
31
+ /**
32
+ * Parse an existing CODEBASE.md to extract file → description mappings.
33
+ * Also scans <!-- gsd:collapsed-descriptions --> comment blocks to preserve
34
+ * descriptions for files in collapsed directories across incremental updates.
35
+ */
36
+ export function parseCodebaseMap(content) {
37
+ const descriptions = new Map();
38
+ let inCollapsedBlock = false;
39
+ for (const line of content.split("\n")) {
40
+ // Track collapsed-description comment blocks
41
+ if (line.trimStart().startsWith("<!-- gsd:collapsed-descriptions")) {
42
+ inCollapsedBlock = true;
43
+ continue;
44
+ }
45
+ if (inCollapsedBlock && line.trimStart().startsWith("-->")) {
46
+ inCollapsedBlock = false;
47
+ continue;
48
+ }
49
+ // Match: - `path/to/file.ts` — Description here
50
+ const match = line.match(/^- `(.+?)` — (.+)$/);
51
+ if (match) {
52
+ descriptions.set(match[1], match[2]);
53
+ continue;
54
+ }
55
+ // Match: - `path/to/file.ts` (no description) — only outside collapsed blocks
56
+ if (!inCollapsedBlock) {
57
+ const bareMatch = line.match(/^- `(.+?)`\s*$/);
58
+ if (bareMatch) {
59
+ descriptions.set(bareMatch[1], "");
60
+ }
61
+ }
62
+ }
63
+ return descriptions;
64
+ }
65
+ // ─── File Enumeration ────────────────────────────────────────────────────────
66
+ function shouldExclude(filePath, excludes) {
67
+ for (const pattern of excludes) {
68
+ if (pattern.endsWith("/")) {
69
+ if (filePath.startsWith(pattern) || filePath.includes(`/${pattern}`))
70
+ return true;
71
+ }
72
+ else if (filePath === pattern || filePath.endsWith(`/${pattern}`)) {
73
+ return true;
74
+ }
75
+ }
76
+ // Skip binary/lock files
77
+ const ext = extname(filePath).toLowerCase();
78
+ if ([".lock", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".svg"].includes(ext)) {
79
+ return true;
80
+ }
81
+ return false;
82
+ }
83
+ function lsFiles(basePath) {
84
+ try {
85
+ const result = execSync("git ls-files", { cwd: basePath, encoding: "utf-8", timeout: 10000 });
86
+ return result.split("\n").filter(Boolean);
87
+ }
88
+ catch {
89
+ return [];
90
+ }
91
+ }
92
+ /**
93
+ * Enumerate tracked files, applying exclusions and the maxFiles cap.
94
+ * Returns both the file list and whether truncation occurred.
95
+ */
96
+ function enumerateFiles(basePath, excludes, maxFiles) {
97
+ const allFiles = lsFiles(basePath);
98
+ const filtered = allFiles.filter((f) => !shouldExclude(f, excludes));
99
+ const truncated = filtered.length > maxFiles;
100
+ return { files: truncated ? filtered.slice(0, maxFiles) : filtered, truncated };
101
+ }
102
+ // ─── Grouping ────────────────────────────────────────────────────────────────
103
+ function groupByDirectory(files, descriptions, collapseThreshold) {
104
+ const dirMap = new Map();
105
+ for (const file of files) {
106
+ const dir = dirname(file);
107
+ const dirKey = dir === "." ? "" : dir;
108
+ if (!dirMap.has(dirKey)) {
109
+ dirMap.set(dirKey, []);
110
+ }
111
+ dirMap.get(dirKey).push({
112
+ path: file,
113
+ description: descriptions.get(file) ?? "",
114
+ });
115
+ }
116
+ const groups = [];
117
+ const sortedDirs = [...dirMap.keys()].sort();
118
+ for (const dir of sortedDirs) {
119
+ const dirFiles = dirMap.get(dir);
120
+ dirFiles.sort((a, b) => a.path.localeCompare(b.path));
121
+ groups.push({
122
+ path: dir,
123
+ files: dirFiles,
124
+ collapsed: dirFiles.length > collapseThreshold,
125
+ });
126
+ }
127
+ return groups;
128
+ }
129
+ // ─── Rendering ───────────────────────────────────────────────────────────────
130
+ function renderCodebaseMap(groups, totalFiles, truncated) {
131
+ const lines = [];
132
+ const now = new Date().toISOString().split(".")[0] + "Z";
133
+ const described = groups.reduce((sum, g) => sum + g.files.filter((f) => f.description).length, 0);
134
+ lines.push("# Codebase Map");
135
+ lines.push("");
136
+ lines.push(`Generated: ${now} | Files: ${totalFiles} | Described: ${described}/${totalFiles}`);
137
+ if (truncated) {
138
+ lines.push(`Note: Truncated to first ${totalFiles} files. Run with higher --max-files to include all.`);
139
+ }
140
+ lines.push("");
141
+ for (const group of groups) {
142
+ const heading = group.path || "(root)";
143
+ lines.push(`### ${heading}/`);
144
+ if (group.collapsed) {
145
+ // Summarize collapsed directories
146
+ const extensions = new Map();
147
+ for (const f of group.files) {
148
+ const ext = extname(f.path) || "(no ext)";
149
+ extensions.set(ext, (extensions.get(ext) ?? 0) + 1);
150
+ }
151
+ const extSummary = [...extensions.entries()]
152
+ .sort((a, b) => b[1] - a[1])
153
+ .map(([ext, count]) => `${count} ${ext}`)
154
+ .join(", ");
155
+ lines.push(`- *(${group.files.length} files: ${extSummary})*`);
156
+ // Preserve any existing descriptions in a hidden comment block so
157
+ // incremental updates can recover them via parseCodebaseMap.
158
+ const descLines = group.files
159
+ .filter((f) => f.description)
160
+ .map((f) => `- \`${f.path}\` — ${f.description}`);
161
+ if (descLines.length > 0) {
162
+ lines.push("<!-- gsd:collapsed-descriptions");
163
+ lines.push(...descLines);
164
+ lines.push("-->");
165
+ }
166
+ }
167
+ else {
168
+ for (const file of group.files) {
169
+ if (file.description) {
170
+ lines.push(`- \`${file.path}\` — ${file.description}`);
171
+ }
172
+ else {
173
+ lines.push(`- \`${file.path}\``);
174
+ }
175
+ }
176
+ }
177
+ lines.push("");
178
+ }
179
+ return lines.join("\n");
180
+ }
181
+ // ─── Public API ──────────────────────────────────────────────────────────────
182
+ /**
183
+ * Generate a fresh CODEBASE.md from scratch.
184
+ * Preserves existing descriptions if `existingDescriptions` is provided.
185
+ */
186
+ export function generateCodebaseMap(basePath, options, existingDescriptions) {
187
+ const excludes = [...DEFAULT_EXCLUDES, ...(options?.excludePatterns ?? [])];
188
+ const maxFiles = options?.maxFiles ?? DEFAULT_MAX_FILES;
189
+ const collapseThreshold = options?.collapseThreshold ?? DEFAULT_COLLAPSE_THRESHOLD;
190
+ const { files, truncated } = enumerateFiles(basePath, excludes, maxFiles);
191
+ const descriptions = existingDescriptions ?? new Map();
192
+ const groups = groupByDirectory(files, descriptions, collapseThreshold);
193
+ const content = renderCodebaseMap(groups, files.length, truncated);
194
+ return { content, fileCount: files.length, truncated, files };
195
+ }
196
+ /**
197
+ * Incremental update: re-scan files, preserve existing descriptions,
198
+ * add new files, remove deleted files.
199
+ */
200
+ export function updateCodebaseMap(basePath, options) {
201
+ const codebasePath = join(gsdRoot(basePath), "CODEBASE.md");
202
+ // Load existing descriptions
203
+ let existingDescriptions = new Map();
204
+ if (existsSync(codebasePath)) {
205
+ const existing = readFileSync(codebasePath, "utf-8");
206
+ existingDescriptions = parseCodebaseMap(existing);
207
+ }
208
+ const existingFiles = new Set(existingDescriptions.keys());
209
+ // Generate new map preserving descriptions — reuse the returned file list
210
+ // to avoid a second enumeration (prevents race between content and stats).
211
+ const result = generateCodebaseMap(basePath, options, existingDescriptions);
212
+ const currentSet = new Set(result.files);
213
+ // Count changes
214
+ let added = 0;
215
+ let removed = 0;
216
+ for (const f of result.files) {
217
+ if (!existingFiles.has(f))
218
+ added++;
219
+ }
220
+ for (const f of existingFiles) {
221
+ if (!currentSet.has(f))
222
+ removed++;
223
+ }
224
+ return {
225
+ content: result.content,
226
+ added,
227
+ removed,
228
+ unchanged: result.files.length - added,
229
+ fileCount: result.fileCount,
230
+ truncated: result.truncated,
231
+ };
232
+ }
233
+ /**
234
+ * Write CODEBASE.md to .gsd/ directory.
235
+ */
236
+ export function writeCodebaseMap(basePath, content) {
237
+ const root = gsdRoot(basePath);
238
+ mkdirSync(root, { recursive: true });
239
+ const outPath = join(root, "CODEBASE.md");
240
+ writeFileSync(outPath, content, "utf-8");
241
+ return outPath;
242
+ }
243
+ /**
244
+ * Read existing CODEBASE.md, or return null if it doesn't exist.
245
+ */
246
+ export function readCodebaseMap(basePath) {
247
+ const codebasePath = join(gsdRoot(basePath), "CODEBASE.md");
248
+ if (!existsSync(codebasePath))
249
+ return null;
250
+ try {
251
+ return readFileSync(codebasePath, "utf-8");
252
+ }
253
+ catch {
254
+ return null;
255
+ }
256
+ }
257
+ /**
258
+ * Get stats about the codebase map.
259
+ */
260
+ export function getCodebaseMapStats(basePath) {
261
+ const content = readCodebaseMap(basePath);
262
+ if (!content) {
263
+ return { exists: false, fileCount: 0, describedCount: 0, undescribedCount: 0, generatedAt: null };
264
+ }
265
+ // Parse total file count from the header line (accurate even for collapsed dirs)
266
+ const fileCountMatch = content.match(/Files:\s*(\d+)/);
267
+ const totalFiles = fileCountMatch ? parseInt(fileCountMatch[1], 10) : 0;
268
+ // Use parseCodebaseMap to count described files (includes collapsed-description blocks)
269
+ const descriptions = parseCodebaseMap(content);
270
+ const described = [...descriptions.values()].filter((d) => d.length > 0).length;
271
+ const dateMatch = content.match(/Generated: (\S+)/);
272
+ return {
273
+ exists: true,
274
+ fileCount: totalFiles,
275
+ describedCount: described,
276
+ undescribedCount: totalFiles - described,
277
+ generatedAt: dateMatch?.[1] ?? null,
278
+ };
279
+ }
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { loadRegistry } from "../workflow-templates.js";
5
5
  import { resolveProjectRoot } from "../worktree.js";
6
6
  const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
7
- export const GSD_COMMAND_DESCRIPTION = "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast|mcp|rethink";
7
+ export const GSD_COMMAND_DESCRIPTION = "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast|mcp|rethink|codebase";
8
8
  export const TOP_LEVEL_SUBCOMMANDS = [
9
9
  { cmd: "help", desc: "Categorized command reference with descriptions" },
10
10
  { cmd: "next", desc: "Explicit step mode (same as /gsd)" },
@@ -59,6 +59,7 @@ export const TOP_LEVEL_SUBCOMMANDS = [
59
59
  { cmd: "mcp", desc: "MCP server status and connectivity check (status, check <server>)" },
60
60
  { cmd: "rethink", desc: "Conversational project reorganization — reorder, park, discard, add milestones" },
61
61
  { cmd: "workflow", desc: "Custom workflow lifecycle (new, run, list, validate, pause, resume)" },
62
+ { cmd: "codebase", desc: "Generate and manage codebase map (.gsd/CODEBASE.md)" },
62
63
  ];
63
64
  const NESTED_COMPLETIONS = {
64
65
  auto: [
@@ -212,6 +213,14 @@ const NESTED_COMPLETIONS = {
212
213
  { cmd: "pause", desc: "Pause custom workflow auto-mode" },
213
214
  { cmd: "resume", desc: "Resume paused custom workflow auto-mode" },
214
215
  ],
216
+ codebase: [
217
+ { cmd: "generate", desc: "Generate or regenerate CODEBASE.md" },
218
+ { cmd: "generate --max-files", desc: "Generate with custom file limit (default: 500)" },
219
+ { cmd: "update", desc: "Incremental update (preserves descriptions)" },
220
+ { cmd: "update --max-files", desc: "Update with custom file limit" },
221
+ { cmd: "stats", desc: "Show file count, description coverage, and generation time" },
222
+ { cmd: "help", desc: "Show usage and available subcommands" },
223
+ ],
215
224
  };
216
225
  function filterOptions(partial, options, prefix = "") {
217
226
  const normalizedPrefix = prefix ? `${prefix} ` : "";
@@ -203,5 +203,10 @@ Examples:
203
203
  await handleRethink(trimmed, ctx, pi);
204
204
  return true;
205
205
  }
206
+ if (trimmed === "codebase" || trimmed.startsWith("codebase ")) {
207
+ const { handleCodebase } = await import("../../commands-codebase.js");
208
+ await handleCodebase(trimmed.replace(/^codebase\s*/, "").trim(), ctx, pi);
209
+ return true;
210
+ }
206
211
  return false;
207
212
  }
@@ -0,0 +1,115 @@
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
+ import { generateCodebaseMap, updateCodebaseMap, writeCodebaseMap, getCodebaseMapStats, readCodebaseMap, } from "./codebase-generator.js";
8
+ const USAGE = "Usage: /gsd codebase [generate|update|stats]\n\n" +
9
+ " generate [--max-files N] — Generate or regenerate CODEBASE.md\n" +
10
+ " update — Incremental update (preserves descriptions)\n" +
11
+ " stats — Show file count, coverage, and generation time\n" +
12
+ " help — Show this help\n\n" +
13
+ "With no subcommand, shows stats if a map exists or help if not.";
14
+ export async function handleCodebase(args, ctx, _pi) {
15
+ const basePath = process.cwd();
16
+ const parts = args.trim().split(/\s+/);
17
+ const sub = parts[0] ?? "";
18
+ switch (sub) {
19
+ case "generate": {
20
+ const maxFiles = parseMaxFiles(args, ctx);
21
+ if (maxFiles === false)
22
+ return; // validation failed, message already shown
23
+ const existing = readCodebaseMap(basePath);
24
+ const existingDescriptions = existing
25
+ ? (await import("./codebase-generator.js")).parseCodebaseMap(existing)
26
+ : undefined;
27
+ const result = generateCodebaseMap(basePath, { maxFiles: maxFiles ?? undefined }, existingDescriptions);
28
+ if (result.fileCount === 0) {
29
+ ctx.ui.notify("Codebase map generated with 0 files.\n" +
30
+ "Is this a git repository? Run 'git ls-files' to verify.", "warning");
31
+ return;
32
+ }
33
+ const outPath = writeCodebaseMap(basePath, result.content);
34
+ ctx.ui.notify(`Codebase map generated: ${result.fileCount} files\n` +
35
+ `Written to: ${outPath}` +
36
+ (result.truncated ? `\n⚠ Truncated — increase --max-files to include all files` : ""), "success");
37
+ return;
38
+ }
39
+ case "update": {
40
+ const existing = readCodebaseMap(basePath);
41
+ if (!existing) {
42
+ ctx.ui.notify("No codebase map found. Run /gsd codebase generate to create one.", "warning");
43
+ return;
44
+ }
45
+ const maxFiles = parseMaxFiles(args, ctx);
46
+ if (maxFiles === false)
47
+ return;
48
+ const result = updateCodebaseMap(basePath, { maxFiles: maxFiles ?? undefined });
49
+ writeCodebaseMap(basePath, result.content);
50
+ ctx.ui.notify(`Codebase map updated: ${result.fileCount} files\n` +
51
+ ` Added: ${result.added} | Removed: ${result.removed} | Unchanged: ${result.unchanged}` +
52
+ (result.truncated ? `\n⚠ Truncated — increase --max-files to include all files` : ""), "success");
53
+ return;
54
+ }
55
+ case "stats": {
56
+ showStats(basePath, ctx);
57
+ return;
58
+ }
59
+ case "help":
60
+ ctx.ui.notify(USAGE, "info");
61
+ return;
62
+ case "": {
63
+ // Safe default: show stats if map exists, help if not
64
+ const existing = readCodebaseMap(basePath);
65
+ if (existing) {
66
+ showStats(basePath, ctx);
67
+ }
68
+ else {
69
+ ctx.ui.notify(USAGE, "info");
70
+ }
71
+ return;
72
+ }
73
+ default:
74
+ ctx.ui.notify(`Unknown subcommand "${sub}".\n\n${USAGE}`, "warning");
75
+ }
76
+ }
77
+ function showStats(basePath, ctx) {
78
+ const stats = getCodebaseMapStats(basePath);
79
+ if (!stats.exists) {
80
+ ctx.ui.notify("No codebase map found. Run /gsd codebase generate to create one.", "info");
81
+ return;
82
+ }
83
+ const coverage = stats.fileCount > 0
84
+ ? Math.round((stats.describedCount / stats.fileCount) * 100)
85
+ : 0;
86
+ ctx.ui.notify(`Codebase Map Stats:\n` +
87
+ ` Files: ${stats.fileCount}\n` +
88
+ ` Described: ${stats.describedCount} (${coverage}%)\n` +
89
+ ` Undescribed: ${stats.undescribedCount}\n` +
90
+ ` Generated: ${stats.generatedAt ?? "unknown"}\n\n` +
91
+ (stats.undescribedCount > 0
92
+ ? `Tip: Run /gsd codebase update to refresh after file changes.`
93
+ : `Coverage is complete.`), "info");
94
+ }
95
+ /**
96
+ * Parse and validate --max-files flag.
97
+ * Returns the parsed number, undefined if flag not present, or false if invalid.
98
+ */
99
+ function parseMaxFiles(args, ctx) {
100
+ const maxFilesStr = extractFlag(args, "--max-files");
101
+ if (!maxFilesStr)
102
+ return undefined;
103
+ const maxFiles = parseInt(maxFilesStr, 10);
104
+ if (isNaN(maxFiles) || maxFiles < 1) {
105
+ ctx.ui.notify("--max-files must be a positive integer (e.g. --max-files 200).", "warning");
106
+ return false;
107
+ }
108
+ return maxFiles;
109
+ }
110
+ function extractFlag(args, flag) {
111
+ const escaped = flag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
112
+ const regex = new RegExp(`${escaped}[=\\s]+(\\S+)`);
113
+ const match = args.match(regex);
114
+ return match?.[1];
115
+ }
@@ -151,11 +151,24 @@ export function buildCategorySummaries(prefs) {
151
151
  }
152
152
  // Git
153
153
  const git = prefs.git;
154
+ const staleThreshold = prefs.stale_commit_threshold_minutes;
155
+ const absorbSnapshots = git?.absorb_snapshot_commits;
154
156
  let gitSummary = "(defaults)";
155
- if (git && Object.keys(git).length > 0) {
156
- const branch = git.main_branch ?? "main";
157
- const push = git.auto_push ? "on" : "off";
158
- gitSummary = `main: ${branch}, push: ${push}`;
157
+ {
158
+ const parts = [];
159
+ if (git && Object.keys(git).length > 0) {
160
+ const branch = git.main_branch ?? "main";
161
+ const push = git.auto_push ? "on" : "off";
162
+ parts.push(`main: ${branch}, push: ${push}`);
163
+ }
164
+ if (staleThreshold !== undefined) {
165
+ parts.push(`stale: ${staleThreshold === 0 ? "off" : `${staleThreshold}m`}`);
166
+ }
167
+ if (absorbSnapshots !== undefined) {
168
+ parts.push(`absorb: ${absorbSnapshots ? "on" : "off"}`);
169
+ }
170
+ if (parts.length > 0)
171
+ gitSummary = parts.join(", ");
159
172
  }
160
173
  // Skills
161
174
  const discovery = prefs.skill_discovery;
@@ -394,9 +407,33 @@ async function configureGit(ctx, prefs) {
394
407
  if (isolationChoice && isolationChoice !== "(keep current)") {
395
408
  git.isolation = isolationChoice;
396
409
  }
410
+ // absorb_snapshot_commits (git sub-key)
411
+ const currentAbsorb = git.absorb_snapshot_commits;
412
+ const absorbStr = currentAbsorb !== undefined ? String(currentAbsorb) : "";
413
+ const absorbChoice = await ctx.ui.select(`Absorb snapshot commits into real commits${absorbStr ? ` (current: ${absorbStr})` : " (default: true)"}:`, ["true", "false", "(keep current)"]);
414
+ if (absorbChoice && absorbChoice !== "(keep current)") {
415
+ git.absorb_snapshot_commits = absorbChoice === "true";
416
+ }
397
417
  if (Object.keys(git).length > 0) {
398
418
  prefs.git = git;
399
419
  }
420
+ // stale_commit_threshold_minutes (top-level pref, shown in Git section)
421
+ const currentThreshold = prefs.stale_commit_threshold_minutes;
422
+ const thresholdStr = currentThreshold !== undefined ? String(currentThreshold) : "";
423
+ const thresholdInput = await ctx.ui.input(`Stale commit threshold (minutes, 0 to disable)${thresholdStr ? ` (current: ${thresholdStr})` : " (default: 30)"}:`, thresholdStr || "30");
424
+ if (thresholdInput !== null && thresholdInput !== undefined) {
425
+ const val = thresholdInput.trim();
426
+ const parsed = tryParseInteger(val);
427
+ if (val && parsed !== null && parsed >= 0) {
428
+ prefs.stale_commit_threshold_minutes = parsed;
429
+ }
430
+ else if (val && parsed === null) {
431
+ ctx.ui.notify(`Invalid value "${val}" — must be a whole number. Keeping previous value.`, "warning");
432
+ }
433
+ else if (!val && currentThreshold !== undefined) {
434
+ delete prefs.stale_commit_threshold_minutes;
435
+ }
436
+ }
400
437
  }
401
438
  async function configureSkills(ctx, prefs) {
402
439
  // Skill discovery mode