Skip to content

Commit 970b3c7

Browse files
anandgupta42claude
andcommitted
feat: add follow-up suggestions after skill completion to reduce first-run churn
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 `</skill_content>` 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) <noreply@anthropic.com>
1 parent 1afbecb commit 970b3c7

4 files changed

Lines changed: 396 additions & 0 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// altimate_change start — skill follow-up suggestions for conversational engagement
2+
export namespace SkillFollowups {
3+
export interface Suggestion {
4+
skill: string // skill name to suggest
5+
label: string // short display label
6+
description: string // why this is a good next step
7+
condition?: string // optional: when this suggestion applies
8+
}
9+
10+
// Map from skill name to follow-up suggestions
11+
const FOLLOWUPS: Record<string, Suggestion[]> = {
12+
"dbt-develop": [
13+
{
14+
skill: "dbt-test",
15+
label: "Add tests",
16+
description: "Write schema tests and unit tests for the model you just created to ensure data quality.",
17+
},
18+
{
19+
skill: "dbt-docs",
20+
label: "Document your model",
21+
description: "Add descriptions to your model and columns in schema.yml for discoverability.",
22+
},
23+
{
24+
skill: "dbt-analyze",
25+
label: "Check downstream impact",
26+
description: "Analyze the blast radius of your changes on downstream models before merging.",
27+
},
28+
{
29+
skill: "sql-review",
30+
label: "Review SQL quality",
31+
description: "Run a quality gate on your SQL — lint for anti-patterns and grade readability.",
32+
},
33+
],
34+
"dbt-troubleshoot": [
35+
{
36+
skill: "dbt-test",
37+
label: "Add regression tests",
38+
description: "Now that the bug is fixed, add tests to prevent it from recurring.",
39+
},
40+
{
41+
skill: "dbt-analyze",
42+
label: "Check downstream impact",
43+
description: "Verify your fix didn't break downstream models.",
44+
},
45+
{
46+
skill: "dbt-develop",
47+
label: "Improve the model",
48+
description: "Refactor or extend the model now that it's working correctly.",
49+
},
50+
],
51+
"dbt-test": [
52+
{
53+
skill: "dbt-develop",
54+
label: "Build more models",
55+
description: "Continue building new models in your dbt project.",
56+
},
57+
{
58+
skill: "dbt-docs",
59+
label: "Document tested models",
60+
description: "Add documentation to the models you just tested.",
61+
},
62+
],
63+
"dbt-docs": [
64+
{
65+
skill: "dbt-test",
66+
label: "Add tests",
67+
description: "Add data quality tests for the models you just documented.",
68+
},
69+
{
70+
skill: "dbt-analyze",
71+
label: "Analyze lineage",
72+
description: "Review column-level lineage to ensure documentation matches data flow.",
73+
},
74+
],
75+
"dbt-analyze": [
76+
{
77+
skill: "dbt-test",
78+
label: "Add tests for affected models",
79+
description: "Add tests to downstream models that could be impacted by changes.",
80+
},
81+
{
82+
skill: "dbt-develop",
83+
label: "Make the changes",
84+
description: "Proceed with implementing the changes now that you understand the impact.",
85+
},
86+
],
87+
"sql-review": [
88+
{
89+
skill: "query-optimize",
90+
label: "Optimize performance",
91+
description: "Improve query performance based on the review findings.",
92+
},
93+
{
94+
skill: "sql-translate",
95+
label: "Translate to another dialect",
96+
description: "Port this SQL to a different database dialect.",
97+
},
98+
],
99+
"sql-translate": [
100+
{
101+
skill: "sql-review",
102+
label: "Review translated SQL",
103+
description: "Run a quality check on the translated SQL to catch dialect-specific issues.",
104+
},
105+
],
106+
"query-optimize": [
107+
{
108+
skill: "sql-review",
109+
label: "Review optimized query",
110+
description: "Run a quality gate on the optimized SQL.",
111+
},
112+
{
113+
skill: "cost-report",
114+
label: "Check cost impact",
115+
description: "Analyze how the optimization affects query costs.",
116+
},
117+
],
118+
"cost-report": [
119+
{
120+
skill: "query-optimize",
121+
label: "Optimize expensive queries",
122+
description: "Optimize the most expensive queries identified in the report.",
123+
},
124+
],
125+
"pii-audit": [
126+
{
127+
skill: "sql-review",
128+
label: "Review SQL for PII exposure",
129+
description: "Check specific queries for PII leakage.",
130+
},
131+
],
132+
"lineage-diff": [
133+
{
134+
skill: "dbt-analyze",
135+
label: "Full impact analysis",
136+
description: "Run a comprehensive impact analysis on the changed models.",
137+
},
138+
{
139+
skill: "dbt-test",
140+
label: "Add tests for changed paths",
141+
description: "Add tests covering the changed data flow paths.",
142+
},
143+
],
144+
"schema-migration": [
145+
{
146+
skill: "dbt-develop",
147+
label: "Update dbt models",
148+
description: "Update your dbt models to reflect the schema changes.",
149+
},
150+
],
151+
}
152+
153+
// A special warehouse nudge for users who haven't connected yet
154+
const WAREHOUSE_NUDGE = "**Tip:** Connect a warehouse to validate against real data. Run `/discover` to auto-detect your connections."
155+
156+
export function get(skillName: string): Suggestion[] {
157+
return FOLLOWUPS[skillName] ?? []
158+
}
159+
160+
export function format(skillName: string): string {
161+
const suggestions = get(skillName)
162+
if (suggestions.length === 0) return ""
163+
164+
const lines = [
165+
"",
166+
"---",
167+
"",
168+
"## What's Next?",
169+
"",
170+
"Now that this task is complete, here are suggested next steps:",
171+
"",
172+
...suggestions.map(
173+
(s, i) => `${i + 1}. **${s.label}** — ${s.description} → Use \`/skill ${s.skill}\` or just ask me.`,
174+
),
175+
"",
176+
WAREHOUSE_NUDGE,
177+
"",
178+
"*You can continue this conversation — just type your next request.*",
179+
]
180+
return lines.join("\n")
181+
}
182+
}
183+
// altimate_change end

packages/opencode/src/tool/skill.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { Tool } from "./tool"
55
import { Skill } from "../skill"
66
import { Ripgrep } from "../file/ripgrep"
77
import { iife } from "@/util/iife"
8+
// altimate_change start — import follow-up suggestions for conversational engagement
9+
import { SkillFollowups } from "../skill/followups"
10+
// altimate_change end
811
// altimate_change start - import for LLM-based dynamic skill selection
912
import { Fingerprint } from "../altimate/fingerprint"
1013
import { Config } from "../config/config"
@@ -156,6 +159,10 @@ export const SkillTool = Tool.define("skill", async (ctx) => {
156159
}
157160
// altimate_change end
158161

162+
// altimate_change start — append follow-up suggestions after skill content
163+
const followups = SkillFollowups.format(skill.name)
164+
// altimate_change end
165+
159166
return {
160167
title: `Loaded skill: ${skill.name}`,
161168
output: [
@@ -172,6 +179,7 @@ export const SkillTool = Tool.define("skill", async (ctx) => {
172179
files,
173180
"</skill_files>",
174181
"</skill_content>",
182+
followups,
175183
].join("\n"),
176184
metadata: {
177185
name: skill.name,
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { describe, test, expect } from "bun:test"
2+
import { SkillFollowups } from "../../src/skill/followups"
3+
4+
describe("SkillFollowups", () => {
5+
describe("get", () => {
6+
test("returns suggestions for known dbt skills", () => {
7+
const suggestions = SkillFollowups.get("dbt-develop")
8+
expect(suggestions.length).toBeGreaterThan(0)
9+
expect(suggestions[0]).toHaveProperty("skill")
10+
expect(suggestions[0]).toHaveProperty("label")
11+
expect(suggestions[0]).toHaveProperty("description")
12+
})
13+
14+
test("returns suggestions for dbt-troubleshoot", () => {
15+
const suggestions = SkillFollowups.get("dbt-troubleshoot")
16+
expect(suggestions.length).toBeGreaterThan(0)
17+
// First suggestion should be to add regression tests
18+
expect(suggestions[0].skill).toBe("dbt-test")
19+
})
20+
21+
test("returns suggestions for sql-review", () => {
22+
const suggestions = SkillFollowups.get("sql-review")
23+
expect(suggestions.length).toBeGreaterThan(0)
24+
})
25+
26+
test("returns empty array for unknown skill", () => {
27+
const suggestions = SkillFollowups.get("nonexistent-skill")
28+
expect(suggestions).toEqual([])
29+
})
30+
31+
test("returns empty array for skills without followups", () => {
32+
const suggestions = SkillFollowups.get("teach")
33+
expect(suggestions).toEqual([])
34+
})
35+
})
36+
37+
describe("format", () => {
38+
test("returns formatted follow-up section for dbt-develop", () => {
39+
const output = SkillFollowups.format("dbt-develop")
40+
expect(output).toContain("## What's Next?")
41+
expect(output).toContain("Add tests")
42+
expect(output).toContain("dbt-test")
43+
expect(output).toContain("Document your model")
44+
expect(output).toContain("dbt-docs")
45+
expect(output).toContain("/discover")
46+
expect(output).toContain("You can continue this conversation")
47+
})
48+
49+
test("returns formatted follow-up section for dbt-troubleshoot", () => {
50+
const output = SkillFollowups.format("dbt-troubleshoot")
51+
expect(output).toContain("## What's Next?")
52+
expect(output).toContain("regression tests")
53+
expect(output).toContain("downstream")
54+
})
55+
56+
test("returns empty string for skill without followups", () => {
57+
const output = SkillFollowups.format("nonexistent-skill")
58+
expect(output).toBe("")
59+
})
60+
61+
test("format includes numbered suggestions", () => {
62+
const output = SkillFollowups.format("dbt-develop")
63+
expect(output).toContain("1.")
64+
expect(output).toContain("2.")
65+
expect(output).toContain("3.")
66+
})
67+
68+
test("format includes warehouse nudge", () => {
69+
const output = SkillFollowups.format("dbt-develop")
70+
expect(output).toContain("Connect a warehouse")
71+
expect(output).toContain("/discover")
72+
})
73+
74+
test("all dbt skills have follow-ups defined", () => {
75+
const dbtSkills = ["dbt-develop", "dbt-troubleshoot", "dbt-test", "dbt-docs", "dbt-analyze"]
76+
for (const skill of dbtSkills) {
77+
const suggestions = SkillFollowups.get(skill)
78+
expect(suggestions.length).toBeGreaterThan(0)
79+
}
80+
})
81+
82+
test("follow-up suggestions reference valid skill names", () => {
83+
const KNOWN_SKILLS = [
84+
"dbt-develop",
85+
"dbt-troubleshoot",
86+
"dbt-test",
87+
"dbt-docs",
88+
"dbt-analyze",
89+
"sql-review",
90+
"sql-translate",
91+
"query-optimize",
92+
"cost-report",
93+
"pii-audit",
94+
"lineage-diff",
95+
"schema-migration",
96+
"data-viz",
97+
"altimate-setup",
98+
"teach",
99+
"train",
100+
"training-status",
101+
]
102+
// Check all follow-up skills point to known skills
103+
for (const skillName of KNOWN_SKILLS) {
104+
const suggestions = SkillFollowups.get(skillName)
105+
for (const s of suggestions) {
106+
expect(KNOWN_SKILLS).toContain(s.skill)
107+
}
108+
}
109+
})
110+
111+
test("no skill suggests itself as a follow-up", () => {
112+
const skills = [
113+
"dbt-develop",
114+
"dbt-troubleshoot",
115+
"dbt-test",
116+
"dbt-docs",
117+
"dbt-analyze",
118+
"sql-review",
119+
"sql-translate",
120+
"query-optimize",
121+
"cost-report",
122+
]
123+
for (const skillName of skills) {
124+
const suggestions = SkillFollowups.get(skillName)
125+
for (const s of suggestions) {
126+
expect(s.skill).not.toBe(skillName)
127+
}
128+
}
129+
})
130+
})
131+
})

0 commit comments

Comments
 (0)