gsd-pi 2.66.0 → 2.66.1-dev.0df32ec

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 (199) hide show
  1. package/dist/claude-cli-check.d.ts +8 -0
  2. package/dist/claude-cli-check.js +36 -0
  3. package/dist/cli.js +40 -0
  4. package/dist/onboarding.js +19 -2
  5. package/dist/resources/extensions/claude-code-cli/readiness.js +63 -12
  6. package/dist/resources/extensions/gsd/auto/phases.js +15 -2
  7. package/dist/resources/extensions/gsd/auto-model-selection.js +12 -3
  8. package/dist/resources/extensions/gsd/auto-prompts.js +167 -19
  9. package/dist/resources/extensions/gsd/auto.js +13 -1
  10. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +32 -1
  11. package/dist/resources/extensions/gsd/bootstrap/provider-error-resume.js +5 -0
  12. package/dist/resources/extensions/gsd/context-store.js +134 -2
  13. package/dist/resources/extensions/gsd/preferences.js +6 -1
  14. package/dist/web/standalone/.next/BUILD_ID +1 -1
  15. package/dist/web/standalone/.next/app-path-routes-manifest.json +20 -20
  16. package/dist/web/standalone/.next/build-manifest.json +3 -3
  17. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  18. package/dist/web/standalone/.next/required-server-files.json +3 -3
  19. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  20. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  22. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  30. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  40. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  41. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  42. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  43. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  44. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  45. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  46. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  47. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  48. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  49. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  58. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  78. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  88. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  94. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  108. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  110. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  112. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  114. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  123. package/dist/web/standalone/.next/server/app/index.html +1 -1
  124. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  125. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  126. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  127. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  128. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  129. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  130. package/dist/web/standalone/.next/server/app/page.js +2 -2
  131. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  132. package/dist/web/standalone/.next/server/app-paths-manifest.json +20 -20
  133. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  134. package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
  135. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  136. package/dist/web/standalone/.next/server/middleware.js +2 -2
  137. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  138. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  139. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  140. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  141. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  142. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  143. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  144. package/dist/web/standalone/.next/static/chunks/app/page-0c485498795110d6.js +1 -0
  145. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  146. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  147. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  148. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  149. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  150. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  151. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  152. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  153. package/dist/web/standalone/server.js +1 -1
  154. package/package.json +1 -1
  155. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +3 -0
  156. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -0
  158. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +16 -0
  160. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  161. package/packages/pi-coding-agent/dist/core/retry-handler.js +58 -1
  162. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  163. package/packages/pi-coding-agent/dist/core/retry-handler.test.js +58 -0
  164. package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
  165. package/packages/pi-coding-agent/dist/core/sdk.d.ts +3 -0
  166. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  167. package/packages/pi-coding-agent/dist/core/sdk.js +1 -0
  168. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  169. package/packages/pi-coding-agent/package.json +1 -1
  170. package/packages/pi-coding-agent/src/core/agent-session.ts +4 -0
  171. package/packages/pi-coding-agent/src/core/retry-handler.test.ts +69 -0
  172. package/packages/pi-coding-agent/src/core/retry-handler.ts +66 -1
  173. package/packages/pi-coding-agent/src/core/sdk.ts +5 -0
  174. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  175. package/packages/pi-tui/dist/tui.js +1 -3
  176. package/packages/pi-tui/dist/tui.js.map +1 -1
  177. package/packages/pi-tui/src/tui.ts +1 -3
  178. package/pkg/package.json +1 -1
  179. package/src/resources/extensions/claude-code-cli/readiness.ts +67 -12
  180. package/src/resources/extensions/gsd/auto/phases.ts +20 -2
  181. package/src/resources/extensions/gsd/auto-model-selection.ts +12 -3
  182. package/src/resources/extensions/gsd/auto-prompts.ts +190 -19
  183. package/src/resources/extensions/gsd/auto.ts +12 -1
  184. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +34 -1
  185. package/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts +6 -0
  186. package/src/resources/extensions/gsd/context-store.ts +167 -2
  187. package/src/resources/extensions/gsd/preferences.ts +6 -1
  188. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +21 -7
  189. package/src/resources/extensions/gsd/tests/context-store.test.ts +176 -0
  190. package/src/resources/extensions/gsd/tests/decision-scope-cascade.test.ts +370 -0
  191. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +2 -2
  192. package/src/resources/extensions/gsd/tests/measurement.test.ts +531 -0
  193. package/src/resources/extensions/gsd/tests/preferences.test.ts +20 -0
  194. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +60 -0
  195. package/dist/web/standalone/.next/static/chunks/app/page-62be3b5fa91e4c8f.js +0 -1
  196. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  197. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  198. /package/dist/web/standalone/.next/static/{Bdk1mnQugYZh7ZxuXUYvc → Zw5aZFHFtOwjJSOsINh1m}/_buildManifest.js +0 -0
  199. /package/dist/web/standalone/.next/static/{Bdk1mnQugYZh7ZxuXUYvc → Zw5aZFHFtOwjJSOsINh1m}/_ssgManifest.js +0 -0
@@ -143,17 +143,17 @@ test("resolvePreferredModelConfig keeps explicit phase models as the ceiling", (
143
143
 
144
144
  // ─── resolveModelId tests ─────────────────────────────────────────────────
145
145
 
146
- test("resolveModelId: bare ID resolves to anthropic over claude-code when session is claude-code (#2905)", () => {
146
+ test("resolveModelId: bare ID resolves to claude-code when session is claude-code (#3772)", () => {
147
147
  const availableModels = [
148
148
  { id: "claude-sonnet-4-6", provider: "anthropic" },
149
149
  { id: "claude-sonnet-4-6", provider: "claude-code" },
150
150
  ];
151
151
 
152
- // Bug: when currentProvider is "claude-code", bare ID "claude-sonnet-4-6"
153
- // resolves to claude-code/claude-sonnet-4-6 instead of anthropic/claude-sonnet-4-6
152
+ // When currentProvider is "claude-code" (set by startup migration for subscription
153
+ // users), bare IDs must resolve to claude-code to avoid the third-party block (#3772).
154
154
  const result = resolveModelId("claude-sonnet-4-6", availableModels, "claude-code");
155
155
  assert.ok(result, "should resolve a model");
156
- assert.equal(result.provider, "anthropic", "bare ID must resolve to anthropic, not claude-code");
156
+ assert.equal(result.provider, "claude-code", "bare ID must resolve to claude-code when session provider is claude-code");
157
157
  });
158
158
 
159
159
  test("resolveModelId: bare ID still prefers current provider when it is a first-class API provider", () => {
@@ -227,14 +227,28 @@ test("model change notify in selectAndApplyModel is gated behind verbose flag",
227
227
  );
228
228
  });
229
229
 
230
- test("resolveModelId: anthropic wins over claude-code regardless of list order", () => {
230
+ test("resolveModelId: anthropic wins over claude-code when session provider is not claude-code", () => {
231
231
  const availableModels = [
232
232
  { id: "claude-sonnet-4-6", provider: "claude-code" },
233
233
  { id: "claude-sonnet-4-6", provider: "anthropic" },
234
234
  ];
235
235
 
236
- // Even when claude-code appears first in the list, anthropic should win
236
+ // When the session is NOT on claude-code, bare IDs should resolve to
237
+ // the canonical anthropic provider (original #2905 behavior preserved).
238
+ const result = resolveModelId("claude-sonnet-4-6", availableModels, undefined);
239
+ assert.ok(result, "should resolve a model");
240
+ assert.equal(result.provider, "anthropic", "anthropic must win when session is not claude-code");
241
+ });
242
+
243
+ test("resolveModelId: claude-code wins when session is claude-code regardless of list order", () => {
244
+ const availableModels = [
245
+ { id: "claude-sonnet-4-6", provider: "claude-code" },
246
+ { id: "claude-sonnet-4-6", provider: "anthropic" },
247
+ ];
248
+
249
+ // When session provider is claude-code (subscription user migration), it must
250
+ // win regardless of candidate ordering to avoid the third-party block (#3772).
237
251
  const result = resolveModelId("claude-sonnet-4-6", availableModels, "claude-code");
238
252
  assert.ok(result, "should resolve a model");
239
- assert.equal(result.provider, "anthropic", "anthropic must win over claude-code regardless of list order");
253
+ assert.equal(result.provider, "claude-code", "claude-code must win when it is the session provider");
240
254
  });
@@ -15,6 +15,8 @@ import {
15
15
  formatRequirementsForPrompt,
16
16
  queryArtifact,
17
17
  queryProject,
18
+ formatRoadmapExcerpt,
19
+ queryKnowledge,
18
20
  } from '../context-store.ts';
19
21
 
20
22
  // ═══════════════════════════════════════════════════════════════════════════
@@ -452,3 +454,177 @@ describe("context-store: queryProject", () => {
452
454
  assert.strictEqual(content, null, 'queryProject returns null when DB closed');
453
455
  });
454
456
  });
457
+
458
+ // ═══════════════════════════════════════════════════════════════════════════
459
+ // context-store: formatRoadmapExcerpt
460
+ // ═══════════════════════════════════════════════════════════════════════════
461
+
462
+ describe("context-store: formatRoadmapExcerpt", () => {
463
+ // Sample roadmap content matching actual M005-ROADMAP.md format
464
+ const sampleRoadmap = `# M005: Tiered Context Injection
465
+
466
+ ## Vision
467
+ Refactor prompt builders to inject relevance-scoped context.
468
+
469
+ ## Slice Overview
470
+ | ID | Slice | Risk | Depends | Done | After this |
471
+ |----|-------|------|---------|------|------------|
472
+ | S01 | Scope existing queries | low | — | ✅ | planSlice prompt scoped. |
473
+ | S02 | KNOWLEDGE scoping | medium | S01 | ⬜ | KNOWLEDGE sections filtered. |
474
+ | S03 | Measurement test | low | S02 | ⬜ | 40% reduction confirmed. |
475
+ `;
476
+
477
+ test("S02 with S01 predecessor includes both rows", () => {
478
+ const result = formatRoadmapExcerpt(sampleRoadmap, 'S02', '.gsd/milestones/M005/M005-ROADMAP.md');
479
+
480
+ // Should have header
481
+ assert.match(result, /\| ID \| Slice \| Risk \| Depends \| Done \| After this \|/, 'has header row');
482
+ // Should have separator
483
+ assert.match(result, /\|----\|/, 'has separator row');
484
+ // Should have S01 predecessor
485
+ assert.match(result, /\| S01 \|/, 'has predecessor S01 row');
486
+ // Should have S02 target
487
+ assert.match(result, /\| S02 \|/, 'has target S02 row');
488
+ // Should have reference directive
489
+ assert.match(result, /See full roadmap:.*M005-ROADMAP\.md/, 'has reference directive');
490
+ // Should NOT have S03 (not relevant)
491
+ assert.ok(!result.includes('| S03 |'), 'does not include unrelated S03');
492
+ });
493
+
494
+ test("S01 with no predecessor includes only target row", () => {
495
+ const result = formatRoadmapExcerpt(sampleRoadmap, 'S01');
496
+
497
+ // Should have header + separator + S01 only
498
+ assert.match(result, /\| ID \| Slice \|/, 'has header row');
499
+ assert.match(result, /\| S01 \|/, 'has target S01 row');
500
+ // Should NOT have S02 or S03
501
+ assert.ok(!result.includes('| S02 |'), 'does not include S02');
502
+ assert.ok(!result.includes('| S03 |'), 'does not include S03');
503
+ // Should have reference
504
+ assert.match(result, /See full roadmap:/, 'has reference directive');
505
+
506
+ // Count rows: header + separator + S01 + blank + directive = 5 lines
507
+ const lines = result.split('\n');
508
+ assert.strictEqual(lines.length, 5, 'correct number of lines (no predecessor)');
509
+ });
510
+
511
+ test("missing slice returns empty string", () => {
512
+ const result = formatRoadmapExcerpt(sampleRoadmap, 'S99');
513
+
514
+ assert.strictEqual(result, '', 'missing slice returns empty string');
515
+ });
516
+
517
+ test("empty input returns empty string", () => {
518
+ assert.strictEqual(formatRoadmapExcerpt('', 'S01'), '', 'empty content returns empty');
519
+ assert.strictEqual(formatRoadmapExcerpt(sampleRoadmap, ''), '', 'empty sliceId returns empty');
520
+ });
521
+
522
+ test("handles table with various column formats", () => {
523
+ // Table with different spacing and content
524
+ const variantRoadmap = `# Milestone
525
+
526
+ | ID | Slice | Risk | Depends | Done | After this |
527
+ |:---|:------|:-----|:--------|:-----|:-----------|
528
+ | S01 | First slice title | low | — | ✅ | First complete. |
529
+ | S02 | Second longer slice title here | medium | S01 | ⬜ | Second working. |
530
+ `;
531
+
532
+ const result = formatRoadmapExcerpt(variantRoadmap, 'S02');
533
+
534
+ assert.match(result, /\| S01 \|/, 'has predecessor with different spacing');
535
+ assert.match(result, /\| S02 \|/, 'has target with different spacing');
536
+ assert.match(result, /Second longer slice title/, 'preserves full slice title');
537
+ });
538
+
539
+ test("handles multiple dependencies by using first one", () => {
540
+ const multiDepRoadmap = `| ID | Slice | Risk | Depends | Done | After this |
541
+ |----|-------|------|---------|------|------------|
542
+ | S01 | First | low | — | ✅ | Done. |
543
+ | S02 | Second | low | — | ✅ | Done. |
544
+ | S03 | Third | medium | S01, S02 | ⬜ | Working. |
545
+ `;
546
+
547
+ const result = formatRoadmapExcerpt(multiDepRoadmap, 'S03');
548
+
549
+ // Should include S01 (first dependency) and S03
550
+ assert.match(result, /\| S01 \|/, 'has first dependency S01');
551
+ assert.match(result, /\| S03 \|/, 'has target S03');
552
+ // S02 is also a dependency but we only include the first one
553
+ // (This is intentional to keep excerpts minimal)
554
+ });
555
+ });
556
+
557
+ // ═══════════════════════════════════════════════════════════════════════════
558
+ // context-store: queryKnowledge
559
+ // ═══════════════════════════════════════════════════════════════════════════
560
+
561
+ describe("context-store: queryKnowledge", () => {
562
+ // Sample KNOWLEDGE.md content
563
+ const sampleKnowledge = `# Project Knowledge
564
+
565
+ ## Database Patterns
566
+ SQLite is used with WAL mode for concurrent reads.
567
+ Always use prepared statements.
568
+
569
+ More database details here.
570
+
571
+ ## API Design
572
+ REST endpoints follow OpenAPI spec.
573
+ Use versioned paths like /v1/resource.
574
+
575
+ ## Testing Guidelines
576
+ Unit tests use node:test.
577
+ Integration tests mock external services.
578
+ `;
579
+
580
+ test("single keyword matches header", async () => {
581
+ const result = await queryKnowledge(sampleKnowledge, ['database']);
582
+
583
+ assert.match(result, /## Database Patterns/, 'includes matching section header');
584
+ assert.match(result, /SQLite is used with WAL mode/, 'includes section content');
585
+ // Should NOT include other sections
586
+ assert.ok(!result.includes('## API Design'), 'does not include non-matching API section');
587
+ assert.ok(!result.includes('## Testing Guidelines'), 'does not include non-matching Testing section');
588
+ });
589
+
590
+ test("multiple keywords match multiple sections", async () => {
591
+ const result = await queryKnowledge(sampleKnowledge, ['database', 'testing']);
592
+
593
+ assert.match(result, /## Database Patterns/, 'includes Database section');
594
+ assert.match(result, /## Testing Guidelines/, 'includes Testing section');
595
+ assert.ok(!result.includes('## API Design'), 'does not include API section');
596
+ });
597
+
598
+ test("no matches returns empty string", async () => {
599
+ const result = await queryKnowledge(sampleKnowledge, ['nonexistent']);
600
+
601
+ assert.strictEqual(result, '', 'no matches returns empty string per D020');
602
+ });
603
+
604
+ test("keyword in first paragraph matches", async () => {
605
+ const result = await queryKnowledge(sampleKnowledge, ['sqlite']);
606
+
607
+ // 'sqlite' appears in first paragraph of Database Patterns
608
+ assert.match(result, /## Database Patterns/, 'matches keyword in first paragraph');
609
+ assert.match(result, /SQLite is used/, 'includes the section with matching paragraph');
610
+ });
611
+
612
+ test("case-insensitive matching", async () => {
613
+ const result = await queryKnowledge(sampleKnowledge, ['DATABASE', 'API']);
614
+
615
+ assert.match(result, /## Database Patterns/, 'case-insensitive header match');
616
+ assert.match(result, /## API Design/, 'case-insensitive header match for API');
617
+ });
618
+
619
+ test("empty keywords returns empty string", async () => {
620
+ const result = await queryKnowledge(sampleKnowledge, []);
621
+
622
+ assert.strictEqual(result, '', 'empty keywords returns empty string');
623
+ });
624
+
625
+ test("empty content returns empty string", async () => {
626
+ const result = await queryKnowledge('', ['database']);
627
+
628
+ assert.strictEqual(result, '', 'empty content returns empty string');
629
+ });
630
+ });
@@ -0,0 +1,370 @@
1
+ // decision-scope-cascade: Tests for R005 fallback cascade and scope derivation
2
+ //
3
+ // Validates:
4
+ // (a) inlineDecisionsFromDb cascade: milestone + scope → milestone only → null
5
+ // (b) deriveSliceScope extracts meaningful scope keywords from slice titles
6
+ // (c) deriveSliceScope returns undefined for generic titles
7
+
8
+ import { describe, test, afterEach, beforeEach } from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import {
11
+ openDatabase,
12
+ closeDatabase,
13
+ isDbAvailable,
14
+ insertDecision,
15
+ } from '../gsd-db.ts';
16
+ import {
17
+ queryDecisions,
18
+ formatDecisionsForPrompt,
19
+ } from '../context-store.ts';
20
+ import { deriveSliceScope } from '../auto-prompts.ts';
21
+
22
+ // ═══════════════════════════════════════════════════════════════════════════
23
+ // deriveSliceScope: Extract meaningful scope from slice titles
24
+ // ═══════════════════════════════════════════════════════════════════════════
25
+
26
+ describe("deriveSliceScope: keyword extraction", () => {
27
+ test("extracts first meaningful noun from title", () => {
28
+ // "Auth Middleware & Protected Route" → "auth"
29
+ assert.strictEqual(
30
+ deriveSliceScope("Auth Middleware & Protected Route"),
31
+ "auth",
32
+ "extracts 'auth' from auth-related title",
33
+ );
34
+
35
+ // "Database & User Model Setup" → "database" (not "setup" which is generic)
36
+ const dbScope = deriveSliceScope("Database & User Model Setup");
37
+ assert.ok(
38
+ dbScope === "database" || dbScope === "user",
39
+ `expected 'database' or 'user', got '${dbScope}'`,
40
+ );
41
+
42
+ // "API Rate Limiting" → "api"
43
+ assert.strictEqual(
44
+ deriveSliceScope("API Rate Limiting"),
45
+ "api",
46
+ "extracts 'api' from API-related title",
47
+ );
48
+
49
+ // "Stripe Payment Integration" → "stripe"
50
+ assert.strictEqual(
51
+ deriveSliceScope("Stripe Payment Integration"),
52
+ "stripe",
53
+ "extracts 'stripe' from payment-related title",
54
+ );
55
+ });
56
+
57
+ test("returns undefined for generic titles", () => {
58
+ // "Integration Testing" → undefined (both words are generic)
59
+ assert.strictEqual(
60
+ deriveSliceScope("Integration Testing"),
61
+ undefined,
62
+ "returns undefined for generic 'Integration Testing'",
63
+ );
64
+
65
+ // "Setup & Configuration" → undefined (all generic)
66
+ assert.strictEqual(
67
+ deriveSliceScope("Setup & Configuration"),
68
+ undefined,
69
+ "returns undefined for generic 'Setup & Configuration'",
70
+ );
71
+
72
+ // "Final Review" → undefined
73
+ assert.strictEqual(
74
+ deriveSliceScope("Final Review"),
75
+ undefined,
76
+ "returns undefined for generic 'Final Review'",
77
+ );
78
+
79
+ // "Basic Implementation" → undefined
80
+ assert.strictEqual(
81
+ deriveSliceScope("Basic Implementation"),
82
+ undefined,
83
+ "returns undefined for generic 'Basic Implementation'",
84
+ );
85
+ });
86
+
87
+ test("handles description as additional context", () => {
88
+ // Generic title but specific description
89
+ const scope = deriveSliceScope(
90
+ "Initial Setup",
91
+ "Configure PostgreSQL database connection",
92
+ );
93
+ assert.ok(
94
+ scope === "postgresql" || scope === "database" || scope === "configure",
95
+ `expected meaningful scope from description, got '${scope}'`,
96
+ );
97
+ });
98
+
99
+ test("handles edge cases", () => {
100
+ // Empty title
101
+ assert.strictEqual(
102
+ deriveSliceScope(""),
103
+ undefined,
104
+ "returns undefined for empty title",
105
+ );
106
+
107
+ // Short words only
108
+ assert.strictEqual(
109
+ deriveSliceScope("A B C"),
110
+ undefined,
111
+ "returns undefined for very short words",
112
+ );
113
+
114
+ // Mixed case and punctuation
115
+ assert.strictEqual(
116
+ deriveSliceScope("OAuth2 + JWT Authentication"),
117
+ "oauth2",
118
+ "handles mixed case and punctuation",
119
+ );
120
+ });
121
+
122
+ test("filters unit IDs (S01, M001, T03)", () => {
123
+ // "S01: Infrastructure" → undefined (S01 is a unit ID, infrastructure is generic)
124
+ assert.strictEqual(
125
+ deriveSliceScope("S01: Infrastructure"),
126
+ undefined,
127
+ "skips S01 ID and returns undefined for generic 'Infrastructure'",
128
+ );
129
+
130
+ // "M001 Setup" → undefined (M001 is a unit ID, setup is generic)
131
+ assert.strictEqual(
132
+ deriveSliceScope("M001 Setup"),
133
+ undefined,
134
+ "skips M001 ID and returns undefined for generic 'Setup'",
135
+ );
136
+
137
+ // "T03: Database Migration" → "database" (skips T03, returns meaningful word)
138
+ assert.strictEqual(
139
+ deriveSliceScope("T03: Database Migration"),
140
+ "database",
141
+ "skips T03 ID and returns 'database'",
142
+ );
143
+
144
+ // "S02 Auth Flow" → "auth" (skips S02, returns meaningful word)
145
+ assert.strictEqual(
146
+ deriveSliceScope("S02 Auth Flow"),
147
+ "auth",
148
+ "skips S02 ID and returns 'auth'",
149
+ );
150
+ });
151
+
152
+ test("filters process/activity words", () => {
153
+ // "Integration Testing + Hardening" → undefined (all generic/process words)
154
+ assert.strictEqual(
155
+ deriveSliceScope("Integration Testing + Hardening"),
156
+ undefined,
157
+ "returns undefined for 'Integration Testing + Hardening'",
158
+ );
159
+
160
+ // "Validation & Verification" → undefined (both are process words)
161
+ assert.strictEqual(
162
+ deriveSliceScope("Validation & Verification"),
163
+ undefined,
164
+ "returns undefined for 'Validation & Verification'",
165
+ );
166
+
167
+ // "Performance Optimization" → "performance" (optimization is generic, performance is domain)
168
+ assert.strictEqual(
169
+ deriveSliceScope("Performance Optimization"),
170
+ "performance",
171
+ "extracts 'performance' before generic 'optimization'",
172
+ );
173
+
174
+ // "Security Enhancement" → "security" (enhancement is generic, security is domain)
175
+ assert.strictEqual(
176
+ deriveSliceScope("Security Enhancement"),
177
+ "security",
178
+ "extracts 'security' before generic 'enhancement'",
179
+ );
180
+
181
+ // "WebSocket Delivery Pipeline" → "websocket"
182
+ assert.strictEqual(
183
+ deriveSliceScope("WebSocket Delivery Pipeline"),
184
+ "websocket",
185
+ "extracts 'websocket' from delivery pipeline title",
186
+ );
187
+
188
+ // "Prisma Schema + Migration" → "prisma"
189
+ assert.strictEqual(
190
+ deriveSliceScope("Prisma Schema + Migration"),
191
+ "prisma",
192
+ "extracts 'prisma' from schema migration title",
193
+ );
194
+ });
195
+ });
196
+
197
+ // ═══════════════════════════════════════════════════════════════════════════
198
+ // inlineDecisionsFromDb cascade: R005 implementation
199
+ // ═══════════════════════════════════════════════════════════════════════════
200
+
201
+ describe("inlineDecisionsFromDb: cascade fallback (R005)", () => {
202
+ beforeEach(() => {
203
+ openDatabase(':memory:');
204
+ });
205
+
206
+ afterEach(() => {
207
+ closeDatabase();
208
+ });
209
+
210
+ test("cascade: scoped query returns scoped decisions when they exist", () => {
211
+ // Insert decisions with different scopes
212
+ insertDecision({
213
+ id: 'D001', when_context: 'M001/S01', scope: 'auth',
214
+ decision: 'use JWT', choice: 'JWT', rationale: 'standard',
215
+ revisable: 'yes', made_by: 'agent', superseded_by: null,
216
+ });
217
+ insertDecision({
218
+ id: 'D002', when_context: 'M001/S02', scope: 'database',
219
+ decision: 'use PostgreSQL', choice: 'PostgreSQL', rationale: 'relational',
220
+ revisable: 'yes', made_by: 'agent', superseded_by: null,
221
+ });
222
+ insertDecision({
223
+ id: 'D003', when_context: 'M001/S01', scope: 'architecture',
224
+ decision: 'use microservices', choice: 'microservices', rationale: 'scalable',
225
+ revisable: 'yes', made_by: 'agent', superseded_by: null,
226
+ });
227
+
228
+ // Query with scope 'auth' should return D001 only
229
+ const authDecisions = queryDecisions({ milestoneId: 'M001', scope: 'auth' });
230
+ assert.strictEqual(authDecisions.length, 1, 'scoped query returns 1 decision');
231
+ assert.strictEqual(authDecisions[0]?.id, 'D001', 'returns D001 for auth scope');
232
+
233
+ // Query with scope 'database' should return D002 only
234
+ const dbDecisions = queryDecisions({ milestoneId: 'M001', scope: 'database' });
235
+ assert.strictEqual(dbDecisions.length, 1, 'scoped query returns 1 decision');
236
+ assert.strictEqual(dbDecisions[0]?.id, 'D002', 'returns D002 for database scope');
237
+ });
238
+
239
+ test("cascade: milestone-only fallback when scoped query returns empty", () => {
240
+ // Insert decisions for M001 with generic scope (e.g. 'architecture')
241
+ insertDecision({
242
+ id: 'D001', when_context: 'M001/S01', scope: 'architecture',
243
+ decision: 'use microservices', choice: 'microservices', rationale: 'scalable',
244
+ revisable: 'yes', made_by: 'agent', superseded_by: null,
245
+ });
246
+ insertDecision({
247
+ id: 'D002', when_context: 'M001/S02', scope: 'performance',
248
+ decision: 'use caching', choice: 'Redis', rationale: 'fast',
249
+ revisable: 'yes', made_by: 'agent', superseded_by: null,
250
+ });
251
+
252
+ // Query with scope 'auth' (no decisions with this scope) should return empty
253
+ const authDecisions = queryDecisions({ milestoneId: 'M001', scope: 'auth' });
254
+ assert.strictEqual(authDecisions.length, 0, 'scoped query for auth returns empty');
255
+
256
+ // Simulate cascade: fallback to milestone-only query
257
+ const milestoneDecisions = queryDecisions({ milestoneId: 'M001' });
258
+ assert.strictEqual(milestoneDecisions.length, 2, 'milestone-only query returns 2 decisions');
259
+ const ids = milestoneDecisions.map(d => d.id).sort();
260
+ assert.deepStrictEqual(ids, ['D001', 'D002'], 'milestone fallback returns all M001 decisions');
261
+ });
262
+
263
+ test("cascade: returns null when both scoped and milestone queries are empty", () => {
264
+ // Insert decisions only for M002
265
+ insertDecision({
266
+ id: 'D001', when_context: 'M002/S01', scope: 'auth',
267
+ decision: 'use OAuth', choice: 'OAuth2', rationale: 'standard',
268
+ revisable: 'yes', made_by: 'agent', superseded_by: null,
269
+ });
270
+
271
+ // Query M001 with scope should return empty (no M001 decisions at all)
272
+ const scopedDecisions = queryDecisions({ milestoneId: 'M001', scope: 'auth' });
273
+ assert.strictEqual(scopedDecisions.length, 0, 'scoped query returns empty');
274
+
275
+ // Fallback to milestone-only should also return empty (no M001 decisions)
276
+ const milestoneDecisions = queryDecisions({ milestoneId: 'M001' });
277
+ assert.strictEqual(milestoneDecisions.length, 0, 'milestone-only query returns empty');
278
+
279
+ // This scenario would result in null from inlineDecisionsFromDb
280
+ // (we can't directly test inlineDecisionsFromDb here without mocking fs)
281
+ });
282
+
283
+ test("cascade: demonstrates the full cascade behavior", () => {
284
+ // This test demonstrates the cascade logic that inlineDecisionsFromDb implements:
285
+ // 1. First try { milestoneId: 'M001', scope: 'payment' } → empty
286
+ // 2. Then try { milestoneId: 'M001' } → gets D001, D002
287
+ // 3. Return the milestone-level decisions
288
+
289
+ // Setup: decisions exist at milestone level but not for 'payment' scope
290
+ insertDecision({
291
+ id: 'D001', when_context: 'M001/S01', scope: 'architecture',
292
+ decision: 'use REST', choice: 'REST API', rationale: 'standard',
293
+ revisable: 'yes', made_by: 'agent', superseded_by: null,
294
+ });
295
+ insertDecision({
296
+ id: 'D002', when_context: 'M001/S02', scope: 'security',
297
+ decision: 'use HTTPS', choice: 'TLS 1.3', rationale: 'secure',
298
+ revisable: 'yes', made_by: 'agent', superseded_by: null,
299
+ });
300
+
301
+ // Step 1: Query with scope 'payment' (no matches)
302
+ const paymentDecisions = queryDecisions({ milestoneId: 'M001', scope: 'payment' });
303
+ assert.strictEqual(paymentDecisions.length, 0, 'payment scope query returns empty');
304
+
305
+ // Step 2: Since scope was provided but returned empty, cascade to milestone-only
306
+ const milestoneDecisions = queryDecisions({ milestoneId: 'M001' });
307
+ assert.strictEqual(milestoneDecisions.length, 2, 'milestone fallback returns 2 decisions');
308
+
309
+ // Step 3: Format and verify content
310
+ const formatted = formatDecisionsForPrompt(milestoneDecisions);
311
+ assert.match(formatted, /D001/, 'formatted output includes D001');
312
+ assert.match(formatted, /D002/, 'formatted output includes D002');
313
+ assert.match(formatted, /architecture/, 'formatted output includes architecture scope');
314
+ assert.match(formatted, /security/, 'formatted output includes security scope');
315
+ });
316
+ });
317
+
318
+ // ═══════════════════════════════════════════════════════════════════════════
319
+ // Integration: scope derivation feeds into cascade
320
+ // ═══════════════════════════════════════════════════════════════════════════
321
+
322
+ describe("integration: scope derivation with cascade", () => {
323
+ beforeEach(() => {
324
+ openDatabase(':memory:');
325
+ });
326
+
327
+ afterEach(() => {
328
+ closeDatabase();
329
+ });
330
+
331
+ test("derived scope finds matching decisions when they exist", () => {
332
+ // Insert decisions with 'auth' scope
333
+ insertDecision({
334
+ id: 'D001', when_context: 'M001/S01', scope: 'auth',
335
+ decision: 'use JWT', choice: 'JWT tokens', rationale: 'stateless',
336
+ revisable: 'yes', made_by: 'agent', superseded_by: null,
337
+ });
338
+
339
+ // Derive scope from slice title
340
+ const derivedScope = deriveSliceScope("Auth Middleware & Protected Routes");
341
+ assert.strictEqual(derivedScope, 'auth', 'derives auth scope from title');
342
+
343
+ // Query with derived scope should find the decision
344
+ const decisions = queryDecisions({ milestoneId: 'M001', scope: derivedScope });
345
+ assert.strictEqual(decisions.length, 1, 'scoped query finds matching decision');
346
+ assert.strictEqual(decisions[0]?.id, 'D001', 'finds the auth decision');
347
+ });
348
+
349
+ test("generic title triggers milestone-level fallback", () => {
350
+ // Insert decisions with various scopes
351
+ insertDecision({
352
+ id: 'D001', when_context: 'M001/S01', scope: 'architecture',
353
+ decision: 'use monolith', choice: 'monolith', rationale: 'simple',
354
+ revisable: 'yes', made_by: 'agent', superseded_by: null,
355
+ });
356
+ insertDecision({
357
+ id: 'D002', when_context: 'M001/S02', scope: 'tooling',
358
+ decision: 'use TypeScript', choice: 'TypeScript', rationale: 'type safety',
359
+ revisable: 'yes', made_by: 'agent', superseded_by: null,
360
+ });
361
+
362
+ // Derive scope from generic slice title
363
+ const derivedScope = deriveSliceScope("Integration Testing");
364
+ assert.strictEqual(derivedScope, undefined, 'generic title returns undefined scope');
365
+
366
+ // Without a scope, query returns all milestone decisions
367
+ const decisions = queryDecisions({ milestoneId: 'M001', scope: derivedScope });
368
+ assert.strictEqual(decisions.length, 2, 'no scope filter returns all decisions');
369
+ });
370
+ });
@@ -590,9 +590,9 @@ test("unit-end event contains errorContext when unit is cancelled with structure
590
590
  resolveAgentEndCancelled({ message: "Hard timeout error: exceeded limit", category: "timeout", isTransient: true });
591
591
 
592
592
  const result = await unitPromise;
593
- // Cancelled units break the loop before emitting unit-end
593
+ // Transient timeout cancellations pause (recoverable) instead of hard-stopping
594
594
  assert.equal(result.action, "break");
595
- assert.equal((result as any).reason, "session-failed");
595
+ assert.equal((result as any).reason, "session-timeout");
596
596
 
597
597
  // Verify error classification used structured errorContext on the window entry
598
598
  const entry = loopState.recentUnits[loopState.recentUnits.length - 1];