summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-03-02 15:18:31 +0000
committerGitHub <noreply@github.com>2026-03-02 15:18:31 +0000
commit78cb861412850889424ae7d5ae5cd952a2b90295 (patch)
tree7a6eb0693457886dbe0eea84c0c1489724791f79
parent2bc1cd4717b587cd2b8ffccd723b62f888e61aa8 (diff)
downloadsoryu-78cb861412850889424ae7d5ae5cd952a2b90295.tar.gz
soryu-78cb861412850889424ae7d5ae5cd952a2b90295.zip
feat: move daemon reauth to daemons page, add contract-backed directive steps, rename Mesh to Exec (#84)
* feat: soryu-co/soryu - makima: Rename Mesh to Exec in navigation * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Add contract-backed steps to directive flow * WIP: heartbeat checkpoint
-rw-r--r--makima/frontend/src/components/NavStrip.tsx2
-rw-r--r--makima/frontend/src/components/PhaseConfirmationNotification.tsx2
-rw-r--r--makima/frontend/src/components/SupervisorQuestionNotification.tsx2
-rw-r--r--makima/frontend/src/components/contracts/CommandModePanel.tsx2
-rw-r--r--makima/frontend/src/components/directives/DirectiveDAG.tsx2
-rw-r--r--makima/frontend/src/components/directives/OrchestratorStepNode.tsx2
-rw-r--r--makima/frontend/src/components/directives/StepNode.tsx25
-rw-r--r--makima/frontend/src/components/mesh/TaskOutput.tsx110
-rw-r--r--makima/frontend/src/lib/api.ts74
-rw-r--r--makima/frontend/src/main.tsx4
-rw-r--r--makima/frontend/src/routes/contracts.tsx6
-rw-r--r--makima/frontend/src/routes/daemons.tsx363
-rw-r--r--makima/frontend/src/routes/files.tsx2
-rw-r--r--makima/frontend/src/routes/login.tsx4
-rw-r--r--makima/frontend/src/routes/mesh.tsx22
-rw-r--r--makima/migrations/20260302000000_add_contract_to_directive_steps.sql4
-rw-r--r--makima/src/daemon/task/manager.rs95
-rw-r--r--makima/src/daemon/ws/protocol.rs26
-rw-r--r--makima/src/db/models.rs9
-rw-r--r--makima/src/db/repository.rs63
-rw-r--r--makima/src/orchestration/directive.rs243
-rw-r--r--makima/src/server/handlers/mesh.rs284
-rw-r--r--makima/src/server/handlers/mesh_daemon.rs31
-rw-r--r--makima/src/server/mod.rs3
-rw-r--r--makima/src/server/state.rs60
25 files changed, 1278 insertions, 162 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index 9556458..17013ac 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -14,7 +14,7 @@ const NAV_LINKS: NavLink[] = [
{ label: "Directives", href: "/directives", requiresAuth: true },
{ label: "Orders", href: "/orders", requiresAuth: true },
{ label: "Contracts", href: "/contracts", requiresAuth: true },
- { label: "Mesh", href: "/mesh", requiresAuth: true },
+ { label: "Exec", href: "/exec", requiresAuth: true },
{ label: "Daemons", href: "/daemons", requiresAuth: true },
{ label: "History", href: "/history", requiresAuth: true },
];
diff --git a/makima/frontend/src/components/PhaseConfirmationNotification.tsx b/makima/frontend/src/components/PhaseConfirmationNotification.tsx
index 516211f..2681fdc 100644
--- a/makima/frontend/src/components/PhaseConfirmationNotification.tsx
+++ b/makima/frontend/src/components/PhaseConfirmationNotification.tsx
@@ -86,7 +86,7 @@ export function PhaseConfirmationToast() {
const handleGoToTask = (question: PendingQuestion) => {
dismissNotification(question.questionId);
- navigate(`/mesh/${question.taskId}`);
+ navigate(`/exec/${question.taskId}`);
};
return (
diff --git a/makima/frontend/src/components/SupervisorQuestionNotification.tsx b/makima/frontend/src/components/SupervisorQuestionNotification.tsx
index b1cbacc..e62638c 100644
--- a/makima/frontend/src/components/SupervisorQuestionNotification.tsx
+++ b/makima/frontend/src/components/SupervisorQuestionNotification.tsx
@@ -16,7 +16,7 @@ export function SupervisorQuestionNotification() {
const handleGoToTask = (questionId: string, taskId: string) => {
dismissNotification(questionId);
- navigate(`/mesh/${taskId}`);
+ navigate(`/exec/${taskId}`);
};
return (
diff --git a/makima/frontend/src/components/contracts/CommandModePanel.tsx b/makima/frontend/src/components/contracts/CommandModePanel.tsx
index 832d5ec..b39b309 100644
--- a/makima/frontend/src/components/contracts/CommandModePanel.tsx
+++ b/makima/frontend/src/components/contracts/CommandModePanel.tsx
@@ -65,7 +65,7 @@ export function CommandModePanel({ contract, onUpdate }: CommandModePanelProps)
const handleGoToSupervisor = useCallback(() => {
if (supervisorStatus.supervisorTaskId) {
- navigate(`/mesh/${supervisorStatus.supervisorTaskId}`);
+ navigate(`/exec/${supervisorStatus.supervisorTaskId}`);
}
}, [supervisorStatus.supervisorTaskId, navigate]);
const config = statusConfig[supervisorStatus.status];
diff --git a/makima/frontend/src/components/directives/DirectiveDAG.tsx b/makima/frontend/src/components/directives/DirectiveDAG.tsx
index 142df41..f225356 100644
--- a/makima/frontend/src/components/directives/DirectiveDAG.tsx
+++ b/makima/frontend/src/components/directives/DirectiveDAG.tsx
@@ -146,7 +146,7 @@ function SpecializedStepNode({ step }: { step: SpecializedStep }) {
{step.name}
</span>
<a
- href={`/mesh/${step.taskId}`}
+ href={`/exec/${step.taskId}`}
className="text-[9px] font-mono text-[#556677] hover:text-white underline"
>
View task
diff --git a/makima/frontend/src/components/directives/OrchestratorStepNode.tsx b/makima/frontend/src/components/directives/OrchestratorStepNode.tsx
index 9c8e95e..3fccb4b 100644
--- a/makima/frontend/src/components/directives/OrchestratorStepNode.tsx
+++ b/makima/frontend/src/components/directives/OrchestratorStepNode.tsx
@@ -151,7 +151,7 @@ export function OrchestratorStepNode({
{/* Task link */}
<a
- href={`/mesh/${taskId}`}
+ href={`/exec/${taskId}`}
className={`text-[9px] font-mono text-[#556677] hover:${colors.text} underline block`}
>
{status === "running" ? "View running task" : "View task"}
diff --git a/makima/frontend/src/components/directives/StepNode.tsx b/makima/frontend/src/components/directives/StepNode.tsx
index 2844b4a..775b898 100644
--- a/makima/frontend/src/components/directives/StepNode.tsx
+++ b/makima/frontend/src/components/directives/StepNode.tsx
@@ -28,10 +28,11 @@ interface StepNodeProps {
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();
+ const isContractBacked = !!step.contractType;
return (
<div
- className={`${colors.bg} ${colors.border} border rounded px-3 py-2 min-w-[160px] max-w-[220px]`}
+ className={`${colors.bg} ${isContractBacked ? "border-2 border-dashed" : "border"} ${colors.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">
@@ -41,14 +42,32 @@ export function StepNode({ step, onComplete, onFail, onSkip }: StepNodeProps) {
{label}
</span>
</div>
+ {isContractBacked && (
+ <div className="flex items-center gap-1 mb-1">
+ <span className="text-[8px] font-mono text-violet-400 bg-violet-400/10 border border-violet-400/20 rounded px-1 py-0.5 uppercase tracking-wide">
+ CONTRACT
+ </span>
+ <span className="text-[8px] font-mono text-violet-300/60">
+ {step.contractType}
+ </span>
+ </div>
+ )}
{step.description && (
<p className="text-[10px] text-[#7788aa] font-mono truncate mb-1">
{step.description}
</p>
)}
- {step.taskId && (
+ {step.contractId && (
+ <a
+ href={`/contracts/${step.contractId}`}
+ className="text-[9px] font-mono text-violet-400/60 hover:text-violet-300 underline block mb-1"
+ >
+ {step.status === "running" ? "Contract running..." : "View contract"}
+ </a>
+ )}
+ {step.taskId && !step.contractId && (
<a
- href={`/mesh/${step.taskId}`}
+ href={`/exec/${step.taskId}`}
className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc] underline block mb-1"
>
{step.status === "running" ? "Auto-executing..." : "View task"}
diff --git a/makima/frontend/src/components/mesh/TaskOutput.tsx b/makima/frontend/src/components/mesh/TaskOutput.tsx
index 2db4250..1a376ad 100644
--- a/makima/frontend/src/components/mesh/TaskOutput.tsx
+++ b/makima/frontend/src/components/mesh/TaskOutput.tsx
@@ -309,7 +309,21 @@ function OutputEntryRenderer({ entry, pendingQuestionIds, onAnswerQuestion }: Ou
);
case "auth_required":
- return <AuthRequiredEntry entry={entry} />;
+ return (
+ <div className="bg-amber-900/30 border border-amber-500/50 rounded p-3 my-2">
+ <div className="flex items-center gap-2 text-amber-400 font-semibold mb-1">
+ <span>&#x26A0;</span>
+ <span>Authentication Expired{entry.toolInput?.hostname ? ` (${entry.toolInput.hostname})` : ""}</span>
+ </div>
+ <p className="text-amber-200/80 text-sm">
+ The daemon's OAuth token has expired. Go to the{" "}
+ <a href="/daemons" className="text-[#75aafc] hover:text-[#9bc3ff] underline">
+ Daemons page
+ </a>{" "}
+ to reauthorize, then retry this task.
+ </p>
+ </div>
+ );
case "supervisor_question":
return (
@@ -501,98 +515,8 @@ function SupervisorQuestionEntry({
);
}
-function AuthRequiredEntry({ entry }: { entry: TaskOutputEvent }) {
- const [authCode, setAuthCode] = useState("");
- const [submitting, setSubmitting] = useState(false);
- const [submitted, setSubmitted] = useState(false);
- const [error, setError] = useState<string | null>(null);
-
- const loginUrl = entry.toolInput?.loginUrl as string | undefined;
- const hostname = entry.toolInput?.hostname as string | undefined;
- // Get taskId from entry or fallback to toolInput (for robustness)
- const taskId = entry.taskId || (entry.toolInput?.taskId as string | undefined);
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- if (!authCode.trim() || !taskId) return;
-
- setSubmitting(true);
- setError(null);
-
- try {
- // Send the auth code to the task via the message endpoint
- await sendTaskMessage(taskId, `AUTH_CODE:${authCode.trim()}`);
- setSubmitted(true);
- } catch (err) {
- setError(err instanceof Error ? err.message : "Failed to submit code");
- } finally {
- setSubmitting(false);
- }
- };
-
- if (submitted) {
- return (
- <div className="bg-green-900/30 border border-green-500/50 rounded p-3 my-2">
- <div className="flex items-center gap-2 text-green-400 font-semibold">
- <span>✓</span>
- <span>Authentication code submitted</span>
- </div>
- <p className="text-green-200/80 text-sm mt-1">
- Waiting for authentication to complete...
- </p>
- </div>
- );
- }
-
- return (
- <div className="bg-amber-900/30 border border-amber-500/50 rounded p-3 my-2">
- <div className="flex items-center gap-2 text-amber-400 font-semibold mb-2">
- <span>🔐</span>
- <span>Authentication Required{hostname ? ` (${hostname})` : ""}</span>
- </div>
- <p className="text-amber-200/80 text-sm mb-3">
- The daemon's OAuth token has expired. Click the button to login, then paste the code below:
- </p>
-
- <div className="flex flex-col gap-3">
- {loginUrl ? (
- <a
- href={loginUrl}
- target="_blank"
- rel="noopener noreferrer"
- className="inline-block bg-amber-500 hover:bg-amber-400 text-black font-medium px-4 py-2 rounded transition-colors text-center"
- >
- 1. Login to Claude
- </a>
- ) : (
- <p className="text-red-400 text-sm">Login URL not available</p>
- )}
-
- <form onSubmit={handleSubmit} className="flex gap-2">
- <input
- type="text"
- value={authCode}
- onChange={(e) => setAuthCode(e.target.value)}
- placeholder="2. Paste authentication code here"
- className="flex-1 bg-[#0a1525] border border-amber-500/30 rounded px-3 py-2 text-amber-100 placeholder-amber-500/50 focus:outline-none focus:border-amber-400"
- disabled={submitting}
- />
- <button
- type="submit"
- disabled={submitting || !authCode.trim()}
- className="bg-amber-500 hover:bg-amber-400 disabled:bg-amber-700 disabled:cursor-not-allowed text-black font-medium px-4 py-2 rounded transition-colors"
- >
- {submitting ? "..." : "Submit"}
- </button>
- </form>
-
- {error && (
- <p className="text-red-400 text-sm">{error}</p>
- )}
- </div>
- </div>
- );
-}
+// AuthRequiredEntry has been removed - auth flow is now on the Daemons page.
+// The "auth_required" case in OutputEntryRenderer shows a redirect message instead.
/** Entry for phase transition confirmations */
function PhaseConfirmationEntry({
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 458b69d..155c716 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -1258,6 +1258,76 @@ export function getDaemonDownloadUrl(platform: string): string {
}
// =============================================================================
+// Daemon Reauthorization
+// =============================================================================
+
+/** Response from the trigger daemon reauth endpoint */
+export interface TriggerReauthResponse {
+ success: boolean;
+ daemonId: string;
+ requestId: string;
+}
+
+/** Response from the reauth status polling endpoint */
+export interface ReauthStatusResponse {
+ status: string; // "pending" | "url_ready" | "completed" | "failed"
+ loginUrl?: string;
+ error?: string;
+}
+
+/**
+ * Trigger OAuth re-authentication on a daemon.
+ * Sends a reauth command to the daemon, which will spawn `claude setup-token`
+ * and return the OAuth login URL.
+ */
+export async function triggerDaemonReauth(daemonId: string): Promise<TriggerReauthResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${daemonId}/reauth`, {
+ method: "POST",
+ });
+ if (!res.ok) {
+ const errorData = await res.json().catch(() => ({}));
+ throw new Error(errorData.message || `Failed to trigger reauth: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/**
+ * Submit an OAuth auth code to a daemon's pending reauth flow.
+ */
+export async function submitDaemonAuthCode(
+ daemonId: string,
+ code: string,
+ requestId: string,
+): Promise<void> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${daemonId}/reauth/code`, {
+ method: "POST",
+ body: JSON.stringify({ code, requestId }),
+ });
+ if (!res.ok) {
+ const errorData = await res.json().catch(() => ({}));
+ throw new Error(errorData.message || `Failed to submit auth code: ${res.statusText}`);
+ }
+}
+
+/**
+ * Get the status of a daemon reauth request.
+ * Used for polling to track reauth flow progress.
+ */
+export async function getDaemonReauthStatus(
+ daemonId: string,
+ requestId: string,
+): Promise<ReauthStatusResponse> {
+ const res = await authFetch(
+ `${API_BASE}/api/v1/mesh/daemons/${daemonId}/reauth/${requestId}/status`,
+ );
+ if (!res.ok) {
+ const errorData = await res.json().catch(() => ({}));
+ throw new Error(errorData.message || `Failed to get reauth status: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+// =============================================================================
// Mesh Chat Types for Task Orchestration
// =============================================================================
@@ -3081,6 +3151,10 @@ export interface DirectiveStep {
dependsOn: string[];
status: StepStatus;
taskId: string | null;
+ /** Contract ID for contract-backed steps */
+ contractId?: string | null;
+ /** Contract type (e.g. "simple", "specification", "execute") for contract-backed steps */
+ contractType?: string | null;
orderIndex: number;
generation: number;
startedAt: string | null;
diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx
index 32c05ba..4f7c525 100644
--- a/makima/frontend/src/main.tsx
+++ b/makima/frontend/src/main.tsx
@@ -98,7 +98,7 @@ createRoot(document.getElementById("root")!).render(
}
/>
<Route
- path="/mesh"
+ path="/exec"
element={
<ProtectedRoute>
<MeshPage />
@@ -106,7 +106,7 @@ createRoot(document.getElementById("root")!).render(
}
/>
<Route
- path="/mesh/:id"
+ path="/exec/:id"
element={
<ProtectedRoute>
<MeshPage />
diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx
index b85d667..ce9ceca 100644
--- a/makima/frontend/src/routes/contracts.tsx
+++ b/makima/frontend/src/routes/contracts.tsx
@@ -448,7 +448,7 @@ function ContractsPageContent() {
const handleTaskSelect = useCallback(
(taskId: string) => {
- navigate(`/mesh/${taskId}`);
+ navigate(`/exec/${taskId}`);
},
[navigate]
);
@@ -469,7 +469,7 @@ function ContractsPageContent() {
const refreshed = await fetchContract(contractDetail.id);
setContractDetail(refreshed);
// Navigate to the new task
- navigate(`/mesh/${task.id}`);
+ navigate(`/exec/${task.id}`);
} catch (e) {
console.error("Failed to create task:", e);
alert(e instanceof Error ? e.message : "Failed to create task");
@@ -515,7 +515,7 @@ function ContractsPageContent() {
const handleContextGoToSupervisor = useCallback(
(contract: ContractSummary) => {
if (contract.supervisorTaskId) {
- navigate(`/mesh/${contract.supervisorTaskId}`);
+ navigate(`/exec/${contract.supervisorTaskId}`);
}
},
[navigate]
diff --git a/makima/frontend/src/routes/daemons.tsx b/makima/frontend/src/routes/daemons.tsx
index 0f55190..ca167fe 100644
--- a/makima/frontend/src/routes/daemons.tsx
+++ b/makima/frontend/src/routes/daemons.tsx
@@ -1,10 +1,13 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useCallback, useRef } from "react";
import { useAuth } from "../contexts/AuthContext";
import { useNavigate } from "react-router";
import { Masthead } from "../components/Masthead";
import {
listDaemons,
restartDaemon,
+ triggerDaemonReauth,
+ submitDaemonAuthCode,
+ getDaemonReauthStatus,
type Daemon,
type DaemonListResponse,
} from "../lib/api";
@@ -34,6 +37,333 @@ function ErrorAlert({ children }: { children: React.ReactNode }) {
}
// =============================================================================
+// Reauth Modal Component
+// =============================================================================
+
+type ReauthState =
+ | { phase: "initiating" }
+ | { phase: "url_ready"; loginUrl: string; requestId: string }
+ | { phase: "submitting"; requestId: string }
+ | { phase: "success" }
+ | { phase: "error"; message: string };
+
+function ReauthModal({
+ daemon,
+ onClose,
+}: {
+ daemon: Daemon;
+ onClose: () => void;
+}) {
+ const [state, setState] = useState<ReauthState>({ phase: "initiating" });
+ const [authCode, setAuthCode] = useState("");
+ const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
+
+ // Cleanup polling on unmount
+ useEffect(() => {
+ return () => {
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ }
+ };
+ }, []);
+
+ // Trigger reauth on mount
+ useEffect(() => {
+ let cancelled = false;
+ const trigger = async () => {
+ try {
+ const res = await triggerDaemonReauth(daemon.id);
+ if (cancelled) return;
+
+ // Start polling for status
+ const requestId = res.requestId;
+ pollingRef.current = setInterval(async () => {
+ try {
+ const status = await getDaemonReauthStatus(daemon.id, requestId);
+ if (cancelled) return;
+
+ if (status.status === "url_ready" && status.loginUrl) {
+ setState({
+ phase: "url_ready",
+ loginUrl: status.loginUrl,
+ requestId,
+ });
+ // Stop polling once we have the URL
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+ } else if (status.status === "failed") {
+ setState({
+ phase: "error",
+ message: status.error || "Reauth failed",
+ });
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+ } else if (status.status === "completed") {
+ setState({ phase: "success" });
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+ }
+ } catch {
+ // Polling errors are non-fatal, keep trying
+ }
+ }, 2000);
+ } catch (err) {
+ if (cancelled) return;
+ setState({
+ phase: "error",
+ message:
+ err instanceof Error ? err.message : "Failed to trigger reauth",
+ });
+ }
+ };
+ trigger();
+ return () => {
+ cancelled = true;
+ };
+ }, [daemon.id]);
+
+ const handleSubmitCode = useCallback(
+ async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!authCode.trim() || state.phase !== "url_ready") return;
+
+ const requestId = state.requestId;
+ setState({ phase: "submitting", requestId });
+
+ try {
+ await submitDaemonAuthCode(daemon.id, authCode.trim(), requestId);
+
+ // Poll for completion
+ pollingRef.current = setInterval(async () => {
+ try {
+ const status = await getDaemonReauthStatus(
+ daemon.id,
+ requestId,
+ );
+ if (status.status === "completed") {
+ setState({ phase: "success" });
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+ } else if (status.status === "failed") {
+ setState({
+ phase: "error",
+ message: status.error || "Auth code submission failed",
+ });
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+ }
+ } catch {
+ // Keep polling
+ }
+ }, 2000);
+
+ // Also set a timeout so we don't poll forever
+ setTimeout(() => {
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+ // If still submitting after 30s, assume success (setup-token completed)
+ setState((prev) =>
+ prev.phase === "submitting" ? { phase: "success" } : prev,
+ );
+ }, 30000);
+ } catch (err) {
+ setState({
+ phase: "error",
+ message:
+ err instanceof Error
+ ? err.message
+ : "Failed to submit auth code",
+ });
+ }
+ },
+ [authCode, daemon.id, state],
+ );
+
+ const handleRetry = useCallback(() => {
+ setAuthCode("");
+ setState({ phase: "initiating" });
+ // Re-trigger will happen via the useEffect dependency change
+ // We need to manually trigger since daemon.id hasn't changed
+ const trigger = async () => {
+ try {
+ const res = await triggerDaemonReauth(daemon.id);
+ const requestId = res.requestId;
+ pollingRef.current = setInterval(async () => {
+ try {
+ const status = await getDaemonReauthStatus(
+ daemon.id,
+ requestId,
+ );
+ if (status.status === "url_ready" && status.loginUrl) {
+ setState({
+ phase: "url_ready",
+ loginUrl: status.loginUrl,
+ requestId,
+ });
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+ } else if (status.status === "failed") {
+ setState({
+ phase: "error",
+ message: status.error || "Reauth failed",
+ });
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+ }
+ } catch {
+ // Keep polling
+ }
+ }, 2000);
+ } catch (err) {
+ setState({
+ phase: "error",
+ message:
+ err instanceof Error
+ ? err.message
+ : "Failed to trigger reauth",
+ });
+ }
+ };
+ trigger();
+ }, [daemon.id]);
+
+ return (
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
+ <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] p-6 max-w-md w-full mx-4 shadow-2xl">
+ {/* Header */}
+ <div className="flex items-center justify-between mb-4">
+ <h3 className="text-xs font-mono uppercase tracking-wide text-[#9bc3ff]">
+ Reauthorize Daemon
+ </h3>
+ <button
+ onClick={onClose}
+ className="text-[#7788aa] hover:text-[#9bc3ff] text-sm font-mono"
+ >
+ &#x2715;
+ </button>
+ </div>
+ <p className="text-[10px] font-mono text-[#7788aa] mb-4">
+ {daemon.hostname || "Unknown Host"}
+ </p>
+
+ {/* Initiating */}
+ {state.phase === "initiating" && (
+ <div className="flex items-center gap-2 py-4">
+ <div className="w-3 h-3 border border-[#75aafc] border-t-transparent rounded-full animate-spin" />
+ <span className="text-[10px] font-mono text-[#7788aa]">
+ Initiating reauthorization...
+ </span>
+ </div>
+ )}
+
+ {/* URL Ready */}
+ {state.phase === "url_ready" && (
+ <div className="space-y-3">
+ <p className="text-[10px] font-mono text-[#7788aa]">
+ Click the button below to open the OAuth login page, then paste the code:
+ </p>
+ <a
+ href={state.loginUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="block text-center bg-amber-500 hover:bg-amber-400 text-black font-mono text-xs font-medium px-4 py-2 transition-colors"
+ >
+ 1. Login to Claude
+ </a>
+ <form onSubmit={handleSubmitCode} className="flex gap-2">
+ <input
+ type="text"
+ value={authCode}
+ onChange={(e) => setAuthCode(e.target.value)}
+ placeholder="2. Paste authentication code"
+ className="flex-1 bg-[#0a1525] border border-amber-500/30 px-3 py-2 text-xs font-mono text-amber-100 placeholder-amber-500/50 focus:outline-none focus:border-amber-400"
+ />
+ <button
+ type="submit"
+ disabled={!authCode.trim()}
+ className="bg-amber-500 hover:bg-amber-400 disabled:bg-amber-700 disabled:cursor-not-allowed text-black font-mono text-xs font-medium px-4 py-2 transition-colors"
+ >
+ Submit
+ </button>
+ </form>
+ </div>
+ )}
+
+ {/* Submitting */}
+ {state.phase === "submitting" && (
+ <div className="flex items-center gap-2 py-4">
+ <div className="w-3 h-3 border border-amber-400 border-t-transparent rounded-full animate-spin" />
+ <span className="text-[10px] font-mono text-[#7788aa]">
+ Submitting auth code...
+ </span>
+ </div>
+ )}
+
+ {/* Success */}
+ {state.phase === "success" && (
+ <div className="py-4">
+ <div className="flex items-center gap-2 text-green-400 mb-2">
+ <span className="text-sm">&#x2713;</span>
+ <span className="text-xs font-mono font-medium">
+ Authentication successful
+ </span>
+ </div>
+ <p className="text-[10px] font-mono text-[#7788aa] mb-3">
+ The daemon's OAuth token has been refreshed. Tasks can now run normally.
+ </p>
+ <button
+ onClick={onClose}
+ className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff]"
+ >
+ Close
+ </button>
+ </div>
+ )}
+
+ {/* Error */}
+ {state.phase === "error" && (
+ <div className="py-4">
+ <div className="border border-red-700/50 bg-red-900/20 text-red-400 px-3 py-2 mb-3 font-mono text-xs">
+ {state.message}
+ </div>
+ <div className="flex gap-2">
+ <button
+ onClick={handleRetry}
+ className="text-[10px] font-mono text-amber-400 hover:text-amber-300 px-2 py-1 border border-amber-700/50 bg-amber-900/20"
+ >
+ Retry
+ </button>
+ <button
+ onClick={onClose}
+ className="text-[10px] font-mono text-[#7788aa] hover:text-[#9bc3ff] px-2 py-1"
+ >
+ Close
+ </button>
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ );
+}
+
+// =============================================================================
// Daemons Page
// =============================================================================
@@ -47,6 +377,7 @@ export default function DaemonsPage() {
const [daemonsError, setDaemonsError] = useState<string | null>(null);
const [restartingDaemonId, setRestartingDaemonId] = useState<string | null>(null);
const [restartConfirmDaemonId, setRestartConfirmDaemonId] = useState<string | null>(null);
+ const [reauthDaemon, setReauthDaemon] = useState<Daemon | null>(null);
// Redirect if not authenticated
useEffect(() => {
@@ -292,7 +623,7 @@ export default function DaemonsPage() {
</div>
)}
</div>
- {/* Restart Section */}
+ {/* Actions Section */}
{daemon.status === "connected" && (
<div className="mt-3 pt-2 border-t border-[rgba(117,170,252,0.1)]">
{restartConfirmDaemonId === daemon.id ? (
@@ -318,12 +649,20 @@ export default function DaemonsPage() {
</div>
</div>
) : (
- <button
- onClick={() => setRestartConfirmDaemonId(daemon.id)}
- className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff]"
- >
- &#x27F3; Restart Daemon
- </button>
+ <div className="flex items-center gap-3">
+ <button
+ onClick={() => setReauthDaemon(daemon)}
+ className="text-[10px] font-mono text-amber-400 hover:text-amber-300"
+ >
+ &#x1F511; Reauthorize
+ </button>
+ <button
+ onClick={() => setRestartConfirmDaemonId(daemon.id)}
+ className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff]"
+ >
+ &#x27F3; Restart Daemon
+ </button>
+ </div>
)}
</div>
)}
@@ -335,6 +674,14 @@ export default function DaemonsPage() {
</div>
</div>
</main>
+
+ {/* Reauth Modal */}
+ {reauthDaemon && (
+ <ReauthModal
+ daemon={reauthDaemon}
+ onClose={() => setReauthDaemon(null)}
+ />
+ )}
</div>
);
}
diff --git a/makima/frontend/src/routes/files.tsx b/makima/frontend/src/routes/files.tsx
index 6cfb3ca..b232aa0 100644
--- a/makima/frontend/src/routes/files.tsx
+++ b/makima/frontend/src/routes/files.tsx
@@ -839,7 +839,7 @@ function FilesPageContent() {
<div className="flex gap-2">
<button
onClick={() => {
- navigate(`/mesh/${createdTask.id}`);
+ navigate(`/exec/${createdTask.id}`);
setCreatedTask(null);
}}
className="px-3 py-1 font-mono text-xs text-[#0a1628] bg-[#75aafc] hover:bg-[#9bc3ff] transition-colors"
diff --git a/makima/frontend/src/routes/login.tsx b/makima/frontend/src/routes/login.tsx
index 63b3af3..0725a2d 100644
--- a/makima/frontend/src/routes/login.tsx
+++ b/makima/frontend/src/routes/login.tsx
@@ -18,7 +18,7 @@ export default function LoginPage() {
// Redirect if already authenticated
if (isAuthenticated && isAuthConfigured) {
- navigate("/mesh");
+ navigate("/exec");
return null;
}
@@ -34,7 +34,7 @@ export default function LoginPage() {
if (error) {
setError(error.message);
} else {
- navigate("/mesh");
+ navigate("/exec");
}
} else if (mode === "signup") {
const { error } = await signUp(email, password);
diff --git a/makima/frontend/src/routes/mesh.tsx b/makima/frontend/src/routes/mesh.tsx
index 1d1db84..67129f9 100644
--- a/makima/frontend/src/routes/mesh.tsx
+++ b/makima/frontend/src/routes/mesh.tsx
@@ -191,14 +191,6 @@ export default function MeshPage() {
// Only process output for the task we're currently viewing
if (event.taskId === activeOutputTaskId) {
setTaskOutputEntries((prev) => {
- // For auth_required, only allow one per task (replace existing)
- if (event.messageType === "auth_required") {
- const hasExisting = prev.some(e => e.messageType === "auth_required");
- if (hasExisting) {
- return prev; // Skip duplicate auth_required
- }
- }
-
// Deduplicate by checking if last entry is identical
// This prevents duplicates from React StrictMode or WebSocket reconnects
const lastEntry = prev[prev.length - 1];
@@ -387,7 +379,7 @@ export default function MeshPage() {
const handleSelectTask = useCallback(
(taskId: string) => {
- navigate(`/mesh/${taskId}`);
+ navigate(`/exec/${taskId}`);
},
[navigate]
);
@@ -395,9 +387,9 @@ export default function MeshPage() {
const handleBack = useCallback(() => {
// If viewing a subtask, go back to parent
if (taskDetail?.parentTaskId) {
- navigate(`/mesh/${taskDetail.parentTaskId}`);
+ navigate(`/exec/${taskDetail.parentTaskId}`);
} else {
- navigate("/mesh");
+ navigate("/exec");
}
}, [navigate, taskDetail]);
@@ -408,9 +400,9 @@ export default function MeshPage() {
if (success && id === taskId) {
// If deleting current task, go back
if (taskDetail?.parentTaskId) {
- navigate(`/mesh/${taskDetail.parentTaskId}`);
+ navigate(`/exec/${taskDetail.parentTaskId}`);
} else {
- navigate("/mesh");
+ navigate("/exec");
}
}
}
@@ -523,7 +515,7 @@ export default function MeshPage() {
});
console.log(`[Mesh] Task branched, new task ID: ${result.task.id}`);
// Navigate to the new branched task
- navigate(`/mesh/${result.task.id}`);
+ navigate(`/exec/${result.task.id}`);
} catch (e) {
console.error("Failed to branch task:", e);
throw e; // Re-throw so the modal can display the error
@@ -617,7 +609,7 @@ export default function MeshPage() {
targetRepoPath: targetPath || undefined,
});
if (newTask) {
- navigate(`/mesh/${newTask.id}`);
+ navigate(`/exec/${newTask.id}`);
}
} finally {
setCreating(false);
diff --git a/makima/migrations/20260302000000_add_contract_to_directive_steps.sql b/makima/migrations/20260302000000_add_contract_to_directive_steps.sql
new file mode 100644
index 0000000..f018aa4
--- /dev/null
+++ b/makima/migrations/20260302000000_add_contract_to_directive_steps.sql
@@ -0,0 +1,4 @@
+-- Add contract_id to directive_steps for contract-backed execution
+ALTER TABLE directive_steps ADD COLUMN contract_id UUID REFERENCES contracts(id);
+-- Add contract_type to specify what kind of contract to create
+ALTER TABLE directive_steps ADD COLUMN contract_type VARCHAR(32);
diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs
index b382507..addcd71 100644
--- a/makima/src/daemon/task/manager.rs
+++ b/makima/src/daemon/task/manager.rs
@@ -1886,6 +1886,66 @@ impl TaskManager {
tracing::info!("Daemon restart: exiting process with code 42 (restart requested)");
std::process::exit(42);
}
+ DaemonCommand::TriggerReauth { request_id } => {
+ tracing::info!(request_id = %request_id, "Received reauth trigger command from server");
+ let claude_command = self.process_manager.claude_command().to_string();
+ let ws_tx = self.ws_tx.clone();
+
+ // Spawn in a task so it doesn't block command handling
+ tokio::spawn(async move {
+ match get_oauth_login_url(&claude_command).await {
+ Some(login_url) => {
+ tracing::info!(request_id = %request_id, login_url = %login_url, "Got OAuth login URL for reauth");
+ let msg = DaemonMessage::ReauthStatus {
+ request_id,
+ status: "url_ready".to_string(),
+ login_url: Some(login_url),
+ error: None,
+ };
+ let _ = ws_tx.send(msg).await;
+ }
+ None => {
+ tracing::error!(request_id = %request_id, "Failed to get OAuth login URL for reauth");
+ let msg = DaemonMessage::ReauthStatus {
+ request_id,
+ status: "failed".to_string(),
+ login_url: None,
+ error: Some("Failed to get OAuth login URL from setup-token".to_string()),
+ };
+ let _ = ws_tx.send(msg).await;
+ }
+ }
+ });
+ }
+ DaemonCommand::SubmitAuthCode { request_id, code } => {
+ tracing::info!(request_id = %request_id, "Received auth code submission from server");
+ let ws_tx = self.ws_tx.clone();
+
+ if send_auth_code(&code) {
+ tracing::info!(request_id = %request_id, "Auth code forwarded to setup-token for reauth");
+ // Wait a short time then report completion
+ // (the setup-token process takes a moment to complete)
+ tokio::spawn(async move {
+ tokio::time::sleep(std::time::Duration::from_secs(3)).await;
+ let msg = DaemonMessage::ReauthStatus {
+ request_id,
+ status: "completed".to_string(),
+ login_url: None,
+ error: None,
+ };
+ let _ = ws_tx.send(msg).await;
+ });
+ } else {
+ tracing::warn!(request_id = %request_id, "No pending auth flow to receive code for reauth");
+ let msg = DaemonMessage::ReauthStatus {
+ request_id,
+ status: "failed".to_string(),
+ login_url: None,
+ error: Some("No pending auth flow to receive the code. Try triggering reauth again.".to_string()),
+ };
+ let _ = self.ws_tx.send(msg).await;
+ }
+ }
DaemonCommand::ApplyPatchToWorktree {
target_task_id,
source_task_id,
@@ -5260,32 +5320,19 @@ impl TaskManagerInner {
break;
}
- // Detect OAuth token expiration and trigger remote login flow
+ // Detect OAuth token expiration - log warning and let the task fail normally.
+ // Users can reauthorize via the Daemons page instead.
if !auth_error_handled && is_oauth_auth_error(&content_for_auth_check, json_type_for_auth_check.as_deref(), is_stdout_for_auth_check) {
auth_error_handled = true;
- tracing::warn!(task_id = %task_id, "OAuth authentication error detected, initiating remote login flow");
-
- // Spawn claude setup-token to get login URL
- if let Some(login_url) = get_oauth_login_url(&claude_command).await {
- tracing::info!(task_id = %task_id, login_url = %login_url, "Got OAuth login URL");
- let auth_msg = DaemonMessage::AuthenticationRequired {
- task_id: Some(task_id),
- login_url,
- hostname: daemon_hostname.clone(),
- };
- if ws_tx.send(auth_msg).await.is_err() {
- tracing::warn!(task_id = %task_id, "Failed to send auth required message");
- }
- } else {
- tracing::error!(task_id = %task_id, "Failed to get OAuth login URL from setup-token");
- let fallback_msg = DaemonMessage::task_output(
- task_id,
- format!("Authentication required on daemon{}. Please run 'claude /login' on the daemon machine.\n",
- daemon_hostname.as_ref().map(|h| format!(" ({})", h)).unwrap_or_default()),
- false,
- );
- let _ = ws_tx.send(fallback_msg).await;
- }
+ tracing::warn!(task_id = %task_id, "OAuth authentication error detected - task will fail. Reauthorize via Daemons page.");
+
+ let error_msg = DaemonMessage::task_output(
+ task_id,
+ format!("⚠ Authentication expired on daemon{}. Go to Daemons page to reauthorize, then retry this task.\n",
+ daemon_hostname.as_ref().map(|h| format!(" ({})", h)).unwrap_or_default()),
+ false,
+ );
+ let _ = ws_tx.send(error_msg).await;
}
}
None => {
diff --git a/makima/src/daemon/ws/protocol.rs b/makima/src/daemon/ws/protocol.rs
index 834e139..bed5ffd 100644
--- a/makima/src/daemon/ws/protocol.rs
+++ b/makima/src/daemon/ws/protocol.rs
@@ -129,6 +129,19 @@ pub enum DaemonMessage {
hostname: Option<String>,
},
+ /// Reauth status update (response to TriggerReauth/SubmitAuthCode commands).
+ ReauthStatus {
+ #[serde(rename = "requestId")]
+ request_id: Uuid,
+ /// Status: "url_ready", "completed", "failed"
+ status: String,
+ /// OAuth login URL (present when status is "url_ready")
+ #[serde(rename = "loginUrl")]
+ login_url: Option<String>,
+ /// Error message (present when status is "failed")
+ error: Option<String>,
+ },
+
// =========================================================================
// Merge Response Messages (sent by daemon after processing merge commands)
// =========================================================================
@@ -787,6 +800,19 @@ pub enum DaemonCommand {
/// Restart the daemon process.
RestartDaemon,
+ /// Trigger OAuth re-authentication on this daemon.
+ TriggerReauth {
+ #[serde(rename = "requestId")]
+ request_id: Uuid,
+ },
+
+ /// Submit auth code for pending reauth.
+ SubmitAuthCode {
+ #[serde(rename = "requestId")]
+ request_id: Uuid,
+ code: String,
+ },
+
/// Apply a patch to a task's worktree (for cross-daemon merge).
/// Sent by server when routing MergePatchToSupervisor to the supervisor's daemon.
ApplyPatchToWorktree {
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index 6b77563..6292e7b 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -2746,6 +2746,11 @@ pub struct DirectiveStep {
/// Status: pending, ready, running, completed, failed, skipped
pub status: String,
pub task_id: Option<Uuid>,
+ /// Optional contract ID for contract-backed execution.
+ pub contract_id: Option<Uuid>,
+ /// Optional contract type (e.g. "simple", "specification", "execute").
+ /// When set, the orchestrator creates a contract instead of a standalone task.
+ pub contract_type: Option<String>,
pub order_index: i32,
pub generation: i32,
pub started_at: Option<DateTime<Utc>>,
@@ -2871,6 +2876,10 @@ pub struct CreateDirectiveStepRequest {
/// Optional order ID to auto-link this step to an order.
#[serde(default)]
pub order_id: Option<Uuid>,
+ /// Optional: create a contract for this step instead of a standalone task.
+ /// Valid values: "simple", "specification", "execute"
+ #[serde(default)]
+ pub contract_type: Option<String>,
}
/// Request to update a directive step.
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 8d7a70c..1af22f6 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -5402,10 +5402,11 @@ pub async fn create_directive_step(
) -> Result<DirectiveStep, sqlx::Error> {
let generation = req.generation.unwrap_or(1);
let order_id = req.order_id;
+ let contract_type = req.contract_type.clone();
let step = 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)
+ INSERT INTO directive_steps (directive_id, name, description, task_plan, depends_on, order_index, generation, contract_type)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *
"#,
)
@@ -5416,6 +5417,7 @@ pub async fn create_directive_step(
.bind(&req.depends_on)
.bind(req.order_index)
.bind(generation)
+ .bind(&contract_type)
.fetch_one(pool)
.await?;
@@ -5727,6 +5729,8 @@ pub struct StepForDispatch {
pub order_index: i32,
pub generation: i32,
pub depends_on: Vec<Uuid>,
+ /// Optional contract type — when set, orchestrator creates a contract instead of a task.
+ pub contract_type: Option<String>,
// Directive fields
pub owner_id: Uuid,
pub directive_title: String,
@@ -5751,6 +5755,7 @@ pub async fn get_ready_steps_for_dispatch(
ds.order_index,
ds.generation,
ds.depends_on,
+ ds.contract_type,
d.owner_id,
d.title AS directive_title,
d.repository_url,
@@ -5760,6 +5765,7 @@ pub async fn get_ready_steps_for_dispatch(
JOIN directives d ON d.id = ds.directive_id
WHERE ds.status = 'ready'
AND ds.task_id IS NULL
+ AND ds.contract_id IS NULL
AND d.status = 'active'
ORDER BY ds.order_index
"#,
@@ -5831,6 +5837,39 @@ pub async fn get_running_steps_with_tasks(
JOIN tasks t ON t.id = ds.task_id
WHERE ds.status = 'running'
AND ds.task_id IS NOT NULL
+ AND ds.contract_id IS NULL
+ "#,
+ )
+ .fetch_all(pool)
+ .await
+}
+
+/// A running step backed by a contract, joined with the contract's current status.
+#[derive(Debug, Clone, sqlx::FromRow)]
+pub struct RunningStepWithContract {
+ pub step_id: Uuid,
+ pub directive_id: Uuid,
+ pub contract_id: Uuid,
+ pub contract_status: String,
+ pub contract_phase: String,
+}
+
+/// Get running steps that are backed by contracts (for contract-based monitoring).
+pub async fn get_running_steps_with_contracts(
+ pool: &PgPool,
+) -> Result<Vec<RunningStepWithContract>, sqlx::Error> {
+ sqlx::query_as::<_, RunningStepWithContract>(
+ r#"
+ SELECT
+ ds.id AS step_id,
+ ds.directive_id,
+ ds.contract_id AS "contract_id!",
+ c.status AS contract_status,
+ c.phase AS contract_phase
+ FROM directive_steps ds
+ JOIN contracts c ON c.id = ds.contract_id
+ WHERE ds.status = 'running'
+ AND ds.contract_id IS NOT NULL
"#,
)
.fetch_all(pool)
@@ -5995,6 +6034,26 @@ pub async fn link_task_to_step(
Ok(())
}
+/// Link a contract to a directive step.
+pub async fn link_contract_to_step(
+ pool: &PgPool,
+ step_id: Uuid,
+ contract_id: Uuid,
+) -> Result<(), sqlx::Error> {
+ sqlx::query(
+ r#"
+ UPDATE directive_steps
+ SET contract_id = $1
+ WHERE id = $2
+ "#,
+ )
+ .bind(contract_id)
+ .bind(step_id)
+ .execute(pool)
+ .await?;
+ Ok(())
+}
+
/// Set a step to 'running' status (after its task has been dispatched).
pub async fn set_step_running(
pool: &PgPool,
diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs
index 98690bb..155cfad 100644
--- a/makima/src/orchestration/directive.rs
+++ b/makima/src/orchestration/directive.rs
@@ -11,7 +11,7 @@ use uuid::Uuid;
use base64::Engine;
-use crate::db::models::{CreateTaskRequest, UpdateTaskRequest};
+use crate::db::models::{CreateContractRequest, CreateTaskRequest, UpdateContractRequest, UpdateTaskRequest};
use crate::db::repository;
use crate::server::state::{DaemonCommand, SharedState};
@@ -86,6 +86,42 @@ impl DirectiveOrchestrator {
let steps = repository::get_ready_steps_for_dispatch(&self.pool).await?;
for step in steps {
+ // If the step has a contract_type, create a contract instead of a standalone task
+ if step.contract_type.is_some() {
+ tracing::info!(
+ step_id = %step.step_id,
+ directive_id = %step.directive_id,
+ step_name = %step.step_name,
+ contract_type = ?step.contract_type,
+ "Spawning contract for contract-backed step"
+ );
+
+ match self
+ .spawn_step_contract(
+ step.step_id,
+ step.directive_id,
+ step.owner_id,
+ &step.step_name,
+ step.step_description.as_deref(),
+ step.task_plan.as_deref(),
+ step.contract_type.as_deref().unwrap_or("simple"),
+ step.repository_url.as_deref(),
+ step.base_branch.as_deref(),
+ )
+ .await
+ {
+ Ok(()) => {}
+ Err(e) => {
+ tracing::warn!(
+ step_id = %step.step_id,
+ error = %e,
+ "Failed to spawn contract for step"
+ );
+ }
+ }
+ continue;
+ }
+
tracing::info!(
step_id = %step.step_id,
directive_id = %step.directive_id,
@@ -218,7 +254,70 @@ impl DirectiveOrchestrator {
/// Phase 3: Monitor running steps and orchestrator tasks.
async fn phase_monitoring(&self) -> Result<(), anyhow::Error> {
- // Check running steps
+ // Check contract-backed running steps first
+ let contract_steps = repository::get_running_steps_with_contracts(&self.pool).await?;
+
+ for step in contract_steps {
+ if let Err(e) = async {
+ match step.contract_status.as_str() {
+ "completed" | "archived" => {
+ tracing::info!(
+ step_id = %step.step_id,
+ directive_id = %step.directive_id,
+ contract_id = %step.contract_id,
+ contract_status = %step.contract_status,
+ "Contract-backed step contract completed — updating step to completed"
+ );
+ let update = crate::db::models::UpdateDirectiveStepRequest {
+ status: Some("completed".to_string()),
+ ..Default::default()
+ };
+ repository::update_directive_step(&self.pool, step.step_id, update).await?;
+
+ // Mark linked orders as done
+ if let Ok(linked_orders) = repository::get_orders_by_step_id(&self.pool, step.step_id).await {
+ for order in linked_orders {
+ if order.status != "done" && order.status != "archived" {
+ let order_update = crate::db::models::UpdateOrderRequest {
+ status: Some("done".to_string()),
+ ..Default::default()
+ };
+ let _ = repository::update_order(&self.pool, order.owner_id, order.id, order_update).await;
+ }
+ }
+ }
+
+ repository::advance_directive_ready_steps(&self.pool, step.directive_id)
+ .await?;
+ repository::check_directive_idle(&self.pool, step.directive_id).await?;
+ }
+ "active" => {
+ // Contract still active — check if the supervisor has failed
+ // by looking at whether there are any failed tasks with no active tasks remaining
+ tracing::debug!(
+ step_id = %step.step_id,
+ contract_id = %step.contract_id,
+ contract_phase = %step.contract_phase,
+ "Contract-backed step still active — monitoring"
+ );
+ }
+ _ => {
+ // Unknown status — log and skip
+ tracing::debug!(
+ step_id = %step.step_id,
+ contract_id = %step.contract_id,
+ contract_status = %step.contract_status,
+ "Contract-backed step in unexpected status"
+ );
+ }
+ }
+ Ok::<(), anyhow::Error>(())
+ }.await {
+ tracing::warn!(step_id = %step.step_id, error = %e, "Error processing contract-backed step — continuing");
+ }
+ }
+
+ // Check task-backed running steps (excludes contract-backed steps)
let running = repository::get_running_steps_with_tasks(&self.pool).await?;
for step in running {
@@ -505,6 +604,142 @@ impl DirectiveOrchestrator {
Ok(())
}
+ /// Spawn a contract for a contract-backed step.
+ /// Creates a contract, adds the directive's repository to it, links it to the step,
+ /// creates a supervisor task, and marks the step as running.
+ async fn spawn_step_contract(
+ &self,
+ step_id: Uuid,
+ directive_id: Uuid,
+ owner_id: Uuid,
+ step_name: &str,
+ step_description: Option<&str>,
+ task_plan: Option<&str>,
+ contract_type: &str,
+ repo_url: Option<&str>,
+ base_branch: Option<&str>,
+ ) -> Result<(), anyhow::Error> {
+ // Build contract description from step info
+ let description = match (step_description, task_plan) {
+ (Some(desc), Some(plan)) => Some(format!("{}\n\n{}", desc, plan)),
+ (Some(desc), None) => Some(desc.to_string()),
+ (None, Some(plan)) => Some(plan.to_string()),
+ (None, None) => None,
+ };
+
+ // Create the contract
+ let contract_req = CreateContractRequest {
+ name: step_name.to_string(),
+ description,
+ contract_type: Some(contract_type.to_string()),
+ template_id: None,
+ initial_phase: None,
+ autonomous_loop: Some(true),
+ phase_guard: None,
+ local_only: None,
+ auto_merge_local: None,
+ };
+
+ let contract = repository::create_contract_for_owner(&self.pool, owner_id, contract_req).await?;
+
+ tracing::info!(
+ step_id = %step_id,
+ contract_id = %contract.id,
+ contract_type = %contract.contract_type,
+ "Created contract for directive step"
+ );
+
+ // Link the contract to the step
+ repository::link_contract_to_step(&self.pool, step_id, contract.id).await?;
+
+ // Add the directive's repository to the contract (if available)
+ if let Some(url) = repo_url {
+ if let Err(e) = repository::add_remote_repository(
+ &self.pool,
+ contract.id,
+ step_name,
+ url,
+ true, // is_primary
+ )
+ .await
+ {
+ tracing::warn!(
+ contract_id = %contract.id,
+ error = %e,
+ "Failed to add repository to contract — continuing without it"
+ );
+ }
+ }
+
+ // Create supervisor task for the contract (following the pattern from contract handlers)
+ let supervisor_name = format!("{} Supervisor", step_name);
+ let supervisor_plan = format!(
+ "You are the supervisor for contract '{}'. Your goal is to drive this contract to completion.\n\n{}",
+ step_name,
+ contract.description.as_deref().unwrap_or("No description provided.")
+ );
+
+ let supervisor_req = CreateTaskRequest {
+ name: supervisor_name.clone(),
+ description: None,
+ plan: supervisor_plan.clone(),
+ repository_url: repo_url.map(|s| s.to_string()),
+ base_branch: base_branch.map(|s| s.to_string()),
+ target_branch: None,
+ parent_task_id: None,
+ contract_id: Some(contract.id),
+ target_repo_path: None,
+ completion_action: None,
+ continue_from_task_id: None,
+ copy_files: None,
+ is_supervisor: true,
+ checkpoint_sha: None,
+ priority: 0,
+ merge_mode: None,
+ branched_from_task_id: None,
+ conversation_history: None,
+ supervisor_worktree_task_id: None,
+ directive_id: Some(directive_id),
+ directive_step_id: Some(step_id),
+ };
+
+ let supervisor_task = repository::create_task_for_owner(&self.pool, owner_id, supervisor_req).await?;
+
+ tracing::info!(
+ contract_id = %contract.id,
+ supervisor_task_id = %supervisor_task.id,
+ "Created supervisor task for contract-backed step"
+ );
+
+ // Link supervisor task to contract
+ let update_req = UpdateContractRequest {
+ supervisor_task_id: Some(supervisor_task.id),
+ version: Some(contract.version),
+ ..Default::default()
+ };
+ if let Err(e) = repository::update_contract_for_owner(&self.pool, contract.id, owner_id, update_req).await {
+ tracing::warn!(
+ contract_id = %contract.id,
+ error = %e,
+ "Failed to link supervisor task to contract"
+ );
+ }
+
+ // Try to dispatch the supervisor task to a daemon
+ if self
+ .try_dispatch_task(supervisor_task.id, owner_id, &supervisor_task.name, &supervisor_task.plan, supervisor_task.version)
+ .await
+ {
+ repository::set_step_running(&self.pool, step_id).await?;
+ } else {
+ // Even if dispatch fails, mark step as running since contract is created.
+ // The supervisor task will be retried by the pending task retry logic.
+ repository::set_step_running(&self.pool, step_id).await?;
+ }
+
+ Ok(())
+ }
+
/// Try to dispatch a task to an available daemon. Returns true if dispatched.
async fn try_dispatch_task(
&self,
@@ -1337,6 +1572,10 @@ For each step, define:
- orderIndex: Execution phase number. Steps only start after ALL steps with a lower orderIndex complete.
Steps with the same orderIndex run in parallel. Use ascending values (0, 1, 2, ...) to create sequential phases.
Use dependsOn for fine-grained control within the same phase.
+- contractType (OPTIONAL): For large, complex work items, set this to create a full contract instead of a
+ standalone task. Valid values: "simple" (Plan → Execute), "specification" (Research → Specify → Plan → Execute → Review),
+ "execute" (Execute only). Only use this for steps that truly need multi-phase orchestration.
+ Most steps should NOT use this — standalone tasks are the default and preferred for typical work.
Submit steps:
makima directive add-step "Step Name" --description "..." --task-plan "..."
diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs
index c840676..0e72bdf 100644
--- a/makima/src/server/handlers/mesh.rs
+++ b/makima/src/server/handlers/mesh.rs
@@ -20,7 +20,7 @@ use crate::db::models::{
use crate::db::repository::{self, RepositoryError};
use crate::server::auth::Authenticated;
use crate::server::messages::ApiError;
-use crate::server::state::{DaemonCommand, SharedState, TaskUpdateNotification};
+use crate::server::state::{DaemonCommand, DaemonReauthStatus, SharedState, TaskUpdateNotification};
// =============================================================================
// Authentication Types
@@ -4283,3 +4283,285 @@ pub async fn restart_daemon(
})
.into_response()
}
+
+// =============================================================================
+// Daemon Reauthorization
+// =============================================================================
+
+/// Response from the trigger reauth endpoint.
+#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
+pub struct TriggerReauthResponse {
+ /// Whether the reauth command was sent successfully.
+ pub success: bool,
+ /// The daemon ID that received the reauth command.
+ #[serde(rename = "daemonId")]
+ pub daemon_id: Uuid,
+ /// Unique request ID for tracking this reauth flow.
+ #[serde(rename = "requestId")]
+ pub request_id: Uuid,
+}
+
+/// Request body for submitting an auth code.
+#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
+pub struct SubmitAuthCodeRequest {
+ /// The auth code obtained from the OAuth login flow.
+ pub code: String,
+ /// The request ID from the trigger reauth response.
+ #[serde(rename = "requestId")]
+ pub request_id: Uuid,
+}
+
+/// Response from the submit auth code endpoint.
+#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
+pub struct SubmitAuthCodeResponse {
+ /// Whether the auth code was sent to the daemon successfully.
+ pub success: bool,
+}
+
+/// Response from the reauth status polling endpoint.
+#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
+pub struct ReauthStatusResponse {
+ /// Current status of the reauth flow: "pending", "url_ready", "completed", "failed"
+ pub status: String,
+ /// OAuth login URL (present when status is "url_ready")
+ #[serde(rename = "loginUrl", skip_serializing_if = "Option::is_none")]
+ pub login_url: Option<String>,
+ /// Error message (present when status is "failed")
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub error: Option<String>,
+}
+
+/// Trigger OAuth re-authentication on a daemon.
+///
+/// Sends a reauth command to the specified daemon, which will spawn `claude setup-token`
+/// and return the OAuth login URL via a status update.
+#[utoipa::path(
+ post,
+ path = "/api/v1/mesh/daemons/{id}/reauth",
+ params(
+ ("id" = Uuid, Path, description = "Daemon ID")
+ ),
+ responses(
+ (status = 200, description = "Reauth command sent", body = TriggerReauthResponse),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Daemon not found or not connected", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Mesh"
+)]
+pub async fn trigger_daemon_reauth(
+ 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 the daemon exists and belongs to this owner
+ match repository::get_daemon_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Daemon not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get daemon {}: {}", id, e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ // Check if daemon is connected
+ if !state.is_daemon_connected(id) {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new(
+ "DAEMON_NOT_CONNECTED",
+ "Daemon is not currently connected",
+ )),
+ )
+ .into_response();
+ }
+
+ // Generate a unique request ID for this reauth flow
+ let request_id = Uuid::new_v4();
+
+ // Initialize the status as "pending"
+ state.set_daemon_reauth_status(id, request_id, "pending".to_string(), None, None);
+
+ // Send reauth command to daemon
+ let command = DaemonCommand::TriggerReauth { request_id };
+ if let Err(e) = state.send_daemon_command(id, command).await {
+ tracing::error!("Failed to send reauth command to daemon {}: {}", id, e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DAEMON_ERROR", e)),
+ )
+ .into_response();
+ }
+
+ tracing::info!(
+ daemon_id = %id,
+ request_id = %request_id,
+ owner_id = %auth.owner_id,
+ "Reauth command sent to daemon"
+ );
+
+ Json(TriggerReauthResponse {
+ success: true,
+ daemon_id: id,
+ request_id,
+ })
+ .into_response()
+}
+
+/// Submit an OAuth auth code to a daemon's pending reauth flow.
+///
+/// Sends the auth code to the daemon, which will forward it to the `claude setup-token` process.
+#[utoipa::path(
+ post,
+ path = "/api/v1/mesh/daemons/{id}/reauth/code",
+ params(
+ ("id" = Uuid, Path, description = "Daemon ID")
+ ),
+ request_body = SubmitAuthCodeRequest,
+ responses(
+ (status = 200, description = "Auth code submitted", body = SubmitAuthCodeResponse),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Daemon not found or not connected", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Mesh"
+)]
+pub async fn submit_daemon_auth_code(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(body): Json<SubmitAuthCodeRequest>,
+) -> 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 the daemon exists and belongs to this owner
+ match repository::get_daemon_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Daemon not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get daemon {}: {}", id, e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ // Check if daemon is connected
+ if !state.is_daemon_connected(id) {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new(
+ "DAEMON_NOT_CONNECTED",
+ "Daemon is not currently connected",
+ )),
+ )
+ .into_response();
+ }
+
+ // Send auth code command to daemon
+ let command = DaemonCommand::SubmitAuthCode {
+ request_id: body.request_id,
+ code: body.code,
+ };
+ if let Err(e) = state.send_daemon_command(id, command).await {
+ tracing::error!("Failed to send auth code to daemon {}: {}", id, e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DAEMON_ERROR", e)),
+ )
+ .into_response();
+ }
+
+ tracing::info!(
+ daemon_id = %id,
+ request_id = %body.request_id,
+ owner_id = %auth.owner_id,
+ "Auth code submitted to daemon"
+ );
+
+ Json(SubmitAuthCodeResponse { success: true }).into_response()
+}
+
+/// Get the status of a daemon reauth request.
+///
+/// Used by the frontend to poll for reauth status updates.
+#[utoipa::path(
+ get,
+ path = "/api/v1/mesh/daemons/{id}/reauth/{request_id}/status",
+ params(
+ ("id" = Uuid, Path, description = "Daemon ID"),
+ ("request_id" = Uuid, Path, description = "Reauth request ID")
+ ),
+ responses(
+ (status = 200, description = "Reauth status", body = ReauthStatusResponse),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Reauth request not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Mesh"
+)]
+pub async fn get_daemon_reauth_status(
+ State(state): State<SharedState>,
+ Authenticated(_auth): Authenticated,
+ Path((id, request_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ match state.get_daemon_reauth_status(id, request_id) {
+ Some(status) => Json(ReauthStatusResponse {
+ status: status.status,
+ login_url: status.login_url,
+ error: status.error,
+ })
+ .into_response(),
+ None => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Reauth request not found")),
+ )
+ .into_response(),
+ }
+}
diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs
index 743a1ca..30439a4 100644
--- a/makima/src/server/handlers/mesh_daemon.rs
+++ b/makima/src/server/handlers/mesh_daemon.rs
@@ -350,6 +350,18 @@ pub enum DaemonMessage {
/// Hostname of the daemon requiring auth
hostname: Option<String>,
},
+ /// Reauth status update (response to TriggerReauth/SubmitAuthCode commands)
+ ReauthStatus {
+ #[serde(rename = "requestId")]
+ request_id: Uuid,
+ /// Status: "url_ready", "completed", "failed"
+ status: String,
+ /// OAuth login URL (present when status is "url_ready")
+ #[serde(rename = "loginUrl")]
+ login_url: Option<String>,
+ /// Error message (present when status is "failed")
+ error: Option<String>,
+ },
/// Response to RetryCompletionAction command
CompletionActionResult {
#[serde(rename = "taskId")]
@@ -1622,6 +1634,25 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re
"OAuth login URL available - user should open this in browser"
);
}
+ Ok(DaemonMessage::ReauthStatus { request_id, status, login_url, error }) => {
+ tracing::info!(
+ daemon_id = %daemon_uuid,
+ request_id = %request_id,
+ status = %status,
+ login_url = ?login_url,
+ error = ?error,
+ "Daemon reauth status update"
+ );
+
+ // Store the reauth status for polling by the frontend
+ state.set_daemon_reauth_status(
+ daemon_uuid,
+ request_id,
+ status.clone(),
+ login_url.clone(),
+ error.clone(),
+ );
+ }
Ok(DaemonMessage::DaemonDirectories { working_directory, home_directory, worktrees_directory }) => {
tracing::info!(
daemon_id = %daemon_uuid,
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index e0f8e7d..b84b90e 100644
--- a/makima/src/server/mod.rs
+++ b/makima/src/server/mod.rs
@@ -98,6 +98,9 @@ pub fn make_router(state: SharedState) -> Router {
.route("/mesh/daemons/directories", get(mesh::get_daemon_directories))
.route("/mesh/daemons/{id}", get(mesh::get_daemon))
.route("/mesh/daemons/{id}/restart", post(mesh::restart_daemon))
+ .route("/mesh/daemons/{id}/reauth", post(mesh::trigger_daemon_reauth))
+ .route("/mesh/daemons/{id}/reauth/code", post(mesh::submit_daemon_auth_code))
+ .route("/mesh/daemons/{id}/reauth/{request_id}/status", get(mesh::get_daemon_reauth_status))
// Merge endpoints for orchestrators
.route("/mesh/tasks/{id}/branches", get(mesh_merge::list_branches))
.route("/mesh/tasks/{id}/merge/start", post(mesh_merge::merge_start))
diff --git a/makima/src/server/state.rs b/makima/src/server/state.rs
index 15fec6b..5c5e24f 100644
--- a/makima/src/server/state.rs
+++ b/makima/src/server/state.rs
@@ -521,6 +521,19 @@ pub enum DaemonCommand {
/// Restart the daemon process
RestartDaemon,
+ /// Trigger OAuth re-authentication on this daemon
+ TriggerReauth {
+ #[serde(rename = "requestId")]
+ request_id: Uuid,
+ },
+
+ /// Submit auth code for pending reauth
+ SubmitAuthCode {
+ #[serde(rename = "requestId")]
+ request_id: Uuid,
+ code: String,
+ },
+
/// Apply a patch to a task's worktree (for cross-daemon merge).
/// Sent by server when routing MergePatchToSupervisor to the supervisor's daemon.
ApplyPatchToWorktree {
@@ -562,6 +575,15 @@ pub struct DaemonConnectionInfo {
pub worktrees_directory: Option<String>,
}
+/// Status of a daemon reauth request (stored in state for polling).
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct DaemonReauthStatus {
+ pub request_id: Uuid,
+ pub status: String,
+ pub login_url: Option<String>,
+ pub error: Option<String>,
+}
+
/// Configuration paths for ML models (used for lazy loading).
#[derive(Clone)]
pub struct ModelConfig {
@@ -616,6 +638,8 @@ pub struct AppState {
pub pending_worktree_info: DashMap<Uuid, oneshot::Sender<WorktreeInfoResponse>>,
/// Lazily-loaded TTS engine (initialized on first Speak connection)
pub tts_engine: OnceCell<Box<dyn TtsEngine>>,
+ /// Daemon reauth status storage (keyed by (daemon_id, request_id))
+ pub daemon_reauth_status: DashMap<(Uuid, Uuid), DaemonReauthStatus>,
}
impl AppState {
@@ -694,6 +718,7 @@ impl AppState {
jwt_verifier,
pending_worktree_info: DashMap::new(),
tts_engine: OnceCell::new(),
+ daemon_reauth_status: DashMap::new(),
}
}
@@ -1201,6 +1226,41 @@ impl AppState {
}
// =========================================================================
+ // Daemon Reauth Status
+ // =========================================================================
+
+ /// Store a daemon reauth status update (from daemon's ReauthStatus message).
+ pub fn set_daemon_reauth_status(
+ &self,
+ daemon_id: Uuid,
+ request_id: Uuid,
+ status: String,
+ login_url: Option<String>,
+ error: Option<String>,
+ ) {
+ self.daemon_reauth_status.insert(
+ (daemon_id, request_id),
+ DaemonReauthStatus {
+ request_id,
+ status,
+ login_url,
+ error,
+ },
+ );
+ }
+
+ /// Get a daemon reauth status (for frontend polling).
+ pub fn get_daemon_reauth_status(
+ &self,
+ daemon_id: Uuid,
+ request_id: Uuid,
+ ) -> Option<DaemonReauthStatus> {
+ self.daemon_reauth_status
+ .get(&(daemon_id, request_id))
+ .map(|entry| entry.value().clone())
+ }
+
+ // =========================================================================
// Supervisor Notifications
// =========================================================================