diff options
| -rw-r--r-- | makima/frontend/src/components/contracts/ContractList.tsx | 5 | ||||
| -rw-r--r-- | makima/frontend/src/hooks/useSpeakWebSocket.ts | 2 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 12 | ||||
| -rw-r--r-- | makima/frontend/src/routes/contracts.tsx | 78 | ||||
| -rw-r--r-- | makima/frontend/tsconfig.tsbuildinfo | 2 | ||||
| -rw-r--r-- | makima/src/bin/makima.rs | 4 | ||||
| -rw-r--r-- | makima/src/daemon/api/supervisor.rs | 9 | ||||
| -rw-r--r-- | makima/src/daemon/cli/supervisor.rs | 8 | ||||
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 40 | ||||
| -rw-r--r-- | makima/src/daemon/ws/protocol.rs | 4 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_supervisor.rs | 41 | ||||
| -rw-r--r-- | makima/src/server/state.rs | 4 |
12 files changed, 140 insertions, 69 deletions
diff --git a/makima/frontend/src/components/contracts/ContractList.tsx b/makima/frontend/src/components/contracts/ContractList.tsx index 98f8ff6..532ab87 100644 --- a/makima/frontend/src/components/contracts/ContractList.tsx +++ b/makima/frontend/src/components/contracts/ContractList.tsx @@ -136,6 +136,11 @@ 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/hooks/useSpeakWebSocket.ts b/makima/frontend/src/hooks/useSpeakWebSocket.ts index 3ef8851..d9fb826 100644 --- a/makima/frontend/src/hooks/useSpeakWebSocket.ts +++ b/makima/frontend/src/hooks/useSpeakWebSocket.ts @@ -22,7 +22,7 @@ export function useSpeakWebSocket() { const wsRef = useRef<WebSocket | null>(null); const audioContextRef = useRef<AudioContext | null>(null); - const audioQueueRef = useRef<Float32Array[]>([]); + const audioQueueRef = useRef<Float32Array<ArrayBuffer>[]>([]); const isPlayingRef = useRef(false); const modelLoadingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const nextPlayTimeRef = useRef(0); diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index ca04ce7..8838dbd 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -1636,6 +1636,8 @@ export interface ContractTypeTemplate { defaultPhase: ContractPhase; /** Whether this is a built-in type (always available) */ isBuiltin: boolean; + /** Optional mapping from phase ID to display name */ + phaseNames?: Record<string, string>; } /** Response from list contract types endpoint */ @@ -1680,6 +1682,8 @@ 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; @@ -1704,6 +1708,10 @@ 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; @@ -1739,6 +1747,10 @@ export interface CreateContractRequest { initialPhase?: ContractPhase; /** 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/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx index aa62bd9..bb66215 100644 --- a/makima/frontend/src/routes/contracts.tsx +++ b/makima/frontend/src/routes/contracts.tsx @@ -93,6 +93,8 @@ 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 - merges built-in types with user templates useEffect(() => { @@ -108,11 +110,12 @@ function ContractsPageContent() { // Convert user templates to ContractTypeTemplate format, excluding built-ins return templates .filter((t: { isBuiltIn?: boolean }) => !t.isBuiltIn) - .map((t: { id: string; name: string; description: string; phases: { id: string }[] }) => ({ + .map((t: { id: string; name: string; description: string; phases: { id: string; name: string }[] }) => ({ id: t.id, name: t.name, description: t.description, phases: t.phases.map((p: { id: string }) => p.id) as ContractPhase[], + phaseNames: Object.fromEntries(t.phases.map((p: { id: string; name: string }) => [p.id, p.name])), defaultPhase: (t.phases[0]?.id || "execute") as ContractPhase, isBuiltin: false, })); @@ -265,6 +268,8 @@ function ContractsPageContent() { contractType: contractType, initialPhase: initialPhase !== defaultPhaseForType ? initialPhase : undefined, localOnly: localOnly || undefined, + redTeamEnabled: redTeamEnabled || undefined, + redTeamPrompt: redTeamEnabled && redTeamPrompt.trim() ? redTeamPrompt.trim() : undefined, }; try { @@ -340,6 +345,8 @@ function ContractsPageContent() { setRepoUrl(""); setRepoPath(""); setLocalOnly(false); + setRedTeamEnabled(false); + setRedTeamPrompt(""); setCreateError(null); }, []); @@ -652,11 +659,17 @@ function ContractsPageContent() { onChange={(e) => setInitialPhase(e.target.value as ContractPhase)} className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]" > - {(contractTypes.find((t) => t.id === contractType)?.phases || []).map((phase) => ( - <option key={phase} value={phase}> - {phase.charAt(0).toUpperCase() + phase.slice(1)} - </option> - ))} + {(() => { + const template = contractTypes.find((t) => t.id === contractType); + return (template?.phases || []).map((phase) => { + const displayName = template?.phaseNames?.[phase] || (phase.charAt(0).toUpperCase() + phase.slice(1)); + return ( + <option key={phase} value={phase}> + {displayName} + </option> + ); + }); + })()} </select> <p className="mt-1 font-mono text-xs text-[#8b949e]"> {contractType === "simple" @@ -705,6 +718,59 @@ function ContractsPageContent() { </p> </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/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo index b02179d..804859b 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/autopilotpanel.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/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/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/autopilotpanel.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 diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index 44fa590..8e83565 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -439,10 +439,10 @@ async fn run_supervisor( } SupervisorCommand::Pr(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - eprintln!("Creating PR for task {}...", args.task_id); + eprintln!("Creating PR for branch {}...", args.branch); let body = args.body.as_deref().unwrap_or(""); let result = client - .supervisor_pr(args.task_id, &args.title, body, &args.base) + .supervisor_pr(&args.branch, &args.title, body) .await?; println!("{}", serde_json::to_string(&result.0)?); } diff --git a/makima/src/daemon/api/supervisor.rs b/makima/src/daemon/api/supervisor.rs index 6b99de0..c841b21 100644 --- a/makima/src/daemon/api/supervisor.rs +++ b/makima/src/daemon/api/supervisor.rs @@ -54,10 +54,9 @@ pub struct MergeRequest { #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct CreatePrRequest { - pub task_id: Uuid, + pub branch: String, pub title: String, pub body: String, - pub base_branch: String, } #[derive(Serialize)] @@ -165,16 +164,14 @@ impl ApiClient { /// Create a pull request. pub async fn supervisor_pr( &self, - task_id: Uuid, + branch: &str, title: &str, body: &str, - base_branch: &str, ) -> Result<JsonValue, ApiError> { let req = CreatePrRequest { - task_id, + branch: branch.to_string(), title: title.to_string(), body: body.to_string(), - base_branch: base_branch.to_string(), }; self.post("/api/v1/mesh/supervisor/pr", &req).await } diff --git a/makima/src/daemon/cli/supervisor.rs b/makima/src/daemon/cli/supervisor.rs index 09f61db..9ad7aef 100644 --- a/makima/src/daemon/cli/supervisor.rs +++ b/makima/src/daemon/cli/supervisor.rs @@ -128,9 +128,9 @@ pub struct PrArgs { #[command(flatten)] pub common: SupervisorArgs, - /// Task ID to create PR for + /// Branch name to create PR from (e.g., "makima/feature-name") #[arg(index = 1)] - pub task_id: Uuid, + pub branch: String, /// PR title #[arg(long)] @@ -139,10 +139,6 @@ pub struct PrArgs { /// PR body/description #[arg(long)] pub body: Option<String>, - - /// Base branch (default: main) - #[arg(long, default_value = "main")] - pub base: String, } /// Arguments for diff command. diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index f0da860..8c5f8d7 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -669,7 +669,7 @@ 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" --base main +makima supervisor pr "makima/user-authentication" --title "Add user authentication" ``` ## Available Tools (via makima supervisor) @@ -701,7 +701,7 @@ makima supervisor branch <branch_name> [--from <task_id|sha>] makima supervisor merge <task_id> [--to <branch>] [--squash] # Create a pull request -makima supervisor pr <task_id> --title "Title" [--body "Body"] [--base main] +makima supervisor pr <branch> --title "Title" [--body "Body"] # View a task's diff makima supervisor diff <task_id> @@ -838,7 +838,7 @@ Common deliverable IDs by phase: 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 "..." --base main` +6. **Create PR when done** - use `pr "makima/{name}" --title "..."` 7. **Ask when unsure** - use `ask` to get user feedback on decisions ## Standard Workflow @@ -849,7 +849,7 @@ Common deliverable IDs by phase: - `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 "..." --base main` - Create PR +4. `pr "makima/{name}" --title "..."` - Create PR ## Important Reminders @@ -875,7 +875,7 @@ When you receive an `[ACTION REQUIRED]` message from the system: 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 "..." --base main` +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` @@ -2016,14 +2016,16 @@ impl TaskManager { title, body, base_branch, + branch, } => { tracing::info!( task_id = %task_id, title = %title, base_branch = %base_branch, + branch = %branch, "Creating pull request" ); - self.handle_create_pr(task_id, title, body, base_branch).await?; + self.handle_create_pr(task_id, title, body, base_branch, branch).await?; } DaemonCommand::GetTaskDiff { task_id, @@ -3135,6 +3137,7 @@ impl TaskManager { title: String, body: Option<String>, base_branch: String, + branch: String, ) -> Result<(), DaemonError> { // Get worktree path - this works even for completed tasks by scanning worktrees directory let worktree_path = match self.get_task_worktree_path(task_id).await { @@ -3153,30 +3156,19 @@ impl TaskManager { } }; - // Get base_branch from in-memory tasks if available (for fallback) - let task_base_branch = { - let tasks = self.tasks.read().await; - tasks.get(&task_id).and_then(|t| t.base_branch.clone()) - }; - - // Use task's base_branch if the provided one is the default "main" and task has a detected one - let effective_base_branch = if base_branch == "main" { - task_base_branch.unwrap_or(base_branch) - } else { - base_branch - }; - tracing::info!( task_id = %task_id, - effective_base_branch = %effective_base_branch, + base_branch = %base_branch, + branch = %branch, worktree_path = %worktree_path.display(), - "Creating PR with effective base branch" + "Creating PR" ); - // Push the current branch first + // Push the branch to origin + let push_refspec = format!("HEAD:refs/heads/{}", branch); let push_result = tokio::process::Command::new("git") .current_dir(&worktree_path) - .args(["push", "-u", "origin", "HEAD"]) + .args(["push", "-u", "origin", &push_refspec]) .output() .await; @@ -3195,7 +3187,7 @@ impl TaskManager { // Create PR using gh CLI let mut pr_cmd = tokio::process::Command::new("gh"); pr_cmd.current_dir(&worktree_path); - pr_cmd.args(["pr", "create", "--title", &title, "--base", &effective_base_branch]); + pr_cmd.args(["pr", "create", "--title", &title, "--base", &base_branch, "--head", &branch]); if let Some(ref body_text) = body { pr_cmd.args(["--body", body_text]); diff --git a/makima/src/daemon/ws/protocol.rs b/makima/src/daemon/ws/protocol.rs index bd13975..e971798 100644 --- a/makima/src/daemon/ws/protocol.rs +++ b/makima/src/daemon/ws/protocol.rs @@ -693,9 +693,11 @@ pub enum DaemonCommand { task_id: Uuid, title: String, body: Option<String>, - /// Base branch for the PR (default: main). + /// Base branch for the PR. #[serde(rename = "baseBranch")] base_branch: String, + /// Source branch name to push and create PR from. + branch: String, }, /// Get the diff for a task's changes. diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs index 016367f..a0a3a96 100644 --- a/makima/src/server/handlers/mesh_supervisor.rs +++ b/makima/src/server/handlers/mesh_supervisor.rs @@ -1267,15 +1267,9 @@ pub struct MergeTaskResponse { #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CreatePRRequest { - pub task_id: Uuid, + pub branch: String, pub title: String, pub body: Option<String>, - #[serde(default = "default_base_branch")] - pub base_branch: String, -} - -fn default_base_branch() -> String { - "main".to_string() } /// Response for PR creation. @@ -1513,48 +1507,53 @@ pub async fn create_pr( headers: HeaderMap, Json(request): Json<CreatePRRequest>, ) -> impl IntoResponse { - let (_supervisor_id, owner_id) = match verify_supervisor_auth(&state, &headers, None).await { + let (supervisor_id, _owner_id) = match verify_supervisor_auth(&state, &headers, None).await { Ok(ids) => ids, Err(e) => return e.into_response(), }; let pool = state.db_pool.as_ref().unwrap(); - // Get the target task - let task = match repository::get_task_for_owner(pool, request.task_id, owner_id).await { + // Get the supervisor's own task to find daemon and base_branch + let task = match repository::get_task(pool, supervisor_id).await { Ok(Some(t)) => t, Ok(None) => { return ( StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Task not found")), + Json(ApiError::new("NOT_FOUND", "Supervisor task not found")), ).into_response(); } Err(e) => { - tracing::error!(error = %e, "Failed to get task"); + tracing::error!(error = %e, "Failed to get supervisor task"); return ( StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", "Failed to get task")), + Json(ApiError::new("DB_ERROR", "Failed to get supervisor task")), ).into_response(); } }; - // Get daemon running the task + // Get daemon running the supervisor let Some(daemon_id) = task.daemon_id else { return ( StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("NO_DAEMON", "Task has no assigned daemon")), + Json(ApiError::new("NO_DAEMON", "Supervisor has no assigned daemon")), ).into_response(); }; + // Use base_branch from the task's repository config, falling back to "main" + let base_branch = task.base_branch.unwrap_or_else(|| "main".to_string()); + // Subscribe to PR results BEFORE sending the command let mut rx = state.pr_results.subscribe(); - // Send CreatePR command to daemon + // Send CreatePR command to daemon using the supervisor's task ID + // (the branch is in the supervisor's worktree) let cmd = DaemonCommand::CreatePR { - task_id: request.task_id, + task_id: supervisor_id, title: request.title.clone(), body: request.body.clone(), - base_branch: request.base_branch.clone(), + base_branch, + branch: request.branch.clone(), }; if let Err(e) = state.send_daemon_command(daemon_id, cmd).await { @@ -1571,7 +1570,7 @@ pub async fn create_pr( loop { match rx.recv().await { Ok(notification) => { - if notification.task_id == request.task_id { + if notification.task_id == supervisor_id { return Some(notification); } // Not our task, keep waiting @@ -1594,7 +1593,7 @@ pub async fn create_pr( ( status, Json(CreatePRResponse { - task_id: request.task_id, + task_id: supervisor_id, success: notification.success, message: notification.message, pr_url: notification.pr_url, @@ -1607,7 +1606,7 @@ pub async fn create_pr( ( StatusCode::GATEWAY_TIMEOUT, Json(CreatePRResponse { - task_id: request.task_id, + task_id: supervisor_id, success: false, message: "PR creation timed out waiting for daemon response".to_string(), pr_url: None, diff --git a/makima/src/server/state.rs b/makima/src/server/state.rs index bf8f6f2..041b101 100644 --- a/makima/src/server/state.rs +++ b/makima/src/server/state.rs @@ -461,9 +461,11 @@ pub enum DaemonCommand { task_id: Uuid, title: String, body: Option<String>, - /// Base branch for the PR (default: main) + /// Base branch for the PR #[serde(rename = "baseBranch")] base_branch: String, + /// Source branch name to push and create PR from + branch: String, }, /// Get the diff for a task's changes |
