diff --git a/claude-plugins/plugins/base/skills/claude-subagents/SKILL.md b/claude-plugins/plugins/base/skills/claude-subagents/SKILL.md index d7f37b3d..c329de04 100644 --- a/claude-plugins/plugins/base/skills/claude-subagents/SKILL.md +++ b/claude-plugins/plugins/base/skills/claude-subagents/SKILL.md @@ -36,6 +36,8 @@ other producer. `continue` takes the extra moment to confirm the resume actually attached, so a `continue` naming a session that isn't there fails the tool call outright instead of looking like it worked; the error says which directory it searched, which is usually the fix (pass `dir`). +`dir` defaults to your work dir, `/agents//work`, which has no +`CLAUDE.md` above it, so a subagent started there does not load yours. Use `status` for a zero-cost "is it still going" check — for a running subagent it also reports how long since that turn last produced output, so a few seconds diff --git a/docs/tools/subagent.md b/docs/tools/subagent.md index b0832325..79160f73 100644 --- a/docs/tools/subagent.md +++ b/docs/tools/subagent.md @@ -76,7 +76,7 @@ claude the bare name — see below. Because the remembered directory goes with the rest of it, a `continue` after a restart has to re-supply `dir` when the session lives anywhere -other than the daemon's own working directory — and a restart is the +other than the agent's work dir (the default) — and a restart is the situation you reach for `continue` in most often. ## Roles @@ -237,8 +237,8 @@ A name is never searched for in any other directory, because claude would resolve a bare title across every worktree of the repo and could pick, or refuse, the wrong session. A session started in another directory needs that directory passed as `dir` — and after a daemon restart, which -forgets the remembered one, a session outside the daemon's own working -directory needs it again. A store location the daemon can't resolve +forgets the remembered one, a session outside the agent's work dir +needs it again. A store location the daemon can't resolve refuses the same way, with the io error. A goal run's next turn does the same lookup; a miss there ends the run as diff --git a/hive-agent/prompts/system.md b/hive-agent/prompts/system.md index 6167fe6f..39655fcc 100644 --- a/hive-agent/prompts/system.md +++ b/hive-agent/prompts/system.md @@ -22,6 +22,8 @@ Durable knowledge: - write to `/agents/{label}/state/notes.md` (free-form) or any other path under `/agents/{label}/state/`. That directory is bind-mounted from the host and persists across container destroy/recreate — claude's `--continue` session only carries short-term context, but `/agents/{label}/state/` is forever. Read it back at the start of relevant turns to remember things across resets. +**Work dir**: `/agents/{label}/work/` is the place for git worktrees, clones, build trees and other bulky scratch. It is bind-mounted from the host, persists across container destroy/recreate and is backed up. Keep `state/` for notes and small durable files. Don't keep work under `/home`, `/tmp` or other container paths — they are outside what's backed up. Start subagents with their `dir` under `work/` (that is also the default): a claude started under `state/` walks up into `state/CLAUDE.md` and takes on your identity, one under `work/` doesn't. + Claude session (OAuth credentials) lives at `/root/.claude/` and persists across restarts. **Shared space**: `/shared` is accessible to all agents (read/write). Only put things here you're willing to lose — other agents may delete them. Use for explicit cross-agent communication or shared artifacts when appropriate. diff --git a/hive-agent/src/prompt.rs b/hive-agent/src/prompt.rs index b1d2edd4..0d7efb35 100644 --- a/hive-agent/src/prompt.rs +++ b/hive-agent/src/prompt.rs @@ -264,6 +264,12 @@ shared closer assert!(rendered.starts_with("You are hyperhive agent")); } + #[test] + fn render_points_agents_at_the_work_dir() { + let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None, None); + assert!(rendered.contains("`/agents/alice/work/`"), "{rendered}"); + } + // Inline fixture for the {hive_identity} / {swarm_identity} // placeholders. Cargo's `cargo test` resolves `PRODUCTION_TEMPLATE` // against `$HIVE_ASSETS_DIR/prompts/system.md`, which the flake diff --git a/hive-subagent-mcp/src/mcp.rs b/hive-subagent-mcp/src/mcp.rs index 6d24e0c5..71c89124 100644 --- a/hive-subagent-mcp/src/mcp.rs +++ b/hive-subagent-mcp/src/mcp.rs @@ -79,8 +79,9 @@ struct StartArgs { trigger: String, /// Working directory for the subagent's session — e.g. a git worktree /// you've already prepared for it, so a parallel batch of subagents - /// never race on the same working tree. Must exist. Omit to inherit - /// this daemon's own working directory (today's default). The daemon + /// never race on the same working tree. Must exist. Omit to use your + /// work dir, `/agents//work` (it has no `CLAUDE.md` above it, so the + /// subagent does not load yours). The daemon /// remembers whichever `dir` you give here against `name`, so a later /// `continue`/`status` for the same name doesn't need to repeat it — /// only pass it there again if you want to point at a *different* @@ -164,7 +165,7 @@ struct ContinueArgs { /// is exactly when you're most likely to be reaching for `continue`. So /// pass it when pointing the session at a *different* directory than /// last time, and pass it again after a restart if the session lives - /// anywhere other than the daemon's own working directory. A name with no + /// anywhere other than your work dir. A name with no /// session in the directory searched is refused, naming it. #[serde(default)] dir: Option, diff --git a/hive-subagent-mcp/src/paths.rs b/hive-subagent-mcp/src/paths.rs index 57ef5d71..fbc3e1f0 100644 --- a/hive-subagent-mcp/src/paths.rs +++ b/hive-subagent-mcp/src/paths.rs @@ -30,6 +30,15 @@ pub fn state_dir() -> PathBuf { PathBuf::from(format!("/agents/{label}/state")) } +/// The agent's persistent work dir, a sibling of [`state_dir`] +/// (`/agents//work`). It has no `CLAUDE.md` above it, so a subagent +/// started here does not load the agent's own. This is the working +/// directory a subagent gets when `start` is given no `dir`. +#[must_use] +pub fn work_dir() -> PathBuf { + state_dir().with_file_name("work") +} + /// Where this agent's named subagent role files live, one markdown file per /// role under the agent's own [`state_dir`]. Per-agent rather than /// hive-wide: the clauses a role carries came out of one agent's own prompt diff --git a/hive-subagent-mcp/src/session.rs b/hive-subagent-mcp/src/session.rs index 33e38989..da604f7d 100644 --- a/hive-subagent-mcp/src/session.rs +++ b/hive-subagent-mcp/src/session.rs @@ -15,7 +15,7 @@ //! session, found again by name. `continue` is how a caller reattaches to //! it, whether that's "give it a new turn" or "the daemon restarted and I //! want to pick this back up" — after a restart, re-supply `dir` once if -//! the session isn't in the daemon's own default directory. +//! the session isn't in the agent's work dir (the default). //! //! **A running turn is not necessarily a working turn.** A wedged child //! satisfies "running" as fully as a busy one, so every line of the child's @@ -1185,7 +1185,7 @@ fn subagent_otel_attrs(name: &str) -> String { /// skill already asks of `model`. `prompt_file`, when given, becomes /// `--append-system-prompt-file` — the subagent's task instructions. /// `dir`, when given, becomes `Config::cwd` (e.g. a worktree the caller -/// already prepared); `None` inherits this daemon's own working directory. +/// already prepared); `None` falls back to [`crate::paths::work_dir`]. /// Always `--dangerously-skip-permissions --strict-mcp-config --tools` — two /// gates covering one half each, neither substituting for the other: /// `strict_mcp_config` over **MCP** tools (no ambient discovery, so exactly @@ -1239,7 +1239,7 @@ fn build_config( Config { model, effort: Some(effort.unwrap_or_else(|| "medium".to_owned())), - cwd: dir.map(PathBuf::from), + cwd: Some(dir.map_or_else(crate::paths::work_dir, PathBuf::from)), mcp_config: crate::mcp_config::build(name, signal_url), strict_mcp_config: true, extra_args, @@ -1387,7 +1387,7 @@ pub struct StartRequest { pub role: Option, /// The first turn's prompt. A goal, when given, is appended to it. pub trigger: String, - /// Working directory for the session; `None` inherits the daemon's. + /// Working directory for the session; `None` means the agent's work dir. pub dir: Option, /// What this session is being continued *toward*. `None` keeps the /// pre-continuation shape: one turn, one todo, no re-prompting. @@ -3345,12 +3345,12 @@ mod tests { } #[test] - fn build_config_sets_cwd_only_when_a_dir_is_given() { + fn build_config_defaults_cwd_to_the_work_dir_when_no_dir_is_given() { let with = build_config("n", None, None, None, Some("/tmp/some-worktree"), None); assert_eq!(with.cwd, Some(PathBuf::from("/tmp/some-worktree"))); let without = build_config("n", None, None, None, None, None); - assert_eq!(without.cwd, None); + assert_eq!(without.cwd, Some(crate::paths::work_dir())); } #[test]