gsd-pi 2.76.0-dev.4c866b677 → 2.76.0-dev.7218806ab

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 (187) hide show
  1. package/dist/claude-cli-check.js +32 -3
  2. package/dist/mcp-server.d.ts +7 -0
  3. package/dist/mcp-server.js +35 -1
  4. package/dist/resources/extensions/claude-code-cli/readiness.js +4 -3
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +77 -17
  6. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
  7. package/dist/resources/extensions/gsd/auto-start.js +11 -15
  8. package/dist/resources/extensions/gsd/auto.js +13 -17
  9. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +17 -1
  10. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  11. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  12. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +40 -4
  14. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +12 -1
  15. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +968 -23
  16. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  17. package/dist/resources/extensions/gsd/error-classifier.js +10 -3
  18. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  19. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  20. package/dist/resources/extensions/gsd/gsd-db.js +3 -1
  21. package/dist/resources/extensions/gsd/guided-flow.js +189 -0
  22. package/dist/resources/extensions/gsd/health-widget.js +4 -1
  23. package/dist/resources/extensions/gsd/key-manager.js +6 -0
  24. package/dist/resources/extensions/gsd/model-router.js +36 -3
  25. package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -9
  26. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  27. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  28. package/dist/resources/extensions/gsd/preferences.js +17 -17
  29. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  30. package/dist/resources/extensions/gsd/prompts/discuss.md +29 -2
  31. package/dist/resources/extensions/gsd/token-counter.js +22 -5
  32. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  33. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  34. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  35. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  36. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  37. package/dist/web/standalone/.next/BUILD_ID +1 -1
  38. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  39. package/dist/web/standalone/.next/build-manifest.json +2 -2
  40. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  41. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.html +1 -1
  58. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  65. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  67. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  68. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  69. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  70. package/package.json +1 -1
  71. package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
  72. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
  73. package/packages/mcp-server/dist/remote-questions.js +732 -0
  74. package/packages/mcp-server/dist/remote-questions.js.map +1 -0
  75. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  76. package/packages/mcp-server/dist/server.js +18 -1
  77. package/packages/mcp-server/dist/server.js.map +1 -1
  78. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  79. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  80. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  81. package/packages/mcp-server/package.json +2 -1
  82. package/packages/mcp-server/src/remote-questions.test.ts +294 -0
  83. package/packages/mcp-server/src/remote-questions.ts +916 -0
  84. package/packages/mcp-server/src/server.ts +19 -1
  85. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  86. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  87. package/packages/mcp-server/tsconfig.test.json +19 -0
  88. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  89. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  90. package/packages/pi-ai/dist/providers/anthropic-shared.js +2 -0
  91. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  92. package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
  93. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  94. package/packages/pi-ai/dist/providers/simple-options.js +16 -1
  95. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  96. package/packages/pi-ai/src/providers/anthropic-shared.ts +3 -1
  97. package/packages/pi-ai/src/providers/simple-options.ts +17 -1
  98. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  99. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
  100. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
  102. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/model-registry.js +14 -0
  105. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  106. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  107. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  108. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  109. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  110. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  111. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  112. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  113. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  114. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  115. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  116. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  118. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
  121. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  122. package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
  123. package/packages/pi-coding-agent/src/core/model-registry.ts +16 -0
  124. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  125. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  126. package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
  127. package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
  128. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
  129. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  130. package/src/resources/extensions/claude-code-cli/readiness.ts +4 -3
  131. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
  132. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
  133. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
  134. package/src/resources/extensions/gsd/auto-post-unit.ts +0 -1
  135. package/src/resources/extensions/gsd/auto-start.ts +13 -16
  136. package/src/resources/extensions/gsd/auto.ts +12 -17
  137. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +23 -1
  138. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  139. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  140. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  141. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +42 -4
  142. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +13 -1
  143. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
  144. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  145. package/src/resources/extensions/gsd/error-classifier.ts +10 -3
  146. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  147. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  148. package/src/resources/extensions/gsd/gsd-db.ts +3 -1
  149. package/src/resources/extensions/gsd/guided-flow.ts +221 -0
  150. package/src/resources/extensions/gsd/health-widget.ts +3 -1
  151. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  152. package/src/resources/extensions/gsd/model-router.ts +42 -1
  153. package/src/resources/extensions/gsd/pre-execution-checks.ts +36 -10
  154. package/src/resources/extensions/gsd/preferences-types.ts +38 -0
  155. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  156. package/src/resources/extensions/gsd/preferences.ts +17 -17
  157. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  158. package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
  159. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  160. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
  161. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  162. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  163. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +64 -0
  164. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  165. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  166. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  167. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +234 -0
  168. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  169. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
  170. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
  171. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
  172. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
  173. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  174. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
  175. package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
  176. package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
  177. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
  178. package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
  179. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  180. package/src/resources/extensions/gsd/token-counter.ts +22 -5
  181. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  182. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  183. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  184. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  185. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  186. /package/dist/web/standalone/.next/static/{jDqWYbuP_CG6Kjc-uKwkN → 5qAwYhcU5Fs2VOq_R8lOc}/_buildManifest.js +0 -0
  187. /package/dist/web/standalone/.next/static/{jDqWYbuP_CG6Kjc-uKwkN → 5qAwYhcU5Fs2VOq_R8lOc}/_ssgManifest.js +0 -0
@@ -68,13 +68,13 @@ export { resolveAllSkillReferences } from "./preferences-skills.js";
68
68
  // These lived in preferences-skills.ts but imported loadEffectiveGSDPreferences
69
69
  // back from this file, creating a circular dependency. Moved here since they
70
70
  // are trivial wrappers over loadEffectiveGSDPreferences.
71
- export function resolveSkillDiscoveryMode(): SkillDiscoveryMode {
72
- const prefs = loadEffectiveGSDPreferences();
71
+ export function resolveSkillDiscoveryMode(basePath?: string): SkillDiscoveryMode {
72
+ const prefs = loadEffectiveGSDPreferences(basePath);
73
73
  return prefs?.preferences.skill_discovery ?? "suggest";
74
74
  }
75
75
 
76
- export function resolveSkillStalenessDays(): number {
77
- const prefs = loadEffectiveGSDPreferences();
76
+ export function resolveSkillStalenessDays(basePath?: string): number {
77
+ const prefs = loadEffectiveGSDPreferences(basePath);
78
78
  return prefs?.preferences.skill_staleness_days ?? 60;
79
79
  }
80
80
 
@@ -109,16 +109,16 @@ function legacyGlobalPreferencesPath(): string {
109
109
  return join(homedir(), ".pi", "agent", "gsd-preferences.md");
110
110
  }
111
111
 
112
- function projectPreferencesPath(): string {
113
- return join(gsdRoot(process.cwd()), "PREFERENCES.md");
112
+ function projectPreferencesPath(basePath: string = process.cwd()): string {
113
+ return join(gsdRoot(basePath), "PREFERENCES.md");
114
114
  }
115
115
  // Legacy lowercase files can still exist in older projects. Keep them as a
116
116
  // compatibility-only fallback, but route new reads/writes through PREFERENCES.md.
117
117
  function legacyGlobalPreferencesPathLowercase(): string {
118
118
  return join(gsdHome(), "preferences.md");
119
119
  }
120
- function legacyProjectPreferencesPathLowercase(): string {
121
- return join(gsdRoot(process.cwd()), "preferences.md");
120
+ function legacyProjectPreferencesPathLowercase(basePath: string = process.cwd()): string {
121
+ return join(gsdRoot(basePath), "preferences.md");
122
122
  }
123
123
 
124
124
  export function getGlobalGSDPreferencesPath(): string {
@@ -129,8 +129,8 @@ export function getLegacyGlobalGSDPreferencesPath(): string {
129
129
  return legacyGlobalPreferencesPath();
130
130
  }
131
131
 
132
- export function getProjectGSDPreferencesPath(): string {
133
- return projectPreferencesPath();
132
+ export function getProjectGSDPreferencesPath(basePath?: string): string {
133
+ return projectPreferencesPath(basePath);
134
134
  }
135
135
 
136
136
  // ─── Loading ────────────────────────────────────────────────────────────────
@@ -141,14 +141,14 @@ export function loadGlobalGSDPreferences(): LoadedGSDPreferences | null {
141
141
  ?? loadPreferencesFile(legacyGlobalPreferencesPath(), "global");
142
142
  }
143
143
 
144
- export function loadProjectGSDPreferences(): LoadedGSDPreferences | null {
145
- return loadPreferencesFile(projectPreferencesPath(), "project")
146
- ?? loadPreferencesFile(legacyProjectPreferencesPathLowercase(), "project");
144
+ export function loadProjectGSDPreferences(basePath?: string): LoadedGSDPreferences | null {
145
+ return loadPreferencesFile(projectPreferencesPath(basePath), "project")
146
+ ?? loadPreferencesFile(legacyProjectPreferencesPathLowercase(basePath), "project");
147
147
  }
148
148
 
149
- export function loadEffectiveGSDPreferences(): LoadedGSDPreferences | null {
149
+ export function loadEffectiveGSDPreferences(basePath?: string): LoadedGSDPreferences | null {
150
150
  const globalPreferences = loadGlobalGSDPreferences();
151
- const projectPreferences = loadProjectGSDPreferences();
151
+ const projectPreferences = loadProjectGSDPreferences(basePath);
152
152
 
153
153
  if (!globalPreferences && !projectPreferences) return null;
154
154
 
@@ -603,8 +603,8 @@ export function resolvePreDispatchHooks(): PreDispatchHookConfig[] {
603
603
  * Worktree isolation requires explicit opt-in because it depends on git
604
604
  * branch infrastructure that must be set up before use.
605
605
  */
606
- export function getIsolationMode(): "none" | "worktree" | "branch" {
607
- const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
606
+ export function getIsolationMode(basePath?: string): "none" | "worktree" | "branch" {
607
+ const prefs = loadEffectiveGSDPreferences(basePath)?.preferences?.git;
608
608
  if (prefs?.isolation === "worktree") return "worktree";
609
609
  if (prefs?.isolation === "branch") return "branch";
610
610
  return "none"; // default — no isolation, work on current branch
@@ -162,6 +162,10 @@ Preserve the specification's exact terminology, emphasis, and specific framing.
162
162
  6. For each architectural or pattern decision, call `gsd_decision_save` — the tool auto-assigns IDs and regenerates `.gsd/DECISIONS.md` automatically.
163
163
  7. {{commitInstruction}}
164
164
 
165
+ ### Ready-phrase pre-condition (NON-BYPASSABLE)
166
+
167
+ Before emitting the ready phrase, verify in the CURRENT turn that you have written `.gsd/PROJECT.md`, `.gsd/REQUIREMENTS.md`, `{{contextPath}}`, and called `gsd_plan_milestone`. If any is missing, **STOP** — emit the missing tool calls in this same turn. The system rejects premature ready signals and retries are capped.
168
+
165
169
  After writing the files, say exactly: "Milestone {{milestoneId}} ready." — nothing else. Auto-mode will start automatically.
166
170
 
167
171
  ### Multi-Milestone
@@ -234,6 +238,10 @@ For single-milestone projects, do NOT write this file.
234
238
 
235
239
  7. {{multiMilestoneCommitInstruction}}
236
240
 
241
+ ### Ready-phrase pre-condition (NON-BYPASSABLE)
242
+
243
+ Before emitting the ready phrase, verify in the CURRENT turn that you have written `.gsd/PROJECT.md`, `.gsd/REQUIREMENTS.md`, the primary `CONTEXT.md`, called `gsd_plan_milestone` for the primary milestone, and written `.gsd/DISCUSSION-MANIFEST.json` with `gates_completed === total`. If any is missing, **STOP** — emit the missing tool calls in this same turn. The system rejects premature ready signals and retries are capped.
244
+
237
245
  After writing the files, say exactly: "Milestone {{milestoneId}} ready." — nothing else. Auto-mode will start automatically.
238
246
 
239
247
  ## Critical Rules
@@ -339,7 +339,20 @@ These sections are in addition to whatever other context the discussion surfaced
339
339
  6. For each architectural or pattern decision made during discussion, call `gsd_decision_save` — the tool auto-assigns IDs and regenerates `.gsd/DECISIONS.md` automatically.
340
340
  7. {{commitInstruction}}
341
341
 
342
- After writing the files, say exactly: "Milestone {{milestoneId}} ready." — nothing else. Auto-mode will start automatically.
342
+ ### Ready-phrase pre-condition (NON-BYPASSABLE)
343
+
344
+ Before emitting the ready phrase, verify in the CURRENT turn that you have:
345
+
346
+ - [ ] Written `.gsd/PROJECT.md` (step 2)
347
+ - [ ] Written `.gsd/REQUIREMENTS.md` (step 3)
348
+ - [ ] Written `{{contextPath}}` (step 4)
349
+ - [ ] Called `gsd_plan_milestone` (step 5)
350
+
351
+ If ANY box is unchecked, **STOP**. Do NOT emit the ready phrase. Emit the missing tool calls in this same turn. The system detects missing artifacts and will reject premature ready signals — you will be asked again and retries are capped.
352
+
353
+ Do not announce the ready phrase as something you are "about to" do. Do not narrate "now writing the files" as a substitute for actually writing them. The ready phrase is a post-write signal, not an intent signal.
354
+
355
+ After completing steps 1–7 above, say exactly: "Milestone {{milestoneId}} ready." — nothing else. Auto-mode will start automatically.
343
356
 
344
357
  ### Multi-Milestone
345
358
 
@@ -418,6 +431,20 @@ For single-milestone projects, do NOT write this file — it is only for multi-m
418
431
 
419
432
  7. {{multiMilestoneCommitInstruction}}
420
433
 
421
- After writing the files, say exactly: "Milestone M001 ready." — nothing else. Auto-mode will start automatically.
434
+ ### Ready-phrase pre-condition (NON-BYPASSABLE)
435
+
436
+ Before emitting the ready phrase, verify in the CURRENT turn that you have:
437
+
438
+ - [ ] Written `.gsd/PROJECT.md` (Phase 1)
439
+ - [ ] Written `.gsd/REQUIREMENTS.md` (Phase 1)
440
+ - [ ] Written primary-milestone `CONTEXT.md` (Phase 2)
441
+ - [ ] Called `gsd_plan_milestone` for the primary milestone (Phase 2)
442
+ - [ ] Written `.gsd/DISCUSSION-MANIFEST.json` with `gates_completed === total` (Phase 3)
443
+
444
+ If ANY box is unchecked, **STOP**. Do NOT emit the ready phrase. Emit the missing tool calls in this same turn. The system detects missing artifacts and will reject premature ready signals — you will be asked again and retries are capped.
445
+
446
+ Do not announce the ready phrase as something you are "about to" do. Do not narrate "now writing the files" as a substitute for actually writing them. The ready phrase is a post-write signal, not an intent signal.
447
+
448
+ After completing all phases above, say exactly: "Milestone M001 ready." — nothing else. Auto-mode will start automatically.
422
449
 
423
450
  {{inlinedTemplates}}
@@ -0,0 +1,123 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import {
8
+ buildSnapshot,
9
+ readCompactionSnapshot,
10
+ writeCompactionSnapshot,
11
+ DEFAULT_SNAPSHOT_BYTES,
12
+ } from '../compaction-snapshot.ts';
13
+ import { closeDatabase, openDatabase } from '../gsd-db.ts';
14
+ import { createMemory } from '../memory-store.ts';
15
+ import { executeResume } from '../tools/resume-tool.ts';
16
+
17
+ function freshBase(): string {
18
+ return mkdtempSync(join(tmpdir(), 'gsd-snap-'));
19
+ }
20
+
21
+ function cleanup(dir: string): void {
22
+ rmSync(dir, { recursive: true, force: true });
23
+ }
24
+
25
+ test('buildSnapshot: renders memories, exec history, and active context', () => {
26
+ const snap = buildSnapshot({
27
+ generatedAt: new Date('2026-04-20T12:00:00.000Z'),
28
+ activeContext: 'M001 / S01 / T01 — wire gsd_exec',
29
+ memories: [
30
+ { id: 'MEM001', category: 'gotcha', content: 'FTS5 needs Porter tokenizer', confidence: 0.9,
31
+ source_unit_type: null, source_unit_id: null, created_at: '', updated_at: '',
32
+ superseded_by: null, hit_count: 0, scope: 'project', seq: 1, tags: [], structured_fields: null },
33
+ ],
34
+ execHistory: [
35
+ {
36
+ id: 'abc',
37
+ runtime: 'bash',
38
+ purpose: 'count TODOs',
39
+ started_at: '', finished_at: '', duration_ms: 10,
40
+ exit_code: 0, signal: null, timed_out: false,
41
+ stdout_bytes: 1, stderr_bytes: 0, stdout_truncated: false, stderr_truncated: false,
42
+ stdout_path: '/tmp/abc.stdout', stderr_path: '/tmp/abc.stderr', meta_path: '/tmp/abc.meta.json',
43
+ },
44
+ ],
45
+ });
46
+ assert.match(snap, /Active context/);
47
+ assert.match(snap, /M001 \/ S01 \/ T01/);
48
+ assert.match(snap, /FTS5 needs Porter tokenizer/);
49
+ assert.match(snap, /\[abc\] bash exit:0 — count TODOs/);
50
+ });
51
+
52
+ test('buildSnapshot: enforces the byte cap with a truncation marker', () => {
53
+ const longMemories = Array.from({ length: 50 }, (_v, i) => ({
54
+ id: `MEM${String(i).padStart(3, '0')}`,
55
+ category: 'gotcha',
56
+ content: 'x'.repeat(200),
57
+ confidence: 0.8,
58
+ source_unit_type: null,
59
+ source_unit_id: null,
60
+ created_at: '',
61
+ updated_at: '',
62
+ superseded_by: null,
63
+ hit_count: 0,
64
+ scope: 'project',
65
+ seq: i,
66
+ tags: [] as string[],
67
+ structured_fields: null,
68
+ }));
69
+ const snap = buildSnapshot(
70
+ { generatedAt: new Date(), memories: longMemories, execHistory: [] },
71
+ { maxBytes: 512, maxMemories: 50 },
72
+ );
73
+ assert.ok(Buffer.byteLength(snap, 'utf-8') <= 512, 'should respect cap');
74
+ assert.match(snap, /\[truncated\]/, 'should include truncation marker');
75
+ });
76
+
77
+ test('buildSnapshot: handles empty state with an explanatory placeholder', () => {
78
+ const snap = buildSnapshot({ generatedAt: new Date(), memories: [], execHistory: [] });
79
+ assert.match(snap, /_No durable memories/);
80
+ assert.ok(Buffer.byteLength(snap, 'utf-8') <= DEFAULT_SNAPSHOT_BYTES);
81
+ });
82
+
83
+ test('writeCompactionSnapshot + readCompactionSnapshot + executeResume: end-to-end', () => {
84
+ const base = freshBase();
85
+ try {
86
+ openDatabase(':memory:');
87
+ createMemory({ category: 'architecture', content: 'Single-writer DB through gsd-db.ts', confidence: 0.95 });
88
+ createMemory({ category: 'convention', content: 'Prefer typed helpers over raw SQL', confidence: 0.9 });
89
+
90
+ const out = writeCompactionSnapshot(base, { activeContext: 'M099 resume check' });
91
+ assert.ok(out.path.endsWith('last-snapshot.md'));
92
+ assert.ok(out.bytes > 0);
93
+ assert.equal(out.memories, 2);
94
+
95
+ const contents = readCompactionSnapshot(base);
96
+ assert.ok(contents);
97
+ assert.match(contents!, /Single-writer DB through gsd-db\.ts/);
98
+ assert.match(contents!, /M099 resume check/);
99
+
100
+ const tool = executeResume({}, { baseDir: base });
101
+ assert.ok(!tool.isError);
102
+ assert.equal(tool.details.found, true);
103
+ assert.match(tool.content[0].text, /Single-writer DB through gsd-db\.ts/);
104
+
105
+ // also verify the file content matches (without trailing newline)
106
+ const raw = readFileSync(out.path, 'utf-8');
107
+ assert.ok(raw.endsWith('\n'));
108
+ } finally {
109
+ closeDatabase();
110
+ cleanup(base);
111
+ }
112
+ });
113
+
114
+ test('executeResume: reports friendly empty state when no snapshot exists', () => {
115
+ const base = freshBase();
116
+ try {
117
+ const result = executeResume({}, { baseDir: base });
118
+ assert.equal(result.details.found, false);
119
+ assert.match(result.content[0].text, /No snapshot found/);
120
+ } finally {
121
+ cleanup(base);
122
+ }
123
+ });
@@ -768,6 +768,37 @@ test("runProviderChecks detects claude.cmd in PATH on Windows (#4503)", { skip:
768
768
  });
769
769
  });
770
770
 
771
+ test("runProviderChecks detects claude.exe in PATH on Windows (#4548)", { skip: process.platform !== "win32" }, () => {
772
+ const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-cc-exe-home-")));
773
+ const binDir = join(tmpHome, "bin");
774
+ mkdirSync(binDir, { recursive: true });
775
+
776
+ // Some Windows installs ship a direct claude.exe binary (not a .cmd shim).
777
+ const fakeClaudeExe = join(binDir, "claude.exe");
778
+ writeFileSync(fakeClaudeExe, "");
779
+
780
+ withEnv({
781
+ HOME: tmpHome,
782
+ ANTHROPIC_API_KEY: undefined,
783
+ ANTHROPIC_OAUTH_TOKEN: undefined,
784
+ COPILOT_GITHUB_TOKEN: undefined,
785
+ GH_TOKEN: undefined,
786
+ GITHUB_TOKEN: undefined,
787
+ PATH: `${binDir};${process.env.PATH ?? ""}`,
788
+ PATHEXT: ".COM;.EXE;.BAT;.CMD",
789
+ }, () => {
790
+ try {
791
+ const results = runProviderChecks();
792
+ const anthropic = results.find(r => r.name === "anthropic");
793
+ assert.ok(anthropic, "anthropic result should exist");
794
+ assert.equal(anthropic!.status, "ok", "should be ok when claude.exe is in PATH (#4548)");
795
+ assert.ok(anthropic!.message.toLowerCase().includes("claude"), "should mention claude-code as source");
796
+ } finally {
797
+ rmSync(tmpHome, { recursive: true, force: true });
798
+ }
799
+ });
800
+ });
801
+
771
802
  test("PROVIDER_ROUTES includes google-gemini-cli as route for google (#2922)", async () => {
772
803
  const { readFileSync: readFS } = await import("node:fs");
773
804
  const { dirname: dirn, join: joinPath } = await import("node:path");
@@ -0,0 +1,124 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { listExecHistory, searchExecHistory } from '../exec-history.ts';
8
+ import { executeExecSearch } from '../tools/exec-search-tool.ts';
9
+
10
+ function freshBase(): string {
11
+ return mkdtempSync(join(tmpdir(), 'gsd-exec-history-'));
12
+ }
13
+
14
+ function cleanup(dir: string): void {
15
+ rmSync(dir, { recursive: true, force: true });
16
+ }
17
+
18
+ function writeRun(base: string, id: string, overrides: Record<string, unknown> = {}): void {
19
+ const dir = join(base, '.gsd', 'exec');
20
+ mkdirSync(dir, { recursive: true });
21
+ const stdoutPath = join(dir, `${id}.stdout`);
22
+ const stderrPath = join(dir, `${id}.stderr`);
23
+ const metaPath = join(dir, `${id}.meta.json`);
24
+ writeFileSync(stdoutPath, (overrides.stdout as string | undefined) ?? `stdout for ${id}\n`);
25
+ writeFileSync(stderrPath, '');
26
+ writeFileSync(
27
+ metaPath,
28
+ JSON.stringify({
29
+ id,
30
+ runtime: 'bash',
31
+ purpose: `purpose for ${id}`,
32
+ started_at: '2026-04-20T12:00:00.000Z',
33
+ finished_at: '2026-04-20T12:00:00.100Z',
34
+ duration_ms: 100,
35
+ exit_code: 0,
36
+ signal: null,
37
+ timed_out: false,
38
+ stdout_bytes: 12,
39
+ stderr_bytes: 0,
40
+ stdout_truncated: false,
41
+ stderr_truncated: false,
42
+ stdout_path: stdoutPath,
43
+ stderr_path: stderrPath,
44
+ ...overrides,
45
+ }),
46
+ );
47
+ }
48
+
49
+ test('listExecHistory: returns empty list when .gsd/exec missing', () => {
50
+ const base = freshBase();
51
+ try {
52
+ assert.deepEqual(listExecHistory(base), []);
53
+ } finally {
54
+ cleanup(base);
55
+ }
56
+ });
57
+
58
+ test('listExecHistory: skips malformed meta files', () => {
59
+ const base = freshBase();
60
+ try {
61
+ const dir = join(base, '.gsd', 'exec');
62
+ mkdirSync(dir, { recursive: true });
63
+ writeFileSync(join(dir, 'bad.meta.json'), '{not-json');
64
+ writeRun(base, 'ok-1');
65
+ const list = listExecHistory(base);
66
+ assert.equal(list.length, 1);
67
+ assert.equal(list[0]!.id, 'ok-1');
68
+ } finally {
69
+ cleanup(base);
70
+ }
71
+ });
72
+
73
+ test('searchExecHistory: filters by query, runtime, and failing_only', () => {
74
+ const base = freshBase();
75
+ try {
76
+ writeRun(base, 'playwright-run', { purpose: 'playwright snapshot' });
77
+ writeRun(base, 'grep-run', { purpose: 'grep TODOs' });
78
+ writeRun(base, 'failing-run', { exit_code: 1, purpose: 'boom' });
79
+ writeRun(base, 'node-run', { runtime: 'node', purpose: 'dedupe' });
80
+
81
+ const playwrightHits = searchExecHistory(base, { query: 'playwright' });
82
+ assert.equal(playwrightHits.length, 1);
83
+ assert.equal(playwrightHits[0]!.entry.id, 'playwright-run');
84
+
85
+ const failingHits = searchExecHistory(base, { failing_only: true });
86
+ assert.equal(failingHits.length, 1);
87
+ assert.equal(failingHits[0]!.entry.id, 'failing-run');
88
+
89
+ const nodeHits = searchExecHistory(base, { runtime: 'node' });
90
+ assert.equal(nodeHits.length, 1);
91
+ assert.equal(nodeHits[0]!.entry.runtime, 'node');
92
+
93
+ const unlimited = searchExecHistory(base, {});
94
+ assert.equal(unlimited.length, 4);
95
+ } finally {
96
+ cleanup(base);
97
+ }
98
+ });
99
+
100
+ test('executeExecSearch: returns helpful empty-state message when no matches', () => {
101
+ const base = freshBase();
102
+ try {
103
+ const result = executeExecSearch({ query: 'missing' }, { baseDir: base });
104
+ assert.ok(!result.isError);
105
+ assert.match(result.content[0].text, /No prior gsd_exec runs/);
106
+ } finally {
107
+ cleanup(base);
108
+ }
109
+ });
110
+
111
+ test('executeExecSearch: includes stdout_path and preview in details', () => {
112
+ const base = freshBase();
113
+ try {
114
+ writeRun(base, 'summary-run', { stdout: 'found 42 TODOs\n' });
115
+ const result = executeExecSearch({ query: 'summary' }, { baseDir: base });
116
+ const details = result.details as { results: Array<{ id: string; stdout_path: string }> };
117
+ assert.equal(details.results.length, 1);
118
+ assert.equal(details.results[0]!.id, 'summary-run');
119
+ assert.match(details.results[0]!.stdout_path, /summary-run\.stdout$/);
120
+ assert.match(result.content[0].text, /found 42 TODOs/);
121
+ } finally {
122
+ cleanup(base);
123
+ }
124
+ });
@@ -0,0 +1,210 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { EXEC_DEFAULTS, runExecSandbox, type ExecSandboxOptions } from '../exec-sandbox.ts';
8
+ import { buildExecOptions, executeGsdExec } from '../tools/exec-tool.ts';
9
+ import { isContextModeEnabled } from '../preferences-types.ts';
10
+
11
+ function freshBase(): string {
12
+ return mkdtempSync(join(tmpdir(), 'gsd-exec-test-'));
13
+ }
14
+
15
+ function cleanup(dir: string): void {
16
+ rmSync(dir, { recursive: true, force: true });
17
+ }
18
+
19
+ function baseOpts(base: string, overrides: Partial<ExecSandboxOptions> = {}): ExecSandboxOptions {
20
+ return {
21
+ baseDir: base,
22
+ clamp_timeout_ms: EXEC_DEFAULTS.clampTimeoutMs,
23
+ default_timeout_ms: 10_000,
24
+ stdout_cap_bytes: 1_024,
25
+ stderr_cap_bytes: 1_024,
26
+ digest_chars: 120,
27
+ env_allowlist: EXEC_DEFAULTS.envAllowlist,
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ test('runExecSandbox: captures stdout, persists artifacts, returns digest', async () => {
33
+ const base = freshBase();
34
+ try {
35
+ const result = await runExecSandbox(
36
+ { runtime: 'bash', script: 'echo hello world' },
37
+ baseOpts(base),
38
+ );
39
+ assert.equal(result.exit_code, 0);
40
+ assert.equal(result.timed_out, false);
41
+ assert.ok(result.digest.includes('hello world'), `digest should contain stdout: ${result.digest}`);
42
+ assert.ok(result.stdout_path.startsWith(join(base, '.gsd', 'exec')), 'stdout path under .gsd/exec');
43
+ assert.equal(readFileSync(result.stdout_path, 'utf-8').trim(), 'hello world');
44
+ const meta = JSON.parse(readFileSync(result.meta_path, 'utf-8')) as Record<string, unknown>;
45
+ assert.equal(meta.runtime, 'bash');
46
+ assert.equal(meta.exit_code, 0);
47
+ } finally {
48
+ cleanup(base);
49
+ }
50
+ });
51
+
52
+ test('runExecSandbox: enforces stdout cap and marks truncation', async () => {
53
+ const base = freshBase();
54
+ try {
55
+ const result = await runExecSandbox(
56
+ // Emit far more than the cap so truncation triggers.
57
+ { runtime: 'bash', script: 'head -c 8000 /dev/urandom | base64' },
58
+ baseOpts(base, { stdout_cap_bytes: 256 }),
59
+ );
60
+ assert.equal(result.stdout_truncated, true, 'should mark stdout truncated');
61
+ assert.ok(result.stdout_bytes <= 256, `stdout_bytes within cap (got ${result.stdout_bytes})`);
62
+ const stdout = readFileSync(result.stdout_path, 'utf-8');
63
+ assert.ok(stdout.endsWith('[truncated: stdout cap reached]\n'), 'truncation marker appended');
64
+ } finally {
65
+ cleanup(base);
66
+ }
67
+ });
68
+
69
+ test('runExecSandbox: enforces timeout and surfaces timed_out', async () => {
70
+ const base = freshBase();
71
+ try {
72
+ const started = Date.now();
73
+ const result = await runExecSandbox(
74
+ { runtime: 'bash', script: 'sleep 10' },
75
+ baseOpts(base, { default_timeout_ms: 150, clamp_timeout_ms: 150 }),
76
+ );
77
+ const elapsed = Date.now() - started;
78
+ assert.equal(result.timed_out, true);
79
+ assert.ok(elapsed < 5_000, `should return well before 10s (took ${elapsed}ms)`);
80
+ } finally {
81
+ cleanup(base);
82
+ }
83
+ });
84
+
85
+ test('runExecSandbox: forwards only allowlisted env vars', async () => {
86
+ const base = freshBase();
87
+ try {
88
+ const result = await runExecSandbox(
89
+ { runtime: 'bash', script: 'echo PATH=$PATH SECRET=$GSD_TEST_SECRET' },
90
+ baseOpts(base, {
91
+ env_allowlist: [],
92
+ env: { PATH: '/usr/bin:/bin', HOME: '/tmp', GSD_TEST_SECRET: 'should-be-blocked' },
93
+ }),
94
+ );
95
+ const stdout = readFileSync(result.stdout_path, 'utf-8');
96
+ assert.ok(stdout.includes('PATH=/usr/bin:/bin'), 'PATH forwarded');
97
+ assert.ok(!stdout.includes('should-be-blocked'), 'non-allowlisted var blocked');
98
+ } finally {
99
+ cleanup(base);
100
+ }
101
+ });
102
+
103
+ test('runExecSandbox: node runtime executes JS', async () => {
104
+ const base = freshBase();
105
+ try {
106
+ const result = await runExecSandbox(
107
+ { runtime: 'node', script: 'console.log("node-ok:" + (1+2))' },
108
+ baseOpts(base),
109
+ );
110
+ assert.equal(result.exit_code, 0);
111
+ assert.ok(result.digest.includes('node-ok:3'));
112
+ } finally {
113
+ cleanup(base);
114
+ }
115
+ });
116
+
117
+ // ── exec-tool executor ────────────────────────────────────────────────────
118
+
119
+ test('executeGsdExec: runs by default when context_mode is unset', async () => {
120
+ const base = freshBase();
121
+ try {
122
+ const result = await executeGsdExec(
123
+ { runtime: 'bash', script: 'echo default-on-run' },
124
+ { baseDir: base, preferences: {} },
125
+ );
126
+ assert.ok(!result.isError, 'should succeed with no preferences');
127
+ assert.equal(result.details.operation, 'gsd_exec');
128
+ assert.equal(result.details.exit_code, 0);
129
+ assert.ok(result.content[0].text.includes('default-on-run'));
130
+ } finally {
131
+ cleanup(base);
132
+ }
133
+ });
134
+
135
+ test('executeGsdExec: runs when preferences is null (fresh project)', async () => {
136
+ const base = freshBase();
137
+ try {
138
+ const result = await executeGsdExec(
139
+ { runtime: 'bash', script: 'echo null-prefs-run' },
140
+ { baseDir: base, preferences: null },
141
+ );
142
+ assert.ok(!result.isError, 'null preferences should not disable');
143
+ assert.ok(result.content[0].text.includes('null-prefs-run'));
144
+ } finally {
145
+ cleanup(base);
146
+ }
147
+ });
148
+
149
+ test('executeGsdExec: blocked only when context_mode.enabled=false', async () => {
150
+ const base = freshBase();
151
+ try {
152
+ const result = await executeGsdExec(
153
+ { runtime: 'bash', script: 'echo should-not-run' },
154
+ { baseDir: base, preferences: { context_mode: { enabled: false } } },
155
+ );
156
+ assert.equal(result.isError, true);
157
+ assert.equal((result.details as { error?: string }).error, 'context_mode_disabled');
158
+ } finally {
159
+ cleanup(base);
160
+ }
161
+ });
162
+
163
+ test('executeGsdExec: runs when enabled explicitly set to true', async () => {
164
+ const base = freshBase();
165
+ try {
166
+ const result = await executeGsdExec(
167
+ { runtime: 'bash', script: 'echo explicit-on' },
168
+ { baseDir: base, preferences: { context_mode: { enabled: true } } },
169
+ );
170
+ assert.ok(!result.isError);
171
+ assert.ok(result.content[0].text.includes('explicit-on'));
172
+ } finally {
173
+ cleanup(base);
174
+ }
175
+ });
176
+
177
+ test('executeGsdExec: rejects empty script', async () => {
178
+ const base = freshBase();
179
+ try {
180
+ const result = await executeGsdExec(
181
+ { runtime: 'bash', script: ' ' },
182
+ { baseDir: base, preferences: { context_mode: { enabled: true } } },
183
+ );
184
+ assert.equal(result.isError, true);
185
+ assert.equal((result.details as { error?: string }).error, 'invalid_params');
186
+ } finally {
187
+ cleanup(base);
188
+ }
189
+ });
190
+
191
+ test('isContextModeEnabled: defaults to true; only explicit false disables', () => {
192
+ assert.equal(isContextModeEnabled(undefined), true, 'undefined prefs → on');
193
+ assert.equal(isContextModeEnabled(null), true, 'null prefs → on');
194
+ assert.equal(isContextModeEnabled({}), true, 'empty prefs → on');
195
+ assert.equal(isContextModeEnabled({ context_mode: {} }), true, 'empty block → on');
196
+ assert.equal(isContextModeEnabled({ context_mode: { enabled: true } }), true);
197
+ assert.equal(isContextModeEnabled({ context_mode: { enabled: false } }), false);
198
+ });
199
+
200
+ test('buildExecOptions: clamps out-of-range values to safe defaults', () => {
201
+ const opts = buildExecOptions('/tmp/base', {
202
+ enabled: true,
203
+ exec_timeout_ms: 999_999_999,
204
+ exec_stdout_cap_bytes: 1,
205
+ exec_digest_chars: -20,
206
+ });
207
+ assert.equal(opts.default_timeout_ms, EXEC_DEFAULTS.clampTimeoutMs, 'timeout clamped to upper bound');
208
+ assert.equal(opts.stdout_cap_bytes, 4_096, 'stdout cap clamped to floor');
209
+ assert.equal(opts.digest_chars, 0, 'digest chars clamped to floor');
210
+ });