research: persist bound repo + shallow-clone on start_topic
This is the minimum viable version of the "agents actually work on
a repo" architecture. Full vision (isolated ZeroClaw container per
topic, dynamic agent provisioning inside, pause/resume, commit
gate) is real weeks of work — this closes the first, most-visible
gap so the ClawHDF5 topic can actually run against its codebase.
Backend
- Migration 0037: research_topics gets repo_id UUID (nullable, FK
to repos ON DELETE SET NULL) and repo_workspace_path TEXT for
the on-disk checkout location. Index on repo_id when set.
- research_topics::create takes repo_id: Option<Uuid>. get + list
select it and repo_workspace_path. set_repo_workspace_path
persists the path once the first clone lands.
- CreateTopicRequest accepts `repo: Option<TopicRepoRef>` — the
same denormalized shape the wizard already sends. Only repo_id
is authoritative; other fields are ignored (dead_code-allowed
so serde still deserializes the full body).
- start_topic branches on topic.repo_id. When set, it calls
ensure_repo_workspace:
· resolves repo.clone_url + repo.default_branch
· target path = CLAWMATES_RESEARCH_WORKSPACE_ROOT
// <topic_id> // repo (defaults under $TMPDIR)
· runs `git clone --depth 1 --single-branch --branch <b>` via
tokio::process. Reuses the checkout if .git already exists.
· persists the path so re-starts skip the clone
· runs `git ls-files` to sample the tree (first 60 entries,
total count reported honestly so the prompt doesn't lie
about coverage)
All best-effort — a clone failure logs but still starts the run
without repo context rather than aborting.
- build_coordinator_task takes Option<&RepoContext>. When present,
the framing gets a REPO block (slug / path / branch / file
sample) and a USING THE REPO section instructing the coordinator
to ground every recommendation in a concrete file reference and
never fabricate paths. The per-topology bodies are unchanged —
the repo guidance sits above them so it applies to every shape.
What this unblocks / doesn't unblock
Unblocks: The coordinator prompt now knows the repo exists, where
it lives on disk, and what's in it. Even without file-editing
tools wired to the checkout, the coordinator can point spokes at
concrete modules and the final artifact can reference real files.
For a spec-shaped outcome like ClawHDF5's, that's the difference
between abstract advice and a spec grounded in the actual crates.
Does NOT unblock: The agents themselves editing files, running
tests, or committing. That requires either mounting the checkout
into the ZeroClaw sandbox or exposing a new MCP tool for
repo-scoped file ops — separate follow-up.
This commit is contained in:
@@ -27,6 +27,15 @@ pub struct ResearchTopic {
|
||||
/// Topology shape start_topic builds when firing this topic. Options:
|
||||
/// hub_spoke, pipeline, hierarchical, star_moe. Defaults to hub_spoke.
|
||||
pub topology_kind: String,
|
||||
/// The workspace repo the wizard bound to this topic (optional). When
|
||||
/// set, `start_topic` clones it and feeds the coordinator prompt with
|
||||
/// the checkout path + a file-tree overview so the agents can reason
|
||||
/// about the actual code.
|
||||
pub repo_id: Option<Uuid>,
|
||||
/// Absolute path on the API host where `start_topic` cloned the bound
|
||||
/// repo. Written once on the first successful clone; subsequent starts
|
||||
/// reuse it. Null until then.
|
||||
pub repo_workspace_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -43,19 +52,21 @@ pub async fn create(
|
||||
description: &str,
|
||||
outcome_kind: &str,
|
||||
topology_kind: &str,
|
||||
repo_id: Option<Uuid>,
|
||||
created_by: Uuid,
|
||||
) -> Result<Uuid, DbError> {
|
||||
let id = Uuid::now_v7();
|
||||
sqlx::query!(
|
||||
"INSERT INTO research_topics
|
||||
(id, workspace_id, title, description, outcome_kind, topology_kind, status, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'standby', $7)",
|
||||
(id, workspace_id, title, description, outcome_kind, topology_kind, repo_id, status, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, 'standby', $8)",
|
||||
id,
|
||||
workspace_id,
|
||||
title,
|
||||
description,
|
||||
outcome_kind,
|
||||
topology_kind,
|
||||
repo_id,
|
||||
created_by,
|
||||
)
|
||||
.execute(pool)
|
||||
@@ -63,12 +74,34 @@ pub async fn create(
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Persist the clone path for a topic's bound repo. Set once, on the first
|
||||
/// successful clone; a re-start reads it back and skips re-cloning.
|
||||
pub async fn set_repo_workspace_path(
|
||||
pool: &PgPool,
|
||||
id: Uuid,
|
||||
workspace_id: Uuid,
|
||||
path: &str,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
"UPDATE research_topics
|
||||
SET repo_workspace_path = $3, updated_at = now()
|
||||
WHERE id = $1 AND workspace_id = $2",
|
||||
id,
|
||||
workspace_id,
|
||||
path,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Workspace's topics, newest-updated first.
|
||||
pub async fn list(pool: &PgPool, workspace_id: Uuid) -> Result<Vec<ResearchTopic>, DbError> {
|
||||
let rows = sqlx::query_as!(
|
||||
ResearchTopic,
|
||||
"SELECT id, workspace_id, title, description, outcome_kind, status,
|
||||
created_by, created_at, updated_at, published_at, topology_kind
|
||||
created_by, created_at, updated_at, published_at, topology_kind,
|
||||
repo_id, repo_workspace_path
|
||||
FROM research_topics
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY updated_at DESC",
|
||||
@@ -87,7 +120,8 @@ pub async fn get(
|
||||
let row = sqlx::query_as!(
|
||||
ResearchTopic,
|
||||
"SELECT id, workspace_id, title, description, outcome_kind, status,
|
||||
created_by, created_at, updated_at, published_at, topology_kind
|
||||
created_by, created_at, updated_at, published_at, topology_kind,
|
||||
repo_id, repo_workspace_path
|
||||
FROM research_topics
|
||||
WHERE id = $1 AND workspace_id = $2",
|
||||
id,
|
||||
|
||||
Reference in New Issue
Block a user