Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ export namespace Config {

// Project config has highest precedence (overrides global and remote)
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
const stop = Flag.OPENCODE_NO_PARENT_CONFIG ? Instance.directory : Instance.worktree
for (const file of ["opencode.jsonc", "opencode.json"]) {
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
const found = await Filesystem.findUp(file, Instance.directory, stop)
for (const resolved of found.toReversed()) {
result = mergeConfigConcatArrays(result, await loadFile(resolved))
}
Expand All @@ -122,7 +123,7 @@ export namespace Config {
Filesystem.up({
targets: [".opencode"],
start: Instance.directory,
stop: Instance.worktree,
stop: Flag.OPENCODE_NO_PARENT_CONFIG ? Instance.directory : Instance.worktree,
}),
)
: []),
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export namespace Flag {
export const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS =
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")
export declare const OPENCODE_DISABLE_PROJECT_CONFIG: boolean
export declare const OPENCODE_NO_PARENT_CONFIG: boolean
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli"
export const OPENCODE_SERVER_PASSWORD = process.env["OPENCODE_SERVER_PASSWORD"]
Expand Down Expand Up @@ -67,6 +68,17 @@ Object.defineProperty(Flag, "OPENCODE_DISABLE_PROJECT_CONFIG", {
configurable: false,
})

// Dynamic getter for OPENCODE_NO_PARENT_CONFIG
// This must be evaluated at access time, not module load time,
// because external tooling may set this env var at runtime
Object.defineProperty(Flag, "OPENCODE_NO_PARENT_CONFIG", {
get() {
return truthy("OPENCODE_NO_PARENT_CONFIG")
},
enumerable: true,
configurable: false,
})

// Dynamic getter for OPENCODE_CONFIG_DIR
// This must be evaluated at access time, not module load time,
// because external tooling may set this env var at runtime
Expand Down
6 changes: 4 additions & 2 deletions packages/opencode/src/session/instruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ function globalFiles() {

async function resolveRelative(instruction: string): Promise<string[]> {
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
return Filesystem.globUp(instruction, Instance.directory, Instance.worktree).catch(() => [])
const stop = Flag.OPENCODE_NO_PARENT_CONFIG ? Instance.directory : Instance.worktree
return Filesystem.globUp(instruction, Instance.directory, stop).catch(() => [])
}
if (!Flag.OPENCODE_CONFIG_DIR) {
log.warn(
Expand Down Expand Up @@ -72,8 +73,9 @@ export namespace InstructionPrompt {
const paths = new Set<string>()

if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
const stop = Flag.OPENCODE_NO_PARENT_CONFIG ? Instance.directory : Instance.worktree
for (const file of FILES) {
const matches = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
const matches = await Filesystem.findUp(file, Instance.directory, stop)
if (matches.length > 0) {
matches.forEach((p) => paths.add(path.resolve(p)))
break
Expand Down
111 changes: 109 additions & 2 deletions packages/opencode/src/session/system.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,44 @@
import { Ripgrep } from "../file/ripgrep"
import { Global } from "../global"
import { Filesystem } from "../util/filesystem"
import { Config } from "../config/config"
import { Log } from "../util/log"

import { Instance } from "../project/instance"
import path from "path"
import os from "os"

import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
import PROMPT_ANTHROPIC_WITHOUT_TODO from "./prompt/qwen.txt"
import PROMPT_BEAST from "./prompt/beast.txt"
import PROMPT_GEMINI from "./prompt/gemini.txt"
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"

import PROMPT_CODEX from "./prompt/codex_header.txt"
import type { Provider } from "@/provider/provider"
import { Flag } from "@/flag/flag"

const log = Log.create({ service: "system-prompt" })

async function resolveRelativeInstruction(instruction: string): Promise<string[]> {
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
return Filesystem.globUp(instruction, Instance.directory, Instance.worktree).catch(() => [])
}
if (!Flag.OPENCODE_CONFIG_DIR) {
log.warn(
`Skipping relative instruction "${instruction}" - no OPENCODE_CONFIG_DIR set while project config is disabled`,
)
return []
}
return Filesystem.globUp(instruction, Flag.OPENCODE_CONFIG_DIR, Flag.OPENCODE_CONFIG_DIR).catch(() => [])
}

export namespace SystemPrompt {
export function header(providerID: string) {
if (providerID.includes("anthropic")) return [PROMPT_ANTHROPIC_SPOOF.trim()]
return []
}

export function instructions() {
return PROMPT_CODEX.trim()
}
Expand All @@ -24,11 +52,10 @@ export namespace SystemPrompt {
return [PROMPT_ANTHROPIC_WITHOUT_TODO]
}

export async function environment(model: Provider.Model) {
export async function environment() {
const project = Instance.project
return [
[
`You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
`Here is some useful information about the environment you are running in:`,
`<env>`,
` Working directory: ${Instance.directory}`,
Expand All @@ -49,4 +76,84 @@ export namespace SystemPrompt {
].join("\n"),
]
}

const LOCAL_RULE_FILES = [
"AGENTS.md",
"CLAUDE.md",
"CONTEXT.md", // deprecated
]
function globalRuleFiles() {
const files = [path.join(Global.Path.config, "AGENTS.md")]
if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
files.push(path.join(os.homedir(), ".claude", "CLAUDE.md"))
}
if (Flag.OPENCODE_CONFIG_DIR) {
files.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md"))
}
return files
}

export async function custom() {
const config = await Config.get()
const paths = new Set<string>()

// Only scan local rule files when project discovery is enabled
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
const stop = Flag.OPENCODE_NO_PARENT_CONFIG ? Instance.directory : Instance.worktree
for (const localRuleFile of LOCAL_RULE_FILES) {
const matches = await Filesystem.findUp(localRuleFile, Instance.directory, stop)
if (matches.length > 0) {
matches.forEach((path) => paths.add(path))
break
}
}
}

for (const globalRuleFile of globalRuleFiles()) {
if (await Bun.file(globalRuleFile).exists()) {
paths.add(globalRuleFile)
break
}
}

const urls: string[] = []
if (config.instructions) {
for (let instruction of config.instructions) {
if (instruction.startsWith("https://") || instruction.startsWith("http://")) {
urls.push(instruction)
continue
}
if (instruction.startsWith("~/")) {
instruction = path.join(os.homedir(), instruction.slice(2))
}
let matches: string[] = []
if (path.isAbsolute(instruction)) {
matches = await Array.fromAsync(
new Bun.Glob(path.basename(instruction)).scan({
cwd: path.dirname(instruction),
absolute: true,
onlyFiles: true,
}),
).catch(() => [])
} else {
matches = await resolveRelativeInstruction(instruction)
}
matches.forEach((path) => paths.add(path))
}
}

const foundFiles = Array.from(paths).map((p) =>
Bun.file(p)
.text()
.catch(() => "")
.then((x) => "Instructions from: " + p + "\n" + x),
)
const foundUrls = urls.map((url) =>
fetch(url, { signal: AbortSignal.timeout(5000) })
.then((res) => (res.ok ? res.text() : ""))
.catch(() => "")
.then((x) => (x ? "Instructions from: " + url + "\n" + x : "")),
)
return Promise.all([...foundFiles, ...foundUrls]).then((result) => result.filter(Boolean))
}
}
Loading
Loading