diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
index 42ac5fbe080a..a2cf6426f557 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
@@ -53,10 +53,17 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
if (!last) return
const total =
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
+ const totalInput = last.tokens.input + last.tokens.cache.read + last.tokens.cache.write
const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
return {
tokens: total.toLocaleString(),
percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null,
+ cacheHitPercent: totalInput > 0 ? ((last.tokens.cache.read / totalInput) * 100).toFixed(3) : null,
+ cacheRead: last.tokens.cache.read,
+ cacheWrite: last.tokens.cache.write,
+ cacheNew: last.tokens.input,
+ cacheInput: totalInput,
+ cacheOutput: last.tokens.output,
}
})
@@ -106,6 +113,19 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
{context()?.percentage ?? 0}% used
{cost()} spent
+
+
+
+ Cache Audit
+
+ {context()!.cacheInput.toLocaleString()} input tokens
+ {context()!.cacheNew.toLocaleString()} new
+ {context()!.cacheRead.toLocaleString()} cache read
+ {context()!.cacheWrite.toLocaleString()} cache write
+ {context()!.cacheHitPercent}% hit rate
+ {context()!.cacheOutput.toLocaleString()} output tokens
+
+
0}>
msg.role === "system").slice(0, 2)
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
+ // Use 1h cache TTL on first system block (2x write cost vs 1.25x for default 5-min)
+ const anthropicCache = extendedTTL ? { type: "ephemeral", ttl: "1h" } : { type: "ephemeral" }
const providerOptions = {
anthropic: {
cacheControl: { type: "ephemeral" },
@@ -194,18 +196,21 @@ export namespace ProviderTransform {
}
for (const msg of unique([...system, ...final])) {
+ const options = msg === system[0]
+ ? { ...providerOptions, anthropic: { cacheControl: anthropicCache } }
+ : providerOptions
const useMessageLevelOptions = model.providerID === "anthropic" || model.providerID.includes("bedrock")
const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0
if (shouldUseContentOptions) {
const lastContent = msg.content[msg.content.length - 1]
if (lastContent && typeof lastContent === "object") {
- lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions)
+ lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, options)
continue
}
}
- msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions)
+ msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, options)
}
return msgs
@@ -261,7 +266,7 @@ export namespace ProviderTransform {
model.api.npm === "@ai-sdk/anthropic") &&
model.api.npm !== "@ai-sdk/gateway"
) {
- msgs = applyCaching(msgs, model)
+ msgs = applyCaching(msgs, model, (options.extendedTTL as boolean) ?? Flag.OPENCODE_EXPERIMENTAL_CACHE_1H_TTL)
}
// Remap providerOptions keys from stored providerID to expected SDK key
diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
index f2d436ff10d9..640fa5eb120f 100644
--- a/packages/opencode/src/session/index.ts
+++ b/packages/opencode/src/session/index.ts
@@ -846,6 +846,15 @@ export namespace Session {
},
}
+ // OPENCODE_CACHE_AUDIT=1 enables per-call cache token accounting in the log
+ if (process.env["OPENCODE_CACHE_AUDIT"]) {
+ const totalInputTokens = tokens.input + tokens.cache.read + tokens.cache.write
+ const cacheHitPercent = totalInputTokens > 0 ? ((tokens.cache.read / totalInputTokens) * 100).toFixed(1) : "0.0"
+ log.info(
+ `[CACHE] ${input.model.id} input=${totalInputTokens} (cache_read=${tokens.cache.read} cache_write=${tokens.cache.write} new=${tokens.input}) hit=${cacheHitPercent}% output=${tokens.output} total=${tokens.total ?? 0}`,
+ )
+ }
+
const costInfo =
input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000
? input.model.cost.experimentalOver200K
diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts
index 86f73d0fd238..901f1bbcc679 100644
--- a/packages/opencode/src/session/instruction.ts
+++ b/packages/opencode/src/session/instruction.ts
@@ -71,14 +71,15 @@ export namespace InstructionPrompt {
export async function systemPaths() {
const config = await Config.get()
- const paths = new Set()
+ const global = new Set()
+ const project = new Set()
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
for (const file of FILES) {
const matches = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
if (matches.length > 0) {
matches.forEach((p) => {
- paths.add(path.resolve(p))
+ project.add(path.resolve(p))
})
break
}
@@ -87,7 +88,7 @@ export namespace InstructionPrompt {
for (const file of globalFiles()) {
if (await Filesystem.exists(file)) {
- paths.add(path.resolve(file))
+ global.add(path.resolve(file))
break
}
}
@@ -106,22 +107,29 @@ export namespace InstructionPrompt {
}).catch(() => [])
: await resolveRelative(instruction)
matches.forEach((p) => {
- paths.add(path.resolve(p))
+ project.add(path.resolve(p))
})
}
}
- return paths
+ return { global, project }
}
- export async function system() {
- const config = await Config.get()
+ export type SystemInstructions = { global: string[]; project: string[] }
+
+ let cached: SystemInstructions | undefined
+
+ export async function system(): Promise {
+ if (Flag.OPENCODE_EXPERIMENTAL_CACHE_STABILIZATION && cached) return cached
+
const paths = await systemPaths()
+ const config = await Config.get()
- const files = Array.from(paths).map(async (p) => {
- const content = await Filesystem.readText(p).catch(() => "")
- return content ? "Instructions from: " + p + "\n" + content : ""
- })
+ const readPaths = (set: Set) =>
+ Array.from(set).map(async (p) => {
+ const content = await Filesystem.readText(p).catch(() => "")
+ return content ? "Instructions from: " + p + "\n" + content : ""
+ })
const urls: string[] = []
if (config.instructions) {
@@ -138,7 +146,14 @@ export namespace InstructionPrompt {
.then((x) => (x ? "Instructions from: " + url + "\n" + x : "")),
)
- return Promise.all([...files, ...fetches]).then((result) => result.filter(Boolean))
+ const [global, project] = await Promise.all([
+ Promise.all(readPaths(paths.global)).then((r) => r.filter(Boolean)),
+ Promise.all([...readPaths(paths.project), ...fetches]).then((r) => r.filter(Boolean)),
+ ])
+
+ const result = { global, project }
+ if (Flag.OPENCODE_EXPERIMENTAL_CACHE_STABILIZATION) cached = result
+ return result
}
export function loaded(messages: MessageV2.WithParts[]) {
@@ -166,7 +181,8 @@ export namespace InstructionPrompt {
}
export async function resolve(messages: MessageV2.WithParts[], filepath: string, messageID: string) {
- const system = await systemPaths()
+ const paths = await systemPaths()
+ const system = new Set([...paths.global, ...paths.project])
const already = loaded(messages)
const results: { filepath: string; content: string }[] = []
diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts
index a8009c49d494..d2e76e6ca135 100644
--- a/packages/opencode/src/session/llm.ts
+++ b/packages/opencode/src/session/llm.ts
@@ -35,6 +35,7 @@ export namespace LLM {
agent: Agent.Info
permission?: Permission.Ruleset
system: string[]
+ systemSplit?: number
abort: AbortSignal
messages: ModelMessage[]
small?: boolean
@@ -67,19 +68,19 @@ export namespace LLM {
// TODO: move this to a proper hook
const isOpenaiOauth = provider.id === "openai" && auth?.type === "oauth"
- const system: string[] = []
- system.push(
- [
- // use agent prompt otherwise provider prompt
- ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),
- // any custom prompt passed into this call
- ...input.system,
- // any custom prompt from last user message
- ...(input.user.system ? [input.user.system] : []),
- ]
- .filter((x) => x)
- .join("\n"),
- )
+ const prompt = input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)
+ const split = input.systemSplit ?? input.system.length
+ const shouldSplit = provider.options?.["splitSystemPrompt"] !== false
+ const system = shouldSplit
+ ? [
+ // block 1: provider/agent prompt + global instructions (stable across repos)
+ [...prompt, ...input.system.slice(0, split)].filter(Boolean).join("\n"),
+ // block 2: env + project instructions + any custom prompt from last user message (dynamic)
+ [...input.system.slice(split), ...(input.user.system ? [input.user.system] : [])].filter(Boolean).join("\n"),
+ ].filter(Boolean)
+ : [
+ [...prompt, ...input.system, ...(input.user.system ? [input.user.system] : [])].filter(Boolean).join("\n"),
+ ].filter(Boolean)
const header = system[0]
await Plugin.trigger(
@@ -88,7 +89,7 @@ export namespace LLM {
{ system },
)
// rejoin to maintain 2-part structure for caching if header unchanged
- if (system.length > 2 && system[0] === header) {
+ if (shouldSplit && system.length > 2 && system[0] === header) {
const rest = system.slice(1)
system.length = 0
system.push(header, rest.join("\n"))
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index dca8085c5b2e..d306889011cc 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -652,13 +652,17 @@ export namespace SessionPrompt {
await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
- // Build system prompt, adding structured output instruction if needed
+ // Build system prompt: global instructions + global skills first (stable), then env + project (dynamic)
+ const instructions = await InstructionPrompt.system()
const skills = await SystemPrompt.skills(agent)
const system = [
+ ...instructions.global,
+ ...(skills.global ? [skills.global] : []),
...(await SystemPrompt.environment(model)),
- ...(skills ? [skills] : []),
- ...(await InstructionPrompt.system()),
+ ...(skills.project ? [skills.project] : []),
+ ...instructions.project,
]
+ const systemSplit = instructions.global.length + (skills.global ? 1 : 0)
const format = lastUser.format ?? { type: "text" }
if (format.type === "json_schema") {
system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
@@ -671,6 +675,7 @@ export namespace SessionPrompt {
abort,
sessionID,
system,
+ systemSplit,
messages: [
...MessageV2.toModelMessages(msgs, model),
...(isLastStep
diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts
index ca324652d9dc..9b1188587688 100644
--- a/packages/opencode/src/session/system.ts
+++ b/packages/opencode/src/session/system.ts
@@ -13,6 +13,7 @@ import type { Provider } from "@/provider/provider"
import type { Agent } from "@/agent/agent"
import { Permission } from "@/permission"
import { Skill } from "@/skill"
+import { Flag } from "@/flag/flag"
export namespace SystemPrompt {
export function provider(model: Provider.Model) {
@@ -25,8 +26,13 @@ export namespace SystemPrompt {
return [PROMPT_DEFAULT]
}
+ let cachedDate: Date | undefined
+
export async function environment(model: Provider.Model) {
const project = Instance.project
+ const date = Flag.OPENCODE_EXPERIMENTAL_CACHE_STABILIZATION
+ ? (cachedDate ??= new Date())
+ : new Date()
return [
[
`You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
@@ -36,7 +42,7 @@ export namespace SystemPrompt {
` Workspace root folder: ${Instance.worktree}`,
` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`,
` Platform: ${process.platform}`,
- ` Today's date: ${new Date().toDateString()}`,
+ ` Today's date: ${date.toDateString()}`,
``,
``,
` ${
@@ -52,17 +58,31 @@ export namespace SystemPrompt {
]
}
- export async function skills(agent: Agent.Info) {
- if (Permission.disabled(["skill"], agent.permission).has("skill")) return
+ export async function skills(agent: Agent.Info): Promise<{ global?: string; project?: string }> {
+ if (Permission.disabled(["skill"], agent.permission).has("skill")) return {}
const list = await Skill.available(agent)
+ const globalSkills = list.filter((s) => s.scope === "global")
+ const projectSkills = list.filter((s) => s.scope === "project")
- return [
+ // the agents seem to ingest the information about skills a bit better if we present a more verbose
+ // version of them here and a less verbose version in tool description, rather than vice versa.
+ const preamble = [
"Skills provide specialized instructions and workflows for specific tasks.",
"Use the skill tool to load a skill when a task matches its description.",
- // the agents seem to ingest the information about skills a bit better if we present a more verbose
- // version of them here and a less verbose version in tool description, rather than vice versa.
- Skill.fmt(list, { verbose: true }),
].join("\n")
+
+ const global = globalSkills.length > 0
+ ? [preamble, Skill.fmt(globalSkills, { verbose: true })].join("\n")
+ : undefined
+
+ const project = projectSkills.length > 0
+ ? [
+ ...(globalSkills.length === 0 ? [preamble] : []),
+ Skill.fmt(projectSkills, { verbose: true }),
+ ].join("\n")
+ : undefined
+
+ return { global, project }
}
}
diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts
index 43a22219edc3..77018bfe6dbe 100644
--- a/packages/opencode/src/skill/index.ts
+++ b/packages/opencode/src/skill/index.ts
@@ -30,6 +30,7 @@ export namespace Skill {
description: z.string(),
location: z.string(),
content: z.string(),
+ scope: z.enum(["global", "project"]).default("project"),
})
export type Info = z.infer
@@ -68,7 +69,7 @@ export namespace Skill {
readonly available: (agent?: Agent.Info) => Effect.Effect
}
- const add = async (state: State, match: string) => {
+ const add = async (state: State, match: string, scope: "global" | "project" = "project") => {
const md = await ConfigMarkdown.parse(match).catch(async (err) => {
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
? err.data.message
@@ -98,10 +99,16 @@ export namespace Skill {
description: parsed.data.description,
location: match,
content: md.content,
+ scope,
}
}
- const scan = async (state: State, root: string, pattern: string, opts?: { dot?: boolean; scope?: string }) => {
+ const scan = async (
+ state: State,
+ root: string,
+ pattern: string,
+ opts?: { dot?: boolean; scope?: "global" | "project" },
+ ) => {
return Glob.scan(pattern, {
cwd: root,
absolute: true,
@@ -109,7 +116,7 @@ export namespace Skill {
symlink: true,
dot: opts?.dot,
})
- .then((matches) => Promise.all(matches.map((match) => add(state, match))))
+ .then((matches) => Promise.all(matches.map((match) => add(state, match, opts?.scope ?? "project"))))
.catch((error) => {
if (!opts?.scope) throw error
log.error(`failed to scan ${opts.scope} skills`, { dir: root, error })
@@ -141,7 +148,7 @@ export namespace Skill {
}
for (const dir of await Config.directories()) {
- await scan(state, dir, OPENCODE_SKILL_PATTERN)
+ await scan(state, dir, OPENCODE_SKILL_PATTERN, { scope: "global" })
}
const cfg = await Config.get()
@@ -153,13 +160,13 @@ export namespace Skill {
continue
}
- await scan(state, dir, SKILL_PATTERN)
+ await scan(state, dir, SKILL_PATTERN, { scope: "global" })
}
for (const url of cfg.skills?.urls ?? []) {
for (const dir of await Effect.runPromise(discovery.pull(url))) {
state.dirs.add(dir)
- await scan(state, dir, SKILL_PATTERN)
+ await scan(state, dir, SKILL_PATTERN, { scope: "global" })
}
}
diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts
index 50ae4abac8de..3472c3ad226a 100644
--- a/packages/opencode/src/tool/bash.ts
+++ b/packages/opencode/src/tool/bash.ts
@@ -57,16 +57,17 @@ export const BashTool = Tool.define("bash", async () => {
log.info("bash tool using shell", { shell })
return {
- description: DESCRIPTION.replaceAll("${directory}", Instance.directory)
- .replaceAll("${maxLines}", String(Truncate.MAX_LINES))
- .replaceAll("${maxBytes}", String(Truncate.MAX_BYTES)),
+ description: DESCRIPTION.replaceAll("${maxLines}", String(Truncate.MAX_LINES)).replaceAll(
+ "${maxBytes}",
+ String(Truncate.MAX_BYTES),
+ ),
parameters: z.object({
command: z.string().describe("The command to execute"),
timeout: z.number().describe("Optional timeout in milliseconds").optional(),
workdir: z
.string()
.describe(
- `The working directory to run the command in. Defaults to ${Instance.directory}. Use this instead of 'cd' commands.`,
+ `The working directory to run the command in. Defaults to the current working directory. Use this instead of 'cd' commands.`,
)
.optional(),
description: z
diff --git a/packages/opencode/src/tool/bash.txt b/packages/opencode/src/tool/bash.txt
index baafb00810ab..44853b4bdf55 100644
--- a/packages/opencode/src/tool/bash.txt
+++ b/packages/opencode/src/tool/bash.txt
@@ -1,6 +1,6 @@
Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
-All commands run in ${directory} by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID using `cd && ` patterns - use `workdir` instead.
+All commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID using `cd && ` patterns - use `workdir` instead.
IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead.
diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts
index 370edeed38b6..cc26976070bd 100644
--- a/packages/opencode/test/provider/transform.test.ts
+++ b/packages/opencode/test/provider/transform.test.ts
@@ -1629,6 +1629,83 @@ describe("ProviderTransform.message - claude w/bedrock custom inference profile"
})
})
+
+describe("ProviderTransform.message - first system block gets 1h TTL when flag set", () => {
+ const anthropicModel = {
+ id: "anthropic/claude-sonnet-4-6",
+ providerID: "anthropic",
+ api: {
+ id: "claude-sonnet-4-6",
+ url: "https://api.anthropic.com",
+ npm: "@ai-sdk/anthropic",
+ },
+ capabilities: {
+ temperature: true,
+ reasoning: false,
+ attachment: true,
+ toolcall: true,
+ input: { text: true, audio: false, image: true, video: false, pdf: true },
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
+ interleaved: false,
+ },
+ cost: { input: 0.003, output: 0.015, cache: { read: 0.0003, write: 0.00375 } },
+ limit: { context: 200000, output: 8192 },
+ status: "active",
+ options: {},
+ headers: {},
+ } as any
+
+ test("first system block gets 1h TTL when extendedTTL is true", () => {
+ const msgs = [
+ { role: "system", content: "Block 1" },
+ { role: "system", content: "Block 2" },
+ { role: "user", content: "Hello" },
+ ] as any[]
+
+ const result = ProviderTransform.message(msgs, anthropicModel, { extendedTTL: true }) as any[]
+
+ expect(result[0].providerOptions.anthropic.cacheControl).toEqual({ type: "ephemeral", ttl: "1h" })
+ })
+
+ test("first system block gets default ephemeral when extendedTTL is not set", () => {
+ const msgs = [
+ { role: "system", content: "Block 1" },
+ { role: "system", content: "Block 2" },
+ { role: "user", content: "Hello" },
+ ] as any[]
+
+ const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[]
+
+ expect(result[0].providerOptions.anthropic.cacheControl).toEqual({ type: "ephemeral" })
+ })
+
+ test("second system block always gets default ephemeral TTL", () => {
+ const msgs = [
+ { role: "system", content: "Block 1" },
+ { role: "system", content: "Block 2" },
+ { role: "user", content: "Hello" },
+ ] as any[]
+
+ const result = ProviderTransform.message(msgs, anthropicModel, { extendedTTL: true }) as any[]
+
+ expect(result[1].providerOptions.anthropic.cacheControl).toEqual({ type: "ephemeral" })
+ })
+
+ test("conversation messages get default ephemeral TTL even with extendedTTL", () => {
+ const msgs = [
+ { role: "system", content: "System" },
+ { role: "user", content: "Hello" },
+ { role: "assistant", content: "Hi" },
+ { role: "user", content: "World" },
+ ] as any[]
+
+ const result = ProviderTransform.message(msgs, anthropicModel, { extendedTTL: true }) as any[]
+
+ const last = result[result.length - 1]
+ expect(last.providerOptions.anthropic.cacheControl).toEqual({ type: "ephemeral" })
+ })
+})
+
describe("ProviderTransform.message - cache control on gateway", () => {
const createModel = (overrides: Partial = {}) =>
({
diff --git a/packages/opencode/test/session/instruction.test.ts b/packages/opencode/test/session/instruction.test.ts
index e0bf94a9500d..ec167852c9d9 100644
--- a/packages/opencode/test/session/instruction.test.ts
+++ b/packages/opencode/test/session/instruction.test.ts
@@ -16,8 +16,8 @@ describe("InstructionPrompt.resolve", () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
- const system = await InstructionPrompt.systemPaths()
- expect(system.has(path.join(tmp.path, "AGENTS.md"))).toBe(true)
+ const paths = await InstructionPrompt.systemPaths()
+ expect(paths.project.has(path.join(tmp.path, "AGENTS.md"))).toBe(true)
const results = await InstructionPrompt.resolve([], path.join(tmp.path, "src", "file.ts"), "test-message-1")
expect(results).toEqual([])
@@ -35,8 +35,8 @@ describe("InstructionPrompt.resolve", () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
- const system = await InstructionPrompt.systemPaths()
- expect(system.has(path.join(tmp.path, "subdir", "AGENTS.md"))).toBe(false)
+ const paths = await InstructionPrompt.systemPaths()
+ expect(paths.project.has(path.join(tmp.path, "subdir", "AGENTS.md"))).toBe(false)
const results = await InstructionPrompt.resolve(
[],
@@ -60,8 +60,8 @@ describe("InstructionPrompt.resolve", () => {
directory: tmp.path,
fn: async () => {
const filepath = path.join(tmp.path, "subdir", "AGENTS.md")
- const system = await InstructionPrompt.systemPaths()
- expect(system.has(filepath)).toBe(false)
+ const paths = await InstructionPrompt.systemPaths()
+ expect(paths.project.has(filepath)).toBe(false)
const results = await InstructionPrompt.resolve([], filepath, "test-message-2")
expect(results).toEqual([])
@@ -107,8 +107,8 @@ describe("InstructionPrompt.systemPaths OPENCODE_CONFIG_DIR", () => {
directory: projectTmp.path,
fn: async () => {
const paths = await InstructionPrompt.systemPaths()
- expect(paths.has(path.join(profileTmp.path, "AGENTS.md"))).toBe(true)
- expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(false)
+ expect(paths.global.has(path.join(profileTmp.path, "AGENTS.md"))).toBe(true)
+ expect(paths.global.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(false)
},
})
} finally {
@@ -134,8 +134,8 @@ describe("InstructionPrompt.systemPaths OPENCODE_CONFIG_DIR", () => {
directory: projectTmp.path,
fn: async () => {
const paths = await InstructionPrompt.systemPaths()
- expect(paths.has(path.join(profileTmp.path, "AGENTS.md"))).toBe(false)
- expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(true)
+ expect(paths.global.has(path.join(profileTmp.path, "AGENTS.md"))).toBe(false)
+ expect(paths.global.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(true)
},
})
} finally {
@@ -160,7 +160,7 @@ describe("InstructionPrompt.systemPaths OPENCODE_CONFIG_DIR", () => {
directory: projectTmp.path,
fn: async () => {
const paths = await InstructionPrompt.systemPaths()
- expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(true)
+ expect(paths.global.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(true)
},
})
} finally {
diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts
index 47f5f6fc25dd..d41a1a0ba0c5 100644
--- a/packages/opencode/test/session/system.test.ts
+++ b/packages/opencode/test/session/system.test.ts
@@ -41,11 +41,12 @@ description: ${description}
const first = await SystemPrompt.skills(build!)
const second = await SystemPrompt.skills(build!)
- expect(first).toBe(second)
+ expect(first).toEqual(second)
- const alpha = first!.indexOf("alpha-skill")
- const middle = first!.indexOf("middle-skill")
- const zeta = first!.indexOf("zeta-skill")
+ const combined = [first.global, first.project].filter(Boolean).join("\n")
+ const alpha = combined.indexOf("alpha-skill")
+ const middle = combined.indexOf("middle-skill")
+ const zeta = combined.indexOf("zeta-skill")
expect(alpha).toBeGreaterThan(-1)
expect(middle).toBeGreaterThan(alpha)
diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts
index 4d680d494f35..34e6820bce08 100644
--- a/packages/opencode/test/tool/bash.test.ts
+++ b/packages/opencode/test/tool/bash.test.ts
@@ -400,4 +400,16 @@ describe("tool.bash truncation", () => {
},
})
})
+
+ test("tool schema does not contain Instance.directory for stable cache hash", async () => {
+ await Instance.provide({
+ directory: projectRoot,
+ fn: async () => {
+ const bash = await BashTool.init()
+ expect(bash.description).not.toContain(Instance.directory)
+ const schema = JSON.stringify(bash.parameters)
+ expect(schema).not.toContain(Instance.directory)
+ },
+ })
+ })
})