summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-16 00:28:16 +0000
committerGitHub <noreply@github.com>2026-02-16 00:28:16 +0000
commita9da99085bc0b1f94e13cb27639915fd1398ccbe (patch)
tree7b990499368002af8aa72b8e7b619674d8d5c654
parentbf087f48af2962d884b861345ae52be4f4a54daa (diff)
downloadsoryu-a9da99085bc0b1f94e13cb27639915fd1398ccbe.tar.gz
soryu-a9da99085bc0b1f94e13cb27639915fd1398ccbe.zip
feat: track directive goal history for intelligent re-planning (#63)
* WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Save previous goal on update and include history in re-planning prompt
-rw-r--r--makima/migrations/20260215100000_add_directive_goal_history.sql9
-rw-r--r--makima/src/db/models.rs10
-rw-r--r--makima/src/db/repository.rs40
-rw-r--r--makima/src/orchestration/directive.rs46
-rw-r--r--makima/src/server/handlers/directives.rs45
5 files changed, 123 insertions, 27 deletions
diff --git a/makima/migrations/20260215100000_add_directive_goal_history.sql b/makima/migrations/20260215100000_add_directive_goal_history.sql
new file mode 100644
index 0000000..1636110
--- /dev/null
+++ b/makima/migrations/20260215100000_add_directive_goal_history.sql
@@ -0,0 +1,9 @@
+CREATE TABLE directive_goal_history (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ directive_id UUID NOT NULL REFERENCES directives(id) ON DELETE CASCADE,
+ goal TEXT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE INDEX idx_directive_goal_history_directive_id ON directive_goal_history(directive_id);
+CREATE INDEX idx_directive_goal_history_created_at ON directive_goal_history(directive_id, created_at DESC);
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index 6ec6cf4..19ebb13 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -2723,6 +2723,16 @@ pub struct Directive {
pub updated_at: DateTime<Utc>,
}
+/// A historical record of a directive goal change.
+#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveGoalHistory {
+ pub id: Uuid,
+ pub directive_id: Uuid,
+ pub goal: String,
+ pub created_at: DateTime<Utc>,
+}
+
/// A step in a directive's DAG.
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 71ad524..4aa09dc 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -12,8 +12,8 @@ use super::models::{
CreateContractRequest, CreateFileRequest, CreateTaskRequest,
CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity,
DeliverableDefinition, Directive, DirectiveStep, DirectiveSummary,
- CreateDirectiveRequest, CreateDirectiveStepRequest, UpdateDirectiveRequest,
- UpdateDirectiveStepRequest,
+ CreateDirectiveRequest, CreateDirectiveStepRequest, DirectiveGoalHistory,
+ UpdateDirectiveRequest, UpdateDirectiveStepRequest,
CreateOrderRequest, Order, UpdateOrderRequest,
File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters,
MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig,
@@ -5600,6 +5600,42 @@ pub async fn update_directive_goal(
.await
}
+/// Save a goal to the directive goal history.
+pub async fn save_directive_goal_history(
+ pool: &PgPool,
+ directive_id: Uuid,
+ goal: &str,
+) -> Result<(), sqlx::Error> {
+ sqlx::query(
+ r#"INSERT INTO directive_goal_history (directive_id, goal)
+ VALUES ($1, $2)"#,
+ )
+ .bind(directive_id)
+ .bind(goal)
+ .execute(pool)
+ .await?;
+ Ok(())
+}
+
+/// Get recent goal history for a directive (most recent first), limited to limit entries.
+pub async fn get_directive_goal_history(
+ pool: &PgPool,
+ directive_id: Uuid,
+ limit: i64,
+) -> Result<Vec<DirectiveGoalHistory>, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveGoalHistory>(
+ r#"SELECT id, directive_id, goal, created_at
+ FROM directive_goal_history
+ WHERE directive_id = $1
+ ORDER BY created_at DESC
+ LIMIT $2"#,
+ )
+ .bind(directive_id)
+ .bind(limit)
+ .fetch_all(pool)
+ .await
+}
+
/// Set a directive's status (used for start/pause/archive transitions).
pub async fn set_directive_status(
pool: &PgPool,
diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs
index 5f8cb486..92aacde 100644
--- a/makima/src/orchestration/directive.rs
+++ b/makima/src/orchestration/directive.rs
@@ -44,7 +44,7 @@ impl DirectiveOrchestrator {
"Directive needs planning — spawning planning task"
);
- let plan = build_planning_prompt(&directive, &[], 1);
+ let plan = build_planning_prompt(&directive, &[], 1, &[]);
if let Err(e) = self
.spawn_orchestrator_task(
@@ -317,8 +317,11 @@ impl DirectiveOrchestrator {
repository::list_directive_steps(&self.pool, directive.id).await?;
let generation =
repository::get_directive_max_generation(&self.pool, directive.id).await? + 1;
+ let goal_history =
+ repository::get_directive_goal_history(&self.pool, directive.id, 3).await?;
- let plan = build_planning_prompt(&directive, &existing_steps, generation);
+ let plan =
+ build_planning_prompt(&directive, &existing_steps, generation, &goal_history);
if let Err(e) = self
.spawn_orchestrator_task(
@@ -846,6 +849,7 @@ fn build_planning_prompt(
directive: &crate::db::models::Directive,
existing_steps: &[crate::db::models::DirectiveStep],
generation: i32,
+ goal_history: &[crate::db::models::DirectiveGoalHistory],
) -> String {
let mut prompt = String::new();
@@ -854,8 +858,42 @@ fn build_planning_prompt(
prompt.push_str(&format!(
"⚠️ 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",
+ relevant. Review each step below and act according to the instructions per status category.\n\n",
+ ));
+
+ // ── Goal changes section ──────────────────────────────────
+ if !goal_history.is_empty() {
+ prompt.push_str("-- GOAL CHANGES --\n");
+ prompt.push_str("The goal has been updated. Compare the previous and current goals to understand what changed:\n\n");
+ for (i, entry) in goal_history.iter().enumerate() {
+ if i == 0 {
+ prompt.push_str(&format!(
+ "PREVIOUS GOAL (replaced at {}):\n{}\n\n",
+ entry.created_at.format("%Y-%m-%d %H:%M:%S UTC"),
+ entry.goal
+ ));
+ } else {
+ prompt.push_str(&format!(
+ "OLDER GOAL (version from {}):\n{}\n\n",
+ entry.created_at.format("%Y-%m-%d %H:%M:%S UTC"),
+ entry.goal
+ ));
+ }
+ }
+ prompt.push_str(&format!(
+ "CURRENT GOAL (what you must plan for):\n{}\n\n",
+ directive.goal
+ ));
+ prompt.push_str(
+ "IMPORTANT: Analyze what CHANGED between the previous goal and the current goal.\n\
+ - If the change is minor (e.g., clarification, small addition), try to KEEP existing pending steps and only add/modify what is needed for the delta.\n\
+ - If the change is major (e.g., completely different objective), you may need to remove most pending steps and create a fresh plan.\n\
+ - Always preserve completed and running steps - they represent work already done.\n\n",
+ );
+ }
+
+ prompt.push_str(&format!(
+ "EXISTING STEPS (generation {}):\n",
generation - 1
));
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs
index 929769c..6060171 100644
--- a/makima/src/server/handlers/directives.rs
+++ b/makima/src/server/handlers/directives.rs
@@ -823,29 +823,32 @@ pub async fn update_goal(
.into_response();
};
- match repository::update_directive_goal(pool, auth.owner_id, id, &req.goal).await {
- 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"
- );
- }
+ // Save old goal to history before overwriting (best-effort)
+ match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some(current)) => {
+ if let Err(e) = repository::save_directive_goal_history(pool, id, &current.goal).await
+ {
+ tracing::warn!(
+ directive_id = %id,
+ error = %e,
+ "Failed to save goal history before update — continuing with goal update"
+ );
}
- Json(directive).into_response()
}
+ Ok(None) => {
+ // Directive not found — update_directive_goal will handle this
+ }
+ Err(e) => {
+ tracing::warn!(
+ directive_id = %id,
+ error = %e,
+ "Failed to fetch current directive for goal history — continuing with goal update"
+ );
+ }
+ }
+
+ match repository::update_directive_goal(pool, auth.owner_id, id, &req.goal).await {
+ Ok(Some(directive)) => Json(directive).into_response(),
Ok(None) => (
StatusCode::NOT_FOUND,
Json(ApiError::new("NOT_FOUND", "Directive not found")),