summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-13 00:57:34 +0000
committersoryu <soryu@soryu.co>2026-02-13 00:57:34 +0000
commit639b4c6bc3b3964c00cdfc64c4f262c61ee22fc7 (patch)
tree6ba4c1cd323f728453b95e19b9215e40974d1377
parent39df770ea4ae37f3f9e30c5be5274b741e7ecf7b (diff)
downloadsoryu-639b4c6bc3b3964c00cdfc64c4f262c61ee22fc7.tar.gz
soryu-639b4c6bc3b3964c00cdfc64c4f262c61ee22fc7.zip
Make sure directive tasks inherit worktrees
-rw-r--r--makima/src/db/repository.rs29
-rw-r--r--makima/src/orchestration/directive.rs74
2 files changed, 95 insertions, 8 deletions
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index a79818f..51f49cd 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -5298,6 +5298,30 @@ pub async fn get_completed_step_tasks(
.await
}
+/// Get the task ID of the most recently completed step for a directive.
+/// Used as a fallback `continue_from_task_id` when dispatching new-generation steps
+/// that have no explicit dependencies and no PR branch to continue from.
+pub async fn get_last_completed_step_task_id(
+ pool: &PgPool,
+ directive_id: Uuid,
+) -> Result<Option<Uuid>, sqlx::Error> {
+ let row: Option<(Uuid,)> = sqlx::query_as(
+ r#"
+ SELECT ds.task_id
+ FROM directive_steps ds
+ WHERE ds.directive_id = $1
+ AND ds.status = 'completed'
+ AND ds.task_id IS NOT NULL
+ ORDER BY ds.updated_at DESC
+ LIMIT 1
+ "#,
+ )
+ .bind(directive_id)
+ .fetch_optional(pool)
+ .await?;
+ Ok(row.map(|r| r.0))
+}
+
// =============================================================================
// Directive Step CRUD
// =============================================================================
@@ -5586,6 +5610,8 @@ pub struct StepForDispatch {
pub repository_url: Option<String>,
pub base_branch: Option<String>,
pub memory_enabled: bool,
+ /// The directive's PR branch (if a PR has already been created from previous steps).
+ pub pr_branch: Option<String>,
}
/// Get ready steps that need task dispatch.
@@ -5607,7 +5633,8 @@ pub async fn get_ready_steps_for_dispatch(
d.title AS directive_title,
d.repository_url,
d.base_branch,
- d.memory_enabled
+ d.memory_enabled,
+ d.pr_branch
FROM directive_steps ds
JOIN directives d ON d.id = ds.directive_id
WHERE ds.status = 'ready'
diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs
index d2fcbfd..344bdf5 100644
--- a/makima/src/orchestration/directive.rs
+++ b/makima/src/orchestration/directive.rs
@@ -101,7 +101,42 @@ impl DirectiveOrchestrator {
// Resolve dependency steps to their task IDs for worktree continuation
let dep_tasks =
repository::get_step_dependency_tasks(&self.pool, &step.depends_on).await?;
- let continue_from_task_id = dep_tasks.first().map(|d| d.task_id);
+ let mut continue_from_task_id = dep_tasks.first().map(|d| d.task_id);
+
+ // If no dependency tasks resolved, try to continue from previous work:
+ // 1) Use the directive's PR branch as base (contains all previous merged work)
+ // 2) Fall back to the last completed step's task for worktree continuation
+ let effective_base_branch = if continue_from_task_id.is_none() {
+ if step.pr_branch.is_some() {
+ tracing::info!(
+ step_id = %step.step_id,
+ pr_branch = ?step.pr_branch,
+ "Step has no deps — using directive PR branch as base"
+ );
+ step.pr_branch.as_deref()
+ } else {
+ // No PR branch yet — try to continue from the last completed step's worktree
+ match repository::get_last_completed_step_task_id(
+ &self.pool,
+ step.directive_id,
+ )
+ .await
+ {
+ Ok(Some(task_id)) => {
+ tracing::info!(
+ step_id = %step.step_id,
+ continue_from = %task_id,
+ "Step has no deps, no PR branch — continuing from last completed task"
+ );
+ continue_from_task_id = Some(task_id);
+ step.base_branch.as_deref()
+ }
+ _ => step.base_branch.as_deref(),
+ }
+ }
+ } else {
+ step.base_branch.as_deref()
+ };
let task_plan = step
.task_plan
@@ -180,7 +215,7 @@ impl DirectiveOrchestrator {
format!("{}: {}", step.directive_title, step.step_name),
plan,
step.repository_url.as_deref(),
- step.base_branch.as_deref(),
+ effective_base_branch,
continue_from_task_id,
)
.await
@@ -746,13 +781,26 @@ fn build_planning_prompt(
"EXISTING STEPS (generation {}):\n",
generation - 1
));
+ let mut last_completed_id: Option<Uuid> = None;
for step in existing_steps {
prompt.push_str(&format!(
- "- {} [{}]: {}\n",
- step.name,
+ "- [{}] {} (id: {}): {}\n",
step.status,
+ step.name,
+ step.id,
step.description.as_deref().unwrap_or("(no description)")
));
+ if step.status == "completed" {
+ last_completed_id = Some(step.id);
+ }
+ }
+ if let Some(last_id) = last_completed_id {
+ prompt.push_str(&format!(
+ "\nNew steps that build on previous work SHOULD use --depends-on \"{}\" (the last completed step) \
+ so their worktree inherits all prior changes. Without this dependency, new steps start from a \
+ fresh checkout and won't see any of the work done by previous steps.\n",
+ last_id
+ ));
}
prompt.push_str(&format!(
"\nAdd new steps that build on or complement existing work. Use generation {}.\n\n",
@@ -789,9 +837,21 @@ Or batch:
makima directive batch-add-steps --json '[{{"name":"...","description":"...","taskPlan":"...","dependsOn":[],"orderIndex":0}}]'
DEPENDENCY WORKTREE CONTINUATION:
-When a step has dependsOn, it automatically continues from the first dependency's worktree (inheriting
-committed and uncommitted changes). If there are multiple dependencies, the first provides the base worktree
-and additional dependency branches are merged in before work starts. Use this for incremental work chains.
+Each step runs in its own git worktree. How that worktree is initialised depends on dependsOn:
+- With dependsOn: the step continues from the first dependency's worktree (inheriting all committed and
+ uncommitted changes). Additional dependencies are merged in as branches before work starts.
+- Without dependsOn: the step starts from a FRESH worktree based on the base branch (or the PR branch if
+ a PR already exists from previous completions).
+
+Because of this, you MUST chain steps using dependsOn whenever one step's work builds on another's.
+If step B modifies files created/changed by step A, step B MUST list step A in its dependsOn — otherwise
+step B will start from a blank worktree and won't see step A's changes at all.
+
+Guidelines:
+- For sequential work, create a linear chain: step1 → step2 → step3 (each depends on the previous).
+- Only omit dependsOn for truly independent steps that can start from a fresh checkout.
+- Parallel steps that share no files can omit mutual dependencies, but if they both build on a prior step
+ they should BOTH list that prior step in dependsOn.
IMPORTANT: Each step's taskPlan must be self-contained. The executing instance won't have your planning context.
"#,