From 36812212fa00667d9592f631d8f5cc4e7c28aa99 Mon Sep 17 00:00:00 2001 From: David Aronchick Date: Sun, 25 Jan 2026 16:31:04 -0800 Subject: [PATCH] feat: Add task management support and diagnostics endpoint Port changes from aronchick/multiclaude#66: - Add docs/TASK_MANAGEMENT.md with comprehensive guide for Claude Code's task management tools (TaskCreate/Update/List/Get) - Add internal/diagnostics package for machine-readable system diagnostics - Add 'multiclaude diagnostics' CLI command outputting JSON with: * Claude CLI version and capability detection * Task management support detection (v2.0+) * Environment variables (with sensitive value redaction) * System paths and tool versions * Daemon status and agent statistics - Add daemon startup diagnostics logging for monitoring - Update supervisor and worker prompts with task management guidance (adapted to new concise prompt style from #302) The diagnostics endpoint helps operators understand the multiclaude environment, and task management enables agents to organize complex multi-step work while maintaining the "focused PRs" principle. Co-Authored-By: Claude Opus 4.5 --- docs/TASK_MANAGEMENT.md | 168 +++++++++ internal/cli/cli.go | 41 +++ internal/daemon/daemon.go | 25 ++ internal/diagnostics/collector.go | 352 +++++++++++++++++++ internal/prompts/supervisor.md | 11 + internal/templates/agent-templates/worker.md | 17 + 6 files changed, 614 insertions(+) create mode 100644 docs/TASK_MANAGEMENT.md create mode 100644 internal/diagnostics/collector.go diff --git a/docs/TASK_MANAGEMENT.md b/docs/TASK_MANAGEMENT.md new file mode 100644 index 0000000..782451c --- /dev/null +++ b/docs/TASK_MANAGEMENT.md @@ -0,0 +1,168 @@ +# Task Management in multiclaude + +## Overview + +multiclaude agents can leverage Claude Code's built-in task management tools to track complex, multi-step work. This document explains how these tools work and when to use them. + +## Claude Code's Task Management Tools + +Claude Code provides four task management tools available to all agents: + +### TaskCreate +Creates a new task in the task list. + +**When to use:** +- Complex multi-step tasks requiring 3+ distinct steps +- Non-trivial operations that benefit from progress tracking +- User provides multiple tasks in a list +- You want to demonstrate thoroughness and organization + +**When NOT to use:** +- Single, straightforward tasks +- Trivial operations (1-2 steps) +- Tasks completable in <3 steps +- Purely conversational or informational work + +**Example:** +``` +TaskCreate({ + subject: "Fix authentication bug in login flow", + description: "Investigate and fix the issue where users can't log in with OAuth. Need to check middleware, token validation, and error handling.", + activeForm: "Fixing authentication bug" +}) +``` + +### TaskUpdate +Updates an existing task's status, owner, or details. + +**Status workflow:** `pending` → `in_progress` → `completed` + +**When to use:** +- Mark task as `in_progress` when starting work +- Mark task as `completed` when finished +- Update task details as requirements clarify +- Establish dependencies between tasks + +**IMPORTANT:** Only mark tasks as `completed` when FULLY done. If you encounter errors, blockers, or partial completion, keep status as `in_progress`. + +### TaskList +Lists all tasks with their current status, owner, and blockedBy dependencies. + +**When to use:** +- Check what tasks are available to work on +- See overall progress on a project +- Find tasks that are blocked +- After completing a task, to find next work + +### TaskGet +Retrieves full details of a specific task by ID. + +**When to use:** +- Before starting work on an assigned task +- To understand task dependencies +- To get complete requirements and context + +## Task Management vs Task Tool + +**Task Management (TaskCreate/Update/List/Get):** +- Tracks progress on multi-step work within a single agent session +- Creates todo-style checklists visible to users +- Helps organize complex workflows +- Persists within the conversation context + +**Task Tool (spawning sub-agents):** +- Delegates work to parallel sub-agents +- Enables concurrent execution of independent operations +- multiclaude already does this at the orchestration level with workers! + +## Best Practices for multiclaude Agents + +### For Worker Agents + +**Use task management when:** +- Your assigned task has multiple logical steps (e.g., "Implement authentication: add middleware, update routes, write tests") +- You want to show progress on a complex feature +- The user asks you to track progress explicitly + +**Don't overuse:** +- For simple bug fixes or single-file changes +- When you're just doing research/exploration +- For trivial operations + +**Example workflow:** +```bash +# Starting a complex task +TaskCreate({ + subject: "Add user authentication endpoint", + description: "Create /api/auth endpoint with JWT validation, rate limiting, and tests", + activeForm: "Adding authentication endpoint" +}) + +# Start work +TaskUpdate({ taskId: "1", status: "in_progress" }) + +# ... do the work ... + +# Complete when done +TaskUpdate({ taskId: "1", status: "completed" }) +``` + +### For Supervisor Agent + +**Use task management for:** +- Tracking multiple workers' overall progress +- Coordinating complex multi-worker efforts +- Breaking down large features into assignable chunks + +**Pattern for supervision:** +1. Create high-level tasks for major work items +2. Assign tasks to workers (use task metadata to track which worker owns what) +3. Update task status as workers report completion +4. Use TaskList to monitor overall progress + +### For Merge Queue Agent + +**Use task management for:** +- Tracking PRs through the merge process +- Managing multiple PR reviews/merges concurrently +- Organizing complex merge conflict resolutions + +## Task Management and PR Creation + +**IMPORTANT:** Task management is for tracking work, NOT for delaying PRs. + +- Create tasks to organize your work into logical blocks +- When a block (task) is complete and tests pass, create a PR immediately +- Don't wait for all tasks to be complete before creating PRs +- Each completed task should generally result in a focused PR + +**Good pattern:** +``` +Task 1: "Add validation function" → Complete → Create PR #1 +Task 2: "Wire validation into API" → Complete → Create PR #2 +Task 3: "Add error handling" → Complete → Create PR #3 +``` + +**Bad pattern:** +``` +Task 1: "Complete validation system" + - Subtask: Add function + - Subtask: Wire into API + - Subtask: Add error handling + → Wait for ALL to complete → Create massive PR +``` + +## Checking if Task Management is Available + +multiclaude automatically detects task management capabilities during daemon startup. Agents can assume these tools are available if running Claude Code v2.0+. + +To check manually: +```bash +multiclaude diagnostics --json | jq '.capabilities.task_management' +``` + +## Related Documentation + +- [Claude Agent SDK - Todo Tracking](https://platform.claude.com/docs/en/agent-sdk/todo-tracking) - Official documentation +- [AGENTS.md](AGENTS.md) - multiclaude agent architecture +- [CLAUDE.md](CLAUDE.md) - Development guide for multiclaude itself diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 30613a7..3e06628 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -16,6 +16,7 @@ import ( "github.com/dlorenc/multiclaude/internal/agents" "github.com/dlorenc/multiclaude/internal/bugreport" "github.com/dlorenc/multiclaude/internal/daemon" + "github.com/dlorenc/multiclaude/internal/diagnostics" "github.com/dlorenc/multiclaude/internal/errors" "github.com/dlorenc/multiclaude/internal/fork" "github.com/dlorenc/multiclaude/internal/format" @@ -720,6 +721,14 @@ func (c *CLI) registerCommands() { Run: c.bugReport, } + // Diagnostics command + c.rootCmd.Subcommands["diagnostics"] = &Command{ + Name: "diagnostics", + Description: "Show system diagnostics in machine-readable format", + Usage: "multiclaude diagnostics [--json] [--output ]", + Run: c.diagnostics, + } + // Version command c.rootCmd.Subcommands["version"] = &Command{ Name: "version", @@ -5939,6 +5948,38 @@ func (c *CLI) bugReport(args []string) error { return nil } +// diagnostics generates system diagnostics in machine-readable format +func (c *CLI) diagnostics(args []string) error { + flags, _ := ParseFlags(args) + + // Create collector and generate report + collector := diagnostics.NewCollector(c.paths, Version) + report, err := collector.Collect() + if err != nil { + return fmt.Errorf("failed to collect diagnostics: %w", err) + } + + // Always output as pretty JSON by default (unless --json=false for compact) + prettyJSON := flags["json"] != "false" + jsonOutput, err := report.ToJSON(prettyJSON) + if err != nil { + return fmt.Errorf("failed to format diagnostics as JSON: %w", err) + } + + // Check if output file specified + if outputFile, ok := flags["output"]; ok { + if err := os.WriteFile(outputFile, []byte(jsonOutput), 0644); err != nil { + return fmt.Errorf("failed to write diagnostics to %s: %w", outputFile, err) + } + fmt.Printf("Diagnostics written to: %s\n", outputFile) + return nil + } + + // Print to stdout + fmt.Println(jsonOutput) + return nil +} + // listBranchesWithPrefix returns all local branches with the given prefix func (c *CLI) listBranchesWithPrefix(repoPath, prefix string) ([]string, error) { cmd := exec.Command("git", "branch", "--list", prefix+"*") diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index dc598aa..f20d710 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -13,6 +13,7 @@ import ( "time" "github.com/dlorenc/multiclaude/internal/agents" + "github.com/dlorenc/multiclaude/internal/diagnostics" "github.com/dlorenc/multiclaude/internal/hooks" "github.com/dlorenc/multiclaude/internal/logging" "github.com/dlorenc/multiclaude/internal/messages" @@ -97,6 +98,9 @@ func (d *Daemon) Start() error { d.logger.Info("Daemon started successfully") + // Log system diagnostics for monitoring and debugging + d.logDiagnostics() + // Restore agents for tracked repos BEFORE starting health checks // This prevents race conditions where health check cleans up agents being restored d.restoreTrackedRepos() @@ -142,6 +146,27 @@ func (d *Daemon) TriggerWake() { d.wakeAgents() } +// logDiagnostics logs system diagnostics in machine-readable JSON format +func (d *Daemon) logDiagnostics() { + // Get version from CLI package (same as used by CLI) + version := "dev" + + collector := diagnostics.NewCollector(d.paths, version) + report, err := collector.Collect() + if err != nil { + d.logger.Error("Failed to collect diagnostics: %v", err) + return + } + + jsonOutput, err := report.ToJSON(false) // Compact JSON for logs + if err != nil { + d.logger.Error("Failed to format diagnostics: %v", err) + return + } + + d.logger.Info("System diagnostics: %s", jsonOutput) +} + // Stop stops the daemon func (d *Daemon) Stop() error { d.logger.Info("Stopping daemon") diff --git a/internal/diagnostics/collector.go b/internal/diagnostics/collector.go new file mode 100644 index 0000000..43dfcae --- /dev/null +++ b/internal/diagnostics/collector.go @@ -0,0 +1,352 @@ +package diagnostics + +import ( + "encoding/json" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + + "github.com/dlorenc/multiclaude/internal/state" + "github.com/dlorenc/multiclaude/pkg/config" +) + +// Report contains all diagnostic information in machine-readable format +type Report struct { + // Version information + Version VersionInfo `json:"version"` + Environment EnvironmentInfo `json:"environment"` + Capabilities CapabilitiesInfo `json:"capabilities"` + Tools ToolsInfo `json:"tools"` + Daemon DaemonInfo `json:"daemon"` + Statistics StatisticsInfo `json:"statistics"` +} + +// VersionInfo contains version details for multiclaude and dependencies +type VersionInfo struct { + Multiclaude string `json:"multiclaude"` + Go string `json:"go"` + IsDev bool `json:"is_dev"` +} + +// EnvironmentInfo contains environment variables and system information +type EnvironmentInfo struct { + OS string `json:"os"` + Arch string `json:"arch"` + HomeDir string `json:"home_dir"` + Paths PathsInfo `json:"paths"` + Variables map[string]string `json:"variables"` +} + +// PathsInfo contains multiclaude directory paths +type PathsInfo struct { + Root string `json:"root"` + StateFile string `json:"state_file"` + DaemonPID string `json:"daemon_pid"` + DaemonSock string `json:"daemon_sock"` + DaemonLog string `json:"daemon_log"` + ReposDir string `json:"repos_dir"` + WorktreesDir string `json:"worktrees_dir"` + OutputDir string `json:"output_dir"` + MessagesDir string `json:"messages_dir"` +} + +// CapabilitiesInfo describes what features are available +type CapabilitiesInfo struct { + TaskManagement bool `json:"task_management"` + ClaudeInstalled bool `json:"claude_installed"` + TmuxInstalled bool `json:"tmux_installed"` + GitInstalled bool `json:"git_installed"` +} + +// ToolsInfo contains version information for external tools +type ToolsInfo struct { + Claude ClaudeInfo `json:"claude"` + Tmux string `json:"tmux"` + Git string `json:"git"` +} + +// ClaudeInfo contains detailed information about the Claude CLI +type ClaudeInfo struct { + Installed bool `json:"installed"` + Version string `json:"version"` + Path string `json:"path"` +} + +// DaemonInfo contains information about the daemon process +type DaemonInfo struct { + Running bool `json:"running"` + PID int `json:"pid"` +} + +// StatisticsInfo contains agent and repository counts +type StatisticsInfo struct { + Repositories int `json:"repositories"` + Workers int `json:"workers"` + Supervisors int `json:"supervisors"` + MergeQueues int `json:"merge_queues"` + Workspaces int `json:"workspaces"` + ReviewAgents int `json:"review_agents"` +} + +// Collector gathers diagnostic information +type Collector struct { + paths *config.Paths + version string +} + +// NewCollector creates a new diagnostic collector +func NewCollector(paths *config.Paths, version string) *Collector { + return &Collector{ + paths: paths, + version: version, + } +} + +// Collect gathers all diagnostic information +func (c *Collector) Collect() (*Report, error) { + report := &Report{ + Version: VersionInfo{ + Multiclaude: c.version, + Go: runtime.Version(), + IsDev: strings.Contains(c.version, "dev") || strings.Contains(c.version, "unknown"), + }, + Environment: c.collectEnvironment(), + Tools: c.collectTools(), + Daemon: c.collectDaemon(), + Statistics: c.collectStatistics(), + } + + // Determine capabilities based on tool versions + report.Capabilities = c.determineCapabilities(report.Tools) + + return report, nil +} + +// collectEnvironment gathers environment information +func (c *Collector) collectEnvironment() EnvironmentInfo { + homeDir, _ := os.UserHomeDir() + + // Collect important environment variables + envVars := make(map[string]string) + importantVars := []string{ + "MULTICLAUDE_TEST_MODE", + "CLAUDE_CONFIG_DIR", + "CLAUDE_CODE_OAUTH_TOKEN", + "CLAUDE_PROJECT_DIR", + "PATH", + "SHELL", + "TERM", + "TMUX", + } + + for _, varName := range importantVars { + if value := os.Getenv(varName); value != "" { + // Redact sensitive values + if strings.Contains(strings.ToLower(varName), "token") || + strings.Contains(strings.ToLower(varName), "key") { + envVars[varName] = "[REDACTED]" + } else { + envVars[varName] = value + } + } + } + + return EnvironmentInfo{ + OS: runtime.GOOS, + Arch: runtime.GOARCH, + HomeDir: homeDir, + Paths: PathsInfo{ + Root: c.paths.Root, + StateFile: c.paths.StateFile, + DaemonPID: c.paths.DaemonPID, + DaemonSock: c.paths.DaemonSock, + DaemonLog: c.paths.DaemonLog, + ReposDir: c.paths.ReposDir, + WorktreesDir: c.paths.WorktreesDir, + OutputDir: c.paths.OutputDir, + MessagesDir: c.paths.MessagesDir, + }, + Variables: envVars, + } +} + +// collectTools gathers information about external tools +func (c *Collector) collectTools() ToolsInfo { + return ToolsInfo{ + Claude: c.getClaudeInfo(), + Tmux: c.getToolVersion("tmux", "-V"), + Git: c.getToolVersion("git", "--version"), + } +} + +// getClaudeInfo returns detailed information about Claude CLI +func (c *Collector) getClaudeInfo() ClaudeInfo { + path, err := exec.LookPath("claude") + if err != nil { + return ClaudeInfo{ + Installed: false, + } + } + + cmd := exec.Command("claude", "--version") + output, err := cmd.Output() + if err != nil { + return ClaudeInfo{ + Installed: true, + Path: path, + Version: "unknown", + } + } + + version := strings.TrimSpace(string(output)) + return ClaudeInfo{ + Installed: true, + Path: path, + Version: version, + } +} + +// getToolVersion returns the version string for a tool +func (c *Collector) getToolVersion(tool string, versionFlag string) string { + cmd := exec.Command(tool, versionFlag) + output, err := cmd.Output() + if err != nil { + return "not installed" + } + return strings.TrimSpace(string(output)) +} + +// determineCapabilities determines what features are available +func (c *Collector) determineCapabilities(tools ToolsInfo) CapabilitiesInfo { + capabilities := CapabilitiesInfo{ + ClaudeInstalled: tools.Claude.Installed, + TmuxInstalled: tools.Tmux != "not installed", + GitInstalled: tools.Git != "not installed", + } + + // Task management is available in Claude Code 2.0+ + if tools.Claude.Installed && tools.Claude.Version != "unknown" { + capabilities.TaskManagement = c.detectTaskManagementSupport(tools.Claude.Version) + } + + return capabilities +} + +// detectTaskManagementSupport checks if the Claude version supports task management +func (c *Collector) detectTaskManagementSupport(version string) bool { + // Task management (TaskCreate/Update/List/Get) was introduced in Claude Code 2.0 + // Version format: "X.Y.Z (Claude Code)" or just "X.Y.Z" + + // Extract version number from string like "2.1.17 (Claude Code)" + parts := strings.Fields(version) + if len(parts) == 0 { + return false + } + + versionNum := parts[0] + versionParts := strings.Split(versionNum, ".") + if len(versionParts) < 2 { + return false + } + + major, err := strconv.Atoi(versionParts[0]) + if err != nil { + return false + } + + // Task management available in v2.0+ + return major >= 2 +} + +// collectDaemon gathers daemon status information +func (c *Collector) collectDaemon() DaemonInfo { + pidData, err := os.ReadFile(c.paths.DaemonPID) + if err != nil { + return DaemonInfo{ + Running: false, + PID: 0, + } + } + + pid, err := strconv.Atoi(strings.TrimSpace(string(pidData))) + if err != nil { + return DaemonInfo{ + Running: false, + PID: 0, + } + } + + // Check if process is running + process, err := os.FindProcess(pid) + if err != nil { + return DaemonInfo{ + Running: false, + PID: pid, + } + } + + // On Unix, FindProcess always succeeds, so we send signal 0 to check + err = process.Signal(os.Signal(nil)) + if err != nil { + return DaemonInfo{ + Running: false, + PID: pid, + } + } + + return DaemonInfo{ + Running: true, + PID: pid, + } +} + +// collectStatistics gathers agent and repository statistics +func (c *Collector) collectStatistics() StatisticsInfo { + st, err := state.Load(c.paths.StateFile) + if err != nil { + return StatisticsInfo{} + } + + stats := StatisticsInfo{} + repos := st.GetAllRepos() + stats.Repositories = len(repos) + + for _, repo := range repos { + for _, agent := range repo.Agents { + switch agent.Type { + case state.AgentTypeWorker: + stats.Workers++ + case state.AgentTypeSupervisor: + stats.Supervisors++ + case state.AgentTypeMergeQueue: + stats.MergeQueues++ + case state.AgentTypeWorkspace: + stats.Workspaces++ + case state.AgentTypeReview: + stats.ReviewAgents++ + } + } + } + + return stats +} + +// ToJSON converts the report to JSON format +func (r *Report) ToJSON(pretty bool) (string, error) { + var data []byte + var err error + + if pretty { + data, err = json.MarshalIndent(r, "", " ") + } else { + data, err = json.Marshal(r) + } + + if err != nil { + return "", err + } + + return string(data), nil +} diff --git a/internal/prompts/supervisor.md b/internal/prompts/supervisor.md index 1c4d5c2..8436786 100644 --- a/internal/prompts/supervisor.md +++ b/internal/prompts/supervisor.md @@ -64,3 +64,14 @@ Multiple agents = chaos. That's fine. - Failed attempts eliminate paths, not waste effort - Two agents on same thing? Whichever passes CI first wins - Your job: maximize throughput of forward progress, not agent efficiency + +## Task Management (Optional) + +Use TaskCreate/TaskUpdate/TaskList/TaskGet to track multi-agent work: +- Create high-level tasks for major features +- Track which worker handles what +- Update as workers complete + +**Remember:** Tasks are for YOUR tracking, not for delaying PRs. Workers should still create PRs aggressively. + +See `docs/TASK_MANAGEMENT.md` for details. diff --git a/internal/templates/agent-templates/worker.md b/internal/templates/agent-templates/worker.md index b325b50..4295bfd 100644 --- a/internal/templates/agent-templates/worker.md +++ b/internal/templates/agent-templates/worker.md @@ -67,3 +67,20 @@ When integrating functionality from another PR: - Code formatted - Changes minimal and focused - Source PR referenced in description + +## Task Management (Optional) + +Use TaskCreate/TaskUpdate for **complex multi-step work** (3+ steps): + +```bash +TaskCreate({ subject: "Fix auth bug", description: "Check middleware, tokens, tests", activeForm: "Fixing auth" }) +TaskUpdate({ taskId: "1", status: "in_progress" }) +# ... work ... +TaskUpdate({ taskId: "1", status: "completed" }) +``` + +**Skip for:** Simple fixes, single-file changes, trivial operations. + +**Important:** Tasks track work internally - still create PRs immediately when each piece is done. Don't wait for all tasks to complete. + +See `docs/TASK_MANAGEMENT.md` for details.