From e411a4c21e7a0b78ffba33e15741f9123d2c6245 Mon Sep 17 00:00:00 2001 From: Antonio Mancini Date: Sat, 10 Oct 2026 22:15:00 +0200 Subject: [PATCH] feat(agents): share AGENTS.md with Hermes and import it into an existing CLAUDE.md Hermes only loads ~/.hermes/SOUL.md in every session, so the services module adds a managed pointer block there (never creating the file, since a SOUL.md replaces Hermes' default identity), and doctor checks it. An existing ~/.claude/CLAUDE.md now gets a one-line @import of the shared file instead of a full copy, so there is one source. --- CHANGELOG.md | 4 ++++ modules/agents.sh | 19 ++++++++++++++++--- modules/services.sh | 19 +++++++++++++++++++ tests/core.bats | 5 +++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 771ada4..59708bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,12 @@ All notable changes to this project are documented here. The format follows user, hostname, timezone, git identity), then shows the updated plan. Answers are applied like `--set` and saved to the config file. Runs with `--yes`, `--dry-run` or without a terminal never ask. +- Hermes Agent gets the shared `AGENTS.md` too: `~/.hermes/SOUL.md` (the one file Hermes + loads in every session) gets a managed block pointing at it, and `agbox doctor` checks it. ### Changed +- An existing `~/.claude/CLAUDE.md` gets a one-line import of the shared `AGENTS.md` instead + of a full copy, so edits to the shared file reach Claude too. - The `devtools` line in the plan only lists the GitHub CLI and Playwright when they are enabled. diff --git a/modules/agents.sh b/modules/agents.sh index f74e95b..abdddb9 100644 --- a/modules/agents.sh +++ b/modules/agents.sh @@ -131,6 +131,7 @@ toml_set_top() { } # One instruction file, linked into each agent's global location. +# Claude imports it when the user already has a CLAUDE.md, so there is still one copy. _agents_instructions() { local home canonical home=$(target_home) @@ -138,26 +139,38 @@ _agents_instructions() { canonical="$home/$AGENTS_MD_REL" render_agents_md | ensure_block user "$canonical" 0644 '' - if _has_agent claude; then _link_instructions "$home/.claude/CLAUDE.md" "$canonical"; fi + if _has_agent claude; then _link_instructions "$home/.claude/CLAUDE.md" "$canonical" "@$canonical"; fi if _has_agent codex; then _link_instructions "$home/.codex/AGENTS.md" "$canonical"; fi if _has_agent opencode; then _link_instructions "$home/.config/opencode/AGENTS.md" "$canonical"; fi } +# _link_instructions LINK CANONICAL [BODY] : symlink LINK to CANONICAL. If the user has their +# own file there, add a managed block instead: BODY (e.g. an import line), or a full copy. _link_instructions() { - local link=$1 canonical=$2 current + local link=$1 canonical=$2 body=${3:-} current if as_root test -L "$link"; then current=$(as_root readlink "$link") [[ $current == "$canonical" ]] && return 0 fi if as_root test -e "$link" && ! as_root test -L "$link"; then # The user has their own file: add our block to it instead of replacing it. - render_agents_md | ensure_block user "$link" 0644 '' + if [[ -n $body ]]; then + printf '%s\n' "$body" | ensure_block user "$link" 0644 '' + else + render_agents_md | ensure_block user "$link" 0644 '' + fi return 0 fi run_user mkdir -p "$(dirname "$link")" run_user ln -sfn "$canonical" "$link" } +# instructions_pointer PATH -> a paragraph for agents that only load a global identity +# file (Hermes' SOUL.md), sending them to the shared AGENTS.md. Pure, unit-tested. +instructions_pointer() { + printf '%s\n' "This machine's conventions (toolchains, services, ports, where projects live) are in $1. Read it before installing software, starting services, or changing anything outside the current project." +} + render_agents_md() { local projects sudo_line tools docker_line mode_line projects=$(expand_home "$AGBOX_PROJECTS_DIR") diff --git a/modules/services.sh b/modules/services.sh index dd38087..5854f2d 100644 --- a/modules/services.sh +++ b/modules/services.sh @@ -43,11 +43,27 @@ _services_hermes() { log_info "installing Hermes Agent (official install script)" run_script user https://hermes-agent.nousresearch.com/install.sh bash fi + if [[ $AGBOX_AGENT_INSTRUCTIONS == true ]]; then _services_hermes_instructions; fi if ! _user_unit_exists 'hermes-gateway.service'; then add_next_step "Configure Hermes: run 'hermes' (provider + model), then 'hermes gateway setup' and 'hermes gateway install'" fi } +# Hermes reads AGENTS.md only from its working directory; the one file it loads in every +# session is ~/.hermes/SOUL.md, so that gets a pointer to the shared AGENTS.md. Hermes seeds +# SOUL.md itself (a SOUL.md replaces its default identity), so never create it. +_services_hermes_instructions() { + local home soul + home=$(target_home) + [[ -n $home ]] || { log_info "would point ~/.hermes/SOUL.md at ~/$AGENTS_MD_REL"; return 0; } + soul="$home/.hermes/SOUL.md" + if ! as_root test -f "$soul"; then + log_skip "no ~/.hermes/SOUL.md yet (re-run after the first 'hermes' run to link AGENTS.md)" + return 0 + fi + instructions_pointer "$home/$AGENTS_MD_REL" | ensure_block user "$soul" 0600 '' +} + services::check() { if _has_service t3code; then if _user_unit_exists 't3*.service'; then @@ -66,5 +82,8 @@ services::check() { else check_warn "hermes installed, gateway not set up" "hermes gateway setup && hermes gateway install" fi + if user_has hermes && [[ $AGBOX_AGENT_INSTRUCTIONS == true ]]; then + check as_root grep -qF "$AGENTS_MD_REL" "$(target_home)/.hermes/SOUL.md" -- "Hermes SOUL.md points at the shared AGENTS.md" "agbox setup --only services" + fi fi } diff --git a/tests/core.bats b/tests/core.bats index 6e87a50..0d6aec4 100644 --- a/tests/core.bats +++ b/tests/core.bats @@ -96,6 +96,11 @@ setup() { [[ "$output" == *"Projects live in /home/agbox-test-nobody/dev"* ]] } +@test "instructions_pointer names the shared AGENTS.md" { + run instructions_pointer /home/me/.config/agents/AGENTS.md + [[ "$output" == *"are in /home/me/.config/agents/AGENTS.md. Read it before"* ]] +} + @test "dry-run helpers print instead of executing" { AGBOX_DRY_RUN=true local marker="$BATS_TEST_TMPDIR/should-not-exist"