Skip to content

Commit 970b1bd

Browse files
committed
feat(instructions): add native global rules and project precedence
Prefer non-empty OpenBitFun global instructions over Codex rules. Select one project instruction family: AGENTS, Claude, then OpenCode. Skip blank files and apply the same selection to conditional rules, source catalogs, and OpenCode project config discovery. Document loading behavior and add regression coverage for fallback, context composition, and local/filesystem-port parity.
1 parent fbf1ed6 commit 970b1bd

13 files changed

Lines changed: 627 additions & 75 deletions

File tree

‎docs/architecture/extensions/opencode-config-assets-adapter-design.md‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,22 @@ OpenCode adapter 在来源发现、解析和审批前不 import module、不读
231231
来源语义:先选 `$XDG_CONFIG_HOME/opencode/AGENTS.md`(默认 `~/.config/opencode/AGENTS.md`),不存在时回退
232232
`~/.claude/CLAUDE.md`;再按 `config.json`、`opencode.json`、`opencode.jsonc` 顺序合并,后出现的 `instructions`
233233
数组覆盖前者。最终数组支持 workspace 相对、`~/`、绝对本地精确文件和有界 glob;HTTP/HTTPS 项直接拒绝,不下载、
234-
缓存或探测网络。Product Assembly 依次合成 OpenCode、Codex、Claude Code 用户来源,再追加项目来源,并按 canonical path
235-
保留首项。非递归 glob 只遍历模式所需层级;递归 glob 超出固定扫描预算时只跳过该项,保留此前已读取的用户来源。
234+
缓存或探测网络。Product Assembly 依次合成 OpenCode、OpenBitFun 原生或 Codex、Claude Code 用户来源,再追加项目来源,并按 canonical path
235+
保留首项。OpenBitFun 从既有 product home(默认 `~/.openbitfun`,遵循 `OPENBITFUN_HOME`)读取 `AGENTS.md`:非空时跳过
236+
整个 Codex 用户指令来源,缺失或仅含空白时回退 Codex 原有的 override/base 选择。原生文件读取失败时隔离该来源、记录诊断并禁止缓存,
237+
不静默替换成 Codex 指令;下次消息重试。来源目录复用同一选择逻辑,原生来源标记为 `openbitfun`。
238+
非递归 glob 只遍历模式所需层级;递归 glob 超出固定扫描预算时只跳过该项,保留此前已读取的用户来源。
236239
用户与项目来源最终进入模型前还共享既有 256 文件、2 MiB 渲染上限,避免两个独立来源预算叠加扩大固定提示词。
237240

238241
Workspace Instructions owner 继续读取项目根 `opencode.json`、`opencode.jsonc`、`.opencode/opencode.json` 和
239-
`.opencode/opencode.jsonc` 中的 `instructions` 数组,只接受工作区内相对精确文件与 glob,确定性排序后追加到既有
240-
项目 `AGENTS`/Claude 来源之后。Remote 只执行这条端口可见的项目路径,不读取控制端的用户目录。文件变更不启动
242+
`.opencode/opencode.jsonc` 中的 `instructions` 数组,只接受工作区内相对精确文件与 glob,并确定性排序。
243+
项目来源按 `AGENTS > Claude > OpenCode` 选择唯一指令族:AGENTS 在 `AGENTS.override.md`、`AGENTS.md` 中选首个非空文件;
244+
Claude 在 `CLAUDE.md`、`.claude/CLAUDE.md` 中选首个非空文件,再追加 `CLAUDE.local.md` 与 `.claude/rules/**/*.md`。
245+
不存在或仅含空白的文件不阻断回退;Claude 仅有 local 或 path-scoped rule 时也算有效指令族。选中上层后不再读取下层族,
246+
包括下层无效配置的诊断;启动上下文、来源目录和条件规则延迟激活共用该选择,避免 `Read` 后重新混入已被 AGENTS 屏蔽的 Claude 规则。
247+
Product Assembly 同时关闭 OpenCode 用户读取器的自动项目配置发现,避免该兼容入口重新混入被屏蔽的项目 `instructions`;
248+
全局配置中用户显式指定的工作区相对路径仍按原语义解析。
249+
Remote 只执行这条端口可见的项目路径,不读取控制端的用户目录。文件变更不启动
241250
watcher;用户通过统一的 `/reload instructions`(或默认 `/reload`)失效当前 Session 的 `UserContext` 缓存,下一条
242251
消息重新读取。用户生态失败不会吞掉项目 instructions,但会阻止本次 user context 写入缓存,因而下一条消息自动重试。
243252

‎scripts/check-core-boundaries.test.mjs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,28 @@ const MODULES = [
7272

7373
const TEST_ROOT = join('C:', 'repo');
7474

75+
test('workspace instruction owner requires family priority and non-empty fallback regressions', async () => {
76+
const rule = requiredContentRules.find(
77+
rule => rule.path === 'src/crates/services/services-core/tests/declarative_workspace_instruction_contracts.rs',
78+
);
79+
assert.ok(rule);
80+
const source = await readFile(new URL(`../${rule.path}`, import.meta.url), 'utf8');
81+
const accepts = text => rule.patterns.every(({ regex }) => regex.test(text));
82+
assert.ok(accepts(source));
83+
84+
for (const name of [
85+
'workspace_instruction_families_select_agents_then_claude_then_opencode',
86+
'workspace_instruction_override_skips_empty_files_and_suppresses_claude',
87+
'opencode_project_instructions_support_local_files_and_globs_only',
88+
]) {
89+
assert.ok(!accepts(source.replace(name, 'removed_regression')), `must require ${name}`);
90+
}
91+
assert.ok(!accepts(source.replace(
92+
'workspace_instruction_families_select_agents_then_claude_then_opencode',
93+
'workspace_instruction_files_reads_agents_then_claude_and_skips_empty_files',
94+
)));
95+
});
96+
7597
test('builtin agent catalog requires the GeneralPurpose fast model default', () => {
7698
const rule = requiredContentRules.find(
7799
rule => rule.path === 'src/crates/execution/agent-runtime/src/agents.rs'

‎scripts/core-boundaries/rules/source/required-rules.mjs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,13 @@ export const requiredContentRules = [
272272
'services-core workspace instruction owner must keep local and declarative discovery behavior contracts',
273273
patterns: [
274274
{
275-
regex: /\bworkspace_instruction_files_reads_agents_then_claude_and_skips_empty_files\b/,
275+
regex: /\bworkspace_instruction_families_select_agents_then_claude_then_opencode\b/,
276276
message: 'missing workspace instruction owner behavior regression',
277277
},
278+
{
279+
regex: /\bworkspace_instruction_override_skips_empty_files_and_suppresses_claude\b/,
280+
message: 'missing non-empty workspace instruction override regression',
281+
},
278282
{
279283
regex: /\bopencode_project_instructions_support_local_files_and_globs_only\b/,
280284
message: 'missing declarative workspace instruction behavior regression',

‎src/apps/desktop/README.md‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,36 @@ LAN session in the account slot attempt to restore the official Relay from the
2121
shared identity; failed sign-in leaves the previous credential on disk. Signing
2222
out closes both connections.
2323

24+
## Global agent instructions
25+
26+
Put personal instructions for local OpenBitFun sessions in
27+
`~/.openbitfun/AGENTS.md` (`%USERPROFILE%\.openbitfun\AGENTS.md` on Windows).
28+
If `OPENBITFUN_HOME` is set, the file is read from that directory instead.
29+
A non-empty file replaces Codex's global instructions, including
30+
`AGENTS.override.md`. If the OpenBitFun file is missing, empty, or whitespace-only,
31+
OpenBitFun keeps loading Codex's global instructions as before. OpenCode,
32+
Claude Code, and project instruction files continue to load independently.
33+
34+
After editing the file, run `/reload instructions` in the session. The next
35+
message rebuilds its instruction context. Remote SSH/Docker workspaces use
36+
their workspace instruction files and do not load the controller's global files.
37+
38+
## Project agent instructions
39+
40+
OpenBitFun selects one instruction family at the workspace root, in this order:
41+
42+
1. AGENTS: the first non-empty file among `AGENTS.override.md` and `AGENTS.md`.
43+
2. Claude: the first non-empty file among `CLAUDE.md` and `.claude/CLAUDE.md`,
44+
followed by `CLAUDE.local.md` and `.claude/rules/**/*.md`.
45+
3. OpenCode: local instruction files referenced by the project OpenCode configs.
46+
47+
Missing, empty, and whitespace-only files do not block a fallback. Once a
48+
family supplies instructions, lower-priority families are skipped. A Claude
49+
family containing only local notes or path-scoped rules still counts as selected.
50+
Path-scoped rules load only after a matching file is read, and only when the
51+
Claude family is selected. Use `/reload instructions` after changing the files
52+
to rebuild startup context on the next message.
53+
2454
## Session storage directories
2555

2656
The workspace and session action menus include **Open session storage directory**.

‎src/crates/adapters/opencode-adapter/AGENTS.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ Product-source boundary:
122122
- This crate must not depend on Codex, Claude Code, or another ecosystem adapter.
123123
New ecosystems are sibling adapters registered by Product Assembly, not modes of
124124
this adapter.
125+
- `OpenCodeInstructionSourceOptions::include_project_config` lets Product Assembly
126+
suppress automatic project config discovery after selecting another instruction
127+
family. Keep user-configured relative paths resolved against the workspace;
128+
this switch must not change `OPENCODE_DISABLE_PROJECT_CONFIG` path semantics.
125129
- Production crates must not depend on `openbitfun_opencode_adapter` internals.
126130
Unsupported capabilities must return diagnostics or typed unsupported states
127131
instead of failing at runtime on external plugin content.
@@ -136,6 +140,7 @@ native import; OpenBitFun source suppression and revision fencing still apply.
136140

137141
## Verification
138142

143+
- `cargo test --locked -p openbitfun-opencode-adapter --lib instruction_source::tests::`
139144
- `cargo test -p openbitfun-opencode-adapter --test opencode_source_adapter`
140145
- `cargo test -p openbitfun-opencode-adapter --test opencode_mcp_adapter`
141146
- `cargo test -p openbitfun-opencode-adapter --test opencode_static_source_contracts opencode_command_adapter::`

‎src/crates/adapters/opencode-adapter/src/instruction_source.rs‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ pub struct OpenCodeInstructionSourceOptions {
2121
pub config: OpenCodeLocalConfigOptions,
2222
pub home_dir: Option<PathBuf>,
2323
pub workspace_root: Option<PathBuf>,
24+
/// Controls project config discovery independently of relative user paths.
25+
/// Product Assembly disables it when another project instruction family wins.
26+
pub include_project_config: bool,
2427
pub display_root: String,
2528
}
2629

@@ -42,6 +45,7 @@ impl OpenCodeInstructionSourceOptions {
4245
config: OpenCodeLocalConfigOptions::from_environment(),
4346
home_dir,
4447
workspace_root: None,
48+
include_project_config: true,
4549
display_root,
4650
}
4751
}
@@ -77,7 +81,9 @@ pub fn load_opencode_user_instructions(
7781
}
7882

7983
let mut configured_instructions = None;
80-
for item in local_source_plan(&options.config, options.workspace_root.as_deref(), None) {
84+
let mut discovery_config = options.config.clone();
85+
discovery_config.project_config_enabled &= options.include_project_config;
86+
for item in local_source_plan(&discovery_config, options.workspace_root.as_deref(), None) {
8187
let LocalSourcePlanItem::Config(document) = item else {
8288
continue;
8389
};
@@ -396,10 +402,44 @@ mod tests {
396402
},
397403
home_dir,
398404
workspace_root,
405+
include_project_config: true,
399406
display_root: "$XDG_CONFIG_HOME/opencode".to_string(),
400407
}
401408
}
402409

410+
#[test]
411+
fn suppressed_project_config_keeps_user_relative_instruction_resolution() {
412+
let temp = tempfile::tempdir().expect("tempdir");
413+
let config = temp.path().join("config");
414+
let workspace = temp.path().join("workspace");
415+
std::fs::create_dir_all(&config).unwrap();
416+
std::fs::create_dir_all(workspace.join(".opencode")).unwrap();
417+
std::fs::write(config.join("AGENTS.md"), "User OpenCode rules").unwrap();
418+
std::fs::write(
419+
config.join("opencode.json"),
420+
r#"{"instructions":["guide.md"]}"#,
421+
)
422+
.unwrap();
423+
std::fs::write(
424+
workspace.join("guide.md"),
425+
"User configured workspace guide",
426+
)
427+
.unwrap();
428+
std::fs::write(workspace.join("opencode.json"), "invalid project config").unwrap();
429+
std::fs::write(
430+
workspace.join(".opencode/opencode.json"),
431+
"invalid directory config",
432+
)
433+
.unwrap();
434+
let mut options = source_options(config, None, Some(workspace));
435+
options.include_project_config = false;
436+
437+
let files = load_opencode_user_instructions(&options).expect("user sources only");
438+
assert_eq!(files.len(), 2);
439+
assert_eq!(files[0].content, "User OpenCode rules");
440+
assert_eq!(files[1].content, "User configured workspace guide");
441+
}
442+
403443
#[test]
404444
fn global_config_uses_native_override_order_and_rejects_urls() {
405445
let temp = tempfile::tempdir().expect("tempdir");

‎src/crates/assembly/core/AGENTS.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,13 @@ For configured OpenCode discovery and explicit skill loading, include their owne
368368
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git,external-sources --lib agentic::tools::implementations::skill
369369
```
370370

371+
For native/Codex user-instruction selection, source catalog consistency, and
372+
local versus remote instruction composition:
373+
374+
```bash
375+
cargo test --locked -p openbitfun-core --no-default-features --features external-sources,git --lib instruction_
376+
```
377+
371378
Detached Dispatch controller, target query compatibility, and managed-baseline checks:
372379

373380
```bash

0 commit comments

Comments
 (0)