summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-13 20:40:26 +0000
committerGitHub <noreply@github.com>2026-02-13 20:40:26 +0000
commitc2bad633593a8ec6ffa83d7ff10776560cf0f69f (patch)
tree44db52e4b818b9298d8b0af172a281cc2faa80c5
parentad5af0f7677c73fc159a3036b9479d1d847adf97 (diff)
downloadsoryu-c2bad633593a8ec6ffa83d7ff10776560cf0f69f.tar.gz
soryu-c2bad633593a8ec6ffa83d7ff10776560cf0f69f.zip
Rerun plan when directive goal is edited (#61)
When a directive's goal is updated, pending/ready/failed/skipped steps are now automatically cleared so that replanning generates fresh steps aligned with the new goal. The planning prompt is also improved to clearly categorize existing steps by status and provide explicit instructions for re-evaluation. Changes: - Add clear_pending_directive_steps() repository function to remove non-started steps when the goal changes - Call step cleanup in the update_goal HTTP handler - Restructure the planning prompt to categorize steps (completed, running, pending, failed, skipped) with clear instructions for each category Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
-rw-r--r--makima/src/db/repository.rs19
-rw-r--r--makima/src/orchestration/directive.rs130
-rw-r--r--makima/src/server/handlers/directives.rs23
3 files changed, 153 insertions, 19 deletions
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index e288eba..d8168f6 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -5465,6 +5465,25 @@ pub async fn delete_directive_step(
Ok(result.rows_affected() > 0)
}
+/// Delete all directive steps that have not started execution (pending, ready, failed, skipped).
+/// Completed and running steps are preserved.
+/// Returns the number of deleted steps.
+pub async fn clear_pending_directive_steps(
+ pool: &PgPool,
+ directive_id: Uuid,
+) -> Result<u64, sqlx::Error> {
+ let result = sqlx::query(
+ r#"DELETE FROM directive_steps
+ WHERE directive_id = $1
+ AND status IN ('pending', 'ready', 'failed', 'skipped')"#,
+ )
+ .bind(directive_id)
+ .execute(pool)
+ .await?;
+
+ Ok(result.rows_affected())
+}
+
// =============================================================================
// Directive DAG Progression
// =============================================================================
diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs
index bb573d4..ea8009d 100644
--- a/makima/src/orchestration/directive.rs
+++ b/makima/src/orchestration/directive.rs
@@ -699,33 +699,127 @@ fn build_planning_prompt(
let mut prompt = String::new();
if !existing_steps.is_empty() {
+ // ── RE-PLANNING header ──────────────────────────────────────
prompt.push_str(&format!(
- "EXISTING STEPS (generation {}):\n",
+ "⚠️ RE-PLANNING: The GOAL has been updated — you must re-evaluate ALL existing steps.\n\
+ Previous steps were planned for an earlier version of the goal. Some may no longer be \
+ relevant. Review each step below and act according to the instructions per status category.\n\n\
+ EXISTING STEPS (generation {}):\n",
generation - 1
));
- let mut last_completed_id: Option<Uuid> = None;
+
+ // Categorise steps by status
+ let mut completed: Vec<&crate::db::models::DirectiveStep> = Vec::new();
+ let mut running: Vec<&crate::db::models::DirectiveStep> = Vec::new();
+ let mut pending_ready: Vec<&crate::db::models::DirectiveStep> = Vec::new();
+ let mut failed: Vec<&crate::db::models::DirectiveStep> = Vec::new();
+ let mut skipped: Vec<&crate::db::models::DirectiveStep> = Vec::new();
+
for step in existing_steps {
- prompt.push_str(&format!(
- "- [{}] {} (id: {}): {}\n",
- step.status,
- step.name,
- step.id,
- step.description.as_deref().unwrap_or("(no description)")
- ));
- if step.status == "completed" {
+ match step.status.as_str() {
+ "completed" => completed.push(step),
+ "running" => running.push(step),
+ "pending" | "ready" => pending_ready.push(step),
+ "failed" => failed.push(step),
+ "skipped" => skipped.push(step),
+ _ => pending_ready.push(step),
+ }
+ }
+
+ // ── Completed steps ─────────────────────────────────────────
+ if !completed.is_empty() {
+ prompt.push_str("\n── COMPLETED steps (KEEP — work already done) ──\n");
+ prompt.push_str("These steps have finished. Their work is committed and available.\n");
+ prompt.push_str("Do NOT remove them. New steps can depend on them to inherit their changes.\n");
+ let mut last_completed_id: Option<Uuid> = None;
+ for step in &completed {
+ prompt.push_str(&format!(
+ " ✅ {} (id: {}): {}\n",
+ step.name,
+ step.id,
+ step.description.as_deref().unwrap_or("(no description)")
+ ));
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
+ ));
+ }
+ }
+
+ // ── Running steps ───────────────────────────────────────────
+ if !running.is_empty() {
+ prompt.push_str("\n── RUNNING steps (cannot remove — note if obsolete) ──\n");
+ prompt.push_str("These steps are currently executing and cannot be removed or skipped.\n");
+ prompt.push_str("If a running step is no longer relevant to the NEW goal, note it but do not attempt to remove it.\n");
+ for step in &running {
+ prompt.push_str(&format!(
+ " 🔄 {} (id: {}): {}\n",
+ step.name,
+ step.id,
+ step.description.as_deref().unwrap_or("(no description)")
+ ));
+ }
}
- 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
- ));
+
+ // ── Pending / Ready steps ───────────────────────────────────
+ if !pending_ready.is_empty() {
+ prompt.push_str("\n── PENDING/READY steps (EVALUATE — remove if no longer relevant) ──\n");
+ prompt.push_str("These steps have NOT started yet. Evaluate each against the NEW goal:\n");
+ prompt.push_str(" • If still relevant → leave it in place (no action needed).\n");
+ prompt.push_str(" • If NO LONGER relevant → remove it with: makima directive remove-step <step_id>\n");
+ for step in &pending_ready {
+ prompt.push_str(&format!(
+ " ⏳ [{}] {} (id: {}): {}\n",
+ step.status,
+ step.name,
+ step.id,
+ step.description.as_deref().unwrap_or("(no description)")
+ ));
+ }
+ }
+
+ // ── Failed steps ────────────────────────────────────────────
+ if !failed.is_empty() {
+ prompt.push_str("\n── FAILED steps (EVALUATE — remove if no longer relevant) ──\n");
+ prompt.push_str("These steps failed. Evaluate each against the NEW goal:\n");
+ prompt.push_str(" • If still relevant → remove the failed step and re-add a corrected version.\n");
+ prompt.push_str(" • If NO LONGER relevant → remove it with: makima directive remove-step <step_id>\n");
+ for step in &failed {
+ prompt.push_str(&format!(
+ " ❌ {} (id: {}): {}\n",
+ step.name,
+ step.id,
+ step.description.as_deref().unwrap_or("(no description)")
+ ));
+ }
}
+
+ // ── Skipped steps ───────────────────────────────────────────
+ if !skipped.is_empty() {
+ prompt.push_str("\n── SKIPPED steps (remove if no longer relevant) ──\n");
+ for step in &skipped {
+ prompt.push_str(&format!(
+ " ⏭️ {} (id: {}): {}\n",
+ step.name,
+ step.id,
+ step.description.as_deref().unwrap_or("(no description)")
+ ));
+ }
+ }
+
+ // ── Instructions ────────────────────────────────────────────
prompt.push_str(&format!(
- "\nAdd new steps that build on or complement existing work. Use generation {}.\n\n",
+ "\n── ACTION PLAN ──\n\
+ 1. First, remove any pending/ready/failed/skipped steps that are NOT relevant to the NEW goal:\n\
+ \x20 makima directive remove-step <step_id>\n\
+ 2. Then, add new steps for the updated goal. Use generation {}.\n\
+ 3. New steps that build on completed work MUST use --depends-on to inherit the worktree.\n\
+ 4. Ensure the new plan fully addresses the UPDATED goal.\n\n",
generation
));
}
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs
index 25b2dc4..929769c 100644
--- a/makima/src/server/handlers/directives.rs
+++ b/makima/src/server/handlers/directives.rs
@@ -824,7 +824,28 @@ pub async fn update_goal(
};
match repository::update_directive_goal(pool, auth.owner_id, id, &req.goal).await {
- Ok(Some(directive)) => Json(directive).into_response(),
+ Ok(Some(directive)) => {
+ // Clear non-started steps so replanning starts fresh
+ match repository::clear_pending_directive_steps(pool, id).await {
+ Ok(count) => {
+ if count > 0 {
+ tracing::info!(
+ directive_id = %id,
+ removed_steps = count,
+ "Cleared pending steps after goal update — replanning will generate new steps"
+ );
+ }
+ }
+ Err(e) => {
+ tracing::warn!(
+ directive_id = %id,
+ error = %e,
+ "Failed to clear pending steps after goal update"
+ );
+ }
+ }
+ Json(directive).into_response()
+ }
Ok(None) => (
StatusCode::NOT_FOUND,
Json(ApiError::new("NOT_FOUND", "Directive not found")),