summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-20 00:46:21 +0000
committerGitHub <noreply@github.com>2026-02-20 00:46:21 +0000
commitaa974c4888851f10c782e07b9d9bff7a6f1aef15 (patch)
treeb1cd40dedeaff64fe447fdd9c6c1000870b6a536
parented84d7ec5ece272a2cb8dabd36cbd6074df0887e (diff)
downloadsoryu-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.tsx23
-rw-r--r--makima/frontend/src/components/directives/DirectiveList.tsx17
-rw-r--r--makima/src/daemon/worktree/manager.rs59
-rw-r--r--makima/src/server/handlers/mesh_supervisor.rs41
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