Commit Graph
16 Commits
Author SHA1 Message Date
Omar Sobh bfcdca0583 research-canvas: managed-by-loop UI + start_topic guard (fold cleanup)
ci / gates (push) Successful in 18s
ci / frontend (push) Successful in 31s
ci / rust (push) Successful in 2m43s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m57s
Closes the UX gap the fold introduced: the topic canvas was still
showing "Start research" for standby-state topics even when a
scheduled loop already owned the runs. Clicking it would 409 (or
worse: race the loop into a duplicate run). Topic status stayed at
standby forever because the loop path bypassed start_topic's
set_status transition.

Four changes:

1. **Backend status transition** — compose_and_enqueue_iteration for
   kind='research' now calls set_status_if(standby, processing) on
   the topic before the run is enqueued. New DB helper set_status_if
   only advances when the current status matches the "from" arg —
   safe against races and re-invocations. Later iterations no-op
   since the topic is already past standby.

2. **has_managed_loop on TopicDetail** — get_topic hydrates a new
   ManagedLoop struct (loop_id, title, enabled, next_fire_at,
   last_run_id, schedule_summary) when a kind='research' loop is
   bound to the topic. summarize_schedule() derives a human string
   from the loop's triggers jsonb (e.g. "cron: 0 3 * * * · on new
   artifact", "one-shot", "manual"). New DB helper
   loops::research_loop_for_topic returns the row.

3. **Canvas branch** — nextAction takes a managedByLoop flag; when
   set + status=standby, returns null (no button). The canvas
   renders a "MANAGED BY LOOP" strip below the topic title showing
   loop name, schedule summary, next fire time, and enabled dot.
   Reviewer buttons (Request publish / Approve / Reject) still show
   normally in later states — reviewers should still promote outcomes
   even when a loop is producing them.

4. **start_topic guard** — refuses with 409 when a research loop
   already owns the topic. Closes the direct-POST hole for anyone
   bypassing the frontend.

TS type + summarize_schedule live in the same commit so an old
client hitting a new backend just ignores the extra field (no
breakage), and a new client hitting an old backend renders the
classic buttons (managed_by_loop is optional).
2026-07-10 17:52:13 -07:00
Omar Sobh f910771bbb research-prompt: add AUTONOMY CONTRACT + relax citation rigidity
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 37s
ci / rust (push) Successful in 2m55s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m35s
Inspection of a stuck 500s+ research run showed both agents were
correctly picking up the topic AND producing rich, structured plans —
then stalling at 'Should I proceed?' and 'Which approach?'. No human
to answer = infinite spin until stale-sweep + retry, forever.

Two prompt changes to compose_research_iteration_task:

1. AUTONOMY CONTRACT at the top:
   - Explicit "no human will answer you"
   - Explicit "do NOT ask for confirmation"
   - Explicit "you MUST emit the completed artifact"
   - Framed as a contract, at the very top, before the task itself.

2. Softer citation rule:
   - Was: 'Cite every claim; never fabricate sources or repo paths.'
   - Now: 'Cite what you can verify. Use [claim needs verification]
     inline when you can't. A written v(N) with rough citations
     beats a blocked v(N) waiting for approval. Do not fabricate
     concrete titles/authors/DOIs.'

The absolute anti-fabrication rule made agents refuse to write
anything unless they could be sure. Coupled with no user available,
the whole loop stalled — turns completed successfully but no output
converged.

Follow-up if this repeats:
- Even shorter prompt (the current one is 300+ words)
- Explicit output-shape enforcement ('respond with only the markdown
  artifact, no preamble')
- Cap iteration count so a broken prompt doesn't burn tokens forever
2026-07-10 17:18:47 -07:00
Omar Sobh 51756d0e68 clippy: struct-bundle enqueue_iteration_with_topic + fix doc list warnings
ci / rust (push) Successful in 3m58s
ci / publish (push) Successful in 4m12s
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 40s
ci / e2e (push) Has been skipped
CI's clippy stage failed with -D warnings on three classes of lint:

1. map_clone in loops::recent_reorders — .map(|a| a.clone()) is the
   pattern clippy wants replaced by .cloned(). Trivial swap.

2. too_many_arguments on enqueue_iteration_with_topic (8 args, ceiling
   7). Refactored the caller-side surface into a new
   IterationEnqueue<'a> struct with fields for each column. Matches
   the pattern research::NewTopic + loops::NewLoop already use for
   the same clippy ceiling. Callers in loops.rs (routes) + the
   enqueue_iteration wrapper updated to build the struct literal.

3. doc_lazy_continuation + doc_list_indentation — two doc comment
   blocks used ambiguous list-like layouts:
   - compose_iteration_task's ASCII-art prepended-block preview:
     wrapped in a ```text fence so clippy stops parsing "RESEARCH
     ARTIFACT:" etc as list continuation.
   - fire_initial_burst_if_set's mention of `burst - 1`: rewrote so
     "- 1" doesn't start a line and get misread as list marker.

Purely a refactor + doc pass; no behavior change.
2026-07-10 09:21:24 -07:00
Omar Sobh c0c5fd7104 research: extract setup helpers into research_setup.rs (fixes file-size gate)
ci / rust (push) Failing after 1m0s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 51s
research.rs hit 1446 lines with the wizard-fold + regression-fix
commits, tripping the CI file-size gate (limit 1250). Pure extraction
into a sibling module; no behavior change.

Moved to routes/research_setup.rs:
- RepoContext (now pub — used by build_coordinator_task)
- TopicSchedule (now pub — request body sub-struct)
- research_workspace_root (now pub)
- prepare_topic_runtime (already pub — used by loops iteration path)
- ensure_repo_workspace (now pub — used by start_topic + prepare)
- materialize_topic_loops (now pub — used by create_topic)

research.rs re-imports them via `use crate::routes::research_setup::{...}`
so the calling code reads identically.

New line counts:
  research.rs        1127 lines (was 1446, limit 1250)
  research_setup.rs   321 lines (new)

Also updated the loop-iteration callsite in routes/loops.rs to point
at the new path (crate::routes::research_setup::prepare_topic_runtime).

No API changes; migrations unaffected.
2026-07-10 09:15:25 -07:00
Omar Sobh 80c23e57ed research: fix wizard loops never firing + missing clone/spawn (regression)
ci / gates (push) Failing after 6s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
ci / rust (push) Has been skipped
ci / frontend (push) Has been skipped
Two bugs surfaced by the first end-to-end wizard run — the pipeline
diagnostic showed "0 run(s)", "Repo bound but never cloned", and
"Container not spawned" for a topic that had been created with a
nightly research loop.

Bug 1: materialize_topic_loops bypassed initial_burst firing.
The wizard-materialized loops path calls cm_db::repo::loops::create
directly (a plain INSERT). The initial_burst-fires-first-iteration
logic lived inside the create_loop HTTP handler, so wizard loops
landed in the DB but never fired their initial iteration.

Fix: extract routes::loops::fire_initial_burst_if_set as a pub helper
that read triggers, ensures the loop container, and calls the
kind-aware compose_and_enqueue_iteration. Both create_loop and
materialize_topic_loops now call it.

Bug 2: research-kind loop iterations skipped clone/spawn.
compose_research_iteration_task only built the coordinator prompt;
the repo clone and topic container spawn lived only in start_topic.
So the first research iteration ran against a nonexistent clone
directory and a stale gateway, and every run failed.

Fix: extract routes::research::prepare_topic_runtime as a pub helper
that runs ensure_repo_workspace + research_container::spawn.
Idempotent — second iteration reattaches. Called from
compose_and_enqueue_iteration before enqueuing a research iteration.
Topics without a repo bound are a no-op.

Both fixes ship as one commit because they surface together on the
same user path (wizard → research loop → first iteration) — you
can't hit one without the other manifesting.

Follow-up: start_topic still runs its own inline clone/spawn code
(now duplicated with prepare_topic_runtime). Next commit collapses
start_topic to just call prepare_topic_runtime + build_task like
the loop path does, so the one-shot and loop paths agree on setup.
2026-07-10 07:38:52 -07:00
Omar Sobh 91a51dce11 loops: kind='research' dispatch — research runs as loops
ci / frontend (push) Successful in 30s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
ci / gates (push) Successful in 5s
ci / rust (push) Failing after 48s
Delivers the research/loop fold: kind='research' loops run the
research pipeline each iteration, appending a new research_outcomes
version. The paired on_artifact_update fan-out then wakes up any
kind='exec' loops bound to the same topic to consume new INTs. Every
runnable thing is now a loop (D1).

Backend — DB helpers:
- cm_db::repo::loops::kind_and_binding — reads (kind, source_topic,
  task_template) so callers can dispatch without hydrating the whole
  Loop struct.
- cm_db::repo::loops::enqueue_iteration_with_topic — new variant that
  sets research_topic_id on topology_runs alongside loop_id, so the
  completion hook's freeze_research_outcome writes a new outcome
  version for research-kind iterations.
- cm_db::repo::research_topics::get_any_workspace — cross-workspace
  fetch used by the research task builder (the loop row is
  authoritative for the workspace binding via kind_and_binding).

Backend — dispatch:
- routes::loops::compose_research_iteration_task — builds the
  coordinator prompt for a research iteration: topic title +
  description + outcome_kind + prior version pointer + refresh
  instructions (survey new sources, preserve stable INT ids, mark
  superseded items as deprecated rather than delete). The completion
  hook writes the resulting synthesis as research_outcomes v(prior+1).
- routes::loops::compose_and_enqueue_iteration — one-shot dispatch:
  reads the kind, picks compose_iteration_task (exec) or
  compose_research_iteration_task (research), enqueues with or
  without research_topic_id set.

All four enqueue callsites now route through compose_and_enqueue:
- create_loop (initial_burst)
- run_now
- webhook_receive
- topology_worker::continue_initial_burst
- topology_worker::freeze_research_outcome (on_artifact_update fan-out)

Research-kind loops naturally form the "nightly refresh" side of a
paired research + coding loop: research writes a fresh outcome
version → fan-out wakes exec loops with on_artifact_update →
coding loops consume the next INT (which the research loop may have
just added). D2 answer (inherit repo binding): repo lives on the
topic; both loops sharing the source topic id read from the same
context, no duplication. D3 answer (coordinator resolves): the
research iteration prompt tells the team to preserve stable INT ids
and mark deprecations rather than delete, so coding loops' consumed
lists stay valid across versions.

Follow-up (next commit): ResearchWizard schedule step — "Just once /
Nightly / Manual" that creates the paired research-kind loop with
initial_burst=1 (just once) or cron 0 3 * * * (nightly) + optional
paired coding loop with on_artifact_update.
2026-07-09 22:56:19 -07:00
Omar Sobh 4ada5557f2 loops: kind column + initial_burst + on_artifact_update trigger fan-out
ci / gates (push) Successful in 6s
ci / publish (push) Has been skipped
ci / frontend (push) Successful in 28s
ci / rust (push) Failing after 1m0s
ci / e2e (push) Has been skipped
Foundation for folding research into loops as a first-class kind.
This commit ships the plumbing; the research-kind dispatch itself
lands next. Behavior for existing exec-kind loops is unchanged unless
they opt into the new trigger fields.

Migration 0044:
- kind TEXT NOT NULL DEFAULT 'exec' CHECK ('exec' | 'research'). New
  research-kind will run the research pipeline each iteration (next
  commit); 'exec' preserves today's behavior.
- initial_burst_remaining INT NOT NULL DEFAULT 0 — countdown for the
  triggers.initial_burst quota. Decremented CAS-safely on each
  completion until it hits 0.
- Two partial indexes: (kind, source_research_topic_id) for kind-
  aware lookups, and (source_research_topic_id) filtered on
  on_artifact_update=true + enabled=true for the fan-out hook.

Trigger schema extended with two optional fields:
- initial_burst: N — fire N iterations back-to-back at create time.
  create_loop enqueues the first iteration inline (subject to
  empty-roster gate), sets remaining=N-1, and the completion hook
  continues the chain until exhausted.
- on_artifact_update: true — when a bound research_outcomes row is
  inserted for the source topic, wake up one iteration of this loop.
  Coalesced against has_active_run so a burst of rapid revisions
  doesn't queue duplicates.

Backend:
- cm_db::repo::loops helpers (all dynamic sqlx, no offline cache
  regen needed):
  - set_initial_burst_remaining
  - take_initial_burst_slot (CAS UPDATE returning prev value; 0 on
    exhausted or race loss)
  - loops_awaiting_topic (fan-out query: kind=exec + enabled +
    on_artifact_update=true bound to the given topic)
  - has_active_run (queued|running iteration existence check)
  - get_any_workspace (bypasses the workspace scope guard; used by
    the completion hook where the run row is authoritative)
- routes/loops::compose_iteration_task made pub so the completion
  hook can build the same enriched task string as run_now.
- topology_worker::freeze_research_outcome now fans out to awakened
  loops after the outcome insert, using compose_iteration_task and
  coalescing on has_active_run.
- topology_worker::continue_initial_burst runs on every completion:
  · take_initial_burst_slot (CAS) — no-op if already exhausted
  · has_active_run coalesce guard
  · re-fetches the loop via get_any_workspace + compose_iteration_task
  · enqueues via loops::enqueue_iteration with parent_run_id set

Follow-ups already queued:
- kind='research' dispatch in run_job — build the research
  coordinator task from the topic config, run the research pipeline
  each iteration. Requires factoring start_topic's task-build.
- ResearchWizard "When should this run?" step (Just once / Nightly /
  Manual) creating the topic + paired research-kind loop.
- LoopsWizard trigger UI matching the design proposal (burst count,
  cron, on-artifact checkbox).
2026-07-09 22:50:01 -07:00
Omar Sobh ce111273bb loops: reorder history collapsed under the progress pill
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 26s
ci / rust (push) Failing after 1m14s
ci / publish (push) Has been skipped
ci / e2e (push) Has been skipped
Completes the reorder rationale loop — the previous commit captured
REORDER: markers server-side but nothing surfaced them. Now the loops
sidebar renders a small "N reorders ▸" button under the progress pill
whenever the loop has any reorder events. Click expands to a
newest-first list of "iter <n> · <rationale>" lines so a reviewer can
see, at a glance, when the plan was adjusted and why.

Backend:
- LoopProgress DTO gains recent_reorders: Vec<Value> — newest-first,
  capped at 5 so the card stays compact. Full history remains on the
  loop row's reorder_events column.
- cm_db::repo::loops::recent_reorders — reads the jsonb array, returns
  the last N in newest-first order.
- list_progress populates it per loop.

Frontend:
- LoopReorderEvent + recent_reorders on LoopProgress type.
- LoopsList tracks openHistoryId per-loop (one open at a time).
- Card renders history button + expanded panel styled to match the
  progress pill above.

Notes:
- The event object schema is {run_id, iteration, text, ts}. Fields are
  optional in the TS type so future schema tweaks don't break the
  render.
- 5-item cap chosen so the sidebar card doesn't grow unbounded. If a
  loop accumulates a lot of reorders, follow-up UI can render the full
  history on the loop detail page.
2026-07-09 19:00:39 -07:00
Omar Sobh 0c17de52dd loops: reorder rationale extraction — REORDER: markers logged per iteration
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 27s
ci / rust (push) Failing after 43s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
Coordinator can now log WHY it worked on an INT-XX out of order
("REORDER: INT-05 before INT-04 because prereq X is unmet") and the
completion hook captures each rationale as an append-only event on
the loop. Sets up a reviewable timeline of when the plan was
adjusted, independent of the underlying `consumed_int_ids` advance.

Migration 0043:
- loops.reorder_events JSONB NOT NULL DEFAULT '[]'::jsonb — append-
  only array of {run_id, iteration, text, ts}. Kept on the loop row
  (rather than a dedicated table) so the mini-timeline is one read
  away from the loop card.

Backend:
- topology_worker::parse_reorder_rationale — line matcher symmetric
  with parse_completed_int_ids. Tolerates list dashes / prefixes /
  markdown emphasis; case-insensitive marker match, preserves case of
  the rationale text.
- cm_db::repo::loops::append_reorder_event — one INSERT-like append
  per rationale, uses jsonb_build_object with postgres now() so ts is
  wall-clock canonical (no client-clock skew).
- topology_runs::iteration_for_run — new helper so events carry the
  iteration index.
- routes::loops::compose_iteration_task — coordinator prompt now
  explicitly asks for `REORDER: <one-sentence>` at the top of the
  first substantive turn when working out of order, AND spells out
  that both markers must appear literally with colons (no bold, no
  code fence) so the line parser doesn't miss them.

Non-loop and standalone-loop runs are unaffected — the hook only
fires when the run belongs to a source-bound loop.

Follow-up: expose reorder_events on the loops list endpoint + render
a small collapsed timeline on the LoopsList card.
2026-07-09 18:58:55 -07:00
Omar Sobh a34be33261 loops: N/M INTs progress pill on source-bound loop cards
ci / rust (push) Failing after 41s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 27s
Users couldn't see how a research-bound loop was progressing through
its integration plan — the consumed_int_ids state existed in the DB
but nothing surfaced it. Now each loop card in the sidebar shows a
cyan pill "3/8 INTs" + source topic title + a thin progress bar, when
the loop is bound to a research topic.

Backend:
- GET /api/loops/progress — bulk read for every loop with a source
  topic. Returns {loop_id, source_topic_id, source_topic_title,
  source_outcome_version, consumed_count, total_int_count,
  current_int_index} per loop. Standalone loops are omitted.
- count_int_ids parses unique INT-<number> ids out of the source
  outcome's markdown — same permissive matcher as the completion
  hook, so what the pill counts matches what the loop can advance.
- Memoized by topic_id inside the endpoint so N loops sharing 1
  source topic only fetch the outcome once.

Frontend:
- listLoopProgress helper + LoopProgress type in the loops API.
- LoopsList fetches loops + progress in parallel on mount.
- Each card looks up progress by loop_id and, when found, renders
  under the schedule line: pill "3/8 INTs · Topic title" with a
  3px cyan progress bar. Title hover shows artifact version.

Follow-ups:
- Refresh button on the card — re-snapshot artifact into
  task_template (cosmetic; the enqueue path already reads latest).
- Reorder rationale extraction — parse "REORDER: <text>" out of run
  output, index as a per-loop event log for a mini-timeline UI.
2026-07-09 18:57:09 -07:00
Omar Sobh ce73abe5ab loops: bridge research artifact into loop iterations (option C + b)
ci / rust (push) Failing after 13m41s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 37s
The bridge lets a coding loop "consume" an integrations research
artifact one INT-XX item per iteration. Options b (order-sequential
iteration) and C (snapshot in task_template + save the pointer for
future refresh) from the design discussion.

Migration 0042 — three new loops columns:
- source_research_topic_id — nullable pointer to research_topics.
- consumed_int_ids TEXT[]   — INT-XX ids the loop has completed.
  Advances when topology_worker parses "COMPLETED: INT-<NN>" markers
  from the run's final output (wired in a follow-up commit).
- current_int_index INT     — monotonic pointer for order-sequential
  iteration. Coordinator addresses INT-<current+1> unless prereqs are
  unmet, in which case it works on the smallest unblocking INT-XX and
  logs the reorder rationale.

Backend:
- cm_db::repo::loops::set_source_research_topic — bind/unbind pointer.
- cm_db::repo::loops::source_research_context   — read pointer + state.
- routes::loops::compose_iteration_task         — new caller-side helper
  that reads the pointer, fetches the topic's latest research_outcome,
  and prepends the artifact + focus instruction to task_template.
- run_now + webhook_receive both pass task_template through
  compose_iteration_task before enqueue. Standalone loops (no pointer)
  behave identically to before.
- CreateLoopRequest accepts `source_research_topic_id`, ownership-
  checked via research_topics::get before persist.

Frontend:
- New ResearchArtifactPicker modal — lists published topics, fetches
  the artifact on pick, returns (topic_id, markdown) to caller.
- LoopsWizard task_template step gains "Import from research artifact"
  button (right-aligned). Click opens the picker. On pick: task
  populates with the artifact markdown, pointer saved, textarea
  expands to 8 rows, small info strip shows "Loop is bound to topic
  <id>. Each iteration will focus on the next unconsumed INT-XX."
- Unlink button reverts to standalone loop mode.

Follow-up (next commit):
- topology_worker completion hook — parse "COMPLETED: INT-<NN>" out
  of the run's final output + update consumed_int_ids +
  current_int_index atomically. Without this, current_int_index stays
  at 0 forever and every iteration works on the same INT.
- Loop card refresh button — re-read source topic's latest outcome
  (useful after a reject-with-revision cycle on the source topic).
2026-07-09 18:47:17 -07:00
Omar Sobh 44d6e95022 fmt: apply rustfmt across the P2 arc
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 37s
ci / rust (push) Successful in 2m50s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m48s
CI's rustfmt check flagged the multi-line sqlx::query() calls I
introduced in P2 (loop_id_for_run, zeroclaw_gateway_url, etc.).
No behavior change — pure formatting.
2026-07-09 16:43:05 -07:00
Omar Sobh 03f1830d1f loops: Path B container isolation (P2)
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 11s
ci / publish (push) Has been skipped
ci / frontend (push) Successful in 28s
ci / e2e (push) Has been skipped
Symmetric with the research pipeline: every enabled loop can now have
its own per-loop team container so scheduled runs don't share state
with other loops or with research. Same daemon image, same clawmates
network, deterministic name loop-<id>-team.

Backend surface:
- Migration 0040 adds nullable `zeroclaw_container` +
  `zeroclaw_gateway_url` columns to loops (parallel to
  research_topics).
- research_container.rs grows loop_container_name_for(), spawn_loop()
  (state-only mount, no repo), and teardown_loop(). Kept in the same
  module to share the docker connect() + inherited_env() plumbing;
  each pattern gets its own labels (clawmates.role=loop-team) so ps
  filters can tell them apart.
- cm_db::repo::loops gains set_zeroclaw_container() +
  zeroclaw_gateway_url() (dynamic sqlx queries — no offline cache
  regen needed).
- cm_db::repo::topology_runs gets loop_id_for_run(): mirror of
  research_topic_id, used by the worker.

Wiring:
- routes/loops::run_now + webhook_receive call ensure_loop_container()
  before enqueuing an iteration. Idempotent: an already-running
  container is just reattached. Failures are logged and do NOT block
  the enqueue — topology_worker falls back to the workspace gateway
  when the URL isn't set on the loop.
- routes/loops::disable_loop + delete_loop both fire teardown_loop()
  so paused / deleted loops don't hold a docker slot.
- topology_worker's per-run URL resolution: existing research fast
  path unchanged; when it doesn't hit, the worker now looks up
  loop_id and reads the loop's gateway URL.

Deploy step (required on gw-04 for state to persist across container
restarts): add a `/var/lib/clawmates-loops:/var/lib/clawmates-loops`
bind mount + `CLAWMATES_LOOPS_STATE_ROOT=/var/lib/clawmates-loops`
env var to clawmates_server_1 in the compose. Without it, loops still
run — the state dir lives inside the API container's filesystem so
persistence is limited to that container's lifetime.

Follow-up:
- Scheduler-tick fires (cron-driven, not run_now) — they call
  enqueue_iteration in cm-scheduler and don't yet go through
  ensure_loop_container. Add a symmetric spawn there so cron fires
  also land on the isolated daemon.
- Compose file reconciliation — deploy/compose/docker-compose.yml in
  the repo has drifted from prod; when we sync it, add the loops mount
  at the same time.
2026-07-09 16:02:08 -07:00
Omar Sobh 763b95a253 api: gate create_loop + create_topic on empty workspace roster (W2)
ci / frontend (push) Successful in 28s
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 3m31s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
Frontend already disables the create button when the roster is empty,
but nothing stopped a direct POST from materializing an orphan loop /
topic with nothing to staff it. Both handlers now check the workspace
agent count up front and return 409 Conflict when it's zero.

- crates/cm-api/src/routes/loops.rs: gate at top of create_loop
- crates/cm-api/src/routes/research.rs: gate at top of create_topic

Uses the existing cm_db::repo::agents::count_active(pool, ws) helper
(count of workspace agents where deleted_at IS NULL). 409 is the right
mapping: state-of-the-workspace-prevents-this, not client-input-bad.
2026-07-09 14:00:54 -07:00
Omar Sobh a6da19430f loops: repo picker + agent/team/org staffing + sidebar edit/delete
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 12s
ci / frontend (push) Successful in 25s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
Adds the missing pieces the wizard needed and the sidebar controls
around it:

- LoopsWizard is now a 6-step flow (identity → repo → task/topology
  → triggers → repeat → assign agents) plus the existing secrets
  card. ResearchWizard picks up the same repo step and a hard gate
  when the workspace has zero agents.
- New LoopStaffingStep with three tabs — Individual / Team /
  Organization — that mix freely per loop; selections persist via
  new loop_agents / loop_teams / loop_orgs join tables (0035
  migration), each cascading on loop_id so hard-delete stays a
  single-row DELETE.
- Backend CreateLoopRequest / UpdateLoopRequest accept the three
  lists and apply_staffing does a transactional replace-all;
  list_loops / get_loop hydrate the lists via a flattened
  LoopWithStaffing response.
- LoopsList sidebar gains per-row enable/disable, edit (reopens the
  wizard prefilled with the current loop, PATCHes on submit), and
  delete with an inline confirm.
- NoAgentsGate blocks launching a loop or research topic from a
  workspace with no roster; the sidebar `+` buttons also disable
  with a tooltip pointing at the TEAM tier.

Not yet wired: the run driver still fills role slots from the
workspace-wide pool; teaching enqueue_iteration to prefer
loop_agents/loop_teams/loop_orgs is a follow-up.
2026-07-07 22:09:06 -07:00
Omar Sobh 4b48c521eb loops: backend routes + repo + cron scheduler + HMAC webhook
ci / frontend (push) Successful in 24s
ci / publish (push) Successful in 2m15s
ci / gates (push) Successful in 5s
ci / rust (push) Successful in 3m43s
ci / e2e (push) Failing after 29m57s
Third commit of the Research + Loops arc. Lights up loops as durable
recurring topology executions:

  GET    /api/loops                 list workspace's loops
  POST   /api/loops                 create — returns webhook_token +
                                    signing_key ONCE when webhook trigger
                                    is enabled; never exposed again
  GET    /api/loops/:id             detail
  PATCH  /api/loops/:id             update definition
  DELETE /api/loops/:id             delete
  POST   /api/loops/:id/run         trigger one iteration NOW
  POST   /api/loops/:id/enable      set enabled=true
  POST   /api/loops/:id/disable     set enabled=false
  POST   /webhooks/loops/:token     public; HMAC-SHA256-verified

Scheduler (cm_runtime::spawn_loop_scheduler) wakes every 10s, queries the
partial index on (next_fire_at) for due loops, enqueues one topology_runs
row per fire with loop_id + iteration + parent_run_id chained back to the
previous iteration. Uses croner via the existing scheduling::next_occurrence
helper. Missed windows fire ONCE and skip the backlog — next_fire_at is
always computed strictly AFTER now(), so a late scheduler doesn't drain a
buildup.

Webhook signatures follow the same pattern as the Stripe billing webhook
(HMAC-SHA256 with constant-time hex compare). Token + signing key are
24-byte OS-RNG values; the URL uses base64-url for the token, and the
signing key is base64-std. Both surface exactly once at create time.

All three fire paths (scheduler, immediate-run, webhook) funnel through
`cm_db::repo::loops::enqueue_iteration` so the invariants stay in one
place. `iters` repeat policy is enforced by the scheduler tick; `until`
and `on_completion` land with the orchestrator hook in commit 4.

Adds cm-llm as a direct cm-api dep, getrandom for the webhook material
generator, and wires the scheduler spawn into the server binary alongside
the resume sweeper and outbox drainer.
2026-07-06 04:39:27 -07:00