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
@@ -1455,4 +1455,72 @@ describe('git-service', async () => {
1455
1455
  try { rmSync(repo, { recursive: true, force: true }); } catch {}
1456
1456
  try { rmSync(externalGsd, { recursive: true, force: true }); } catch {}
1457
1457
  });
1458
+
1459
+ // ─── autoCommit: absorbs preceding gsd snapshot commits ─────────────────
1460
+
1461
+ test('autoCommit: absorbs preceding gsd snapshot commits', () => {
1462
+ const repo = initTempRepo();
1463
+
1464
+ // Simulate 2 gsd snapshot commits
1465
+ createFile(repo, "file1.ts", "v1");
1466
+ run("git add -A", repo);
1467
+ run('git commit -m "gsd snapshot: uncommitted changes after 35m inactivity"', repo);
1468
+
1469
+ createFile(repo, "file2.ts", "v2");
1470
+ run("git add -A", repo);
1471
+ run('git commit -m "gsd snapshot: pre-dispatch, uncommitted changes after 40m inactivity"', repo);
1472
+
1473
+ // Verify we have 3 commits (init + 2 snapshots)
1474
+ const countBefore = run("git rev-list --count HEAD", repo);
1475
+ assert.deepStrictEqual(countBefore, "3", "precondition: 3 commits before autoCommit");
1476
+
1477
+ // Now make a real change and autoCommit
1478
+ createFile(repo, "feature.ts", "real work");
1479
+
1480
+ const svc = new GitServiceImpl(repo);
1481
+ const msg = svc.autoCommit("execute-task", "S01/T01");
1482
+ assert.ok(msg !== null, "autoCommit succeeds");
1483
+
1484
+ // Should be 2 commits: init + squashed real commit (snapshots absorbed)
1485
+ const countAfter = run("git rev-list --count HEAD", repo);
1486
+ assert.deepStrictEqual(countAfter, "2", "snapshot commits absorbed into real commit");
1487
+
1488
+ // All files should be present
1489
+ const files = run("git show --name-only HEAD", repo);
1490
+ assert.ok(files.includes("file1.ts"), "file1.ts from snapshot 1 preserved");
1491
+ assert.ok(files.includes("file2.ts"), "file2.ts from snapshot 2 preserved");
1492
+ assert.ok(files.includes("feature.ts"), "feature.ts from real commit preserved");
1493
+
1494
+ // No gsd snapshot commits in log
1495
+ const log = run("git log --oneline", repo);
1496
+ assert.ok(!log.includes("gsd snapshot"), "no gsd snapshot commits remain in history");
1497
+
1498
+ rmSync(repo, { recursive: true, force: true });
1499
+ });
1500
+
1501
+ // ─── autoCommit: does not absorb non-snapshot commits ───────────────────
1502
+
1503
+ test('autoCommit: does not absorb non-snapshot commits', () => {
1504
+ const repo = initTempRepo();
1505
+
1506
+ // Create a normal (non-snapshot) commit
1507
+ createFile(repo, "earlier.ts", "earlier work");
1508
+ run("git add -A", repo);
1509
+ run('git commit -m "feat: earlier work"', repo);
1510
+
1511
+ const countBefore = run("git rev-list --count HEAD", repo);
1512
+ assert.deepStrictEqual(countBefore, "2", "precondition: 2 commits before autoCommit");
1513
+
1514
+ // Make a real change and autoCommit
1515
+ createFile(repo, "feature.ts", "new work");
1516
+
1517
+ const svc = new GitServiceImpl(repo);
1518
+ svc.autoCommit("execute-task", "S01/T02");
1519
+
1520
+ // Should be 3 commits — earlier commit not absorbed
1521
+ const countAfter = run("git rev-list --count HEAD", repo);
1522
+ assert.deepStrictEqual(countAfter, "3", "non-snapshot commits NOT absorbed");
1523
+
1524
+ rmSync(repo, { recursive: true, force: true });
1525
+ });
1458
1526
  });
@@ -82,3 +82,47 @@ test("#2572: 'SyntaxError' with JSON context (truncated stream) is transient", (
82
82
  assert.equal(isTransient(result), true, "'SyntaxError...JSON' should be transient");
83
83
  assert.equal(result.kind, "stream", "JSON parse errors are stream kind");
84
84
  });
85
+
86
+ // --- Catch-all: all V8 JSON.parse variants matched by "in JSON at position" ---
87
+
88
+ test("V8 JSON.parse: 'No number after minus sign in JSON' is transient (#2882)", () => {
89
+ const result = classifyError("No number after minus sign in JSON at position 42");
90
+ assert.equal(isTransient(result), true);
91
+ assert.equal(result.kind, "stream");
92
+ });
93
+
94
+ test("V8 JSON.parse: 'Expected property value after colon' is transient", () => {
95
+ const result = classifyError("Expected ',' or '}' after property value in JSON at position 108");
96
+ assert.equal(isTransient(result), true);
97
+ assert.equal(result.kind, "stream");
98
+ });
99
+
100
+ test("V8 JSON.parse: 'Bad control character in string literal' is transient", () => {
101
+ const result = classifyError("Bad control character in string literal in JSON at position 5");
102
+ assert.equal(isTransient(result), true);
103
+ assert.equal(result.kind, "stream");
104
+ });
105
+
106
+ test("V8 JSON.parse: 'Bad escaped character' is transient", () => {
107
+ const result = classifyError("Bad escaped character in JSON at position 17");
108
+ assert.equal(isTransient(result), true);
109
+ assert.equal(result.kind, "stream");
110
+ });
111
+
112
+ test("V8 JSON.parse: 'Unexpected number' is transient", () => {
113
+ const result = classifyError("Unexpected number in JSON at position 0");
114
+ assert.equal(isTransient(result), true);
115
+ assert.equal(result.kind, "stream");
116
+ });
117
+
118
+ test("V8 JSON.parse: 'Unexpected string' is transient", () => {
119
+ const result = classifyError("Unexpected string in JSON at position 12");
120
+ assert.equal(isTransient(result), true);
121
+ assert.equal(result.kind, "stream");
122
+ });
123
+
124
+ test("V8 JSON.parse with line/column suffix is transient", () => {
125
+ const result = classifyError("Unexpected token x in JSON at position 99 (line 3 column 14)");
126
+ assert.equal(isTransient(result), true);
127
+ assert.equal(result.kind, "stream");
128
+ });
@@ -0,0 +1,275 @@
1
+ // GSD Watch — Header renderer: ASCII logo, session info, MCP status, remote questions
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+
4
+ import { execFileSync } from "node:child_process";
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { visibleWidth, truncateToWidth } from "@gsd/pi-tui";
9
+ import { loadEffectiveGSDPreferences } from "../preferences.js";
10
+
11
+ // ─── Constants ────────────────────────────────────────────────────────────────
12
+
13
+ /**
14
+ * GSD ASCII logo — inlined here because the canonical src/logo.ts is outside
15
+ * the resources rootDir and cannot be imported directly.
16
+ */
17
+ const GSD_LOGO: readonly string[] = [
18
+ ' ██████╗ ███████╗██████╗ ',
19
+ ' ██╔════╝ ██╔════╝██╔══██╗',
20
+ ' ██║ ███╗███████╗██║ ██║',
21
+ ' ██║ ██║╚════██║██║ ██║',
22
+ ' ╚██████╔╝███████║██████╔╝',
23
+ ' ╚═════╝ ╚══════╝╚═════╝ ',
24
+ ];
25
+
26
+ /** Separator character for the horizontal divider line. */
27
+ const SEPARATOR_CHAR = "─";
28
+
29
+ /** Vertical bar between logo and info panel. */
30
+ const PANEL_DIVIDER = "│";
31
+
32
+ /** Label column width for Model/Provider/Directory/Branch rows. */
33
+ const LABEL_COL_WIDTH = 10;
34
+
35
+ // ─── Data Readers ─────────────────────────────────────────────────────────────
36
+
37
+ /**
38
+ * Read the configured execution model from GSD preferences.
39
+ * Falls back through execution -> planning -> research -> first found.
40
+ * Returns "default" if nothing is configured.
41
+ */
42
+ export function readModelFromPreferences(): string {
43
+ try {
44
+ const prefs = loadEffectiveGSDPreferences();
45
+ if (!prefs?.preferences.models) return "default";
46
+ const m = prefs.preferences.models as Record<string, unknown>;
47
+ // Try common phases in priority order
48
+ for (const phase of ["execution", "planning", "research", "discuss", "subagent"]) {
49
+ const val = m[phase];
50
+ if (typeof val === "string") return val;
51
+ if (val && typeof val === "object" && "model" in val) {
52
+ const model = (val as { model: string }).model;
53
+ if (typeof model === "string") return model;
54
+ }
55
+ }
56
+ } catch {
57
+ // Non-fatal
58
+ }
59
+ return "default";
60
+ }
61
+
62
+ /**
63
+ * Derive provider name from model ID prefix.
64
+ */
65
+ export function deriveProvider(modelId: string): string {
66
+ if (modelId.startsWith("claude")) return "anthropic";
67
+ if (modelId.startsWith("gpt") || modelId.startsWith("o1") || modelId.startsWith("o3")) return "openai";
68
+ if (modelId.startsWith("gemini")) return "google";
69
+ if (modelId.startsWith("deepseek")) return "deepseek";
70
+ if (modelId === "default") return "anthropic";
71
+ return "unknown";
72
+ }
73
+
74
+ /**
75
+ * Shorten a directory path by replacing the home directory with ~.
76
+ */
77
+ export function shortenPath(fullPath: string): string {
78
+ const home = homedir();
79
+ if (fullPath.startsWith(home)) {
80
+ return "~" + fullPath.slice(home.length);
81
+ }
82
+ return fullPath;
83
+ }
84
+
85
+ /**
86
+ * Read the current git branch name. Returns "unknown" on failure.
87
+ */
88
+ export function readGitBranch(projectRoot: string): string {
89
+ try {
90
+ return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
91
+ cwd: projectRoot,
92
+ encoding: "utf-8",
93
+ timeout: 2000,
94
+ }).trim();
95
+ } catch {
96
+ return "unknown";
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Read MCP server names from .mcp.json or .gsd/mcp.json.
102
+ * Returns array of server name strings.
103
+ */
104
+ export function readMcpServerNames(projectRoot: string): string[] {
105
+ const configPaths = [
106
+ join(projectRoot, ".mcp.json"),
107
+ join(projectRoot, ".gsd", "mcp.json"),
108
+ ];
109
+ const names: string[] = [];
110
+ const seen = new Set<string>();
111
+
112
+ for (const configPath of configPaths) {
113
+ try {
114
+ if (!existsSync(configPath)) continue;
115
+ const raw = readFileSync(configPath, "utf-8");
116
+ const data = JSON.parse(raw) as Record<string, unknown>;
117
+ const mcpServers = (data.mcpServers ?? data.servers) as
118
+ | Record<string, unknown>
119
+ | undefined;
120
+ if (!mcpServers || typeof mcpServers !== "object") continue;
121
+ for (const name of Object.keys(mcpServers)) {
122
+ if (!seen.has(name)) {
123
+ seen.add(name);
124
+ names.push(name);
125
+ }
126
+ }
127
+ } catch {
128
+ // Non-fatal
129
+ }
130
+ }
131
+
132
+ return names;
133
+ }
134
+
135
+ // ─── Header Layout ────────────────────────────────────────────────────────────
136
+
137
+ export interface HeaderData {
138
+ model: string;
139
+ provider: string;
140
+ directory: string;
141
+ branch: string;
142
+ mcpServers: string[];
143
+ }
144
+
145
+ /**
146
+ * Gather all header data from filesystem and preferences.
147
+ */
148
+ export function gatherHeaderData(projectRoot: string): HeaderData {
149
+ const model = readModelFromPreferences();
150
+ const provider = deriveProvider(model);
151
+ const directory = shortenPath(projectRoot);
152
+ const branch = readGitBranch(projectRoot);
153
+ const mcpServers = readMcpServerNames(projectRoot);
154
+
155
+ return { model, provider, directory, branch, mcpServers };
156
+ }
157
+
158
+ /**
159
+ * Build an info panel line: "Label value" with proper padding.
160
+ * Returns empty string if value is empty.
161
+ */
162
+ function formatInfoLine(label: string, value: string, availableWidth: number): string {
163
+ const bold = `\x1b[1m${label}\x1b[0m`;
164
+ const labelVis = visibleWidth(bold);
165
+ const padding = " ".repeat(Math.max(1, LABEL_COL_WIDTH - labelVis));
166
+ const maxValueWidth = Math.max(1, availableWidth - LABEL_COL_WIDTH);
167
+ const truncValue = truncateToWidth(value, maxValueWidth, "…");
168
+ return bold + padding + truncValue;
169
+ }
170
+
171
+ /**
172
+ * Format MCP server names as a dot-separated row with checkmarks.
173
+ * e.g. "Brave ✓ · Answers ✓ · Context7 ✓"
174
+ */
175
+ export function formatMcpRow(servers: string[], width: number): string {
176
+ if (servers.length === 0) return "";
177
+
178
+ // Capitalize first letter of each server name
179
+ const items = servers.map(s => {
180
+ const cap = s.charAt(0).toUpperCase() + s.slice(1);
181
+ return `${cap} ✓`;
182
+ });
183
+
184
+ const full = items.join(" · ");
185
+ if (visibleWidth(full) <= width) return full;
186
+
187
+ // Truncate if too wide
188
+ return truncateToWidth(full, width, "…");
189
+ }
190
+
191
+ /**
192
+ * Render the full header as an array of terminal-safe strings.
193
+ *
194
+ * Layout: GSD ASCII logo on the left, info panel on the right separated by │.
195
+ * Below: MCP server row, remote questions row, separator line.
196
+ */
197
+ export function renderHeaderLines(data: HeaderData, width: number): string[] {
198
+ const lines: string[] = [];
199
+
200
+ // Logo is 6 lines tall. Info panel has: title + blank + model + provider + directory + branch = 6 lines
201
+ const logoLines = GSD_LOGO;
202
+ const logoWidth = Math.max(...logoLines.map(l => visibleWidth(l)));
203
+
204
+ // Calculate available width for the info panel
205
+ // Layout: logo + " " + "│" + " " = logoWidth + 3
206
+ const dividerOverhead = 3; // " │ "
207
+ const infoPanelWidth = width - logoWidth - dividerOverhead;
208
+
209
+ // If terminal is too narrow for side-by-side, fall back to stacked layout
210
+ if (infoPanelWidth < 20) {
211
+ return renderStackedHeader(data, width);
212
+ }
213
+
214
+ // Build info panel lines (6 lines to match logo height)
215
+ const infoLines: string[] = [
216
+ `\x1b[1mGet Shit Done\x1b[0m`,
217
+ "",
218
+ formatInfoLine("Model", data.model, infoPanelWidth),
219
+ formatInfoLine("Provider", data.provider, infoPanelWidth),
220
+ formatInfoLine("Directory", data.directory, infoPanelWidth),
221
+ formatInfoLine("Branch", data.branch, infoPanelWidth),
222
+ ];
223
+
224
+ // Merge logo and info panel side by side
225
+ const maxLines = Math.max(logoLines.length, infoLines.length);
226
+ for (let i = 0; i < maxLines; i++) {
227
+ const logoLine = i < logoLines.length ? logoLines[i] : "";
228
+ const infoLine = i < infoLines.length ? infoLines[i] : "";
229
+
230
+ // Pad logo line to consistent width
231
+ const logoPad = " ".repeat(Math.max(0, logoWidth - visibleWidth(logoLine)));
232
+ lines.push(`${logoLine}${logoPad} ${PANEL_DIVIDER} ${infoLine}`);
233
+ }
234
+
235
+ // Blank line after logo+info block
236
+ lines.push("");
237
+
238
+ // MCP server row
239
+ const mcpRow = formatMcpRow(data.mcpServers, width);
240
+ if (mcpRow) {
241
+ lines.push(` ${mcpRow}`);
242
+ }
243
+
244
+ // Separator line
245
+ lines.push(SEPARATOR_CHAR.repeat(width));
246
+
247
+ return lines;
248
+ }
249
+
250
+ /**
251
+ * Fallback stacked layout for narrow terminals (< 20 cols for info panel).
252
+ */
253
+ function renderStackedHeader(data: HeaderData, width: number): string[] {
254
+ const lines: string[] = [];
255
+
256
+ // Title
257
+ lines.push(`\x1b[1mGet Shit Done\x1b[0m`);
258
+ lines.push("");
259
+
260
+ // Info
261
+ lines.push(formatInfoLine("Model", data.model, width));
262
+ lines.push(formatInfoLine("Provider", data.provider, width));
263
+ lines.push(formatInfoLine("Directory", data.directory, width));
264
+ lines.push(formatInfoLine("Branch", data.branch, width));
265
+ lines.push("");
266
+
267
+ // MCP
268
+ const mcpRow = formatMcpRow(data.mcpServers, width);
269
+ if (mcpRow) lines.push(` ${mcpRow}`);
270
+
271
+ // Separator
272
+ lines.push(SEPARATOR_CHAR.repeat(width));
273
+
274
+ return lines;
275
+ }
@@ -21,11 +21,30 @@ const PRIVATE_IP_PATTERNS = [
21
21
  /^fe80:/i,
22
22
  ];
23
23
 
24
+ /**
25
+ * Hostnames exempted from SSRF blocking. Set via setFetchAllowedUrls()
26
+ * from global settings.json or GSD_FETCH_ALLOWED_URLS env var.
27
+ */
28
+ let fetchAllowedHostnames: Set<string> = new Set();
29
+
30
+ /**
31
+ * Replace the fetch URL allowlist (hostnames exempted from SSRF checks).
32
+ */
33
+ export function setFetchAllowedUrls(hostnames: string[]): void {
34
+ fetchAllowedHostnames = new Set(hostnames.map((h) => h.toLowerCase()));
35
+ }
36
+
37
+ /** Get the currently active fetch URL allowlist. */
38
+ export function getFetchAllowedUrls(): readonly string[] {
39
+ return [...fetchAllowedHostnames];
40
+ }
41
+
24
42
  export function isBlockedUrl(url: string): boolean {
25
43
  try {
26
44
  const parsed = new URL(url);
27
45
  if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return true;
28
46
  const hostname = parsed.hostname.toLowerCase();
47
+ if (fetchAllowedHostnames.has(hostname)) return false;
29
48
  if (BLOCKED_HOSTNAMES.has(hostname)) return true;
30
49
  for (const pattern of PRIVATE_IP_PATTERNS) {
31
50
  if (pattern.test(hostname)) return true;