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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
124 changes: 104 additions & 20 deletions R/agent_context.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -44,17 +46,17 @@
#' @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
#' \donttest{
#' # 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)
Expand All @@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -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)
)
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -317,4 +402,3 @@ same_file <- function(a, b) {
identical(norm_a, norm_b)
}


4 changes: 2 additions & 2 deletions R/blast.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

19 changes: 7 additions & 12 deletions R/doc_scan.R
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}

11 changes: 3 additions & 8 deletions R/pkg.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
4 changes: 2 additions & 2 deletions R/projects.R
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand All @@ -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.
Expand Down
13 changes: 7 additions & 6 deletions cran-comments.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading