Commit Graph
14 Commits
Author SHA1 Message Date
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 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 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 37f3f5abfd fix: mission_runtime_pairing_code in single-row mapping + fmt
ci / gates (push) Successful in 7s
ci / rust (push) Failing after 10s
ci / frontend (push) Successful in 28s
ci / e2e (push) Skipped
ci / publish (push) Skipped
2026-07-22 13:07:17 -07:00
Omar Sobh b569688e04 fix(mission_runtime): per-mission auto-pair via container log scrape (C3 auth)
ci / gates (push) Successful in 10s
ci / e2e (push) Skipped
ci / publish (push) Skipped
ci / rust (push) Failing after 23s
ci / frontend (push) Successful in 38s
The seed-mount approach didnt work: even with the shared runtimes
data dir bind-mounted, a fresh gateway instance mints a new pairing
key and requires re-pairing. The topology_worker connect returned
401 forever.

New approach — per-mission gateways self-pair:
- Provisioner tails container logs after start, extracts the
  X-Pairing-Code from the boot banner
- Persists it on missions.runtime_pairing_code (migration 0059)
- topology_worker constructs ZeroClawDriveExecutor with THAT code
  via from_env_for_gateway_with_code, which triggers the lazy
  /pair handshake on first turn and caches the returned bearer

Drops the shared-runtime data-dir mount — each per-mission gateway
now owns its own state, restoring the C3 isolation guarantee.
2026-07-22 13:06:25 -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 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 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 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 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 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