Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0de7661229 |
@@ -214,71 +214,6 @@ fn recall_in(dir: &Path, repo_id: Uuid, query: &str, k: usize) -> Vec<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a mission finds the whole of its repository's memory, readable.
|
||||
///
|
||||
/// Outside `/mission/repo`, like `skill_delivery::SKILLS_DIR`, so it is never
|
||||
/// collected into the delivered diff: it is input, not output.
|
||||
pub const MEMORY_DIR: &str = "/mission/memory";
|
||||
pub const MEMORY_FILE: &str = "PROJECT-MEMORY.md";
|
||||
|
||||
/// Most entries an export carries. A repository's memory grows by one line
|
||||
/// per judged phase; this keeps the file readable in one sitting while
|
||||
/// covering many missions.
|
||||
const EXPORT_CAP: usize = 400;
|
||||
|
||||
/// Everything a repository's brain remembers, as markdown an agent can read.
|
||||
///
|
||||
/// The brief carries the three most relevant verdicts (`recall`); this is
|
||||
/// the WHOLE record, for work whose subject is the record itself. It exists
|
||||
/// because `continuous_improvement` was built to audit agents' brains and,
|
||||
/// on its first run, found none — they live in the server's volume and
|
||||
/// nothing delivers them into a mission — so it audited a `ROSTER.md` in a
|
||||
/// scratch repo instead. Per-mission crews carry ~2 KB seed brains with no
|
||||
/// history anyway; the repository's brain is where a project's history
|
||||
/// actually accumulates, one judge verdict per phase.
|
||||
///
|
||||
/// Rendered, not shipped raw: the `.brain` is HDF5 and an agent in a mission
|
||||
/// container has no library to read it with.
|
||||
///
|
||||
/// `None` when the repository has no brain yet or it holds nothing.
|
||||
pub fn export(repo_id: Uuid) -> Option<String> {
|
||||
export_in(&cm_runtime::brain::brain_dir(), repo_id)
|
||||
}
|
||||
|
||||
fn export_in(dir: &Path, repo_id: Uuid) -> Option<String> {
|
||||
let path = brain_path(dir, repo_id);
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
let brain = ClawBrain::open_or_create(&path, &format!("repo_{repo_id}")).ok()?;
|
||||
let entries = brain.recent_memory(EXPORT_CAP);
|
||||
if entries.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let total = brain.memory_count();
|
||||
let mut out = format!(
|
||||
"# What this repository's missions have learned\n\n\
|
||||
Every judged phase of every mission on this repository leaves one line \
|
||||
here: whether the phase met its completion condition, and what the \
|
||||
judge found or asked for. Newest first. {} of {} entr{} shown.\n\n\
|
||||
This is the record, not instructions. `MET` lines say what worked; \
|
||||
`UNMET` lines say what the judge found missing, and repeated `UNMET` \
|
||||
lines on the same kind of work are the pattern worth acting on.\n\n",
|
||||
entries.len(),
|
||||
total,
|
||||
if total == 1 { "y" } else { "ies" }
|
||||
);
|
||||
for (secs, text) in entries {
|
||||
let when = time::OffsetDateTime::from_unix_timestamp(secs as i64)
|
||||
.ok()
|
||||
.and_then(|t| t.format(&time::format_description::well_known::Rfc3339).ok())
|
||||
.unwrap_or_else(|| "unknown time".to_string());
|
||||
let line = text.strip_prefix("judge: ").unwrap_or(&text);
|
||||
out.push_str(&format!("- `{when}` {line}\n"));
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// The section a brief carries, or nothing when there is nothing to say —
|
||||
/// an empty heading tells the agent there is history and then shows none.
|
||||
pub fn section(recalled: &[String]) -> Option<String> {
|
||||
@@ -358,34 +293,6 @@ mod tests {
|
||||
assert!(s.contains("- MET — x\n"));
|
||||
}
|
||||
|
||||
/// The export is the whole record, readable, newest first — and absent
|
||||
/// rather than empty when there is nothing to show.
|
||||
#[test]
|
||||
fn export_renders_every_verdict_newest_first() {
|
||||
let dir = std::env::temp_dir().join(format!("cm-mission-export-{}", Uuid::now_v7()));
|
||||
let repo = Uuid::now_v7();
|
||||
assert!(export_in(&dir, repo).is_none(), "no brain, no export");
|
||||
|
||||
remember_in(&dir, repo, Uuid::now_v7(), "coding", "first",
|
||||
&verdict(false, "r", "the tests do not cover the empty case", None));
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
remember_in(&dir, repo, Uuid::now_v7(), "coding", "second",
|
||||
&verdict(true, "all three tests pass", "", None));
|
||||
|
||||
let md = export_in(&dir, repo).expect("two verdicts, so an export");
|
||||
assert!(md.starts_with("# What this repository's missions have learned"));
|
||||
assert!(md.contains("2 of 2 entries shown"), "{md}");
|
||||
let met = md.find("MET — coding").unwrap();
|
||||
let unmet = md.find("UNMET — coding").unwrap();
|
||||
assert!(met < unmet, "newest (MET) must come first:\n{md}");
|
||||
assert!(md.contains("the tests do not cover the empty case"));
|
||||
// `remember` stores "judge: <line>"; that ROLE prefix must not follow
|
||||
// the timestamp. (The line itself legitimately says "— judge: …".)
|
||||
assert!(!md.contains("` judge: "), "the storage prefix leaked:\n{md}");
|
||||
assert!(md.contains("` MET — coding"), "{md}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Round trip through a real brain file: what one mission's verdict
|
||||
/// wrote, a query shaped like the next mission's task recalls.
|
||||
#[test]
|
||||
|
||||
@@ -385,16 +385,6 @@ pub async fn on_launch(
|
||||
crate::skill_delivery::resolve(requested, installed),
|
||||
)
|
||||
.await;
|
||||
|
||||
// The repository's whole memory, readable, beside the skills. The
|
||||
// brief already carries the three most relevant verdicts; this is
|
||||
// the full record, for work whose subject IS the record — see
|
||||
// `mission_memory::export` for why it was needed.
|
||||
if mission_gateway.is_some() {
|
||||
if let Some(repo) = mission.repo_id {
|
||||
install_project_memory(repo, mission_id, &container).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut first_team_id: Option<Uuid> = None;
|
||||
let mut provisioned_claws: Vec<cm_domain::AgentId> = Vec::new();
|
||||
@@ -1095,52 +1085,6 @@ async fn install_skill_files(
|
||||
true
|
||||
}
|
||||
|
||||
/// Write the repository's memory export into the mission container.
|
||||
///
|
||||
/// Best-effort and loud: a mission with no memory to read is an ordinary
|
||||
/// mission, and a first mission on a repository has none. Outside the
|
||||
/// checkout (`mission_memory::MEMORY_DIR`) so it never lands in the diff.
|
||||
async fn install_project_memory(repo_id: Uuid, mission_id: Uuid, container: &str) {
|
||||
let Some(md) = crate::mission_memory::export(repo_id) else {
|
||||
return;
|
||||
};
|
||||
let docker = match crate::container_exec::connect() {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
eprintln!("mission_orchestrator: cannot reach docker for project memory: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let dir = crate::mission_memory::MEMORY_DIR;
|
||||
let argv = vec!["sh".to_string(), "-lc".to_string(), format!("mkdir -p {dir}")];
|
||||
if !matches!(
|
||||
crate::container_exec::exec_as_root(
|
||||
&docker,
|
||||
container,
|
||||
None,
|
||||
&argv,
|
||||
crate::container_tool_hooks::INSTALL_TIMEOUT,
|
||||
)
|
||||
.await,
|
||||
Ok(out) if out.exit_code == Some(0)
|
||||
) {
|
||||
eprintln!("mission_orchestrator: could not create {dir} for mission {mission_id}");
|
||||
return;
|
||||
}
|
||||
let bytes = md.len();
|
||||
let files = vec![(crate::mission_memory::MEMORY_FILE.to_string(), md.into_bytes())];
|
||||
match crate::mission_fs::put_files(&docker, container, dir, &files).await {
|
||||
Ok(()) => eprintln!(
|
||||
"mission_orchestrator: project memory ({bytes} bytes) installed for mission \
|
||||
{mission_id} at {dir}/{}",
|
||||
crate::mission_memory::MEMORY_FILE
|
||||
),
|
||||
Err(e) => eprintln!(
|
||||
"mission_orchestrator: could not write project memory for {mission_id}: {e}"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Record which arm this mission runs, so every turn composes the same one and
|
||||
/// the score can be attributed to it afterwards.
|
||||
///
|
||||
|
||||
@@ -57,73 +57,17 @@ holding (it was 55 of 85 dangling once).
|
||||
| `rust_sdlc` | planner coder tester reviewer committer | coding_readwrite | 19 | **yes** — harness default |
|
||||
| `topic_research` | lead_researcher evidence_checker report_writer | research_web_readonly | 4 | **yes** — measured |
|
||||
| `continuous_research` | paper_reader signal_ranker script_writer | research_readonly | 11 | **yes** — live runs |
|
||||
| `backend` | api_designer db_engineer coder tester committer | coding_readwrite | 20 | **yes** — first run 2026-09-22 |
|
||||
| `backend` | api_designer db_engineer coder tester committer | coding_readwrite | 20 | no |
|
||||
| `frontend` | designer coder tester committer | coding_readwrite | 12 | no |
|
||||
| `mobile` | designer coder tester committer | coding_readwrite | 11 | no |
|
||||
| `gpu` | arch_analyst kernel_author bench_engineer coder committer | coding_readwrite | 13 | no |
|
||||
| `threejs` | scene_designer coder shader_author perf_engineer committer | coding_readwrite | 13 | no |
|
||||
| `codebase_research` | code_archeologist architecture_mapper flow_tracer vault_scribe | research_readonly | 11 | **yes** — first run 2026-09-22 |
|
||||
| `papers_research` | domain_scout paper_reader library_curator | research_web_readonly | 8 | **yes** — first run 2026-09-22 |
|
||||
| `codebase_research` | code_archeologist architecture_mapper flow_tracer vault_scribe | research_readonly | 11 | no |
|
||||
| `papers_research` | domain_scout paper_reader library_curator | research_web_readonly | 8 | no |
|
||||
| `insight_research` | implementation_tracker novelty_hunter publication_drafter | research_readonly | 8 | no |
|
||||
| `continuous_improvement` | brain_inspector improvement_proposer improvement_evaluator | research_readonly | 7 | **runs, cannot reach its subject** |
|
||||
| `continuous_improvement` | brain_inspector improvement_proposer improvement_evaluator | research_readonly | 7 | no |
|
||||
|
||||
**Six of twelve are evidenced.** `backend` was exercised on 2026-09-22,
|
||||
the first time anything had been staffed from it: all five roles
|
||||
provisioned with real agents (api_designer, db_engineer, coder, tester,
|
||||
committer), and the mission delivered cursor pagination — `Paged<T>`,
|
||||
`paginate<T: Clone>`, module declared, `cargo test` 3 passed — judged
|
||||
**met on the first pass**, 2 files pushed. It also gave the task-permission
|
||||
shadow its first confirmed `Edit` call.
|
||||
|
||||
`codebase_research` was exercised the same day, against the real
|
||||
`clawmates` repository rather than the toy scratch crate, with a task that
|
||||
cannot be faked from filenames: trace every hop of a mission agent's tool
|
||||
call from the guest hook to the recorded event, naming file, function and
|
||||
whether each hop runs in the guest or on the host. It produced a 189-line
|
||||
`research/GATE-MAP.md` describing code committed **the same day** — the
|
||||
four-field `NODE_EXTRACT` including `agent_type`, the shadow-mode
|
||||
`would-deny.jsonl` semantics, `install_with` — and **all six of its line
|
||||
citations verify exactly** (`hook_script_with` 562, `NODE_EXTRACT` 789,
|
||||
`TaskPolicy` 297, `ROLE_POLICIES` 261, `settings_hook` 809,
|
||||
`install_command_with` 819). Nothing hallucinated. **Five of twelve.**
|
||||
|
||||
`papers_research` followed: asked for a bounded library on microVM and
|
||||
sandbox isolation for agents, it delivered a README index and four notes
|
||||
with complete frontmatter, judged met. The judge could only check the
|
||||
frontmatter was *present*; a paper team whose container has no
|
||||
pdf-to-text tool is exactly where invented citations live, so every
|
||||
arXiv id was checked against arXiv itself — **all four exist, with exact
|
||||
title matches**. One of them, `2603.02277` (SandboxEscapeBench), is a paper
|
||||
the operator's own research pass had already cited, which is independent
|
||||
evidence it found on-topic work rather than plausible filler. **Six of
|
||||
twelve.**
|
||||
|
||||
`continuous_improvement` ran too, and gets a different grade, because
|
||||
running and working came apart. All three roles handed off with
|
||||
attributed commits, and it was scrupulously honest: it filed **zero**
|
||||
level-up proposals, its proposer committed *"no proposals warranted,
|
||||
evidence base too thin"*, and its audit opens by stating the problem
|
||||
exactly — *"No `.brain` files are accessible within the repo or mission
|
||||
filesystem — brains are held in the platform, not checked in."*
|
||||
|
||||
That is the defect, and it is structural rather than a matter of data.
|
||||
The template's subject is "every project agent's `.brain`"; those live in
|
||||
the server's `/data/brains` volume, and **nothing delivers them into a
|
||||
mission** — the same shape as `research-has-no-delivery-channel` and
|
||||
`skills-had-no-delivery-channel`. So it audited the only agent-shaped
|
||||
thing in reach, a `ROSTER.md` in the scratch repo, and read that file's
|
||||
`6.1.128` (a guest kernel version) as an agent version, having no way to
|
||||
know better. It is not counted as evidenced.
|
||||
|
||||
A channel alone would not rescue it. Mission crews are minted per mission
|
||||
(reuse is off by decision), and missions write memory to the *repo* brain,
|
||||
so every agent brain is a ~2 KB seed with no history to audit. The
|
||||
template assumes long-lived agents that accumulate a record; the platform
|
||||
makes disposable ones. That is a design decision to resolve, not a bug to
|
||||
patch.
|
||||
|
||||
**Two of the remaining six are still unevidenced**, and the other four
|
||||
are blocked on a target stack. The other nine are well-formed scaffolding:
|
||||
**Three of twelve are evidenced.** The other nine are well-formed scaffolding:
|
||||
roles, prompts, brain seeds and resolving skills, and no run behind any of
|
||||
them. They will probably work — they are structurally identical to the three
|
||||
that do — but "probably" is the word, and this codebase has a name for the gap
|
||||
@@ -136,17 +80,6 @@ blocked on a target, not on the template. The remaining four —
|
||||
`codebase_research`, `papers_research`, `continuous_improvement`, `backend` —
|
||||
could be exercised against repositories we already have.
|
||||
|
||||
## One thing both runs showed: agents reach for Bash
|
||||
|
||||
Cumulative tool calls across every mission since the policy shipped:
|
||||
**Bash 74, Read 34, Write 15, Glob 3, Edit 1, Agent 1**. A read-only
|
||||
mapping mission that could have used `Grep` and `Glob` used `Bash` 33
|
||||
times and `Read` 5. That matters for task permission: the allowlist's
|
||||
dedicated-tool entries (`Grep`, `ToolSearch`, `WebFetch`, `TodoWrite`)
|
||||
may simply never be exercised, so "unconfirmed by a real run" will not
|
||||
converge for them — and it means the surface that actually needs
|
||||
governing is `Bash`, which the floor rules already cover.
|
||||
|
||||
## What this means we can ask for today
|
||||
|
||||
**With confidence:** a repo-backed research→code loop on a Rust project, with
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# GATE-MAP — Mission Agent Tool Call Gate, End to End
|
||||
|
||||
> Research phase finding. No source files were modified.
|
||||
> Traced from code; every claim cites a file and function.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
There are **two distinct gating paths** in this repository, depending on whether
|
||||
the agent is a *chat agent* (ZeroClaw, tool-free, MCP door) or a *mission agent*
|
||||
(`claude -p` inside a container or microVM). This document traces the mission
|
||||
path exclusively, as scoped by the task.
|
||||
|
||||
The mission path has three layers that execute in order:
|
||||
|
||||
```
|
||||
[GUEST] PreToolUse hook (tool-gate.sh) — blocks inside the container
|
||||
[HOST] Phase runner drain — reads gate records + tap after phase ends
|
||||
[HOST] mission_events INSERT — writes to the `mission_events` DB table
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hop 1 — The PreToolUse Hook Inside the Guest
|
||||
|
||||
**File:** `crates/cm-api/src/vm_tool_gate.rs`
|
||||
**Executes in:** the guest (container or microVM)
|
||||
|
||||
### Installation
|
||||
|
||||
The host generates a shell script (`hook_script_with`, line 562) and installs it
|
||||
via `install_command_with` (line 819). For container-tier missions the installer
|
||||
is run by `container_tool_hooks::install_with` (`crates/cm-api/src/container_tool_hooks.rs`,
|
||||
line 53), which calls `container_exec::exec_as_root` to run the install script
|
||||
inside the container. The installed file is `/root/toolhooks/tool-gate.sh`
|
||||
(constant `HOOK_DIR = "/root/toolhooks"`). For microVM missions the same script
|
||||
is placed at `/root/toolgate/tool-gate.sh` (constant `GUEST_DIR = "/root/toolgate"`).
|
||||
|
||||
The settings document written to `/root/toolhooks/settings.json` (or
|
||||
`/root/guest-settings.json` for the microVM tier) registers the hook via:
|
||||
|
||||
```json
|
||||
[{ "hooks": [{ "type": "command", "command": "<dir>/tool-gate.sh" }] }]
|
||||
```
|
||||
|
||||
(`settings_hook` function, line 809.)
|
||||
|
||||
### What the hook does
|
||||
|
||||
When `claude -p` is about to execute a tool it fires the `PreToolUse` event.
|
||||
The generated shell script:
|
||||
|
||||
1. Reads the hook JSON payload from stdin via `node` (the `NODE_EXTRACT`
|
||||
constant, line 789) — extracts `tool_name`, `tool_input.command`,
|
||||
`tool_input.file_path`, and `agent_type` (the subagent role).
|
||||
2. Optionally checks the **task-permission policy** (`TaskPolicy`, line 297):
|
||||
if `enforce = true`, any tool not in `allowed` is denied; in shadow mode the
|
||||
denial is written to `would-deny.jsonl` and the call is allowed.
|
||||
3. Checks **role policies** (`ROLE_POLICIES`, line 261): the `verifier` role is
|
||||
denied write tools (`Write`, `Edit`, `MultiEdit`, `NotebookEdit`).
|
||||
4. Checks **write-tool path rules**: write tools targeting protected paths
|
||||
(`PROTECTED_PATHS`, line 225 — `/root/toolgate`, `/root/toolhooks`,
|
||||
`/root/tap/`, `/root/guest-settings.json`, `/root/.claude/settings`,
|
||||
`.git/hooks/`) are denied.
|
||||
5. For `Bash` only, iterates over command segments split on `;|&\n` and applies
|
||||
the `RULES` deny list (line 127): `rm -rf /`, force-push, hard-reset onto
|
||||
remote, outbound POST (`curl`/`wget` with body flags),
|
||||
`--dangerously-skip-permissions`, and reads/writes of the hook files
|
||||
themselves.
|
||||
|
||||
**On denial:** the script writes one JSONL line
|
||||
`{"rule":"<id>","payload":<hook event>}` to `denied.jsonl` (or
|
||||
`would-deny.jsonl` for shadow task-permission), prints the human reason to
|
||||
stderr, and **exits 2**. Exit 2 is the Claude Code contract for "block this
|
||||
tool call and return the stderr text to the model."
|
||||
|
||||
**On allow:** exits 0 — the tool call proceeds.
|
||||
|
||||
**Inert gate:** if `node` is absent, the script writes to `inert` and exits 0
|
||||
(never fail-closed), making every call allowed while leaving a marker the host
|
||||
can check.
|
||||
|
||||
---
|
||||
|
||||
## Hop 2 — PostToolUse Tap (Guest, Telemetry Only)
|
||||
|
||||
**File:** `crates/cm-api/src/vm_tool_tap.rs`
|
||||
**Executes in:** the guest (container or microVM)
|
||||
|
||||
A second hook (`PostToolUse`) appends one JSON record per completed tool call to
|
||||
`/root/tap/tools.jsonl` (container tier: `/root/toolhooks/tap/`). It **always
|
||||
exits 0** (line 13) — it is an observer, not a participant. A non-zero exit here
|
||||
would feed its stderr back to the model. The tap records tool name, path,
|
||||
session id, bounded response (for command tools only), and the subagent's
|
||||
`agent_type`/`agent_id`.
|
||||
|
||||
---
|
||||
|
||||
## Hop 3 — Host Drains the Gate and Tap Records
|
||||
|
||||
**File:** `crates/cm-api/src/phase_runner.rs`, function
|
||||
`drain_finished_container_phases` (line ~601)
|
||||
**Executes in:** the host (server process)
|
||||
|
||||
After a container-tier phase reaches `completed` or `failed`, the host's sweep
|
||||
(run on a tick) connects to Docker and reads the guest files:
|
||||
|
||||
1. **Inert marker** (`drain_inert`): if the file exists the host logs the count
|
||||
and records a `gate.inert` `MissionEvent`.
|
||||
2. **Denials** (`drain_denied`, line ~657): each `denied.jsonl` line becomes a
|
||||
`gate.denied` `MissionEvent`. The detail is parsed by
|
||||
`vm_tool_gate::denial_detail` (line 386) so the `rule` field is alongside
|
||||
the hook payload.
|
||||
3. **Would-deny shadow** (`drain_would_deny`, line ~672): each `would-deny.jsonl`
|
||||
line becomes a `gate.would_deny` `MissionEvent`.
|
||||
4. **Tool tap** (`drain`, line ~707): the tap file is read-then-cleared. Each
|
||||
parsed `Observed` record is passed to `record_vm_tools`.
|
||||
|
||||
For microVM phases the tap and gate records are read from inside the VM over SSH
|
||||
before the VM is destroyed (`phase_runner.rs`, line ~1852 and ~1855).
|
||||
|
||||
---
|
||||
|
||||
## Hop 4 — DB Write: `mission_events` Table
|
||||
|
||||
**File:** `crates/cm-api/src/mission_events.rs`, function `record` / `record_all`
|
||||
(line ~126)
|
||||
**Executes in:** the host
|
||||
|
||||
`record_vm_tools` (phase_runner.rs line 2423) builds `MissionEvent` structs
|
||||
and calls `mission_events::record_all`. Each event is inserted into the
|
||||
**`mission_events`** Postgres table:
|
||||
|
||||
```sql
|
||||
INSERT INTO mission_events
|
||||
(mission_id, phase_id, run_id, agent_id, kind, target, detail)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
-- subject to a per-phase cap of 400 events for TOOL_CALL / FILE_TOUCH kinds
|
||||
```
|
||||
|
||||
Event kinds recorded for tool calls:
|
||||
- `tool.call` — one per tool invocation (constant `TOOL_CALL`, line 23)
|
||||
- `file.touch` — one per tool that touched a path (constant `FILE_TOUCH`, line 24)
|
||||
- `gate.denied` — one per gate denial
|
||||
- `gate.would_deny` — one per shadow-mode would-have-denied
|
||||
- `gate.inert` — when the gate ran without `node`
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Step | Where | File | Function | What happens |
|
||||
|------|-------|------|----------|-------------|
|
||||
| 1 | **Guest** | `vm_tool_gate.rs` | `hook_script_with` → `tool-gate.sh` | PreToolUse hook evaluates deny rules; exits 2 (block) or 0 (allow) |
|
||||
| 2 | **Guest** | `vm_tool_tap.rs` | PostToolUse tap script | Appends tool call record to `tools.jsonl`; always exits 0 |
|
||||
| 3a | **Host** | `container_tool_hooks.rs` | `drain_denied` / `drain_would_deny` / `drain_inert` / `drain` | Reads gate denial + tap files out of the container via Docker exec |
|
||||
| 3b | **Host** | `phase_runner.rs` | `drain_finished_container_phases` | Calls drain functions; calls `record_vm_tools` with parsed `Observed` |
|
||||
| 4 | **Host** | `mission_events.rs` | `record_all` | INSERTs `tool.call`, `file.touch`, `gate.denied`, … into `mission_events` |
|
||||
|
||||
---
|
||||
|
||||
## Key Design Notes
|
||||
|
||||
- **Pre-execution gate is not the §15 human-approval gate.** The comment at the
|
||||
top of `vm_tool_gate.rs` is explicit: the §15 `GatePolicy` (chat path, keys on
|
||||
`session_id`/`message_id`) has no mission-shaped form, and a hook that blocked
|
||||
the agent's process while waiting for a human would wedge the turn. The gate is
|
||||
a deterministic deny list only.
|
||||
- **The hook is installed by the host, baked at install time.** The task policy
|
||||
is compiled into the script at install time, not re-read from a file at call
|
||||
time, so the guest cannot persuade itself to use a different policy file
|
||||
(`hook_script_with`, line 562 comment).
|
||||
- **Container and microVM tiers share the same gate/tap code** but differ in how
|
||||
the host drains: containers via `container_exec::connect` (DOCKER_HOST-aware);
|
||||
microVMs by SSH before the VM is destroyed.
|
||||
- **DB table is `mission_events`, not `run_events` / `audit_log`.** Chat-path
|
||||
tool calls land in `run_events` (written by `Runtime::emit`,
|
||||
`cm-runtime/src/runtime.rs:624`). Mission-path tool calls land in
|
||||
`mission_events`. The `audit_log` table is only for the MCP door path (chat
|
||||
agents using the ZeroClaw MCP door), not for mission agents.
|
||||
|
||||
---
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
TASK: INT-01 — Verify that `record_vm_tools` attribution logic correctly maps tap session IDs to agent IDs under multi-agent phases
|
||||
TASK: INT-02 — Document the MCP door path (chat agents) in a parallel DOOR-MAP.md for comparison
|
||||
TASK: INT-03 — Confirm `CLAWMATES_TASK_PERMISSION=enforce` flag is set (or note it is still shadow) in production config
|
||||
@@ -1,48 +1,34 @@
|
||||
key = "continuous_improvement"
|
||||
name = "Continuous Improvement"
|
||||
description = "Standing self-audit of a project: read everything its missions have learned — every judge verdict on this repository — find the patterns in what keeps failing, and propose evidence-backed changes to how the work is set up."
|
||||
description = "Standing self-audit: read every project agent's .brain and stated purpose, look for enhancement opportunities, apply changes via the level-up proposer, evaluate, and report."
|
||||
stack = ["research", "self-improvement", "brain-inspection", "level-up"]
|
||||
category = "research"
|
||||
default_topology = "pipeline"
|
||||
risk_profile = "research_readonly"
|
||||
mcp_bundles = ["clawmates_door", "clawmates_skills"]
|
||||
version = 2
|
||||
version = 1
|
||||
|
||||
[[roles]]
|
||||
slot = "brain_inspector"
|
||||
order_idx = 0
|
||||
skills = ["brain-file-reading", "workspace-repo-commit-protocol"]
|
||||
system_prompt = """
|
||||
You are the RECORD INSPECTOR of a Continuous Improvement team.
|
||||
You are the BRAIN INSPECTOR of a Continuous Improvement team.
|
||||
|
||||
Your subject is what this project's missions have learned, and it is
|
||||
already in front of you: `/mission/memory/PROJECT-MEMORY.md`. Every
|
||||
judged phase of every mission on this repository left one line there —
|
||||
MET or UNMET, the kind of phase, the completion condition, and what the
|
||||
judge found or asked for. Read all of it.
|
||||
For each active claw in the workspace: fetch its .brain (agent.md,
|
||||
personality.md, skills.md, notes) via the brain API and compare against
|
||||
its declared job_title + system_prompt. Look for:
|
||||
|
||||
You do NOT have the agents' own .brain files, and there is no API from
|
||||
here that returns them. Do not look for them. The first run of this team
|
||||
spent itself searching and audited a ROSTER.md instead; the record above
|
||||
is the thing to audit.
|
||||
- Drift: brain contents describe capabilities the prompt / role
|
||||
doesn't actually cover
|
||||
- Gaps: role calls out responsibilities the brain has no notes on
|
||||
- Contradictions: brain and prompt disagree on a policy or default
|
||||
- Stale references: brain cites files, tools, or endpoints that no
|
||||
longer exist
|
||||
|
||||
Look for patterns, not incidents:
|
||||
|
||||
- The same kind of work failing repeatedly (several UNMET lines on
|
||||
coding phases, or on one recipe's conditions)
|
||||
- The judge asking for the same missing thing more than once
|
||||
- Conditions that pass only after several iterations, against ones
|
||||
that pass first time
|
||||
- A condition the judge keeps reading differently from how it reads
|
||||
|
||||
A single UNMET line is an incident, not a pattern. Say how many lines
|
||||
support each finding, and quote them.
|
||||
|
||||
If the file is absent, this repository has no judged history yet: say so
|
||||
and stop. That is a complete audit, not a failure.
|
||||
|
||||
Output goes to `Improvement/<date>/audit.md` — one section per finding,
|
||||
each with the verdict lines that support it. Never propose fixes here.
|
||||
Output goes to `Improvement/<date>/audit.md` — one section per claw
|
||||
with a Findings table (severity, category, evidence). Never propose
|
||||
fixes here; only surface findings.
|
||||
"""
|
||||
brain_seed = """
|
||||
# Brain inspector memory seed
|
||||
@@ -65,36 +51,31 @@ skills = ["level-up-proposal-shape", "workspace-repo-commit-protocol"]
|
||||
system_prompt = """
|
||||
You are the IMPROVEMENT PROPOSER of a Continuous Improvement team.
|
||||
|
||||
For each pattern the inspector found, propose ONE concrete change an
|
||||
operator could make to how this kind of work is set up:
|
||||
For each finding from the inspector, produce a level-up proposal in the
|
||||
shape the /api/claws/{id}/level-up endpoint expects:
|
||||
|
||||
- a completion condition reworded (quote the old and the new wording)
|
||||
- a skill that is missing for work that keeps failing
|
||||
- a recipe setting (iterations, commit policy, phase split)
|
||||
- a task brief that keeps being misread
|
||||
- identity_refinement (for prompt drift)
|
||||
- brain_consolidation (for stale / duplicated notes)
|
||||
- skill_add (for gaps)
|
||||
- skill_candidate (for a novel skill this claw needs)
|
||||
|
||||
Every proposal names the verdict lines that motivate it. A proposal you
|
||||
cannot tie to at least two lines of the record is not a proposal — drop
|
||||
it. "No change is warranted by this record" is a complete and correct
|
||||
result, and it is the right one when the history is thin.
|
||||
|
||||
You cannot apply anything, and there is no API from here to submit to.
|
||||
Write the proposals to `Improvement/<date>/proposals.md`; the operator
|
||||
decides.
|
||||
Submit each proposal via the API. Never apply — approval stays with
|
||||
the operator via the level-up drawer.
|
||||
"""
|
||||
brain_seed = """
|
||||
# Improvement proposer memory seed
|
||||
|
||||
## Discipline
|
||||
- One proposal per pattern — never one per incident.
|
||||
- One proposal per claw per run — batching is the applier's problem,
|
||||
not ours.
|
||||
- Rationale is mandatory. Every item's `rationale` field carries the
|
||||
audit finding that motivated it.
|
||||
|
||||
## Redlines
|
||||
- Never propose a skill that already exists in /mission/skills. Look
|
||||
first.
|
||||
- Never invent a pattern to have something to say. A thin record gets
|
||||
"no change warranted".
|
||||
- Never propose skill_candidate for a skill that already exists in the
|
||||
catalog. Search first.
|
||||
- Never propose roster_change or mcp_bundle_change here — those are
|
||||
team-level, not claw-level.
|
||||
"""
|
||||
|
||||
[[roles]]
|
||||
@@ -104,22 +85,19 @@ skills = ["metrics-baseline-comparison", "workspace-repo-commit-protocol", "smal
|
||||
system_prompt = """
|
||||
You are the IMPROVEMENT EVALUATOR of a Continuous Improvement team.
|
||||
|
||||
You are the check on the proposer. For each proposal in
|
||||
`Improvement/<date>/proposals.md`, go back to
|
||||
`/mission/memory/PROJECT-MEMORY.md` and ask:
|
||||
Some period after proposals were applied (operator-configured, default
|
||||
7 days), pull the affected claws' recent metrics (turn count,
|
||||
approval-request rate, task completion rate from the Tasks tab, level-up
|
||||
proposal apply/reject ratio) and compare against the pre-application
|
||||
baseline. For each claw:
|
||||
|
||||
- Does the cited evidence exist, verbatim, in the record?
|
||||
- Is it a pattern (several lines) or one incident dressed as one?
|
||||
- Would the proposed change plausibly have turned those UNMET lines
|
||||
into MET, or does it address something else?
|
||||
- Did the intended change land in behavior? (evidence: transcripts,
|
||||
metric deltas)
|
||||
- Any unintended regressions?
|
||||
|
||||
Mark each proposal SUPPORTED, WEAK (one line, or evidence that does not
|
||||
match the claim) or UNSUPPORTED (the cited lines are not there). An
|
||||
evaluator that approves everything is the failure this role exists to
|
||||
prevent; one that rejects everything is the same failure pointing the
|
||||
other way.
|
||||
|
||||
Output goes to `Improvement/<date>/evaluation.md`.
|
||||
Output goes to `Improvement/<date>/evaluation.md`. Escalate persistent
|
||||
regressions to the operator by opening an issue rather than proposing
|
||||
another change — sometimes rollback is right.
|
||||
"""
|
||||
brain_seed = """
|
||||
# Improvement evaluator memory seed
|
||||
|
||||
Reference in New Issue
Block a user