gsd-pi 2.46.0 → 2.46.1-dev.44f59e2

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 (191) hide show
  1. package/README.md +47 -29
  2. package/dist/resources/extensions/claude-code-cli/index.js +25 -0
  3. package/dist/resources/extensions/claude-code-cli/models.js +40 -0
  4. package/dist/resources/extensions/claude-code-cli/package.json +11 -0
  5. package/dist/resources/extensions/claude-code-cli/partial-builder.js +223 -0
  6. package/dist/resources/extensions/claude-code-cli/readiness.js +26 -0
  7. package/dist/resources/extensions/claude-code-cli/sdk-types.js +8 -0
  8. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +293 -0
  9. package/dist/resources/extensions/gsd/auto-start.js +16 -15
  10. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +2 -0
  11. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  12. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +2 -2
  13. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  14. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  15. package/dist/resources/extensions/gsd/prompts/research-milestone.md +2 -2
  16. package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -2
  17. package/dist/resources/extensions/gsd/repo-identity.js +5 -2
  18. package/dist/resources/extensions/gsd/state.js +29 -2
  19. package/dist/resources/extensions/gsd/workflow-events.js +1 -1
  20. package/dist/resources/extensions/remote-questions/config.js +42 -0
  21. package/dist/web/standalone/.next/BUILD_ID +1 -1
  22. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  23. package/dist/web/standalone/.next/build-manifest.json +3 -3
  24. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  25. package/dist/web/standalone/.next/required-server-files.json +3 -3
  26. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  29. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  37. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  47. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  48. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  49. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  53. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +5 -5
  91. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  97. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  111. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  113. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  115. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  117. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  126. package/dist/web/standalone/.next/server/app/index.html +1 -1
  127. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  128. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  129. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  130. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  131. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  132. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  133. package/dist/web/standalone/.next/server/app/page.js +2 -2
  134. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  135. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  136. package/dist/web/standalone/.next/server/chunks/229.js +1 -1
  137. package/dist/web/standalone/.next/server/chunks/471.js +3 -3
  138. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  139. package/dist/web/standalone/.next/server/middleware.js +2 -2
  140. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  141. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  142. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  143. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  144. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  145. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  146. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  147. package/dist/web/standalone/.next/static/chunks/app/page-6654a8cca61a3d1c.js +1 -0
  148. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  149. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  150. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  151. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  152. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  153. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  154. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  155. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  156. package/dist/web/standalone/server.js +1 -1
  157. package/package.json +3 -1
  158. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +27 -2
  159. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  160. package/packages/pi-coding-agent/package.json +1 -1
  161. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +27 -2
  162. package/pkg/package.json +1 -1
  163. package/src/resources/extensions/claude-code-cli/index.ts +28 -0
  164. package/src/resources/extensions/claude-code-cli/models.ts +42 -0
  165. package/src/resources/extensions/claude-code-cli/package.json +11 -0
  166. package/src/resources/extensions/claude-code-cli/partial-builder.ts +258 -0
  167. package/src/resources/extensions/claude-code-cli/readiness.ts +30 -0
  168. package/src/resources/extensions/claude-code-cli/sdk-types.ts +149 -0
  169. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +355 -0
  170. package/src/resources/extensions/gsd/auto-start.ts +16 -15
  171. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +3 -0
  172. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  173. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +2 -2
  174. package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  175. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  176. package/src/resources/extensions/gsd/prompts/research-milestone.md +2 -2
  177. package/src/resources/extensions/gsd/prompts/run-uat.md +2 -2
  178. package/src/resources/extensions/gsd/repo-identity.ts +5 -2
  179. package/src/resources/extensions/gsd/state.ts +33 -1
  180. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +7 -3
  181. package/src/resources/extensions/gsd/tests/inherited-repo-home-dir.test.ts +70 -0
  182. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +40 -0
  183. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +84 -0
  184. package/src/resources/extensions/gsd/tests/run-uat.test.ts +25 -0
  185. package/src/resources/extensions/gsd/workflow-events.ts +1 -1
  186. package/src/resources/extensions/remote-questions/config.ts +45 -0
  187. package/dist/web/standalone/.next/static/chunks/app/page-12dd5ece0df4badc.js +0 -1
  188. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  189. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  190. /package/dist/web/standalone/.next/static/{8zT99piZz8u3xAU3Omz2g → sQCECerYL5daQCVxqF23f}/_buildManifest.js +0 -0
  191. /package/dist/web/standalone/.next/static/{8zT99piZz8u3xAU3Omz2g → sQCECerYL5daQCVxqF23f}/_ssgManifest.js +0 -0
@@ -49,6 +49,7 @@ import {
49
49
  getReplanHistory,
50
50
  getSlice,
51
51
  insertMilestone,
52
+ updateTaskStatus,
52
53
  type MilestoneRow,
53
54
  type SliceRow,
54
55
  type TaskRow,
@@ -629,7 +630,38 @@ export async function deriveStateFromDb(basePath: string): Promise<GSDState> {
629
630
  }
630
631
 
631
632
  // ── Get tasks from DB ────────────────────────────────────────────────
632
- const tasks = getSliceTasks(activeMilestone.id, activeSlice.id);
633
+ let tasks = getSliceTasks(activeMilestone.id, activeSlice.id);
634
+
635
+ // ── Reconcile stale task status (#2514) ──────────────────────────────
636
+ // When a session disconnects after the agent writes SUMMARY + VERIFY
637
+ // artifacts but before postUnitPostVerification updates the DB, tasks
638
+ // remain "pending" in the DB despite being complete on disk. Without
639
+ // reconciliation, deriveState keeps returning the stale task as active,
640
+ // causing the dispatcher to re-dispatch the same completed task forever.
641
+ let reconciled = false;
642
+ for (const t of tasks) {
643
+ if (isStatusDone(t.status)) continue;
644
+ const summaryPath = resolveTaskFile(basePath, activeMilestone.id, activeSlice.id, t.id, "SUMMARY");
645
+ if (summaryPath && existsSync(summaryPath)) {
646
+ try {
647
+ updateTaskStatus(activeMilestone.id, activeSlice.id, t.id, "complete");
648
+ process.stderr.write(
649
+ `gsd-reconcile: task ${activeMilestone.id}/${activeSlice.id}/${t.id} had SUMMARY on disk but DB status was "${t.status}" — updated to "complete" (#2514)\n`,
650
+ );
651
+ reconciled = true;
652
+ } catch (e) {
653
+ // DB write failed — continue with stale status rather than crash
654
+ process.stderr.write(
655
+ `gsd-reconcile: failed to update task ${t.id}: ${(e as Error).message}\n`,
656
+ );
657
+ }
658
+ }
659
+ }
660
+ // Re-fetch tasks if any were reconciled so downstream logic sees fresh status
661
+ if (reconciled) {
662
+ tasks = getSliceTasks(activeMilestone.id, activeSlice.id);
663
+ }
664
+
633
665
  const taskProgress = {
634
666
  done: tasks.filter(t => isStatusDone(t.status)).length,
635
667
  total: tasks.length,
@@ -136,9 +136,10 @@ describe('ensure-db-open', () => {
136
136
  // ensureDbOpen returns false for empty .gsd/ (no Markdown, no DB)
137
137
  // ═══════════════════════════════════════════════════════════════════════════
138
138
 
139
- test('ensureDbOpen: empty .gsd/ returns false', async () => {
139
+ test('ensureDbOpen: empty .gsd/ creates empty DB (#2510)', async () => {
140
140
  const tmpDir = makeTmpDir();
141
- fs.mkdirSync(path.join(tmpDir, '.gsd'), { recursive: true });
141
+ const gsdDir = path.join(tmpDir, '.gsd');
142
+ fs.mkdirSync(gsdDir, { recursive: true });
142
143
  // .gsd/ exists but no DECISIONS.md, REQUIREMENTS.md, or milestones/
143
144
 
144
145
  try { closeDatabase(); } catch { /* ok */ }
@@ -148,9 +149,12 @@ describe('ensure-db-open', () => {
148
149
  try {
149
150
  const { ensureDbOpen } = await import('../bootstrap/dynamic-tools.ts');
150
151
  const result = await ensureDbOpen();
151
- assert.ok(result === false, 'ensureDbOpen should return false for empty .gsd/');
152
+ assert.ok(result === true, 'ensureDbOpen should create empty DB for fresh .gsd/');
153
+ assert.ok(fs.existsSync(path.join(gsdDir, 'gsd.db')), 'DB file should be created');
154
+ assert.ok(isDbAvailable(), 'DB should be available');
152
155
  } finally {
153
156
  process.cwd = origCwd;
157
+ closeDatabase();
154
158
  cleanupDir(tmpDir);
155
159
  }
156
160
  });
@@ -119,3 +119,73 @@ describe("isInheritedRepo when git root is HOME (#2393)", () => {
119
119
  );
120
120
  });
121
121
  });
122
+
123
+ describe("isInheritedRepo with stale .gsd at parent git root", () => {
124
+ let parentRepo: string;
125
+
126
+ beforeEach(() => {
127
+ parentRepo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-stale-parent-")));
128
+ run("git", ["init", "-b", "main"], parentRepo);
129
+ run("git", ["config", "user.name", "Test"], parentRepo);
130
+ run("git", ["config", "user.email", "test@example.com"], parentRepo);
131
+ writeFileSync(join(parentRepo, "README.md"), "# Parent\n", "utf-8");
132
+ run("git", ["add", "README.md"], parentRepo);
133
+ run("git", ["commit", "-m", "init"], parentRepo);
134
+ });
135
+
136
+ afterEach(() => {
137
+ rmSync(parentRepo, { recursive: true, force: true });
138
+ });
139
+
140
+ test("stale .gsd dir at parent git root does not suppress inherited detection", () => {
141
+ // Simulate a stale .gsd directory at the parent git root (e.g. from a
142
+ // prior doctor run or accidental init). This is a real directory, NOT
143
+ // a symlink, and NOT the global GSD home.
144
+ mkdirSync(join(parentRepo, ".gsd"), { recursive: true });
145
+
146
+ const projectDir = join(parentRepo, "my-project");
147
+ mkdirSync(projectDir, { recursive: true });
148
+
149
+ // Without fix: isProjectGsd(join(root, ".gsd")) returns true because
150
+ // the stale .gsd is a real directory that isn't the global GSD home,
151
+ // causing isInheritedRepo to return false (false negative).
152
+ //
153
+ // The stale .gsd at parent is still treated as a "project .gsd" by
154
+ // isProjectGsd(), so the git root check at line 128 returns false.
155
+ // This is the expected behavior for that check — the defense-in-depth
156
+ // fix in auto-start.ts handles this case by checking for local .git.
157
+ //
158
+ // Verify the function behavior is consistent:
159
+ assert.strictEqual(
160
+ isInheritedRepo(projectDir),
161
+ false,
162
+ "stale .gsd dir at git root still causes isInheritedRepo to return false " +
163
+ "(defense-in-depth in auto-start.ts handles this case)",
164
+ );
165
+ });
166
+
167
+ test("basePath's own .gsd symlink does not suppress inherited detection", () => {
168
+ // Create a project subdir with its own .gsd symlink (set up during
169
+ // the discuss phase, before auto-mode bootstrap runs).
170
+ const projectDir = join(parentRepo, "my-project");
171
+ mkdirSync(projectDir, { recursive: true });
172
+
173
+ const externalState = mkdtempSync(join(tmpdir(), "gsd-ext-state-"));
174
+ symlinkSync(externalState, join(projectDir, ".gsd"));
175
+
176
+ // Before fix: the walk-up loop started at normalizedBase (projectDir),
177
+ // found .gsd at projectDir, and returned false — even though projectDir
178
+ // has no .git of its own. The .gsd at basePath is irrelevant to whether
179
+ // the git repo is inherited from a parent.
180
+ //
181
+ // After fix: the walk-up starts at dirname(normalizedBase), skipping
182
+ // basePath's own .gsd.
183
+ assert.strictEqual(
184
+ isInheritedRepo(projectDir),
185
+ true,
186
+ "project's own .gsd symlink must not suppress inherited repo detection",
187
+ );
188
+
189
+ rmSync(externalState, { recursive: true, force: true });
190
+ });
191
+ });
@@ -61,6 +61,18 @@ test("plan-slice prompt: DB-backed tool names survive template substitution", ()
61
61
  assert.ok(result.includes("canonical write path"), "canonical write path language should survive substitution");
62
62
  });
63
63
 
64
+ test("plan-slice prompt: footer references gsd_plan_slice tool, not direct write", () => {
65
+ const result = loadPrompt("plan-slice", { ...BASE_VARS, commitInstruction: "Do not commit." });
66
+ assert.ok(
67
+ result.includes("MUST call `gsd_plan_slice`"),
68
+ "footer should instruct calling gsd_plan_slice tool",
69
+ );
70
+ assert.ok(
71
+ !result.includes("MUST write the file"),
72
+ "footer should not instruct direct file write",
73
+ );
74
+ });
75
+
64
76
  test("domain-work prompts use skillActivation placeholder", () => {
65
77
  const prompts = [
66
78
  "research-milestone",
@@ -174,6 +186,34 @@ test("research-milestone prompt substitutes skillActivation", () => {
174
186
  assert.ok(!result.includes("{{skillActivation}}"));
175
187
  });
176
188
 
189
+ test("research-milestone prompt references gsd_summary_save, not direct write", () => {
190
+ const result = loadPrompt("research-milestone", {
191
+ workingDirectory: "/tmp/test-project",
192
+ milestoneId: "M001",
193
+ milestoneTitle: "Test Milestone",
194
+ milestonePath: ".gsd/milestones/M001",
195
+ contextPath: ".gsd/milestones/M001/M001-CONTEXT.md",
196
+ outputPath: "/tmp/test-project/.gsd/milestones/M001/M001-RESEARCH.md",
197
+ inlinedContext: "Context",
198
+ skillDiscoveryMode: "manual",
199
+ skillDiscoveryInstructions: " Discover skills manually.",
200
+ skillActivation: "Load research skills first.",
201
+ });
202
+
203
+ assert.ok(
204
+ result.includes("gsd_summary_save"),
205
+ "research-milestone should reference gsd_summary_save tool",
206
+ );
207
+ assert.ok(
208
+ result.includes('artifact_type: "RESEARCH"'),
209
+ "research-milestone should specify RESEARCH artifact type",
210
+ );
211
+ assert.ok(
212
+ !result.includes("MUST write the file"),
213
+ "research-milestone should not instruct direct file write",
214
+ );
215
+ });
216
+
177
217
  test("research-slice prompt substitutes skillActivation", () => {
178
218
  const result = loadPrompt("research-slice", {
179
219
  workingDirectory: "/tmp/test-project",
@@ -640,3 +640,87 @@ test("DiscordAdapter source-level: sendPrompt sets threadUrl in ref", () => {
640
640
  "sendPrompt should set threadUrl to the constructed message URL",
641
641
  );
642
642
  });
643
+
644
+ // ═══════════════════════════════════════════════════════════════════════════
645
+ // Auth.json Token Hydration Tests
646
+ // ═══════════════════════════════════════════════════════════════════════════
647
+
648
+ test("config source-level: hydrateRemoteTokensFromAuth is called before env check in resolveRemoteConfig", () => {
649
+ const configSrc = readFileSync(
650
+ join(__dirname, "..", "..", "remote-questions", "config.ts"),
651
+ "utf-8",
652
+ );
653
+ // Find the body of resolveRemoteConfig by slicing from its declaration to the next export function.
654
+ const resolveStart = configSrc.indexOf("export function resolveRemoteConfig()");
655
+ const resolveEnd = configSrc.indexOf("\nexport function", resolveStart + 1);
656
+ const resolveFnBody = configSrc.slice(resolveStart, resolveEnd);
657
+
658
+ const hydrationIdx = resolveFnBody.indexOf("hydrateRemoteTokensFromAuth()");
659
+ const envCheckIdx = resolveFnBody.indexOf("process.env[ENV_KEYS[");
660
+ assert.ok(hydrationIdx !== -1, "hydrateRemoteTokensFromAuth() should be called inside resolveRemoteConfig");
661
+ assert.ok(envCheckIdx !== -1, "process.env[ENV_KEYS[ lookup should exist inside resolveRemoteConfig");
662
+ assert.ok(hydrationIdx < envCheckIdx, "hydration call should appear before the process.env env-key lookup");
663
+ });
664
+
665
+ test("config source-level: hydrateRemoteTokensFromAuth is called in getRemoteConfigStatus", () => {
666
+ const configSrc = readFileSync(
667
+ join(__dirname, "..", "..", "remote-questions", "config.ts"),
668
+ "utf-8",
669
+ );
670
+ const statusFnIdx = configSrc.indexOf("export function getRemoteConfigStatus()");
671
+ const hydrationInStatus = configSrc.indexOf("hydrateRemoteTokensFromAuth()", statusFnIdx);
672
+ assert.ok(hydrationInStatus > statusFnIdx, "hydrateRemoteTokensFromAuth should be called inside getRemoteConfigStatus");
673
+ });
674
+
675
+ test("config source-level: AUTH_PROVIDER_ENV_MAP covers all three remote channels", () => {
676
+ const configSrc = readFileSync(
677
+ join(__dirname, "..", "..", "remote-questions", "config.ts"),
678
+ "utf-8",
679
+ );
680
+ assert.ok(configSrc.includes("discord_bot"), "AUTH_PROVIDER_ENV_MAP should include discord_bot");
681
+ assert.ok(configSrc.includes("slack_bot"), "AUTH_PROVIDER_ENV_MAP should include slack_bot");
682
+ assert.ok(configSrc.includes("telegram_bot"), "AUTH_PROVIDER_ENV_MAP should include telegram_bot");
683
+ assert.ok(configSrc.includes("DISCORD_BOT_TOKEN"), "should map discord_bot to DISCORD_BOT_TOKEN");
684
+ assert.ok(configSrc.includes("SLACK_BOT_TOKEN"), "should map slack_bot to SLACK_BOT_TOKEN");
685
+ assert.ok(configSrc.includes("TELEGRAM_BOT_TOKEN"), "should map telegram_bot to TELEGRAM_BOT_TOKEN");
686
+ });
687
+
688
+ test("config source-level: hydration skips env vars already set", () => {
689
+ const configSrc = readFileSync(
690
+ join(__dirname, "..", "..", "remote-questions", "config.ts"),
691
+ "utf-8",
692
+ );
693
+ // The guard that skips already-set vars must be present.
694
+ assert.ok(
695
+ configSrc.includes("!process.env[envVar]"),
696
+ "hydrateRemoteTokensFromAuth should skip env vars that are already populated",
697
+ );
698
+ });
699
+
700
+ test("resolveRemoteConfig returns null when preferences are absent (no env side-effects)", () => {
701
+ // Guard: ensure that with no prefs configured, resolveRemoteConfig returns null cleanly.
702
+ // This exercises the hydration path without auth.json present (it should no-op silently).
703
+ const savedHome = process.env.HOME;
704
+ const savedUserProfile = process.env.USERPROFILE;
705
+ const savedDiscord = process.env.DISCORD_BOT_TOKEN;
706
+ const savedSlack = process.env.SLACK_BOT_TOKEN;
707
+ const savedTelegram = process.env.TELEGRAM_BOT_TOKEN;
708
+ try {
709
+ // Point HOME to a nonexistent dir so auth.json lookup finds nothing.
710
+ process.env.HOME = "/tmp/gsd-no-such-home-for-test";
711
+ process.env.USERPROFILE = "/tmp/gsd-no-such-home-for-test";
712
+ delete process.env.DISCORD_BOT_TOKEN;
713
+ delete process.env.SLACK_BOT_TOKEN;
714
+ delete process.env.TELEGRAM_BOT_TOKEN;
715
+
716
+ const result = resolveRemoteConfig();
717
+ // With no prefs file, result is null — not an exception.
718
+ assert.equal(result, null, "resolveRemoteConfig should return null when no preferences are configured");
719
+ } finally {
720
+ process.env.HOME = savedHome;
721
+ process.env.USERPROFILE = savedUserProfile;
722
+ if (savedDiscord !== undefined) process.env.DISCORD_BOT_TOKEN = savedDiscord;
723
+ if (savedSlack !== undefined) process.env.SLACK_BOT_TOKEN = savedSlack;
724
+ if (savedTelegram !== undefined) process.env.TELEGRAM_BOT_TOKEN = savedTelegram;
725
+ }
726
+ });
@@ -228,6 +228,31 @@ test('(k) run-uat prompt template', () => {
228
228
  );
229
229
  });
230
230
 
231
+ test('(k2) run-uat prompt references gsd_summary_save, not direct write', () => {
232
+ const promptResult = loadPromptFromWorktree('run-uat', {
233
+ workingDirectory: '/tmp/test-project',
234
+ milestoneId: 'M001',
235
+ sliceId: 'S01',
236
+ uatPath: '.gsd/milestones/M001/slices/S01/S01-UAT.md',
237
+ uatResultPath: '.gsd/milestones/M001/slices/S01/S01-UAT-RESULT.md',
238
+ uatType: 'artifact-driven',
239
+ inlinedContext: '<!-- no context -->',
240
+ });
241
+
242
+ assert.ok(
243
+ promptResult.includes('gsd_summary_save'),
244
+ 'run-uat prompt should reference gsd_summary_save tool',
245
+ );
246
+ assert.ok(
247
+ promptResult.includes('artifact_type: "ASSESSMENT"'),
248
+ 'run-uat prompt should specify ASSESSMENT artifact type',
249
+ );
250
+ assert.ok(
251
+ !promptResult.includes('MUST write'),
252
+ 'run-uat prompt should not instruct direct file write in footer',
253
+ );
254
+ });
255
+
231
256
  test('(l) dispatch preconditions via resolveSliceFile', () => {
232
257
  const base = createFixtureBase();
233
258
  const uatContent = makeUatContent('artifact-driven');
@@ -40,7 +40,7 @@ export function appendEvent(
40
40
  event: Omit<WorkflowEvent, "hash" | "session_id"> & { actor_name?: string; trigger_reason?: string },
41
41
  ): void {
42
42
  const hash = createHash("sha256")
43
- .update(JSON.stringify({ cmd: event.cmd, params: event.params, ts: event.ts }))
43
+ .update(JSON.stringify({ cmd: event.cmd, params: event.params }))
44
44
  .digest("hex")
45
45
  .slice(0, 16);
46
46
 
@@ -2,6 +2,7 @@
2
2
  * Remote Questions — configuration resolution and validation
3
3
  */
4
4
 
5
+ import { join } from "node:path";
5
6
  import { loadEffectiveGSDPreferences, type RemoteQuestionsConfig } from "../gsd/preferences.js";
6
7
  import type { RemoteChannel } from "./types.js";
7
8
 
@@ -33,7 +34,50 @@ const MAX_TIMEOUT_MINUTES = 30;
33
34
  const MIN_POLL_INTERVAL_SECONDS = 2;
34
35
  const MAX_POLL_INTERVAL_SECONDS = 30;
35
36
 
37
+ // Provider IDs in auth.json that correspond to remote channel env vars.
38
+ const AUTH_PROVIDER_ENV_MAP: Record<string, string> = {
39
+ discord_bot: "DISCORD_BOT_TOKEN",
40
+ slack_bot: "SLACK_BOT_TOKEN",
41
+ telegram_bot: "TELEGRAM_BOT_TOKEN",
42
+ };
43
+
44
+ /**
45
+ * Populate remote channel env vars from auth.json when they are not already
46
+ * set in the environment. Called before every config resolution so that tokens
47
+ * saved via `/gsd remote discord` (or `/gsd keys add discord_bot`) survive
48
+ * process restarts without requiring the user to export env vars manually.
49
+ *
50
+ * Silently no-ops if auth.json is absent, unreadable, or malformed.
51
+ */
52
+ function hydrateRemoteTokensFromAuth(): void {
53
+ const needed = Object.entries(AUTH_PROVIDER_ENV_MAP).filter(([, envVar]) => !process.env[envVar]);
54
+ if (needed.length === 0) return;
55
+
56
+ try {
57
+ const { AuthStorage } = require("@gsd/pi-coding-agent") as typeof import("@gsd/pi-coding-agent");
58
+ const authPath = join(process.env.HOME ?? "~", ".gsd", "agent", "auth.json");
59
+ const auth = AuthStorage.create(authPath);
60
+
61
+ for (const [providerId, envVar] of needed) {
62
+ try {
63
+ const creds = auth.getCredentialsForProvider(providerId);
64
+ const apiKeyCred = creds.find((c: { type: string }) => c.type === "api_key") as
65
+ | { type: "api_key"; key: string }
66
+ | undefined;
67
+ if (apiKeyCred?.key) {
68
+ process.env[envVar] = apiKeyCred.key;
69
+ }
70
+ } catch {
71
+ // Per-provider failure is non-fatal — skip and move on.
72
+ }
73
+ }
74
+ } catch {
75
+ // AuthStorage unavailable (unit tests, stripped build) — skip silently.
76
+ }
77
+ }
78
+
36
79
  export function resolveRemoteConfig(): ResolvedConfig | null {
80
+ hydrateRemoteTokensFromAuth();
37
81
  const prefs = loadEffectiveGSDPreferences();
38
82
  const rq: RemoteQuestionsConfig | undefined = prefs?.preferences.remote_questions;
39
83
  if (!rq || !rq.channel || !rq.channel_id) return null;
@@ -58,6 +102,7 @@ export function resolveRemoteConfig(): ResolvedConfig | null {
58
102
  }
59
103
 
60
104
  export function getRemoteConfigStatus(): string {
105
+ hydrateRemoteTokensFromAuth();
61
106
  const prefs = loadEffectiveGSDPreferences();
62
107
  const rq: RemoteQuestionsConfig | undefined = prefs?.preferences.remote_questions;
63
108
  if (!rq || !rq.channel || !rq.channel_id) return "Remote questions: not configured";
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[8974],{5214:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"workAsyncStorage",{enumerable:!0,get:function(){return r.workAsyncStorageInstance}});let r=n(17828)},15726:(e,t,n)=>{Promise.resolve().then(n.bind(n,66919))},17828:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"workAsyncStorageInstance",{enumerable:!0,get:function(){return r}});let r=(0,n(64054).createAsyncLocalStorage)()},21957:(e,t,n)=>{"use strict";function r({moduleIds:e}){return null}Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"PreloadChunks",{enumerable:!0,get:function(){return r}}),n(95155),n(47650),n(5214),n(2451),n(53887)},37206:(e,t,n)=>{"use strict";n.d(t,{default:()=>u.a});var r=n(75707),u=n.n(r)},41112:(e,t,n)=>{"use strict";function r({reason:e,children:t}){return t}Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"BailoutToCSR",{enumerable:!0,get:function(){return r}}),n(1980)},64054:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n={bindSnapshot:function(){return s},createAsyncLocalStorage:function(){return a},createSnapshot:function(){return i}};for(var r in n)Object.defineProperty(t,r,{enumerable:!0,get:n[r]});let u=Object.defineProperty(Error("Invariant: AsyncLocalStorage accessed in runtime where it is not available"),"__NEXT_ERROR_CODE",{value:"E504",enumerable:!1,configurable:!0});class l{disable(){throw u}getStore(){}run(){throw u}exit(){throw u}enterWith(){throw u}static bind(e){return e}}let o="u">typeof globalThis&&globalThis.AsyncLocalStorage;function a(){return o?new o:new l}function s(e){return o?o.bind(e):l.bind(e)}function i(){return o?o.snapshot():function(e,...t){return e(...t)}}},66919:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>l});var r=n(95155);let u=(0,n(37206).default)(()=>Promise.all([n.e(1838),n.e(6079),n.e(4986),n.e(485),n.e(4024)]).then(n.bind(n,4024)).then(e=>e.GSDAppShell),{loadableGenerated:{webpack:()=>[4024]},ssr:!1,loading:()=>(0,r.jsx)("div",{className:"flex h-screen items-center justify-center bg-background text-sm text-muted-foreground",children:"Loading workspace…"})});function l(){return(0,r.jsx)(u,{})}},68635:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return s}});let r=n(95155),u=n(12115),l=n(41112);function o(e){return{default:e&&"default"in e?e.default:e}}n(21957);let a={loader:()=>Promise.resolve(o(()=>null)),loading:null,ssr:!0},s=function(e){let t={...a,...e},n=(0,u.lazy)(()=>t.loader().then(o)),s=t.loading;function i(e){let o=s?(0,r.jsx)(s,{isLoading:!0,pastDelay:!0,error:null}):null,a=!t.ssr||!!t.loading,i=a?u.Suspense:u.Fragment,c=t.ssr?(0,r.jsxs)(r.Fragment,{children:[null,(0,r.jsx)(n,{...e})]}):(0,r.jsx)(l.BailoutToCSR,{reason:"next/dynamic",children:(0,r.jsx)(n,{...e})});return(0,r.jsx)(i,{...a?{fallback:o}:{},children:c})}return i.displayName="LoadableComponent",i}},75707:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return u}});let r=n(73623)._(n(68635));function u(e,t){let n={};"function"==typeof e&&(n.loader=e);let u={...n,...t};return(0,r.default)({...u,modules:u.loadableGenerated?.modules})}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}},e=>{e.O(0,[8441,3794,7358],()=>e(e.s=15726)),_N_E=e.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[7358],{2852:(e,s,n)=>{Promise.resolve().then(n.t.bind(n,27123,23)),Promise.resolve().then(n.t.bind(n,61304,23)),Promise.resolve().then(n.t.bind(n,78616,23)),Promise.resolve().then(n.t.bind(n,64777,23)),Promise.resolve().then(n.t.bind(n,57121,23)),Promise.resolve().then(n.t.bind(n,74581,23)),Promise.resolve().then(n.t.bind(n,90484,23)),Promise.resolve().then(n.bind(n,86869))},19393:()=>{}},e=>{var s=s=>e(e.s=s);e.O(0,[8441,3794],()=>(s(83861),s(2852))),_N_E=e.O()}]);
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[9337],{52560:(e,s,_)=>{Promise.resolve().then(_.t.bind(_,27123,23))}},e=>{e.O(0,[8441,3794,7358],()=>e(e.s=52560)),_N_E=e.O()}]);