diff options
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveDetail.tsx | 193 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 51 | ||||
| -rw-r--r-- | makima/migrations/20260218000000_create_directive_memories.sql | 13 | ||||
| -rw-r--r-- | makima/src/bin/makima.rs | 45 | ||||
| -rw-r--r-- | makima/src/daemon/api/directive.rs | 83 | ||||
| -rw-r--r-- | makima/src/daemon/cli/directive.rs | 44 | ||||
| -rw-r--r-- | makima/src/daemon/cli/mod.rs | 18 | ||||
| -rw-r--r-- | makima/src/db/models.rs | 44 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 105 | ||||
| -rw-r--r-- | makima/src/orchestration/directive.rs | 22 | ||||
| -rw-r--r-- | makima/src/server/handlers/directives.rs | 374 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 12 |
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", |
