feat(agent): isolate agent cwd in .ideai/run/<id> to kill convention-file collisions
ARCHITECTURE §14.1: an agent's PTY cwd is now its own
`<project_root>/.ideai/run/<agent-id>/` directory, never the project root, so
N agents of the same profile no longer collide on a single conventional file
(CLAUDE.md/AGENTS.md/...).
- profile: cwd_template is now "{agentRunDir}" (built-in catalogue + docs).
- runtime: resolve_cwd substitutes {agentRunDir} (legacy {projectRoot} kept).
- LaunchAgent: computes + creates the run dir via FileSystem::create_dir_all,
passes it as the cwd base to the pure prepare_invocation. Contract chosen:
pass run_dir as the `cwd` argument (no PreparedContext change) — keeps
prepare_invocation pure, I/O stays in the use case.
- convention file is generated by IdeA inside the run dir via a pure
compose_convention_file(project_root, agent_md): absolute project-root header
+ agent persona (extensible for skills, §14.2).
- .gitignore: ignore .ideai/run/.
- run-dir cleanup left as a TODO (FileSystem port exposes no delete).
Tests: anti-collision (2 agents -> 2 distinct cwd, 2 distinct convention files,
none at root), run-dir creation order, composed convention file; pure unit
tests for agent_run_dir + compose_convention_file; runtime {agentRunDir}
substitution. cargo test --workspace + clippy -D warnings green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -80,16 +80,23 @@ impl CliAgentRuntime {
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolves the profile's `cwd_template` against the project root.
|
||||
/// Resolves the profile's `cwd_template` against the supplied `base` cwd.
|
||||
///
|
||||
/// The only recognised placeholder is `{projectRoot}` (CONTEXT §9). An empty
|
||||
/// template defaults to the project root itself.
|
||||
fn resolve_cwd(profile: &AgentProfile, root: &ProjectPath) -> Result<ProjectPath, RuntimeError> {
|
||||
/// The base is the agent's **run directory** (`.ideai/run/<agent-id>/`),
|
||||
/// already computed (and created) by `LaunchAgent` and passed as the `cwd`
|
||||
/// argument of [`prepare_invocation`](AgentRuntime::prepare_invocation). The
|
||||
/// recognised placeholder is `{agentRunDir}` (ARCHITECTURE §14.1); the legacy
|
||||
/// `{projectRoot}` is still substituted with the same base for backwards
|
||||
/// compatibility (the caller always passes the run dir now). An empty template
|
||||
/// defaults to the base itself.
|
||||
fn resolve_cwd(profile: &AgentProfile, base: &ProjectPath) -> Result<ProjectPath, RuntimeError> {
|
||||
let template = profile.cwd_template.trim();
|
||||
if template.is_empty() {
|
||||
return Ok(root.clone());
|
||||
return Ok(base.clone());
|
||||
}
|
||||
let resolved = template.replace("{projectRoot}", root.as_str());
|
||||
let resolved = template
|
||||
.replace("{agentRunDir}", base.as_str())
|
||||
.replace("{projectRoot}", base.as_str());
|
||||
ProjectPath::new(resolved).map_err(|e| RuntimeError::Invocation(e.to_string()))
|
||||
}
|
||||
|
||||
|
||||
@ -217,15 +217,27 @@ fn prepare_substitutes_project_root_in_cwd_template() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_empty_cwd_template_defaults_to_root() {
|
||||
fn prepare_empty_cwd_template_defaults_to_base() {
|
||||
let rt = pure_runtime();
|
||||
let p = profile(ContextInjection::stdin(), "");
|
||||
let root = ProjectPath::new("/home/me/proj").unwrap();
|
||||
let base = ProjectPath::new("/home/me/proj").unwrap();
|
||||
|
||||
let spec = rt.prepare_invocation(&p, &ctx(), &root).unwrap();
|
||||
let spec = rt.prepare_invocation(&p, &ctx(), &base).unwrap();
|
||||
assert_eq!(spec.cwd.as_str(), "/home/me/proj");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_substitutes_agent_run_dir_in_cwd_template() {
|
||||
// The canonical template (ARCHITECTURE §14.1): `{agentRunDir}` resolves to the
|
||||
// base cwd the launcher passes — the agent's isolated run directory.
|
||||
let rt = pure_runtime();
|
||||
let p = profile(ContextInjection::convention_file("CLAUDE.md").unwrap(), "{agentRunDir}");
|
||||
let run_dir = ProjectPath::new("/home/me/proj/.ideai/run/agent-1").unwrap();
|
||||
|
||||
let spec = rt.prepare_invocation(&p, &ctx(), &run_dir).unwrap();
|
||||
assert_eq!(spec.cwd.as_str(), "/home/me/proj/.ideai/run/agent-1");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// detection_spec (pure)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user