docs: hand off — what shipped, what is verified, and what is blocked
deploy / test (push) Successful in 4m42s
deploy / build (push) Successful in 58s

Seven commits this pass, all deployed. The handoff leads with the thing that
will otherwise waste the next session's first hour: `glm-5.3` hit a hard z.ai
quota on 2026-08-29 (code 1310, resets 09-04), it is the DEFAULT validator on
both stacks, and the `ZAI_API_KEY` fingerprints are identical — so every mission
declaring a `done_when` fails its evaluation on local and production alike,
with its artifacts fully delivered and correct.

That failure is not a bug to fix. `evaluator.rs:480` refuses to fall back to the
agent's own provider because a same-family verdict would claim an independence
it does not have. It is also NOT the malformed-prompt 429 we hit before: this
one carries a code and a reset date.

Records the validation run honestly rather than as a clean sweep. Three of four
things confirmed live — `always_inject` delivering a body beside an index entry
in one prompt, retrieval still firing through the door, the corrected gate
installed and quiet against 23 body-free Bash calls, attribution 34/34. The
fourth did not happen: those agents never delegated, so the subagent field is
written and null, and the path that motivated it has still never been watched
populating `mission_events`. A task that invites delegation does not force it;
the next attempt should instruct it outright.

Also carried forward: local test state that production does not share
(`workspace-repo-commit-protocol.always_inject = true`, set by hand), the three
fork items sitting behind one runtime image rebuild with tank offline, and two
silent-discard defects found by sweep and left unfixed — `container_tool_hooks::
install`'s outcome is recorded nowhere, which makes "did this mission run gated?"
unanswerable once the container is reaped.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
This commit is contained in:
Omar Sobh
2026-08-29 22:31:43 -05:00
co-authored by Claude Opus 5
parent f26de3ba76
commit daf8d12157
+113 -280
View File
@@ -1,315 +1,148 @@
# Where this left off — 2026-08-21 (second pass) # Where this left off — 2026-08-29
Read `CAPABILITY-REVIEW.md` for the system picture, Read `MISSION-EGRESS.md` for what a mission container can reach,
`TOOL-CALL-ARCHITECTURE.md` for how mission tools actually work, and `UPSTREAM-SCAN.md` for the fork-vs-upstream position, and
`SKILL-USE-BASELINE.md` for the measurement — which now scores behaviour rather `SKILL-USE-BASELINE.md` for the delivery measurement.
than the agent's own account of it.
## State of the tree ## Read this first — the judge is down until 2026-09-04
Local suite green: **108 test binaries, 815 tests** (`cargo test --workspace`).
**Pushed** — 22 commits, CI green on gw-04, deployed.
Eight measurement missions ran on the local stack
(`scripts/skill-use-run.sh`); all are held 90 days and re-scorable with
`--score <id>`.
## The premise of the last handoff's item 1 was wrong
It said the container-tier gate and tap were "proven locally and unproven in
prod", and told you to watch for the first production mission.
**Production has never run a mission.**
``` ```
gw-04$ select count(*) from missions; -> 0 429 Too Many Requests
gw-04$ select count(*) from mission_events; -> 0 [1310][Weekly/Monthly Limit Exhausted. Your limit will reset at 2026-09-04 10:01:33]
``` ```
Prod is armed correctly — server restarted with `glm-5.3` is the default validator on **both** stacks — neither sets
`CLAWMATES_RUNTIME_IMAGE=clawmates-runtime:hooks`, image present. There is `validator_model`, and the `ZAI_API_KEY` fingerprints are identical
simply nothing to watch. Prod auth is Clerk, so a mission cannot be launched (`sha256[0:12] = 616fb5076696`). So **every mission that declares a `done_when`
from a terminal; someone has to click. Generalise the lesson: before debugging will fail its evaluation on local AND production until the quota resets.**
why a deployed thing shows no evidence, check whether anything ran.
### A leaked mission container nothing can reap The code is behaving correctly and this is worth understanding before "fixing"
it. `evaluator.rs:480` deliberately refuses to fall back to the agent's own
provider, because a verdict produced by the same family the agent ran on would
claim an independence it does not have. The phase stays unmet, says why, and the
sweep retries. Three retries, three identical 429s, then `failed`.
`cm-runtime-mission-019ff5b157ce77028f308ebd3dc92748` has been `Up` since Unlike the subscription-OAuth 429 (which was a malformed system prompt), this one is REAL: it carries
**2026-08-12** on `clawmates-runtime:sync`, with no `missions` row behind it. code 1310 and a reset date. Do not go looking for a malformed prompt.
`mission_runtime`'s terminal sweeper selects `FROM missions WHERE status IN Options, with the trade stated:
(…)`, and `teardown_container(mission_id)` is only ever called with an id from
that query. Nothing enumerates Docker for `cm-runtime-mission-*` containers with
no matching row, so a container whose row is gone is invisible to every reaper.
Same shape as the earlier agent-container reap drift, different table.
**It was not idle.** Its checkout held **ten commits on a branch that had never 1. Top up the z.ai quota — restores the judge we actually measured.
been pushed** — +3451/-30 across 30 files, eighteen INT items on `clawhdf5` 2. Wait until 2026-09-04 — nothing else breaks; `done_when` phases just can't pass.
including AES-256-GCM, Ed25519 signing and HNSW batch insert. The remote had 3. Switch to kimi — available now, and our own eval found it produces FALSE
eight other `clawmates/*` branches and not this one. POSITIVES including passing deleted tests. Worse than no judge. Not advised.
4. The Anthropic subscription judge — works, but same family as the agent, so
Handled: bundled and verified, branch pushed to git.redclaw.dev, confirmed on the verdict loses the independence the design exists for.
the remote at the tip (`87039e9`), container removed. 57G → 59G free. The
bundle is kept at `/opt/clawmates/rescued/rescue-019ff5b1.bundle`.
`mission_runtime::sweep_orphans` now closes the gap, and **that container is
why it refuses to reap a checkout holding commits no remote has.** A reaper
that deleted on sight would have destroyed all of it silently, as its designed
behaviour. Every unanswerable case — docker will not date it, git will not
answer, the clock skewed — resolves to *do not reap*.
## What shipped this pass ## What shipped this pass
### The tool tap kept the name and discarded the argument Seven commits on `main`, all deployed to gw-04 (server recreated
2026-08-29T02:07:48). Suite at the end: **108 binaries, 842 Rust tests, 92
frontend tests, tsc clean.**
The container tier's first measured mission recorded `Bash × 6` and not one of ### The gate's exfiltration rule matched a spelling production never writes
them said what it ran. `vm_tool_tap::parse` read `tool_input` to pull the path
out of it and dropped the rest, so every behavioural question about a phase was
unanswerable from a record that looked complete.
`Observed.input` now keeps it, bounded: file bodies become a byte count, other `5a2ed8f`. One rule stood between an agent and sending the checkout off the
long strings truncate with a marker. **Host-side only, no image rebuild** — the machine, anchored to a segment STARTING with `curl -X POST`. The tap held the
arguments were always in the tap file. `tool.call` also gained `detail.path` answer: 166 curl invocations across two production missions, every one a GET,
(the World's SSE reads it and had been getting null on every container-tier and every one beginning `curl -s`. The `-s` pushes the needle off position zero.
call) and `file.touch` gained `detail.abs`. `curl -s -X POST … -d @secret`, `-d @file`, `-F`, `-T`, `--upload-file`,
`wget --post-file` — all allowed.
### Skill-Use is scored from actions `Match::Carries` matches a segment that starts with the command and contains the
needle anywhere; `CarriesExact` exists for flags whose CASE is their meaning
(curl `-F` uploads, `-f` fails quietly, as in the ordinary `curl -fsSL`).
Segments are lowercased individually, not up front. All 158 recorded production
commands replay through the new script with **0 false positives**.
`skill_use::Evidence` carries `tool.call` rows alongside the narrative, and ### A subagent's tool calls are no longer credited to its parent
every check prefers them. `workspace-repo-commit-protocol`'s boundary was a
substring search for `/workspace/repo` in prose — an agent that wrote to the
wrong root **without narrating it scored a clean pass**. Two verdicts changed
for honesty: silence is `NotObservable` rather than `Pass`, and a test that ran
after the first write is undecidable rather than a failure.
**Trigger is still `NotObservable`, and half of its old reason is now wrong.** `fe5c7d2`. Measured against the real claude binary, not reasoned about:
"`claude_cli` cannot surface a tool call" is false. What still holds is that we
**inline** skill bodies, so there is no retrieval to observe. The blocker moved
from the transport to the delivery model, and the door (§3) closes it with no
scorer change at all.
### Research phases are staffed by a research team 1. Subagent tool calls DO fire both hooks. `PreToolUse` blocked a subagent's
denied curl. **`Agent` is not a gate bypass.**
2. They carry the PARENT's `session_id` — which is why `attribute_sessions`
keeps working; a subagent never adds a session.
3. Only `agent_type`/`agent_id` tell them apart, and `parse()` read past both.
`research_only` — repo-less, one research phase — defaulted to `rust_sdlc`, so The tap appends the raw payload, so those fields were always on disk. Host-side
it was staffed with a planner, coder, tester, reviewer and committer, four of fix only, no image rebuild.
whom had nothing to do. New `topic_research` team, plus `default_phase_teams`
so a recipe can staff each phase *purpose* separately. Measured: 5 roles → 3,
14 skill deliveries → 4, 50KB of prompt → 24KB, and **1 of 9 delivered skills
applicable → 4 of 4**.
The scores barely moved, and that is the honest reading: what changed is that ### The rest
`not_applicable` now means "no machine-checkable consequence" rather than "this
skill had nothing to do with this phase".
Three existing research templates were also wrong in ways nothing checked. - `bd7fd46` — a mission that cannot attach to `clawmates_edge` now FAILS to
`papers_research` bound **`arxiv-daily`** — a skill whose content is "do not launch. `clawmates_core` is `internal: true` with no default route, so a
search arXiv yourself" — to the DOMAIN SCOUT, the role whose job is searching. discarded attach error meant a mission running with zero egress while
Its PAPER READER was told to "fetch the PDF, extract text"; the runtime image reporting success. **Behaviour change: it can now stop a mission starting.**
has no pdftotext, no mutool and no pypdf, so every paper would have hit the - `2f1a870` — `skills.always_inject` (migration 0083). Delivery and scoring
`[read: abstract only]` fallback, which reads exactly like the fallback working. both, because they are different mistakes.
`insight_research` cross-referenced "our repos'" history when a mission binds - `f26de3b` — `agent.last_run`, the metric band's historical half.
one. `codebase_research` wrote to a vault that is not mounted. - `fde1341`, `563b074` — the egress measurement and the upstream scan.
### The wrong repo path was in the team templates too ## Verified live, and one honest negative
The `/workspace/repo` guard was written against `skills/` only. The same path A validation mission ran locally on the `index` arm
was in four team templates — including `rust_sdlc`, default for five of six (`01a04f74-564b-7cd3-9b55-9d8f29d3e2f4`):
recipes, whose coder was told "your working directory is /workspace/repo". The
guards now walk one corpus: skills, team templates and recipes together.
### Two more skills contradicted the platform
Both found by reading the source of truth before writing a check against it —
which is the only reason they were found.
- `decompose-int-items` taught `PLAN_COMPLETE: INT-01..05`. Ids are strictly
`INT-<digits>`, so the range form is rejected and the plan pass records
nothing while every item stays open.
- `workspace-repo-commit-protocol` claimed the task-card parser advances mission
state on the INT id in your commit subject. **Nothing in the platform reads
commit messages** — `apply_for_run` reads `run_events`, the turn output.
`no_skill_shows_a_marker_the_parser_would_reject` guards the class, running the
real parser over every marker in every skill's fenced blocks.
### Trigger is measured, and the door is what made it possible
`skill_use` scores Trigger from `ReadMcpResourceTool` calls, parsed through
`mcp_skills::parse_uri` — the function that wrote the URI. Run 8, one mission,
one clean A/B:
| skill | delivered by | Trigger |
|---|---|---|
| `workspace-repo-commit-protocol` | retrieval | **pass** |
| `web-search-triage` | inlined | not observable |
| `structured-paper-summary` | inlined | not observable |
| `scientific-writing-conventions` | inlined | not observable |
Careful with what that proves: the retrieval was *instructed* by the task. It
demonstrates the instrument, not a spontaneous relevance judgement.
### Red-first is observable, and it needed neither ordering nor the diff
The previous version of this list said the TDD check needed the repository
diff. That was wrong, and `tdd-red-green-refactor` says why: *"Commit the
RED-to-GREEN pair as one commit."* The failing test and its fix land together
by instruction, so the diff and the commit history are as blind as tool
ordering already was.
The witness is what each run PRINTED, and the tap was discarding it. Claude
Code's `PostToolUse` payload carries `tool_response` (stdout/stderr/
interrupted) — verified against the binary. `bounded_response` keeps the END of
that output, the opposite of `bounded_input`, because a command's meaning is
its verdict and `cargo test` prints it last.
failing run then a passing one → Pass
every run failed → Fail
every run passed → NotObservable, because a test that never
failed is equally what a correct
implementation written first looks like
## The four days after that pass went into infrastructure
No product commits landed between 2026-08-21 and 2026-08-25. What did happen,
re-verified live on the 25th:
- `PasswordAuthentication no` is effective on gw-01/gw-02/gw-04 (checked with
`sudo sshd -T`, not by reading a config a drop-in can override), and
`fail2ban` is active on all three — **95 / 74 / 77 total bans**, so it is
catching real traffic. `ignoreip` must keep `100.64.0.0/10`: every operator
and every fleet node reaches these hosts from CGNAT space.
- gw-02's three Postgres containers were published on `0.0.0.0` and are now
bound to `127.0.0.1` (5434 mindbridge, 5435 clawbridge, 5437 smartclaw).
- Roughly 93G reclaimed; the largest single piece was a source-level leak of one
test database per test, fixed in `b58f034`.
Two failure shapes worth keeping:
- Recreating a DB container leaves the owning app holding **dead pool
connections**. The port binding reads perfect and the app still returns its
baseline status code, so a shallow check calls it done while every pool is
stale. Restart the owning unit and assert on the established-connection count.
- A `grep` for the service name matched `smartclaw-email` before `smartclaw`,
so the wrong unit was restarted. Only the connection count being `0` caught it.
**Open, and not safe to assume otherwise:** gw-02 has *no host firewall* —
`INPUT` policy `ACCEPT`, `DOCKER-USER` empty. Ports 18789/3000/3010/8082 are
bound `0.0.0.0` by native processes (a compose edit will not move them), and the
only thing keeping them off the internet is an upstream cloud firewall the host
cannot see. gw-05 and web-01 have no fail2ban and `sudo` there needs a password.
## Next, in order
1. ~~**Watch the first production mission.**~~ **Done — 2026-08-25.** Two were
launched from the UI (`ClawHDF5`, `JEPA Research`); `ClawHDF5` completed and
every mechanism from the previous two passes fired correctly the first time.
| | | | | |
|---|---| |---|---|
| skills door | installed — `api_origin()` derived the host from the server's own container id, first run outside where it was tested | | `always_inject` | **CONFIRMED** — same prompt, same arm: `workspace-repo-commit-protocol` inlined as a full body, `scientific-writing-conventions` offered as a uri |
| staffing | `Topic Research`, 3 roles, 4 deliveries (not `rust_sdlc`'s 5/14) | | retrieval still works | **CONFIRMED** — `ReadMcpResourceTool(uri="skill:global/web-search-triage")`; the flag did not collapse the arm |
| tap drain | **92** tool calls | | the corrected gate | **CONFIRMED installed** — new rules present in the live container, node present, no inert marker, 0 denials against 23 body-free `Bash` calls |
| attribution | **92 of 92**, across a phase with TWO passes and six turns | | attribution | 34/34, third perfect run |
| boundary | all 8 `Write`/`Edit` paths under `/mission/repo` | | `subagent` column | written, all null — **correct**, no subagents spawned |
| delivery arm | `inline`, 0 retrievals — correct, prod leaves the env unset | | **subagent path** | **STILL UNEXERCISED.** These agents did not delegate. The mechanism is proven against the real binary and unit-tested; it has never been watched populating `mission_events`. |
| judge | pass 0 `met=false` *"zero URLs — `grep -c http` returns 0"*; pass 1 `met=true` *"57 http references"* |
Two traps it surfaced. **The drain waits for the PHASE, not the pass** — To close it: run one mission whose task EXPLICITLY instructs delegation. That is
a phase on its second pass reports `tool.call = 0` and looks broken while what forces the path; a task that merely invites it does not.
being correct. And **an unfiltered diagnostic query is a time bomb**: a
`select ... from mission_events` with no `WHERE mission_id` was fine while
prod held one mission and silently wrong the moment a second launched. The
real drain is `WHERE phase_id = $1`; the sloppiness was in the diagnostic.
Unexplained and worth a look: one agent called the **`Agent` tool 4 times**. ## Open, in the order I would take them
Mission agents are spawning subagents, which nothing in our design accounts
for.
2. ~~**Measure Trigger properly.**~~ **Done — 2026-08-25.** `skill_delivery` 1. **The judge quota.** Everything with a `done_when` is blocked until it is
ships both arms; `index` sends name + `when_to_use` + a uri and the agent resolved. See the top of this file.
fetches bodies through the door. Runs 9/10 in `SKILL-USE-BASELINE.md` are the 2. **Watch the subagent field populate.** One mission, task explicitly
A/B: identical task, one server process, and **the first retrieval this instructing `Agent` use.
project has seen that the task did not ask for**. Each agent fetched the 3. **Confirm the last-run cards in the prod UI.** They read HISTORICAL data, so
skill bound to its own role and neither fetched another's. No Compliance unlike everything else this pass they should work right now against the two
regression appeared — 34% fewer tokens, both arms judged met, deliverables 2026-08-25 missions. If blank, suspect the roster join: `agent_last_run`
slightly larger. n=1 per arm, so that is a signal and not a rate. reaches agents via `team_members → mission_teams → missions`, so an agent no
longer on the mission's team will not resolve.
4. **Three fork items, all behind one runtime image rebuild.** Branch
`port/config-write-lock` on the zeroclaw fork carries upstream `841f28c7f1`
(cherry-picked CLEANLY despite being 331 behind; `handle_prop_put` — the
endpoint ClawMates writes through — now takes the lock). Not merged, not
deployed. Still to take: credential-fragment redaction (`eadaee0b62`) and
`/api/pair` lockout hardening (`47adb9863e`). Tank is offline, so the rebuild
has to happen on gw-04 and push to the web-01 registry.
5. **Mission egress remediation** — written up in `MISSION-EGRESS.md`,
deliberately NOT applied on the operator's call. A mission container reaches
the entire tailnet and host SSH.
6. **The 331-commit upstream merge.** 18 overlapping files; `claude_cli.rs`
cannot conflict (zero upstream files).
7. **Two silent-discard defects found by sweep, not yet fixed.**
`container_tool_hooks::install`'s outcome is discarded at both call sites and
recorded nowhere, so "did this mission run gated?" is unanswerable once the
container is reaped — and `PreToolUse` is now a security control, not just
telemetry. `vm_tool_gate::INERT_FILE` has no reader anywhere (low severity:
node v22 is in the image, so it cannot currently trigger).
What is left here is volume: run more pairs before believing any number, and ## State you should know about
decide whether `index` becomes the default. It is currently opt-in per
mission (`config.skill_delivery`) or per deployment
(`CLAWMATES_SKILL_DELIVERY`).
3. **Fold the microVM tier onto `container_tool_hooks`.** Two mechanisms for - **Local DB has test state**: `workspace-repo-commit-protocol.always_inject =
one job is how they drift — the argument-discarding bug lived in the shared true`, set by hand for the validation run. **Production has none set** —
parser precisely because nobody looked at it from the container side. The `always_inject` is deployed, defaulting off, changing nothing until someone
microVM path also still passes no turn agents, so its tool calls stay marks a skill.
unattributed. Blocked: the fleet has been offline for over a week. - **No production mission since 2026-08-25.** Everything above is deployed and,
apart from the last-run cards, unexercised in prod.
4. ~~**Pull upstream's egress policy**~~ — **mis-scoped; see - **Fleet offline 17 days** — `architect`, `morpheus`, `tank`; `nodes` table is
`MISSION-EGRESS.md` (2026-08-27).** `0db7d999a` guards ZeroClaw's own Rust empty. microVM tier stays blocked.
HTTP paths (`link_enricher`, `domain_guard`, `plugins/egress`). `claude_cli` - **`target/` is a symlink to `/Volumes/NVMeRAID`.** If it detaches mid-run,
spawns the claude binary as a subprocess, so a mission agent's `curl` never cargo dies with SIGKILL and then `Not a directory`. It is the drive, not a
touches that stack. Pulling it hardens the CHAT tier and leaves mission flaky test.
egress untouched — worth doing on its own merits, not as this fix.
What was measured instead: a mission container reaches the **entire tailnet**
and **SSH on its own host** (`clawmates_edge` supplies a default route; the
host runs tailscale; NAT does the rest). Postgres is reachable but
password-protected, and no DB credentials are forwarded. Remediation is
written down and **deliberately not applied** — operator's call was measure
and report.
5. **A narrow credential for the rest of the door.** `SCOPE_SKILLS_READ` covers
`/mcp/skills`. The `/mcp` door proper (`mcp_door.rs`) still authenticates
with a full session, and it is the one that can `delegate`. Nothing hands it
a token today; anything that does should not hand it a person's.
## Open decisions that are yours
- **Self-authoring scope.** Agents apply their own `skill_candidate` items with
no human click (`CLAWMATES_SKILL_SELF_AUTHORING=0` restores the gate).
`identity_refinement` and `brain_consolidation` still wait for a human,
because they change what an agent IS rather than adding a procedure it can
consult.
## Deliberately not done ## Deliberately not done
- **The mission executor swap.** Blockers are structural: `cm-runtime`'s `files` - The mission executor swap — blockers are structural (see prior handoffs).
tool rejects absolute paths by construction, `shell` runs in a per-agent - A tap for the direct-session tier — dormant, `CLAWMATES_MISSION_EXECUTOR`
sandbox with no mission mount, `ToolContext` carries no path or VM handle, and unset in production.
approvals key on `(session_id, message_id)`. - `cm-brain` offline tests — 6 of 9 need live `clawbrainhub.com`.
- **A tap for the direct-session tier.** Dormant —
`CLAWMATES_MISSION_EXECUTOR` is unset in production, so it never runs.
- **`cm-brain` offline tests** — 6 of 9 need live `clawbrainhub.com`.
- **Graph memory / `clawhdf5-agent`** — in the workspace manifest, used by no
crate.
## Operational facts that cost time to learn
- The Gitea **actions-log API returns 403** for the token in
`deploy/compose/.env`. A token with the `actions` scope remains the
highest-value thing to obtain.
- **gw-04 uses legacy `docker-compose`**, not the v2 plugin.
- **Do not build images by hand on gw-04 while CI may run** — same 150G volume,
and the frontend image build is what loses.
- The server reaches Docker through a **socket proxy** (`DOCKER_HOST`). Use
`container_exec::connect()`, never `connect_with_local_defaults()`.
- Prod auth is **Clerk**; the bootstrap password in `deploy/compose/.env` works
only against the local stack.
- Rebuilding the local server image is a **full Rust compile inside Docker**
(~8 min); the layer cache does not preserve `target/`. Budget for it before
any measurement that needs new server code.
- macOS has no `timeout(1)`.
## The recurring shape, now seven times over
**A claim in a comment or a doc, believed and never checked.** Every significant
finding this pass came from reading the source of truth — the parser, the
recipe, the production table — rather than the text describing it. The two new
skill contradictions were found *while writing checks against those skills*,
which is the cheapest place to catch them and the reason to always read first.
The corollary the measurement itself demonstrated: **its own first verdict was
wrong**, and scoring a research phase as a TDD failure would have buried the
real finding (item 1). A check that reports a system defect as an agent defect
is worse than no check.