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
|
|
@ -24,6 +24,7 @@ pub fn render(
|
|||
operator_pronouns: &str,
|
||||
hive_name: Option<&str>,
|
||||
swarm_name: Option<&str>,
|
||||
docs_dir: Option<&str>,
|
||||
) -> String {
|
||||
let body = filter_role_blocks(template, "agent");
|
||||
let qualified = crate::identity::qualify(label);
|
||||
|
|
@ -33,11 +34,25 @@ pub fn render(
|
|||
let swarm_identity = swarm_name
|
||||
.filter(|n| !n.is_empty())
|
||||
.map_or(String::new(), |n| format!(" in swarm `{n}`"));
|
||||
body.replace("{label}", label)
|
||||
let rendered = body
|
||||
.replace("{label}", label)
|
||||
.replace("{qualified_label}", &qualified)
|
||||
.replace("{operator_pronouns}", operator_pronouns)
|
||||
.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,
|
||||
|
|
@ -124,12 +139,18 @@ pub async fn write_system_prompt(_socket: &Path, label: &str) -> Result<PathBuf>
|
|||
// verbatim (single-hive deployments see no diff).
|
||||
let hive_name = crate::identity::hive_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(
|
||||
&template,
|
||||
label,
|
||||
&pronouns,
|
||||
hive_name.as_deref(),
|
||||
swarm_name.as_deref(),
|
||||
docs_dir.as_deref(),
|
||||
);
|
||||
let path = parent.join("claude-system-prompt.md");
|
||||
tokio::fs::write(&path, body).await?;
|
||||
|
|
@ -269,7 +290,7 @@ shared closer
|
|||
// Real template's first agent line — keeps the renderer
|
||||
// honest about the {label} / {operator_pronouns} pair the
|
||||
// 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("**they/them** pronouns"));
|
||||
assert!(!rendered.contains("{label}"));
|
||||
|
|
@ -279,7 +300,7 @@ shared closer
|
|||
#[test]
|
||||
fn render_no_role_markers_in_output() {
|
||||
// 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:"));
|
||||
// Shared tools appear.
|
||||
|
|
@ -289,7 +310,7 @@ shared closer
|
|||
|
||||
#[test]
|
||||
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"));
|
||||
}
|
||||
|
||||
|
|
@ -308,7 +329,14 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
|
||||
#[test]
|
||||
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}");
|
||||
// swarm clause stays absent when only hive is set.
|
||||
assert!(!rendered.contains("in swarm"));
|
||||
|
|
@ -325,6 +353,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
"she/her",
|
||||
None,
|
||||
Some("constellat1on"),
|
||||
None,
|
||||
);
|
||||
assert!(rendered.contains("in swarm `constellat1on`"));
|
||||
assert!(!rendered.contains("on hive"));
|
||||
|
|
@ -338,6 +367,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
"she/her",
|
||||
Some("pr1ma"),
|
||||
Some("constellat1on"),
|
||||
None,
|
||||
);
|
||||
// Order: hive then swarm, both inline before "in a multi-agent
|
||||
// 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() {
|
||||
// None / None must round-trip the non-identity opener verbatim
|
||||
// — 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("in swarm"));
|
||||
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
|
||||
// filters empty), but `render` should still no-op on a
|
||||
// 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("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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue