From 151e9d87e117b7980e6aad522ac8f3633eeca87a Mon Sep 17 00:00:00 2001 From: soryu Date: Mon, 2 Feb 2026 02:34:50 +0000 Subject: Make makima more opinionated and structured --- makima/src/bin/makima.rs | 16 +- makima/src/daemon/api/contract.rs | 4 - makima/src/daemon/api/mod.rs | 2 - makima/src/daemon/api/red_team.rs | 39 -- makima/src/daemon/api/supervisor.rs | 4 - makima/src/daemon/cli/mod.rs | 57 +- makima/src/daemon/cli/red_team.rs | 26 - makima/src/daemon/cli/supervisor.rs | 4 - makima/src/daemon/task/manager.rs | 658 ++++++---------------- makima/src/db/models.rs | 94 ---- makima/src/db/repository.rs | 131 +---- makima/src/server/handlers/contract_chat.rs | 10 +- makima/src/server/handlers/contracts.rs | 5 - makima/src/server/handlers/mesh.rs | 4 - makima/src/server/handlers/mesh_chat.rs | 1 - makima/src/server/handlers/mesh_daemon.rs | 62 ++ makima/src/server/handlers/mesh_red_team.rs | 497 ---------------- makima/src/server/handlers/mesh_supervisor.rs | 306 +--------- makima/src/server/handlers/mod.rs | 1 - makima/src/server/handlers/templates.rs | 419 +------------- makima/src/server/handlers/transcript_analysis.rs | 4 - makima/src/server/mod.rs | 18 +- 22 files changed, 262 insertions(+), 2100 deletions(-) delete mode 100644 makima/src/daemon/api/red_team.rs delete mode 100644 makima/src/daemon/cli/red_team.rs delete mode 100644 makima/src/server/handlers/mesh_red_team.rs (limited to 'makima/src') 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> { 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 Result<(), Box> { - match cmd { - RedTeamCommand::Notify(args) => { - makima::daemon::cli::handle_notify(args).await?; - Ok(()) - } - } -} - /// Load contracts from API async fn load_contracts(client: &ApiClient) -> Result, Box> { 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, #[serde(skip_serializing_if = "Option::is_none")] pub auto_merge_local: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub red_team_enabled: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub red_team_prompt: Option, } 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, - - /// The file path where the issue was detected (optional) - #[serde(skip_serializing_if = "Option::is_none")] - pub file_path: Option, - - /// Additional context about the issue (optional) - #[serde(skip_serializing_if = "Option::is_none")] - pub context: Option, -} - -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 { - 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, #[serde(skip_serializing_if = "Option::is_none")] pub checkpoint_sha: Option, - /// 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, - - /// Related file path (optional) - #[arg(long)] - pub file: Option, - - /// Additional context (optional) - #[arg(long)] - pub context: Option, -} - 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> { - 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, - - /// 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 ` +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 [timeout_seconds] - -# Read a file from any task's worktree -makima supervisor read-file - -# Get the full task tree structure -makima supervisor tree -``` - -### Git Operations -```bash -# Create a new branch -makima supervisor branch [--from ] - -# Merge a task's changes to a branch -makima supervisor merge [--to ] [--squash] - -# Create a pull request -makima supervisor pr --title "Title" [--body "Body"] - -# View a task's diff -makima supervisor diff - -# 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 - -# Mark a phase deliverable as complete (e.g., 'plan-document', 'pull-request') -makima supervisor mark-deliverable [--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 [timeout_seconds] # Wait for task completion +makima supervisor tasks # List all tasks +makima supervisor tree # View task tree +makima supervisor diff # View task changes +makima supervisor read-file # 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 ` -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 # 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 -makima supervisor merge --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 -makima supervisor merge --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 -makima supervisor merge --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 -``` - -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 --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 ` +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 { + // 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 = None; - let mut patch_base_sha: Option = None; - let mut patch_files_count: Option = 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, @@ -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 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, /// 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, - /// 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, - /// 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, } /// Request payload for updating a contract @@ -2542,67 +2509,6 @@ pub struct SupervisorHeartbeatRequest { pub pending_task_ids: Vec, } -// ============================================================================= -// 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, - - pub message: String, - pub severity: String, - pub file_path: Option, - pub context: Option, - - pub delivered: bool, - pub delivered_at: Option>, - pub acknowledged: bool, - pub acknowledged_at: Option>, - - pub created_at: DateTime, -} - -/// 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 { - 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 Result Result, 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( 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, - file_path: Option<&str>, - context: Option<&str>, -) -> Result { - 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 { - 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, 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 { - 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, - /// File path related to the issue (optional) - pub file_path: Option, - /// Additional context about the issue - pub context: Option, -} - -/// 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, -} - -/// 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, - pub file_path: Option, - pub context: Option, - pub delivered: bool, - pub created_at: chrono::DateTime, -} - -// ============================================================================= -// 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)> { - 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, - 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, - headers: HeaderMap, - Json(request): Json, -) -> 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, - 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, /// Repository URL for the task (optional - if not provided, will be looked up from contract). pub repository_url: Option, - /// 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; } @@ -2583,239 +2518,6 @@ pub async fn rewind_conversation( .into_response() } -// ============================================================================= -// 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 \ - --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 { - // 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, } -/// 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, - 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, - Authenticated(auth): Authenticated, - Json(req): Json, -) -> 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, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> 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, - Authenticated(auth): Authenticated, - Path(id): Path, - Json(req): Json, -) -> 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, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> 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", -- cgit v1.2.3