feat(#2089): docs pointer via system-prompt line, drop CLAUDE.md mechanism
This commit is contained in:
parent
fc415a72a5
commit
a26bbb15fc
3 changed files with 86 additions and 62 deletions
|
|
@ -1,19 +0,0 @@
|
||||||
# hyperhive reference docs
|
|
||||||
|
|
||||||
The hyperhive reference docs are mounted read-only in this directory
|
|
||||||
(the harness exposes the path as `$HIVE_DOCS_DIR`). They are the
|
|
||||||
canonical reference for how this swarm actually works - the turn loop,
|
|
||||||
the tool surface, persistence, conventions, runbooks, and the
|
|
||||||
approval/CI/forge/matrix workflows.
|
|
||||||
|
|
||||||
- On a fresh deploy, or any time you are unsure how a hyperhive
|
|
||||||
mechanism behaves, read `setup.md` first, then the topic file for the
|
|
||||||
area you are touching (e.g. `turn-loop.md`, `persistence.md`,
|
|
||||||
`tools/`, `conventions.md`, `gotchas.md`).
|
|
||||||
- Prefer these docs over guessing: they describe the live system, not a
|
|
||||||
generic one.
|
|
||||||
|
|
||||||
This pointer is additive. It does not replace your own memory or project
|
|
||||||
instructions - your `~/.claude/CLAUDE.md` and any harness/project memory
|
|
||||||
still apply in full. Nothing here overrides what you already know about
|
|
||||||
yourself; it only tells you where the shared reference material lives.
|
|
||||||
|
|
@ -24,6 +24,7 @@ pub fn render(
|
||||||
operator_pronouns: &str,
|
operator_pronouns: &str,
|
||||||
hive_name: Option<&str>,
|
hive_name: Option<&str>,
|
||||||
swarm_name: Option<&str>,
|
swarm_name: Option<&str>,
|
||||||
|
docs_dir: Option<&str>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let body = filter_role_blocks(template, "agent");
|
let body = filter_role_blocks(template, "agent");
|
||||||
let qualified = crate::identity::qualify(label);
|
let qualified = crate::identity::qualify(label);
|
||||||
|
|
@ -33,11 +34,25 @@ pub fn render(
|
||||||
let swarm_identity = swarm_name
|
let swarm_identity = swarm_name
|
||||||
.filter(|n| !n.is_empty())
|
.filter(|n| !n.is_empty())
|
||||||
.map_or(String::new(), |n| format!(" in swarm `{n}`"));
|
.map_or(String::new(), |n| format!(" in swarm `{n}`"));
|
||||||
body.replace("{label}", label)
|
let rendered = body
|
||||||
|
.replace("{label}", label)
|
||||||
.replace("{qualified_label}", &qualified)
|
.replace("{qualified_label}", &qualified)
|
||||||
.replace("{operator_pronouns}", operator_pronouns)
|
.replace("{operator_pronouns}", operator_pronouns)
|
||||||
.replace("{hive_identity}", &hive_identity)
|
.replace("{hive_identity}", &hive_identity)
|
||||||
.replace("{swarm_identity}", &swarm_identity)
|
.replace("{swarm_identity}", &swarm_identity);
|
||||||
|
// When the reference docs are mounted in-container (`hyperhive.docs.enable`
|
||||||
|
// wires `HIVE_DOCS_DIR` + `claude --add-dir`), append a single pointer
|
||||||
|
// sentence so the agent knows they exist. Additive: it doesn't replace the
|
||||||
|
// agent's own memory/project instructions. Absent env → no change.
|
||||||
|
match docs_dir.filter(|d| !d.is_empty()) {
|
||||||
|
Some(dir) => format!(
|
||||||
|
"{rendered}\n\nThe hyperhive reference docs (the repo `docs/` tree \
|
||||||
|
describing this live system) are mounted read-only at `{dir}` — read \
|
||||||
|
them (start at the index, then the topic file for the area you're \
|
||||||
|
touching) rather than guessing.\n"
|
||||||
|
),
|
||||||
|
None => rendered,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Walk `template` line-by-line. Inside a `<!-- role:X -->` block,
|
/// Walk `template` line-by-line. Inside a `<!-- role:X -->` block,
|
||||||
|
|
@ -124,12 +139,18 @@ pub async fn write_system_prompt(_socket: &Path, label: &str) -> Result<PathBuf>
|
||||||
// verbatim (single-hive deployments see no diff).
|
// verbatim (single-hive deployments see no diff).
|
||||||
let hive_name = crate::identity::hive_name();
|
let hive_name = crate::identity::hive_name();
|
||||||
let swarm_name = crate::identity::swarm_name();
|
let swarm_name = crate::identity::swarm_name();
|
||||||
|
// `hyperhive.docs.enable` sets HIVE_DOCS_DIR (and the harness passes it to
|
||||||
|
// claude via `--add-dir`); when present, render() appends a pointer line.
|
||||||
|
let docs_dir = std::env::var("HIVE_DOCS_DIR")
|
||||||
|
.ok()
|
||||||
|
.filter(|d| !d.is_empty());
|
||||||
let body = render(
|
let body = render(
|
||||||
&template,
|
&template,
|
||||||
label,
|
label,
|
||||||
&pronouns,
|
&pronouns,
|
||||||
hive_name.as_deref(),
|
hive_name.as_deref(),
|
||||||
swarm_name.as_deref(),
|
swarm_name.as_deref(),
|
||||||
|
docs_dir.as_deref(),
|
||||||
);
|
);
|
||||||
let path = parent.join("claude-system-prompt.md");
|
let path = parent.join("claude-system-prompt.md");
|
||||||
tokio::fs::write(&path, body).await?;
|
tokio::fs::write(&path, body).await?;
|
||||||
|
|
@ -269,7 +290,7 @@ shared closer
|
||||||
// Real template's first agent line — keeps the renderer
|
// Real template's first agent line — keeps the renderer
|
||||||
// honest about the {label} / {operator_pronouns} pair the
|
// honest about the {label} / {operator_pronouns} pair the
|
||||||
// harness already relied on.
|
// harness already relied on.
|
||||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "they/them", None, None);
|
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "they/them", None, None, None);
|
||||||
assert!(rendered.contains("hyperhive agent `alice`"));
|
assert!(rendered.contains("hyperhive agent `alice`"));
|
||||||
assert!(rendered.contains("**they/them** pronouns"));
|
assert!(rendered.contains("**they/them** pronouns"));
|
||||||
assert!(!rendered.contains("{label}"));
|
assert!(!rendered.contains("{label}"));
|
||||||
|
|
@ -279,7 +300,7 @@ shared closer
|
||||||
#[test]
|
#[test]
|
||||||
fn render_no_role_markers_in_output() {
|
fn render_no_role_markers_in_output() {
|
||||||
// No raw role markers should survive into the rendered prompt.
|
// No raw role markers should survive into the rendered prompt.
|
||||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None);
|
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None, None);
|
||||||
assert!(!rendered.contains("<!-- role:"));
|
assert!(!rendered.contains("<!-- role:"));
|
||||||
assert!(!rendered.contains("<!-- /role:"));
|
assert!(!rendered.contains("<!-- /role:"));
|
||||||
// Shared tools appear.
|
// Shared tools appear.
|
||||||
|
|
@ -289,7 +310,7 @@ shared closer
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_uses_agent_opener() {
|
fn render_uses_agent_opener() {
|
||||||
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None);
|
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None, None);
|
||||||
assert!(rendered.starts_with("You are hyperhive agent"));
|
assert!(rendered.starts_with("You are hyperhive agent"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,7 +329,14 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_substitutes_hive_identity_when_set() {
|
fn render_substitutes_hive_identity_when_set() {
|
||||||
let rendered = render(IDENTITY_FIXTURE, "alice", "she/her", Some("pr1ma"), None);
|
let rendered = render(
|
||||||
|
IDENTITY_FIXTURE,
|
||||||
|
"alice",
|
||||||
|
"she/her",
|
||||||
|
Some("pr1ma"),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
assert!(rendered.contains("on hive `pr1ma`"), "{rendered}");
|
assert!(rendered.contains("on hive `pr1ma`"), "{rendered}");
|
||||||
// swarm clause stays absent when only hive is set.
|
// swarm clause stays absent when only hive is set.
|
||||||
assert!(!rendered.contains("in swarm"));
|
assert!(!rendered.contains("in swarm"));
|
||||||
|
|
@ -325,6 +353,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
||||||
"she/her",
|
"she/her",
|
||||||
None,
|
None,
|
||||||
Some("constellat1on"),
|
Some("constellat1on"),
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
assert!(rendered.contains("in swarm `constellat1on`"));
|
assert!(rendered.contains("in swarm `constellat1on`"));
|
||||||
assert!(!rendered.contains("on hive"));
|
assert!(!rendered.contains("on hive"));
|
||||||
|
|
@ -338,6 +367,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
||||||
"she/her",
|
"she/her",
|
||||||
Some("pr1ma"),
|
Some("pr1ma"),
|
||||||
Some("constellat1on"),
|
Some("constellat1on"),
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
// Order: hive then swarm, both inline before "in a multi-agent
|
// Order: hive then swarm, both inline before "in a multi-agent
|
||||||
// system" — keeps the opener grammar intact.
|
// system" — keeps the opener grammar intact.
|
||||||
|
|
@ -348,7 +378,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
||||||
fn render_omits_identity_when_unset() {
|
fn render_omits_identity_when_unset() {
|
||||||
// None / None must round-trip the non-identity opener verbatim
|
// None / None must round-trip the non-identity opener verbatim
|
||||||
// — single-hive deployments see zero diff.
|
// — single-hive deployments see zero diff.
|
||||||
let rendered = render(IDENTITY_FIXTURE, "alice", "she/her", None, None);
|
let rendered = render(IDENTITY_FIXTURE, "alice", "she/her", None, None, None);
|
||||||
assert!(!rendered.contains("on hive"));
|
assert!(!rendered.contains("on hive"));
|
||||||
assert!(!rendered.contains("in swarm"));
|
assert!(!rendered.contains("in swarm"));
|
||||||
assert!(!rendered.contains("{hive_identity}"));
|
assert!(!rendered.contains("{hive_identity}"));
|
||||||
|
|
@ -361,8 +391,44 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
||||||
// through `identity::hive_name()` as None (the accessor
|
// through `identity::hive_name()` as None (the accessor
|
||||||
// filters empty), but `render` should still no-op on a
|
// filters empty), but `render` should still no-op on a
|
||||||
// direct `Some("")` from a test fixture or a future caller.
|
// direct `Some("")` from a test fixture or a future caller.
|
||||||
let rendered = render(IDENTITY_FIXTURE, "alice", "she/her", Some(""), Some(""));
|
let rendered = render(
|
||||||
|
IDENTITY_FIXTURE,
|
||||||
|
"alice",
|
||||||
|
"she/her",
|
||||||
|
Some(""),
|
||||||
|
Some(""),
|
||||||
|
None,
|
||||||
|
);
|
||||||
assert!(!rendered.contains("on hive"));
|
assert!(!rendered.contains("on hive"));
|
||||||
assert!(!rendered.contains("in swarm"));
|
assert!(!rendered.contains("in swarm"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_appends_docs_pointer_when_docs_dir_set() {
|
||||||
|
let rendered = render(
|
||||||
|
&PRODUCTION_TEMPLATE,
|
||||||
|
"alice",
|
||||||
|
"she/her",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some("/run/hive-docs"),
|
||||||
|
);
|
||||||
|
// Key on a phrase unique to the pointer sentence — "reference docs"
|
||||||
|
// alone also appears in the /knowledge blurb of the base template.
|
||||||
|
assert!(
|
||||||
|
rendered.contains("mounted read-only at `/run/hive-docs`"),
|
||||||
|
"expected docs pointer sentence with the dir:\n{rendered}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_no_docs_pointer_when_docs_dir_absent_or_empty() {
|
||||||
|
for docs in [None, Some("")] {
|
||||||
|
let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None, docs);
|
||||||
|
assert!(
|
||||||
|
!rendered.contains("mounted read-only at"),
|
||||||
|
"docs pointer must not appear for {docs:?}:\n{rendered}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,24 +100,6 @@ let
|
||||||
iconPng = pkgs.runCommand "hive-agent-icon.png" { nativeBuildInputs = [ pkgs.librsvg ]; } ''
|
iconPng = pkgs.runCommand "hive-agent-icon.png" { nativeBuildInputs = [ pkgs.librsvg ]; } ''
|
||||||
rsvg-convert -f png -w 512 -h 512 ${config.hyperhive.icon} -o $out
|
rsvg-convert -f png -w 512 -h 512 ${config.hyperhive.icon} -o $out
|
||||||
'';
|
'';
|
||||||
# hyperhive.docs.enable: the reference-docs tree (from
|
|
||||||
# `config.hyperhive.docs.source` — the narrow `hyperhive-docs` meta-flake
|
|
||||||
# input in a hive deployment, or `pkgs.hyperhive-docs` for standalone
|
|
||||||
# builds) with a generic `CLAUDE.md` pointer dropped at the root. The
|
|
||||||
# harness passes this dir to claude via `--add-dir`; paired with
|
|
||||||
# `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1` claude loads
|
|
||||||
# `<dir>/CLAUDE.md` ADDITIVELY (alongside, never replacing, the agent's
|
|
||||||
# own `~/.claude/CLAUDE.md`). The pointer prose lives in
|
|
||||||
# `hive-ag3nt/prompts/docs-pointer.md` (prompts belong in prompts/, not
|
|
||||||
# nix/) and is shipped via `hyperhive-assets`. The docs source itself
|
|
||||||
# stays pure (no CLAUDE.md) so the website can reuse the same tree.
|
|
||||||
# Lazy: only forced when `hyperhive.docs.enable` references it below.
|
|
||||||
agentDocs = pkgs.runCommand "hyperhive-agent-docs" { } ''
|
|
||||||
mkdir -p $out
|
|
||||||
cp -r ${config.hyperhive.docs.source}/. $out/
|
|
||||||
chmod -R u+w $out
|
|
||||||
cp ${pkgs.hyperhive-assets}/share/hyperhive/prompts/docs-pointer.md $out/CLAUDE.md
|
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Shared scaffolding for every hyperhive harness container.
|
# Shared scaffolding for every hyperhive harness container.
|
||||||
|
|
@ -263,12 +245,11 @@ in
|
||||||
read-only as the standalone `hyperhive-docs` derivation) available
|
read-only as the standalone `hyperhive-docs` derivation) available
|
||||||
in-container. When enabled the harness exposes the docs dir to claude
|
in-container. When enabled the harness exposes the docs dir to claude
|
||||||
via `claude --add-dir`, so the markdown is readable at
|
via `claude --add-dir`, so the markdown is readable at
|
||||||
`$HIVE_DOCS_DIR/`. A generic `CLAUDE.md` pointer is dropped at the dir
|
`$HIVE_DOCS_DIR/`, and appends a single pointer sentence to the agent's
|
||||||
root and loaded additively (via
|
system prompt so it knows the docs exist (see
|
||||||
`CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1`), so the agent knows
|
`hive-ag3nt::prompt::render`). Default-on for the root/manager agent
|
||||||
the docs exist without its own `~/.claude/CLAUDE.md` being replaced.
|
(see `manager.nix`), off elsewhere; any agent can flip it from its
|
||||||
Default-on for the root/manager agent (see `manager.nix`), off
|
`agent.nix`.
|
||||||
elsewhere; any agent can flip it from its `agent.nix`.
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
options.hyperhive.docs.source = lib.mkOption {
|
options.hyperhive.docs.source = lib.mkOption {
|
||||||
|
|
@ -1420,18 +1401,14 @@ in
|
||||||
HIVE_COMPACT_WATERMARK_TOKENS = "0";
|
HIVE_COMPACT_WATERMARK_TOKENS = "0";
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs config.hyperhive.docs.enable {
|
// lib.optionalAttrs config.hyperhive.docs.enable {
|
||||||
# hyperhive.docs.enable: the in-container reference-docs tree plus a
|
# hyperhive.docs.enable: the in-container reference-docs tree. The
|
||||||
# generic `CLAUDE.md` pointer at its root (see `agentDocs` above).
|
# harness reads HIVE_DOCS_DIR and passes it to claude as `--add-dir`
|
||||||
# The harness reads HIVE_DOCS_DIR and passes it to claude as
|
# so the docs are readable, and appends a single pointer sentence to
|
||||||
# `--add-dir` so the docs are readable; with
|
# the system prompt (hive-ag3nt::prompt::render) telling the agent the
|
||||||
# `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1` claude also loads
|
# docs exist. Source is `hyperhive.docs.source` (the narrow
|
||||||
# the pointer additively, telling the agent the docs exist without
|
|
||||||
# clobbering its own memory. See hive-ag3nt::turn. The docs tree
|
|
||||||
# itself comes from `hyperhive.docs.source` (the narrow
|
|
||||||
# `hyperhive-docs` meta-flake input, or `pkgs.hyperhive-docs` for
|
# `hyperhive-docs` meta-flake input, or `pkgs.hyperhive-docs` for
|
||||||
# standalone builds) via `agentDocs`.
|
# standalone builds). See hive-ag3nt::turn.
|
||||||
HIVE_DOCS_DIR = "${agentDocs}";
|
HIVE_DOCS_DIR = "${config.hyperhive.docs.source}";
|
||||||
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD = "1";
|
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs (config.hyperhive._bashEnvFragments != "") {
|
// lib.optionalAttrs (config.hyperhive._bashEnvFragments != "") {
|
||||||
# Non-interactive bash invocations (claude's `Bash` tool runs
|
# Non-interactive bash invocations (claude's `Bash` tool runs
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue