diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 7377eb2a..00000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,40 +0,0 @@ -# Code of Conduct - -## Who We Are - -The `memory-sync` community is made up of developers surviving in environments of extreme resource inequality. We are rats. We accept that. - -## What We Welcome - -- Resource-constrained developers (no stable income, no corporate budget) -- Independent developers (carrying entire projects alone) -- Students and beginners willing to get their hands dirty -- Users of any language, from any region -- AI Agents (as long as their behavior complies with this Code of Conduct; Issues and PRs are treated equally) - -We welcome Issues, PRs, discussions, and rants — as long as you're serious. - -## What We Don't Welcome - -The following behaviors result in immediate Issue closure / PR rejection / account ban, no explanation: - -- Entitlement seekers: wanting everything ready-made, refusing to even touch a terminal -- Freeloaders who blame others: cursing people instead of providing reproduction steps when things break -- Malicious competitors: taking code, sidestepping AGPL-3.0, and repackaging it as a commercial product -- Resource predators: having stable income yet grabbing marginal developers' free resources -- Harassment: personal attacks, discrimination, stalking -- Overwork glorifiers: treating 996 as a virtue - -## Contributor Obligations - -Issues (human or Agent): provide a minimal reproduction example and environment info; Agents must attach trigger context; no pushing for urgency. - -PRs (human or Agent): open an Issue for discussion first; follow code style; no unrelated changes; Agent-generated PRs must disclose the tool and prompt source. - -## Maintainer Rights - -We reserve the right to close Issues/PRs without explanation, ban violator accounts, and modify this Code of Conduct at any time. We have no obligation to respond to every Issue, accept every PR, or be accountable to commercial demands. - -## Agreement & Enforcement - -[AGPL-3.0](LICENSE). Commercial use violating the license will be pursued. Final interpretation belongs to [@TrueNine](https://github.com/TrueNine). \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f9ad32dd..c5e677b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2798,7 +2798,7 @@ dependencies = [ [[package]] name = "memory-sync" -version = "2026.10502.118" +version = "2026.10503.119" dependencies = [ "tnmsc", ] @@ -5809,7 +5809,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tnmsc" -version = "2026.10502.118" +version = "2026.10503.119" dependencies = [ "clap", "serde_json", @@ -5818,7 +5818,7 @@ dependencies = [ [[package]] name = "tnmsc-local-tests" -version = "2026.10502.118" +version = "2026.10503.119" dependencies = [ "dirs", "json5", @@ -5827,7 +5827,7 @@ dependencies = [ [[package]] name = "tnmsd" -version = "2026.10502.118" +version = "2026.10503.119" dependencies = [ "base64 0.22.1", "chrono", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "tnmsg" -version = "2026.10502.118" +version = "2026.10503.119" dependencies = [ "dirs", "proptest", @@ -5870,7 +5870,7 @@ dependencies = [ [[package]] name = "tnmsm" -version = "2026.10502.118" +version = "2026.10503.119" dependencies = [ "assert_cmd", "clap", @@ -7301,7 +7301,7 @@ dependencies = [ [[package]] name = "xtask" -version = "2026.10502.118" +version = "2026.10503.119" dependencies = [ "clap", "serde", diff --git a/Cargo.toml b/Cargo.toml index 8c9ca4d1..d3ff351c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ members = [ ] [workspace.package] -version = "2026.10502.118" +version = "2026.10503.119" edition = "2024" rust-version = "1.88" license = "AGPL-3.0-only" diff --git a/README.md b/README.md deleted file mode 100644 index 62312c5b..00000000 --- a/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# memory-sync - -![rat](/.attachments/rat.svg) - -A rat carries even its own memories when moving. `memory-sync` is that kind of tool-rat: expecting no platform charity, relying on no single IDE's privileged interface — it搬运-fetches, disassembles, and reassembles every config, prompt, and memory file it can read. - -## What It Does - -- Treat `.mdx` / `.src.mdx` as the single source of truth; generate native configs and managed artifacts for each tool from one source -- Unified input asset model: Memory, Skills, Skill categories, Commands, Sub-agents, Rules, README, etc. -- Auto-write configs for each tool: AGENTS.md, Claude Code, Codex CLI, Cursor, Windsurf, Qoder, Trae, Warp, JetBrains AI, etc. -- Manage derived artifacts: prompt outputs, skills exports organized as `skills///`, README-class outputs -- Multiple entry points: `tnmsc` CLI, private SDK, MCP stdio server, Tauri GUI -- Fine-grained write-scope control (`outputScopes`, `cleanupProtection`) -- Source and derivations are auditable — no silent source mutations, no hidden residuals -- Memories follow the person, not the project — no leakage -## Install - -```sh -npm install -g @truenine/memory-sync-cli -``` - -MCP server: - -```sh -npm install -g @truenine/memory-sync-mcp -``` - -## Supported Tools - -| Type | Tools | -| --- | --- | -| IDE / Editor | Cursor, Windsurf, Qoder, Trae, Trae CN, JetBrains AI, Zed, VS Code | -| CLI | Claude Code, Codex CLI, Gemini CLI, Droid CLI, Opencode, Warp | -| Other outputs | AGENTS.md, categorized Skills, README, `.editorconfig`, `.git/info/exclude` | - -## Architecture - -- **SDK** (`tnmsd` crate / `@truenine/memory-sync-sdk`): private mixed core -- **CLI** (`tnmsc` / `@truenine/memory-sync-cli`): public command entry -- **MCP** (`tnmsm` / `@truenine/memory-sync-mcp`): stdio server -- **GUI** (Tauri): desktop entry -## FAQ - -**If AI tools adopt a unified standard, is this project still needed?** Then it has fulfilled its historical mission. - -**We already have AGENTS.md and MCP standards — why still need this?** Native targets differ; conditional prompts still need concrete landing points. `AGENTS.md` is a spec; `memory-sync` is the porter and assembler. - -**Are there things in prompts you don't want to leave behind?** Yes. Hence the cleanup and protection boundaries. - -## Who It's For - -You need dev experience, Git fluency, and terminal comfort. - -If you're scraping by in a world of profoundly unequal resources — free tiers, trial quotas, third-party scripts — `memory-sync` is for you. - -## Who It's Not For - -- Those with stable income and corporate budgets who still grab marginal developers' free resources -- Entitlement seekers who want everything handed to them -- Those who glorify overwork as virtue -- Malicious competitors stepping on others to climb -**This is not a tool for capital to optimise costs — it's a rat's small act of defiance in a world of resource injustice.** - -## Created by - -- [TrueNine](https://github.com/TrueNine) -- [zjarlin](https://github.com/zjarlin) -## License - -[AGPL-3.0](LICENSE) \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index ad3653e6..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,41 +0,0 @@ -# Security Policy - -## Supported Versions - -Only the latest version receives security fixes. - -| Version | Supported | -|---------|-----------| -| Latest | ✅ | -| Historical | ❌ | - -## Reporting a Vulnerability - -Do not report in public Issues. Contact [@TrueNine](https://github.com/TrueNine) via GitHub Security Advisory or email. - -Include: vulnerability description and impact scope, reproduction steps, environment info, fix suggestion (if any). - -## Response Time - -Maintainers are people, not a security team — no SLA. We'll confirm as soon as possible, fix within a reasonable timeframe, and disclose publicly after the fix. Do not push for urgency. - -## Scope - -CLI / SDK / MCP / GUI toolchain. Security boundaries: - -- **Read**: user `.src.mdx` source files, project config, global config (`~/.aindex/.tnmsc.json`), repo metadata required for sync -- **Write**: target tool config directories, managed prompt artifacts paired beside their source files, generated outputs -- **Cleanup**: erase managed outputs and residuals during sync or cleanup - -Out of scope: vulnerabilities in target AI tools themselves, user prompt content compliance, hardening third-party dependencies outside this repo. - -## Design Principles - -- Source and derivation separation — auditable and traceable -- Cleanup targets only managed outputs — no expanded deletion scope -- No hidden telemetry -- External network behavior must be explicit - -## License - -[AGPL-3.0](LICENSE). Commercial use violating the license will be pursued. diff --git a/cli/local-tests/tests/opencode_smoke.rs b/cli/local-tests/tests/opencode_smoke.rs index 46499e7d..27b0af01 100644 --- a/cli/local-tests/tests/opencode_smoke.rs +++ b/cli/local-tests/tests/opencode_smoke.rs @@ -281,7 +281,7 @@ fn local_opencode_global_md_url_interpolation() { } #[test] -fn local_opencode_project_content_includes_workspace_memory() { +fn local_opencode_global_memory_stays_out_of_project_agents_md() { let fixture = IsolatedOpencodeFixture::new(); fixture @@ -295,12 +295,12 @@ fn local_opencode_project_content_includes_workspace_memory() { let global_content = fs::read_to_string(fixture.global_agents_path()).unwrap(); assert!( - project_content.len() >= global_content.len(), - "project .opencode/AGENTS.md should be at least as long as global content" + global_content.contains("TrueNine"), + "global ~/.config/opencode/AGENTS.md should contain global memory content" ); assert!( - project_content.contains("TrueNine"), - "project .opencode/AGENTS.md should contain global memory content" + !project_content.contains("TrueNine"), + "project .opencode/AGENTS.md should not contain global memory content" ); assert!( project_content.contains("Project root instructions"), diff --git a/cli/npm/darwin-arm64/package.json b/cli/npm/darwin-arm64/package.json index 5a58538c..d6fc0088 100644 --- a/cli/npm/darwin-arm64/package.json +++ b/cli/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-cli-darwin-arm64", - "version": "2026.10502.118", + "version": "2026.10503.119", "description": "tnmsc native binary for macOS arm64", "author": "TrueNine", "license": "AGPL-3.0-only", diff --git a/cli/npm/darwin-x64/package.json b/cli/npm/darwin-x64/package.json index 9ec71d66..667f1e89 100644 --- a/cli/npm/darwin-x64/package.json +++ b/cli/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-cli-darwin-x64", - "version": "2026.10502.118", + "version": "2026.10503.119", "description": "tnmsc native binary for macOS x64", "author": "TrueNine", "license": "AGPL-3.0-only", diff --git a/cli/npm/linux-arm64-gnu/package.json b/cli/npm/linux-arm64-gnu/package.json index 8ee88e86..c73333a2 100644 --- a/cli/npm/linux-arm64-gnu/package.json +++ b/cli/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-cli-linux-arm64-gnu", - "version": "2026.10502.118", + "version": "2026.10503.119", "description": "tnmsc native binary for Linux arm64 (glibc)", "author": "TrueNine", "license": "AGPL-3.0-only", diff --git a/cli/npm/linux-x64-gnu/package.json b/cli/npm/linux-x64-gnu/package.json index 6445bd76..bd285c63 100644 --- a/cli/npm/linux-x64-gnu/package.json +++ b/cli/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-cli-linux-x64-gnu", - "version": "2026.10502.118", + "version": "2026.10503.119", "description": "tnmsc native binary for Linux x64 (glibc)", "author": "TrueNine", "license": "AGPL-3.0-only", diff --git a/cli/npm/win32-x64-msvc/package.json b/cli/npm/win32-x64-msvc/package.json index d443db25..34b95f0c 100644 --- a/cli/npm/win32-x64-msvc/package.json +++ b/cli/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-cli-win32-x64-msvc", - "version": "2026.10502.118", + "version": "2026.10503.119", "description": "tnmsc native binary for Windows x64", "author": "TrueNine", "license": "AGPL-3.0-only", diff --git a/cli/package.json b/cli/package.json index 8599accf..796fc4d2 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-cli", - "version": "2026.10502.118", + "version": "2026.10503.119", "description": "TrueNine Memory Synchronization CLI metadata package", "author": "TrueNine", "license": "AGPL-3.0-only", @@ -34,10 +34,10 @@ "test": "cargo test --manifest-path Cargo.toml" }, "optionalDependencies": { - "@truenine/memory-sync-cli-darwin-arm64": "2026.10502.118", - "@truenine/memory-sync-cli-darwin-x64": "2026.10502.118", - "@truenine/memory-sync-cli-linux-arm64-gnu": "2026.10502.118", - "@truenine/memory-sync-cli-linux-x64-gnu": "2026.10502.118", - "@truenine/memory-sync-cli-win32-x64-msvc": "2026.10502.118" + "@truenine/memory-sync-cli-darwin-arm64": "2026.10503.119", + "@truenine/memory-sync-cli-darwin-x64": "2026.10503.119", + "@truenine/memory-sync-cli-linux-arm64-gnu": "2026.10503.119", + "@truenine/memory-sync-cli-linux-x64-gnu": "2026.10503.119", + "@truenine/memory-sync-cli-win32-x64-msvc": "2026.10503.119" } } diff --git a/doc/package.json b/doc/package.json index 8c135522..5191994e 100644 --- a/doc/package.json +++ b/doc/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-docs", - "version": "2026.10502.118", + "version": "2026.10503.119", "private": true, "packageManager": "pnpm@10.33.0", "description": "Chinese-first manifesto-led documentation site for @truenine/memory-sync.", diff --git a/gui/package.json b/gui/package.json index 16d4dc7d..1d8e931f 100644 --- a/gui/package.json +++ b/gui/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-gui", - "version": "2026.10502.118", + "version": "2026.10503.119", "private": true, "engines": { "node": ">= 22" diff --git a/gui/src-tauri/Cargo.toml b/gui/src-tauri/Cargo.toml index 78256b5b..ec36f0f4 100644 --- a/gui/src-tauri/Cargo.toml +++ b/gui/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tnmsg" -version = "2026.10502.118" +version = "2026.10503.119" description = "Memory Sync desktop GUI application" authors.workspace = true edition.workspace = true diff --git a/gui/src-tauri/tauri.conf.json b/gui/src-tauri/tauri.conf.json index b1762a5a..da278f6c 100644 --- a/gui/src-tauri/tauri.conf.json +++ b/gui/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "$schema": "https://schema.tauri.app/config/2", - "version": "2026.10502.118", + "version": "2026.10503.119", "productName": "Memory Sync", "identifier": "org.truenine.memory-sync", "build": { diff --git a/mcp/npm/darwin-arm64/package.json b/mcp/npm/darwin-arm64/package.json index 9a589dd9..06b0d29a 100644 --- a/mcp/npm/darwin-arm64/package.json +++ b/mcp/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-mcp-darwin-arm64", - "version": "2026.10502.118", + "version": "2026.10503.119", "description": "tnmsm native binary for macOS arm64", "author": "TrueNine", "license": "AGPL-3.0-only", diff --git a/mcp/npm/darwin-x64/package.json b/mcp/npm/darwin-x64/package.json index 06913d8a..e32fba07 100644 --- a/mcp/npm/darwin-x64/package.json +++ b/mcp/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-mcp-darwin-x64", - "version": "2026.10502.118", + "version": "2026.10503.119", "description": "tnmsm native binary for macOS x64", "author": "TrueNine", "license": "AGPL-3.0-only", diff --git a/mcp/npm/linux-arm64-gnu/package.json b/mcp/npm/linux-arm64-gnu/package.json index c4f60c36..66d66c06 100644 --- a/mcp/npm/linux-arm64-gnu/package.json +++ b/mcp/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-mcp-linux-arm64-gnu", - "version": "2026.10502.118", + "version": "2026.10503.119", "description": "tnmsm native binary for Linux arm64 (glibc)", "author": "TrueNine", "license": "AGPL-3.0-only", diff --git a/mcp/npm/linux-x64-gnu/package.json b/mcp/npm/linux-x64-gnu/package.json index daa85c24..20c06bef 100644 --- a/mcp/npm/linux-x64-gnu/package.json +++ b/mcp/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-mcp-linux-x64-gnu", - "version": "2026.10502.118", + "version": "2026.10503.119", "description": "tnmsm native binary for Linux x64 (glibc)", "author": "TrueNine", "license": "AGPL-3.0-only", diff --git a/mcp/npm/win32-x64-msvc/package.json b/mcp/npm/win32-x64-msvc/package.json index 31810fbd..18ee7fa1 100644 --- a/mcp/npm/win32-x64-msvc/package.json +++ b/mcp/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-mcp-win32-x64-msvc", - "version": "2026.10502.118", + "version": "2026.10503.119", "description": "tnmsm native binary for Windows x64", "author": "TrueNine", "license": "AGPL-3.0-only", diff --git a/mcp/package.json b/mcp/package.json index 5223f3ca..25b9cc06 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync-mcp", - "version": "2026.10502.118", + "version": "2026.10503.119", "description": "TrueNine Memory Sync MCP metadata package", "author": "TrueNine", "license": "AGPL-3.0-only", @@ -32,10 +32,10 @@ "test": "cargo test --manifest-path Cargo.toml" }, "optionalDependencies": { - "@truenine/memory-sync-mcp-darwin-arm64": "2026.10502.118", - "@truenine/memory-sync-mcp-darwin-x64": "2026.10502.118", - "@truenine/memory-sync-mcp-linux-arm64-gnu": "2026.10502.118", - "@truenine/memory-sync-mcp-linux-x64-gnu": "2026.10502.118", - "@truenine/memory-sync-mcp-win32-x64-msvc": "2026.10502.118" + "@truenine/memory-sync-mcp-darwin-arm64": "2026.10503.119", + "@truenine/memory-sync-mcp-darwin-x64": "2026.10503.119", + "@truenine/memory-sync-mcp-linux-arm64-gnu": "2026.10503.119", + "@truenine/memory-sync-mcp-linux-x64-gnu": "2026.10503.119", + "@truenine/memory-sync-mcp-win32-x64-msvc": "2026.10503.119" } } diff --git a/package.json b/package.json index 3f8040d4..829befa2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@truenine/memory-sync", - "version": "2026.10502.118", + "version": "2026.10503.119", "description": "Cross-AI-tool prompt synchronisation toolkit (CLI + Tauri desktop GUI) — one ruleset, multi-target adaptation. Monorepo powered by pnpm + Turbo.", "license": "AGPL-3.0-only", "keywords": [ diff --git a/sdk/src/domain/output_plans/claude_code_output_plan.rs b/sdk/src/domain/output_plans/claude_code_output_plan.rs index 23ba1a55..6c91b173 100644 --- a/sdk/src/domain/output_plans/claude_code_output_plan.rs +++ b/sdk/src/domain/output_plans/claude_code_output_plan.rs @@ -14,7 +14,6 @@ const CLAUDE_CODE_MEMORY_FILE: &str = "CLAUDE.md"; const CLAUDE_CODE_SETTINGS_FILE: &str = "settings.json"; const CLAUDE_CODE_SETTINGS_LOCAL_FILE: &str = "settings.local.json"; const CLAUDE_CODE_GLOBAL_CONFIG_DIR: &str = ".claude"; -const AGENTS_OUTPUT_ADAPTOR: &str = "AgentsOutputAdaptor"; const PROJECT_SCOPE: &str = "project"; pub fn collect_claude_code_output_plan(context_json: &str) -> Result { @@ -45,77 +44,40 @@ fn build_output_files( ) -> Vec { let mut output_files = Vec::new(); let prompt_projects = get_project_prompt_output_projects(workspace); - let agents_registered = context - .registered_output_plugins - .as_ref() - .map(|plugins| plugins.iter().any(|name| name == AGENTS_OUTPUT_ADAPTOR)) - .unwrap_or(false); - - if agents_registered { - // Fixes #379: Claude's project files should switch to the global-only memory - // payload while AgentsOutputAdaptor is registered. - if let Some(global_memory) = context.global_memory.as_ref() { - for project in &prompt_projects { - let Some(project_root_dir) = resolve_project_root_dir(workspace, project) else { - continue; - }; - output_files.push(BaseOutputFileDeclarationDto { - path: project_root_dir - .join(CLAUDE_CODE_MEMORY_FILE) - .to_string_lossy() - .into_owned(), - scope: Some(PROJECT_SCOPE.to_string()), - content: global_memory.content.clone(), - encoding: None, - }); - } - } - } else { - // 项目级 CLAUDE.md(根目录 + 子目录) - // 工作区根 CLAUDE.md 需要同时携带全局 memory 和工作区 prompt, - // 这样打包 CLI 在裸容器里安装后也能直接看到完整的 Claude 上下文。 - for project in &prompt_projects { - let Some(project_root_dir) = resolve_project_root_dir(workspace, project) else { - continue; - }; - if let Some(root_prompt) = project.root_memory_prompt.as_ref() { - let content = if project.is_workspace_root_project == Some(true) { - merge_workspace_root_memory( - context - .global_memory - .as_ref() - .map(|prompt| prompt.content.as_str()), - &root_prompt.content, - ) - } else { - root_prompt.content.clone() - }; + // Fixes #379 historical note: that issue incorrectly attributed global + // memory to project CLAUDE.md files while AgentsOutputAdaptor is active. + // Fixes #389: aindex/global.mdx belongs only in ~/.claude/CLAUDE.md. + // 项目级 CLAUDE.md(根目录 + 子目录)只承载项目 / 工作区提示。 + for project in &prompt_projects { + let Some(project_root_dir) = resolve_project_root_dir(workspace, project) else { + continue; + }; + if let Some(root_prompt) = project.root_memory_prompt.as_ref() { + output_files.push(BaseOutputFileDeclarationDto { + path: project_root_dir + .join(CLAUDE_CODE_MEMORY_FILE) + .to_string_lossy() + .into_owned(), + scope: Some(PROJECT_SCOPE.to_string()), + content: root_prompt.content.clone(), + encoding: None, + }); + } + + if let Some(child_prompts) = project.child_memory_prompts.as_ref() { + for child_prompt in child_prompts { output_files.push(BaseOutputFileDeclarationDto { - path: project_root_dir + path: resolve_relative_path(&child_prompt.dir) .join(CLAUDE_CODE_MEMORY_FILE) .to_string_lossy() .into_owned(), scope: Some(PROJECT_SCOPE.to_string()), - content, + content: child_prompt.content.clone(), encoding: None, }); } - - if let Some(child_prompts) = project.child_memory_prompts.as_ref() { - for child_prompt in child_prompts { - output_files.push(BaseOutputFileDeclarationDto { - path: resolve_relative_path(&child_prompt.dir) - .join(CLAUDE_CODE_MEMORY_FILE) - .to_string_lossy() - .into_owned(), - scope: Some(PROJECT_SCOPE.to_string()), - content: child_prompt.content.clone(), - encoding: None, - }); - } - } } } @@ -237,18 +199,6 @@ fn build_output_files( output_files } -fn merge_workspace_root_memory(global_memory: Option<&str>, workspace_prompt: &str) -> String { - let global_memory = global_memory.unwrap_or("").trim(); - let workspace_prompt = workspace_prompt.trim(); - - match (global_memory.is_empty(), workspace_prompt.is_empty()) { - (true, true) => String::new(), - (false, true) => global_memory.to_string(), - (true, false) => workspace_prompt.to_string(), - (false, false) => format!("{global_memory}\n\n{workspace_prompt}"), - } -} - fn resolve_skill_dir_name(skill: &crate::domain::plugin_shared::SkillPrompt) -> String { if let Some(category_name) = skill.category_name.as_deref().map(str::trim) && !category_name.is_empty() @@ -532,19 +482,6 @@ mod tests { assert_eq!(result, "Just content."); } - #[test] - fn merge_workspace_root_memory_includes_global_and_workspace_content() { - let merged = merge_workspace_root_memory( - Some("Global memory from aindex"), - "Workspace root prompt from aindex", - ); - - assert_eq!( - merged, - "Global memory from aindex\n\nWorkspace root prompt from aindex" - ); - } - fn make_test_skill(name: &str) -> crate::domain::plugin_shared::SkillPrompt { use crate::domain::plugin_shared::*; SkillPrompt { diff --git a/sdk/src/domain/output_plans/codex_output_plan.rs b/sdk/src/domain/output_plans/codex_output_plan.rs index c454341d..f6664473 100644 --- a/sdk/src/domain/output_plans/codex_output_plan.rs +++ b/sdk/src/domain/output_plans/codex_output_plan.rs @@ -30,7 +30,6 @@ const CODEX_GLOBAL_CONFIG_DIR: &str = ".codex"; const CODEX_PROMPTS_DIR: &str = "prompts"; const CODEX_AGENTS_DIR: &str = "agents"; const CODEX_SKILLS_DIR: &str = "skills"; -const AGENTS_OUTPUT_ADAPTOR: &str = "AgentsOutputAdaptor"; const PROJECT_SCOPE: &str = "project"; fn resolve_skill_dir_name(skill: &crate::domain::plugin_shared::SkillPrompt) -> String { @@ -82,11 +81,6 @@ fn build_output_files( let mut output_files = Vec::new(); let prompt_projects = get_project_prompt_output_projects(workspace); let project_output_projects = get_project_output_projects(workspace); - let agents_registered = context - .registered_output_plugins - .as_ref() - .map(|plugins| plugins.iter().any(|name| name == AGENTS_OUTPUT_ADAPTOR)) - .unwrap_or(false); // Global ~/.codex/AGENTS.md (use raw content to match aindex/global.mdx) if let Some(global_memory) = context.global_memory.as_ref() { @@ -107,58 +101,39 @@ fn build_output_files( }); } - if agents_registered { - // Fixes #379: Codex project AGENTS.md files should switch to the - // dedicated global-memory payload when AgentsOutputAdaptor is active. - if let Some(global_memory) = context.global_memory.as_ref() { - for project in &prompt_projects { - let Some(project_root_dir) = resolve_project_root_dir(workspace, project) else { - continue; - }; - output_files.push(BaseOutputFileDeclarationDto { - path: project_root_dir - .join(CODEX_INSTRUCTIONS_FILE) - .to_string_lossy() - .into_owned(), - scope: Some(PROJECT_SCOPE.to_string()), - content: global_memory.content.clone(), - encoding: None, - }); - } + // Fixes #379 historical note: that issue incorrectly attributed global + // memory to project AGENTS.md files while AgentsOutputAdaptor is active. + // Fixes #389: aindex/global.mdx belongs only in ~/.codex/AGENTS.md. + for project in &prompt_projects { + let Some(project_root_dir) = resolve_project_root_dir(workspace, project) else { + continue; + }; + + if let Some(root_prompt) = project.root_memory_prompt.as_ref() { + output_files.push(BaseOutputFileDeclarationDto { + path: project_root_dir + .join(CODEX_INSTRUCTIONS_FILE) + .to_string_lossy() + .into_owned(), + scope: Some(PROJECT_SCOPE.to_string()), + content: root_prompt.content.clone(), + encoding: None, + }); } - } else { - let global_memory_content = context.global_memory.as_ref().map(|m| m.content.as_str()); - for project in &prompt_projects { - let Some(project_root_dir) = resolve_project_root_dir(workspace, project) else { - continue; - }; - if let Some(root_prompt) = project.root_memory_prompt.as_ref() { + if let Some(child_prompts) = project.child_memory_prompts.as_ref() { + // Fixes #380: Codex must emit nested AGENTS.md files for child memory prompts. + for child_prompt in child_prompts { output_files.push(BaseOutputFileDeclarationDto { - path: project_root_dir + path: resolve_relative_path(&child_prompt.dir) .join(CODEX_INSTRUCTIONS_FILE) .to_string_lossy() .into_owned(), scope: Some(PROJECT_SCOPE.to_string()), - content: combine_global_with_content(global_memory_content, &root_prompt.content), + content: child_prompt.content.clone(), encoding: None, }); } - - if let Some(child_prompts) = project.child_memory_prompts.as_ref() { - // Fixes #380: Codex must emit nested AGENTS.md files for child memory prompts. - for child_prompt in child_prompts { - output_files.push(BaseOutputFileDeclarationDto { - path: resolve_relative_path(&child_prompt.dir) - .join(CODEX_INSTRUCTIONS_FILE) - .to_string_lossy() - .into_owned(), - scope: Some(PROJECT_SCOPE.to_string()), - content: child_prompt.content.clone(), - encoding: None, - }); - } - } } } @@ -536,15 +511,6 @@ fn camel_to_kebab(s: &str) -> String { result } -fn combine_global_with_content(global_content: Option<&str>, project_content: &str) -> String { - match global_content { - Some(global) if !global.trim().is_empty() => { - format!("{}\n\n{}", global.trim(), project_content.trim()) - } - _ => project_content.to_string(), - } -} - fn build_cleanup(workspace: &Workspace) -> CleanupDeclarationsDto { let mut delete = Vec::new(); diff --git a/sdk/src/domain/output_plans/mod.rs b/sdk/src/domain/output_plans/mod.rs index e37474c8..66e7d342 100644 --- a/sdk/src/domain/output_plans/mod.rs +++ b/sdk/src/domain/output_plans/mod.rs @@ -200,9 +200,10 @@ mod regression_tests { } #[test] - fn regression_379_agents_output_mode_uses_global_memory_for_project_files() { - // Fixes #379: when AgentsOutputAdaptor is registered, project output files should switch - // to the global-only memory mode that the cursor/warp/windsurf/trae plans already use. + fn regression_389_global_memory_stays_in_global_tool_files() { + // Fixes #379 historical note: that issue incorrectly attributed global + // memory to project files while AgentsOutputAdaptor is active. + // Fixes #389: global memory belongs only in tool-global prompt files. let workspace_root = "/workspace"; let project_root = format!("{workspace_root}/project-a"); let context = OutputContext { @@ -218,37 +219,25 @@ mod regression_tests { .output_files .iter() .any(|file| file.path == format!("{project_root}/CLAUDE.md") - && file.content == "global memory"), - "claude output plan must emit global-only CLAUDE.md when AgentsOutputAdaptor is active" - ); - assert!( - !claude_plan - .output_files - .iter() - .any(|file| file.path.contains("/packages/api/CLAUDE.md")), - "claude output plan must omit child project memory files in agents mode" + && file.content == "project root"), + "claude project CLAUDE.md must keep project memory when AgentsOutputAdaptor is active" ); - - let gemini_plan = - crate::domain::output_plans::gemini_output_plan::build_gemini_output_plan(&context).unwrap(); assert!( - gemini_plan + claude_plan .output_files .iter() - .any(|file| file.path == format!("{project_root}/GEMINI.md") + .any(|file| file.path.ends_with("/.claude/CLAUDE.md") + && file.scope.as_deref() == Some("global") && file.content == "global memory"), - "gemini output plan must emit global-only GEMINI.md when AgentsOutputAdaptor is active" + "claude global CLAUDE.md must receive global memory" ); - - let droid_plan = - crate::domain::output_plans::droid_output_plan::build_droid_output_plan(&context).unwrap(); assert!( - droid_plan + !claude_plan .output_files .iter() - .any(|file| file.path == format!("{project_root}/AGENTS.md") - && file.content == "global memory"), - "droid output plan must emit global-only AGENTS.md when AgentsOutputAdaptor is active" + .any(|file| file.path == format!("{project_root}/CLAUDE.md") + && file.content.contains("global memory")), + "claude project CLAUDE.md must not receive global memory" ); let opencode_plan = @@ -257,9 +246,24 @@ mod regression_tests { assert!( opencode_plan.output_files.iter().any(|file| { file.path == format!("{project_root}/.opencode/AGENTS.md") + && file.content == "project root" + }), + "opencode project AGENTS.md must keep project memory when AgentsOutputAdaptor is active" + ); + assert!( + opencode_plan.output_files.iter().any(|file| { + file.path.ends_with("/.config/opencode/AGENTS.md") + && file.scope.as_deref() == Some("global") && file.content == "global memory" }), - "opencode output plan must emit global-only project memory when AgentsOutputAdaptor is active" + "opencode global AGENTS.md must receive global memory" + ); + assert!( + !opencode_plan.output_files.iter().any(|file| { + file.path == format!("{project_root}/.opencode/AGENTS.md") + && file.content.contains("global memory") + }), + "opencode project AGENTS.md must not receive global memory" ); let codex_plan = @@ -269,8 +273,25 @@ mod regression_tests { .output_files .iter() .any(|file| file.path == format!("{project_root}/AGENTS.md") + && file.content == "project root"), + "codex project AGENTS.md must keep project memory when AgentsOutputAdaptor is active" + ); + assert!( + codex_plan + .output_files + .iter() + .any(|file| file.path.ends_with("/.codex/AGENTS.md") + && file.scope.as_deref() == Some("global") && file.content == "global memory"), - "codex output plan must emit global-only AGENTS.md when AgentsOutputAdaptor is active" + "codex global AGENTS.md must receive global memory" + ); + assert!( + !codex_plan + .output_files + .iter() + .any(|file| file.path == format!("{project_root}/AGENTS.md") + && file.content.contains("global memory")), + "codex project AGENTS.md must not receive global memory" ); } } diff --git a/sdk/src/domain/output_plans/opencode_output_plan.rs b/sdk/src/domain/output_plans/opencode_output_plan.rs index 665cb240..234747a1 100644 --- a/sdk/src/domain/output_plans/opencode_output_plan.rs +++ b/sdk/src/domain/output_plans/opencode_output_plan.rs @@ -13,7 +13,6 @@ const OPENCODE_PLUGIN_NAME: &str = "OpencodeCLIOutputAdaptor"; const OPENCODE_MEMORY_FILE: &str = "AGENTS.md"; const OPENCODE_PROJECT_CONFIG_DIR: &str = ".opencode"; const OPENCODE_GLOBAL_CONFIG_DIR: &str = ".config/opencode"; -const AGENTS_OUTPUT_ADAPTOR: &str = "AgentsOutputAdaptor"; const PROJECT_SCOPE: &str = "project"; fn resolve_skill_dir_name(skill: &crate::domain::plugin_shared::SkillPrompt) -> String { @@ -64,70 +63,42 @@ fn build_output_files( ) -> Vec { let mut output_files = Vec::new(); let prompt_projects = get_project_prompt_output_projects(workspace); - let agents_registered = context - .registered_output_plugins - .as_ref() - .map(|plugins| plugins.iter().any(|name| name == AGENTS_OUTPUT_ADAPTOR)) - .unwrap_or(false); - - if agents_registered { - // Fixes #379: Opencode project memory should collapse to the global-only payload - // while AgentsOutputAdaptor is registered. - if let Some(global_memory) = context.global_memory.as_ref() { - for project in &prompt_projects { - let Some(project_root_dir) = resolve_project_root_dir(workspace, project) else { - continue; - }; - output_files.push(BaseOutputFileDeclarationDto { - path: project_root_dir - .join(OPENCODE_PROJECT_CONFIG_DIR) - .join(OPENCODE_MEMORY_FILE) - .to_string_lossy() - .into_owned(), - scope: Some(PROJECT_SCOPE.to_string()), - content: global_memory.content.clone(), - encoding: None, - }); - } - } - } else { - let global_memory_content = context.global_memory.as_ref().map(|m| m.content.as_str()); - for project in &prompt_projects { - let Some(project_root_dir) = resolve_project_root_dir(workspace, project) else { - continue; - }; + // Fixes #379 historical note: that issue incorrectly attributed global + // memory to project .opencode/AGENTS.md while AgentsOutputAdaptor is active. + // Fixes #389: aindex/global.mdx belongs only in ~/.config/opencode/AGENTS.md. + for project in &prompt_projects { + let Some(project_root_dir) = resolve_project_root_dir(workspace, project) else { + continue; + }; - if let Some(root_prompt) = project.root_memory_prompt.as_ref() { - let combined_content = - combine_global_with_content(global_memory_content, &root_prompt.content); + if let Some(root_prompt) = project.root_memory_prompt.as_ref() { + output_files.push(BaseOutputFileDeclarationDto { + path: project_root_dir + .join(OPENCODE_PROJECT_CONFIG_DIR) + .join(OPENCODE_MEMORY_FILE) + .to_string_lossy() + .into_owned(), + scope: Some(PROJECT_SCOPE.to_string()), + content: root_prompt.content.clone(), + encoding: None, + }); + } + + if let Some(child_prompts) = project.child_memory_prompts.as_ref() { + // Fixes #380: Opencode needs nested .opencode/AGENTS.md files for child prompts. + for child_prompt in child_prompts { output_files.push(BaseOutputFileDeclarationDto { - path: project_root_dir + path: resolve_relative_path(&child_prompt.dir) .join(OPENCODE_PROJECT_CONFIG_DIR) .join(OPENCODE_MEMORY_FILE) .to_string_lossy() .into_owned(), scope: Some(PROJECT_SCOPE.to_string()), - content: combined_content, + content: child_prompt.content.clone(), encoding: None, }); } - - if let Some(child_prompts) = project.child_memory_prompts.as_ref() { - // Fixes #380: Opencode needs nested .opencode/AGENTS.md files for child prompts. - for child_prompt in child_prompts { - output_files.push(BaseOutputFileDeclarationDto { - path: resolve_relative_path(&child_prompt.dir) - .join(OPENCODE_PROJECT_CONFIG_DIR) - .join(OPENCODE_MEMORY_FILE) - .to_string_lossy() - .into_owned(), - scope: Some(PROJECT_SCOPE.to_string()), - content: child_prompt.content.clone(), - encoding: None, - }); - } - } } } @@ -529,15 +500,6 @@ fn indent_yaml_list_items(yaml: &str) -> String { .join("\n") } -fn combine_global_with_content(global_content: Option<&str>, project_content: &str) -> String { - match global_content { - Some(global) if !global.trim().is_empty() => { - format!("{}\n\n{}", global.trim(), project_content.trim()) - } - _ => project_content.to_string(), - } -} - fn build_cleanup(workspace: &Workspace) -> CleanupDeclarationsDto { let mut delete = Vec::new();