diff --git a/flake.nix b/flake.nix
index d9d3fbee..85b4f623 100644
--- a/flake.nix
+++ b/flake.nix
@@ -212,6 +212,13 @@
# NOT of `packages.default`, so the binary derivation stays
# cached when a prompt edit ripples through.
assets = pkgs.callPackage ./nix/assets.nix { };
+ # The repo docs/ markdown tree as a standalone derivation —
+ # agents read it in-container (added as a claude additional
+ # directory) and the website repo reuses it as a flake input,
+ # neither of which needs the branding/prompt assets. See
+ # nix/reference-docs.nix. (`docs` above is the auto-generated
+ # nix-options reference, a different artifact.)
+ reference-docs = pkgs.callPackage ./nix/reference-docs.nix { };
# Pre-built per-container system closures. Exposed as packages
# so operators can `nix build .#agent-base-toplevel` (or wire
# them into their host system closure via the
@@ -255,6 +262,10 @@
# so the harness module can wire $HIVE_ASSETS_DIR straight
# to `${pkgs.hyperhive-assets}/share/hyperhive`.
hyperhive-assets = self.packages.${prev.stdenv.hostPlatform.system}.assets;
+ # Standalone docs/ tree (see nix/reference-docs.nix). Exposed
+ # via the overlay so the harness module can build the
+ # in-container agent docs dir from it.
+ hyperhive-docs = self.packages.${prev.stdenv.hostPlatform.system}.reference-docs;
};
claude-unstable =
final: prev:
diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs
index be80156b..9f7aed02 100644
--- a/hive-ag3nt/src/turn.rs
+++ b/hive-ag3nt/src/turn.rs
@@ -694,6 +694,19 @@ async fn run_claude(prompt: &str, files: &TurnFiles, bus: &Bus) -> Result<(bool,
.arg(mcp::builtin_tools_arg())
.arg("--allowedTools")
.arg(mcp::allowed_tools_arg());
+ // hyperhive.docs.enable wires HIVE_DOCS_DIR to the in-container
+ // reference-docs dir (the docs/ tree plus a generic `CLAUDE.md`
+ // pointer at its root). Expose it to claude as an additional
+ // directory so the docs are readable, and enable additive CLAUDE.md
+ // loading so claude picks up `
/CLAUDE.md` ALONGSIDE the agent's
+ // own memory files — never clobbering ~/.claude/CLAUDE.md or the
+ // project one. Unset (docs disabled) → neither flag is passed.
+ if let Some(docs_dir) = std::env::var_os("HIVE_DOCS_DIR")
+ && !docs_dir.is_empty()
+ {
+ cmd.arg("--add-dir").arg(&docs_dir);
+ cmd.env("CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD", "1");
+ }
let mut child = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
diff --git a/nix/agent-claude.md b/nix/agent-claude.md
new file mode 100644
index 00000000..11f4547b
--- /dev/null
+++ b/nix/agent-claude.md
@@ -0,0 +1,9 @@
+# Hyperhive reference docs
+
+The hyperhive subsystem docs ship read-only inside this container at
+`$HIVE_DOCS_DIR/docs/` (resolve the env var, e.g.
+`ls "$HIVE_DOCS_DIR/docs"`).
+
+On a fresh deploy, read `$HIVE_DOCS_DIR/docs/setup.md` first for the
+first-run hive bootstrap commands (forge / gateway / matrix
+provisioning, spawning the first sub-agents).
diff --git a/nix/assets.nix b/nix/assets.nix
index e726c0f3..3b2e2d90 100644
--- a/nix/assets.nix
+++ b/nix/assets.nix
@@ -12,28 +12,25 @@
# Output layout:
# $out/share/hyperhive/branding/{hyperhive,agent-configs}.{svg,png}
# $out/share/hyperhive/prompts/{system.md, claude-settings.json}
-# $out/share/hyperhive/docs/ — the repo docs/ tree
+#
+# The repo docs/ tree is a SEPARATE derivation (nix/reference-docs.nix)
+# so agents can consume the docs without the branding+prompt assets and
+# the website repo can reuse it — see that file.
stdenv.mkDerivation {
pname = "hyperhive-assets";
version = "0.1.0";
- # Narrow `srcs` (branding/ + hive-ag3nt/prompts/ + docs/) is what
- # decouples this derivation's input hash from the rest of the tree.
- # Including docs/ surfaces the reference docs in-container at
- # `$HIVE_ASSETS_DIR/docs/` (no host mount, pulled from the nix store);
- # a docs/ edit re-hashes this asset, the accepted tradeoff for shipping
- # the docs declaratively.
+ # Narrow `srcs` (branding/ + hive-ag3nt/prompts/) is what decouples
+ # this derivation's input hash from the rest of the tree.
srcs = [
../branding
../hive-ag3nt/prompts
- ../docs
];
unpackPhase = ''
runHook preUnpack
cp -r ${../branding} branding
cp -r ${../hive-ag3nt/prompts} prompts
- cp -r ${../docs} docs
- chmod -R u+w branding prompts docs
+ chmod -R u+w branding prompts
runHook postUnpack
'';
@@ -54,7 +51,6 @@ stdenv.mkDerivation {
mkdir -p $out/share/hyperhive
cp -r branding $out/share/hyperhive/branding
cp -r prompts $out/share/hyperhive/prompts
- cp -r docs $out/share/hyperhive/docs
runHook postInstall
'';
@@ -62,7 +58,7 @@ stdenv.mkDerivation {
dontFixup = true;
meta = {
- description = "hyperhive static assets (branding + claude prompts + docs)";
+ description = "hyperhive static assets (branding + claude prompts)";
homepage = "https://forge.darkest.space/hyperhive/hyperhive";
license = lib.licenses.mit;
};
diff --git a/nix/reference-docs.nix b/nix/reference-docs.nix
new file mode 100644
index 00000000..f3496f90
--- /dev/null
+++ b/nix/reference-docs.nix
@@ -0,0 +1,43 @@
+{
+ stdenv,
+ lib,
+}:
+
+# The repo `docs/` markdown tree, shipped as a standalone derivation so
+# agents can read the reference docs in-container (added as a claude
+# additional directory by the harness) WITHOUT pulling the branding +
+# prompt assets they don't need, and so the `hyperhive/website` repo can
+# reuse the exact same tree as a flake input.
+#
+# Pure data: the docs are copied verbatim (never transformed), so there
+# is no writable/build step — `$out` is the docs tree as-is.
+#
+# Output layout:
+# $out/ — the repo `docs/` tree verbatim (e.g. `$out/setup.md`)
+
+stdenv.mkDerivation {
+ pname = "hyperhive-docs";
+ version = "0.1.0";
+ # Narrow src (just docs/) keeps this derivation's input hash decoupled
+ # from the rest of the tree — a doc edit only re-hashes this.
+ src = ../docs;
+
+ # No build: pure markdown, nothing to compile or render.
+ dontBuild = true;
+ dontConfigure = true;
+
+ installPhase = ''
+ runHook preInstall
+ mkdir -p $out
+ cp -r ./* $out/
+ runHook postInstall
+ '';
+
+ dontFixup = true;
+
+ meta = {
+ description = "hyperhive reference docs (the repo docs/ tree)";
+ homepage = "https://forge.darkest.space/hyperhive/hyperhive";
+ license = lib.licenses.mit;
+ };
+}
diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix
index 017f3c28..39de4026 100644
--- a/nix/templates/harness-base.nix
+++ b/nix/templates/harness-base.nix
@@ -70,23 +70,22 @@ let
iconPng = pkgs.runCommand "hive-agent-icon.png" { nativeBuildInputs = [ pkgs.librsvg ]; } ''
rsvg-convert -f png -w 512 -h 512 ${config.hyperhive.icon} -o $out
'';
- # Generic, agent-agnostic `~/.claude/CLAUDE.md` installed when
- # `hyperhive.docs.enable` is on (root/manager default-on; see
- # `manager.nix`). claude auto-loads this user-global memory file every
- # session, so it's the discovery hook for the docs asset — no
- # per-agent prompt injection. It references the STABLE `$HIVE_ASSETS_DIR`
- # env var (not the hashed nix-store path) which the agent resolves at
- # read time.
- docsClaudeMd = pkgs.writeText "hive-docs-claude.md" ''
- # Hyperhive reference docs
-
- The hyperhive subsystem docs ship read-only inside this container at
- `$HIVE_ASSETS_DIR/docs/` (resolve the env var, e.g.
- `ls "$HIVE_ASSETS_DIR/docs"`).
-
- On a fresh deploy, read `$HIVE_ASSETS_DIR/docs/setup.md` first for the
- first-run hive bootstrap commands (forge / gateway / matrix
- provisioning, spawning the first sub-agents).
+ # hyperhive.docs.enable: the in-container directory claude is pointed
+ # at for the hyperhive reference docs (root/manager default-on; see
+ # `manager.nix`). Combines the standalone docs tree
+ # (`pkgs.hyperhive-docs`) under `docs/` with a generic, agent-agnostic
+ # `CLAUDE.md` pointer (`nix/agent-claude.md`) at its root. The harness
+ # adds this dir via `--add-dir` and sets
+ # `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1`, so claude loads the
+ # sibling `CLAUDE.md` ADDITIVELY (never replacing the agent's own
+ # `~/.claude/CLAUDE.md` or the project one) and gets read access to
+ # `docs/`. Built as a runCommand (like `iconPng` above) so the tree is
+ # GC-rooted by the system closure — no persistent symlink into the
+ # store to dangle after a content change + GC.
+ agentDocs = pkgs.runCommand "hive-agent-docs" { } ''
+ mkdir -p $out
+ cp ${../agent-claude.md} $out/CLAUDE.md
+ cp -r ${pkgs.hyperhive-docs} $out/docs
'';
in
{
@@ -229,16 +228,16 @@ in
};
options.hyperhive.docs.enable = lib.mkEnableOption ''
- install a generic `~/.claude/CLAUDE.md` pointing this agent at the
- hyperhive reference docs shipped read-only at `$HIVE_ASSETS_DIR/docs/`
- (the repo `docs/` tree, packaged into the assets derivation). claude
- auto-loads the file every session, so the docs are discoverable
- without any per-agent prompt injection. The docs themselves are
- always present at `$HIVE_ASSETS_DIR/docs/`; this option only controls
- the auto-loaded pointer. When enabled the managed symlink clobbers any
- existing `~/.claude/CLAUDE.md`. Default-on for the root/manager agent
- (see `manager.nix`), off elsewhere; any agent can flip it from its
- `agent.nix`.
+ point this agent at the hyperhive reference docs (the repo `docs/`
+ tree, shipped read-only as the standalone `hyperhive-docs`
+ derivation). When enabled the harness adds the in-container docs dir
+ via `claude --add-dir` and turns on additive CLAUDE.md loading
+ (`CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1`), so claude loads a
+ generic pointer `CLAUDE.md` from that dir ALONGSIDE — never replacing
+ — the agent's own `~/.claude/CLAUDE.md` and project memory. The docs
+ are readable at `$HIVE_DOCS_DIR/docs/`. Default-on for the
+ root/manager agent (see `manager.nix`), off elsewhere; any agent can
+ flip it from its `agent.nix`.
'';
options.hyperhive.allowedBashPatterns = lib.mkOption {
@@ -1042,21 +1041,6 @@ in
[ -d "$configDir" ] || continue
chown -hR "$userName:$userName" "$configDir" 2>/dev/null || true
done
- ${lib.optionalString config.hyperhive.docs.enable ''
- # hyperhive.docs.enable: point this agent at the hyperhive docs via
- # a managed `~/.claude/CLAUDE.md` (claude auto-loads it every
- # session). The link lives in the bind-mounted (persistent) home, so
- # it must NOT target a bare `/nix/store` path — that path is only
- # GC-rooted by the current generation, and a content change (new
- # hash) plus `nix-collect-garbage` would leave the persistent link
- # dangling. Instead it targets the STABLE `/etc/hyperhive/claude/
- # CLAUDE.md` path, which the `environment.etc` entry below
- # regenerates declaratively every rebuild (and which holds the
- # docs store-path reference that keeps the content GC-rooted). The
- # chown -h below sets the link's ownership.
- mkdir -p "$homeDir/.claude"
- ln -sfn /etc/hyperhive/claude/CLAUDE.md "$homeDir/.claude/CLAUDE.md"
- ''}
if [ -d "$homeDir/.claude" ]; then
chown -hR "$userName:$userName" "$homeDir/.claude" 2>/dev/null || true
# 0755 so hive-core (a different unix user) can list the dir and
@@ -1097,16 +1081,6 @@ in
environment.etc."hyperhive/extra-mcp.json".text = builtins.toJSON config.hyperhive.extraMcpServers;
- # hyperhive.docs.enable: the managed `~/.claude/CLAUDE.md` content,
- # placed declaratively in /etc so it's regenerated every rebuild and
- # GC-rooted by the system closure. The activation script above symlinks
- # the agent's (bind-mounted, persistent) `~/.claude/CLAUDE.md` at this
- # stable path rather than at the bare `${docsClaudeMd}` store path, so
- # the persistent link can never dangle after a content change + GC.
- environment.etc."hyperhive/claude/CLAUDE.md" = lib.mkIf config.hyperhive.docs.enable {
- source = docsClaudeMd;
- };
-
# Operator-set per-agent icon (hyperhive.icon). When configured, the
# SVG lands at /etc/hyperhive/icon.svg; the harness serves it at
# GET /icon, falling back to the bundled hyperhive logo when absent.
@@ -1295,6 +1269,15 @@ in
# (compact-on-overflow) still fires when the session is truly full.
HIVE_COMPACT_WATERMARK_TOKENS = "0";
}
+ // lib.optionalAttrs config.hyperhive.docs.enable {
+ # hyperhive.docs.enable: the in-container reference-docs dir — the
+ # docs/ tree plus the agent CLAUDE.md pointer (see `agentDocs`
+ # above). The harness reads this and passes it to claude as
+ # `--add-dir` with additive CLAUDE.md loading enabled, so the docs
+ # are discoverable without touching the agent's own memory files.
+ # See hive-ag3nt::turn.
+ HIVE_DOCS_DIR = "${agentDocs}";
+ }
// lib.optionalAttrs (config.hyperhive._bashEnvFragments != "") {
# Non-interactive bash invocations (claude's `Bash` tool runs
# `bash -c`) source $BASH_ENV at startup — drops every active
diff --git a/nix/templates/manager.nix b/nix/templates/manager.nix
index 04fee81d..a64e196c 100644
--- a/nix/templates/manager.nix
+++ b/nix/templates/manager.nix
@@ -6,8 +6,9 @@
imports = [ ./harness-base.nix ];
# The root/manager bootstraps a fresh hive, so it gets the hyperhive
- # reference docs surfaced by default (the `~/.claude/CLAUDE.md` pointer
- # → `$HIVE_ASSETS_DIR/docs/setup.md`). `mkDefault` so a manager's own
- # `agent.nix` can still turn it off. Other agents default off.
+ # reference docs surfaced by default (the additive `CLAUDE.md` pointer
+ # → `$HIVE_DOCS_DIR/docs/setup.md`, added via `claude --add-dir`).
+ # `mkDefault` so a manager's own `agent.nix` can still turn it off.
+ # Other agents default off.
hyperhive.docs.enable = lib.mkDefault true;
}