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; }