1678452a93aa9a306da23638c22b98b41d52139f
909
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
452b419729 |
Merge: an empty coding phase is a failure, not a completion
Verified on the deployed stack: verify-mission-delivery.sh all → 9/9, with the noop negative control showing 'phase 0 failed 0' where the same shape read 'completed' in mission 019fcf62. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
da3731d753 |
fix(missions): a coding phase that delivers nothing is a failure
The last open item in the silent-success class: a coding phase that changed no files reported `completed` — the same status a phase gets for delivering tested, reviewed, pushed work. Mission `019fcf62` completed that way with its agents silently unpinned from the repo, and nothing in the platform disagreed; it was found by a script diffing the forge. The verdict is applied at capture rather than at completion, because capture selects on `status = 'completed'` — the platform does not know whether a phase produced anything until after it has already finished. Three conditions must hold before failing a phase, because a false positive here fails honest work: the phase is a coding phase (research phases legitimately write nothing to the tree), the diff was actually computed (an uncomputable diff also reports zero files — blaming the agent for a platform fault is the same defect wearing different clothes), and `allow_empty` is not set. Only an explicit `true` opts out, so a typo leaves the check armed. Registered in phase_config with its reader named, per the seam-2 rule. Also closes an ordering hazard this exposed: capture is batched and runs after a phase completes, so a backlogged mission could close as 'completed' and only then have capture discover an empty phase — leaving a 'completed' mission holding a 'failed' phase, unfixable because the mission-close CASE only touches 'running' rows. A repo-bearing mission now waits for its work to be captured before closing. Adds a `noop` scenario to the harness: a phase told to change nothing, which PASSES only when the phase comes back `failed`. Same discipline as the uid self-test — a check that has never been seen to fire has not been shown to work. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
f8ca0ced9a |
Merge: copy-in/copy-out is the default mission filesystem
Verified with CLAWMATES_MISSION_FS removed from gw-04's .env — compose passes it through as empty, which under the old opt-in logic would have selected bind. scripts/verify-mission-delivery.sh all → 7/7. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
4f6719c80e |
feat(missions): make copy-in/copy-out the default filesystem model
Copy mode shipped opt-in so that changing how every mission receives its
code required someone to type it. Four production missions and a
fail-closed harness later, opt-in is the riskier setting: the bind path
is the one with four documented work-loss incidents, and leaving it as
the default means the untested path runs whenever nobody sets the
variable. `CLAWMATES_MISSION_FS=bind` still selects it; anything else —
unset, empty, misspelt — gets copy mode, so a typo lands on the safer
path rather than the one being retired.
Also fixes a real leak found while scoping the deletion below: the git
helper built its `safe.directory` argument with `Box::leak`, justified as
"the process is short-lived". That is true of a CLI and false of cm-api,
which is a long-running server — so it leaked one allocation per git
call, growing with every phase of every mission.
The A5 deletion is NOT done here, and two of its items should never be
done:
- `scrub_remote_credentials` is a security control, not a uid
workaround. Copy mode uploads the whole `.git` into a container the
agent controls as root, which makes stripping the token from
`.git/config` more necessary, not less.
- `has_local_work` / `checkout_in_use` guard `fetch_and_reset` at every
phase launch and have nothing to do with who writes the checkout.
The host checkout still persists across phases under copy mode —
mission `019fcf62` shows the marker firing there. Deleting them
reintroduces PRIOR-PHASE-WORK-WAS-LOST.
The rest (`share_repository_across_uids`, `clear_stale_commit_editmsg`,
`-c safe.directory`) are genuinely obsolete under copy mode but stay
while `bind` remains selectable: a workaround may only be deleted once
the situation it works around can no longer be chosen.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
3c91d0e172 |
Merge: stop three launch failures from passing as success
Verified against the deployed stack: scripts/verify-mission-delivery.sh all → 7/7, chain phase 0 now files=1 pushed=true (was 0 files, no error). Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
1253595ba7 |
fix(missions): stop three launch failures from passing as success
A verification run against the deployed stack found a chain mission whose phase 0 reported `completed` with zero files, no commit error and no push error — indistinguishable from a phase that correctly had nothing to do. Three separate defects had to line up, each of them the same shape: a failure sharing its representation with a legitimate negative result. 1. `pin_agent_workspaces` embedded the whole config in one `sh -c` argv. That works until the file grows — config gains a block per provisioned claw — then fails with `argument list too long`. Now written through the tar upload API, which has no argv limit, so the failure mode is gone rather than merely further away. 2. A failed pin was logged "(continuing)". Without the pin, agents write to their sandboxes and the committer finds nothing in /mission/repo — the mission cannot deliver, so the launch now fails where someone is still looking. The restart that applies the pin is fatal for the same reason. 3. `capture_phase_diff_at` swallowed `git diff` failures with `unwrap_or_default`, so an unreadable base landed `empty: true, files_changed: 0` — byte-identical to an honest no-op. The error is now recorded as `diff_error`, and an empty patch that came from a failed diff is no longer trusted to mean an unchanged tree. Adds scripts/verify-mission-delivery.sh, which found #1 and #2 on its first real run. Its probes are fail-closed: no placeholder values, a self-test that proves the uid probe can detect the split it looks for, and FAIL-NORUN for a scenario that never executed. Its own first version had this bug too — a `die` inside `$(...)` exited the subshell, so a run that could not authenticate printed "all checks passed" and exited 0. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
bb274d08c6 | Merge: copy-in/copy-out mission filesystem (flag-gated) | ||
|
|
7e07c389c6 |
feat(missions): wire copy-in/copy-out behind CLAWMATES_MISSION_FS=copy
With the flag set, ensure_container omits the /mission bind, the checkout is pushed into the container at phase launch, and the agent's work is pulled back before capture. The simplification that makes this small: sync_out unpacks over the SAME host path the checkout came from. The host directory stays a server-owned staging area with exactly one writer, and capture_phase_diff_at needs no change at all — it still finds a normal checkout exactly where it always has. Delivery, gating, commit and push are untouched. Two failures are deliberately loud rather than silent: - copy-IN failure fails the phase launch. Continuing would start a phase against an empty directory, and the agent would cheerfully report having done work on a repo that was not there. - copy-OUT failure SKIPS capture. Capturing anyway would diff a stale host tree and record "no changes" for work that exists — success reported for nothing, which is the exact failure mode this codebase keeps paying for. Opt-in: the bind path is what production has run since the beginning, and the test asserts a near-miss value leaves it there rather than silently switching every mission. 414 tests, clippy clean. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
389b41f8e6 |
feat(missions): copy-in/copy-out primitive for the mission checkout
The first half of removing the shared bind mount. Not wired yet — this adds the mechanism and its tests. One cause, four fixes so far: .git/objects permission denied (core.sharedRepository), the capture base being overwritten each phase, COMMIT_EDITMSG root-owned, and reset --hard deleting a prior phase's work (.git/clawmates-in-use). core.sharedRepository was never a general solution — it covers objects and refs, and every OTHER file git touches is a fresh opportunity. Copy-in/copy-out removes the cause instead: the agent owns its filesystem with no second writer. Measured before building, because the plan named copy cost as the open risk: a real 65 MB checkout of this repo copies in 0.23s and out 0.18s on gw-04. Not a risk at this size; re-measure an order of magnitude larger. No compression — the payload crosses a local socket, so gzip would spend CPU to save nothing. Two safety properties, both tested: - The archive comes back from a container the agent controls as ROOT, so it is untrusted input. A `../ESCAPED` entry must not write outside the destination. The test writes the tar header bytes by hand because the tar crate refuses to BUILD such an entry through its safe API — which is reassuring, but means the hostile case has to be constructed the way an attacker would. - Symlinks are packed as links, never dereferenced. Following them on copy-IN would smuggle host files into the container; the test plants a host secret behind a symlink and asserts its contents never appear in the archive. Ownership is deliberately not preserved on unpack: the archive's uids are the container's root, and re-applying them on the host would recreate the exact uid split this exists to remove. 413 tests, clippy clean. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
ac6bf72943 | Merge: per-mission runtime data (stop sharing the door token) | ||
|
|
0d9498ec6e |
fix(missions): copy an allow-list, not the whole 1.7GB seed dir
Checking before deploying caught a mistake in the previous commit. The seed dir on gw-04 is 1.7 GB and the first version copied all of it per mission — tens of seconds each, and ~17 GB across ten concurrent missions. 1.5 GB of that is .rustup: a Rust toolchain that installed itself into the data dir back when HOME=/zeroclaw-data and the image had no toolchain. The image now ships Rust at /usr/local/cargo, which is what the container's PATH actually resolves — verified live. The data-dir copy is dead weight and is not even reachable. SEEDED_PATHS now copies only what carries per-mission identity or secrets: .zeroclaw (config.toml with the door token, sessions.db, devices.db), clawmates-mcp.json, .claude + .claude.json, .kimi-code, glm-home, agents. Roughly 46 MB instead of 1.7 GB — about 37x smaller. Caches and toolchains are deliberately excluded: .rustup, .npm, .cargo, .cache, .local. They hold no secrets and a mission reads the image's. Absent paths are tolerated: a fresh deployment has no .kimi-code until Kimi is first used, and that must not fail container creation. The test asserts both directions — the token-bearing paths ARE copied and the caches are NOT — because either mistake is silent: copying everything just makes missions slow, and copying nothing quietly restores the credential sharing. 409 tests, clippy clean. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
15e7608e4a |
fix(missions): give each mission its own runtime data
Every per-mission container bind-mounted the SAME host seed dir as
/zeroclaw-data — shared with each other AND with the singleton runtime.
That directory holds config.toml, which carries the §15 door bearer
token, plus sessions.db and devices.db.
So one mission could read another mission's credential, and anything it
wrote there was inherited by every later mission. teardown_container
only removes /var/lib/clawmates-missions/{id}, so the shared directory
was never cleaned — the contamination was permanent.
The code already knew. The comment on DEFAULT_SEED_DIR names the sqlite
race and calls copy-on-write per mission the long-term fix. This is that
fix: seed_runtime_data copies the seed into
<missions_root>/<mission>/runtime-data at container create, and the
mount points there. Cleanup is free — teardown already removes that tree.
The copy runs in a throwaway container because cm-api cannot see the seed
dir: it hands that host path to Docker but never mounts it itself. The
runtime image is reused so nothing extra is pulled, and `cp -a /seed/.`
copies dotfiles — `/seed/*` would silently skip .zeroclaw/ and produce a
runtime with no config at all.
A copy failure is fatal to container creation on purpose. Falling back to
the shared mount would silently restore the credential sharing this
removes, and silent fallback to a weaker posture is the failure mode this
codebase keeps paying for.
The test asserts path shape rather than behaviour: an edit that points
the mount back at the seed dir restores credential sharing with no other
visible symptom, so the path IS the invariant.
408 tests, clippy clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
5d98fcf44a |
feat(missions): forward ZAI/KIMI keys so one binary serves three backends
All three providers run through the SAME `claude` binary, verified live: Anthropic CLAUDE_CODE_OAUTH_TOKEN -> ANTHROPIC-OK GLM ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic -> GLM-OK Kimi ANTHROPIC_BASE_URL=https://api.kimi.com/coding/ -> KIMI-OK That is a stronger multi-provider story than a provider-per-implementation: skills, subagents, MCP, hooks and tool policy are identical across all three because it is literally the same harness. The `kimi` CLI (0.31.1, shipped in the image) 401s on this key and is not needed -- the claude binary reaches Kimi's Anthropic-compatible endpoint directly. Worth knowing before someone debugs the CLI. forwarded_provider_keys now ships ZAI_API_KEY and KIMI_API_KEY into mission containers in BOTH auth modes: they are unrelated to the Anthropic credential, so the api_key/subscription split does not apply to them. A mission that selects a backend without its key present would otherwise fail at the first turn. Keys persisted in /opt/clawmates/.env and passed through compose. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
4ff4e6f7ee |
fix(missions): a root-owned COMMIT_EDITMSG must not block delivery
Mission 019fcd0c produced correct work — a reviewed, tested function plus a REVIEW.md quoting a real cargo test summary — and delivered none of it: git commit → exit 128: could not open '.git/COMMIT_EDITMSG': Permission denied The agent ran `git commit` itself inside the mission container (as root), leaving that file owned by root at 0644. core.sharedRepository covers objects and refs — .git/index lands at 0666, which is why commits work at all — but not COMMIT_EDITMSG, which git writes with the default umask. Unlinking works where overwriting does not: removing a file needs write permission on the DIRECTORY, and .git/ is owned by the server. Silent on failure by design, so the commit reports the real error rather than this speculative cleanup. Third distinct instance of the same uid-split class (objects, then the capture base, now this). The pattern holds: the checkout is one directory written by two users, and each new file git touches is a new opportunity. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
deb60be98d | Merge: direct session executor for missions (flag-gated) | ||
|
|
758b2dbd96 |
feat(missions): run a phase as one direct session, behind a flag
CLAWMATES_MISSION_EXECUTOR=session makes launch_phase run the whole phase as a single `claude -p` against /mission/repo instead of driving turns through ZeroClaw. Opt-in, because silently changing how every mission executes is exactly the sort of change that should require someone to have typed it. It still writes ONE topology_runs row. The entire downstream lifecycle -- close_finished_phases, evaluation, capture, commit, gate, publish -- keys off those rows, and inventing a second completion path would mean two ways for a phase to finish with one of them untested. The session is simply a run with tier='session' and an empty graph. Spawned rather than awaited: launch_phase runs inside the sweep loop, and blocking it for the length of a coding session would stall every other mission. The session's own summary is logged as diagnostics only. Whether the phase actually did anything is still decided downstream by capture and delivery against the repository -- a 0-exit session that pushed nothing was measured at ~5%, so the agent's account can never be the verdict. 406 tests, clippy clean. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
37fac288d2 |
feat(missions): bring the direct session executor onto a live branch
Rescues session_executor from the stranded spike branch. Multi-provider
missions are not needed for now, so the direct path is worth nailing down:
run a mission as one `claude -p` session against its checkout instead of
routing turns through ZeroClaw.
Measured today against a real checkout in the runtime container, using the
executor's exact argv:
direct `claude -p` 7s, file written
via ZeroClaw minutes per turn, and THREE config failures before
it worked at all (no credential in the mission
container; Write/Edit denied; no tools granted --
the last of which COMPLETED a mission having
written nothing)
Each of those failures came from the same root: with claude_cli, ZeroClaw
is a WebSocket-to-subprocess adapter whose own controls (risk profiles,
tool gating, memory) do not reach the subprocess. The adapter adds
failure modes without adding governance.
What ZeroClaw still earns for the rest of the platform is unchanged and
not in question here: interactive chat, the brain, A2A and door identity,
terminal, agent routines, and non-Claude providers.
Not yet wired into phase_runner — this commit only makes the executor
reachable and keeps it building. SessionOutcome::delivered() still
requires a clean exit AND an observed branch, because a 0-exit session
that pushed nothing was measured at ~5%.
405 tests, clippy clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
5232175c88 |
fix(missions): forward the subscription token into mission containers
Switching agents to claude_cli left missions hanging: the per-mission container had claude_cli configured but no credential, so `claude -p` waited forever. A phase sat at `running` for ten minutes with nothing in the logs — no error, because there is nothing to error on. The original subscription design assumed a persisted `claude /login` under a bind-mounted $HOME. That holds for the shared runtime and NOT for a mission container, which gets its own data dir and therefore no login. So subscription mode now forwards CLAUDE_CODE_OAUTH_TOKEN. The two Anthropic credentials remain mutually exclusive, and there is now a test asserting it in both directions: Claude Code ranks ANTHROPIC_API_KEY above the OAuth token, so shipping both bills the API while the deployment believes it is on the subscription — visible only on the invoice. Deployment: CLAWMATES_RUNTIME_AUTH=subscription and CLAUDE_CODE_OAUTH_TOKEN added to compose + .env on gw-04. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
ac47dcbe94 |
feat(runtime): run agents on the subscription via the real claude binary
provider_alias_for now resolves Claude models to `claude_cli.default`,
which spawns the actual `claude` binary, instead of `anthropic.default`,
which posts to the raw API with Claude Code identity headers. Agent work
is ~99% of tokens, so this moves essentially all of it onto the Max
subscription and onto the supported client.
The judge deliberately stays on the API key. If both rode one credential,
a single subscription limit would blind the verifier at exactly the
moment there is most to verify; this way a throttle degrades missions but
verification keeps working.
Runtime config (applied on gw-04, reloaded via loopback — remote admin
reload is disabled by design):
- [providers.models.claude_cli.default] with mcp_config pointing at the
§15 door, so a subscription agent can ACT and not merely reason
- disallowed_tools denies Claude Code's own Bash/Write/Edit/WebFetch so
the gated door is the ONLY actuator and nothing bypasses the audit log
- env CLAUDE_CODE_OAUTH_TOKEN = "$CLAUDE_CODE_OAUTH_TOKEN" — the $NAME
form reads the daemon env, keeping the token out of config.toml
- anthropic.judge swapped to the API key (0 oat01 left in config)
Verified before changing anything: the real binary returned SUBSCRIPTION-OK
through the token, then ENV-OK once the daemon carried it in env.
Note for future readers: /api/config/prop reflects what is CONFIGURED, not
what the binary supports — `openai` 404s there too. An earlier note that
the image "has no claude_cli in its schema" was true of the old :sync
image and is not true of the rebuilt one.
400 tests, clippy clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
ad89ef94cd |
feat(library): attribute a run to a mission, and prove what it contributed
`corpus_items.mission_id` has existed since the table landed and nothing could populate it. `POST /api/library/runs` now accepts `missionId`, which is the seam the wizard needs: a mission-driven run is the same run, tagged. `corpus::contributed()` answers the question a continuous mission has to be able to answer — did THIS run add anything new. Because `record` never reassigns mission_id on conflict, the mission that first found a source keeps the credit, so a rerun cannot inflate its own count by re-recording what an earlier run already held. The test asserts exactly that: two missions see the same paper, the finder reports 1 and the rerun reports 0. This is the check the 0030-0044 generation of continuous research did not have. It could run weekly forever and every run looked like success. The test also earned its FK: the first version attributed to a bare UUID and the database refused it. Attribution to a mission that does not exist is not attribution, so the test now seeds real mission rows. 400 tests, clippy clean. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
9de2cf34e4 |
feat(auto-merge): merge additive branches, refuse everything else
Closes the branch pile-up: a catalogue branch that only adds notes now merges into main by itself, so the work is actually in the vault rather than waiting in a branch nobody opened. Additive-only is measured from the diff, not assumed from the mission type. Three conditions, all required: the type declares additive_only, the run verified, and `git diff --name-status base...branch` contains only A entries. A research harvest that somehow rewrote a hand-written note is refused by the same check that lets its new notes through — which is the case the test pins down, asserting README.md on main is byte-identical afterwards. Renames and deletes count as non-additive. A rename is a delete plus an add and the delete half can destroy hand-written work. Unknown merge_policy values fail closed to Never. A typo must not grant auto-merge. The diff is taken against FETCH_HEAD, freshly fetched, using `...` so an unrelated commit landing on main meanwhile is not misread as ours. A conflicted merge aborts and leaves the branch for a human rather than wedging the checkout for the next run. merge_reason is always populated and surfaced in the API: a branch that quietly did not merge is indistinguishable from one never delivered. 399 tests, clippy clean. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
3124fd3c8f |
feat(library): weekly harvest on a systemd timer
Monday 07:00, Persistent=true so a week missed to downtime fires on next boot rather than leaving a silently empty library. 30-minute timeout so a wedged run cannot hold the slot until the following week. The script is deliberately thin — it calls the API and reports — so it never needs changing when the harvest does. Auth is a long-lived operator session in /etc/clawmates/library.token (root-only, 600); rotate by replacing the file. Exit status follows `healthy`, not paper count. A mature library shelves nothing most weeks and that is success; a run that errored is a failure even if it shelved something. The first manual fire caught a real bug in this script, in the opposite direction to this week's usual: the harvest genuinely shelved 15 papers and pushed them, and the reporter crashed on an escaped quote inside an f-string, so systemd marked the unit FAILED. A false failure destroys trust in the signal exactly as a false success does. The reporter now avoids backslashes entirely (it is embedded in a single-quoted shell string) and was proved against the real response shape before being trusted. Verified end to end on gw-04: run 1: 25 candidates, 10 already held, 15 shelved, pushed run 2: 25 candidates, 25 already held, 0 shelved, no branch, healthy Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
cf076bd8ea | Merge: paper library — corpus, arXiv harvest, vault catalogue, API | ||
|
|
107f0dbced |
feat(library): expose the library over the API
POST /api/library/runs harvests now; GET /api/library/items lists what the library holds. Thin wrappers — the work stays in crate::library — so a run can be started by a person, a schedule or the UI rather than only from an integration test. The response reports `healthy` explicitly rather than leaving a caller to infer it from an empty `shelved` list. A quiet week and a broken run both shelve zero papers, and collapsing those two is the exact ambiguity that cost most of this week. Failure reasons go to the log, not the response body: they can carry the remote URL and raw git stderr. AppState gains an optional blob store (the shelf), wired from the server binary where storage is already constructed. Optional because AppState::new is used by tests that never touch blobs; a route that needs it fails loudly rather than the constructor demanding it everywhere. 393 tests, clippy clean. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
09c6496725 |
feat(library): clone the vault, harvest our topics, push the catalogue
Completes the loop: the notes now land in the real vault. Topics come from what the project is actually working on — papers/dynamic-agentic- topologies.md (topology search, ADAS/Darwin-Godel/SwarmAgentic) plus the two problems this week ran into, verifying what an agent did and giving a long-running agent memory of what it covered. Never pushes to main. The vault is a live Obsidian vault a human edits and syncs; pushing to main races that sync and can lose hand-written work. Every run lands on its own branch for a human to merge, the same rule the mission delivery path was validated 20/20 under. PDFs are NOT committed. A few hundred papers is gigabytes and would make the vault painful to clone and slow to open, so they stay on the blob store shelf and the note carries the key. My own test caught me repeating this week's branch-collision bug: I named branches from the HEAD of a UUIDv7, which is a 48-bit timestamp, so two runs in the same millisecond produce the identical name — exactly what hit mission 019fc42b. Fixed by taking the tail. The test now loops 100 ids instead of sampling two (a one-shot check passes by luck whenever the millisecond ticks between calls) and additionally asserts the head-based scheme DOES collide, so it cannot rot into a no-op. Live against the real vault: 10 candidates, 1 already held, 9 shelved, 0 failed branch clawmates/library-019fc82292e8, pushed 9 notes verified on the forge, 9 PDFs verified %PDF on the shelf (the "1 already held" is cross-topic dedupe inside a single run) Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
30eaa50c50 |
feat(harvest): one run — find, skip what we hold, shelve the rest
Turns the parts into a job. Order is the point: the checkmark list is consulted BEFORE anything downloads. Checking afterwards would still dedupe the catalogue while re-downloading every paper we already have, every week, forever. Two properties the tests pin down, both learned the hard way this week: - A quiet week is not a failure. `shelved == 0` with no errors is a healthy run against a mature library; `shelved == 0` with errors is broken. Harvest::healthy() and ::added_anything() keep those apart rather than collapsing them into one ambiguous "did nothing". - A failed download leaves the paper UNSEEN. Checking it off before the PDF is safely shelved would mean one transient network error retires that paper permanently. The checkmark is written last, after the bytes and the note are both on disk. The skip test gives every candidate a pdf_url pointing at a closed port, so if the skip ever regresses the test fails loudly instead of quietly re-fetching. Live end-to-end against arXiv, run twice: RUN1 3 candidates, 0 already held, 3 shelved, 0 failed RUN2 3 candidates, 3 already held, 0 shelved, 0 failed Library<'_> groups the five values that always describe one library; passing them loose is how a run shelves into one place and catalogues into another (also silences clippy::too_many_arguments honestly rather than by allow). 391 tests, clippy clean. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
e4a395b72e |
feat(papers): find papers on arXiv, shelve the PDF, catalogue the note
Corrects a misread of the design. I had built this as "read the vault to find papers"; the vault is the CARD CATALOGUE, not the source. Papers are found on arXiv, the PDF is pulled down and shelved in our own library, and a note recording it goes in the vault. Three parts, and which is which matters: arXiv — where papers are found blob store — the shelf; the PDF lives there (cm-files, local + S3) the vault — the catalogue; one note per paper, pointing at the shelf The checkmark list (corpus, 0064) is what makes this continuous rather than a job that redoes itself every week — the failure that killed the previous attempt (0030-0044, dropped in 0053). The load-bearing detail: every catalogue note carries `source_id: arxiv:NNNN.NNNNN` in frontmatter, which is exactly the key corpus::parse_note reads. So the checkmark list is rebuildable FROM the vault. If the database were lost, re-indexing restores what we have — the catalogue is authoritative, the index is derived. A test asserts that round trip rather than trusting the two halves to agree. Version suffixes are stripped (2401.12345v3 -> 2401.12345) or a weekly job re-downloads a paper every time authors post a revision. Fetches are rejected unless the bytes start with %PDF: arXiv serves an HTML holding page while a PDF renders, and shelving that leaves a file that looks present and is unreadable. Verified against live arXiv, not fixtures: arxiv:2607.29678 TokTier: Exact Stateful Tokenization for Agentic LLM… arxiv:2607.29677 ExtractBench: A Benchmark for Schema-Guided Enterpri… arxiv:2607.29658 Reusing Past Repairs Through Hierarchical Trajectory… pdf: 1,361,770 bytes, %PDF verified 388 tests, clippy clean. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
6e5ccc25a6 |
feat(corpus): record what a continuous mission has already covered
Slice 2 of the adopt-or-build plan. A recurring mission's hard problem is
not running the agent — that is 23 seconds — it is knowing what it did
last time. This repository already tried continuous research once:
migrations 0030-0044 built research_topics/loops, 0053 dropped them all,
and the reason they could not survive is that research_topics carried a
status lifecycle but no seen-set. It could run forever and never know
what it had covered.
Two kinds of row, because the real vault forced it. The plan assumed
notes carry arxiv:/doi:/url: frontmatter. Measured against the actual
valhalla-vault: 416 notes, 145 with frontmatter, and ZERO with any of
those keys — the dominant keys are repo-sync metadata (node, org, gitea)
and course fields (presenter, session). An ingester keyed only on
external identity would have indexed nothing, which is the same shape of
failure as everything else found this week. So `note` rows record
coverage (keyed by path) and `source` rows record consumption (keyed by
natural id); a continuous mission needs both.
Two decisions the data forced:
- `source:` is deliberately NOT an identity key. The vault uses it for
local paths of course material (/Users/quantum/Downloads/...), which is
provenance, not citable identity. Accepting it would fill the seen-set
with 25 rows keyed on a laptop path.
- The hash covers the body, not the whole file. Repo-sync notes rewrite
updated:/size_kb: on every sync without the prose changing; hashing the
file would report 103 phantom edits per run and make "unchanged"
meaningless.
Authoritative in Postgres rather than ZeroClaw memory, per the Slice 1
spike: memory is agent-scoped and mission agents are ephemeral
claw_<uuid> aliases (~100 already present). A seen-set that disappears
with the agent that wrote it is not a seen-set. The spike did find that
POST /api/memory upserts by key, so mirroring content there later would
inherit idempotence for free if keyed by source_id.
Verified against the live 416-note vault, not a fixture:
PASS1 { scanned: 416, inserted: 416, updated: 0, unchanged: 0 }
PASS2 { scanned: 416, inserted: 0, updated: 0, unchanged: 416 }
382 tests, clippy clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
2380c2cb0b |
fix(deploy): identify agent images by build stamp, not image ID
The post-transfer verification added in
|
||
|
|
ec85f6c8da |
fix(missions): close the three seams behind this run of failures
Seam 1 — delivery inferred checkout state from the tree, so whether work survived depended on what the agent happened to do. 019fc444 committed and left a clean tree; 019fc476 had its base advanced to match HEAD; 019fc450 survived only because a phase FAILED to commit and left the tree dirty. Same code, opposite outcomes, decided by the agent. mark_phase_started records the fact at phase launch, before the agent acts, so every one of those states answers identically. The tree checks remain as a second line of defence for pre-existing checkouts. Seam 2 — phase config was accepted, stored and read by nobody. That was `task`: every phase of every mission got identical instructions. The new phase_config registry names the reader for each live key and lists the eight that are declared-but-unimplemented, reporting both at mission creation so an author sees what will not happen. Its CI test found one I had missed: security_hardening.toml sets phase-level mcp_bundles asking for gitea_forge + security_scan, but bundles come from the TEAM template and the phase gets neither. Seam 4 — push_url_for collapsed a failed query, an unbound repo and a missing clone_url into one None, so a database fault was recorded as "nothing to push to" and metadata read `pushed: null, push_error: null` — the same ambiguity commit_error already fixed. Each case now carries its reason into the artifact, and a local git failure during publish is recorded rather than dropped by .ok(). Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
bb34ef1b7e |
fix(deploy): verify agent images landed instead of trusting the pipe
`docker save | docker load` across two SSH connections spliced through a workstation truncates when either side stalls — observed as `unexpected EOF` mid-deploy. Nothing checked afterwards, and `docker load` can exit 0 on a short stream, so a partially-populated image could ship to every fleet node and look like a success. Now compressed, pipefail-guarded, and verified by comparing image IDs on the target after the transfer, with one retry for the transient stall. A failed transfer fails the deploy rather than passing quietly. The runtime image no longer travels this path at all: it is registry- hosted now (100.94.185.103:5000/clawmates-runtime:v083-toolchain), built from deploy/clawmates-runtime/Dockerfile on tank. Only agent-base / agent-browser / agent-terminal still need save|load, because they exist in no registry. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
f7e336ff5f |
fix(missions): make an unrunnable test suite legible, and check the runtime at boot
Two changes against the same defect: the platform could not tell a missing capability from a legitimate negative result. verify_tests returned Option<bool>, collapsing four outcomes into None: no suite found, docker unreachable, exec failed, and no exit status. When clawmates-runtime shipped without cargo, every on_green_tests phase returned None and landed on -wip — identical to the reading for "this repo has no tests", which is the conclusion I drew and reported. The gate was correct throughout; it simply could not say why it was unproven. TestOutcome now names the four cases. Gating is unchanged (only Passed clears, unproven is never a pass), and tests_verified keeps its tri-state meaning for existing readers. tests_status and tests_detail are new, so an artifact distinguishes no_suite from could_not_run, and a CouldNotRun is logged as the infrastructure fault it is rather than passing quietly. runtime_preflight probes the runtime container at boot for every tool the platform invokes inside it and names what each absence disables. This is the check that was missing: the Dockerfile gained a toolchain, the image was never built, gw-04 ran the old one for days, and the only symptoms were an ungated suite and a security scan that scanned nothing. A report, not a gate — a missing scanner should stop us believing a scan, not stop the server. Its test guards the probes themselves, since a typo would produce a permanent false "missing" and train operators to ignore it. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
9bdc3cd89b |
fix(missions): stop titling commits "phase phase work"
Mission 019fc4e0 pushed "clawmates: phase phase work" — the iteration
marker was interpolated into a slot whose default already said "phase".
A rerun read correctly ("pass 2 phase work"), so only the common case was
wrong. Cosmetic, but it lands in the operator's git history under their
own name now that delivery commits as them.
Subject is now "clawmates: phase work" and "clawmates: phase work
(pass 2)". The test covers both, since the bug lived only in the branch
the previous shape did not exercise.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
ddab8e35f5 |
feat(missions): commit as the operator, overridable per deployment
Delivery commits now carry "Omar Sobh <[email protected]>" by default, so pushed branches associate with the operator's forge account the way their own commits do. CLAWMATES_COMMIT_NAME / CLAWMATES_COMMIT_EMAIL override it — a shared instance wants a bot identity, not a person's. This is attribution, not the fix. What made 019fc450's phase fail was the *absence* of any identity: the server container has none of its own, so git commit exits 128 regardless of which name would have been used. That was fixed in 25d9805; this only changes the value. The push credential is GITEA_TOKEN throughout and is untouched by any of it. Since the author line now names a person, the commit body says plainly that agents authored the work — otherwise autonomous commits would be indistinguishable from hand-written ones in git log. Also fixes 13 stray spaces that a string continuation had baked into every message body. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
1a979f500f |
fix(missions): judge local work against the remote tip, not the capture base
Mission 019fc476 lost phase 0's work again, and this time the cause was the interaction between two fixes I had just shipped. has_local_work compared HEAD against .git/clawmates-base to decide whether a checkout held mission work. advance_base_commit moves that marker to each phase's committed head. So the moment a phase committed successfully, base == HEAD, has_local_work reported "pristine", and the next phase's launch reset the work away. Phase 1 wrote CHAIN_MISSING.md. The preceding mission survived only because its phase 0 FAILED to commit and left a dirty tree. Fixing that failure is what exposed this one. One marker was carrying two meanings: "where should the next diff start" (rolling, per phase) and "is this checkout untouched" (fixed for the mission). Only the first belongs to clawmates-base. The second is now `origin/<branch>`, which does not move for the life of the mission, so a HEAD that differs from it means a phase committed — one commit ago or five. An unresolvable remote ref preserves, since wrongly skipping a refresh costs staleness while wrongly resetting destroys a phase. The existing test passed throughout because it never advanced the base. It now does, which makes it a reproduction rather than a restatement, and it needs a real bare origin to resolve origin/main the way a clone does. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
25d9805806 |
fix(missions): commit under the pipeline's own git identity
Mission 019fc450 lost its first phase to: git commit → exit 128: Author identity unknown The server container has no git identity — `git config --global user.email` exits 1 — so any commit fails unless one is supplied. This is the third consecutive failure whose trigger was agent behaviour rather than our code. Earlier missions committed only because an agent had happened to run `git config user.email` in the checkout, leaving a local identity the server inherited. Alongside the object-permission split and the reset, the pattern is the same: delivery depended on incidental side effects of what an agent chose to do, so identical missions succeeded or failed for reasons invisible in our code. Supplied via GIT_AUTHOR_*/GIT_COMMITTER_* env on every git call, which overrides config without a leaked string per invocation and names the committer as the pipeline. Agents' own commits keep the identity they set. The test asserts the identity *overrides* an existing local config rather than trying to unset the developer's global — an override necessarily also applies when config is absent, and it does not race parallel tests. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
08b2adae23 |
fix(missions): stop resetting a checkout that holds mission work
Mission 019fc444 ran two coding phases. Phase 0 created ALPHA.md and
delivery committed it; phase 1 then started and ALPHA.md was gone from
the working tree, so the second phase never saw the first's output.
`ensure_checkout` is called at every phase launch, not once per mission,
and its reuse path runs `git reset --hard origin/<branch>`. That is right
for a checkout picked up cold and destructive for one mid-mission.
Delivery is what made this reachable. Before the mission branch existed,
agent output stayed untracked and a hard reset left it alone. Committing
it makes it tracked, and tracked files absent from origin/<branch> are
exactly what a hard reset removes — so the slice written to stop work
being destroyed is what put it in reach of the thing destroying it. The
flagship shape is the casualty: in research_and_code, the coding phase
never sees the research brief.
`has_local_work` now gates the refresh. It checks both a dirty tree and a
HEAD that has moved off the recorded base, because the two failure shapes
differ: an agent that committed leaves a CLEAN tree at a new HEAD, which
a dirty-tree check alone would miss — and that is precisely the shape
being destroyed. With no recorded base it preserves, since wrongly
skipping a refresh costs staleness while wrongly resetting costs a phase.
This also makes the base-advance fix in
|
||
|
|
5b53705c97 |
fix(missions): let the server and the agent share one git checkout
Mission 019fc437 lost both phases' work to:
git add → exit 128: insufficient permission for adding an object
to repository database .git/objects
cm-api runs as uid 65532; the mission runtime container runs as root;
they share one bind-mounted checkout. Git's .git/objects/xx/ fan-out
directories inherit the ownership of whoever creates them, so an agent
that writes objects first locks the server out of those directories.
The failure is intermittent, which is why the previous run looked clean.
Mission 019fc42b's agents committed their own work, so the blobs already
existed and the server's `git add` never had to write one. Same template,
different agent behaviour, opposite outcome.
`core.sharedRepository` is git's own mechanism for this: objects and refs
are created group- and world-writable, and both parties read the setting
from the shared .git/config. It grants the agent nothing — it is already
root over the whole checkout — and unblocks the server, which was the
party being refused. Applied on clone and on checkout reuse.
Two supporting changes. The artifact now records `commit_error`: this
failure surfaced as `branch: null, push_error: null`, indistinguishable
from a phase that never had work to commit, with the reason only in host
stderr. And the test seeder now calls the production setup function
instead of reimplementing it — building the checkout by hand is what let
a clone-path defect stay invisible to fourteen tests.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
8bad869248 |
fix(missions): give each phase its own task and its own capture base
The first push run against a scratch repo (mission 019fc42b) delivered two branches correctly but exposed two bugs behind them. Per-phase instructions were inert. `phase_task_text` took only (kind, title, description), so `mission_phases.config.task` was accepted by the API, stored, and read by nothing. Every phase of a mission received byte-identical text differing only by the kind directive — so both coding phases did the whole mission instead of their slice, producing the same two files. The task now reaches the agent as a trailing THIS PHASE'S TASK block, scoped against the shared brief. The capture base never advanced. `.git/clawmates-base` is written once at clone time, so phase two diffed against the original clone point and reported the union of both phases' files as its own. It now moves to each phase's committed head after the patch is on disk; the pushed branch stays cumulative because it is built from HEAD. Both regression tests were confirmed to fail with their fix disabled. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
e2871c4361 |
feat(missions): publish the mission branch, gated by commit_policy
Completes delivery. A phase's work is now captured, committed, gated and pushed — in that order, so every failure costs strictly less than the one before it. Publishing is last for a reason. By the time it runs the patch is on disk, the artifact is registered and the work is on a local branch, so a rejected ref, a rotated token or an unreachable forge costs a push and nothing else. A test pushes at a path that does not exist and asserts the commit is still there afterwards. The gate decides the branch name, never whether the work survives: - green, or policy `always` → `clawmates/mission-<m8>-<p8>` - red / unrunnable / no suite → `…-wip` - `on_reviewer_approval` → `…-review` Both land on the forge. A human can inspect, fix and re-push a branch; nobody can recover work discarded for failing a test. Deleting a red branch reproduces the old behaviour on purpose rather than by accident. `verify_tests` runs the project's own suite through the runtime container and returns `Option<bool>` — `None` for "could not establish", which the gate treats as unproven. An unreadable exit status is not a pass. That is the same fail-closed stance as the phase evaluator, and it is here because this tranche has now found four separate things reporting success while doing nothing. Never force-push. A rejected update is reported and left alone: the remote ref belongs to whoever set it, and overwriting it to make delivery look tidy is how a mission eats someone else's commit. The push URL is built fresh from the repo row and the ambient token, not read from `.git/config` — which no longer carries credentials, since agents run as root in a container that mounts the checkout. Tests push to a real `git init --bare` remote and assert the ref and its content actually arrived. A mock would have accepted anything. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
3ea288dbb5 |
fix(missions): every phase of a mission shared one branch
`branch_name` took `[..8]` of both the mission and the phase id. Both are
UUIDv7, which leads with a 48-bit timestamp, so ids minted in the same
millisecond — which is exactly what happens when a mission inserts its phases
in one transaction — share their leading hex. Production produced:
clawmates/mission-019fc40e-019fc40e
for both the research and the coding phase. Each phase's commit moved the ref
the previous one had just set, so a two-phase mission ended with one branch
and the earlier phase's work reachable only by sha.
The segments now come from opposite ends: the mission keeps its time-ordered
prefix so branches group and sort usefully, and the phase contributes its
random tail so siblings cannot collide.
The existing test missed this because it compared iteration 0 against
iteration 1 of the *same* phase, where the `-i2` suffix guaranteed a
difference. The new test asserts the precondition explicitly — two v7 ids
minted together do share leading hex — and then that their branches differ
anyway.
Also adds the `commit_policy` gate, which three workflow recipes have declared
since they were written with nothing reading it. Two properties it must have:
a failed gate redirects work to `<branch>-wip` rather than discarding it, and
an unrunnable or undiscoverable test suite counts as unproven, never as green.
`discover_test_command` returns None for a `package.json` with no test script,
because `npm test` exits non-zero for a missing script and would read as a red
suite rather than an absent one. Not yet wired to publishing.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
ca1fd46e08 |
feat(missions): commit captured work to a branch of its own
Second half of delivery, minus the push. After the patch is on disk and the artifact registered, the phase's work is committed onto `clawmates/mission-<mission8>-<phase8>`, with `-i<N>` for re-runs so a second pass cannot collide with the first. Three rules hold throughout: - Never the default branch. The name is derived from the mission and phase, so a mission can only ever add a ref nobody else owns. - Never force. A rejected update gets reported, not overwritten. - The same exclusions as capture. What was too noisy for a patch is too noisy for someone's history — build output, vendored trees, and the workaround files agents write when infrastructure fights them. A test drops a 50 KB binary in `target/` and a `.gitconfig_temp` beside the real change and asserts neither is committed. Ordering is deliberate: commit runs *after* capture, and a commit failure is logged without failing the capture. The patch is the guarantee; the branch is the convenience on top. The branch is created even when there is nothing to stage, because agents often commit their own work — `rust_sdlc` has a committer role — and that commit is unreachable once the checkout is reaped unless a ref points at it. One test changed meaning rather than breaking: it asserted capture left the working tree untouched, which was correct while capture stood alone. Capture now commits, so it asserts the new invariant — work on a namespaced branch, a clean tree, and the created file present in the commit. Push is still deliberately absent. Everything here is local, so a bug costs a retry rather than reaching a remote. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
a0e6b16abc |
fix(missions): stop agents having to work around git ownership
The captured diff from mission 019fc3ba contained the deliverable and, beside
it, a file the agent had invented:
+++ b/.gitconfig_temp
+[safe]
+ directory = /mission/repo
The server clones as uid 65532 and the mission container runs as root, so
every `git` an agent runs is refused with "detected dubious ownership". Agents
do not surface that as a failure — they improvise around it, and the
improvisation lands in the repository. Left alone it would have been committed
and pushed to the user's repo alongside the real work.
The judge got `GIT_CONFIG_*` for this in dd8dad2; the mission containers never
did. They do now — git's environment form of `-c`, inherited by subprocesses,
so it covers the agent's own git, the `git_operations` tool, and anything that
shells out. Scoped to the checkout, never `--global`.
`.gitconfig_temp` is also added to the capture exclusions. The cause is fixed,
but a stray workaround from some future agent should not reach a user's
repository, and the exclusion costs nothing.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
3a383aede6 |
fix(missions): give the uncapturable marker a real file
The marker registered an artifact at a path with nothing behind it, so any reader following it would get a bare 404. `_outputs` survives teardown even when the checkout does not, so the file can and should be written — and it says plainly what happened rather than leaving an operator to infer it from an empty response. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
e089360ac8 |
fix(missions): unblock the capture batch, and restore fetch auth
Two defects, both found by running a second real coding mission (019fc3ba)
after the first round of fixes. The agent created the file correctly this
time — `file_write` did its job — and capture still produced nothing.
**Head-of-line blocking.** `capture_phase_diff` returns `Ok(None)` when the
checkout is gone, and the caller treated that as success without recording
anything. The phase therefore stayed eligible forever, and because the batch
is bounded at five, five reaped phases from earlier test missions occupied
every slot permanently. A freshly finished coding phase, with its checkout
still on disk, was never reached — and nothing was logged, because nothing had
failed.
Fixed on both axes: an unreachable checkout now writes a `code_diff` marker
recording `captured: false` and why, so the row stops being selected; and the
batch orders newest-first, so live work is captured before archaeology. The
marker also distinguishes "this phase changed nothing" from "we lost the
checkout before looking", which an operator reading the mission needs to be
able to tell apart.
**Fetch lost its credentials.** `scrub_remote_credentials` (P1.1) strips the
token from `.git/config` so agents running as root cannot read it — but
`fetch_and_reset` fetched from the stored remote, which is now anonymous:
git fetch origin <branch> → exit 128:
fatal: could not read Username for 'https://git.redclaw.dev'
I accounted for push building a fresh authenticated URL and overlooked that
fetch needs one too. `fetch_and_reset` now takes the authenticated URL the
caller already computes, as does the `--unshallow` deepen. Stderr stays
redacted.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
409ca65ee7 |
fix(missions): capture from the clone point, and let agents create files
Two defects found by running a real coding mission (019fc372) rather than a test. Both made a coding phase look like it produced nothing. **Capture measured the wrong baseline.** It diffed the working tree against HEAD, which is correct only while work stays uncommitted. `rust_sdlc` has a *committer* role, so committing is the intended path — meaning a mission that did its job properly leaves a clean tree and captured nothing. That is exactly what happened: the agent created `DELIVERY_PROBE.md`, committed it as `aa3be95`, and the artifact recorded `empty: true` beside a commit that plainly contained the work. `mission_workspace` now records the clone point in `.git/clawmates-base` (in `.git/`, so it travels with the checkout, stays invisible to the repository, and cannot be reached by an agent through its pinned workspace), refreshed whenever `fetch_and_reset` moves HEAD. Capture diffs from there, covering committed, staged and unstaged changes in one pass. Checkouts predating the marker fall back to HEAD and say so via `base_recorded: false`. **Agents could not create files.** `coding_readwrite` granted `file_edit` but not `file_write`. `file_edit` replaces an exact existing string and rejects an empty `old_string`, so creating a new file was impossible. The mission transcript is unambiguous: "the tool rejected empty old_string... the shell is restricted", after which the agent worked around it through `shell`. The comment above that profile has claimed it grants file_write since the day it was written; the list never contained it. Also broadens capture from coding/benchmark/security_scan to every phase kind of a repo-bearing mission: `phase_task_text` tells research phases to "save findings under /mission/repo/research/", so filtering by kind would have discarded every research brief such a mission produced. Regression tests cover committed-only and committed-plus-uncommitted work against a real git repo. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
322c1be89c |
feat(missions): capture runs automatically, and once more before teardown
Wires diff capture into the two sweeps that matter. `phase_runner::sweep_once` gains `capture_finished_coding_phases`, guarded by `NOT EXISTS (code_diff for this phase)`. Deliberately a separate step rather than a hook on `close_finished_phases` or `evaluate_finished_phases`: a phase reaches `completed` through one or the other depending on whether it declared a `done_when`, so hanging capture off either would silently skip half the missions. The guard also makes it retryable — a capture that errors is simply re-selected next tick. `mission_runtime::sweep_once` captures anything still outstanding immediately before `teardown_container`, which deletes the checkout. This covers what the phase sweep structurally cannot: a mission that ended `failed` mid-coding still has real work on disk, and reaping it unexamined destroys the only evidence of what the agents actually did. Applies to coding, benchmark and security_scan phases — all three operate on a repo. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
716ee9a304 |
feat(missions): capture a coding phase's diff to durable storage
First half of mission delivery: the work is captured before anything is published. A coding mission has until now produced nothing durable — the checkout is deleted thirty minutes after completion and `register_artifact` had no callers at all, so the only surviving output was an LLM narrative of what the agents said they did. `capture_phase_diff` writes `diff.patch`, `diffstat.txt` and `delivery.json` under `<missions_root>/_outputs/<mission>/<phase>/` and registers a `code_diff` artifact. That directory is a *sibling* of the per-mission directories the sweeper removes, and outside every bind mount handed to a container — so teardown cannot take the record with it and agents cannot edit their own evidence. Three details that decide whether this works at all: - `git add --intent-to-add` before diffing. Untracked files are invisible to `git diff`, and a phase that only *creates* files is the likeliest shape for generated code — silently capturing an empty patch would be the worst possible failure. The index is reset afterwards so capture leaves the tree exactly as the agents left it, which the test asserts. - Build output is excluded by pathspec (`target`, `node_modules`, `.venv`, …). A phase that ran `cargo build` leaves a directory larger than the repo. - An empty diff is still an artifact, flagged `empty: true`. "This coding phase wrote no code" is currently invisible to an operator and is worth saying out loud. `RegisterArtifact` gains `metadata`, which the column has had since 0047 and nothing ever wrote; the diffstat and base sha go there. No migration needed — `kind` is unconstrained TEXT and the column already exists. Tests run against a real `git init` repo rather than a mock: every bug in this area so far came from git behaving differently than assumed, and a fake git would have agreed with the assumption. `capture_phase_diff_at` takes explicit paths so parallel tests cannot race through the process-global CLAWMATES_MISSIONS_ROOT — the first version of these tests did exactly that and two of four failed non-deterministically. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
ea3d145aac |
fix(missions): stop leaving an access token in every mission checkout
`with_ambient_auth` embeds GITEA_TOKEN in the clone URL, and git persists that
URL verbatim as the `origin` remote. The checkout is bind-mounted into a
container the agents run in as root, so the token sat in a file every mission
agent could read — and it reaches every repository that token reaches, not
just the one being worked on.
The remote is now rewritten to the bare URL immediately after clone. Delivery
does not depend on the stored URL: it will build a fresh authenticated URL at
push time, which also means a rotated token starts working at once rather than
after the next clone. Best-effort and non-fatal — a checkout that keeps its
token still works, and failing a mission over it would trade a real capability
for a situation already logged.
`strip_credentials` only treats an `@` in the *authority* as a separator, so a
path containing `@` (scoped npm-style names) is left alone.
Also, two changes delivery needs:
- `--depth 1` becomes `--filter=blob:none --single-branch`. A shallow clone
usually cannot push a new branch ("shallow update not allowed"), which is
exactly what mission delivery must do. A partial clone keeps full history —
so a base commit stays meaningful and a diff has something to be relative
to — while fetching blobs on demand.
- `fetch_and_reset` deepens a pre-existing shallow checkout once, up front,
rather than letting the push fail later with work on the line.
Fetch stderr is now redacted too; it can echo the remote URL.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
dd8dad2ad4 |
fix(evaluator): git ownership exception now reaches tools that call git
The argv rewrite added in
|