Skip to content

Commit df386bd

Browse files
authored
feat(skill): enable customize-opencode by default, link full schema (#26899)
1 parent 25b12ed commit df386bd

5 files changed

Lines changed: 48 additions & 43 deletions

File tree

packages/core/src/flag/flag.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Config } from "effect"
2-
import { InstallationChannel } from "../installation/version"
32

43
function truthy(key: string) {
54
const value = process.env[key]?.toLowerCase()
@@ -11,13 +10,6 @@ function falsy(key: string) {
1110
return value === "false" || value === "0"
1211
}
1312

14-
// Channels where new experiments default to ON (unstable / internal users).
15-
// Stable channels (`prod`, `latest`) stay opt-in.
16-
const UNSTABLE_CHANNELS = new Set(["dev", "beta", "local"])
17-
function unstableDefault(key: string) {
18-
return truthy(key) || (!falsy(key) && UNSTABLE_CHANNELS.has(InstallationChannel))
19-
}
20-
2113
function number(key: string) {
2214
const value = process.env[key]
2315
if (!value) return undefined
@@ -56,9 +48,6 @@ export const Flag = {
5648
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT: OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT"),
5749
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS,
5850
OPENCODE_DISABLE_EXTERNAL_SKILLS: truthy("OPENCODE_DISABLE_EXTERNAL_SKILLS"),
59-
// Default-on for dev/beta/local; opt-in for stable. Set
60-
// OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL=false to force off, =true to force on.
61-
OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL: unstableDefault("OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL"),
6251
OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"],
6352
OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"],
6453
OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"],

packages/opencode/src/skill/index.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -242,13 +242,11 @@ export const layer = Layer.effect(
242242
const s: State = { skills: {}, dirs: new Set() }
243243
// Register the built-in skill BEFORE disk discovery so a user-disk
244244
// skill with the same name can override it.
245-
if (Flag.OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL) {
246-
s.skills[CUSTOMIZE_OPENCODE_SKILL_NAME] = {
247-
name: CUSTOMIZE_OPENCODE_SKILL_NAME,
248-
description: CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION,
249-
location: "<built-in>",
250-
content: CUSTOMIZE_OPENCODE_SKILL_BODY,
251-
}
245+
s.skills[CUSTOMIZE_OPENCODE_SKILL_NAME] = {
246+
name: CUSTOMIZE_OPENCODE_SKILL_NAME,
247+
description: CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION,
248+
location: "<built-in>",
249+
content: CUSTOMIZE_OPENCODE_SKILL_BODY,
252250
}
253251
yield* loadSkills(s, yield* InstanceState.get(discovered), bus)
254252
return s

packages/opencode/src/skill/prompt/customize-opencode.md

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,39 @@
11
<!--
22
Built-in skill. Name and description are registered in code at
3-
packages/opencode/src/skill/index.ts (see SKILL_NAME and SKILL_DESCRIPTION).
4-
The body below becomes the skill's content.
3+
packages/opencode/src/skill/index.ts (see CUSTOMIZE_OPENCODE_SKILL_NAME
4+
and CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION). The body below becomes the
5+
skill's content.
56
-->
67

78
# Customizing opencode
89

910
opencode validates its own config strictly and refuses to start when a field
10-
is wrong. The shapes below are the accepted shapes. When in doubt, fetch
11-
`https://opencode.ai/config.json` (the JSON Schema) and validate against it.
11+
is wrong. The shapes below cover the common surface area, but they are a
12+
**summary, not the source of truth**.
1213

13-
Every `opencode.json` should declare `"$schema": "https://opencode.ai/config.json"`
14-
so the user's editor catches mistakes as they type.
14+
## Full schema reference
15+
16+
The authoritative list of every config option — with field types, enums,
17+
defaults, and descriptions — lives in the published JSON Schema:
18+
19+
**<https://opencode.ai/config.json>**
20+
21+
If a field is not documented in this skill, or you need to confirm an exact
22+
shape before writing config, **fetch that URL and read the schema directly**
23+
rather than guessing. opencode hard-fails on invalid config, so the cost of a
24+
wrong shape is a broken startup.
25+
26+
Independently, every `opencode.json` should declare
27+
`"$schema": "https://opencode.ai/config.json"` so the user's editor catches
28+
mistakes as they type.
29+
30+
## Applying changes
31+
32+
Config is loaded once when opencode starts and is not hot-reloaded. After
33+
saving changes to `opencode.json`, an agent file, a skill, a plugin, or any
34+
other config-time file, **tell the user to quit and restart opencode** for
35+
the changes to take effect. The running session will keep using the
36+
already-loaded config until then.
1537

1638
## Where files live
1739

@@ -343,12 +365,13 @@ When a user's config is broken and opencode won't start, these env vars help:
343365
## When proposing edits
344366

345367
- Validate against the schema before writing. If you are unsure of a field's
346-
exact shape, fetch `https://opencode.ai/config.json` rather than guessing.
368+
exact shape, or the field is not covered in this skill, fetch
369+
`https://opencode.ai/config.json` and read the schema rather than guessing.
347370
- Preserve `$schema` and any existing fields the user did not ask to change.
348371
- For agent, skill, and plugin definitions, prefer creating new files in the
349372
correct location over inlining everything in `opencode.json`.
350373
- If the user's existing config is malformed, point them at the env-var escape
351-
hatch above so they can edit from inside opencode without breaking their
374+
hatches above so they can edit from inside opencode without breaking their
352375
session.
353-
- opencode hard-fails on invalid config by design. There is no graceful
354-
degradation, so get the shape right the first time.
376+
- After saving any config change, remind the user to quit and restart opencode
377+
— running sessions keep using the already-loaded config.

packages/opencode/test/preload.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,6 @@ process.env["XDG_CONFIG_HOME"] = path.join(dir, "config")
3535
process.env["XDG_STATE_HOME"] = path.join(dir, "state")
3636
process.env["OPENCODE_MODELS_PATH"] = path.join(import.meta.dir, "tool", "fixtures", "models-api.json")
3737
process.env["OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"] = "true"
38-
// Tests assert exact skill counts from disk discovery; the built-in
39-
// customize-opencode skill is opt-in for stable channels and on by default
40-
// for unstable channels (including "local" where CI runs). Disable it here
41-
// so disk-discovery tests aren't off-by-one.
42-
process.env["OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL"] = "false"
4338

4439
// Set test home directory to isolate tests from user's actual home directory
4540
// This prevents tests from picking up real user configs/skills from ~/.claude/skills

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Instructions here.
6363
)
6464

6565
const skill = yield* Skill.Service
66-
const list = yield* skill.all()
66+
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
6767
expect(list.length).toBe(1)
6868
const item = list.find((x) => x.name === "test-skill")
6969
expect(item).toBeDefined()
@@ -133,7 +133,7 @@ description: Second test skill.
133133
)
134134

135135
const skill = yield* Skill.Service
136-
const list = yield* skill.all()
136+
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
137137
expect(list.length).toBe(2)
138138
expect(list.find((x) => x.name === "skill-one")).toBeDefined()
139139
expect(list.find((x) => x.name === "skill-two")).toBeDefined()
@@ -157,7 +157,7 @@ Just some content without YAML frontmatter.
157157
)
158158

159159
const skill = yield* Skill.Service
160-
expect(yield* skill.all()).toEqual([])
160+
expect((yield* skill.all()).filter((s) => s.location !== "<built-in>")).toEqual([])
161161
}),
162162
{ git: true },
163163
),
@@ -182,7 +182,7 @@ Instructions here.
182182
)
183183

184184
const skill = yield* Skill.Service
185-
const list = yield* skill.all()
185+
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
186186
expect(list.length).toBe(1)
187187
const item = list.find((x) => x.name === "manual-skill")
188188
expect(item).toBeDefined()
@@ -212,7 +212,7 @@ description: A skill in the .claude/skills directory.
212212
)
213213

214214
const skill = yield* Skill.Service
215-
const list = yield* skill.all()
215+
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
216216
expect(list.length).toBe(1)
217217
const item = list.find((x) => x.name === "claude-skill")
218218
expect(item).toBeDefined()
@@ -235,7 +235,7 @@ description: A skill in the .claude/skills directory.
235235
yield* Effect.promise(() => createGlobalSkill(tmp.path))
236236
yield* Effect.gen(function* () {
237237
const skill = yield* Skill.Service
238-
const list = yield* skill.all()
238+
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
239239
expect(list.length).toBe(1)
240240
expect(list[0].name).toBe("global-test-skill")
241241
expect(list[0].description).toBe("A global skill from ~/.claude/skills for testing.")
@@ -251,7 +251,7 @@ description: A skill in the .claude/skills directory.
251251
() =>
252252
Effect.gen(function* () {
253253
const skill = yield* Skill.Service
254-
expect(yield* skill.all()).toEqual([])
254+
expect((yield* skill.all()).filter((s) => s.location !== "<built-in>")).toEqual([])
255255
}),
256256
{ git: true },
257257
),
@@ -275,7 +275,7 @@ description: A skill in the .agents/skills directory.
275275
)
276276

277277
const skill = yield* Skill.Service
278-
const list = yield* skill.all()
278+
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
279279
expect(list.length).toBe(1)
280280
const item = list.find((x) => x.name === "agent-skill")
281281
expect(item).toBeDefined()
@@ -314,7 +314,7 @@ This skill is loaded from the global home directory.
314314

315315
yield* Effect.gen(function* () {
316316
const skill = yield* Skill.Service
317-
const list = yield* skill.all()
317+
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
318318
expect(list.length).toBe(1)
319319
expect(list[0].name).toBe("global-agent-skill")
320320
expect(list[0].description).toBe("A global skill from ~/.agents/skills for testing.")
@@ -355,7 +355,7 @@ description: A skill in the .agents/skills directory.
355355
)
356356

357357
const skill = yield* Skill.Service
358-
const list = yield* skill.all()
358+
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
359359
expect(list.length).toBe(2)
360360
expect(list.find((x) => x.name === "claude-skill")).toBeDefined()
361361
expect(list.find((x) => x.name === "agent-skill")).toBeDefined()

0 commit comments

Comments
 (0)