diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index 3fe765575997..175c723c5fd8 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -1,5 +1,4 @@ import { Config } from "effect" -import { InstallationChannel } from "../installation/version" function truthy(key: string) { const value = process.env[key]?.toLowerCase() @@ -11,13 +10,6 @@ function falsy(key: string) { 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) { const value = process.env[key] 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_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_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"], OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"], diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 01bffdb02adb..e532efa3d823 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -242,13 +242,11 @@ export const layer = Layer.effect( const s: State = { skills: {}, dirs: new Set() } // Register the built-in skill BEFORE disk discovery so a user-disk // skill with the same name can override it. - if (Flag.OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL) { - s.skills[CUSTOMIZE_OPENCODE_SKILL_NAME] = { - name: CUSTOMIZE_OPENCODE_SKILL_NAME, - description: CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION, - location: "", - content: CUSTOMIZE_OPENCODE_SKILL_BODY, - } + s.skills[CUSTOMIZE_OPENCODE_SKILL_NAME] = { + name: CUSTOMIZE_OPENCODE_SKILL_NAME, + description: CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION, + location: "", + content: CUSTOMIZE_OPENCODE_SKILL_BODY, } yield* loadSkills(s, yield* InstanceState.get(discovered), bus) return s diff --git a/packages/opencode/src/skill/prompt/customize-opencode.md b/packages/opencode/src/skill/prompt/customize-opencode.md index 6158aae085ff..744690b15a0f 100644 --- a/packages/opencode/src/skill/prompt/customize-opencode.md +++ b/packages/opencode/src/skill/prompt/customize-opencode.md @@ -1,17 +1,39 @@ # Customizing opencode 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 -`https://opencode.ai/config.json` (the JSON Schema) and validate against it. +is wrong. The shapes below cover the common surface area, but they are a +**summary, not the source of truth**. -Every `opencode.json` should declare `"$schema": "https://opencode.ai/config.json"` -so the user's editor catches mistakes as they type. +## Full schema reference + +The authoritative list of every config option — with field types, enums, +defaults, and descriptions — lives in the published JSON Schema: + +**** + +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 @@ -343,12 +365,13 @@ When a user's config is broken and opencode won't start, these env vars help: ## When proposing edits - 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. - For agent, skill, and plugin definitions, prefer creating new files in the correct location over inlining everything in `opencode.json`. - 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. -- opencode hard-fails on invalid config by design. There is no graceful - degradation, so get the shape right the first time. +- After saving any config change, remind the user to quit and restart opencode + — running sessions keep using the already-loaded config. diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index 1ba0554d3ee4..b408f7ef11b8 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -35,11 +35,6 @@ process.env["XDG_CONFIG_HOME"] = path.join(dir, "config") 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_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 // This prevents tests from picking up real user configs/skills from ~/.claude/skills diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts index d73750b0831b..969014e6b384 100644 --- a/packages/opencode/test/skill/skill.test.ts +++ b/packages/opencode/test/skill/skill.test.ts @@ -63,7 +63,7 @@ Instructions here. ) const skill = yield* Skill.Service - const list = yield* skill.all() + const list = (yield* skill.all()).filter((s) => s.location !== "") expect(list.length).toBe(1) const item = list.find((x) => x.name === "test-skill") expect(item).toBeDefined() @@ -133,7 +133,7 @@ description: Second test skill. ) const skill = yield* Skill.Service - const list = yield* skill.all() + const list = (yield* skill.all()).filter((s) => s.location !== "") expect(list.length).toBe(2) expect(list.find((x) => x.name === "skill-one")).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 - expect(yield* skill.all()).toEqual([]) + expect((yield* skill.all()).filter((s) => s.location !== "")).toEqual([]) }), { git: true }, ), @@ -182,7 +182,7 @@ Instructions here. ) const skill = yield* Skill.Service - const list = yield* skill.all() + const list = (yield* skill.all()).filter((s) => s.location !== "") expect(list.length).toBe(1) const item = list.find((x) => x.name === "manual-skill") expect(item).toBeDefined() @@ -212,7 +212,7 @@ description: A skill in the .claude/skills directory. ) const skill = yield* Skill.Service - const list = yield* skill.all() + const list = (yield* skill.all()).filter((s) => s.location !== "") expect(list.length).toBe(1) const item = list.find((x) => x.name === "claude-skill") expect(item).toBeDefined() @@ -235,7 +235,7 @@ description: A skill in the .claude/skills directory. yield* Effect.promise(() => createGlobalSkill(tmp.path)) yield* Effect.gen(function* () { const skill = yield* Skill.Service - const list = yield* skill.all() + const list = (yield* skill.all()).filter((s) => s.location !== "") expect(list.length).toBe(1) expect(list[0].name).toBe("global-test-skill") 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* () { const skill = yield* Skill.Service - expect(yield* skill.all()).toEqual([]) + expect((yield* skill.all()).filter((s) => s.location !== "")).toEqual([]) }), { git: true }, ), @@ -275,7 +275,7 @@ description: A skill in the .agents/skills directory. ) const skill = yield* Skill.Service - const list = yield* skill.all() + const list = (yield* skill.all()).filter((s) => s.location !== "") expect(list.length).toBe(1) const item = list.find((x) => x.name === "agent-skill") expect(item).toBeDefined() @@ -314,7 +314,7 @@ This skill is loaded from the global home directory. yield* Effect.gen(function* () { const skill = yield* Skill.Service - const list = yield* skill.all() + const list = (yield* skill.all()).filter((s) => s.location !== "") expect(list.length).toBe(1) expect(list[0].name).toBe("global-agent-skill") 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 list = yield* skill.all() + const list = (yield* skill.all()).filter((s) => s.location !== "") expect(list.length).toBe(2) expect(list.find((x) => x.name === "claude-skill")).toBeDefined() expect(list.find((x) => x.name === "agent-skill")).toBeDefined()