From 40c9b044b8725241198b0b5493876ad8be99fe4f Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Tue, 6 Oct 2026 10:24:09 -0400 Subject: [PATCH 1/7] feat: support user-level AGENTS.md fallback and scoped path_btw If no user-level btw.md exists, btw_client() and btw_app() now fall back to a user-level AGENTS.md, searched in ~/.btw/, ~/.config/btw/, tools::R_user_dir("btw"), and ~/.agents/, in that order; a user-level CLAUDE.md is never used. edit_btw_md("user") opens a user-level AGENTS.md when no user-level btw.md exists, and use_btw_md("user") warns before creating a btw.md that would take priority over an existing AGENTS.md. The path_btw argument of btw_client() and btw_app() now also accepts a named list with project and/or user fields, each TRUE (search the default locations), FALSE (skip that scope), or a path to a specific file; a missing field keeps the default for that scope. A scalar path now uses only that file (previously the user-level btw.md was also loaded alongside it); pass list(project = "path") to combine a specific project file with the default user-level context. The new btw.client.path_btw_user option sets the default for the user scope. Fixes #226 --- NEWS.md | 4 + R/btw-config.R | 13 ++ R/btw_chat_history_store.R | 8 +- R/btw_client.R | 141 ++++++++++++++++-- R/edit_btw_md.R | 103 ++++++++++--- R/utils.R | 33 ++++- man/btw-config.Rd | 13 ++ man/btw_client.Rd | 50 ++++++- man/use_btw_md.Rd | 34 +++-- tests/testthat/_snaps/btw_client.md | 51 +++++++ tests/testthat/_snaps/edit_btw_md.md | 40 ++++- tests/testthat/_snaps/utils.md | 12 ++ tests/testthat/test-btw_chat_history_store.R | 15 ++ tests/testthat/test-btw_client.R | 147 ++++++++++++++++++- tests/testthat/test-edit_btw_md.R | 103 +++++++++++++ tests/testthat/test-utils.R | 68 +++++++++ vignettes/btw-md.Rmd | 8 +- 17 files changed, 777 insertions(+), 66 deletions(-) diff --git a/NEWS.md b/NEWS.md index ae2261c7..11a9d7fe 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # btw (development version) +* If no user-level `btw.md` exists, `btw_client()` and `btw_app()` now fall back to a user-level `AGENTS.md`, searched in `~/.btw/`, `~/.config/btw/`, `tools::R_user_dir("btw")`, and `~/.agents/`, in that order. This mirrors the project-level precedence `btw.md` > `AGENTS.md`; a user-level `CLAUDE.md` is never used. `edit_btw_md("user")` opens a user-level `AGENTS.md` when no user-level `btw.md` exists, and `use_btw_md("user")` asks for confirmation before creating a `btw.md` that would take priority over an existing `AGENTS.md` (#226). + +* The `path_btw` argument of `btw_client()` and `btw_app()` now also accepts a named list with `project` and/or `user` fields, each `TRUE` (search the default locations), `FALSE` (skip that scope), or a path to a specific file; a missing field keeps the default for that scope, e.g. `path_btw = list(user = FALSE)` skips user-level context but keeps project context. A scalar path now uses only that file (previously the user-level `btw.md` was also loaded alongside it); pass `list(project = "path")` to combine a specific project file with the default user-level context. `NULL`/`TRUE` and `FALSE` keep their previous meaning. The new `btw.client.path_btw_user` option sets the default for the user scope, so `options(btw.client.path_btw_user = FALSE)` opts out of user-level context globally (#226). + * `path_find_in_project()` now stops its upward search when it reaches the home directory (either `fs::path_home()` or `fs::path_home_r()`, which differ on Windows) and never treats files there as project files, so `~/AGENTS.md` or `~/CLAUDE.md` are no longer picked up as project context when the working directory is under `~` but outside a project. Home-level files are handled only by the user-level lookup. This also fixes `~/btw.md` instructions being included twice — once as the project file and once as the user-level config (#225). * `btw_client(path_btw = ...)` no longer duplicates instructions when `path_btw` is set explicitly to the same file as the user-level `btw.md` (#225). diff --git a/R/btw-config.R b/R/btw-config.R index 911319d6..b8710655 100644 --- a/R/btw-config.R +++ b/R/btw-config.R @@ -35,6 +35,19 @@ #' user-level `btw.md` exists, the highest-priority one is used and btw warns #' once per session. #' +#' If no user-level `btw.md` exists anywhere, [btw_client()] and [btw_app()] +#' fall back to a user-level `AGENTS.md`, searched in each of the directories +#' above (`~/.btw/AGENTS.md`, `~/.config/btw/AGENTS.md`, +#' `tools::R_user_dir("btw")/AGENTS.md`) followed by `~/.agents/AGENTS.md`, a +#' cross-tool convention shared with other coding agents. This mirrors the +#' project-level precedence `btw.md` > `AGENTS.md` > `CLAUDE.md`, except a +#' user-level `CLAUDE.md` is never used. +#' +#' User-level context is combined with the project-level context file by +#' default; use `path_btw = list(user = FALSE)` in [btw_client()] or +#' [btw_app()] to skip it for a single chat, or set the +#' `btw.client.path_btw_user` option to `FALSE` to change the default. +#' #' On Windows, R's notion of your home directory (`fs::path_home_r()`, #' typically your `Documents` folder) can differ from your user profile #' directory (`fs::path_home()`); btw searches both. diff --git a/R/btw_chat_history_store.R b/R/btw_chat_history_store.R index bc6481e9..286c5569 100644 --- a/R/btw_chat_history_store.R +++ b/R/btw_chat_history_store.R @@ -221,8 +221,12 @@ btw_conversation_store_sqlite <- function() { } btw_app_history_project_dir <- function(path_btw = NULL) { - if (!is.null(path_btw) && !identical(path_btw, FALSE)) { - path <- fs::path_abs(fs::path_expand(path_btw)) + # Only an explicit project path scopes the chat history; TRUE/FALSE fall + # through to the marker-based detection below. + project <- normalize_path_btw(path_btw)$project + + if (!isTRUE(project) && !isFALSE(project)) { + path <- fs::path_abs(fs::path_expand(project)) if (fs::dir_exists(path)) { return(as.character(fs::path_norm(path))) } diff --git a/R/btw_client.R b/R/btw_client.R index b15568b3..83cc8f60 100644 --- a/R/btw_client.R +++ b/R/btw_client.R @@ -26,7 +26,10 @@ #' `AGENTS.md` file take precedence. If a project file doesn't specify a #' setting, btw will fall back to settings in a user-level `btw.md` file. This #' is `~/btw.md` if present, otherwise `~/.btw/btw.md`, `~/.config/btw/btw.md`, -#' or `tools::R_user_dir("btw")`, in that order. See `?btw-config` for the +#' or `tools::R_user_dir("btw")`, in that order. If no user-level `btw.md` +#' exists anywhere, btw falls back to a user-level `AGENTS.md` in `~/.btw/`, +#' `~/.config/btw/`, `tools::R_user_dir("btw")`, or `~/.agents/`, in that +#' order; a user-level `CLAUDE.md` is never used. See `?btw-config` for the #' complete list of user-level locations. Project-level btw tool options under #' the `options` key are merged with user-level options, with project-level #' options taking precedence. @@ -46,6 +49,12 @@ #' * `btw.tools`: The btw tools to include by default when starting a new #' btw chat, see [btw_tools()] for details.` #' +#' One additional option is consulted when resolving context files: +#' +#' * `btw.client.path_btw_user`: `TRUE` (default) to include user-level context +#' in new chats, `FALSE` to skip it, or a path to a specific user-level +#' context file. This is the default for the `user` field of `path_btw`. +#' #' ## Multiple Providers and Models #' #' You can configure multiple client options in your `btw.md` file. When @@ -103,13 +112,25 @@ #' alternatively in the shorter form `tools = "docs_help_page"`. Finally, #' set `tools = FALSE` to skip registering \pkg{btw} tools with the chat #' client. -#' @param path_btw A path to a `btw.md`, `AGENTS.md`, or `CLAUDE.md` project -#' context file. If `NULL`, btw will find a project-specific context file by -#' walking up the parents of the current working directory, preferring -#' `btw.md`, then `AGENTS.md`, then `CLAUDE.md`, with fallback to a -#' user-level `btw.md` file if no project file is found. See `?btw-config` -#' for the complete list of locations. Set `path_btw = FALSE` to create a -#' chat client without using a `btw.md` file. +#' @param path_btw Path to a `btw.md`, `AGENTS.md`, or `CLAUDE.md` project +#' context file, or a named list with `project` and/or `user` fields to +#' control project-level and user-level context independently. Each field +#' can be `TRUE` (search the default locations), `FALSE` (skip that scope), +#' or a path to a specific file; a missing (or `NULL`) field keeps the +#' default for that scope: `project` searches the project locations, and +#' `user` follows the `btw.client.path_btw_user` option described below. +#' If `NULL`, btw will find a +#' project-specific context file by walking up the parents of the current +#' working directory, preferring `btw.md`, then `AGENTS.md`, then +#' `CLAUDE.md`, and always combines it with the user-level context file: +#' the highest-priority user-level `btw.md`, or if none exists, a +#' user-level `AGENTS.md` (a user-level `CLAUDE.md` is never used). The +#' default for the `user` scope can be changed globally with the +#' `btw.client.path_btw_user` option, e.g. `options(btw.client.path_btw_user +#' = FALSE)`. A scalar path uses only that file, skipping user-level context +#' (equivalent to `list(project = path, user = FALSE)`), and a scalar `FALSE` +#' skips all context files. See `?btw-config` for the complete list of +#' locations. #' @param path_llms_txt A path to an `llms.txt` file containing context about #' the current project. By default, btw will look for an `llms.txt` file in #' the your current working directory or its parents. Set `path_llms_txt = @@ -558,6 +579,76 @@ maybe_find_in_project <- function(path, file_name, arg = "path") { path } +# Default for the `user` scope of `path_btw` when it isn't specified: an R +# option so users can opt out of (or pin) user-level context globally, e.g. +# `options(btw.client.path_btw_user = FALSE)` in ~/.Rprofile. +btw_user_path_default <- function() { + user <- getOption("btw.client.path_btw_user", TRUE) + if (isTRUE(user) || isFALSE(user)) { + return(user) + } + check_string(user, arg = "btw.client.path_btw_user") + user +} + +# Normalize the `path_btw` argument of btw_client()/btw_app() into a list with +# `project` and `user` fields, each TRUE (search the default locations), FALSE +# (skip that scope), or a path to a specific file. +# +# Scalar values: NULL/TRUE search project and user locations (with the user +# default from `btw.client.path_btw_user`), FALSE skips context entirely, and +# a string uses only that file as the project context, skipping user-level +# context -- a shortcut for list(project = path, user = FALSE). +# +# A named list controls each scope independently: a missing (or NULL) field +# keeps the default for that scope -- `project` searches project locations, +# and `user` uses the `btw.client.path_btw_user` option default. +normalize_path_btw <- function(path_btw, arg = "path_btw") { + if (is.list(path_btw)) { + nms <- names(path_btw) + if ( + length(path_btw) == 0 || + is.null(nms) || + any(!nzchar(nms)) || + !all(nms %in% c("project", "user")) + ) { + cli::cli_abort(c( + "{.arg {arg}} must be a named list with {.code project} and/or {.code user} fields.", + "i" = "Each field can be {.code TRUE} (search the default locations), {.code FALSE} (skip), or a path to a file." + )) + } + return(list( + project = normalize_path_btw_field(path_btw$project, TRUE, paste0(arg, "$project")), + user = normalize_path_btw_field(path_btw$user, btw_user_path_default(), paste0(arg, "$user")) + )) + } + + if (is.null(path_btw) || isTRUE(path_btw)) { + return(list( + project = TRUE, + user = btw_user_path_default() + )) + } + + if (isFALSE(path_btw)) { + return(list(project = FALSE, user = FALSE)) + } + + check_string(path_btw, arg = arg) + list(project = path_btw, user = FALSE) +} + +normalize_path_btw_field <- function(value, default, arg) { + if (is.null(value)) { + default + } else if (isTRUE(value) || isFALSE(value)) { + value + } else { + check_string(value, arg = arg) + value + } +} + find_btw_context_file <- function(path = NULL, search_user = TRUE) { # 1. Local closest btw.md file path <- maybe_find_in_project(path, "btw.md", "path_btw") @@ -572,9 +663,9 @@ find_btw_context_file <- function(path = NULL, search_user = TRUE) { path <- maybe_find_in_project(NULL, "CLAUDE.md", "path_btw") } - # 4. User btw.md file + # 4. User-level btw.md file, falling back to a user-level AGENTS.md if (search_user && is.null(path)) { - path <- path_find_user("btw.md") + path <- path_find_user_context() } path @@ -602,12 +693,34 @@ read_single_btw_file <- function(path) { } read_btw_file <- function(path = NULL) { - if (isFALSE(path)) { - return(list()) + paths <- normalize_path_btw(path) + + # Locate the project-level context file: search, skip, or an explicit path + project_path <- if (isTRUE(paths$project)) { + find_btw_context_file(NULL, search_user = FALSE) + } else if (isFALSE(paths$project)) { + NULL + } else { + maybe_find_in_project(paths$project, "btw.md", "path_btw") } - project_path <- find_btw_context_file(path, search_user = FALSE) - user_path <- path_find_user("btw.md") + # Locate the user-level context file: the highest-priority user-level + # btw.md, or if none exists anywhere, the highest-priority user-level + # AGENTS.md (a user-level CLAUDE.md is never used) + user_path <- if (isTRUE(paths$user)) { + path_find_user_context() + } else if (isFALSE(paths$user)) { + NULL + } else { + user_path <- fs::path_norm(fs::path_expand(paths$user)) + if (!fs::file_exists(user_path)) { + cli::cli_abort(c( + "x" = "Invalid {.arg path_btw$user}: {.path {user_path}} does not exist.", + "i" = "Point {.code path_btw$user} at an existing file, or set {.code user = FALSE} to skip user-level context." + )) + } + user_path + } # If the project path was set explicitly to the same file as the user-level # config, don't read it twice. diff --git a/R/edit_btw_md.R b/R/edit_btw_md.R index f0c27e4f..a4b19066 100644 --- a/R/edit_btw_md.R +++ b/R/edit_btw_md.R @@ -172,11 +172,18 @@ #' directly in your home directory (`~/btw.md`) takes precedence; otherwise btw #' looks in `~/.btw/btw.md`, `~/.config/btw/btw.md`, and #' `tools::R_user_dir("btw")`, in that order (see [btw-config] for the full -#' picture, including skills and agents). `use_btw_md("user")` creates new -#' configuration in the recommended `~/.btw/` directory, and offers to migrate -#' an existing user-level configuration found elsewhere. When more than one -#' user-level `btw.md` file exists, `edit_btw_md("user")` asks which one to open. -#' Note that \pkg{btw} does not look for `AGENTS.md` in your home directory. +#' picture, including skills and agents). If no user-level `btw.md` exists +#' anywhere, btw falls back to a user-level `AGENTS.md`, searched in +#' `~/.btw/`, `~/.config/btw/`, `tools::R_user_dir("btw")`, and the +#' cross-tool `~/.agents/` directory, in that order. A user-level `CLAUDE.md` +#' is never used. `use_btw_md("user")` creates new configuration in the +#' recommended `~/.btw/` directory, offers to migrate an existing user-level +#' configuration found elsewhere, and asks for confirmation when creating a +#' `btw.md` would take priority over an existing user-level `AGENTS.md`. When +#' more than one user-level context file exists, `edit_btw_md("user")` asks +#' which one to open. To skip user-level context in a specific chat, use +#' `path_btw = list(user = FALSE)` in [btw_client()] or [btw_app()], or change +#' the default globally with the `btw.client.path_btw_user` option. #' #' The user-level `btw.md` file is used alongside your project's context file, #' not only when a project file is missing. When both are present, @@ -212,18 +219,21 @@ #' @param scope The scope of the context file. Can be: #' - `"project"` (default): Creates/opens `btw.md` (by default) or `AGENTS.md` #' in the project root -#' - `"user"`: Opens your user-level `btw.md`. When multiple user-level -#' configuration files exist, `edit_btw_md("user")` asks which one to open, -#' indicating the file that [btw_client()] and [btw_app()] load. When -#' creating, `use_btw_md("user")` uses the recommended `~/.btw/` location and -#' offers to migrate an existing user-level configuration found elsewhere. +#' - `"user"`: Opens your user-level `btw.md`, or if only a user-level +#' `AGENTS.md` exists, that file. When multiple user-level context files +#' exist, `edit_btw_md("user")` asks which one to open, indicating the file +#' that [btw_client()] and [btw_app()] load. When creating, +#' `use_btw_md("user")` uses the recommended `~/.btw/` location, offers to +#' migrate an existing user-level configuration found elsewhere, and asks +#' for confirmation when creating a `btw.md` would take priority over an +#' existing user-level `AGENTS.md`. #' - A directory path: Creates/opens `btw.md` in that directory #' - A file path: Creates/opens that specific file #' #' For `edit_btw_md()`, `scope = NULL` (default) will find and open the #' context file that [btw_client()] would use, searching first for `btw.md`, -#' then `AGENTS.md`, then `CLAUDE.md` in the project directory and then for a -#' user-level `btw.md`. +#' then `AGENTS.md`, then `CLAUDE.md` in the project directory and then for +#' a user-level `btw.md` (or, if none exists, a user-level `AGENTS.md`). #' #' @return `use_btw_md()` returns the path to the context file, invisibly. #' `edit_btw_md()` is called for its side effect of opening the file. @@ -315,12 +325,33 @@ existing_user_btw_md <- function() { fs::path(paths[fs::file_exists(paths)]) } +# Existing user-level AGENTS.md files, in decreasing priority order (empty if +# none). These are fallbacks for the user-level btw.md: loaded by +# path_find_user_context() only when no user-level btw.md exists. +existing_user_agents_md <- function() { + paths <- user_agents_md_paths() + fs::path(paths[fs::file_exists(paths)]) +} + use_btw_md_user <- function() { canonical <- fs::path(btw_user_dir_preferred(), "btw.md") existing <- existing_user_btw_md() - # No user-level config anywhere: create it in the recommended ~/.btw/ location. + # No user-level btw.md anywhere: check for a user-level AGENTS.md, which a + # new ~/.btw/btw.md would take priority over. if (length(existing) == 0) { + existing_agents <- existing_user_agents_md() + if (length(existing_agents) > 0) { + active_agents <- existing_agents[[1]] + if (!confirm_btw_md_over_agents_md(active_agents)) { + cli::cli_inform(c( + "i" = "Keeping {.path {path_home_display(active_agents)}}." + )) + return(invisible(active_agents)) + } + } + + # Create new configuration in the recommended ~/.btw/ location. fs::dir_create(fs::path_dir(canonical)) fs::file_copy(btw_md_template(canonical), canonical) cli::cli_inform(c("v" = "Created {.file {path_display(canonical)}}")) @@ -349,6 +380,35 @@ use_btw_md_user <- function() { invisible(path) } +# Called by use_btw_md("user") when a user-level AGENTS.md exists but no +# user-level btw.md does: creating a ~/.btw/btw.md would take priority over +# the AGENTS.md, so warn (or confirm interactively) and point at +# edit_btw_md("user") to edit the AGENTS.md instead. Returns TRUE if the +# btw.md should be created. +confirm_btw_md_over_agents_md <- function(agents_md) { + cli::cli_inform(c( + "!" = "Found a user-level {.file AGENTS.md} at {.path {path_home_display(agents_md)}}.", + "i" = "A new {.file btw.md} in {.path {path_home_display(btw_user_dir_preferred())}} would take priority over it.", + "i" = "Call {.run btw::edit_btw_md(\"user\")} to edit the existing {.file AGENTS.md} instead." + )) + + if (!is_interactive()) { + return(TRUE) + } + + choice <- utils::menu( + c("Create user-level btw.md", "Cancel"), + graphics = FALSE, + title = paste( + "Create a btw.md that takes priority over", + path_home_display(agents_md), + "?" + ) + ) + + identical(choice, 1L) +} + # Offer to move an existing user config to the recommended ~/.btw/ location. # Returns the path in effect afterward (canonical if migrated, otherwise the # original `source`). @@ -386,11 +446,16 @@ maybe_migrate_user_btw_md <- function(source, canonical) { canonical } -# Choose which user-level btw.md edit_btw_md("user") opens. When several exist, -# prompt interactively, making clear which one btw_client()/btw_app() actually -# read and nudging toward consolidating in ~/.btw/. +# Choose which user-level context file edit_btw_md("user") opens: the +# user-level btw.md files, or if none exist, the user-level AGENTS.md files, +# in decreasing priority order. When several exist, prompt interactively, +# making clear which one btw_client()/btw_app() actually read and nudging +# toward consolidating in ~/.btw/. resolve_user_btw_md_for_edit <- function() { - existing <- existing_user_btw_md() + existing <- fs::path(c( + as.character(existing_user_btw_md()), + as.character(existing_user_agents_md()) + )) recommended <- fs::path(btw_user_dir_preferred(), "btw.md") # Nothing exists yet: point at the recommended location so edit_btw_md() @@ -409,7 +474,7 @@ resolve_user_btw_md_for_edit <- function() { labels[[1]] <- paste0(labels[[1]], " (read by btw_client()/btw_app())") cli::cli_inform(c( - "!" = "You have more than one user-level {.file btw.md} config file.", + "!" = "You have more than one user-level context file.", "i" = "{.run btw::btw_client()} and {.run btw::btw_app()} read the highest-priority one, marked below.", "i" = "Consider consolidating your config into {.path {path_home_display(recommended)}}." )) @@ -417,7 +482,7 @@ resolve_user_btw_md_for_edit <- function() { choice <- utils::menu( labels, graphics = FALSE, - title = "Which user-level btw.md do you want to edit?" + title = "Which user-level context file do you want to edit?" ) # Selection cancelled (0): default to the file btw actually reads. diff --git a/R/utils.R b/R/utils.R index 2e01bf68..92314ca3 100644 --- a/R/utils.R +++ b/R/utils.R @@ -224,13 +224,13 @@ user_config_paths <- function(filename) { ))) } -path_find_user <- function(filename) { +path_find_user <- function(filename, paths = user_config_paths(filename)) { if (identical(Sys.getenv("TESTTHAT"), "true")) { # In testthat, we don't want to use the home directory return(NULL) } - possibilities <- user_config_paths(filename) + possibilities <- paths existing <- possibilities[fs::file_exists(possibilities)] if (length(existing) == 0) { @@ -244,14 +244,37 @@ path_find_user <- function(filename) { fs::path_norm(existing[[1]]) } +# Ordered user-level candidate paths for AGENTS.md, in decreasing priority. +# Mirrors btw.md's directory locations (via btw_user_dirs()) and adds the +# cross-tool ~/.agents/ convention shared with other coding agents. Unlike +# btw.md, a loose AGENTS.md in the home root is not searched: no tool reads +# one there, and btw's project search no longer walks through the home +# directory, so a loose ~/AGENTS.md is never picked up by accident either. +user_agents_md_paths <- function() { + homes <- unique(c(fs::path_home(), fs::path_home_r())) + unique(as.character(c( + file.path(btw_user_dirs(), "AGENTS.md"), + fs::path(homes, ".agents", "AGENTS.md") + ))) +} + +# The active user-level context file, mirroring the project-level precedence +# btw.md > AGENTS.md > CLAUDE.md, except CLAUDE.md is never considered at user +# level: the highest-priority existing user-level btw.md, or if none exists +# anywhere, the highest-priority existing user-level AGENTS.md. +path_find_user_context <- function() { + path_find_user("btw.md") %||% + path_find_user("AGENTS.md", paths = user_agents_md_paths()) +} + # Warn once per session when more than one user-level config file exists, so a # user editing a now-shadowed file has a hint about which one btw reads. -warn_multiple_user_config <- function(paths) { +warn_multiple_user_config <- function(paths, filename = basename(paths[[1]])) { shown <- path_home_display(paths) - target <- path_home_display(fs::path(btw_user_dir_preferred(), "btw.md")) + target <- path_home_display(fs::path(btw_user_dir_preferred(), filename)) cli::cli_warn( c( - "!" = "Found more than one user-level {.file btw.md} config file.", + "!" = "Found more than one user-level {.file {filename}} config file.", "i" = "Using {.path {shown[[1]]}}.", "i" = "Ignoring lower-priority: {.path {shown[-1]}}.", "i" = "Consider consolidating your btw configuration into {.path {target}}." diff --git a/man/btw-config.Rd b/man/btw-config.Rd index 14c3895b..317ec529 100644 --- a/man/btw-config.Rd +++ b/man/btw-config.Rd @@ -40,6 +40,19 @@ configuration in the recommended \verb{~/.btw/} directory. If more than one user-level \code{btw.md} exists, the highest-priority one is used and btw warns once per session. +If no user-level \code{btw.md} exists anywhere, \code{\link[=btw_client]{btw_client()}} and \code{\link[=btw_app]{btw_app()}} +fall back to a user-level \code{AGENTS.md}, searched in each of the directories +above (\verb{~/.btw/AGENTS.md}, \verb{~/.config/btw/AGENTS.md}, +\code{tools::R_user_dir("btw")/AGENTS.md}) followed by \verb{~/.agents/AGENTS.md}, a +cross-tool convention shared with other coding agents. This mirrors the +project-level precedence \code{btw.md} > \code{AGENTS.md} > \code{CLAUDE.md}, except a +user-level \code{CLAUDE.md} is never used. + +User-level context is combined with the project-level context file by +default; use \code{path_btw = list(user = FALSE)} in \code{\link[=btw_client]{btw_client()}} or +\code{\link[=btw_app]{btw_app()}} to skip it for a single chat, or set the +\code{btw.client.path_btw_user} option to \code{FALSE} to change the default. + On Windows, R's notion of your home directory (\code{fs::path_home_r()}, typically your \code{Documents} folder) can differ from your user profile directory (\code{fs::path_home()}); btw searches both. diff --git a/man/btw_client.Rd b/man/btw_client.Rd index dd43bf47..8e743293 100644 --- a/man/btw_client.Rd +++ b/man/btw_client.Rd @@ -48,13 +48,24 @@ alternatively in the shorter form \code{tools = "docs_help_page"}. Finally, set \code{tools = FALSE} to skip registering \pkg{btw} tools with the chat client.} -\item{path_btw}{A path to a \code{btw.md}, \code{AGENTS.md}, or \code{CLAUDE.md} project -context file. If \code{NULL}, btw will find a project-specific context file by -walking up the parents of the current working directory, preferring -\code{btw.md}, then \code{AGENTS.md}, then \code{CLAUDE.md}, with fallback to a -user-level \code{btw.md} file if no project file is found. See \code{?btw-config} -for the complete list of locations. Set \code{path_btw = FALSE} to create a -chat client without using a \code{btw.md} file.} +\item{path_btw}{Path to a \code{btw.md}, \code{AGENTS.md}, or \code{CLAUDE.md} project +context file, or a named list with \code{project} and/or \code{user} fields to +control project-level and user-level context independently. Each field +can be \code{TRUE} (search the default locations), \code{FALSE} (skip that scope), +or a path to a specific file; a missing (or \code{NULL}) field keeps the +default for that scope: \code{project} searches the project locations, and +\code{user} follows the \code{btw.client.path_btw_user} option described below. +If \code{NULL}, btw will find a +project-specific context file by walking up the parents of the current +working directory, preferring \code{btw.md}, then \code{AGENTS.md}, then +\code{CLAUDE.md}, and always combines it with the user-level context file: +the highest-priority user-level \code{btw.md}, or if none exists, a +user-level \code{AGENTS.md} (a user-level \code{CLAUDE.md} is never used). The +default for the \code{user} scope can be changed globally with the +\code{btw.client.path_btw_user} option, e.g. \code{options(btw.client.path_btw_user = FALSE)}. A scalar path uses only that file, skipping user-level context +(equivalent to \code{list(project = path, user = FALSE)}), and a scalar \code{FALSE} +skips all context files. See \code{?btw-config} for the complete list of +locations.} \item{path_llms_txt}{A path to an \code{llms.txt} file containing context about the current project. By default, btw will look for an \code{llms.txt} file in @@ -100,7 +111,10 @@ Client settings in \code{client} and \code{tools} from a project-level \code{btw \code{AGENTS.md} file take precedence. If a project file doesn't specify a setting, btw will fall back to settings in a user-level \code{btw.md} file. This is \verb{~/btw.md} if present, otherwise \verb{~/.btw/btw.md}, \verb{~/.config/btw/btw.md}, -or \code{tools::R_user_dir("btw")}, in that order. See \code{?btw-config} for the +or \code{tools::R_user_dir("btw")}, in that order. If no user-level \code{btw.md} +exists anywhere, btw falls back to a user-level \code{AGENTS.md} in \verb{~/.btw/}, +\verb{~/.config/btw/}, \code{tools::R_user_dir("btw")}, or \verb{~/.agents/}, in that +order; a user-level \code{CLAUDE.md} is never used. See \code{?btw-config} for the complete list of user-level locations. Project-level btw tool options under the \code{options} key are merged with user-level options, with project-level options taking precedence. @@ -121,6 +135,13 @@ take precedence over settings in a \code{btw.md} file: \item \code{btw.tools}: The btw tools to include by default when starting a new btw chat, see \code{\link[=btw_tools]{btw_tools()}} for details.` } + +One additional option is consulted when resolving context files: +\itemize{ +\item \code{btw.client.path_btw_user}: \code{TRUE} (default) to include user-level context +in new chats, \code{FALSE} to skip it, or a path to a specific user-level +context file. This is the default for the \code{user} field of \code{path_btw}. +} } \subsection{Multiple Providers and Models}{ @@ -201,6 +222,19 @@ configuration in the recommended \verb{~/.btw/} directory. If more than one user-level \code{btw.md} exists, the highest-priority one is used and btw warns once per session. +If no user-level \code{btw.md} exists anywhere, \code{\link[=btw_client]{btw_client()}} and \code{\link[=btw_app]{btw_app()}} +fall back to a user-level \code{AGENTS.md}, searched in each of the directories +above (\verb{~/.btw/AGENTS.md}, \verb{~/.config/btw/AGENTS.md}, +\code{tools::R_user_dir("btw")/AGENTS.md}) followed by \verb{~/.agents/AGENTS.md}, a +cross-tool convention shared with other coding agents. This mirrors the +project-level precedence \code{btw.md} > \code{AGENTS.md} > \code{CLAUDE.md}, except a +user-level \code{CLAUDE.md} is never used. + +User-level context is combined with the project-level context file by +default; use \code{path_btw = list(user = FALSE)} in \code{\link[=btw_client]{btw_client()}} or +\code{\link[=btw_app]{btw_app()}} to skip it for a single chat, or set the +\code{btw.client.path_btw_user} option to \code{FALSE} to change the default. + On Windows, R's notion of your home directory (\code{fs::path_home_r()}, typically your \code{Documents} folder) can differ from your user profile directory (\code{fs::path_home()}); btw searches both. diff --git a/man/use_btw_md.Rd b/man/use_btw_md.Rd index da06410b..9c930252 100644 --- a/man/use_btw_md.Rd +++ b/man/use_btw_md.Rd @@ -14,19 +14,22 @@ edit_btw_md(scope = NULL) \itemize{ \item \code{"project"} (default): Creates/opens \code{btw.md} (by default) or \code{AGENTS.md} in the project root -\item \code{"user"}: Opens your user-level \code{btw.md}. When multiple user-level -configuration files exist, \code{edit_btw_md("user")} asks which one to open, -indicating the file that \code{\link[=btw_client]{btw_client()}} and \code{\link[=btw_app]{btw_app()}} load. When -creating, \code{use_btw_md("user")} uses the recommended \verb{~/.btw/} location and -offers to migrate an existing user-level configuration found elsewhere. +\item \code{"user"}: Opens your user-level \code{btw.md}, or if only a user-level +\code{AGENTS.md} exists, that file. When multiple user-level context files +exist, \code{edit_btw_md("user")} asks which one to open, indicating the file +that \code{\link[=btw_client]{btw_client()}} and \code{\link[=btw_app]{btw_app()}} load. When creating, +\code{use_btw_md("user")} uses the recommended \verb{~/.btw/} location, offers to +migrate an existing user-level configuration found elsewhere, and asks +for confirmation when creating a \code{btw.md} would take priority over an +existing user-level \code{AGENTS.md}. \item A directory path: Creates/opens \code{btw.md} in that directory \item A file path: Creates/opens that specific file } For \code{edit_btw_md()}, \code{scope = NULL} (default) will find and open the context file that \code{\link[=btw_client]{btw_client()}} would use, searching first for \code{btw.md}, -then \code{AGENTS.md}, then \code{CLAUDE.md} in the project directory and then for a -user-level \code{btw.md}.} +then \code{AGENTS.md}, then \code{CLAUDE.md} in the project directory and then for +a user-level \code{btw.md} (or, if none exists, a user-level \code{AGENTS.md}).} } \value{ \code{use_btw_md()} returns the path to the context file, invisibly. @@ -214,11 +217,18 @@ For global configuration, btw uses a user-level \code{btw.md} file. A \code{btw. directly in your home directory (\verb{~/btw.md}) takes precedence; otherwise btw looks in \verb{~/.btw/btw.md}, \verb{~/.config/btw/btw.md}, and \code{tools::R_user_dir("btw")}, in that order (see \link{btw-config} for the full -picture, including skills and agents). \code{use_btw_md("user")} creates new -configuration in the recommended \verb{~/.btw/} directory, and offers to migrate -an existing user-level configuration found elsewhere. When more than one -user-level \code{btw.md} file exists, \code{edit_btw_md("user")} asks which one to open. -Note that \pkg{btw} does not look for \code{AGENTS.md} in your home directory. +picture, including skills and agents). If no user-level \code{btw.md} exists +anywhere, btw falls back to a user-level \code{AGENTS.md}, searched in +\verb{~/.btw/}, \verb{~/.config/btw/}, \code{tools::R_user_dir("btw")}, and the +cross-tool \verb{~/.agents/} directory, in that order. A user-level \code{CLAUDE.md} +is never used. \code{use_btw_md("user")} creates new configuration in the +recommended \verb{~/.btw/} directory, offers to migrate an existing user-level +configuration found elsewhere, and asks for confirmation when creating a +\code{btw.md} would take priority over an existing user-level \code{AGENTS.md}. When +more than one user-level context file exists, \code{edit_btw_md("user")} asks +which one to open. To skip user-level context in a specific chat, use +\code{path_btw = list(user = FALSE)} in \code{\link[=btw_client]{btw_client()}} or \code{\link[=btw_app]{btw_app()}}, or change +the default globally with the \code{btw.client.path_btw_user} option. The user-level \code{btw.md} file is used alongside your project's context file, not only when a project file is missing. When both are present, diff --git a/tests/testthat/_snaps/btw_client.md b/tests/testthat/_snaps/btw_client.md index ff36ace4..2341abe8 100644 --- a/tests/testthat/_snaps/btw_client.md +++ b/tests/testthat/_snaps/btw_client.md @@ -222,3 +222,54 @@ +# normalize_path_btw() / rejects malformed values + + Code + normalize_path_btw(list()) + Condition + Error in `normalize_path_btw()`: + ! `path_btw` must be a named list with `project` and/or `user` fields. + i Each field can be `TRUE` (search the default locations), `FALSE` (skip), or a path to a file. + +--- + + Code + normalize_path_btw(list("x.md")) + Condition + Error in `normalize_path_btw()`: + ! `path_btw` must be a named list with `project` and/or `user` fields. + i Each field can be `TRUE` (search the default locations), `FALSE` (skip), or a path to a file. + +--- + + Code + normalize_path_btw(list(other = TRUE)) + Condition + Error in `normalize_path_btw()`: + ! `path_btw` must be a named list with `project` and/or `user` fields. + i Each field can be `TRUE` (search the default locations), `FALSE` (skip), or a path to a file. + +--- + + Code + normalize_path_btw(list(project = 1)) + Condition + Error in `normalize_path_btw_field()`: + ! `path_btw$project` must be a single string, not the number 1. + +--- + + Code + normalize_path_btw(1) + Condition + Error in `normalize_path_btw()`: + ! `path_btw` must be a single string, not the number 1. + +--- + + Code + withr::with_options(list(btw.client.path_btw_user = 1), normalize_path_btw(NULL)) + Condition + Error in `btw_user_path_default()`: + ! `btw.client.path_btw_user` must be a single string, not the number 1. + diff --git a/tests/testthat/_snaps/edit_btw_md.md b/tests/testthat/_snaps/edit_btw_md.md index 3e4bc64a..27cb5f7c 100644 --- a/tests/testthat/_snaps/edit_btw_md.md +++ b/tests/testthat/_snaps/edit_btw_md.md @@ -112,7 +112,7 @@ Code path <- edit_btw_md("user") Message - ! You have more than one user-level 'btw.md' config file. + ! You have more than one user-level context file. i `btw::btw_client()` and `btw::btw_app()` read the highest-priority one, marked below. i Consider consolidating your config into '~/.btw/btw.md'. v Opening '~/.btw/btw.md' @@ -122,8 +122,44 @@ Code path <- edit_btw_md("user") Message - ! You have more than one user-level 'btw.md' config file. + ! You have more than one user-level context file. i `btw::btw_client()` and `btw::btw_app()` read the highest-priority one, marked below. i Consider consolidating your config into '~/.btw/btw.md'. v Opening '~/btw.md' +# use_btw_md('user') warns and creates btw.md when an AGENTS.md exists + + Code + path <- use_btw_md("user") + Message + ! Found a user-level 'AGENTS.md' at '~/.agents/AGENTS.md'. + i A new 'btw.md' in '~/.btw' would take priority over it. + i Call `btw::edit_btw_md("user")` to edit the existing 'AGENTS.md' instead. + v Created '~/.btw/btw.md' + i See `?btw::btw_client()` for format details + i See `?btw::btw_tools()` for available tools + i Call `btw::btw_task_create_btw_md()` to use an LLM to help you initialize the project context. + +# use_btw_md('user') creates btw.md over an AGENTS.md when confirmed + + Code + path <- use_btw_md("user") + Message + ! Found a user-level 'AGENTS.md' at '~/.agents/AGENTS.md'. + i A new 'btw.md' in '~/.btw' would take priority over it. + i Call `btw::edit_btw_md("user")` to edit the existing 'AGENTS.md' instead. + v Created '~/.btw/btw.md' + i See `?btw::btw_client()` for format details + i See `?btw::btw_tools()` for available tools + i Call `btw::btw_task_create_btw_md()` to use an LLM to help you initialize the project context. + +# use_btw_md('user') keeps an existing AGENTS.md when creation is declined + + Code + path <- use_btw_md("user") + Message + ! Found a user-level 'AGENTS.md' at '~/.agents/AGENTS.md'. + i A new 'btw.md' in '~/.btw' would take priority over it. + i Call `btw::edit_btw_md("user")` to edit the existing 'AGENTS.md' instead. + i Keeping '~/.agents/AGENTS.md'. + diff --git a/tests/testthat/_snaps/utils.md b/tests/testthat/_snaps/utils.md index 7d98c4b6..a551eb9e 100644 --- a/tests/testthat/_snaps/utils.md +++ b/tests/testthat/_snaps/utils.md @@ -19,3 +19,15 @@ i Consider consolidating your btw configuration into '~/.btw/btw.md'. This warning is displayed once per session. +# path_find_user() warns for multiple user-level AGENTS.md files + + Code + result <- path_find_user("AGENTS.md", paths = user_agents_md_paths()) + Condition + Warning: + ! Found more than one user-level 'AGENTS.md' config file. + i Using '~/.btw/AGENTS.md'. + i Ignoring lower-priority: '~/.agents/AGENTS.md'. + i Consider consolidating your btw configuration into '~/.btw/AGENTS.md'. + This warning is displayed once per session. + diff --git a/tests/testthat/test-btw_chat_history_store.R b/tests/testthat/test-btw_chat_history_store.R index a1d026c0..c497a672 100644 --- a/tests/testthat/test-btw_chat_history_store.R +++ b/tests/testthat/test-btw_chat_history_store.R @@ -256,3 +256,18 @@ test_that("project pointer lifecycle restores once and follows local changes", { } ) }) + +test_that("btw_app_history_project_dir() resolves the project field of a list path_btw", { + path <- withr::local_tempfile(lines = "# btw") + + expect_identical( + btw:::btw_app_history_project_dir(list(project = path, user = FALSE)), + as.character(fs::path_norm(fs::path_dir(fs::path_abs(path)))) + ) + + # A list with only user fields falls back to marker-based detection + expect_identical( + btw:::btw_app_history_project_dir(list(user = path)), + btw:::btw_app_history_project_dir() + ) +}) diff --git a/tests/testthat/test-btw_client.R b/tests/testthat/test-btw_client.R index 502c9c10..1137c17d 100644 --- a/tests/testthat/test-btw_client.R +++ b/tests/testthat/test-btw_client.R @@ -308,7 +308,7 @@ describe("btw_client() project vs user settings", { path_user_btw <- withr::local_tempfile(fileext = ".md") local_mocked_bindings( - path_find_user = function(filename) { + path_find_user = function(filename, ...) { if (filename == "btw.md") path_user_btw else NULL } ) @@ -480,7 +480,7 @@ describe("btw_client() project vs user settings", { ) withr::defer(unlink(path_user_btw)) - config <- read_btw_file(path_user_btw) + config <- read_btw_file(list(project = path_user_btw)) expect_equal(config$btw_system_prompt, "home btw instructions") }) @@ -559,6 +559,149 @@ describe("btw_client() project vs user settings", { }) }) +describe("normalize_path_btw()", { + it("keeps the meaning of scalar values", { + expect_equal(normalize_path_btw(NULL), list(project = TRUE, user = TRUE)) + expect_equal(normalize_path_btw(TRUE), list(project = TRUE, user = TRUE)) + expect_equal(normalize_path_btw(FALSE), list(project = FALSE, user = FALSE)) + # A scalar path uses only that file, skipping user-level context + expect_equal(normalize_path_btw("x.md"), list(project = "x.md", user = FALSE)) + }) + + it("controls each scope via a named list", { + expect_equal( + normalize_path_btw(list(project = "x.md")), + list(project = "x.md", user = TRUE) + ) + expect_equal( + normalize_path_btw(list(user = "u.md")), + list(project = TRUE, user = "u.md") + ) + expect_equal( + normalize_path_btw(list(project = FALSE, user = TRUE)), + list(project = FALSE, user = TRUE) + ) + }) + + it("reads the btw.client.path_btw_user option as the user default", { + withr::local_options(btw.client.path_btw_user = FALSE) + expect_equal(normalize_path_btw(NULL), list(project = TRUE, user = FALSE)) + expect_equal( + normalize_path_btw(list(project = "x.md")), + list(project = "x.md", user = FALSE) + ) + + withr::local_options(btw.client.path_btw_user = "u.md") + expect_equal(normalize_path_btw(NULL), list(project = TRUE, user = "u.md")) + expect_equal( + normalize_path_btw(list(project = "x.md")), + list(project = "x.md", user = "u.md") + ) + + # An explicit user field wins over the option + expect_equal( + normalize_path_btw(list(user = TRUE)), + list(project = TRUE, user = TRUE) + ) + + # A scalar path never consults the option: it uses only that file + expect_equal(normalize_path_btw("x.md"), list(project = "x.md", user = FALSE)) + }) + + it("rejects malformed values", { + expect_snapshot(error = TRUE, normalize_path_btw(list())) + expect_snapshot(error = TRUE, normalize_path_btw(list("x.md"))) + expect_snapshot(error = TRUE, normalize_path_btw(list(other = TRUE))) + expect_snapshot(error = TRUE, normalize_path_btw(list(project = 1))) + expect_snapshot(error = TRUE, normalize_path_btw(1)) + expect_snapshot( + error = TRUE, + withr::with_options(list(btw.client.path_btw_user = 1), normalize_path_btw(NULL)) + ) + }) +}) + +describe("read_btw_file() with scoped path_btw", { + project_dir <- withr::local_tempdir("btw-test-project-") + path_project <- withr::local_tempfile( + lines = c("---", "client: openai", "---", "PROJECT CONTEXT") + ) + path_user <- withr::local_tempfile( + lines = c("---", "tools: docs", "---", "USER CONTEXT") + ) + path_agents <- withr::local_tempfile( + lines = c("---", "client: anthropic", "---", "USER AGENTS CONTEXT") + ) + + it("combines project and user files by default", { + local_btw_md(project = path_project, user = path_user) + + config <- read_btw_file() + expect_equal(config$btw_system_prompt, "USER CONTEXT\n\n---\n\nPROJECT CONTEXT") + expect_equal(config$client, "openai") + expect_equal(config$tools, "docs") + }) + + it("skips user context with list(user = FALSE)", { + local_btw_md(project = path_project, user = path_user) + + config <- read_btw_file(list(user = FALSE)) + expect_equal(config$btw_system_prompt, "PROJECT CONTEXT") + expect_equal(config$client, "openai") + expect_null(config$tools) + }) + + it("keeps user context when the list names only project", { + local_btw_md(project = path_project, user = path_user) + + config <- read_btw_file(list(project = path_project)) + expect_equal(config$btw_system_prompt, "USER CONTEXT\n\n---\n\nPROJECT CONTEXT") + expect_equal(config$client, "openai") + expect_equal(config$tools, "docs") + }) + + it("uses only an explicit user file with list(project = FALSE, user = path)", { + config <- read_btw_file(list(project = FALSE, user = path_user)) + expect_equal(config$btw_system_prompt, "USER CONTEXT") + expect_equal(config$tools, "docs") + }) + + it("falls back to a user-level AGENTS.md when no user-level btw.md exists", { + local_mocked_bindings( + find_btw_context_file = function(...) NULL, + path_find_user = function(filename, ...) { + if (filename == "btw.md") NULL else path_agents + } + ) + + config <- read_btw_file(list(project = FALSE, user = TRUE)) + expect_equal(config$btw_system_prompt, "USER AGENTS CONTEXT") + expect_equal(config$client, "anthropic") + }) + + it("a scalar path uses only that file, without user-level context", { + local_btw_md(project = path_project, user = path_user) + + config <- read_btw_file(path_project) + expect_equal(config$btw_system_prompt, "PROJECT CONTEXT") + expect_equal(config$client, "openai") + expect_null(config$tools) + }) + + it("throws for a user path that does not exist", { + expect_error( + read_btw_file(list(project = FALSE, user = tempfile("missing-"))), + "does not exist" + ) + }) + + it("returns an empty config when both scopes are skipped", { + local_btw_md(project = path_project, user = path_user) + config <- read_btw_file(list(project = FALSE, user = FALSE)) + expect_equal(config, list()) + }) +}) + test_that("btw_client() throws for deprecated `model` and `provider` fields in btw.md", { withr::local_envvar(list(OPENAI_API_KEY = "beep")) diff --git a/tests/testthat/test-edit_btw_md.R b/tests/testthat/test-edit_btw_md.R index f271d775..3db3cee5 100644 --- a/tests/testthat/test-edit_btw_md.R +++ b/tests/testthat/test-edit_btw_md.R @@ -452,3 +452,106 @@ test_that("btw_md_template() selects correct template", { template <- btw_md_template("custom.md") expect_match(basename(template), "^btw.*\\.md$") }) + +test_that("edit_btw_md('user') opens a user-level AGENTS.md when no btw.md exists", { + wd <- withr::local_tempdir() + local_user_home(wd) + local_mocked_bindings(is_interactive = function() TRUE) + + fs::dir_create(fs::path(wd, ".agents")) + fs::file_create(fs::path(wd, ".agents", "AGENTS.md")) + + local_mocked_bindings( + menu = function(...) stop("menu should not be called"), + .package = "utils" + ) + local_mocked_bindings( + is_installed = function(pkg) pkg != "rstudioapi", + .package = "rlang" + ) + + edited <- NULL + with_mocked_bindings( + file.edit = function(file) edited <<- file, + .package = "utils", + suppressMessages(path <- edit_btw_md("user")) + ) + + expect_equal(edited, fs::path(wd, ".agents", "AGENTS.md")) +}) + +test_that("edit_btw_md('user') prefers btw.md files over AGENTS.md files", { + wd <- withr::local_tempdir() + local_user_home(wd) + local_mocked_bindings(is_interactive = function() FALSE) + + fs::dir_create(fs::path(wd, ".btw")) + fs::file_create(fs::path(wd, ".btw", "AGENTS.md")) + fs::file_create(fs::path(wd, "btw.md")) + + local_mocked_bindings( + is_installed = function(pkg) pkg != "rstudioapi", + .package = "rlang" + ) + + edited <- NULL + with_mocked_bindings( + file.edit = function(file) edited <<- file, + .package = "utils", + suppressMessages(path <- edit_btw_md("user")) + ) + + expect_equal(edited, fs::path(wd, "btw.md")) +}) + +test_that("use_btw_md('user') warns and creates btw.md when an AGENTS.md exists", { + wd <- withr::local_tempdir() + local_user_home(wd) + local_mocked_bindings(is_interactive = function() FALSE) + + fs::dir_create(fs::path(wd, ".agents")) + fs::file_create(fs::path(wd, ".agents", "AGENTS.md")) + + expect_snapshot( + path <- use_btw_md("user") + ) + + expect_equal(path, fs::path(wd, ".btw", "btw.md")) + expect_true(fs::file_exists(fs::path(wd, ".btw", "btw.md"))) + expect_true(fs::file_exists(fs::path(wd, ".agents", "AGENTS.md"))) +}) + +test_that("use_btw_md('user') creates btw.md over an AGENTS.md when confirmed", { + wd <- withr::local_tempdir() + local_user_home(wd) + local_mocked_bindings(is_interactive = function() TRUE) + local_mocked_bindings(menu = function(...) 1L, .package = "utils") + + fs::dir_create(fs::path(wd, ".agents")) + fs::file_create(fs::path(wd, ".agents", "AGENTS.md")) + + expect_snapshot( + path <- use_btw_md("user") + ) + + expect_equal(path, fs::path(wd, ".btw", "btw.md")) + expect_true(fs::file_exists(fs::path(wd, ".btw", "btw.md"))) +}) + +test_that("use_btw_md('user') keeps an existing AGENTS.md when creation is declined", { + wd <- withr::local_tempdir() + local_user_home(wd) + local_mocked_bindings(is_interactive = function() TRUE) + local_mocked_bindings(menu = function(...) 2L, .package = "utils") + + fs::dir_create(fs::path(wd, ".agents")) + fs::file_create(fs::path(wd, ".agents", "AGENTS.md")) + + expect_snapshot( + path <- use_btw_md("user") + ) + + expect_equal(path, fs::path(wd, ".agents", "AGENTS.md")) + expect_false(fs::file_exists(fs::path(wd, ".btw", "btw.md"))) + expect_true(fs::file_exists(fs::path(wd, ".agents", "AGENTS.md"))) +}) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 8dcd76ff..5fe9e583 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -486,3 +486,71 @@ test_that("path_find_user() prefers ~/btw.md and warns on multiple configs", { # The loose ~/btw.md takes precedence over ~/.btw/btw.md. expect_equal(result, fs::path_norm(fs::path(user_dir, "btw.md"))) }) + +test_that("user_agents_md_paths() covers btw user dirs then ~/.agents/", { + user_dir <- withr::local_tempdir() + local_user_home(user_dir) + + paths <- user_agents_md_paths() + expect_equal( + paths, + unique(as.character(c( + file.path(btw_user_dirs(), "AGENTS.md"), + fs::path(user_dir, ".agents", "AGENTS.md") + ))) + ) + # No loose home-root AGENTS.md candidate + expect_false(fs::path(user_dir, "AGENTS.md") %in% paths) +}) + +test_that("path_find_user_context() prefers any btw.md over any AGENTS.md", { + user_dir <- withr::local_tempdir() + local_user_home(user_dir) + withr::local_envvar(TESTTHAT = NA) + + expect_null(path_find_user_context()) + + # Only one AGENTS.md: it's used + fs::dir_create(fs::path(user_dir, ".agents")) + fs::file_create(fs::path(user_dir, ".agents", "AGENTS.md")) + expect_equal( + path_find_user_context(), + fs::path_norm(fs::path(user_dir, ".agents", "AGENTS.md")) + ) + + # A btw.md anywhere beats an AGENTS.md anywhere + fs::dir_create(fs::path(user_dir, ".config", "btw")) + fs::file_create(fs::path(user_dir, ".config", "btw", "btw.md")) + expect_equal( + path_find_user_context(), + fs::path_norm(fs::path(user_dir, ".config", "btw", "btw.md")) + ) +}) + +test_that("path_find_user_context() never uses a user-level CLAUDE.md", { + user_dir <- withr::local_tempdir() + local_user_home(user_dir) + withr::local_envvar(TESTTHAT = NA) + + fs::file_create(fs::path(user_dir, "CLAUDE.md")) + fs::dir_create(fs::path(user_dir, ".btw")) + fs::file_create(fs::path(user_dir, ".btw", "CLAUDE.md")) + expect_null(path_find_user_context()) +}) + +test_that("path_find_user() warns for multiple user-level AGENTS.md files", { + user_dir <- withr::local_tempdir() + local_user_home(user_dir) + withr::local_envvar(TESTTHAT = NA) + withr::local_options(rlib_warning_verbosity = "verbose") + + fs::dir_create(fs::path(user_dir, ".btw")) + fs::file_create(fs::path(user_dir, ".btw", "AGENTS.md")) + fs::dir_create(fs::path(user_dir, ".agents")) + fs::file_create(fs::path(user_dir, ".agents", "AGENTS.md")) + + expect_snapshot( + result <- path_find_user("AGENTS.md", paths = user_agents_md_paths()) + ) + expect_equal(result, fs::path_norm(fs::path(user_dir, ".btw", "AGENTS.md"))) +}) diff --git a/vignettes/btw-md.Rmd b/vignettes/btw-md.Rmd index 6f798105..248981a2 100644 --- a/vignettes/btw-md.Rmd +++ b/vignettes/btw-md.Rmd @@ -29,7 +29,7 @@ A `btw.md` file plays the same role for btw, and btw falls back to those files w Using `btw.md` gives you a few things those files don't: - **It configures the chat, not just the instructions.** The front matter chooses the model, tools, and options. btw ignores front matter in `CLAUDE.md`, and the other tools that read `AGENTS.md` don't expect front matter there. -- **It has a user-level version.** btw combines a `btw.md` in your home directory, such as `~/.btw/btw.md`, with each project's file. btw doesn't look for `AGENTS.md` or `CLAUDE.md` in your home directory. +- **It has a user-level version.** btw combines a `btw.md` in your home directory, such as `~/.btw/btw.md`, with each project's file. If there's no user-level `btw.md`, btw falls back to a user-level `AGENTS.md` in `~/.btw/`, `~/.config/btw/`, or `~/.agents/`. A user-level `CLAUDE.md` is never used. - **It keeps btw-specific instructions separate.** Instructions about btw's tools or your R workflow don't end up in the files that other coding agents read. In this vignette, we'll set up a chat by hand, save that setup in a `btw.md` file, and then learn how btw decides which file to use. @@ -232,6 +232,7 @@ btw reads the front matter in `AGENTS.md` like `btw.md`, but ignores the front m btw reads only one of these files, so if your project has both `btw.md` and `AGENTS.md`, btw uses `btw.md` and ignores `AGENTS.md`. btw also reads a **user-level** `btw.md` that applies to all of your projects, such as `~/.btw/btw.md`. +If there's no user-level `btw.md`, btw falls back to a user-level `AGENTS.md` in `~/.btw/`, `~/.config/btw/`, or `~/.agents/`, in that order. The two files are combined: - The project's `client` and `tools` fields are used when they're present; otherwise the user-level fields are used. @@ -242,12 +243,15 @@ This lets you keep your general preferences, such as a default model or coding s See `?btw-config` for every location btw checks. To use a particular file instead of searching, pass its path to `path_btw`. -To start a chat without any `btw.md` file, use `path_btw = FALSE`: +To start a chat without any `btw.md` file, use `path_btw = FALSE`. +Or pass a named list to control project and user context separately, e.g. `path_btw = list(user = FALSE)`; the `btw.client.path_btw_user` option sets the default for the user scope. ```{r, eval = FALSE} btw_app(path_btw = "analysis/btw.md") btw_client(path_btw = FALSE) + +btw_client(path_btw = list(user = FALSE)) ``` ## Next: the same format for agents From d6834e693dfcdc4735a2c8d655084338c6f594de Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Tue, 6 Oct 2026 10:35:05 -0400 Subject: [PATCH 2/7] fix: search user-level AGENTS.md only in ~/.agents/ A user-level AGENTS.md is a cross-tool convention, so it is now searched only in ~/.agents/ rather than in btw's own config directories, which remain reserved for btw.md. The multiple-user-config warning now points AGENTS.md duplicates at ~/.agents/AGENTS.md as the consolidation target. --- NEWS.md | 2 +- R/btw-config.R | 6 ++---- R/btw_client.R | 8 ++++---- R/edit_btw_md.R | 6 ++---- R/utils.R | 25 ++++++++++++++----------- man/btw-config.Rd | 6 ++---- man/btw_client.Rd | 14 ++++++-------- man/use_btw_md.Rd | 6 ++---- tests/testthat/_snaps/utils.md | 4 ++-- tests/testthat/test-utils.R | 30 ++++++++++++++++++++---------- vignettes/btw-md.Rmd | 4 ++-- 11 files changed, 57 insertions(+), 54 deletions(-) diff --git a/NEWS.md b/NEWS.md index 11a9d7fe..08484f79 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,6 @@ # btw (development version) -* If no user-level `btw.md` exists, `btw_client()` and `btw_app()` now fall back to a user-level `AGENTS.md`, searched in `~/.btw/`, `~/.config/btw/`, `tools::R_user_dir("btw")`, and `~/.agents/`, in that order. This mirrors the project-level precedence `btw.md` > `AGENTS.md`; a user-level `CLAUDE.md` is never used. `edit_btw_md("user")` opens a user-level `AGENTS.md` when no user-level `btw.md` exists, and `use_btw_md("user")` asks for confirmation before creating a `btw.md` that would take priority over an existing `AGENTS.md` (#226). +* If no user-level `btw.md` exists, `btw_client()` and `btw_app()` now fall back to a user-level `AGENTS.md` in the cross-tool `~/.agents/` directory. This mirrors the project-level precedence `btw.md` > `AGENTS.md`; a user-level `CLAUDE.md` is never used. `edit_btw_md("user")` opens a user-level `AGENTS.md` when no user-level `btw.md` exists, and `use_btw_md("user")` asks for confirmation before creating a `btw.md` that would take priority over an existing `AGENTS.md` (#226). * The `path_btw` argument of `btw_client()` and `btw_app()` now also accepts a named list with `project` and/or `user` fields, each `TRUE` (search the default locations), `FALSE` (skip that scope), or a path to a specific file; a missing field keeps the default for that scope, e.g. `path_btw = list(user = FALSE)` skips user-level context but keeps project context. A scalar path now uses only that file (previously the user-level `btw.md` was also loaded alongside it); pass `list(project = "path")` to combine a specific project file with the default user-level context. `NULL`/`TRUE` and `FALSE` keep their previous meaning. The new `btw.client.path_btw_user` option sets the default for the user scope, so `options(btw.client.path_btw_user = FALSE)` opts out of user-level context globally (#226). diff --git a/R/btw-config.R b/R/btw-config.R index b8710655..b32adde3 100644 --- a/R/btw-config.R +++ b/R/btw-config.R @@ -36,10 +36,8 @@ #' once per session. #' #' If no user-level `btw.md` exists anywhere, [btw_client()] and [btw_app()] -#' fall back to a user-level `AGENTS.md`, searched in each of the directories -#' above (`~/.btw/AGENTS.md`, `~/.config/btw/AGENTS.md`, -#' `tools::R_user_dir("btw")/AGENTS.md`) followed by `~/.agents/AGENTS.md`, a -#' cross-tool convention shared with other coding agents. This mirrors the +#' fall back to a user-level `AGENTS.md` in the cross-tool `~/.agents/` +#' directory, a convention shared with other coding agents. This mirrors the #' project-level precedence `btw.md` > `AGENTS.md` > `CLAUDE.md`, except a #' user-level `CLAUDE.md` is never used. #' diff --git a/R/btw_client.R b/R/btw_client.R index 83cc8f60..8b7fc3c7 100644 --- a/R/btw_client.R +++ b/R/btw_client.R @@ -27,10 +27,10 @@ #' setting, btw will fall back to settings in a user-level `btw.md` file. This #' is `~/btw.md` if present, otherwise `~/.btw/btw.md`, `~/.config/btw/btw.md`, #' or `tools::R_user_dir("btw")`, in that order. If no user-level `btw.md` -#' exists anywhere, btw falls back to a user-level `AGENTS.md` in `~/.btw/`, -#' `~/.config/btw/`, `tools::R_user_dir("btw")`, or `~/.agents/`, in that -#' order; a user-level `CLAUDE.md` is never used. See `?btw-config` for the -#' complete list of user-level locations. Project-level btw tool options under +#' exists anywhere, btw falls back to a user-level `AGENTS.md` in the +#' cross-tool `~/.agents/` directory; a user-level `CLAUDE.md` is never used. +#' See `?btw-config` for the complete list of user-level locations. +#' Project-level btw tool options under #' the `options` key are merged with user-level options, with project-level #' options taking precedence. #' diff --git a/R/edit_btw_md.R b/R/edit_btw_md.R index a4b19066..e8bff1cc 100644 --- a/R/edit_btw_md.R +++ b/R/edit_btw_md.R @@ -173,10 +173,8 @@ #' looks in `~/.btw/btw.md`, `~/.config/btw/btw.md`, and #' `tools::R_user_dir("btw")`, in that order (see [btw-config] for the full #' picture, including skills and agents). If no user-level `btw.md` exists -#' anywhere, btw falls back to a user-level `AGENTS.md`, searched in -#' `~/.btw/`, `~/.config/btw/`, `tools::R_user_dir("btw")`, and the -#' cross-tool `~/.agents/` directory, in that order. A user-level `CLAUDE.md` -#' is never used. `use_btw_md("user")` creates new configuration in the +#' anywhere, btw falls back to a user-level `AGENTS.md` in the cross-tool +#' `~/.agents/` directory. A user-level `CLAUDE.md` is never used. `use_btw_md("user")` creates new configuration in the #' recommended `~/.btw/` directory, offers to migrate an existing user-level #' configuration found elsewhere, and asks for confirmation when creating a #' `btw.md` would take priority over an existing user-level `AGENTS.md`. When diff --git a/R/utils.R b/R/utils.R index 92314ca3..2242c0e6 100644 --- a/R/utils.R +++ b/R/utils.R @@ -245,23 +245,21 @@ path_find_user <- function(filename, paths = user_config_paths(filename)) { } # Ordered user-level candidate paths for AGENTS.md, in decreasing priority. -# Mirrors btw.md's directory locations (via btw_user_dirs()) and adds the -# cross-tool ~/.agents/ convention shared with other coding agents. Unlike -# btw.md, a loose AGENTS.md in the home root is not searched: no tool reads -# one there, and btw's project search no longer walks through the home -# directory, so a loose ~/AGENTS.md is never picked up by accident either. +# Unlike btw.md, which is searched in btw's own config directories, a +# user-level AGENTS.md lives in the cross-tool ~/.agents/ convention shared +# with other coding agents. A loose AGENTS.md in the home root is not +# searched: no tool reads one there, and btw's project search no longer walks +# through the home directory, so a loose ~/AGENTS.md is never picked up by +# accident either. user_agents_md_paths <- function() { homes <- unique(c(fs::path_home(), fs::path_home_r())) - unique(as.character(c( - file.path(btw_user_dirs(), "AGENTS.md"), - fs::path(homes, ".agents", "AGENTS.md") - ))) + unique(as.character(fs::path(homes, ".agents", "AGENTS.md"))) } # The active user-level context file, mirroring the project-level precedence # btw.md > AGENTS.md > CLAUDE.md, except CLAUDE.md is never considered at user # level: the highest-priority existing user-level btw.md, or if none exists -# anywhere, the highest-priority existing user-level AGENTS.md. +# anywhere, a user-level AGENTS.md in ~/.agents/. path_find_user_context <- function() { path_find_user("btw.md") %||% path_find_user("AGENTS.md", paths = user_agents_md_paths()) @@ -271,7 +269,12 @@ path_find_user_context <- function() { # user editing a now-shadowed file has a hint about which one btw reads. warn_multiple_user_config <- function(paths, filename = basename(paths[[1]])) { shown <- path_home_display(paths) - target <- path_home_display(fs::path(btw_user_dir_preferred(), filename)) + recommended <- if (identical(filename, "AGENTS.md")) { + user_agents_md_paths()[[1]] + } else { + fs::path(btw_user_dir_preferred(), filename) + } + target <- path_home_display(recommended) cli::cli_warn( c( "!" = "Found more than one user-level {.file {filename}} config file.", diff --git a/man/btw-config.Rd b/man/btw-config.Rd index 317ec529..5aa98a72 100644 --- a/man/btw-config.Rd +++ b/man/btw-config.Rd @@ -41,10 +41,8 @@ user-level \code{btw.md} exists, the highest-priority one is used and btw warns once per session. If no user-level \code{btw.md} exists anywhere, \code{\link[=btw_client]{btw_client()}} and \code{\link[=btw_app]{btw_app()}} -fall back to a user-level \code{AGENTS.md}, searched in each of the directories -above (\verb{~/.btw/AGENTS.md}, \verb{~/.config/btw/AGENTS.md}, -\code{tools::R_user_dir("btw")/AGENTS.md}) followed by \verb{~/.agents/AGENTS.md}, a -cross-tool convention shared with other coding agents. This mirrors the +fall back to a user-level \code{AGENTS.md} in the cross-tool \verb{~/.agents/} +directory, a convention shared with other coding agents. This mirrors the project-level precedence \code{btw.md} > \code{AGENTS.md} > \code{CLAUDE.md}, except a user-level \code{CLAUDE.md} is never used. diff --git a/man/btw_client.Rd b/man/btw_client.Rd index 8e743293..faa656e7 100644 --- a/man/btw_client.Rd +++ b/man/btw_client.Rd @@ -112,10 +112,10 @@ Client settings in \code{client} and \code{tools} from a project-level \code{btw setting, btw will fall back to settings in a user-level \code{btw.md} file. This is \verb{~/btw.md} if present, otherwise \verb{~/.btw/btw.md}, \verb{~/.config/btw/btw.md}, or \code{tools::R_user_dir("btw")}, in that order. If no user-level \code{btw.md} -exists anywhere, btw falls back to a user-level \code{AGENTS.md} in \verb{~/.btw/}, -\verb{~/.config/btw/}, \code{tools::R_user_dir("btw")}, or \verb{~/.agents/}, in that -order; a user-level \code{CLAUDE.md} is never used. See \code{?btw-config} for the -complete list of user-level locations. Project-level btw tool options under +exists anywhere, btw falls back to a user-level \code{AGENTS.md} in the +cross-tool \verb{~/.agents/} directory; a user-level \code{CLAUDE.md} is never used. +See \code{?btw-config} for the complete list of user-level locations. +Project-level btw tool options under the \code{options} key are merged with user-level options, with project-level options taking precedence. @@ -223,10 +223,8 @@ user-level \code{btw.md} exists, the highest-priority one is used and btw warns once per session. If no user-level \code{btw.md} exists anywhere, \code{\link[=btw_client]{btw_client()}} and \code{\link[=btw_app]{btw_app()}} -fall back to a user-level \code{AGENTS.md}, searched in each of the directories -above (\verb{~/.btw/AGENTS.md}, \verb{~/.config/btw/AGENTS.md}, -\code{tools::R_user_dir("btw")/AGENTS.md}) followed by \verb{~/.agents/AGENTS.md}, a -cross-tool convention shared with other coding agents. This mirrors the +fall back to a user-level \code{AGENTS.md} in the cross-tool \verb{~/.agents/} +directory, a convention shared with other coding agents. This mirrors the project-level precedence \code{btw.md} > \code{AGENTS.md} > \code{CLAUDE.md}, except a user-level \code{CLAUDE.md} is never used. diff --git a/man/use_btw_md.Rd b/man/use_btw_md.Rd index 9c930252..ae4d79be 100644 --- a/man/use_btw_md.Rd +++ b/man/use_btw_md.Rd @@ -218,10 +218,8 @@ directly in your home directory (\verb{~/btw.md}) takes precedence; otherwise bt looks in \verb{~/.btw/btw.md}, \verb{~/.config/btw/btw.md}, and \code{tools::R_user_dir("btw")}, in that order (see \link{btw-config} for the full picture, including skills and agents). If no user-level \code{btw.md} exists -anywhere, btw falls back to a user-level \code{AGENTS.md}, searched in -\verb{~/.btw/}, \verb{~/.config/btw/}, \code{tools::R_user_dir("btw")}, and the -cross-tool \verb{~/.agents/} directory, in that order. A user-level \code{CLAUDE.md} -is never used. \code{use_btw_md("user")} creates new configuration in the +anywhere, btw falls back to a user-level \code{AGENTS.md} in the cross-tool +\verb{~/.agents/} directory. A user-level \code{CLAUDE.md} is never used. \code{use_btw_md("user")} creates new configuration in the recommended \verb{~/.btw/} directory, offers to migrate an existing user-level configuration found elsewhere, and asks for confirmation when creating a \code{btw.md} would take priority over an existing user-level \code{AGENTS.md}. When diff --git a/tests/testthat/_snaps/utils.md b/tests/testthat/_snaps/utils.md index a551eb9e..3e310958 100644 --- a/tests/testthat/_snaps/utils.md +++ b/tests/testthat/_snaps/utils.md @@ -26,8 +26,8 @@ Condition Warning: ! Found more than one user-level 'AGENTS.md' config file. - i Using '~/.btw/AGENTS.md'. + i Using '/.agents/AGENTS.md'. i Ignoring lower-priority: '~/.agents/AGENTS.md'. - i Consider consolidating your btw configuration into '~/.btw/AGENTS.md'. + i Consider consolidating your btw configuration into '/.agents/AGENTS.md'. This warning is displayed once per session. diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 5fe9e583..a6b15fe7 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -487,19 +487,17 @@ test_that("path_find_user() prefers ~/btw.md and warns on multiple configs", { expect_equal(result, fs::path_norm(fs::path(user_dir, "btw.md"))) }) -test_that("user_agents_md_paths() covers btw user dirs then ~/.agents/", { +test_that("user_agents_md_paths() covers only the cross-tool ~/.agents/", { user_dir <- withr::local_tempdir() local_user_home(user_dir) paths <- user_agents_md_paths() expect_equal( paths, - unique(as.character(c( - file.path(btw_user_dirs(), "AGENTS.md"), - fs::path(user_dir, ".agents", "AGENTS.md") - ))) + unique(as.character(fs::path(user_dir, ".agents", "AGENTS.md"))) ) - # No loose home-root AGENTS.md candidate + # No btw config directory and no loose home-root AGENTS.md candidates + expect_false(any(file.path(btw_user_dirs(), "AGENTS.md") %in% paths)) expect_false(fs::path(user_dir, "AGENTS.md") %in% paths) }) @@ -510,6 +508,11 @@ test_that("path_find_user_context() prefers any btw.md over any AGENTS.md", { expect_null(path_find_user_context()) + # An AGENTS.md in a btw config directory is not a user-level candidate + fs::dir_create(fs::path(user_dir, ".btw")) + fs::file_create(fs::path(user_dir, ".btw", "AGENTS.md")) + expect_null(path_find_user_context()) + # Only one AGENTS.md: it's used fs::dir_create(fs::path(user_dir, ".agents")) fs::file_create(fs::path(user_dir, ".agents", "AGENTS.md")) @@ -544,13 +547,20 @@ test_that("path_find_user() warns for multiple user-level AGENTS.md files", { withr::local_envvar(TESTTHAT = NA) withr::local_options(rlib_warning_verbosity = "verbose") - fs::dir_create(fs::path(user_dir, ".btw")) - fs::file_create(fs::path(user_dir, ".btw", "AGENTS.md")) + # Windows-style: the two notions of home differ, so both are searched + local_mocked_bindings( + path_home_r = function(...) fs::path(user_dir, "home_r", ...), + .package = "fs" + ) + fs::dir_create(fs::path(user_dir, ".agents")) fs::file_create(fs::path(user_dir, ".agents", "AGENTS.md")) + fs::dir_create(fs::path(user_dir, "home_r", ".agents")) + fs::file_create(fs::path(user_dir, "home_r", ".agents", "AGENTS.md")) expect_snapshot( - result <- path_find_user("AGENTS.md", paths = user_agents_md_paths()) + result <- path_find_user("AGENTS.md", paths = user_agents_md_paths()), + transform = function(lines) gsub(as.character(fs::path_norm(user_dir)), "", lines, fixed = TRUE) ) - expect_equal(result, fs::path_norm(fs::path(user_dir, ".btw", "AGENTS.md"))) + expect_equal(result, fs::path_norm(fs::path(user_dir, ".agents", "AGENTS.md"))) }) diff --git a/vignettes/btw-md.Rmd b/vignettes/btw-md.Rmd index 248981a2..91b183ae 100644 --- a/vignettes/btw-md.Rmd +++ b/vignettes/btw-md.Rmd @@ -29,7 +29,7 @@ A `btw.md` file plays the same role for btw, and btw falls back to those files w Using `btw.md` gives you a few things those files don't: - **It configures the chat, not just the instructions.** The front matter chooses the model, tools, and options. btw ignores front matter in `CLAUDE.md`, and the other tools that read `AGENTS.md` don't expect front matter there. -- **It has a user-level version.** btw combines a `btw.md` in your home directory, such as `~/.btw/btw.md`, with each project's file. If there's no user-level `btw.md`, btw falls back to a user-level `AGENTS.md` in `~/.btw/`, `~/.config/btw/`, or `~/.agents/`. A user-level `CLAUDE.md` is never used. +- **It has a user-level version.** btw combines a `btw.md` in your home directory, such as `~/.btw/btw.md`, with each project's file. If there's no user-level `btw.md`, btw falls back to a user-level `AGENTS.md` in the cross-tool `~/.agents/` directory. A user-level `CLAUDE.md` is never used. - **It keeps btw-specific instructions separate.** Instructions about btw's tools or your R workflow don't end up in the files that other coding agents read. In this vignette, we'll set up a chat by hand, save that setup in a `btw.md` file, and then learn how btw decides which file to use. @@ -232,7 +232,7 @@ btw reads the front matter in `AGENTS.md` like `btw.md`, but ignores the front m btw reads only one of these files, so if your project has both `btw.md` and `AGENTS.md`, btw uses `btw.md` and ignores `AGENTS.md`. btw also reads a **user-level** `btw.md` that applies to all of your projects, such as `~/.btw/btw.md`. -If there's no user-level `btw.md`, btw falls back to a user-level `AGENTS.md` in `~/.btw/`, `~/.config/btw/`, or `~/.agents/`, in that order. +If there's no user-level `btw.md`, btw falls back to a user-level `AGENTS.md` in the cross-tool `~/.agents/` directory. The two files are combined: - The project's `client` and `tools` fields are used when they're present; otherwise the user-level fields are used. From 613ec950dc7a56e14162e543dc6d4d583e4d6f2a Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Tue, 6 Oct 2026 10:58:22 -0400 Subject: [PATCH 3/7] docs: tighten NEWS entries for user-level AGENTS.md fallback Trim implementation details now covered in the function docs and break up the dense path_btw bullet into separate sentences. --- NEWS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 08484f79..d1f595bf 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,8 +1,8 @@ # btw (development version) -* If no user-level `btw.md` exists, `btw_client()` and `btw_app()` now fall back to a user-level `AGENTS.md` in the cross-tool `~/.agents/` directory. This mirrors the project-level precedence `btw.md` > `AGENTS.md`; a user-level `CLAUDE.md` is never used. `edit_btw_md("user")` opens a user-level `AGENTS.md` when no user-level `btw.md` exists, and `use_btw_md("user")` asks for confirmation before creating a `btw.md` that would take priority over an existing `AGENTS.md` (#226). +* `btw_client()` and `btw_app()` now fall back to a user-level `AGENTS.md` in the cross-tool `~/.agents/` directory when no user-level `btw.md` exists, mirroring the project-level precedence `btw.md` > `AGENTS.md`. A user-level `CLAUDE.md` is never used. `edit_btw_md()` and `use_btw_md()` are aware of the new fallback, e.g. `use_btw_md("user")` asks before creating a `btw.md` that would take priority over an existing `AGENTS.md` (#226). -* The `path_btw` argument of `btw_client()` and `btw_app()` now also accepts a named list with `project` and/or `user` fields, each `TRUE` (search the default locations), `FALSE` (skip that scope), or a path to a specific file; a missing field keeps the default for that scope, e.g. `path_btw = list(user = FALSE)` skips user-level context but keeps project context. A scalar path now uses only that file (previously the user-level `btw.md` was also loaded alongside it); pass `list(project = "path")` to combine a specific project file with the default user-level context. `NULL`/`TRUE` and `FALSE` keep their previous meaning. The new `btw.client.path_btw_user` option sets the default for the user scope, so `options(btw.client.path_btw_user = FALSE)` opts out of user-level context globally (#226). +* The `path_btw` argument of `btw_client()` and `btw_app()` now also accepts a named list with `project` and/or `user` fields, each `TRUE` (search the default locations), `FALSE` (skip that scope), or a path to a specific file; a missing field keeps the default for that scope. For example, `path_btw = list(user = FALSE)` skips user-level context for a single chat. A scalar path now uses only that file, no longer combining it with user-level context; pass `list(project = "path")` to keep the user-level context. The new `btw.client.path_btw_user` option sets the global default for the user scope (#226). * `path_find_in_project()` now stops its upward search when it reaches the home directory (either `fs::path_home()` or `fs::path_home_r()`, which differ on Windows) and never treats files there as project files, so `~/AGENTS.md` or `~/CLAUDE.md` are no longer picked up as project context when the working directory is under `~` but outside a project. Home-level files are handled only by the user-level lookup. This also fixes `~/btw.md` instructions being included twice — once as the project file and once as the user-level config (#225). From c8c2b6b7ed9ca55c744a3306ce65b5f1352d2ecd Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Tue, 6 Oct 2026 10:58:48 -0400 Subject: [PATCH 4/7] docs: simplify path_find_in_project NEWS entry Drop a redundant restatement and merge two sentences explaining the user-level lookup. --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index d1f595bf..65ab5e95 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,7 +4,7 @@ * The `path_btw` argument of `btw_client()` and `btw_app()` now also accepts a named list with `project` and/or `user` fields, each `TRUE` (search the default locations), `FALSE` (skip that scope), or a path to a specific file; a missing field keeps the default for that scope. For example, `path_btw = list(user = FALSE)` skips user-level context for a single chat. A scalar path now uses only that file, no longer combining it with user-level context; pass `list(project = "path")` to keep the user-level context. The new `btw.client.path_btw_user` option sets the global default for the user scope (#226). -* `path_find_in_project()` now stops its upward search when it reaches the home directory (either `fs::path_home()` or `fs::path_home_r()`, which differ on Windows) and never treats files there as project files, so `~/AGENTS.md` or `~/CLAUDE.md` are no longer picked up as project context when the working directory is under `~` but outside a project. Home-level files are handled only by the user-level lookup. This also fixes `~/btw.md` instructions being included twice — once as the project file and once as the user-level config (#225). +* `path_find_in_project()` now stops its upward search at the home directory (`fs::path_home()` or `fs::path_home_r()`, which differ on Windows), so files like `~/AGENTS.md` or `~/CLAUDE.md` are no longer picked up as project context when the working directory is under `~` but outside a project. Home-level files are used only by the user-level lookup, which also fixes `~/btw.md` instructions being included twice — once as the project file and once as the user-level config (#225). * `btw_client(path_btw = ...)` no longer duplicates instructions when `path_btw` is set explicitly to the same file as the user-level `btw.md` (#225). From badbd8975212dcc7d45f17fc9495083eff703c00 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Tue, 6 Oct 2026 11:02:15 -0400 Subject: [PATCH 5/7] docs: drop non-feature note from NEWS A user-level CLAUDE.md not being used is covered in ?btw_client and ?btw-config; NEWS needn't mention a non-feature. --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 65ab5e95..e61cf9ec 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,6 @@ # btw (development version) -* `btw_client()` and `btw_app()` now fall back to a user-level `AGENTS.md` in the cross-tool `~/.agents/` directory when no user-level `btw.md` exists, mirroring the project-level precedence `btw.md` > `AGENTS.md`. A user-level `CLAUDE.md` is never used. `edit_btw_md()` and `use_btw_md()` are aware of the new fallback, e.g. `use_btw_md("user")` asks before creating a `btw.md` that would take priority over an existing `AGENTS.md` (#226). +* `btw_client()` and `btw_app()` now fall back to a user-level `AGENTS.md` in the cross-tool `~/.agents/` directory when no user-level `btw.md` exists, mirroring the project-level precedence `btw.md` > `AGENTS.md`. `edit_btw_md()` and `use_btw_md()` are aware of the new fallback, e.g. `use_btw_md("user")` asks before creating a `btw.md` that would take priority over an existing `AGENTS.md` (#226). * The `path_btw` argument of `btw_client()` and `btw_app()` now also accepts a named list with `project` and/or `user` fields, each `TRUE` (search the default locations), `FALSE` (skip that scope), or a path to a specific file; a missing field keeps the default for that scope. For example, `path_btw = list(user = FALSE)` skips user-level context for a single chat. A scalar path now uses only that file, no longer combining it with user-level context; pass `list(project = "path")` to keep the user-level context. The new `btw.client.path_btw_user` option sets the global default for the user scope (#226). From 8e74db75d98a4f29c14a16017ef26ec7eb4b39a9 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Tue, 6 Oct 2026 11:10:06 -0400 Subject: [PATCH 6/7] feat: add btw.client.path_btw_project option Generalize the user-scope option default into btw_path_scope_default() so the project scope has a matching option. Both options are documented in ?btw_client and set the defaults for the corresponding path_btw scope. Also mention the user-level CLAUDE.md exclusion only in ?btw-config, dropping it from the vignette, ?use_btw_md, and ?btw_client where it appeared without context. --- NEWS.md | 2 +- R/btw_client.R | 71 ++++++++++++++++------------- R/edit_btw_md.R | 4 +- man/btw_client.Rd | 20 ++++---- man/use_btw_md.Rd | 4 +- tests/testthat/_snaps/btw_client.md | 11 ++++- tests/testthat/test-btw_client.R | 29 ++++++++++++ vignettes/btw-md.Rmd | 2 +- 8 files changed, 97 insertions(+), 46 deletions(-) diff --git a/NEWS.md b/NEWS.md index e61cf9ec..13f29aeb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,7 +2,7 @@ * `btw_client()` and `btw_app()` now fall back to a user-level `AGENTS.md` in the cross-tool `~/.agents/` directory when no user-level `btw.md` exists, mirroring the project-level precedence `btw.md` > `AGENTS.md`. `edit_btw_md()` and `use_btw_md()` are aware of the new fallback, e.g. `use_btw_md("user")` asks before creating a `btw.md` that would take priority over an existing `AGENTS.md` (#226). -* The `path_btw` argument of `btw_client()` and `btw_app()` now also accepts a named list with `project` and/or `user` fields, each `TRUE` (search the default locations), `FALSE` (skip that scope), or a path to a specific file; a missing field keeps the default for that scope. For example, `path_btw = list(user = FALSE)` skips user-level context for a single chat. A scalar path now uses only that file, no longer combining it with user-level context; pass `list(project = "path")` to keep the user-level context. The new `btw.client.path_btw_user` option sets the global default for the user scope (#226). +* The `path_btw` argument of `btw_client()` and `btw_app()` now also accepts a named list with `project` and/or `user` fields, each `TRUE` (search the default locations), `FALSE` (skip that scope), or a path to a specific file; a missing field keeps the default for that scope. For example, `path_btw = list(user = FALSE)` skips user-level context for a single chat. A scalar path now uses only that file, no longer combining it with user-level context; pass `list(project = "path")` to keep the user-level context. The new `btw.client.path_btw_project` and `btw.client.path_btw_user` options set the global defaults for each scope (#226). * `path_find_in_project()` now stops its upward search at the home directory (`fs::path_home()` or `fs::path_home_r()`, which differ on Windows), so files like `~/AGENTS.md` or `~/CLAUDE.md` are no longer picked up as project context when the working directory is under `~` but outside a project. Home-level files are used only by the user-level lookup, which also fixes `~/btw.md` instructions being included twice — once as the project file and once as the user-level config (#225). diff --git a/R/btw_client.R b/R/btw_client.R index 8b7fc3c7..0db666eb 100644 --- a/R/btw_client.R +++ b/R/btw_client.R @@ -28,8 +28,8 @@ #' is `~/btw.md` if present, otherwise `~/.btw/btw.md`, `~/.config/btw/btw.md`, #' or `tools::R_user_dir("btw")`, in that order. If no user-level `btw.md` #' exists anywhere, btw falls back to a user-level `AGENTS.md` in the -#' cross-tool `~/.agents/` directory; a user-level `CLAUDE.md` is never used. -#' See `?btw-config` for the complete list of user-level locations. +#' cross-tool `~/.agents/` directory. See `?btw-config` for the complete list +#' of user-level locations. #' Project-level btw tool options under #' the `options` key are merged with user-level options, with project-level #' options taking precedence. @@ -49,8 +49,11 @@ #' * `btw.tools`: The btw tools to include by default when starting a new #' btw chat, see [btw_tools()] for details.` #' -#' One additional option is consulted when resolving context files: +#' Two additional options are consulted when resolving context files: #' +#' * `btw.client.path_btw_project`: `TRUE` (default) to search the default +#' project locations, `FALSE` to skip them, or a path to a specific project +#' context file. This is the default for the `project` field of `path_btw`. #' * `btw.client.path_btw_user`: `TRUE` (default) to include user-level context #' in new chats, `FALSE` to skip it, or a path to a specific user-level #' context file. This is the default for the `user` field of `path_btw`. @@ -117,17 +120,17 @@ #' control project-level and user-level context independently. Each field #' can be `TRUE` (search the default locations), `FALSE` (skip that scope), #' or a path to a specific file; a missing (or `NULL`) field keeps the -#' default for that scope: `project` searches the project locations, and -#' `user` follows the `btw.client.path_btw_user` option described below. +#' default for that scope, set by the `btw.client.path_btw_project` and +#' `btw.client.path_btw_user` options described below. #' If `NULL`, btw will find a #' project-specific context file by walking up the parents of the current #' working directory, preferring `btw.md`, then `AGENTS.md`, then #' `CLAUDE.md`, and always combines it with the user-level context file: #' the highest-priority user-level `btw.md`, or if none exists, a -#' user-level `AGENTS.md` (a user-level `CLAUDE.md` is never used). The -#' default for the `user` scope can be changed globally with the -#' `btw.client.path_btw_user` option, e.g. `options(btw.client.path_btw_user -#' = FALSE)`. A scalar path uses only that file, skipping user-level context +#' user-level `AGENTS.md`. The defaults for each scope can be changed globally +#' with the `btw.client.path_btw_project` and `btw.client.path_btw_user` +#' options, e.g. `options(btw.client.path_btw_user = FALSE)`. A scalar path +#' uses only that file, skipping user-level context #' (equivalent to `list(project = path, user = FALSE)`), and a scalar `FALSE` #' skips all context files. See `?btw-config` for the complete list of #' locations. @@ -579,30 +582,31 @@ maybe_find_in_project <- function(path, file_name, arg = "path") { path } -# Default for the `user` scope of `path_btw` when it isn't specified: an R -# option so users can opt out of (or pin) user-level context globally, e.g. +# Default for a scope of `path_btw` when it isn't specified: an R option so +# users can opt out of (or pin) a scope globally, e.g. # `options(btw.client.path_btw_user = FALSE)` in ~/.Rprofile. -btw_user_path_default <- function() { - user <- getOption("btw.client.path_btw_user", TRUE) - if (isTRUE(user) || isFALSE(user)) { - return(user) - } - check_string(user, arg = "btw.client.path_btw_user") - user +btw_path_scope_default <- function(scope) { + option <- paste0("btw.client.path_btw_", scope) + value <- getOption(option, TRUE) + if (isTRUE(value) || isFALSE(value)) { + return(value) + } + check_string(value, arg = option) + value } # Normalize the `path_btw` argument of btw_client()/btw_app() into a list with # `project` and `user` fields, each TRUE (search the default locations), FALSE # (skip that scope), or a path to a specific file. # -# Scalar values: NULL/TRUE search project and user locations (with the user -# default from `btw.client.path_btw_user`), FALSE skips context entirely, and -# a string uses only that file as the project context, skipping user-level -# context -- a shortcut for list(project = path, user = FALSE). +# Scalar values: NULL/TRUE search project and user locations (each scope's +# default comes from its `btw.client.path_btw_*` option), FALSE skips context +# entirely, and a string uses only that file as the project context, skipping +# user-level context -- a shortcut for list(project = path, user = FALSE). # # A named list controls each scope independently: a missing (or NULL) field -# keeps the default for that scope -- `project` searches project locations, -# and `user` uses the `btw.client.path_btw_user` option default. +# keeps the default for that scope, from the corresponding +# `btw.client.path_btw_*` option. normalize_path_btw <- function(path_btw, arg = "path_btw") { if (is.list(path_btw)) { nms <- names(path_btw) @@ -618,15 +622,23 @@ normalize_path_btw <- function(path_btw, arg = "path_btw") { )) } return(list( - project = normalize_path_btw_field(path_btw$project, TRUE, paste0(arg, "$project")), - user = normalize_path_btw_field(path_btw$user, btw_user_path_default(), paste0(arg, "$user")) + project = normalize_path_btw_field( + path_btw$project, + btw_path_scope_default("project"), + paste0(arg, "$project") + ), + user = normalize_path_btw_field( + path_btw$user, + btw_path_scope_default("user"), + paste0(arg, "$user") + ) )) } if (is.null(path_btw) || isTRUE(path_btw)) { return(list( - project = TRUE, - user = btw_user_path_default() + project = btw_path_scope_default("project"), + user = btw_path_scope_default("user") )) } @@ -704,9 +716,6 @@ read_btw_file <- function(path = NULL) { maybe_find_in_project(paths$project, "btw.md", "path_btw") } - # Locate the user-level context file: the highest-priority user-level - # btw.md, or if none exists anywhere, the highest-priority user-level - # AGENTS.md (a user-level CLAUDE.md is never used) user_path <- if (isTRUE(paths$user)) { path_find_user_context() } else if (isFALSE(paths$user)) { diff --git a/R/edit_btw_md.R b/R/edit_btw_md.R index e8bff1cc..15c08e62 100644 --- a/R/edit_btw_md.R +++ b/R/edit_btw_md.R @@ -174,8 +174,8 @@ #' `tools::R_user_dir("btw")`, in that order (see [btw-config] for the full #' picture, including skills and agents). If no user-level `btw.md` exists #' anywhere, btw falls back to a user-level `AGENTS.md` in the cross-tool -#' `~/.agents/` directory. A user-level `CLAUDE.md` is never used. `use_btw_md("user")` creates new configuration in the -#' recommended `~/.btw/` directory, offers to migrate an existing user-level +#' `~/.agents/` directory. `use_btw_md("user")` creates new configuration in +#' the recommended `~/.btw/` directory, offers to migrate an existing user-level #' configuration found elsewhere, and asks for confirmation when creating a #' `btw.md` would take priority over an existing user-level `AGENTS.md`. When #' more than one user-level context file exists, `edit_btw_md("user")` asks diff --git a/man/btw_client.Rd b/man/btw_client.Rd index faa656e7..e2daf649 100644 --- a/man/btw_client.Rd +++ b/man/btw_client.Rd @@ -53,16 +53,17 @@ context file, or a named list with \code{project} and/or \code{user} fields to control project-level and user-level context independently. Each field can be \code{TRUE} (search the default locations), \code{FALSE} (skip that scope), or a path to a specific file; a missing (or \code{NULL}) field keeps the -default for that scope: \code{project} searches the project locations, and -\code{user} follows the \code{btw.client.path_btw_user} option described below. +default for that scope, set by the \code{btw.client.path_btw_project} and +\code{btw.client.path_btw_user} options described below. If \code{NULL}, btw will find a project-specific context file by walking up the parents of the current working directory, preferring \code{btw.md}, then \code{AGENTS.md}, then \code{CLAUDE.md}, and always combines it with the user-level context file: the highest-priority user-level \code{btw.md}, or if none exists, a -user-level \code{AGENTS.md} (a user-level \code{CLAUDE.md} is never used). The -default for the \code{user} scope can be changed globally with the -\code{btw.client.path_btw_user} option, e.g. \code{options(btw.client.path_btw_user = FALSE)}. A scalar path uses only that file, skipping user-level context +user-level \code{AGENTS.md}. The defaults for each scope can be changed globally +with the \code{btw.client.path_btw_project} and \code{btw.client.path_btw_user} +options, e.g. \code{options(btw.client.path_btw_user = FALSE)}. A scalar path +uses only that file, skipping user-level context (equivalent to \code{list(project = path, user = FALSE)}), and a scalar \code{FALSE} skips all context files. See \code{?btw-config} for the complete list of locations.} @@ -113,8 +114,8 @@ setting, btw will fall back to settings in a user-level \code{btw.md} file. This is \verb{~/btw.md} if present, otherwise \verb{~/.btw/btw.md}, \verb{~/.config/btw/btw.md}, or \code{tools::R_user_dir("btw")}, in that order. If no user-level \code{btw.md} exists anywhere, btw falls back to a user-level \code{AGENTS.md} in the -cross-tool \verb{~/.agents/} directory; a user-level \code{CLAUDE.md} is never used. -See \code{?btw-config} for the complete list of user-level locations. +cross-tool \verb{~/.agents/} directory. See \code{?btw-config} for the complete list +of user-level locations. Project-level btw tool options under the \code{options} key are merged with user-level options, with project-level options taking precedence. @@ -136,8 +137,11 @@ take precedence over settings in a \code{btw.md} file: btw chat, see \code{\link[=btw_tools]{btw_tools()}} for details.` } -One additional option is consulted when resolving context files: +Two additional options are consulted when resolving context files: \itemize{ +\item \code{btw.client.path_btw_project}: \code{TRUE} (default) to search the default +project locations, \code{FALSE} to skip them, or a path to a specific project +context file. This is the default for the \code{project} field of \code{path_btw}. \item \code{btw.client.path_btw_user}: \code{TRUE} (default) to include user-level context in new chats, \code{FALSE} to skip it, or a path to a specific user-level context file. This is the default for the \code{user} field of \code{path_btw}. diff --git a/man/use_btw_md.Rd b/man/use_btw_md.Rd index ae4d79be..830524b0 100644 --- a/man/use_btw_md.Rd +++ b/man/use_btw_md.Rd @@ -219,8 +219,8 @@ looks in \verb{~/.btw/btw.md}, \verb{~/.config/btw/btw.md}, and \code{tools::R_user_dir("btw")}, in that order (see \link{btw-config} for the full picture, including skills and agents). If no user-level \code{btw.md} exists anywhere, btw falls back to a user-level \code{AGENTS.md} in the cross-tool -\verb{~/.agents/} directory. A user-level \code{CLAUDE.md} is never used. \code{use_btw_md("user")} creates new configuration in the -recommended \verb{~/.btw/} directory, offers to migrate an existing user-level +\verb{~/.agents/} directory. \code{use_btw_md("user")} creates new configuration in +the recommended \verb{~/.btw/} directory, offers to migrate an existing user-level configuration found elsewhere, and asks for confirmation when creating a \code{btw.md} would take priority over an existing user-level \code{AGENTS.md}. When more than one user-level context file exists, \code{edit_btw_md("user")} asks diff --git a/tests/testthat/_snaps/btw_client.md b/tests/testthat/_snaps/btw_client.md index 2341abe8..48dda5d6 100644 --- a/tests/testthat/_snaps/btw_client.md +++ b/tests/testthat/_snaps/btw_client.md @@ -270,6 +270,15 @@ Code withr::with_options(list(btw.client.path_btw_user = 1), normalize_path_btw(NULL)) Condition - Error in `btw_user_path_default()`: + Error in `btw_path_scope_default()`: ! `btw.client.path_btw_user` must be a single string, not the number 1. +--- + + Code + withr::with_options(list(btw.client.path_btw_project = 1), normalize_path_btw( + NULL)) + Condition + Error in `btw_path_scope_default()`: + ! `btw.client.path_btw_project` must be a single string, not the number 1. + diff --git a/tests/testthat/test-btw_client.R b/tests/testthat/test-btw_client.R index 1137c17d..2cba6e7c 100644 --- a/tests/testthat/test-btw_client.R +++ b/tests/testthat/test-btw_client.R @@ -608,6 +608,31 @@ describe("normalize_path_btw()", { expect_equal(normalize_path_btw("x.md"), list(project = "x.md", user = FALSE)) }) + it("reads the btw.client.path_btw_project option as the project default", { + withr::local_options(btw.client.path_btw_project = FALSE) + expect_equal(normalize_path_btw(NULL), list(project = FALSE, user = TRUE)) + expect_equal( + normalize_path_btw(list(user = FALSE)), + list(project = FALSE, user = FALSE) + ) + + withr::local_options(btw.client.path_btw_project = "p.md") + expect_equal(normalize_path_btw(NULL), list(project = "p.md", user = TRUE)) + expect_equal( + normalize_path_btw(list(user = FALSE)), + list(project = "p.md", user = FALSE) + ) + + # An explicit project field wins over the option + expect_equal( + normalize_path_btw(list(project = TRUE)), + list(project = TRUE, user = TRUE) + ) + + # A scalar path never consults the option: it uses only that file + expect_equal(normalize_path_btw("x.md"), list(project = "x.md", user = FALSE)) + }) + it("rejects malformed values", { expect_snapshot(error = TRUE, normalize_path_btw(list())) expect_snapshot(error = TRUE, normalize_path_btw(list("x.md"))) @@ -618,6 +643,10 @@ describe("normalize_path_btw()", { error = TRUE, withr::with_options(list(btw.client.path_btw_user = 1), normalize_path_btw(NULL)) ) + expect_snapshot( + error = TRUE, + withr::with_options(list(btw.client.path_btw_project = 1), normalize_path_btw(NULL)) + ) }) }) diff --git a/vignettes/btw-md.Rmd b/vignettes/btw-md.Rmd index 91b183ae..54f50bb1 100644 --- a/vignettes/btw-md.Rmd +++ b/vignettes/btw-md.Rmd @@ -29,7 +29,7 @@ A `btw.md` file plays the same role for btw, and btw falls back to those files w Using `btw.md` gives you a few things those files don't: - **It configures the chat, not just the instructions.** The front matter chooses the model, tools, and options. btw ignores front matter in `CLAUDE.md`, and the other tools that read `AGENTS.md` don't expect front matter there. -- **It has a user-level version.** btw combines a `btw.md` in your home directory, such as `~/.btw/btw.md`, with each project's file. If there's no user-level `btw.md`, btw falls back to a user-level `AGENTS.md` in the cross-tool `~/.agents/` directory. A user-level `CLAUDE.md` is never used. +- **It has a user-level version.** btw combines a `btw.md` in your home directory, such as `~/.btw/btw.md`, with each project's file. If there's no user-level `btw.md`, btw falls back to a user-level `AGENTS.md` in the cross-tool `~/.agents/` directory. - **It keeps btw-specific instructions separate.** Instructions about btw's tools or your R workflow don't end up in the files that other coding agents read. In this vignette, we'll set up a chat by hand, save that setup in a `btw.md` file, and then learn how btw decides which file to use. From eb7867d33f06a7a0d300a0d4c59e48d55e42e60e Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Tue, 6 Oct 2026 11:18:19 -0400 Subject: [PATCH 7/7] docs: mention path_btw scope options in ?btw-config --- R/btw-config.R | 4 +++- man/btw-config.Rd | 4 +++- man/btw_client.Rd | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/R/btw-config.R b/R/btw-config.R index b32adde3..a885c6a1 100644 --- a/R/btw-config.R +++ b/R/btw-config.R @@ -44,7 +44,9 @@ #' User-level context is combined with the project-level context file by #' default; use `path_btw = list(user = FALSE)` in [btw_client()] or #' [btw_app()] to skip it for a single chat, or set the -#' `btw.client.path_btw_user` option to `FALSE` to change the default. +#' `btw.client.path_btw_user` option to `FALSE` to change the default. The +#' `project` scope of `path_btw` has a matching +#' `btw.client.path_btw_project` option. #' #' On Windows, R's notion of your home directory (`fs::path_home_r()`, #' typically your `Documents` folder) can differ from your user profile diff --git a/man/btw-config.Rd b/man/btw-config.Rd index 5aa98a72..93676703 100644 --- a/man/btw-config.Rd +++ b/man/btw-config.Rd @@ -49,7 +49,9 @@ user-level \code{CLAUDE.md} is never used. User-level context is combined with the project-level context file by default; use \code{path_btw = list(user = FALSE)} in \code{\link[=btw_client]{btw_client()}} or \code{\link[=btw_app]{btw_app()}} to skip it for a single chat, or set the -\code{btw.client.path_btw_user} option to \code{FALSE} to change the default. +\code{btw.client.path_btw_user} option to \code{FALSE} to change the default. The +\code{project} scope of \code{path_btw} has a matching +\code{btw.client.path_btw_project} option. On Windows, R's notion of your home directory (\code{fs::path_home_r()}, typically your \code{Documents} folder) can differ from your user profile diff --git a/man/btw_client.Rd b/man/btw_client.Rd index e2daf649..e4d2ce91 100644 --- a/man/btw_client.Rd +++ b/man/btw_client.Rd @@ -235,7 +235,9 @@ user-level \code{CLAUDE.md} is never used. User-level context is combined with the project-level context file by default; use \code{path_btw = list(user = FALSE)} in \code{\link[=btw_client]{btw_client()}} or \code{\link[=btw_app]{btw_app()}} to skip it for a single chat, or set the -\code{btw.client.path_btw_user} option to \code{FALSE} to change the default. +\code{btw.client.path_btw_user} option to \code{FALSE} to change the default. The +\code{project} scope of \code{path_btw} has a matching +\code{btw.client.path_btw_project} option. On Windows, R's notion of your home directory (\code{fs::path_home_r()}, typically your \code{Documents} folder) can differ from your user profile