research-wizard: schedule step (once/nightly/manual) + paired coding loop
ci / gates (push) Failing after 5s
ci / rust (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped

Completes the research/loop fold. The wizard now closes with a
"How should this research run?" step; picking any mode materializes a
kind='research' loop bound to the topic, and an optional checkbox
adds a paired kind='exec' loop that consumes each new artifact.

Frontend:
- ResearchWizard grows from 5 to 6 steps. Step 6 is the schedule
  picker:
    · Just once — initial_burst=1, no other triggers
    · Nightly   — initial_burst=1 + cron "0 3 * * *"
    · Manual    — webhook_enabled=true
  Below the radios, an optional card offers "Also create a coding
  loop that consumes each new artifact" — creates a paired
  kind='exec' loop with on_artifact_update=true + initial_burst=1
  bound to the same topic.
- createTopic API type extended with `schedule` + `create_paired_coding_loop`.
- Both fields ride the existing POST /api/research call; back-compat
  is preserved when the wizard omits them.

Backend:
- CreateTopicRequest gains TopicSchedule + create_paired_coding_loop.
- After topic + agent attach, create_topic calls
  materialize_topic_loops which:
    1. Creates a research-kind loop titled "Research · <topic>" bound
       to the topic. Triggers vary by schedule mode; next_fire_at
       computed from cron for nightly. Falls back silently if
       loop-create errors so the topic still lands.
    2. Flips kind to 'research' via loops::set_kind (NewLoop doesn't
       take kind directly — default is 'exec' for backward compat).
    3. Optionally creates a coding loop titled "Coding · <topic>"
       with on_artifact_update=true + initial_burst=1.
- cm_db::repo::loops::set_kind — trivial UPDATE helper used by the
  materialize path.

D-answer callouts:
- D1 (fold): every runnable thing is now a loop. "Just once" is a
  research loop with initial_burst=1 and no other triggers.
- D2 (inherit): both paired loops carry the same source_research_topic_id
  — repo binding lives on the topic, not duplicated.
- D3 (coordinator resolves): the research iteration prompt (from the
  earlier commit) instructs the team to preserve stable INT ids and
  mark deprecations; coding loops' consumed lists stay valid across
  versions.

Follow-ups queued:
- Kind pill on LoopsList cards (research=purple, exec=cyan) so users
  can tell them apart at a glance.
- Extract start_topic's task-build so kind='research' iterations
  reuse the same coordinator prompt shape as one-shot runs (they
  currently use a simpler refresh-oriented prompt; that's fine for
  MVP but a rich shared build would give better parity).
- Research topic sidebar shows "linked to N loops" badge.
This commit is contained in:
Omar Sobh
2026-07-09 23:00:04 -07:00
parent 91a51dce11
commit 3e42b1ea39
4 changed files with 272 additions and 5 deletions
+13
View File
@@ -322,6 +322,19 @@ pub async fn set_zeroclaw_container(
/// downstream mini-timeline can show WHEN the plan was adjusted and
/// WHY. Idempotent: appending a duplicate text/run_id combo is allowed
/// (rare — indicates the parser matched twice on the same line).
/// Set a loop's kind. Used by materialize_topic_loops right after
/// create() — the create path doesn't take a kind parameter (default
/// 'exec' matches every legacy loop), so research-kind loops flip the
/// column in a follow-up UPDATE.
pub async fn set_kind(pool: &PgPool, loop_id: Uuid, kind: &str) -> Result<(), DbError> {
sqlx::query("UPDATE loops SET kind = $2, updated_at = now() WHERE id = $1")
.bind(loop_id)
.bind(kind)
.execute(pool)
.await?;
Ok(())
}
/// Set the countdown for the triggers.initial_burst quota. On
/// create_loop we set this to `burst - 1` after firing the first
/// iteration inline; on each subsequent completion we decrement and,