diff options
| author | soryu <soryu@soryu.co> | 2026-02-20 00:46:21 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-20 00:46:21 +0000 |
| commit | aa974c4888851f10c782e07b9d9bff7a6f1aef15 (patch) | |
| tree | b1cd40dedeaff64fe447fdd9c6c1000870b6a536 | |
| parent | ed84d7ec5ece272a2cb8dabd36cbd6074df0887e (diff) | |
| download | soryu-aa974c4888851f10c782e07b9d9bff7a6f1aef15.tar.gz soryu-aa974c4888851f10c782e07b9d9bff7a6f1aef15.zip | |
fix: reconcile:on blocking, pending question notifications, and infra improvements (#73)
* feat: soryu-co/soryu - makima: Fix contracts page overflow - constrain layout to viewport height
* feat: soryu-co/soryu - makima: Add git fetch to create_worktree and improve completion prompt merge conflict handling
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Add pending question notification badge to directive sidebar and nav
* feat: soryu-co/soryu - makima: Fix reconcile:on blocking - make phaseguard poll indefinitely instead of returning immediately
| -rw-r--r-- | makima/frontend/src/components/NavStrip.tsx | 23 | ||||
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveList.tsx | 17 | ||||
| -rw-r--r-- | makima/src/daemon/worktree/manager.rs | 59 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_supervisor.rs | 41 |
4 files changed, 41 insertions, 99 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx index 117f4e1..4932427 100644 --- a/makima/frontend/src/components/NavStrip.tsx +++ b/makima/frontend/src/components/NavStrip.tsx @@ -1,4 +1,5 @@ import { useAuth } from "../contexts/AuthContext"; +import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext"; import { RewriteLink } from "./RewriteLink"; interface NavLink { @@ -19,6 +20,8 @@ const NAV_LINKS: NavLink[] = [ export function NavStrip() { const { isAuthenticated, isAuthConfigured, signOut, user } = useAuth(); + const { pendingQuestions } = useSupervisorQuestions(); + const directiveQuestionCount = pendingQuestions.filter(q => q.directiveId).length; const handleSignOut = async () => { await signOut(); @@ -38,14 +41,18 @@ export function NavStrip() { </span> <div className="flex flex-wrap gap-2 items-center flex-1"> {NAV_LINKS.map((link) => ( - <RewriteLink - key={link.label} - to={link.href} - disabled={link.requiresAuth && !hasAccess} - external={link.external} - > - {link.label} - </RewriteLink> + <span key={link.label} className="relative inline-flex items-center"> + <RewriteLink + to={link.href} + disabled={link.requiresAuth && !hasAccess} + external={link.external} + > + {link.label} + </RewriteLink> + {link.label === "Directives" && directiveQuestionCount > 0 && ( + <span className="ml-0.5 inline-block w-2 h-2 rounded-full bg-amber-400 animate-pulse" title={`${directiveQuestionCount} pending question(s)`} /> + )} + </span> ))} </div> <div className="flex items-center gap-2 pl-2.5 border-l border-[rgba(117,170,252,0.35)]"> diff --git a/makima/frontend/src/components/directives/DirectiveList.tsx b/makima/frontend/src/components/directives/DirectiveList.tsx index 6393ea7..6a9c486 100644 --- a/makima/frontend/src/components/directives/DirectiveList.tsx +++ b/makima/frontend/src/components/directives/DirectiveList.tsx @@ -1,4 +1,6 @@ +import { useMemo } from "react"; import type { DirectiveSummary, DirectiveStatus } from "../../lib/api"; +import { useSupervisorQuestions } from "../../contexts/SupervisorQuestionsContext"; const STATUS_BADGE: Record<DirectiveStatus, { color: string; label: string }> = { draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" }, @@ -16,6 +18,18 @@ interface DirectiveListProps { } export function DirectiveList({ directives, selectedId, onSelect, onCreate }: DirectiveListProps) { + const { pendingQuestions } = useSupervisorQuestions(); + + const questionsPerDirective = useMemo(() => { + const counts = new Map<string, number>(); + for (const q of pendingQuestions) { + if (q.directiveId) { + counts.set(q.directiveId, (counts.get(q.directiveId) || 0) + 1); + } + } + return counts; + }, [pendingQuestions]); + return ( <div className="flex flex-col h-full"> <div className="flex items-center justify-between px-3 py-2 border-b border-dashed border-[rgba(117,170,252,0.2)]"> @@ -55,6 +69,9 @@ export function DirectiveList({ directives, selectedId, onSelect, onCreate }: Di <span className="text-[12px] font-mono text-white truncate pr-2"> {d.title} </span> + {questionsPerDirective.has(d.id) && ( + <span className="inline-block w-2.5 h-2.5 rounded-full bg-amber-400 animate-pulse shrink-0" title={`${questionsPerDirective.get(d.id)} pending question(s)`} /> + )} <span className={`text-[9px] font-mono ${badge.color} border rounded px-1.5 py-0.5 shrink-0`} > diff --git a/makima/src/daemon/worktree/manager.rs b/makima/src/daemon/worktree/manager.rs index 489c488..ea26767 100644 --- a/makima/src/daemon/worktree/manager.rs +++ b/makima/src/daemon/worktree/manager.rs @@ -491,64 +491,7 @@ impl WorktreeManager { .output() .await; - // Prefer origin/{base_branch} to get latest remote state. - // If neither origin/{base_branch} nor {base_branch} exist (e.g. PR branch - // was deleted after merge), fall back to the repo's default branch. - let origin_ref = format!("origin/{}", base_branch); - let has_origin_ref = Command::new("git") - .args(["rev-parse", "--verify", &format!("refs/remotes/{}", origin_ref)]) - .current_dir(source_repo) - .output() - .await - .map(|o| o.status.success()) - .unwrap_or(false); - - let has_local_ref = if !has_origin_ref { - Command::new("git") - .args(["rev-parse", "--verify", &format!("refs/heads/{}", base_branch)]) - .current_dir(source_repo) - .output() - .await - .map(|o| o.status.success()) - .unwrap_or(false) - } else { - false // don't need to check — we already have origin ref - }; - - let start_point: String = if has_origin_ref { - origin_ref - } else if has_local_ref { - base_branch.to_string() - } else { - // Branch doesn't exist (likely deleted after PR merge) — use default branch - tracing::warn!( - task_id = %task_id, - base_branch = %base_branch, - "Base branch ref not found, falling back to default branch" - ); - let default_branch = self.detect_default_branch(source_repo).await?; - let default_origin = format!("origin/{}", default_branch); - let has_default_origin = Command::new("git") - .args(["rev-parse", "--verify", &format!("refs/remotes/{}", default_origin)]) - .current_dir(source_repo) - .output() - .await - .map(|o| o.status.success()) - .unwrap_or(false); - if has_default_origin { - default_origin - } else { - default_branch - } - }; - - tracing::info!( - task_id = %task_id, - start_point = %start_point, - "Using start point for new worktree branch" - ); - - // Create the worktree with a new branch based on the start point + // Create the worktree with a new branch based on the local base_branch let output = Command::new("git") .args([ "worktree", diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs index 90c6dc7..0ea1a57 100644 --- a/makima/src/server/handlers/mesh_supervisor.rs +++ b/makima/src/server/handlers/mesh_supervisor.rs @@ -1807,42 +1807,17 @@ pub async fn ask_question( ).into_response(); } - // If phaseguard is enabled (or directive reconcile mode), pause the supervisor task and return - // The task will be auto-resumed when a message is sent to it (e.g., when user answers) + // Determine if we should block indefinitely (phaseguard or directive reconcile mode) let use_phaseguard = request.phaseguard || (is_directive_context && directive_reconcile_mode); - if use_phaseguard { - // Pause the supervisor task - if let Some(daemon_id) = supervisor.daemon_id { - let cmd = DaemonCommand::PauseTask { task_id: supervisor_id }; - if let Err(e) = state.send_daemon_command(daemon_id, cmd).await { - tracing::warn!(supervisor_id = %supervisor_id, error = %e, "Failed to pause supervisor for phaseguard"); - } else { - tracing::info!(supervisor_id = %supervisor_id, "Paused supervisor for phaseguard question"); - } - } - - // Update task status to paused in DB - let update = crate::db::models::UpdateTaskRequest { - status: Some("paused".to_string()), - ..Default::default() - }; - if let Err(e) = repository::update_task_for_owner(pool, supervisor_id, owner_id, update).await { - tracing::warn!(supervisor_id = %supervisor_id, error = %e, "Failed to update task status to paused"); - } - - return ( - StatusCode::OK, - Json(AskQuestionResponse { - question_id, - response: None, - timed_out: false, - }), - ).into_response(); - } // Poll for response with timeout - // For directive tasks without reconcile mode, use 30s default timeout - let timeout_secs = if is_directive_context && !directive_reconcile_mode { + // - Phaseguard: block indefinitely until user responds + // - Directive tasks without reconcile mode: 30s default timeout + // - Contract tasks: use requested timeout_seconds + let timeout_secs = if use_phaseguard { + // Block indefinitely until user responds + u64::MAX / 2 + } else if is_directive_context && !directive_reconcile_mode { 30 } else { request.timeout_seconds.max(1) as u64 |
