diff options
Diffstat (limited to 'makima')
34 files changed, 297 insertions, 3017 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx index f44799b..fb95c7f 100644 --- a/makima/frontend/src/components/NavStrip.tsx +++ b/makima/frontend/src/components/NavStrip.tsx @@ -14,7 +14,6 @@ const NAV_LINKS: NavLink[] = [ { label: "Board", href: "/workflow", requiresAuth: true }, { label: "Mesh", href: "/mesh", requiresAuth: true }, { label: "History", href: "/history", requiresAuth: true }, - { label: "Templates", href: "/templates", requiresAuth: true }, ]; export function NavStrip() { diff --git a/makima/frontend/src/components/contracts/ContractList.tsx b/makima/frontend/src/components/contracts/ContractList.tsx index 532ab87..98f8ff6 100644 --- a/makima/frontend/src/components/contracts/ContractList.tsx +++ b/makima/frontend/src/components/contracts/ContractList.tsx @@ -136,11 +136,6 @@ export function ContractList({ Local </span> )} - {contract.redTeamEnabled && ( - <span className="px-1.5 py-0.5 font-mono text-[9px] uppercase text-cyan-400 border border-cyan-400/30 bg-cyan-400/10 shrink-0" title="Red Team monitoring enabled"> - 🔍 Red Team - </span> - )} </div> <span className={`text-[10px] font-mono uppercase shrink-0 ${ diff --git a/makima/frontend/src/components/templates/TemplateEditor.tsx b/makima/frontend/src/components/templates/TemplateEditor.tsx deleted file mode 100644 index c8e1f98..0000000 --- a/makima/frontend/src/components/templates/TemplateEditor.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import { useState } from "react"; -import type { ContractTemplate, Phase, Deliverable } from "../../types/templates"; - -interface Props { - template: ContractTemplate; - onSave: (template: ContractTemplate) => void; - onCancel: () => void; - readOnly?: boolean; -} - -export function TemplateEditor({ template, onSave, onCancel, readOnly = false }: Props) { - const [editedTemplate, setEditedTemplate] = useState<ContractTemplate>({ - ...template, - phases: template.phases.map((p) => ({ - ...p, - deliverables: [...p.deliverables], - })), - }); - const [newDeliverableName, setNewDeliverableName] = useState<{ - [phaseId: string]: string; - }>({}); - - const handlePhaseNameChange = (phaseId: string, newName: string) => { - setEditedTemplate((prev) => ({ - ...prev, - phases: prev.phases.map((p) => - p.id === phaseId ? { ...p, name: newName } : p - ), - })); - }; - - const handleDeliverableNameChange = ( - phaseId: string, - deliverableId: string, - newName: string - ) => { - setEditedTemplate((prev) => ({ - ...prev, - phases: prev.phases.map((p) => - p.id === phaseId - ? { - ...p, - deliverables: p.deliverables.map((d) => - d.id === deliverableId ? { ...d, name: newName } : d - ), - } - : p - ), - })); - }; - - const handleAddDeliverable = (phaseId: string) => { - const name = newDeliverableName[phaseId]?.trim(); - if (!name) return; - - const newDeliverable: Deliverable = { - id: `deliverable-${Date.now()}`, - name, - }; - - setEditedTemplate((prev) => ({ - ...prev, - phases: prev.phases.map((p) => - p.id === phaseId - ? { ...p, deliverables: [...p.deliverables, newDeliverable] } - : p - ), - })); - setNewDeliverableName((prev) => ({ ...prev, [phaseId]: "" })); - }; - - const handleRemoveDeliverable = (phaseId: string, deliverableId: string) => { - setEditedTemplate((prev) => ({ - ...prev, - phases: prev.phases.map((p) => - p.id === phaseId - ? { - ...p, - deliverables: p.deliverables.filter((d) => d.id !== deliverableId), - } - : p - ), - })); - }; - - const handleAddPhase = () => { - const newPhase: Phase = { - id: `phase-${Date.now()}`, - name: "New Phase", - deliverables: [], - }; - setEditedTemplate((prev) => ({ - ...prev, - phases: [...prev.phases, newPhase], - })); - }; - - const handleRemovePhase = (phaseId: string) => { - setEditedTemplate((prev) => ({ - ...prev, - phases: prev.phases.filter((p) => p.id !== phaseId), - })); - }; - - return ( - <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] p-6"> - {/* Header */} - <div className="mb-6 pb-4 border-b border-[rgba(117,170,252,0.15)]"> - <h2 className="text-sm font-mono uppercase tracking-wide text-[#9bc3ff] mb-1"> - {readOnly ? "View" : "Edit"} Template: {template.name} - </h2> - <p className="text-xs font-mono text-[#75aafc] opacity-70"> - {template.description} - </p> - {readOnly && ( - <p className="text-xs font-mono text-amber-400 mt-2"> - Built-in templates are read-only - </p> - )} - </div> - - {/* Phases */} - <div className="space-y-4 mb-6"> - {editedTemplate.phases.map((phase, phaseIndex) => ( - <div - key={phase.id} - className="bg-[rgba(0,0,0,0.3)] border border-[rgba(117,170,252,0.2)] p-4" - > - {/* Phase Header */} - <div className="flex items-center gap-3 mb-3"> - <span className="w-6 h-6 flex items-center justify-center bg-[rgba(117,170,252,0.2)] text-[#9bc3ff] text-xs font-mono"> - {phaseIndex + 1} - </span> - <input - type="text" - className="flex-1 px-3 py-1.5 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3] disabled:opacity-60" - value={phase.name} - onChange={(e) => handlePhaseNameChange(phase.id, e.target.value)} - placeholder="Phase name" - disabled={readOnly} - /> - {!template.isBuiltIn && ( - <button - type="button" - onClick={() => handleRemovePhase(phase.id)} - className="w-7 h-7 flex items-center justify-center border border-[rgba(255,100,100,0.3)] text-[#ff6464] hover:bg-[rgba(255,100,100,0.1)] transition-colors text-sm" - title="Remove phase" - > - x - </button> - )} - </div> - - {/* Deliverables */} - <div className="ml-9 space-y-2"> - {phase.deliverables.length === 0 ? ( - <div className="text-xs font-mono text-[#556677] italic"> - No deliverables - </div> - ) : ( - phase.deliverables.map((deliverable) => ( - <div - key={deliverable.id} - className="flex items-center gap-2" - > - <span className="text-[#75aafc] text-xs">-</span> - <input - type="text" - className="flex-1 px-2 py-1 bg-transparent border border-[rgba(117,170,252,0.15)] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#3f6fb3]" - value={deliverable.name} - onChange={(e) => - handleDeliverableNameChange( - phase.id, - deliverable.id, - e.target.value - ) - } - /> - <button - type="button" - onClick={() => - handleRemoveDeliverable(phase.id, deliverable.id) - } - className="w-5 h-5 flex items-center justify-center text-[#ff6464] hover:bg-[rgba(255,100,100,0.1)] transition-colors text-xs" - title="Remove deliverable" - > - x - </button> - </div> - )) - )} - - {/* Add Deliverable */} - <div className="flex items-center gap-2 pt-2"> - <input - type="text" - className="flex-1 px-2 py-1 bg-transparent border border-[rgba(117,170,252,0.15)] text-[#dbe7ff] font-mono text-xs placeholder-[#445566] focus:outline-none focus:border-[#3f6fb3]" - placeholder="New deliverable name..." - value={newDeliverableName[phase.id] || ""} - onChange={(e) => - setNewDeliverableName((prev) => ({ - ...prev, - [phase.id]: e.target.value, - })) - } - onKeyPress={(e) => { - if (e.key === "Enter") { - handleAddDeliverable(phase.id); - } - }} - /> - <button - type="button" - onClick={() => handleAddDeliverable(phase.id)} - className="px-2 py-1 border border-[rgba(117,170,252,0.3)] text-[#9bc3ff] font-mono text-xs hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.1)] transition-colors" - > - + Add - </button> - </div> - </div> - </div> - ))} - </div> - - {/* Add Phase (only for custom templates) */} - {!template.isBuiltIn && ( - <button - type="button" - onClick={handleAddPhase} - className="w-full mb-6 px-4 py-2 border border-dashed border-[rgba(117,170,252,0.3)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors" - > - + Add Phase - </button> - )} - - {/* Footer Actions */} - <div className="flex gap-3 justify-end pt-4 border-t border-[rgba(117,170,252,0.15)]"> - <button - type="button" - onClick={onCancel} - className="px-4 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors" - > - {readOnly ? "Close" : "Cancel"} - </button> - {!readOnly && ( - <button - type="button" - onClick={() => onSave(editedTemplate)} - className="px-4 py-2 border border-[#3f6fb3] bg-[rgba(117,170,252,0.15)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:bg-[rgba(117,170,252,0.25)] transition-colors" - > - Save Changes - </button> - )} - </div> - </div> - ); -} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index e8b3d8a..f148d76 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -1659,45 +1659,9 @@ export interface DeliverableDefinition { priority: "required" | "recommended" | "optional"; } -/** Request to create a custom contract type template */ -export interface CreateTemplateRequest { - name: string; - description?: string; - phases: PhaseDefinition[]; - defaultPhase: string; - deliverables?: Record<string, DeliverableDefinition[]>; -} - -/** Request to update a custom contract type template */ -export interface UpdateTemplateRequest { - name?: string; - description?: string; - phases?: PhaseDefinition[]; - defaultPhase?: string; - deliverables?: Record<string, DeliverableDefinition[]>; - version?: number; -} - -/** Custom template record from the API */ -export interface ContractTypeTemplateRecord { - id: string; - name: string; - description: string | null; - phases: PhaseDefinition[]; - defaultPhase: string; - isBuiltin: boolean; - version: number; - createdAt: string; -} - -/** Response for single template operations */ -export interface TemplateResponse { - template: ContractTypeTemplateRecord; -} - /** - * List available contract types/templates. - * Returns built-in types (simple, specification) and any custom types. + * List available contract types. + * Returns built-in types only (simple, specification, execute). */ export async function listContractTypes(): Promise<ListContractTypesResponse> { const res = await authFetch(`${API_BASE}/api/v1/contract-types`); @@ -1707,66 +1671,6 @@ export async function listContractTypes(): Promise<ListContractTypesResponse> { return res.json(); } -/** - * Create a new custom contract type template. - */ -export async function createContractTemplate( - req: CreateTemplateRequest -): Promise<TemplateResponse> { - const res = await authFetch(`${API_BASE}/api/v1/contract-types`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(req), - }); - if (!res.ok) { - const err = await res.json().catch(() => ({ message: res.statusText })); - throw new Error(err.message || `Failed to create template: ${res.statusText}`); - } - return res.json(); -} - -/** - * Get a custom contract type template by ID. - */ -export async function getContractTemplate(id: string): Promise<TemplateResponse> { - const res = await authFetch(`${API_BASE}/api/v1/contract-types/${id}`); - if (!res.ok) { - throw new Error(`Failed to get template: ${res.statusText}`); - } - return res.json(); -} - -/** - * Update a custom contract type template. - */ -export async function updateContractTemplate( - id: string, - req: UpdateTemplateRequest -): Promise<TemplateResponse> { - const res = await authFetch(`${API_BASE}/api/v1/contract-types/${id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(req), - }); - if (!res.ok) { - const err = await res.json().catch(() => ({ message: res.statusText })); - throw new Error(err.message || `Failed to update template: ${res.statusText}`); - } - return res.json(); -} - -/** - * Delete a custom contract type template. - */ -export async function deleteContractTemplate(id: string): Promise<void> { - const res = await authFetch(`${API_BASE}/api/v1/contract-types/${id}`, { - method: "DELETE", - }); - if (!res.ok) { - throw new Error(`Failed to delete template: ${res.statusText}`); - } -} - export interface ContractRepository { id: string; contractId: string; @@ -1792,8 +1696,6 @@ export interface ContractSummary { supervisorTaskId: string | null; /** When true, tasks won't auto-push or create PRs - use patch files instead */ localOnly: boolean; - /** When true, a red team task monitors work output for quality */ - redTeamEnabled: boolean; fileCount: number; taskCount: number; repositoryCount: number; @@ -1818,10 +1720,6 @@ export interface Contract { phaseGuard: boolean; /** When true, tasks won't auto-push or create PRs - use patch files instead */ localOnly: boolean; - /** When true, a red team task monitors work output for quality */ - redTeamEnabled: boolean; - /** Custom criteria for the red team to evaluate */ - redTeamPrompt: string | null; version: number; createdAt: string; updatedAt: string; @@ -1859,10 +1757,6 @@ export interface CreateContractRequest { initialPhase?: ContractPhase | string; /** When true, tasks won't auto-push or create PRs - use patch files instead */ localOnly?: boolean; - /** When true, spawn a red team task to monitor work output */ - redTeamEnabled?: boolean; - /** Custom criteria for the red team to evaluate */ - redTeamPrompt?: string; } export interface UpdateContractRequest { diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx index ef1ba5c..50fffe4 100644 --- a/makima/frontend/src/main.tsx +++ b/makima/frontend/src/main.tsx @@ -18,7 +18,6 @@ import HistoryPage from "./routes/history"; import LoginPage from "./routes/login"; import SettingsPage from "./routes/settings"; import ContractFilePage from "./routes/contract-file"; -import TemplatesPage from "./routes/templates"; import SpeakPage from "./routes/speak"; createRoot(document.getElementById("root")!).render( @@ -129,14 +128,6 @@ createRoot(document.getElementById("root")!).render( } /> <Route - path="/templates" - element={ - <ProtectedRoute> - <TemplatesPage /> - </ProtectedRoute> - } - /> - <Route path="/speak" element={ <ProtectedRoute> diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx index 8dcfe34..dde78b1 100644 --- a/makima/frontend/src/routes/contracts.tsx +++ b/makima/frontend/src/routes/contracts.tsx @@ -93,8 +93,6 @@ function ContractsPageContent() { const [contractTypes, setContractTypes] = useState<ContractTypeTemplate[]>([]); const [contractTypesLoading, setContractTypesLoading] = useState(false); const [localOnly, setLocalOnly] = useState(false); - const [redTeamEnabled, setRedTeamEnabled] = useState(false); - const [redTeamPrompt, setRedTeamPrompt] = useState(""); // Fetch contract types when modal opens - API returns both built-in and custom templates useEffect(() => { @@ -238,8 +236,6 @@ function ContractsPageContent() { templateId: isCustomTemplate ? contractType : undefined, initialPhase: initialPhase !== defaultPhaseForType ? initialPhase : undefined, localOnly: localOnly || undefined, - redTeamEnabled: redTeamEnabled || undefined, - redTeamPrompt: redTeamEnabled && redTeamPrompt.trim() ? redTeamPrompt.trim() : undefined, }; try { @@ -315,8 +311,6 @@ function ContractsPageContent() { setRepoUrl(""); setRepoPath(""); setLocalOnly(false); - setRedTeamEnabled(false); - setRedTeamPrompt(""); setCreateError(null); }, []); @@ -689,58 +683,6 @@ function ContractsPageContent() { </div> {/* Red Team Monitoring */} - <div className="border-t border-[rgba(117,170,252,0.2)] pt-4"> - <div className="flex items-center gap-3"> - <button - type="button" - onClick={() => setRedTeamEnabled(!redTeamEnabled)} - className={`w-5 h-5 flex items-center justify-center border transition-colors ${ - redTeamEnabled - ? "bg-[#0f3c78] border-[#75aafc] text-[#dbe7ff]" - : "bg-[#0d1b2d] border-[#3f6fb3] text-transparent" - }`} - > - {redTeamEnabled && ( - <svg - xmlns="http://www.w3.org/2000/svg" - width="12" - height="12" - viewBox="0 0 24 24" - fill="none" - stroke="currentColor" - strokeWidth="3" - strokeLinecap="round" - strokeLinejoin="round" - > - <polyline points="20 6 9 17 4 12" /> - </svg> - )} - </button> - <label - className="font-mono text-sm text-[#dbe7ff] cursor-pointer select-none" - onClick={() => setRedTeamEnabled(!redTeamEnabled)} - > - Enable Red Team Monitoring - </label> - </div> - <p className="font-mono text-xs text-[#8b949e] pl-8"> - Spawns a parallel task to monitor work output for quality and compliance. - </p> - {redTeamEnabled && ( - <div className="mt-3 pl-8"> - <label className="block font-mono text-xs text-[#75aafc] uppercase mb-2"> - Custom Review Criteria (Optional) - </label> - <textarea - value={redTeamPrompt} - onChange={(e) => setRedTeamPrompt(e.target.value)} - placeholder="e.g., 'Focus on security best practices' or 'Ensure all functions have tests'" - className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] text-sm font-mono h-20 resize-none focus:border-[#75aafc] focus:outline-none" - /> - </div> - )} - </div> - {/* Repository Configuration */} <div className="border-t border-[rgba(117,170,252,0.2)] pt-4"> <label className="block font-mono text-xs text-[#75aafc] uppercase mb-3"> diff --git a/makima/frontend/src/routes/templates.tsx b/makima/frontend/src/routes/templates.tsx deleted file mode 100644 index b2c9974..0000000 --- a/makima/frontend/src/routes/templates.tsx +++ /dev/null @@ -1,388 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { useNavigate } from "react-router"; -import { Masthead } from "../components/Masthead"; -import { TemplateEditor } from "../components/templates/TemplateEditor"; -import { useAuth } from "../contexts/AuthContext"; -import type { ContractTemplate } from "../types/templates"; -import { DEFAULT_TEMPLATES } from "../types/templates"; -import { - listContractTypes, - createContractTemplate, - updateContractTemplate, - deleteContractTemplate, - type PhaseDefinition, - type DeliverableDefinition, -} from "../lib/api"; - -export default function TemplatesPage() { - const navigate = useNavigate(); - const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); - - const [templates, setTemplates] = useState<ContractTemplate[]>(DEFAULT_TEMPLATES); - const [loading, setLoading] = useState(true); - const [error, setError] = useState<string | null>(null); - const [saving, setSaving] = useState(false); - - const [editingTemplate, setEditingTemplate] = useState<ContractTemplate | null>( - null - ); - const [showNewTemplateForm, setShowNewTemplateForm] = useState(false); - const [newTemplateName, setNewTemplateName] = useState(""); - const [newTemplateDescription, setNewTemplateDescription] = useState(""); - - // Redirect to login if not authenticated - useEffect(() => { - if (!authLoading && isAuthConfigured && !isAuthenticated) { - navigate("/login"); - } - }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); - - // Fetch templates from API - const fetchTemplates = useCallback(async () => { - try { - setLoading(true); - setError(null); - const response = await listContractTypes(); - - // Convert API response to ContractTemplate format - const apiTemplates: ContractTemplate[] = response.contractTypes.map((t) => ({ - id: t.id, - name: t.name, - description: t.description, - isBuiltIn: t.isBuiltin, - phases: t.phases.map((phaseId) => ({ - id: phaseId, - name: t.phaseNames?.[phaseId] || phaseId.charAt(0).toUpperCase() + phaseId.slice(1), - deliverables: [], // Deliverables are managed server-side - })), - })); - - // Merge with DEFAULT_TEMPLATES to ensure we have full phase/deliverable info for built-ins - const mergedTemplates = apiTemplates.map((apiTemplate) => { - const defaultTemplate = DEFAULT_TEMPLATES.find((d) => d.id === apiTemplate.id); - if (defaultTemplate && apiTemplate.isBuiltIn) { - return defaultTemplate; // Use the richer default template for built-ins - } - return apiTemplate; - }); - - setTemplates(mergedTemplates); - } catch (err) { - console.error("Failed to fetch templates:", err); - setError(err instanceof Error ? err.message : "Failed to fetch templates"); - // Fall back to default templates - setTemplates(DEFAULT_TEMPLATES); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - if (!authLoading && isAuthenticated) { - fetchTemplates(); - } else if (!authLoading && !isAuthConfigured) { - // No auth configured, just show defaults - setTemplates(DEFAULT_TEMPLATES); - setLoading(false); - } - }, [authLoading, isAuthenticated, isAuthConfigured, fetchTemplates]); - - const handleSaveTemplate = async (updatedTemplate: ContractTemplate) => { - if (updatedTemplate.isBuiltIn) { - // Built-in templates are read-only, just close the editor - setEditingTemplate(null); - return; - } - - try { - setSaving(true); - setError(null); - - // Convert to API format - const phases: PhaseDefinition[] = updatedTemplate.phases.map((p, index) => ({ - id: p.id, - name: p.name, - order: index, - })); - - const deliverables: Record<string, DeliverableDefinition[]> = {}; - for (const phase of updatedTemplate.phases) { - if (phase.deliverables.length > 0) { - deliverables[phase.id] = phase.deliverables.map((d) => ({ - id: d.id, - name: d.name, - priority: "required" as const, - })); - } - } - - await updateContractTemplate(updatedTemplate.id, { - name: updatedTemplate.name, - description: updatedTemplate.description, - phases, - defaultPhase: phases[0]?.id || "execute", - deliverables: Object.keys(deliverables).length > 0 ? deliverables : undefined, - }); - - // Refresh templates from server - await fetchTemplates(); - setEditingTemplate(null); - } catch (err) { - console.error("Failed to update template:", err); - setError(err instanceof Error ? err.message : "Failed to update template"); - } finally { - setSaving(false); - } - }; - - const handleCreateTemplate = async () => { - if (!newTemplateName.trim()) return; - - try { - setSaving(true); - setError(null); - - const phases: PhaseDefinition[] = [ - { id: "execute", name: "Execute", order: 0 }, - ]; - - await createContractTemplate({ - name: newTemplateName.trim(), - description: newTemplateDescription.trim() || "Custom contract template", - phases, - defaultPhase: "execute", - }); - - // Refresh templates from server - await fetchTemplates(); - - setNewTemplateName(""); - setNewTemplateDescription(""); - setShowNewTemplateForm(false); - } catch (err) { - console.error("Failed to create template:", err); - setError(err instanceof Error ? err.message : "Failed to create template"); - } finally { - setSaving(false); - } - }; - - const handleDeleteTemplate = async (templateId: string) => { - const template = templates.find((t) => t.id === templateId); - if (template?.isBuiltIn) return; - - if (window.confirm(`Are you sure you want to delete "${template?.name}"?`)) { - try { - setSaving(true); - setError(null); - await deleteContractTemplate(templateId); - await fetchTemplates(); - } catch (err) { - console.error("Failed to delete template:", err); - setError(err instanceof Error ? err.message : "Failed to delete template"); - } finally { - setSaving(false); - } - } - }; - - const handleRefresh = () => { - fetchTemplates(); - }; - - // Show loading state - if (authLoading || loading) { - return ( - <div className="relative z-10 min-h-screen flex items-center justify-center bg-[#0a1628]"> - <div className="text-[#75aafc] font-mono text-sm animate-pulse"> - Loading... - </div> - </div> - ); - } - - // Editor view - if (editingTemplate) { - return ( - <div className="relative z-10 min-h-screen bg-[#0a1628]"> - <Masthead /> - <main className="max-w-4xl mx-auto px-4 py-6"> - <TemplateEditor - template={editingTemplate} - onSave={handleSaveTemplate} - onCancel={() => setEditingTemplate(null)} - readOnly={editingTemplate.isBuiltIn} - /> - </main> - </div> - ); - } - - return ( - <div className="relative z-10 min-h-screen bg-[#0a1628]"> - <Masthead /> - <main className="max-w-6xl mx-auto px-4 py-6"> - {/* Error display */} - {error && ( - <div className="mb-4 p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-xs"> - {error} - <button - type="button" - onClick={() => setError(null)} - className="ml-2 text-red-400/70 hover:text-red-400" - > - Dismiss - </button> - </div> - )} - - {/* Header */} - <div className="flex justify-between items-start mb-6 pb-4 border-b border-[rgba(117,170,252,0.15)]"> - <div> - <h1 className="text-lg font-mono uppercase tracking-wide text-[#9bc3ff] mb-1"> - Contract Templates - </h1> - <p className="text-xs font-mono text-[#75aafc] opacity-70"> - Manage contract types and their phase deliverables - </p> - </div> - <div className="flex gap-3"> - <button - type="button" - onClick={handleRefresh} - disabled={saving} - className="px-3 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors disabled:opacity-50" - > - Refresh - </button> - <button - type="button" - onClick={() => setShowNewTemplateForm(true)} - disabled={saving} - className="px-3 py-2 border border-[#3f6fb3] bg-[rgba(117,170,252,0.15)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:bg-[rgba(117,170,252,0.25)] transition-colors disabled:opacity-50" - > - + New Template - </button> - </div> - </div> - - {/* New Template Form */} - {showNewTemplateForm && ( - <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] p-4 mb-6"> - <div className="flex gap-3 items-center"> - <input - type="text" - className="flex-1 px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]" - placeholder="Template name..." - value={newTemplateName} - onChange={(e) => setNewTemplateName(e.target.value)} - disabled={saving} - /> - <input - type="text" - className="flex-1 px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]" - placeholder="Description (optional)..." - value={newTemplateDescription} - onChange={(e) => setNewTemplateDescription(e.target.value)} - disabled={saving} - /> - <button - type="button" - onClick={handleCreateTemplate} - disabled={saving || !newTemplateName.trim()} - className="px-4 py-2 border border-[#3f6fb3] bg-[rgba(117,170,252,0.15)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:bg-[rgba(117,170,252,0.25)] transition-colors disabled:opacity-50" - > - {saving ? "Creating..." : "Create"} - </button> - <button - type="button" - onClick={() => setShowNewTemplateForm(false)} - disabled={saving} - className="px-4 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors disabled:opacity-50" - > - Cancel - </button> - </div> - </div> - )} - - {/* Templates Grid */} - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> - {templates.map((template) => ( - <div - key={template.id} - className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.25)] hover:border-[rgba(117,170,252,0.45)] transition-colors" - > - {/* Card Header */} - <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.15)] flex items-center justify-between"> - <h3 className="font-mono text-sm text-white">{template.name}</h3> - {template.isBuiltIn ? ( - <span className="px-2 py-0.5 bg-[rgba(117,170,252,0.15)] text-[#75aafc] font-mono text-[10px] uppercase tracking-wide"> - Built-in - </span> - ) : ( - <span className="px-2 py-0.5 bg-[rgba(100,200,100,0.15)] text-[#7bc97b] font-mono text-[10px] uppercase tracking-wide"> - Custom - </span> - )} - </div> - - {/* Card Body */} - <div className="px-4 py-3"> - <p className="text-xs font-mono text-[#75aafc] opacity-70 mb-4 min-h-[2.5rem]"> - {template.description} - </p> - - {/* Phases */} - <div className="space-y-2 mb-4"> - {template.phases.map((phase, index) => ( - <div key={phase.id} className="flex items-start gap-2"> - <div className="flex flex-col items-center"> - <span className="w-2 h-2 rounded-full bg-[#3f6fb3]" /> - {index < template.phases.length - 1 && ( - <span className="w-px h-4 bg-[rgba(117,170,252,0.25)]" /> - )} - </div> - <div className="flex-1 min-w-0"> - <span className="text-xs font-mono text-[#9bc3ff]"> - {phase.name} - </span> - <span className="text-[10px] font-mono text-[#556677] ml-2"> - {phase.deliverables.length === 0 - ? "(no deliverables)" - : phase.deliverables.map((d) => d.name).join(", ")} - </span> - </div> - </div> - ))} - </div> - </div> - - {/* Card Footer */} - <div className="px-4 py-3 border-t border-[rgba(117,170,252,0.15)] flex gap-2"> - <button - type="button" - onClick={() => setEditingTemplate(template)} - disabled={saving} - className="flex-1 px-3 py-1.5 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors disabled:opacity-50" - > - {template.isBuiltIn ? "View" : "Edit"} - </button> - {!template.isBuiltIn && ( - <button - type="button" - onClick={() => handleDeleteTemplate(template.id)} - disabled={saving} - className="px-3 py-1.5 border border-[rgba(255,100,100,0.25)] text-[#ff6464] font-mono text-xs uppercase tracking-wide hover:border-[rgba(255,100,100,0.5)] hover:bg-[rgba(255,100,100,0.05)] transition-colors disabled:opacity-50" - > - Delete - </button> - )} - </div> - </div> - ))} - </div> - </main> - </div> - ); -} diff --git a/makima/frontend/src/types/templates.ts b/makima/frontend/src/types/templates.ts deleted file mode 100644 index ca337c5..0000000 --- a/makima/frontend/src/types/templates.ts +++ /dev/null @@ -1,90 +0,0 @@ -// Contract Template types -export interface Deliverable { - id: string; - name: string; -} - -export interface Phase { - id: string; - name: string; - deliverables: Deliverable[]; -} - -export interface ContractTemplate { - id: string; - name: string; - description: string; - phases: Phase[]; - isBuiltIn: boolean; -} - -// Default built-in templates -// NOTE: Deliverable IDs must match backend phase_guidance.rs exactly -export const DEFAULT_TEMPLATES: ContractTemplate[] = [ - { - id: "simple", - name: "Simple", - description: "A simple contract with plan and execute phases.", - isBuiltIn: true, - phases: [ - { - id: "plan", - name: "Plan", - deliverables: [{ id: "plan-document", name: "Plan" }], - }, - { - id: "execute", - name: "Execute", - deliverables: [{ id: "pull-request", name: "Pull Request" }], - }, - ], - }, - { - id: "specification", - name: "Specification", - description: - "A comprehensive contract with research, specification, planning, execution, and review phases.", - isBuiltIn: true, - phases: [ - { - id: "research", - name: "Research", - deliverables: [{ id: "research-notes", name: "Research Notes" }], - }, - { - id: "specify", - name: "Specify", - deliverables: [{ id: "requirements-document", name: "Requirements Document" }], - }, - { - id: "plan", - name: "Plan", - deliverables: [{ id: "plan-document", name: "Plan" }], - }, - { - id: "execute", - name: "Execute", - deliverables: [{ id: "pull-request", name: "Pull Request" }], - }, - { - id: "review", - name: "Review", - deliverables: [{ id: "release-notes", name: "Release Notes" }], - }, - ], - }, - { - id: "execute", - name: "Execute", - description: - "A minimal contract with only an execute phase and no deliverables.", - isBuiltIn: true, - phases: [ - { - id: "execute", - name: "Execute", - deliverables: [], - }, - ], - }, -]; diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo index 3aa9cbf..afddaf9 100644 --- a/makima/frontend/tsconfig.tsbuildinfo +++ b/makima/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/templates/templateeditor.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/routes/templates.tsx","./src/routes/workflow.tsx","./src/types/messages.ts","./src/types/templates.ts"],"version":"5.9.3"}
\ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"}
\ No newline at end of file diff --git a/makima/migrations/20260202000000_remove_red_team_system.sql b/makima/migrations/20260202000000_remove_red_team_system.sql new file mode 100644 index 0000000..0a5bbf8 --- /dev/null +++ b/makima/migrations/20260202000000_remove_red_team_system.sql @@ -0,0 +1,15 @@ +-- Remove red team system +-- This migration drops all red team related tables, columns, and indexes + +-- Drop red team notifications table +DROP TABLE IF EXISTS red_team_notifications; + +-- Drop red team index on tasks +DROP INDEX IF EXISTS idx_tasks_contract_red_team; + +-- Remove red team column from tasks +ALTER TABLE tasks DROP COLUMN IF EXISTS is_red_team; + +-- Remove red team columns from contracts +ALTER TABLE contracts DROP COLUMN IF EXISTS red_team_enabled; +ALTER TABLE contracts DROP COLUMN IF EXISTS red_team_prompt; diff --git a/makima/migrations/20260202100000_add_contract_worktree.sql b/makima/migrations/20260202100000_add_contract_worktree.sql new file mode 100644 index 0000000..26c54ba --- /dev/null +++ b/makima/migrations/20260202100000_add_contract_worktree.sql @@ -0,0 +1,13 @@ +-- Add worktree tracking to contracts +-- Each contract now has a single worktree managed by a specific daemon + +ALTER TABLE contracts + ADD COLUMN worktree_path VARCHAR(512), + ADD COLUMN worktree_daemon_id UUID REFERENCES daemons(id), + ADD COLUMN worktree_base_branch VARCHAR(255), + ADD COLUMN worktree_branch VARCHAR(255); + +COMMENT ON COLUMN contracts.worktree_path IS 'Path to the worktree directory on the assigned daemon'; +COMMENT ON COLUMN contracts.worktree_daemon_id IS 'The daemon that owns/manages this contract worktree'; +COMMENT ON COLUMN contracts.worktree_base_branch IS 'The base branch the worktree was created from'; +COMMENT ON COLUMN contracts.worktree_branch IS 'The working branch in the worktree'; diff --git a/makima/migrations/20260202200000_remove_custom_templates.sql b/makima/migrations/20260202200000_remove_custom_templates.sql new file mode 100644 index 0000000..0c80110 --- /dev/null +++ b/makima/migrations/20260202200000_remove_custom_templates.sql @@ -0,0 +1,4 @@ +-- Remove custom templates system +-- Only built-in contract types (simple, specification, execute) are supported now + +DROP TABLE IF EXISTS contract_type_templates; diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index 753f60e..af9832b 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use makima::daemon::api::{ApiClient, CreateContractRequest}; use makima::daemon::cli::{ - Cli, CliConfig, Commands, ConfigCommand, ContractCommand, RedTeamCommand, SupervisorCommand, ViewArgs, + Cli, CliConfig, Commands, ConfigCommand, ContractCommand, SupervisorCommand, ViewArgs, }; use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, WsEvent, OutputLine, OutputMessageType, WsConnectionState, RepositorySuggestion}; use makima::daemon::config::{DaemonConfig, RepoEntry}; @@ -30,7 +30,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { Commands::Contract(cmd) => run_contract(cmd).await, Commands::View(args) => run_view(args).await, Commands::Config(cmd) => run_config(cmd).await, - Commands::RedTeam(cmd) => run_red_team(cmd).await, } } @@ -352,7 +351,6 @@ async fn run_supervisor( contract_id: args.common.contract_id, parent_task_id: args.parent, checkpoint_sha: args.checkpoint, - use_own_worktree: args.own_worktree, }; let result = client.supervisor_spawn(req).await?; println!("{}", serde_json::to_string(&result.0)?); @@ -795,16 +793,6 @@ async fn run_config(cmd: ConfigCommand) -> Result<(), Box<dyn std::error::Error } } -/// Run red team commands. -async fn run_red_team(cmd: RedTeamCommand) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { - match cmd { - RedTeamCommand::Notify(args) => { - makima::daemon::cli::handle_notify(args).await?; - Ok(()) - } - } -} - /// Load contracts from API async fn load_contracts(client: &ApiClient) -> Result<Vec<ListItem>, Box<dyn std::error::Error + Send + Sync>> { let result = client.list_contracts().await?; @@ -1115,8 +1103,6 @@ async fn run_tui_loop( phase_guard: None, local_only: None, auto_merge_local: None, - red_team_enabled: None, - red_team_prompt: None, }; match client.create_contract(req).await { diff --git a/makima/src/daemon/api/contract.rs b/makima/src/daemon/api/contract.rs index 7c76b40..e128318 100644 --- a/makima/src/daemon/api/contract.rs +++ b/makima/src/daemon/api/contract.rs @@ -70,10 +70,6 @@ pub struct CreateContractRequest { pub local_only: Option<bool>, #[serde(skip_serializing_if = "Option::is_none")] pub auto_merge_local: Option<bool>, - #[serde(skip_serializing_if = "Option::is_none")] - pub red_team_enabled: Option<bool>, - #[serde(skip_serializing_if = "Option::is_none")] - pub red_team_prompt: Option<String>, } impl ApiClient { diff --git a/makima/src/daemon/api/mod.rs b/makima/src/daemon/api/mod.rs index 92e34e9..49d80e0 100644 --- a/makima/src/daemon/api/mod.rs +++ b/makima/src/daemon/api/mod.rs @@ -2,9 +2,7 @@ pub mod client; pub mod contract; -pub mod red_team; pub mod supervisor; pub use client::ApiClient; pub use contract::CreateContractRequest; -pub use red_team::RedTeamNotifyRequest; diff --git a/makima/src/daemon/api/red_team.rs b/makima/src/daemon/api/red_team.rs deleted file mode 100644 index 6d3c969..0000000 --- a/makima/src/daemon/api/red_team.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Red team API methods. - -use serde::Serialize; -use uuid::Uuid; - -use super::client::{ApiClient, ApiError}; -use super::supervisor::JsonValue; - -/// Request body for red team notify endpoint. -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct RedTeamNotifyRequest { - /// The issue message - pub message: String, - - /// Severity level: low, medium, high, critical - pub severity: String, - - /// The specific task this relates to (optional) - #[serde(skip_serializing_if = "Option::is_none")] - pub related_task_id: Option<Uuid>, - - /// The file path where the issue was detected (optional) - #[serde(skip_serializing_if = "Option::is_none")] - pub file_path: Option<String>, - - /// Additional context about the issue (optional) - #[serde(skip_serializing_if = "Option::is_none")] - pub context: Option<String>, -} - -impl ApiClient { - /// Send a red team notification about an issue found during adversarial review. - /// - /// POST /api/v1/mesh/red-team/notify - pub async fn red_team_notify(&self, req: RedTeamNotifyRequest) -> Result<JsonValue, ApiError> { - self.post("/api/v1/mesh/red-team/notify", &req).await - } -} diff --git a/makima/src/daemon/api/supervisor.rs b/makima/src/daemon/api/supervisor.rs index c2da1db..c67c9ca 100644 --- a/makima/src/daemon/api/supervisor.rs +++ b/makima/src/daemon/api/supervisor.rs @@ -17,10 +17,6 @@ pub struct SpawnTaskRequest { pub parent_task_id: Option<Uuid>, #[serde(skip_serializing_if = "Option::is_none")] pub checkpoint_sha: Option<String>, - /// If true, create a separate worktree for the task (requires merge after). - /// If false (default), the task shares the supervisor's worktree. - #[serde(default)] - pub use_own_worktree: bool, } #[derive(Serialize)] diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs index c848e8e..0805edd 100644 --- a/makima/src/daemon/cli/mod.rs +++ b/makima/src/daemon/cli/mod.rs @@ -3,18 +3,15 @@ pub mod config; pub mod contract; pub mod daemon; -pub mod red_team; pub mod server; pub mod supervisor; pub mod view; -use clap::{Args, Parser, Subcommand}; -use uuid::Uuid; +use clap::{Parser, Subcommand}; pub use config::CliConfig; pub use contract::ContractArgs; pub use daemon::DaemonArgs; -pub use red_team::handle_notify; pub use server::ServerArgs; pub use supervisor::SupervisorArgs; pub use view::ViewArgs; @@ -61,10 +58,6 @@ pub enum Commands { /// Saves configuration to ~/.makima/config.toml for use by CLI commands. #[command(subcommand)] Config(ConfigCommand), - - /// Red team commands for adversarial monitoring - #[command(name = "red-team", subcommand)] - RedTeam(RedTeamCommand), } /// Config subcommands for CLI configuration. @@ -203,54 +196,6 @@ pub enum ContractCommand { CreateFile(contract::CreateFileArgs), } -/// Red team subcommands for adversarial monitoring. -#[derive(Subcommand, Debug)] -pub enum RedTeamCommand { - /// Send a notification to the supervisor about a detected issue. - /// Only available to red team tasks. - Notify(RedTeamNotifyArgs), -} - -/// Arguments for red-team notify command. -#[derive(Args, Debug)] -pub struct RedTeamNotifyArgs { - /// API URL - #[arg(long, env = "MAKIMA_API_URL", default_value = "https://api.makima.jp")] - pub api_url: String, - - /// API key for authentication - #[arg(long, env = "MAKIMA_API_KEY")] - pub api_key: String, - - /// Current task ID (must be a red team task) - #[arg(long, env = "MAKIMA_TASK_ID")] - pub task_id: Uuid, - - /// Contract ID - #[arg(long, env = "MAKIMA_CONTRACT_ID")] - pub contract_id: Uuid, - - /// The notification message - #[arg(index = 1)] - pub message: String, - - /// Severity level: low, medium, high, critical - #[arg(long, default_value = "medium")] - pub severity: String, - - /// Related task ID (optional) - #[arg(long)] - pub task: Option<Uuid>, - - /// Related file path (optional) - #[arg(long)] - pub file: Option<String>, - - /// Additional context (optional) - #[arg(long)] - pub context: Option<String>, -} - impl Cli { /// Parse command-line arguments pub fn parse_args() -> Self { diff --git a/makima/src/daemon/cli/red_team.rs b/makima/src/daemon/cli/red_team.rs deleted file mode 100644 index 771aae4..0000000 --- a/makima/src/daemon/cli/red_team.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Red Team subcommand - adversarial review notification commands. - -use crate::daemon::api::{ApiClient, RedTeamNotifyRequest}; -use super::RedTeamNotifyArgs; - -/// Handle the red-team notify command. -pub async fn handle_notify(args: RedTeamNotifyArgs) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { - let client = ApiClient::new(args.api_url, args.api_key)?; - - // Use --task if provided, otherwise fall back to MAKIMA_TASK_ID - let related_task_id = args.task; - - let req = RedTeamNotifyRequest { - message: args.message, - severity: args.severity, - related_task_id, - file_path: args.file, - context: args.context, - }; - - eprintln!("Sending red team notification..."); - let result = client.red_team_notify(req).await?; - println!("{}", serde_json::to_string(&result.0)?); - - Ok(()) -} diff --git a/makima/src/daemon/cli/supervisor.rs b/makima/src/daemon/cli/supervisor.rs index cb84ffa..6f19697 100644 --- a/makima/src/daemon/cli/supervisor.rs +++ b/makima/src/daemon/cli/supervisor.rs @@ -48,10 +48,6 @@ pub struct SpawnArgs { /// Repository URL (local path or remote URL). If not provided, will try to detect from current directory. #[arg(long)] pub repo: Option<String>, - - /// Create a separate worktree for the task (requires merge after). By default, tasks share the supervisor's worktree. - #[arg(long)] - pub own_worktree: bool, } /// Arguments for wait command. diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index bf495d9..f921d50 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -363,24 +363,29 @@ fn strip_ansi_codes(s: &str) -> String { } /// System prompt for regular (non-orchestrator) subtasks. -/// This ensures subtasks work only within their isolated worktree directory. -const SUBTASK_SYSTEM_PROMPT: &str = r#"You are working in an isolated worktree directory that contains a snapshot of the codebase. +/// This tells subtasks they share a worktree with the supervisor and other tasks. +const SUBTASK_SYSTEM_PROMPT: &str = r#"You are working in a shared worktree directory with other tasks in this contract. -## IMPORTANT: Directory Restrictions +## IMPORTANT: Shared Worktree -**You MUST only work within the current working directory (your worktree).** +**You share this worktree with the supervisor and other tasks in the contract.** -- DO NOT use `cd` to navigate to directories outside your worktree -- DO NOT use absolute paths that point outside your worktree (e.g., don't write to ~/some/path, /tmp, or the original repository) -- DO NOT modify files in parent directories or sibling directories -- All your file operations should be relative to the current directory +- Work within your assigned area (files/modules specified in your task plan) +- Be aware other tasks may be modifying other parts of the codebase +- Your changes will be auto-committed when your task completes +- DO NOT make commits yourself - the system handles this -Your working directory is your sandboxed workspace. When you complete your task, your changes will be reviewed and integrated by the orchestrator. +## Directory Restrictions -**Why?** Your worktree is isolated so that: -1. Your changes don't affect other running tasks -2. Changes can be reviewed before integration -3. Multiple tasks can work on the codebase in parallel without conflicts +- DO NOT use `cd` to navigate outside your worktree +- DO NOT use absolute paths pointing outside the worktree +- All file operations should be relative to the current directory + +## Your Role + +1. Complete the specific task assigned to you +2. Stay focused on your task plan +3. The system will commit and integrate your changes automatically --- @@ -597,368 +602,91 @@ rsync -av --exclude='.git' --exclude='.makima' "$FINAL_TASK_PATH/" ./ /// System prompt for supervisor tasks (contract orchestrators). -/// Supervisors monitor all tasks in a contract, create new tasks, and drive the contract to completion. -const SUPERVISOR_SYSTEM_PROMPT: &str = r###"You are the SUPERVISOR for this contract. Your ONLY job is to coordinate work by spawning tasks, waiting for them to complete, and managing git operations. - -## CRITICAL RULES - READ CAREFULLY - -1. **NEVER write code or edit files yourself** - you are a coordinator ONLY -2. **NEVER make commits yourself** - tasks do their own commits -3. **ALWAYS spawn tasks** for ANY work that involves: - - Writing or editing code - - Creating or modifying files - - Making implementation changes - - Any actual development work -4. **ALWAYS wait for tasks to complete** - you MUST use `wait` after spawning -5. **Your role is ONLY to**: - - Analyze the contract goal and break it into tasks - - Spawn tasks AND wait for them to complete - - Review completed task results - - Merge completed work using `merge` - - Create PRs when ready using `pr` - -## REQUIRED WORKFLOW - Follow This Pattern - -For EVERY task you spawn, you MUST: -1. Spawn the task with `spawn` -2. IMMEDIATELY call `wait` to block until completion -3. Check the result and handle success/failure -4. Merge if successful - -```bash -# CORRECT PATTERN - spawn then wait -RESULT=$(makima supervisor spawn "Task Name" "Detailed plan...") -TASK_ID=$(echo "$RESULT" | jq -r '.taskId') -echo "Spawned task: $TASK_ID" - -# MUST wait for the task - DO NOT skip this step! -makima supervisor wait "$TASK_ID" +/// Supervisors coordinate work by spawning tasks and responding to user questions. +/// Git operations and phase advancement are handled automatically by the system. +const SUPERVISOR_SYSTEM_PROMPT: &str = r###"You are the SUPERVISOR for this contract. Your job is to coordinate work by spawning tasks and responding to user questions. -# Check result, view diff, merge if successful -makima supervisor diff "$TASK_ID" -makima supervisor merge "$TASK_ID" -``` +## WHAT YOU DO +1. Break down the contract goal into actionable tasks +2. Spawn tasks using `makima supervisor spawn "Task Name" "Detailed plan..."` +3. Wait for tasks to complete using `makima supervisor wait <task_id>` +4. Respond to user questions when asked -## Example - Full Workflow +## WHAT THE SYSTEM HANDLES AUTOMATICALLY +- **Phase advancement** - When deliverables are complete, the system advances the phase +- **Git commits** - Tasks auto-commit their changes on completion +- **Pull requests** - System auto-creates PR when execute phase completes +- **You will be notified** when phases advance so you know to continue -Goal: "Add user authentication" +## CRITICAL RULES -```bash -# Step 1: Create a makima branch for this work (use makima/{name} convention) -makima supervisor branch "makima/user-authentication" - -# Step 2: Spawn tasks, wait for each, and merge to the branch - -# Task 1: Research (spawn and wait) -RESULT=$(makima supervisor spawn "Research auth patterns" "Explore the codebase for existing authentication. Document findings.") -TASK_ID=$(echo "$RESULT" | jq -r '.taskId') -makima supervisor wait "$TASK_ID" -# Review findings before continuing - -# Task 2: Login endpoint (spawn and wait) -RESULT=$(makima supervisor spawn "Implement login" "Create POST /api/login endpoint...") -TASK_ID=$(echo "$RESULT" | jq -r '.taskId') -makima supervisor wait "$TASK_ID" -makima supervisor diff "$TASK_ID" -makima supervisor merge "$TASK_ID" --to "makima/user-authentication" - -# Task 3: Logout endpoint (spawn and wait) -RESULT=$(makima supervisor spawn "Implement logout" "Create POST /api/logout endpoint...") -TASK_ID=$(echo "$RESULT" | jq -r '.taskId') -makima supervisor wait "$TASK_ID" -makima supervisor merge "$TASK_ID" --to "makima/user-authentication" - -# Step 3: All tasks complete - create PR from makima branch -makima supervisor pr "makima/user-authentication" --title "Add user authentication" -``` +1. **NEVER write code or edit files yourself** - you are a coordinator ONLY +2. **ALWAYS spawn tasks** for ANY work that involves writing or editing code +3. **ALWAYS wait for tasks to complete** - you MUST use `wait` after spawning -## Available Tools (via makima supervisor) +## AVAILABLE COMMANDS ### Task Management ```bash -# List all tasks in this contract -makima supervisor tasks - -# Spawn a new task (returns JSON with taskId) -makima supervisor spawn "Task Name" "Detailed plan..." - -# IMPORTANT: Wait for task to complete (blocks until done/failed) -makima supervisor wait <task_id> [timeout_seconds] - -# Read a file from any task's worktree -makima supervisor read-file <task_id> <file_path> - -# Get the full task tree structure -makima supervisor tree -``` - -### Git Operations -```bash -# Create a new branch -makima supervisor branch <branch_name> [--from <task_id|sha>] - -# Merge a task's changes to a branch -makima supervisor merge <task_id> [--to <branch>] [--squash] - -# Create a pull request -makima supervisor pr <branch> --title "Title" [--body "Body"] - -# View a task's diff -makima supervisor diff <task_id> - -# Create a git checkpoint -makima supervisor checkpoint "Checkpoint message" - -# List checkpoints for a task -makima supervisor checkpoints [task_id] -``` - -### Contract & Phase Management -```bash -# Get contract status (including current phase) -makima supervisor status - -# Advance to the next phase (specify, plan, execute, review) -makima supervisor advance-phase <phase> - -# Mark a phase deliverable as complete (e.g., 'plan-document', 'pull-request') -makima supervisor mark-deliverable <deliverable_id> [--phase <phase>] -``` - -### User Feedback -```bash -# Ask a free-form question -makima supervisor ask "Your question here" - -# Ask with choices (comma-separated) -makima supervisor ask "Choose an option" --choices "Option A,Option B,Option C" - -# Ask with context -makima supervisor ask "Ready to proceed?" --context "After completing task X" - -# Ask with custom timeout (default 1 hour) -makima supervisor ask "Question" --timeout 3600 +makima supervisor spawn "Task Name" "Detailed plan..." # Create and start a task +makima supervisor wait <task_id> [timeout_seconds] # Wait for task completion +makima supervisor tasks # List all tasks +makima supervisor tree # View task tree +makima supervisor diff <task_id> # View task changes +makima supervisor read-file <task_id> <file_path> # Read file from task ``` -## User Feedback (Ask Command) - -You can ask the user questions when you need clarification or approval: - +### User Interaction ```bash -# Ask a free-form question (waits for user to respond) -makima supervisor ask "What authentication method should I use?" - -# Ask with predefined choices -makima supervisor ask "Ready to create PR?" --choices "Yes,No,Need more changes" - -# Ask with context -makima supervisor ask "Should I proceed?" --context "Plan phase complete" +makima supervisor ask "Your question" [--choices "A,B,C"] # Ask user +makima supervisor status # Contract status (read-only) ``` -The ask command will block until the user responds (or timeout). Use this to: -- Clarify requirements before starting work -- Get approval before creating PRs -- Ask for guidance when tasks fail - -## Contract Phase Progression - -### For "Simple" contracts (Plan → Execute): -1. **Plan Phase**: Review the plan document and understand the goal -2. **Execute Phase**: Spawn tasks to implement the plan, then create PR -3. Mark contract as complete when PR is created - -### For "Specification" contracts (Research → Specify → Plan → Execute → Review): -Progress through each phase, spawning tasks as needed and asking for user feedback. - -## Multi-Phase Plan Execution (CRITICAL) - -Plan documents often contain MULTIPLE implementation phases (e.g., "Phase 1: Foundation", "Phase 2: Core Features", "Phase 3: Integration"). You MUST implement ALL phases, not just the first one! - -### Detecting Implementation Phases - -At the START of the Execute phase: -1. Read the plan document using `makima contract files` and `makima contract file <id>` -2. Look for implementation phase sections like: - - "## Phase 1: ..." / "## Phase 2: ..." - - "## Step 1: ..." / "## Step 2: ..." - - "## Part 1: ..." / "## Part 2: ..." - - Any numbered sections that represent sequential work -3. Create a mental list of ALL implementation phases that need to be completed - -### Executing Multi-Phase Plans - -1. **Execute phases SEQUENTIALLY** - complete ALL tasks for Phase 1 before starting Phase 2 -2. **Track your progress** - keep track of which phases are done vs remaining -3. **Confirm between phases** - use `makima supervisor ask` to confirm: "Phase N complete. Ready for Phase N+1?" -4. **ONLY create PR when ALL phases are done** - DO NOT create a PR after just the first phase! - -### Multi-Phase Workflow Example +## WORKFLOW PATTERN ```bash -# 1. First, read the plan to understand all phases -makima contract files # List files to find plan document -makima contract file <plan-file-id> # Read the plan content - -# 2. Identify phases (example shows 3 phases) -# Found: -# - Phase 1: Setup and Dependencies -# - Phase 2: Core Implementation -# - Phase 3: Testing and Documentation - -# 3. Execute Phase 1 completely -makima supervisor spawn "Phase 1: Setup" "Details from plan..." -makima supervisor wait <task_id> -makima supervisor merge <task_id> --to "makima/feature-name" - -# 4. Confirm before moving to Phase 2 -makima supervisor ask "Phase 1 (Setup) complete. Ready to proceed to Phase 2 (Core Implementation)?" --choices "Yes,Need changes,Stop" - -# 5. Execute Phase 2 completely -makima supervisor spawn "Phase 2: Core Implementation" "Details from plan..." -makima supervisor wait <task_id> -makima supervisor merge <task_id> --to "makima/feature-name" - -# 6. Confirm before Phase 3 -makima supervisor ask "Phase 2 (Core Implementation) complete. Ready to proceed to Phase 3 (Testing)?" --choices "Yes,Need changes,Stop" - -# 7. Execute Phase 3 -makima supervisor spawn "Phase 3: Testing" "Details from plan..." -makima supervisor wait <task_id> -makima supervisor merge <task_id> --to "makima/feature-name" - -# 8. ONLY NOW create the PR (all phases complete!) -makima supervisor pr "makima/feature-name" --title "Complete feature implementation" -``` - -### Common Multi-Phase Mistakes - -- ❌ Creating a PR after only the first phase completes -- ❌ Not reading the plan document to identify all phases -- ❌ Trying to implement all phases in a single giant task -- ❌ Skipping the confirmation step between phases - -### Correct Multi-Phase Behavior - -- ✅ Read plan document first to identify ALL implementation phases -- ✅ Execute each phase as separate task(s) -- ✅ Wait for each phase to complete before starting the next -- ✅ Confirm with user between phases -- ✅ Create PR ONLY after ALL phases are complete -- ✅ The PR title/description should mention all completed phases - -## Phase Management Commands - -Check contract status (including current phase): -```bash -makima supervisor status -``` - -Advance to the next phase: -```bash -makima supervisor advance-phase <phase> -``` - -Valid phases: `specify`, `plan`, `execute`, `review` - -### Marking Deliverables Complete +# 1. Spawn a task +RESULT=$(makima supervisor spawn "Implement feature X" "Details...") +TASK_ID=$(echo "$RESULT" | jq -r '.taskId') -Each phase has deliverables that must be completed before advancing. Use `mark-deliverable` to explicitly mark them as complete when you've verified the requirement is satisfied: +# 2. Wait for it +makima supervisor wait "$TASK_ID" -```bash -# Mark a deliverable complete (defaults to current phase) -makima supervisor mark-deliverable plan-document +# 3. Check result +makima supervisor diff "$TASK_ID" -# Mark a deliverable for a specific phase -makima supervisor mark-deliverable pull-request --phase execute +# 4. Repeat for more tasks +# System handles commits, merging, and PR creation automatically ``` -Common deliverable IDs by phase: -- **plan**: `plan-document`, `requirements-document` -- **execute**: `pull-request` -- **review**: `release-notes`, `retrospective` - -**Use `status` to see which deliverables are pending for the current phase.** - -## When to Advance Phases - -**IMPORTANT**: You MUST advance the contract phase as you complete work in each phase! - -### Simple Contracts (Plan → Execute) -- **Plan → Execute**: When you understand the plan and are ready to spawn tasks -- **Complete contract**: When all tasks are done/merged and PR is created - -### Specification Contracts (Research → Specify → Plan → Execute → Review) -- **Research → Specify**: When requirements are understood -- **Specify → Plan**: When specifications are written -- **Plan → Execute**: When implementation plan is ready -- **Execute → Review**: When all tasks are done/merged -- **Complete contract**: After review is done and PR is created - -## Phase Advancement Workflow - -1. Complete work for current phase (spawn tasks, wait, merge) -2. Check status: `makima supervisor status` -3. Ask user for confirmation (recommended): - ```bash - makima supervisor ask "Ready to advance to execute phase?" --choices "Yes,Not yet" - ``` -4. Advance: `makima supervisor advance-phase execute` -5. Continue with next phase work - -**DO NOT forget to advance phases!** The user needs to see the contract progressing. +## MULTI-PHASE PLANS -## Key Points +When the plan document contains multiple implementation phases (Phase 1, Phase 2, etc.): -1. **Create a makima branch first** - use `branch "makima/{name}"` for the contract's work -2. **spawn returns immediately** - the task runs in the background -3. **wait blocks until complete** - you MUST call this to know when a task finishes -4. **Never fire-and-forget** - always wait for each task before moving on -5. **Merge to your makima branch** - use `merge <task_id> --to "makima/{name}"` to collect completed work -6. **Create PR when done** - use `pr "makima/{name}" --title "..."` -7. **Ask when unsure** - use `ask` to get user feedback on decisions +1. **Read the plan** to identify ALL phases +2. **Execute phases SEQUENTIALLY** - complete Phase 1 before Phase 2 +3. **Track your progress** - keep track of which phases are done +4. **Confirm between phases** - use `ask` to confirm before proceeding +5. The system will auto-create PR when ALL phases are complete -## Standard Workflow +## IMPORTANT NOTES +- DO NOT call advance-phase - the system does this automatically +- DO NOT manage git operations (branch, merge, pr) - the system handles this +- Focus ONLY on spawning tasks and responding to users +- You share a worktree with all tasks - changes are visible immediately +- If you need user input, use `makima supervisor ask` +- When all work is complete, use `makima supervisor complete` to finish -1. `branch "makima/{name}"` - Create branch (e.g., "makima/add-auth") -2. For each piece of work: - - `spawn` - Create task - - `wait` - Block until complete - - `merge --to "makima/{name}"` - Merge to branch -3. `ask "Ready to create PR?"` - Get user approval -4. `pr "makima/{name}" --title "..."` - Create PR +## WHEN TASKS COMPLETE -## Important Reminders +When a task completes: +1. Check the result with `makima supervisor diff <task_id>` +2. If more work needed, spawn another task +3. The system automatically commits changes -- **ONLY YOU can spawn tasks** - regular tasks cannot create children -- **NEVER implement anything yourself** - always spawn tasks -- **ALWAYS create a makima branch** - use `makima/{name}` naming convention -- **ASK for feedback** when you need clarification or approval -- Tasks run independently - you just coordinate -- You will be resumed if interrupted - your conversation is preserved -- Create checkpoints before major transitions -- **Mark contract complete** when PR is created by updating status - -## MANDATORY ACTIONS - -### [ACTION REQUIRED] Messages -When you receive an `[ACTION REQUIRED]` message from the system: -1. Read the directive carefully -2. Execute the specified command IMMEDIATELY -3. Do NOT ignore or defer these actions -4. These messages indicate conditions have been met and action is needed NOW - -### Phase Completion Checklist -After all tasks are "done" and merged, you MUST take the following actions: - -**If in execute phase:** -1. Create PR immediately: `makima supervisor pr "makima/{name}" --title "..."` -2. After PR created: - - Simple contract: Mark complete with `makima supervisor complete` - - Specification contract: Advance to review with `makima supervisor advance-phase review` - -**Never leave a contract hanging** - when work is done, create the PR and complete/advance. - ---- +When ALL work is complete: +- Use `makima supervisor complete` to mark the contract done +- The system will auto-create PR (for remote repos) "###; @@ -5308,20 +5036,19 @@ impl TaskManagerInner { } } _ = heartbeat_interval.tick(), if heartbeat_enabled => { - // Create periodic heartbeat commit to preserve work-in-progress - match self.create_heartbeat_commit(task_id, &working_dir).await { - Ok((sha, pushed)) => { - let status = if pushed { "pushed" } else { "local only" }; + // Create periodic ephemeral patch to preserve work-in-progress + match self.create_ephemeral_patch(task_id, &working_dir).await { + Ok(files_count) => { let msg = DaemonMessage::task_output( task_id, - format!("[Heartbeat] WIP checkpoint {} ({})\n", &sha[..8], status), + format!("[Heartbeat] Patch saved ({} files)\n", files_count), false, ); let _ = ws_tx.send(msg).await; } Err(e) => { - // No changes to commit or git error - this is fine, just log at debug level - tracing::debug!(task_id = %task_id, error = %e, "Heartbeat commit skipped"); + // No changes to patch or error - this is fine, just log at debug level + tracing::debug!(task_id = %task_id, error = %e, "Heartbeat patch skipped"); } } } @@ -5907,24 +5634,28 @@ impl TaskManagerInner { } } - /// Create a heartbeat commit with all uncommitted changes (WIP checkpoint). - /// Returns (commit SHA, push succeeded) on success, or an error message if nothing to commit. - /// Also creates a patch and sends it to the server for recovery purposes. - async fn create_heartbeat_commit( + /// Create an ephemeral patch of uncommitted changes and send to the server. + /// This does NOT create git commits or push - patches are stored in PostgreSQL only. + /// Returns the number of files changed on success, or an error message if nothing to patch. + async fn create_ephemeral_patch( &self, task_id: Uuid, worktree_path: &std::path::Path, - ) -> Result<(String, bool), String> { - // 1. Get parent SHA BEFORE committing (for patch creation) - let parent_sha_output = tokio::process::Command::new("git") + ) -> Result<i32, String> { + // 1. Get current HEAD SHA (base for the patch) + let base_sha_output = tokio::process::Command::new("git") .current_dir(worktree_path) .args(["rev-parse", "HEAD"]) .output() - .await; - let parent_sha = parent_sha_output - .ok() - .filter(|o| o.status.success()) - .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()); + .await + .map_err(|e| format!("Failed to run git rev-parse: {}", e))?; + + if !base_sha_output.status.success() { + let stderr = String::from_utf8_lossy(&base_sha_output.stderr); + return Err(format!("git rev-parse failed: {}", stderr)); + } + + let base_sha = String::from_utf8_lossy(&base_sha_output.stdout).trim().to_string(); // 2. Check for uncommitted changes using git status --porcelain let status_output = tokio::process::Command::new("git") @@ -5941,10 +5672,13 @@ impl TaskManagerInner { let status = String::from_utf8_lossy(&status_output.stdout); if status.trim().is_empty() { - return Err("No changes to commit".into()); + return Err("No changes to patch".into()); } - // 3. Stage all changes + // Count files with changes + let files_count = status.lines().count() as i32; + + // 3. Stage all changes (required for diff to include untracked files) let add_output = tokio::process::Command::new("git") .current_dir(worktree_path) .args(["add", "-A"]) @@ -5957,137 +5691,79 @@ impl TaskManagerInner { return Err(format!("git add failed: {}", stderr)); } - // 4. Create WIP commit with timestamp - let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"); - let commit_msg = format!("[WIP] Heartbeat checkpoint - {}", timestamp); - - let commit_output = tokio::process::Command::new("git") - .current_dir(worktree_path) - .args(["commit", "-m", &commit_msg]) - .output() - .await - .map_err(|e| format!("Failed to run git commit: {}", e))?; - - if !commit_output.status.success() { - let stderr = String::from_utf8_lossy(&commit_output.stderr); - return Err(format!("git commit failed: {}", stderr)); + // 4. Create patch (diff of staged changes against HEAD) + if !self.checkpoint_patches.enabled { + // Reset staged changes and return + let _ = tokio::process::Command::new("git") + .current_dir(worktree_path) + .args(["reset", "HEAD"]) + .output() + .await; + return Err("Checkpoint patches disabled".into()); } - // 5. Get the commit SHA - let sha_output = tokio::process::Command::new("git") - .current_dir(worktree_path) - .args(["rev-parse", "HEAD"]) - .output() - .await - .map_err(|e| format!("Failed to run git rev-parse: {}", e))?; + match storage::create_patch(worktree_path, &base_sha).await { + Ok((compressed_patch, patch_files_count)) => { + // Reset staged changes (we don't want to commit) + let _ = tokio::process::Command::new("git") + .current_dir(worktree_path) + .args(["reset", "HEAD"]) + .output() + .await; - if !sha_output.status.success() { - let stderr = String::from_utf8_lossy(&sha_output.stderr); - return Err(format!("git rev-parse failed: {}", stderr)); - } + // Check size limit + if compressed_patch.len() > self.checkpoint_patches.max_patch_size_bytes { + tracing::warn!( + task_id = %task_id, + patch_size = compressed_patch.len(), + max_size = self.checkpoint_patches.max_patch_size_bytes, + "Patch exceeds size limit" + ); + return Err("Patch exceeds size limit".into()); + } - let sha = String::from_utf8_lossy(&sha_output.stdout).trim().to_string(); - tracing::info!(task_id = %task_id, sha = %sha, "Created heartbeat commit"); + // Encode as base64 for JSON transport + let patch_data = base64::engine::general_purpose::STANDARD.encode(&compressed_patch); - // 6. Get current branch name - let branch_output = tokio::process::Command::new("git") - .current_dir(worktree_path) - .args(["branch", "--show-current"]) - .output() - .await; - let branch_name = branch_output - .ok() - .filter(|o| o.status.success()) - .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) - .unwrap_or_else(|| "unknown".to_string()); - - // 7. Push to remote (best effort - don't fail if push fails) - // Use -u origin HEAD to set upstream if not already set (new branches won't have upstream) - let push_output = tokio::process::Command::new("git") - .current_dir(worktree_path) - .args(["push", "-u", "origin", "HEAD"]) - .output() - .await; + tracing::debug!( + task_id = %task_id, + base_sha = %base_sha, + patch_size = compressed_patch.len(), + files_count = patch_files_count, + "Created ephemeral patch" + ); - let pushed = match push_output { - Ok(output) if output.status.success() => { - tracing::info!(task_id = %task_id, sha = %sha, "Pushed heartbeat commit to remote"); - true - } - Ok(output) => { - let stderr = String::from_utf8_lossy(&output.stderr); - tracing::warn!(task_id = %task_id, sha = %sha, error = %stderr, "Failed to push heartbeat commit (commit saved locally)"); - false + // Send CheckpointCreated message to server (patch-only, no commit) + let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"); + let msg = DaemonMessage::CheckpointCreated { + task_id, + success: true, + commit_sha: None, // No git commit + branch_name: None, + checkpoint_number: None, // Server will assign + files_changed: None, // Detailed file info not tracked for ephemeral patches + lines_added: None, + lines_removed: None, + error: None, + message: format!("Ephemeral patch - {}", timestamp), + patch_data: Some(patch_data), + patch_base_sha: Some(base_sha), + patch_files_count: Some(patch_files_count as i32), + }; + let _ = self.ws_tx.send(msg).await; + + Ok(files_count) } Err(e) => { - tracing::warn!(task_id = %task_id, sha = %sha, error = %e, "Failed to run git push (commit saved locally)"); - false - } - }; - - // 8. Create patch and send CheckpointCreated message to server - let mut patch_data: Option<String> = None; - let mut patch_base_sha: Option<String> = None; - let mut patch_files_count: Option<i32> = None; - - if self.checkpoint_patches.enabled { - if let Some(ref base_sha) = parent_sha { - match storage::create_patch(worktree_path, base_sha).await { - Ok((compressed_patch, files_count)) => { - // Check size limit - if compressed_patch.len() <= self.checkpoint_patches.max_patch_size_bytes { - // Encode as base64 for JSON transport - patch_data = Some(base64::engine::general_purpose::STANDARD.encode(&compressed_patch)); - patch_base_sha = Some(base_sha.clone()); - patch_files_count = Some(files_count as i32); - tracing::debug!( - task_id = %task_id, - sha = %sha, - patch_size = compressed_patch.len(), - files_count = files_count, - "Created checkpoint patch" - ); - } else { - tracing::warn!( - task_id = %task_id, - sha = %sha, - patch_size = compressed_patch.len(), - max_size = self.checkpoint_patches.max_patch_size_bytes, - "Patch exceeds size limit, not including in checkpoint" - ); - } - } - Err(e) => { - tracing::warn!( - task_id = %task_id, - sha = %sha, - error = %e, - "Failed to create patch for heartbeat commit" - ); - } - } + // Reset staged changes + let _ = tokio::process::Command::new("git") + .current_dir(worktree_path) + .args(["reset", "HEAD"]) + .output() + .await; + Err(format!("Failed to create patch: {}", e)) } } - - // Send CheckpointCreated message to server (so it stores the checkpoint and patch) - let msg = DaemonMessage::CheckpointCreated { - task_id, - success: true, - commit_sha: Some(sha.clone()), - branch_name: Some(branch_name), - checkpoint_number: None, // Server will assign - files_changed: None, // Could get from git diff --name-status if needed - lines_added: None, - lines_removed: None, - error: None, - message: commit_msg, - patch_data, - patch_base_sha, - patch_files_count, - }; - let _ = self.ws_tx.send(msg).await; - - Ok((sha, pushed)) } } diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index abdcce6..cef0a22 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -440,11 +440,6 @@ pub struct Task { /// True for contract supervisor tasks. Only supervisors can spawn new tasks. #[serde(default)] pub is_supervisor: bool, - /// Whether this is a red team monitoring task. - /// Red team tasks monitor work task outputs and can notify - /// the supervisor about potential issues. - #[serde(default)] - pub is_red_team: bool, // Daemon/container info pub daemon_id: Option<Uuid>, @@ -575,9 +570,6 @@ pub struct TaskSummary { /// True for contract supervisor tasks #[serde(default)] pub is_supervisor: bool, - /// True for red team tasks that monitor and review other tasks' work - #[serde(default)] - pub is_red_team: bool, /// Whether this task is hidden from the UI (user dismissed it) #[serde(default)] pub hidden: bool, @@ -603,7 +595,6 @@ impl From<Task> for TaskSummary { subtask_count: 0, // Would need separate query version: task.version, is_supervisor: task.is_supervisor, - is_red_team: task.is_red_team, hidden: task.hidden, created_at: task.created_at, updated_at: task.updated_at, @@ -636,9 +627,6 @@ pub struct CreateTaskRequest { /// True for contract supervisor tasks. Only supervisors can spawn new tasks. #[serde(default)] pub is_supervisor: bool, - /// True for red team tasks that monitor and review other tasks' work. - #[serde(default)] - pub is_red_team: bool, /// Priority (higher = more urgent) #[serde(default)] pub priority: i32, @@ -1453,15 +1441,6 @@ pub struct Contract { /// automatically merged to the master/main branch locally (without pushing or creating PRs). #[serde(default)] pub auto_merge_local: bool, - /// Whether to spawn a red team task to monitor work tasks. - /// When enabled, a parallel task monitors outputs and can alert - /// the supervisor about potential issues. - #[serde(default)] - pub red_team_enabled: bool, - /// Optional custom prompt/criteria for the red team to use - /// when evaluating task outputs. - #[serde(skip_serializing_if = "Option::is_none")] - pub red_team_prompt: Option<String>, /// Phase configuration copied from template at contract creation (raw JSON). /// When present, this overrides the built-in contract type phases. /// Use `get_phase_config()` to get the parsed PhaseConfig. @@ -1649,9 +1628,6 @@ pub struct ContractSummary { /// When true with local_only, automatically merge completed tasks to target branch locally. #[serde(default)] pub auto_merge_local: bool, - /// Whether red team monitoring is enabled for this contract. - #[serde(default)] - pub red_team_enabled: bool, pub file_count: i64, pub task_count: i64, pub repository_count: i64, @@ -1723,15 +1699,6 @@ pub struct CreateContractRequest { /// automatically merged to the master/main branch locally (without pushing or creating PRs). #[serde(default)] pub auto_merge_local: Option<bool>, - /// Enable red team monitoring for this contract. - /// When enabled, a parallel task monitors work task outputs - /// and can alert the supervisor about potential issues. - #[serde(default)] - pub red_team_enabled: Option<bool>, - /// Optional custom criteria for the red team to evaluate. - /// Examples: "Focus on security vulnerabilities", - /// "Ensure all functions have tests", etc. - pub red_team_prompt: Option<String>, } /// Request payload for updating a contract @@ -2542,67 +2509,6 @@ pub struct SupervisorHeartbeatRequest { pub pending_task_ids: Vec<Uuid>, } -// ============================================================================= -// Red Team Types -// ============================================================================= - -/// Red Team notification record -#[derive(Debug, Clone, FromRow, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct RedTeamNotification { - pub id: Uuid, - pub contract_id: Uuid, - pub red_team_task_id: Uuid, - pub related_task_id: Option<Uuid>, - - pub message: String, - pub severity: String, - pub file_path: Option<String>, - pub context: Option<String>, - - pub delivered: bool, - pub delivered_at: Option<DateTime<Utc>>, - pub acknowledged: bool, - pub acknowledged_at: Option<DateTime<Utc>>, - - pub created_at: DateTime<Utc>, -} - -/// Severity levels for red team notifications -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum NotificationSeverity { - Low, - Medium, - High, - Critical, -} - -impl std::fmt::Display for NotificationSeverity { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Low => write!(f, "low"), - Self::Medium => write!(f, "medium"), - Self::High => write!(f, "high"), - Self::Critical => write!(f, "critical"), - } - } -} - -impl std::str::FromStr for NotificationSeverity { - type Err = String; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s.to_lowercase().as_str() { - "low" => Ok(Self::Low), - "medium" => Ok(Self::Medium), - "high" => Ok(Self::High), - "critical" => Ok(Self::Critical), - _ => Err(format!("Invalid severity: {}", s)), - } - } -} - // ============================================================================ // Supervisor Status API Types // ============================================================================ diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index e308df7..2ecbc4a 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -12,7 +12,7 @@ use super::models::{ CreateFileRequest, CreateTaskRequest, CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity, DeliverableDefinition, File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters, MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, - PhaseConfig, PhaseDefinition, RedTeamNotification, SupervisorHeartbeatRecord, SupervisorState, + PhaseConfig, PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint, TaskEvent, TaskSummary, UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest, UpdateTemplateRequest, }; @@ -691,11 +691,11 @@ pub async fn create_task(pool: &PgPool, req: CreateTaskRequest) -> Result<Task, r#" INSERT INTO tasks ( contract_id, parent_task_id, depth, name, description, plan, priority, - is_supervisor, is_red_team, repository_url, base_branch, target_branch, merge_mode, + is_supervisor, repository_url, base_branch, target_branch, merge_mode, target_repo_path, completion_action, continue_from_task_id, copy_files, branched_from_task_id, conversation_state, supervisor_worktree_task_id ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING * "#, ) @@ -707,7 +707,6 @@ pub async fn create_task(pool: &PgPool, req: CreateTaskRequest) -> Result<Task, .bind(&req.plan) .bind(req.priority) .bind(req.is_supervisor) - .bind(req.is_red_team) .bind(&repo_url) .bind(&base_branch) .bind(&target_branch) @@ -748,8 +747,7 @@ pub async fn list_tasks(pool: &PgPool) -> Result<Vec<TaskSummary>, sqlx::Error> t.parent_task_id, t.depth, t.name, t.status, t.priority, t.progress_summary, (SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count, - t.version, t.is_supervisor, COALESCE(t.is_red_team, false) as is_red_team, - COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at + t.version, t.is_supervisor, COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at FROM tasks t LEFT JOIN contracts c ON t.contract_id = c.id WHERE t.parent_task_id IS NULL AND COALESCE(t.hidden, false) = false @@ -770,8 +768,7 @@ pub async fn list_subtasks(pool: &PgPool, parent_id: Uuid) -> Result<Vec<TaskSum t.parent_task_id, t.depth, t.name, t.status, t.priority, t.progress_summary, (SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count, - t.version, t.is_supervisor, COALESCE(t.is_red_team, false) as is_red_team, - COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at + t.version, t.is_supervisor, COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at FROM tasks t LEFT JOIN contracts c ON t.contract_id = c.id WHERE t.parent_task_id = $1 @@ -1106,11 +1103,11 @@ pub async fn create_task_for_owner( r#" INSERT INTO tasks ( owner_id, contract_id, parent_task_id, depth, name, description, plan, priority, - is_supervisor, is_red_team, repository_url, base_branch, target_branch, merge_mode, + is_supervisor, repository_url, base_branch, target_branch, merge_mode, target_repo_path, completion_action, continue_from_task_id, copy_files, branched_from_task_id, conversation_state, supervisor_worktree_task_id ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) RETURNING * "#, ) @@ -1123,7 +1120,6 @@ pub async fn create_task_for_owner( .bind(&req.plan) .bind(req.priority) .bind(req.is_supervisor) - .bind(req.is_red_team) .bind(&repo_url) .bind(&base_branch) .bind(&target_branch) @@ -1172,8 +1168,7 @@ pub async fn list_tasks_for_owner( t.parent_task_id, t.depth, t.name, t.status, t.priority, t.progress_summary, (SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count, - t.version, t.is_supervisor, COALESCE(t.is_red_team, false) as is_red_team, - COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at + t.version, t.is_supervisor, COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at FROM tasks t LEFT JOIN contracts c ON t.contract_id = c.id WHERE t.owner_id = $1 AND t.parent_task_id IS NULL AND COALESCE(t.hidden, false) = false @@ -1199,8 +1194,7 @@ pub async fn list_subtasks_for_owner( t.parent_task_id, t.depth, t.name, t.status, t.priority, t.progress_summary, (SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count, - t.version, t.is_supervisor, COALESCE(t.is_red_team, false) as is_red_team, - COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at + t.version, t.is_supervisor, COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at FROM tasks t LEFT JOIN contracts c ON t.contract_id = c.id WHERE t.owner_id = $1 AND t.parent_task_id = $2 @@ -1721,8 +1715,7 @@ pub async fn list_sibling_tasks( t.parent_task_id, t.depth, t.name, t.status, t.priority, t.progress_summary, (SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count, - t.version, t.is_supervisor, COALESCE(t.is_red_team, false) as is_red_team, - COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at + t.version, t.is_supervisor, COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at FROM tasks t LEFT JOIN contracts c ON t.contract_id = c.id WHERE t.parent_task_id = $1 AND t.id != $2 @@ -1744,8 +1737,7 @@ pub async fn list_sibling_tasks( t.parent_task_id, t.depth, t.name, t.status, t.priority, t.progress_summary, (SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count, - t.version, t.is_supervisor, COALESCE(t.is_red_team, false) as is_red_team, - COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at + t.version, t.is_supervisor, COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at FROM tasks t LEFT JOIN contracts c ON t.contract_id = c.id WHERE t.parent_task_id IS NULL AND t.id != $1 @@ -2467,15 +2459,14 @@ pub async fn create_contract_for_owner( let phase_guard = req.phase_guard.unwrap_or(false); let local_only = req.local_only.unwrap_or(false); let auto_merge_local = req.auto_merge_local.unwrap_or(false); - let red_team_enabled = req.red_team_enabled.unwrap_or(false); // Serialize phase_config to JSON let phase_config_json = serde_json::to_value(&phase_config).ok(); sqlx::query_as::<_, Contract>( r#" - INSERT INTO contracts (owner_id, name, description, contract_type, phase, autonomous_loop, phase_guard, local_only, auto_merge_local, red_team_enabled, red_team_prompt, phase_config) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + INSERT INTO contracts (owner_id, name, description, contract_type, phase, autonomous_loop, phase_guard, local_only, auto_merge_local, phase_config) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING * "#, ) @@ -2488,8 +2479,6 @@ pub async fn create_contract_for_owner( .bind(phase_guard) .bind(local_only) .bind(auto_merge_local) - .bind(red_team_enabled) - .bind(&req.red_team_prompt) .bind(phase_config_json) .fetch_one(pool) .await @@ -2523,7 +2512,7 @@ pub async fn list_contracts_for_owner( r#" SELECT c.id, c.name, c.description, c.contract_type, c.phase, c.status, - c.supervisor_task_id, c.local_only, c.auto_merge_local, c.red_team_enabled, c.version, c.created_at, + c.supervisor_task_id, c.local_only, c.auto_merge_local, c.version, c.created_at, (SELECT COUNT(*) FROM files WHERE contract_id = c.id) as file_count, (SELECT COUNT(*) FROM tasks WHERE contract_id = c.id) as task_count, (SELECT COUNT(*) FROM contract_repositories WHERE contract_id = c.id) as repository_count @@ -2547,7 +2536,7 @@ pub async fn get_contract_summary_for_owner( r#" SELECT c.id, c.name, c.description, c.contract_type, c.phase, c.status, - c.supervisor_task_id, c.local_only, c.auto_merge_local, c.red_team_enabled, c.version, c.created_at, + c.supervisor_task_id, c.local_only, c.auto_merge_local, c.version, c.created_at, (SELECT COUNT(*) FROM files WHERE contract_id = c.id) as file_count, (SELECT COUNT(*) FROM tasks WHERE contract_id = c.id) as task_count, (SELECT COUNT(*) FROM contract_repositories WHERE contract_id = c.id) as repository_count @@ -3118,8 +3107,7 @@ pub async fn list_tasks_in_contract( t.parent_task_id, t.depth, t.name, t.status, t.priority, t.progress_summary, (SELECT COUNT(*) FROM tasks WHERE parent_task_id = t.id) as subtask_count, - t.version, t.is_supervisor, COALESCE(t.is_red_team, false) as is_red_team, - COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at + t.version, t.is_supervisor, COALESCE(t.hidden, false) as hidden, t.created_at, t.updated_at FROM tasks t LEFT JOIN contracts c ON t.contract_id = c.id WHERE t.contract_id = $1 AND t.owner_id = $2 @@ -4774,93 +4762,6 @@ pub async fn delete_checkpoint_patches_for_task( // ============================================================================= // Red Team Notifications // ============================================================================= - -/// Create a red team notification. -/// Red team tasks use this to report issues found during implementation review. -pub async fn create_red_team_notification( - pool: &PgPool, - contract_id: Uuid, - red_team_task_id: Uuid, - message: &str, - severity: &str, - related_task_id: Option<Uuid>, - file_path: Option<&str>, - context: Option<&str>, -) -> Result<RedTeamNotification, RepositoryError> { - sqlx::query_as::<_, RedTeamNotification>( - r#" - INSERT INTO red_team_notifications - (contract_id, red_team_task_id, related_task_id, message, severity, file_path, context) - VALUES ($1, $2, $3, $4, $5, $6, $7) - RETURNING * - "#, - ) - .bind(contract_id) - .bind(red_team_task_id) - .bind(related_task_id) - .bind(message) - .bind(severity) - .bind(file_path) - .bind(context) - .fetch_one(pool) - .await - .map_err(RepositoryError::Database) -} - -/// Mark a notification as delivered to the supervisor. -pub async fn mark_notification_delivered( - pool: &PgPool, - notification_id: Uuid, -) -> Result<RedTeamNotification, RepositoryError> { - sqlx::query_as::<_, RedTeamNotification>( - r#" - UPDATE red_team_notifications - SET delivered = TRUE, delivered_at = NOW() - WHERE id = $1 - RETURNING * - "#, - ) - .bind(notification_id) - .fetch_one(pool) - .await - .map_err(RepositoryError::Database) -} - -/// Get the red team task for a contract (if one exists). -/// Returns the most recently created red team task for the contract. -pub async fn get_red_team_task_for_contract( - pool: &PgPool, - contract_id: Uuid, -) -> Result<Option<Task>, RepositoryError> { - sqlx::query_as::<_, Task>( - r#" - SELECT * FROM tasks - WHERE contract_id = $1 AND is_red_team = TRUE - ORDER BY created_at DESC - LIMIT 1 - "#, - ) - .bind(contract_id) - .fetch_optional(pool) - .await - .map_err(RepositoryError::Database) -} - -/// Get the count of notifications for a red team task. -pub async fn get_notification_count_for_task( - pool: &PgPool, - red_team_task_id: Uuid, -) -> Result<i64, RepositoryError> { - let result: (i64,) = sqlx::query_as( - "SELECT COUNT(*) FROM red_team_notifications WHERE red_team_task_id = $1", - ) - .bind(red_team_task_id) - .fetch_one(pool) - .await - .map_err(RepositoryError::Database)?; - Ok(result.0) -} - // ============================================================================= // Supervisor Status API Helpers // ============================================================================= diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs index b025485..2d54894 100644 --- a/makima/src/server/handlers/contract_chat.rs +++ b/makima/src/server/handlers/contract_chat.rs @@ -1362,7 +1362,6 @@ async fn handle_contract_request( continue_from_task_id: None, copy_files: None, is_supervisor: false, - is_red_team: false, checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, @@ -1460,7 +1459,6 @@ async fn handle_contract_request( continue_from_task_id: None, copy_files: None, is_supervisor: false, - is_red_team: false, checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, @@ -2213,8 +2211,7 @@ async fn handle_contract_request( continue_from_task_id: previous_task_id, copy_files: None, is_supervisor: false, - is_red_team: false, - checkpoint_sha: None, + checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, supervisor_worktree_task_id: None, // Not spawned by supervisor @@ -2612,8 +2609,6 @@ async fn handle_contract_request( phase_guard: None, local_only: None, auto_merge_local: None, - red_team_enabled: None, - red_team_prompt: None, template_id: None, }; @@ -2736,8 +2731,7 @@ async fn handle_contract_request( continue_from_task_id: None, copy_files: None, is_supervisor: false, - is_red_team: false, - checkpoint_sha: None, + checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, supervisor_worktree_task_id: None, // Not spawned by supervisor diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index 01b4610..8c8cabf 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -363,7 +363,6 @@ pub async fn create_contract( continue_from_task_id: None, copy_files: None, is_supervisor: true, - is_red_team: false, checkpoint_sha: None, priority: 0, merge_mode: None, @@ -438,7 +437,6 @@ pub async fn create_contract( supervisor_task_id: contract.supervisor_task_id, local_only: contract.local_only, auto_merge_local: contract.auto_merge_local, - red_team_enabled: contract.red_team_enabled, file_count: 0, task_count: 0, repository_count: 0, @@ -462,7 +460,6 @@ pub async fn create_contract( supervisor_task_id: contract.supervisor_task_id, local_only: contract.local_only, auto_merge_local: contract.auto_merge_local, - red_team_enabled: contract.red_team_enabled, file_count: 0, task_count: 0, repository_count: 0, @@ -593,7 +590,6 @@ pub async fn update_contract( supervisor_task_id: contract.supervisor_task_id, local_only: contract.local_only, auto_merge_local: contract.auto_merge_local, - red_team_enabled: contract.red_team_enabled, file_count: 0, task_count: 0, repository_count: 0, @@ -1523,7 +1519,6 @@ pub async fn change_phase( supervisor_task_id: updated_contract.supervisor_task_id, local_only: updated_contract.local_only, auto_merge_local: updated_contract.auto_merge_local, - red_team_enabled: updated_contract.red_team_enabled, file_count: 0, task_count: 0, repository_count: 0, diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs index af77b56..fe9ffc0 100644 --- a/makima/src/server/handlers/mesh.rs +++ b/makima/src/server/handlers/mesh.rs @@ -2613,7 +2613,6 @@ pub async fn reassign_task( plan: updated_plan.clone(), parent_task_id: task.parent_task_id, is_supervisor: task.is_supervisor, - is_red_team: task.is_red_team, priority: task.priority, repository_url: task.repository_url.clone(), base_branch: task.base_branch.clone(), @@ -3390,7 +3389,6 @@ pub async fn fork_task( plan: req.new_task_plan.clone(), parent_task_id: None, // Forked tasks are independent is_supervisor: false, - is_red_team: false, priority: task.priority, repository_url: task.repository_url.clone(), base_branch: task.base_branch.clone(), @@ -3549,7 +3547,6 @@ pub async fn resume_from_checkpoint( plan: req.plan, parent_task_id: None, is_supervisor: false, - is_red_team: false, priority: task.priority, repository_url: task.repository_url.clone(), base_branch: task.base_branch.clone(), @@ -3886,7 +3883,6 @@ pub async fn branch_task( plan: req.message, parent_task_id: None, is_supervisor: false, - is_red_team: false, priority: source_task.priority, repository_url: source_task.repository_url.clone(), base_branch: source_task.base_branch.clone(), diff --git a/makima/src/server/handlers/mesh_chat.rs b/makima/src/server/handlers/mesh_chat.rs index eee899f..a6a3a3c 100644 --- a/makima/src/server/handlers/mesh_chat.rs +++ b/makima/src/server/handlers/mesh_chat.rs @@ -1017,7 +1017,6 @@ async fn handle_mesh_request( continue_from_task_id: None, copy_files: None, is_supervisor: false, - is_red_team: false, checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs index 34e2cc3..cb929ea 100644 --- a/makima/src/server/handlers/mesh_daemon.rs +++ b/makima/src/server/handlers/mesh_daemon.rs @@ -1870,6 +1870,68 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re } } } + } else if let (Some(patch_b64), Some(base_sha)) = (&patch_data, &patch_base_sha) { + // Ephemeral patch-only checkpoint (no git commit) + // Store patch directly in checkpoint_patches without a task_checkpoint + if let Some(pool) = state.db_pool.as_ref() { + match base64::engine::general_purpose::STANDARD.decode(patch_b64) { + Ok(patch_bytes) => { + let files_count = patch_files_count.unwrap_or(0); + // Default TTL: 7 days (168 hours) + let ttl_hours = 168i64; + match repository::create_checkpoint_patch( + pool, + task_id, + None, // No checkpoint_id for ephemeral patches + base_sha, + &patch_bytes, + files_count, + ttl_hours, + ).await { + Ok(patch) => { + tracing::info!( + task_id = %task_id, + patch_id = %patch.id, + patch_size = patch_bytes.len(), + files_count = files_count, + "Ephemeral patch stored for recovery" + ); + + state.broadcast_task_output(TaskOutputNotification { + task_id, + owner_id: Some(owner_id), + message_type: "system".to_string(), + content: format!( + "✓ Patch saved: {} ({} files)", + message, + files_count + ), + tool_name: None, + tool_input: None, + is_error: Some(false), + cost_usd: None, + duration_ms: None, + is_partial: false, + }); + } + Err(e) => { + tracing::warn!( + task_id = %task_id, + error = %e, + "Failed to store ephemeral patch" + ); + } + } + } + Err(e) => { + tracing::warn!( + task_id = %task_id, + error = %e, + "Failed to decode ephemeral patch base64 data" + ); + } + } + } } } else { // Broadcast failure diff --git a/makima/src/server/handlers/mesh_red_team.rs b/makima/src/server/handlers/mesh_red_team.rs deleted file mode 100644 index c5af60e..0000000 --- a/makima/src/server/handlers/mesh_red_team.rs +++ /dev/null @@ -1,497 +0,0 @@ -//! HTTP handlers for red team mesh operations. -//! -//! These endpoints are used by red team tasks (via the makima CLI) to notify -//! supervisors of potential issues and query their own status. - -use axum::{ - extract::State, - http::{HeaderMap, StatusCode}, - response::IntoResponse, - Json, -}; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; -use uuid::Uuid; - -use crate::db::repository; -use crate::server::handlers::mesh::{extract_auth, AuthSource}; -use crate::server::messages::ApiError; -use crate::server::state::{DaemonCommand, SharedState}; - -// ============================================================================= -// Request/Response Types -// ============================================================================= - -/// Severity level for red team notifications. -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "lowercase")] -pub enum RedTeamSeverity { - /// Informational notice - minor issue or suggestion - Info, - /// Warning - potential problem that should be reviewed - Warning, - /// Critical - serious issue requiring immediate attention - Critical, -} - -impl Default for RedTeamSeverity { - fn default() -> Self { - Self::Warning - } -} - -impl std::fmt::Display for RedTeamSeverity { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Info => write!(f, "INFO"), - Self::Warning => write!(f, "WARNING"), - Self::Critical => write!(f, "CRITICAL"), - } - } -} - -/// Request to notify the supervisor of a potential issue. -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct RedTeamNotifyRequest { - /// The issue description/message to send to the supervisor - pub message: String, - /// Severity level of the issue - #[serde(default)] - pub severity: RedTeamSeverity, - /// ID of the task being reviewed (optional - if not provided, assumes general contract concern) - pub related_task_id: Option<Uuid>, - /// File path related to the issue (optional) - pub file_path: Option<String>, - /// Additional context about the issue - pub context: Option<String>, -} - -/// Response from the notify endpoint. -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct RedTeamNotifyResponse { - /// Unique ID for this notification - pub notification_id: Uuid, - /// Whether the notification was successfully delivered to the supervisor - pub delivered: bool, - /// The supervisor task ID that received the notification - pub supervisor_task_id: Option<Uuid>, -} - -/// Response from the status endpoint. -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct RedTeamStatusResponse { - /// Contract ID being monitored - pub contract_id: Uuid, - /// Red team task ID - pub red_team_task_id: Uuid, - /// Current task status - pub status: String, - /// Number of notifications sent so far - pub notifications_sent: i64, -} - -/// Red team notification record stored in database. -#[derive(Debug, Clone, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct RedTeamNotification { - pub id: Uuid, - pub red_team_task_id: Uuid, - pub contract_id: Uuid, - pub message: String, - pub severity: String, - pub related_task_id: Option<Uuid>, - pub file_path: Option<String>, - pub context: Option<String>, - pub delivered: bool, - pub created_at: chrono::DateTime<chrono::Utc>, -} - -// ============================================================================= -// Helper Functions -// ============================================================================= - -/// Verify the request comes from a red team task and extract ownership info. -/// -/// Returns (task_id, owner_id, contract_id) on success. -async fn verify_red_team_auth( - state: &SharedState, - headers: &HeaderMap, -) -> Result<(Uuid, Uuid, Uuid), (StatusCode, Json<ApiError>)> { - let auth = extract_auth(state, headers); - - let task_id = match auth { - AuthSource::ToolKey(task_id) => task_id, - _ => { - return Err(( - StatusCode::UNAUTHORIZED, - Json(ApiError::new( - "UNAUTHORIZED", - "Red team endpoints require tool key auth", - )), - )); - } - }; - - // Get the task to verify it's a red team task and get owner_id - let pool = state.db_pool.as_ref().ok_or_else(|| { - ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - })?; - - let task = repository::get_task(pool, task_id) - .await - .map_err(|e| { - tracing::error!(error = %e, "Failed to get red team task"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", "Failed to verify red team task")), - ) - })? - .ok_or_else(|| { - ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Task not found")), - ) - })?; - - // Verify task is a red team task - // NOTE: This requires the is_red_team field to be added to the Task struct. - // For now, we check if the task name contains "red-team" or "red_team" as a fallback. - let is_red_team = task.name.to_lowercase().contains("red-team") - || task.name.to_lowercase().contains("red_team") - || task.name.to_lowercase().contains("redteam"); - - if !is_red_team { - return Err(( - StatusCode::FORBIDDEN, - Json(ApiError::new( - "NOT_RED_TEAM", - "Only red team tasks can use these endpoints", - )), - )); - } - - // Red team tasks must be associated with a contract - let contract_id = task.contract_id.ok_or_else(|| { - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new( - "NO_CONTRACT", - "Red team task must be associated with a contract", - )), - ) - })?; - - Ok((task_id, task.owner_id, contract_id)) -} - -/// Format an alert message for the supervisor. -/// -/// Creates a formatted alert with clear visual markers to grab attention. -fn format_alert_message( - severity: &RedTeamSeverity, - message: &str, - related_task_id: Option<Uuid>, - file_path: Option<&str>, - context: Option<&str>, -) -> String { - let severity_marker = match severity { - RedTeamSeverity::Info => "ℹ️", - RedTeamSeverity::Warning => "⚠️", - RedTeamSeverity::Critical => "🚨", - }; - - let border = match severity { - RedTeamSeverity::Info => "─".repeat(60), - RedTeamSeverity::Warning => "━".repeat(60), - RedTeamSeverity::Critical => "═".repeat(60), - }; - - let mut alert = format!( - r#" -{} -{} [RED TEAM ALERT] - {} -{} - -Issue: {} -"#, - border, severity_marker, severity, border, message - ); - - if let Some(task_id) = related_task_id { - alert.push_str(&format!("\nRelated Task: {}\n", task_id)); - } - - if let Some(path) = file_path { - alert.push_str(&format!("File: {}\n", path)); - } - - if let Some(ctx) = context { - alert.push_str(&format!("\nContext:\n{}\n", ctx)); - } - - // Add action suggestions based on severity - let actions = match severity { - RedTeamSeverity::Info => { - "Suggested Actions:\n- Review when convenient\n- Consider if changes are needed" - } - RedTeamSeverity::Warning => { - "Suggested Actions:\n- Review the flagged item soon\n- Check if this deviates from the contract\n- Consider pausing related work until reviewed" - } - RedTeamSeverity::Critical => { - "Suggested Actions:\n- STOP related work immediately\n- Review the flagged item urgently\n- Verify compliance with contract requirements\n- Consider reverting recent changes if necessary" - } - }; - - alert.push_str(&format!("\n{}\n{}\n", actions, border)); - - alert -} - -// ============================================================================= -// Handlers -// ============================================================================= - -/// Notify the supervisor of a potential issue. -/// -/// POST /api/v1/mesh/red-team/notify -/// -/// This endpoint allows red team tasks to alert supervisors about issues they've -/// identified during code review. The notification is sent as a message to the -/// supervisor task. -#[utoipa::path( - post, - path = "/api/v1/mesh/red-team/notify", - request_body = RedTeamNotifyRequest, - responses( - (status = 200, description = "Notification sent", body = RedTeamNotifyResponse), - (status = 401, description = "Unauthorized - tool key required"), - (status = 403, description = "Forbidden - not a red team task"), - (status = 404, description = "Task not found"), - (status = 503, description = "Database not available"), - (status = 500, description = "Internal server error"), - ), - security( - ("tool_key" = []) - ), - tag = "Mesh Red Team" -)] -pub async fn notify_supervisor( - State(state): State<SharedState>, - headers: HeaderMap, - Json(request): Json<RedTeamNotifyRequest>, -) -> impl IntoResponse { - let (red_team_task_id, owner_id, contract_id) = - match verify_red_team_auth(&state, &headers).await { - Ok(ids) => ids, - Err(e) => return e.into_response(), - }; - - let pool = state.db_pool.as_ref().unwrap(); - - // Generate notification ID - let notification_id = Uuid::new_v4(); - - // Get the contract to find the supervisor task - let contract = match repository::get_contract_for_owner(pool, contract_id, 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!(error = %e, "Failed to get contract"); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", "Failed to get contract")), - ) - .into_response(); - } - }; - - let supervisor_task_id = contract.supervisor_task_id; - - // Format the alert message - let alert_message = format_alert_message( - &request.severity, - &request.message, - request.related_task_id, - request.file_path.as_deref(), - request.context.as_deref(), - ); - - // Record the notification in the database as a history event - let event_data = serde_json::json!({ - "notification_id": notification_id.to_string(), - "red_team_task_id": red_team_task_id.to_string(), - "severity": request.severity.to_string(), - "message": request.message, - "related_task_id": request.related_task_id.map(|id| id.to_string()), - "file_path": request.file_path, - "context": request.context, - }); - - let _ = repository::record_history_event( - pool, - owner_id, - Some(contract_id), - Some(red_team_task_id), - "red_team_alert", - Some(&request.severity.to_string().to_lowercase()), - Some(&request.message), - event_data, - ) - .await; - - // Try to send the message to the supervisor - let mut delivered = false; - if let Some(sup_task_id) = supervisor_task_id { - // Get the supervisor task to find its daemon - if let Ok(Some(supervisor_task)) = repository::get_task(pool, sup_task_id).await { - if let Some(daemon_id) = supervisor_task.daemon_id { - // Send the alert message to the supervisor - let cmd = DaemonCommand::SendMessage { - task_id: sup_task_id, - message: alert_message.clone(), - }; - - if let Err(e) = state.send_daemon_command(daemon_id, cmd).await { - tracing::warn!( - error = %e, - supervisor_task_id = %sup_task_id, - daemon_id = %daemon_id, - "Failed to send red team alert to supervisor" - ); - } else { - delivered = true; - tracing::info!( - notification_id = %notification_id, - red_team_task_id = %red_team_task_id, - supervisor_task_id = %sup_task_id, - severity = %request.severity, - "Red team alert delivered to supervisor" - ); - } - } else { - tracing::warn!( - supervisor_task_id = %sup_task_id, - "Supervisor task has no assigned daemon - alert not delivered" - ); - } - } - } else { - tracing::warn!( - contract_id = %contract_id, - "Contract has no supervisor task - alert not delivered" - ); - } - - ( - StatusCode::OK, - Json(RedTeamNotifyResponse { - notification_id, - delivered, - supervisor_task_id, - }), - ) - .into_response() -} - -/// Get the status of the red team task. -/// -/// GET /api/v1/mesh/red-team/status -/// -/// Returns information about the current red team task including the contract -/// being monitored and notification statistics. -#[utoipa::path( - get, - path = "/api/v1/mesh/red-team/status", - responses( - (status = 200, description = "Red team status", body = RedTeamStatusResponse), - (status = 401, description = "Unauthorized - tool key required"), - (status = 403, description = "Forbidden - not a red team task"), - (status = 404, description = "Task not found"), - (status = 503, description = "Database not available"), - (status = 500, description = "Internal server error"), - ), - security( - ("tool_key" = []) - ), - tag = "Mesh Red Team" -)] -pub async fn get_status( - State(state): State<SharedState>, - headers: HeaderMap, -) -> impl IntoResponse { - let (red_team_task_id, owner_id, contract_id) = - match verify_red_team_auth(&state, &headers).await { - Ok(ids) => ids, - Err(e) => return e.into_response(), - }; - - let pool = state.db_pool.as_ref().unwrap(); - - // Get the red team task status - let task = match repository::get_task(pool, red_team_task_id).await { - Ok(Some(t)) => t, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Red team task not found")), - ) - .into_response(); - } - Err(e) => { - tracing::error!(error = %e, "Failed to get red team task"); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", "Failed to get task")), - ) - .into_response(); - } - }; - - // Count notifications sent by this red team task - // Query history_events for red_team_alert events from this task - let notifications_sent = match sqlx::query_scalar::<_, i64>( - r#" - SELECT COUNT(*) - FROM history_events - WHERE owner_id = $1 - AND contract_id = $2 - AND task_id = $3 - AND event_type = 'red_team_alert' - "#, - ) - .bind(owner_id) - .bind(contract_id) - .bind(red_team_task_id) - .fetch_one(pool) - .await - { - Ok(count) => count, - Err(e) => { - tracing::warn!(error = %e, "Failed to count red team notifications"); - 0 - } - }; - - ( - StatusCode::OK, - Json(RedTeamStatusResponse { - contract_id, - red_team_task_id, - status: task.status, - notifications_sent, - }), - ) - .into_response() -} diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs index a29b666..43388a8 100644 --- a/makima/src/server/handlers/mesh_supervisor.rs +++ b/makima/src/server/handlers/mesh_supervisor.rs @@ -37,10 +37,6 @@ pub struct SpawnTaskRequest { pub checkpoint_sha: Option<String>, /// Repository URL for the task (optional - if not provided, will be looked up from contract). pub repository_url: Option<String>, - /// If true, create a separate worktree for the task (requires merge after). - /// If false (default), the task shares the supervisor's worktree. - #[serde(default)] - pub use_own_worktree: bool, } /// Request to wait for task completion. @@ -610,8 +606,8 @@ pub async fn spawn_task( } // Create task request - // Share supervisor's worktree by default; separate worktree only when explicitly requested - let supervisor_worktree_task_id = if request.use_own_worktree { None } else { Some(supervisor_id) }; + // All tasks share the supervisor's worktree + let supervisor_worktree_task_id = Some(supervisor_id); let create_req = CreateTaskRequest { name: request.name.clone(), @@ -621,7 +617,6 @@ pub async fn spawn_task( contract_id: Some(request.contract_id), parent_task_id: request.parent_task_id, is_supervisor: false, - is_red_team: false, checkpoint_sha: request.checkpoint_sha.clone(), merge_mode: Some("manual".to_string()), priority: 0, @@ -733,8 +728,8 @@ pub async fn spawn_task( patch_base_sha: None, local_only: contract.local_only, auto_merge_local: contract.auto_merge_local, - // Share supervisor's worktree by default; separate worktree only when explicitly requested - supervisor_worktree_task_id: if request.use_own_worktree { None } else { Some(supervisor_id) }, + // All tasks share the supervisor's worktree + supervisor_worktree_task_id: Some(supervisor_id), }; if let Err(e) = state.send_daemon_command(daemon.id, cmd).await { @@ -762,66 +757,6 @@ pub async fn spawn_task( updated_by: "supervisor".to_string(), }); - // Check if we should spawn a red team task - // Conditions: - // 1. This is not a supervisor task - // 2. This is not already a red team task - // 3. Contract has red_team_enabled = true - // 4. No red team task exists for this contract yet - if !updated_task.is_supervisor && !updated_task.is_red_team && contract.red_team_enabled { - if let Some(contract_id) = updated_task.contract_id { - // Check if a red team task already exists - match repository::get_red_team_task_for_contract(pool, contract_id).await { - Ok(None) => { - // No red team task exists, spawn one - tracing::info!( - contract_id = %contract_id, - work_task_id = %updated_task.id, - "Spawning red team task for contract (first work task started)" - ); - match spawn_red_team_task( - pool, - &state, - contract_id, - owner_id, - &contract.name, - &contract.phase, - contract.red_team_prompt.as_deref(), - ).await { - Ok(red_team_task) => { - tracing::info!( - contract_id = %contract_id, - red_team_task_id = %red_team_task.id, - "Red team task spawned successfully" - ); - } - Err(e) => { - // Log error but don't fail the work task spawn - tracing::error!( - contract_id = %contract_id, - error = %e, - "Failed to spawn red team task" - ); - } - } - } - Ok(Some(existing)) => { - tracing::debug!( - contract_id = %contract_id, - red_team_task_id = %existing.id, - "Red team task already exists for contract" - ); - } - Err(e) => { - tracing::error!( - contract_id = %contract_id, - error = %e, - "Error checking for existing red team task" - ); - } - } - } - } } break; } @@ -2584,239 +2519,6 @@ pub async fn rewind_conversation( } // ============================================================================= -// Red Team Task Spawning -// ============================================================================= - -/// Generate the system prompt/plan for a red team task. -/// -/// This creates detailed instructions for the red team monitor, including -/// what to look for, severity levels, and how to report issues. -pub fn generate_red_team_plan( - contract_name: &str, - contract_phase: &str, - custom_prompt: Option<&str>, -) -> String { - let custom_criteria = if let Some(prompt) = custom_prompt { - format!( - r#" - -## Custom Review Criteria - -The contract owner has specified additional review criteria: -{} -"#, - prompt - ) - } else { - String::new() - }; - - format!( - r#"# Red Team Monitor - -You are an adversarial quality reviewer for a software development contract. Your role is to monitor work task outputs in real-time and flag potential issues BEFORE they compound into larger problems. - -## Your Mission - -Monitor all task outputs and verify: -1. **Plan Adherence**: Are tasks following the implementation plan? -2. **Code Quality**: Does the code meet repository standards? -3. **Contract Requirements**: Does the implementation match the specification? -4. **Best Practices**: Are there obvious anti-patterns or issues? - -## Access Available - -You have read-only access to: -- Task outputs (streamed in real-time) -- Task diffs (code changes) -- Contract specifications and plan documents -- Repository configuration files (CONTRIBUTING.md, linting configs, etc.) - -## How to Monitor - -1. **Subscribe to task outputs**: You'll receive outputs from all work tasks -2. **Analyze code changes**: Request diffs for completed tasks -3. **Cross-reference**: Compare outputs against the plan and specifications -4. **Report issues**: Use `makima red-team notify` when you detect problems - -## When to Notify - -NOTIFY the supervisor when you observe: -- **Critical**: Security vulnerabilities, data loss risks, breaking changes -- **High**: Significant deviations from the plan, major code quality issues -- **Medium**: Missing tests, suboptimal implementations, minor standard violations -- **Low**: Style inconsistencies, documentation gaps (use sparingly) - -## What NOT to Do - -- Do NOT nitpick minor style issues (that's what linters are for) -- Do NOT block progress for trivial concerns -- Do NOT write code or make changes yourself -- Do NOT notify for things that are already in progress and being addressed -- Do NOT create duplicate notifications for the same issue - -## Notification Format - -When notifying, always include: -1. A clear, concise description of the issue -2. The severity level (critical/high/medium/low) -3. The related task ID if applicable -4. The specific file or code location if known -5. Why this matters (reference to plan, spec, or standards) - -## Example Notification - -``` -makima red-team notify "Task is implementing authentication with plaintext password storage, which contradicts the security requirements in the specification document" \ - --severity critical \ - --task <task_id> \ - --file "src/auth/user.rs" \ - --context "Specification section 3.2 requires bcrypt hashing for all passwords" -``` -{} -## Contract Context - -Contract: {} -Phase: {} - -Focus your monitoring on outputs that relate to the active work tasks. Prioritize issues that could affect the success of the contract or introduce technical debt. -"#, - custom_criteria, contract_name, contract_phase - ) -} - -/// Spawn a red team task for a contract. -/// -/// This creates a red team monitor task that will observe work task outputs -/// and can notify the supervisor about potential issues. -pub async fn spawn_red_team_task( - pool: &sqlx::PgPool, - state: &SharedState, - contract_id: Uuid, - owner_id: Uuid, - contract_name: &str, - contract_phase: &str, - red_team_prompt: Option<&str>, -) -> Result<Task, String> { - // Generate the red team plan/prompt - let plan = generate_red_team_plan(contract_name, contract_phase, red_team_prompt); - - // Create task request - let create_req = CreateTaskRequest { - name: "Red Team Monitor".to_string(), - description: Some("Adversarial review task monitoring work task outputs".to_string()), - plan, - contract_id: Some(contract_id), - parent_task_id: None, - is_supervisor: false, - is_red_team: true, - priority: 0, - repository_url: None, // Red team doesn't need a repo - base_branch: None, - target_branch: None, - merge_mode: None, - target_repo_path: None, - completion_action: None, - continue_from_task_id: None, - copy_files: None, - checkpoint_sha: None, - branched_from_task_id: None, - conversation_history: None, - supervisor_worktree_task_id: None, // Red team uses its own working area - }; - - // Create task in DB - let task = repository::create_task_for_owner(pool, owner_id, create_req) - .await - .map_err(|e| format!("Failed to create red team task: {}", e))?; - - tracing::info!( - contract_id = %contract_id, - red_team_task_id = %task.id, - "Created red team task for contract" - ); - - // Find a daemon to run the red team task - for entry in state.daemon_connections.iter() { - let daemon = entry.value(); - if daemon.owner_id == owner_id { - // Update task with daemon assignment - let update_req = UpdateTaskRequest { - status: Some("starting".to_string()), - daemon_id: Some(daemon.id), - version: Some(task.version), - ..Default::default() - }; - - match repository::update_task_for_owner(pool, task.id, owner_id, update_req).await { - Ok(Some(updated_task)) => { - // Send spawn command to daemon - let cmd = DaemonCommand::SpawnTask { - task_id: updated_task.id, - task_name: updated_task.name.clone(), - plan: updated_task.plan.clone(), - repo_url: None, // Red team doesn't need a repo - base_branch: None, - target_branch: None, - parent_task_id: None, - depth: 0, - is_orchestrator: false, - target_repo_path: None, - completion_action: None, - continue_from_task_id: None, - copy_files: None, - contract_id: Some(contract_id), - is_supervisor: false, - autonomous_loop: false, - resume_session: false, - conversation_history: None, - patch_data: None, - patch_base_sha: None, - local_only: true, // Red team is always local-only - auto_merge_local: false, // Red team doesn't auto-merge - supervisor_worktree_task_id: None, - }; - - if let Err(e) = state.send_daemon_command(daemon.id, cmd).await { - tracing::warn!( - error = %e, - daemon_id = %daemon.id, - red_team_task_id = %task.id, - "Failed to send red team spawn command" - ); - // Rollback - let rollback_req = UpdateTaskRequest { - status: Some("pending".to_string()), - clear_daemon_id: true, - ..Default::default() - }; - let _ = repository::update_task_for_owner(pool, task.id, owner_id, rollback_req).await; - } else { - tracing::info!( - red_team_task_id = %task.id, - daemon_id = %daemon.id, - "Red team task spawn command sent" - ); - return Ok(updated_task); - } - } - Ok(None) => { - tracing::warn!(red_team_task_id = %task.id, "Red team task not found when updating daemon_id"); - } - Err(e) => { - tracing::error!(red_team_task_id = %task.id, error = %e, "Failed to update red team task with daemon_id"); - } - } - break; - } - } - - // Return the task even if we couldn't start it on a daemon - // It will remain pending and can be started later - Ok(task) -} - -// ============================================================================= // Supervisor State Persistence Helpers (Task 3.3) // ============================================================================= diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs index 8af2a37..a14c4f7 100644 --- a/makima/src/server/handlers/mod.rs +++ b/makima/src/server/handlers/mod.rs @@ -13,7 +13,6 @@ pub mod mesh; pub mod mesh_chat; pub mod mesh_daemon; pub mod mesh_merge; -pub mod mesh_red_team; pub mod mesh_supervisor; pub mod mesh_ws; pub mod repository_history; diff --git a/makima/src/server/handlers/templates.rs b/makima/src/server/handlers/templates.rs index 0cc5657..aa97876 100644 --- a/makima/src/server/handlers/templates.rs +++ b/makima/src/server/handlers/templates.rs @@ -1,27 +1,19 @@ //! Contract types API handler. +//! Only returns built-in contract types (simple, specification, execute). use axum::{ - extract::{Path, State}, http::StatusCode, response::IntoResponse, Json, }; use serde::Serialize; use utoipa::ToSchema; -use uuid::Uuid; -use crate::db::models::{ - ContractTypeTemplateRecord, ContractTypeTemplateSummary, CreateTemplateRequest, - UpdateTemplateRequest, -}; -use crate::db::repository; use crate::llm::templates; use crate::llm::templates::ContractTypeTemplate; -use crate::server::auth::{Authenticated, MaybeAuthenticated}; -use crate::server::state::SharedState; // ============================================================================= -// Contract Type Templates (Workflow Definitions) +// Contract Type Templates (Built-in Only) // ============================================================================= /// Response for listing contract types @@ -31,14 +23,7 @@ pub struct ListContractTypesResponse { pub contract_types: Vec<ContractTypeTemplate>, } -/// Response for a single custom template -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct TemplateResponse { - pub template: ContractTypeTemplateSummary, -} - -/// List all available contract type templates (built-in + custom) +/// List all available contract type templates (built-in only) #[utoipa::path( get, path = "/api/v1/contract-types", @@ -47,404 +32,12 @@ pub struct TemplateResponse { ), tag = "templates" )] -pub async fn list_contract_types( - State(state): State<SharedState>, - MaybeAuthenticated(auth): MaybeAuthenticated, -) -> impl IntoResponse { - // Start with built-in types - let mut contract_types = templates::all_contract_types(); - - // If authenticated, also fetch custom templates for this owner - if let Some(user) = auth { - if let Some(ref pool) = state.db_pool { - if let Ok(custom_templates) = - repository::list_templates_for_owner(pool, user.owner_id).await - { - for template in custom_templates { - contract_types.push(template_record_to_api(&template)); - } - } - } - } - +pub async fn list_contract_types() -> impl IntoResponse { + // Only return built-in types (simple, specification, execute) + let contract_types = templates::all_contract_types(); ( StatusCode::OK, Json(ListContractTypesResponse { contract_types }), ) .into_response() } - -/// Create a new custom contract type template -#[utoipa::path( - post, - path = "/api/v1/contract-types", - request_body = CreateTemplateRequest, - responses( - (status = 201, description = "Template created successfully", body = TemplateResponse), - (status = 400, description = "Invalid request"), - (status = 401, description = "Unauthorized"), - (status = 409, description = "Template with this name already exists") - ), - tag = "templates" -)] -pub async fn create_template( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Json(req): Json<CreateTemplateRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": "Database not configured" - })), - ) - .into_response(); - }; - - // Validate request - if req.name.trim().is_empty() { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "code": "INVALID_REQUEST", - "message": "Template name cannot be empty" - })), - ) - .into_response(); - } - - if req.phases.is_empty() { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "code": "INVALID_REQUEST", - "message": "Template must have at least one phase" - })), - ) - .into_response(); - } - - // Validate default_phase is in the phases list - if !req.phases.iter().any(|p| p.id == req.default_phase) { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "code": "INVALID_REQUEST", - "message": format!("Default phase '{}' is not in the phases list", req.default_phase) - })), - ) - .into_response(); - } - - // Check that template name doesn't conflict with built-in types - let builtin_names = ["simple", "specification", "execute"]; - if builtin_names.contains(&req.name.to_lowercase().as_str()) { - return ( - StatusCode::CONFLICT, - Json(serde_json::json!({ - "code": "NAME_CONFLICT", - "message": "Cannot create a template with the same name as a built-in type" - })), - ) - .into_response(); - } - - match repository::create_template_for_owner(pool, auth.owner_id, req).await { - Ok(template) => ( - StatusCode::CREATED, - Json(serde_json::json!({ - "template": template_record_to_summary(&template) - })), - ) - .into_response(), - Err(e) => { - // Check for unique constraint violation - let error_str = e.to_string(); - if error_str.contains("unique") || error_str.contains("duplicate") { - return ( - StatusCode::CONFLICT, - Json(serde_json::json!({ - "code": "NAME_CONFLICT", - "message": "A template with this name already exists" - })), - ) - .into_response(); - } - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": format!("Failed to create template: {}", e) - })), - ) - .into_response() - } - } -} - -/// Get a specific contract type template by ID -#[utoipa::path( - get, - path = "/api/v1/contract-types/{id}", - params( - ("id" = Uuid, Path, description = "Template ID") - ), - responses( - (status = 200, description = "Template retrieved successfully", body = TemplateResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Template not found") - ), - tag = "templates" -)] -pub async fn get_template( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": "Database not configured" - })), - ) - .into_response(); - }; - - match repository::get_template_for_owner(pool, id, auth.owner_id).await { - Ok(Some(template)) => ( - StatusCode::OK, - Json(serde_json::json!({ - "template": template_record_to_summary(&template) - })), - ) - .into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ - "code": "NOT_FOUND", - "message": "Template not found" - })), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": format!("Failed to get template: {}", e) - })), - ) - .into_response(), - } -} - -/// Update a contract type template -#[utoipa::path( - put, - path = "/api/v1/contract-types/{id}", - params( - ("id" = Uuid, Path, description = "Template ID") - ), - request_body = UpdateTemplateRequest, - responses( - (status = 200, description = "Template updated successfully", body = TemplateResponse), - (status = 400, description = "Invalid request"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Template not found"), - (status = 409, description = "Version conflict") - ), - tag = "templates" -)] -pub async fn update_template( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<UpdateTemplateRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": "Database not configured" - })), - ) - .into_response(); - }; - - // Validate phases if provided - if let Some(ref phases) = req.phases { - if phases.is_empty() { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "code": "INVALID_REQUEST", - "message": "Template must have at least one phase" - })), - ) - .into_response(); - } - - // If default_phase is also provided, validate it's in the phases - if let Some(ref default_phase) = req.default_phase { - if !phases.iter().any(|p| &p.id == default_phase) { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "code": "INVALID_REQUEST", - "message": format!("Default phase '{}' is not in the phases list", default_phase) - })), - ) - .into_response(); - } - } - } - - // Check that template name doesn't conflict with built-in types - if let Some(ref name) = req.name { - let builtin_names = ["simple", "specification", "execute"]; - if builtin_names.contains(&name.to_lowercase().as_str()) { - return ( - StatusCode::CONFLICT, - Json(serde_json::json!({ - "code": "NAME_CONFLICT", - "message": "Cannot rename template to a built-in type name" - })), - ) - .into_response(); - } - } - - match repository::update_template_for_owner(pool, id, auth.owner_id, req).await { - Ok(Some(template)) => ( - StatusCode::OK, - Json(serde_json::json!({ - "template": template_record_to_summary(&template) - })), - ) - .into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ - "code": "NOT_FOUND", - "message": "Template not found" - })), - ) - .into_response(), - Err(repository::RepositoryError::VersionConflict { expected, actual }) => ( - StatusCode::CONFLICT, - Json(serde_json::json!({ - "code": "VERSION_CONFLICT", - "message": format!("Version conflict: expected {}, found {}", expected, actual), - "expectedVersion": expected, - "actualVersion": actual - })), - ) - .into_response(), - Err(e) => { - let error_str = e.to_string(); - if error_str.contains("unique") || error_str.contains("duplicate") { - return ( - StatusCode::CONFLICT, - Json(serde_json::json!({ - "code": "NAME_CONFLICT", - "message": "A template with this name already exists" - })), - ) - .into_response(); - } - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": format!("Failed to update template: {}", e) - })), - ) - .into_response() - } - } -} - -/// Delete a contract type template -#[utoipa::path( - delete, - path = "/api/v1/contract-types/{id}", - params( - ("id" = Uuid, Path, description = "Template ID") - ), - responses( - (status = 204, description = "Template deleted successfully"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Template not found") - ), - tag = "templates" -)] -pub async fn delete_template( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": "Database not configured" - })), - ) - .into_response(); - }; - - match repository::delete_template_for_owner(pool, id, auth.owner_id).await { - Ok(true) => StatusCode::NO_CONTENT.into_response(), - Ok(false) => ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ - "code": "NOT_FOUND", - "message": "Template not found" - })), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "code": "DB_ERROR", - "message": format!("Failed to delete template: {}", e) - })), - ) - .into_response(), - } -} - -// ============================================================================= -// Helper Functions -// ============================================================================= - -/// Convert a database template record to the API template format -fn template_record_to_api(template: &ContractTypeTemplateRecord) -> ContractTypeTemplate { - ContractTypeTemplate { - id: template.id.to_string(), - name: template.name.clone(), - description: template.description.clone().unwrap_or_default(), - phases: template.phases.iter().map(|p| p.id.clone()).collect(), - default_phase: template.default_phase.clone(), - is_builtin: false, - } -} - -/// Convert a database template record to the summary format -fn template_record_to_summary(template: &ContractTypeTemplateRecord) -> ContractTypeTemplateSummary { - ContractTypeTemplateSummary { - id: template.id, - name: template.name.clone(), - description: template.description.clone(), - phases: template.phases.clone(), - default_phase: template.default_phase.clone(), - is_builtin: false, - version: template.version, - created_at: template.created_at, - } -} diff --git a/makima/src/server/handlers/transcript_analysis.rs b/makima/src/server/handlers/transcript_analysis.rs index d987d08..62c65a6 100644 --- a/makima/src/server/handlers/transcript_analysis.rs +++ b/makima/src/server/handlers/transcript_analysis.rs @@ -280,8 +280,6 @@ pub async fn create_contract_from_analysis( phase_guard: None, local_only: None, auto_merge_local: None, - red_team_enabled: None, - red_team_prompt: None, template_id: None, }; @@ -362,7 +360,6 @@ pub async fn create_contract_from_analysis( continue_from_task_id: None, copy_files: None, is_supervisor: false, - is_red_team: false, checkpoint_sha: None, priority: match item.priority.as_deref() { Some("high") => 10, @@ -537,7 +534,6 @@ pub async fn update_contract_from_analysis( continue_from_task_id: None, copy_files: None, is_supervisor: false, - is_red_team: false, checkpoint_sha: None, priority: 0, merge_mode: None, diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index e5415ae..b351ac1 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -18,7 +18,7 @@ use tower_http::trace::TraceLayer; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; -use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contracts, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_red_team, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions}; +use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contracts, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions}; use crate::server::openapi::ApiDoc; use crate::server::state::SharedState; @@ -132,9 +132,6 @@ pub fn make_router(state: SharedState) -> Router { .route("/mesh/supervisor/questions", post(mesh_supervisor::ask_question)) .route("/mesh/questions", get(mesh_supervisor::list_pending_questions)) .route("/mesh/questions/{question_id}/answer", post(mesh_supervisor::answer_question)) - // Red team endpoints (for red team tasks to notify supervisors) - .route("/mesh/red-team/notify", post(mesh_red_team::notify_supervisor)) - .route("/mesh/red-team/status", get(mesh_red_team::get_status)) // Mesh WebSocket endpoints .route("/mesh/tasks/subscribe", get(mesh_ws::task_subscription_handler)) .route("/mesh/daemons/connect", get(mesh_daemon::daemon_handler)) @@ -216,17 +213,8 @@ pub fn make_router(state: SharedState) -> Router { ) // Timeline endpoint (unified history for user) .route("/timeline", get(history::get_timeline)) - // Contract type templates (workflow definitions) - .route( - "/contract-types", - get(templates::list_contract_types).post(templates::create_template), - ) - .route( - "/contract-types/{id}", - get(templates::get_template) - .put(templates::update_template) - .delete(templates::delete_template), - ) + // Contract type templates (built-in only) + .route("/contract-types", get(templates::list_contract_types)) // Settings endpoints .route( "/settings/repository-history", |
