From 99aacab4bb1d53db6ccd7a74c66e07651e7286fd Mon Sep 17 00:00:00 2001 From: Michiel De Smet Date: Wed, 25 Mar 2026 15:29:38 -0700 Subject: [PATCH 1/7] feat(dbt-tools): build entire project when no --model flag given `altimate-dbt build` without arguments now builds the whole project via `unsafeBuildProjectImmediately`, replacing the need for the separate `build-project` command. Updated all dbt skill references accordingly. Co-Authored-By: Claude Sonnet 4.6 --- .opencode/skills/dbt-analyze/SKILL.md | 2 +- .../skills/dbt-analyze/references/altimate-dbt-commands.md | 4 ++-- .../skills/dbt-develop/references/altimate-dbt-commands.md | 4 ++-- .opencode/skills/dbt-docs/references/altimate-dbt-commands.md | 4 ++-- .opencode/skills/dbt-test/references/altimate-dbt-commands.md | 4 ++-- .../dbt-troubleshoot/references/altimate-dbt-commands.md | 4 ++-- packages/dbt-tools/src/commands/build.ts | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) 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..6bad491756 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..6bad491756 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..6bad491756 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..6bad491756 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..6bad491756 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: "", From a885a7e55e899b9458f0298c528aee35648c1652 Mon Sep 17 00:00:00 2001 From: Michiel De Smet Date: Wed, 25 Mar 2026 16:58:33 -0700 Subject: [PATCH 2/7] test(dbt-tools): add unit tests for build command routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers: no-model → project build, --model → single model, --downstream flag. Co-Authored-By: Claude Sonnet 4.6 --- packages/dbt-tools/test/build.test.ts | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 packages/dbt-tools/test/build.test.ts diff --git a/packages/dbt-tools/test/build.test.ts b/packages/dbt-tools/test/build.test.ts new file mode 100644 index 0000000000..586c1b1f19 --- /dev/null +++ b/packages/dbt-tools/test/build.test.ts @@ -0,0 +1,47 @@ +import { describe, test, expect, mock } from "bun:test" +import { build, project } 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: "+", + }) + }) +}) From b00c1cd40eff6e472d5b0c4003af70b3ff7f7dbe Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Thu, 26 Mar 2026 12:05:39 -0700 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20address=20code=20review=20=E2=80=94?= =?UTF-8?q?=20remove=20`build-project`=20command,=20update=20help=20text,?= =?UTF-8?q?=20add=20stderr=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove `build-project` from command map and switch case in `index.ts` (now redundant since `build` without `--model` does the same thing) - Update `build` help text to reflect optional `--model` - Remove unused `project` import from `build.test.ts` - Add test for `format()` stderr error path - Fix comment alignment in all 5 `altimate-dbt-commands.md` reference files Co-Authored-By: Claude Opus 4.6 (1M context) --- .../dbt-analyze/references/altimate-dbt-commands.md | 2 +- .../dbt-develop/references/altimate-dbt-commands.md | 2 +- .../dbt-docs/references/altimate-dbt-commands.md | 2 +- .../dbt-test/references/altimate-dbt-commands.md | 2 +- .../references/altimate-dbt-commands.md | 2 +- packages/dbt-tools/src/index.ts | 6 +----- packages/dbt-tools/test/build.test.ts | 12 +++++++++++- 7 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.opencode/skills/dbt-analyze/references/altimate-dbt-commands.md b/.opencode/skills/dbt-analyze/references/altimate-dbt-commands.md index 6bad491756..8109ac84d2 100644 --- a/.opencode/skills/dbt-analyze/references/altimate-dbt-commands.md +++ b/.opencode/skills/dbt-analyze/references/altimate-dbt-commands.md @@ -20,7 +20,7 @@ altimate-dbt info # Project name, adapter, root ## Build & Run ```bash -altimate-dbt build # full project build (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 diff --git a/.opencode/skills/dbt-develop/references/altimate-dbt-commands.md b/.opencode/skills/dbt-develop/references/altimate-dbt-commands.md index 6bad491756..8109ac84d2 100644 --- a/.opencode/skills/dbt-develop/references/altimate-dbt-commands.md +++ b/.opencode/skills/dbt-develop/references/altimate-dbt-commands.md @@ -20,7 +20,7 @@ altimate-dbt info # Project name, adapter, root ## Build & Run ```bash -altimate-dbt build # full project build (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 diff --git a/.opencode/skills/dbt-docs/references/altimate-dbt-commands.md b/.opencode/skills/dbt-docs/references/altimate-dbt-commands.md index 6bad491756..8109ac84d2 100644 --- a/.opencode/skills/dbt-docs/references/altimate-dbt-commands.md +++ b/.opencode/skills/dbt-docs/references/altimate-dbt-commands.md @@ -20,7 +20,7 @@ altimate-dbt info # Project name, adapter, root ## Build & Run ```bash -altimate-dbt build # full project build (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 diff --git a/.opencode/skills/dbt-test/references/altimate-dbt-commands.md b/.opencode/skills/dbt-test/references/altimate-dbt-commands.md index 6bad491756..8109ac84d2 100644 --- a/.opencode/skills/dbt-test/references/altimate-dbt-commands.md +++ b/.opencode/skills/dbt-test/references/altimate-dbt-commands.md @@ -20,7 +20,7 @@ altimate-dbt info # Project name, adapter, root ## Build & Run ```bash -altimate-dbt build # full project build (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 diff --git a/.opencode/skills/dbt-troubleshoot/references/altimate-dbt-commands.md b/.opencode/skills/dbt-troubleshoot/references/altimate-dbt-commands.md index 6bad491756..8109ac84d2 100644 --- a/.opencode/skills/dbt-troubleshoot/references/altimate-dbt-commands.md +++ b/.opencode/skills/dbt-troubleshoot/references/altimate-dbt-commands.md @@ -20,7 +20,7 @@ altimate-dbt info # Project name, adapter, root ## Build & Run ```bash -altimate-dbt build # full project build (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 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 index 586c1b1f19..27fb116f1e 100644 --- a/packages/dbt-tools/test/build.test.ts +++ b/packages/dbt-tools/test/build.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, mock } from "bun:test" -import { build, project } from "../src/commands/build" +import { build } from "../src/commands/build" import type { DBTProjectIntegrationAdapter } from "@altimateai/dbt-integration" function makeAdapter(overrides: Partial = {}): DBTProjectIntegrationAdapter { @@ -44,4 +44,14 @@ describe("build command", () => { plusOperatorRight: "+", }) }) + + test("build surfaces stderr as error", async () => { + const adapter = makeAdapter({ + unsafeBuildProjectImmediately: mock(() => + Promise.resolve({ stdout: "partial output", stderr: "compilation error" }), + ), + }) + const result = await build(adapter, []) + expect(result).toEqual({ error: "compilation error", stdout: "partial output" }) + }) }) From 1afbecb969f4c2e7e970ad5c2ec161416e65cbc4 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Thu, 26 Mar 2026 12:06:06 -0700 Subject: [PATCH 4/7] fix: add missing `fullOutput` field to satisfy `CommandProcessResult` type Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/dbt-tools/test/build.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dbt-tools/test/build.test.ts b/packages/dbt-tools/test/build.test.ts index 27fb116f1e..f73a89af9b 100644 --- a/packages/dbt-tools/test/build.test.ts +++ b/packages/dbt-tools/test/build.test.ts @@ -48,7 +48,7 @@ describe("build command", () => { test("build surfaces stderr as error", async () => { const adapter = makeAdapter({ unsafeBuildProjectImmediately: mock(() => - Promise.resolve({ stdout: "partial output", stderr: "compilation error" }), + Promise.resolve({ stdout: "partial output", stderr: "compilation error", fullOutput: "" }), ), }) const result = await build(adapter, []) From 970b3c7d2bbb668dc12963f328d7f1bfb45d7b84 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sat, 28 Mar 2026 10:14:49 -0700 Subject: [PATCH 5/7] feat: add follow-up suggestions after skill completion to reduce first-run churn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Telemetry analysis showed 78% of users churn after a single 2.4-minute session, mostly dbt developers who run one skill and leave. Users who reach warehouse/schema tools retain at 24.8% vs 3.3% for those who don't. This adds contextual "What's Next?" suggestions after skill execution: - Maps each skill to relevant follow-up skills (e.g., `dbt-develop` → `dbt-test`, `dbt-docs`) - Includes a warehouse discovery nudge to bridge non-warehouse users - Appended after `` so it doesn't interfere with skill parsing - Covers all dbt skills, SQL skills, and data quality skills (12 skills total) Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/opencode/src/skill/followups.ts | 183 ++++++++++++++++++ packages/opencode/src/tool/skill.ts | 8 + .../opencode/test/skill/followups.test.ts | 131 +++++++++++++ .../skill/skill-followups-integration.test.ts | 74 +++++++ 4 files changed, 396 insertions(+) create mode 100644 packages/opencode/src/skill/followups.ts create mode 100644 packages/opencode/test/skill/followups.test.ts create mode 100644 packages/opencode/test/skill/skill-followups-integration.test.ts diff --git a/packages/opencode/src/skill/followups.ts b/packages/opencode/src/skill/followups.ts new file mode 100644 index 0000000000..1f3ddcc96c --- /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): Suggestion[] { + return 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..ed1ed5d6d5 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" @@ -156,6 +159,10 @@ export const SkillTool = Tool.define("skill", async (ctx) => { } // altimate_change end + // altimate_change start — append follow-up suggestions after skill content + const followups = SkillFollowups.format(skill.name) + // altimate_change end + return { title: `Loaded skill: ${skill.name}`, output: [ @@ -172,6 +179,7 @@ export const SkillTool = Tool.define("skill", async (ctx) => { files, "", "", + followups, ].join("\n"), metadata: { name: skill.name, 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..de80c74649 --- /dev/null +++ b/packages/opencode/test/skill/skill-followups-integration.test.ts @@ -0,0 +1,74 @@ +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 tool output includes follow-up suggestions for skills with followups", 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 for this skill + const followups = SkillFollowups.format("dbt-develop") + expect(followups).toContain("## What's Next?") + expect(followups).toContain("dbt-test") + expect(followups).toContain("dbt-docs") + }, + }) +}) + +test("skill tool output has no follow-ups for skills without followups defined", 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. +--- + +# 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("") + }, + }) +}) From 930a4179f4c7804ba8ff85b2c917197ad3b5999e Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sat, 28 Mar 2026 17:37:53 -0700 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20address=20code=20review=20=E2=80=94?= =?UTF-8?q?=20truncation-safe=20followups,=20telemetry,=20immutable=20`get?= =?UTF-8?q?()`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move follow-up suggestions before `` so they survive output truncation - Add `has_followups` and `followup_count` to `skill_used` telemetry event - Return `readonly Suggestion[]` via `Object.freeze()` from `get()` to prevent shared state mutation - Rewrite integration tests to verify followups appear before `` in output assembly - Wrap custom return block in `altimate_change` markers for upstream safety Co-Authored-By: Claude Opus 4.6 (1M context) --- .../opencode/src/altimate/telemetry/index.ts | 2 ++ packages/opencode/src/skill/followups.ts | 4 +-- packages/opencode/src/tool/skill.ts | 14 ++++++--- .../skill/skill-followups-integration.test.ts | 31 +++++++++++++++++-- 4 files changed, 41 insertions(+), 10 deletions(-) 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 index 1f3ddcc96c..03904b22e1 100644 --- a/packages/opencode/src/skill/followups.ts +++ b/packages/opencode/src/skill/followups.ts @@ -153,8 +153,8 @@ export namespace SkillFollowups { // 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): Suggestion[] { - return FOLLOWUPS[skillName] ?? [] + export function get(skillName: string): readonly Suggestion[] { + return Object.freeze(FOLLOWUPS[skillName] ?? []) } export function format(skillName: string): string { diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index ed1ed5d6d5..f3f26d8a1f 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -143,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({ @@ -153,19 +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 — append follow-up suggestions after skill content - const followups = SkillFollowups.format(skill.name) - // 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}`, "", @@ -179,13 +183,13 @@ export const SkillTool = Tool.define("skill", async (ctx) => { files, "", "", - followups, ].join("\n"), metadata: { name: skill.name, dir, }, } + // altimate_change end }, } }) diff --git a/packages/opencode/test/skill/skill-followups-integration.test.ts b/packages/opencode/test/skill/skill-followups-integration.test.ts index de80c74649..c022d8be0a 100644 --- a/packages/opencode/test/skill/skill-followups-integration.test.ts +++ b/packages/opencode/test/skill/skill-followups-integration.test.ts @@ -5,7 +5,7 @@ import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" import path from "path" -test("skill tool output includes follow-up suggestions for skills with followups", async () => { +test("skill with followups: format appears before skill_content", async () => { await using tmp = await tmpdir({ git: true, init: async (dir) => { @@ -31,16 +31,31 @@ Instructions here. const skill = await Skill.get("dbt-develop") expect(skill).toBeDefined() - // Verify followups exist for this skill + // 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(" { +test("skill without followups: no extra content before skill_content", async () => { await using tmp = await tmpdir({ git: true, init: async (dir) => { @@ -69,6 +84,16 @@ Do custom things. // 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(/^ Date: Sat, 28 Mar 2026 17:39:49 -0700 Subject: [PATCH 7/7] test: add e2e tests for skill follow-up suggestions via `SkillTool.execute` - Test that `dbt-develop` skill output includes follow-ups before `` - Test that unmapped skills produce no follow-up section - Both tests invoke the real `SkillTool.execute` path end-to-end Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/opencode/test/tool/skill.test.ts | 109 ++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts index b80610c635..be13e7f3ba 100644 --- a/packages/opencode/test/tool/skill.test.ts +++ b/packages/opencode/test/tool/skill.test.ts @@ -145,6 +145,115 @@ Use this skill. } }) + // altimate_change start — e2e tests for follow-up suggestions in skill tool output + test("execute includes follow-up suggestions before skill_content for mapped skills", 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 + +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