gsd-pi 2.59.0-dev.d77b3dd → 2.60.0-dev.2580e65
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/ask-user-questions.js +7 -4
- package/dist/resources/extensions/gsd/auto/phases.js +15 -7
- package/dist/resources/extensions/gsd/auto-dashboard.js +21 -8
- package/dist/resources/extensions/gsd/auto-dispatch.js +6 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +58 -9
- package/dist/resources/extensions/gsd/auto-post-unit.js +3 -2
- package/dist/resources/extensions/gsd/auto-prompts.js +36 -20
- package/dist/resources/extensions/gsd/auto-recovery.js +37 -18
- package/dist/resources/extensions/gsd/auto-start.js +9 -5
- package/dist/resources/extensions/gsd/auto-timers.js +11 -5
- package/dist/resources/extensions/gsd/auto-unit-closeout.js +5 -3
- package/dist/resources/extensions/gsd/auto-verification.js +3 -2
- package/dist/resources/extensions/gsd/auto-worktree.js +120 -55
- package/dist/resources/extensions/gsd/auto.js +39 -17
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +6 -3
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +2 -2
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +4 -10
- package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +2 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +7 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +11 -10
- package/dist/resources/extensions/gsd/commands/catalog.js +2 -0
- package/dist/resources/extensions/gsd/commands-codebase.js +48 -21
- package/dist/resources/extensions/gsd/commands-inspect.js +2 -1
- package/dist/resources/extensions/gsd/commands-maintenance.js +32 -19
- package/dist/resources/extensions/gsd/complexity-classifier.js +8 -4
- package/dist/resources/extensions/gsd/custom-verification.js +3 -2
- package/dist/resources/extensions/gsd/gsd-db.js +33 -13
- package/dist/resources/extensions/gsd/guided-flow.js +19 -9
- package/dist/resources/extensions/gsd/init-wizard.js +12 -0
- package/dist/resources/extensions/gsd/markdown-renderer.js +11 -9
- package/dist/resources/extensions/gsd/md-importer.js +5 -4
- package/dist/resources/extensions/gsd/milestone-actions.js +3 -2
- package/dist/resources/extensions/gsd/milestone-ids.js +2 -1
- package/dist/resources/extensions/gsd/model-router.js +156 -121
- package/dist/resources/extensions/gsd/parallel-merge.js +5 -3
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +26 -14
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +45 -0
- package/dist/resources/extensions/gsd/preferences.js +15 -3
- package/dist/resources/extensions/gsd/prompt-loader.js +3 -2
- package/dist/resources/extensions/gsd/prompts/rethink.md +1 -1
- package/dist/resources/extensions/gsd/rule-registry.js +7 -6
- package/dist/resources/extensions/gsd/safe-fs.js +6 -8
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -2
- package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -2
- package/dist/resources/extensions/gsd/tools/complete-task.js +3 -2
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +3 -2
- package/dist/resources/extensions/gsd/tools/plan-slice.js +3 -2
- package/dist/resources/extensions/gsd/tools/plan-task.js +2 -1
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +4 -4
- package/dist/resources/extensions/gsd/tools/reopen-slice.js +2 -1
- package/dist/resources/extensions/gsd/tools/reopen-task.js +2 -1
- package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -1
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +2 -1
- package/dist/resources/extensions/gsd/triage-resolution.js +11 -4
- package/dist/resources/extensions/gsd/workflow-events.js +2 -1
- package/dist/resources/extensions/gsd/workflow-logger.js +37 -4
- package/dist/resources/extensions/gsd/workflow-migration.js +14 -12
- package/dist/resources/extensions/gsd/workflow-projections.js +2 -2
- package/dist/resources/extensions/gsd/workflow-reconcile.js +2 -2
- package/dist/resources/extensions/gsd/worktree-manager.js +26 -14
- package/dist/resources/extensions/shared/interview-ui.js +3 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +19 -19
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +19 -19
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +16 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +26 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.js +6 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/defaults.json +2 -2
- package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.js +47 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.js.map +1 -0
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +19 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +26 -0
- package/packages/pi-coding-agent/src/core/lsp/config.ts +7 -1
- package/packages/pi-coding-agent/src/core/lsp/defaults.json +2 -2
- package/packages/pi-coding-agent/src/core/lsp/lsp-legacy-alias.test.ts +70 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/ask-user-questions.ts +7 -3
- package/src/resources/extensions/gsd/auto/phases.ts +17 -7
- package/src/resources/extensions/gsd/auto-dashboard.ts +22 -8
- package/src/resources/extensions/gsd/auto-dispatch.ts +7 -3
- package/src/resources/extensions/gsd/auto-model-selection.ts +77 -15
- package/src/resources/extensions/gsd/auto-post-unit.ts +4 -4
- package/src/resources/extensions/gsd/auto-prompts.ts +37 -20
- package/src/resources/extensions/gsd/auto-recovery.ts +38 -18
- package/src/resources/extensions/gsd/auto-start.ts +10 -9
- package/src/resources/extensions/gsd/auto-timers.ts +12 -5
- package/src/resources/extensions/gsd/auto-unit-closeout.ts +6 -2
- package/src/resources/extensions/gsd/auto-verification.ts +3 -6
- package/src/resources/extensions/gsd/auto-worktree.ts +121 -55
- package/src/resources/extensions/gsd/auto.ts +40 -17
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +4 -3
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +2 -2
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +4 -16
- package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +2 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +8 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +11 -10
- package/src/resources/extensions/gsd/commands/catalog.ts +2 -0
- package/src/resources/extensions/gsd/commands-codebase.ts +52 -20
- package/src/resources/extensions/gsd/commands-inspect.ts +2 -1
- package/src/resources/extensions/gsd/commands-maintenance.ts +28 -19
- package/src/resources/extensions/gsd/complexity-classifier.ts +9 -4
- package/src/resources/extensions/gsd/custom-verification.ts +3 -2
- package/src/resources/extensions/gsd/gsd-db.ts +12 -14
- package/src/resources/extensions/gsd/guided-flow.ts +9 -8
- package/src/resources/extensions/gsd/init-wizard.ts +12 -0
- package/src/resources/extensions/gsd/markdown-renderer.ts +11 -17
- package/src/resources/extensions/gsd/md-importer.ts +5 -4
- package/src/resources/extensions/gsd/milestone-actions.ts +3 -2
- package/src/resources/extensions/gsd/milestone-ids.ts +2 -1
- package/src/resources/extensions/gsd/model-router.ts +199 -173
- package/src/resources/extensions/gsd/parallel-merge.ts +5 -3
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +18 -14
- package/src/resources/extensions/gsd/preferences-types.ts +13 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +45 -0
- package/src/resources/extensions/gsd/preferences.ts +16 -3
- package/src/resources/extensions/gsd/prompt-loader.ts +3 -2
- package/src/resources/extensions/gsd/prompts/rethink.md +1 -1
- package/src/resources/extensions/gsd/rule-registry.ts +7 -6
- package/src/resources/extensions/gsd/safe-fs.ts +6 -5
- package/src/resources/extensions/gsd/tests/capability-router.test.ts +347 -0
- package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +27 -2
- package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +1188 -0
- package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +841 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +403 -3
- package/src/resources/extensions/gsd/tests/preferences.test.ts +62 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/silent-catch-diagnostics.test.ts +284 -0
- package/src/resources/extensions/gsd/tests/workflow-logger-audit.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +6 -6
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -6
- package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -6
- package/src/resources/extensions/gsd/tools/complete-task.ts +3 -6
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +3 -6
- package/src/resources/extensions/gsd/tools/plan-slice.ts +3 -6
- package/src/resources/extensions/gsd/tools/plan-task.ts +2 -3
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +4 -6
- package/src/resources/extensions/gsd/tools/reopen-slice.ts +2 -3
- package/src/resources/extensions/gsd/tools/reopen-task.ts +2 -3
- package/src/resources/extensions/gsd/tools/replan-slice.ts +2 -3
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +2 -3
- package/src/resources/extensions/gsd/triage-resolution.ts +11 -4
- package/src/resources/extensions/gsd/types.ts +1 -0
- package/src/resources/extensions/gsd/workflow-events.ts +2 -1
- package/src/resources/extensions/gsd/workflow-logger.ts +52 -5
- package/src/resources/extensions/gsd/workflow-migration.ts +14 -12
- package/src/resources/extensions/gsd/workflow-projections.ts +2 -2
- package/src/resources/extensions/gsd/workflow-reconcile.ts +2 -2
- package/src/resources/extensions/gsd/worktree-manager.ts +16 -14
- package/src/resources/extensions/shared/interview-ui.ts +3 -1
- package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +144 -0
- /package/dist/web/standalone/.next/static/{t_cBZAENjaOJIRST3dw08 → ogyMN7M-3bGGuRY08L5HR}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{t_cBZAENjaOJIRST3dw08 → ogyMN7M-3bGGuRY08L5HR}/_ssgManifest.js +0 -0
|
@@ -76,6 +76,9 @@ const MODEL_COST_PER_1K_INPUT = {
|
|
|
76
76
|
"gemini-2.5-pro": 0.00125,
|
|
77
77
|
"deepseek-chat": 0.00014,
|
|
78
78
|
};
|
|
79
|
+
// ─── Capability Profiles Data Table ──────────────────────────────────────────
|
|
80
|
+
// Per-model capability profiles (0–100 scale). Used for capability-aware
|
|
81
|
+
// model selection within an eligible tier set.
|
|
79
82
|
export const MODEL_CAPABILITY_PROFILES = {
|
|
80
83
|
"claude-opus-4-6": { coding: 95, debugging: 90, research: 85, reasoning: 95, speed: 30, longContext: 80, instruction: 90 },
|
|
81
84
|
"claude-sonnet-4-6": { coding: 85, debugging: 80, research: 75, reasoning: 80, speed: 60, longContext: 75, instruction: 85 },
|
|
@@ -87,7 +90,10 @@ export const MODEL_CAPABILITY_PROFILES = {
|
|
|
87
90
|
"deepseek-chat": { coding: 75, debugging: 65, research: 55, reasoning: 70, speed: 70, longContext: 55, instruction: 65 },
|
|
88
91
|
"o3": { coding: 80, debugging: 85, research: 80, reasoning: 92, speed: 25, longContext: 70, instruction: 85 },
|
|
89
92
|
};
|
|
90
|
-
|
|
93
|
+
// ─── Base Task Requirements Data Table ───────────────────────────────────────
|
|
94
|
+
// Per-unit-type base requirement vectors. Weights indicate how important each
|
|
95
|
+
// capability dimension is for this unit type.
|
|
96
|
+
export const BASE_REQUIREMENTS = {
|
|
91
97
|
"execute-task": { coding: 0.9, instruction: 0.7, speed: 0.3 },
|
|
92
98
|
"research-milestone": { research: 0.9, longContext: 0.7, reasoning: 0.5 },
|
|
93
99
|
"research-slice": { research: 0.9, longContext: 0.7, reasoning: 0.5 },
|
|
@@ -100,11 +106,28 @@ const BASE_REQUIREMENTS = {
|
|
|
100
106
|
"discuss-milestone": { reasoning: 0.6, instruction: 0.7 },
|
|
101
107
|
"complete-milestone": { instruction: 0.8, reasoning: 0.5 },
|
|
102
108
|
};
|
|
109
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
110
|
+
/**
|
|
111
|
+
* Score a model's suitability for a task given a requirement vector.
|
|
112
|
+
* Returns a weighted average of capability dimensions (0–100).
|
|
113
|
+
* Returns 50 if requirements are empty (neutral score).
|
|
114
|
+
*/
|
|
115
|
+
export function scoreModel(model, requirements) {
|
|
116
|
+
let weightedSum = 0;
|
|
117
|
+
let weightSum = 0;
|
|
118
|
+
for (const [dim, weight] of Object.entries(requirements)) {
|
|
119
|
+
const capability = model[dim] ?? 50;
|
|
120
|
+
weightedSum += weight * capability;
|
|
121
|
+
weightSum += weight;
|
|
122
|
+
}
|
|
123
|
+
return weightSum > 0 ? weightedSum / weightSum : 50;
|
|
124
|
+
}
|
|
103
125
|
/**
|
|
104
|
-
* Compute
|
|
126
|
+
* Compute dynamic task requirements from unit type and optional task metadata.
|
|
127
|
+
* Returns a requirement vector refined by task-specific signals.
|
|
105
128
|
*/
|
|
106
129
|
export function computeTaskRequirements(unitType, metadata) {
|
|
107
|
-
const base =
|
|
130
|
+
const base = BASE_REQUIREMENTS[unitType] ?? { reasoning: 0.5 };
|
|
108
131
|
if (unitType === "execute-task" && metadata) {
|
|
109
132
|
if (metadata.tags?.some(t => /^(docs?|readme|comment|config|typo|rename)$/i.test(t))) {
|
|
110
133
|
return { ...base, instruction: 0.9, coding: 0.3, speed: 0.7 };
|
|
@@ -122,32 +145,108 @@ export function computeTaskRequirements(unitType, metadata) {
|
|
|
122
145
|
return base;
|
|
123
146
|
}
|
|
124
147
|
/**
|
|
125
|
-
* Score
|
|
126
|
-
*
|
|
148
|
+
* Score all eligible models against a requirement vector and return them
|
|
149
|
+
* sorted by score descending. Within 2 points: prefer cheaper; equal cost:
|
|
150
|
+
* lexicographic tie-break by model ID.
|
|
127
151
|
*/
|
|
128
|
-
export function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
152
|
+
export function scoreEligibleModels(eligibleModelIds, requirements, capabilityOverrides) {
|
|
153
|
+
const scored = eligibleModelIds.map(modelId => {
|
|
154
|
+
const builtin = MODEL_CAPABILITY_PROFILES[modelId];
|
|
155
|
+
const override = capabilityOverrides?.[modelId];
|
|
156
|
+
const profile = builtin
|
|
157
|
+
? override ? { ...builtin, ...override } : builtin
|
|
158
|
+
: { coding: 50, debugging: 50, research: 50, reasoning: 50, speed: 50, longContext: 50, instruction: 50 };
|
|
159
|
+
return { modelId, score: scoreModel(profile, requirements) };
|
|
160
|
+
});
|
|
161
|
+
scored.sort((a, b) => {
|
|
162
|
+
const scoreDiff = b.score - a.score;
|
|
163
|
+
if (Math.abs(scoreDiff) > 2)
|
|
164
|
+
return scoreDiff;
|
|
165
|
+
const costA = MODEL_COST_PER_1K_INPUT[a.modelId] ?? Infinity;
|
|
166
|
+
const costB = MODEL_COST_PER_1K_INPUT[b.modelId] ?? Infinity;
|
|
167
|
+
if (costA !== costB)
|
|
168
|
+
return costA - costB;
|
|
169
|
+
return a.modelId.localeCompare(b.modelId);
|
|
170
|
+
});
|
|
171
|
+
return scored;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Return all models eligible for a given tier, sorted cheapest first.
|
|
175
|
+
* If routingConfig.tier_models[tier] is set and available, returns only that
|
|
176
|
+
* model. Otherwise filters availableModelIds by tier from MODEL_CAPABILITY_TIER.
|
|
177
|
+
*/
|
|
178
|
+
export function getEligibleModels(tier, availableModelIds, routingConfig) {
|
|
179
|
+
// 1. Check explicit tier_models config
|
|
180
|
+
const explicitModel = routingConfig.tier_models?.[tier];
|
|
181
|
+
if (explicitModel) {
|
|
182
|
+
// Exact match
|
|
183
|
+
if (availableModelIds.includes(explicitModel))
|
|
184
|
+
return [explicitModel];
|
|
185
|
+
// Provider-prefix-stripped match
|
|
186
|
+
const match = availableModelIds.find(id => {
|
|
187
|
+
const bareAvail = id.includes("/") ? id.split("/").pop() : id;
|
|
188
|
+
const bareExplicit = explicitModel.includes("/") ? explicitModel.split("/").pop() : explicitModel;
|
|
189
|
+
return bareAvail === bareExplicit;
|
|
190
|
+
});
|
|
191
|
+
if (match)
|
|
192
|
+
return [match];
|
|
135
193
|
}
|
|
136
|
-
|
|
194
|
+
// 2. Auto-detect: filter by tier, sort cheapest first
|
|
195
|
+
return availableModelIds
|
|
196
|
+
.filter(id => getModelTier(id) === tier)
|
|
197
|
+
.sort((a, b) => {
|
|
198
|
+
const costA = getModelCost(a);
|
|
199
|
+
const costB = getModelCost(b);
|
|
200
|
+
return costA - costB;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Build a fallback chain for a selected model: [selectedModel, ...configuredFallbacks, configuredPrimary]
|
|
205
|
+
* Deduplicates entries while preserving order.
|
|
206
|
+
*/
|
|
207
|
+
function buildFallbackChain(selectedModelId, phaseConfig) {
|
|
208
|
+
return [
|
|
209
|
+
...phaseConfig.fallbacks.filter(f => f !== selectedModelId),
|
|
210
|
+
phaseConfig.primary,
|
|
211
|
+
].filter(f => f !== selectedModelId);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Load capability overrides from user preferences' modelOverrides section.
|
|
215
|
+
* Returns a map of model ID → partial capability overrides to deep-merge with built-in profiles.
|
|
216
|
+
*
|
|
217
|
+
* Per D-17: partial capability overrides via models.json modelOverrides, deep-merged with defaults.
|
|
218
|
+
*/
|
|
219
|
+
export function loadCapabilityOverrides(prefs) {
|
|
220
|
+
const result = {};
|
|
221
|
+
if (!prefs.modelOverrides)
|
|
222
|
+
return result;
|
|
223
|
+
for (const [modelId, overrideEntry] of Object.entries(prefs.modelOverrides)) {
|
|
224
|
+
if (overrideEntry.capabilities) {
|
|
225
|
+
result[modelId] = overrideEntry.capabilities;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
137
229
|
}
|
|
138
|
-
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
139
230
|
/**
|
|
140
231
|
* Resolve the model to use for a given complexity tier.
|
|
141
232
|
*
|
|
142
233
|
* Downgrade-only: the returned model is always equal to or cheaper than
|
|
143
234
|
* the user's configured primary model. Never upgrades beyond configuration.
|
|
144
235
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
236
|
+
* STEP 1: Filter to eligible models for the requested tier.
|
|
237
|
+
* STEP 2: Capability scoring — ranks eligible models by task-capability match
|
|
238
|
+
* when capability_routing is enabled and multiple eligible models exist.
|
|
239
|
+
* STEP 3: Fallback chain assembly.
|
|
240
|
+
*
|
|
241
|
+
* @param classification The complexity classification result
|
|
242
|
+
* @param phaseConfig The user's configured model for this phase (ceiling)
|
|
243
|
+
* @param routingConfig Dynamic routing configuration
|
|
244
|
+
* @param availableModelIds List of available model IDs (from registry)
|
|
245
|
+
* @param unitType The unit type for capability requirement computation (optional)
|
|
246
|
+
* @param taskMetadata Task metadata for refined requirement vectors (optional)
|
|
247
|
+
* @param capabilityOverrides User-provided capability overrides (deep-merged with built-in profiles, optional)
|
|
149
248
|
*/
|
|
150
|
-
export function resolveModelForComplexity(classification, phaseConfig, routingConfig, availableModelIds, unitType,
|
|
249
|
+
export function resolveModelForComplexity(classification, phaseConfig, routingConfig, availableModelIds, unitType, taskMetadata, capabilityOverrides) {
|
|
151
250
|
// If no phase config or routing disabled, pass through
|
|
152
251
|
if (!phaseConfig || !routingConfig.enabled) {
|
|
153
252
|
return {
|
|
@@ -156,6 +255,7 @@ export function resolveModelForComplexity(classification, phaseConfig, routingCo
|
|
|
156
255
|
tier: classification.tier,
|
|
157
256
|
wasDowngraded: false,
|
|
158
257
|
reason: "dynamic routing disabled or no phase config",
|
|
258
|
+
selectionMethod: "tier-only",
|
|
159
259
|
};
|
|
160
260
|
}
|
|
161
261
|
const configuredPrimary = phaseConfig.primary;
|
|
@@ -173,6 +273,7 @@ export function resolveModelForComplexity(classification, phaseConfig, routingCo
|
|
|
173
273
|
tier: requestedTier,
|
|
174
274
|
wasDowngraded: false,
|
|
175
275
|
reason: `configured model "${configuredPrimary}" is not in the known tier map — honoring explicit config`,
|
|
276
|
+
selectionMethod: "tier-only",
|
|
176
277
|
};
|
|
177
278
|
}
|
|
178
279
|
// Downgrade-only: if requested tier >= configured tier, no change
|
|
@@ -183,47 +284,55 @@ export function resolveModelForComplexity(classification, phaseConfig, routingCo
|
|
|
183
284
|
tier: requestedTier,
|
|
184
285
|
wasDowngraded: false,
|
|
185
286
|
reason: `tier ${requestedTier} >= configured ${configuredTier}`,
|
|
287
|
+
selectionMethod: "tier-only",
|
|
186
288
|
};
|
|
187
289
|
}
|
|
188
|
-
//
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
let taskRequirements;
|
|
193
|
-
let selectionMethod = "tier-only";
|
|
194
|
-
if (useCapabilityScoring) {
|
|
195
|
-
const result = findModelForTierWithCapability(requestedTier, routingConfig, availableModelIds, routingConfig.cross_provider !== false, unitType, metadata);
|
|
196
|
-
targetModelId = result.modelId;
|
|
197
|
-
capabilityScores = Object.keys(result.scores).length > 0 ? result.scores : undefined;
|
|
198
|
-
taskRequirements = Object.keys(result.requirements).length > 0 ? result.requirements : undefined;
|
|
199
|
-
selectionMethod = capabilityScores ? "capability-scored" : "tier-only";
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
targetModelId = findModelForTier(requestedTier, routingConfig, availableModelIds, routingConfig.cross_provider !== false);
|
|
203
|
-
}
|
|
204
|
-
if (!targetModelId) {
|
|
290
|
+
// STEP 1: Get all eligible models for the requested tier
|
|
291
|
+
const eligible = getEligibleModels(requestedTier, availableModelIds, routingConfig);
|
|
292
|
+
if (eligible.length === 0) {
|
|
293
|
+
// No suitable model found — use configured primary
|
|
205
294
|
return {
|
|
206
295
|
modelId: configuredPrimary,
|
|
207
296
|
fallbacks: phaseConfig.fallbacks,
|
|
208
297
|
tier: requestedTier,
|
|
209
298
|
wasDowngraded: false,
|
|
210
299
|
reason: `no ${requestedTier}-tier model available`,
|
|
211
|
-
selectionMethod,
|
|
300
|
+
selectionMethod: "tier-only",
|
|
212
301
|
};
|
|
213
302
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
303
|
+
// STEP 2: Capability scoring (when enabled and multiple eligible models exist)
|
|
304
|
+
if (routingConfig.capability_routing !== false && eligible.length > 1 && unitType) {
|
|
305
|
+
const requirements = computeTaskRequirements(unitType, taskMetadata);
|
|
306
|
+
const scored = scoreEligibleModels(eligible, requirements, capabilityOverrides);
|
|
307
|
+
const winner = scored[0];
|
|
308
|
+
if (winner) {
|
|
309
|
+
const capScores = {};
|
|
310
|
+
for (const s of scored)
|
|
311
|
+
capScores[s.modelId] = s.score;
|
|
312
|
+
const fallbacks = buildFallbackChain(winner.modelId, phaseConfig);
|
|
313
|
+
return {
|
|
314
|
+
modelId: winner.modelId,
|
|
315
|
+
fallbacks,
|
|
316
|
+
tier: requestedTier,
|
|
317
|
+
wasDowngraded: true,
|
|
318
|
+
reason: `capability-scored: ${winner.modelId} (${winner.score.toFixed(1)}) for ${unitType}`,
|
|
319
|
+
capabilityScores: capScores,
|
|
320
|
+
taskRequirements: requirements,
|
|
321
|
+
selectionMethod: "capability-scored",
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// STEP 3: Fallback — use first eligible model (cheapest in tier, or single eligible)
|
|
326
|
+
const targetModelId = eligible[0];
|
|
327
|
+
// Build fallback chain: [downgraded_model, ...configured_fallbacks, configured_primary]
|
|
328
|
+
const fallbacks = buildFallbackChain(targetModelId, phaseConfig);
|
|
218
329
|
return {
|
|
219
330
|
modelId: targetModelId,
|
|
220
331
|
fallbacks,
|
|
221
332
|
tier: requestedTier,
|
|
222
333
|
wasDowngraded: true,
|
|
223
334
|
reason: classification.reason,
|
|
224
|
-
selectionMethod,
|
|
225
|
-
capabilityScores,
|
|
226
|
-
taskRequirements,
|
|
335
|
+
selectionMethod: "tier-only",
|
|
227
336
|
};
|
|
228
337
|
}
|
|
229
338
|
/**
|
|
@@ -243,7 +352,7 @@ export function escalateTier(currentTier) {
|
|
|
243
352
|
export function defaultRoutingConfig() {
|
|
244
353
|
return {
|
|
245
354
|
enabled: true,
|
|
246
|
-
capability_routing:
|
|
355
|
+
capability_routing: true,
|
|
247
356
|
escalate_on_failure: true,
|
|
248
357
|
budget_pressure: true,
|
|
249
358
|
cross_provider: true,
|
|
@@ -262,8 +371,8 @@ function getModelTier(modelId) {
|
|
|
262
371
|
if (bareId.includes(knownId) || knownId.includes(bareId))
|
|
263
372
|
return tier;
|
|
264
373
|
}
|
|
265
|
-
// Unknown models are assumed
|
|
266
|
-
return "
|
|
374
|
+
// Unknown models are assumed standard (per D-15: avoids silently ignoring user config)
|
|
375
|
+
return "standard";
|
|
267
376
|
}
|
|
268
377
|
/** Check if a model ID has a known capability tier mapping. (#2192) */
|
|
269
378
|
function isKnownModel(modelId) {
|
|
@@ -276,80 +385,6 @@ function isKnownModel(modelId) {
|
|
|
276
385
|
}
|
|
277
386
|
return false;
|
|
278
387
|
}
|
|
279
|
-
function findModelForTier(tier, config, availableModelIds, crossProvider) {
|
|
280
|
-
// 1. Check explicit tier_models config
|
|
281
|
-
const explicitModel = config.tier_models?.[tier];
|
|
282
|
-
if (explicitModel && availableModelIds.includes(explicitModel)) {
|
|
283
|
-
return explicitModel;
|
|
284
|
-
}
|
|
285
|
-
// Also check with provider prefix stripped
|
|
286
|
-
if (explicitModel) {
|
|
287
|
-
const match = availableModelIds.find(id => {
|
|
288
|
-
const bareAvail = id.includes("/") ? id.split("/").pop() : id;
|
|
289
|
-
const bareExplicit = explicitModel.includes("/") ? explicitModel.split("/").pop() : explicitModel;
|
|
290
|
-
return bareAvail === bareExplicit;
|
|
291
|
-
});
|
|
292
|
-
if (match)
|
|
293
|
-
return match;
|
|
294
|
-
}
|
|
295
|
-
// 2. Auto-detect: find the cheapest available model in the requested tier
|
|
296
|
-
const candidates = availableModelIds
|
|
297
|
-
.filter(id => {
|
|
298
|
-
const modelTier = getModelTier(id);
|
|
299
|
-
return modelTier === tier;
|
|
300
|
-
})
|
|
301
|
-
.sort((a, b) => {
|
|
302
|
-
if (!crossProvider)
|
|
303
|
-
return 0;
|
|
304
|
-
const costA = getModelCost(a);
|
|
305
|
-
const costB = getModelCost(b);
|
|
306
|
-
return costA - costB;
|
|
307
|
-
});
|
|
308
|
-
return candidates[0] ?? null;
|
|
309
|
-
}
|
|
310
|
-
function findModelForTierWithCapability(tier, config, availableModelIds, crossProvider, unitType, metadata) {
|
|
311
|
-
const explicitModel = config.tier_models?.[tier];
|
|
312
|
-
if (explicitModel) {
|
|
313
|
-
const match = availableModelIds.find(id => {
|
|
314
|
-
const bareAvail = id.includes("/") ? id.split("/").pop() : id;
|
|
315
|
-
const bareExplicit = explicitModel.includes("/") ? explicitModel.split("/").pop() : explicitModel;
|
|
316
|
-
return bareAvail === bareExplicit || id === explicitModel;
|
|
317
|
-
});
|
|
318
|
-
if (match)
|
|
319
|
-
return { modelId: match, scores: {}, requirements: {} };
|
|
320
|
-
}
|
|
321
|
-
const requirements = computeTaskRequirements(unitType, metadata);
|
|
322
|
-
const candidates = availableModelIds.filter(id => getModelTier(id) === tier);
|
|
323
|
-
if (candidates.length === 0)
|
|
324
|
-
return { modelId: null, scores: {}, requirements };
|
|
325
|
-
const scores = {};
|
|
326
|
-
for (const id of candidates) {
|
|
327
|
-
const bareId = id.includes("/") ? id.split("/").pop() : id;
|
|
328
|
-
const profile = getModelProfile(bareId);
|
|
329
|
-
scores[id] = scoreModel(profile, requirements);
|
|
330
|
-
}
|
|
331
|
-
candidates.sort((a, b) => {
|
|
332
|
-
const scoreDiff = scores[b] - scores[a];
|
|
333
|
-
if (Math.abs(scoreDiff) > 2)
|
|
334
|
-
return scoreDiff;
|
|
335
|
-
if (crossProvider) {
|
|
336
|
-
const costDiff = getModelCost(a) - getModelCost(b);
|
|
337
|
-
if (costDiff !== 0)
|
|
338
|
-
return costDiff;
|
|
339
|
-
}
|
|
340
|
-
return a.localeCompare(b);
|
|
341
|
-
});
|
|
342
|
-
return { modelId: candidates[0], scores, requirements };
|
|
343
|
-
}
|
|
344
|
-
function getModelProfile(bareId) {
|
|
345
|
-
if (MODEL_CAPABILITY_PROFILES[bareId])
|
|
346
|
-
return MODEL_CAPABILITY_PROFILES[bareId];
|
|
347
|
-
for (const [knownId, profile] of Object.entries(MODEL_CAPABILITY_PROFILES)) {
|
|
348
|
-
if (bareId.includes(knownId) || knownId.includes(bareId))
|
|
349
|
-
return profile;
|
|
350
|
-
}
|
|
351
|
-
return { coding: 50, debugging: 50, research: 50, reasoning: 50, speed: 50, longContext: 50, instruction: 50 };
|
|
352
|
-
}
|
|
353
388
|
function getModelCost(modelId) {
|
|
354
389
|
const bareId = modelId.includes("/") ? modelId.split("/").pop() : modelId;
|
|
355
390
|
if (MODEL_COST_PER_1K_INPUT[bareId] !== undefined) {
|
|
@@ -13,6 +13,7 @@ import { mergeMilestoneToMain } from "./auto-worktree.js";
|
|
|
13
13
|
import { MergeConflictError } from "./git-service.js";
|
|
14
14
|
import { removeSessionStatus } from "./session-status-io.js";
|
|
15
15
|
import { getErrorMessage } from "./error-utils.js";
|
|
16
|
+
import { logWarning } from "./workflow-logger.js";
|
|
16
17
|
// ─── Merge Queue ───────────────────────────────────────────────────────────
|
|
17
18
|
/**
|
|
18
19
|
* Check whether a milestone is complete by querying its worktree SQLite DB.
|
|
@@ -27,7 +28,8 @@ export function isMilestoneCompleteInWorktreeDb(basePath, mid) {
|
|
|
27
28
|
const result = spawnSync("sqlite3", [dbPath, `SELECT status FROM milestones WHERE id='${mid}' LIMIT 1`], { timeout: 3000, encoding: "utf-8" });
|
|
28
29
|
return (result.stdout || "").trim() === "complete";
|
|
29
30
|
}
|
|
30
|
-
catch {
|
|
31
|
+
catch (e) {
|
|
32
|
+
logWarning("parallel", `spawnSync milestone completion check failed for ${mid}: ${e.message}`);
|
|
31
33
|
return false;
|
|
32
34
|
}
|
|
33
35
|
}
|
|
@@ -45,8 +47,8 @@ function discoverDbCompletedMilestones(basePath) {
|
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
|
-
catch {
|
|
49
|
-
|
|
50
|
+
catch (e) {
|
|
51
|
+
logWarning("parallel", `readdirSync for completed set failed: ${e.message}`);
|
|
50
52
|
}
|
|
51
53
|
return completed;
|
|
52
54
|
}
|
|
@@ -19,6 +19,7 @@ import { resolveParallelConfig } from "./preferences.js";
|
|
|
19
19
|
import { writeSessionStatus, readAllSessionStatuses, readSessionStatus, removeSessionStatus, sendSignal, cleanupStaleSessions, } from "./session-status-io.js";
|
|
20
20
|
import { analyzeParallelEligibility, } from "./parallel-eligibility.js";
|
|
21
21
|
import { getErrorMessage } from "./error-utils.js";
|
|
22
|
+
import { logWarning } from "./workflow-logger.js";
|
|
22
23
|
// ─── Module State ──────────────────────────────────────────────────────────
|
|
23
24
|
let state = null;
|
|
24
25
|
// ─── Persistence ──────────────────────────────────────────────────────────
|
|
@@ -61,7 +62,9 @@ export function persistState(basePath) {
|
|
|
61
62
|
writeFileSync(tmp, JSON.stringify(persisted, null, 2), "utf-8");
|
|
62
63
|
renameSync(tmp, dest);
|
|
63
64
|
}
|
|
64
|
-
catch {
|
|
65
|
+
catch (e) {
|
|
66
|
+
logWarning("parallel", `persist parallel state failed: ${e.message}`);
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
69
|
/**
|
|
67
70
|
* Remove the persisted state file.
|
|
@@ -72,7 +75,9 @@ function removeStateFile(basePath) {
|
|
|
72
75
|
if (existsSync(p))
|
|
73
76
|
unlinkSync(p);
|
|
74
77
|
}
|
|
75
|
-
catch {
|
|
78
|
+
catch (e) {
|
|
79
|
+
logWarning("parallel", `clear parallel state file failed: ${e.message}`);
|
|
80
|
+
}
|
|
76
81
|
}
|
|
77
82
|
function isPidAlive(pid) {
|
|
78
83
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
@@ -81,7 +86,8 @@ function isPidAlive(pid) {
|
|
|
81
86
|
process.kill(pid, 0);
|
|
82
87
|
return true;
|
|
83
88
|
}
|
|
84
|
-
catch {
|
|
89
|
+
catch (e) {
|
|
90
|
+
logWarning("parallel", `pid alive check failed for pid ${pid}: ${e.message}`);
|
|
85
91
|
return false;
|
|
86
92
|
}
|
|
87
93
|
}
|
|
@@ -112,7 +118,8 @@ export function restoreState(basePath) {
|
|
|
112
118
|
}
|
|
113
119
|
return persisted;
|
|
114
120
|
}
|
|
115
|
-
catch {
|
|
121
|
+
catch (e) {
|
|
122
|
+
logWarning("parallel", `readParallelState JSON parse failed: ${e.message}`);
|
|
116
123
|
return null;
|
|
117
124
|
}
|
|
118
125
|
}
|
|
@@ -126,8 +133,8 @@ function appendWorkerLog(basePath, milestoneId, chunk) {
|
|
|
126
133
|
mkdirSync(dir, { recursive: true });
|
|
127
134
|
appendFileSync(workerLogPath(basePath, milestoneId), chunk, "utf-8");
|
|
128
135
|
}
|
|
129
|
-
catch {
|
|
130
|
-
|
|
136
|
+
catch (e) {
|
|
137
|
+
logWarning("parallel", `appendFileSync worker log failed for ${milestoneId}: ${e.message}`);
|
|
131
138
|
}
|
|
132
139
|
}
|
|
133
140
|
function restoreRuntimeState(basePath) {
|
|
@@ -331,9 +338,8 @@ export async function startParallel(basePath, milestoneIds, prefs) {
|
|
|
331
338
|
try {
|
|
332
339
|
wtPath = createMilestoneWorktree(basePath, mid);
|
|
333
340
|
}
|
|
334
|
-
catch {
|
|
335
|
-
|
|
336
|
-
// is not available. Fall back to a placeholder path.
|
|
341
|
+
catch (e) {
|
|
342
|
+
logWarning("parallel", `createMilestoneWorktree fallback for ${mid}: ${e.message}`);
|
|
337
343
|
wtPath = worktreePath(basePath, mid);
|
|
338
344
|
}
|
|
339
345
|
const worker = {
|
|
@@ -451,7 +457,8 @@ export function spawnWorker(basePath, milestoneId) {
|
|
|
451
457
|
detached: false,
|
|
452
458
|
});
|
|
453
459
|
}
|
|
454
|
-
catch {
|
|
460
|
+
catch (e) {
|
|
461
|
+
logWarning("parallel", `spawnSync worker failed for ${milestoneId}: ${e.message}`);
|
|
455
462
|
return false;
|
|
456
463
|
}
|
|
457
464
|
// Handle spawn errors (e.g., ENOENT when binary doesn't exist)
|
|
@@ -572,7 +579,8 @@ function resolveGsdBin() {
|
|
|
572
579
|
try {
|
|
573
580
|
thisDir = dirname(fileURLToPath(import.meta.url));
|
|
574
581
|
}
|
|
575
|
-
catch {
|
|
582
|
+
catch (e) {
|
|
583
|
+
logWarning("parallel", `dirname(fileURLToPath) failed: ${e.message}`);
|
|
576
584
|
thisDir = process.cwd();
|
|
577
585
|
}
|
|
578
586
|
const candidates = [
|
|
@@ -599,7 +607,7 @@ function processWorkerLine(basePath, milestoneId, line) {
|
|
|
599
607
|
event = JSON.parse(line);
|
|
600
608
|
}
|
|
601
609
|
catch {
|
|
602
|
-
return; //
|
|
610
|
+
return; // Non-NDJSON lines (progress text, tool output) are expected — silent drop
|
|
603
611
|
}
|
|
604
612
|
const type = String(event.type ?? "");
|
|
605
613
|
// message_end carries usage data with cost
|
|
@@ -684,7 +692,9 @@ export async function stopParallel(basePath, milestoneId) {
|
|
|
684
692
|
process.kill(worker.pid, "SIGTERM");
|
|
685
693
|
}
|
|
686
694
|
}
|
|
687
|
-
catch {
|
|
695
|
+
catch (e) {
|
|
696
|
+
logWarning("parallel", `process.kill SIGTERM failed for pid ${worker.pid}: ${e.message}`);
|
|
697
|
+
}
|
|
688
698
|
}
|
|
689
699
|
// Wait for the headless process to cascade SIGTERM to its RPC child.
|
|
690
700
|
// The headless signal handler calls client.stop() which sends SIGTERM
|
|
@@ -701,7 +711,9 @@ export async function stopParallel(basePath, milestoneId) {
|
|
|
701
711
|
process.kill(worker.pid, "SIGKILL");
|
|
702
712
|
}
|
|
703
713
|
}
|
|
704
|
-
catch {
|
|
714
|
+
catch (e) {
|
|
715
|
+
logWarning("parallel", `process.kill SIGKILL failed for pid ${worker.pid}: ${e.message}`);
|
|
716
|
+
}
|
|
705
717
|
await waitForWorkerExit(worker, 250);
|
|
706
718
|
}
|
|
707
719
|
// Remove stream listeners before releasing the process handle
|
|
@@ -894,5 +894,50 @@ export function validatePreferences(preferences) {
|
|
|
894
894
|
errors.push("experimental must be an object");
|
|
895
895
|
}
|
|
896
896
|
}
|
|
897
|
+
// ─── Codebase Map ──────────────────────────────────────────────────
|
|
898
|
+
if (preferences.codebase !== undefined) {
|
|
899
|
+
if (typeof preferences.codebase === "object" && preferences.codebase !== null) {
|
|
900
|
+
const cb = preferences.codebase;
|
|
901
|
+
const validCb = {};
|
|
902
|
+
if (cb.exclude_patterns !== undefined) {
|
|
903
|
+
if (Array.isArray(cb.exclude_patterns) && cb.exclude_patterns.every((p) => typeof p === "string")) {
|
|
904
|
+
validCb.exclude_patterns = cb.exclude_patterns;
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
errors.push("codebase.exclude_patterns must be an array of strings");
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (cb.max_files !== undefined) {
|
|
911
|
+
const mf = typeof cb.max_files === "number" ? cb.max_files : Number(cb.max_files);
|
|
912
|
+
if (Number.isFinite(mf) && mf >= 1) {
|
|
913
|
+
validCb.max_files = Math.floor(mf);
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
errors.push("codebase.max_files must be a positive integer");
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (cb.collapse_threshold !== undefined) {
|
|
920
|
+
const ct = typeof cb.collapse_threshold === "number" ? cb.collapse_threshold : Number(cb.collapse_threshold);
|
|
921
|
+
if (Number.isFinite(ct) && ct >= 1) {
|
|
922
|
+
validCb.collapse_threshold = Math.floor(ct);
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
errors.push("codebase.collapse_threshold must be a positive integer");
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
const knownCbKeys = new Set(["exclude_patterns", "max_files", "collapse_threshold"]);
|
|
929
|
+
for (const key of Object.keys(cb)) {
|
|
930
|
+
if (!knownCbKeys.has(key)) {
|
|
931
|
+
warnings.push(`unknown codebase key "${key}" — ignored`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (Object.keys(validCb).length > 0) {
|
|
935
|
+
validated.codebase = validCb;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
errors.push("codebase must be an object");
|
|
940
|
+
}
|
|
941
|
+
}
|
|
897
942
|
return { preferences: validated, errors, warnings };
|
|
898
943
|
}
|
|
@@ -15,6 +15,7 @@ import { join } from "node:path";
|
|
|
15
15
|
import { gsdRoot } from "./paths.js";
|
|
16
16
|
import { parse as parseYaml } from "yaml";
|
|
17
17
|
import { normalizeStringArray } from "../shared/format-utils.js";
|
|
18
|
+
import { logWarning } from "./workflow-logger.js";
|
|
18
19
|
import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js";
|
|
19
20
|
import { KNOWN_PREFERENCE_KEYS, MODE_DEFAULTS, } from "./preferences-types.js";
|
|
20
21
|
import { validatePreferences } from "./preferences-validation.js";
|
|
@@ -162,7 +163,7 @@ function parseFrontmatterBlock(frontmatter) {
|
|
|
162
163
|
return parsed;
|
|
163
164
|
}
|
|
164
165
|
catch (e) {
|
|
165
|
-
|
|
166
|
+
logWarning("guided", `YAML parse error in frontmatter block: ${e.message}`);
|
|
166
167
|
return {};
|
|
167
168
|
}
|
|
168
169
|
}
|
|
@@ -217,8 +218,8 @@ function parseHeadingListFormat(content) {
|
|
|
217
218
|
}
|
|
218
219
|
typed[targetSection] = value;
|
|
219
220
|
}
|
|
220
|
-
catch {
|
|
221
|
-
|
|
221
|
+
catch (e) {
|
|
222
|
+
logWarning("guided", `preferences section parse failed: ${e.message}`);
|
|
222
223
|
}
|
|
223
224
|
}
|
|
224
225
|
return typed;
|
|
@@ -289,6 +290,17 @@ function mergePreferences(base, override) {
|
|
|
289
290
|
service_tier: override.service_tier ?? base.service_tier,
|
|
290
291
|
forensics_dedup: override.forensics_dedup ?? base.forensics_dedup,
|
|
291
292
|
show_token_cost: override.show_token_cost ?? base.show_token_cost,
|
|
293
|
+
codebase: (base.codebase || override.codebase)
|
|
294
|
+
? {
|
|
295
|
+
...(base.codebase ?? {}),
|
|
296
|
+
...(override.codebase ?? {}),
|
|
297
|
+
// Merge exclude_patterns arrays rather than overriding
|
|
298
|
+
exclude_patterns: [
|
|
299
|
+
...((base.codebase?.exclude_patterns) ?? []),
|
|
300
|
+
...((override.codebase?.exclude_patterns) ?? []),
|
|
301
|
+
].filter(Boolean),
|
|
302
|
+
}
|
|
303
|
+
: undefined,
|
|
292
304
|
};
|
|
293
305
|
}
|
|
294
306
|
function mergeStringLists(base, override) {
|
|
@@ -21,6 +21,7 @@ import { GSDError, GSD_PARSE_ERROR } from "./errors.js";
|
|
|
21
21
|
import { join, dirname } from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { homedir } from "node:os";
|
|
24
|
+
import { logWarning } from "./workflow-logger.js";
|
|
24
25
|
/**
|
|
25
26
|
* Resolve the GSD extension directory.
|
|
26
27
|
*
|
|
@@ -69,7 +70,7 @@ function warmCache() {
|
|
|
69
70
|
// prompts/ may not exist in test environments — lazy loading still works.
|
|
70
71
|
// Emit a diagnostic when running outside tests so wrong-path bugs are visible.
|
|
71
72
|
if (!process.env.VITEST && !process.env.NODE_TEST) {
|
|
72
|
-
|
|
73
|
+
logWarning("prompt", `warmCache: prompts dir not found: ${promptsDir}`);
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
try {
|
|
@@ -85,7 +86,7 @@ function warmCache() {
|
|
|
85
86
|
catch {
|
|
86
87
|
// templates/ may not exist in test environments — lazy loading still works.
|
|
87
88
|
if (!process.env.VITEST && !process.env.NODE_TEST) {
|
|
88
|
-
|
|
89
|
+
logWarning("prompt", `warmCache: templates dir not found: ${templatesDir}`);
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
}
|
|
@@ -48,7 +48,7 @@ Remove the `{ID}-PARKED.md` file from the milestone directory to reactivate it.
|
|
|
48
48
|
### Skip a slice
|
|
49
49
|
Mark a slice as skipped so auto-mode advances past it without executing. Use the `gsd_skip_slice` tool:
|
|
50
50
|
```
|
|
51
|
-
gsd_skip_slice({
|
|
51
|
+
gsd_skip_slice({ milestoneId: "M003", sliceId: "S02", reason: "Descoped — feature moved to M005" })
|
|
52
52
|
```
|
|
53
53
|
Skipped slices are treated as closed by the state machine (like "complete" but distinct). Use when a slice is no longer needed or has been superseded. The slice data is preserved for reference.
|
|
54
54
|
|