mirror of
https://github.com/anomalyco/opencode.git
synced 2026-06-01 19:05:38 +00:00
feat(skill): enable customize-opencode by default, link full schema (#26899)
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import { Config } from "effect"
|
import { Config } from "effect"
|
||||||
import { InstallationChannel } from "../installation/version"
|
|
||||||
|
|
||||||
function truthy(key: string) {
|
function truthy(key: string) {
|
||||||
const value = process.env[key]?.toLowerCase()
|
const value = process.env[key]?.toLowerCase()
|
||||||
@@ -11,13 +10,6 @@ function falsy(key: string) {
|
|||||||
return value === "false" || value === "0"
|
return value === "false" || value === "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Channels where new experiments default to ON (unstable / internal users).
|
|
||||||
// Stable channels (`prod`, `latest`) stay opt-in.
|
|
||||||
const UNSTABLE_CHANNELS = new Set(["dev", "beta", "local"])
|
|
||||||
function unstableDefault(key: string) {
|
|
||||||
return truthy(key) || (!falsy(key) && UNSTABLE_CHANNELS.has(InstallationChannel))
|
|
||||||
}
|
|
||||||
|
|
||||||
function number(key: string) {
|
function number(key: string) {
|
||||||
const value = process.env[key]
|
const value = process.env[key]
|
||||||
if (!value) return undefined
|
if (!value) return undefined
|
||||||
@@ -56,9 +48,6 @@ export const Flag = {
|
|||||||
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT: OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT"),
|
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT: OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT"),
|
||||||
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS,
|
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS,
|
||||||
OPENCODE_DISABLE_EXTERNAL_SKILLS: truthy("OPENCODE_DISABLE_EXTERNAL_SKILLS"),
|
OPENCODE_DISABLE_EXTERNAL_SKILLS: truthy("OPENCODE_DISABLE_EXTERNAL_SKILLS"),
|
||||||
// Default-on for dev/beta/local; opt-in for stable. Set
|
|
||||||
// OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL=false to force off, =true to force on.
|
|
||||||
OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL: unstableDefault("OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL"),
|
|
||||||
OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"],
|
OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"],
|
||||||
OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"],
|
OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"],
|
||||||
OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"],
|
OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"],
|
||||||
|
|||||||
@@ -242,13 +242,11 @@ export const layer = Layer.effect(
|
|||||||
const s: State = { skills: {}, dirs: new Set() }
|
const s: State = { skills: {}, dirs: new Set() }
|
||||||
// Register the built-in skill BEFORE disk discovery so a user-disk
|
// Register the built-in skill BEFORE disk discovery so a user-disk
|
||||||
// skill with the same name can override it.
|
// skill with the same name can override it.
|
||||||
if (Flag.OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL) {
|
s.skills[CUSTOMIZE_OPENCODE_SKILL_NAME] = {
|
||||||
s.skills[CUSTOMIZE_OPENCODE_SKILL_NAME] = {
|
name: CUSTOMIZE_OPENCODE_SKILL_NAME,
|
||||||
name: CUSTOMIZE_OPENCODE_SKILL_NAME,
|
description: CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION,
|
||||||
description: CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION,
|
location: "<built-in>",
|
||||||
location: "<built-in>",
|
content: CUSTOMIZE_OPENCODE_SKILL_BODY,
|
||||||
content: CUSTOMIZE_OPENCODE_SKILL_BODY,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
yield* loadSkills(s, yield* InstanceState.get(discovered), bus)
|
yield* loadSkills(s, yield* InstanceState.get(discovered), bus)
|
||||||
return s
|
return s
|
||||||
|
|||||||
@@ -1,17 +1,39 @@
|
|||||||
<!--
|
<!--
|
||||||
Built-in skill. Name and description are registered in code at
|
Built-in skill. Name and description are registered in code at
|
||||||
packages/opencode/src/skill/index.ts (see SKILL_NAME and SKILL_DESCRIPTION).
|
packages/opencode/src/skill/index.ts (see CUSTOMIZE_OPENCODE_SKILL_NAME
|
||||||
The body below becomes the skill's content.
|
and CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION). The body below becomes the
|
||||||
|
skill's content.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Customizing opencode
|
# Customizing opencode
|
||||||
|
|
||||||
opencode validates its own config strictly and refuses to start when a field
|
opencode validates its own config strictly and refuses to start when a field
|
||||||
is wrong. The shapes below are the accepted shapes. When in doubt, fetch
|
is wrong. The shapes below cover the common surface area, but they are a
|
||||||
`https://opencode.ai/config.json` (the JSON Schema) and validate against it.
|
**summary, not the source of truth**.
|
||||||
|
|
||||||
Every `opencode.json` should declare `"$schema": "https://opencode.ai/config.json"`
|
## Full schema reference
|
||||||
so the user's editor catches mistakes as they type.
|
|
||||||
|
The authoritative list of every config option — with field types, enums,
|
||||||
|
defaults, and descriptions — lives in the published JSON Schema:
|
||||||
|
|
||||||
|
**<https://opencode.ai/config.json>**
|
||||||
|
|
||||||
|
If a field is not documented in this skill, or you need to confirm an exact
|
||||||
|
shape before writing config, **fetch that URL and read the schema directly**
|
||||||
|
rather than guessing. opencode hard-fails on invalid config, so the cost of a
|
||||||
|
wrong shape is a broken startup.
|
||||||
|
|
||||||
|
Independently, every `opencode.json` should declare
|
||||||
|
`"$schema": "https://opencode.ai/config.json"` so the user's editor catches
|
||||||
|
mistakes as they type.
|
||||||
|
|
||||||
|
## Applying changes
|
||||||
|
|
||||||
|
Config is loaded once when opencode starts and is not hot-reloaded. After
|
||||||
|
saving changes to `opencode.json`, an agent file, a skill, a plugin, or any
|
||||||
|
other config-time file, **tell the user to quit and restart opencode** for
|
||||||
|
the changes to take effect. The running session will keep using the
|
||||||
|
already-loaded config until then.
|
||||||
|
|
||||||
## Where files live
|
## Where files live
|
||||||
|
|
||||||
@@ -343,12 +365,13 @@ When a user's config is broken and opencode won't start, these env vars help:
|
|||||||
## When proposing edits
|
## When proposing edits
|
||||||
|
|
||||||
- Validate against the schema before writing. If you are unsure of a field's
|
- Validate against the schema before writing. If you are unsure of a field's
|
||||||
exact shape, fetch `https://opencode.ai/config.json` rather than guessing.
|
exact shape, or the field is not covered in this skill, fetch
|
||||||
|
`https://opencode.ai/config.json` and read the schema rather than guessing.
|
||||||
- Preserve `$schema` and any existing fields the user did not ask to change.
|
- Preserve `$schema` and any existing fields the user did not ask to change.
|
||||||
- For agent, skill, and plugin definitions, prefer creating new files in the
|
- For agent, skill, and plugin definitions, prefer creating new files in the
|
||||||
correct location over inlining everything in `opencode.json`.
|
correct location over inlining everything in `opencode.json`.
|
||||||
- If the user's existing config is malformed, point them at the env-var escape
|
- If the user's existing config is malformed, point them at the env-var escape
|
||||||
hatch above so they can edit from inside opencode without breaking their
|
hatches above so they can edit from inside opencode without breaking their
|
||||||
session.
|
session.
|
||||||
- opencode hard-fails on invalid config by design. There is no graceful
|
- After saving any config change, remind the user to quit and restart opencode
|
||||||
degradation, so get the shape right the first time.
|
— running sessions keep using the already-loaded config.
|
||||||
|
|||||||
@@ -35,11 +35,6 @@ process.env["XDG_CONFIG_HOME"] = path.join(dir, "config")
|
|||||||
process.env["XDG_STATE_HOME"] = path.join(dir, "state")
|
process.env["XDG_STATE_HOME"] = path.join(dir, "state")
|
||||||
process.env["OPENCODE_MODELS_PATH"] = path.join(import.meta.dir, "tool", "fixtures", "models-api.json")
|
process.env["OPENCODE_MODELS_PATH"] = path.join(import.meta.dir, "tool", "fixtures", "models-api.json")
|
||||||
process.env["OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"] = "true"
|
process.env["OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"] = "true"
|
||||||
// Tests assert exact skill counts from disk discovery; the built-in
|
|
||||||
// customize-opencode skill is opt-in for stable channels and on by default
|
|
||||||
// for unstable channels (including "local" where CI runs). Disable it here
|
|
||||||
// so disk-discovery tests aren't off-by-one.
|
|
||||||
process.env["OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL"] = "false"
|
|
||||||
|
|
||||||
// Set test home directory to isolate tests from user's actual home directory
|
// Set test home directory to isolate tests from user's actual home directory
|
||||||
// This prevents tests from picking up real user configs/skills from ~/.claude/skills
|
// This prevents tests from picking up real user configs/skills from ~/.claude/skills
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Instructions here.
|
|||||||
)
|
)
|
||||||
|
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
const list = yield* skill.all()
|
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
|
||||||
expect(list.length).toBe(1)
|
expect(list.length).toBe(1)
|
||||||
const item = list.find((x) => x.name === "test-skill")
|
const item = list.find((x) => x.name === "test-skill")
|
||||||
expect(item).toBeDefined()
|
expect(item).toBeDefined()
|
||||||
@@ -133,7 +133,7 @@ description: Second test skill.
|
|||||||
)
|
)
|
||||||
|
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
const list = yield* skill.all()
|
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
|
||||||
expect(list.length).toBe(2)
|
expect(list.length).toBe(2)
|
||||||
expect(list.find((x) => x.name === "skill-one")).toBeDefined()
|
expect(list.find((x) => x.name === "skill-one")).toBeDefined()
|
||||||
expect(list.find((x) => x.name === "skill-two")).toBeDefined()
|
expect(list.find((x) => x.name === "skill-two")).toBeDefined()
|
||||||
@@ -157,7 +157,7 @@ Just some content without YAML frontmatter.
|
|||||||
)
|
)
|
||||||
|
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
expect(yield* skill.all()).toEqual([])
|
expect((yield* skill.all()).filter((s) => s.location !== "<built-in>")).toEqual([])
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ git: true },
|
||||||
),
|
),
|
||||||
@@ -182,7 +182,7 @@ Instructions here.
|
|||||||
)
|
)
|
||||||
|
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
const list = yield* skill.all()
|
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
|
||||||
expect(list.length).toBe(1)
|
expect(list.length).toBe(1)
|
||||||
const item = list.find((x) => x.name === "manual-skill")
|
const item = list.find((x) => x.name === "manual-skill")
|
||||||
expect(item).toBeDefined()
|
expect(item).toBeDefined()
|
||||||
@@ -212,7 +212,7 @@ description: A skill in the .claude/skills directory.
|
|||||||
)
|
)
|
||||||
|
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
const list = yield* skill.all()
|
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
|
||||||
expect(list.length).toBe(1)
|
expect(list.length).toBe(1)
|
||||||
const item = list.find((x) => x.name === "claude-skill")
|
const item = list.find((x) => x.name === "claude-skill")
|
||||||
expect(item).toBeDefined()
|
expect(item).toBeDefined()
|
||||||
@@ -235,7 +235,7 @@ description: A skill in the .claude/skills directory.
|
|||||||
yield* Effect.promise(() => createGlobalSkill(tmp.path))
|
yield* Effect.promise(() => createGlobalSkill(tmp.path))
|
||||||
yield* Effect.gen(function* () {
|
yield* Effect.gen(function* () {
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
const list = yield* skill.all()
|
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
|
||||||
expect(list.length).toBe(1)
|
expect(list.length).toBe(1)
|
||||||
expect(list[0].name).toBe("global-test-skill")
|
expect(list[0].name).toBe("global-test-skill")
|
||||||
expect(list[0].description).toBe("A global skill from ~/.claude/skills for testing.")
|
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.
|
|||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
expect(yield* skill.all()).toEqual([])
|
expect((yield* skill.all()).filter((s) => s.location !== "<built-in>")).toEqual([])
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ git: true },
|
||||||
),
|
),
|
||||||
@@ -275,7 +275,7 @@ description: A skill in the .agents/skills directory.
|
|||||||
)
|
)
|
||||||
|
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
const list = yield* skill.all()
|
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
|
||||||
expect(list.length).toBe(1)
|
expect(list.length).toBe(1)
|
||||||
const item = list.find((x) => x.name === "agent-skill")
|
const item = list.find((x) => x.name === "agent-skill")
|
||||||
expect(item).toBeDefined()
|
expect(item).toBeDefined()
|
||||||
@@ -314,7 +314,7 @@ This skill is loaded from the global home directory.
|
|||||||
|
|
||||||
yield* Effect.gen(function* () {
|
yield* Effect.gen(function* () {
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
const list = yield* skill.all()
|
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
|
||||||
expect(list.length).toBe(1)
|
expect(list.length).toBe(1)
|
||||||
expect(list[0].name).toBe("global-agent-skill")
|
expect(list[0].name).toBe("global-agent-skill")
|
||||||
expect(list[0].description).toBe("A global skill from ~/.agents/skills for testing.")
|
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.
|
|||||||
)
|
)
|
||||||
|
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
const list = yield* skill.all()
|
const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
|
||||||
expect(list.length).toBe(2)
|
expect(list.length).toBe(2)
|
||||||
expect(list.find((x) => x.name === "claude-skill")).toBeDefined()
|
expect(list.find((x) => x.name === "claude-skill")).toBeDefined()
|
||||||
expect(list.find((x) => x.name === "agent-skill")).toBeDefined()
|
expect(list.find((x) => x.name === "agent-skill")).toBeDefined()
|
||||||
|
|||||||
Reference in New Issue
Block a user