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
@@ -261,7 +261,12 @@ export async function inlineGsdRootFile(
261
261
 
262
262
  /**
263
263
  * Inline decisions with optional milestone scoping from the DB.
264
- * Falls back to filesystem via inlineGsdRootFile when DB unavailable or empty.
264
+ * Falls back to filesystem via inlineGsdRootFile only when DB is unavailable.
265
+ *
266
+ * Cascade logic (R005):
267
+ * 1. Query with { milestoneId, scope } if scope provided
268
+ * 2. If empty AND scope was provided, retry with { milestoneId } only (drop scope)
269
+ * 3. If still empty, return null (intentional per D020)
265
270
  */
266
271
  export async function inlineDecisionsFromDb(
267
272
  base: string, milestoneId?: string, scope?: string, level?: InlineLevel,
@@ -271,7 +276,15 @@ export async function inlineDecisionsFromDb(
271
276
  const { isDbAvailable } = await import("./gsd-db.js");
272
277
  if (isDbAvailable()) {
273
278
  const { queryDecisions, formatDecisionsForPrompt } = await import("./context-store.js");
274
- const decisions = queryDecisions({ milestoneId, scope });
279
+
280
+ // First query: try with both milestoneId and scope (if scope provided)
281
+ let decisions = queryDecisions({ milestoneId, scope });
282
+
283
+ // Cascade: if empty AND scope was provided, retry without scope
284
+ if (decisions.length === 0 && scope) {
285
+ decisions = queryDecisions({ milestoneId });
286
+ }
287
+
275
288
  if (decisions.length > 0) {
276
289
  // Use compact format for non-full levels to save ~35% tokens
277
290
  const formatted = inlineLevel !== "full"
@@ -279,26 +292,29 @@ export async function inlineDecisionsFromDb(
279
292
  : formatDecisionsForPrompt(decisions);
280
293
  return `### Decisions\nSource: \`.gsd/DECISIONS.md\`\n\n${formatted}`;
281
294
  }
295
+ // DB available but cascade returned empty — intentional per D020, don't fall back to file
296
+ return null;
282
297
  }
283
298
  } catch (err) {
284
299
  logWarning("prompt", `inlineDecisionsFromDb failed: ${err instanceof Error ? err.message : String(err)}`);
285
300
  }
301
+ // DB unavailable — fall back to filesystem
286
302
  return inlineGsdRootFile(base, "decisions.md", "Decisions");
287
303
  }
288
304
 
289
305
  /**
290
- * Inline requirements with optional slice scoping from the DB.
306
+ * Inline requirements with optional milestone and slice scoping from the DB.
291
307
  * Falls back to filesystem via inlineGsdRootFile when DB unavailable or empty.
292
308
  */
293
309
  export async function inlineRequirementsFromDb(
294
- base: string, sliceId?: string, level?: InlineLevel,
310
+ base: string, milestoneId?: string, sliceId?: string, level?: InlineLevel,
295
311
  ): Promise<string | null> {
296
312
  const inlineLevel = level ?? resolveInlineLevel();
297
313
  try {
298
314
  const { isDbAvailable } = await import("./gsd-db.js");
299
315
  if (isDbAvailable()) {
300
316
  const { queryRequirements, formatRequirementsForPrompt } = await import("./context-store.js");
301
- const requirements = queryRequirements({ sliceId });
317
+ const requirements = queryRequirements({ milestoneId, sliceId });
302
318
  if (requirements.length > 0) {
303
319
  // Use compact format for non-full levels to save ~40% tokens
304
320
  const formatted = inlineLevel !== "full"
@@ -335,6 +351,131 @@ export async function inlineProjectFromDb(
335
351
  return inlineGsdRootFile(base, "project.md", "Project");
336
352
  }
337
353
 
354
+ // ─── Stopwords for keyword extraction ─────────────────────────────────────
355
+ const STOPWORDS = new Set(['of', 'the', 'and', 'a', 'for', '+', '-', 'to', 'in', 'on', 'with', 'is', 'as', 'by']);
356
+
357
+ // Generic words that don't provide meaningful scope differentiation
358
+ const GENERIC_WORDS = new Set([
359
+ 'setup', 'integration', 'implementation', 'testing', 'test', 'tests',
360
+ 'config', 'configuration', 'init', 'initial', 'basic', 'core',
361
+ 'main', 'primary', 'final', 'complete', 'finish', 'end',
362
+ 'start', 'begin', 'first', 'last', 'update', 'updates',
363
+ 'fix', 'fixes', 'add', 'adds', 'remove', 'removes',
364
+ 'create', 'creates', 'build', 'builds', 'deploy', 'deployment',
365
+ 'refactor', 'refactoring', 'cleanup', 'polish', 'review',
366
+ // Process/activity words that describe what you're doing, not what domain
367
+ 'hardening', 'validation', 'verification', 'optimization',
368
+ 'improvement', 'enhancement', 'infrastructure',
369
+ ]);
370
+
371
+ // Pattern to match slice/milestone/task IDs (e.g., S01, M001, T03)
372
+ const UNIT_ID_PATTERN = /^[smt]\d+$/i;
373
+
374
+ /**
375
+ * Derive a scope keyword from slice title and optional description.
376
+ * Returns the most specific noun (first non-generic keyword) for decision scoping.
377
+ *
378
+ * Examples:
379
+ * - "Auth Middleware & Protected Route" → "auth"
380
+ * - "Database & User Model Setup" → "database"
381
+ * - "Integration Testing" → undefined (too generic)
382
+ * - "API Rate Limiting" → "api"
383
+ *
384
+ * @param sliceTitle - The slice title
385
+ * @param sliceDescription - Optional roadmap description (demo text)
386
+ * @returns A single lowercase keyword or undefined if no meaningful scope
387
+ */
388
+ export function deriveSliceScope(sliceTitle: string, sliceDescription?: string): string | undefined {
389
+ // Combine title and description for keyword extraction
390
+ const combinedText = sliceDescription
391
+ ? `${sliceTitle} ${sliceDescription}`
392
+ : sliceTitle;
393
+
394
+ // Extract all words, lowercase, remove punctuation
395
+ const words = combinedText
396
+ .split(/[\s&+,;:|/\\()-]+/)
397
+ .map(w => w.toLowerCase().replace(/[^a-z0-9]/g, ''))
398
+ .filter(w => w.length >= 2);
399
+
400
+ // Find the first word that is:
401
+ // 1. Not a stopword
402
+ // 2. Not a generic word
403
+ // 3. Not a unit ID (S01, M001, T03)
404
+ // 4. At least 3 characters (meaningful scope)
405
+ for (const word of words) {
406
+ if (STOPWORDS.has(word)) continue;
407
+ if (GENERIC_WORDS.has(word)) continue;
408
+ if (UNIT_ID_PATTERN.test(word)) continue;
409
+ if (word.length < 3) continue;
410
+ return word;
411
+ }
412
+
413
+ return undefined;
414
+ }
415
+ /**
416
+ * Extract keywords from a slice title for scoped knowledge queries.
417
+ * Splits on whitespace, filters stopwords, lowercases.
418
+ * Example: 'KNOWLEDGE scoping + roadmap excerpt' → ['knowledge', 'scoping', 'roadmap', 'excerpt']
419
+ */
420
+ function extractKeywords(title: string): string[] {
421
+ return title
422
+ .split(/\s+/)
423
+ .map(w => w.toLowerCase().replace(/[^a-z0-9]/g, ''))
424
+ .filter(w => w.length > 0 && !STOPWORDS.has(w));
425
+ }
426
+
427
+ /**
428
+ * Inline scoped KNOWLEDGE.md content based on keywords from slice title.
429
+ * Reads KNOWLEDGE.md, filters to sections matching keywords, formats with header.
430
+ * Returns null if no KNOWLEDGE.md exists or no sections match.
431
+ */
432
+ export async function inlineKnowledgeScoped(
433
+ base: string,
434
+ keywords: string[],
435
+ ): Promise<string | null> {
436
+ const knowledgePath = resolveGsdRootFile(base, "KNOWLEDGE");
437
+ if (!existsSync(knowledgePath)) return null;
438
+
439
+ const content = await loadFile(knowledgePath);
440
+ if (!content) return null;
441
+
442
+ // Import queryKnowledge from context-store
443
+ const { queryKnowledge } = await import("./context-store.js");
444
+ const scoped = await queryKnowledge(content, keywords);
445
+
446
+ // Return null if no sections matched (empty string from queryKnowledge)
447
+ if (!scoped) return null;
448
+
449
+ return `### Project Knowledge (scoped)\nSource: \`${relGsdRootFile("KNOWLEDGE")}\`\n\n${scoped.trim()}`;
450
+ }
451
+
452
+ /**
453
+ * Inline a roadmap excerpt for a specific slice.
454
+ * Reads full roadmap, extracts minimal excerpt with header + predecessor + target row.
455
+ * Returns null if roadmap doesn't exist or slice not found.
456
+ */
457
+ export async function inlineRoadmapExcerpt(
458
+ base: string,
459
+ mid: string,
460
+ sid: string,
461
+ ): Promise<string | null> {
462
+ const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
463
+ if (!roadmapPath || !existsSync(roadmapPath)) return null;
464
+
465
+ const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
466
+ const content = await loadFile(roadmapPath);
467
+ if (!content) return null;
468
+
469
+ // Import formatRoadmapExcerpt from context-store
470
+ const { formatRoadmapExcerpt } = await import("./context-store.js");
471
+ const excerpt = formatRoadmapExcerpt(content, sid, roadmapRel);
472
+
473
+ // Return null if slice not found in roadmap
474
+ if (!excerpt) return null;
475
+
476
+ return `### Milestone Roadmap (excerpt)\nSource: \`${roadmapRel}\`\n\n${excerpt}`;
477
+ }
478
+
338
479
  // ─── Skill Activation & Discovery ─────────────────────────────────────────
339
480
 
340
481
  function normalizeSkillReference(ref: string): string {
@@ -880,7 +1021,7 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
880
1021
  inlined.push(await inlineFile(contextPath, contextRel, "Milestone Context"));
881
1022
  const projectInline = await inlineProjectFromDb(base);
882
1023
  if (projectInline) inlined.push(projectInline);
883
- const requirementsInline = await inlineRequirementsFromDb(base);
1024
+ const requirementsInline = await inlineRequirementsFromDb(base, mid);
884
1025
  if (requirementsInline) inlined.push(requirementsInline);
885
1026
  const decisionsInline = await inlineDecisionsFromDb(base, mid);
886
1027
  if (decisionsInline) inlined.push(decisionsInline);
@@ -930,7 +1071,7 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
930
1071
  if (inlineLevel !== "minimal") {
931
1072
  const projectInline = await inlineProjectFromDb(base);
932
1073
  if (projectInline) inlined.push(projectInline);
933
- const requirementsInline = await inlineRequirementsFromDb(base, undefined, inlineLevel);
1074
+ const requirementsInline = await inlineRequirementsFromDb(base, mid, undefined, inlineLevel);
934
1075
  if (requirementsInline) inlined.push(requirementsInline);
935
1076
  const decisionsInline = await inlineDecisionsFromDb(base, mid, undefined, inlineLevel);
936
1077
  if (decisionsInline) inlined.push(decisionsInline);
@@ -999,19 +1140,35 @@ export async function buildResearchSlicePrompt(
999
1140
  const sliceContextRel = relSliceFile(base, mid, sid, "CONTEXT");
1000
1141
 
1001
1142
  const inlined: string[] = [];
1002
- inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
1143
+
1144
+ // Use roadmap excerpt instead of full roadmap for context reduction
1145
+ const roadmapExcerptRS = await inlineRoadmapExcerpt(base, mid, sid);
1146
+ if (roadmapExcerptRS) {
1147
+ inlined.push(roadmapExcerptRS);
1148
+ } else {
1149
+ // Fall back to full roadmap if excerpt fails
1150
+ inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
1151
+ }
1152
+
1003
1153
  const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
1004
1154
  if (contextInline) inlined.push(contextInline);
1005
1155
  const sliceCtxInline = await inlineFileOptional(sliceContextPath, sliceContextRel, "Slice Context (from discussion)");
1006
1156
  if (sliceCtxInline) inlined.push(sliceCtxInline);
1007
1157
  const researchInline = await inlineFileOptional(milestoneResearchPath, milestoneResearchRel, "Milestone Research");
1008
1158
  if (researchInline) inlined.push(researchInline);
1009
- const decisionsInline = await inlineDecisionsFromDb(base, mid);
1159
+
1160
+ // Derive scope from slice title for decision filtering (R005)
1161
+ const derivedScope = deriveSliceScope(sTitle);
1162
+ const decisionsInline = await inlineDecisionsFromDb(base, mid, derivedScope);
1010
1163
  if (decisionsInline) inlined.push(decisionsInline);
1011
- const requirementsInline = await inlineRequirementsFromDb(base, sid);
1164
+ const requirementsInline = await inlineRequirementsFromDb(base, mid, sid);
1012
1165
  if (requirementsInline) inlined.push(requirementsInline);
1013
- const knowledgeInlineRS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1166
+
1167
+ // Use scoped knowledge based on slice title keywords
1168
+ const keywords = extractKeywords(sTitle);
1169
+ const knowledgeInlineRS = await inlineKnowledgeScoped(base, keywords);
1014
1170
  if (knowledgeInlineRS) inlined.push(knowledgeInlineRS);
1171
+
1015
1172
  inlined.push(inlineTemplate("research", "Research"));
1016
1173
 
1017
1174
  const depContent = await inlineDependencySummaries(mid, sid, base);
@@ -1060,19 +1217,33 @@ export async function buildPlanSlicePrompt(
1060
1217
  const researchSliceAnchor = readPhaseAnchor(base, mid, "research-slice");
1061
1218
  if (researchSliceAnchor) inlined.push(formatAnchorForPrompt(researchSliceAnchor));
1062
1219
 
1063
- inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
1220
+ // Use roadmap excerpt instead of full roadmap for context reduction
1221
+ const roadmapExcerptPS = await inlineRoadmapExcerpt(base, mid, sid);
1222
+ if (roadmapExcerptPS) {
1223
+ inlined.push(roadmapExcerptPS);
1224
+ } else {
1225
+ // Fall back to full roadmap if excerpt fails
1226
+ inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
1227
+ }
1228
+
1064
1229
  const sliceCtxInline = await inlineFileOptional(sliceContextPath, sliceContextRel, "Slice Context (from discussion)");
1065
1230
  if (sliceCtxInline) inlined.push(sliceCtxInline);
1066
1231
  const researchInline = await inlineFileOptional(researchPath, researchRel, "Slice Research");
1067
1232
  if (researchInline) inlined.push(researchInline);
1068
1233
  if (inlineLevel !== "minimal") {
1069
- const decisionsInline = await inlineDecisionsFromDb(base, mid, undefined, inlineLevel);
1234
+ // Derive scope from slice title for decision filtering (R005)
1235
+ const derivedScopePS = deriveSliceScope(sTitle);
1236
+ const decisionsInline = await inlineDecisionsFromDb(base, mid, derivedScopePS, inlineLevel);
1070
1237
  if (decisionsInline) inlined.push(decisionsInline);
1071
- const requirementsInline = await inlineRequirementsFromDb(base, sid, inlineLevel);
1238
+ const requirementsInline = await inlineRequirementsFromDb(base, mid, sid, inlineLevel);
1072
1239
  if (requirementsInline) inlined.push(requirementsInline);
1073
1240
  }
1074
- const knowledgeInlinePS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1241
+
1242
+ // Use scoped knowledge based on slice title keywords
1243
+ const keywordsPS = extractKeywords(sTitle);
1244
+ const knowledgeInlinePS = await inlineKnowledgeScoped(base, keywordsPS);
1075
1245
  if (knowledgeInlinePS) inlined.push(knowledgeInlinePS);
1246
+
1076
1247
  inlined.push(inlineTemplate("plan", "Slice Plan"));
1077
1248
  if (inlineLevel === "full") {
1078
1249
  inlined.push(inlineTemplate("task-plan", "Task Plan"));
@@ -1272,7 +1443,7 @@ export async function buildCompleteSlicePrompt(
1272
1443
  if (sliceCtxInline) inlined.push(sliceCtxInline);
1273
1444
  inlined.push(await inlineFile(slicePlanPath, slicePlanRel, "Slice Plan"));
1274
1445
  if (inlineLevel !== "minimal") {
1275
- const requirementsInline = await inlineRequirementsFromDb(base, sid, inlineLevel);
1446
+ const requirementsInline = await inlineRequirementsFromDb(base, mid, sid, inlineLevel);
1276
1447
  if (requirementsInline) inlined.push(requirementsInline);
1277
1448
  }
1278
1449
  const knowledgeInlineCS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
@@ -1355,7 +1526,7 @@ export async function buildCompleteMilestonePrompt(
1355
1526
 
1356
1527
  // Inline root GSD files (skip for minimal — completion can read these if needed)
1357
1528
  if (inlineLevel !== "minimal") {
1358
- const requirementsInline = await inlineRequirementsFromDb(base, undefined, inlineLevel);
1529
+ const requirementsInline = await inlineRequirementsFromDb(base, mid, undefined, inlineLevel);
1359
1530
  if (requirementsInline) inlined.push(requirementsInline);
1360
1531
  const decisionsInline = await inlineDecisionsFromDb(base, mid, undefined, inlineLevel);
1361
1532
  if (decisionsInline) inlined.push(decisionsInline);
@@ -1480,7 +1651,7 @@ export async function buildValidateMilestonePrompt(
1480
1651
 
1481
1652
  // Inline root GSD files
1482
1653
  if (inlineLevel !== "minimal") {
1483
- const requirementsInline = await inlineRequirementsFromDb(base, undefined, inlineLevel);
1654
+ const requirementsInline = await inlineRequirementsFromDb(base, mid, undefined, inlineLevel);
1484
1655
  if (requirementsInline) inlined.push(requirementsInline);
1485
1656
  const decisionsInline = await inlineDecisionsFromDb(base, mid, undefined, inlineLevel);
1486
1657
  if (decisionsInline) inlined.push(decisionsInline);
@@ -1656,7 +1827,7 @@ export async function buildReassessRoadmapPrompt(
1656
1827
  if (inlineLevel !== "minimal") {
1657
1828
  const projectInline = await inlineProjectFromDb(base);
1658
1829
  if (projectInline) inlined.push(projectInline);
1659
- const requirementsInline = await inlineRequirementsFromDb(base, undefined, inlineLevel);
1830
+ const requirementsInline = await inlineRequirementsFromDb(base, mid, undefined, inlineLevel);
1660
1831
  if (requirementsInline) inlined.push(requirementsInline);
1661
1832
  const decisionsInline = await inlineDecisionsFromDb(base, mid, undefined, inlineLevel);
1662
1833
  if (decisionsInline) inlined.push(decisionsInline);
@@ -1205,7 +1205,18 @@ export async function startAuto(
1205
1205
  s.active = true;
1206
1206
  s.verbose = verboseMode;
1207
1207
  s.stepMode = requestedStepMode;
1208
- s.cmdCtx = ctx;
1208
+ // Preserve the original cmdCtx (ExtensionCommandContext with newSession)
1209
+ // when resuming from a provider-error pause. The resume callback receives
1210
+ // an ExtensionContext (from the agent_end hook) which lacks newSession —
1211
+ // using it would crash runUnit with "newSession is not a function".
1212
+ // Only override if the new ctx actually has newSession (user-initiated resume).
1213
+ if ("newSession" in ctx && typeof (ctx as any).newSession === "function") {
1214
+ s.cmdCtx = ctx;
1215
+ } else if (!s.cmdCtx) {
1216
+ // No saved cmdCtx — this shouldn't happen, but handle gracefully
1217
+ s.cmdCtx = ctx as ExtensionCommandContext;
1218
+ }
1219
+ // else: keep existing s.cmdCtx which has the real newSession
1209
1220
  s.basePath = base;
1210
1221
  setLogBasePath(base);
1211
1222
  if (!s.autoStartTime || s.autoStartTime <= 0) s.autoStartTime = Date.now();
@@ -19,7 +19,17 @@ import {
19
19
 
20
20
  const retryState = createRetryState();
21
21
  const MAX_NETWORK_RETRIES = 2;
22
- const MAX_TRANSIENT_AUTO_RESUMES = 3;
22
+ const MAX_TRANSIENT_AUTO_RESUMES = 8;
23
+
24
+ /**
25
+ * Reset the module-level retry state so a resumed auto-session starts fresh.
26
+ * Called by provider-error-resume.ts before startAuto() — without this, the
27
+ * consecutiveTransientCount accumulates across pause/resume cycles and locks
28
+ * out auto-resume after MAX_TRANSIENT_AUTO_RESUMES total (not consecutive) errors.
29
+ */
30
+ export function resetTransientRetryState(): void {
31
+ resetRetryState(retryState);
32
+ }
23
33
 
24
34
  async function pauseTransientWithBackoff(
25
35
  cls: ErrorClass,
@@ -114,6 +124,29 @@ export async function handleAgentEnd(
114
124
  // ── 1. Classify using rawErrorMsg to avoid prose false-positives ────
115
125
  const cls = classifyError(rawErrorMsg, explicitRetryAfterMs);
116
126
 
127
+ // ── 1b. Defer to Core RetryHandler for transient errors ─────────────
128
+ // The Core RetryHandler (agent-session.ts) processes retryable errors
129
+ // AFTER this extension handler, in the same _processAgentEvent() call.
130
+ // For transient errors (overloaded, rate limit, server), the Core will
131
+ // retry in-context — same session, same conversation — which is strictly
132
+ // better than our Layer 2 pause+resume (which creates a new session).
133
+ //
134
+ // If we react here AND the Core also retries, we race: pauseAuto tears
135
+ // down the session while agent.continue() starts a new turn.
136
+ //
137
+ // Solution: Do nothing for transient errors. The Core RetryHandler
138
+ // runs next in _processAgentEvent and will either:
139
+ // a) Retry successfully → new agent_end (success) → we see it next time
140
+ // b) Exhaust retries → the agent stays idle, autoLoop's unit timeout
141
+ // or stuck detection handles it
142
+ //
143
+ // We do NOT call resolveAgentEnd here — that would unblock autoLoop
144
+ // prematurely while the Core is still retrying in the same session.
145
+ // We do NOT call pauseAuto — that would tear down the session.
146
+ if (isTransient(cls)) {
147
+ return;
148
+ }
149
+
117
150
  // Cap rate-limit backoff for CLI-style providers (openai-codex, google-gemini-cli)
118
151
  // which use per-user quotas with shorter windows (#2922).
119
152
  if (cls.kind === "rate-limit") {
@@ -5,6 +5,7 @@ import type {
5
5
  } from "@gsd/pi-coding-agent";
6
6
 
7
7
  import { getAutoDashboardData, startAuto, type AutoDashboardData } from "../auto.js";
8
+ import { resetTransientRetryState } from "./agent-end-recovery.js";
8
9
 
9
10
  type AutoResumeSnapshot = Pick<AutoDashboardData, "active" | "paused" | "stepMode" | "basePath">;
10
11
 
@@ -42,6 +43,11 @@ export async function resumeAutoAfterProviderDelay(
42
43
  return "missing-base";
43
44
  }
44
45
 
46
+ // Reset the transient retry counter before restarting — without this,
47
+ // consecutiveTransientCount accumulates across pause/resume cycles and
48
+ // permanently locks out auto-resume after MAX_TRANSIENT_AUTO_RESUMES errors.
49
+ resetTransientRetryState();
50
+
45
51
  await deps.startAuto(
46
52
  ctx as ExtensionCommandContext,
47
53
  pi,
@@ -15,6 +15,7 @@ export interface DecisionQueryOpts {
15
15
  }
16
16
 
17
17
  export interface RequirementQueryOpts {
18
+ milestoneId?: string;
18
19
  sliceId?: string;
19
20
  status?: string;
20
21
  }
@@ -67,7 +68,8 @@ export function queryDecisions(opts?: DecisionQueryOpts): Decision[] {
67
68
 
68
69
  /**
69
70
  * Query active (non-superseded) requirements with optional filters.
70
- * - sliceId: filters where primary_owner LIKE '%sliceId%' OR supporting_slices LIKE '%sliceId%'
71
+ * - milestoneId: combined with sliceId for precise filtering (e.g. %M005/S01%)
72
+ * - sliceId: filters where primary_owner LIKE '%pattern%' OR supporting_slices LIKE '%pattern%'
71
73
  * - status: filters where status = :status (exact match)
72
74
  *
73
75
  * Returns [] if DB is not available. Never throws.
@@ -81,9 +83,19 @@ export function queryRequirements(opts?: RequirementQueryOpts): Requirement[] {
81
83
  const clauses: string[] = ['superseded_by IS NULL'];
82
84
  const params: Record<string, unknown> = {};
83
85
 
84
- if (opts?.sliceId) {
86
+ // Combined milestone+slice filtering for precise scoping
87
+ if (opts?.milestoneId && opts?.sliceId) {
88
+ // Use combined pattern like %M005/S01% to avoid cross-milestone contamination
89
+ clauses.push('(primary_owner LIKE :combined_pattern OR supporting_slices LIKE :combined_pattern)');
90
+ params[':combined_pattern'] = `%${opts.milestoneId}/${opts.sliceId}%`;
91
+ } else if (opts?.sliceId) {
92
+ // Slice-only filtering (legacy behavior)
85
93
  clauses.push('(primary_owner LIKE :slice_pattern OR supporting_slices LIKE :slice_pattern)');
86
94
  params[':slice_pattern'] = `%${opts.sliceId}%`;
95
+ } else if (opts?.milestoneId) {
96
+ // Milestone-only filtering
97
+ clauses.push('(primary_owner LIKE :milestone_pattern OR supporting_slices LIKE :milestone_pattern)');
98
+ params[':milestone_pattern'] = `%${opts.milestoneId}%`;
87
99
  }
88
100
 
89
101
  if (opts?.status) {
@@ -194,3 +206,156 @@ export function queryArtifact(path: string): string | null {
194
206
  export function queryProject(): string | null {
195
207
  return queryArtifact('PROJECT.md');
196
208
  }
209
+
210
+ // ─── Knowledge Query ───────────────────────────────────────────────────────
211
+
212
+ /**
213
+ * Filter KNOWLEDGE.md sections by keyword matching.
214
+ * Uses H2 sections, matches keywords case-insensitively against:
215
+ * 1. Section header text
216
+ * 2. First paragraph of section content (up to first blank line or next heading)
217
+ *
218
+ * Per D020, returns empty string (not null) when no matches found.
219
+ * This signals "no relevant knowledge" vs "file not found".
220
+ *
221
+ * @param content - Full KNOWLEDGE.md content
222
+ * @param keywords - Keywords to match (case-insensitive)
223
+ * @returns Concatenated matching sections with H2 headers, or empty string
224
+ */
225
+ export async function queryKnowledge(content: string, keywords: string[]): Promise<string> {
226
+ if (!content || keywords.length === 0) return '';
227
+
228
+ // Lazy import to avoid circular dependency
229
+ const { extractAllSections } = await import('./files.js');
230
+
231
+ const sections = extractAllSections(content, 2);
232
+ if (sections.size === 0) return '';
233
+
234
+ // Normalize keywords for case-insensitive matching
235
+ const normalizedKeywords = keywords.map(k => k.toLowerCase());
236
+
237
+ const matchingSections: string[] = [];
238
+
239
+ for (const [header, body] of sections) {
240
+ // Extract first paragraph: everything up to first blank line or next heading
241
+ const firstParagraph = body.split(/\n\s*\n|\n#/)[0] || '';
242
+
243
+ // Check if any keyword matches header or first paragraph
244
+ const headerLower = header.toLowerCase();
245
+ const paragraphLower = firstParagraph.toLowerCase();
246
+
247
+ const matches = normalizedKeywords.some(kw =>
248
+ headerLower.includes(kw) || paragraphLower.includes(kw)
249
+ );
250
+
251
+ if (matches) {
252
+ matchingSections.push(`## ${header}\n\n${body}`);
253
+ }
254
+ }
255
+
256
+ return matchingSections.join('\n\n');
257
+ }
258
+
259
+ // ─── Roadmap Excerpt Formatter ─────────────────────────────────────────────
260
+
261
+ /**
262
+ * Format a minimal roadmap excerpt for prompt injection.
263
+ * Parses the slice table from roadmap content, extracts:
264
+ * 1. Header row + separator
265
+ * 2. Predecessor row (if sliceId depends on one via the Depends column)
266
+ * 3. Target slice row
267
+ * 4. Reference directive pointing to full roadmap path
268
+ *
269
+ * Per D021, this minimizes injected content while preserving dependency awareness.
270
+ * Returns empty string if sliceId is not found in the table.
271
+ * Never throws.
272
+ *
273
+ * @param roadmapContent - Full content of the M###-ROADMAP.md file
274
+ * @param sliceId - Target slice ID (e.g. 'S02')
275
+ * @param roadmapPath - Optional path for reference directive (defaults to generic)
276
+ */
277
+ export function formatRoadmapExcerpt(
278
+ roadmapContent: string,
279
+ sliceId: string,
280
+ roadmapPath = 'ROADMAP.md',
281
+ ): string {
282
+ if (!roadmapContent || !sliceId) return '';
283
+
284
+ const lines = roadmapContent.split('\n');
285
+
286
+ // Find the slice table header: | ID | Slice | ... (case insensitive)
287
+ let headerIndex = -1;
288
+ for (let i = 0; i < lines.length; i++) {
289
+ const line = lines[i];
290
+ if (line && /^\s*\|\s*ID\s*\|\s*Slice\s*\|/i.test(line)) {
291
+ headerIndex = i;
292
+ break;
293
+ }
294
+ }
295
+
296
+ if (headerIndex === -1) return '';
297
+
298
+ // The separator should be the next line (|---|---|...)
299
+ const separatorIndex = headerIndex + 1;
300
+ if (separatorIndex >= lines.length) return '';
301
+
302
+ const headerLine = lines[headerIndex];
303
+ const separatorLine = lines[separatorIndex];
304
+
305
+ // Validate separator line looks like |---|---|... (may include : for alignment)
306
+ if (!separatorLine || !/^\s*\|[\s:\-|]+\|/.test(separatorLine)) return '';
307
+
308
+ // Parse table rows after separator
309
+ interface SliceRow {
310
+ line: string;
311
+ id: string;
312
+ depends: string;
313
+ }
314
+
315
+ const sliceRows: SliceRow[] = [];
316
+ for (let i = separatorIndex + 1; i < lines.length; i++) {
317
+ const line = lines[i];
318
+ if (!line || !line.trim().startsWith('|')) break; // End of table
319
+
320
+ // Parse row: | ID | Slice | Risk | Depends | Done | After this |
321
+ const cells = line.split('|').map(c => c.trim());
322
+ // cells[0] is empty (before first |), cells[1] is ID, etc.
323
+ if (cells.length < 5) continue;
324
+
325
+ const id = cells[1] || '';
326
+ const depends = cells[4] || ''; // Depends column (0-indexed: empty, ID, Slice, Risk, Depends, ...)
327
+
328
+ sliceRows.push({ line, id, depends });
329
+ }
330
+
331
+ // Find target slice row
332
+ const targetRow = sliceRows.find(r => r.id === sliceId);
333
+ if (!targetRow) return '';
334
+
335
+ // Find predecessor if target depends on one
336
+ // Depends column may contain: '—', 'S01', 'S01, S02', etc.
337
+ let predecessorRow: SliceRow | undefined;
338
+ const dependsRaw = targetRow.depends;
339
+ if (dependsRaw && dependsRaw !== '—' && dependsRaw !== '-') {
340
+ // Extract first dependency (e.g. 'S01' from 'S01, S02')
341
+ const depMatch = dependsRaw.match(/S\d+/);
342
+ if (depMatch) {
343
+ predecessorRow = sliceRows.find(r => r.id === depMatch[0]);
344
+ }
345
+ }
346
+
347
+ // Build excerpt
348
+ const excerptLines: string[] = [headerLine!, separatorLine!];
349
+
350
+ if (predecessorRow) {
351
+ excerptLines.push(predecessorRow.line);
352
+ }
353
+
354
+ excerptLines.push(targetRow.line);
355
+
356
+ // Add reference directive
357
+ excerptLines.push('');
358
+ excerptLines.push(`> See full roadmap: ${roadmapPath}`);
359
+
360
+ return excerptLines.join('\n');
361
+ }
@@ -200,11 +200,13 @@ function loadPreferencesFile(path: string, scope: "global" | "project"): LoadedG
200
200
  }
201
201
 
202
202
  let _warnedUnrecognizedFormat = false;
203
+ let _warnedSectionParse = false;
203
204
 
204
205
  /** @internal Reset the warn-once flags — exported for testing only. */
205
206
  export function _resetParseWarningFlag(): void {
206
207
  _warnedUnrecognizedFormat = false;
207
208
  _warnedFrontmatterParse = false;
209
+ _warnedSectionParse = false;
208
210
  }
209
211
 
210
212
  /** @internal Exported for testing only */
@@ -309,7 +311,10 @@ function parseHeadingListFormat(content: string): GSDPreferences {
309
311
 
310
312
  typed[targetSection] = value;
311
313
  } catch (e) {
312
- logWarning("guided", `preferences section parse failed: ${(e as Error).message}`);
314
+ if (!_warnedSectionParse) {
315
+ _warnedSectionParse = true;
316
+ logWarning("guided", `preferences section parse failed: ${(e as Error).message}`);
317
+ }
313
318
  }
314
319
  }
315
320