Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
79a1043
implement Claude Skills
simonpcouch Dec 18, 2025
b2ccb6b
only snapshot skills prompt once
simonpcouch Dec 18, 2025
95792da
Merge commit 'fb9ad8132bb9479928df6a6ae3f4b9ffdf0d711f'
gadenbuie Feb 15, 2026
8d4af4d
refactor(skills): switch from yaml to frontmatter package for SKILL.m…
gadenbuie Feb 15, 2026
8920cf7
feat(skills): add spec-compliant validation and enhanced metadata
gadenbuie Feb 15, 2026
aecf068
feat(skills): add btw_skill_create(), btw_skill_validate(), btw_skill…
gadenbuie Feb 15, 2026
b5af0b3
test(skills): comprehensive tests for skill validation, discovery, an…
gadenbuie Feb 15, 2026
ec47042
feat(skills): discover project skills from .btw/, .agents/, and .claude/
gadenbuie Feb 16, 2026
8aed34b
feat(skills): add <location> to system prompt, exclude skills from MCP
gadenbuie Feb 16, 2026
c362a5a
docs(skills): clarify script execution limitation in system prompt
gadenbuie Feb 16, 2026
50231d6
feat(skills): replace btw_skill_install() with GitHub and package sou…
gadenbuie Feb 16, 2026
c7cfcdb
fix(skills): harden validation, escaping, and install ergonomics
gadenbuie Feb 17, 2026
0897edf
fix(skills): harden validation, escaping, and install ergonomics
gadenbuie Feb 17, 2026
39a11a6
tests: Refactor into helpers, use frontmatter
gadenbuie Feb 17, 2026
12ce4a5
feat: skill icon
gadenbuie Feb 17, 2026
0f0605e
remove feature tracking doc from repo
gadenbuie Feb 17, 2026
5651faa
feat(skills): parse `owner/repo@ref` in btw_skill_install_github()
gadenbuie Feb 17, 2026
5968e60
refactor(skills): two-tier validation, improved error messages, and h…
gadenbuie Feb 17, 2026
aff5e74
refactor(skills): code review fixes
gadenbuie Feb 17, 2026
5baec0f
tests(skills): normalize paths in Windows-failing assertions
gadenbuie Feb 17, 2026
fc0339a
Merged origin/main into feat/skills
gadenbuie Feb 17, 2026
82c1b9d
feat(skills): prompt interactively when overwrite conflict exists
gadenbuie Feb 17, 2026
94a845d
feat(skills): warn when skills tool enabled without read file tool
gadenbuie Feb 18, 2026
fc72d10
feat: make toast a warning
gadenbuie Feb 18, 2026
4acbbb7
feat: adjust toast style
gadenbuie Feb 18, 2026
a39b43d
app: list skills first
gadenbuie Feb 18, 2026
04232e0
chore: remove unintended new line
gadenbuie Feb 18, 2026
a4a57f6
feat(skills): rename btw_tool_fetch_skill to btw_tool_skill
gadenbuie Feb 19, 2026
55f45a4
refactor(skills): remove btw_skill_create() and btw_skill_validate()
gadenbuie Feb 20, 2026
000c72f
feat(skills): add btw_task_create_skill() interactive task
gadenbuie Feb 20, 2026
fc3f407
Merge remote-tracking branch 'origin/main' into feat/skills
gadenbuie Feb 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ Collate:
'mcp.R'
'task_create_btw_md.R'
'task_create_readme.R'
'task_create_skill.R'
'tool-result.R'
'tool-agent-subagent.R'
'tool-agent-custom.R'
Expand All @@ -121,6 +122,7 @@ Collate:
'tool-run.R'
'tool-session-package-installed.R'
'tool-sessioninfo.R'
'tool-skills.R'
'tool-web.R'
'tools.R'
'utils-ellmer.R'
Expand Down
4 changes: 4 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ export(btw_app)
export(btw_client)
export(btw_mcp_server)
export(btw_mcp_session)
export(btw_skill_install_github)
export(btw_skill_install_package)
export(btw_task)
export(btw_task_create_btw_md)
export(btw_task_create_readme)
export(btw_task_create_skill)
export(btw_this)
export(btw_tool_agent_subagent)
export(btw_tool_cran_package)
Expand Down Expand Up @@ -73,6 +76,7 @@ export(btw_tool_session_platform_info)
export(btw_tool_sessioninfo_is_package_installed)
export(btw_tool_sessioninfo_package)
export(btw_tool_sessioninfo_platform)
export(btw_tool_skill)
export(btw_tool_web_read_url)
export(btw_tools)
export(edit_btw_md)
Expand Down
19 changes: 19 additions & 0 deletions R/btw_client.R
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,16 @@ btw_client <- function(
llms_txt <- read_llms_txt(path_llms_txt)
project_context <- c(llms_txt, config$btw_system_prompt)
project_context <- paste(project_context, collapse = "\n\n")
skills_prompt <- btw_skills_system_prompt()

sys_prompt <- c(
btw_prompt("btw-system_session.md"),
if (!skip_tools) {
btw_prompt("btw-system_tools.md")
},
if (nzchar(skills_prompt)) {
skills_prompt
},
if (nzchar(project_context)) {
btw_prompt("btw-system_project.md")
},
Expand All @@ -170,6 +174,8 @@ btw_client <- function(
client$set_tools(tools = c(client$get_tools(), config$tools))
}

warn_skills_without_read_file(client$get_tools())

client
}

Expand Down Expand Up @@ -661,3 +667,16 @@ remove_hidden_content <- function(text) {

paste(lines[starts - shift(cumsum(ends)) <= 0], collapse = "\n")
}

warn_skills_without_read_file <- function(tools) {
tool_names <- names(tools)
has_skills <- "btw_tool_skill" %in% tool_names
has_read_file <- "btw_tool_files_read" %in% tool_names
if (has_skills && !has_read_file) {
cli::cli_warn(c(
"The {.fn btw_tool_skill} tool is enabled but {.fn btw_tool_files_read} is not.",
"i" = "Skills work best with the read file tool, which lets the model read skill resource files.",
"i" = "Add {.code btw_tools(\"files\")} or enable {.fn btw_tool_files_read} to get full skill support."
))
}
}
121 changes: 80 additions & 41 deletions R/btw_client_app.R
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,43 @@ btw_app_from_client <- function(
}
})

skills_read_file_mismatch <- shiny::reactive({
sel <- selected_tools()
"btw_tool_skill" %in% sel && !"btw_tool_files_read" %in% sel
})

shiny::observeEvent(skills_read_file_mismatch(), {
if (isTRUE(skills_read_file_mismatch())) {
notifier(
id = "skills_read_file_mismatch",
shiny::icon("triangle-exclamation", class = "text-warning"),
shiny::tagList(
shiny::HTML(
"The <strong>Load Skill tool</strong> works best with the <strong>Read File tool</strong> enabled"
),
shiny::actionButton(
class = "btn-sm mt-2",
"enable_read_file_tool",
"Enable Read File Tool"
)
)
)
}
})

shiny::observeEvent(input$enable_read_file_tool, {
file_tools <- c(input$tools_files, "btw_tool_files_read")
shiny::updateCheckboxGroupInput(
session = session,
inputId = "tools_files",
selected = file_tools
)
bslib_hide_toast <- asNamespace("bslib")[["hide_toast"]]
if (!is.null(bslib_hide_toast)) {
bslib_hide_toast("skills_read_file_mismatch")
}
})

output$ui_other_tools <- shiny::renderUI({
if (length(other_tools) == 0) {
return(NULL)
Expand Down Expand Up @@ -392,6 +429,47 @@ btw_app_from_client <- function(

# Status Bar ----

notifier <- function(icon, action, error = NULL, ...) {
error_body <- if (!is.null(error)) {
shiny::p(shiny::HTML(sprintf("<code>%s</code>", error$message)))
}

bslib_toast <- asNamespace("bslib")[["toast"]]
bslib_show_toast <- asNamespace("bslib")[["show_toast"]]
bslib_toast_header <- asNamespace("bslib")[["toast_header"]]

if (is.null(bslib_toast) || is.null(bslib_show_toast)) {
if (!is.null(error)) {
body <- shiny::span(icon, action)
} else {
body <- shiny::tagList(
shiny::p(
shiny::icon("warning"),
"Failed to update system prompt",
class = "fw-bold"
),
error_body
)
}
shiny::showNotification(
body,
type = if (is.null(error)) "message" else "error"
)
return()
}

toast <- bslib_toast(
if (is.null(error)) action else error_body,
header = if (!is.null(error)) {
bslib_toast_header(action, icon = icon)
},
icon = if (is.null(error)) icon,
position = "top-right",
...
)
bslib_show_toast(toast)
}

btw_status_bar_ui <- function(id, provider_model) {
ns <- shiny::NS(id)
shiny::tagList(
Expand Down Expand Up @@ -575,46 +653,6 @@ btw_status_bar_server <- function(id, chat) {
shiny::showModal(modal)
})

notifier <- function(icon, action, error = NULL) {
error_body <- if (!is.null(error)) {
shiny::p(shiny::HTML(sprintf("<code>%s</code>", error$message)))
}

bslib_toast <- asNamespace("bslib")[["toast"]]
bslib_show_toast <- asNamespace("bslib")[["show_toast"]]
bslib_toast_header <- asNamespace("bslib")[["toast_header"]]

if (is.null(bslib_toast) || is.null(bslib_show_toast)) {
if (!is.null(error)) {
body <- shiny::span(icon, action)
} else {
body <- shiny::tagList(
shiny::p(
shiny::icon("warning"),
"Failed to update system prompt",
class = "fw-bold"
),
error_body
)
}
shiny::showNotification(
body,
type = if (is.null(error)) "message" else "error"
)
return()
}

toast <- bslib_toast(
if (is.null(error)) action else error_body,
header = if (!is.null(error)) {
bslib_toast_header(action, icon = icon)
},
icon = if (is.null(error)) icon,
position = "top-right"
)
bslib_show_toast(toast)
}

shiny::observeEvent(
input$system_prompt,
ignoreInit = TRUE,
Expand Down Expand Up @@ -691,7 +729,7 @@ app_tool_group_inputs <- function(tools_df, initial_tool_names = NULL) {

# then other, then deprecated (if shown)
group_names <- names(tools_df)
priority_groups <- c("agent", "docs", "files", "env")
priority_groups <- c("agent", "skills", "docs", "files", "env")
trailing_groups <- c("other", "deprecated")
priority_present <- intersect(priority_groups, group_names)
middle_groups <- sort(setdiff(
Expand Down Expand Up @@ -737,6 +775,7 @@ app_tool_group_choice_input <- function(
"pkg" = shiny::span(label_icon, "Package Tools"),
"run" = shiny::span(label_icon, "Run Code"),
"sessioninfo" = shiny::span(label_icon, "Session Info"),
"skills" = shiny::span(label_icon, "Skills"),
"web" = shiny::span(label_icon, "Web Tools"),
"other" = shiny::span(label_icon, "Other Tools"),
to_title_case(group)
Expand Down
15 changes: 14 additions & 1 deletion R/mcp.R
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
#'
#' @name mcp
#' @export
btw_mcp_server <- function(tools = btw_tools()) {
btw_mcp_server <- function(tools = btw_mcp_tools()) {
# If given a path to an R script, we'll pass it on to mcp_server()
is_likely_r_file <-
is.character(tools) &&
Expand All @@ -172,3 +172,16 @@ btw_mcp_server <- function(tools = btw_tools()) {
btw_mcp_session <- function() {
mcptools::mcp_session()
}

btw_mcp_tools <- function() {
# Skills are excluded from MCP by default: the skill system prompt
# (with <available_skills> metadata) is injected by btw_client(), not by
# MCP. Without that context the model has no way to know which skills are
# available. Filesystem-based agents (e.g. Claude Code) can read SKILL.md
# files directly via their <location> paths in the system prompt.
all_tools <- btw_tools()
Filter(
function(tool) !identical(tool@annotations$btw_group, "skills"),
all_tools
)
}
Loading
Loading