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:
Jeremy 2026-03-27 12:36:42 -05:00
parent 36930694e4
commit c622eec0e1
11 changed files with 248 additions and 23 deletions

View file

@ -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":

View file

@ -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}`;

View file

@ -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",

View file

@ -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",

View file

@ -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";

View file

@ -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`.

View file

@ -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", {

View file

@ -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);
}

View file

@ -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/")) {

View file

@ -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;
}

View file

@ -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");
});