Point agents at /agents/<name>/work and default subagents to it
The system prompt now names the work dir for worktrees, clones and build trees, and keeps state/ for notes. A subagent started without a dir runs in the agent's work dir instead of the daemon's own cwd.
This commit is contained in:
parent
db8f144a11
commit
043c9445f6
7 changed files with 32 additions and 12 deletions
|
|
@ -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
|
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
|
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`).
|
says which directory it searched, which is usually the fix (pass `dir`).
|
||||||
|
`dir` defaults to your work dir, `/agents/<you>/work`, which has no
|
||||||
|
`CLAUDE.md` above it, so a subagent started there does not load yours.
|
||||||
Use `status` for a
|
Use `status` for a
|
||||||
zero-cost "is it still going" check — for a running subagent it also
|
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
|
reports how long since that turn last produced output, so a few seconds
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ claude the bare name — see below.
|
||||||
|
|
||||||
Because the remembered directory goes with the rest of it, a `continue`
|
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
|
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.
|
situation you reach for `continue` in most often.
|
||||||
|
|
||||||
## Roles
|
## 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
|
resolve a bare title across every worktree of the repo and could pick, or
|
||||||
refuse, the wrong session. A session started in another directory
|
refuse, the wrong session. A session started in another directory
|
||||||
needs that directory passed as `dir` — and after a daemon restart, which
|
needs that directory passed as `dir` — and after a daemon restart, which
|
||||||
forgets the remembered one, a session outside the daemon's own working
|
forgets the remembered one, a session outside the agent's work dir
|
||||||
directory needs it again. A store location the daemon can't resolve
|
needs it again. A store location the daemon can't resolve
|
||||||
refuses the same way, with the io error.
|
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
|
A goal run's next turn does the same lookup; a miss there ends the run as
|
||||||
|
|
|
||||||
|
|
@ -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.
|
- 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.
|
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.
|
**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.
|
||||||
|
|
|
||||||
|
|
@ -264,6 +264,12 @@ shared closer
|
||||||
assert!(rendered.starts_with("You are hyperhive agent"));
|
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}
|
// Inline fixture for the {hive_identity} / {swarm_identity}
|
||||||
// placeholders. Cargo's `cargo test` resolves `PRODUCTION_TEMPLATE`
|
// placeholders. Cargo's `cargo test` resolves `PRODUCTION_TEMPLATE`
|
||||||
// against `$HIVE_ASSETS_DIR/prompts/system.md`, which the flake
|
// against `$HIVE_ASSETS_DIR/prompts/system.md`, which the flake
|
||||||
|
|
|
||||||
|
|
@ -79,8 +79,9 @@ struct StartArgs {
|
||||||
trigger: String,
|
trigger: String,
|
||||||
/// Working directory for the subagent's session — e.g. a git worktree
|
/// Working directory for the subagent's session — e.g. a git worktree
|
||||||
/// you've already prepared for it, so a parallel batch of subagents
|
/// you've already prepared for it, so a parallel batch of subagents
|
||||||
/// never race on the same working tree. Must exist. Omit to inherit
|
/// never race on the same working tree. Must exist. Omit to use your
|
||||||
/// this daemon's own working directory (today's default). The daemon
|
/// work dir, `/agents/<you>/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
|
/// remembers whichever `dir` you give here against `name`, so a later
|
||||||
/// `continue`/`status` for the same name doesn't need to repeat it —
|
/// `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*
|
/// 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
|
/// is exactly when you're most likely to be reaching for `continue`. So
|
||||||
/// pass it when pointing the session at a *different* directory than
|
/// pass it when pointing the session at a *different* directory than
|
||||||
/// last time, and pass it again after a restart if the session lives
|
/// 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.
|
/// session in the directory searched is refused, naming it.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
dir: Option<String>,
|
dir: Option<String>,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,15 @@ pub fn state_dir() -> PathBuf {
|
||||||
PathBuf::from(format!("/agents/{label}/state"))
|
PathBuf::from(format!("/agents/{label}/state"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The agent's persistent work dir, a sibling of [`state_dir`]
|
||||||
|
/// (`/agents/<name>/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
|
/// 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
|
/// 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
|
/// hive-wide: the clauses a role carries came out of one agent's own prompt
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
//! session, found again by name. `continue` is how a caller reattaches to
|
//! 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
|
//! 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
|
//! 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
|
//! **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
|
//! 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
|
/// skill already asks of `model`. `prompt_file`, when given, becomes
|
||||||
/// `--append-system-prompt-file` — the subagent's task instructions.
|
/// `--append-system-prompt-file` — the subagent's task instructions.
|
||||||
/// `dir`, when given, becomes `Config::cwd` (e.g. a worktree the caller
|
/// `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
|
/// Always `--dangerously-skip-permissions --strict-mcp-config --tools` — two
|
||||||
/// gates covering one half each, neither substituting for the other:
|
/// gates covering one half each, neither substituting for the other:
|
||||||
/// `strict_mcp_config` over **MCP** tools (no ambient discovery, so exactly
|
/// `strict_mcp_config` over **MCP** tools (no ambient discovery, so exactly
|
||||||
|
|
@ -1239,7 +1239,7 @@ fn build_config(
|
||||||
Config {
|
Config {
|
||||||
model,
|
model,
|
||||||
effort: Some(effort.unwrap_or_else(|| "medium".to_owned())),
|
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),
|
mcp_config: crate::mcp_config::build(name, signal_url),
|
||||||
strict_mcp_config: true,
|
strict_mcp_config: true,
|
||||||
extra_args,
|
extra_args,
|
||||||
|
|
@ -1387,7 +1387,7 @@ pub struct StartRequest {
|
||||||
pub role: Option<String>,
|
pub role: Option<String>,
|
||||||
/// The first turn's prompt. A goal, when given, is appended to it.
|
/// The first turn's prompt. A goal, when given, is appended to it.
|
||||||
pub trigger: String,
|
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<String>,
|
pub dir: Option<String>,
|
||||||
/// What this session is being continued *toward*. `None` keeps the
|
/// What this session is being continued *toward*. `None` keeps the
|
||||||
/// pre-continuation shape: one turn, one todo, no re-prompting.
|
/// pre-continuation shape: one turn, one todo, no re-prompting.
|
||||||
|
|
@ -3345,12 +3345,12 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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);
|
let with = build_config("n", None, None, None, Some("/tmp/some-worktree"), None);
|
||||||
assert_eq!(with.cwd, Some(PathBuf::from("/tmp/some-worktree")));
|
assert_eq!(with.cwd, Some(PathBuf::from("/tmp/some-worktree")));
|
||||||
|
|
||||||
let without = build_config("n", None, None, None, None, None);
|
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]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue