diff options
| author | soryu <soryu@soryu.co> | 2026-01-15 11:57:43 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-15 17:12:04 +0000 |
| commit | 3efdab36ca61a6795454668881d5b925abe22bd3 (patch) | |
| tree | 0fd96e527f45a3da31dfc073b07cd55ba284e550 /makima/frontend/src/components | |
| parent | 63b2e347b2ecadc6a48062e10e0a7e19b6102631 (diff) | |
| download | soryu-3efdab36ca61a6795454668881d5b925abe22bd3.tar.gz soryu-3efdab36ca61a6795454668881d5b925abe22bd3.zip | |
Fixup: Add cleanup and isolation features to makima
Add comprehensive CLI documentation
- Create makima/docs/CLI.md with complete command reference for:
- makima server: HTTP/WebSocket server options
- makima daemon: Worker daemon configuration
- makima supervisor: Contract orchestration commands
- makima contract: Task-contract interaction commands
- Include configuration file examples and environment variables
- Add usage workflows for common scenarios
- Update makima/README.md with CLI overview and link to docs
Add GitHub Actions release workflow for v0.1.0
Creates automated release workflow that:
- Triggers on v* tag pushes
- Builds binaries for Linux x86_64, macOS x86_64, and macOS ARM64
- Uses Rust nightly toolchain (required for edition 2024)
- Packages binaries as .tar.gz archives
- Creates GitHub release with installation instructions
fix(ci): update macOS runner for x86_64 builds
Replace deprecated macos-13 runner with macos-15-intel for
x86_64-apple-darwin target. The macos-13 runner has been retired
by GitHub Actions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add dismissing notifications and fix CLI task ID arg
Add worktree cleanup when contracts complete or are deleted (#21)
- Add CleanupWorktree daemon command variant
- Handle CleanupWorktree in daemon task manager
- Add cleanup_contract_worktrees helper function
- Trigger cleanup when contract status becomes 'completed'
- Trigger cleanup before contract deletion
Add Autonomous Loop Mode for persistent task completion (#20)
Implements the "Autonomous Loop Mode" feature inspired by Ralph for Claude Code.
This enables tasks to automatically restart and continue working until they
explicitly signal completion via a COMPLETION_GATE block.
Key features:
- Exit confirmation via COMPLETION_GATE: Tasks must output a <COMPLETION_GATE>
block with `ready: true` to signal completion. Without this, the task
auto-restarts using `claude --continue` to resume the conversation.
- Circuit breaker: Prevents infinite loops by detecting:
* Maximum iteration limit (default: 10)
* No progress for N consecutive iterations (default: 3)
* Same error repeated N times (default: 5)
- spawn_continue: New ProcessManager method to spawn Claude with the
`--continue` flag, resuming from the previous session state.
Toggle: Enable via `autonomous_loop` flag on contracts. When set, all tasks
spawned for that contract will run in autonomous loop mode.
Files changed:
- completion_gate.rs: COMPLETION_GATE parser and CircuitBreaker logic
- claude.rs: spawn_continue() for --continue mode spawning
- manager.rs: Autonomous loop iteration logic in run_task()
- protocol.rs: autonomousLoop field in DaemonCommand::SpawnTask
- models.rs/repository.rs: autonomous_loop column on contracts/tasks
- Migration: Adds autonomous_loop columns to contracts and tasks tables
Add get-task and output commands to supervisor CLI (#24)
Add two new supervisor subcommands:
- `makima supervisor task <task_id>` - Get individual task details
- `makima supervisor output <task_id>` - Get task output/claude log
This allows supervisors to fetch task details and claude output
directly from the CLI instead of using curl to call the task API.
Add optional bubblewrap sandboxing for Claude processes (#23)
Add --bubblewrap flag and process.bubblewrap config section to enable
running Claude Code in a bubblewrap sandbox for process isolation.
When enabled, claude processes run with filesystem restrictions:
- Root filesystem mounted read-only
- Working directory (worktree) mounted read-write
- Fresh /dev, /proc, /tmp
- Network access preserved for API calls
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/frontend/src/components')
| -rw-r--r-- | makima/frontend/src/components/SupervisorQuestionNotification.tsx | 106 | ||||
| -rw-r--r-- | makima/frontend/src/components/mesh/TaskOutput.tsx | 151 |
2 files changed, 161 insertions, 96 deletions
diff --git a/makima/frontend/src/components/SupervisorQuestionNotification.tsx b/makima/frontend/src/components/SupervisorQuestionNotification.tsx index 6a71de2..1457d86 100644 --- a/makima/frontend/src/components/SupervisorQuestionNotification.tsx +++ b/makima/frontend/src/components/SupervisorQuestionNotification.tsx @@ -1,50 +1,22 @@ -import { useState } from "react"; import { useNavigate } from "react-router"; import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext"; -import type { PendingQuestion } from "../lib/api"; export function SupervisorQuestionNotification() { const navigate = useNavigate(); - const { pendingQuestions, submitAnswer } = useSupervisorQuestions(); - const [expandedQuestion, setExpandedQuestion] = useState<string | null>(null); - const [response, setResponse] = useState(""); - const [submitting, setSubmitting] = useState(false); + const { notificationQuestions, dismissNotification } = useSupervisorQuestions(); - if (pendingQuestions.length === 0) { + if (notificationQuestions.length === 0) { return null; } - const handleGoToTask = (taskId: string) => { + const handleGoToTask = (questionId: string, taskId: string) => { + dismissNotification(questionId); navigate(`/mesh/${taskId}`); }; - const handleExpand = (questionId: string) => { - setExpandedQuestion(expandedQuestion === questionId ? null : questionId); - setResponse(""); - }; - - const handleSubmit = async (question: PendingQuestion) => { - if (!response.trim()) return; - - setSubmitting(true); - const success = await submitAnswer(question.questionId, response.trim()); - setSubmitting(false); - - if (success) { - setExpandedQuestion(null); - setResponse(""); - } - }; - - const handleChoiceSelect = async (question: PendingQuestion, choice: string) => { - setSubmitting(true); - await submitAnswer(question.questionId, choice); - setSubmitting(false); - }; - return ( <div className="fixed bottom-4 right-4 z-50 max-w-md space-y-2"> - {pendingQuestions.map((question) => ( + {notificationQuestions.map((question) => ( <div key={question.questionId} className="bg-[#0d1b2d] border border-amber-500/50 rounded-lg shadow-lg overflow-hidden" @@ -54,24 +26,15 @@ export function SupervisorQuestionNotification() { <div className="flex items-center gap-2"> <span className="text-amber-400 text-lg">?</span> <span className="font-mono text-sm text-amber-300 uppercase"> - Supervisor Question + Task needs input </span> </div> - <div className="flex items-center gap-2"> - <button - onClick={() => handleGoToTask(question.taskId)} - className="px-2 py-1 font-mono text-xs text-amber-400 hover:text-amber-300 transition-colors" - title="Go to task" - > - View Task - </button> - <button - onClick={() => handleExpand(question.questionId)} - className="px-2 py-1 font-mono text-xs text-amber-400 border border-amber-500/30 hover:border-amber-400/50 transition-colors uppercase" - > - {expandedQuestion === question.questionId ? "Collapse" : "Answer"} - </button> - </div> + <button + onClick={() => handleGoToTask(question.questionId, question.taskId)} + className="px-3 py-1 font-mono text-xs text-amber-400 border border-amber-500/30 hover:border-amber-400/50 hover:bg-amber-900/20 transition-colors uppercase" + > + View Task + </button> </div> {/* Question preview */} @@ -81,53 +44,10 @@ export function SupervisorQuestionNotification() { {question.context} </div> )} - <p className="text-sm text-[#dbe7ff] font-mono"> + <p className="text-sm text-[#dbe7ff] font-mono line-clamp-2"> {question.question} </p> </div> - - {/* Expanded answer section */} - {expandedQuestion === question.questionId && ( - <div className="px-4 pb-4 border-t border-amber-500/20 pt-3"> - {question.choices.length > 0 ? ( - // Choice buttons - <div className="space-y-2"> - <p className="text-xs text-[#8b949e] font-mono uppercase mb-2"> - Select an option: - </p> - {question.choices.map((choice, idx) => ( - <button - key={idx} - onClick={() => handleChoiceSelect(question, choice)} - disabled={submitting} - className="w-full px-3 py-2 text-left font-mono text-sm text-[#dbe7ff] bg-[#0a1628] border border-[#3f6fb3] hover:border-amber-400/50 hover:bg-amber-900/20 disabled:opacity-50 transition-colors" - > - {choice} - </button> - ))} - </div> - ) : ( - // Free-form text input - <div className="space-y-2"> - <textarea - value={response} - onChange={(e) => setResponse(e.target.value)} - placeholder="Type your response..." - rows={3} - className="w-full px-3 py-2 bg-[#0a1628] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-amber-400 resize-none" - disabled={submitting} - /> - <button - onClick={() => handleSubmit(question)} - disabled={submitting || !response.trim()} - className="w-full px-4 py-2 font-mono text-xs text-[#0a1628] bg-amber-500 hover:bg-amber-400 disabled:bg-amber-700 disabled:cursor-not-allowed transition-colors uppercase" - > - {submitting ? "Submitting..." : "Submit Response"} - </button> - </div> - )} - </div> - )} </div> ))} </div> diff --git a/makima/frontend/src/components/mesh/TaskOutput.tsx b/makima/frontend/src/components/mesh/TaskOutput.tsx index cb0eba3..d53429d 100644 --- a/makima/frontend/src/components/mesh/TaskOutput.tsx +++ b/makima/frontend/src/components/mesh/TaskOutput.tsx @@ -16,9 +16,23 @@ interface TaskOutputProps { taskId?: string | null; /** Callback when user sends input (to show it immediately in output) */ onUserInput?: (message: string) => void; + /** Set of pending question IDs (for supervisor questions) */ + pendingQuestionIds?: Set<string>; + /** Callback to answer a supervisor question */ + onAnswerQuestion?: (questionId: string, response: string) => Promise<void>; } -export function TaskOutput({ entries, isStreaming, viewingSubtaskName, onClearSubtaskView, onClear, taskId, onUserInput }: TaskOutputProps) { +export function TaskOutput({ + entries, + isStreaming, + viewingSubtaskName, + onClearSubtaskView, + onClear, + taskId, + onUserInput, + pendingQuestionIds, + onAnswerQuestion, +}: TaskOutputProps) { const containerRef = useRef<HTMLDivElement>(null); const [autoScroll, setAutoScroll] = useState(true); const [inputValue, setInputValue] = useState(""); @@ -135,7 +149,12 @@ export function TaskOutput({ entries, isStreaming, viewingSubtaskName, onClearSu ) : ( <div className="space-y-3"> {entries.map((entry, idx) => ( - <OutputEntryRenderer key={idx} entry={entry} /> + <OutputEntryRenderer + key={idx} + entry={entry} + pendingQuestionIds={pendingQuestionIds} + onAnswerQuestion={onAnswerQuestion} + /> ))} {isStreaming && ( <span className="inline-block w-2 h-4 bg-[#9bc3ff] animate-pulse" /> @@ -177,7 +196,13 @@ export function TaskOutput({ entries, isStreaming, viewingSubtaskName, onClearSu ); } -function OutputEntryRenderer({ entry }: { entry: TaskOutputEvent }) { +interface OutputEntryRendererProps { + entry: TaskOutputEvent; + pendingQuestionIds?: Set<string>; + onAnswerQuestion?: (questionId: string, response: string) => Promise<void>; +} + +function OutputEntryRenderer({ entry, pendingQuestionIds, onAnswerQuestion }: OutputEntryRendererProps) { const [expanded, setExpanded] = useState(false); switch (entry.messageType) { @@ -278,11 +303,131 @@ function OutputEntryRenderer({ entry }: { entry: TaskOutputEvent }) { case "auth_required": return <AuthRequiredEntry entry={entry} />; + case "supervisor_question": + return ( + <SupervisorQuestionEntry + entry={entry} + pendingQuestionIds={pendingQuestionIds} + onAnswerQuestion={onAnswerQuestion} + /> + ); + default: return null; } } +function SupervisorQuestionEntry({ + entry, + pendingQuestionIds, + onAnswerQuestion, +}: { + entry: TaskOutputEvent; + pendingQuestionIds?: Set<string>; + onAnswerQuestion?: (questionId: string, response: string) => Promise<void>; +}) { + const questionId = entry.toolInput?.question_id as string; + const choices = (entry.toolInput?.choices as string[]) || []; + const context = entry.toolInput?.context as string | null; + + const [customInput, setCustomInput] = useState(""); + const [showOther, setShowOther] = useState(false); + const [submitting, setSubmitting] = useState(false); + + const isPending = pendingQuestionIds?.has(questionId) ?? false; + + const handleChoiceSelect = async (choice: string) => { + if (!onAnswerQuestion || submitting) return; + setSubmitting(true); + try { + await onAnswerQuestion(questionId, choice); + } finally { + setSubmitting(false); + } + }; + + const handleOtherSubmit = async () => { + if (!onAnswerQuestion || !customInput.trim() || submitting) return; + setSubmitting(true); + try { + await onAnswerQuestion(questionId, customInput.trim()); + setCustomInput(""); + } finally { + setSubmitting(false); + } + }; + + return ( + <div className="bg-amber-900/20 border border-amber-500/50 rounded p-3 my-2"> + <div className="flex items-center gap-2 text-amber-400 font-semibold mb-2"> + <span>?</span> + <span>Question</span> + {!isPending && ( + <span className="text-green-400 text-xs font-normal">(Answered)</span> + )} + </div> + + {context && ( + <p className="text-amber-200/60 text-xs mb-2 uppercase">{context}</p> + )} + + <p className="text-amber-100 mb-3">{entry.content}</p> + + {isPending && ( + <div className="space-y-2"> + {choices.length > 0 && ( + <div className="flex flex-wrap gap-2"> + {choices.map((choice, idx) => ( + <button + key={idx} + onClick={() => handleChoiceSelect(choice)} + disabled={submitting} + className="px-3 py-1.5 text-sm font-mono bg-amber-500/20 border border-amber-500/50 hover:bg-amber-500/30 disabled:opacity-50 text-amber-100 transition-colors" + > + {choice} + </button> + ))} + </div> + )} + + {/* Other option */} + {!showOther ? ( + <button + onClick={() => setShowOther(true)} + className="text-xs text-amber-400 hover:text-amber-300 transition-colors" + > + + Other (custom response) + </button> + ) : ( + <div className="flex gap-2"> + <input + type="text" + value={customInput} + onChange={(e) => setCustomInput(e.target.value)} + placeholder="Type custom response..." + disabled={submitting} + className="flex-1 px-2 py-1 bg-[#0a1525] border border-amber-500/30 text-amber-100 text-sm rounded focus:outline-none focus:border-amber-400" + onKeyDown={(e) => { + if (e.key === "Enter" && customInput.trim()) { + handleOtherSubmit(); + } + }} + /> + <button + onClick={handleOtherSubmit} + disabled={submitting || !customInput.trim()} + className="px-3 py-1 bg-amber-500 text-black text-sm font-medium rounded disabled:opacity-50 disabled:cursor-not-allowed transition-colors hover:bg-amber-400" + > + {submitting ? "..." : "Submit"} + </button> + </div> + )} + </div> + )} + </div> + ); +} + function AuthRequiredEntry({ entry }: { entry: TaskOutputEvent }) { const [authCode, setAuthCode] = useState(""); const [submitting, setSubmitting] = useState(false); |
