diff --git a/NEWS.md b/NEWS.md index 8bdf492..7e018bb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,11 @@ - Rebranded package as "Context Engineering for R". - `briefing()` now emits output via `message()` instead of `cat()` for CRAN compliance. - `agent_context()` examples use `\donttest{}` instead of `\dontrun{}`. +- `agent_context()` and the SessionStart hook now load memory reciprocally: + Codex receives Claude Code `MEMORY.md`, while Claude Code, Corteza, and + other non-Codex agents receive Codex memories. +- Codex hook setup docs now use `[features].hooks` instead of deprecated + `[features].codex_hooks`. - Added `Depends: R (>= 4.4.0)` and removed local `%||%` definition (now in base R). - Added copyright holder `person("cornball.ai", role = "cph")` to `Authors@R`. diff --git a/R/agent_context.R b/R/agent_context.R index e6a82f0..6edd3e6 100644 --- a/R/agent_context.R +++ b/R/agent_context.R @@ -10,12 +10,13 @@ #' #' Defaults per agent: #' \itemize{ -#' \item \code{"claude"} - skips project memory and CLAUDE.md files -#' (autoloaded by 'Claude Code'). Loads AGENTS.md, USER.md, and SOUL.md -#' when present. -#' \item \code{"codex"} - skips AGENTS.md (autoloaded by Codex). Loads -#' project memory, CLAUDE.md, USER.md, and SOUL.md when present. -#' \item \code{"llamar"} or \code{NULL} - loads everything available. +#' \item \code{"claude"} - skips Claude Code project memory and CLAUDE.md +#' files (autoloaded by 'Claude Code'). Loads Codex memories, AGENTS.md, +#' USER.md, and SOUL.md when present. +#' \item \code{"codex"} - skips AGENTS.md and Codex memories (autoloaded by +#' Codex). Loads Claude Code project memory, CLAUDE.md, USER.md, and +#' SOUL.md when present. +#' \item \code{"corteza"} or \code{NULL} - loads everything available. #' } #' #' Project and global instructions are resolved by trying both naming @@ -29,10 +30,11 @@ #' Override the defaults with the \code{include_*} parameters. #' #' @param agent Consumer identifier: \code{"claude"}, \code{"codex"}, -#' \code{"llamar"}, or \code{NULL} (interactive / unknown). +#' \code{"corteza"}, or \code{NULL} (interactive / unknown). The legacy +#' \code{"llamar"} identifier is accepted as an alias for \code{"corteza"}. #' @param project_dir Project directory to scan for CLAUDE.md / AGENTS.md. #' @param workspace_dir Optional directory containing SOUL.md and USER.md -#' (e.g. \code{~/.llamar/workspace}). If \code{NULL}, those files are +#' (e.g. \code{~/.corteza/workspace}). If \code{NULL}, those files are #' skipped. #' @param memory_base Base directory for 'Claude Code' project memory files. #' @param claude_global_path Path to the global 'Claude Code' instructions @@ -44,7 +46,7 @@ #' @param include_global Override default for global instructions #' (~/.claude/CLAUDE.md / USER.md). #' @param include_soul Override default for SOUL.md inclusion. -#' @param max_memory_lines Maximum lines to include from the memory file. +#' @param max_memory_lines Maximum lines to include from each memory source. #' @return Character string of assembled context, or empty string if no #' context applies. #' @examples @@ -52,9 +54,9 @@ #' # Codex agent in current project #' saber::agent_context(agent = "codex") #' -#' # llamaR with workspace files -#' saber::agent_context(agent = "llamar", -#' workspace_dir = "~/.llamar/workspace") +#' # Corteza with workspace files +#' saber::agent_context(agent = "corteza", +#' workspace_dir = "~/.corteza/workspace") #' #' # Force-include memory regardless of agent default #' saber::agent_context(agent = "claude", include_memory = TRUE) @@ -75,6 +77,7 @@ agent_context <- function(agent = NULL, project_dir = getwd(), defaults <- agent_context_defaults(agent_key) incl_mem <- include_memory %||% defaults$memory + incl_codex_mem <- include_memory %||% defaults$codex_memory incl_proj <- include_project %||% defaults$project incl_glob <- include_global %||% defaults$global incl_soul <- include_soul %||% defaults$soul @@ -88,6 +91,13 @@ agent_context <- function(agent = NULL, project_dir = getwd(), } } + if (isTRUE(incl_codex_mem)) { + codex_mem <- agent_context_codex_memory(max_memory_lines) + if (length(codex_mem) > 0L) { + parts <- c(parts, codex_mem, "") + } + } + if (isTRUE(incl_proj)) { proj <- agent_context_project(project_dir, agent_key, forced = !is.null(include_project)) @@ -119,15 +129,20 @@ agent_context <- function(agent = NULL, project_dir = getwd(), #' @noRd agent_context_defaults <- function(agent) { if (is.na(agent)) { - return(list(memory = TRUE, project = TRUE, global = TRUE, soul = TRUE)) + return(list(memory = TRUE, codex_memory = TRUE, project = TRUE, + global = TRUE, soul = TRUE)) } switch(agent, - claude = list(memory = FALSE, project = TRUE, + claude = list(memory = FALSE, codex_memory = TRUE, project = TRUE, global = TRUE, soul = TRUE), - codex = list(memory = TRUE, project = TRUE, global = TRUE, soul = TRUE), - llamar = list(memory = TRUE, project = TRUE, + codex = list(memory = TRUE, codex_memory = FALSE, project = TRUE, + global = TRUE, soul = TRUE), + corteza = list(memory = TRUE, codex_memory = TRUE, project = TRUE, + global = TRUE, soul = TRUE), + llamar = list(memory = TRUE, codex_memory = TRUE, project = TRUE, global = TRUE, soul = TRUE), - list(memory = TRUE, project = TRUE, global = TRUE, soul = TRUE) + list(memory = TRUE, codex_memory = TRUE, project = TRUE, + global = TRUE, soul = TRUE) ) } @@ -169,6 +184,75 @@ agent_context_memory <- function(project_dir, memory_base, max_lines) { lines } +#' Load Codex memories for non-Codex agents +#' @noRd +agent_context_codex_memory <- function(max_lines) { + mem_dir <- agent_context_codex_memory_dir() + if (!dir.exists(mem_dir)) { + return(character(0L)) + } + + files <- list.files(mem_dir, recursive = TRUE, full.names = TRUE) + if (length(files) == 0L) { + return(character(0L)) + } + + info <- file.info(files) + keep <- !is.na(info$isdir) & !info$isdir & !is.na(info$size) & + info$size > 0L + files <- sort(files[keep]) + if (length(files) == 0L) { + return(character(0L)) + } + + max_lines <- max(1L, as.integer(max_lines)[1L]) + remaining <- max_lines + out <- "## Codex Memories" + truncated <- FALSE + + for (i in seq_along(files)) { + if (remaining <= 0L) { + truncated <- TRUE + break + } + + content <- tryCatch(readLines(files[[i]], warn = FALSE), + error = function(e) character(0L)) + if (length(content) == 0L) { + next + } + + take <- min(length(content), remaining) + label <- substring(files[[i]], nchar(mem_dir) + 2L) + label <- gsub("\\\\", "/", label) + out <- c(out, "", sprintf("### %s", label), "", content[seq_len(take)]) + remaining <- remaining - take + + if (take < length(content)) { + truncated <- TRUE + break + } + } + + if (length(out) == 1L) { + return(character(0L)) + } + if (truncated) { + out <- c(out, sprintf("_... truncated after %d lines_", max_lines)) + } + out +} + +#' Resolve the Codex memory directory +#' @noRd +agent_context_codex_memory_dir <- function() { + codex_home <- Sys.getenv("CODEX_HOME", unset = "") + if (nchar(codex_home) == 0L) { + codex_home <- file.path(path.expand("~"), ".codex") + } + file.path(path.expand(codex_home), "memories") +} + #' Resolve and load project instructions (CLAUDE.md or AGENTS.md) #' #' Picks the file the consumer doesn't already autoload. Ties broken by @@ -205,7 +289,7 @@ agent_context_project <- function(project_dir, agent, forced = FALSE) { file_to_load <- claude_path } } else { - # llamar / unknown: prefer CLAUDE.md, fall back to AGENTS.md + # corteza / legacy aliases / unknown: prefer CLAUDE.md, fall back to AGENTS.md if (claude_exists) { file_to_load <- claude_path } else { @@ -258,7 +342,8 @@ agent_context_global <- function(workspace_dir, agent, claude_global, file_to_load <- user_path } } else { - # codex / llamar / unknown: prefer claude global, fall back to USER.md + # codex / corteza / legacy aliases / unknown: prefer claude global, + # fall back to USER.md if (claude_exists) { file_to_load <- claude_global } else { @@ -317,4 +402,3 @@ same_file <- function(a, b) { identical(norm_a, norm_b) } - diff --git a/R/blast.R b/R/blast.R index 76af36e..adb6e19 100644 --- a/R/blast.R +++ b/R/blast.R @@ -120,7 +120,7 @@ blast_radius <- function(fn, project = NULL, include = "r", #' @noRd empty_blast_results <- function() { data.frame(caller = character(), project = character(), - file = character(), line = integer(), - source = character(), stringsAsFactors = FALSE) + file = character(), line = integer(), source = character(), + stringsAsFactors = FALSE) } diff --git a/R/doc_scan.R b/R/doc_scan.R index 01854d4..10a5ed9 100644 --- a/R/doc_scan.R +++ b/R/doc_scan.R @@ -42,12 +42,9 @@ scan_examples <- function(project_dir, fn) { next } results <- rbind(results, - data.frame(caller = b$documented_fn, - project = project_name, - file = basename(fp), - line = b$line_nums[hits], - source = "example", - stringsAsFactors = FALSE)) + data.frame(caller = b$documented_fn, project = project_name, + file = basename(fp), line = b$line_nums[hits], + source = "example", stringsAsFactors = FALSE)) } } @@ -138,10 +135,8 @@ extract_example_blocks <- function(lines) { } documented_fn <- next_defined_fn(lines, i, n) - blocks[[length(blocks) + 1L]] <- list( - line_nums = line_nums, - documented_fn = documented_fn - ) + blocks[[length(blocks) + 1L]] <- list(line_nums = line_nums, + documented_fn = documented_fn) } else { i <- i + 1L } @@ -235,7 +230,7 @@ match_fn_lines <- function(lines, fn) { #' @noRd empty_doc_results <- function() { data.frame(caller = character(), project = character(), - file = character(), line = integer(), - source = character(), stringsAsFactors = FALSE) + file = character(), line = integer(), source = character(), + stringsAsFactors = FALSE) } diff --git a/R/pkg.R b/R/pkg.R index bd9e59d..e61f6d4 100644 --- a/R/pkg.R +++ b/R/pkg.R @@ -258,14 +258,9 @@ rd2hugo <- function(rd, topic, package) { body <- rd2md(rd) - front <- paste0( - "---\n", - "title: \"", topic, "\"\n", - "package: \"", package, "\"\n", - "description: >-\n", - " ", gsub("\n", " ", description), "\n", - "---\n" - ) + front <- paste0("---\n", "title: \"", topic, "\"\n", "package: \"", + package, "\"\n", "description: >-\n", " ", + gsub("\n", " ", description), "\n", "---\n") paste0(front, "\n", body) } diff --git a/R/projects.R b/R/projects.R index 2502b3e..5117a6c 100644 --- a/R/projects.R +++ b/R/projects.R @@ -33,8 +33,8 @@ projects <- function(scan_dir = path.expand("~"), exclude = default_exclude()) { dcf <- tryCatch( read.dcf(desc_file, - fields = c("Package", "Title", "Version", - "Depends", "Imports", "LinkingTo")), + fields = c("Package", "Title", "Version", "Depends", + "Imports", "LinkingTo")), error = function(e) NULL ) if (is.null(dcf) || nrow(dcf) == 0L) { diff --git a/README.md b/README.md index 4fe5d13..9780166 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,13 @@ Enable hooks in your Codex config (`~/.codex/config.toml`): ```toml [features] -codex_hooks = true +hooks = true +``` + +You can also enable the same feature from the CLI: + +```bash +codex --enable hooks ``` Then add the hook to `~/.codex/hooks.json`: @@ -192,6 +198,9 @@ Then add the hook to `~/.codex/hooks.json`: } ``` +Codex may require new or changed hooks to be reviewed before they run. Open +`/hooks` in Codex and approve the `session-start.R` command after adding it. + If you want neutral cross-agent preferences injected too, create `~/.config/agents/GLOBAL.md`. The hook appends it automatically after the project briefing. Set `AGENTS_GLOBAL_MD` if you want a different path. diff --git a/cran-comments.md b/cran-comments.md index 1721bd0..378431b 100644 --- a/cran-comments.md +++ b/cran-comments.md @@ -1,15 +1,16 @@ -## Resubmission +## Submission -This is a resubmission of saber 0.7.1. +This is saber 0.7.1, an update to the current CRAN version 0.3.0. -Changes since 0.2.0 (current CRAN version): +Changes since 0.3.0: - Rebranded as "Context Engineering for R" with updated title and description. -- Added `agent_context()` for assembling agent context from memory and instruction files. +- Added `agent_context()` for assembling agent context from memory and instruction files, with reciprocal cross-agent memory loading (Codex receives Claude `MEMORY.md`; Claude and other agents receive Codex memories). - Added `fn_graph()`, `pkg_graph()`, and `graph_svg()` for interactive SVG call graphs. - `blast_radius()` gains `include` parameter for scanning roxygen `@examples` and vignettes. -- `briefing()` now uses `message()` instead of `cat()` for CRAN compliance. -- Added `Depends: R (>= 4.4.0)` and removed local `%||%` operator definition. +- `briefing()` now emits output via `message()` instead of `cat()` for CRAN compliance. +- `agent_context()` examples use `\donttest{}` instead of `\dontrun{}`. +- Added `Depends: R (>= 4.4.0)` and removed local `%||%` operator definition (now in base R). - Added copyright holder `cornball.ai` to `Authors@R`. ## Test environments diff --git a/inst/scripts/session-start.R b/inst/scripts/session-start.R index 793b6cf..ef7b08a 100644 --- a/inst/scripts/session-start.R +++ b/inst/scripts/session-start.R @@ -1,5 +1,5 @@ #!/usr/bin/env Rscript -# saber - generate project briefing at session start +# saber - generate project context at session start # For use as a Claude Code or Codex SessionStart hook # # Usage: Rscript session-start.R [agent] @@ -55,7 +55,7 @@ resolve_repo_root <- function(path) { path.expand(root) } -load_briefing_fun <- function(repo_root = NULL) { +load_saber_fun <- function(name, repo_root = NULL) { if (!is.null(repo_root)) { local_fun <- tryCatch( { @@ -71,7 +71,7 @@ load_briefing_fun <- function(repo_root = NULL) { for (f in r_files) { sys.source(f, envir = env) } - get("briefing", envir = env, inherits = FALSE) + get(name, envir = env, inherits = FALSE) }, error = function(e) NULL ) @@ -81,24 +81,41 @@ load_briefing_fun <- function(repo_root = NULL) { } if (requireNamespace("saber", quietly = TRUE)) { - return(saber::briefing) + return(getExportedValue("saber", name)) } stop("saber not available") } +load_agent_memory <- function(agent, project_dir, repo_root = NULL) { + context_fun <- load_saber_fun("agent_context", repo_root) + context_fun(agent = agent, project_dir = project_dir, + include_project = FALSE, + include_global = FALSE, + include_soul = FALSE) +} + +append_context <- function(text, section) { + if (is.null(section) || nchar(trimws(section)) == 0L) { + return(text) + } + paste0(text, "\n\n", section) +} + repo_root <- resolve_repo_root(session_cwd) if (!is.null(repo_root)) { project <- basename(repo_root) scan_dir <- dirname(repo_root) + project_dir <- repo_root } else { project <- basename(session_cwd) scan_dir <- path.expand("~") + project_dir <- session_cwd } briefing_text <- tryCatch( { - briefing_fun <- load_briefing_fun(repo_root) + briefing_fun <- load_saber_fun("briefing", repo_root) msg_lines <- utils::capture.output( briefing_fun(project, scan_dir = scan_dir), type = "message" @@ -116,9 +133,15 @@ if (is.null(briefing_text) || nchar(briefing_text) == 0L) { "\n_No briefing available._\n") } +memory_text <- tryCatch( + load_agent_memory(agent, project_dir, repo_root), + error = function(e) NULL +) +briefing_text <- append_context(briefing_text, memory_text) + global_preferences <- load_global_preferences() if (!is.null(global_preferences)) { - briefing_text <- paste0(briefing_text, "\n\n", global_preferences) + briefing_text <- append_context(briefing_text, global_preferences) } escaped <- gsub("\\\\", "\\\\\\\\", briefing_text) diff --git a/inst/tinytest/test_agent_context.R b/inst/tinytest/test_agent_context.R index 59f03b2..57e8a23 100644 --- a/inst/tinytest/test_agent_context.R +++ b/inst/tinytest/test_agent_context.R @@ -15,15 +15,29 @@ mem_base <- file.path(root, "claude_mem") mem_proj_dir <- file.path(mem_base, "-home-user-demopkg", "memory") dir.create(mem_proj_dir, recursive = TRUE, showWarnings = FALSE) +codex_home <- file.path(root, "codex_home") +codex_mem_dir <- file.path(codex_home, "memories") +dir.create(codex_mem_dir, recursive = TRUE, showWarnings = FALSE) + # Fake claude global path (points to nonexistent file by default). # Use this in all tests to avoid leaking the real ~/.claude/CLAUDE.md. fake_claude_global <- file.path(root, "fake_home_claude.md") # Helper that injects the test fixtures -ac <- function(agent, ...) { +ac <- function(agent, workspace = workspace_dir, ...) { + old_codex_home <- Sys.getenv("CODEX_HOME", unset = NA_character_) + Sys.setenv(CODEX_HOME = codex_home) + on.exit({ + if (is.na(old_codex_home)) { + Sys.unsetenv("CODEX_HOME") + } else { + Sys.setenv(CODEX_HOME = old_codex_home) + } + }, add = TRUE) + saber::agent_context(agent = agent, project_dir = project_dir, - workspace_dir = workspace_dir, + workspace_dir = workspace, memory_base = mem_base, claude_global_path = fake_claude_global, ...) @@ -34,7 +48,7 @@ write_lines_to <- function(path, lines) { } # --- Empty case: no files at all --- -expect_equal(ac("llamar"), "") +expect_equal(ac("corteza"), "") # --- AGENTS.md only, codex agent (codex autoloads it -> skipped) --- write_lines_to(file.path(project_dir, "AGENTS.md"), "Project rules.") @@ -58,31 +72,57 @@ result <- ac("codex") expect_true(grepl("CLAUDE.md", result)) expect_true(grepl("Claude project rules", result)) -# --- Both files exist, llamar prefers CLAUDE.md --- +# --- Both files exist, corteza prefers CLAUDE.md --- write_lines_to(file.path(project_dir, "AGENTS.md"), "Agents version.") -result <- ac("llamar") +result <- ac("corteza") expect_true(grepl("Claude project rules", result)) expect_false(grepl("Agents version", result)) # --- Memory loading --- write_lines_to(file.path(mem_proj_dir, "MEMORY.md"), c("- [Test](t.md) - a test memory")) -result <- ac("llamar") +write_lines_to(file.path(codex_mem_dir, "reciprocal.md"), + "saber is meant to be reciprocal") +result <- ac("corteza") expect_true(grepl("## Memory", result)) expect_true(grepl("test memory", result)) +expect_true(grepl("## Codex Memories", result)) +expect_true(grepl("saber is meant to be reciprocal", result)) -# --- Memory skipped for claude by default --- +# --- Claude memory skipped for claude by default, Codex memory is included --- result <- ac("claude") expect_false(grepl("## Memory", result)) +expect_true(grepl("saber is meant to be reciprocal", result)) -# --- Memory force-included for claude --- +# --- Codex memory skipped for codex by default, Claude memory is included --- +result <- ac("codex") +expect_true(grepl("test memory", result)) +expect_false(grepl("saber is meant to be reciprocal", result)) + +# --- Corteza receives both memory systems --- +result <- ac("corteza") +expect_true(grepl("test memory", result)) +expect_true(grepl("saber is meant to be reciprocal", result)) + +# --- Memory force-included for claude includes both memory systems --- result <- ac("claude", include_memory = TRUE) expect_true(grepl("## Memory", result)) +expect_true(grepl("saber is meant to be reciprocal", result)) + +# --- Memory can be disabled for all memory systems --- +result <- ac("corteza", include_memory = FALSE) +expect_false(grepl("test memory", result)) +expect_false(grepl("saber is meant to be reciprocal", result)) + +# --- Legacy llamar identifier remains a corteza alias --- +result <- ac("llamar") +expect_true(grepl("test memory", result)) +expect_true(grepl("saber is meant to be reciprocal", result)) # --- SOUL.md and USER.md from workspace --- write_lines_to(file.path(workspace_dir, "SOUL.md"), "I am the soul.") write_lines_to(file.path(workspace_dir, "USER.md"), "User preferences here.") -result <- ac("llamar") +result <- ac("corteza") expect_true(grepl("SOUL.md", result)) expect_true(grepl("I am the soul", result)) @@ -90,9 +130,9 @@ expect_true(grepl("I am the soul", result)) result <- ac("claude") expect_true(grepl("User preferences here", result)) -# --- claude_global_path file is loaded for codex/llamar --- +# --- claude_global_path file is loaded for codex/corteza --- write_lines_to(fake_claude_global, "Fake claude global content.") -result <- ac("llamar") +result <- ac("corteza") expect_true(grepl("Fake claude global content", result)) # When claude global exists, USER.md is the fallback (not loaded) expect_false(grepl("User preferences here", result)) @@ -107,13 +147,11 @@ expect_true(grepl("User preferences here", result)) file.remove(fake_claude_global) # --- include_soul = FALSE skips SOUL.md --- -result <- ac("llamar", include_soul = FALSE) +result <- ac("corteza", include_soul = FALSE) expect_false(grepl("I am the soul", result)) # --- workspace_dir = NULL skips SOUL.md and USER.md --- -result <- saber::agent_context(agent = "llamar", project_dir = project_dir, - workspace_dir = NULL, memory_base = mem_base, - claude_global_path = fake_claude_global) +result <- ac("corteza", workspace = NULL) expect_false(grepl("SOUL", result)) expect_false(grepl("USER", result)) @@ -124,7 +162,7 @@ expect_true(grepl("Claude project rules", result)) expect_true(grepl("I am the soul", result)) # --- include_project = FALSE skips even when files exist --- -result <- ac("llamar", include_project = FALSE) +result <- ac("corteza", include_project = FALSE) expect_false(grepl("Claude project rules", result)) # --- Symlink dedup: AGENTS.md symlink to CLAUDE.md, claude agent skips both --- diff --git a/inst/tinytest/test_session_start.R b/inst/tinytest/test_session_start.R index 450d81e..b970978 100644 --- a/inst/tinytest/test_session_start.R +++ b/inst/tinytest/test_session_start.R @@ -27,17 +27,31 @@ on.exit(setwd(old_wd), add = TRUE) setwd(sub_dir) home_dir <- file.path(scan_dir, "home") +codex_home <- file.path(scan_dir, "codex_home") dir.create(file.path(home_dir, ".config", "agents"), recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(codex_home, "memories"), recursive = TRUE, + showWarnings = FALSE) writeLines(c( "# Global Development Preferences", "", "- Use saber before guessing." ), file.path(home_dir, ".config", "agents", "GLOBAL.md")) +writeLines("saber is meant to be reciprocal", + file.path(codex_home, "memories", "reciprocal.md")) +memory_dir <- file.path(home_dir, ".claude", "projects", + "-home-test-hookpkg", "memory") +dir.create(memory_dir, recursive = TRUE, showWarnings = FALSE) +writeLines(c( + "- [Memory body](memory-body.md) - hookpkg memory index entry" +), file.path(memory_dir, "MEMORY.md")) +writeLines("This body file should not be preloaded.", + file.path(memory_dir, "memory-body.md")) output <- system2(file.path(R.home("bin"), "Rscript"), c(script, "claude"), stdout = TRUE, stderr = TRUE, - env = c(sprintf("HOME=%s", home_dir))) + env = c(sprintf("HOME=%s", home_dir), + sprintf("CODEX_HOME=%s", codex_home))) expect_true(length(output) > 0L) expect_true(identical(trimws(output[[1L]]), "{")) @@ -47,6 +61,32 @@ expect_true(any(grepl('"additionalContext": "# Briefing: hookpkg\\\\n', expect_true(any(grepl("## Global Preferences\\n\\n# Global Development Preferences", output, fixed = TRUE))) expect_true(any(grepl("Use saber before guessing.", output, fixed = TRUE))) +expect_false(any(grepl("hookpkg memory index entry", output, fixed = TRUE))) +expect_true(any(grepl("saber is meant to be reciprocal", output, + fixed = TRUE))) expect_false(any(grepl("^# Briefing: hookpkg$", output))) +codex_output <- system2(file.path(R.home("bin"), "Rscript"), c(script, "codex"), + stdout = TRUE, stderr = TRUE, + env = c(sprintf("HOME=%s", home_dir), + sprintf("CODEX_HOME=%s", codex_home))) + +expect_true(any(grepl("## Memory", codex_output, fixed = TRUE))) +expect_true(any(grepl("hookpkg memory index entry", codex_output, fixed = TRUE))) +expect_false(any(grepl("saber is meant to be reciprocal", codex_output, + fixed = TRUE))) +expect_false(any(grepl("This body file should not be preloaded.", + codex_output, fixed = TRUE))) + +corteza_output <- system2(file.path(R.home("bin"), "Rscript"), + c(script, "corteza"), + stdout = TRUE, stderr = TRUE, + env = c(sprintf("HOME=%s", home_dir), + sprintf("CODEX_HOME=%s", codex_home))) + +expect_true(any(grepl("hookpkg memory index entry", corteza_output, + fixed = TRUE))) +expect_true(any(grepl("saber is meant to be reciprocal", corteza_output, + fixed = TRUE))) + unlink(scan_dir, recursive = TRUE) diff --git a/man/agent_context.Rd b/man/agent_context.Rd index 270fda2..cf8432d 100644 --- a/man/agent_context.Rd +++ b/man/agent_context.Rd @@ -12,12 +12,13 @@ agent_context(agent = NULL, project_dir = getwd(), workspace_dir = NULL, } \arguments{ \item{agent}{Consumer identifier: \code{"claude"}, \code{"codex"}, -\code{"llamar"}, or \code{NULL} (interactive / unknown).} +\code{"corteza"}, or \code{NULL} (interactive / unknown). The legacy +\code{"llamar"} identifier is accepted as an alias for \code{"corteza"}.} \item{project_dir}{Project directory to scan for CLAUDE.md / AGENTS.md.} \item{workspace_dir}{Optional directory containing SOUL.md and USER.md -(e.g. \code{~/.llamar/workspace}). If \code{NULL}, those files are +(e.g. \code{~/.corteza/workspace}). If \code{NULL}, those files are skipped.} \item{memory_base}{Base directory for 'Claude Code' project memory files.} @@ -36,7 +37,7 @@ file. Defaults to \code{~/.claude/CLAUDE.md}.} \item{include_soul}{Override default for SOUL.md inclusion.} -\item{max_memory_lines}{Maximum lines to include from the memory file.} +\item{max_memory_lines}{Maximum lines to include from each memory source.} } \value{ Character string of assembled context, or empty string if no @@ -53,12 +54,13 @@ agent natively are skipped to avoid duplication. Defaults per agent: \itemize{ - \item \code{"claude"} - skips project memory and CLAUDE.md files - (autoloaded by 'Claude Code'). Loads AGENTS.md, USER.md, and SOUL.md - when present. - \item \code{"codex"} - skips AGENTS.md (autoloaded by Codex). Loads - project memory, CLAUDE.md, USER.md, and SOUL.md when present. - \item \code{"llamar"} or \code{NULL} - loads everything available. + \item \code{"claude"} - skips Claude Code project memory and CLAUDE.md + files (autoloaded by 'Claude Code'). Loads Codex memories, AGENTS.md, + USER.md, and SOUL.md when present. + \item \code{"codex"} - skips AGENTS.md and Codex memories (autoloaded by + Codex). Loads Claude Code project memory, CLAUDE.md, USER.md, and + SOUL.md when present. + \item \code{"corteza"} or \code{NULL} - loads everything available. } Project and global instructions are resolved by trying both naming @@ -76,9 +78,9 @@ Override the defaults with the \code{include_*} parameters. # Codex agent in current project saber::agent_context(agent = "codex") -# llamaR with workspace files -saber::agent_context(agent = "llamar", - workspace_dir = "~/.llamar/workspace") +# Corteza with workspace files +saber::agent_context(agent = "corteza", + workspace_dir = "~/.corteza/workspace") # Force-include memory regardless of agent default saber::agent_context(agent = "claude", include_memory = TRUE)