summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-23 14:28:01 +0000
committersoryu <soryu@soryu.co>2026-02-23 14:28:01 +0000
commit0d00b5bf060c3bd2d60a4c8402c4b6b218b0e9a2 (patch)
tree9af53f63049f5a2e64cafcb2855c849e9ae89dca
parentcd6a3b438592656849824e7c78a95a25d5f0a1b3 (diff)
parentd8e50fb4c150c8d4f25f9e4c7361eba80a90e4fe (diff)
downloadsoryu-makima/directive-soryu-co-soryu---makima-19fd3e1d-v1771856855.tar.gz
soryu-makima/directive-soryu-co-soryu---makima-19fd3e1d-v1771856855.zip
Merge remote-tracking branch 'origin/makima/soryu-co-soryu---makima--add-directive-memory-syst-81091fc0' into makima/directive-soryu-co-soryu---makima-19fd3e1d-v1771856855makima/directive-soryu-co-soryu---makima-19fd3e1d-v1771856855
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx193
-rw-r--r--makima/frontend/src/lib/api.ts51
-rw-r--r--makima/migrations/20260218000000_create_directive_memories.sql13
-rw-r--r--makima/src/bin/makima.rs45
-rw-r--r--makima/src/daemon/api/directive.rs83
-rw-r--r--makima/src/daemon/cli/directive.rs44
-rw-r--r--makima/src/daemon/cli/mod.rs18
-rw-r--r--makima/src/db/models.rs44
-rw-r--r--makima/src/db/repository.rs105
-rw-r--r--makima/src/orchestration/directive.rs22
-rw-r--r--makima/src/server/handlers/directives.rs374
-rw-r--r--makima/src/server/mod.rs12
12 files changed, 997 insertions, 7 deletions
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx
index 8f39207..a21b319 100644
--- a/makima/frontend/src/components/directives/DirectiveDetail.tsx
+++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx
@@ -1,5 +1,6 @@
-import { useState, useMemo, useEffect, useRef } from "react";
-import type { DirectiveWithSteps, DirectiveStatus, UpdateDirectiveRequest } from "../../lib/api";
+import { useState, useMemo, useEffect, useRef, useCallback } from "react";
+import type { DirectiveWithSteps, DirectiveStatus, UpdateDirectiveRequest, DirectiveMemory } from "../../lib/api";
+import { listDirectiveMemories, setDirectiveMemory, deleteDirectiveMemory } from "../../lib/api";
import { DirectiveDAG } from "./DirectiveDAG";
import type { SpecializedStep } from "./DirectiveDAG";
import { DirectiveLogStream } from "./DirectiveLogStream";
@@ -54,6 +55,40 @@ export function DirectiveDetail({
const [pickUpResult, setPickUpResult] = useState<string | null>(null);
const [creatingPR, setCreatingPR] = useState(false);
+ // Memory state
+ const [memories, setMemories] = useState<DirectiveMemory[]>([]);
+ const [memoryExpanded, setMemoryExpanded] = useState(false);
+ const [memoryLoading, setMemoryLoading] = useState(false);
+ const [newMemKey, setNewMemKey] = useState("");
+ const [newMemValue, setNewMemValue] = useState("");
+ const [editingMemKey, setEditingMemKey] = useState<string | null>(null);
+ const [editingMemValue, setEditingMemValue] = useState("");
+
+ const fetchMemories = useCallback(async () => {
+ try {
+ setMemoryLoading(true);
+ const mems = await listDirectiveMemories(directive.id);
+ setMemories(mems);
+ } catch (e) {
+ console.error("Failed to load memories:", e);
+ } finally {
+ setMemoryLoading(false);
+ }
+ }, [directive.id]);
+
+ // Load memories when section is expanded
+ useEffect(() => {
+ if (memoryExpanded) {
+ fetchMemories();
+ }
+ }, [memoryExpanded, fetchMemories]);
+
+ // Reset memory expansion when directive changes
+ useEffect(() => {
+ setMemoryExpanded(false);
+ setMemories([]);
+ }, [directive.id]);
+
// Sync goalText and reset editing state when directive changes
useEffect(() => {
setGoalText(directive.goal);
@@ -413,6 +448,160 @@ export function DirectiveDetail({
)}
</div>
+ {/* Memory */}
+ <div className="px-4 py-2 border-b border-[rgba(117,170,252,0.1)]">
+ <button
+ type="button"
+ onClick={() => setMemoryExpanded((prev) => !prev)}
+ className="flex items-center gap-1.5 w-full text-left"
+ >
+ <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
+ Memory {memories.length > 0 && `(${memories.length})`}
+ </span>
+ <span className="text-[9px] font-mono text-[#556677]">
+ {memoryExpanded ? "▼" : "▶"}
+ </span>
+ </button>
+ {memoryExpanded && (
+ <div className="mt-2">
+ <div className="flex items-center gap-2 mb-2">
+ <button
+ type="button"
+ onClick={fetchMemories}
+ className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]"
+ >
+ [refresh]
+ </button>
+ </div>
+ {memoryLoading ? (
+ <p className="text-[10px] font-mono text-[#556677]">Loading...</p>
+ ) : memories.length === 0 ? (
+ <p className="text-[10px] font-mono text-[#556677]">No memory entries</p>
+ ) : (
+ <div className="space-y-1 mb-2 max-h-[200px] overflow-y-auto">
+ {memories.map((mem) => (
+ <div
+ key={mem.id}
+ className="flex items-start gap-2 px-2 py-1.5 bg-[#0a1525] rounded text-[10px] font-mono group"
+ >
+ <span className="text-[#75aafc] shrink-0 min-w-[80px] break-all">
+ {mem.key}
+ </span>
+ {editingMemKey === mem.key ? (
+ <div className="flex-1 flex items-center gap-1">
+ <input
+ value={editingMemValue}
+ onChange={(e) => setEditingMemValue(e.target.value)}
+ className="flex-1 bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-1.5 py-0.5 text-[10px] font-mono text-white"
+ autoFocus
+ onKeyDown={async (e) => {
+ if (e.key === "Enter" && editingMemValue.trim()) {
+ await setDirectiveMemory(directive.id, mem.key, editingMemValue.trim());
+ setEditingMemKey(null);
+ fetchMemories();
+ } else if (e.key === "Escape") {
+ setEditingMemKey(null);
+ }
+ }}
+ />
+ <button
+ type="button"
+ onClick={async () => {
+ if (editingMemValue.trim()) {
+ await setDirectiveMemory(directive.id, mem.key, editingMemValue.trim());
+ setEditingMemKey(null);
+ fetchMemories();
+ }
+ }}
+ className="text-emerald-400 hover:text-emerald-300"
+ >
+ ✓
+ </button>
+ <button
+ type="button"
+ onClick={() => setEditingMemKey(null)}
+ className="text-[#556677] hover:text-white"
+ >
+ ✕
+ </button>
+ </div>
+ ) : (
+ <>
+ <span className="flex-1 text-[#c0d0e0] break-all whitespace-pre-wrap">
+ {mem.value}
+ </span>
+ <span className="shrink-0 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
+ <button
+ type="button"
+ onClick={() => {
+ setEditingMemKey(mem.key);
+ setEditingMemValue(mem.value);
+ }}
+ className="text-[#556677] hover:text-[#75aafc]"
+ title="Edit"
+ >
+ [edit]
+ </button>
+ <button
+ type="button"
+ onClick={async () => {
+ await deleteDirectiveMemory(directive.id, mem.key);
+ fetchMemories();
+ }}
+ className="text-[#556677] hover:text-red-400"
+ title="Delete"
+ >
+ [del]
+ </button>
+ </span>
+ </>
+ )}
+ </div>
+ ))}
+ </div>
+ )}
+ {/* Add new memory entry */}
+ <div className="flex items-center gap-1.5 mt-1">
+ <input
+ value={newMemKey}
+ onChange={(e) => setNewMemKey(e.target.value)}
+ placeholder="key"
+ className="w-[100px] bg-[#0a1628] border border-[rgba(117,170,252,0.15)] rounded px-1.5 py-0.5 text-[10px] font-mono text-white placeholder:text-[#334455]"
+ />
+ <input
+ value={newMemValue}
+ onChange={(e) => setNewMemValue(e.target.value)}
+ placeholder="value"
+ className="flex-1 bg-[#0a1628] border border-[rgba(117,170,252,0.15)] rounded px-1.5 py-0.5 text-[10px] font-mono text-white placeholder:text-[#334455]"
+ onKeyDown={async (e) => {
+ if (e.key === "Enter" && newMemKey.trim() && newMemValue.trim()) {
+ await setDirectiveMemory(directive.id, newMemKey.trim(), newMemValue.trim());
+ setNewMemKey("");
+ setNewMemValue("");
+ fetchMemories();
+ }
+ }}
+ />
+ <button
+ type="button"
+ disabled={!newMemKey.trim() || !newMemValue.trim()}
+ onClick={async () => {
+ if (newMemKey.trim() && newMemValue.trim()) {
+ await setDirectiveMemory(directive.id, newMemKey.trim(), newMemValue.trim());
+ setNewMemKey("");
+ setNewMemValue("");
+ fetchMemories();
+ }
+ }}
+ className="text-[9px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-1.5 py-0.5 disabled:opacity-30"
+ >
+ +Add
+ </button>
+ </div>
+ </div>
+ )}
+ </div>
+
{/* DAG */}
<div className="px-4 py-3 flex-1">
<span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-2">
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 458b69d..2f6059d 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -3313,6 +3313,57 @@ export async function pickUpOrders(directiveId: string): Promise<PickUpOrdersRes
}
// =============================================================================
+// Directive Memory API
+// =============================================================================
+
+export interface DirectiveMemory {
+ id: string;
+ directiveId: string;
+ key: string;
+ value: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export async function listDirectiveMemories(directiveId: string): Promise<DirectiveMemory[]> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/memory`);
+ if (!res.ok) throw new Error(`Failed to list memories: ${res.statusText}`);
+ return res.json();
+}
+
+export async function getDirectiveMemory(directiveId: string, key: string): Promise<DirectiveMemory> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/memory/${encodeURIComponent(key)}`);
+ if (!res.ok) throw new Error(`Failed to get memory: ${res.statusText}`);
+ return res.json();
+}
+
+export async function setDirectiveMemory(directiveId: string, key: string, value: string): Promise<DirectiveMemory> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/memory/${encodeURIComponent(key)}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ value }),
+ });
+ if (!res.ok) throw new Error(`Failed to set memory: ${res.statusText}`);
+ return res.json();
+}
+
+export async function deleteDirectiveMemory(directiveId: string, key: string): Promise<{ deleted: boolean }> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/memory/${encodeURIComponent(key)}`, {
+ method: "DELETE",
+ });
+ if (!res.ok) throw new Error(`Failed to delete memory: ${res.statusText}`);
+ return res.json();
+}
+
+export async function clearDirectiveMemories(directiveId: string): Promise<{ deleted: number }> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/memory`, {
+ method: "DELETE",
+ });
+ if (!res.ok) throw new Error(`Failed to clear memories: ${res.statusText}`);
+ return res.json();
+}
+
+// =============================================================================
// Orders API
// =============================================================================
diff --git a/makima/migrations/20260218000000_create_directive_memories.sql b/makima/migrations/20260218000000_create_directive_memories.sql
new file mode 100644
index 0000000..a9fd6c1
--- /dev/null
+++ b/makima/migrations/20260218000000_create_directive_memories.sql
@@ -0,0 +1,13 @@
+-- Re-implement directive memories (previous version was in 20260211000000 and dropped in 20260213000000)
+CREATE TABLE IF NOT EXISTS directive_memories (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ directive_id UUID NOT NULL REFERENCES directives(id) ON DELETE CASCADE,
+ key VARCHAR(255) NOT NULL,
+ value TEXT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ UNIQUE(directive_id, key)
+);
+
+CREATE INDEX IF NOT EXISTS idx_directive_memories_directive_id ON directive_memories(directive_id);
+CREATE INDEX IF NOT EXISTS idx_directive_memories_key ON directive_memories(directive_id, key);
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs
index 954f4a6..3a3defe 100644
--- a/makima/src/bin/makima.rs
+++ b/makima/src/bin/makima.rs
@@ -889,6 +889,51 @@ async fn run_directive(
}
}
}
+ DirectiveCommand::MemorySet(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let result = client
+ .directive_memory_set(args.common.directive_id, &args.key, &args.value)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::MemoryGet(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let result = client
+ .directive_memory_get(args.common.directive_id, &args.key)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::MemoryList(args) => {
+ let client = ApiClient::new(args.api_url, args.api_key)?;
+ let result = client
+ .directive_memory_list(args.directive_id)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::MemoryDelete(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let result = client
+ .directive_memory_delete(args.common.directive_id, &args.key)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::MemoryClear(args) => {
+ let client = ApiClient::new(args.api_url, args.api_key)?;
+ let result = client
+ .directive_memory_clear(args.directive_id)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::MemoryBatchSet(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let entries: std::collections::HashMap<String, String> =
+ serde_json::from_str(&args.json)
+ .map_err(|e| format!("Invalid JSON: {}", e))?;
+ let result = client
+ .directive_memory_batch_set(args.common.directive_id, entries)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
}
Ok(())
diff --git a/makima/src/daemon/api/directive.rs b/makima/src/daemon/api/directive.rs
index 1088eb7..f5d0912 100644
--- a/makima/src/daemon/api/directive.rs
+++ b/makima/src/daemon/api/directive.rs
@@ -147,6 +147,77 @@ impl ApiClient {
self.put(&format!("/api/v1/directives/{}", directive_id), &req).await
}
+ // ── Directive Memory ──────────────────────────────────────────────
+
+ /// Set (upsert) a directive memory entry.
+ pub async fn directive_memory_set(
+ &self,
+ directive_id: Uuid,
+ key: &str,
+ value: &str,
+ ) -> Result<JsonValue, ApiError> {
+ let req = SetMemoryRequest { value: value.to_string() };
+ self.put(
+ &format!("/api/v1/directives/{}/memory/{}", directive_id, key),
+ &req,
+ )
+ .await
+ }
+
+ /// Get a single directive memory entry.
+ pub async fn directive_memory_get(
+ &self,
+ directive_id: Uuid,
+ key: &str,
+ ) -> Result<JsonValue, ApiError> {
+ self.get(&format!("/api/v1/directives/{}/memory/{}", directive_id, key))
+ .await
+ }
+
+ /// List all directive memory entries.
+ pub async fn directive_memory_list(
+ &self,
+ directive_id: Uuid,
+ ) -> Result<JsonValue, ApiError> {
+ self.get(&format!("/api/v1/directives/{}/memory", directive_id))
+ .await
+ }
+
+ /// Delete a single directive memory entry.
+ pub async fn directive_memory_delete(
+ &self,
+ directive_id: Uuid,
+ key: &str,
+ ) -> Result<JsonValue, ApiError> {
+ self.delete_with_response(&format!(
+ "/api/v1/directives/{}/memory/{}",
+ directive_id, key
+ ))
+ .await
+ }
+
+ /// Clear all directive memory entries.
+ pub async fn directive_memory_clear(
+ &self,
+ directive_id: Uuid,
+ ) -> Result<JsonValue, ApiError> {
+ self.delete_with_response(&format!("/api/v1/directives/{}/memory", directive_id))
+ .await
+ }
+
+ /// Batch set multiple directive memory entries.
+ pub async fn directive_memory_batch_set(
+ &self,
+ directive_id: Uuid,
+ entries: std::collections::HashMap<String, String>,
+ ) -> Result<JsonValue, ApiError> {
+ let req = BatchSetMemoryClientRequest { entries };
+ self.post(
+ &format!("/api/v1/directives/{}/memory/batch", directive_id),
+ &req,
+ )
+ .await
+ }
}
#[derive(Serialize)]
@@ -159,3 +230,15 @@ pub struct UpdateDirectiveMetadataRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SetMemoryRequest {
+ pub value: String,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BatchSetMemoryClientRequest {
+ pub entries: std::collections::HashMap<String, String>,
+}
diff --git a/makima/src/daemon/cli/directive.rs b/makima/src/daemon/cli/directive.rs
index 7c8451f..c3c7490 100644
--- a/makima/src/daemon/cli/directive.rs
+++ b/makima/src/daemon/cli/directive.rs
@@ -169,3 +169,47 @@ pub struct UpdateArgs {
pub status: Option<String>,
}
+/// Arguments for memory-set command.
+#[derive(Args, Debug)]
+pub struct MemorySetArgs {
+ #[command(flatten)]
+ pub common: DirectiveArgs,
+
+ /// Memory key
+ pub key: String,
+
+ /// Memory value
+ pub value: String,
+}
+
+/// Arguments for memory-get command.
+#[derive(Args, Debug)]
+pub struct MemoryGetArgs {
+ #[command(flatten)]
+ pub common: DirectiveArgs,
+
+ /// Memory key
+ pub key: String,
+}
+
+/// Arguments for memory-delete command.
+#[derive(Args, Debug)]
+pub struct MemoryDeleteArgs {
+ #[command(flatten)]
+ pub common: DirectiveArgs,
+
+ /// Memory key to delete
+ pub key: String,
+}
+
+/// Arguments for memory-batch-set command.
+#[derive(Args, Debug)]
+pub struct MemoryBatchSetArgs {
+ #[command(flatten)]
+ pub common: DirectiveArgs,
+
+ /// JSON object of key-value pairs: {"key1": "value1", "key2": "value2"}
+ #[arg(long)]
+ pub json: String,
+}
+
diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs
index 8063541..1c13f7b 100644
--- a/makima/src/daemon/cli/mod.rs
+++ b/makima/src/daemon/cli/mod.rs
@@ -252,6 +252,24 @@ pub enum DirectiveCommand {
/// Ask a question and wait for user feedback
Ask(directive::AskArgs),
+
+ /// Set a memory entry
+ MemorySet(directive::MemorySetArgs),
+
+ /// Get a memory entry
+ MemoryGet(directive::MemoryGetArgs),
+
+ /// List all memory entries
+ MemoryList(DirectiveArgs),
+
+ /// Delete a memory entry
+ MemoryDelete(directive::MemoryDeleteArgs),
+
+ /// Clear all memory entries
+ MemoryClear(DirectiveArgs),
+
+ /// Batch set memory entries from JSON
+ MemoryBatchSet(directive::MemoryBatchSetArgs),
}
impl Cli {
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index 6b77563..5e929f0 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -2988,4 +2988,48 @@ pub struct LinkDirectiveRequest {
pub directive_id: Uuid,
}
+// =============================================================================
+// Directive Memory Models
+// =============================================================================
+
+/// A key-value memory entry associated with a directive.
+#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveMemory {
+ pub id: Uuid,
+ pub directive_id: Uuid,
+ pub key: String,
+ pub value: String,
+ pub created_at: DateTime<Utc>,
+ pub updated_at: DateTime<Utc>,
+}
+
+/// Request to set a single memory entry value.
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct SetMemoryRequest {
+ pub value: String,
+}
+
+/// Request to batch-set multiple memory entries.
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct BatchSetMemoryRequest {
+ pub entries: std::collections::HashMap<String, String>,
+}
+
+/// Response for memory clear/delete operations.
+#[derive(Debug, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct MemoryClearResponse {
+ pub deleted: i64,
+}
+
+/// Response for memory delete operation.
+#[derive(Debug, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct MemoryDeleteResponse {
+ pub deleted: bool,
+}
+
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 8d7a70c..b81ab5c 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -11,7 +11,7 @@ use super::models::{
ContractTypeTemplateRecord, ConversationMessage, ConversationSnapshot,
CreateContractRequest, CreateFileRequest, CreateTaskRequest,
CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity,
- DeliverableDefinition, Directive, DirectiveStep, DirectiveSummary,
+ DeliverableDefinition, Directive, DirectiveMemory, DirectiveStep, DirectiveSummary,
CreateDirectiveRequest, CreateDirectiveStepRequest, DirectiveGoalHistory,
UpdateDirectiveRequest, UpdateDirectiveStepRequest,
CreateOrderRequest, Order, UpdateOrderRequest,
@@ -6045,6 +6045,109 @@ pub async fn get_directive_max_generation(
}
// =============================================================================
+// Directive Memory CRUD
+// =============================================================================
+
+/// Set (upsert) a directive memory entry.
+pub async fn directive_memory_set(
+ pool: &PgPool,
+ directive_id: Uuid,
+ key: &str,
+ value: &str,
+) -> Result<DirectiveMemory, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveMemory>(
+ r#"INSERT INTO directive_memories (directive_id, key, value)
+ VALUES ($1, $2, $3)
+ ON CONFLICT (directive_id, key) DO UPDATE
+ SET value = EXCLUDED.value, updated_at = NOW()
+ RETURNING id, directive_id, key, value, created_at, updated_at"#,
+ )
+ .bind(directive_id)
+ .bind(key)
+ .bind(value)
+ .fetch_one(pool)
+ .await
+}
+
+/// Get a single directive memory entry by key.
+pub async fn directive_memory_get(
+ pool: &PgPool,
+ directive_id: Uuid,
+ key: &str,
+) -> Result<Option<DirectiveMemory>, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveMemory>(
+ r#"SELECT id, directive_id, key, value, created_at, updated_at
+ FROM directive_memories
+ WHERE directive_id = $1 AND key = $2"#,
+ )
+ .bind(directive_id)
+ .bind(key)
+ .fetch_optional(pool)
+ .await
+}
+
+/// List all directive memory entries, ordered by key.
+pub async fn directive_memory_list(
+ pool: &PgPool,
+ directive_id: Uuid,
+) -> Result<Vec<DirectiveMemory>, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveMemory>(
+ r#"SELECT id, directive_id, key, value, created_at, updated_at
+ FROM directive_memories
+ WHERE directive_id = $1
+ ORDER BY key"#,
+ )
+ .bind(directive_id)
+ .fetch_all(pool)
+ .await
+}
+
+/// Delete a single directive memory entry by key. Returns true if a row was deleted.
+pub async fn directive_memory_delete(
+ pool: &PgPool,
+ directive_id: Uuid,
+ key: &str,
+) -> Result<bool, sqlx::Error> {
+ let result = sqlx::query(
+ r#"DELETE FROM directive_memories WHERE directive_id = $1 AND key = $2"#,
+ )
+ .bind(directive_id)
+ .bind(key)
+ .execute(pool)
+ .await?;
+ Ok(result.rows_affected() > 0)
+}
+
+/// Clear all directive memory entries. Returns number of deleted rows.
+pub async fn directive_memory_clear(
+ pool: &PgPool,
+ directive_id: Uuid,
+) -> Result<i64, sqlx::Error> {
+ let result = sqlx::query(
+ r#"DELETE FROM directive_memories WHERE directive_id = $1"#,
+ )
+ .bind(directive_id)
+ .execute(pool)
+ .await?;
+ Ok(result.rows_affected() as i64)
+}
+
+/// Batch set directive memory entries (upsert multiple).
+pub async fn directive_memory_batch_set(
+ pool: &PgPool,
+ directive_id: Uuid,
+ entries: &std::collections::HashMap<String, String>,
+) -> Result<Vec<DirectiveMemory>, sqlx::Error> {
+ let mut results = Vec::with_capacity(entries.len());
+ for (key, value) in entries {
+ let mem = directive_memory_set(pool, directive_id, key, value).await?;
+ results.push(mem);
+ }
+ results.sort_by(|a, b| a.key.cmp(&b.key));
+ Ok(results)
+}
+
+// =============================================================================
// Order CRUD
// =============================================================================
diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs
index b91781c..02cbe03 100644
--- a/makima/src/orchestration/directive.rs
+++ b/makima/src/orchestration/directive.rs
@@ -167,7 +167,16 @@ impl DirectiveOrchestrator {
When done, the system will automatically mark this step as completed.\n\
If you cannot complete the task, report the failure clearly.\n\n\
If you need clarification or encounter a decision that requires user input, you can ask:\n\
- \x20 makima directive ask \"Your question\" --phaseguard",
+ \x20 makima directive ask \"Your question\" --phaseguard\n\n\
+ DIRECTIVE MEMORY:\n\
+ You can read/write directive memory to share context between steps:\n\
+ \x20 makima directive memory-list # see all stored context\n\
+ \x20 makima directive memory-get <key> # read a specific entry\n\
+ \x20 makima directive memory-set <key> <value> # store a value\n\
+ \x20 makima directive memory-delete <key> # remove an entry\n\
+ \x20 makima directive memory-batch-set --json '{{...}}' # set multiple entries\n\
+ At the start of your task, run `makima directive memory-list` to see context from previous steps.\n\
+ Before finishing, store any decisions or discovered information that downstream steps may need.",
directive_title = step.directive_title,
step_name = step.step_name,
description = step.step_description.as_deref().unwrap_or("(none)"),
@@ -1385,6 +1394,17 @@ Do NOT ask questions for:
- Implementation details you can determine from the codebase
- Standard engineering decisions with clear best practices
- Trivial choices that do not significantly affect the outcome
+
+DIRECTIVE MEMORY:
+You can use directive memory to share context, decisions, and discovered information between steps:
+ makima directive memory-list # see all stored context
+ makima directive memory-get <key> # read a specific entry
+ makima directive memory-set <key> <value> # store a value
+ makima directive memory-batch-set --json '{{...}}' # set multiple entries at once
+
+Use memory during planning to record architectural decisions, technology choices, or patterns discovered
+while exploring the repository. Steps you create will have access to these memories and can use them
+to avoid re-discovering information. Include instructions in step taskPlans to check memory-list first.
"#,
title = directive.title,
goal = directive.goal,
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs
index 992affe..b6bf11d 100644
--- a/makima/src/server/handlers/directives.rs
+++ b/makima/src/server/handlers/directives.rs
@@ -9,9 +9,10 @@ use axum::{
use uuid::Uuid;
use crate::db::models::{
- CleanupResponse, CleanupTasksResponse, CreateDirectiveRequest, CreateTaskRequest,
- CreateDirectiveStepRequest, Directive, DirectiveListResponse,
- DirectiveStep, DirectiveWithSteps, PickUpOrdersResponse,
+ BatchSetMemoryRequest, CleanupResponse, CleanupTasksResponse, CreateDirectiveRequest,
+ CreateTaskRequest, CreateDirectiveStepRequest, Directive, DirectiveListResponse,
+ DirectiveMemory, DirectiveStep, DirectiveWithSteps, MemoryClearResponse,
+ MemoryDeleteResponse, PickUpOrdersResponse, SetMemoryRequest,
UpdateDirectiveRequest, UpdateDirectiveStepRequest, UpdateGoalRequest,
UpdateOrderRequest,
};
@@ -1375,3 +1376,370 @@ pub async fn pick_up_orders(
})
.into_response()
}
+
+// =============================================================================
+// Directive Memory
+// =============================================================================
+
+/// Set (upsert) a directive memory entry.
+#[utoipa::path(
+ put,
+ path = "/api/v1/directives/{id}/memory/{key}",
+ request_body = SetMemoryRequest,
+ responses(
+ (status = 200, description = "Memory entry set", body = DirectiveMemory),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Directive not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directive Memory"
+)]
+pub async fn set_memory(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, key)): Path<(Uuid, String)>,
+ Json(body): Json<SetMemoryRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify directive belongs to this owner
+ match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get directive: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::directive_memory_set(pool, id, &key, &body.value).await {
+ Ok(mem) => Json(mem).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to set memory: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("SET_MEMORY_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Get a single directive memory entry.
+#[utoipa::path(
+ get,
+ path = "/api/v1/directives/{id}/memory/{key}",
+ responses(
+ (status = 200, description = "Memory entry", body = DirectiveMemory),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Entry not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directive Memory"
+)]
+pub async fn get_memory(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, key)): Path<(Uuid, String)>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify directive belongs to this owner
+ match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get directive: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::directive_memory_get(pool, id, &key).await {
+ Ok(Some(mem)) => Json(mem).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", &format!("Memory key '{}' not found", key))),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to get memory: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("GET_MEMORY_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// List all directive memory entries.
+#[utoipa::path(
+ get,
+ path = "/api/v1/directives/{id}/memory",
+ responses(
+ (status = 200, description = "List of memory entries", body = Vec<DirectiveMemory>),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Directive not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directive Memory"
+)]
+pub async fn list_memory(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify directive belongs to this owner
+ match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get directive: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::directive_memory_list(pool, id).await {
+ Ok(memories) => Json(memories).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to list memory: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("LIST_MEMORY_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Delete a single directive memory entry.
+#[utoipa::path(
+ delete,
+ path = "/api/v1/directives/{id}/memory/{key}",
+ responses(
+ (status = 200, description = "Memory entry deleted", body = MemoryDeleteResponse),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Directive not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directive Memory"
+)]
+pub async fn delete_memory(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, key)): Path<(Uuid, String)>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify directive belongs to this owner
+ match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get directive: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::directive_memory_delete(pool, id, &key).await {
+ Ok(deleted) => Json(MemoryDeleteResponse { deleted }).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to delete memory: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DELETE_MEMORY_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Clear all directive memory entries.
+#[utoipa::path(
+ delete,
+ path = "/api/v1/directives/{id}/memory",
+ responses(
+ (status = 200, description = "All memory entries cleared", body = MemoryClearResponse),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Directive not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directive Memory"
+)]
+pub async fn clear_memory(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify directive belongs to this owner
+ match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get directive: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::directive_memory_clear(pool, id).await {
+ Ok(deleted) => Json(MemoryClearResponse { deleted }).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to clear memory: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("CLEAR_MEMORY_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Batch set multiple directive memory entries.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/memory/batch",
+ request_body = BatchSetMemoryRequest,
+ responses(
+ (status = 200, description = "Batch memory entries set", body = Vec<DirectiveMemory>),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Directive not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directive Memory"
+)]
+pub async fn batch_set_memory(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(body): Json<BatchSetMemoryRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify directive belongs to this owner
+ match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get directive: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::directive_memory_batch_set(pool, id, &body.entries).await {
+ Ok(memories) => Json(memories).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to batch set memory: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("BATCH_SET_MEMORY_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index 6044418..3255e1f 100644
--- a/makima/src/server/mod.rs
+++ b/makima/src/server/mod.rs
@@ -244,6 +244,18 @@ pub fn make_router(state: SharedState) -> Router {
.route("/directives/{id}/cleanup", post(directives::cleanup_directive))
.route("/directives/{id}/create-pr", post(directives::create_pr))
.route("/directives/{id}/pick-up-orders", post(directives::pick_up_orders))
+ // Directive memory endpoints
+ .route(
+ "/directives/{id}/memory",
+ get(directives::list_memory).delete(directives::clear_memory),
+ )
+ .route("/directives/{id}/memory/batch", post(directives::batch_set_memory))
+ .route(
+ "/directives/{id}/memory/{key}",
+ get(directives::get_memory)
+ .put(directives::set_memory)
+ .delete(directives::delete_memory),
+ )
// Order endpoints
.route(
"/orders",