Skip to content
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Child-agent handoff schema and verify gate (#383, part 1): `tole_core::handoff` defines the typed handoff document (role `explore` / `implement` / `verify` / `fix`, artifact references by path and rev, verification evidence), strict validation of the model-authored input (workspace-relative paths, option-safe refs, bounded fields), the close gate (an implement/fix child without passing verification is `Unverified` / `VerifyFailed`, never closed silently) and the prompt for the follow-up `fix` child. Design and anti-goals (no DAG engine) in `docs/child-agents-handoff.md`; the child tool and the `agent_poll` gate follow.
- Child-agent handoff schema and verify gate (#383): `tole_core::handoff` defines the typed handoff document (role `explore` / `implement` / `verify` / `fix`, artifact references by path and rev, verification evidence), strict validation of the model-authored input (workspace-relative paths, option-safe refs, bounded fields), the close gate (an implement/fix child without passing verification is `Unverified` / `VerifyFailed`, never closed silently) and the prompt for the follow-up `fix` child. `agent_start` takes `role` and `consumes` (artifact references the child reads itself), a role child gets a `handoff_submit` tool, and `agent_poll` reports the gate and a ready-made `fix_prompt` (the parent decides whether to start the fix child; nothing is spawned automatically). Design and anti-goals (no DAG engine) in `docs/child-agents-handoff.md`.
- Sibling change notices, core (#380): `sibling_watch` tracks which files each child agent reads and writes in one shared append-only log (owner-only, bounded) and tells a child, once per stale path, when a sibling changed a file it had read or written; the `Tracked` tool decorator records `read_file` / `write_file` / `edit_file` / `delete_file` on success. Interrupts gain a `source`: a notice is delivered at the next safe point as an attributed entry (`source: notice`) and never withdraws output or supersedes a final answer. Wiring into `tole run` children follows separately.
- Sibling change notices (#380): `sibling_watch` tracks which files each child agent reads and writes in one shared append-only log (owner-only, bounded) and tells a child, once per stale path, when a sibling changed a file it had read or written; the `Tracked` tool decorator records `read_file` / `write_file` / `edit_file` / `delete_file` on success. Interrupts gain a `source`: a notice is delivered at the next safe point as an attributed entry (`source: notice`) and never withdraws output or supersedes a final answer. Children spawned by `agent_start` get `TOLE_AGENT_ID` and a shared `TOLE_AGENT_TOUCH_LOG`; in a child, `tole run` wraps its file tools and runs the watcher, so a sibling's change reaches the model at its next safe point.
- `tole doctor` coverage ledger (#378): every run is recorded per provider + model (level reached, first blocker, fix command, timestamp) in an owner-only `doctor-ledger.json` (atomic write, a corrupt file is an error, never replaced); `--summary` prints `READY n-of-m` + the first blocker and its fix, `--no-ledger` skips recording.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ hard error.
| `verify_package` | RO | crates.io / npm registry check before any install — hallucinated names get NOT FOUND + candidates, edit-distance-1 candidates get a typo-squat warning |
| `load_skill` | RO | loads a discovered SKILL.md on demand (`--skill` pins one upfront; `--no-skills` disables) |
| `gitea` | Write | Gitea counterpart of `gh` over the instance REST API — registers when `origin` is a Gitea remote AND `TOLE_GITEA_TOKEN`/`GITEA_TOKEN` is set; same six ops |
| `agent_start`, `agent_poll` | Write / Write | depth-1 child agents: spawn durable child sessions (structurally no grandchildren), results via per-child uteke mailboxes (ephemeral by default); `agent_poll` is Write because a settled poll consumes (cleans) the mailbox, and `--trust internal` allowlists it by name; `--agents-worktree` gives each child its own git worktree; siblings are told (#380) when another child changes a file they read or wrote, at their next safe point |
| `agent_start`, `agent_poll` | Write / Write | depth-1 child agents: spawn durable child sessions (structurally no grandchildren), results via per-child uteke mailboxes (ephemeral by default); `agent_poll` is Write because a settled poll consumes (cleans) the mailbox, and `--trust internal` allowlists it by name; `--agents-worktree` gives each child its own git worktree; siblings are told (#380) when another child changes a file they read or wrote, at their next safe point; `role` (`explore`/`implement`/`verify`/`fix`) and `consumes` (artifacts by reference) give children a typed handoff, and an implement/fix child without passing verification is not closed (#383) |
| `systemone_decide` | RO | typed decisions (choice/score/noul + confidence) from a System One backend — active when `SYSTEMONE_API_KEY` is set; `SYSTEMONE_BASE_URL` picks the backend (hosted Jev default, self-hosted compatible) |
| `job_start`, `job_poll` | Write / RO | detached long-running jobs with log tailing |
| `cora_search` | RO | hybrid codebase search via `cora brain`; native fallback — skipped when the cora MCP surface is attached |
Expand Down
52 changes: 52 additions & 0 deletions crates/tole-cli/src/handoff.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! Host wiring for the child-agent handoff (#383): a child started with a
//! role finds the role and the handoff path in its environment and gets the
//! `handoff_submit` tool. Outside such a child this is a no-op.

use std::path::PathBuf;
use tole_core::handoff::{HandoffSubmitTool, Role};

pub const ENV_ROLE: &str = "TOLE_AGENT_ROLE";
pub const ENV_HANDOFF: &str = "TOLE_AGENT_HANDOFF";

/// The `handoff_submit` tool for this process, when it is a role child.
/// Both variables must be present and the role must parse; anything else
/// (including a role set without a path) registers nothing.
pub fn tool_from_env(env: &dyn Fn(&str) -> Option<String>) -> Option<HandoffSubmitTool> {
let role = Role::parse(env(ENV_ROLE)?.trim())?;
let path = env(ENV_HANDOFF)?.trim().to_string();
if path.is_empty() {
return None;
}
Some(HandoffSubmitTool::new(PathBuf::from(path), role))
}

#[cfg(test)]
mod tests {
use super::*;
use tole_core::tool::Tool;

fn env<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
move |k| {
pairs
.iter()
.find(|(n, _)| *n == k)
.map(|(_, v)| v.to_string())
}
}

#[test]
fn a_role_child_gets_the_tool_and_nothing_else_does() {
let t = tool_from_env(&env(&[(ENV_ROLE, "fix"), (ENV_HANDOFF, "/x/handoff.json")]));
assert_eq!(
t.map(|t| t.name().to_string()),
Some("handoff_submit".into())
);
assert!(tool_from_env(&env(&[])).is_none());
assert!(
tool_from_env(&env(&[(ENV_ROLE, "fix")])).is_none(),
"no path"
);
assert!(tool_from_env(&env(&[(ENV_ROLE, "boss"), (ENV_HANDOFF, "/x")])).is_none());
assert!(tool_from_env(&env(&[(ENV_ROLE, "fix"), (ENV_HANDOFF, " ")])).is_none());
}
}
1 change: 1 addition & 0 deletions crates/tole-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod config;
pub mod config_apply;
pub mod config_trust;
pub mod doctor;
pub mod handoff;
pub mod session_host;
pub mod session_tools;
pub mod sibling;
Expand Down
10 changes: 10 additions & 0 deletions crates/tole-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1473,6 +1473,11 @@ fn build_registry(
let file_root = resolve_workspace_root(workspace)?;
// Child agents (#380): present only when spawned by `agent_start`.
let tracking = tole_cli::sibling::tracking_from_env(&tole_cli::config_apply::real_env);
// Role children (#383) get the handoff tool; everyone else does not.
if let Some(tool) = tole_cli::handoff::tool_from_env(&tole_cli::config_apply::real_env) {
reg.register(Box::new(tool))
.map_err(|e| anyhow::anyhow!("registering handoff_submit: {e}"))?;
}
// Uteke first-class (B4): recall (read) + document (write), behind
// startup probing — a missing uteke binary degrades to a warning,
// not phantom tools.
Expand Down Expand Up @@ -1644,6 +1649,11 @@ fn build_server_registry(
let file_root = resolve_workspace_root(workspace)?;
// Child agents (#380): present only when spawned by `agent_start`.
let tracking = tole_cli::sibling::tracking_from_env(&tole_cli::config_apply::real_env);
// Role children (#383) get the handoff tool; everyone else does not.
if let Some(tool) = tole_cli::handoff::tool_from_env(&tole_cli::config_apply::real_env) {
reg.register(Box::new(tool))
.map_err(|e| anyhow::anyhow!("registering handoff_submit: {e}"))?;
}
let count = |reg: &ToolRegistry| reg.specs().len();

#[cfg(feature = "shell-tools")]
Expand Down
Loading
Loading