Commit Graph
11 Commits
Author SHA1 Message Date
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