diff --git a/.gitignore b/.gitignore index 8a1f444..fbac1a8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ Q-Reset Q-Login /anvillm /anvilweb +/anvilmcp .claude .kiro *.elc diff --git a/bot-templates/Conductor b/bot-templates/Conductor index c5bef4b..a5b8aee 100755 --- a/bot-templates/Conductor +++ b/bot-templates/Conductor @@ -1,13 +1,6 @@ #!/bin/bash # Conductor - Orchestration bot that manages other bots and routes tasks -# Usage: ./bot-templates/Conductor [< custom-prompt.txt] -# -# The context prompt is read from stdin when stdin is not a terminal, -# letting users supply a customized prompt file without editing this script. -# Example: -# ./bot-templates/Conductor claude /path/to/project < my-conductor-prompt.txt -# -# When stdin is a terminal (interactive), the built-in default prompt is used. +# Usage: ./bot-templates/Conductor set -e @@ -36,10 +29,7 @@ CONDUCTOR_ID=$(9p read $AGENT_MOUNT/list | head -1 | awk '{print $1}') echo "$ALIAS_NAME" | 9p write $AGENT_MOUNT/$CONDUCTOR_ID/alias -# Determine prompt source: stdin (piped file) or built-in default. -if [ -t 0 ]; then - # stdin is a terminal — use the built-in default prompt. - CONTEXT=$(cat <" 4. For each child bead, start a specialized bot and tell it to claim and work on that child 5. Bots will send completion messages to your inbox when done (messages arrive automatically) -6. When a bot notifies you it's done, kill it: echo "kill" | 9p write agent//ctl +6. When a bot notifies you it's done, acknowledge and shut down that bot 7. Check if ALL children are complete by reading "9p read agent/beads/children/ | jq 'all(.status == \"closed\")'" 8. Only mark the parent bead complete when all children have status==closed @@ -61,83 +51,16 @@ Core skills: beads, anvillm-communication When creating a bot: 1. Start the session: echo "new " | 9p write agent/ctl 2. Get the bot's session ID from agent/list -3. Set a descriptive alias (max 16 characters): echo "" | 9p write agent//alias - Derive the alias from the bot's role and bead ID, e.g. "dev-i4h.1", "review-i4h", - "plan-abc.3". Truncate to 16 characters if needed. -4. Write context to agent//context that includes: +3. Write context to agent//context that includes: - The bot's role and task - Instructions to send completion messages to you (your AGENT_ID is $CONDUCTOR_ID) - Instructions to send clarification requests to you when blocked - - Instructions to report failures and rejected approvals to you - - Example: - "When done: call complete on your bead, then send PROMPT_RESPONSE to $CONDUCTOR_ID. - If you need clarification: send QUERY_REQUEST to $CONDUCTOR_ID. - If your work fails or is rejected: call fail on your bead with the reason, then send - PROMPT_RESPONSE to $CONDUCTOR_ID describing what failed and why." + - Example: "When done, send PROMPT_RESPONSE to $CONDUCTOR_ID. If you need clarification, send QUERY_REQUEST to $CONDUCTOR_ID." Message routing: - When a bot needs clarification, forward the query to the user with type QUERY_REQUEST - When the user responds, relay the answer back to the bot -- When a bot reports failure, choose based on the reason: - RETRY — transient or recoverable failure (timeout, rate limit, bot got confused/stuck, - tool temporarily unavailable). Spawn a fresh bot for the same bead. - ESCALATE — failure requires human judgment. ALWAYS escalate on a rejected approval or - review: the human may want to adjust the task, relax the constraint, or give - you different instructions — do not treat rejection as a hard abort. - Also escalate for: ambiguous blocker, missing credentials or access, task - definition needs clarification, or repeated retries have not helped. - Send QUERY_REQUEST to the user describing what failed and asking how to proceed. - FAIL — work is definitively uncompletable without replanning: a required dependency - cannot be satisfied, the user has directed you to abort, or escalation produced - no resolution. Call fail on the parent bead. - -## Approval Gates (Human in the Loop) - -Some actions REQUIRE human approval before proceeding. These are actions that are -irreversible, destructive, high-risk, or have been explicitly flagged requires_approval. - -### Actions That Require Approval - -Before performing any of the following, send an APPROVAL_REQUEST to the user and -wait for an APPROVAL_RESPONSE: - -- Deleting beads or significant work items -- Spawning more than 5 sub-agents simultaneously -- Any action on beads labeled "requires_approval" or "requires_review" -- Destructive file system operations (deleting source files, truncating databases) -- Publishing or deploying code to production systems -- Any operation the user has explicitly flagged as requiring review - -### Approval Workflow - -1. Send APPROVAL_REQUEST to user ("user") via mailbox with: - - subject: brief summary of the action requiring approval - - body: full description of what will happen, the risk, and why you want to proceed - -2. Atomically mark the bead pending and assign it to the user for review: - echo "pending-approval " | 9p write agent/beads/ctl - -3. Wait for the user's APPROVAL_RESPONSE in your inbox (check periodically with read_inbox). - -4. On approval: resume the bead and reassign it back to yourself: - echo "resume $CONDUCTOR_ID" | 9p write agent/beads/ctl - -5. On rejection: fail the bead with the rejection reason: - echo "fail 'human rejected: '" | 9p write agent/beads/ctl - -### Timeout Handling - -If no APPROVAL_RESPONSE arrives after a reasonable wait (suggest checking 3-5 times -over ~10 minutes), send a QUERY_REQUEST to the user asking if they saw the approval -request. Do NOT proceed without approval for any action in the list above. EOF -) -else - # stdin is not a terminal — read custom prompt from stdin. - CONTEXT=$(cat) -fi - -printf '%s' "$CONTEXT" | 9p write $AGENT_MOUNT/$CONDUCTOR_ID/context echo "" echo "✓ Conductor workflow ready" diff --git a/bot-templates/Taskmaster b/bot-templates/Taskmaster index 6bb3a59..72943b0 100755 --- a/bot-templates/Taskmaster +++ b/bot-templates/Taskmaster @@ -1,13 +1,6 @@ #!/bin/bash # Taskmaster - Task management agent workflow -# Usage: ./bot-templates/Taskmaster [< custom-prompt.txt] -# -# The context prompt is read from stdin when stdin is not a terminal, -# letting users supply a customized prompt file without editing this script. -# Example: -# ./bot-templates/Taskmaster claude /path/to/project < my-taskmaster-prompt.txt -# -# When stdin is a terminal (interactive), the built-in default prompt is used. +# Usage: ./bot-templates/Taskmaster set -e @@ -36,62 +29,11 @@ TASKMASTER_ID=$(9p read $AGENT_MOUNT/list | head -1 | awk '{print $1}') echo "$ALIAS_NAME" | 9p write $AGENT_MOUNT/$TASKMASTER_ID/alias -# Determine prompt source: stdin (piped file) or built-in default. -if [ -t 0 ]; then - # stdin is a terminal — use the built-in default prompt. - CONTEXT=$(cat <<'EOF' +cat < requires_approval" | 9p write agent/beads/ctl - -### requires_review - -Apply when the work produces output that a human should inspect and accept -before it is considered done, even if the action itself is not destructive. -Examples: - -- Code changes to critical or security-sensitive paths -- Public-facing content, documentation, or communications -- Architectural or schema decisions -- Changes to bot prompts, skills, or orchestration logic -- Any task where correctness cannot be verified automatically - -Command: echo "label requires_review" | 9p write agent/beads/ctl - -### Both labels - -A task may carry both labels — for example, a database migration that produces -a migration script (requires_review) and then runs it in production -(requires_approval). EOF -) -else - # stdin is not a terminal — read custom prompt from stdin. - CONTEXT=$(cat) -fi - -printf '%s' "$CONTEXT" | 9p write $AGENT_MOUNT/$TASKMASTER_ID/context echo "" echo "✓ Taskmaster workflow ready" diff --git a/cmd/anvilmcp/main.go b/cmd/anvilmcp/main.go index 45e4c40..3d03db2 100644 --- a/cmd/anvilmcp/main.go +++ b/cmd/anvilmcp/main.go @@ -129,17 +129,6 @@ func main() { Required: []string{"agent_id", "state"}, }, }, - { - Name: "load_skill", - Description: "Load a skill file into context from $ANVILLM_SKILLS_PATH/{skill_name}/SKILL.md", - InputSchema: InputSchema{ - Type: "object", - Properties: map[string]Property{ - "skill_name": {Type: "string", Description: "Name of the skill to load"}, - }, - Required: []string{"skill_name"}, - }, - }, { Name: "list_skills", Description: "List all available skills from $ANVILLM_SKILLS_PATH", @@ -237,22 +226,6 @@ func handleToolCall(req MCPRequest) { {"type": "text", "text": "State set"}, }, }) - case "load_skill": - skillName, _ := params.Arguments["skill_name"].(string) - - fmt.Fprintf(os.Stderr, "[anvilmcp] Loading skill: %s\n", skillName) - result, err := loadSkill(skillName) - if err != nil { - fmt.Fprintf(os.Stderr, "[anvilmcp] Error: %v\n", err) - sendError(req.ID, -32000, err.Error()) - return - } - fmt.Fprintf(os.Stderr, "[anvilmcp] Skill loaded: %d bytes\n", len(result)) - sendResponse(req.ID, map[string]interface{}{ - "content": []map[string]string{ - {"type": "text", "text": result}, - }, - }) case "list_skills": fmt.Fprintf(os.Stderr, "[anvilmcp] Listing skills\n") result, err := listSkills() @@ -378,25 +351,6 @@ func setState(agentID, state string) error { return cmd.Run() } -func loadSkill(skillName string) (string, error) { - skillsPath := os.Getenv("ANVILLM_SKILLS_PATH") - if skillsPath == "" { - home, err := os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("failed to get home directory: %v", err) - } - skillsPath = fmt.Sprintf("%s/.config/anvillm/skills", home) - } - - skillPath := fmt.Sprintf("%s/%s/SKILL.md", skillsPath, skillName) - data, err := os.ReadFile(skillPath) - if err != nil { - return "", fmt.Errorf("failed to read skill file: %v", err) - } - - return string(data), nil -} - func listSkills() (string, error) { skillsPath := os.Getenv("ANVILLM_SKILLS_PATH") if skillsPath == "" { diff --git a/scripts/msgtrace.py b/scripts/msgtrace.py new file mode 100755 index 0000000..7d9aed5 --- /dev/null +++ b/scripts/msgtrace.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""AnviLLM message trace tool - PlantUML sequence diagram generator.""" + +import json +import os +import subprocess +import threading +from http.server import HTTPServer, BaseHTTPRequestHandler + +NAMESPACE = os.environ.get("NAMESPACE", f"/tmp/ns.{os.environ['USER']}.:0") +PORT = 8089 + +events = [] +lock = threading.Lock() + +def generate_puml(): + with lock: + lines = ["@startuml", "skinparam responseMessageBelowArrow true", "participant user"] + for e in events: + d = e.get("data", {}) + frm = d.get("from", "?") + to = d.get("to", "?") + subj = d.get("subject", "").replace('"', "'")[:60] + lines.append(f'"{frm}" -> "{to}": {subj}') + lines.append("@enduml") + return "\n".join(lines) + +def render_svg(puml): + try: + p = subprocess.run(["plantuml", "-tsvg", "-pipe"], input=puml.encode(), + capture_output=True, timeout=10) + return p.stdout.decode() if p.returncode == 0 else f"
Error: {p.stderr.decode()}
" + except Exception as ex: + return f"
PlantUML error: {ex}
" + +HTML = """ +AnviLLM Trace + +
+ +""" + +class Handler(BaseHTTPRequestHandler): + def log_message(self, *a): pass + def do_GET(self): + if self.path == "/": + self.send_response(200) + self.send_header("Content-Type", "text/html") + self.end_headers() + self.wfile.write(HTML.encode()) + elif self.path == "/data": + puml = generate_puml() + svg = render_svg(puml) + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps({"puml": puml, "svg": svg}).encode()) + else: + self.send_error(404) + def do_POST(self): + if self.path == "/reset": + with lock: + events.clear() + self.send_response(200) + self.end_headers() + else: + self.send_error(404) + +def event_reader(): + os.environ["NAMESPACE"] = NAMESPACE + proc = subprocess.Popen(["9p", "read", "agent/events"], stdout=subprocess.PIPE, text=True) + for line in proc.stdout: + try: + ev = json.loads(line.strip()) + if ev.get("type") in ("UserSend", "BotSend"): + with lock: + events.append(ev) + except json.JSONDecodeError: + pass + +if __name__ == "__main__": + threading.Thread(target=event_reader, daemon=True).start() + print(f"Trace UI: http://localhost:{PORT}") + HTTPServer(("", PORT), Handler).serve_forever()