Commit Graph
13 Commits
Author SHA1 Message Date
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 9bc5f6a142 fix(missions): bind graph nodes to claws via attrs, not a dropped top-level key
`inject_node_agents` wrote the claw alias as a top-level `"agent"` key on
each graph node, but `cm_topology::Node` only deserializes `{id, role,
level, attrs}` — serde silently dropped it. `TurnRequest::agent` came back
`None` and every mission turn fell back to `ZEROCLAW_DEFAULT_AGENT`
(`scout`), running with scout's workspace and tools instead of the
mission's claws. The runtime trace confirms it: every turn logged
`"agent_alias":"scout"`.

That is why mission agents reported an "empty greenfield" workspace and
emitted artifacts inline instead of writing them: scout is jailed to
`/zeroclaw-data/.zeroclaw/agents/scout/workspace` and cannot see
`/mission/repo`. The per-mission provisioning and `workspace.path` pinning
shipped earlier were correct — they were just applied to agents that
nothing ever drove.

- bind into `node.attrs["agent"]` (top-level key kept for display/debug)
- extract the DB-free `apply_node_agents` and add a regression test that
  round-trips through the real `TopologyGraph` deserializer, which is the
  guard that was missing
- log loudly in `topology_exec::run_turn` when a node falls back to the
  default agent, instead of silently swapping in a different agent

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 14:02:08 +02: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 212e68f6b1 research/probe: end-to-end smoke test endpoint (skip topics/loops/spawn)
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 48s
ci / rust (push) Failing after 2m34s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
New: POST /api/research/probe — one-shot pipeline probe against the
workspace's shared ZeroClaw gateway. Drives a trivial 'reply OK'
turn via ZeroClawDriveExecutor::drive and reports per-step timings.

Purpose: stop guessing what's broken in the wizard flow by exercising
JUST the executor→daemon→claude→response path. If probe succeeds
within a few seconds, we know:
- ZEROCLAW_TOKEN + gateway URL config is correct
- Daemon can reach and authenticate against claude
- The full ws round-trip works
…and every other failure we've been chasing (turn timed out, LLM
request failed, ws connect DNS error, etc.) is spawn-config or
prompt-size specific.

Request body (both optional):
  { "prompt": "Reply OK", "agent": "coordinator" }

Response:
  {
    "verdict": "ok" | "fail",
    "total_duration_ms": …,
    "prompt_len": …,
    "response_preview": "OK",
    "steps": [
      { name: "build_executor", duration_ms: …, status: "ok" },
      { name: "drive_turn",     duration_ms: …, status: "ok", detail: "tokens=N, output_len=M" }
    ]
  }

Wire-up:
- new routes/probe.rs
- pub mod probe in routes/mod.rs
- POST /api/research/probe registered in lib.rs router
- ZeroClawDriveExecutor::drive promoted from private to pub so the
  probe handler can call it (behavior unchanged, other callers were
  all inside the same struct).

Usage from anywhere (curl, browser dev tools, etc.):
  curl -X POST https://clawmates.work/api/research/probe \
    -H "Content-Type: application/json" \
    -H "Cookie: <session cookie>" \
    -d '{}'

Follow-up: a small frontend button (e.g. bottom of ResearchList) that
POSTs this and renders the response inline, so users don't need to
curl. Skipping in this commit to ship the useful part first.
2026-07-11 10:21:23 -07:00
Omar Sobh 3373f57da0 topology_exec: bump TURN_TIMEOUT to 700s to outlast daemon claude_cli
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 28s
ci / rust (push) Failing after 1m51s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
Verbose daemon logs revealed the real turn failure: the daemon's own
claude_cli provider was timing out at 180s while running a 10-15k
token coordinator prompt. The daemon reads its per-provider timeout
from ZEROCLAW_providers__models__claude_cli__default__timeout_ms —
we set that to 600000 (10 min) on gw-04 via a compose patch. Now the
executor's TURN_TIMEOUT (was 300s) must exceed the daemon's own
limit, or we kill the ws before the daemon can reply.

New sequence: daemon has up to 600s to invoke claude and return a
response; executor waits up to 700s (100s headroom) for the ws
event stream to drain. If claude takes 500s, both survive. If the
daemon really does hang past 600s, its own timeout fires first and
we get a proper "provider timed out" error instead of a phantom
executor timeout.

Compose env applied on gw-04 in the same session:
- ZEROCLAW_providers__models__claude_cli__default__timeout_ms=600000
- ZEROCLAW_providers__models__claude_cli__door__timeout_ms=600000
(backup: /opt/clawmates/docker-compose.yml.bak-timeout)
Server container recreated to pick them up; env verified.

Follow-up: the coordinator prompt is legitimately huge (autonomy
contract + roster + description + repo tree + integration-plan
template + operator notes = 10-15k tokens). We should consider
either shrinking it or breaking the work into multiple smaller
turns so the daemon isn't gambling on a single call taking 3-8
minutes.
2026-07-11 09:37:52 -07:00
Omar Sobh 63305689be topology_exec: bump TURN_TIMEOUT to 300s
ci / gates (push) Successful in 20s
ci / rust (push) Successful in 4m25s
ci / frontend (push) Successful in 36s
ci / publish (push) Successful in 3m57s
ci / e2e (push) Has been skipped
Turn timeout was 90s. Research coordinator turns run 60-120s
routinely (long prompt + cold tool selection) — 90s was tripping
legitimate turns while the daemon was still working, throwing away
completed inference. 300s gives real turns room while still bounding
worst-case at a walk-away limit.

Latest wizard-created topic hit this precisely: bridge attached,
handshake worked, claude auth verified in the container, but the
run's status went to failed with 'turn executor failed: turn timed
out' after exactly ~90s.

The topology worker's stale-run sweep at 180s covers the case where
the worker itself dies mid-turn — a 300s turn still checkpoints
every step so a stuck worker will get requeued.
2026-07-10 15:33:20 -07:00
Omar Sobh 88c78bd16e research: topology_worker points executor at per-topic gateway (commit 2/3)
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 31s
ci / rust (push) Successful in 2m41s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m28s
Commit 2 of the path-B plan. The container that commit 1 spawns
now actually receives the run's turns — up until now it was
started but unused. This is the payoff commit: research runs are
truly isolated per topic.

Backend

- topology_exec.rs: from_env() refactored to a thin wrapper over a
  new from_env_for_gateway(url) helper. Same shape (env-derived
  aliases + default + token) but the caller supplies the URL. The
  auth token/pairing code still comes from ZEROCLAW_TOKEN /
  ZEROCLAW_PAIRING_CODE on the server; research_container's
  inherited_env propagates those into the team container so the
  same credentials work at both endpoints.

- research_container.rs: new wait_ready(url, deadline) that polls
  <url>/health with a 1.5s per-request timeout every 500ms until
  it 200s or the deadline passes. reqwest-based so it doesn't need
  bollard. Called by the worker after claim, before pair, to bridge
  the "container is starting, gateway not yet listening" gap.

- topology_worker.rs run_job:
    1. Look up research_topic_id for the claimed run.
    2. If Some, load the topic and read zeroclaw_gateway_url.
    3. If a URL is present:
         - best-effort wait_ready(url, 30s); a timeout logs but
           doesn't abort — the pair call below will just fail
           faster than pinging forever
         - build the leaf via from_env_for_gateway(url)
       Else fall back to from_env() (workspace-wide gateway).
    4. The rest of run_job is unchanged — the leaf drops into
       either SubTopologyExecutor (org/company) or direct drive
       (team tier) as before.

What now works end-to-end

Starting a research topic with a bound repo:
  1. clone-shallow into per-topic workspace
  2. docker create + start the clawmates-runtime container, name
     = research-<topic>-team, joined to clawmates_core so the
     server reaches it by name
  3. persist container name + gateway URL on the topic row
  4. enqueue the topology run tagged with research_topic_id
  5. worker claims → looks up the topic → waits for the team
     gateway's /health → constructs a from_env_for_gateway
     executor pointed at http://research-<topic>-team:42617
  6. every turn's `/ws/chat?agent=…` hits the isolated container;
     agents inside see the repo at /workspace/repo (rw); each
     topic's memory/state lives under its own /zeroclaw-data mount

Deploy prereqs (unchanged from commit 1)

- clawmates_server compose service needs a bind-mount of
  CLAWMATES_RESEARCH_WORKSPACE_ROOT so the paths spawn() writes to
  are visible on the host and the spawned team container mounts
  the same underlying data.
- socket-proxy ACL needs POST + DELETE on /containers (prod ✓).
2026-07-09 04:32:40 -07:00
Omar SobhandClaude Opus 4.8 cbfa0ff24f feat: agent-to-agent platform on ZeroClaw 0.8.2 — rooms, delegation, A2A ingress
Builds on the v0.8.2 runtime. Four workstreams, all behind the §15 MCP door:

- Group rooms (Phase 1): migration 0026; N-way threads repo with a DM/room
  count-guard; chat.send {room} + room.create/invite/leave tools; RoomMessage
  -> room.message SSE; /api/claw-chat/rooms* APIs; Observer room badge.
- Per-claw door identity: door caller_agent resolves the X-ZeroClaw-Agent
  header (set by the fork) to the specific claw, falling back to roster[0].
- Gated delegation bridge (Phase 3): clawmates__delegate door tool drives a
  sibling via the existing /ws/chat ZeroClawDriveExecutor (not A2A); self-deny,
  per-workspace hourly budget, audit trail, untrusted-banner result. Native
  in-daemon delegation stays off (it would bypass the door).
- A2A tenant ingress (Phase 2): migration 0027 (workspace_a2a + a2a_tokens);
  runtime_provision enable_a2a_server/publish_claw; routes/a2a.rs tenant-aware
  proxy (per-workspace tokens, injected internal bearer, daemon stays internal,
  cards URL-rewritten to the cm-api edge); a2a.invoked taxonomy.

Tests: cm-db room repos, cm-runtime chat tools, door units. sqlx cache updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-28 16:11:01 -07:00
Omar SobhandClaude Opus 4.8 3eca4ed70c Recursive deploy ladder: Company + Org tiers, mesh mark, two-tier rail
Completes the scale ladder (single → team → company → org). Every tier is a
topology whose nodes are the tier below; running a parent recursively runs each
child's sub-topology down to the leaf claws.

Backend:
- migration 0011: companies/company_teams, orgs/org_companies, topology_runs.tier
- cm-db repos for companies + orgs (mirror teams)
- TurnRequest.attrs (forwarded from node.attrs) for child-id binding
- SubTopologyExecutor (recursive_exec.rs): a parent "turn" runs the child's
  sub-topology; durability via parent updated_at keepalive + cancel propagation
  + depth cap; boxed future breaks the org→company recursion
- topology_worker selects executor by job.tier
- routes: /api/companies, /api/orgs (create/list/get/run) + unified
  /api/structure/{level}/{id} for the zoom canvas

Frontend:
- MeshMark: node-mesh brand glyph (replaces the claw PNG), tier variants
- TopologyGraphView: optional onNodeClick/nodeMeta + dark-token theming
- StructureCanvas + Breadcrumb: one recursive zoom view for every tier
  (drill down on node click, breadcrumb up); TeamRunPanel extracted + shared
- two-tier Discord-style rail: StructureRail (mesh mark + org/company/team
  glyphs + tools popover + deploy + user) | RosterColumn (selected group's
  children, or your claws); SecondaryNav for cross-cutting tools
- ComposeWizard (company/org) wired into DeployWizard; /companies + /orgs pages

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 14:25:06 -07:00
Omar SobhandClaude Opus 4.8 3402a3b56d Door governor: runtime-agent judge (Kimi-as-judge on the subscription)
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled
The §15 governor can now be judged by a ZeroClaw runtime agent instead of a
server-side registry/API model. ZeroClawDriveExecutor::judge drives an agent
with the governor prompt and parses ALLOW/DENY (fail-open). mcp_door routes to
it when CLAWMATES_JUDGE_MODEL=runtime:<alias>.

This unblocks Kimi-as-judge with NO Kimi Platform key: Kimi runs on the
membership via kimi_cli, so CLAWMATES_JUDGE_MODEL=runtime:judge_kimi makes the
coding agent the governor. (Same path works for any subscription-only model.)
cm-runtime re-exports judge_model. clippy clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 09:42:46 -07:00
Omar SobhandClaude Opus 4.8 99e2fd3ec3 orchestrator: per-node model selection via node.attrs["agent"]
TurnRequest gains an optional `agent` sourced from the graph node's
attrs["agent"]. The ZeroClaw executor binds a node to that alias directly
when present, falling back to the role→alias map otherwise. This lets a
single POST /api/topologies/run specify a different model per role
(heterogeneous topologies) entirely in the graph JSON — no server
ZEROCLAW_AGENT_MAP change or recreate per configuration, which makes
quota-frugal model×role sweeps practical.

cm-orchestrator 13 + cm-api topology_exec 4 tests pass, clippy clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-17 12:52:18 -07:00
Omar SobhandClaude Opus 4.8 d1409a0776 Topology executor: accept a durable ZEROCLAW_TOKEN (skip one-time pairing)
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled
ZeroClaw pairing codes are single-use, so a static ZEROCLAW_PAIRING_CODE only
works for the first run. Add ZEROCLAW_TOKEN: pair once out-of-band, set the
durable bearer, and runs are repeatable. PAIRING_CODE stays as a fallback; one
of the two is required.

Validated live on gw-04: a single-node pipeline driven through the deployed
authed POST /api/topologies/run drove a real ZeroClaw role-agent over /ws/chat
and returned a RunRecord with real output + token metering, persisted to
topology_runs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 13:19:43 -07:00
Omar SobhandClaude Opus 4.8 6d77a0acc1 Topology P-zc 1A: ZeroClawDriveExecutor — real role-agents over /ws/chat
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled
cm-orchestrator owns the topology graph; each turn now drives a real ZeroClaw
role-agent in a container via the proven gateway drive recipe, instead of a
tool-free cm-llm call.

- crates/cm-api/src/topology_exec.rs: ZeroClawDriveExecutor impl TurnExecutor —
  pair (POST /pair + X-Pairing-Code, token cached) -> ws /ws/chat?agent=<alias>
  -> send {type:message,content} -> drain chunk/done/approval_request/error.
  approval_request is recorded as a BLOCKED GatedAction, never auto-approved (§15).
  Role->alias via ZEROCLAW_AGENT_MAP, fallback ZEROCLAW_DEFAULT_AGENT (scout).
- POST /api/topologies/run {task,graph} -> execute() -> RunRecord, persisted
  best-effort to the existing topology_runs table (no migration). compare stays
  tool-free. from_env() is read in-handler so cm-api still boots unset.
- deploy/clawmates-runtime: example config now declares a tool-free multi-agent
  role-cast; README documents the ZEROCLAW_* knobs + run endpoint.

tokio-tungstenite 0.26 (already in lock) + dev axum `ws` for the hermetic test.
3 lib tests green, clippy clean, SQLX_OFFLINE build clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 12:52:54 -07:00