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
@@ -11,14 +11,16 @@ const UNIT_TYPE_TIERS = {
11
11
  // Tier 1 — Light: structured summaries, completion, UAT
12
12
  "complete-slice": "light",
13
13
  "run-uat": "light",
14
- // Tier 2 — Standard: research, routine planning, discussion
14
+ // Tier 2 — Standard: research, routine discussion
15
15
  "discuss-milestone": "standard",
16
16
  "discuss-slice": "standard",
17
17
  "research-milestone": "standard",
18
18
  "research-slice": "standard",
19
- "plan-milestone": "standard",
20
- "plan-slice": "standard",
21
- // Tier 3 Heavy: execution, replanning (requires deep reasoning)
19
+ // Tier 3 — Heavy: planning, execution, replanning (requires deep reasoning)
20
+ // Planning is heavy so it uses the best configured model (e.g. Opus) and is
21
+ // not downgraded by dynamic routing when a capable model is configured.
22
+ "plan-milestone": "heavy",
23
+ "plan-slice": "heavy",
22
24
  "execute-task": "standard", // default standard, upgraded by metadata
23
25
  "replan-slice": "heavy",
24
26
  "reassess-roadmap": "heavy",
@@ -124,8 +126,8 @@ function analyzePlanComplexity(unitId, basePath) {
124
126
  // Check if this is a milestone-level plan (more complex) vs single slice
125
127
  const { milestone: mid, slice: sid } = parseUnitId(unitId);
126
128
  if (!sid) {
127
- // Milestone-level planning is always at least standard
128
- return { tier: "standard", reason: "milestone-level planning" };
129
+ // Milestone-level planning is always heavy requires full context and best model
130
+ return { tier: "heavy", reason: "milestone-level planning" };
129
131
  }
130
132
  // For slice planning, try to read the context/research to gauge complexity
131
133
  // If research exists and is large, bump to heavy
@@ -8,7 +8,7 @@ import { deriveState, isMilestoneComplete } from "./state.js";
8
8
  import { listWorktrees, resolveGitDir, worktreesDir } from "./worktree-manager.js";
9
9
  import { abortAndReset } from "./git-self-heal.js";
10
10
  import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
11
- import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached } from "./native-git-bridge.js";
11
+ import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
12
12
  import { getAllWorktreeHealth } from "./worktree-health.js";
13
13
  import { loadEffectiveGSDPreferences } from "./preferences.js";
14
14
  /**
@@ -359,6 +359,53 @@ export async function checkGitHealth(basePath, issues, fixesApplied, shouldFix,
359
359
  catch {
360
360
  // Non-fatal — orphaned worktree directory check failed
361
361
  }
362
+ // ── Stale uncommitted changes ────────────────────────────────────────────
363
+ // If the working tree has uncommitted changes and the last commit was
364
+ // longer ago than the configured threshold, flag it and optionally
365
+ // auto-commit a safety snapshot so work isn't lost.
366
+ try {
367
+ const prefs = loadEffectiveGSDPreferences()?.preferences ?? {};
368
+ const thresholdMinutes = prefs.stale_commit_threshold_minutes ?? 30;
369
+ if (thresholdMinutes > 0) {
370
+ const dirty = nativeHasChanges(basePath);
371
+ if (dirty) {
372
+ const branch = nativeGetCurrentBranch(basePath);
373
+ const lastEpoch = nativeLastCommitEpoch(basePath, branch || "HEAD");
374
+ const nowEpoch = Math.floor(Date.now() / 1000);
375
+ const minutesSinceCommit = lastEpoch > 0 ? (nowEpoch - lastEpoch) / 60 : Infinity;
376
+ if (minutesSinceCommit >= thresholdMinutes) {
377
+ const mins = Math.floor(minutesSinceCommit);
378
+ issues.push({
379
+ severity: "warning",
380
+ code: "stale_uncommitted_changes",
381
+ scope: "project",
382
+ unitId: "project",
383
+ message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting tracked files.`,
384
+ fixable: true,
385
+ });
386
+ if (shouldFix("stale_uncommitted_changes")) {
387
+ try {
388
+ nativeAddTracked(basePath);
389
+ const commitMsg = `gsd snapshot: uncommitted changes after ${mins}m inactivity`;
390
+ const result = nativeCommit(basePath, commitMsg);
391
+ if (result) {
392
+ fixesApplied.push(`created gsd snapshot after ${mins}m of uncommitted changes`);
393
+ }
394
+ else {
395
+ fixesApplied.push("gsd snapshot skipped — nothing to commit after staging tracked files");
396
+ }
397
+ }
398
+ catch {
399
+ fixesApplied.push("failed to create gsd snapshot commit");
400
+ }
401
+ }
402
+ }
403
+ }
404
+ }
405
+ }
406
+ catch {
407
+ // Non-fatal — stale commit check failed
408
+ }
362
409
  // ── Worktree lifecycle checks ──────────────────────────────────────────
363
410
  // Check GSD-managed worktrees for: merged branches, stale work, dirty
364
411
  // state, and unpushed commits. Only worktrees under .gsd/worktrees/.
@@ -21,7 +21,7 @@ import { abortAndReset } from "./git-self-heal.js";
21
21
  import { rebuildState } from "./doctor.js";
22
22
  import { deriveState } from "./state.js";
23
23
  import { resolveMilestoneIntegrationBranch } from "./git-service.js";
24
- import { nativeIsRepo } from "./native-git-bridge.js";
24
+ import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
25
25
  import { loadEffectiveGSDPreferences } from "./preferences.js";
26
26
  import { runEnvironmentChecks } from "./doctor-environment.js";
27
27
  /** In-memory health history for the current auto-mode session. */
@@ -232,6 +232,39 @@ export async function preDispatchHealthGate(basePath) {
232
232
  catch {
233
233
  // Non-fatal — dispatch continues if state/branch check fails
234
234
  }
235
+ // ── Stale uncommitted changes — auto-snapshot before dispatch ──
236
+ // If the working tree is dirty and no commit has happened recently,
237
+ // create a safety snapshot so work isn't lost if the next unit crashes.
238
+ try {
239
+ if (nativeIsRepo(basePath)) {
240
+ const prefs = loadEffectiveGSDPreferences()?.preferences ?? {};
241
+ const thresholdMinutes = prefs.stale_commit_threshold_minutes ?? 30;
242
+ if (thresholdMinutes > 0 && nativeHasChanges(basePath)) {
243
+ const branch = nativeGetCurrentBranch(basePath);
244
+ const lastEpoch = nativeLastCommitEpoch(basePath, branch || "HEAD");
245
+ const nowEpoch = Math.floor(Date.now() / 1000);
246
+ const minutesSinceCommit = lastEpoch > 0 ? (nowEpoch - lastEpoch) / 60 : Infinity;
247
+ if (minutesSinceCommit >= thresholdMinutes) {
248
+ const mins = Math.floor(minutesSinceCommit);
249
+ try {
250
+ nativeAddTracked(basePath);
251
+ const commitMsg = `gsd snapshot: pre-dispatch, uncommitted changes after ${mins}m inactivity`;
252
+ const result = nativeCommit(basePath, commitMsg);
253
+ if (result) {
254
+ fixesApplied.push(`pre-dispatch: created gsd snapshot after ${mins}m of uncommitted changes`);
255
+ }
256
+ }
257
+ catch {
258
+ // Non-blocking — snapshot failed but dispatch can continue
259
+ fixesApplied.push("pre-dispatch: gsd snapshot failed");
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ catch {
266
+ // Non-fatal
267
+ }
235
268
  // ── Disk space check ──
236
269
  // Catches low-disk conditions before dispatch rather than letting the unit
237
270
  // fail mid-execution with ENOSPC (which wastes a full LLM turn).
@@ -24,7 +24,9 @@ const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fet
24
24
  const SERVER_RE = /internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable/i;
25
25
  // ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
26
26
  const CONNECTION_RE = /terminated|connection.?refused|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
27
- const STREAM_RE = /Unexpected end of JSON|Unexpected token.*JSON|Expected.*in JSON|Unterminated.*in JSON|SyntaxError.*JSON/i;
27
+ // Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
28
+ // This eliminates the need to enumerate every error message variant individually.
29
+ const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
28
30
  const RESET_DELAY_RE = /reset in (\d+)s/i;
29
31
  /**
30
32
  * Classify an error message into one of the ErrorClass kinds.
@@ -62,9 +64,6 @@ export function classifyError(errorMsg, retryAfterMs) {
62
64
  return { kind: "network", retryAfterMs: retryAfterMs ?? 3_000 };
63
65
  }
64
66
  // 4. Stream truncation — downstream symptom of connection drop
65
- // Checked before server/connection because JSON parse errors can contain
66
- // substrings like "position 500" (matches SERVER_RE) or "Unterminated"
67
- // (matches CONNECTION_RE's "terminated" pattern).
68
67
  if (STREAM_RE.test(errorMsg)) {
69
68
  return { kind: "stream", retryAfterMs: retryAfterMs ?? 15_000 };
70
69
  }
@@ -15,7 +15,7 @@ import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
15
15
  import { loadEffectiveGSDPreferences } from "./preferences.js";
16
16
  import { detectWorktreeName, } from "./worktree.js";
17
17
  import { SLICE_BRANCH_RE, QUICK_BRANCH_RE, WORKFLOW_BRANCH_RE } from "./branch-patterns.js";
18
- import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAllWithExclusions, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, } from "./native-git-bridge.js";
18
+ import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAllWithExclusions, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, nativeResetSoft, nativeCommitSubject, } from "./native-git-bridge.js";
19
19
  import { GSDError, GSD_MERGE_CONFLICT, GSD_GIT_ERROR } from "./errors.js";
20
20
  import { getErrorMessage } from "./error-utils.js";
21
21
  export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/;
@@ -412,8 +412,89 @@ export class GitServiceImpl {
412
412
  ? buildTaskCommitMessage(taskContext)
413
413
  : `chore: auto-commit after ${unitType}\n\nGSD-Unit: ${unitId}`;
414
414
  nativeCommit(this.basePath, message, { allowEmpty: false });
415
+ // Absorb any preceding gsd snapshot commits into this real commit.
416
+ // Walk backwards from HEAD~1 counting consecutive snapshot subjects,
417
+ // then soft-reset to before them and re-commit with the same message.
418
+ this.absorbSnapshotCommits(message);
415
419
  return message;
416
420
  }
421
+ /**
422
+ * Squash consecutive `gsd snapshot:` commits that sit immediately below
423
+ * HEAD into the current HEAD commit. This keeps the git history clean
424
+ * after automated snapshot commits are superseded by real work.
425
+ *
426
+ * Guards:
427
+ * - Opt-in via `absorb_snapshot_commits` preference (default: true).
428
+ * - Refuses to rewrite commits that have been pushed to the remote
429
+ * tracking branch (checks merge-base ancestry).
430
+ * - Saves HEAD SHA before reset; restores it if the re-commit fails.
431
+ *
432
+ * Does nothing if there are no snapshot commits to absorb.
433
+ */
434
+ absorbSnapshotCommits(headMessage) {
435
+ try {
436
+ // Opt-in guard — users can disable to keep snapshot commits for forensics
437
+ if (this.prefs.absorb_snapshot_commits === false)
438
+ return;
439
+ const GSD_SNAPSHOT_PREFIX = "gsd snapshot:";
440
+ let count = 0;
441
+ // Walk back from HEAD~1 counting consecutive snapshot commits (cap at 10)
442
+ for (let i = 1; i <= 10; i++) {
443
+ const subject = nativeCommitSubject(this.basePath, `HEAD~${i}`);
444
+ if (!subject.startsWith(GSD_SNAPSHOT_PREFIX))
445
+ break;
446
+ count = i;
447
+ }
448
+ if (count === 0)
449
+ return;
450
+ // Guard: don't rewrite history that has been pushed to the remote.
451
+ // Check whether the newest snapshot commit (HEAD~1) is already
452
+ // reachable from the remote tracking branch. If it is, the snapshots
453
+ // have been pushed and must not be squashed via local history rewrite.
454
+ // (Checking resetTarget instead would false-positive when the remote
455
+ // is at the pre-snapshot base but the snapshots themselves are local.)
456
+ const resetTarget = `HEAD~${count + 1}`;
457
+ try {
458
+ const branch = nativeGetCurrentBranch(this.basePath);
459
+ if (branch) {
460
+ const remoteBranch = `origin/${branch}`;
461
+ // merge-base --is-ancestor exits 0 if HEAD~1 is ancestor of remote
462
+ execFileSync("git", ["merge-base", "--is-ancestor", "HEAD~1", remoteBranch], {
463
+ cwd: this.basePath,
464
+ stdio: ["ignore", "pipe", "pipe"],
465
+ });
466
+ // If we get here, newest snapshot IS reachable from remote — already pushed
467
+ return;
468
+ }
469
+ }
470
+ catch {
471
+ // Not an ancestor or remote doesn't exist — safe to proceed
472
+ }
473
+ // Save HEAD SHA so we can restore if the re-commit fails
474
+ const savedHead = execFileSync("git", ["rev-parse", "HEAD"], {
475
+ cwd: this.basePath,
476
+ stdio: ["ignore", "pipe", "pipe"],
477
+ encoding: "utf-8",
478
+ }).trim();
479
+ nativeResetSoft(this.basePath, resetTarget);
480
+ // Re-run smartStage so the same RUNTIME_EXCLUSION_PATHS apply.
481
+ // Snapshot commits used nativeAddTracked (git add -u) which stages
482
+ // ALL tracked modifications including .gsd/ state files. Without
483
+ // re-staging, those .gsd/ changes leak into the absorbed commit.
484
+ this.smartStage();
485
+ try {
486
+ nativeCommit(this.basePath, headMessage, { allowEmpty: false });
487
+ }
488
+ catch {
489
+ // Re-commit failed — restore original HEAD to avoid leaving the
490
+ // repo in a partially-reset state with no commit
491
+ nativeResetSoft(this.basePath, savedHead);
492
+ }
493
+ }
494
+ catch {
495
+ // Non-fatal — if squash fails, the commits remain unsquashed
496
+ }
497
+ }
417
498
  // ─── Branch Queries ────────────────────────────────────────────────────
418
499
  /**
419
500
  * Get the integration branch for this repo — the branch that slice
@@ -525,6 +525,15 @@ export function nativeAddAll(basePath) {
525
525
  }
526
526
  gitFileExec(basePath, ["add", "-A"]);
527
527
  }
528
+ /**
529
+ * Stage only already-tracked files (git add -u).
530
+ * Does NOT add new untracked files — only updates modifications and deletions
531
+ * for files git already knows about. Safe for automated snapshots where
532
+ * pulling in unknown untracked files (secrets, binaries) would be dangerous.
533
+ */
534
+ export function nativeAddTracked(basePath) {
535
+ gitFileExec(basePath, ["add", "-u"]);
536
+ }
528
537
  /**
529
538
  * Stage all files with pathspec exclusions (git add -A -- ':!pattern' ...).
530
539
  * Excluded paths are never hashed by git, preventing hangs on large
@@ -758,6 +767,19 @@ export function nativeResetHard(basePath) {
758
767
  }
759
768
  execSync("git reset --hard HEAD", { cwd: basePath, stdio: "pipe" });
760
769
  }
770
+ /**
771
+ * Soft reset to a target ref (git reset --soft <ref>).
772
+ * Moves HEAD to `target` while keeping all changes staged in the index.
773
+ * Used to squash snapshot commits back into a single real commit.
774
+ */
775
+ export function nativeResetSoft(basePath, target) {
776
+ execFileSync("git", ["reset", "--soft", target], {
777
+ cwd: basePath,
778
+ stdio: ["ignore", "pipe", "pipe"],
779
+ encoding: "utf-8",
780
+ env: GIT_NO_PROMPT_ENV,
781
+ });
782
+ }
761
783
  /**
762
784
  * Get the subject line of a commit (git log -1 --format=%s <ref>).
763
785
  * Returns empty string if the ref doesn't exist.
@@ -254,6 +254,7 @@ export const GSD_ROOT_FILES = {
254
254
  REQUIREMENTS: "REQUIREMENTS.md",
255
255
  OVERRIDES: "OVERRIDES.md",
256
256
  KNOWLEDGE: "KNOWLEDGE.md",
257
+ CODEBASE: "CODEBASE.md",
257
258
  };
258
259
  const LEGACY_GSD_ROOT_FILES = {
259
260
  PROJECT: "project.md",
@@ -263,6 +264,7 @@ const LEGACY_GSD_ROOT_FILES = {
263
264
  REQUIREMENTS: "requirements.md",
264
265
  OVERRIDES: "overrides.md",
265
266
  KNOWLEDGE: "knowledge.md",
267
+ CODEBASE: "codebase.md",
266
268
  };
267
269
  // ─── GSD Root Discovery ───────────────────────────────────────────────────────
268
270
  const gsdRootCache = new Map();
@@ -70,6 +70,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
70
70
  "service_tier",
71
71
  "forensics_dedup",
72
72
  "show_token_cost",
73
+ "stale_commit_threshold_minutes",
73
74
  "experimental",
74
75
  ]);
75
76
  /** Canonical list of all dispatch unit types. */
@@ -0,0 +1,241 @@
1
+ // GSD Watch — Header renderer: ASCII logo, session info, MCP status, remote questions
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+ import { execFileSync } from "node:child_process";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { visibleWidth, truncateToWidth } from "@gsd/pi-tui";
8
+ import { loadEffectiveGSDPreferences } from "../preferences.js";
9
+ // ─── Constants ────────────────────────────────────────────────────────────────
10
+ /**
11
+ * GSD ASCII logo — inlined here because the canonical src/logo.ts is outside
12
+ * the resources rootDir and cannot be imported directly.
13
+ */
14
+ const GSD_LOGO = [
15
+ ' ██████╗ ███████╗██████╗ ',
16
+ ' ██╔════╝ ██╔════╝██╔══██╗',
17
+ ' ██║ ███╗███████╗██║ ██║',
18
+ ' ██║ ██║╚════██║██║ ██║',
19
+ ' ╚██████╔╝███████║██████╔╝',
20
+ ' ╚═════╝ ╚══════╝╚═════╝ ',
21
+ ];
22
+ /** Separator character for the horizontal divider line. */
23
+ const SEPARATOR_CHAR = "─";
24
+ /** Vertical bar between logo and info panel. */
25
+ const PANEL_DIVIDER = "│";
26
+ /** Label column width for Model/Provider/Directory/Branch rows. */
27
+ const LABEL_COL_WIDTH = 10;
28
+ // ─── Data Readers ─────────────────────────────────────────────────────────────
29
+ /**
30
+ * Read the configured execution model from GSD preferences.
31
+ * Falls back through execution -> planning -> research -> first found.
32
+ * Returns "default" if nothing is configured.
33
+ */
34
+ export function readModelFromPreferences() {
35
+ try {
36
+ const prefs = loadEffectiveGSDPreferences();
37
+ if (!prefs?.preferences.models)
38
+ return "default";
39
+ const m = prefs.preferences.models;
40
+ // Try common phases in priority order
41
+ for (const phase of ["execution", "planning", "research", "discuss", "subagent"]) {
42
+ const val = m[phase];
43
+ if (typeof val === "string")
44
+ return val;
45
+ if (val && typeof val === "object" && "model" in val) {
46
+ const model = val.model;
47
+ if (typeof model === "string")
48
+ return model;
49
+ }
50
+ }
51
+ }
52
+ catch {
53
+ // Non-fatal
54
+ }
55
+ return "default";
56
+ }
57
+ /**
58
+ * Derive provider name from model ID prefix.
59
+ */
60
+ export function deriveProvider(modelId) {
61
+ if (modelId.startsWith("claude"))
62
+ return "anthropic";
63
+ if (modelId.startsWith("gpt") || modelId.startsWith("o1") || modelId.startsWith("o3"))
64
+ return "openai";
65
+ if (modelId.startsWith("gemini"))
66
+ return "google";
67
+ if (modelId.startsWith("deepseek"))
68
+ return "deepseek";
69
+ if (modelId === "default")
70
+ return "anthropic";
71
+ return "unknown";
72
+ }
73
+ /**
74
+ * Shorten a directory path by replacing the home directory with ~.
75
+ */
76
+ export function shortenPath(fullPath) {
77
+ const home = homedir();
78
+ if (fullPath.startsWith(home)) {
79
+ return "~" + fullPath.slice(home.length);
80
+ }
81
+ return fullPath;
82
+ }
83
+ /**
84
+ * Read the current git branch name. Returns "unknown" on failure.
85
+ */
86
+ export function readGitBranch(projectRoot) {
87
+ try {
88
+ return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
89
+ cwd: projectRoot,
90
+ encoding: "utf-8",
91
+ timeout: 2000,
92
+ }).trim();
93
+ }
94
+ catch {
95
+ return "unknown";
96
+ }
97
+ }
98
+ /**
99
+ * Read MCP server names from .mcp.json or .gsd/mcp.json.
100
+ * Returns array of server name strings.
101
+ */
102
+ export function readMcpServerNames(projectRoot) {
103
+ const configPaths = [
104
+ join(projectRoot, ".mcp.json"),
105
+ join(projectRoot, ".gsd", "mcp.json"),
106
+ ];
107
+ const names = [];
108
+ const seen = new Set();
109
+ for (const configPath of configPaths) {
110
+ try {
111
+ if (!existsSync(configPath))
112
+ continue;
113
+ const raw = readFileSync(configPath, "utf-8");
114
+ const data = JSON.parse(raw);
115
+ const mcpServers = (data.mcpServers ?? data.servers);
116
+ if (!mcpServers || typeof mcpServers !== "object")
117
+ continue;
118
+ for (const name of Object.keys(mcpServers)) {
119
+ if (!seen.has(name)) {
120
+ seen.add(name);
121
+ names.push(name);
122
+ }
123
+ }
124
+ }
125
+ catch {
126
+ // Non-fatal
127
+ }
128
+ }
129
+ return names;
130
+ }
131
+ /**
132
+ * Gather all header data from filesystem and preferences.
133
+ */
134
+ export function gatherHeaderData(projectRoot) {
135
+ const model = readModelFromPreferences();
136
+ const provider = deriveProvider(model);
137
+ const directory = shortenPath(projectRoot);
138
+ const branch = readGitBranch(projectRoot);
139
+ const mcpServers = readMcpServerNames(projectRoot);
140
+ return { model, provider, directory, branch, mcpServers };
141
+ }
142
+ /**
143
+ * Build an info panel line: "Label value" with proper padding.
144
+ * Returns empty string if value is empty.
145
+ */
146
+ function formatInfoLine(label, value, availableWidth) {
147
+ const bold = `\x1b[1m${label}\x1b[0m`;
148
+ const labelVis = visibleWidth(bold);
149
+ const padding = " ".repeat(Math.max(1, LABEL_COL_WIDTH - labelVis));
150
+ const maxValueWidth = Math.max(1, availableWidth - LABEL_COL_WIDTH);
151
+ const truncValue = truncateToWidth(value, maxValueWidth, "…");
152
+ return bold + padding + truncValue;
153
+ }
154
+ /**
155
+ * Format MCP server names as a dot-separated row with checkmarks.
156
+ * e.g. "Brave ✓ · Answers ✓ · Context7 ✓"
157
+ */
158
+ export function formatMcpRow(servers, width) {
159
+ if (servers.length === 0)
160
+ return "";
161
+ // Capitalize first letter of each server name
162
+ const items = servers.map(s => {
163
+ const cap = s.charAt(0).toUpperCase() + s.slice(1);
164
+ return `${cap} ✓`;
165
+ });
166
+ const full = items.join(" · ");
167
+ if (visibleWidth(full) <= width)
168
+ return full;
169
+ // Truncate if too wide
170
+ return truncateToWidth(full, width, "…");
171
+ }
172
+ /**
173
+ * Render the full header as an array of terminal-safe strings.
174
+ *
175
+ * Layout: GSD ASCII logo on the left, info panel on the right separated by │.
176
+ * Below: MCP server row, remote questions row, separator line.
177
+ */
178
+ export function renderHeaderLines(data, width) {
179
+ const lines = [];
180
+ // Logo is 6 lines tall. Info panel has: title + blank + model + provider + directory + branch = 6 lines
181
+ const logoLines = GSD_LOGO;
182
+ const logoWidth = Math.max(...logoLines.map(l => visibleWidth(l)));
183
+ // Calculate available width for the info panel
184
+ // Layout: logo + " " + "│" + " " = logoWidth + 3
185
+ const dividerOverhead = 3; // " │ "
186
+ const infoPanelWidth = width - logoWidth - dividerOverhead;
187
+ // If terminal is too narrow for side-by-side, fall back to stacked layout
188
+ if (infoPanelWidth < 20) {
189
+ return renderStackedHeader(data, width);
190
+ }
191
+ // Build info panel lines (6 lines to match logo height)
192
+ const infoLines = [
193
+ `\x1b[1mGet Shit Done\x1b[0m`,
194
+ "",
195
+ formatInfoLine("Model", data.model, infoPanelWidth),
196
+ formatInfoLine("Provider", data.provider, infoPanelWidth),
197
+ formatInfoLine("Directory", data.directory, infoPanelWidth),
198
+ formatInfoLine("Branch", data.branch, infoPanelWidth),
199
+ ];
200
+ // Merge logo and info panel side by side
201
+ const maxLines = Math.max(logoLines.length, infoLines.length);
202
+ for (let i = 0; i < maxLines; i++) {
203
+ const logoLine = i < logoLines.length ? logoLines[i] : "";
204
+ const infoLine = i < infoLines.length ? infoLines[i] : "";
205
+ // Pad logo line to consistent width
206
+ const logoPad = " ".repeat(Math.max(0, logoWidth - visibleWidth(logoLine)));
207
+ lines.push(`${logoLine}${logoPad} ${PANEL_DIVIDER} ${infoLine}`);
208
+ }
209
+ // Blank line after logo+info block
210
+ lines.push("");
211
+ // MCP server row
212
+ const mcpRow = formatMcpRow(data.mcpServers, width);
213
+ if (mcpRow) {
214
+ lines.push(` ${mcpRow}`);
215
+ }
216
+ // Separator line
217
+ lines.push(SEPARATOR_CHAR.repeat(width));
218
+ return lines;
219
+ }
220
+ /**
221
+ * Fallback stacked layout for narrow terminals (< 20 cols for info panel).
222
+ */
223
+ function renderStackedHeader(data, width) {
224
+ const lines = [];
225
+ // Title
226
+ lines.push(`\x1b[1mGet Shit Done\x1b[0m`);
227
+ lines.push("");
228
+ // Info
229
+ lines.push(formatInfoLine("Model", data.model, width));
230
+ lines.push(formatInfoLine("Provider", data.provider, width));
231
+ lines.push(formatInfoLine("Directory", data.directory, width));
232
+ lines.push(formatInfoLine("Branch", data.branch, width));
233
+ lines.push("");
234
+ // MCP
235
+ const mcpRow = formatMcpRow(data.mcpServers, width);
236
+ if (mcpRow)
237
+ lines.push(` ${mcpRow}`);
238
+ // Separator
239
+ lines.push(SEPARATOR_CHAR.repeat(width));
240
+ return lines;
241
+ }
@@ -18,12 +18,29 @@ const PRIVATE_IP_PATTERNS = [
18
18
  /^fd/i,
19
19
  /^fe80:/i,
20
20
  ];
21
+ /**
22
+ * Hostnames exempted from SSRF blocking. Set via setFetchAllowedUrls()
23
+ * from global settings.json or GSD_FETCH_ALLOWED_URLS env var.
24
+ */
25
+ let fetchAllowedHostnames = new Set();
26
+ /**
27
+ * Replace the fetch URL allowlist (hostnames exempted from SSRF checks).
28
+ */
29
+ export function setFetchAllowedUrls(hostnames) {
30
+ fetchAllowedHostnames = new Set(hostnames.map((h) => h.toLowerCase()));
31
+ }
32
+ /** Get the currently active fetch URL allowlist. */
33
+ export function getFetchAllowedUrls() {
34
+ return [...fetchAllowedHostnames];
35
+ }
21
36
  export function isBlockedUrl(url) {
22
37
  try {
23
38
  const parsed = new URL(url);
24
39
  if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
25
40
  return true;
26
41
  const hostname = parsed.hostname.toLowerCase();
42
+ if (fetchAllowedHostnames.has(hostname))
43
+ return false;
27
44
  if (BLOCKED_HOSTNAMES.has(hostname))
28
45
  return true;
29
46
  for (const pattern of PRIVATE_IP_PATTERNS) {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Apply user-configured security overrides from global settings.json and env vars.
3
+ *
4
+ * Both overrides are global-only (not project-level) because the threat model is
5
+ * malicious project-level config in cloned repos. Global settings and env vars
6
+ * represent the user's own authority on their machine.
7
+ *
8
+ * Precedence: env var > settings.json > built-in defaults
9
+ */
10
+ import { type SettingsManager } from '@gsd/pi-coding-agent';
11
+ export declare function applySecurityOverrides(settingsManager: SettingsManager): void;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Apply user-configured security overrides from global settings.json and env vars.
3
+ *
4
+ * Both overrides are global-only (not project-level) because the threat model is
5
+ * malicious project-level config in cloned repos. Global settings and env vars
6
+ * represent the user's own authority on their machine.
7
+ *
8
+ * Precedence: env var > settings.json > built-in defaults
9
+ */
10
+ import { setAllowedCommandPrefixes } from '@gsd/pi-coding-agent';
11
+ import { setFetchAllowedUrls } from './resources/extensions/search-the-web/url-utils.js';
12
+ export function applySecurityOverrides(settingsManager) {
13
+ // --- Command prefix allowlist ---
14
+ const envPrefixes = process.env.GSD_ALLOWED_COMMAND_PREFIXES;
15
+ if (envPrefixes) {
16
+ const prefixes = envPrefixes.split(',').map(s => s.trim()).filter(Boolean);
17
+ if (prefixes.length > 0) {
18
+ setAllowedCommandPrefixes(prefixes);
19
+ }
20
+ }
21
+ else {
22
+ const settingsPrefixes = settingsManager.getAllowedCommandPrefixes();
23
+ if (settingsPrefixes && settingsPrefixes.length > 0) {
24
+ setAllowedCommandPrefixes(settingsPrefixes);
25
+ }
26
+ }
27
+ // --- Fetch URL allowlist (SSRF exemptions) ---
28
+ const envUrls = process.env.GSD_FETCH_ALLOWED_URLS;
29
+ if (envUrls) {
30
+ const urls = envUrls.split(',').map(s => s.trim()).filter(Boolean);
31
+ if (urls.length > 0) {
32
+ setFetchAllowedUrls(urls);
33
+ }
34
+ }
35
+ else {
36
+ const settingsUrls = settingsManager.getFetchAllowedUrls();
37
+ if (settingsUrls && settingsUrls.length > 0) {
38
+ setFetchAllowedUrls(settingsUrls);
39
+ }
40
+ }
41
+ }
@@ -1 +1 @@
1
- R0D4xaIPl5kg93edN7Oo0
1
+ nUA6d2OJrDSVq9RNb-c8b