fix(model-routing): use honest unitTypes for discuss dispatches and map all auto-dispatch phases
Discuss dispatches in guided-flow.ts were aliased to "plan-milestone"/"plan-slice" unitTypes, causing the planning model preference to silently override the user's active model. This was discovered when a user configured Codex as their model but got switched to Opus during discuss phases because models.planning was set. Changes: - Add "discuss" and "validation" keys to GSDModelConfig/GSDModelConfigV2 - Map discuss-milestone/discuss-slice to models.discuss (falls back to planning) - Map reassess-roadmap/rewrite-docs/gate-evaluate/validate-milestone to models.validation (falls back to planning) - Map reactive-execute to models.execution, complete-milestone to models.completion - Fix 15 dispatchWorkflow calls in guided-flow.ts to use honest unitTypes - Add discuss-slice to LIFECYCLE_ONLY_UNITS, artifact paths, metrics phase classification, complexity tiers, and all dashboard/overlay label functions Closes #2865
This commit is contained in:
parent
36930694e4
commit
c622eec0e1
11 changed files with 248 additions and 23 deletions
|
|
@ -30,6 +30,10 @@ export function resolveExpectedArtifactPath(
|
|||
const dir = resolveMilestonePath(base, mid);
|
||||
return dir ? join(dir, buildMilestoneFileName(mid, "CONTEXT")) : null;
|
||||
}
|
||||
case "discuss-slice": {
|
||||
const dir = resolveSlicePath(base, mid, sid!);
|
||||
return dir ? join(dir, buildSliceFileName(sid!, "CONTEXT")) : null;
|
||||
}
|
||||
case "research-milestone": {
|
||||
const dir = resolveMilestonePath(base, mid);
|
||||
return dir ? join(dir, buildMilestoneFileName(mid, "RESEARCH")) : null;
|
||||
|
|
@ -98,6 +102,8 @@ export function diagnoseExpectedArtifact(
|
|||
switch (unitType) {
|
||||
case "discuss-milestone":
|
||||
return `${relMilestoneFile(base, mid, "CONTEXT")} (milestone context from discussion)`;
|
||||
case "discuss-slice":
|
||||
return `${relSliceFile(base, mid, sid!, "CONTEXT")} (slice context from discussion)`;
|
||||
case "research-milestone":
|
||||
return `${relMilestoneFile(base, mid, "RESEARCH")} (milestone research)`;
|
||||
case "plan-milestone":
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@ export interface AutoDashboardData {
|
|||
export function unitVerb(unitType: string): string {
|
||||
if (unitType.startsWith("hook/")) return `hook: ${unitType.slice(5)}`;
|
||||
switch (unitType) {
|
||||
case "discuss-milestone": return "discussing";
|
||||
case "discuss-milestone":
|
||||
case "discuss-slice": return "discussing";
|
||||
case "research-milestone":
|
||||
case "research-slice": return "researching";
|
||||
case "plan-milestone":
|
||||
|
|
@ -96,7 +97,8 @@ export function unitVerb(unitType: string): string {
|
|||
export function unitPhaseLabel(unitType: string): string {
|
||||
if (unitType.startsWith("hook/")) return "HOOK";
|
||||
switch (unitType) {
|
||||
case "discuss-milestone": return "DISCUSS";
|
||||
case "discuss-milestone":
|
||||
case "discuss-slice": return "DISCUSS";
|
||||
case "research-milestone": return "RESEARCH";
|
||||
case "research-slice": return "RESEARCH";
|
||||
case "plan-milestone": return "PLAN";
|
||||
|
|
@ -123,6 +125,7 @@ function peekNext(unitType: string, state: GSDState): string {
|
|||
if (unitType.startsWith("hook/")) return `continue ${sid}`;
|
||||
switch (unitType) {
|
||||
case "discuss-milestone": return "research or plan milestone";
|
||||
case "discuss-slice": return "plan slice";
|
||||
case "research-milestone": return "plan milestone roadmap";
|
||||
case "plan-milestone": return "plan or execute first slice";
|
||||
case "research-slice": return `plan ${sid}`;
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ function enqueueSidecar(
|
|||
* Auto-commit is skipped for these — their state files are picked up by the
|
||||
* next actual task commit via `smartStage()`. */
|
||||
const LIFECYCLE_ONLY_UNITS = new Set([
|
||||
"research-milestone", "discuss-milestone", "plan-milestone",
|
||||
"research-milestone", "discuss-milestone", "discuss-slice", "plan-milestone",
|
||||
"validate-milestone", "research-slice", "plan-slice",
|
||||
"replan-slice", "complete-slice", "run-uat",
|
||||
"reassess-roadmap", "rewrite-docs",
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ const UNIT_TYPE_TIERS: Record<string, ComplexityTier> = {
|
|||
|
||||
// Tier 2 — Standard: research, routine planning, discussion
|
||||
"discuss-milestone": "standard",
|
||||
"discuss-slice": "standard",
|
||||
"research-milestone": "standard",
|
||||
"research-slice": "standard",
|
||||
"plan-milestone": "standard",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import { runEnvironmentChecks, type EnvironmentCheckResult } from "./doctor-envi
|
|||
|
||||
function unitLabel(type: string): string {
|
||||
switch (type) {
|
||||
case "discuss-milestone":
|
||||
case "discuss-slice": return "Discuss";
|
||||
case "research-milestone": return "Research";
|
||||
case "plan-milestone": return "Plan";
|
||||
case "research-slice": return "Research";
|
||||
|
|
|
|||
|
|
@ -102,12 +102,14 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|||
|
||||
- `custom_instructions`: extra durable instructions related to skill use. For operational project knowledge (recurring rules, gotchas, patterns), use `.gsd/KNOWLEDGE.md` instead — it's injected into every agent prompt automatically and agents can append to it during execution.
|
||||
|
||||
- `models`: per-stage model selection for auto-mode. Keys: `research`, `planning`, `execution`, `execution_simple`, `completion`, `subagent`. Values can be:
|
||||
- `models`: per-stage model selection (applies to both auto-mode and guided-flow dispatches). Keys: `research`, `planning`, `discuss`, `execution`, `execution_simple`, `completion`, `validation`, `subagent`. Values can be:
|
||||
- Simple string: `"claude-sonnet-4-6"` — single model, no fallbacks
|
||||
- Provider-qualified string: `"bedrock/claude-sonnet-4-6"` — targets a specific provider when the same model ID exists across multiple providers
|
||||
- Object with fallbacks: `{ model: "claude-opus-4-6", fallbacks: ["glm-5", "minimax-m2.5"] }` — tries fallbacks in order if primary fails
|
||||
- Object with provider: `{ model: "claude-opus-4-6", provider: "bedrock" }` — explicit provider targeting in object format
|
||||
- Omit a key to use whatever model is currently active. Fallbacks are tried when model switching fails (provider unavailable, rate limited, etc.).
|
||||
- Omit a key to use whatever model is currently active (except `discuss` and `validation` which fall back to `planning` when unset). Fallbacks are tried when model switching fails (provider unavailable, rate limited, etc.).
|
||||
- `discuss` — used for milestone/slice discussion (interactive context gathering). Falls back to `planning` if unset.
|
||||
- `validation` — used for gate evaluation, roadmap reassessment, milestone validation, and doc rewrites. Falls back to `planning` if unset.
|
||||
|
||||
- `skill_staleness_days`: number — skills unused for this many days get deprioritized during discovery. Set to `0` to disable staleness tracking. Default: `60`.
|
||||
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ type UIContext = ExtensionContext;
|
|||
* This is the only way the wizard triggers work — everything else is the LLM's job.
|
||||
*
|
||||
* When a unitType is provided, resolves the user's model preference for that
|
||||
* phase (e.g., models.planning → "plan-milestone") and applies it before
|
||||
* phase (e.g., models.planning → "plan-milestone", models.discuss → "discuss-milestone") and applies it before
|
||||
* dispatching. This ensures guided-flow dispatches respect the same
|
||||
* per-phase model preferences that auto-mode uses.
|
||||
*/
|
||||
|
|
@ -573,7 +573,7 @@ export async function showDiscuss(
|
|||
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
||||
: basePrompt;
|
||||
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
|
||||
await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
|
||||
await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "discuss-milestone");
|
||||
} else if (choice === "discuss_fresh") {
|
||||
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
||||
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
||||
|
|
@ -581,13 +581,13 @@ export async function showDiscuss(
|
|||
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
||||
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
||||
commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`),
|
||||
}), "gsd-discuss", ctx, "plan-milestone");
|
||||
}), "gsd-discuss", ctx, "discuss-milestone");
|
||||
} else if (choice === "skip_milestone") {
|
||||
const milestoneIds = findMilestoneIds(basePath);
|
||||
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
||||
const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
|
||||
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: false };
|
||||
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
|
||||
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -717,7 +717,7 @@ export async function showDiscuss(
|
|||
}
|
||||
|
||||
const prompt = await buildDiscussSlicePrompt(mid, chosen.id, chosen.title, basePath, { rediscuss: isRediscuss });
|
||||
await dispatchWorkflow(pi, prompt, "gsd-discuss", ctx, "plan-slice");
|
||||
await dispatchWorkflow(pi, prompt, "gsd-discuss", ctx, "discuss-slice");
|
||||
|
||||
// Wait for the discuss session to finish, then loop back to the picker
|
||||
await ctx.waitForIdle();
|
||||
|
|
@ -795,7 +795,7 @@ async function dispatchDiscussForMilestone(
|
|||
const prompt = draftContent
|
||||
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
||||
: basePrompt;
|
||||
await dispatchWorkflow(pi, prompt, "gsd-discuss", ctx, "plan-milestone");
|
||||
await dispatchWorkflow(pi, prompt, "gsd-discuss", ctx, "discuss-milestone");
|
||||
}
|
||||
|
||||
// ─── Smart Entry Point ────────────────────────────────────────────────────────
|
||||
|
|
@ -935,7 +935,7 @@ async function handleMilestoneActions(
|
|||
await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
||||
`New milestone ${nextId}.`,
|
||||
basePath
|
||||
), "gsd-run", ctx, "plan-milestone");
|
||||
), "gsd-run", ctx, "discuss-milestone");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1086,7 +1086,7 @@ export async function showSmartEntry(
|
|||
await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
||||
`New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`,
|
||||
basePath
|
||||
), "gsd-run", ctx, "plan-milestone");
|
||||
), "gsd-run", ctx, "discuss-milestone");
|
||||
} else {
|
||||
const choice = await showNextAction(ctx, {
|
||||
title: "GSD — Get Shit Done",
|
||||
|
|
@ -1107,7 +1107,7 @@ export async function showSmartEntry(
|
|||
await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
||||
`New milestone ${nextId}.`,
|
||||
basePath
|
||||
), "gsd-run", ctx, "plan-milestone");
|
||||
), "gsd-run", ctx, "discuss-milestone");
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
|
@ -1146,7 +1146,7 @@ export async function showSmartEntry(
|
|||
await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
||||
`New milestone ${nextId}.`,
|
||||
basePath
|
||||
), "gsd-run", ctx, "plan-milestone");
|
||||
), "gsd-run", ctx, "discuss-milestone");
|
||||
} else if (choice === "status") {
|
||||
const { fireStatusViaCommand } = await import("./commands.js");
|
||||
await fireStatusViaCommand(ctx);
|
||||
|
|
@ -1194,7 +1194,7 @@ export async function showSmartEntry(
|
|||
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
||||
: basePrompt;
|
||||
pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
|
||||
await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
|
||||
await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "discuss-milestone");
|
||||
} else if (choice === "discuss_fresh") {
|
||||
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
||||
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
||||
|
|
@ -1202,7 +1202,7 @@ export async function showSmartEntry(
|
|||
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
||||
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
||||
commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
|
||||
}), "gsd-discuss", ctx, "plan-milestone");
|
||||
}), "gsd-discuss", ctx, "discuss-milestone");
|
||||
} else if (choice === "skip_milestone") {
|
||||
const milestoneIds = findMilestoneIds(basePath);
|
||||
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
||||
|
|
@ -1211,7 +1211,7 @@ export async function showSmartEntry(
|
|||
await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
||||
`New milestone ${nextId}.`,
|
||||
basePath
|
||||
), "gsd-run", ctx, "plan-milestone");
|
||||
), "gsd-run", ctx, "discuss-milestone");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -1286,7 +1286,7 @@ export async function showSmartEntry(
|
|||
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
||||
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
||||
commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
|
||||
}), "gsd-run", ctx, "plan-milestone");
|
||||
}), "gsd-run", ctx, "discuss-milestone");
|
||||
} else if (choice === "skip_milestone") {
|
||||
const milestoneIds = findMilestoneIds(basePath);
|
||||
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
||||
|
|
@ -1295,7 +1295,7 @@ export async function showSmartEntry(
|
|||
await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
||||
`New milestone ${nextId}.`,
|
||||
basePath
|
||||
), "gsd-run", ctx, "plan-milestone");
|
||||
), "gsd-run", ctx, "discuss-milestone");
|
||||
} else if (choice === "discard_milestone") {
|
||||
const confirmed = await showConfirm(ctx, {
|
||||
title: "Discard milestone?",
|
||||
|
|
@ -1421,7 +1421,7 @@ export async function showSmartEntry(
|
|||
}),
|
||||
}), "gsd-run", ctx, "plan-slice");
|
||||
} else if (choice === "discuss") {
|
||||
await dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }), "gsd-run", ctx, "plan-slice");
|
||||
await dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }), "gsd-run", ctx, "discuss-slice");
|
||||
} else if (choice === "research") {
|
||||
const researchTemplates = inlineTemplate("research", "Research");
|
||||
await dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
|
||||
|
|
|
|||
|
|
@ -75,13 +75,16 @@ export interface MetricsLedger {
|
|||
|
||||
// ─── Phase classification ─────────────────────────────────────────────────────
|
||||
|
||||
export type MetricsPhase = "research" | "planning" | "execution" | "completion" | "reassessment";
|
||||
export type MetricsPhase = "research" | "discussion" | "planning" | "execution" | "completion" | "reassessment";
|
||||
|
||||
export function classifyUnitPhase(unitType: string): MetricsPhase {
|
||||
switch (unitType) {
|
||||
case "research-milestone":
|
||||
case "research-slice":
|
||||
return "research";
|
||||
case "discuss-milestone":
|
||||
case "discuss-slice":
|
||||
return "discussion";
|
||||
case "plan-milestone":
|
||||
case "plan-slice":
|
||||
return "planning";
|
||||
|
|
@ -299,7 +302,7 @@ export function aggregateByPhase(units: UnitMetrics[]): PhaseAggregate[] {
|
|||
agg.duration += u.finishedAt - u.startedAt;
|
||||
}
|
||||
// Return in a stable order
|
||||
const order: MetricsPhase[] = ["research", "planning", "execution", "completion", "reassessment"];
|
||||
const order: MetricsPhase[] = ["research", "discussion", "planning", "execution", "completion", "reassessment"];
|
||||
return order.map(p => map.get(p)).filter((a): a is PhaseAggregate => !!a);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,16 +56,28 @@ export function resolveModelWithFallbacksForUnit(unitType: string): ResolvedMode
|
|||
case "replan-slice":
|
||||
phaseConfig = m.planning;
|
||||
break;
|
||||
case "discuss-milestone":
|
||||
case "discuss-slice":
|
||||
phaseConfig = m.discuss ?? m.planning;
|
||||
break;
|
||||
case "execute-task":
|
||||
case "reactive-execute":
|
||||
phaseConfig = m.execution;
|
||||
break;
|
||||
case "execute-task-simple":
|
||||
phaseConfig = m.execution_simple ?? m.execution;
|
||||
break;
|
||||
case "complete-slice":
|
||||
case "complete-milestone":
|
||||
case "run-uat":
|
||||
phaseConfig = m.completion;
|
||||
break;
|
||||
case "reassess-roadmap":
|
||||
case "rewrite-docs":
|
||||
case "gate-evaluate":
|
||||
case "validate-milestone":
|
||||
phaseConfig = m.validation ?? m.planning;
|
||||
break;
|
||||
default:
|
||||
// Subagent unit types (e.g., "subagent", "subagent/scout")
|
||||
if (unitType === "subagent" || unitType.startsWith("subagent/")) {
|
||||
|
|
|
|||
|
|
@ -134,9 +134,11 @@ export interface GSDPhaseModelConfig {
|
|||
export interface GSDModelConfig {
|
||||
research?: string;
|
||||
planning?: string;
|
||||
discuss?: string;
|
||||
execution?: string;
|
||||
execution_simple?: string;
|
||||
completion?: string;
|
||||
validation?: string;
|
||||
subagent?: string;
|
||||
}
|
||||
|
||||
|
|
@ -147,9 +149,11 @@ export interface GSDModelConfig {
|
|||
export interface GSDModelConfigV2 {
|
||||
research?: string | GSDPhaseModelConfig;
|
||||
planning?: string | GSDPhaseModelConfig;
|
||||
discuss?: string | GSDPhaseModelConfig;
|
||||
execution?: string | GSDPhaseModelConfig;
|
||||
execution_simple?: string | GSDPhaseModelConfig;
|
||||
completion?: string | GSDPhaseModelConfig;
|
||||
validation?: string | GSDPhaseModelConfig;
|
||||
subagent?: string | GSDPhaseModelConfig;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,192 @@
|
|||
/**
|
||||
* Model UnitType Mapping — regression tests for #2865.
|
||||
*
|
||||
* Verifies that all auto-dispatch unitTypes have corresponding entries in:
|
||||
* - resolveModelWithFallbacksForUnit (preferences-models.ts)
|
||||
* - classifyUnitPhase (metrics.ts)
|
||||
* - LIFECYCLE_ONLY_UNITS (auto-post-unit.ts)
|
||||
* - unitVerb / unitPhaseLabel (auto-dashboard.ts)
|
||||
* - resolveExpectedArtifactPath (auto-artifact-paths.ts)
|
||||
*
|
||||
* Uses source-level checks to avoid import resolution issues in dev.
|
||||
*
|
||||
* Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const gsdDir = join(__dirname, "..");
|
||||
|
||||
function readSrc(file: string): string {
|
||||
return readFileSync(join(gsdDir, file), "utf-8");
|
||||
}
|
||||
|
||||
const preferencesSrc = readSrc("preferences-models.ts");
|
||||
const metricsSrc = readSrc("metrics.ts");
|
||||
const postUnitSrc = readSrc("auto-post-unit.ts");
|
||||
const dashboardSrc = readSrc("auto-dashboard.ts");
|
||||
const artifactSrc = readSrc("auto-artifact-paths.ts");
|
||||
const guidedFlowSrc = readSrc("guided-flow.ts");
|
||||
const autoDispatchSrc = readSrc("auto-dispatch.ts");
|
||||
|
||||
// Derive unitTypes directly from auto-dispatch.ts source so the test
|
||||
// automatically tracks dispatch rule changes (Copilot review feedback).
|
||||
const AUTO_DISPATCH_UNIT_TYPES = (() => {
|
||||
const unitTypeRegex = /unitType:\s*["']([^"']+)["']/g;
|
||||
const unitTypes = new Set<string>();
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = unitTypeRegex.exec(autoDispatchSrc)) !== null) {
|
||||
unitTypes.add(match[1]);
|
||||
}
|
||||
return Array.from(unitTypes);
|
||||
})();
|
||||
|
||||
// Additionally include unitTypes used by guided-flow but not auto-dispatch
|
||||
// (e.g., discuss-slice is dispatched by guided-flow but not auto-dispatch).
|
||||
const ALL_KNOWN_UNIT_TYPES = [
|
||||
...new Set([...AUTO_DISPATCH_UNIT_TYPES, "discuss-slice"]),
|
||||
];
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// #2865: discuss dispatches must NOT alias to plan unitTypes
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test("#2865: no dispatchWorkflow with gsd-discuss customType uses plan-milestone", () => {
|
||||
// Match dispatchWorkflow calls where "gsd-discuss" appears before "plan-milestone"
|
||||
// in the same call (the 5 args are on consecutive lines).
|
||||
const blocks = guidedFlowSrc.split(/dispatchWorkflow\(/);
|
||||
for (const block of blocks) {
|
||||
const callEnd = block.indexOf(");");
|
||||
if (callEnd === -1) continue;
|
||||
const call = block.slice(0, callEnd);
|
||||
if (call.includes('"gsd-discuss"') && call.includes('"plan-milestone"')) {
|
||||
assert.fail(`Discuss dispatch should not use plan-milestone: ...dispatchWorkflow(${call.slice(0, 120).trim()}...`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("#2865: no dispatchWorkflow with gsd-discuss customType uses plan-slice", () => {
|
||||
const blocks = guidedFlowSrc.split(/dispatchWorkflow\(/);
|
||||
for (const block of blocks) {
|
||||
const callEnd = block.indexOf(");");
|
||||
if (callEnd === -1) continue;
|
||||
const call = block.slice(0, callEnd);
|
||||
if (call.includes('"gsd-discuss"') && call.includes('"plan-slice"')) {
|
||||
assert.fail(`Discuss slice dispatch should not use plan-slice: ...dispatchWorkflow(${call.slice(0, 120).trim()}...`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("#2865: no buildDiscussPrompt call dispatches with plan-milestone", () => {
|
||||
const blocks = guidedFlowSrc.split(/dispatchWorkflow\(/);
|
||||
for (const block of blocks) {
|
||||
const callEnd = block.indexOf(");");
|
||||
if (callEnd === -1) continue;
|
||||
const call = block.slice(0, callEnd);
|
||||
if (call.includes("buildDiscussPrompt") && call.includes('"plan-milestone"')) {
|
||||
assert.fail(`buildDiscussPrompt dispatch should not use plan-milestone`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("#2865: no buildDiscussSlicePrompt call dispatches with plan-slice", () => {
|
||||
const blocks = guidedFlowSrc.split(/dispatchWorkflow\(/);
|
||||
for (const block of blocks) {
|
||||
const callEnd = block.indexOf(");");
|
||||
if (callEnd === -1) continue;
|
||||
const call = block.slice(0, callEnd);
|
||||
if (call.includes("buildDiscussSlicePrompt") && call.includes('"plan-slice"')) {
|
||||
assert.fail(`buildDiscussSlicePrompt dispatch should not use plan-slice`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("#2865: no guided-discuss-milestone loadPrompt dispatches with plan-milestone", () => {
|
||||
const blocks = guidedFlowSrc.split(/dispatchWorkflow\(/);
|
||||
for (const block of blocks) {
|
||||
const callEnd = block.indexOf(");");
|
||||
if (callEnd === -1) continue;
|
||||
const call = block.slice(0, callEnd);
|
||||
if (call.includes("guided-discuss-milestone") && call.includes('"plan-milestone"')) {
|
||||
assert.fail(`guided-discuss-milestone dispatch should not use plan-milestone`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// preferences-models.ts: resolveModelWithFallbacksForUnit coverage
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test("resolveModelWithFallbacksForUnit handles discuss-milestone", () => {
|
||||
assert.ok(preferencesSrc.includes('"discuss-milestone"'), "missing discuss-milestone case");
|
||||
});
|
||||
|
||||
test("resolveModelWithFallbacksForUnit handles discuss-slice", () => {
|
||||
assert.ok(preferencesSrc.includes('"discuss-slice"'), "missing discuss-slice case");
|
||||
});
|
||||
|
||||
test("discuss unitTypes fall back to planning when models.discuss is unset", () => {
|
||||
assert.ok(
|
||||
preferencesSrc.includes("m.discuss ?? m.planning"),
|
||||
"discuss should fall back to m.planning",
|
||||
);
|
||||
});
|
||||
|
||||
test("validation unitTypes fall back to planning when models.validation is unset", () => {
|
||||
assert.ok(
|
||||
preferencesSrc.includes("m.validation ?? m.planning"),
|
||||
"validation should fall back to m.planning",
|
||||
);
|
||||
});
|
||||
|
||||
test("all auto-dispatch unitTypes have preference mapping or subagent handling", () => {
|
||||
const unmapped: string[] = [];
|
||||
for (const ut of ALL_KNOWN_UNIT_TYPES) {
|
||||
if (!preferencesSrc.includes(`"${ut}"`)) {
|
||||
unmapped.push(ut);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(unmapped, [], `Unmapped unitTypes in preferences-models.ts: ${unmapped.join(", ")}`);
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// metrics.ts: classifyUnitPhase coverage
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test("classifyUnitPhase includes discussion phase", () => {
|
||||
assert.ok(metricsSrc.includes('"discussion"'), "MetricsPhase should include discussion");
|
||||
});
|
||||
|
||||
test("classifyUnitPhase maps discuss-milestone and discuss-slice", () => {
|
||||
assert.ok(metricsSrc.includes('"discuss-milestone"'), "missing discuss-milestone in metrics");
|
||||
assert.ok(metricsSrc.includes('"discuss-slice"'), "missing discuss-slice in metrics");
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// auto-post-unit.ts: LIFECYCLE_ONLY_UNITS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test("LIFECYCLE_ONLY_UNITS includes discuss-slice", () => {
|
||||
assert.ok(postUnitSrc.includes('"discuss-slice"'), "discuss-slice should be lifecycle-only");
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// auto-dashboard.ts: display label coverage
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test("unitVerb handles discuss-slice", () => {
|
||||
assert.ok(dashboardSrc.includes('"discuss-slice"'), "missing discuss-slice in dashboard");
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// auto-artifact-paths.ts: artifact resolution
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test("resolveExpectedArtifactPath handles discuss-slice", () => {
|
||||
assert.ok(artifactSrc.includes('"discuss-slice"'), "missing discuss-slice in artifact paths");
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue