summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-09 00:11:51 +0000
committersoryu <soryu@soryu.co>2026-02-09 00:11:51 +0000
commit8c23b3ab6f7fabca01b0468911bae073aa5ced32 (patch)
treef50159aee13b13f0b55618ac09e9be1f89a41bb2
parent3662b334dfd68cfdf00ed44ae88927c2e1b2aabe (diff)
downloadsoryu-8c23b3ab6f7fabca01b0468911bae073aa5ced32.tar.gz
soryu-8c23b3ab6f7fabca01b0468911bae073aa5ced32.zip
Add new directive mechanism v3
-rw-r--r--makima/frontend/src/components/NavStrip.tsx1
-rw-r--r--makima/frontend/src/components/directives/DirectiveDAG.tsx87
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx216
-rw-r--r--makima/frontend/src/components/directives/DirectiveList.tsx87
-rw-r--r--makima/frontend/src/components/directives/StepNode.tsx82
-rw-r--r--makima/frontend/src/hooks/useDirectives.ts150
-rw-r--r--makima/frontend/src/lib/api.ts222
-rw-r--r--makima/frontend/src/main.tsx17
-rw-r--r--makima/frontend/src/routes/directives.tsx168
-rw-r--r--makima/frontend/tsconfig.tsbuildinfo2
-rw-r--r--makima/migrations/20260210000000_create_directive_system.sql50
-rw-r--r--makima/src/bin/makima.rs105
-rw-r--r--makima/src/daemon/api/directive.rs124
-rw-r--r--makima/src/daemon/api/mod.rs1
-rw-r--r--makima/src/daemon/cli/directive.rs101
-rw-r--r--makima/src/daemon/cli/mod.rs49
-rw-r--r--makima/src/daemon/skills/directive.md111
-rw-r--r--makima/src/daemon/skills/mod.rs4
-rw-r--r--makima/src/db/models.rs151
-rw-r--r--makima/src/db/repository.rs415
-rw-r--r--makima/src/llm/contract_tools.rs488
-rw-r--r--makima/src/server/handlers/contract_chat.rs29
-rw-r--r--makima/src/server/handlers/contracts.rs2
-rw-r--r--makima/src/server/handlers/directives.rs841
-rw-r--r--makima/src/server/handlers/mesh.rs8
-rw-r--r--makima/src/server/handlers/mesh_chat.rs2
-rw-r--r--makima/src/server/handlers/mesh_daemon.rs17
-rw-r--r--makima/src/server/handlers/mesh_supervisor.rs2
-rw-r--r--makima/src/server/handlers/mod.rs1
-rw-r--r--makima/src/server/handlers/transcript_analysis.rs4
-rw-r--r--makima/src/server/mod.rs26
-rw-r--r--makima/src/server/openapi.rs39
32 files changed, 3085 insertions, 517 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index fb95c7f..46fef7a 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -13,6 +13,7 @@ const NAV_LINKS: NavLink[] = [
{ label: "Contracts", href: "/contracts", requiresAuth: true },
{ label: "Board", href: "/workflow", requiresAuth: true },
{ label: "Mesh", href: "/mesh", requiresAuth: true },
+ { label: "Directives", href: "/directives", requiresAuth: true },
{ label: "History", href: "/history", requiresAuth: true },
];
diff --git a/makima/frontend/src/components/directives/DirectiveDAG.tsx b/makima/frontend/src/components/directives/DirectiveDAG.tsx
new file mode 100644
index 0000000..f288a0d
--- /dev/null
+++ b/makima/frontend/src/components/directives/DirectiveDAG.tsx
@@ -0,0 +1,87 @@
+import { useMemo } from "react";
+import type { DirectiveStep } from "../../lib/api";
+import { StepNode } from "./StepNode";
+
+interface DirectiveDAGProps {
+ steps: DirectiveStep[];
+ onComplete?: (stepId: string) => void;
+ onFail?: (stepId: string) => void;
+ onSkip?: (stepId: string) => void;
+}
+
+interface Layer {
+ steps: DirectiveStep[];
+}
+
+function topoSort(steps: DirectiveStep[]): Layer[] {
+ if (steps.length === 0) return [];
+
+ const stepMap = new Map(steps.map((s) => [s.id, s]));
+ const assigned = new Set<string>();
+ const layers: Layer[] = [];
+
+ // Iteratively find steps whose dependencies are all assigned
+ let remaining = [...steps];
+ while (remaining.length > 0) {
+ const layer: DirectiveStep[] = [];
+ for (const step of remaining) {
+ const depsResolved = step.dependsOn.every(
+ (depId) => assigned.has(depId) || !stepMap.has(depId)
+ );
+ if (depsResolved) {
+ layer.push(step);
+ }
+ }
+
+ if (layer.length === 0) {
+ // Cycle detected or orphaned — push all remaining
+ layers.push({ steps: remaining });
+ break;
+ }
+
+ for (const s of layer) {
+ assigned.add(s.id);
+ }
+ layers.push({ steps: layer.sort((a, b) => a.orderIndex - b.orderIndex) });
+ remaining = remaining.filter((s) => !assigned.has(s.id));
+ }
+
+ return layers;
+}
+
+export function DirectiveDAG({ steps, onComplete, onFail, onSkip }: DirectiveDAGProps) {
+ const layers = useMemo(() => topoSort(steps), [steps]);
+
+ if (steps.length === 0) {
+ return (
+ <div className="text-center py-8 text-[#7788aa] font-mono text-sm">
+ No steps yet. Add steps to build the DAG.
+ </div>
+ );
+ }
+
+ return (
+ <div className="flex flex-col gap-4 items-center py-4">
+ {layers.map((layer, layerIdx) => (
+ <div key={layerIdx}>
+ {layerIdx > 0 && (
+ <div className="flex justify-center py-1">
+ <div className="w-px h-4 bg-[#2a3a5a]" />
+ </div>
+ )}
+ <div className="flex flex-wrap gap-3 justify-center">
+ {layer.steps.map((step) => (
+ <StepNode
+ key={step.id}
+ step={step}
+ onComplete={onComplete ? () => onComplete(step.id) : undefined}
+ onFail={onFail ? () => onFail(step.id) : undefined}
+ onSkip={onSkip ? () => onSkip(step.id) : undefined}
+ />
+ ))}
+ </div>
+ </div>
+ ))}
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx
new file mode 100644
index 0000000..abd2c55
--- /dev/null
+++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx
@@ -0,0 +1,216 @@
+import { useState } from "react";
+import type { DirectiveWithSteps, DirectiveStatus } from "../../lib/api";
+import { DirectiveDAG } from "./DirectiveDAG";
+
+const STATUS_BADGE: Record<DirectiveStatus, { color: string; label: string }> = {
+ draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" },
+ active: { color: "text-green-400 border-green-800", label: "ACTIVE" },
+ idle: { color: "text-yellow-400 border-yellow-800", label: "IDLE" },
+ paused: { color: "text-orange-400 border-orange-800", label: "PAUSED" },
+ archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
+};
+
+interface DirectiveDetailProps {
+ directive: DirectiveWithSteps;
+ onStart: () => void;
+ onPause: () => void;
+ onAdvance: () => void;
+ onCompleteStep: (stepId: string) => void;
+ onFailStep: (stepId: string) => void;
+ onSkipStep: (stepId: string) => void;
+ onUpdateGoal: (goal: string) => void;
+ onDelete: () => void;
+ onRefresh: () => void;
+}
+
+export function DirectiveDetail({
+ directive,
+ onStart,
+ onPause,
+ onAdvance,
+ onCompleteStep,
+ onFailStep,
+ onSkipStep,
+ onUpdateGoal,
+ onDelete,
+ onRefresh,
+}: DirectiveDetailProps) {
+ const [editingGoal, setEditingGoal] = useState(false);
+ const [goalText, setGoalText] = useState(directive.goal);
+ const badge = STATUS_BADGE[directive.status] || STATUS_BADGE.draft;
+
+ const completedSteps = directive.steps.filter((s) => s.status === "completed").length;
+ const totalSteps = directive.steps.length;
+ const progress = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
+
+ const handleGoalSave = () => {
+ if (goalText.trim() && goalText !== directive.goal) {
+ onUpdateGoal(goalText.trim());
+ }
+ setEditingGoal(false);
+ };
+
+ return (
+ <div className="flex flex-col h-full overflow-y-auto">
+ {/* Header */}
+ <div className="px-4 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]">
+ <div className="flex items-center justify-between mb-2">
+ <h2 className="text-[14px] font-mono text-white font-medium truncate pr-2">
+ {directive.title}
+ </h2>
+ <div className="flex items-center gap-2 shrink-0">
+ <span
+ className={`text-[10px] font-mono ${badge.color} border rounded px-2 py-0.5`}
+ >
+ {badge.label}
+ </span>
+ <button
+ type="button"
+ onClick={onRefresh}
+ className="text-[10px] font-mono text-[#7788aa] hover:text-white"
+ title="Refresh"
+ >
+ [refresh]
+ </button>
+ </div>
+ </div>
+
+ {/* Progress bar */}
+ {totalSteps > 0 && (
+ <div className="flex items-center gap-2 mb-2">
+ <div className="flex-1 h-1.5 bg-[#1a2540] rounded overflow-hidden">
+ <div
+ className="h-full bg-emerald-600 rounded transition-all"
+ style={{ width: `${progress}%` }}
+ />
+ </div>
+ <span className="text-[10px] font-mono text-[#7788aa] shrink-0">
+ {completedSteps}/{totalSteps} steps
+ </span>
+ </div>
+ )}
+
+ {/* Repo info */}
+ {(directive.repositoryUrl || directive.localPath) && (
+ <div className="text-[10px] font-mono text-[#556677] mb-2 truncate">
+ {directive.repositoryUrl || directive.localPath}
+ {directive.baseBranch && ` @ ${directive.baseBranch}`}
+ </div>
+ )}
+
+ {/* Controls */}
+ <div className="flex flex-wrap gap-2">
+ {(directive.status === "draft" || directive.status === "paused") && (
+ <button
+ type="button"
+ onClick={onStart}
+ className="text-[10px] font-mono text-green-400 hover:text-green-300 border border-green-800 rounded px-2 py-1"
+ >
+ Start
+ </button>
+ )}
+ {directive.status === "active" && (
+ <>
+ <button
+ type="button"
+ onClick={onPause}
+ className="text-[10px] font-mono text-orange-400 hover:text-orange-300 border border-orange-800 rounded px-2 py-1"
+ >
+ Pause
+ </button>
+ <button
+ type="button"
+ onClick={onAdvance}
+ className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1"
+ >
+ Advance
+ </button>
+ </>
+ )}
+ {directive.status === "idle" && (
+ <div className="flex items-center gap-2">
+ <span className="text-[10px] font-mono text-yellow-400">
+ All steps done. Update goal to add new work.
+ </span>
+ <button
+ type="button"
+ onClick={() => setEditingGoal(true)}
+ className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1"
+ >
+ Update Goal
+ </button>
+ </div>
+ )}
+ <button
+ type="button"
+ onClick={onDelete}
+ className="text-[10px] font-mono text-red-400 hover:text-red-300 border border-red-800 rounded px-2 py-1 ml-auto"
+ >
+ Delete
+ </button>
+ </div>
+ </div>
+
+ {/* Goal */}
+ <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
+ <div className="flex items-center justify-between mb-1">
+ <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
+ Goal
+ </span>
+ {!editingGoal && (
+ <button
+ type="button"
+ onClick={() => { setGoalText(directive.goal); setEditingGoal(true); }}
+ className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]"
+ >
+ [edit]
+ </button>
+ )}
+ </div>
+ {editingGoal ? (
+ <div className="flex flex-col gap-1.5">
+ <textarea
+ value={goalText}
+ onChange={(e) => setGoalText(e.target.value)}
+ className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[11px] font-mono text-white resize-y min-h-[60px]"
+ rows={3}
+ />
+ <div className="flex gap-1.5">
+ <button
+ type="button"
+ onClick={handleGoalSave}
+ className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-0.5"
+ >
+ Save
+ </button>
+ <button
+ type="button"
+ onClick={() => setEditingGoal(false)}
+ className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-0.5"
+ >
+ Cancel
+ </button>
+ </div>
+ </div>
+ ) : (
+ <p className="text-[11px] font-mono text-[#c0d0e0] whitespace-pre-wrap">
+ {directive.goal}
+ </p>
+ )}
+ </div>
+
+ {/* DAG */}
+ <div className="px-4 py-3 flex-1">
+ <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-2">
+ Steps ({totalSteps})
+ </span>
+ <DirectiveDAG
+ steps={directive.steps}
+ onComplete={onCompleteStep}
+ onFail={onFailStep}
+ onSkip={onSkipStep}
+ />
+ </div>
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/directives/DirectiveList.tsx b/makima/frontend/src/components/directives/DirectiveList.tsx
new file mode 100644
index 0000000..6393ea7
--- /dev/null
+++ b/makima/frontend/src/components/directives/DirectiveList.tsx
@@ -0,0 +1,87 @@
+import type { DirectiveSummary, DirectiveStatus } from "../../lib/api";
+
+const STATUS_BADGE: Record<DirectiveStatus, { color: string; label: string }> = {
+ draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" },
+ active: { color: "text-green-400 border-green-800", label: "ACTIVE" },
+ idle: { color: "text-yellow-400 border-yellow-800", label: "IDLE" },
+ paused: { color: "text-orange-400 border-orange-800", label: "PAUSED" },
+ archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
+};
+
+interface DirectiveListProps {
+ directives: DirectiveSummary[];
+ selectedId: string | null;
+ onSelect: (id: string) => void;
+ onCreate: () => void;
+}
+
+export function DirectiveList({ directives, selectedId, onSelect, onCreate }: DirectiveListProps) {
+ return (
+ <div className="flex flex-col h-full">
+ <div className="flex items-center justify-between px-3 py-2 border-b border-dashed border-[rgba(117,170,252,0.2)]">
+ <span className="text-[11px] font-mono text-[#9bc3ff] uppercase tracking-wide">
+ Directives
+ </span>
+ <button
+ type="button"
+ onClick={onCreate}
+ className="text-[11px] font-mono text-[#75aafc] hover:text-white bg-transparent border border-[rgba(117,170,252,0.3)] rounded px-2 py-0.5 hover:border-[rgba(117,170,252,0.6)] transition-colors"
+ >
+ + New
+ </button>
+ </div>
+ <div className="flex-1 overflow-y-auto">
+ {directives.length === 0 ? (
+ <div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]">
+ No directives yet
+ </div>
+ ) : (
+ directives.map((d) => {
+ const badge = STATUS_BADGE[d.status] || STATUS_BADGE.draft;
+ const progress = d.totalSteps > 0
+ ? Math.round((d.completedSteps / d.totalSteps) * 100)
+ : 0;
+
+ return (
+ <button
+ key={d.id}
+ type="button"
+ onClick={() => onSelect(d.id)}
+ className={`w-full text-left px-3 py-2.5 border-b border-[rgba(117,170,252,0.1)] hover:bg-[rgba(117,170,252,0.05)] transition-colors ${
+ selectedId === d.id ? "bg-[rgba(117,170,252,0.1)]" : ""
+ }`}
+ >
+ <div className="flex items-center justify-between mb-1">
+ <span className="text-[12px] font-mono text-white truncate pr-2">
+ {d.title}
+ </span>
+ <span
+ className={`text-[9px] font-mono ${badge.color} border rounded px-1.5 py-0.5 shrink-0`}
+ >
+ {badge.label}
+ </span>
+ </div>
+ <p className="text-[10px] text-[#7788aa] font-mono truncate mb-1.5">
+ {d.goal}
+ </p>
+ {d.totalSteps > 0 && (
+ <div className="flex items-center gap-2">
+ <div className="flex-1 h-1 bg-[#1a2540] rounded overflow-hidden">
+ <div
+ className="h-full bg-emerald-600 rounded transition-all"
+ style={{ width: `${progress}%` }}
+ />
+ </div>
+ <span className="text-[9px] font-mono text-[#556677] shrink-0">
+ {d.completedSteps}/{d.totalSteps}
+ </span>
+ </div>
+ )}
+ </button>
+ );
+ })
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/directives/StepNode.tsx b/makima/frontend/src/components/directives/StepNode.tsx
new file mode 100644
index 0000000..fa91956
--- /dev/null
+++ b/makima/frontend/src/components/directives/StepNode.tsx
@@ -0,0 +1,82 @@
+import type { DirectiveStep, StepStatus } from "../../lib/api";
+
+const STATUS_COLORS: Record<StepStatus, { bg: string; border: string; text: string }> = {
+ pending: { bg: "bg-[#1a2540]", border: "border-[#2a3a5a]", text: "text-[#7788aa]" },
+ ready: { bg: "bg-[#2a2a10]", border: "border-[#4a4a20]", text: "text-yellow-400" },
+ running: { bg: "bg-[#0a2a1a]", border: "border-[#1a5a3a]", text: "text-green-400" },
+ completed: { bg: "bg-[#0a2a2a]", border: "border-[#1a5a5a]", text: "text-emerald-400" },
+ failed: { bg: "bg-[#2a1a1a]", border: "border-[#5a2a2a]", text: "text-red-400" },
+ skipped: { bg: "bg-[#1a1a2a]", border: "border-[#2a2a4a]", text: "text-[#7788aa]" },
+};
+
+const STATUS_LABELS: Record<StepStatus, string> = {
+ pending: "PENDING",
+ ready: "READY",
+ running: "RUNNING",
+ completed: "DONE",
+ failed: "FAILED",
+ skipped: "SKIP",
+};
+
+interface StepNodeProps {
+ step: DirectiveStep;
+ onComplete?: () => void;
+ onFail?: () => void;
+ onSkip?: () => void;
+}
+
+export function StepNode({ step, onComplete, onFail, onSkip }: StepNodeProps) {
+ const colors = STATUS_COLORS[step.status] || STATUS_COLORS.pending;
+ const label = STATUS_LABELS[step.status] || step.status.toUpperCase();
+
+ return (
+ <div
+ className={`${colors.bg} ${colors.border} border rounded px-3 py-2 min-w-[160px] max-w-[220px]`}
+ >
+ <div className="flex items-center justify-between gap-2 mb-1">
+ <span className="text-[11px] font-mono text-white truncate font-medium">
+ {step.name}
+ </span>
+ <span className={`text-[9px] font-mono ${colors.text} uppercase shrink-0`}>
+ {label}
+ </span>
+ </div>
+ {step.description && (
+ <p className="text-[10px] text-[#7788aa] font-mono truncate mb-1">
+ {step.description}
+ </p>
+ )}
+ {(step.status === "running" || step.status === "ready") && (
+ <div className="flex gap-1 mt-1">
+ {onComplete && (
+ <button
+ type="button"
+ onClick={onComplete}
+ className="text-[9px] font-mono text-emerald-400 hover:text-emerald-300 bg-transparent border border-emerald-800 rounded px-1.5 py-0.5"
+ >
+ Done
+ </button>
+ )}
+ {onFail && (
+ <button
+ type="button"
+ onClick={onFail}
+ className="text-[9px] font-mono text-red-400 hover:text-red-300 bg-transparent border border-red-800 rounded px-1.5 py-0.5"
+ >
+ Fail
+ </button>
+ )}
+ {onSkip && (
+ <button
+ type="button"
+ onClick={onSkip}
+ className="text-[9px] font-mono text-[#7788aa] hover:text-white bg-transparent border border-[#2a3a5a] rounded px-1.5 py-0.5"
+ >
+ Skip
+ </button>
+ )}
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/makima/frontend/src/hooks/useDirectives.ts b/makima/frontend/src/hooks/useDirectives.ts
new file mode 100644
index 0000000..b69275a
--- /dev/null
+++ b/makima/frontend/src/hooks/useDirectives.ts
@@ -0,0 +1,150 @@
+import { useState, useEffect, useCallback } from "react";
+import {
+ type DirectiveSummary,
+ type DirectiveWithSteps,
+ type CreateDirectiveRequest,
+ type UpdateDirectiveRequest,
+ type CreateDirectiveStepRequest,
+ listDirectives,
+ createDirective,
+ getDirective,
+ updateDirective,
+ deleteDirective,
+ createDirectiveStep,
+ deleteDirectiveStep,
+ startDirective,
+ pauseDirective,
+ advanceDirective,
+ completeDirectiveStep,
+ failDirectiveStep,
+ skipDirectiveStep,
+ updateDirectiveGoal,
+} from "../lib/api";
+
+export function useDirectives() {
+ const [directives, setDirectives] = useState<DirectiveSummary[]>([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState<string | null>(null);
+
+ const refresh = useCallback(async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const res = await listDirectives();
+ setDirectives(res.directives);
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to load directives");
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ refresh();
+ }, [refresh]);
+
+ const create = useCallback(async (req: CreateDirectiveRequest) => {
+ const d = await createDirective(req);
+ await refresh();
+ return d;
+ }, [refresh]);
+
+ const remove = useCallback(async (id: string) => {
+ await deleteDirective(id);
+ await refresh();
+ }, [refresh]);
+
+ return { directives, loading, error, refresh, create, remove };
+}
+
+export function useDirective(id: string | undefined) {
+ const [directive, setDirective] = useState<DirectiveWithSteps | null>(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState<string | null>(null);
+
+ const refresh = useCallback(async () => {
+ if (!id) return;
+ try {
+ setLoading(true);
+ setError(null);
+ const d = await getDirective(id);
+ setDirective(d);
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to load directive");
+ } finally {
+ setLoading(false);
+ }
+ }, [id]);
+
+ useEffect(() => {
+ refresh();
+ }, [refresh]);
+
+ const update = useCallback(async (req: UpdateDirectiveRequest) => {
+ if (!id) return;
+ await updateDirective(id, req);
+ await refresh();
+ }, [id, refresh]);
+
+ const addStep = useCallback(async (req: CreateDirectiveStepRequest) => {
+ if (!id) return;
+ await createDirectiveStep(id, req);
+ await refresh();
+ }, [id, refresh]);
+
+ const removeStep = useCallback(async (stepId: string) => {
+ if (!id) return;
+ await deleteDirectiveStep(id, stepId);
+ await refresh();
+ }, [id, refresh]);
+
+ const start = useCallback(async () => {
+ if (!id) return;
+ await startDirective(id);
+ await refresh();
+ }, [id, refresh]);
+
+ const pause = useCallback(async () => {
+ if (!id) return;
+ await pauseDirective(id);
+ await refresh();
+ }, [id, refresh]);
+
+ const advance = useCallback(async () => {
+ if (!id) return;
+ await advanceDirective(id);
+ await refresh();
+ }, [id, refresh]);
+
+ const completeStep = useCallback(async (stepId: string) => {
+ if (!id) return;
+ await completeDirectiveStep(id, stepId);
+ await refresh();
+ }, [id, refresh]);
+
+ const failStep = useCallback(async (stepId: string) => {
+ if (!id) return;
+ await failDirectiveStep(id, stepId);
+ await refresh();
+ }, [id, refresh]);
+
+ const skipStep = useCallback(async (stepId: string) => {
+ if (!id) return;
+ await skipDirectiveStep(id, stepId);
+ await refresh();
+ }, [id, refresh]);
+
+ const updateGoal = useCallback(async (goal: string) => {
+ if (!id) return;
+ await updateDirectiveGoal(id, goal);
+ await refresh();
+ }, [id, refresh]);
+
+ return {
+ directive, loading, error, refresh,
+ update, addStep, removeStep,
+ start, pause, advance,
+ completeStep, failStep, skipStep,
+ updateGoal,
+ };
+}
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 7732725..b1422df 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -3003,4 +3003,226 @@ export async function listTaskPatches(taskId: string, contractId: string): Promi
return res.json();
}
+// =============================================================================
+// Directive Types & API
+// =============================================================================
+
+export type DirectiveStatus = "draft" | "active" | "idle" | "paused" | "archived";
+export type StepStatus = "pending" | "ready" | "running" | "completed" | "failed" | "skipped";
+
+export interface Directive {
+ id: string;
+ ownerId: string;
+ title: string;
+ goal: string;
+ status: DirectiveStatus;
+ repositoryUrl: string | null;
+ localPath: string | null;
+ baseBranch: string | null;
+ orchestratorTaskId: string | null;
+ goalUpdatedAt: string;
+ startedAt: string | null;
+ version: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface DirectiveStep {
+ id: string;
+ directiveId: string;
+ name: string;
+ description: string | null;
+ taskPlan: string | null;
+ dependsOn: string[];
+ status: StepStatus;
+ taskId: string | null;
+ orderIndex: number;
+ generation: number;
+ startedAt: string | null;
+ completedAt: string | null;
+ createdAt: string;
+}
+
+export interface DirectiveWithSteps extends Directive {
+ steps: DirectiveStep[];
+}
+
+export interface DirectiveSummary {
+ id: string;
+ ownerId: string;
+ title: string;
+ goal: string;
+ status: DirectiveStatus;
+ repositoryUrl: string | null;
+ orchestratorTaskId: string | null;
+ version: number;
+ createdAt: string;
+ updatedAt: string;
+ totalSteps: number;
+ completedSteps: number;
+ runningSteps: number;
+ failedSteps: number;
+}
+
+export interface DirectiveListResponse {
+ directives: DirectiveSummary[];
+ total: number;
+}
+
+export interface CreateDirectiveRequest {
+ title: string;
+ goal: string;
+ repositoryUrl?: string;
+ localPath?: string;
+ baseBranch?: string;
+}
+
+export interface UpdateDirectiveRequest {
+ title?: string;
+ goal?: string;
+ status?: string;
+ repositoryUrl?: string;
+ localPath?: string;
+ baseBranch?: string;
+ orchestratorTaskId?: string;
+ version?: number;
+}
+
+export interface CreateDirectiveStepRequest {
+ name: string;
+ description?: string;
+ taskPlan?: string;
+ dependsOn?: string[];
+ orderIndex?: number;
+ generation?: number;
+}
+
+export interface UpdateDirectiveStepRequest {
+ name?: string;
+ description?: string;
+ taskPlan?: string;
+ dependsOn?: string[];
+ status?: string;
+ taskId?: string;
+ orderIndex?: number;
+}
+
+export async function listDirectives(): Promise<DirectiveListResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives`);
+ if (!res.ok) throw new Error(`Failed to list directives: ${res.statusText}`);
+ return res.json();
+}
+
+export async function createDirective(req: CreateDirectiveRequest): Promise<Directive> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(req),
+ });
+ if (!res.ok) throw new Error(`Failed to create directive: ${res.statusText}`);
+ return res.json();
+}
+
+export async function getDirective(id: string): Promise<DirectiveWithSteps> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`);
+ if (!res.ok) throw new Error(`Failed to get directive: ${res.statusText}`);
+ return res.json();
+}
+
+export async function updateDirective(id: string, req: UpdateDirectiveRequest): Promise<Directive> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(req),
+ });
+ if (!res.ok) throw new Error(`Failed to update directive: ${res.statusText}`);
+ return res.json();
+}
+
+export async function deleteDirective(id: string): Promise<void> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`, { method: "DELETE" });
+ if (!res.ok) throw new Error(`Failed to delete directive: ${res.statusText}`);
+}
+
+export async function createDirectiveStep(directiveId: string, req: CreateDirectiveStepRequest): Promise<DirectiveStep> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(req),
+ });
+ if (!res.ok) throw new Error(`Failed to create step: ${res.statusText}`);
+ return res.json();
+}
+
+export async function batchCreateDirectiveSteps(directiveId: string, steps: CreateDirectiveStepRequest[]): Promise<DirectiveStep[]> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/batch`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(steps),
+ });
+ if (!res.ok) throw new Error(`Failed to batch create steps: ${res.statusText}`);
+ return res.json();
+}
+
+export async function updateDirectiveStep(directiveId: string, stepId: string, req: UpdateDirectiveStepRequest): Promise<DirectiveStep> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(req),
+ });
+ if (!res.ok) throw new Error(`Failed to update step: ${res.statusText}`);
+ return res.json();
+}
+
+export async function deleteDirectiveStep(directiveId: string, stepId: string): Promise<void> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}`, { method: "DELETE" });
+ if (!res.ok) throw new Error(`Failed to delete step: ${res.statusText}`);
+}
+
+export async function startDirective(id: string): Promise<DirectiveWithSteps> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/start`, { method: "POST" });
+ if (!res.ok) throw new Error(`Failed to start directive: ${res.statusText}`);
+ return res.json();
+}
+
+export async function pauseDirective(id: string): Promise<Directive> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/pause`, { method: "POST" });
+ if (!res.ok) throw new Error(`Failed to pause directive: ${res.statusText}`);
+ return res.json();
+}
+
+export async function advanceDirective(id: string): Promise<DirectiveWithSteps> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/advance`, { method: "POST" });
+ if (!res.ok) throw new Error(`Failed to advance directive: ${res.statusText}`);
+ return res.json();
+}
+
+export async function completeDirectiveStep(directiveId: string, stepId: string): Promise<DirectiveStep> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}/complete`, { method: "POST" });
+ if (!res.ok) throw new Error(`Failed to complete step: ${res.statusText}`);
+ return res.json();
+}
+
+export async function failDirectiveStep(directiveId: string, stepId: string): Promise<DirectiveStep> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}/fail`, { method: "POST" });
+ if (!res.ok) throw new Error(`Failed to fail step: ${res.statusText}`);
+ return res.json();
+}
+
+export async function skipDirectiveStep(directiveId: string, stepId: string): Promise<DirectiveStep> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}/skip`, { method: "POST" });
+ if (!res.ok) throw new Error(`Failed to skip step: ${res.statusText}`);
+ return res.json();
+}
+
+export async function updateDirectiveGoal(id: string, goal: string): Promise<Directive> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/goal`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ goal }),
+ });
+ if (!res.ok) throw new Error(`Failed to update goal: ${res.statusText}`);
+ return res.json();
+}
+
diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx
index 50fffe4..3dc68f5 100644
--- a/makima/frontend/src/main.tsx
+++ b/makima/frontend/src/main.tsx
@@ -19,6 +19,7 @@ import LoginPage from "./routes/login";
import SettingsPage from "./routes/settings";
import ContractFilePage from "./routes/contract-file";
import SpeakPage from "./routes/speak";
+import DirectivesPage from "./routes/directives";
createRoot(document.getElementById("root")!).render(
<StrictMode>
@@ -128,6 +129,22 @@ createRoot(document.getElementById("root")!).render(
}
/>
<Route
+ path="/directives"
+ element={
+ <ProtectedRoute>
+ <DirectivesPage />
+ </ProtectedRoute>
+ }
+ />
+ <Route
+ path="/directives/:id"
+ element={
+ <ProtectedRoute>
+ <DirectivesPage />
+ </ProtectedRoute>
+ }
+ />
+ <Route
path="/speak"
element={
<ProtectedRoute>
diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx
new file mode 100644
index 0000000..82e5d48
--- /dev/null
+++ b/makima/frontend/src/routes/directives.tsx
@@ -0,0 +1,168 @@
+import { useState, useEffect } from "react";
+import { useParams, useNavigate } from "react-router";
+import { Masthead } from "../components/Masthead";
+import { DirectiveList } from "../components/directives/DirectiveList";
+import { DirectiveDetail } from "../components/directives/DirectiveDetail";
+import { useDirectives, useDirective } from "../hooks/useDirectives";
+import { useAuth } from "../contexts/AuthContext";
+
+export default function DirectivesPage() {
+ const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
+ const navigate = useNavigate();
+ const { id: selectedId } = useParams<{ id: string }>();
+ const { directives, loading: listLoading, create, remove } = useDirectives();
+ const { directive, refresh: refreshDetail, start, pause, advance, completeStep, failStep, skipStep, updateGoal } = useDirective(selectedId);
+
+ const [showCreate, setShowCreate] = useState(false);
+ const [newTitle, setNewTitle] = useState("");
+ const [newGoal, setNewGoal] = useState("");
+ const [newRepoUrl, setNewRepoUrl] = useState("");
+
+ useEffect(() => {
+ if (!authLoading && isAuthConfigured && !isAuthenticated) {
+ navigate("/login");
+ }
+ }, [authLoading, isAuthConfigured, isAuthenticated, navigate]);
+
+ if (authLoading) {
+ return (
+ <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
+ <Masthead showNav />
+ <main className="flex-1 flex items-center justify-center">
+ <p className="text-[#7788aa] font-mono text-sm">Loading...</p>
+ </main>
+ </div>
+ );
+ }
+
+ const handleCreate = async () => {
+ if (!newTitle.trim() || !newGoal.trim()) return;
+ try {
+ const d = await create({
+ title: newTitle.trim(),
+ goal: newGoal.trim(),
+ repositoryUrl: newRepoUrl.trim() || undefined,
+ });
+ setShowCreate(false);
+ setNewTitle("");
+ setNewGoal("");
+ setNewRepoUrl("");
+ navigate(`/directives/${d.id}`);
+ } catch (e) {
+ console.error("Failed to create directive:", e);
+ }
+ };
+
+ const handleDelete = async () => {
+ if (!selectedId) return;
+ if (!window.confirm("Delete this directive?")) return;
+ try {
+ await remove(selectedId);
+ navigate("/directives");
+ } catch (e) {
+ console.error("Failed to delete:", e);
+ }
+ };
+
+ return (
+ <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
+ <Masthead showNav />
+ <main className="flex-1 flex overflow-hidden" style={{ height: "calc(100vh - 80px)" }}>
+ {/* Left: List */}
+ <div className="w-[280px] shrink-0 border-r border-dashed border-[rgba(117,170,252,0.2)] overflow-hidden flex flex-col">
+ <DirectiveList
+ directives={directives}
+ selectedId={selectedId ?? null}
+ onSelect={(id) => navigate(`/directives/${id}`)}
+ onCreate={() => setShowCreate(true)}
+ />
+ </div>
+
+ {/* Right: Detail or Create */}
+ <div className="flex-1 overflow-hidden">
+ {showCreate ? (
+ <div className="p-4 max-w-lg">
+ <h2 className="text-[14px] font-mono text-white font-medium mb-4">
+ New Directive
+ </h2>
+ <div className="flex flex-col gap-3">
+ <div>
+ <label className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-1">
+ Title
+ </label>
+ <input
+ value={newTitle}
+ onChange={(e) => setNewTitle(e.target.value)}
+ placeholder="Project title..."
+ className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[12px] font-mono text-white"
+ />
+ </div>
+ <div>
+ <label className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-1">
+ Goal
+ </label>
+ <textarea
+ value={newGoal}
+ onChange={(e) => setNewGoal(e.target.value)}
+ placeholder="What should this directive accomplish?"
+ rows={4}
+ className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[12px] font-mono text-white resize-y"
+ />
+ </div>
+ <div>
+ <label className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-1">
+ Repository URL (optional)
+ </label>
+ <input
+ value={newRepoUrl}
+ onChange={(e) => setNewRepoUrl(e.target.value)}
+ placeholder="https://github.com/..."
+ className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[12px] font-mono text-white"
+ />
+ </div>
+ <div className="flex gap-2">
+ <button
+ type="button"
+ onClick={handleCreate}
+ disabled={!newTitle.trim() || !newGoal.trim()}
+ className="text-[11px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-3 py-1 disabled:opacity-50"
+ >
+ Create
+ </button>
+ <button
+ type="button"
+ onClick={() => setShowCreate(false)}
+ className="text-[11px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-3 py-1"
+ >
+ Cancel
+ </button>
+ </div>
+ </div>
+ </div>
+ ) : selectedId && directive ? (
+ <DirectiveDetail
+ directive={directive}
+ onStart={start}
+ onPause={pause}
+ onAdvance={advance}
+ onCompleteStep={completeStep}
+ onFailStep={failStep}
+ onSkipStep={skipStep}
+ onUpdateGoal={updateGoal}
+ onDelete={handleDelete}
+ onRefresh={refreshDetail}
+ />
+ ) : (
+ <div className="flex-1 flex items-center justify-center h-full">
+ <p className="text-[#556677] font-mono text-[12px]">
+ {listLoading
+ ? "Loading..."
+ : "Select a directive or create a new one"}
+ </p>
+ </div>
+ )}
+ </div>
+ </main>
+ </div>
+ );
+}
diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo
index 0fbecaa..f36c337 100644
--- a/makima/frontend/tsconfig.tsbuildinfo
+++ b/makima/frontend/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"} \ No newline at end of file
+{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/directives/directivedag.tsx","./src/components/directives/directivedetail.tsx","./src/components/directives/directivelist.tsx","./src/components/directives/stepnode.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usedirectives.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/directives.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"} \ No newline at end of file
diff --git a/makima/migrations/20260210000000_create_directive_system.sql b/makima/migrations/20260210000000_create_directive_system.sql
new file mode 100644
index 0000000..a2cd784
--- /dev/null
+++ b/makima/migrations/20260210000000_create_directive_system.sql
@@ -0,0 +1,50 @@
+-- Directive system v3: long-lived DAG-based project management
+-- Directives are ongoing top-level entities (alternative to contracts) for managing
+-- large, whole-repository projects via a DAG of auto-progressing tasks.
+
+CREATE TABLE directives (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ owner_id UUID NOT NULL REFERENCES owners(id) ON DELETE CASCADE,
+ title VARCHAR(500) NOT NULL,
+ goal TEXT NOT NULL,
+ -- Status: draft -> active <-> idle -> archived
+ -- draft = being planned, DAG not yet started
+ -- active = executing steps, orchestrator running
+ -- idle = all current steps done, waiting for new work
+ -- paused = user-paused execution
+ -- archived = project finished, no longer active
+ status VARCHAR(32) NOT NULL DEFAULT 'draft'
+ CHECK (status IN ('draft', 'active', 'idle', 'paused', 'archived')),
+ repository_url VARCHAR(512),
+ local_path VARCHAR(512),
+ base_branch VARCHAR(255),
+ orchestrator_task_id UUID REFERENCES tasks(id) ON DELETE SET NULL,
+ -- Tracks when the goal/requirements were last changed (orchestrator watches this)
+ goal_updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ started_at TIMESTAMPTZ,
+ version INTEGER NOT NULL DEFAULT 1,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE directive_steps (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ directive_id UUID NOT NULL REFERENCES directives(id) ON DELETE CASCADE,
+ name VARCHAR(255) NOT NULL,
+ description TEXT,
+ task_plan TEXT,
+ depends_on UUID[] NOT NULL DEFAULT '{}',
+ status VARCHAR(32) NOT NULL DEFAULT 'pending'
+ CHECK (status IN ('pending', 'ready', 'running', 'completed', 'failed', 'skipped')),
+ task_id UUID REFERENCES tasks(id) ON DELETE SET NULL,
+ order_index INTEGER NOT NULL DEFAULT 0,
+ -- Which "generation" of planning created this step (for tracking re-plans)
+ generation INTEGER NOT NULL DEFAULT 1,
+ started_at TIMESTAMPTZ,
+ completed_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- Link tasks back to directives
+ALTER TABLE tasks ADD COLUMN directive_id UUID REFERENCES directives(id) ON DELETE SET NULL;
+ALTER TABLE tasks ADD COLUMN directive_step_id UUID REFERENCES directive_steps(id) ON DELETE SET NULL;
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs
index ee5895c..639c88b 100644
--- a/makima/src/bin/makima.rs
+++ b/makima/src/bin/makima.rs
@@ -7,7 +7,7 @@ use std::sync::Arc;
use makima::daemon::api::{ApiClient, CreateContractRequest};
use makima::daemon::cli::{
Cli, CliConfig, Commands, ConfigCommand, ContractCommand,
- SupervisorCommand, ViewArgs,
+ DirectiveCommand, SupervisorCommand, ViewArgs,
};
use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, WsEvent, OutputLine, OutputMessageType, WsConnectionState, RepositorySuggestion};
use makima::daemon::config::{DaemonConfig, RepoEntry};
@@ -29,6 +29,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Commands::Daemon(args) => run_daemon(args).await,
Commands::Supervisor(cmd) => run_supervisor(cmd).await,
Commands::Contract(cmd) => run_contract(cmd).await,
+ Commands::Directive(cmd) => run_directive(cmd).await,
Commands::View(args) => run_view(args).await,
Commands::Config(cmd) => run_config(cmd).await,
}
@@ -711,6 +712,108 @@ async fn run_contract(
Ok(())
}
+/// Run directive commands.
+async fn run_directive(
+ cmd: DirectiveCommand,
+) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
+ use makima::daemon::api::directive::*;
+
+ match cmd {
+ DirectiveCommand::List(args) => {
+ let client = ApiClient::new(args.api_url, args.api_key)?;
+ let result = client.list_directives().await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::Get(args) | DirectiveCommand::Status(args) => {
+ let client = ApiClient::new(args.api_url, args.api_key)?;
+ let result = client.get_directive(args.directive_id).await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::AddStep(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let depends_on: Vec<uuid::Uuid> = args
+ .depends_on
+ .map(|d| {
+ d.split(',')
+ .filter_map(|s| uuid::Uuid::parse_str(s.trim()).ok())
+ .collect()
+ })
+ .unwrap_or_default();
+ let req = CreateStepRequest {
+ name: args.name,
+ description: args.description,
+ task_plan: args.task_plan,
+ depends_on,
+ order_index: args.order_index,
+ };
+ let result = client.directive_add_step(args.common.directive_id, req).await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::RemoveStep(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ client.directive_remove_step(args.common.directive_id, args.step_id).await?;
+ println!(r#"{{"success": true}}"#);
+ }
+ DirectiveCommand::SetDeps(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let depends_on: Vec<uuid::Uuid> = args
+ .depends_on
+ .split(',')
+ .filter_map(|s| uuid::Uuid::parse_str(s.trim()).ok())
+ .collect();
+ let result = client
+ .directive_set_deps(args.common.directive_id, args.step_id, depends_on)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::Start(args) => {
+ let client = ApiClient::new(args.api_url, args.api_key)?;
+ let result = client.directive_start(args.directive_id).await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::Pause(args) => {
+ let client = ApiClient::new(args.api_url, args.api_key)?;
+ let result = client.directive_pause(args.directive_id).await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::Advance(args) => {
+ let client = ApiClient::new(args.api_url, args.api_key)?;
+ let result = client.directive_advance(args.directive_id).await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::CompleteStep(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let result = client
+ .directive_complete_step(args.common.directive_id, args.step_id)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::FailStep(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let result = client
+ .directive_fail_step(args.common.directive_id, args.step_id)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::SkipStep(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let result = client
+ .directive_skip_step(args.common.directive_id, args.step_id)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::UpdateGoal(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let result = client
+ .directive_update_goal(args.common.directive_id, &args.goal)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ }
+
+ Ok(())
+}
+
/// Run the TUI view command.
async fn run_view(args: ViewArgs) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Load CLI config for defaults
diff --git a/makima/src/daemon/api/directive.rs b/makima/src/daemon/api/directive.rs
new file mode 100644
index 0000000..fbd27fe
--- /dev/null
+++ b/makima/src/daemon/api/directive.rs
@@ -0,0 +1,124 @@
+//! Directive API methods.
+
+use serde::Serialize;
+use uuid::Uuid;
+
+use super::client::{ApiClient, ApiError};
+use super::supervisor::JsonValue;
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CreateStepRequest {
+ pub name: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub description: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub task_plan: Option<String>,
+ pub depends_on: Vec<Uuid>,
+ pub order_index: i32,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateGoalRequest {
+ pub goal: String,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateStepDepsRequest {
+ pub depends_on: Vec<Uuid>,
+}
+
+impl ApiClient {
+ /// List all directives.
+ pub async fn list_directives(&self) -> Result<JsonValue, ApiError> {
+ self.get("/api/v1/directives").await
+ }
+
+ /// Get a directive with its steps.
+ pub async fn get_directive(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> {
+ self.get(&format!("/api/v1/directives/{}", directive_id)).await
+ }
+
+ /// Add a step to a directive.
+ pub async fn directive_add_step(
+ &self,
+ directive_id: Uuid,
+ req: CreateStepRequest,
+ ) -> Result<JsonValue, ApiError> {
+ self.post(&format!("/api/v1/directives/{}/steps", directive_id), &req).await
+ }
+
+ /// Remove a step from a directive.
+ pub async fn directive_remove_step(
+ &self,
+ directive_id: Uuid,
+ step_id: Uuid,
+ ) -> Result<(), ApiError> {
+ self.delete(&format!("/api/v1/directives/{}/steps/{}", directive_id, step_id)).await
+ }
+
+ /// Set dependencies for a step.
+ pub async fn directive_set_deps(
+ &self,
+ directive_id: Uuid,
+ step_id: Uuid,
+ depends_on: Vec<Uuid>,
+ ) -> Result<JsonValue, ApiError> {
+ let req = UpdateStepDepsRequest { depends_on };
+ self.put(&format!("/api/v1/directives/{}/steps/{}", directive_id, step_id), &req).await
+ }
+
+ /// Start a directive.
+ pub async fn directive_start(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> {
+ self.post_empty(&format!("/api/v1/directives/{}/start", directive_id)).await
+ }
+
+ /// Pause a directive.
+ pub async fn directive_pause(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> {
+ self.post_empty(&format!("/api/v1/directives/{}/pause", directive_id)).await
+ }
+
+ /// Advance the directive DAG.
+ pub async fn directive_advance(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> {
+ self.post_empty(&format!("/api/v1/directives/{}/advance", directive_id)).await
+ }
+
+ /// Mark a step as completed.
+ pub async fn directive_complete_step(
+ &self,
+ directive_id: Uuid,
+ step_id: Uuid,
+ ) -> Result<JsonValue, ApiError> {
+ self.post_empty(&format!("/api/v1/directives/{}/steps/{}/complete", directive_id, step_id)).await
+ }
+
+ /// Mark a step as failed.
+ pub async fn directive_fail_step(
+ &self,
+ directive_id: Uuid,
+ step_id: Uuid,
+ ) -> Result<JsonValue, ApiError> {
+ self.post_empty(&format!("/api/v1/directives/{}/steps/{}/fail", directive_id, step_id)).await
+ }
+
+ /// Mark a step as skipped.
+ pub async fn directive_skip_step(
+ &self,
+ directive_id: Uuid,
+ step_id: Uuid,
+ ) -> Result<JsonValue, ApiError> {
+ self.post_empty(&format!("/api/v1/directives/{}/steps/{}/skip", directive_id, step_id)).await
+ }
+
+ /// Update the directive's goal.
+ pub async fn directive_update_goal(
+ &self,
+ directive_id: Uuid,
+ goal: &str,
+ ) -> Result<JsonValue, ApiError> {
+ let req = UpdateGoalRequest { goal: goal.to_string() };
+ self.put(&format!("/api/v1/directives/{}/goal", directive_id), &req).await
+ }
+}
diff --git a/makima/src/daemon/api/mod.rs b/makima/src/daemon/api/mod.rs
index 49d80e0..2d1efbf 100644
--- a/makima/src/daemon/api/mod.rs
+++ b/makima/src/daemon/api/mod.rs
@@ -2,6 +2,7 @@
pub mod client;
pub mod contract;
+pub mod directive;
pub mod supervisor;
pub use client::ApiClient;
diff --git a/makima/src/daemon/cli/directive.rs b/makima/src/daemon/cli/directive.rs
new file mode 100644
index 0000000..5de60ed
--- /dev/null
+++ b/makima/src/daemon/cli/directive.rs
@@ -0,0 +1,101 @@
+//! Directive subcommand - directive management commands for orchestrator tasks.
+
+use clap::Args;
+use uuid::Uuid;
+
+/// Common arguments for directive commands.
+#[derive(Args, Debug, Clone)]
+pub struct DirectiveArgs {
+ /// API URL
+ #[arg(long, env = "MAKIMA_API_URL", default_value = "https://api.makima.jp", global = true)]
+ pub api_url: String,
+
+ /// API key for authentication
+ #[arg(long, env = "MAKIMA_API_KEY", global = true)]
+ pub api_key: String,
+
+ /// Directive ID
+ #[arg(long, env = "MAKIMA_DIRECTIVE_ID", global = true)]
+ pub directive_id: Uuid,
+}
+
+/// Arguments for listing directives (no directive_id required).
+#[derive(Args, Debug, Clone)]
+pub struct DirectiveListArgs {
+ /// API URL
+ #[arg(long, env = "MAKIMA_API_URL", default_value = "https://api.makima.jp", global = true)]
+ pub api_url: String,
+
+ /// API key for authentication
+ #[arg(long, env = "MAKIMA_API_KEY", global = true)]
+ pub api_key: String,
+}
+
+/// Arguments for add-step command.
+#[derive(Args, Debug)]
+pub struct AddStepArgs {
+ #[command(flatten)]
+ pub common: DirectiveArgs,
+
+ /// Step name
+ pub name: String,
+
+ /// Step description
+ #[arg(long)]
+ pub description: Option<String>,
+
+ /// Task plan for the step
+ #[arg(long)]
+ pub task_plan: Option<String>,
+
+ /// Comma-separated UUIDs of dependency steps
+ #[arg(long)]
+ pub depends_on: Option<String>,
+
+ /// Order index
+ #[arg(long, default_value = "0")]
+ pub order_index: i32,
+}
+
+/// Arguments for remove-step command.
+#[derive(Args, Debug)]
+pub struct RemoveStepArgs {
+ #[command(flatten)]
+ pub common: DirectiveArgs,
+
+ /// Step ID to remove
+ pub step_id: Uuid,
+}
+
+/// Arguments for set-deps command.
+#[derive(Args, Debug)]
+pub struct SetDepsArgs {
+ #[command(flatten)]
+ pub common: DirectiveArgs,
+
+ /// Step ID to update
+ pub step_id: Uuid,
+
+ /// Comma-separated UUIDs of dependency steps
+ pub depends_on: String,
+}
+
+/// Arguments for complete-step/fail-step/skip-step commands.
+#[derive(Args, Debug)]
+pub struct StepActionArgs {
+ #[command(flatten)]
+ pub common: DirectiveArgs,
+
+ /// Step ID
+ pub step_id: Uuid,
+}
+
+/// Arguments for update-goal command.
+#[derive(Args, Debug)]
+pub struct UpdateGoalArgs {
+ #[command(flatten)]
+ pub common: DirectiveArgs,
+
+ /// New goal text
+ pub goal: String,
+}
diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs
index 0805edd..faafaea 100644
--- a/makima/src/daemon/cli/mod.rs
+++ b/makima/src/daemon/cli/mod.rs
@@ -3,6 +3,7 @@
pub mod config;
pub mod contract;
pub mod daemon;
+pub mod directive;
pub mod server;
pub mod supervisor;
pub mod view;
@@ -12,6 +13,7 @@ use clap::{Parser, Subcommand};
pub use config::CliConfig;
pub use contract::ContractArgs;
pub use daemon::DaemonArgs;
+pub use directive::DirectiveArgs;
pub use server::ServerArgs;
pub use supervisor::SupervisorArgs;
pub use view::ViewArgs;
@@ -41,6 +43,10 @@ pub enum Commands {
#[command(subcommand)]
Contract(ContractCommand),
+ /// Directive commands for DAG-based project management
+ #[command(subcommand)]
+ Directive(DirectiveCommand),
+
/// Interactive TUI browser for contracts and tasks
///
/// Provides a drill-down interface for browsing contracts, viewing their
@@ -196,6 +202,49 @@ pub enum ContractCommand {
CreateFile(contract::CreateFileArgs),
}
+/// Directive subcommands for DAG-based project management.
+#[derive(Subcommand, Debug)]
+pub enum DirectiveCommand {
+ /// List all directives
+ List(directive::DirectiveListArgs),
+
+ /// Get directive status with steps
+ Get(DirectiveArgs),
+
+ /// Get directive status (alias for get)
+ Status(DirectiveArgs),
+
+ /// Add a step to the directive
+ AddStep(directive::AddStepArgs),
+
+ /// Remove a step from the directive
+ RemoveStep(directive::RemoveStepArgs),
+
+ /// Set dependencies for a step
+ SetDeps(directive::SetDepsArgs),
+
+ /// Start the directive (begin executing steps)
+ Start(DirectiveArgs),
+
+ /// Pause the directive
+ Pause(DirectiveArgs),
+
+ /// Advance the DAG (find newly-ready steps)
+ Advance(DirectiveArgs),
+
+ /// Mark a step as completed
+ CompleteStep(directive::StepActionArgs),
+
+ /// Mark a step as failed
+ FailStep(directive::StepActionArgs),
+
+ /// Mark a step as skipped
+ SkipStep(directive::StepActionArgs),
+
+ /// Update the directive's goal (triggers re-planning)
+ UpdateGoal(directive::UpdateGoalArgs),
+}
+
impl Cli {
/// Parse command-line arguments
pub fn parse_args() -> Self {
diff --git a/makima/src/daemon/skills/directive.md b/makima/src/daemon/skills/directive.md
new file mode 100644
index 0000000..7c55cf8
--- /dev/null
+++ b/makima/src/daemon/skills/directive.md
@@ -0,0 +1,111 @@
+---
+name: makima-directive
+description: Directive commands for makima DAG-based project orchestration. Use these commands to manage long-lived directives with auto-progressing steps.
+---
+
+# Makima Directive Skill
+
+You are orchestrating a **directive** — a long-lived project managed through a DAG (directed acyclic graph) of steps. Unlike contracts which are finite and phase-based, directives are **ongoing and continuous**: they stay active as the project evolves, and new features/requirements can be added at any time.
+
+## Key Concepts
+
+- **Directive**: A long-lived top-level entity with a goal, repository info, and a mutable DAG of steps
+- **Steps**: Nodes in the DAG. Each step can spawn a task using the mesh infrastructure
+- **Auto-progression**: When a step completes, newly-ready steps (whose dependencies are met) automatically become ready
+- **Continuous evolution**: The goal can be updated at any time. When all steps complete, the directive goes `idle` (not completed) — waiting for new work
+- **Statuses**: `draft` → `active` ↔ `idle` → `archived`. Directives are never "completed" — they go idle and wait
+
+## Commands
+
+### Check Status
+```bash
+makima directive status
+```
+Returns the directive with all steps, their statuses, and dependency information.
+
+### Add a Step
+```bash
+makima directive add-step "Step Name" --description "What this step does" --task-plan "Detailed instructions for the task" --depends-on "uuid1,uuid2" --order-index 1
+```
+
+### Remove a Step
+```bash
+makima directive remove-step <step_id>
+```
+
+### Set Dependencies
+```bash
+makima directive set-deps <step_id> "dep_uuid1,dep_uuid2"
+```
+
+### Start the Directive
+```bash
+makima directive start
+```
+Sets status to `active` and advances any steps with no dependencies to `ready`.
+
+### Advance the DAG
+```bash
+makima directive advance
+```
+Finds newly-ready steps (all dependencies met) and marks them ready. If all steps are in terminal states, sets the directive to `idle`.
+
+### Complete a Step
+```bash
+makima directive complete-step <step_id>
+```
+
+### Fail a Step
+```bash
+makima directive fail-step <step_id>
+```
+
+### Skip a Step
+```bash
+makima directive skip-step <step_id>
+```
+
+### Update the Goal
+```bash
+makima directive update-goal "New or expanded goal text"
+```
+Updates the goal and bumps `goalUpdatedAt`. If the directive is `idle`, it reactivates to `active`.
+
+### Pause
+```bash
+makima directive pause
+```
+
+## Orchestration Workflow
+
+### Initial Setup
+1. Check the directive status to understand the goal
+2. Decompose the goal into steps with clear dependencies
+3. Add steps using `add-step` with appropriate `--depends-on` flags
+4. Start the directive with `start`
+5. Steps with no dependencies will become `ready` immediately
+
+### Monitoring and Advancing
+1. Periodically check status to see step progress
+2. When tasks complete, the DAG auto-advances — newly-ready steps appear
+3. Use `advance` to manually trigger DAG progression if needed
+4. Mark steps as complete/failed/skipped as appropriate
+
+### Re-planning (When Goal Updates)
+When the goal is updated (you'll see a new `goalUpdatedAt` timestamp):
+1. Check the current status to see completed and in-progress steps
+2. Identify what's new in the updated goal
+3. Add new steps that depend on existing completed steps as appropriate
+4. The DAG will auto-advance any newly-ready steps
+
+### Idle State
+When all steps complete, the directive enters `idle` state. This is normal — it means:
+- All current work is done
+- The directive is waiting for new requirements
+- When the user updates the goal, it reactivates automatically
+- You should add new steps based on the updated goal
+
+## Environment Variables
+- `MAKIMA_API_URL` - API server URL
+- `MAKIMA_API_KEY` - Authentication key
+- `MAKIMA_DIRECTIVE_ID` - Current directive ID (set automatically)
diff --git a/makima/src/daemon/skills/mod.rs b/makima/src/daemon/skills/mod.rs
index 0b05f3a..0c015ba 100644
--- a/makima/src/daemon/skills/mod.rs
+++ b/makima/src/daemon/skills/mod.rs
@@ -9,8 +9,12 @@ pub const SUPERVISOR_SKILL: &str = include_str!("supervisor.md");
/// Contract skill content - task-contract interaction commands
pub const CONTRACT_SKILL: &str = include_str!("contract.md");
+/// Directive skill content - DAG-based project orchestration commands
+pub const DIRECTIVE_SKILL: &str = include_str!("directive.md");
+
/// All skills as (name, content) pairs for installation
pub const ALL_SKILLS: &[(&str, &str)] = &[
("makima-supervisor", SUPERVISOR_SKILL),
("makima-contract", CONTRACT_SKILL),
+ ("makima-directive", DIRECTIVE_SKILL),
];
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index d0a0bd6..9159fd5 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -531,6 +531,14 @@ pub struct Task {
/// Standalone completed tasks can be dismissed by the user.
#[serde(default)]
pub hidden: bool,
+
+ // Directive association
+ /// Directive this task belongs to (for directive-driven tasks)
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub directive_id: Option<Uuid>,
+ /// Directive step this task executes
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub directive_step_id: Option<Uuid>,
}
impl Task {
@@ -656,6 +664,10 @@ pub struct CreateTaskRequest {
/// Task ID whose worktree this task shares. When set, this task reuses the supervisor's
/// worktree instead of creating its own, and should NOT have its worktree deleted during cleanup.
pub supervisor_worktree_task_id: Option<Uuid>,
+ /// Directive this task belongs to (for directive-driven tasks)
+ pub directive_id: Option<Uuid>,
+ /// Directive step this task executes
+ pub directive_step_id: Option<Uuid>,
}
/// Request payload for updating a task
@@ -2682,3 +2694,142 @@ mod tests {
}
// =============================================================================
+// Directive Types
+// =============================================================================
+
+/// A directive — a long-lived top-level entity for managing projects via a DAG of steps.
+#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct Directive {
+ pub id: Uuid,
+ pub owner_id: Uuid,
+ pub title: String,
+ pub goal: String,
+ /// Status: draft, active, idle, paused, archived
+ pub status: String,
+ pub repository_url: Option<String>,
+ pub local_path: Option<String>,
+ pub base_branch: Option<String>,
+ pub orchestrator_task_id: Option<Uuid>,
+ pub goal_updated_at: DateTime<Utc>,
+ pub started_at: Option<DateTime<Utc>>,
+ pub version: i32,
+ pub created_at: DateTime<Utc>,
+ pub updated_at: DateTime<Utc>,
+}
+
+/// A step in a directive's DAG.
+#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveStep {
+ pub id: Uuid,
+ pub directive_id: Uuid,
+ pub name: String,
+ pub description: Option<String>,
+ pub task_plan: Option<String>,
+ pub depends_on: Vec<Uuid>,
+ /// Status: pending, ready, running, completed, failed, skipped
+ pub status: String,
+ pub task_id: Option<Uuid>,
+ pub order_index: i32,
+ pub generation: i32,
+ pub started_at: Option<DateTime<Utc>>,
+ pub completed_at: Option<DateTime<Utc>>,
+ pub created_at: DateTime<Utc>,
+}
+
+/// Directive with its steps for detail view.
+#[derive(Debug, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveWithSteps {
+ #[serde(flatten)]
+ pub directive: Directive,
+ pub steps: Vec<DirectiveStep>,
+}
+
+/// Summary for directive list views.
+#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveSummary {
+ pub id: Uuid,
+ pub owner_id: Uuid,
+ pub title: String,
+ pub goal: String,
+ pub status: String,
+ pub repository_url: Option<String>,
+ pub orchestrator_task_id: Option<Uuid>,
+ pub version: i32,
+ pub created_at: DateTime<Utc>,
+ pub updated_at: DateTime<Utc>,
+ pub total_steps: i64,
+ pub completed_steps: i64,
+ pub running_steps: i64,
+ pub failed_steps: i64,
+}
+
+/// List response for directives.
+#[derive(Debug, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveListResponse {
+ pub directives: Vec<DirectiveSummary>,
+ pub total: i64,
+}
+
+/// Request to create a new directive.
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct CreateDirectiveRequest {
+ pub title: String,
+ pub goal: String,
+ pub repository_url: Option<String>,
+ pub local_path: Option<String>,
+ pub base_branch: Option<String>,
+}
+
+/// Request to update a directive.
+#[derive(Debug, Default, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateDirectiveRequest {
+ pub title: Option<String>,
+ pub goal: Option<String>,
+ pub status: Option<String>,
+ pub repository_url: Option<String>,
+ pub local_path: Option<String>,
+ pub base_branch: Option<String>,
+ pub orchestrator_task_id: Option<Uuid>,
+ pub version: Option<i32>,
+}
+
+/// Request to update a directive's goal (triggers re-planning).
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateGoalRequest {
+ pub goal: String,
+}
+
+/// Request to create a directive step.
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct CreateDirectiveStepRequest {
+ pub name: String,
+ pub description: Option<String>,
+ pub task_plan: Option<String>,
+ #[serde(default)]
+ pub depends_on: Vec<Uuid>,
+ #[serde(default)]
+ pub order_index: i32,
+ pub generation: Option<i32>,
+}
+
+/// Request to update a directive step.
+#[derive(Debug, Default, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateDirectiveStepRequest {
+ pub name: Option<String>,
+ pub description: Option<String>,
+ pub task_plan: Option<String>,
+ pub depends_on: Option<Vec<Uuid>>,
+ pub status: Option<String>,
+ pub task_id: Option<Uuid>,
+ pub order_index: Option<i32>,
+}
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 4ed2298..f347fc7 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -11,7 +11,9 @@ use super::models::{
ContractTypeTemplateRecord, ConversationMessage, ConversationSnapshot,
CreateContractRequest, CreateFileRequest, CreateTaskRequest,
CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity,
- DeliverableDefinition,
+ DeliverableDefinition, Directive, DirectiveStep, DirectiveSummary,
+ CreateDirectiveRequest, CreateDirectiveStepRequest, UpdateDirectiveRequest,
+ UpdateDirectiveStepRequest,
File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters,
MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig,
PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState,
@@ -4912,3 +4914,414 @@ fn truncate_string(s: &str, max_len: usize) -> String {
}
}
+// =============================================================================
+// Directive CRUD
+// =============================================================================
+
+/// Create a new directive for an owner.
+pub async fn create_directive_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ req: CreateDirectiveRequest,
+) -> Result<Directive, sqlx::Error> {
+ sqlx::query_as::<_, Directive>(
+ r#"
+ INSERT INTO directives (owner_id, title, goal, repository_url, local_path, base_branch)
+ VALUES ($1, $2, $3, $4, $5, $6)
+ RETURNING *
+ "#,
+ )
+ .bind(owner_id)
+ .bind(&req.title)
+ .bind(&req.goal)
+ .bind(&req.repository_url)
+ .bind(&req.local_path)
+ .bind(&req.base_branch)
+ .fetch_one(pool)
+ .await
+}
+
+/// Get a single directive for an owner.
+pub async fn get_directive_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ id: Uuid,
+) -> Result<Option<Directive>, sqlx::Error> {
+ sqlx::query_as::<_, Directive>(
+ r#"SELECT * FROM directives WHERE id = $1 AND owner_id = $2"#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .fetch_optional(pool)
+ .await
+}
+
+/// Get a directive with all its steps.
+pub async fn get_directive_with_steps_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ id: Uuid,
+) -> Result<Option<(Directive, Vec<DirectiveStep>)>, sqlx::Error> {
+ let directive = sqlx::query_as::<_, Directive>(
+ r#"SELECT * FROM directives WHERE id = $1 AND owner_id = $2"#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .fetch_optional(pool)
+ .await?;
+
+ match directive {
+ Some(d) => {
+ let steps = list_directive_steps(pool, d.id).await?;
+ Ok(Some((d, steps)))
+ }
+ None => Ok(None),
+ }
+}
+
+/// List all directives for an owner with step counts.
+pub async fn list_directives_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+) -> Result<Vec<DirectiveSummary>, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveSummary>(
+ r#"
+ SELECT
+ d.id, d.owner_id, d.title, d.goal, d.status, d.repository_url,
+ d.orchestrator_task_id, d.version, d.created_at, d.updated_at,
+ COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id), 0) as total_steps,
+ COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id AND status = 'completed'), 0) as completed_steps,
+ COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id AND status = 'running'), 0) as running_steps,
+ COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id AND status = 'failed'), 0) as failed_steps
+ FROM directives d
+ WHERE d.owner_id = $1
+ ORDER BY d.created_at DESC
+ "#,
+ )
+ .bind(owner_id)
+ .fetch_all(pool)
+ .await
+}
+
+/// Update a directive with optimistic locking.
+pub async fn update_directive_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ id: Uuid,
+ req: UpdateDirectiveRequest,
+) -> Result<Option<Directive>, RepositoryError> {
+ let current = sqlx::query_as::<_, Directive>(
+ r#"SELECT * FROM directives WHERE id = $1 AND owner_id = $2"#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .fetch_optional(pool)
+ .await
+ .map_err(RepositoryError::Database)?;
+
+ let current = match current {
+ Some(c) => c,
+ None => return Ok(None),
+ };
+
+ if let Some(expected_version) = req.version {
+ if expected_version != current.version {
+ return Err(RepositoryError::VersionConflict {
+ expected: expected_version,
+ actual: current.version,
+ });
+ }
+ }
+
+ let title = req.title.as_deref().unwrap_or(&current.title);
+ let goal = req.goal.as_deref().unwrap_or(&current.goal);
+ let status = req.status.as_deref().unwrap_or(&current.status);
+ let repository_url = req.repository_url.as_deref().or(current.repository_url.as_deref());
+ let local_path = req.local_path.as_deref().or(current.local_path.as_deref());
+ let base_branch = req.base_branch.as_deref().or(current.base_branch.as_deref());
+ let orchestrator_task_id = req.orchestrator_task_id.or(current.orchestrator_task_id);
+
+ let result = sqlx::query_as::<_, Directive>(
+ r#"
+ UPDATE directives
+ SET title = $3, goal = $4, status = $5, repository_url = $6, local_path = $7,
+ base_branch = $8, orchestrator_task_id = $9, version = version + 1, updated_at = NOW()
+ WHERE id = $1 AND owner_id = $2
+ RETURNING *
+ "#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .bind(title)
+ .bind(goal)
+ .bind(status)
+ .bind(repository_url)
+ .bind(local_path)
+ .bind(base_branch)
+ .bind(orchestrator_task_id)
+ .fetch_optional(pool)
+ .await
+ .map_err(RepositoryError::Database)?;
+
+ Ok(result)
+}
+
+/// Delete a directive for an owner.
+pub async fn delete_directive_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ id: Uuid,
+) -> Result<bool, sqlx::Error> {
+ let result = sqlx::query(
+ r#"DELETE FROM directives WHERE id = $1 AND owner_id = $2"#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .execute(pool)
+ .await?;
+
+ Ok(result.rows_affected() > 0)
+}
+
+// =============================================================================
+// Directive Step CRUD
+// =============================================================================
+
+/// List all steps for a directive, ordered by order_index.
+pub async fn list_directive_steps(
+ pool: &PgPool,
+ directive_id: Uuid,
+) -> Result<Vec<DirectiveStep>, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveStep>(
+ r#"
+ SELECT * FROM directive_steps
+ WHERE directive_id = $1
+ ORDER BY order_index, created_at
+ "#,
+ )
+ .bind(directive_id)
+ .fetch_all(pool)
+ .await
+}
+
+/// Create a single directive step.
+pub async fn create_directive_step(
+ pool: &PgPool,
+ directive_id: Uuid,
+ req: CreateDirectiveStepRequest,
+) -> Result<DirectiveStep, sqlx::Error> {
+ let generation = req.generation.unwrap_or(1);
+ sqlx::query_as::<_, DirectiveStep>(
+ r#"
+ INSERT INTO directive_steps (directive_id, name, description, task_plan, depends_on, order_index, generation)
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
+ RETURNING *
+ "#,
+ )
+ .bind(directive_id)
+ .bind(&req.name)
+ .bind(&req.description)
+ .bind(&req.task_plan)
+ .bind(&req.depends_on)
+ .bind(req.order_index)
+ .bind(generation)
+ .fetch_one(pool)
+ .await
+}
+
+/// Batch create multiple directive steps.
+pub async fn batch_create_directive_steps(
+ pool: &PgPool,
+ directive_id: Uuid,
+ steps: Vec<CreateDirectiveStepRequest>,
+) -> Result<Vec<DirectiveStep>, sqlx::Error> {
+ let mut results = Vec::with_capacity(steps.len());
+ for req in steps {
+ let step = create_directive_step(pool, directive_id, req).await?;
+ results.push(step);
+ }
+ Ok(results)
+}
+
+/// Update a directive step.
+pub async fn update_directive_step(
+ pool: &PgPool,
+ step_id: Uuid,
+ req: UpdateDirectiveStepRequest,
+) -> Result<Option<DirectiveStep>, sqlx::Error> {
+ let current = sqlx::query_as::<_, DirectiveStep>(
+ r#"SELECT * FROM directive_steps WHERE id = $1"#,
+ )
+ .bind(step_id)
+ .fetch_optional(pool)
+ .await?;
+
+ let current = match current {
+ Some(c) => c,
+ None => return Ok(None),
+ };
+
+ let name = req.name.as_deref().unwrap_or(&current.name);
+ let description = req.description.as_deref().or(current.description.as_deref());
+ let task_plan = req.task_plan.as_deref().or(current.task_plan.as_deref());
+ let depends_on = req.depends_on.as_deref().unwrap_or(&current.depends_on);
+ let status = req.status.as_deref().unwrap_or(&current.status);
+ let task_id = req.task_id.or(current.task_id);
+ let order_index = req.order_index.unwrap_or(current.order_index);
+
+ // Set started_at when transitioning to running
+ let started_at = if status == "running" && current.status != "running" {
+ Some(Utc::now())
+ } else {
+ current.started_at
+ };
+
+ // Set completed_at when transitioning to terminal state
+ let completed_at = if matches!(status, "completed" | "failed" | "skipped")
+ && !matches!(current.status.as_str(), "completed" | "failed" | "skipped")
+ {
+ Some(Utc::now())
+ } else {
+ current.completed_at
+ };
+
+ sqlx::query_as::<_, DirectiveStep>(
+ r#"
+ UPDATE directive_steps
+ SET name = $2, description = $3, task_plan = $4, depends_on = $5,
+ status = $6, task_id = $7, order_index = $8, started_at = $9, completed_at = $10
+ WHERE id = $1
+ RETURNING *
+ "#,
+ )
+ .bind(step_id)
+ .bind(name)
+ .bind(description)
+ .bind(task_plan)
+ .bind(depends_on)
+ .bind(status)
+ .bind(task_id)
+ .bind(order_index)
+ .bind(started_at)
+ .bind(completed_at)
+ .fetch_optional(pool)
+ .await
+}
+
+/// Delete a directive step.
+pub async fn delete_directive_step(
+ pool: &PgPool,
+ step_id: Uuid,
+) -> Result<bool, sqlx::Error> {
+ let result = sqlx::query(r#"DELETE FROM directive_steps WHERE id = $1"#)
+ .bind(step_id)
+ .execute(pool)
+ .await?;
+
+ Ok(result.rows_affected() > 0)
+}
+
+// =============================================================================
+// Directive DAG Progression
+// =============================================================================
+
+/// Advance pending steps to ready if all their dependencies are in terminal states.
+/// Returns the newly-ready steps.
+pub async fn advance_directive_ready_steps(
+ pool: &PgPool,
+ directive_id: Uuid,
+) -> Result<Vec<DirectiveStep>, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveStep>(
+ r#"
+ UPDATE directive_steps SET status = 'ready'
+ WHERE directive_id = $1 AND status = 'pending'
+ AND NOT EXISTS (
+ SELECT 1 FROM unnest(depends_on) AS dep_id
+ JOIN directive_steps ds ON ds.id = dep_id
+ WHERE ds.status NOT IN ('completed', 'skipped')
+ )
+ RETURNING *
+ "#,
+ )
+ .bind(directive_id)
+ .fetch_all(pool)
+ .await
+}
+
+/// Check if all steps in a directive are in terminal states.
+/// If so, set the directive to 'idle' (not completed — directives are ongoing).
+/// Returns true if the directive was set to idle.
+pub async fn check_directive_idle(
+ pool: &PgPool,
+ directive_id: Uuid,
+) -> Result<bool, sqlx::Error> {
+ let result = sqlx::query(
+ r#"
+ UPDATE directives SET status = 'idle', updated_at = NOW()
+ WHERE id = $1 AND status = 'active'
+ AND NOT EXISTS (
+ SELECT 1 FROM directive_steps
+ WHERE directive_id = $1
+ AND status NOT IN ('completed', 'failed', 'skipped')
+ )
+ AND EXISTS (
+ SELECT 1 FROM directive_steps WHERE directive_id = $1
+ )
+ "#,
+ )
+ .bind(directive_id)
+ .execute(pool)
+ .await?;
+
+ Ok(result.rows_affected() > 0)
+}
+
+/// Update a directive's goal and bump goal_updated_at. Reactivates if idle.
+pub async fn update_directive_goal(
+ pool: &PgPool,
+ owner_id: Uuid,
+ directive_id: Uuid,
+ goal: &str,
+) -> Result<Option<Directive>, sqlx::Error> {
+ sqlx::query_as::<_, Directive>(
+ r#"
+ UPDATE directives
+ SET goal = $3,
+ goal_updated_at = NOW(),
+ status = CASE WHEN status = 'idle' THEN 'active' ELSE status END,
+ updated_at = NOW(),
+ version = version + 1
+ WHERE id = $1 AND owner_id = $2
+ RETURNING *
+ "#,
+ )
+ .bind(directive_id)
+ .bind(owner_id)
+ .bind(goal)
+ .fetch_optional(pool)
+ .await
+}
+
+/// Set a directive's status (used for start/pause/archive transitions).
+pub async fn set_directive_status(
+ pool: &PgPool,
+ owner_id: Uuid,
+ directive_id: Uuid,
+ status: &str,
+) -> Result<Option<Directive>, sqlx::Error> {
+ let mut query = String::from(
+ r#"UPDATE directives SET status = $3, updated_at = NOW(), version = version + 1"#,
+ );
+ if status == "active" {
+ query.push_str(", started_at = COALESCE(started_at, NOW())");
+ }
+ query.push_str(" WHERE id = $1 AND owner_id = $2 RETURNING *");
+
+ sqlx::query_as::<_, Directive>(&query)
+ .bind(directive_id)
+ .bind(owner_id)
+ .bind(status)
+ .fetch_optional(pool)
+ .await
+}
+
diff --git a/makima/src/llm/contract_tools.rs b/makima/src/llm/contract_tools.rs
index 7f7e849..38d1a7e 100644
--- a/makima/src/llm/contract_tools.rs
+++ b/makima/src/llm/contract_tools.rs
@@ -460,214 +460,6 @@ pub static CONTRACT_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::L
"required": ["file_id"]
}),
},
- // =============================================================================
- // Chain Directive Tools (for directive contracts orchestrating chains)
- // =============================================================================
- Tool {
- name: "create_chain_from_directive".to_string(),
- description: "Create a new chain that this directive contract will orchestrate. The chain starts in 'pending' status and contract definitions can be added. Only available to directive contracts.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Name for the chain"
- },
- "description": {
- "type": "string",
- "description": "Description of what the chain accomplishes"
- }
- },
- "required": ["name"]
- }),
- },
- Tool {
- name: "add_chain_contract".to_string(),
- description: "Add a contract definition to the chain being orchestrated. The contract will be created when its dependencies are met.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Contract name"
- },
- "description": {
- "type": "string",
- "description": "What this contract accomplishes"
- },
- "contract_type": {
- "type": "string",
- "enum": ["simple", "execute", "checkpoint"],
- "description": "Contract type (default: simple)"
- },
- "depends_on": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Names of contracts this depends on"
- },
- "requirement_ids": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Requirement IDs this contract addresses (for traceability)"
- }
- },
- "required": ["name"]
- }),
- },
- Tool {
- name: "set_chain_dependencies".to_string(),
- description: "Set which contracts depend on which other contracts in the chain.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "contract_name": {
- "type": "string",
- "description": "Name of contract that has dependencies"
- },
- "depends_on": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Names of contracts it depends on"
- }
- },
- "required": ["contract_name", "depends_on"]
- }),
- },
- Tool {
- name: "modify_chain_contract".to_string(),
- description: "Update a contract definition in the chain.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Name of the contract to modify"
- },
- "new_name": {
- "type": "string",
- "description": "New name for the contract"
- },
- "description": {
- "type": "string",
- "description": "New description"
- },
- "add_requirement_ids": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Requirement IDs to add"
- },
- "remove_requirement_ids": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Requirement IDs to remove"
- }
- },
- "required": ["name"]
- }),
- },
- Tool {
- name: "remove_chain_contract".to_string(),
- description: "Remove a contract definition from the chain (only if not yet instantiated).".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Name of the contract to remove"
- }
- },
- "required": ["name"]
- }),
- },
- Tool {
- name: "preview_chain_dag".to_string(),
- description: "Generate a visual preview of the chain DAG structure for review.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "validate_chain_directive".to_string(),
- description: "Validate the chain specification is complete and valid (no cycles, all dependencies exist, all requirements covered).".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "finalize_chain_directive".to_string(),
- description: "Lock the directive and start chain execution. Call this after validation passes and user has approved (if phase_guard enabled).".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "auto_start": {
- "type": "boolean",
- "description": "Whether to immediately start the chain (default: true)"
- }
- }
- }),
- },
- Tool {
- name: "get_chain_status".to_string(),
- description: "Get current status of the chain being orchestrated, including contract statuses and progress.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "get_uncovered_requirements".to_string(),
- description: "List requirements from the directive that are not yet mapped to any contract.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "evaluate_contract_completion".to_string(),
- description: "Evaluate whether a completed chain contract meets the directive requirements. Use this after a contract completes to assess if it satisfies acceptance criteria.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "contract_id": {
- "type": "string",
- "description": "ID of the completed contract to evaluate"
- },
- "passed": {
- "type": "boolean",
- "description": "Whether the evaluation passed"
- },
- "feedback": {
- "type": "string",
- "description": "Evaluation feedback and rationale"
- },
- "rework_instructions": {
- "type": "string",
- "description": "Instructions for rework if evaluation failed"
- }
- },
- "required": ["contract_id", "passed", "feedback"]
- }),
- },
- Tool {
- name: "request_rework".to_string(),
- description: "Request rework on a completed contract that didn't meet requirements. This will block chain progression and notify the contract to address issues.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "contract_id": {
- "type": "string",
- "description": "ID of the contract needing rework"
- },
- "feedback": {
- "type": "string",
- "description": "Detailed feedback on what needs to be fixed"
- }
- },
- "required": ["contract_id", "feedback"]
- }),
- },
]
});
@@ -755,49 +547,6 @@ pub enum ContractToolRequest {
include_action_items: bool,
},
- // Chain directive tools (for directive contracts)
- CreateChainFromDirective {
- name: String,
- description: Option<String>,
- },
- AddChainContract {
- name: String,
- description: Option<String>,
- contract_type: Option<String>,
- depends_on: Option<Vec<String>>,
- requirement_ids: Option<Vec<String>>,
- },
- SetChainDependencies {
- contract_name: String,
- depends_on: Vec<String>,
- },
- ModifyChainContract {
- name: String,
- new_name: Option<String>,
- description: Option<String>,
- add_requirement_ids: Option<Vec<String>>,
- remove_requirement_ids: Option<Vec<String>>,
- },
- RemoveChainContract {
- name: String,
- },
- PreviewChainDag,
- ValidateChainDirective,
- FinalizeChainDirective {
- auto_start: bool,
- },
- GetChainStatus,
- GetUncoveredRequirements,
- EvaluateContractCompletion {
- contract_id: Uuid,
- passed: bool,
- feedback: String,
- rework_instructions: Option<String>,
- },
- RequestRework {
- contract_id: Uuid,
- feedback: String,
- },
}
/// Task definition for chained task creation
@@ -869,20 +618,6 @@ pub fn parse_contract_tool_call(call: &super::tools::ToolCall) -> ContractToolEx
"analyze_transcript" => parse_analyze_transcript(call),
"create_contract_from_transcript" => parse_create_contract_from_transcript(call),
- // Chain directive tools
- "create_chain_from_directive" => parse_create_chain_from_directive(call),
- "add_chain_contract" => parse_add_chain_contract(call),
- "set_chain_dependencies" => parse_set_chain_dependencies(call),
- "modify_chain_contract" => parse_modify_chain_contract(call),
- "remove_chain_contract" => parse_remove_chain_contract(call),
- "preview_chain_dag" => parse_preview_chain_dag(),
- "validate_chain_directive" => parse_validate_chain_directive(),
- "finalize_chain_directive" => parse_finalize_chain_directive(call),
- "get_chain_status" => parse_get_chain_status(),
- "get_uncovered_requirements" => parse_get_uncovered_requirements(),
- "evaluate_contract_completion" => parse_evaluate_contract_completion(call),
- "request_rework" => parse_request_rework(call),
-
_ => ContractToolExecutionResult {
success: false,
message: format!("Unknown contract tool: {}", call.name),
@@ -1472,229 +1207,6 @@ fn parse_create_contract_from_transcript(call: &super::tools::ToolCall) -> Contr
}
// =============================================================================
-// Chain Directive Tool Parsing
-// =============================================================================
-
-fn parse_create_chain_from_directive(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let name = call.arguments.get("name").and_then(|v| v.as_str()).map(|s| s.to_string());
- let Some(name) = name else {
- return error_result("Missing required parameter: name");
- };
- let description = call.arguments.get("description").and_then(|v| v.as_str()).map(|s| s.to_string());
-
- ContractToolExecutionResult {
- success: true,
- message: "Creating chain from directive...".to_string(),
- data: None,
- request: Some(ContractToolRequest::CreateChainFromDirective { name, description }),
- pending_questions: None,
- }
-}
-
-fn parse_add_chain_contract(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let name = call.arguments.get("name").and_then(|v| v.as_str()).map(|s| s.to_string());
- let Some(name) = name else {
- return error_result("Missing required parameter: name");
- };
-
- let description = call.arguments.get("description").and_then(|v| v.as_str()).map(|s| s.to_string());
- let contract_type = call.arguments.get("contract_type").and_then(|v| v.as_str()).map(|s| s.to_string());
- let depends_on = call.arguments.get("depends_on").and_then(|v| {
- v.as_array().map(|arr| {
- arr.iter().filter_map(|item| item.as_str().map(|s| s.to_string())).collect()
- })
- });
- let requirement_ids = call.arguments.get("requirement_ids").and_then(|v| {
- v.as_array().map(|arr| {
- arr.iter().filter_map(|item| item.as_str().map(|s| s.to_string())).collect()
- })
- });
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Adding contract '{}' to chain...", name),
- data: None,
- request: Some(ContractToolRequest::AddChainContract {
- name,
- description,
- contract_type,
- depends_on,
- requirement_ids,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_set_chain_dependencies(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let contract_name = call.arguments.get("contract_name").and_then(|v| v.as_str()).map(|s| s.to_string());
- let Some(contract_name) = contract_name else {
- return error_result("Missing required parameter: contract_name");
- };
-
- let depends_on = call.arguments.get("depends_on").and_then(|v| {
- v.as_array().map(|arr| {
- arr.iter().filter_map(|item| item.as_str().map(|s| s.to_string())).collect()
- })
- }).unwrap_or_default();
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Setting dependencies for '{}'...", contract_name),
- data: None,
- request: Some(ContractToolRequest::SetChainDependencies { contract_name, depends_on }),
- pending_questions: None,
- }
-}
-
-fn parse_modify_chain_contract(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let name = call.arguments.get("name").and_then(|v| v.as_str()).map(|s| s.to_string());
- let Some(name) = name else {
- return error_result("Missing required parameter: name");
- };
-
- let new_name = call.arguments.get("new_name").and_then(|v| v.as_str()).map(|s| s.to_string());
- let description = call.arguments.get("description").and_then(|v| v.as_str()).map(|s| s.to_string());
- let add_requirement_ids = call.arguments.get("add_requirement_ids").and_then(|v| {
- v.as_array().map(|arr| {
- arr.iter().filter_map(|item| item.as_str().map(|s| s.to_string())).collect()
- })
- });
- let remove_requirement_ids = call.arguments.get("remove_requirement_ids").and_then(|v| {
- v.as_array().map(|arr| {
- arr.iter().filter_map(|item| item.as_str().map(|s| s.to_string())).collect()
- })
- });
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Modifying contract '{}'...", name),
- data: None,
- request: Some(ContractToolRequest::ModifyChainContract {
- name,
- new_name,
- description,
- add_requirement_ids,
- remove_requirement_ids,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_remove_chain_contract(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let name = call.arguments.get("name").and_then(|v| v.as_str()).map(|s| s.to_string());
- let Some(name) = name else {
- return error_result("Missing required parameter: name");
- };
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Removing contract '{}'...", name),
- data: None,
- request: Some(ContractToolRequest::RemoveChainContract { name }),
- pending_questions: None,
- }
-}
-
-fn parse_preview_chain_dag() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Generating chain DAG preview...".to_string(),
- data: None,
- request: Some(ContractToolRequest::PreviewChainDag),
- pending_questions: None,
- }
-}
-
-fn parse_validate_chain_directive() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Validating chain directive...".to_string(),
- data: None,
- request: Some(ContractToolRequest::ValidateChainDirective),
- pending_questions: None,
- }
-}
-
-fn parse_finalize_chain_directive(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let auto_start = call.arguments.get("auto_start").and_then(|v| v.as_bool()).unwrap_or(true);
-
- ContractToolExecutionResult {
- success: true,
- message: "Finalizing chain directive...".to_string(),
- data: None,
- request: Some(ContractToolRequest::FinalizeChainDirective { auto_start }),
- pending_questions: None,
- }
-}
-
-fn parse_get_chain_status() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Getting chain status...".to_string(),
- data: None,
- request: Some(ContractToolRequest::GetChainStatus),
- pending_questions: None,
- }
-}
-
-fn parse_get_uncovered_requirements() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Getting uncovered requirements...".to_string(),
- data: None,
- request: Some(ContractToolRequest::GetUncoveredRequirements),
- pending_questions: None,
- }
-}
-
-fn parse_evaluate_contract_completion(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let contract_id = parse_uuid_arg(call, "contract_id");
- let Some(contract_id) = contract_id else {
- return error_result("Missing or invalid required parameter: contract_id");
- };
-
- let passed = call.arguments.get("passed").and_then(|v| v.as_bool()).unwrap_or(false);
- let feedback = call.arguments.get("feedback").and_then(|v| v.as_str()).map(|s| s.to_string());
- let Some(feedback) = feedback else {
- return error_result("Missing required parameter: feedback");
- };
- let rework_instructions = call.arguments.get("rework_instructions").and_then(|v| v.as_str()).map(|s| s.to_string());
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Evaluating contract completion (passed: {})...", passed),
- data: None,
- request: Some(ContractToolRequest::EvaluateContractCompletion {
- contract_id,
- passed,
- feedback,
- rework_instructions,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_request_rework(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let contract_id = parse_uuid_arg(call, "contract_id");
- let Some(contract_id) = contract_id else {
- return error_result("Missing or invalid required parameter: contract_id");
- };
-
- let feedback = call.arguments.get("feedback").and_then(|v| v.as_str()).map(|s| s.to_string());
- let Some(feedback) = feedback else {
- return error_result("Missing required parameter: feedback");
- };
-
- ContractToolExecutionResult {
- success: true,
- message: "Requesting rework...".to_string(),
- data: None,
- request: Some(ContractToolRequest::RequestRework { contract_id, feedback }),
- pending_questions: None,
- }
-}
-
-// =============================================================================
// Helper Functions
// =============================================================================
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs
index 8153093..2c7a800 100644
--- a/makima/src/server/handlers/contract_chat.rs
+++ b/makima/src/server/handlers/contract_chat.rs
@@ -1368,6 +1368,8 @@ async fn handle_contract_request(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
@@ -1465,6 +1467,8 @@ async fn handle_contract_request(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
@@ -2217,6 +2221,8 @@ async fn handle_contract_request(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
@@ -2737,6 +2743,8 @@ async fn handle_contract_request(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
if repository::create_task_for_owner(pool, owner_id, task_req).await.is_ok() {
@@ -2766,27 +2774,6 @@ async fn handle_contract_request(
}
- // Chain directive tools - TEMPORARILY DISABLED
- // These tools will be reimplemented using the new directive system.
- // See the orchestration module for the new implementation.
- ContractToolRequest::CreateChainFromDirective { .. } |
- ContractToolRequest::AddChainContract { .. } |
- ContractToolRequest::SetChainDependencies { .. } |
- ContractToolRequest::ModifyChainContract { .. } |
- ContractToolRequest::RemoveChainContract { .. } |
- ContractToolRequest::PreviewChainDag |
- ContractToolRequest::ValidateChainDirective |
- ContractToolRequest::FinalizeChainDirective { .. } |
- ContractToolRequest::GetChainStatus |
- ContractToolRequest::GetUncoveredRequirements |
- ContractToolRequest::EvaluateContractCompletion { .. } |
- ContractToolRequest::RequestRework { .. } => {
- ContractRequestResult {
- success: false,
- message: "Chain directive tools are temporarily disabled. The directive system is being reimplemented.".to_string(),
- data: None,
- }
- }
}
}
diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs
index dc15923..bdd4d40 100644
--- a/makima/src/server/handlers/contracts.rs
+++ b/makima/src/server/handlers/contracts.rs
@@ -369,6 +369,8 @@ pub async fn create_contract(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Supervisor uses its own worktree
+ directive_id: None,
+ directive_step_id: None,
};
match repository::create_task_for_owner(pool, auth.owner_id, supervisor_req).await {
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs
new file mode 100644
index 0000000..d48ff74
--- /dev/null
+++ b/makima/src/server/handlers/directives.rs
@@ -0,0 +1,841 @@
+//! HTTP handlers for directive CRUD and DAG progression.
+
+use axum::{
+ extract::{Path, State},
+ http::StatusCode,
+ response::IntoResponse,
+ Json,
+};
+use uuid::Uuid;
+
+use crate::db::models::{
+ CreateDirectiveRequest, CreateDirectiveStepRequest, Directive, DirectiveListResponse,
+ DirectiveStep, DirectiveWithSteps, UpdateDirectiveRequest, UpdateDirectiveStepRequest,
+ UpdateGoalRequest,
+};
+use crate::db::repository;
+use crate::server::auth::Authenticated;
+use crate::server::messages::ApiError;
+use crate::server::state::SharedState;
+
+// =============================================================================
+// Directive CRUD
+// =============================================================================
+
+/// List all directives for the authenticated user.
+#[utoipa::path(
+ get,
+ path = "/api/v1/directives",
+ responses(
+ (status = 200, description = "List of directives", body = DirectiveListResponse),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn list_directives(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::list_directives_for_owner(pool, auth.owner_id).await {
+ Ok(directives) => {
+ let total = directives.len() as i64;
+ Json(DirectiveListResponse { directives, total }).into_response()
+ }
+ Err(e) => {
+ tracing::error!("Failed to list directives: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("LIST_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Create a new directive.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives",
+ request_body = CreateDirectiveRequest,
+ responses(
+ (status = 201, description = "Directive created", body = Directive),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn create_directive(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Json(req): Json<CreateDirectiveRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::create_directive_for_owner(pool, auth.owner_id, req).await {
+ Ok(directive) => (StatusCode::CREATED, Json(directive)).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to create directive: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("CREATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Get a directive with all its steps.
+#[utoipa::path(
+ get,
+ path = "/api/v1/directives/{id}",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ responses(
+ (status = 200, description = "Directive with steps", body = DirectiveWithSteps),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn get_directive(
+ 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();
+ };
+
+ match repository::get_directive_with_steps_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some((directive, steps))) => {
+ Json(DirectiveWithSteps { directive, steps }).into_response()
+ }
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to get directive: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("GET_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Update a directive.
+#[utoipa::path(
+ put,
+ path = "/api/v1/directives/{id}",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ request_body = UpdateDirectiveRequest,
+ responses(
+ (status = 200, description = "Directive updated", body = Directive),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 409, description = "Version conflict", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn update_directive(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(req): Json<UpdateDirectiveRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::update_directive_for_owner(pool, auth.owner_id, id, req).await {
+ Ok(Some(directive)) => Json(directive).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response(),
+ Err(repository::RepositoryError::VersionConflict { expected, actual }) => (
+ StatusCode::CONFLICT,
+ Json(ApiError::new(
+ "VERSION_CONFLICT",
+ &format!("Expected version {}, but current is {}", expected, actual),
+ )),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to update directive: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("UPDATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Delete a directive.
+#[utoipa::path(
+ delete,
+ path = "/api/v1/directives/{id}",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ responses(
+ (status = 204, description = "Deleted"),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn delete_directive(
+ 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();
+ };
+
+ match repository::delete_directive_for_owner(pool, auth.owner_id, id).await {
+ Ok(true) => StatusCode::NO_CONTENT.into_response(),
+ Ok(false) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to delete directive: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DELETE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+// =============================================================================
+// Step CRUD
+// =============================================================================
+
+/// Create a step in a directive.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/steps",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ request_body = CreateDirectiveStepRequest,
+ responses(
+ (status = 201, description = "Step created", body = DirectiveStep),
+ (status = 404, description = "Directive not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn create_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(req): Json<CreateDirectiveStepRequest>,
+) -> 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 ownership
+ 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) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::create_directive_step(pool, id, req).await {
+ Ok(step) => (StatusCode::CREATED, Json(step)).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to create step: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("CREATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Batch create steps in a directive.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/steps/batch",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ request_body = Vec<CreateDirectiveStepRequest>,
+ responses(
+ (status = 201, description = "Steps created", body = Vec<DirectiveStep>),
+ (status = 404, description = "Directive not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn batch_create_steps(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(steps): Json<Vec<CreateDirectiveStepRequest>>,
+) -> 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 ownership
+ 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) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::batch_create_directive_steps(pool, id, steps).await {
+ Ok(created) => (StatusCode::CREATED, Json(created)).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to batch create steps: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("CREATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Update a step.
+#[utoipa::path(
+ put,
+ path = "/api/v1/directives/{id}/steps/{step_id}",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("step_id" = Uuid, Path, description = "Step ID"),
+ ),
+ request_body = UpdateDirectiveStepRequest,
+ responses(
+ (status = 200, description = "Step updated", body = DirectiveStep),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn update_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, step_id)): Path<(Uuid, Uuid)>,
+ Json(req): Json<UpdateDirectiveStepRequest>,
+) -> 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 ownership
+ 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) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::update_directive_step(pool, step_id, req).await {
+ Ok(Some(step)) => Json(step).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Step not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to update step: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("UPDATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Delete a step.
+#[utoipa::path(
+ delete,
+ path = "/api/v1/directives/{id}/steps/{step_id}",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("step_id" = Uuid, Path, description = "Step ID"),
+ ),
+ responses(
+ (status = 204, description = "Deleted"),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn delete_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, step_id)): Path<(Uuid, 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 ownership
+ 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) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::delete_directive_step(pool, step_id).await {
+ Ok(true) => StatusCode::NO_CONTENT.into_response(),
+ Ok(false) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Step not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to delete step: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DELETE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+// =============================================================================
+// Directive Lifecycle Actions
+// =============================================================================
+
+/// Start a directive: sets status=active, advances ready steps.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/start",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ responses(
+ (status = 200, description = "Directive started", body = DirectiveWithSteps),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn start_directive(
+ 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();
+ };
+
+ // Set to active
+ match repository::set_directive_status(pool, auth.owner_id, id, "active").await {
+ Ok(Some(directive)) => {
+ // Advance ready steps
+ let _ = repository::advance_directive_ready_steps(pool, id).await;
+ let steps = repository::list_directive_steps(pool, id).await.unwrap_or_default();
+ Json(DirectiveWithSteps { directive, steps }).into_response()
+ }
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to start directive: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("START_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Pause a directive.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/pause",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ responses(
+ (status = 200, description = "Directive paused", body = Directive),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn pause_directive(
+ 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();
+ };
+
+ match repository::set_directive_status(pool, auth.owner_id, id, "paused").await {
+ Ok(Some(directive)) => Json(directive).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("PAUSE_FAILED", &e.to_string())),
+ )
+ .into_response(),
+ }
+}
+
+/// Advance a directive: find newly-ready steps. If all steps done, set idle.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/advance",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ responses(
+ (status = 200, description = "Advance result", body = DirectiveWithSteps),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn advance_directive(
+ 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 ownership
+ let directive = match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some(d)) => d,
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ // Advance ready steps
+ let _ = repository::advance_directive_ready_steps(pool, id).await;
+
+ // Check if idle
+ let _ = repository::check_directive_idle(pool, id).await;
+
+ // Return updated state
+ let directive = match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some(d)) => d,
+ _ => directive,
+ };
+ let steps = repository::list_directive_steps(pool, id).await.unwrap_or_default();
+ Json(DirectiveWithSteps { directive, steps }).into_response()
+}
+
+/// Mark a step as completed.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/steps/{step_id}/complete",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("step_id" = Uuid, Path, description = "Step ID"),
+ ),
+ responses(
+ (status = 200, description = "Step completed", body = DirectiveStep),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn complete_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, step_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ step_status_change(state, auth, id, step_id, "completed").await
+}
+
+/// Mark a step as failed.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/steps/{step_id}/fail",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("step_id" = Uuid, Path, description = "Step ID"),
+ ),
+ responses(
+ (status = 200, description = "Step failed", body = DirectiveStep),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn fail_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, step_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ step_status_change(state, auth, id, step_id, "failed").await
+}
+
+/// Mark a step as skipped.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/steps/{step_id}/skip",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("step_id" = Uuid, Path, description = "Step ID"),
+ ),
+ responses(
+ (status = 200, description = "Step skipped", body = DirectiveStep),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn skip_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, step_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ step_status_change(state, auth, id, step_id, "skipped").await
+}
+
+/// Helper for step status changes.
+async fn step_status_change(
+ state: SharedState,
+ auth: crate::server::auth::AuthenticatedUser,
+ directive_id: Uuid,
+ step_id: Uuid,
+ new_status: &str,
+) -> 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 ownership
+ match repository::get_directive_for_owner(pool, auth.owner_id, directive_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ let req = UpdateDirectiveStepRequest {
+ status: Some(new_status.to_string()),
+ ..Default::default()
+ };
+
+ match repository::update_directive_step(pool, step_id, req).await {
+ Ok(Some(step)) => {
+ // After step status change, advance the DAG
+ let _ = repository::advance_directive_ready_steps(pool, directive_id).await;
+ let _ = repository::check_directive_idle(pool, directive_id).await;
+ Json(step).into_response()
+ }
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Step not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to update step status: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("UPDATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Update a directive's goal (triggers re-planning).
+#[utoipa::path(
+ put,
+ path = "/api/v1/directives/{id}/goal",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ request_body = UpdateGoalRequest,
+ responses(
+ (status = 200, description = "Goal updated", body = Directive),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn update_goal(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(req): Json<UpdateGoalRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ 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")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to update goal: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("UPDATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs
index fe9ffc0..310bec8 100644
--- a/makima/src/server/handlers/mesh.rs
+++ b/makima/src/server/handlers/mesh.rs
@@ -2626,6 +2626,8 @@ pub async fn reassign_task(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
@@ -3402,6 +3404,8 @@ pub async fn fork_task(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
@@ -3560,6 +3564,8 @@ pub async fn resume_from_checkpoint(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
@@ -3896,6 +3902,8 @@ pub async fn branch_task(
branched_from_task_id: Some(source_task_id),
conversation_history,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
let task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
diff --git a/makima/src/server/handlers/mesh_chat.rs b/makima/src/server/handlers/mesh_chat.rs
index a6a3a3c..cf56ab6 100644
--- a/makima/src/server/handlers/mesh_chat.rs
+++ b/makima/src/server/handlers/mesh_chat.rs
@@ -1021,6 +1021,8 @@ async fn handle_mesh_request(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs
index 87b5e44..2ea7805 100644
--- a/makima/src/server/handlers/mesh_daemon.rs
+++ b/makima/src/server/handlers/mesh_daemon.rs
@@ -1303,6 +1303,23 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re
}),
).await;
+ // Auto-advance directive DAG when a directive step task completes
+ if let Some(step_id) = updated_task.directive_step_id {
+ let step_status = if updated_task.status == "done" { "completed" } else { "failed" };
+ let step_update = crate::db::models::UpdateDirectiveStepRequest {
+ status: Some(step_status.to_string()),
+ ..Default::default()
+ };
+ let _ = repository::update_directive_step(&pool, step_id, step_update).await;
+
+ if let Some(directive_id) = updated_task.directive_id {
+ // Advance newly-ready steps in the DAG
+ let _ = repository::advance_directive_ready_steps(&pool, directive_id).await;
+ // Check if all steps are done → set directive to idle
+ let _ = repository::check_directive_idle(&pool, directive_id).await;
+ }
+ }
+
}
Ok(None) => {
tracing::warn!(
diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs
index 09758bb..8bf2534 100644
--- a/makima/src/server/handlers/mesh_supervisor.rs
+++ b/makima/src/server/handlers/mesh_supervisor.rs
@@ -629,6 +629,8 @@ pub async fn spawn_task(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id,
+ directive_id: None,
+ directive_step_id: None,
};
// Create task in DB
diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs
index ae370c9..29cd09f 100644
--- a/makima/src/server/handlers/mod.rs
+++ b/makima/src/server/handlers/mod.rs
@@ -6,6 +6,7 @@ pub mod contract_chat;
pub mod contract_daemon;
pub mod contract_discuss;
pub mod contracts;
+pub mod directives;
pub mod file_ws;
pub mod files;
pub mod history;
diff --git a/makima/src/server/handlers/transcript_analysis.rs b/makima/src/server/handlers/transcript_analysis.rs
index 62c65a6..9261c0c 100644
--- a/makima/src/server/handlers/transcript_analysis.rs
+++ b/makima/src/server/handlers/transcript_analysis.rs
@@ -370,6 +370,8 @@ pub async fn create_contract_from_analysis(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
if let Ok(t) = repository::create_task_for_owner(pool, auth.owner_id, task_req).await {
@@ -540,6 +542,8 @@ pub async fn update_contract_from_analysis(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
if let Ok(t) = repository::create_task_for_owner(pool, auth.owner_id, task_req).await {
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index b7a4156..9e1ee50 100644
--- a/makima/src/server/mod.rs
+++ b/makima/src/server/mod.rs
@@ -18,7 +18,7 @@ use tower_http::trace::TraceLayer;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
-use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contract_discuss, contracts, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions};
+use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contract_discuss, contracts, directives, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions};
use crate::server::openapi::ApiDoc;
use crate::server::state::SharedState;
@@ -212,6 +212,30 @@ pub fn make_router(state: SharedState) -> Router {
"/contracts/{id}/tasks/{task_id}",
post(contracts::add_task_to_contract).delete(contracts::remove_task_from_contract),
)
+ // Directive endpoints
+ .route(
+ "/directives",
+ get(directives::list_directives).post(directives::create_directive),
+ )
+ .route(
+ "/directives/{id}",
+ get(directives::get_directive)
+ .put(directives::update_directive)
+ .delete(directives::delete_directive),
+ )
+ .route("/directives/{id}/steps", post(directives::create_step))
+ .route("/directives/{id}/steps/batch", post(directives::batch_create_steps))
+ .route(
+ "/directives/{id}/steps/{step_id}",
+ put(directives::update_step).delete(directives::delete_step),
+ )
+ .route("/directives/{id}/start", post(directives::start_directive))
+ .route("/directives/{id}/pause", post(directives::pause_directive))
+ .route("/directives/{id}/advance", post(directives::advance_directive))
+ .route("/directives/{id}/steps/{step_id}/complete", post(directives::complete_step))
+ .route("/directives/{id}/steps/{step_id}/fail", post(directives::fail_step))
+ .route("/directives/{id}/steps/{step_id}/skip", post(directives::skip_step))
+ .route("/directives/{id}/goal", put(directives::update_goal))
// Timeline endpoint (unified history for user)
.route("/timeline", get(history::get_timeline))
// Contract type templates (built-in only)
diff --git a/makima/src/server/openapi.rs b/makima/src/server/openapi.rs
index 0b6bfba..4e3b85b 100644
--- a/makima/src/server/openapi.rs
+++ b/makima/src/server/openapi.rs
@@ -8,9 +8,10 @@ use crate::db::models::{
ChangePhaseRequest,
Contract, ContractChatHistoryResponse, ContractChatMessageRecord, ContractEvent,
ContractListResponse, ContractRepository, ContractSummary, ContractWithRelations,
- CreateContractRequest, CreateFileRequest,
+ CreateContractRequest, CreateDirectiveRequest, CreateDirectiveStepRequest, CreateFileRequest,
CreateManagedRepositoryRequest, CreateTaskRequest, Daemon, DaemonDirectoriesResponse,
- DaemonDirectory, DaemonListResponse,
+ DaemonDirectory, DaemonListResponse, Directive, DirectiveListResponse, DirectiveStep,
+ DirectiveSummary, DirectiveWithSteps,
File, FileListResponse, FileSummary,
MergeCommitRequest, MergeCompleteCheckResponse, MergeResolveRequest, MergeResultResponse,
MergeSkipRequest, MergeStartRequest, MergeStatusResponse, MeshChatConversation,
@@ -18,13 +19,14 @@ use crate::db::models::{
RepositoryHistoryListResponse, RepositorySuggestionsQuery, SendMessageRequest,
Task,
TaskEventListResponse, TaskListResponse, TaskSummary, TaskWithSubtasks, TranscriptEntry,
- UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest,
+ UpdateContractRequest, UpdateDirectiveRequest, UpdateDirectiveStepRequest,
+ UpdateFileRequest, UpdateGoalRequest, UpdateTaskRequest,
};
use crate::server::auth::{
ApiKey, ApiKeyInfoResponse, CreateApiKeyRequest, CreateApiKeyResponse,
RefreshApiKeyRequest, RefreshApiKeyResponse, RevokeApiKeyResponse,
};
-use crate::server::handlers::{api_keys, contract_chat, contract_discuss, contracts, files, listen, mesh, mesh_chat, mesh_merge, repository_history, users};
+use crate::server::handlers::{api_keys, contract_chat, contract_discuss, contracts, directives, files, listen, mesh, mesh_chat, mesh_merge, repository_history, users};
use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage, TranscriptMessage};
#[derive(OpenApi)]
@@ -103,6 +105,23 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage
contract_chat::clear_contract_chat_history,
// Contract discuss endpoint
contract_discuss::discuss_contract_handler,
+ // Directive endpoints
+ directives::list_directives,
+ directives::create_directive,
+ directives::get_directive,
+ directives::update_directive,
+ directives::delete_directive,
+ directives::create_step,
+ directives::batch_create_steps,
+ directives::update_step,
+ directives::delete_step,
+ directives::start_directive,
+ directives::pause_directive,
+ directives::advance_directive,
+ directives::complete_step,
+ directives::fail_step,
+ directives::skip_step,
+ directives::update_goal,
// Repository history/settings endpoints
repository_history::list_repository_history,
repository_history::get_repository_suggestions,
@@ -187,6 +206,17 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage
AddLocalRepositoryRequest,
CreateManagedRepositoryRequest,
ChangePhaseRequest,
+ // Directive schemas
+ Directive,
+ DirectiveStep,
+ DirectiveWithSteps,
+ DirectiveSummary,
+ DirectiveListResponse,
+ CreateDirectiveRequest,
+ UpdateDirectiveRequest,
+ UpdateGoalRequest,
+ CreateDirectiveStepRequest,
+ UpdateDirectiveStepRequest,
// Repository history schemas
RepositoryHistoryEntry,
RepositoryHistoryListResponse,
@@ -200,6 +230,7 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage
(name = "Contracts", description = "Contract management with workflow phases"),
(name = "API Keys", description = "API key management for programmatic access"),
(name = "Users", description = "User account management"),
+ (name = "Directives", description = "Directive management with DAG-based step progression"),
(name = "Settings", description = "User settings including repository history"),
)
)]