gsd-pi 2.60.0-dev.d9052f5 → 2.61.0-dev.7aed0bf

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 (198) hide show
  1. package/dist/resources/extensions/ask-user-questions.js +7 -4
  2. package/dist/resources/extensions/gsd/auto/phases.js +15 -7
  3. package/dist/resources/extensions/gsd/auto-dashboard.js +21 -8
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +6 -3
  5. package/dist/resources/extensions/gsd/auto-model-selection.js +58 -9
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +3 -2
  7. package/dist/resources/extensions/gsd/auto-prompts.js +36 -20
  8. package/dist/resources/extensions/gsd/auto-recovery.js +37 -18
  9. package/dist/resources/extensions/gsd/auto-start.js +9 -5
  10. package/dist/resources/extensions/gsd/auto-timers.js +11 -5
  11. package/dist/resources/extensions/gsd/auto-unit-closeout.js +5 -3
  12. package/dist/resources/extensions/gsd/auto-verification.js +3 -2
  13. package/dist/resources/extensions/gsd/auto-worktree.js +120 -55
  14. package/dist/resources/extensions/gsd/auto.js +39 -17
  15. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +6 -3
  16. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +2 -2
  17. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +4 -10
  18. package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +2 -1
  19. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +7 -0
  20. package/dist/resources/extensions/gsd/bootstrap/system-context.js +11 -10
  21. package/dist/resources/extensions/gsd/commands/catalog.js +2 -0
  22. package/dist/resources/extensions/gsd/commands-codebase.js +48 -21
  23. package/dist/resources/extensions/gsd/commands-inspect.js +2 -1
  24. package/dist/resources/extensions/gsd/commands-maintenance.js +32 -19
  25. package/dist/resources/extensions/gsd/complexity-classifier.js +8 -4
  26. package/dist/resources/extensions/gsd/custom-verification.js +3 -2
  27. package/dist/resources/extensions/gsd/gsd-db.js +33 -13
  28. package/dist/resources/extensions/gsd/guided-flow.js +19 -9
  29. package/dist/resources/extensions/gsd/init-wizard.js +12 -0
  30. package/dist/resources/extensions/gsd/markdown-renderer.js +11 -9
  31. package/dist/resources/extensions/gsd/md-importer.js +5 -4
  32. package/dist/resources/extensions/gsd/milestone-actions.js +3 -2
  33. package/dist/resources/extensions/gsd/milestone-ids.js +2 -1
  34. package/dist/resources/extensions/gsd/model-router.js +156 -121
  35. package/dist/resources/extensions/gsd/parallel-merge.js +5 -3
  36. package/dist/resources/extensions/gsd/parallel-orchestrator.js +26 -14
  37. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  38. package/dist/resources/extensions/gsd/preferences-validation.js +45 -0
  39. package/dist/resources/extensions/gsd/preferences.js +15 -3
  40. package/dist/resources/extensions/gsd/prompt-loader.js +3 -2
  41. package/dist/resources/extensions/gsd/prompts/rethink.md +1 -1
  42. package/dist/resources/extensions/gsd/rule-registry.js +7 -6
  43. package/dist/resources/extensions/gsd/safe-fs.js +6 -8
  44. package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -2
  45. package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -2
  46. package/dist/resources/extensions/gsd/tools/complete-task.js +3 -2
  47. package/dist/resources/extensions/gsd/tools/plan-milestone.js +3 -2
  48. package/dist/resources/extensions/gsd/tools/plan-slice.js +3 -2
  49. package/dist/resources/extensions/gsd/tools/plan-task.js +2 -1
  50. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +4 -4
  51. package/dist/resources/extensions/gsd/tools/reopen-slice.js +2 -1
  52. package/dist/resources/extensions/gsd/tools/reopen-task.js +2 -1
  53. package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -1
  54. package/dist/resources/extensions/gsd/tools/validate-milestone.js +2 -1
  55. package/dist/resources/extensions/gsd/triage-resolution.js +11 -4
  56. package/dist/resources/extensions/gsd/workflow-events.js +2 -1
  57. package/dist/resources/extensions/gsd/workflow-logger.js +37 -4
  58. package/dist/resources/extensions/gsd/workflow-migration.js +14 -12
  59. package/dist/resources/extensions/gsd/workflow-projections.js +2 -2
  60. package/dist/resources/extensions/gsd/workflow-reconcile.js +2 -2
  61. package/dist/resources/extensions/gsd/worktree-manager.js +26 -14
  62. package/dist/resources/extensions/shared/interview-ui.js +3 -1
  63. package/dist/web/standalone/.next/BUILD_ID +1 -1
  64. package/dist/web/standalone/.next/app-path-routes-manifest.json +19 -19
  65. package/dist/web/standalone/.next/build-manifest.json +2 -2
  66. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  67. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  68. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/index.html +1 -1
  84. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app-paths-manifest.json +19 -19
  91. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  92. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  93. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  94. package/package.json +1 -1
  95. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  97. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -1
  99. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/core/extensions/runner.js +16 -0
  101. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +26 -0
  103. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
  106. package/packages/pi-coding-agent/dist/core/lsp/config.js +6 -1
  107. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/lsp/defaults.json +2 -2
  109. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.d.ts +2 -0
  110. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.d.ts.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.js +47 -0
  112. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.js.map +1 -0
  113. package/packages/pi-coding-agent/package.json +1 -1
  114. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  115. package/packages/pi-coding-agent/src/core/extensions/runner.ts +19 -0
  116. package/packages/pi-coding-agent/src/core/extensions/types.ts +26 -0
  117. package/packages/pi-coding-agent/src/core/lsp/config.ts +7 -1
  118. package/packages/pi-coding-agent/src/core/lsp/defaults.json +2 -2
  119. package/packages/pi-coding-agent/src/core/lsp/lsp-legacy-alias.test.ts +70 -0
  120. package/pkg/package.json +1 -1
  121. package/src/resources/extensions/ask-user-questions.ts +7 -3
  122. package/src/resources/extensions/gsd/auto/phases.ts +17 -7
  123. package/src/resources/extensions/gsd/auto-dashboard.ts +22 -8
  124. package/src/resources/extensions/gsd/auto-dispatch.ts +7 -3
  125. package/src/resources/extensions/gsd/auto-model-selection.ts +77 -15
  126. package/src/resources/extensions/gsd/auto-post-unit.ts +4 -4
  127. package/src/resources/extensions/gsd/auto-prompts.ts +37 -20
  128. package/src/resources/extensions/gsd/auto-recovery.ts +38 -18
  129. package/src/resources/extensions/gsd/auto-start.ts +10 -9
  130. package/src/resources/extensions/gsd/auto-timers.ts +12 -5
  131. package/src/resources/extensions/gsd/auto-unit-closeout.ts +6 -2
  132. package/src/resources/extensions/gsd/auto-verification.ts +3 -6
  133. package/src/resources/extensions/gsd/auto-worktree.ts +121 -55
  134. package/src/resources/extensions/gsd/auto.ts +40 -17
  135. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +4 -3
  136. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +2 -2
  137. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +4 -16
  138. package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +2 -1
  139. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +8 -0
  140. package/src/resources/extensions/gsd/bootstrap/system-context.ts +11 -10
  141. package/src/resources/extensions/gsd/commands/catalog.ts +2 -0
  142. package/src/resources/extensions/gsd/commands-codebase.ts +52 -20
  143. package/src/resources/extensions/gsd/commands-inspect.ts +2 -1
  144. package/src/resources/extensions/gsd/commands-maintenance.ts +28 -19
  145. package/src/resources/extensions/gsd/complexity-classifier.ts +9 -4
  146. package/src/resources/extensions/gsd/custom-verification.ts +3 -2
  147. package/src/resources/extensions/gsd/gsd-db.ts +12 -14
  148. package/src/resources/extensions/gsd/guided-flow.ts +9 -8
  149. package/src/resources/extensions/gsd/init-wizard.ts +12 -0
  150. package/src/resources/extensions/gsd/markdown-renderer.ts +11 -17
  151. package/src/resources/extensions/gsd/md-importer.ts +5 -4
  152. package/src/resources/extensions/gsd/milestone-actions.ts +3 -2
  153. package/src/resources/extensions/gsd/milestone-ids.ts +2 -1
  154. package/src/resources/extensions/gsd/model-router.ts +199 -173
  155. package/src/resources/extensions/gsd/parallel-merge.ts +5 -3
  156. package/src/resources/extensions/gsd/parallel-orchestrator.ts +18 -14
  157. package/src/resources/extensions/gsd/preferences-types.ts +13 -0
  158. package/src/resources/extensions/gsd/preferences-validation.ts +45 -0
  159. package/src/resources/extensions/gsd/preferences.ts +16 -3
  160. package/src/resources/extensions/gsd/prompt-loader.ts +3 -2
  161. package/src/resources/extensions/gsd/prompts/rethink.md +1 -1
  162. package/src/resources/extensions/gsd/rule-registry.ts +7 -6
  163. package/src/resources/extensions/gsd/safe-fs.ts +6 -5
  164. package/src/resources/extensions/gsd/tests/capability-router.test.ts +347 -0
  165. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +63 -0
  166. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +27 -2
  167. package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +4 -4
  168. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +1188 -0
  169. package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +841 -0
  170. package/src/resources/extensions/gsd/tests/model-router.test.ts +403 -3
  171. package/src/resources/extensions/gsd/tests/preferences.test.ts +62 -0
  172. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +21 -0
  173. package/src/resources/extensions/gsd/tests/silent-catch-diagnostics.test.ts +284 -0
  174. package/src/resources/extensions/gsd/tests/workflow-logger-audit.test.ts +120 -0
  175. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +6 -6
  176. package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -6
  177. package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -6
  178. package/src/resources/extensions/gsd/tools/complete-task.ts +3 -6
  179. package/src/resources/extensions/gsd/tools/plan-milestone.ts +3 -6
  180. package/src/resources/extensions/gsd/tools/plan-slice.ts +3 -6
  181. package/src/resources/extensions/gsd/tools/plan-task.ts +2 -3
  182. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +4 -6
  183. package/src/resources/extensions/gsd/tools/reopen-slice.ts +2 -3
  184. package/src/resources/extensions/gsd/tools/reopen-task.ts +2 -3
  185. package/src/resources/extensions/gsd/tools/replan-slice.ts +2 -3
  186. package/src/resources/extensions/gsd/tools/validate-milestone.ts +2 -3
  187. package/src/resources/extensions/gsd/triage-resolution.ts +11 -4
  188. package/src/resources/extensions/gsd/types.ts +1 -0
  189. package/src/resources/extensions/gsd/workflow-events.ts +2 -1
  190. package/src/resources/extensions/gsd/workflow-logger.ts +52 -5
  191. package/src/resources/extensions/gsd/workflow-migration.ts +14 -12
  192. package/src/resources/extensions/gsd/workflow-projections.ts +2 -2
  193. package/src/resources/extensions/gsd/workflow-reconcile.ts +2 -2
  194. package/src/resources/extensions/gsd/worktree-manager.ts +16 -14
  195. package/src/resources/extensions/shared/interview-ui.ts +3 -1
  196. package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +144 -0
  197. /package/dist/web/standalone/.next/static/{JVkoVYumy0cDhOQISEYdG → b7FOoMHaUb3FPoLNbxar4}/_buildManifest.js +0 -0
  198. /package/dist/web/standalone/.next/static/{JVkoVYumy0cDhOQISEYdG → b7FOoMHaUb3FPoLNbxar4}/_ssgManifest.js +0 -0
@@ -103,6 +103,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
103
103
  "stale_commit_threshold_minutes",
104
104
  "context_management",
105
105
  "experimental",
106
+ "codebase",
106
107
  ]);
107
108
 
108
109
  /** Canonical list of all dispatch unit types. */
@@ -211,6 +212,16 @@ export interface ExperimentalPreferences {
211
212
  rtk?: boolean;
212
213
  }
213
214
 
215
+ /** Configuration for the codebase map generator (/gsd codebase). */
216
+ export interface CodebaseMapPreferences {
217
+ /** Additional directory/file patterns to exclude (e.g. ["docs/", "fixtures/"]). Merged with built-in defaults. */
218
+ exclude_patterns?: string[];
219
+ /** Max files to include in the map. Default: 500. */
220
+ max_files?: number;
221
+ /** Files-per-directory threshold before collapsing to a summary line. Default: 20. */
222
+ collapse_threshold?: number;
223
+ }
224
+
214
225
  export interface GSDPreferences {
215
226
  version?: number;
216
227
  mode?: WorkflowMode;
@@ -275,6 +286,8 @@ export interface GSDPreferences {
275
286
  * See the preferences reference for details on each feature.
276
287
  */
277
288
  experimental?: ExperimentalPreferences;
289
+ /** Configuration for the codebase map generator (/gsd codebase). */
290
+ codebase?: CodebaseMapPreferences;
278
291
  }
279
292
 
280
293
  export interface LoadedGSDPreferences {
@@ -857,5 +857,50 @@ export function validatePreferences(preferences: GSDPreferences): {
857
857
  }
858
858
  }
859
859
 
860
+ // ─── Codebase Map ──────────────────────────────────────────────────
861
+ if (preferences.codebase !== undefined) {
862
+ if (typeof preferences.codebase === "object" && preferences.codebase !== null) {
863
+ const cb = preferences.codebase as Record<string, unknown>;
864
+ const validCb: import("./preferences-types.js").CodebaseMapPreferences = {};
865
+
866
+ if (cb.exclude_patterns !== undefined) {
867
+ if (Array.isArray(cb.exclude_patterns) && cb.exclude_patterns.every((p: unknown) => typeof p === "string")) {
868
+ validCb.exclude_patterns = cb.exclude_patterns as string[];
869
+ } else {
870
+ errors.push("codebase.exclude_patterns must be an array of strings");
871
+ }
872
+ }
873
+ if (cb.max_files !== undefined) {
874
+ const mf = typeof cb.max_files === "number" ? cb.max_files : Number(cb.max_files);
875
+ if (Number.isFinite(mf) && mf >= 1) {
876
+ validCb.max_files = Math.floor(mf);
877
+ } else {
878
+ errors.push("codebase.max_files must be a positive integer");
879
+ }
880
+ }
881
+ if (cb.collapse_threshold !== undefined) {
882
+ const ct = typeof cb.collapse_threshold === "number" ? cb.collapse_threshold : Number(cb.collapse_threshold);
883
+ if (Number.isFinite(ct) && ct >= 1) {
884
+ validCb.collapse_threshold = Math.floor(ct);
885
+ } else {
886
+ errors.push("codebase.collapse_threshold must be a positive integer");
887
+ }
888
+ }
889
+
890
+ const knownCbKeys = new Set(["exclude_patterns", "max_files", "collapse_threshold"]);
891
+ for (const key of Object.keys(cb)) {
892
+ if (!knownCbKeys.has(key)) {
893
+ warnings.push(`unknown codebase key "${key}" — ignored`);
894
+ }
895
+ }
896
+
897
+ if (Object.keys(validCb).length > 0) {
898
+ validated.codebase = validCb;
899
+ }
900
+ } else {
901
+ errors.push("codebase must be an object");
902
+ }
903
+ }
904
+
860
905
  return { preferences: validated, errors, warnings };
861
906
  }
@@ -19,6 +19,7 @@ import { parse as parseYaml } from "yaml";
19
19
  import type { PostUnitHookConfig, PreDispatchHookConfig, TokenProfile } from "./types.js";
20
20
  import type { DynamicRoutingConfig } from "./model-router.js";
21
21
  import { normalizeStringArray } from "../shared/format-utils.js";
22
+ import { logWarning } from "./workflow-logger.js";
22
23
  import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js";
23
24
 
24
25
  import {
@@ -48,6 +49,7 @@ export type {
48
49
  AutoSupervisorConfig,
49
50
  RemoteQuestionsConfig,
50
51
  CmuxPreferences,
52
+ CodebaseMapPreferences,
51
53
  GSDPreferences,
52
54
  LoadedGSDPreferences,
53
55
  SkillResolution,
@@ -237,7 +239,7 @@ function parseFrontmatterBlock(frontmatter: string): GSDPreferences {
237
239
  }
238
240
  return parsed as GSDPreferences;
239
241
  } catch (e) {
240
- console.error("[parseFrontmatterBlock] YAML parse error:", e);
242
+ logWarning("guided", `YAML parse error in frontmatter block: ${(e as Error).message}`);
241
243
  return {} as GSDPreferences;
242
244
  }
243
245
  }
@@ -296,8 +298,8 @@ function parseHeadingListFormat(content: string): GSDPreferences {
296
298
  }
297
299
 
298
300
  typed[targetSection] = value;
299
- } catch {
300
- /* malformed section skip */
301
+ } catch (e) {
302
+ logWarning("guided", `preferences section parse failed: ${(e as Error).message}`);
301
303
  }
302
304
  }
303
305
 
@@ -371,6 +373,17 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
371
373
  service_tier: override.service_tier ?? base.service_tier,
372
374
  forensics_dedup: override.forensics_dedup ?? base.forensics_dedup,
373
375
  show_token_cost: override.show_token_cost ?? base.show_token_cost,
376
+ codebase: (base.codebase || override.codebase)
377
+ ? {
378
+ ...(base.codebase ?? {}),
379
+ ...(override.codebase ?? {}),
380
+ // Merge exclude_patterns arrays rather than overriding
381
+ exclude_patterns: [
382
+ ...((base.codebase?.exclude_patterns) ?? []),
383
+ ...((override.codebase?.exclude_patterns) ?? []),
384
+ ].filter(Boolean),
385
+ }
386
+ : undefined,
374
387
  };
375
388
  }
376
389
 
@@ -22,6 +22,7 @@ import { GSDError, GSD_PARSE_ERROR } from "./errors.js";
22
22
  import { join, dirname } from "node:path";
23
23
  import { fileURLToPath } from "node:url";
24
24
  import { homedir } from "node:os";
25
+ import { logWarning } from "./workflow-logger.js";
25
26
 
26
27
  /**
27
28
  * Resolve the GSD extension directory.
@@ -72,7 +73,7 @@ function warmCache(): void {
72
73
  // prompts/ may not exist in test environments — lazy loading still works.
73
74
  // Emit a diagnostic when running outside tests so wrong-path bugs are visible.
74
75
  if (!process.env.VITEST && !process.env.NODE_TEST) {
75
- process.stderr.write(`[gsd:prompt-loader] warmCache: prompts dir not found: ${promptsDir}\n`);
76
+ logWarning("prompt", `warmCache: prompts dir not found: ${promptsDir}`);
76
77
  }
77
78
  }
78
79
 
@@ -87,7 +88,7 @@ function warmCache(): void {
87
88
  } catch {
88
89
  // templates/ may not exist in test environments — lazy loading still works.
89
90
  if (!process.env.VITEST && !process.env.NODE_TEST) {
90
- process.stderr.write(`[gsd:prompt-loader] warmCache: templates dir not found: ${templatesDir}\n`);
91
+ logWarning("prompt", `warmCache: templates dir not found: ${templatesDir}`);
91
92
  }
92
93
  }
93
94
  }
@@ -48,7 +48,7 @@ Remove the `{ID}-PARKED.md` file from the milestone directory to reactivate it.
48
48
  ### Skip a slice
49
49
  Mark a slice as skipped so auto-mode advances past it without executing. Use the `gsd_skip_slice` tool:
50
50
  ```
51
- gsd_skip_slice({ milestone_id: "M003", slice_id: "S02", reason: "Descoped — feature moved to M005" })
51
+ gsd_skip_slice({ milestoneId: "M003", sliceId: "S02", reason: "Descoped — feature moved to M005" })
52
52
  ```
53
53
  Skipped slices are treated as closed by the state machine (like "complete" but distinct). Use when a slice is no longer needed or has been superseded. The slice data is preserved for reference.
54
54
 
@@ -6,6 +6,7 @@
6
6
  //
7
7
  // A module-level singleton accessor allows existing code to migrate incrementally.
8
8
 
9
+ import { logWarning } from "./workflow-logger.js";
9
10
  import type { UnifiedRule, RulePhase } from "./rule-types.js";
10
11
  import type { DispatchAction, DispatchContext, DispatchRule } from "./auto-dispatch.js";
11
12
  import type {
@@ -387,8 +388,8 @@ export class RuleRegistry {
387
388
  const dir = join(basePath, ".gsd");
388
389
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
389
390
  writeFileSync(this._hookStatePath(basePath), JSON.stringify(state, null, 2), "utf-8");
390
- } catch {
391
- // Non-fatal state is recreatable from artifacts
391
+ } catch (e) {
392
+ logWarning("registry", `failed to persist hook state: ${(e as Error).message}`);
392
393
  }
393
394
  }
394
395
 
@@ -407,8 +408,8 @@ export class RuleRegistry {
407
408
  }
408
409
  }
409
410
  }
410
- } catch {
411
- // Non-fatal fresh state is fine
411
+ } catch (e) {
412
+ logWarning("registry", `failed to restore hook state: ${(e as Error).message}`);
412
413
  }
413
414
  }
414
415
 
@@ -423,8 +424,8 @@ export class RuleRegistry {
423
424
  "utf-8",
424
425
  );
425
426
  }
426
- } catch {
427
- // Non-fatal
427
+ } catch (e) {
428
+ logWarning("registry", `failed to clear hook state: ${(e as Error).message}`);
428
429
  }
429
430
  }
430
431
 
@@ -1,23 +1,24 @@
1
1
  import { existsSync, mkdirSync, cpSync, type CopySyncOptions } from "node:fs"
2
2
  import { dirname } from "node:path"
3
+ import { logWarning } from "./workflow-logger.js"
3
4
 
4
5
  /**
5
6
  * Safely creates a directory. Returns true if successful, false on error.
6
- * Logs to stderr when GSD_DEBUG is set.
7
+ * Logs warnings via workflow-logger on failure.
7
8
  */
8
9
  export function safeMkdir(dirPath: string): boolean {
9
10
  try {
10
11
  mkdirSync(dirPath, { recursive: true })
11
12
  return true
12
13
  } catch (err) {
13
- if (process.env.GSD_DEBUG) console.error(`[gsd] mkdir failed: ${dirPath}`, err)
14
+ logWarning("fs", `mkdir failed: ${dirPath}: ${(err as Error).message}`)
14
15
  return false
15
16
  }
16
17
  }
17
18
 
18
19
  /**
19
20
  * Safely copies src to dst. Returns true if successful, false if src doesn't exist or copy fails.
20
- * Logs to stderr when GSD_DEBUG is set.
21
+ * Logs warnings via workflow-logger on failure.
21
22
  */
22
23
  export function safeCopy(src: string, dst: string, opts?: CopySyncOptions): boolean {
23
24
  if (!existsSync(src)) return false
@@ -25,7 +26,7 @@ export function safeCopy(src: string, dst: string, opts?: CopySyncOptions): bool
25
26
  cpSync(src, dst, opts)
26
27
  return true
27
28
  } catch (err) {
28
- if (process.env.GSD_DEBUG) console.error(`[gsd] copy failed: ${src} → ${dst}`, err)
29
+ logWarning("fs", `copy failed: ${src} → ${dst}: ${(err as Error).message}`)
29
30
  return false
30
31
  }
31
32
  }
@@ -41,7 +42,7 @@ export function safeCopyRecursive(src: string, dst: string, opts?: Omit<CopySync
41
42
  cpSync(src, dst, { ...opts, recursive: true })
42
43
  return true
43
44
  } catch (err) {
44
- if (process.env.GSD_DEBUG) console.error(`[gsd] recursive copy failed: ${src} → ${dst}`, err)
45
+ logWarning("fs", `recursive copy failed: ${src} → ${dst}: ${(err as Error).message}`)
45
46
  return false
46
47
  }
47
48
  }
@@ -0,0 +1,347 @@
1
+ // GSD Extension — Capability-Aware Router Tests
2
+ // Tests for new capability scoring functions and data tables (Plan 01-01)
3
+
4
+ import { describe, test } from "node:test";
5
+ import assert from "node:assert/strict";
6
+
7
+ import {
8
+ scoreModel,
9
+ computeTaskRequirements,
10
+ scoreEligibleModels,
11
+ getEligibleModels,
12
+ resolveModelForComplexity,
13
+ MODEL_CAPABILITY_PROFILES,
14
+ BASE_REQUIREMENTS,
15
+ defaultRoutingConfig,
16
+ } from "../model-router.js";
17
+ import type { ModelCapabilities, DynamicRoutingConfig, RoutingDecision } from "../model-router.js";
18
+
19
+ // ─── scoreModel ──────────────────────────────────────────────────────────────
20
+
21
+ describe("scoreModel", () => {
22
+ const sonnetProfile: ModelCapabilities = {
23
+ coding: 85, debugging: 80, research: 75, reasoning: 80,
24
+ speed: 60, longContext: 75, instruction: 85,
25
+ };
26
+
27
+ test("produces correct weighted average for single dimension", () => {
28
+ // Only coding weight 1.0 → result should be the coding score
29
+ const score = scoreModel(sonnetProfile, { coding: 1.0 });
30
+ assert.equal(score, 85);
31
+ });
32
+
33
+ test("produces correct weighted average for two dimensions (coding 0.9, instruction 0.7)", () => {
34
+ // (0.9*85 + 0.7*85) / (0.9+0.7) = (76.5+59.5)/1.6 = 136/1.6 = 85.0
35
+ const score = scoreModel(sonnetProfile, { coding: 0.9, instruction: 0.7 });
36
+ assert.ok(Math.abs(score - 85.0) < 0.01, `Expected ~85.0, got ${score}`);
37
+ });
38
+
39
+ test("returns 50 when requirements is empty", () => {
40
+ const score = scoreModel(sonnetProfile, {});
41
+ assert.equal(score, 50);
42
+ });
43
+
44
+ test("uses 50 as fallback for unknown dimension in requirements", () => {
45
+ // 'unknown' dimension not in profile → treated as 50
46
+ const score = scoreModel(sonnetProfile, { coding: 0.5, unknown: 1.0 } as any);
47
+ // (0.5*85 + 1.0*50) / (0.5+1.0) = (42.5+50)/1.5 = 92.5/1.5 = 61.67
48
+ assert.ok(score > 61 && score < 62, `Expected ~61.67, got ${score}`);
49
+ });
50
+ });
51
+
52
+ // ─── computeTaskRequirements ─────────────────────────────────────────────────
53
+
54
+ describe("computeTaskRequirements", () => {
55
+ test("execute-task with no metadata returns base requirements", () => {
56
+ const req = computeTaskRequirements("execute-task", undefined);
57
+ assert.deepStrictEqual(req, { coding: 0.9, instruction: 0.7, speed: 0.3 });
58
+ });
59
+
60
+ test("execute-task with docs tag returns docs-adjusted requirements", () => {
61
+ const req = computeTaskRequirements("execute-task", { tags: ["docs"] });
62
+ assert.equal(req.instruction, 0.9);
63
+ assert.equal(req.coding, 0.3);
64
+ assert.equal(req.speed, 0.7);
65
+ });
66
+
67
+ test("execute-task with readme tag returns docs-adjusted requirements", () => {
68
+ const req = computeTaskRequirements("execute-task", { tags: ["readme"] });
69
+ assert.equal(req.instruction, 0.9);
70
+ });
71
+
72
+ test("execute-task with concurrency keyword boosts debugging and reasoning", () => {
73
+ const req = computeTaskRequirements("execute-task", { complexityKeywords: ["concurrency"] });
74
+ assert.equal(req.debugging, 0.9);
75
+ assert.equal(req.reasoning, 0.8);
76
+ });
77
+
78
+ test("execute-task with compatibility keyword boosts debugging and reasoning", () => {
79
+ const req = computeTaskRequirements("execute-task", { complexityKeywords: ["compatibility"] });
80
+ assert.equal(req.debugging, 0.9);
81
+ assert.equal(req.reasoning, 0.8);
82
+ });
83
+
84
+ test("execute-task with migration keyword boosts reasoning and coding", () => {
85
+ const req = computeTaskRequirements("execute-task", { complexityKeywords: ["migration"] });
86
+ assert.equal(req.reasoning, 0.9);
87
+ assert.equal(req.coding, 0.8);
88
+ });
89
+
90
+ test("execute-task with architecture keyword boosts reasoning and coding", () => {
91
+ const req = computeTaskRequirements("execute-task", { complexityKeywords: ["architecture"] });
92
+ assert.equal(req.reasoning, 0.9);
93
+ assert.equal(req.coding, 0.8);
94
+ });
95
+
96
+ test("execute-task with fileCount >= 6 boosts coding and reasoning", () => {
97
+ const req = computeTaskRequirements("execute-task", { fileCount: 8 });
98
+ assert.equal(req.coding, 0.9);
99
+ assert.equal(req.reasoning, 0.7);
100
+ });
101
+
102
+ test("execute-task with fileCount exactly 6 triggers large-file boost", () => {
103
+ const req = computeTaskRequirements("execute-task", { fileCount: 6 });
104
+ assert.equal(req.coding, 0.9);
105
+ assert.equal(req.reasoning, 0.7);
106
+ });
107
+
108
+ test("execute-task with estimatedLines >= 500 boosts coding and reasoning", () => {
109
+ const req = computeTaskRequirements("execute-task", { estimatedLines: 500 });
110
+ assert.equal(req.coding, 0.9);
111
+ assert.equal(req.reasoning, 0.7);
112
+ });
113
+
114
+ test("research-milestone with no metadata returns base requirements", () => {
115
+ const req = computeTaskRequirements("research-milestone", undefined);
116
+ assert.deepStrictEqual(req, { research: 0.9, longContext: 0.7, reasoning: 0.5 });
117
+ });
118
+
119
+ test("unknown unit type returns default reasoning requirement", () => {
120
+ const req = computeTaskRequirements("unknown-type", undefined);
121
+ assert.deepStrictEqual(req, { reasoning: 0.5 });
122
+ });
123
+ });
124
+
125
+ // ─── MODEL_CAPABILITY_PROFILES ───────────────────────────────────────────────
126
+
127
+ describe("MODEL_CAPABILITY_PROFILES", () => {
128
+ test("contains all 9 required models", () => {
129
+ const required = [
130
+ "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5",
131
+ "gpt-4o", "gpt-4o-mini", "gemini-2.5-pro", "gemini-2.0-flash",
132
+ "deepseek-chat", "o3",
133
+ ];
134
+ for (const model of required) {
135
+ assert.ok(MODEL_CAPABILITY_PROFILES[model], `Missing profile for ${model}`);
136
+ }
137
+ });
138
+
139
+ test("each profile has all 7 capability dimensions", () => {
140
+ const dims: Array<keyof ModelCapabilities> = [
141
+ "coding", "debugging", "research", "reasoning",
142
+ "speed", "longContext", "instruction",
143
+ ];
144
+ for (const [modelId, profile] of Object.entries(MODEL_CAPABILITY_PROFILES)) {
145
+ for (const dim of dims) {
146
+ assert.ok(profile[dim] !== undefined, `${modelId} missing dimension ${dim}`);
147
+ assert.ok(profile[dim] >= 0 && profile[dim] <= 100, `${modelId}.${dim} out of range`);
148
+ }
149
+ }
150
+ });
151
+
152
+ test("claude-opus-4-6 has high reasoning and coding", () => {
153
+ const opus = MODEL_CAPABILITY_PROFILES["claude-opus-4-6"];
154
+ assert.ok(opus.reasoning >= 90, `Expected reasoning >= 90, got ${opus.reasoning}`);
155
+ assert.ok(opus.coding >= 90, `Expected coding >= 90, got ${opus.coding}`);
156
+ });
157
+
158
+ test("claude-haiku-4-5 has high speed but lower reasoning", () => {
159
+ const haiku = MODEL_CAPABILITY_PROFILES["claude-haiku-4-5"];
160
+ assert.ok(haiku.speed >= 90, `Expected speed >= 90, got ${haiku.speed}`);
161
+ assert.ok(haiku.reasoning < 70, `Expected reasoning < 70, got ${haiku.reasoning}`);
162
+ });
163
+ });
164
+
165
+ // ─── BASE_REQUIREMENTS ───────────────────────────────────────────────────────
166
+
167
+ describe("BASE_REQUIREMENTS", () => {
168
+ test("contains all 11 unit types", () => {
169
+ const required = [
170
+ "execute-task", "research-milestone", "research-slice",
171
+ "plan-milestone", "plan-slice", "replan-slice",
172
+ "reassess-roadmap", "complete-slice", "run-uat",
173
+ "discuss-milestone", "complete-milestone",
174
+ ];
175
+ for (const unitType of required) {
176
+ assert.ok(BASE_REQUIREMENTS[unitType], `Missing requirements for ${unitType}`);
177
+ }
178
+ });
179
+ });
180
+
181
+ // ─── scoreEligibleModels ─────────────────────────────────────────────────────
182
+
183
+ describe("scoreEligibleModels", () => {
184
+ test("returns array sorted by score descending", () => {
185
+ const requirements = { research: 0.9, longContext: 0.7, reasoning: 0.5 };
186
+ const results = scoreEligibleModels(["claude-sonnet-4-6", "gpt-4o"], requirements);
187
+ assert.ok(results.length === 2);
188
+ assert.ok(results[0].score >= results[1].score, "Should be sorted descending by score");
189
+ });
190
+
191
+ test("returns single model when only one eligible", () => {
192
+ const requirements = { coding: 0.9 };
193
+ const results = scoreEligibleModels(["claude-sonnet-4-6"], requirements);
194
+ assert.equal(results.length, 1);
195
+ assert.equal(results[0].modelId, "claude-sonnet-4-6");
196
+ });
197
+
198
+ test("models without profiles get uniform 50s score", () => {
199
+ const requirements = { coding: 1.0 };
200
+ const results = scoreEligibleModels(["unknown-model-xyz"], requirements);
201
+ assert.equal(results[0].score, 50);
202
+ });
203
+
204
+ test("when two models score within 2 points, prefers cheaper model", () => {
205
+ // gemini-2.0-flash is cheaper than gpt-4o-mini ($0.0001 vs $0.00015)
206
+ // Use a requirement that causes similar scores for both
207
+ const requirements = { speed: 1.0 };
208
+ const results = scoreEligibleModels(["gpt-4o-mini", "gemini-2.0-flash"], requirements);
209
+ // Both are high-speed: gpt-4o-mini=90, gemini-2.0-flash=95 — scores differ by 5, not within 2
210
+ // So top should be gemini-2.0-flash by score
211
+ assert.equal(results[0].modelId, "gemini-2.0-flash");
212
+ });
213
+
214
+ test("tie-breaks by lexicographic model ID when cost and score are equal", () => {
215
+ // Use models without cost entries — both get Infinity cost
216
+ const requirements = { coding: 1.0 };
217
+ const results = scoreEligibleModels(["model-z", "model-a"], requirements);
218
+ // Both unknown → score=50, cost=Infinity → tiebreak by ID
219
+ assert.equal(results[0].modelId, "model-a");
220
+ });
221
+
222
+ test("scoreEligibleModels respects capabilityOverrides", () => {
223
+ const requirements = { coding: 1.0 };
224
+ // Override claude-sonnet-4-6's coding to 30 (worse)
225
+ const results = scoreEligibleModels(
226
+ ["claude-sonnet-4-6", "gpt-4o"],
227
+ requirements,
228
+ { "claude-sonnet-4-6": { coding: 30 } },
229
+ );
230
+ // gpt-4o coding=80 should beat overridden sonnet coding=30
231
+ assert.equal(results[0].modelId, "gpt-4o");
232
+ });
233
+ });
234
+
235
+ // ─── getEligibleModels ───────────────────────────────────────────────────────
236
+
237
+ describe("getEligibleModels", () => {
238
+ const MODELS = [
239
+ "claude-opus-4-6", // heavy
240
+ "claude-sonnet-4-6", // standard
241
+ "claude-haiku-4-5", // light
242
+ "gpt-4o-mini", // light
243
+ ];
244
+
245
+ test("returns light-tier models sorted by cost when no explicit config", () => {
246
+ const config: DynamicRoutingConfig = defaultRoutingConfig();
247
+ const result = getEligibleModels("light", MODELS, config);
248
+ assert.ok(result.length >= 1);
249
+ // All results should be light-tier
250
+ for (const id of result) {
251
+ assert.ok(
252
+ ["claude-haiku-4-5", "gpt-4o-mini"].includes(id),
253
+ `Expected light-tier model, got ${id}`,
254
+ );
255
+ }
256
+ });
257
+
258
+ test("returns explicit tier_models when configured and available", () => {
259
+ const config: DynamicRoutingConfig = {
260
+ ...defaultRoutingConfig(),
261
+ tier_models: { light: "gpt-4o-mini" },
262
+ };
263
+ const result = getEligibleModels("light", MODELS, config);
264
+ assert.deepStrictEqual(result, ["gpt-4o-mini"]);
265
+ });
266
+
267
+ test("returns empty array when no eligible models for tier", () => {
268
+ const config: DynamicRoutingConfig = defaultRoutingConfig();
269
+ // Only heavy model available, requesting light
270
+ const result = getEligibleModels("light", ["claude-opus-4-6"], config);
271
+ assert.equal(result.length, 0);
272
+ });
273
+ });
274
+
275
+ // ─── DynamicRoutingConfig extension ─────────────────────────────────────────
276
+
277
+ describe("DynamicRoutingConfig.capability_routing", () => {
278
+ test("defaultRoutingConfig includes capability_routing: true", () => {
279
+ const config = defaultRoutingConfig();
280
+ assert.equal(config.capability_routing, true);
281
+ });
282
+ });
283
+
284
+ // ─── RoutingDecision.selectionMethod ─────────────────────────────────────────
285
+
286
+ describe("RoutingDecision.selectionMethod", () => {
287
+ const MODELS = ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5", "gpt-4o-mini"];
288
+
289
+ function makeClassification(tier: "light" | "standard" | "heavy") {
290
+ return { tier, reason: "test", downgraded: false };
291
+ }
292
+
293
+ test("returns selectionMethod: tier-only when routing is disabled", () => {
294
+ const config = { ...defaultRoutingConfig(), enabled: false };
295
+ const result: RoutingDecision = resolveModelForComplexity(
296
+ makeClassification("light"),
297
+ { primary: "claude-opus-4-6", fallbacks: [] },
298
+ config,
299
+ MODELS,
300
+ );
301
+ assert.equal(result.selectionMethod, "tier-only");
302
+ });
303
+
304
+ test("returns selectionMethod: tier-only for no phase config passthrough", () => {
305
+ const config = { ...defaultRoutingConfig(), enabled: true };
306
+ const result: RoutingDecision = resolveModelForComplexity(
307
+ makeClassification("light"),
308
+ undefined,
309
+ config,
310
+ MODELS,
311
+ );
312
+ assert.equal(result.selectionMethod, "tier-only");
313
+ });
314
+
315
+ test("returns selectionMethod: tier-only for unknown model passthrough", () => {
316
+ const config = { ...defaultRoutingConfig(), enabled: true };
317
+ const result: RoutingDecision = resolveModelForComplexity(
318
+ makeClassification("light"),
319
+ { primary: "custom-provider/my-model-v3", fallbacks: [] },
320
+ config,
321
+ ["custom-provider/my-model-v3", ...MODELS],
322
+ );
323
+ assert.equal(result.selectionMethod, "tier-only");
324
+ });
325
+
326
+ test("returns selectionMethod: tier-only for no-downgrade passthrough", () => {
327
+ const config = { ...defaultRoutingConfig(), enabled: true };
328
+ const result: RoutingDecision = resolveModelForComplexity(
329
+ makeClassification("heavy"),
330
+ { primary: "claude-opus-4-6", fallbacks: [] },
331
+ config,
332
+ MODELS,
333
+ );
334
+ assert.equal(result.selectionMethod, "tier-only");
335
+ });
336
+
337
+ test("returns selectionMethod: tier-only when downgraded", () => {
338
+ const config = { ...defaultRoutingConfig(), enabled: true };
339
+ const result: RoutingDecision = resolveModelForComplexity(
340
+ makeClassification("light"),
341
+ { primary: "claude-opus-4-6", fallbacks: [] },
342
+ config,
343
+ MODELS,
344
+ );
345
+ assert.equal(result.selectionMethod, "tier-only");
346
+ });
347
+ });
@@ -486,3 +486,66 @@ test("getCodebaseMapStats: reads total file count from header for accuracy with
486
486
  cleanup(base);
487
487
  }
488
488
  });
489
+
490
+ // ─── excludePatterns from options ────────────────────────────────────────
491
+
492
+ test("generateCodebaseMap: custom excludePatterns filters additional directories", () => {
493
+ const base = makeTmpRepo();
494
+ try {
495
+ addFile(base, "src/main.ts");
496
+ addFile(base, "src/utils.ts");
497
+ addFile(base, ".cache-data/data/index.lance");
498
+ addFile(base, "docs/guide.md");
499
+
500
+ const result = generateCodebaseMap(base, {
501
+ excludePatterns: [".cache-data/", "docs/"],
502
+ });
503
+ assert.ok(result.content.includes("`src/main.ts`"));
504
+ assert.ok(result.content.includes("`src/utils.ts`"));
505
+ assert.ok(!result.content.includes(".cache-data"));
506
+ assert.ok(!result.content.includes("guide.md"));
507
+ assert.equal(result.fileCount, 2);
508
+ } finally {
509
+ cleanup(base);
510
+ }
511
+ });
512
+
513
+ test("generateCodebaseMap: collapseThreshold option overrides default", () => {
514
+ const base = makeTmpRepo();
515
+ try {
516
+ // Create 10 files in one directory — below default threshold (20)
517
+ // but above a custom threshold of 5
518
+ for (let i = 0; i < 10; i++) {
519
+ addFile(base, `src/comp${i}.ts`);
520
+ }
521
+
522
+ // With default threshold (20), files should NOT collapse
523
+ const expanded = generateCodebaseMap(base);
524
+ assert.ok(expanded.content.includes("`src/comp0.ts`"));
525
+
526
+ // With custom threshold (5), files SHOULD collapse
527
+ const collapsed = generateCodebaseMap(base, { collapseThreshold: 5 });
528
+ assert.ok(collapsed.content.includes("10 files"));
529
+ assert.ok(!collapsed.content.includes("`src/comp0.ts`\n"));
530
+ } finally {
531
+ cleanup(base);
532
+ }
533
+ });
534
+
535
+ test("updateCodebaseMap: respects excludePatterns option", () => {
536
+ const base = makeTmpRepo();
537
+ try {
538
+ addFile(base, "src/main.ts");
539
+ addFile(base, "vendor-extra/lib.js");
540
+
541
+ const initial = generateCodebaseMap(base);
542
+ writeCodebaseMap(base, initial.content);
543
+
544
+ // Update with exclusion should remove vendor-extra files
545
+ const result = updateCodebaseMap(base, { excludePatterns: ["vendor-extra/"] });
546
+ assert.ok(result.content.includes("`src/main.ts`"));
547
+ assert.ok(!result.content.includes("vendor-extra"));
548
+ } finally {
549
+ cleanup(base);
550
+ }
551
+ });