diff --git a/.opencode/skills/dbt-analyze/SKILL.md b/.opencode/skills/dbt-analyze/SKILL.md index f993e17974..d300284e86 100644 --- a/.opencode/skills/dbt-analyze/SKILL.md +++ b/.opencode/skills/dbt-analyze/SKILL.md @@ -111,7 +111,7 @@ If no manifest is available: 1. Run `lineage_check` on the changed SQL 2. Show column-level data flow 3. Note: downstream impact requires a manifest -4. Suggest: `altimate-dbt build-project` to generate one +4. Suggest: `altimate-dbt build` to generate one ## Common Mistakes diff --git a/.opencode/skills/dbt-analyze/references/altimate-dbt-commands.md b/.opencode/skills/dbt-analyze/references/altimate-dbt-commands.md index 6dbb8e9731..8109ac84d2 100644 --- a/.opencode/skills/dbt-analyze/references/altimate-dbt-commands.md +++ b/.opencode/skills/dbt-analyze/references/altimate-dbt-commands.md @@ -20,10 +20,10 @@ altimate-dbt info # Project name, adapter, root ## Build & Run ```bash -altimate-dbt build --model [--downstream] # compile + run + test +altimate-dbt build # full project build (compile + run + test) +altimate-dbt build --model [--downstream] # build a single model altimate-dbt run --model [--downstream] # materialize only altimate-dbt test --model # run tests only -altimate-dbt build-project # full project build ``` ## Compile diff --git a/.opencode/skills/dbt-develop/references/altimate-dbt-commands.md b/.opencode/skills/dbt-develop/references/altimate-dbt-commands.md index 6dbb8e9731..8109ac84d2 100644 --- a/.opencode/skills/dbt-develop/references/altimate-dbt-commands.md +++ b/.opencode/skills/dbt-develop/references/altimate-dbt-commands.md @@ -20,10 +20,10 @@ altimate-dbt info # Project name, adapter, root ## Build & Run ```bash -altimate-dbt build --model [--downstream] # compile + run + test +altimate-dbt build # full project build (compile + run + test) +altimate-dbt build --model [--downstream] # build a single model altimate-dbt run --model [--downstream] # materialize only altimate-dbt test --model # run tests only -altimate-dbt build-project # full project build ``` ## Compile diff --git a/.opencode/skills/dbt-docs/references/altimate-dbt-commands.md b/.opencode/skills/dbt-docs/references/altimate-dbt-commands.md index 6dbb8e9731..8109ac84d2 100644 --- a/.opencode/skills/dbt-docs/references/altimate-dbt-commands.md +++ b/.opencode/skills/dbt-docs/references/altimate-dbt-commands.md @@ -20,10 +20,10 @@ altimate-dbt info # Project name, adapter, root ## Build & Run ```bash -altimate-dbt build --model [--downstream] # compile + run + test +altimate-dbt build # full project build (compile + run + test) +altimate-dbt build --model [--downstream] # build a single model altimate-dbt run --model [--downstream] # materialize only altimate-dbt test --model # run tests only -altimate-dbt build-project # full project build ``` ## Compile diff --git a/.opencode/skills/dbt-test/references/altimate-dbt-commands.md b/.opencode/skills/dbt-test/references/altimate-dbt-commands.md index 6dbb8e9731..8109ac84d2 100644 --- a/.opencode/skills/dbt-test/references/altimate-dbt-commands.md +++ b/.opencode/skills/dbt-test/references/altimate-dbt-commands.md @@ -20,10 +20,10 @@ altimate-dbt info # Project name, adapter, root ## Build & Run ```bash -altimate-dbt build --model [--downstream] # compile + run + test +altimate-dbt build # full project build (compile + run + test) +altimate-dbt build --model [--downstream] # build a single model altimate-dbt run --model [--downstream] # materialize only altimate-dbt test --model # run tests only -altimate-dbt build-project # full project build ``` ## Compile diff --git a/.opencode/skills/dbt-troubleshoot/references/altimate-dbt-commands.md b/.opencode/skills/dbt-troubleshoot/references/altimate-dbt-commands.md index 6dbb8e9731..8109ac84d2 100644 --- a/.opencode/skills/dbt-troubleshoot/references/altimate-dbt-commands.md +++ b/.opencode/skills/dbt-troubleshoot/references/altimate-dbt-commands.md @@ -20,10 +20,10 @@ altimate-dbt info # Project name, adapter, root ## Build & Run ```bash -altimate-dbt build --model [--downstream] # compile + run + test +altimate-dbt build # full project build (compile + run + test) +altimate-dbt build --model [--downstream] # build a single model altimate-dbt run --model [--downstream] # materialize only altimate-dbt test --model # run tests only -altimate-dbt build-project # full project build ``` ## Compile diff --git a/packages/dbt-tools/src/commands/build.ts b/packages/dbt-tools/src/commands/build.ts index 07432ea4ad..5d796c764f 100644 --- a/packages/dbt-tools/src/commands/build.ts +++ b/packages/dbt-tools/src/commands/build.ts @@ -2,7 +2,7 @@ import type { DBTProjectIntegrationAdapter, CommandProcessResult } from "@altima export async function build(adapter: DBTProjectIntegrationAdapter, args: string[]) { const model = flag(args, "model") - if (!model) return { error: "Missing --model" } + if (!model) return project(adapter) const downstream = args.includes("--downstream") const result = await adapter.unsafeBuildModelImmediately({ plusOperatorLeft: "", diff --git a/packages/dbt-tools/src/index.ts b/packages/dbt-tools/src/index.ts index 24cb8a9ce5..ff7fdd9d3c 100644 --- a/packages/dbt-tools/src/index.ts +++ b/packages/dbt-tools/src/index.ts @@ -11,10 +11,9 @@ const USAGE = { info: "Get project info (paths, targets, version)", compile: "Compile a model (Jinja to SQL) --model ", "compile-query": "Compile a raw query --query [--model ]", - build: "Build a model --model [--downstream]", + build: "Build project, or a single model with --model [--downstream]", run: "Run a model --model [--downstream]", test: "Test a model --model ", - "build-project": "Build entire project", execute: "Execute SQL --query [--model ] [--limit ]", columns: "Get columns of model --model ", "columns-source": "Get columns of source --source --table ", @@ -171,9 +170,6 @@ async function main() { case "test": result = await (await import("./commands/build")).test(adapter, rest) break - case "build-project": - result = await (await import("./commands/build")).project(adapter) - break case "execute": result = await (await import("./commands/execute")).execute(adapter, rest) break diff --git a/packages/dbt-tools/test/build.test.ts b/packages/dbt-tools/test/build.test.ts new file mode 100644 index 0000000000..f73a89af9b --- /dev/null +++ b/packages/dbt-tools/test/build.test.ts @@ -0,0 +1,57 @@ +import { describe, test, expect, mock } from "bun:test" +import { build } from "../src/commands/build" +import type { DBTProjectIntegrationAdapter } from "@altimateai/dbt-integration" + +function makeAdapter(overrides: Partial = {}): DBTProjectIntegrationAdapter { + return { + unsafeBuildModelImmediately: mock(() => Promise.resolve({ stdout: "model built", stderr: "" })), + unsafeBuildProjectImmediately: mock(() => Promise.resolve({ stdout: "project built", stderr: "" })), + unsafeRunModelImmediately: mock(() => Promise.resolve({ stdout: "", stderr: "" })), + unsafeRunModelTestImmediately: mock(() => Promise.resolve({ stdout: "", stderr: "" })), + dispose: mock(() => Promise.resolve()), + ...overrides, + } as unknown as DBTProjectIntegrationAdapter +} + +describe("build command", () => { + test("build without --model builds entire project", async () => { + const adapter = makeAdapter() + const result = await build(adapter, []) + expect(adapter.unsafeBuildProjectImmediately).toHaveBeenCalledTimes(1) + expect(adapter.unsafeBuildModelImmediately).not.toHaveBeenCalled() + expect(result).toEqual({ stdout: "project built" }) + }) + + test("build --model builds single model", async () => { + const adapter = makeAdapter() + const result = await build(adapter, ["--model", "orders"]) + expect(adapter.unsafeBuildModelImmediately).toHaveBeenCalledTimes(1) + expect(adapter.unsafeBuildModelImmediately).toHaveBeenCalledWith({ + plusOperatorLeft: "", + modelName: "orders", + plusOperatorRight: "", + }) + expect(adapter.unsafeBuildProjectImmediately).not.toHaveBeenCalled() + expect(result).toEqual({ stdout: "model built" }) + }) + + test("build --model --downstream sets plusOperatorRight", async () => { + const adapter = makeAdapter() + await build(adapter, ["--model", "orders", "--downstream"]) + expect(adapter.unsafeBuildModelImmediately).toHaveBeenCalledWith({ + plusOperatorLeft: "", + modelName: "orders", + plusOperatorRight: "+", + }) + }) + + test("build surfaces stderr as error", async () => { + const adapter = makeAdapter({ + unsafeBuildProjectImmediately: mock(() => + Promise.resolve({ stdout: "partial output", stderr: "compilation error", fullOutput: "" }), + ), + }) + const result = await build(adapter, []) + expect(result).toEqual({ error: "compilation error", stdout: "partial output" }) + }) +}) diff --git a/packages/opencode/src/altimate/telemetry/index.ts b/packages/opencode/src/altimate/telemetry/index.ts index 0767b9210e..9e1564eae6 100644 --- a/packages/opencode/src/altimate/telemetry/index.ts +++ b/packages/opencode/src/altimate/telemetry/index.ts @@ -349,6 +349,8 @@ export namespace Telemetry { skill_name: string skill_source: "builtin" | "global" | "project" duration_ms: number + has_followups: boolean + followup_count: number } // altimate_change start — first_launch event for new user counting (privacy-safe: only version + machine_id) | { diff --git a/packages/opencode/src/skill/followups.ts b/packages/opencode/src/skill/followups.ts new file mode 100644 index 0000000000..03904b22e1 --- /dev/null +++ b/packages/opencode/src/skill/followups.ts @@ -0,0 +1,183 @@ +// altimate_change start — skill follow-up suggestions for conversational engagement +export namespace SkillFollowups { + export interface Suggestion { + skill: string // skill name to suggest + label: string // short display label + description: string // why this is a good next step + condition?: string // optional: when this suggestion applies + } + + // Map from skill name to follow-up suggestions + const FOLLOWUPS: Record = { + "dbt-develop": [ + { + skill: "dbt-test", + label: "Add tests", + description: "Write schema tests and unit tests for the model you just created to ensure data quality.", + }, + { + skill: "dbt-docs", + label: "Document your model", + description: "Add descriptions to your model and columns in schema.yml for discoverability.", + }, + { + skill: "dbt-analyze", + label: "Check downstream impact", + description: "Analyze the blast radius of your changes on downstream models before merging.", + }, + { + skill: "sql-review", + label: "Review SQL quality", + description: "Run a quality gate on your SQL — lint for anti-patterns and grade readability.", + }, + ], + "dbt-troubleshoot": [ + { + skill: "dbt-test", + label: "Add regression tests", + description: "Now that the bug is fixed, add tests to prevent it from recurring.", + }, + { + skill: "dbt-analyze", + label: "Check downstream impact", + description: "Verify your fix didn't break downstream models.", + }, + { + skill: "dbt-develop", + label: "Improve the model", + description: "Refactor or extend the model now that it's working correctly.", + }, + ], + "dbt-test": [ + { + skill: "dbt-develop", + label: "Build more models", + description: "Continue building new models in your dbt project.", + }, + { + skill: "dbt-docs", + label: "Document tested models", + description: "Add documentation to the models you just tested.", + }, + ], + "dbt-docs": [ + { + skill: "dbt-test", + label: "Add tests", + description: "Add data quality tests for the models you just documented.", + }, + { + skill: "dbt-analyze", + label: "Analyze lineage", + description: "Review column-level lineage to ensure documentation matches data flow.", + }, + ], + "dbt-analyze": [ + { + skill: "dbt-test", + label: "Add tests for affected models", + description: "Add tests to downstream models that could be impacted by changes.", + }, + { + skill: "dbt-develop", + label: "Make the changes", + description: "Proceed with implementing the changes now that you understand the impact.", + }, + ], + "sql-review": [ + { + skill: "query-optimize", + label: "Optimize performance", + description: "Improve query performance based on the review findings.", + }, + { + skill: "sql-translate", + label: "Translate to another dialect", + description: "Port this SQL to a different database dialect.", + }, + ], + "sql-translate": [ + { + skill: "sql-review", + label: "Review translated SQL", + description: "Run a quality check on the translated SQL to catch dialect-specific issues.", + }, + ], + "query-optimize": [ + { + skill: "sql-review", + label: "Review optimized query", + description: "Run a quality gate on the optimized SQL.", + }, + { + skill: "cost-report", + label: "Check cost impact", + description: "Analyze how the optimization affects query costs.", + }, + ], + "cost-report": [ + { + skill: "query-optimize", + label: "Optimize expensive queries", + description: "Optimize the most expensive queries identified in the report.", + }, + ], + "pii-audit": [ + { + skill: "sql-review", + label: "Review SQL for PII exposure", + description: "Check specific queries for PII leakage.", + }, + ], + "lineage-diff": [ + { + skill: "dbt-analyze", + label: "Full impact analysis", + description: "Run a comprehensive impact analysis on the changed models.", + }, + { + skill: "dbt-test", + label: "Add tests for changed paths", + description: "Add tests covering the changed data flow paths.", + }, + ], + "schema-migration": [ + { + skill: "dbt-develop", + label: "Update dbt models", + description: "Update your dbt models to reflect the schema changes.", + }, + ], + } + + // A special warehouse nudge for users who haven't connected yet + const WAREHOUSE_NUDGE = "**Tip:** Connect a warehouse to validate against real data. Run `/discover` to auto-detect your connections." + + export function get(skillName: string): readonly Suggestion[] { + return Object.freeze(FOLLOWUPS[skillName] ?? []) + } + + export function format(skillName: string): string { + const suggestions = get(skillName) + if (suggestions.length === 0) return "" + + const lines = [ + "", + "---", + "", + "## What's Next?", + "", + "Now that this task is complete, here are suggested next steps:", + "", + ...suggestions.map( + (s, i) => `${i + 1}. **${s.label}** — ${s.description} → Use \`/skill ${s.skill}\` or just ask me.`, + ), + "", + WAREHOUSE_NUDGE, + "", + "*You can continue this conversation — just type your next request.*", + ] + return lines.join("\n") + } +} +// altimate_change end diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 77e14ffdb0..f3f26d8a1f 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -5,6 +5,9 @@ import { Tool } from "./tool" import { Skill } from "../skill" import { Ripgrep } from "../file/ripgrep" import { iife } from "@/util/iife" +// altimate_change start — import follow-up suggestions for conversational engagement +import { SkillFollowups } from "../skill/followups" +// altimate_change end // altimate_change start - import for LLM-based dynamic skill selection import { Fingerprint } from "../altimate/fingerprint" import { Config } from "../config/config" @@ -140,6 +143,10 @@ export const SkillTool = Tool.define("skill", async (ctx) => { }).then((f) => f.map((file) => `${file}`).join("\n")) // altimate_change end + // altimate_change start — append follow-up suggestions after skill content + const followups = SkillFollowups.format(skill.name) + // altimate_change end + // altimate_change start — telemetry instrumentation for skill loading try { Telemetry.track({ @@ -150,15 +157,19 @@ export const SkillTool = Tool.define("skill", async (ctx) => { skill_name: skill.name, skill_source: classifySkillSource(skill.location), duration_ms: Date.now() - startTime, + has_followups: followups.length > 0, + followup_count: SkillFollowups.get(skill.name).length, }) } catch { // Telemetry must never break skill loading } // altimate_change end + // altimate_change start — custom return with follow-ups, file listing, and base directory return { title: `Loaded skill: ${skill.name}`, output: [ + ...(followups ? [followups, ""] : []), ``, `# Skill: ${skill.name}`, "", @@ -178,6 +189,7 @@ export const SkillTool = Tool.define("skill", async (ctx) => { dir, }, } + // altimate_change end }, } }) diff --git a/packages/opencode/test/skill/followups.test.ts b/packages/opencode/test/skill/followups.test.ts new file mode 100644 index 0000000000..7112ba4319 --- /dev/null +++ b/packages/opencode/test/skill/followups.test.ts @@ -0,0 +1,131 @@ +import { describe, test, expect } from "bun:test" +import { SkillFollowups } from "../../src/skill/followups" + +describe("SkillFollowups", () => { + describe("get", () => { + test("returns suggestions for known dbt skills", () => { + const suggestions = SkillFollowups.get("dbt-develop") + expect(suggestions.length).toBeGreaterThan(0) + expect(suggestions[0]).toHaveProperty("skill") + expect(suggestions[0]).toHaveProperty("label") + expect(suggestions[0]).toHaveProperty("description") + }) + + test("returns suggestions for dbt-troubleshoot", () => { + const suggestions = SkillFollowups.get("dbt-troubleshoot") + expect(suggestions.length).toBeGreaterThan(0) + // First suggestion should be to add regression tests + expect(suggestions[0].skill).toBe("dbt-test") + }) + + test("returns suggestions for sql-review", () => { + const suggestions = SkillFollowups.get("sql-review") + expect(suggestions.length).toBeGreaterThan(0) + }) + + test("returns empty array for unknown skill", () => { + const suggestions = SkillFollowups.get("nonexistent-skill") + expect(suggestions).toEqual([]) + }) + + test("returns empty array for skills without followups", () => { + const suggestions = SkillFollowups.get("teach") + expect(suggestions).toEqual([]) + }) + }) + + describe("format", () => { + test("returns formatted follow-up section for dbt-develop", () => { + const output = SkillFollowups.format("dbt-develop") + expect(output).toContain("## What's Next?") + expect(output).toContain("Add tests") + expect(output).toContain("dbt-test") + expect(output).toContain("Document your model") + expect(output).toContain("dbt-docs") + expect(output).toContain("/discover") + expect(output).toContain("You can continue this conversation") + }) + + test("returns formatted follow-up section for dbt-troubleshoot", () => { + const output = SkillFollowups.format("dbt-troubleshoot") + expect(output).toContain("## What's Next?") + expect(output).toContain("regression tests") + expect(output).toContain("downstream") + }) + + test("returns empty string for skill without followups", () => { + const output = SkillFollowups.format("nonexistent-skill") + expect(output).toBe("") + }) + + test("format includes numbered suggestions", () => { + const output = SkillFollowups.format("dbt-develop") + expect(output).toContain("1.") + expect(output).toContain("2.") + expect(output).toContain("3.") + }) + + test("format includes warehouse nudge", () => { + const output = SkillFollowups.format("dbt-develop") + expect(output).toContain("Connect a warehouse") + expect(output).toContain("/discover") + }) + + test("all dbt skills have follow-ups defined", () => { + const dbtSkills = ["dbt-develop", "dbt-troubleshoot", "dbt-test", "dbt-docs", "dbt-analyze"] + for (const skill of dbtSkills) { + const suggestions = SkillFollowups.get(skill) + expect(suggestions.length).toBeGreaterThan(0) + } + }) + + test("follow-up suggestions reference valid skill names", () => { + const KNOWN_SKILLS = [ + "dbt-develop", + "dbt-troubleshoot", + "dbt-test", + "dbt-docs", + "dbt-analyze", + "sql-review", + "sql-translate", + "query-optimize", + "cost-report", + "pii-audit", + "lineage-diff", + "schema-migration", + "data-viz", + "altimate-setup", + "teach", + "train", + "training-status", + ] + // Check all follow-up skills point to known skills + for (const skillName of KNOWN_SKILLS) { + const suggestions = SkillFollowups.get(skillName) + for (const s of suggestions) { + expect(KNOWN_SKILLS).toContain(s.skill) + } + } + }) + + test("no skill suggests itself as a follow-up", () => { + const skills = [ + "dbt-develop", + "dbt-troubleshoot", + "dbt-test", + "dbt-docs", + "dbt-analyze", + "sql-review", + "sql-translate", + "query-optimize", + "cost-report", + ] + for (const skillName of skills) { + const suggestions = SkillFollowups.get(skillName) + for (const s of suggestions) { + expect(s.skill).not.toBe(skillName) + } + } + }) + }) +}) diff --git a/packages/opencode/test/skill/skill-followups-integration.test.ts b/packages/opencode/test/skill/skill-followups-integration.test.ts new file mode 100644 index 0000000000..c022d8be0a --- /dev/null +++ b/packages/opencode/test/skill/skill-followups-integration.test.ts @@ -0,0 +1,99 @@ +import { test, expect } from "bun:test" +import { Skill } from "../../src/skill" +import { SkillFollowups } from "../../src/skill/followups" +import { Instance } from "../../src/project/instance" +import { tmpdir } from "../fixture/fixture" +import path from "path" + +test("skill with followups: format appears before skill_content", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + const skillDir = path.join(dir, ".opencode", "skill", "dbt-develop") + await Bun.write( + path.join(skillDir, "SKILL.md"), + `--- +name: dbt-develop +description: Create dbt models. +--- + +# dbt Model Development + +Instructions here. +`, + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const skill = await Skill.get("dbt-develop") + expect(skill).toBeDefined() + + // Verify followups exist and are well-formed + const followups = SkillFollowups.format("dbt-develop") + expect(followups).toContain("## What's Next?") + expect(followups).toContain("dbt-test") + expect(followups).toContain("dbt-docs") + + // Simulate the output assembly order from SkillTool to verify + // followups come BEFORE (survives truncation) + const output = [ + ...(followups ? [followups, ""] : []), + ``, + skill!.content.trim(), + "", + ].join("\n") + + const followupsIdx = output.indexOf("## What's Next?") + const skillContentIdx = output.indexOf(" { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + const skillDir = path.join(dir, ".opencode", "skill", "custom-skill") + await Bun.write( + path.join(skillDir, "SKILL.md"), + `--- +name: custom-skill +description: A custom skill. +--- + +# Custom Skill + +Do custom things. +`, + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const skill = await Skill.get("custom-skill") + expect(skill).toBeDefined() + + // No followups for unknown skills + const followups = SkillFollowups.format("custom-skill") + expect(followups).toBe("") + + // Output should start directly with skill_content + const output = [ + ...(followups ? [followups, ""] : []), + ``, + skill!.content.trim(), + "", + ].join("\n") + + expect(output).toMatch(/^ { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + const skillDir = path.join(dir, ".opencode", "skill", "dbt-develop") + await Bun.write( + path.join(skillDir, "SKILL.md"), + `--- +name: dbt-develop +description: Create dbt models. +--- + +# dbt Model Development + +Build models with dbt. +`, + ) + }, + }) + + const home = process.env.OPENCODE_TEST_HOME + process.env.OPENCODE_TEST_HOME = tmp.path + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const tool = await SkillTool.init() + const ctx: Tool.Context = { + ...baseCtx, + ask: async () => {}, + } + + const result = await tool.execute({ name: "dbt-develop" }, ctx) + + // Follow-ups present + expect(result.output).toContain("## What's Next?") + expect(result.output).toContain("dbt-test") + expect(result.output).toContain("dbt-docs") + expect(result.output).toContain("dbt-analyze") + expect(result.output).toContain("/discover") + + // Follow-ups appear BEFORE skill_content (truncation-safe) + const followupsIdx = result.output.indexOf("## What's Next?") + const contentIdx = result.output.indexOf("`) + expect(result.output).toContain("Build models with dbt.") + expect(result.output).toContain("") + }, + }) + } finally { + process.env.OPENCODE_TEST_HOME = home + } + }) + + test("execute omits follow-up section for skills without followup mappings", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + const skillDir = path.join(dir, ".opencode", "skill", "custom-skill") + await Bun.write( + path.join(skillDir, "SKILL.md"), + `--- +name: custom-skill +description: A custom skill with no followups. +--- + +# Custom Skill + +Do custom things. +`, + ) + }, + }) + + const home = process.env.OPENCODE_TEST_HOME + process.env.OPENCODE_TEST_HOME = tmp.path + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const tool = await SkillTool.init() + const ctx: Tool.Context = { + ...baseCtx, + ask: async () => {}, + } + + const result = await tool.execute({ name: "custom-skill" }, ctx) + + // No follow-up section + expect(result.output).not.toContain("## What's Next?") + expect(result.output).not.toContain("/discover") + + // Output starts directly with skill_content + expect(result.output).toMatch(/^ { // Pre-populate cache — if selector were called, it would return this cached subset