Skip to content

Commit 2d217e6

Browse files
kulvirgitclaude
andcommitted
feat: replace dynamic_skills with env_fingerprint_skill_selection, add config guard tests
- Remove dynamic_skills config; env_fingerprint_skill_selection is the single toggle (default: true when absent, set false to disable) - Remove MessageContext import and per-turn message extraction from prompt.ts - Add integration tests in tool/skill.test.ts verifying the config guard: - env_fingerprint_skill_selection: false → selector bypassed, all skills shown - env_fingerprint_skill_selection: true → selector called, uses cached subset - Fix pre-existing test assertion format (bold markdown → XML) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 910f334 commit 2d217e6

4 files changed

Lines changed: 122 additions & 21 deletions

File tree

packages/opencode/src/config/config.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,11 +1266,7 @@ export namespace Config {
12661266
.describe(
12671267
"Automatically enhance prompts with AI before sending (default: false). Uses a small model to rewrite rough prompts into clearer versions.",
12681268
),
1269-
// altimate_change start - dynamic skill loading toggle
1270-
dynamic_skills: z
1271-
.boolean()
1272-
.optional()
1273-
.describe("Enable dynamic skill filtering by environment fingerprint and per-turn message rescue"),
1269+
// altimate_change start - env fingerprint skill selection toggle
12741270
env_fingerprint_skill_selection: z
12751271
.boolean()
12761272
.optional()

packages/opencode/src/session/prompt.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,8 @@ import { iife } from "@/util/iife"
5050
import { Shell } from "@/shell/shell"
5151
import { Truncate } from "@/tool/truncation"
5252
import { decodeDataUrl } from "@/util/data-url"
53-
// altimate_change start - import fingerprint for dynamic skill loading
53+
// altimate_change start - import fingerprint for env-based skill selection
5454
import { Fingerprint } from "../altimate/fingerprint"
55-
import { MessageContext } from "../altimate/context/message-context"
5655
import { Config } from "../config/config"
5756
// altimate_change end
5857
import { Telemetry } from "@/telemetry" // altimate_change — session telemetry
@@ -304,7 +303,7 @@ export namespace SessionPrompt {
304303
const session = await Session.get(sessionID)
305304
// altimate_change start - detect environment fingerprint at session start
306305
const altCfg = await Config.get()
307-
if (altCfg.experimental?.dynamic_skills) {
306+
if (altCfg.experimental?.env_fingerprint_skill_selection !== false) {
308307
await Fingerprint.detect(Instance.directory, Instance.worktree).catch((e) => {
309308
log.warn("fingerprint detection failed", { error: e })
310309
})
@@ -646,16 +645,7 @@ export namespace SessionPrompt {
646645
const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
647646
const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
648647

649-
// altimate_change start - set message context for per-turn skill rescue
650-
if ((await Config.get()).experimental?.dynamic_skills) {
651-
const lastUserText =
652-
lastUserMsg?.parts
653-
.filter((p): p is Extract<typeof p, { type: "text" }> => p.type === "text")
654-
.map((p) => p.text)
655-
.join(" ") ?? ""
656-
MessageContext.set(lastUserText)
657-
}
658-
// altimate_change end
648+
// altimate_change - MessageContext removed: skill selection uses fingerprint only, not per-turn message
659649

660650
const tools = await resolveTools({
661651
agent,

packages/opencode/src/tool/skill.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const SkillTool = Tool.define("skill", async (ctx) => {
1919
// altimate_change start - LLM-based dynamic skill selection
2020
const cfg = await Config.get()
2121
let allAllowed: Skill.Info[]
22-
if (cfg.experimental?.dynamic_skills && cfg.experimental?.env_fingerprint_skill_selection !== false) {
22+
if (cfg.experimental?.env_fingerprint_skill_selection !== false) {
2323
allAllowed = await selectSkillsWithLLM(
2424
list,
2525
Fingerprint.get(),

packages/opencode/test/tool/skill.test.ts

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, test } from "bun:test"
1+
import { afterEach, describe, expect, test } from "bun:test"
22
import path from "path"
33
import { pathToFileURL } from "url"
44
import type { PermissionNext } from "../../src/permission/next"
@@ -7,6 +7,9 @@ import { Instance } from "../../src/project/instance"
77
import { SkillTool } from "../../src/tool/skill"
88
import { tmpdir } from "../fixture/fixture"
99
import { SessionID, MessageID } from "../../src/session/schema"
10+
import { resetSkillSelectorCache, selectSkillsWithLLM, type SkillSelectorDeps } from "../../src/altimate/skill-selector"
11+
import type { Skill } from "../../src/skill"
12+
import type { LanguageModelV2 } from "@openrouter/ai-sdk-provider"
1013

1114
const baseCtx: Omit<Tool.Context, "ask"> = {
1215
sessionID: SessionID.make("ses_test"),
@@ -18,6 +21,25 @@ const baseCtx: Omit<Tool.Context, "ask"> = {
1821
metadata: () => {},
1922
}
2023

24+
const FAKE_MODEL = { modelId: "test", provider: "test" } as unknown as LanguageModelV2
25+
26+
/** Pre-populate the skill selector cache with a specific subset */
27+
function seedCache(skillNames: string[]) {
28+
resetSkillSelectorCache()
29+
const skills = skillNames.map((name) => ({
30+
name,
31+
description: `Skill: ${name}`,
32+
location: `/fake/${name}/SKILL.md`,
33+
content: `# ${name}`,
34+
})) as Skill.Info[]
35+
// Call with mock deps that return exactly these names → populates the cache
36+
const deps: SkillSelectorDeps = {
37+
resolveModel: async () => FAKE_MODEL,
38+
generate: async () => ({ object: { selected: skillNames } }),
39+
}
40+
return selectSkillsWithLLM(skills, undefined, deps)
41+
}
42+
2143
describe("tool.skill", () => {
2244
test("description lists skill location URL", async () => {
2345
await using tmp = await tmpdir({
@@ -46,14 +68,19 @@ description: Skill for tool tests.
4668
fn: async () => {
4769
const tool = await SkillTool.init()
4870
const skillPath = path.join(tmp.path, ".opencode", "skill", "tool-skill", "SKILL.md")
49-
expect(tool.description).toContain(`**tool-skill**: Skill for tool tests.`)
71+
expect(tool.description).toContain(`<name>tool-skill</name>`)
72+
expect(tool.description).toContain(`<description>Skill for tool tests.</description>`)
5073
},
5174
})
5275
} finally {
5376
process.env.OPENCODE_TEST_HOME = home
5477
}
5578
})
5679

80+
afterEach(() => {
81+
resetSkillSelectorCache()
82+
})
83+
5784
test("execute returns skill content block with files", async () => {
5885
await using tmp = await tmpdir({
5986
git: true,
@@ -110,4 +137,92 @@ Use this skill.
110137
process.env.OPENCODE_TEST_HOME = home
111138
}
112139
})
140+
141+
test("env_fingerprint_skill_selection: false → selector bypassed, all skills shown", async () => {
142+
// Pre-populate cache with only "skill-alpha" — if selector is called, it returns this cached subset
143+
await seedCache(["skill-alpha"])
144+
145+
await using tmp = await tmpdir({
146+
git: true,
147+
config: {
148+
experimental: {
149+
env_fingerprint_skill_selection: false,
150+
},
151+
},
152+
init: async (dir) => {
153+
for (const name of ["skill-alpha", "skill-beta"]) {
154+
await Bun.write(
155+
path.join(dir, ".opencode", "skill", name, "SKILL.md"),
156+
`---\nname: ${name}\ndescription: Test ${name}\n---\n# ${name}\n`,
157+
)
158+
}
159+
},
160+
})
161+
162+
const home = process.env.OPENCODE_TEST_HOME
163+
process.env.OPENCODE_TEST_HOME = tmp.path
164+
165+
try {
166+
await Instance.provide({
167+
directory: tmp.path,
168+
fn: async () => {
169+
const tool = await SkillTool.init()
170+
// Selector was bypassed → both skills appear (from Skill.available, not cache)
171+
expect(tool.description).toContain("<name>skill-alpha</name>")
172+
expect(tool.description).toContain("<name>skill-beta</name>")
173+
},
174+
})
175+
} finally {
176+
process.env.OPENCODE_TEST_HOME = home
177+
}
178+
})
179+
180+
test("env_fingerprint_skill_selection: true → selector called, uses cached subset", async () => {
181+
await using tmp = await tmpdir({
182+
git: true,
183+
config: {
184+
experimental: {
185+
env_fingerprint_skill_selection: true,
186+
},
187+
},
188+
init: async (dir) => {
189+
for (const name of ["skill-alpha", "skill-beta"]) {
190+
await Bun.write(
191+
path.join(dir, ".opencode", "skill", name, "SKILL.md"),
192+
`---\nname: ${name}\ndescription: Test ${name}\n---\n# ${name}\n`,
193+
)
194+
}
195+
},
196+
})
197+
198+
// Pre-populate cache with only "skill-alpha" AFTER tmpdir so location matches
199+
const alphaLocation = path.join(tmp.path, ".opencode", "skill", "skill-alpha", "SKILL.md")
200+
resetSkillSelectorCache()
201+
const deps: SkillSelectorDeps = {
202+
resolveModel: async () => FAKE_MODEL,
203+
generate: async () => ({ object: { selected: ["skill-alpha"] } }),
204+
}
205+
await selectSkillsWithLLM(
206+
[{ name: "skill-alpha", description: "Test skill-alpha", location: alphaLocation, content: "# skill-alpha" } as Skill.Info],
207+
undefined,
208+
deps,
209+
)
210+
211+
const home = process.env.OPENCODE_TEST_HOME
212+
process.env.OPENCODE_TEST_HOME = tmp.path
213+
214+
try {
215+
await Instance.provide({
216+
directory: tmp.path,
217+
fn: async () => {
218+
const tool = await SkillTool.init()
219+
// Selector was called → returns cached subset (only skill-alpha)
220+
expect(tool.description).toContain("<name>skill-alpha</name>")
221+
expect(tool.description).not.toContain("<name>skill-beta</name>")
222+
},
223+
})
224+
} finally {
225+
process.env.OPENCODE_TEST_HOME = home
226+
}
227+
})
113228
})

0 commit comments

Comments
 (0)