diff --git a/README.md b/README.md index ca39ef418..78de65940 100644 --- a/README.md +++ b/README.md @@ -449,7 +449,7 @@ overwrite user-modified agents. | Agent | Activation | MCP config | Durable context / augmentation | |-------|------------|------------|--------------------------------| | Claude Code | Detected | `~/.claude.json` | Skill + three exact-tool graph agents; `SessionStart`, `SubagentStart`, non-blocking `PreToolUse` for `Grep`/`Glob`, and post-`Read` coverage | -| Codex CLI | Detected | `$CODEX_HOME/config.toml` | `AGENTS.md`, skill, three read-only agents; `SessionStart` + `SubagentStart` | +| Codex CLI | Detected | `$CODEX_HOME/config.toml` | Skill, three read-only agents; `SessionStart` + `SubagentStart` | | Gemini CLI | Detected | `.gemini/settings.json` | `GEMINI.md`, three explicit read/graph-tool subagents; `BeforeTool`, `AfterTool` `read_file` coverage, and `SessionStart` | | Zed | Detected | platform `settings.json` (JSONC) | `AGENTS.md` + shared skill | | OpenCode | Detected | `$OPENCODE_CONFIG` or resolved global config | `AGENTS.md`, skill, three deny-by-default read-only agents | @@ -492,6 +492,10 @@ overwrite user-modified agents. | IBM Bob IDE | Conditional | Existing `~/.bob/mcp.json` | Shared rule + IDE skill; no invented hook or agent | | Sourcegraph Cody | Explicit opt-in | Existing `$CBM_CODY_CONFIG_PATH` | MCP only | +For Codex, install no longer creates global `$CODEX_HOME/AGENTS.md` guidance. When that file +already exists, install removes only the legacy `codebase-memory-mcp` managed block and preserves +all user-owned bytes; the file itself remains even when the cleanup leaves it empty. + ### Sessions, compaction, and subagents Hooks installed by this project are fail-open and context-only. Claude Code's diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 53dc130ae..f2c5d6d37 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -144,11 +144,12 @@ run_no_crash() { TMPDIR=$(smoke_mktemp_dir) DRYRUN_HOME="" +CODEX_LIFECYCLE_HOME="" # On MSYS2/Windows, convert POSIX path to native Windows path for the binary if command -v cygpath &>/dev/null; then TMPDIR=$(cygpath -m "$TMPDIR") fi -trap 'smoke_rmtree "$TMPDIR" "${DRYRUN_HOME:-}"' EXIT +trap 'smoke_rmtree "$TMPDIR" "${DRYRUN_HOME:-}" "${CODEX_LIFECYCLE_HOME:-}"' EXIT CLI_STDERR=$(smoke_mktemp_file) # 10 of the cli call sites assign directly (VAR=$(cli ...)). Under @@ -1427,6 +1428,7 @@ echo '# Personal Rovo guidance' > "$ROVO_INSTRUCTIONS" PHASE8_INSTALL_RC=0 PHASE8_INSTALL_LOG=$(smoke_mktemp_file) HOME="$FAKE_HOME" \ + CODEX_HOME="$FAKE_HOME/.codex" \ XDG_CONFIG_HOME="$FAKE_HOME/.config" \ APPDATA="$FAKE_HOME/AppData/Roaming" \ LOCALAPPDATA="$FAKE_HOME/AppData/Local" \ @@ -1699,12 +1701,116 @@ if ! grep -q 'existing_section' "$FAKE_HOME/.codex/config.toml"; then fi echo "OK 8f-h: Codex TOML (MCP + preserved existing)" -# 8i: Codex instructions -if [ ! -f "$FAKE_HOME/.codex/AGENTS.md" ] || ! grep -q 'codebase-memory-mcp' "$FAKE_HOME/.codex/AGENTS.md"; then - echo "FAIL 8i: Codex AGENTS.md missing" - exit 1 -fi -echo "OK 8i: Codex instructions" +# 8i: Fresh Codex installs do not create global instructions. Exercise the +# complete legacy lifecycle in a second HOME so Codex-only reinstalls cannot +# perturb the all-agent fixture that the remaining Phase 8 checks inspect. +if [ -e "$FAKE_HOME/.codex/AGENTS.md" ]; then + echo "FAIL 8i: fresh Codex install created global AGENTS.md" + exit 1 +fi + +CODEX_LIFECYCLE_HOME=$(smoke_mktemp_dir) +CODEX_LIFECYCLE_ROOT="$CODEX_LIFECYCLE_HOME/.codex" +CODEX_INSTRUCTIONS="$CODEX_LIFECYCLE_ROOT/AGENTS.md" +mkdir -p "$CODEX_LIFECYCLE_ROOT" + +CODEX_FRESH_LOG=$(smoke_mktemp_file) +HOME="$CODEX_LIFECYCLE_HOME" \ + CODEX_HOME="$CODEX_LIFECYCLE_ROOT" \ + XDG_CONFIG_HOME="$CODEX_LIFECYCLE_HOME/.config" \ + APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ + LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ + "$BINARY" install --skip-binary --clients=codex -y > "$CODEX_FRESH_LOG" 2>&1 +if [ -e "$CODEX_INSTRUCTIONS" ] || + ! grep -q '\[mcp_servers.codebase-memory-mcp\]' "$CODEX_LIFECYCLE_ROOT/config.toml" || + [ ! -s "$CODEX_LIFECYCLE_ROOT/skills/codebase-memory/SKILL.md" ] || + [ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory-scout.toml" ] || + [ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory.toml" ] || + [ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory-auditor.toml" ] || + ! grep -q 'SessionStart' "$CODEX_LIFECYCLE_ROOT/config.toml" || + ! grep -q 'SubagentStart' "$CODEX_LIFECYCLE_ROOT/config.toml"; then + echo "FAIL 8i: isolated fresh Codex install changed instructions or lost another surface" + exit 1 +fi + +printf '%s\n' \ + '# Personal Codex guidance' \ + '' \ + 'legacy managed guidance' \ + '' \ + '# Keep this line' > "$CODEX_INSTRUCTIONS" +CODEX_EXPECTED=$(smoke_mktemp_file) +printf '%s\n' '# Personal Codex guidance' '# Keep this line' > "$CODEX_EXPECTED" +CODEX_LEGACY_SHA=$(smoke_file_sha256 "$CODEX_INSTRUCTIONS") + +CODEX_PLAN=$(smoke_mktemp_file) +HOME="$CODEX_LIFECYCLE_HOME" \ + CODEX_HOME="$CODEX_LIFECYCLE_ROOT" \ + XDG_CONFIG_HOME="$CODEX_LIFECYCLE_HOME/.config" \ + APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ + LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ + "$BINARY" install --plan --skip-binary --clients=codex > "$CODEX_PLAN" +CODEX_CLEANUP_PATH=$(json_get "$CODEX_PLAN" \ + "next((x.get('path','') for x in d.get('cleanup_actions_planned', []) if x.get('agent') == 'Codex CLI' and x.get('kind') == 'instructions' and x.get('operation') == 'remove_managed_block_if_present'), '')") +CODEX_PLANS_INSTRUCTIONS=$(json_get "$CODEX_PLAN" \ + "any(str(x).replace('\\\\','/').endswith('/.codex/AGENTS.md') for x in d.get('instruction_files_planned', []))") +if ! exact_path_match "$CODEX_CLEANUP_PATH" "$CODEX_INSTRUCTIONS" || + [ "$CODEX_PLANS_INSTRUCTIONS" != "False" ] || + [ "$CODEX_LEGACY_SHA" != "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" ]; then + echo "FAIL 8i: Codex plan receipt or no-mutation contract is wrong" + exit 1 +fi + +CODEX_DRY_LOG=$(smoke_mktemp_file) +HOME="$CODEX_LIFECYCLE_HOME" \ + CODEX_HOME="$CODEX_LIFECYCLE_ROOT" \ + XDG_CONFIG_HOME="$CODEX_LIFECYCLE_HOME/.config" \ + APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ + LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ + "$BINARY" install --dry-run --skip-binary --clients=codex -y > "$CODEX_DRY_LOG" 2>&1 +if ! grep -q 'managed block if present' "$CODEX_DRY_LOG" || + [ "$CODEX_LEGACY_SHA" != "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" ]; then + echo "FAIL 8i: Codex dry-run did not preview byte-identical legacy cleanup" + exit 1 +fi + +CODEX_MIGRATE_LOG=$(smoke_mktemp_file) +HOME="$CODEX_LIFECYCLE_HOME" \ + CODEX_HOME="$CODEX_LIFECYCLE_ROOT" \ + XDG_CONFIG_HOME="$CODEX_LIFECYCLE_HOME/.config" \ + APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ + LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ + "$BINARY" install --skip-binary --clients=codex -y > "$CODEX_MIGRATE_LOG" 2>&1 +if [ ! -f "$CODEX_INSTRUCTIONS" ] || + [ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != "$(smoke_file_sha256 "$CODEX_EXPECTED")" ]; then + echo "FAIL 8i: Codex migration changed user-owned AGENTS.md bytes" + exit 1 +fi + +HOME="$CODEX_LIFECYCLE_HOME" \ + CODEX_HOME="$CODEX_LIFECYCLE_ROOT" \ + XDG_CONFIG_HOME="$CODEX_LIFECYCLE_HOME/.config" \ + APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ + LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ + "$BINARY" install --skip-binary --clients=codex -y > /dev/null 2>&1 +if [ ! -f "$CODEX_INSTRUCTIONS" ] || + [ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != "$(smoke_file_sha256 "$CODEX_EXPECTED")" ]; then + echo "FAIL 8i: Codex reinstall restored global managed instructions" + exit 1 +fi +CODEX_UNINSTALL_LOG=$(smoke_mktemp_file) +HOME="$CODEX_LIFECYCLE_HOME" \ + CODEX_HOME="$CODEX_LIFECYCLE_ROOT" \ + XDG_CONFIG_HOME="$CODEX_LIFECYCLE_HOME/.config" \ + APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ + LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ + "$BINARY" uninstall -y -n > "$CODEX_UNINSTALL_LOG" 2>&1 || true +if [ ! -f "$CODEX_INSTRUCTIONS" ] || + [ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != "$(smoke_file_sha256 "$CODEX_EXPECTED")" ]; then + echo "FAIL 8i: Codex uninstall changed user-owned AGENTS.md bytes" + exit 1 +fi +echo "OK 8i: isolated Codex fresh/plan/dry-run/migration/reinstall/uninstall lifecycle" # 8j-l: Gemini MCP + hooks + merge CMD=$(json_get "$FAKE_HOME/.gemini/settings.json" "d['mcpServers']['codebase-memory-mcp']['command']") @@ -2569,6 +2675,7 @@ if [[ "$BINARY" == *.exe ]]; then UNINSTALL_BINARY="$SELF_PATH" fi HOME="$FAKE_HOME" \ + CODEX_HOME="$FAKE_HOME/.codex" \ XDG_CONFIG_HOME="$FAKE_HOME/.config" \ APPDATA="$FAKE_HOME/AppData/Roaming" \ LOCALAPPDATA="$FAKE_HOME/AppData/Local" \ diff --git a/src/cli/cli.c b/src/cli/cli.c index d981af266..a72e64711 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -7433,7 +7433,8 @@ static void print_detected_agents(const cbm_detected_agents_t *a, const char *ho * behavior (it is the same code path with mutations disabled). */ typedef struct { char agent[CLI_BUF_32]; - char kind[CLI_BUF_32]; /* mcp_config | instructions | skills | hook */ + /* mcp_config | instructions | skills | hook | cleanup_instructions */ + char kind[CLI_BUF_32]; char path[CLI_BUF_1K]; } cbm_plan_entry_t; @@ -7771,6 +7772,20 @@ static bool install_generic_agent_config(const char *label, const char *binary_p return mcp_installed; } +static void cleanup_codex_legacy_instructions(const char *path, bool dry_run) { + if (!path || !cbm_file_exists(path)) { + return; + } + if (g_install_plan) { + plan_record("Codex CLI", "cleanup_instructions", path); + return; + } + printf(" instructions cleanup: %s (managed block if present)\n", path); + if (!dry_run && cbm_remove_instructions(path) != CLI_OK) { + record_agent_config_error(false, "Codex CLI", "legacy_instructions_cleanup", path); + } +} + static void install_windsurf_config(const char *binary_path, const char *config_path, const char *rules_path, bool dry_run) { if (g_install_plan) { @@ -8678,8 +8693,9 @@ static void install_cli_agent_configs(const cbm_detected_agents_t *agents, const reason); goto codex_install_done; } - install_generic_agent_config("Codex CLI", binary_path, cp, ip, dry_run, + install_generic_agent_config("Codex CLI", binary_path, cp, NULL, dry_run, cbm_upsert_codex_mcp); + cleanup_codex_legacy_instructions(ip, dry_run); install_agent_skill("Codex CLI", skills_dir, force, dry_run); install_tiered_agent_profiles( (cbm_tiered_profile_set_t){ @@ -9730,9 +9746,10 @@ static bool cli_binary_is_externally_managed(const char *self_path, bool self_pa } /* Build the agent.install.plan.v1 receipt (#388): a machine-readable list of - * the config / instruction / skill / agent / hook files `install` WOULD write, produced by - * running the real install dispatch in record-only mode (no mutation, no - * network). Returns a heap JSON string (caller frees) or NULL. */ + * the config / instruction / skill / agent / hook files `install` WOULD write + * plus conditional cleanup actions, produced by running the real install + * dispatch in record-only mode (no mutation, no network). Returns a heap JSON + * string (caller frees) or NULL. */ static char *cbm_build_install_plan_json_options(const char *home, const char *binary_path, bool skip_config) { if (!home || !binary_path) { @@ -9807,10 +9824,18 @@ static char *cbm_build_install_plan_json_options(const char *home, const char *b yyjson_mut_val *agent_files = yyjson_mut_arr(doc); yyjson_mut_val *prompt_files = yyjson_mut_arr(doc); yyjson_mut_val *hooks = yyjson_mut_arr(doc); + yyjson_mut_val *cleanups = yyjson_mut_arr(doc); for (int i = 0; i < plan.count; i++) { cbm_plan_entry_t *e = &plan.items[i]; if (strcmp(e->kind, "mcp_config") == 0) { yyjson_mut_arr_add_strcpy(doc, configs, e->path); + } else if (strcmp(e->kind, "cleanup_instructions") == 0) { + yyjson_mut_val *cleanup = yyjson_mut_obj(doc); + yyjson_mut_obj_add_strcpy(doc, cleanup, "agent", e->agent); + yyjson_mut_obj_add_str(doc, cleanup, "kind", "instructions"); + yyjson_mut_obj_add_str(doc, cleanup, "operation", "remove_managed_block_if_present"); + yyjson_mut_obj_add_strcpy(doc, cleanup, "path", e->path); + yyjson_mut_arr_add_val(cleanups, cleanup); } else if (strcmp(e->kind, "hook") == 0) { yyjson_mut_val *h = yyjson_mut_obj(doc); yyjson_mut_obj_add_strcpy(doc, h, "agent", e->agent); @@ -9835,6 +9860,7 @@ static char *cbm_build_install_plan_json_options(const char *home, const char *b yyjson_mut_obj_add_val(doc, root, "agent_files_planned", agent_files); yyjson_mut_obj_add_val(doc, root, "prompt_files_planned", prompt_files); yyjson_mut_obj_add_val(doc, root, "hooks_planned", hooks); + yyjson_mut_obj_add_val(doc, root, "cleanup_actions_planned", cleanups); yyjson_mut_obj_add_bool(doc, root, "writes_started", false); yyjson_mut_obj_add_bool(doc, root, "network_after_install", false); yyjson_mut_obj_add_str(doc, root, "next_safe_command", "codebase-memory-mcp install -y"); diff --git a/tests/test_cli.c b/tests/test_cli.c index 95fff9d76..6ee68f257 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -293,6 +293,32 @@ static bool test_plan_has_hook_for_agent(yyjson_val *root, const char *agent) { return false; } +static bool test_plan_cleanup_contains(yyjson_val *root, const char *agent, const char *kind, + const char *operation, const char *path) { + yyjson_val *items = root ? yyjson_obj_get(root, "cleanup_actions_planned") : NULL; + if (!items || !yyjson_is_arr(items)) { + return false; + } + size_t index; + size_t count; + yyjson_val *item; + yyjson_arr_foreach(items, index, count, item) { + yyjson_val *agent_value = yyjson_obj_get(item, "agent"); + yyjson_val *kind_value = yyjson_obj_get(item, "kind"); + yyjson_val *operation_value = yyjson_obj_get(item, "operation"); + yyjson_val *path_value = yyjson_obj_get(item, "path"); + if (agent_value && yyjson_is_str(agent_value) && kind_value && yyjson_is_str(kind_value) && + operation_value && yyjson_is_str(operation_value) && path_value && + yyjson_is_str(path_value) && strcmp(yyjson_get_str(agent_value), agent) == 0 && + strcmp(yyjson_get_str(kind_value), kind) == 0 && + strcmp(yyjson_get_str(operation_value), operation) == 0 && + strcmp(yyjson_get_str(path_value), path) == 0) { + return true; + } + } + return false; +} + static size_t test_count_substring(const char *text, const char *needle) { size_t count = 0U; size_t needle_len = strlen(needle); @@ -4454,6 +4480,12 @@ TEST(cli_install_plan_receipt_no_mutation_issue388) { ASSERT(strstr(json, "cursor") != NULL); ASSERT(strstr(json, ".cursor/mcp.json") != NULL); ASSERT(strstr(json, ".codex/config.toml") != NULL); + yyjson_doc *doc = yyjson_read(json, strlen(json), 0); + ASSERT_NOT_NULL(doc); + yyjson_val *cleanups = yyjson_obj_get(yyjson_doc_get_root(doc), "cleanup_actions_planned"); + ASSERT(cleanups && yyjson_is_arr(cleanups)); + ASSERT_EQ(yyjson_arr_size(cleanups), 0U); + yyjson_doc_free(doc); free(json); /* Critical: building the plan must NOT have created any config file. */ @@ -7330,23 +7362,149 @@ TEST(cli_codex_respects_codex_home) { char *saved = save_test_env("CODEX_HOME"); cbm_setenv("CODEX_HOME", codex_home, 1); + char expected_instructions[640]; + snprintf(expected_instructions, sizeof(expected_instructions), "%s/AGENTS.md", codex_home); + const char *user_instructions = "# Personal Codex guidance\n"; + ASSERT_EQ(write_test_file(expected_instructions, user_instructions), 0); + cbm_detected_agents_t agents = cbm_detect_agents(tmpdir); char *json = cbm_build_install_plan_json(tmpdir, "/usr/local/bin/codebase-memory-mcp"); char expected_config[640]; - char expected_instructions[640]; snprintf(expected_config, sizeof(expected_config), "%s/config.toml", codex_home); - snprintf(expected_instructions, sizeof(expected_instructions), "%s/AGENTS.md", codex_home); - bool plans_config = json && strstr(json, expected_config) != NULL; - bool plans_instructions = json && strstr(json, expected_instructions) != NULL; - + yyjson_doc *plan_doc = json ? yyjson_read(json, strlen(json), 0) : NULL; + yyjson_val *plan_root = plan_doc ? yyjson_doc_get_root(plan_doc) : NULL; + bool plans_config = test_json_string_array_contains(plan_root, "config_files_planned", + expected_config); + bool plans_instructions = test_json_string_array_contains( + plan_root, "instruction_files_planned", expected_instructions); + bool plans_cleanup = test_plan_cleanup_contains( + plan_root, "Codex CLI", "instructions", "remove_managed_block_if_present", + expected_instructions); + char *instructions_after = read_test_file_alloc(expected_instructions); + bool plan_preserved_user_file = + instructions_after && strcmp(instructions_after, user_instructions) == 0; + + free(instructions_after); + yyjson_doc_free(plan_doc); free(json); restore_test_env("CODEX_HOME", saved); test_rmdir_r(tmpdir); if (!agents.codex) FAIL("Codex detection must honor CODEX_HOME"); - if (!plans_config || !plans_instructions) - FAIL("Codex install plan must place config and AGENTS.md under CODEX_HOME"); + if (!plans_config || plans_instructions || !plans_cleanup || !plan_preserved_user_file) + FAIL("Codex plan must keep config under CODEX_HOME and report legacy AGENTS cleanup " + "without planning a new instruction file or mutating user content"); + PASS(); +} + +TEST(cli_codex_install_removes_only_legacy_global_instructions_issue1689) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-agents-cleanup-XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) + FAIL("cbm_mkdtemp failed"); + + char codex_home[512]; + char agents_path[640]; + char config_path[640]; + char skill_path[768]; + char profile_path[768]; + snprintf(codex_home, sizeof(codex_home), "%s/.codex", tmpdir); + snprintf(agents_path, sizeof(agents_path), "%s/AGENTS.md", codex_home); + snprintf(config_path, sizeof(config_path), "%s/config.toml", codex_home); + snprintf(skill_path, sizeof(skill_path), "%s/skills/codebase-memory/SKILL.md", codex_home); + snprintf(profile_path, sizeof(profile_path), "%s/agents/codebase-memory.toml", codex_home); + ASSERT_EQ(test_mkdirp(codex_home), 0); + + char *saved_home = save_test_env("HOME"); + char *saved_path = save_test_env("PATH"); + char *saved_codex = save_test_env("CODEX_HOME"); + cbm_setenv("HOME", tmpdir, 1); + cbm_setenv("PATH", tmpdir, 1); + cbm_setenv("CODEX_HOME", codex_home, 1); + + struct stat state; + int fresh_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + bool fresh_did_not_create_agents = stat(agents_path, &state) != 0; + char *config = read_test_file_alloc(config_path); + bool other_surfaces_installed = + fresh_rc == 0 && config && strstr(config, "[mcp_servers.codebase-memory-mcp]") && + strstr(config, "SessionStart") && stat(skill_path, &state) == 0 && + stat(profile_path, &state) == 0; + free(config); + + const char *user_only = "# Personal Codex guidance\nKeep this byte-for-byte.\n"; + ASSERT_EQ(write_test_file(agents_path, user_only), 0); + int dry_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, true); + char *after_dry = read_test_file_alloc(agents_path); + int unowned_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + char *after_unowned = read_test_file_alloc(agents_path); + bool unowned_preserved = dry_rc == 0 && unowned_rc == 0 && after_dry && after_unowned && + strcmp(after_dry, user_only) == 0 && + strcmp(after_unowned, user_only) == 0; + free(after_dry); + free(after_unowned); + + const char *legacy = "# Before\n\nlegacy\n" + "\n# After\n"; + const char *legacy_removed = "# Before\n# After\n"; + ASSERT_EQ(write_test_file(agents_path, legacy), 0); + int managed_dry_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, true); + char *after_managed_dry = read_test_file_alloc(agents_path); + int managed_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + char *after_managed = read_test_file_alloc(agents_path); + int repeat_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + char *after_repeat = read_test_file_alloc(agents_path); + bool managed_removed_once = managed_dry_rc == 0 && after_managed_dry && + strcmp(after_managed_dry, legacy) == 0 && managed_rc == 0 && + repeat_rc == 0 && after_managed && after_repeat && + strcmp(after_managed, legacy_removed) == 0 && + strcmp(after_repeat, legacy_removed) == 0; + free(after_managed_dry); + free(after_managed); + free(after_repeat); + + const char *managed_only = "\nlegacy\n" + "\n"; + ASSERT_EQ(write_test_file(agents_path, managed_only), 0); + int managed_only_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + char *after_managed_only = read_test_file_alloc(agents_path); + bool empty_file_preserved = managed_only_rc == 0 && after_managed_only && + after_managed_only[0] == '\0' && stat(agents_path, &state) == 0; + free(after_managed_only); + + const char *malformed = "# Keep\n\nunterminated\n"; + ASSERT_EQ(write_test_file(agents_path, malformed), 0); + ASSERT_EQ(remove(config_path), 0); + ASSERT_EQ(remove(skill_path), 0); + ASSERT_EQ(remove(profile_path), 0); + int malformed_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + char *after_malformed = read_test_file_alloc(agents_path); + config = read_test_file_alloc(config_path); + bool malformed_preserved = malformed_rc != 0 && after_malformed && + strcmp(after_malformed, malformed) == 0 && config && + strstr(config, "[mcp_servers.codebase-memory-mcp]") && + strstr(config, "SessionStart") && stat(skill_path, &state) == 0 && + stat(profile_path, &state) == 0; + free(config); + free(after_malformed); + + restore_test_env("HOME", saved_home); + restore_test_env("PATH", saved_path); + restore_test_env("CODEX_HOME", saved_codex); + test_rmdir_r(tmpdir); + + if (!fresh_did_not_create_agents || !other_surfaces_installed || !unowned_preserved || + !managed_removed_once || !empty_file_preserved || !malformed_preserved) + FAIL("Codex install must stop creating global AGENTS guidance, remove only its owned " + "legacy block, preserve malformed or user-owned bytes, and keep other surfaces"); PASS(); } @@ -13117,6 +13275,7 @@ SUITE(cli) { RUN_TEST(cli_openclaw_resolves_active_json5_workspace); RUN_TEST(cli_claude_user_scope_avoids_nested_mcp_json); RUN_TEST(cli_codex_respects_codex_home); + RUN_TEST(cli_codex_install_removes_only_legacy_global_instructions_issue1689); RUN_TEST(cli_gemini_session_hook_uses_json_for_all_sources); RUN_TEST(cli_gemini_installs_dedicated_graph_subagent); RUN_TEST(cli_antigravity_does_not_imply_gemini);