gsd-pi 2.17.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/README.md +39 -0
- package/dist/onboarding.js +2 -2
- package/dist/remote-questions-config.d.ts +10 -0
- package/dist/remote-questions-config.js +36 -0
- package/dist/resources/extensions/gsd/activity-log.ts +37 -7
- package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +65 -16
- package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/dist/resources/extensions/gsd/auto.ts +399 -29
- package/dist/resources/extensions/gsd/captures.ts +384 -0
- package/dist/resources/extensions/gsd/commands.ts +382 -23
- 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/dispatch-guard.ts +7 -19
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/dist/resources/extensions/gsd/files.ts +123 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
- package/dist/resources/extensions/gsd/index.ts +47 -3
- 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/paths.ts +9 -0
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +132 -1
- package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
- 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/system.md +2 -0
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/dist/resources/extensions/gsd/queue-order.ts +231 -0
- package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/dist/resources/extensions/gsd/state.ts +15 -3
- package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -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/derive-state-deps.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -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/preferences-wizard-fields.test.ts +168 -0
- package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -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/stale-worktree-cwd.test.ts +139 -0
- 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/gsd/worktree-manager.ts +8 -5
- package/dist/resources/extensions/gsd/worktree.ts +22 -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/dist/resources/extensions/shared/next-action-ui.ts +16 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +21 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
- package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +5 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -2
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +21 -0
- package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
- package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
- package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +5 -0
- package/packages/pi-coding-agent/src/main.ts +19 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
- package/src/resources/extensions/gsd/activity-log.ts +37 -7
- package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +65 -16
- package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/src/resources/extensions/gsd/auto.ts +399 -29
- package/src/resources/extensions/gsd/captures.ts +384 -0
- package/src/resources/extensions/gsd/commands.ts +382 -23
- 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/dispatch-guard.ts +7 -19
- package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/src/resources/extensions/gsd/files.ts +123 -1
- package/src/resources/extensions/gsd/guided-flow.ts +237 -4
- package/src/resources/extensions/gsd/index.ts +47 -3
- 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/paths.ts +9 -0
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +132 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
- 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/system.md +2 -0
- package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/src/resources/extensions/gsd/queue-order.ts +231 -0
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/src/resources/extensions/gsd/state.ts +15 -3
- package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/src/resources/extensions/gsd/templates/preferences.md +14 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -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/derive-state-deps.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -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/preferences-wizard-fields.test.ts +168 -0
- package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -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/stale-worktree-cwd.test.ts +139 -0
- 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/gsd/worktree-manager.ts +8 -5
- package/src/resources/extensions/gsd/worktree.ts +22 -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
- package/src/resources/extensions/shared/next-action-ui.ts +16 -1
|
@@ -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
|
+
}
|
|
@@ -15,6 +15,9 @@ import { nativeScanGsdTree, type GsdTreeEntry } from "./native-parser-bridge.js"
|
|
|
15
15
|
|
|
16
16
|
// ─── Directory Listing Cache ──────────────────────────────────────────────────
|
|
17
17
|
|
|
18
|
+
/** Max entries before eviction. Prevents unbounded growth in long sessions (#611). */
|
|
19
|
+
const DIR_CACHE_MAX = 200;
|
|
20
|
+
|
|
18
21
|
const dirEntryCache = new Map<string, Dirent[]>();
|
|
19
22
|
const dirListCache = new Map<string, string[]>();
|
|
20
23
|
|
|
@@ -85,6 +88,7 @@ function cachedReaddirWithTypes(dirPath: string): Dirent[] {
|
|
|
85
88
|
d.isSocket = () => false;
|
|
86
89
|
return d;
|
|
87
90
|
});
|
|
91
|
+
if (dirEntryCache.size >= DIR_CACHE_MAX) dirEntryCache.clear();
|
|
88
92
|
dirEntryCache.set(dirPath, dirents);
|
|
89
93
|
return dirents;
|
|
90
94
|
}
|
|
@@ -92,6 +96,7 @@ function cachedReaddirWithTypes(dirPath: string): Dirent[] {
|
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
99
|
+
if (dirEntryCache.size >= DIR_CACHE_MAX) dirEntryCache.clear();
|
|
95
100
|
dirEntryCache.set(dirPath, entries);
|
|
96
101
|
return entries;
|
|
97
102
|
}
|
|
@@ -107,6 +112,7 @@ function cachedReaddir(dirPath: string): string[] {
|
|
|
107
112
|
const treeEntries = nativeTreeCache.get(key);
|
|
108
113
|
if (treeEntries) {
|
|
109
114
|
const names = treeEntries.map(e => e.name);
|
|
115
|
+
if (dirListCache.size >= DIR_CACHE_MAX) dirListCache.clear();
|
|
110
116
|
dirListCache.set(dirPath, names);
|
|
111
117
|
return names;
|
|
112
118
|
}
|
|
@@ -114,6 +120,7 @@ function cachedReaddir(dirPath: string): string[] {
|
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
const entries = readdirSync(dirPath);
|
|
123
|
+
if (dirListCache.size >= DIR_CACHE_MAX) dirListCache.clear();
|
|
117
124
|
dirListCache.set(dirPath, entries);
|
|
118
125
|
return entries;
|
|
119
126
|
}
|
|
@@ -248,6 +255,7 @@ export const GSD_ROOT_FILES = {
|
|
|
248
255
|
STATE: "STATE.md",
|
|
249
256
|
REQUIREMENTS: "REQUIREMENTS.md",
|
|
250
257
|
OVERRIDES: "OVERRIDES.md",
|
|
258
|
+
KNOWLEDGE: "KNOWLEDGE.md",
|
|
251
259
|
} as const;
|
|
252
260
|
|
|
253
261
|
export type GSDRootFileKey = keyof typeof GSD_ROOT_FILES;
|
|
@@ -259,6 +267,7 @@ const LEGACY_GSD_ROOT_FILES: Record<GSDRootFileKey, string> = {
|
|
|
259
267
|
STATE: "state.md",
|
|
260
268
|
REQUIREMENTS: "requirements.md",
|
|
261
269
|
OVERRIDES: "overrides.md",
|
|
270
|
+
KNOWLEDGE: "knowledge.md",
|
|
262
271
|
};
|
|
263
272
|
|
|
264
273
|
export function gsdRoot(basePath: string): string {
|
|
@@ -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 =>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
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> = {};
|
|
@@ -1252,3 +1325,61 @@ export function resolvePreDispatchHooks(): PreDispatchHookConfig[] {
|
|
|
1252
1325
|
return (prefs?.preferences.pre_dispatch_hooks ?? [])
|
|
1253
1326
|
.filter(h => h.enabled !== false);
|
|
1254
1327
|
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Validate a model ID string.
|
|
1331
|
+
* Returns true if the ID looks like a valid model identifier.
|
|
1332
|
+
*/
|
|
1333
|
+
export function validateModelId(modelId: string): boolean {
|
|
1334
|
+
if (!modelId || typeof modelId !== "string") return false;
|
|
1335
|
+
const trimmed = modelId.trim();
|
|
1336
|
+
if (trimmed.length === 0 || trimmed.length > 256) return false;
|
|
1337
|
+
// Allow alphanumeric, hyphens, underscores, dots, slashes, colons
|
|
1338
|
+
return /^[a-zA-Z0-9\-_./:]+$/.test(trimmed);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* Update the models section of the global GSD preferences file.
|
|
1343
|
+
* Performs a safe read-modify-write: reads current content, updates the models
|
|
1344
|
+
* YAML block, and writes back. Creates the file if it doesn't exist.
|
|
1345
|
+
*/
|
|
1346
|
+
export function updatePreferencesModels(models: GSDModelConfigV2): void {
|
|
1347
|
+
const prefsPath = getGlobalGSDPreferencesPath();
|
|
1348
|
+
|
|
1349
|
+
let content = "";
|
|
1350
|
+
if (existsSync(prefsPath)) {
|
|
1351
|
+
content = readFileSync(prefsPath, "utf-8");
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Build the new models block
|
|
1355
|
+
const lines: string[] = ["models:"];
|
|
1356
|
+
for (const [phase, value] of Object.entries(models)) {
|
|
1357
|
+
if (typeof value === "string") {
|
|
1358
|
+
lines.push(` ${phase}: ${value}`);
|
|
1359
|
+
} else if (value && typeof value === "object") {
|
|
1360
|
+
const config = value as GSDPhaseModelConfig;
|
|
1361
|
+
lines.push(` ${phase}:`);
|
|
1362
|
+
lines.push(` model: ${config.model}`);
|
|
1363
|
+
if (config.provider) {
|
|
1364
|
+
lines.push(` provider: ${config.provider}`);
|
|
1365
|
+
}
|
|
1366
|
+
if (config.fallbacks && config.fallbacks.length > 0) {
|
|
1367
|
+
lines.push(` fallbacks:`);
|
|
1368
|
+
for (const fb of config.fallbacks) {
|
|
1369
|
+
lines.push(` - ${fb}`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
const modelsBlock = lines.join("\n");
|
|
1375
|
+
|
|
1376
|
+
// Replace existing models block or append
|
|
1377
|
+
const modelsRegex = /^models:[\s\S]*?(?=\n[a-z_]|\n*$)/m;
|
|
1378
|
+
if (modelsRegex.test(content)) {
|
|
1379
|
+
content = content.replace(modelsRegex, modelsBlock);
|
|
1380
|
+
} else {
|
|
1381
|
+
content = content.trimEnd() + "\n\n" + modelsBlock + "\n";
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
writeFileSync(prefsPath, content, "utf-8");
|
|
1385
|
+
}
|
|
@@ -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
|
*
|
|
@@ -54,11 +54,12 @@ Then:
|
|
|
54
54
|
- Don't fix symptoms. Understand *why* something fails before changing code. A test that passes after a change you don't understand is luck, not a fix.
|
|
55
55
|
11. **Blocker discovery:** If execution reveals that the remaining slice plan is fundamentally invalid — not just a bug or minor deviation, but a plan-invalidating finding like a wrong API, missing capability, or architectural mismatch — set `blocker_discovered: true` in the task summary frontmatter and describe the blocker clearly in the summary narrative. Do NOT set `blocker_discovered: true` for ordinary debugging, minor deviations, or issues that can be fixed within the current task or the remaining plan. This flag triggers an automatic replan of the slice.
|
|
56
56
|
12. If you made an architectural, pattern, library, or observability decision during this task that downstream work should know about, append it to `.gsd/DECISIONS.md` (use the **Decisions** output template from the inlined templates below if the file doesn't exist yet). Not every task produces decisions — only append when a meaningful choice was made.
|
|
57
|
-
13.
|
|
58
|
-
14.
|
|
59
|
-
15.
|
|
60
|
-
16.
|
|
61
|
-
17.
|
|
57
|
+
13. If you discover a non-obvious rule, recurring gotcha, or useful pattern during execution, append it to `.gsd/KNOWLEDGE.md`. Only add entries that would save future agents from repeating your investigation. Don't add obvious things.
|
|
58
|
+
14. Use the **Task Summary** output template from the inlined templates below
|
|
59
|
+
15. Write `{{taskSummaryPath}}`
|
|
60
|
+
16. Mark {{taskId}} done in `{{planPath}}` (change `[ ]` to `[x]`)
|
|
61
|
+
17. Do not commit manually — the system auto-commits your changes after this unit completes.
|
|
62
|
+
18. Update `.gsd/STATE.md`
|
|
62
63
|
|
|
63
64
|
All work stays in your working directory: `{{workingDirectory}}`.
|
|
64
65
|
|
|
@@ -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.
|
|
@@ -65,6 +65,7 @@ Titles live inside file content (headings, frontmatter), not in file or director
|
|
|
65
65
|
PROJECT.md (living doc - what the project is right now)
|
|
66
66
|
REQUIREMENTS.md (requirement contract - tracks active/validated/deferred/out-of-scope)
|
|
67
67
|
DECISIONS.md (append-only register of architectural and pattern decisions)
|
|
68
|
+
KNOWLEDGE.md (append-only register of project-specific rules, patterns, and lessons learned)
|
|
68
69
|
OVERRIDES.md (user-issued overrides that supersede plan content via /gsd steer)
|
|
69
70
|
QUEUE.md (append-only log of queued milestones via /gsd queue)
|
|
70
71
|
STATE.md
|
|
@@ -100,6 +101,7 @@ All auto-mode work happens inside a worktree at `.gsd/worktrees/<MID>/`. This is
|
|
|
100
101
|
- **PROJECT.md** is a living document describing what the project is right now - current state only, updated at slice completion when stale
|
|
101
102
|
- **REQUIREMENTS.md** tracks the requirement contract — requirements move between Active, Validated, Deferred, Blocked, and Out of Scope as slices prove or invalidate them. Update at slice completion when evidence supports a status change.
|
|
102
103
|
- **DECISIONS.md** is an append-only register of architectural and pattern decisions - read it during planning/research, append to it during execution when a meaningful decision is made
|
|
104
|
+
- **KNOWLEDGE.md** is an append-only register of project-specific rules, patterns, and lessons learned. Read it at the start of every unit. Append to it when you discover a recurring issue, a non-obvious pattern, or a rule that future agents should follow.
|
|
103
105
|
- **CONTEXT.md** files (milestone or slice level) capture the brief — scope, goals, constraints, and key decisions from discussion. When present, they are the authoritative source for what a milestone or slice is trying to achieve. Read them before planning or executing.
|
|
104
106
|
- **Milestones** are major project phases (M001, M002, ...)
|
|
105
107
|
- **Slices** are demoable vertical increments (S01, S02, ...) ordered by risk. After each slice completes, the roadmap is reassessed before the next slice begins.
|