summaryrefslogtreecommitdiff
path: root/makima
diff options
context:
space:
mode:
Diffstat (limited to 'makima')
-rw-r--r--makima/frontend/src/components/PhaseConfirmationNotification.tsx141
-rw-r--r--makima/frontend/src/components/contracts/AutopilotPanel.tsx50
-rw-r--r--makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx411
-rw-r--r--makima/frontend/src/components/mesh/TaskOutput.tsx66
-rw-r--r--makima/frontend/src/lib/api.ts21
-rw-r--r--makima/frontend/src/main.tsx2
-rw-r--r--makima/migrations/20250119000000_add_phase_guard.sql9
-rw-r--r--makima/src/db/models.rs60
-rw-r--r--makima/src/db/repository.rs15
-rw-r--r--makima/src/llm/contract_tools.rs34
-rw-r--r--makima/src/server/handlers/contract_chat.rs87
-rw-r--r--makima/src/server/handlers/contracts.rs112
-rw-r--r--makima/src/server/handlers/mesh.rs1
-rw-r--r--makima/src/server/handlers/transcript_analysis.rs1
14 files changed, 988 insertions, 22 deletions
diff --git a/makima/frontend/src/components/PhaseConfirmationNotification.tsx b/makima/frontend/src/components/PhaseConfirmationNotification.tsx
new file mode 100644
index 0000000..516211f
--- /dev/null
+++ b/makima/frontend/src/components/PhaseConfirmationNotification.tsx
@@ -0,0 +1,141 @@
+import { useNavigate } from "react-router";
+import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext";
+import { PhaseConfirmationModal, type PhaseConfirmationData } from "./contracts/PhaseConfirmationModal";
+import type { PendingQuestion } from "../lib/api";
+
+/**
+ * Notification component for phase confirmation requests.
+ * Shows a modal when there are pending phase_confirmation type questions.
+ * Uses the same question infrastructure as supervisor questions.
+ */
+export function PhaseConfirmationNotification() {
+ const { notificationQuestions, submitAnswer, dismissNotification } =
+ useSupervisorQuestions();
+
+ // Filter for phase_confirmation type questions
+ const phaseConfirmationQuestions = notificationQuestions.filter(
+ (q) => q.questionType === "phase_confirmation"
+ );
+
+ if (phaseConfirmationQuestions.length === 0) {
+ return null;
+ }
+
+ // Show the first phase confirmation question as a modal
+ const question = phaseConfirmationQuestions[0];
+
+ // Build phase confirmation data from the question
+ const data: PhaseConfirmationData = {
+ questionId: question.questionId,
+ contractId: question.contractId,
+ contractName: question.phaseConfirmation?.contractName,
+ currentPhase: question.phaseConfirmation?.currentPhase || "research",
+ nextPhase: question.phaseConfirmation?.nextPhase || "specify",
+ summary: question.phaseConfirmation?.summary,
+ deliverables: question.phaseConfirmation?.deliverables,
+ };
+
+ const handleApprove = async (questionId: string) => {
+ const success = await submitAnswer(questionId, "APPROVE");
+ if (success) {
+ dismissNotification(questionId);
+ }
+ };
+
+ const handleRequestChanges = async (questionId: string, feedback: string) => {
+ const success = await submitAnswer(
+ questionId,
+ `CHANGES_REQUESTED: ${feedback}`
+ );
+ if (success) {
+ dismissNotification(questionId);
+ }
+ };
+
+ const handleDismiss = () => {
+ // Dismiss to notification (user can still respond via task output)
+ dismissNotification(question.questionId);
+ };
+
+ return (
+ <PhaseConfirmationModal
+ data={data}
+ onApprove={handleApprove}
+ onRequestChanges={handleRequestChanges}
+ onDismiss={handleDismiss}
+ />
+ );
+}
+
+/**
+ * Alternative: Notification toast-style for phase confirmations
+ * Shows as a small notification in the corner (like regular supervisor questions)
+ */
+export function PhaseConfirmationToast() {
+ const navigate = useNavigate();
+ const { notificationQuestions, dismissNotification } = useSupervisorQuestions();
+
+ // Filter for phase_confirmation type questions
+ const phaseConfirmationQuestions = notificationQuestions.filter(
+ (q) => q.questionType === "phase_confirmation"
+ );
+
+ if (phaseConfirmationQuestions.length === 0) {
+ return null;
+ }
+
+ const handleGoToTask = (question: PendingQuestion) => {
+ dismissNotification(question.questionId);
+ navigate(`/mesh/${question.taskId}`);
+ };
+
+ return (
+ <div className="fixed bottom-4 right-4 z-50 max-w-md space-y-2">
+ {phaseConfirmationQuestions.map((question) => (
+ <div
+ key={question.questionId}
+ className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.5)] rounded-lg shadow-lg overflow-hidden"
+ >
+ {/* Header */}
+ <div className="flex items-center justify-between px-4 py-3 bg-[rgba(117,170,252,0.1)]">
+ <div className="flex items-center gap-2">
+ <span className="text-[#75aafc] text-lg">?</span>
+ <span className="font-mono text-sm text-[#9bc3ff] uppercase">
+ Phase Transition
+ </span>
+ </div>
+ <button
+ onClick={() => handleGoToTask(question)}
+ className="px-3 py-1 font-mono text-xs text-[#75aafc] border border-[rgba(117,170,252,0.3)] hover:border-[rgba(117,170,252,0.5)] hover:bg-[rgba(117,170,252,0.1)] transition-colors uppercase"
+ >
+ Review
+ </button>
+ </div>
+
+ {/* Content preview */}
+ <div className="px-4 py-3">
+ {question.phaseConfirmation && (
+ <div className="flex items-center gap-2 mb-2">
+ <span className="font-mono text-xs text-purple-400">
+ {question.phaseConfirmation.currentPhase}
+ </span>
+ <span className="text-[#555] font-mono text-xs">&rarr;</span>
+ <span className="font-mono text-xs text-green-400">
+ {question.phaseConfirmation.nextPhase}
+ </span>
+ </div>
+ )}
+ <p className="text-sm text-[#dbe7ff] font-mono line-clamp-2">
+ {question.question}
+ </p>
+ {question.phaseConfirmation?.contractName && (
+ <p className="text-xs text-[#555] font-mono mt-1">
+ Contract: {question.phaseConfirmation.contractName}
+ </p>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/contracts/AutopilotPanel.tsx b/makima/frontend/src/components/contracts/AutopilotPanel.tsx
index a8a8e2e..cf42e44 100644
--- a/makima/frontend/src/components/contracts/AutopilotPanel.tsx
+++ b/makima/frontend/src/components/contracts/AutopilotPanel.tsx
@@ -5,6 +5,7 @@ import {
startSupervisor,
stopSupervisor,
resumeSupervisor,
+ updateContract,
type SupervisorStatus,
} from "../../lib/api";
@@ -111,6 +112,23 @@ export function AutopilotPanel({ contract, onUpdate }: AutopilotPanelProps) {
}
}, [contract.id, supervisorStatus.supervisorTaskId, onUpdate]);
+ const handlePhaseGuardChange = useCallback(async (enabled: boolean) => {
+ setLoading(true);
+ setError(null);
+
+ try {
+ await updateContract(contract.id, {
+ phaseGuard: enabled,
+ version: contract.version,
+ });
+ onUpdate();
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to update phase guard setting");
+ } finally {
+ setLoading(false);
+ }
+ }, [contract.id, contract.version, onUpdate]);
+
// Don't show panel for task-type contracts (they don't have supervisors)
if (contract.contractType === "task") {
return null;
@@ -185,6 +203,38 @@ export function AutopilotPanel({ contract, onUpdate }: AutopilotPanelProps) {
)}
</div>
+ {/* Phase Guard Toggle */}
+ <div className="pt-3 border-t border-dashed border-[rgba(117,170,252,0.2)]">
+ <label className="flex items-start gap-3 cursor-pointer group">
+ <div className="relative mt-0.5">
+ <input
+ type="checkbox"
+ checked={contract.phaseGuard ?? false}
+ onChange={(e) => handlePhaseGuardChange(e.target.checked)}
+ disabled={loading}
+ className="sr-only peer"
+ />
+ <div className="w-9 h-5 bg-[rgba(117,170,252,0.1)] border border-[rgba(117,170,252,0.3)] rounded-full peer-checked:bg-[rgba(117,170,252,0.3)] transition-colors peer-disabled:opacity-50" />
+ <div className="absolute left-0.5 top-0.5 w-4 h-4 bg-[#555] rounded-full transition-transform peer-checked:translate-x-4 peer-checked:bg-[#75aafc] peer-disabled:opacity-50" />
+ </div>
+ <div className="flex-1">
+ <div className="flex items-center gap-2">
+ <span className="font-mono text-sm text-[#dbe7ff] group-hover:text-white transition-colors">
+ Phase Guard
+ </span>
+ {contract.phaseGuard && (
+ <span className="px-1.5 py-0.5 text-[9px] font-mono uppercase bg-yellow-500/20 text-yellow-400 border border-yellow-400/30 rounded">
+ active
+ </span>
+ )}
+ </div>
+ <div className="font-mono text-xs text-[#555] mt-0.5">
+ Ask for confirmation before advancing to the next phase
+ </div>
+ </div>
+ </label>
+ </div>
+
{/* Show running indicator when active */}
{supervisorStatus.status === "running" && (
<div className="flex items-center gap-2 pt-2 border-t border-dashed border-[rgba(117,170,252,0.2)]">
diff --git a/makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx b/makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx
new file mode 100644
index 0000000..96cc1d4
--- /dev/null
+++ b/makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx
@@ -0,0 +1,411 @@
+import { useState } from "react";
+import type { ContractPhase } from "../../lib/api";
+
+/** Phase configuration for styling */
+const phaseConfig: Record<
+ ContractPhase,
+ { label: string; color: string; borderColor: string }
+> = {
+ research: {
+ label: "Research",
+ color: "text-purple-400",
+ borderColor: "border-purple-400/50",
+ },
+ specify: {
+ label: "Specify",
+ color: "text-blue-400",
+ borderColor: "border-blue-400/50",
+ },
+ plan: {
+ label: "Plan",
+ color: "text-cyan-400",
+ borderColor: "border-cyan-400/50",
+ },
+ execute: {
+ label: "Execute",
+ color: "text-green-400",
+ borderColor: "border-green-400/50",
+ },
+ review: {
+ label: "Review",
+ color: "text-yellow-400",
+ borderColor: "border-yellow-400/50",
+ },
+};
+
+export interface PhaseConfirmationData {
+ questionId: string;
+ contractId: string;
+ contractName?: string;
+ currentPhase: ContractPhase;
+ nextPhase: ContractPhase;
+ summary?: string;
+ deliverables?: Array<{
+ name: string;
+ completed: boolean;
+ }>;
+}
+
+interface PhaseConfirmationModalProps {
+ data: PhaseConfirmationData;
+ onApprove: (questionId: string) => Promise<void>;
+ onRequestChanges: (questionId: string, feedback: string) => Promise<void>;
+ onDismiss?: () => void;
+}
+
+export function PhaseConfirmationModal({
+ data,
+ onApprove,
+ onRequestChanges,
+ onDismiss,
+}: PhaseConfirmationModalProps) {
+ const [mode, setMode] = useState<"choice" | "feedback">("choice");
+ const [feedback, setFeedback] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+
+ const currentConfig = phaseConfig[data.currentPhase];
+ const nextConfig = phaseConfig[data.nextPhase];
+
+ const handleApprove = async () => {
+ setSubmitting(true);
+ setError(null);
+ try {
+ await onApprove(data.questionId);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to approve");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleRequestChanges = () => {
+ setMode("feedback");
+ };
+
+ const handleSubmitFeedback = async () => {
+ if (!feedback.trim()) return;
+ setSubmitting(true);
+ setError(null);
+ try {
+ await onRequestChanges(data.questionId, feedback.trim());
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to submit feedback");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleBack = () => {
+ setMode("choice");
+ setFeedback("");
+ setError(null);
+ };
+
+ return (
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm">
+ <div className="w-full max-w-lg mx-4 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-2xl">
+ {/* Header */}
+ <div className="flex items-center justify-between p-4 border-b border-[rgba(117,170,252,0.2)]">
+ <div className="flex items-center gap-2">
+ <span className="text-amber-400 text-lg">?</span>
+ <h2 className="font-mono text-sm text-[#75aafc] uppercase tracking-wide">
+ Phase Transition Confirmation
+ </h2>
+ </div>
+ {onDismiss && (
+ <button
+ onClick={onDismiss}
+ className="text-[#555] hover:text-[#9bc3ff] transition-colors"
+ aria-label="Dismiss"
+ >
+ <span className="text-xl">&times;</span>
+ </button>
+ )}
+ </div>
+
+ {/* Content */}
+ <div className="p-4 space-y-4">
+ {/* Contract name */}
+ {data.contractName && (
+ <p className="font-mono text-sm text-[#9bc3ff]">
+ Contract: <span className="text-[#dbe7ff]">{data.contractName}</span>
+ </p>
+ )}
+
+ {/* Phase transition indicator */}
+ <div className="flex items-center justify-center gap-3 py-4">
+ <div
+ className={`px-4 py-2 border ${currentConfig.borderColor} ${currentConfig.color} font-mono text-sm uppercase`}
+ >
+ {currentConfig.label}
+ </div>
+ <div className="text-[#555] font-mono">&rarr;</div>
+ <div
+ className={`px-4 py-2 border ${nextConfig.borderColor} ${nextConfig.color} font-mono text-sm uppercase`}
+ >
+ {nextConfig.label}
+ </div>
+ </div>
+
+ {/* Summary */}
+ {data.summary && (
+ <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.15)] p-3">
+ <p className="font-mono text-xs text-[#9bc3ff] mb-2 uppercase">
+ Phase Summary
+ </p>
+ <p className="font-mono text-sm text-[#dbe7ff] whitespace-pre-wrap">
+ {data.summary}
+ </p>
+ </div>
+ )}
+
+ {/* Deliverables checklist */}
+ {data.deliverables && data.deliverables.length > 0 && (
+ <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.15)] p-3">
+ <p className="font-mono text-xs text-[#9bc3ff] mb-2 uppercase">
+ Phase Deliverables
+ </p>
+ <ul className="space-y-1">
+ {data.deliverables.map((d, idx) => (
+ <li key={idx} className="flex items-center gap-2">
+ <span
+ className={
+ d.completed ? "text-green-400" : "text-[#555]"
+ }
+ >
+ {d.completed ? "+" : "-"}
+ </span>
+ <span
+ className={`font-mono text-sm ${
+ d.completed ? "text-[#9bc3ff]" : "text-[#555]"
+ }`}
+ >
+ {d.name}
+ </span>
+ </li>
+ ))}
+ </ul>
+ </div>
+ )}
+
+ {/* Error message */}
+ {error && (
+ <div className="bg-red-900/20 border border-red-500/30 p-3 text-red-400 font-mono text-sm">
+ {error}
+ </div>
+ )}
+
+ {/* Choice mode */}
+ {mode === "choice" && (
+ <div className="space-y-3 pt-4 border-t border-[rgba(117,170,252,0.2)]">
+ <p className="font-mono text-sm text-[#dbe7ff]">
+ Ready to advance to the {nextConfig.label.toLowerCase()} phase?
+ </p>
+ <div className="flex gap-3">
+ <button
+ onClick={handleApprove}
+ disabled={submitting}
+ className="flex-1 px-4 py-2.5 font-mono text-sm text-[#dbe7ff] bg-green-700/30 border border-green-400/50 hover:bg-green-700/40 hover:border-green-400/70 disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
+ >
+ {submitting ? "Advancing..." : "Approve & Advance"}
+ </button>
+ <button
+ onClick={handleRequestChanges}
+ disabled={submitting}
+ className="flex-1 px-4 py-2.5 font-mono text-sm text-amber-300 border border-amber-500/50 hover:border-amber-400/70 hover:bg-amber-900/20 disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
+ >
+ Request Changes
+ </button>
+ </div>
+ </div>
+ )}
+
+ {/* Feedback mode */}
+ {mode === "feedback" && (
+ <div className="space-y-3 pt-4 border-t border-[rgba(117,170,252,0.2)]">
+ <div>
+ <label className="block font-mono text-xs text-[#9bc3ff] uppercase mb-2">
+ What changes are needed before advancing?
+ </label>
+ <textarea
+ value={feedback}
+ onChange={(e) => setFeedback(e.target.value)}
+ placeholder="Describe what needs to be changed or completed..."
+ rows={4}
+ className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none"
+ autoFocus
+ />
+ </div>
+ <div className="flex gap-3">
+ <button
+ onClick={handleBack}
+ disabled={submitting}
+ className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors uppercase"
+ >
+ &larr; Back
+ </button>
+ <button
+ onClick={handleSubmitFeedback}
+ disabled={submitting || !feedback.trim()}
+ className="flex-1 px-4 py-2 font-mono text-sm text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
+ >
+ {submitting ? "Submitting..." : "Submit Feedback"}
+ </button>
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}
+
+/** Inline variant for task output view */
+interface PhaseConfirmationInlineProps {
+ data: PhaseConfirmationData;
+ isPending: boolean;
+ onApprove: (questionId: string) => Promise<void>;
+ onRequestChanges: (questionId: string, feedback: string) => Promise<void>;
+}
+
+export function PhaseConfirmationInline({
+ data,
+ isPending,
+ onApprove,
+ onRequestChanges,
+}: PhaseConfirmationInlineProps) {
+ const [mode, setMode] = useState<"choice" | "feedback">("choice");
+ const [feedback, setFeedback] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+
+ const currentConfig = phaseConfig[data.currentPhase];
+ const nextConfig = phaseConfig[data.nextPhase];
+
+ const handleApprove = async () => {
+ setSubmitting(true);
+ try {
+ await onApprove(data.questionId);
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleSubmitFeedback = async () => {
+ if (!feedback.trim()) return;
+ setSubmitting(true);
+ try {
+ await onRequestChanges(data.questionId, feedback.trim());
+ setFeedback("");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+ <div className="bg-[#0f1825] border border-[rgba(117,170,252,0.4)] rounded p-3 my-2">
+ <div className="flex items-center gap-2 mb-3">
+ <span className="text-[#75aafc] text-lg">?</span>
+ <span className="font-mono text-sm text-[#75aafc] uppercase">
+ Phase Transition
+ </span>
+ {!isPending && (
+ <span className="text-green-400 text-xs font-mono">(Responded)</span>
+ )}
+ </div>
+
+ {/* Phase transition indicator */}
+ <div className="flex items-center gap-2 mb-3">
+ <span className={`font-mono text-sm ${currentConfig.color}`}>
+ {currentConfig.label}
+ </span>
+ <span className="text-[#555] font-mono">&rarr;</span>
+ <span className={`font-mono text-sm ${nextConfig.color}`}>
+ {nextConfig.label}
+ </span>
+ </div>
+
+ {/* Summary */}
+ {data.summary && (
+ <p className="font-mono text-xs text-[#9bc3ff] mb-3 whitespace-pre-wrap">
+ {data.summary}
+ </p>
+ )}
+
+ {/* Deliverables */}
+ {data.deliverables && data.deliverables.length > 0 && (
+ <div className="mb-3">
+ <p className="font-mono text-[10px] text-[#555] uppercase mb-1">
+ Deliverables
+ </p>
+ <div className="flex flex-wrap gap-2">
+ {data.deliverables.map((d, idx) => (
+ <span
+ key={idx}
+ className={`px-2 py-0.5 font-mono text-xs border ${
+ d.completed
+ ? "border-green-400/30 text-green-400"
+ : "border-[#555] text-[#555]"
+ }`}
+ >
+ {d.completed ? "+" : "-"} {d.name}
+ </span>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {isPending && (
+ <>
+ {mode === "choice" && (
+ <div className="flex flex-wrap gap-2">
+ <button
+ onClick={handleApprove}
+ disabled={submitting}
+ className="px-3 py-1.5 font-mono text-xs bg-green-700/30 border border-green-400/50 text-green-300 hover:bg-green-700/40 disabled:opacity-50 transition-colors"
+ >
+ {submitting ? "..." : "Approve & Advance"}
+ </button>
+ <button
+ onClick={() => setMode("feedback")}
+ disabled={submitting}
+ className="px-3 py-1.5 font-mono text-xs border border-amber-500/50 text-amber-300 hover:bg-amber-900/20 disabled:opacity-50 transition-colors"
+ >
+ Request Changes
+ </button>
+ </div>
+ )}
+
+ {mode === "feedback" && (
+ <div className="space-y-2">
+ <textarea
+ value={feedback}
+ onChange={(e) => setFeedback(e.target.value)}
+ placeholder="Describe what changes are needed..."
+ rows={2}
+ className="w-full px-2 py-1.5 bg-[#0a1525] border border-[rgba(117,170,252,0.3)] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc] resize-none"
+ autoFocus
+ />
+ <div className="flex gap-2">
+ <button
+ onClick={() => setMode("choice")}
+ disabled={submitting}
+ className="px-2 py-1 font-mono text-[10px] text-[#555] hover:text-[#9bc3ff] transition-colors"
+ >
+ Cancel
+ </button>
+ <button
+ onClick={handleSubmitFeedback}
+ disabled={submitting || !feedback.trim()}
+ className="px-3 py-1 font-mono text-xs bg-[#0f3c78] border border-[#3f6fb3] text-[#dbe7ff] hover:bg-[#153667] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
+ >
+ {submitting ? "..." : "Submit"}
+ </button>
+ </div>
+ </div>
+ )}
+ </>
+ )}
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/mesh/TaskOutput.tsx b/makima/frontend/src/components/mesh/TaskOutput.tsx
index d53429d..7140c8a 100644
--- a/makima/frontend/src/components/mesh/TaskOutput.tsx
+++ b/makima/frontend/src/components/mesh/TaskOutput.tsx
@@ -2,6 +2,11 @@ import { useRef, useEffect, useState, useCallback } from "react";
import { SimpleMarkdown } from "../SimpleMarkdown";
import type { TaskOutputEvent } from "../../hooks/useTaskSubscription";
import { sendTaskMessage } from "../../lib/api";
+import {
+ PhaseConfirmationInline,
+ type PhaseConfirmationData,
+} from "../contracts/PhaseConfirmationModal";
+import type { ContractPhase } from "../../lib/api";
interface TaskOutputProps {
/** Array of parsed output events from the backend */
@@ -312,6 +317,15 @@ function OutputEntryRenderer({ entry, pendingQuestionIds, onAnswerQuestion }: Ou
/>
);
+ case "phase_confirmation":
+ return (
+ <PhaseConfirmationEntry
+ entry={entry}
+ pendingQuestionIds={pendingQuestionIds}
+ onAnswerQuestion={onAnswerQuestion}
+ />
+ );
+
default:
return null;
}
@@ -520,3 +534,55 @@ function AuthRequiredEntry({ entry }: { entry: TaskOutputEvent }) {
</div>
);
}
+
+/** Entry for phase transition confirmations */
+function PhaseConfirmationEntry({
+ entry,
+ pendingQuestionIds,
+ onAnswerQuestion,
+}: {
+ entry: TaskOutputEvent;
+ pendingQuestionIds?: Set<string>;
+ onAnswerQuestion?: (questionId: string, response: string) => Promise<void>;
+}) {
+ const questionId = entry.toolInput?.question_id as string;
+ const currentPhase = entry.toolInput?.current_phase as ContractPhase;
+ const nextPhase = entry.toolInput?.next_phase as ContractPhase;
+ const contractId = entry.toolInput?.contract_id as string;
+ const contractName = entry.toolInput?.contract_name as string | undefined;
+ const summary = entry.toolInput?.summary as string | undefined;
+ const deliverables = entry.toolInput?.deliverables as
+ | Array<{ name: string; completed: boolean }>
+ | undefined;
+
+ const isPending = pendingQuestionIds?.has(questionId) ?? false;
+
+ const data: PhaseConfirmationData = {
+ questionId,
+ contractId,
+ contractName,
+ currentPhase,
+ nextPhase,
+ summary,
+ deliverables,
+ };
+
+ const handleApprove = async (qId: string) => {
+ if (!onAnswerQuestion) return;
+ await onAnswerQuestion(qId, "APPROVE");
+ };
+
+ const handleRequestChanges = async (qId: string, feedback: string) => {
+ if (!onAnswerQuestion) return;
+ await onAnswerQuestion(qId, `CHANGES_REQUESTED: ${feedback}`);
+ };
+
+ return (
+ <PhaseConfirmationInline
+ data={data}
+ isPending={isPending}
+ onApprove={handleApprove}
+ onRequestChanges={handleRequestChanges}
+ />
+ );
+}
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index ee04935..9b725bc 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -1478,6 +1478,10 @@ export interface Contract {
status: ContractStatus;
/** Supervisor task ID for contract orchestration */
supervisorTaskId: string | null;
+ /** Whether tasks for this contract should run in autonomous loop mode */
+ autonomousLoop: boolean;
+ /** Whether to wait for user confirmation before progressing to the next phase */
+ phaseGuard: boolean;
version: number;
createdAt: string;
updatedAt: string;
@@ -1518,6 +1522,10 @@ export interface UpdateContractRequest {
description?: string;
phase?: ContractPhase;
status?: ContractStatus;
+ /** Enable or disable autonomous loop mode */
+ autonomousLoop?: boolean;
+ /** Enable or disable phase guard mode */
+ phaseGuard?: boolean;
version?: number;
}
@@ -1938,6 +1946,19 @@ export interface PendingQuestion {
choices: string[];
context: string | null;
createdAt: string;
+ /** Question type - "general" for regular questions, "phase_confirmation" for phase transitions */
+ questionType?: "general" | "phase_confirmation";
+ /** Phase confirmation specific data (when questionType is "phase_confirmation") */
+ phaseConfirmation?: {
+ currentPhase: ContractPhase;
+ nextPhase: ContractPhase;
+ contractName?: string;
+ summary?: string;
+ deliverables?: Array<{
+ name: string;
+ completed: boolean;
+ }>;
+ };
}
export interface AnswerQuestionRequest {
diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx
index 5fd6a4e..19f02d1 100644
--- a/makima/frontend/src/main.tsx
+++ b/makima/frontend/src/main.tsx
@@ -6,6 +6,7 @@ import { AuthProvider } from "./contexts/AuthContext";
import { SupervisorQuestionsProvider } from "./contexts/SupervisorQuestionsContext";
import { GridOverlay } from "./components/GridOverlay";
import { SupervisorQuestionNotification } from "./components/SupervisorQuestionNotification";
+import { PhaseConfirmationNotification } from "./components/PhaseConfirmationNotification";
import { ProtectedRoute } from "./components/ProtectedRoute";
import HomePage from "./routes/_index";
import ListenPage from "./routes/listen";
@@ -24,6 +25,7 @@ createRoot(document.getElementById("root")!).render(
<BrowserRouter>
<GridOverlay />
<SupervisorQuestionNotification />
+ <PhaseConfirmationNotification />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
diff --git a/makima/migrations/20250119000000_add_phase_guard.sql b/makima/migrations/20250119000000_add_phase_guard.sql
new file mode 100644
index 0000000..dfa642a
--- /dev/null
+++ b/makima/migrations/20250119000000_add_phase_guard.sql
@@ -0,0 +1,9 @@
+-- Add phase_guard column to contracts table
+-- When enabled, the supervisor will wait for user confirmation before progressing to the next phase.
+-- This allows users to review and potentially amend phase outputs (plans, requirements, etc.)
+-- before the contract continues to the next phase.
+
+ALTER TABLE contracts
+ADD COLUMN IF NOT EXISTS phase_guard BOOLEAN NOT NULL DEFAULT FALSE;
+
+COMMENT ON COLUMN contracts.phase_guard IS 'Whether to wait for user confirmation before progressing to the next phase';
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index 72ba6f2..99c8b8e 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -1264,6 +1264,11 @@ pub struct Contract {
/// without a COMPLETION_GATE indicating ready: true.
#[serde(default)]
pub autonomous_loop: bool,
+ /// Whether to wait for user confirmation before progressing to the next phase.
+ /// When enabled, the supervisor will pause and ask the user to review and approve
+ /// phase outputs (like plans, requirements, etc.) before continuing.
+ #[serde(default)]
+ pub phase_guard: bool,
pub version: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
@@ -1389,6 +1394,11 @@ pub struct CreateContractRequest {
/// without a COMPLETION_GATE indicating ready: true.
#[serde(default)]
pub autonomous_loop: Option<bool>,
+ /// Enable phase guard mode for this contract.
+ /// When enabled, the supervisor will pause and ask the user to review and approve
+ /// phase outputs before progressing to the next phase.
+ #[serde(default)]
+ pub phase_guard: Option<bool>,
}
/// Request payload for updating a contract
@@ -1405,6 +1415,11 @@ pub struct UpdateContractRequest {
/// Enable or disable autonomous loop mode for tasks in this contract.
#[serde(default)]
pub autonomous_loop: Option<bool>,
+ /// Enable or disable phase guard mode for this contract.
+ /// When enabled, the supervisor will pause and ask the user to review and approve
+ /// phase outputs before progressing to the next phase.
+ #[serde(default)]
+ pub phase_guard: Option<bool>,
/// Version for optimistic locking
pub version: Option<i32>,
}
@@ -1443,6 +1458,51 @@ pub struct CreateManagedRepositoryRequest {
#[serde(rename_all = "camelCase")]
pub struct ChangePhaseRequest {
pub phase: String,
+ /// If phase_guard is enabled, this must be true to confirm the transition.
+ /// If not provided or false, returns phase deliverables for review.
+ #[serde(default)]
+ pub confirmed: Option<bool>,
+ /// User feedback for changes (used when not confirming)
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub feedback: Option<String>,
+}
+
+/// Response for phase transition when phase_guard is enabled
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct PhaseTransitionRequest {
+ /// Current contract phase
+ pub current_phase: String,
+ /// Requested next phase
+ pub next_phase: String,
+ /// Summary of phase deliverables/outputs
+ pub deliverables_summary: String,
+ /// List of files created in this phase
+ pub phase_files: Vec<PhaseFileInfo>,
+ /// List of completed tasks in this phase
+ pub phase_tasks: Vec<PhaseTaskInfo>,
+ /// Whether user confirmation is required
+ pub requires_confirmation: bool,
+ /// Unique ID for tracking this transition request
+ pub transition_id: String,
+}
+
+/// File info for phase transition review
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct PhaseFileInfo {
+ pub id: Uuid,
+ pub name: String,
+ pub description: Option<String>,
+}
+
+/// Task info for phase transition review
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct PhaseTaskInfo {
+ pub id: Uuid,
+ pub name: String,
+ pub status: String,
}
/// Contract event record from the database
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 43b8e3a..3d1efd1 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -2136,11 +2136,12 @@ pub async fn create_contract_for_owner(
}
let autonomous_loop = req.autonomous_loop.unwrap_or(false);
+ let phase_guard = req.phase_guard.unwrap_or(false);
sqlx::query_as::<_, Contract>(
r#"
- INSERT INTO contracts (owner_id, name, description, contract_type, phase, autonomous_loop)
- VALUES ($1, $2, $3, $4, $5, $6)
+ INSERT INTO contracts (owner_id, name, description, contract_type, phase, autonomous_loop, phase_guard)
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *
"#,
)
@@ -2150,6 +2151,7 @@ pub async fn create_contract_for_owner(
.bind(contract_type)
.bind(phase)
.bind(autonomous_loop)
+ .bind(phase_guard)
.fetch_one(pool)
.await
}
@@ -2249,14 +2251,15 @@ pub async fn update_contract_for_owner(
let status = req.status.unwrap_or(existing.status);
let supervisor_task_id = req.supervisor_task_id.or(existing.supervisor_task_id);
let autonomous_loop = req.autonomous_loop.unwrap_or(existing.autonomous_loop);
+ let phase_guard = req.phase_guard.unwrap_or(existing.phase_guard);
let result = if req.version.is_some() {
sqlx::query_as::<_, Contract>(
r#"
UPDATE contracts
SET name = $3, description = $4, phase = $5, status = $6,
- supervisor_task_id = $7, autonomous_loop = $8, version = version + 1, updated_at = NOW()
- WHERE id = $1 AND owner_id = $2 AND version = $9
+ supervisor_task_id = $7, autonomous_loop = $8, phase_guard = $9, version = version + 1, updated_at = NOW()
+ WHERE id = $1 AND owner_id = $2 AND version = $10
RETURNING *
"#,
)
@@ -2268,6 +2271,7 @@ pub async fn update_contract_for_owner(
.bind(&status)
.bind(supervisor_task_id)
.bind(autonomous_loop)
+ .bind(phase_guard)
.bind(req.version.unwrap())
.fetch_optional(pool)
.await?
@@ -2276,7 +2280,7 @@ pub async fn update_contract_for_owner(
r#"
UPDATE contracts
SET name = $3, description = $4, phase = $5, status = $6,
- supervisor_task_id = $7, autonomous_loop = $8, version = version + 1, updated_at = NOW()
+ supervisor_task_id = $7, autonomous_loop = $8, phase_guard = $9, version = version + 1, updated_at = NOW()
WHERE id = $1 AND owner_id = $2
RETURNING *
"#,
@@ -2289,6 +2293,7 @@ pub async fn update_contract_for_owner(
.bind(&status)
.bind(supervisor_task_id)
.bind(autonomous_loop)
+ .bind(phase_guard)
.fetch_optional(pool)
.await?
};
diff --git a/makima/src/llm/contract_tools.rs b/makima/src/llm/contract_tools.rs
index 7a3d09a..07de1fe 100644
--- a/makima/src/llm/contract_tools.rs
+++ b/makima/src/llm/contract_tools.rs
@@ -203,7 +203,7 @@ pub static CONTRACT_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::L
},
Tool {
name: "advance_phase".to_string(),
- description: "Advance the contract to the NEXT phase in sequence. Phases progress: research -> specify -> plan -> execute -> review. You can ONLY advance forward one step. Always use suggest_phase_transition first to check readiness and find the correct next phase.".to_string(),
+ description: "Advance the contract to the NEXT phase in sequence. Phases progress: research -> specify -> plan -> execute -> review. You can ONLY advance forward one step. Always use suggest_phase_transition first to check readiness and find the correct next phase. If the contract has phase_guard enabled, this will first return a pending_confirmation status with phase deliverables for user review. Call again with confirmed=true to complete the transition, or with feedback to request changes.".to_string(),
parameters: json!({
"type": "object",
"properties": {
@@ -211,6 +211,14 @@ pub static CONTRACT_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::L
"type": "string",
"enum": ["specify", "plan", "execute", "review"],
"description": "The next phase to transition to. Must be exactly one step ahead of current phase (e.g., research->specify, specify->plan, plan->execute, execute->review)"
+ },
+ "confirmed": {
+ "type": "boolean",
+ "description": "Set to true to confirm the phase transition when phase_guard is enabled. If omitted or false, returns deliverables for review."
+ },
+ "feedback": {
+ "type": "string",
+ "description": "User feedback when requesting changes instead of confirming the transition. The feedback will be passed back to the task to address."
}
},
"required": ["new_phase"]
@@ -500,7 +508,13 @@ pub enum ContractToolRequest {
// Phase management
GetPhaseInfo,
SuggestPhaseTransition,
- AdvancePhase { new_phase: String },
+ AdvancePhase {
+ new_phase: String,
+ /// Whether the user has confirmed the phase transition (for phase_guard)
+ confirmed: bool,
+ /// User feedback when they request changes instead of confirming
+ feedback: Option<String>,
+ },
// Repository management
ListDaemonDirectories,
@@ -870,12 +884,28 @@ fn parse_advance_phase(call: &super::tools::ToolCall) -> ContractToolExecutionRe
return error_result("Invalid phase. Must be one of: research, specify, plan, execute, review");
}
+ // Parse optional confirmed flag (defaults to false for initial phase_guard check)
+ let confirmed = call
+ .arguments
+ .get("confirmed")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false);
+
+ // Parse optional feedback (for when user requests changes)
+ let feedback = call
+ .arguments
+ .get("feedback")
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string());
+
ContractToolExecutionResult {
success: true,
message: format!("Advancing to '{}' phase...", new_phase),
data: None,
request: Some(ContractToolRequest::AdvancePhase {
new_phase: new_phase.to_string(),
+ confirmed,
+ feedback,
}),
pending_questions: None,
}
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs
index 101b257..29ec620 100644
--- a/makima/src/server/handlers/contract_chat.rs
+++ b/makima/src/server/handlers/contract_chat.rs
@@ -1689,7 +1689,7 @@ async fn handle_contract_request(
}
}
- ContractToolRequest::AdvancePhase { new_phase } => {
+ ContractToolRequest::AdvancePhase { new_phase, confirmed, feedback } => {
let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
Ok(Some(c)) => c,
Ok(None) => {
@@ -1723,7 +1723,88 @@ async fn handle_contract_request(
};
}
- // Update phase
+ // Check if phase_guard is enabled
+ if contract.phase_guard {
+ // If user provided feedback, return it for the task to address
+ if let Some(ref user_feedback) = feedback {
+ return ContractRequestResult {
+ success: true,
+ message: format!(
+ "Phase transition to '{}' requires changes. User feedback: {}",
+ new_phase, user_feedback
+ ),
+ data: Some(json!({
+ "status": "changes_requested",
+ "currentPhase": current_phase,
+ "requestedPhase": new_phase,
+ "feedback": user_feedback,
+ "action": "Address the user feedback and try again when ready"
+ })),
+ };
+ }
+
+ // If not confirmed, return pending confirmation with phase deliverables
+ if !confirmed {
+ // Get files created in this phase
+ let phase_files = match repository::list_files_in_contract(pool, contract_id, owner_id).await {
+ Ok(files) => files
+ .into_iter()
+ .filter(|f| f.contract_phase.as_deref() == Some(current_phase))
+ .map(|f| json!({
+ "id": f.id,
+ "name": f.name,
+ "description": f.description
+ }))
+ .collect::<Vec<_>>(),
+ Err(_) => Vec::new(),
+ };
+
+ // Get tasks completed in this contract
+ let phase_tasks = match repository::list_tasks_in_contract(pool, contract_id, owner_id).await {
+ Ok(tasks) => tasks
+ .into_iter()
+ .filter(|t| t.status == "done" || t.status == "completed")
+ .map(|t| json!({
+ "id": t.id,
+ "name": t.name,
+ "status": t.status
+ }))
+ .collect::<Vec<_>>(),
+ Err(_) => Vec::new(),
+ };
+
+ // Build deliverables summary
+ let deliverables_summary = format!(
+ "Phase '{}' deliverables: {} files created, {} tasks completed.",
+ current_phase,
+ phase_files.len(),
+ phase_tasks.len()
+ );
+
+ let transition_id = uuid::Uuid::new_v4().to_string();
+
+ return ContractRequestResult {
+ success: true,
+ message: format!(
+ "Phase transition to '{}' requires user confirmation. Review the deliverables and call advance_phase again with confirmed=true to proceed, or provide feedback to request changes.",
+ new_phase
+ ),
+ data: Some(json!({
+ "status": "pending_confirmation",
+ "transitionId": transition_id,
+ "currentPhase": current_phase,
+ "nextPhase": new_phase,
+ "deliverablesSummary": deliverables_summary,
+ "phaseFiles": phase_files,
+ "phaseTasks": phase_tasks,
+ "requiresConfirmation": true,
+ "instructions": "To proceed: call advance_phase with confirmed=true. To request changes: call advance_phase with feedback='your feedback here'"
+ })),
+ };
+ }
+ }
+
+ // Update phase (either phase_guard is disabled, or user confirmed)
match repository::change_contract_phase_for_owner(pool, contract_id, owner_id, &new_phase).await {
Ok(Some(updated)) => {
// Get deliverables for the new phase
@@ -1748,6 +1829,7 @@ async fn handle_contract_request(
current_phase, new_phase, deliverables.guidance
),
data: Some(json!({
+ "status": "advanced",
"previousPhase": current_phase,
"newPhase": updated.phase,
"phaseGuidance": deliverables.guidance,
@@ -2377,6 +2459,7 @@ async fn handle_contract_request(
contract_type: Some("specification".to_string()),
initial_phase: Some("research".to_string()),
autonomous_loop: None,
+ phase_guard: None,
};
let contract = match repository::create_contract_for_owner(pool, owner_id, contract_req).await {
diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs
index 684ab2b..4f4a94b 100644
--- a/makima/src/server/handlers/contracts.rs
+++ b/makima/src/server/handlers/contracts.rs
@@ -1267,14 +1267,100 @@ pub async fn change_phase(
.into_response();
};
+ // First, get the contract to check phase_guard
+ let contract = match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(c)) => c,
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Contract not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get contract {}: {}", id, e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ // If phase_guard is enabled and not confirmed, return phase deliverables for review
+ if contract.phase_guard && !req.confirmed.unwrap_or(false) {
+ // If user provided feedback, return it
+ if let Some(ref feedback) = req.feedback {
+ return Json(serde_json::json!({
+ "status": "changes_requested",
+ "currentPhase": contract.phase,
+ "requestedPhase": req.phase,
+ "feedback": feedback,
+ "message": "Feedback has been noted. Address the changes and try again."
+ }))
+ .into_response();
+ }
+
+ // Get files created in this phase
+ let phase_files = match repository::list_files_in_contract(pool, id, auth.owner_id).await {
+ Ok(files) => files
+ .into_iter()
+ .filter(|f| f.contract_phase.as_deref() == Some(&contract.phase))
+ .map(|f| serde_json::json!({
+ "id": f.id,
+ "name": f.name,
+ "description": f.description
+ }))
+ .collect::<Vec<_>>(),
+ Err(_) => Vec::new(),
+ };
+
+ // Get tasks completed in this contract
+ let phase_tasks = match repository::list_tasks_in_contract(pool, id, auth.owner_id).await {
+ Ok(tasks) => tasks
+ .into_iter()
+ .filter(|t| t.status == "done" || t.status == "completed")
+ .map(|t| serde_json::json!({
+ "id": t.id,
+ "name": t.name,
+ "status": t.status
+ }))
+ .collect::<Vec<_>>(),
+ Err(_) => Vec::new(),
+ };
+
+ let deliverables_summary = format!(
+ "Phase '{}' deliverables: {} files created, {} tasks completed.",
+ contract.phase,
+ phase_files.len(),
+ phase_tasks.len()
+ );
+
+ let transition_id = uuid::Uuid::new_v4().to_string();
+
+ return Json(serde_json::json!({
+ "status": "pending_confirmation",
+ "transitionId": transition_id,
+ "currentPhase": contract.phase,
+ "nextPhase": req.phase,
+ "deliverablesSummary": deliverables_summary,
+ "phaseFiles": phase_files,
+ "phaseTasks": phase_tasks,
+ "requiresConfirmation": true,
+ "message": "Phase transition requires confirmation. Set confirmed=true in the request to proceed."
+ }))
+ .into_response();
+ }
+
+ // Phase guard is disabled or user confirmed - proceed with phase change
match repository::change_contract_phase_for_owner(pool, id, auth.owner_id, &req.phase).await {
- Ok(Some(contract)) => {
+ Ok(Some(updated_contract)) => {
// Notify supervisor of phase change
- if let Some(supervisor_task_id) = contract.supervisor_task_id {
+ if let Some(supervisor_task_id) = updated_contract.supervisor_task_id {
if let Ok(Some(supervisor)) = repository::get_task_for_owner(pool, supervisor_task_id, auth.owner_id).await {
let state_clone = state.clone();
- let contract_id = contract.id;
- let new_phase = contract.phase.clone();
+ let contract_id = updated_contract.id;
+ let new_phase = updated_contract.phase.clone();
tokio::spawn(async move {
state_clone.notify_supervisor_of_phase_change(
supervisor.id,
@@ -1302,21 +1388,21 @@ pub async fn change_phase(
).await;
// Get summary with counts
- match repository::get_contract_summary_for_owner(pool, contract.id, auth.owner_id).await
+ match repository::get_contract_summary_for_owner(pool, updated_contract.id, auth.owner_id).await
{
Ok(Some(summary)) => Json(summary).into_response(),
_ => Json(ContractSummary {
- id: contract.id,
- name: contract.name,
- description: contract.description,
- contract_type: contract.contract_type,
- phase: contract.phase,
- status: contract.status,
+ id: updated_contract.id,
+ name: updated_contract.name,
+ description: updated_contract.description,
+ contract_type: updated_contract.contract_type,
+ phase: updated_contract.phase,
+ status: updated_contract.status,
file_count: 0,
task_count: 0,
repository_count: 0,
- version: contract.version,
- created_at: contract.created_at,
+ version: updated_contract.version,
+ created_at: updated_contract.created_at,
})
.into_response(),
}
diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs
index 5a08a49..f8df69f 100644
--- a/makima/src/server/handlers/mesh.rs
+++ b/makima/src/server/handlers/mesh.rs
@@ -3239,6 +3239,7 @@ pub async fn create_adhoc_task(
contract_type: Some(CONTRACT_TYPE_TASK.to_string()),
initial_phase: Some("execute".to_string()), // Skip planning
autonomous_loop: Some(false),
+ phase_guard: None,
};
let contract = match repository::create_contract_for_owner(pool, auth.owner_id, contract_req).await {
diff --git a/makima/src/server/handlers/transcript_analysis.rs b/makima/src/server/handlers/transcript_analysis.rs
index 275905e..99f9ea7 100644
--- a/makima/src/server/handlers/transcript_analysis.rs
+++ b/makima/src/server/handlers/transcript_analysis.rs
@@ -277,6 +277,7 @@ pub async fn create_contract_from_analysis(
contract_type: Some("specification".to_string()),
initial_phase: Some("research".to_string()),
autonomous_loop: None,
+ phase_guard: None,
};
let contract = match repository::create_contract_for_owner(pool, auth.owner_id, contract_req).await {