gsd-pi 2.68.1-dev.362687a → 2.68.1-dev.abc8f2b

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 (140) hide show
  1. package/dist/resources/extensions/gsd/auto-model-selection.js +27 -1
  2. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +7 -0
  3. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -5
  4. package/dist/resources/extensions/gsd/guided-flow.js +25 -70
  5. package/dist/resources/extensions/gsd/model-router.js +85 -2
  6. package/dist/resources/extensions/gsd/prompts/discuss.md +2 -0
  7. package/dist/resources/extensions/gsd/templates/context.md +34 -2
  8. package/dist/web/standalone/.next/BUILD_ID +1 -1
  9. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  10. package/dist/web/standalone/.next/build-manifest.json +2 -2
  11. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  12. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/index.html +1 -1
  29. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  36. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  37. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  38. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  39. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  40. package/package.json +1 -1
  41. package/packages/pi-ai/dist/index.d.ts +3 -0
  42. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  43. package/packages/pi-ai/dist/index.js +2 -0
  44. package/packages/pi-ai/dist/index.js.map +1 -1
  45. package/packages/pi-ai/dist/providers/amazon-bedrock.js +2 -2
  46. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  47. package/packages/pi-ai/dist/providers/anthropic-shared.js +2 -2
  48. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  49. package/packages/pi-ai/dist/providers/google-shared.js +2 -2
  50. package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
  51. package/packages/pi-ai/dist/providers/mistral.js +2 -2
  52. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  53. package/packages/pi-ai/dist/providers/openai-completions.js +2 -2
  54. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  55. package/packages/pi-ai/dist/providers/openai-responses-shared.js +2 -2
  56. package/packages/pi-ai/dist/providers/openai-responses-shared.js.map +1 -1
  57. package/packages/pi-ai/dist/providers/provider-capabilities.d.ts +59 -0
  58. package/packages/pi-ai/dist/providers/provider-capabilities.d.ts.map +1 -0
  59. package/packages/pi-ai/dist/providers/provider-capabilities.js +173 -0
  60. package/packages/pi-ai/dist/providers/provider-capabilities.js.map +1 -0
  61. package/packages/pi-ai/dist/providers/provider-capabilities.test.d.ts +2 -0
  62. package/packages/pi-ai/dist/providers/provider-capabilities.test.d.ts.map +1 -0
  63. package/packages/pi-ai/dist/providers/provider-capabilities.test.js +132 -0
  64. package/packages/pi-ai/dist/providers/provider-capabilities.test.js.map +1 -0
  65. package/packages/pi-ai/dist/providers/transform-messages-report.test.d.ts +2 -0
  66. package/packages/pi-ai/dist/providers/transform-messages-report.test.d.ts.map +1 -0
  67. package/packages/pi-ai/dist/providers/transform-messages-report.test.js +172 -0
  68. package/packages/pi-ai/dist/providers/transform-messages-report.test.js.map +1 -0
  69. package/packages/pi-ai/dist/providers/transform-messages.d.ts +34 -1
  70. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  71. package/packages/pi-ai/dist/providers/transform-messages.js +73 -2
  72. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  73. package/packages/pi-ai/src/index.ts +3 -0
  74. package/packages/pi-ai/src/providers/amazon-bedrock.ts +2 -2
  75. package/packages/pi-ai/src/providers/anthropic-shared.ts +2 -2
  76. package/packages/pi-ai/src/providers/google-shared.ts +2 -2
  77. package/packages/pi-ai/src/providers/mistral.ts +2 -2
  78. package/packages/pi-ai/src/providers/openai-completions.ts +2 -2
  79. package/packages/pi-ai/src/providers/openai-responses-shared.ts +2 -2
  80. package/packages/pi-ai/src/providers/provider-capabilities.test.ts +174 -0
  81. package/packages/pi-ai/src/providers/provider-capabilities.ts +215 -0
  82. package/packages/pi-ai/src/providers/transform-messages-report.test.ts +189 -0
  83. package/packages/pi-ai/src/providers/transform-messages.ts +94 -1
  84. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts +1 -1
  85. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/extensions/index.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/extensions/loader.js +10 -1
  89. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  90. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -1
  91. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/extensions/runner.js +15 -0
  93. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +41 -0
  95. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -0
  98. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -0
  100. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/tools/tool-compatibility-registry.d.ts +27 -0
  102. package/packages/pi-coding-agent/dist/core/tools/tool-compatibility-registry.d.ts.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/tools/tool-compatibility-registry.js +69 -0
  104. package/packages/pi-coding-agent/dist/core/tools/tool-compatibility-registry.js.map +1 -0
  105. package/packages/pi-coding-agent/dist/index.d.ts +2 -2
  106. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  107. package/packages/pi-coding-agent/dist/index.js +3 -1
  108. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  109. package/packages/pi-coding-agent/src/core/extensions/index.ts +4 -0
  110. package/packages/pi-coding-agent/src/core/extensions/loader.ts +11 -1
  111. package/packages/pi-coding-agent/src/core/extensions/runner.ts +18 -0
  112. package/packages/pi-coding-agent/src/core/extensions/types.ts +45 -0
  113. package/packages/pi-coding-agent/src/core/tools/index.ts +7 -0
  114. package/packages/pi-coding-agent/src/core/tools/tool-compatibility-registry.ts +83 -0
  115. package/packages/pi-coding-agent/src/index.ts +9 -0
  116. package/src/resources/extensions/gsd/auto-model-selection.ts +36 -4
  117. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +8 -0
  118. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -5
  119. package/src/resources/extensions/gsd/guided-flow.ts +22 -84
  120. package/src/resources/extensions/gsd/model-router.ts +117 -10
  121. package/src/resources/extensions/gsd/preferences-types.ts +3 -1
  122. package/src/resources/extensions/gsd/prompts/discuss.md +2 -0
  123. package/src/resources/extensions/gsd/templates/context.md +34 -2
  124. package/src/resources/extensions/gsd/tests/capability-router.test.ts +31 -7
  125. package/src/resources/extensions/gsd/tests/model-router.test.ts +2 -2
  126. package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +199 -0
  127. package/src/resources/extensions/gsd/tests/write-gate.test.ts +13 -16
  128. package/dist/resources/extensions/gsd/prompt-validation.js +0 -67
  129. package/dist/resources/extensions/gsd/prompts/discuss-prepared.md +0 -424
  130. package/dist/resources/extensions/gsd/templates/context-enhanced.md +0 -138
  131. package/src/resources/extensions/gsd/prompt-validation.ts +0 -88
  132. package/src/resources/extensions/gsd/prompts/discuss-prepared.md +0 -424
  133. package/src/resources/extensions/gsd/templates/context-enhanced.md +0 -138
  134. package/src/resources/extensions/gsd/tests/adversarial-review-fixes.test.ts +0 -223
  135. package/src/resources/extensions/gsd/tests/integration/test-isolation.ts +0 -53
  136. package/src/resources/extensions/gsd/tests/integration-prepared-discussion.test.ts +0 -525
  137. package/src/resources/extensions/gsd/tests/preparation.test.ts +0 -1211
  138. package/src/resources/extensions/gsd/tests/prompt-builder.test.ts +0 -669
  139. /package/dist/web/standalone/.next/static/{VkiZZ5UjK7EfSjrWWd5RC → 3HMOXcBoys84RYd2F8a79}/_buildManifest.js +0 -0
  140. /package/dist/web/standalone/.next/static/{VkiZZ5UjK7EfSjrWWd5RC → 3HMOXcBoys84RYd2F8a79}/_ssgManifest.js +0 -0
@@ -5,6 +5,9 @@
5
5
  import type { ComplexityTier, ClassificationResult, TaskMetadata } from "./complexity-classifier.js";
6
6
  import { tierOrdinal } from "./complexity-classifier.js";
7
7
  import type { ResolvedModelConfig } from "./preferences.js";
8
+ import { getProviderCapabilities, type ProviderCapabilities } from "@gsd/pi-ai";
9
+ import { getToolCompatibility, getAllToolCompatibility } from "@gsd/pi-coding-agent";
10
+ import type { ToolCompatibility } from "@gsd/pi-coding-agent";
8
11
 
9
12
  // ─── Types ───────────────────────────────────────────────────────────────────
10
13
 
@@ -37,6 +40,8 @@ export interface RoutingDecision {
37
40
  selectionMethod: "tier-only" | "capability-scored";
38
41
  /** Capability scores per eligible model (capability-scored path only) */
39
42
  capabilityScores?: Record<string, number>;
43
+ /** Tools filtered out due to provider incompatibility (ADR-005) */
44
+ filteredTools?: string[];
40
45
  /** Task requirement vector used for scoring */
41
46
  taskRequirements?: Partial<Record<string, number>>;
42
47
  }
@@ -58,7 +63,7 @@ export interface ModelCapabilities {
58
63
  // Maps known model IDs to their capability tier. Used when tier_models is not
59
64
  // explicitly configured to pick the best available model for each tier.
60
65
 
61
- const MODEL_CAPABILITY_TIER: Record<string, ComplexityTier> = {
66
+ export const MODEL_CAPABILITY_TIER: Record<string, ComplexityTier> = {
62
67
  // Light-tier models (cheapest)
63
68
  "claude-haiku-4-5": "light",
64
69
  "claude-3-5-haiku-latest": "light",
@@ -139,15 +144,49 @@ const MODEL_COST_PER_1K_INPUT: Record<string, number> = {
139
144
  // model selection within an eligible tier set.
140
145
 
141
146
  export const MODEL_CAPABILITY_PROFILES: Record<string, ModelCapabilities> = {
142
- "claude-opus-4-6": { coding: 95, debugging: 90, research: 85, reasoning: 95, speed: 30, longContext: 80, instruction: 90 },
143
- "claude-sonnet-4-6": { coding: 85, debugging: 80, research: 75, reasoning: 80, speed: 60, longContext: 75, instruction: 85 },
144
- "claude-haiku-4-5": { coding: 60, debugging: 50, research: 45, reasoning: 50, speed: 95, longContext: 50, instruction: 75 },
145
- "gpt-4o": { coding: 80, debugging: 75, research: 70, reasoning: 75, speed: 65, longContext: 70, instruction: 80 },
146
- "gpt-4o-mini": { coding: 55, debugging: 45, research: 40, reasoning: 45, speed: 90, longContext: 45, instruction: 70 },
147
- "gemini-2.5-pro": { coding: 75, debugging: 70, research: 85, reasoning: 75, speed: 55, longContext: 90, instruction: 75 },
148
- "gemini-2.0-flash": { coding: 50, debugging: 40, research: 50, reasoning: 40, speed: 95, longContext: 60, instruction: 65 },
149
- "deepseek-chat": { coding: 75, debugging: 65, research: 55, reasoning: 70, speed: 70, longContext: 55, instruction: 65 },
150
- "o3": { coding: 80, debugging: 85, research: 80, reasoning: 92, speed: 25, longContext: 70, instruction: 85 },
147
+ // ── Anthropic ──────────────────────────────────────────────────────────────
148
+ "claude-opus-4-6": { coding: 95, debugging: 90, research: 85, reasoning: 95, speed: 30, longContext: 80, instruction: 90 },
149
+ "claude-sonnet-4-6": { coding: 85, debugging: 80, research: 75, reasoning: 80, speed: 60, longContext: 75, instruction: 85 },
150
+ "claude-sonnet-4-5-20250514": { coding: 85, debugging: 80, research: 75, reasoning: 80, speed: 60, longContext: 75, instruction: 85 },
151
+ "claude-3-5-sonnet-latest": { coding: 82, debugging: 78, research: 72, reasoning: 78, speed: 62, longContext: 70, instruction: 82 },
152
+ "claude-haiku-4-5": { coding: 60, debugging: 50, research: 45, reasoning: 50, speed: 95, longContext: 50, instruction: 75 },
153
+ "claude-3-5-haiku-latest": { coding: 60, debugging: 50, research: 45, reasoning: 50, speed: 95, longContext: 50, instruction: 75 },
154
+ "claude-3-haiku-20240307": { coding: 50, debugging: 40, research: 35, reasoning: 40, speed: 95, longContext: 40, instruction: 65 },
155
+ "claude-3-opus-latest": { coding: 90, debugging: 85, research: 82, reasoning: 90, speed: 35, longContext: 75, instruction: 88 },
156
+
157
+ // ── OpenAI GPT ─────────────────────────────────────────────────────────────
158
+ "gpt-4o": { coding: 80, debugging: 75, research: 70, reasoning: 75, speed: 65, longContext: 70, instruction: 80 },
159
+ "gpt-4o-mini": { coding: 55, debugging: 45, research: 40, reasoning: 45, speed: 90, longContext: 45, instruction: 70 },
160
+ "gpt-4-turbo": { coding: 78, debugging: 72, research: 68, reasoning: 72, speed: 50, longContext: 65, instruction: 78 },
161
+ "gpt-4.1": { coding: 82, debugging: 78, research: 72, reasoning: 78, speed: 62, longContext: 72, instruction: 82 },
162
+ "gpt-4.1-mini": { coding: 58, debugging: 48, research: 42, reasoning: 48, speed: 88, longContext: 48, instruction: 72 },
163
+ "gpt-4.1-nano": { coding: 40, debugging: 30, research: 25, reasoning: 30, speed: 95, longContext: 30, instruction: 60 },
164
+ "gpt-5": { coding: 92, debugging: 88, research: 85, reasoning: 92, speed: 40, longContext: 85, instruction: 90 },
165
+ "gpt-5-mini": { coding: 62, debugging: 52, research: 48, reasoning: 52, speed: 88, longContext: 52, instruction: 74 },
166
+ "gpt-5-nano": { coding: 42, debugging: 32, research: 28, reasoning: 32, speed: 95, longContext: 32, instruction: 62 },
167
+ "gpt-5-pro": { coding: 94, debugging: 90, research: 88, reasoning: 94, speed: 35, longContext: 88, instruction: 92 },
168
+ "gpt-5.1": { coding: 93, debugging: 89, research: 86, reasoning: 93, speed: 42, longContext: 86, instruction: 91 },
169
+ "gpt-5.1-codex-max": { coding: 90, debugging: 85, research: 70, reasoning: 85, speed: 55, longContext: 75, instruction: 85 },
170
+ "gpt-5.1-codex-mini": { coding: 65, debugging: 55, research: 40, reasoning: 50, speed: 88, longContext: 48, instruction: 72 },
171
+ "gpt-5.2": { coding: 93, debugging: 90, research: 87, reasoning: 93, speed: 42, longContext: 87, instruction: 91 },
172
+ "gpt-5.2-codex": { coding: 93, debugging: 90, research: 72, reasoning: 88, speed: 50, longContext: 78, instruction: 88 },
173
+ "gpt-5.3-codex": { coding: 94, debugging: 91, research: 74, reasoning: 89, speed: 50, longContext: 80, instruction: 89 },
174
+ "gpt-5.3-codex-spark": { coding: 68, debugging: 58, research: 42, reasoning: 52, speed: 90, longContext: 50, instruction: 74 },
175
+ "gpt-5.4": { coding: 95, debugging: 92, research: 88, reasoning: 94, speed: 42, longContext: 88, instruction: 92 },
176
+
177
+ // ── OpenAI o-series (reasoning-first) ──────────────────────────────────────
178
+ "o1": { coding: 78, debugging: 82, research: 78, reasoning: 90, speed: 20, longContext: 65, instruction: 82 },
179
+ "o3": { coding: 80, debugging: 85, research: 80, reasoning: 92, speed: 25, longContext: 70, instruction: 85 },
180
+ "o4-mini": { coding: 75, debugging: 80, research: 72, reasoning: 88, speed: 60, longContext: 65, instruction: 80 },
181
+ "o4-mini-deep-research": { coding: 75, debugging: 80, research: 85, reasoning: 88, speed: 30, longContext: 80, instruction: 80 },
182
+
183
+ // ── Google ─────────────────────────────────────────────────────────────────
184
+ "gemini-2.5-pro": { coding: 75, debugging: 70, research: 85, reasoning: 75, speed: 55, longContext: 90, instruction: 75 },
185
+ "gemini-2.0-flash": { coding: 50, debugging: 40, research: 50, reasoning: 40, speed: 95, longContext: 60, instruction: 65 },
186
+ "gemini-flash-2.0": { coding: 50, debugging: 40, research: 50, reasoning: 40, speed: 95, longContext: 60, instruction: 65 },
187
+
188
+ // ── DeepSeek ───────────────────────────────────────────────────────────────
189
+ "deepseek-chat": { coding: 75, debugging: 65, research: 55, reasoning: 70, speed: 70, longContext: 55, instruction: 65 },
151
190
  };
152
191
 
153
192
  // ─── Base Task Requirements Data Table ───────────────────────────────────────
@@ -502,3 +541,71 @@ function getModelCost(modelId: string): number {
502
541
  // Unknown cost — assume expensive to avoid routing to unknown cheap models
503
542
  return 999;
504
543
  }
544
+
545
+ // ─── Tool Compatibility Filter (ADR-005 Phase 3) ───────────────────────────
546
+
547
+ /**
548
+ * Check if a tool is compatible with a provider's capabilities.
549
+ * Returns true if the tool can be used with the provider.
550
+ */
551
+ export function isToolCompatibleWithProvider(
552
+ toolName: string,
553
+ providerCaps: ProviderCapabilities,
554
+ ): boolean {
555
+ const compat = getToolCompatibility(toolName);
556
+ if (!compat) return true; // no metadata = always compatible
557
+
558
+ // Hard filter: provider doesn't support image tool results
559
+ if (compat.producesImages && !providerCaps.imageToolResults) return false;
560
+
561
+ // Hard filter: tool uses schema features provider doesn't support
562
+ if (compat.schemaFeatures?.some(f => providerCaps.unsupportedSchemaFeatures.includes(f))) {
563
+ return false;
564
+ }
565
+
566
+ return true;
567
+ }
568
+
569
+ /**
570
+ * Filter a list of tool names to only those compatible with a provider.
571
+ * Used by the routing pipeline to adjust tool sets when switching providers.
572
+ */
573
+ export function filterToolsForProvider(
574
+ toolNames: string[],
575
+ providerApi: string,
576
+ ): { compatible: string[]; filtered: string[] } {
577
+ const providerCaps = getProviderCapabilities(providerApi);
578
+
579
+ // Provider doesn't support tool calling at all
580
+ if (!providerCaps.toolCalling) {
581
+ return { compatible: [], filtered: toolNames };
582
+ }
583
+
584
+ const compatible: string[] = [];
585
+ const filtered: string[] = [];
586
+
587
+ for (const name of toolNames) {
588
+ if (isToolCompatibleWithProvider(name, providerCaps)) {
589
+ compatible.push(name);
590
+ } else {
591
+ filtered.push(name);
592
+ }
593
+ }
594
+
595
+ return { compatible, filtered };
596
+ }
597
+
598
+ /**
599
+ * Adjust the active tool set for a selected model's provider capabilities.
600
+ * Returns tool names that should be active — removes incompatible tools.
601
+ *
602
+ * This is a hard filter only — it removes tools that would fail at the
603
+ * provider level. It does NOT remove tools based on soft heuristics.
604
+ */
605
+ export function adjustToolSet(
606
+ activeToolNames: string[],
607
+ selectedModelApi: string,
608
+ ): { toolNames: string[]; removedTools: string[] } {
609
+ const { compatible, filtered } = filterToolsForProvider(activeToolNames, selectedModelApi);
610
+ return { toolNames: compatible, removedTools: filtered };
611
+ }
@@ -20,7 +20,7 @@ import type {
20
20
  ReactiveExecutionConfig,
21
21
  GateEvaluationConfig,
22
22
  } from "./types.js";
23
- import type { DynamicRoutingConfig } from "./model-router.js";
23
+ import type { DynamicRoutingConfig, ModelCapabilities } from "./model-router.js";
24
24
 
25
25
  export interface ContextManagementConfig {
26
26
  observation_masking?: boolean; // default: true
@@ -255,6 +255,8 @@ export interface GSDPreferences {
255
255
  post_unit_hooks?: PostUnitHookConfig[];
256
256
  pre_dispatch_hooks?: PreDispatchHookConfig[];
257
257
  dynamic_routing?: DynamicRoutingConfig;
258
+ /** Per-model capability overrides. Deep-merged with built-in profiles for capability-aware routing (ADR-004). */
259
+ modelOverrides?: Record<string, { capabilities?: Partial<ModelCapabilities> }>;
258
260
  context_management?: ContextManagementConfig;
259
261
  token_profile?: TokenProfile;
260
262
  phases?: PhaseSkipPreferences;
@@ -28,6 +28,8 @@ After reflection is confirmed, decide the approach based on the actual scope —
28
28
 
29
29
  **Anti-reduction rule:** If the user describes a big vision, plan the big vision. Do not ask "what's the minimum viable version?" or try to reduce scope unless the user explicitly asks for an MVP or minimal version. When something is complex or risky, phase it into a later milestone — do not cut it. The user's ambition is the target, and your job is to sequence it intelligently, not shrink it.
30
30
 
31
+ {{preparationContext}}
32
+
31
33
  ## Mandatory Investigation Before First Question Round
32
34
 
33
35
  Before asking your first question, do a mandatory investigation pass. This is not optional.
@@ -38,6 +38,28 @@ To call this milestone complete, we must prove:
38
38
  - {{one real end-to-end scenario}}
39
39
  - {{what cannot be simulated if this milestone is to be considered truly done}}
40
40
 
41
+ ## Architectural Decisions
42
+
43
+ ### {{decisionTitle}}
44
+
45
+ **Decision:** {{decisionStatement}}
46
+
47
+ **Rationale:** {{rationale}}
48
+
49
+ **Alternatives Considered:**
50
+ - {{alternative}} — {{whyNotChosen}}
51
+
52
+ ---
53
+
54
+ > Add additional decisions as separate `### Decision Title` blocks following the same structure above.
55
+ > See `.gsd/DECISIONS.md` for the full append-only register of all project decisions.
56
+
57
+ ## Error Handling Strategy
58
+
59
+ {{errorHandlingStrategy}}
60
+
61
+ > Describe the approach for handling failures, edge cases, and error propagation. Include retry policies, fallback behaviors, and user-facing error messages where relevant.
62
+
41
63
  ## Risks and Unknowns
42
64
 
43
65
  - {{riskOrUnknown}} — {{whyItMatters}}
@@ -47,8 +69,6 @@ To call this milestone complete, we must prove:
47
69
  - `{{fileOrModule}}` — {{howItRelates}}
48
70
  - `{{fileOrModule}}` — {{howItRelates}}
49
71
 
50
- > See `.gsd/DECISIONS.md` for all architectural and pattern decisions — it is an append-only register; read it during planning, append to it during execution.
51
-
52
72
  ## Relevant Requirements
53
73
 
54
74
  - {{requirementId}} — {{howThisMilestoneAdvancesIt}}
@@ -71,6 +91,18 @@ To call this milestone complete, we must prove:
71
91
 
72
92
  - {{systemOrService}} — {{howThisMilestoneInteractsWithIt}}
73
93
 
94
+ ## Testing Requirements
95
+
96
+ {{testingRequirements}}
97
+
98
+ > Specify test types (unit, integration, e2e), coverage expectations, and specific test scenarios that must pass.
99
+
100
+ ## Acceptance Criteria
101
+
102
+ {{acceptanceCriteria}}
103
+
104
+ > Per-slice acceptance criteria gathered during discussion. Each slice should have clear, testable criteria.
105
+
74
106
  ## Open Questions
75
107
 
76
108
  - {{question}} — {{currentThinking}}
@@ -11,6 +11,7 @@ import {
11
11
  getEligibleModels,
12
12
  resolveModelForComplexity,
13
13
  MODEL_CAPABILITY_PROFILES,
14
+ MODEL_CAPABILITY_TIER,
14
15
  BASE_REQUIREMENTS,
15
16
  defaultRoutingConfig,
16
17
  } from "../model-router.js";
@@ -125,13 +126,9 @@ describe("computeTaskRequirements", () => {
125
126
  // ─── MODEL_CAPABILITY_PROFILES ───────────────────────────────────────────────
126
127
 
127
128
  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) {
129
+ test("contains profiles for all tier-mapped models", () => {
130
+ const tierModels = Object.keys(MODEL_CAPABILITY_TIER);
131
+ for (const model of tierModels) {
135
132
  assert.ok(MODEL_CAPABILITY_PROFILES[model], `Missing profile for ${model}`);
136
133
  }
137
134
  });
@@ -345,3 +342,30 @@ describe("RoutingDecision.selectionMethod", () => {
345
342
  assert.equal(result.selectionMethod, "tier-only");
346
343
  });
347
344
  });
345
+
346
+ // ─── ADR-004: Profile Completeness Lint ─────────────────────────────────────
347
+ // Every model in MODEL_CAPABILITY_TIER must have an entry in
348
+ // MODEL_CAPABILITY_PROFILES. This prevents profile staleness as new models
349
+ // are added to the tier map without corresponding capability data.
350
+
351
+ describe("profile completeness (ADR-004 lint)", () => {
352
+ test("every model in MODEL_CAPABILITY_TIER has a MODEL_CAPABILITY_PROFILES entry", () => {
353
+ const tierModels = Object.keys(MODEL_CAPABILITY_TIER);
354
+ const missing = tierModels.filter(id => !MODEL_CAPABILITY_PROFILES[id]);
355
+ assert.equal(
356
+ missing.length,
357
+ 0,
358
+ `Models in MODEL_CAPABILITY_TIER but missing from MODEL_CAPABILITY_PROFILES:\n ${missing.join("\n ")}\n\nAdd capability profiles for these models in model-router.ts.`,
359
+ );
360
+ });
361
+
362
+ test("MODEL_CAPABILITY_PROFILES does not contain models absent from MODEL_CAPABILITY_TIER", () => {
363
+ const profileModels = Object.keys(MODEL_CAPABILITY_PROFILES);
364
+ const orphaned = profileModels.filter(id => !MODEL_CAPABILITY_TIER[id]);
365
+ assert.equal(
366
+ orphaned.length,
367
+ 0,
368
+ `Models in MODEL_CAPABILITY_PROFILES but not in MODEL_CAPABILITY_TIER:\n ${orphaned.join("\n ")}\n\nEither add these to MODEL_CAPABILITY_TIER or remove stale profiles.`,
369
+ );
370
+ });
371
+ });
@@ -287,9 +287,9 @@ test("resolveModelForComplexity falls back to tier-only when capability_routing
287
287
  assert.ok(!result.selectionMethod || result.selectionMethod === "tier-only");
288
288
  });
289
289
 
290
- test("MODEL_CAPABILITY_PROFILES has entries for core models", () => {
290
+ test("MODEL_CAPABILITY_PROFILES has entries for all tier-mapped models", () => {
291
291
  const profiledModels = Object.keys(MODEL_CAPABILITY_PROFILES);
292
- assert.ok(profiledModels.length >= 9, `Expected ≥9 profiles, got ${profiledModels.length}`);
292
+ assert.ok(profiledModels.length >= 30, `Expected ≥30 profiles, got ${profiledModels.length}`);
293
293
  assert.ok(MODEL_CAPABILITY_PROFILES["claude-opus-4-6"]);
294
294
  assert.ok(MODEL_CAPABILITY_PROFILES["claude-haiku-4-5"]);
295
295
  });
@@ -0,0 +1,199 @@
1
+ // GSD-2 — Tool Compatibility + Model Router Tool Filtering Tests (ADR-005 Phases 2-3)
2
+ import { describe, test, beforeEach } from "node:test";
3
+ import assert from "node:assert/strict";
4
+
5
+ import {
6
+ registerToolCompatibility,
7
+ getToolCompatibility,
8
+ getAllToolCompatibility,
9
+ registerMcpToolCompatibility,
10
+ resetToolCompatibilityRegistry,
11
+ } from "@gsd/pi-coding-agent";
12
+
13
+ import {
14
+ isToolCompatibleWithProvider,
15
+ filterToolsForProvider,
16
+ adjustToolSet,
17
+ } from "../model-router.js";
18
+
19
+ import {
20
+ getProviderCapabilities,
21
+ } from "@gsd/pi-ai";
22
+
23
+ // ─── Tool Compatibility Registry ────────────────────────────────────────────
24
+
25
+ describe("tool compatibility registry", () => {
26
+ beforeEach(() => {
27
+ resetToolCompatibilityRegistry();
28
+ });
29
+
30
+ test("built-in tools are pre-registered", () => {
31
+ const builtins = ["bash", "read", "write", "edit", "grep", "find", "ls", "lsp"];
32
+ for (const name of builtins) {
33
+ const compat = getToolCompatibility(name);
34
+ assert.ok(compat !== undefined, `${name} should be pre-registered`);
35
+ }
36
+ });
37
+
38
+ test("unknown tool returns undefined", () => {
39
+ assert.equal(getToolCompatibility("nonexistent_tool_xyz"), undefined);
40
+ });
41
+
42
+ test("registerToolCompatibility stores and retrieves metadata", () => {
43
+ registerToolCompatibility("screenshot_tool", {
44
+ producesImages: true,
45
+ minCapabilityTier: "standard",
46
+ });
47
+ const compat = getToolCompatibility("screenshot_tool");
48
+ assert.ok(compat);
49
+ assert.equal(compat.producesImages, true);
50
+ assert.equal(compat.minCapabilityTier, "standard");
51
+ });
52
+
53
+ test("registerMcpToolCompatibility sets default schema features", () => {
54
+ registerMcpToolCompatibility("mcp__test__tool");
55
+ const compat = getToolCompatibility("mcp__test__tool");
56
+ assert.ok(compat);
57
+ assert.ok(compat.schemaFeatures?.includes("patternProperties"));
58
+ });
59
+
60
+ test("registerMcpToolCompatibility allows overrides", () => {
61
+ registerMcpToolCompatibility("mcp__test__override", { producesImages: true });
62
+ const compat = getToolCompatibility("mcp__test__override");
63
+ assert.ok(compat);
64
+ assert.equal(compat.producesImages, true);
65
+ assert.ok(compat.schemaFeatures?.includes("patternProperties"));
66
+ });
67
+
68
+ test("getAllToolCompatibility returns all entries", () => {
69
+ const all = getAllToolCompatibility();
70
+ assert.ok(all.size >= 10); // at least built-in tools
71
+ assert.ok(all.has("bash"));
72
+ assert.ok(all.has("read"));
73
+ });
74
+
75
+ test("resetToolCompatibilityRegistry clears custom entries but keeps builtins", () => {
76
+ registerToolCompatibility("custom_tool", { producesImages: true });
77
+ assert.ok(getToolCompatibility("custom_tool"));
78
+ resetToolCompatibilityRegistry();
79
+ assert.equal(getToolCompatibility("custom_tool"), undefined);
80
+ assert.ok(getToolCompatibility("bash")); // built-in preserved
81
+ });
82
+ });
83
+
84
+ // ─── isToolCompatibleWithProvider ───────────────────────────────────────────
85
+
86
+ describe("isToolCompatibleWithProvider", () => {
87
+ beforeEach(() => {
88
+ resetToolCompatibilityRegistry();
89
+ });
90
+
91
+ test("tool without compatibility metadata is always compatible", () => {
92
+ const caps = getProviderCapabilities("anthropic-messages");
93
+ assert.equal(isToolCompatibleWithProvider("unknown_tool", caps), true);
94
+ });
95
+
96
+ test("built-in tools are compatible with all providers", () => {
97
+ const providers = ["anthropic-messages", "openai-responses", "google-generative-ai", "mistral-conversations"];
98
+ const tools = ["bash", "read", "write", "edit"];
99
+ for (const api of providers) {
100
+ const caps = getProviderCapabilities(api);
101
+ for (const tool of tools) {
102
+ assert.equal(
103
+ isToolCompatibleWithProvider(tool, caps),
104
+ true,
105
+ `${tool} should be compatible with ${api}`,
106
+ );
107
+ }
108
+ }
109
+ });
110
+
111
+ test("image-producing tool filtered for providers without image support", () => {
112
+ registerToolCompatibility("screenshot", { producesImages: true });
113
+ const openaiCaps = getProviderCapabilities("openai-responses");
114
+ assert.equal(isToolCompatibleWithProvider("screenshot", openaiCaps), false);
115
+
116
+ const anthropicCaps = getProviderCapabilities("anthropic-messages");
117
+ assert.equal(isToolCompatibleWithProvider("screenshot", anthropicCaps), true);
118
+ });
119
+
120
+ test("tool with unsupported schema features filtered for Google", () => {
121
+ registerToolCompatibility("complex_schema_tool", {
122
+ schemaFeatures: ["patternProperties"],
123
+ });
124
+ const googleCaps = getProviderCapabilities("google-generative-ai");
125
+ assert.equal(isToolCompatibleWithProvider("complex_schema_tool", googleCaps), false);
126
+
127
+ const anthropicCaps = getProviderCapabilities("anthropic-messages");
128
+ assert.equal(isToolCompatibleWithProvider("complex_schema_tool", anthropicCaps), true);
129
+ });
130
+ });
131
+
132
+ // ─── filterToolsForProvider ─────────────────────────────────────────────────
133
+
134
+ describe("filterToolsForProvider", () => {
135
+ beforeEach(() => {
136
+ resetToolCompatibilityRegistry();
137
+ });
138
+
139
+ test("all built-in tools pass for any provider", () => {
140
+ const toolNames = ["bash", "read", "write", "edit", "grep", "find", "ls"];
141
+ const { compatible, filtered } = filterToolsForProvider(toolNames, "mistral-conversations");
142
+ assert.deepEqual(compatible, toolNames);
143
+ assert.deepEqual(filtered, []);
144
+ });
145
+
146
+ test("image tool filtered for OpenAI Responses", () => {
147
+ registerToolCompatibility("browser_screenshot", { producesImages: true });
148
+ const toolNames = ["bash", "read", "browser_screenshot"];
149
+ const { compatible, filtered } = filterToolsForProvider(toolNames, "openai-responses");
150
+ assert.deepEqual(compatible, ["bash", "read"]);
151
+ assert.deepEqual(filtered, ["browser_screenshot"]);
152
+ });
153
+
154
+ test("MCP tool with patternProperties filtered for Google", () => {
155
+ registerMcpToolCompatibility("mcp__repowise__search");
156
+ const toolNames = ["bash", "read", "mcp__repowise__search"];
157
+ const { compatible, filtered } = filterToolsForProvider(toolNames, "google-generative-ai");
158
+ assert.deepEqual(compatible, ["bash", "read"]);
159
+ assert.deepEqual(filtered, ["mcp__repowise__search"]);
160
+ });
161
+
162
+ test("unknown provider passes all tools (permissive default)", () => {
163
+ registerToolCompatibility("image_tool", { producesImages: true });
164
+ registerMcpToolCompatibility("mcp_tool");
165
+ const toolNames = ["bash", "image_tool", "mcp_tool"];
166
+ const { compatible, filtered } = filterToolsForProvider(toolNames, "unknown-provider-xyz");
167
+ assert.deepEqual(compatible, toolNames);
168
+ assert.deepEqual(filtered, []);
169
+ });
170
+ });
171
+
172
+ // ─── adjustToolSet ──────────────────────────────────────────────────────────
173
+
174
+ describe("adjustToolSet", () => {
175
+ beforeEach(() => {
176
+ resetToolCompatibilityRegistry();
177
+ });
178
+
179
+ test("returns all tools for Anthropic (most permissive)", () => {
180
+ registerToolCompatibility("screenshot", { producesImages: true });
181
+ const toolNames = ["bash", "read", "screenshot"];
182
+ const { toolNames: result, removedTools } = adjustToolSet(toolNames, "anthropic-messages");
183
+ assert.deepEqual(result, toolNames);
184
+ assert.deepEqual(removedTools, []);
185
+ });
186
+
187
+ test("removes incompatible tools and reports them", () => {
188
+ registerToolCompatibility("screenshot", { producesImages: true });
189
+ registerMcpToolCompatibility("mcp_complex");
190
+ const toolNames = ["bash", "read", "screenshot", "mcp_complex"];
191
+ const { toolNames: result, removedTools } = adjustToolSet(toolNames, "google-generative-ai");
192
+ // Google supports images but not patternProperties
193
+ assert.ok(result.includes("bash"));
194
+ assert.ok(result.includes("read"));
195
+ assert.ok(result.includes("screenshot")); // Google supports images
196
+ assert.ok(!result.includes("mcp_complex")); // patternProperties not supported
197
+ assert.deepEqual(removedTools, ["mcp_complex"]);
198
+ });
199
+ });
@@ -230,16 +230,13 @@ import {
230
230
  // ─── Scenario 19: isGateQuestionId recognizes all gate patterns ──
231
231
 
232
232
  test('write-gate: isGateQuestionId recognizes all gate patterns', () => {
233
- assert.strictEqual(isGateQuestionId('layer1_scope_gate'), true);
234
- assert.strictEqual(isGateQuestionId('layer2_architecture_gate'), true);
235
- assert.strictEqual(isGateQuestionId('layer3_error_gate'), true);
236
- assert.strictEqual(isGateQuestionId('layer4_quality_gate'), true);
237
233
  assert.strictEqual(isGateQuestionId('depth_verification'), true);
238
234
  assert.strictEqual(isGateQuestionId('depth_verification_M002'), true);
239
- assert.strictEqual(isGateQuestionId('my_layer1_scope_gate_question'), true);
235
+ assert.strictEqual(isGateQuestionId('depth_verification_confirm'), true);
240
236
  // Non-gate question IDs
241
237
  assert.strictEqual(isGateQuestionId('project_intent'), false);
242
238
  assert.strictEqual(isGateQuestionId('feature_priority'), false);
239
+ assert.strictEqual(isGateQuestionId('layer1_scope_gate'), false);
243
240
  assert.strictEqual(isGateQuestionId(''), false);
244
241
  });
245
242
 
@@ -249,14 +246,14 @@ test('write-gate: pending gate lifecycle (set, get, clear)', () => {
249
246
  clearDiscussionFlowState();
250
247
  assert.strictEqual(getPendingGate(), null, 'starts null');
251
248
 
252
- setPendingGate('layer1_scope_gate');
253
- assert.strictEqual(getPendingGate(), 'layer1_scope_gate', 'set correctly');
249
+ setPendingGate('depth_verification');
250
+ assert.strictEqual(getPendingGate(), 'depth_verification', 'set correctly');
254
251
 
255
252
  clearPendingGate();
256
253
  assert.strictEqual(getPendingGate(), null, 'cleared correctly');
257
254
 
258
255
  // clearDiscussionFlowState also clears pending gate
259
- setPendingGate('layer2_architecture_gate');
256
+ setPendingGate('depth_verification_M002');
260
257
  clearDiscussionFlowState();
261
258
  assert.strictEqual(getPendingGate(), null, 'clearDiscussionFlowState clears pending gate');
262
259
  });
@@ -265,12 +262,12 @@ test('write-gate: pending gate lifecycle (set, get, clear)', () => {
265
262
 
266
263
  test('write-gate: shouldBlockPendingGate blocks write/edit during pending gate', () => {
267
264
  clearDiscussionFlowState();
268
- setPendingGate('layer1_scope_gate');
265
+ setPendingGate('depth_verification');
269
266
 
270
267
  // write should be blocked during discussion
271
268
  const writeResult = shouldBlockPendingGate('write', 'M001', false);
272
269
  assert.strictEqual(writeResult.block, true, 'write should be blocked');
273
- assert.ok(writeResult.reason!.includes('layer1_scope_gate'), 'reason mentions the gate');
270
+ assert.ok(writeResult.reason!.includes('depth_verification'), 'reason mentions the gate');
274
271
 
275
272
  // edit should be blocked
276
273
  const editResult = shouldBlockPendingGate('edit', 'M001', false);
@@ -287,7 +284,7 @@ test('write-gate: shouldBlockPendingGate blocks write/edit during pending gate',
287
284
 
288
285
  test('write-gate: shouldBlockPendingGate allows read-only and ask_user_questions during pending gate', () => {
289
286
  clearDiscussionFlowState();
290
- setPendingGate('layer1_scope_gate');
287
+ setPendingGate('depth_verification');
291
288
 
292
289
  // ask_user_questions is always safe (model needs to re-ask)
293
290
  assert.strictEqual(shouldBlockPendingGate('ask_user_questions', 'M001').block, false);
@@ -304,7 +301,7 @@ test('write-gate: shouldBlockPendingGate allows read-only and ask_user_questions
304
301
 
305
302
  test('write-gate: shouldBlockPendingGate blocks outside discussion when a gate is pending', () => {
306
303
  clearDiscussionFlowState();
307
- setPendingGate('layer1_scope_gate');
304
+ setPendingGate('depth_verification');
308
305
 
309
306
  // No milestoneId and no queue phase — still block because the gate is pending
310
307
  const result = shouldBlockPendingGate('write', null, false);
@@ -330,7 +327,7 @@ test('write-gate: shouldBlockPendingGate blocks in queue mode when gate is pendi
330
327
 
331
328
  test('write-gate: shouldBlockPendingGateBash allows read-only commands during pending gate', () => {
332
329
  clearDiscussionFlowState();
333
- setPendingGate('layer2_architecture_gate');
330
+ setPendingGate('depth_verification');
334
331
 
335
332
  assert.strictEqual(shouldBlockPendingGateBash('cat file.txt', 'M001').block, false);
336
333
  assert.strictEqual(shouldBlockPendingGateBash('git log --oneline', 'M001').block, false);
@@ -344,11 +341,11 @@ test('write-gate: shouldBlockPendingGateBash allows read-only commands during pe
344
341
 
345
342
  test('write-gate: shouldBlockPendingGateBash blocks mutating commands during pending gate', () => {
346
343
  clearDiscussionFlowState();
347
- setPendingGate('layer2_architecture_gate');
344
+ setPendingGate('depth_verification');
348
345
 
349
346
  const result = shouldBlockPendingGateBash('npm run build', 'M001');
350
347
  assert.strictEqual(result.block, true, 'mutating bash should be blocked');
351
- assert.ok(result.reason!.includes('layer2_architecture_gate'));
348
+ assert.ok(result.reason!.includes('depth_verification'));
352
349
 
353
350
  clearDiscussionFlowState();
354
351
  });
@@ -365,7 +362,7 @@ test('write-gate: no pending gate means no blocking', () => {
365
362
  // ─── Scenario 28: resetWriteGateState clears pending gate ──
366
363
 
367
364
  test('write-gate: resetWriteGateState clears pending gate', () => {
368
- setPendingGate('layer3_error_gate');
365
+ setPendingGate('depth_verification');
369
366
  resetWriteGateState();
370
367
  assert.strictEqual(getPendingGate(), null);
371
368
  });