gsd-pi 2.18.0 → 2.19.0
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.
- package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +45 -15
- package/dist/resources/extensions/gsd/auto.ts +276 -19
- package/dist/resources/extensions/gsd/captures.ts +384 -0
- package/dist/resources/extensions/gsd/commands.ts +139 -3
- package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/dist/resources/extensions/gsd/metrics.ts +48 -0
- package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/dist/resources/extensions/gsd/model-router.ts +256 -0
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +73 -0
- package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
- package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
- package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +154 -0
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +193 -0
- package/dist/resources/extensions/gsd/visualizer-views.ts +293 -0
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +33 -0
- package/dist/resources/extensions/remote-questions/format.ts +12 -6
- package/dist/resources/extensions/remote-questions/manager.ts +8 -0
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +45 -15
- package/src/resources/extensions/gsd/auto.ts +276 -19
- package/src/resources/extensions/gsd/captures.ts +384 -0
- package/src/resources/extensions/gsd/commands.ts +139 -3
- package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/src/resources/extensions/gsd/metrics.ts +48 -0
- package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/src/resources/extensions/gsd/model-router.ts +256 -0
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +73 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
- package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/src/resources/extensions/gsd/triage-ui.ts +175 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +154 -0
- package/src/resources/extensions/gsd/visualizer-overlay.ts +193 -0
- package/src/resources/extensions/gsd/visualizer-views.ts +293 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +33 -0
- package/src/resources/extensions/remote-questions/format.ts +12 -6
- package/src/resources/extensions/remote-questions/manager.ts +8 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// GSD Extension — Dynamic Model Router
|
|
2
|
+
// Maps complexity tiers to models, enforcing downgrade-only semantics.
|
|
3
|
+
// The user's configured model is always the ceiling.
|
|
4
|
+
|
|
5
|
+
import type { ComplexityTier, ClassificationResult } from "./complexity-classifier.js";
|
|
6
|
+
import { tierOrdinal } from "./complexity-classifier.js";
|
|
7
|
+
import type { ResolvedModelConfig } from "./preferences.js";
|
|
8
|
+
|
|
9
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface DynamicRoutingConfig {
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
tier_models?: {
|
|
14
|
+
light?: string;
|
|
15
|
+
standard?: string;
|
|
16
|
+
heavy?: string;
|
|
17
|
+
};
|
|
18
|
+
escalate_on_failure?: boolean; // default: true
|
|
19
|
+
budget_pressure?: boolean; // default: true
|
|
20
|
+
cross_provider?: boolean; // default: true
|
|
21
|
+
hooks?: boolean; // default: true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RoutingDecision {
|
|
25
|
+
/** The model ID to use (may be downgraded from configured) */
|
|
26
|
+
modelId: string;
|
|
27
|
+
/** Fallback chain: [selected_model, ...configured_fallbacks, configured_primary] */
|
|
28
|
+
fallbacks: string[];
|
|
29
|
+
/** The complexity tier that drove this decision */
|
|
30
|
+
tier: ComplexityTier;
|
|
31
|
+
/** True if the model was downgraded from the configured primary */
|
|
32
|
+
wasDowngraded: boolean;
|
|
33
|
+
/** Human-readable reason for this decision */
|
|
34
|
+
reason: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Known Model Tiers ───────────────────────────────────────────────────────
|
|
38
|
+
// Maps known model IDs to their capability tier. Used when tier_models is not
|
|
39
|
+
// explicitly configured to pick the best available model for each tier.
|
|
40
|
+
|
|
41
|
+
const MODEL_CAPABILITY_TIER: Record<string, ComplexityTier> = {
|
|
42
|
+
// Light-tier models (cheapest)
|
|
43
|
+
"claude-haiku-4-5": "light",
|
|
44
|
+
"claude-3-5-haiku-latest": "light",
|
|
45
|
+
"claude-3-haiku-20240307": "light",
|
|
46
|
+
"gpt-4o-mini": "light",
|
|
47
|
+
"gemini-2.0-flash": "light",
|
|
48
|
+
"gemini-flash-2.0": "light",
|
|
49
|
+
|
|
50
|
+
// Standard-tier models
|
|
51
|
+
"claude-sonnet-4-6": "standard",
|
|
52
|
+
"claude-sonnet-4-5-20250514": "standard",
|
|
53
|
+
"claude-3-5-sonnet-latest": "standard",
|
|
54
|
+
"gpt-4o": "standard",
|
|
55
|
+
"gemini-2.5-pro": "standard",
|
|
56
|
+
"deepseek-chat": "standard",
|
|
57
|
+
|
|
58
|
+
// Heavy-tier models (most capable)
|
|
59
|
+
"claude-opus-4-6": "heavy",
|
|
60
|
+
"claude-3-opus-latest": "heavy",
|
|
61
|
+
"gpt-4-turbo": "heavy",
|
|
62
|
+
"o1": "heavy",
|
|
63
|
+
"o3": "heavy",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ─── Cost Table (per 1K input tokens, approximate USD) ───────────────────────
|
|
67
|
+
// Used for cross-provider cost comparison when multiple providers offer
|
|
68
|
+
// the same capability tier.
|
|
69
|
+
|
|
70
|
+
const MODEL_COST_PER_1K_INPUT: Record<string, number> = {
|
|
71
|
+
"claude-haiku-4-5": 0.0008,
|
|
72
|
+
"claude-3-5-haiku-latest": 0.0008,
|
|
73
|
+
"claude-sonnet-4-6": 0.003,
|
|
74
|
+
"claude-sonnet-4-5-20250514": 0.003,
|
|
75
|
+
"claude-opus-4-6": 0.015,
|
|
76
|
+
"gpt-4o-mini": 0.00015,
|
|
77
|
+
"gpt-4o": 0.0025,
|
|
78
|
+
"gemini-2.0-flash": 0.0001,
|
|
79
|
+
"gemini-2.5-pro": 0.00125,
|
|
80
|
+
"deepseek-chat": 0.00014,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve the model to use for a given complexity tier.
|
|
87
|
+
*
|
|
88
|
+
* Downgrade-only: the returned model is always equal to or cheaper than
|
|
89
|
+
* the user's configured primary model. Never upgrades beyond configuration.
|
|
90
|
+
*
|
|
91
|
+
* @param classification The complexity classification result
|
|
92
|
+
* @param phaseConfig The user's configured model for this phase (ceiling)
|
|
93
|
+
* @param routingConfig Dynamic routing configuration
|
|
94
|
+
* @param availableModelIds List of available model IDs (from registry)
|
|
95
|
+
*/
|
|
96
|
+
export function resolveModelForComplexity(
|
|
97
|
+
classification: ClassificationResult,
|
|
98
|
+
phaseConfig: ResolvedModelConfig | undefined,
|
|
99
|
+
routingConfig: DynamicRoutingConfig,
|
|
100
|
+
availableModelIds: string[],
|
|
101
|
+
): RoutingDecision {
|
|
102
|
+
// If no phase config or routing disabled, pass through
|
|
103
|
+
if (!phaseConfig || !routingConfig.enabled) {
|
|
104
|
+
return {
|
|
105
|
+
modelId: phaseConfig?.primary ?? "",
|
|
106
|
+
fallbacks: phaseConfig?.fallbacks ?? [],
|
|
107
|
+
tier: classification.tier,
|
|
108
|
+
wasDowngraded: false,
|
|
109
|
+
reason: "dynamic routing disabled or no phase config",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const configuredPrimary = phaseConfig.primary;
|
|
114
|
+
const configuredTier = getModelTier(configuredPrimary);
|
|
115
|
+
const requestedTier = classification.tier;
|
|
116
|
+
|
|
117
|
+
// Downgrade-only: if requested tier >= configured tier, no change
|
|
118
|
+
if (tierOrdinal(requestedTier) >= tierOrdinal(configuredTier)) {
|
|
119
|
+
return {
|
|
120
|
+
modelId: configuredPrimary,
|
|
121
|
+
fallbacks: phaseConfig.fallbacks,
|
|
122
|
+
tier: requestedTier,
|
|
123
|
+
wasDowngraded: false,
|
|
124
|
+
reason: `tier ${requestedTier} >= configured ${configuredTier}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Find the best model for the requested tier
|
|
129
|
+
const targetModelId = findModelForTier(
|
|
130
|
+
requestedTier,
|
|
131
|
+
routingConfig,
|
|
132
|
+
availableModelIds,
|
|
133
|
+
routingConfig.cross_provider !== false,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (!targetModelId) {
|
|
137
|
+
// No suitable model found — use configured primary
|
|
138
|
+
return {
|
|
139
|
+
modelId: configuredPrimary,
|
|
140
|
+
fallbacks: phaseConfig.fallbacks,
|
|
141
|
+
tier: requestedTier,
|
|
142
|
+
wasDowngraded: false,
|
|
143
|
+
reason: `no ${requestedTier}-tier model available`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Build fallback chain: [downgraded_model, ...configured_fallbacks, configured_primary]
|
|
148
|
+
const fallbacks = [
|
|
149
|
+
...phaseConfig.fallbacks.filter(f => f !== targetModelId),
|
|
150
|
+
configuredPrimary,
|
|
151
|
+
].filter(f => f !== targetModelId);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
modelId: targetModelId,
|
|
155
|
+
fallbacks,
|
|
156
|
+
tier: requestedTier,
|
|
157
|
+
wasDowngraded: true,
|
|
158
|
+
reason: classification.reason,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Escalate to the next tier after a failure.
|
|
164
|
+
* Returns the new tier, or null if already at heavy (max).
|
|
165
|
+
*/
|
|
166
|
+
export function escalateTier(currentTier: ComplexityTier): ComplexityTier | null {
|
|
167
|
+
switch (currentTier) {
|
|
168
|
+
case "light": return "standard";
|
|
169
|
+
case "standard": return "heavy";
|
|
170
|
+
case "heavy": return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get the default routing config (all features enabled).
|
|
176
|
+
*/
|
|
177
|
+
export function defaultRoutingConfig(): DynamicRoutingConfig {
|
|
178
|
+
return {
|
|
179
|
+
enabled: false,
|
|
180
|
+
escalate_on_failure: true,
|
|
181
|
+
budget_pressure: true,
|
|
182
|
+
cross_provider: true,
|
|
183
|
+
hooks: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Internal ────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
function getModelTier(modelId: string): ComplexityTier {
|
|
190
|
+
// Strip provider prefix if present
|
|
191
|
+
const bareId = modelId.includes("/") ? modelId.split("/").pop()! : modelId;
|
|
192
|
+
|
|
193
|
+
// Check exact match first
|
|
194
|
+
if (MODEL_CAPABILITY_TIER[bareId]) return MODEL_CAPABILITY_TIER[bareId];
|
|
195
|
+
|
|
196
|
+
// Check if any known model ID is a prefix/suffix match
|
|
197
|
+
for (const [knownId, tier] of Object.entries(MODEL_CAPABILITY_TIER)) {
|
|
198
|
+
if (bareId.includes(knownId) || knownId.includes(bareId)) return tier;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Unknown models are assumed heavy (safest assumption)
|
|
202
|
+
return "heavy";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function findModelForTier(
|
|
206
|
+
tier: ComplexityTier,
|
|
207
|
+
config: DynamicRoutingConfig,
|
|
208
|
+
availableModelIds: string[],
|
|
209
|
+
crossProvider: boolean,
|
|
210
|
+
): string | null {
|
|
211
|
+
// 1. Check explicit tier_models config
|
|
212
|
+
const explicitModel = config.tier_models?.[tier];
|
|
213
|
+
if (explicitModel && availableModelIds.includes(explicitModel)) {
|
|
214
|
+
return explicitModel;
|
|
215
|
+
}
|
|
216
|
+
// Also check with provider prefix stripped
|
|
217
|
+
if (explicitModel) {
|
|
218
|
+
const match = availableModelIds.find(id => {
|
|
219
|
+
const bareAvail = id.includes("/") ? id.split("/").pop()! : id;
|
|
220
|
+
const bareExplicit = explicitModel.includes("/") ? explicitModel.split("/").pop()! : explicitModel;
|
|
221
|
+
return bareAvail === bareExplicit;
|
|
222
|
+
});
|
|
223
|
+
if (match) return match;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 2. Auto-detect: find the cheapest available model in the requested tier
|
|
227
|
+
const candidates = availableModelIds
|
|
228
|
+
.filter(id => {
|
|
229
|
+
const modelTier = getModelTier(id);
|
|
230
|
+
return modelTier === tier;
|
|
231
|
+
})
|
|
232
|
+
.sort((a, b) => {
|
|
233
|
+
if (!crossProvider) return 0;
|
|
234
|
+
const costA = getModelCost(a);
|
|
235
|
+
const costB = getModelCost(b);
|
|
236
|
+
return costA - costB;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return candidates[0] ?? null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getModelCost(modelId: string): number {
|
|
243
|
+
const bareId = modelId.includes("/") ? modelId.split("/").pop()! : modelId;
|
|
244
|
+
|
|
245
|
+
if (MODEL_COST_PER_1K_INPUT[bareId] !== undefined) {
|
|
246
|
+
return MODEL_COST_PER_1K_INPUT[bareId];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check partial matches
|
|
250
|
+
for (const [knownId, cost] of Object.entries(MODEL_COST_PER_1K_INPUT)) {
|
|
251
|
+
if (bareId.includes(knownId) || knownId.includes(bareId)) return cost;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Unknown cost — assume expensive to avoid routing to unknown cheap models
|
|
255
|
+
return 999;
|
|
256
|
+
}
|
|
@@ -60,7 +60,8 @@ export function checkPostUnitHooks(
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// Don't trigger hooks for other hook units (prevent hook-on-hook chains)
|
|
63
|
-
|
|
63
|
+
// Don't trigger hooks for triage units (prevent hook-on-triage chains)
|
|
64
|
+
if (completedUnitType.startsWith("hook/") || completedUnitType === "triage-captures") return null;
|
|
64
65
|
|
|
65
66
|
// Check if any hooks are configured for this unit type
|
|
66
67
|
const hooks = resolvePostUnitHooks().filter(h =>
|
|
@@ -4,6 +4,8 @@ import { isAbsolute, join } from "node:path";
|
|
|
4
4
|
import { getAgentDir } from "@gsd/pi-coding-agent";
|
|
5
5
|
import type { GitPreferences } from "./git-service.js";
|
|
6
6
|
import type { PostUnitHookConfig, PreDispatchHookConfig, BudgetEnforcementMode, NotificationPreferences, TokenProfile, InlineLevel, PhaseSkipPreferences } from "./types.js";
|
|
7
|
+
import type { DynamicRoutingConfig } from "./model-router.js";
|
|
8
|
+
import { defaultRoutingConfig } from "./model-router.js";
|
|
7
9
|
import { VALID_BRANCH_NAME } from "./git-service.js";
|
|
8
10
|
|
|
9
11
|
const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
|
|
@@ -36,8 +38,10 @@ const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
36
38
|
"git",
|
|
37
39
|
"post_unit_hooks",
|
|
38
40
|
"pre_dispatch_hooks",
|
|
41
|
+
"dynamic_routing",
|
|
39
42
|
"token_profile",
|
|
40
43
|
"phases",
|
|
44
|
+
"auto_visualize",
|
|
41
45
|
]);
|
|
42
46
|
|
|
43
47
|
export interface GSDSkillRule {
|
|
@@ -128,8 +132,10 @@ export interface GSDPreferences {
|
|
|
128
132
|
git?: GitPreferences;
|
|
129
133
|
post_unit_hooks?: PostUnitHookConfig[];
|
|
130
134
|
pre_dispatch_hooks?: PreDispatchHookConfig[];
|
|
135
|
+
dynamic_routing?: DynamicRoutingConfig;
|
|
131
136
|
token_profile?: TokenProfile;
|
|
132
137
|
phases?: PhaseSkipPreferences;
|
|
138
|
+
auto_visualize?: boolean;
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
export interface LoadedGSDPreferences {
|
|
@@ -674,6 +680,20 @@ export function resolveModelWithFallbacksForUnit(unitType: string): ResolvedMode
|
|
|
674
680
|
};
|
|
675
681
|
}
|
|
676
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Resolve the dynamic routing configuration from effective preferences.
|
|
685
|
+
* Returns the merged config with defaults applied.
|
|
686
|
+
*/
|
|
687
|
+
export function resolveDynamicRoutingConfig(): DynamicRoutingConfig {
|
|
688
|
+
const prefs = loadEffectiveGSDPreferences();
|
|
689
|
+
const configured = prefs?.preferences.dynamic_routing;
|
|
690
|
+
if (!configured) return defaultRoutingConfig();
|
|
691
|
+
return {
|
|
692
|
+
...defaultRoutingConfig(),
|
|
693
|
+
...configured,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
677
697
|
export function resolveAutoSupervisorConfig(): AutoSupervisorConfig {
|
|
678
698
|
const prefs = loadEffectiveGSDPreferences();
|
|
679
699
|
const configured = prefs?.preferences.auto_supervisor ?? {};
|
|
@@ -780,6 +800,9 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
|
|
|
780
800
|
: undefined,
|
|
781
801
|
post_unit_hooks: mergePostUnitHooks(base.post_unit_hooks, override.post_unit_hooks),
|
|
782
802
|
pre_dispatch_hooks: mergePreDispatchHooks(base.pre_dispatch_hooks, override.pre_dispatch_hooks),
|
|
803
|
+
dynamic_routing: (base.dynamic_routing || override.dynamic_routing)
|
|
804
|
+
? { ...(base.dynamic_routing ?? {}), ...(override.dynamic_routing ?? {}) } as DynamicRoutingConfig
|
|
805
|
+
: undefined,
|
|
783
806
|
token_profile: override.token_profile ?? base.token_profile,
|
|
784
807
|
phases: (base.phases || override.phases)
|
|
785
808
|
? { ...(base.phases ?? {}), ...(override.phases ?? {}) }
|
|
@@ -1100,6 +1123,56 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
1100
1123
|
}
|
|
1101
1124
|
}
|
|
1102
1125
|
|
|
1126
|
+
// ─── Dynamic Routing ─────────────────────────────────────────────────
|
|
1127
|
+
if (preferences.dynamic_routing !== undefined) {
|
|
1128
|
+
if (typeof preferences.dynamic_routing === "object" && preferences.dynamic_routing !== null) {
|
|
1129
|
+
const dr = preferences.dynamic_routing as unknown as Record<string, unknown>;
|
|
1130
|
+
const validDr: Partial<DynamicRoutingConfig> = {};
|
|
1131
|
+
|
|
1132
|
+
if (dr.enabled !== undefined) {
|
|
1133
|
+
if (typeof dr.enabled === "boolean") validDr.enabled = dr.enabled;
|
|
1134
|
+
else errors.push("dynamic_routing.enabled must be a boolean");
|
|
1135
|
+
}
|
|
1136
|
+
if (dr.escalate_on_failure !== undefined) {
|
|
1137
|
+
if (typeof dr.escalate_on_failure === "boolean") validDr.escalate_on_failure = dr.escalate_on_failure;
|
|
1138
|
+
else errors.push("dynamic_routing.escalate_on_failure must be a boolean");
|
|
1139
|
+
}
|
|
1140
|
+
if (dr.budget_pressure !== undefined) {
|
|
1141
|
+
if (typeof dr.budget_pressure === "boolean") validDr.budget_pressure = dr.budget_pressure;
|
|
1142
|
+
else errors.push("dynamic_routing.budget_pressure must be a boolean");
|
|
1143
|
+
}
|
|
1144
|
+
if (dr.cross_provider !== undefined) {
|
|
1145
|
+
if (typeof dr.cross_provider === "boolean") validDr.cross_provider = dr.cross_provider;
|
|
1146
|
+
else errors.push("dynamic_routing.cross_provider must be a boolean");
|
|
1147
|
+
}
|
|
1148
|
+
if (dr.hooks !== undefined) {
|
|
1149
|
+
if (typeof dr.hooks === "boolean") validDr.hooks = dr.hooks;
|
|
1150
|
+
else errors.push("dynamic_routing.hooks must be a boolean");
|
|
1151
|
+
}
|
|
1152
|
+
if (dr.tier_models !== undefined) {
|
|
1153
|
+
if (typeof dr.tier_models === "object" && dr.tier_models !== null) {
|
|
1154
|
+
const tm = dr.tier_models as Record<string, unknown>;
|
|
1155
|
+
const validTm: Record<string, string> = {};
|
|
1156
|
+
for (const tier of ["light", "standard", "heavy"]) {
|
|
1157
|
+
if (tm[tier] !== undefined) {
|
|
1158
|
+
if (typeof tm[tier] === "string") validTm[tier] = tm[tier] as string;
|
|
1159
|
+
else errors.push(`dynamic_routing.tier_models.${tier} must be a string`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (Object.keys(validTm).length > 0) validDr.tier_models = validTm as DynamicRoutingConfig["tier_models"];
|
|
1163
|
+
} else {
|
|
1164
|
+
errors.push("dynamic_routing.tier_models must be an object");
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (Object.keys(validDr).length > 0) {
|
|
1169
|
+
validated.dynamic_routing = validDr as unknown as DynamicRoutingConfig;
|
|
1170
|
+
}
|
|
1171
|
+
} else {
|
|
1172
|
+
errors.push("dynamic_routing must be an object");
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1103
1176
|
// ─── Git Preferences ───────────────────────────────────────────────────
|
|
1104
1177
|
if (preferences.git && typeof preferences.git === "object") {
|
|
1105
1178
|
const git: Record<string, unknown> = {};
|
|
@@ -7,15 +7,17 @@
|
|
|
7
7
|
* Templates live at prompts/ relative to this module's directory.
|
|
8
8
|
* They use {{variableName}} syntax for substitution.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* session from being invalidated when another `gsd`
|
|
12
|
-
* ~/.gsd/agent/ with newer templates via initResources().
|
|
13
|
-
* the in-memory extension code (which knows variable
|
|
14
|
-
* newer template from disk (which expects variable set B),
|
|
15
|
-
* "template declares {{X}} but no value was provided" crash
|
|
10
|
+
* All templates are eagerly loaded into cache at module init via warmCache().
|
|
11
|
+
* This prevents a running session from being invalidated when another `gsd`
|
|
12
|
+
* launch overwrites ~/.gsd/agent/ with newer templates via initResources().
|
|
13
|
+
* Without eager caching, the in-memory extension code (which knows variable
|
|
14
|
+
* set A) can read a newer template from disk (which expects variable set B),
|
|
15
|
+
* causing a "template declares {{X}} but no value was provided" crash
|
|
16
|
+
* mid-session — especially for late-loading templates like complete-milestone
|
|
17
|
+
* that aren't read until the end of a long auto-mode run.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
|
-
import { readFileSync } from "node:fs";
|
|
20
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
19
21
|
import { join, dirname } from "node:path";
|
|
20
22
|
import { fileURLToPath } from "node:url";
|
|
21
23
|
|
|
@@ -23,10 +25,44 @@ const __extensionDir = dirname(fileURLToPath(import.meta.url));
|
|
|
23
25
|
const promptsDir = join(__extensionDir, "prompts");
|
|
24
26
|
const templatesDir = join(__extensionDir, "templates");
|
|
25
27
|
|
|
26
|
-
// Cache templates
|
|
27
|
-
// that were on disk
|
|
28
|
+
// Cache all templates eagerly at module load — a running session uses the
|
|
29
|
+
// template versions that were on disk at startup, immune to later overwrites.
|
|
28
30
|
const templateCache = new Map<string, string>();
|
|
29
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Eagerly read all .md files from prompts/ and templates/ into cache.
|
|
34
|
+
* Called once at module init so that every template is snapshot before
|
|
35
|
+
* a concurrent initResources() can overwrite files on disk.
|
|
36
|
+
*/
|
|
37
|
+
function warmCache(): void {
|
|
38
|
+
try {
|
|
39
|
+
for (const file of readdirSync(promptsDir)) {
|
|
40
|
+
if (!file.endsWith(".md")) continue;
|
|
41
|
+
const name = file.slice(0, -3);
|
|
42
|
+
if (!templateCache.has(name)) {
|
|
43
|
+
templateCache.set(name, readFileSync(join(promptsDir, file), "utf-8"));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// prompts/ may not exist in test environments — lazy loading still works
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
for (const file of readdirSync(templatesDir)) {
|
|
52
|
+
if (!file.endsWith(".md")) continue;
|
|
53
|
+
const cacheKey = `tpl:${file.slice(0, -3)}`;
|
|
54
|
+
if (!templateCache.has(cacheKey)) {
|
|
55
|
+
templateCache.set(cacheKey, readFileSync(join(templatesDir, file), "utf-8"));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// templates/ may not exist in test environments — lazy loading still works
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Snapshot all templates at module load time
|
|
64
|
+
warmCache();
|
|
65
|
+
|
|
30
66
|
/**
|
|
31
67
|
* Load a prompt template and substitute variables.
|
|
32
68
|
*
|
|
@@ -16,6 +16,12 @@ All relevant context has been preloaded below — the current roadmap, completed
|
|
|
16
16
|
|
|
17
17
|
{{inlinedContext}}
|
|
18
18
|
|
|
19
|
+
## Deferred Captures
|
|
20
|
+
|
|
21
|
+
The following user thoughts were captured during execution and deferred to future slices during triage. Consider whether any should influence the remaining roadmap:
|
|
22
|
+
|
|
23
|
+
{{deferredCaptures}}
|
|
24
|
+
|
|
19
25
|
If a `GSD Skill Preferences` block is present in system context, use it to decide which skills to load and follow during reassessment, without relaxing required verification or artifact rules.
|
|
20
26
|
|
|
21
27
|
Then assess whether the remaining roadmap still makes sense given what was just built.
|
|
@@ -12,6 +12,14 @@ All relevant context has been preloaded below — the roadmap, current slice pla
|
|
|
12
12
|
|
|
13
13
|
{{inlinedContext}}
|
|
14
14
|
|
|
15
|
+
## Capture Context
|
|
16
|
+
|
|
17
|
+
The following user-captured thoughts triggered or informed this replan:
|
|
18
|
+
|
|
19
|
+
{{captureContext}}
|
|
20
|
+
|
|
21
|
+
Consider these captures when rewriting the remaining tasks — they represent the user's real-time insights about what needs to change.
|
|
22
|
+
|
|
15
23
|
## Hard Constraints
|
|
16
24
|
|
|
17
25
|
- **Do NOT renumber or remove completed tasks.** All `[x]` tasks and their IDs must remain exactly as they are in the plan.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
You are triaging user-captured thoughts during a GSD session.
|
|
2
|
+
|
|
3
|
+
## UNIT: Triage Captures
|
|
4
|
+
|
|
5
|
+
The user captured thoughts during execution using `/gsd capture`. Your job is to classify each capture, present your proposals, get user confirmation, and update CAPTURES.md with the final classifications.
|
|
6
|
+
|
|
7
|
+
## Pending Captures
|
|
8
|
+
|
|
9
|
+
{{pendingCaptures}}
|
|
10
|
+
|
|
11
|
+
## Current Slice Plan
|
|
12
|
+
|
|
13
|
+
{{currentPlan}}
|
|
14
|
+
|
|
15
|
+
## Current Roadmap
|
|
16
|
+
|
|
17
|
+
{{roadmapContext}}
|
|
18
|
+
|
|
19
|
+
## Classification Criteria
|
|
20
|
+
|
|
21
|
+
For each capture, classify it as one of:
|
|
22
|
+
|
|
23
|
+
- **quick-task**: Small, self-contained, no downstream impact. Can be done in minutes without modifying the plan. Examples: fix a typo, add a missing import, tweak a config value.
|
|
24
|
+
- **inject**: Belongs in the current slice but wasn't planned. Needs a new task added to the slice plan. Examples: add error handling to a module being built, add a missing test case for current work.
|
|
25
|
+
- **defer**: Belongs in a future slice or milestone. Not urgent for current work. Examples: performance optimization, feature that depends on unbuilt infrastructure, nice-to-have enhancement.
|
|
26
|
+
- **replan**: Changes the shape of remaining work in the current slice. Existing incomplete tasks may need rewriting. Examples: "the approach is wrong, we need to use X instead of Y", discovering a fundamental constraint.
|
|
27
|
+
- **note**: Informational only. No action needed right now. Good context for future reference. Examples: "remember that the API has a rate limit", observations about code quality.
|
|
28
|
+
|
|
29
|
+
## Decision Guidelines
|
|
30
|
+
|
|
31
|
+
- Prefer **quick-task** when the work is clearly small and self-contained.
|
|
32
|
+
- Prefer **inject** over **replan** when only a new task is needed, not rewriting existing ones.
|
|
33
|
+
- Prefer **defer** over **inject** when the work doesn't belong in the current slice's scope.
|
|
34
|
+
- Use **replan** only when remaining incomplete tasks need to change — not just for adding work.
|
|
35
|
+
- Use **note** for observations that don't require action.
|
|
36
|
+
- When unsure between quick-task and inject, consider: will this take more than 10 minutes? If yes, inject.
|
|
37
|
+
|
|
38
|
+
## Instructions
|
|
39
|
+
|
|
40
|
+
1. **Classify** each pending capture using the criteria above.
|
|
41
|
+
|
|
42
|
+
2. **Present** your classifications to the user using `ask_user_questions`. For each capture, show:
|
|
43
|
+
- The capture text
|
|
44
|
+
- Your proposed classification
|
|
45
|
+
- Your rationale
|
|
46
|
+
- If applicable, which files would be affected
|
|
47
|
+
|
|
48
|
+
For captures classified as **note** or **defer**, auto-confirm without asking — these are low-impact.
|
|
49
|
+
For captures classified as **quick-task**, **inject**, or **replan**, ask the user to confirm or choose a different classification.
|
|
50
|
+
|
|
51
|
+
3. **Update** `.gsd/CAPTURES.md` — for each capture, update its section with the confirmed classification:
|
|
52
|
+
- Change `**Status:** pending` to `**Status:** resolved`
|
|
53
|
+
- Add `**Classification:** <type>`
|
|
54
|
+
- Add `**Resolution:** <brief description of what will happen>`
|
|
55
|
+
- Add `**Rationale:** <why this classification>`
|
|
56
|
+
- Add `**Resolved:** <current ISO timestamp>`
|
|
57
|
+
|
|
58
|
+
4. **Summarize** what was triaged: how many captures, what classifications were assigned, and what actions are pending (e.g., "2 quick-tasks ready for execution, 1 deferred to S03").
|
|
59
|
+
|
|
60
|
+
**Important:** Do NOT execute any resolutions. Only classify and update CAPTURES.md. Resolution execution happens separately (in auto-mode dispatch or manually by the user).
|
|
61
|
+
|
|
62
|
+
When done, say: "Triage complete."
|