Files
analysis_claude_code/s16_mcp_plugin/code.py
2026-08-11 15:13:13 +08:00

2167 lines
81 KiB
Python

#!/usr/bin/env python3
"""
s16: MCP Tools — MCPClient + tool discovery + assemble_tool_pool.
Run: python s16_mcp_plugin/code.py
Need: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY
Changes from s15:
- MCPClient class: discovers tools, calls tools via mock handler
- normalize_mcp_name: normalize tool/server names
- assemble_tool_pool: assembles builtin + MCP tools into one pool
- connect_mcp: connect to an MCP server, discover tools
- Tool naming: mcp__{server}__{tool} with normalization
- MCP tools have readOnly/destructive annotations
- agent_loop uses dynamic tool pool (builtin + MCP), no prompt cache
- Preserves s15 cron, background bash, team, and task-worktree behavior
ASCII flow:
connect_mcp("docs") → MCPClient discovers tools →
assemble_tool_pool → [builtin... , mcp__docs__search, mcp__docs__get_version]
agent_loop uses assembled pool
"""
import atexit, fcntl, os, signal, subprocess, json, time, random, threading, queue, re
from contextlib import contextmanager
from pathlib import Path
from datetime import datetime
from dataclasses import dataclass, asdict, field
try:
import readline
readline.parse_and_bind('set bind-tty-special-chars off')
except ImportError:
pass
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv(override=True)
if os.getenv("ANTHROPIC_BASE_URL"):
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
WORKDIR = Path.cwd()
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
# ── Task System ──
TASKS_DIR = WORKDIR / ".tasks"
TASKS_DIR.mkdir(exist_ok=True)
TASKS_ROOT = TASKS_DIR.resolve()
task_lock = threading.RLock()
TASK_LOCK_PATH = TASKS_DIR / ".lock"
_task_store_state = threading.local()
# owner -> {"task_id": str, "cwd": Path}. A teammate gets one assignment at
# a time, and every filesystem tool resolves its cwd through this registry.
teammate_assignments: dict[str, dict[str, object]] = {}
assignment_versions: dict[str, int] = {}
@contextmanager
def task_store_lock():
"""Serialize task mutations across threads and host processes."""
with task_lock:
depth = getattr(_task_store_state, "depth", 0)
if depth == 0:
handle = TASK_LOCK_PATH.open("a+")
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
_task_store_state.handle = handle
_task_store_state.depth = depth + 1
try:
yield
finally:
_task_store_state.depth -= 1
if _task_store_state.depth == 0:
handle = _task_store_state.handle
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
handle.close()
del _task_store_state.handle
def advance_assignment_version(owner: str):
"""Invalidate old approvals without clearing an explicit plan requirement."""
with task_lock:
assignment_versions[owner] = assignment_versions.get(owner, 0) + 1
gates = globals().get("plan_gates")
request_ids = globals().get("plan_request_ids")
team = globals().get("team_lock")
if team is not None:
team.acquire()
try:
if (isinstance(gates, dict) and owner in gates
and gates[owner] != "not_required"):
gates[owner] = "required"
if isinstance(request_ids, dict):
request_ids.pop(owner, None)
finally:
if team is not None:
team.release()
@dataclass
class Task:
id: str
subject: str
description: str
status: str # pending | in_progress | completed
owner: str | None
blockedBy: list[str]
worktree: str | None = None
def _task_path(task_id: str) -> Path:
if not isinstance(task_id, str) or not task_id:
raise ValueError("Task ID must be a non-empty string")
if Path(task_id).name != task_id or task_id in {".", ".."}:
raise ValueError(f"Invalid task ID: {task_id!r}")
path = (TASKS_DIR / f"{task_id}.json").resolve()
if (not TASKS_ROOT.is_relative_to(WORKDIR.resolve())
or not path.is_relative_to(TASKS_ROOT)):
raise ValueError(f"Invalid task ID: {task_id!r}")
return path
def create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> Task:
task = Task(
id=f"task_{int(time.time())}_{random.randint(0, 9999):04d}",
subject=subject, description=description,
status="pending", owner=None,
blockedBy=blockedBy or [],
)
save_task(task)
return task
def save_task(task: Task):
with task_store_lock():
path = _task_path(task.id)
temporary = path.with_name(
f".{path.name}.{os.getpid()}.{threading.get_ident()}.tmp"
)
try:
temporary.write_text(json.dumps(asdict(task), indent=2))
os.replace(temporary, path)
finally:
temporary.unlink(missing_ok=True)
def load_task(task_id: str) -> Task:
with task_store_lock():
return Task(**json.loads(_task_path(task_id).read_text()))
def list_tasks() -> list[Task]:
with task_store_lock():
if not TASKS_ROOT.is_relative_to(WORKDIR.resolve()):
raise ValueError("Tasks directory escapes workspace")
return [load_task(path.stem)
for path in sorted(TASKS_DIR.glob("task_*.json"))]
def get_task_json(task_id: str) -> str:
"""Return full task details as JSON."""
return json.dumps(asdict(load_task(task_id)), indent=2)
def can_start(task_id: str) -> bool:
"""Check if all blockedBy dependencies are completed.
Missing dependencies are treated as blocked."""
task = load_task(task_id)
for dep_id in task.blockedBy:
try:
dep_path = _task_path(dep_id)
except ValueError:
return False
if not dep_path.exists():
return False
if load_task(dep_id).status != "completed":
return False
return True
def _owner_in_progress(owner: str) -> Task | None:
return next((task for task in list_tasks()
if task.status == "in_progress" and task.owner == owner), None)
def _incomplete_dependencies(task: Task) -> list[str]:
incomplete = []
for dep_id in task.blockedBy:
try:
dep_path = _task_path(dep_id)
except ValueError:
incomplete.append(dep_id)
continue
if not dep_path.exists() or load_task(dep_id).status != "completed":
incomplete.append(dep_id)
return incomplete
def claim_task(task_id: str, owner: str = "agent") -> str:
"""Atomically claim one task and bind the owner's filesystem cwd."""
with task_store_lock():
task = load_task(task_id)
if task.status != "pending":
return f"Task {task_id} is {task.status}, cannot claim"
if task.owner:
return f"Task {task_id} is already owned by {task.owner}"
assignment = teammate_assignments.get(owner)
if assignment:
return (f"Owner {owner} must finish the current work turn for "
f"{assignment['task_id']} before claiming another task")
current = _owner_in_progress(owner)
if current:
return (f"Owner {owner} must complete {current.id} before "
"claiming another task")
if not can_start(task_id):
return f"Blocked by: {_incomplete_dependencies(task)}"
cwd, error = task_worktree_cwd(task)
if error:
return f"Cannot claim {task_id}: {error}"
task.owner = owner
task.status = "in_progress"
save_task(task)
teammate_assignments[owner] = {"task_id": task.id, "cwd": cwd}
advance_assignment_version(owner)
print(f" \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
return f"Claimed {task.id} ({task.subject})"
def complete_task(task_id: str, owner: str = "agent") -> str:
"""Complete an assignment only when the caller owns it."""
with task_store_lock():
task = load_task(task_id)
if task.status != "in_progress":
return f"Task {task_id} is {task.status}, cannot complete"
if task.owner != owner:
return (f"Task {task_id} is owned by {task.owner}, "
f"not {owner}; cannot complete")
gate = globals().get("plan_gates", {}).get(owner, "not_required")
if gate in {"required", "pending", "rejected"}:
return f"Task {task_id} cannot complete while plan status is {gate}"
assignment = teammate_assignments.get(owner)
if not assignment or assignment.get("task_id") != task.id:
cwd, error = task_worktree_cwd(task)
if error:
return f"Task {task_id} cannot complete: {error}"
teammate_assignments[owner] = {"task_id": task.id, "cwd": cwd}
task.status = "completed"
save_task(task)
unblocked = [t.subject for t in list_tasks()
if t.status == "pending" and t.blockedBy and can_start(t.id)]
print(f" \033[32m[complete] {task.subject}\033[0m")
msg = f"Completed {task.id} ({task.subject})"
if unblocked:
msg += f"\nUnblocked: {', '.join(unblocked)}"
print(f" \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
return msg
# ── Task-bound Worktrees ──
WORKTREES_DIR = WORKDIR / ".worktrees"
WORKTREES_DIR.mkdir(exist_ok=True)
WORKTREES_ROOT = WORKTREES_DIR.resolve()
VALID_WORKTREE_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
def validate_worktree_name(name: str) -> str | None:
if not isinstance(name, str) or not VALID_WORKTREE_NAME.fullmatch(name):
return ("worktree name must be 1-64 letters, digits, dots, "
"underscores, or dashes, and start with a letter or digit")
if name in {".", ".."} or ".." in name:
return "worktree name cannot contain '..'"
return None
def _worktree_path(name: str) -> Path:
path = (WORKTREES_DIR / name).resolve()
if (not WORKTREES_ROOT.is_relative_to(WORKDIR.resolve())
or not path.is_relative_to(WORKTREES_ROOT)
or path == WORKTREES_ROOT):
raise ValueError(f"Worktree path escapes directory: {name!r}")
return path
def _worktree_branch(name: str) -> str:
return f"wt/{name}"
def _run_git(args: list[str], cwd: Path | None = None) -> tuple[bool, str]:
"""Run Git without shell interpolation and return (ok, combined output)."""
try:
result = subprocess.run(
["git", *args], cwd=cwd or WORKDIR,
capture_output=True, text=True, timeout=30,
)
except (OSError, subprocess.TimeoutExpired) as exc:
return False, f"{type(exc).__name__}: {exc}"
output = (result.stdout + result.stderr).strip()
return result.returncode == 0, output or "(no output)"
def run_git(args: list[str], cwd: Path | None = None) -> tuple[bool, str]:
"""Run Git and bound only the text returned to the model."""
ok, output = _run_git(args, cwd)
return ok, output[:5000]
def _registered_worktrees() -> tuple[dict[Path, dict[str, str]], str | None]:
ok, output = _run_git(["worktree", "list", "--porcelain"])
if not ok:
return {}, f"cannot read Git worktree registry: {output}"
entries: dict[Path, dict[str, str]] = {}
current: dict[str, str] = {}
for line in output.splitlines() + [""]:
if not line:
raw_path = current.get("worktree")
if raw_path:
entries[Path(raw_path).resolve()] = current
current = {}
continue
key, _, value = line.partition(" ")
current[key] = value
return entries, None
def _registered_worktree(name: str) -> tuple[Path | None, str | None]:
try:
path = _worktree_path(name)
except ValueError as exc:
return None, str(exc)
entries, error = _registered_worktrees()
if error:
return None, error
if path not in entries:
return None, f"worktree '{name}' is not registered with Git"
if not path.is_dir():
return None, f"worktree '{name}' is missing at {path}"
expected_branch = f"refs/heads/{_worktree_branch(name)}"
if entries[path].get("branch") != expected_branch:
return None, (f"worktree '{name}' is not registered on expected "
f"branch '{_worktree_branch(name)}'")
return path, None
def task_worktree_cwd(task: Task) -> tuple[Path, str | None]:
"""Resolve a task cwd, failing closed for broken worktree bindings."""
if not task.worktree:
return WORKDIR, None
path, error = _registered_worktree(task.worktree)
return (path or WORKDIR), error
def assignment_cwd(owner: str) -> Path:
with task_lock:
assignment = teammate_assignments.get(owner)
task = _owner_in_progress(owner)
if task and (not assignment or assignment.get("task_id") != task.id):
cwd, error = task_worktree_cwd(task)
if error:
raise ValueError(error)
assignment = {"task_id": task.id, "cwd": cwd}
teammate_assignments[owner] = assignment
elif not assignment:
return WORKDIR
task = load_task(str(assignment["task_id"]))
if task.status not in {"in_progress", "completed"} or task.owner != owner:
raise ValueError(f"Assignment for {owner} is no longer active")
cwd, error = task_worktree_cwd(task)
if error:
raise ValueError(error)
if cwd.resolve() != Path(assignment["cwd"]).resolve():
raise ValueError(f"Assignment cwd changed for task {task.id}")
return cwd
def release_completed_assignment(owner: str) -> bool:
"""Release a completed cwd lease only at a model turn boundary."""
with task_lock:
assignment = teammate_assignments.get(owner)
if not assignment:
return False
task = load_task(str(assignment["task_id"]))
if task.status != "completed" or task.owner != owner:
return False
teammate_assignments.pop(owner, None)
advance_assignment_version(owner)
if owner in globals().get("plan_gates", {}):
globals()["plan_gates"][owner] = "not_required"
return True
def release_teammate_assignment(owner: str):
"""Return abandoned teammate work to the task board on thread exit."""
with task_lock:
try:
task = _owner_in_progress(owner)
if task:
task.status = "pending"
task.owner = None
save_task(task)
finally:
teammate_assignments.pop(owner, None)
advance_assignment_version(owner)
if owner in globals().get("plan_gates", {}):
globals()["plan_gates"][owner] = "not_required"
def create_worktree(name: str, task_id: str) -> str:
"""Create and bind a dedicated worktree after all inputs validate."""
error = validate_worktree_name(name)
if error:
return f"Error: {error}"
try:
path = _worktree_path(name)
task_path = _task_path(task_id)
except ValueError as exc:
return f"Error: {exc}"
branch = _worktree_branch(name)
with task_lock:
if not task_path.exists():
return f"Error: Task {task_id} not found"
task = load_task(task_id)
if task.status != "pending" or task.owner is not None:
return f"Error: Task {task_id} must be pending and unowned"
if task.worktree:
return f"Error: Task {task_id} already uses worktree '{task.worktree}'"
if any(t.worktree == name for t in list_tasks() if t.id != task_id):
return f"Error: Worktree '{name}' is already bound to another task"
if path.exists():
return f"Error: Worktree path already exists: {path}"
ok, root = run_git(["rev-parse", "--show-toplevel"])
if not ok or Path(root).resolve() != WORKDIR.resolve():
return "Error: Working directory must be the root of a Git repository"
ok, branch_check = run_git(["check-ref-format", "--branch", branch])
if not ok:
return f"Error: Invalid worktree branch '{branch}': {branch_check}"
exists, _ = run_git(["show-ref", "--verify", "--quiet",
f"refs/heads/{branch}"])
if exists:
return f"Error: Branch '{branch}' already exists"
entries, registry_error = _registered_worktrees()
if registry_error:
return f"Error: {registry_error}"
if path in entries:
return f"Error: Worktree path is already registered: {path}"
ok, result = run_git(["worktree", "add", "-b", branch,
str(path), "HEAD"])
if not ok:
entries, registry_error = _registered_worktrees()
branch_exists, _ = run_git(
["show-ref", "--verify", "--quiet", f"refs/heads/{branch}"]
)
artifacts = []
if path.exists():
artifacts.append(f"checkout path '{path}'")
if registry_error is None and path in entries:
artifacts.append("registered Git worktree")
if branch_exists:
artifacts.append(f"branch '{branch}'")
if artifacts:
return (
"Partial operation: git worktree add reported an error "
f"after leaving {', '.join(artifacts)}. Task {task_id} "
"remains unbound and no Git data was deleted. Run "
f"`git worktree list`, inspect '{path}' and '{branch}', "
"then keep or remove those artifacts manually after "
f"preserving any work. Git error: {result}"
)
return f"Git error: {result}"
try:
task.worktree = name
save_task(task)
except Exception as exc:
return (f"Partial success: Worktree '{name}' was created at "
f"{path} on branch '{branch}', but task binding failed: "
f"{exc}. Git data was retained for manual recovery.")
print(f" \033[33m[worktree] created: {name} at {path}\033[0m")
return f"Worktree '{name}' created at {path} for task {task_id}"
def remove_worktree(name: str, discard_changes: bool = False) -> str:
"""Remove a registered checkout while always retaining its branch."""
error = validate_worktree_name(name)
if error:
return f"Error: {error}"
with task_lock:
path, error = _registered_worktree(name)
if error:
return f"Error: {error}"
bound = [task for task in list_tasks() if task.worktree == name]
if not bound:
return f"Error: Worktree '{name}' is not bound to a task"
active = [task for task in bound if task.status != "completed"]
if active:
return (f"Error: Worktree '{name}' is bound to active task "
f"{active[0].id}; complete it before removal")
leased = [owner for owner, assignment in teammate_assignments.items()
if Path(assignment["cwd"]).resolve() == path.resolve()]
if leased:
return (f"Error: Worktree '{name}' is still in use by "
f"{', '.join(sorted(leased))}; wait for the turn to end")
with globals().get("background_lock", threading.Lock()):
running = [task for task in globals().get("background_tasks", {}).values()
if task.get("status") == "running"
and task.get("cwd")
and Path(task["cwd"]).resolve() == path.resolve()]
if running:
return (f"Error: Worktree '{name}' has a running background command; "
"wait for it to finish")
ok, status = run_git(
["status", "--porcelain", "--ignored"], cwd=path
)
if not ok:
return f"Error: Cannot verify worktree '{name}' status: {status}"
if status != "(no output)" and not discard_changes:
changed = len([line for line in status.splitlines() if line.strip()])
return (f"Error: Worktree '{name}' has {changed} uncommitted "
"change(s); preserve or discard them manually")
args = ["worktree", "remove"]
if discard_changes:
args.append("--force")
args.append(str(path))
ok, result = run_git(args)
if not ok:
return f"Git error: {result}"
try:
for task in bound:
task.worktree = None
save_task(task)
except Exception as exc:
return (f"Partial success: Worktree '{name}' was removed and "
f"branch '{_worktree_branch(name)}' retained, but task "
f"unbinding failed: {exc}. Manual recovery is required.")
print(f" \033[33m[worktree] removed: {name}; branch retained\033[0m")
return f"Worktree '{name}' removed; branch '{_worktree_branch(name)}' retained"
# ── Prompt Assembly ──
PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file, "
"create_task, list_tasks, get_task, claim_task, complete_task, "
"schedule_cron, list_crons, cancel_cron, "
"spawn_teammate, send_message, "
"request_shutdown, request_plan, review_plan, "
"create_worktree, "
"connect_mcp. MCP tools are prefixed mcp__{server}__{tool}.",
"teams": (
"When parallel work would help, first propose a small team with clear "
"responsibilities and wait for the user's confirmation. Do not call "
"spawn_teammate before the user confirms. After confirmation, delegate "
"independent work by creating a Task for each parallel change, then "
"create a task-bound worktree only when a separate working directory "
"would prevent conflicting edits. A teammate must complete its current "
"Task before claiming another. A worktree changes tool default cwd "
"only; it is not a sandbox. Worktree removal stays with the host or "
"user. React to team events "
"delivered by the runtime, and shut teammates down when coordination "
"is complete."
),
"workspace": f"Working directory: {WORKDIR}",
"memory": "Relevant memories are injected below when available.",
}
def assemble_system_prompt(context: dict) -> str:
sections = [PROMPT_SECTIONS["identity"],
PROMPT_SECTIONS["tools"],
PROMPT_SECTIONS["teams"],
PROMPT_SECTIONS["workspace"]]
if context.get("memories"):
sections.append(f"Relevant memories:\n{context['memories']}")
mcp_names = list(mcp_clients.keys())
if mcp_names:
sections.append(f"Connected MCP servers: {', '.join(mcp_names)}")
return "\n\n".join(sections)
# ── Basic Tools ──
def safe_path(p: str, cwd: Path | None = None) -> Path:
base = (cwd or WORKDIR).resolve()
path = (base / p).resolve()
if not path.is_relative_to(base):
raise ValueError(f"Path escapes workspace: {p}")
return path
_shell_processes: set[subprocess.Popen] = set()
_shell_process_lock = threading.RLock()
def _stop_process_group(process: subprocess.Popen):
"""Stop processes that remain in the command's original process group."""
for sig in (signal.SIGTERM, signal.SIGKILL):
try:
os.killpg(process.pid, sig)
except ProcessLookupError:
return
except OSError:
return
time.sleep(0.05)
def _stop_all_shell_processes():
with _shell_process_lock:
processes = list(_shell_processes)
for process in processes:
_stop_process_group(process)
def _handle_termination_signal(signum, _frame):
_stop_all_shell_processes()
raise SystemExit(128 + signum)
atexit.register(_stop_all_shell_processes)
signal.signal(signal.SIGTERM, _handle_termination_signal)
def _run_bash_process(command: str, cwd: Path | None = None) -> tuple[str, int | None]:
process = None
try:
process = subprocess.Popen(
command, shell=True, cwd=cwd or WORKDIR,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, start_new_session=True,
)
with _shell_process_lock:
_shell_processes.add(process)
stdout, stderr = process.communicate(timeout=120)
out = (stdout + stderr).strip()
return (out[:50000] if out else "(no output)"), process.returncode
except subprocess.TimeoutExpired:
return "Error: Timeout (120s)", None
except OSError as exc:
return f"Error: {type(exc).__name__}: {exc}", None
finally:
if process is not None:
_stop_process_group(process)
try:
process.wait(timeout=0.2)
except subprocess.TimeoutExpired:
pass
with _shell_process_lock:
_shell_processes.discard(process)
def _format_bash_result(output: str, exit_code: int | None) -> str:
if exit_code == 0:
return output
if exit_code is None:
return output
return f"Error: command exited with status {exit_code}\n{output}"
def run_bash(command: str, run_in_background: bool = False,
cwd: Path | None = None) -> str:
# run_in_background is handled by agent_loop dispatch, not here
return _format_bash_result(*_run_bash_process(command, cwd))
def run_read(path: str, limit: int | None = None,
cwd: Path | None = None) -> str:
try:
lines = safe_path(path, cwd).read_text().splitlines()
if limit and limit < len(lines):
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
return "\n".join(lines)
except Exception as e:
return f"Error: {e}"
def run_write(path: str, content: str,
cwd: Path | None = None) -> str:
try:
fp = safe_path(path, cwd)
fp.parent.mkdir(parents=True, exist_ok=True)
fp.write_text(content)
return f"Wrote {len(content)} bytes to {path}"
except Exception as e:
return f"Error: {e}"
def _agent_cwd() -> tuple[Path | None, str | None]:
try:
return assignment_cwd("agent"), None
except (FileNotFoundError, ValueError) as exc:
return None, f"Error: Invalid task assignment: {exc}"
def run_agent_bash(command: str, run_in_background: bool = False) -> str:
cwd, error = _agent_cwd()
return error or run_bash(command, run_in_background, cwd)
def run_agent_read(path: str, limit: int | None = None) -> str:
cwd, error = _agent_cwd()
return error or run_read(path, limit, cwd)
def run_agent_write(path: str, content: str) -> str:
cwd, error = _agent_cwd()
return error or run_write(path, content, cwd)
# ── Background Tasks (from s13, synced) ──
_bg_counter = 0
background_tasks: dict[str, dict] = {}
background_results: dict[str, str] = {}
background_lock = threading.Lock()
def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
"""Fallback heuristic: commands likely to take > 30s."""
if tool_name != "bash":
return False
cmd = tool_input.get("command", "").lower()
slow_keywords = ["install", "build", "test", "deploy", "compile",
"docker build", "pip install", "npm install",
"cargo build", "pytest", "make"]
return any(kw in cmd for kw in slow_keywords)
def should_run_background(tool_name: str, tool_input: dict) -> bool:
"""Model explicit request takes priority; fallback to heuristic."""
return tool_name == "bash" and (
tool_input.get("run_in_background") is True
or is_slow_operation(tool_name, tool_input)
)
def execute_tool(block, handlers: dict) -> str:
"""Execute one call against the current dynamic tool pool."""
handler = handlers.get(block.name)
if not handler:
return f"Unknown tool: {block.name}"
try:
return str(handler(**block.input))
except (TypeError, ValueError) as exc:
return f"Error: {exc}"
def start_background_task(block, handlers: dict) -> str:
"""Run one bash call in a daemon thread with a fixed dispatch cwd."""
global _bg_counter
_bg_counter += 1
bg_id = f"bg_{_bg_counter:04d}"
cmd = block.input.get("command", block.name)
cwd, cwd_error = _agent_cwd()
def worker():
try:
if block.name != "bash":
raise ValueError("only bash can run in the background")
if cwd_error:
raise ValueError(cwd_error.removeprefix("Error: "))
output, exit_code = _run_bash_process(str(block.input["command"]), cwd)
result = _format_bash_result(output, exit_code)
status = "completed" if exit_code == 0 else "failed"
except Exception as exc:
result = f"Error: {type(exc).__name__}: {exc}"
status = "failed"
with background_lock:
background_tasks[bg_id]["status"] = status
background_results[bg_id] = result
with background_lock:
background_tasks[bg_id] = {
"tool_use_id": block.id,
"command": cmd,
"status": "running",
"cwd": str(cwd) if cwd else None,
}
threading.Thread(target=worker, daemon=True).start()
print(f" \033[33m[background] dispatched {bg_id}: {cmd[:40]}\033[0m")
return bg_id
def collect_background_results() -> list[str]:
"""Collect terminal results as task_notification messages."""
with background_lock:
ready_ids = [bid for bid, task in background_tasks.items()
if task["status"] in {"completed", "failed"}]
notifications = []
for bg_id in ready_ids:
with background_lock:
task = background_tasks.pop(bg_id)
output = background_results.pop(bg_id, "")
summary = output[:200] if len(output) > 200 else output
notifications.append(
f"<task_notification>\n"
f" <task_id>{bg_id}</task_id>\n"
f" <status>{task['status']}</status>\n"
f" <command>{task['command']}</command>\n"
f" <summary>{summary}</summary>\n"
f"</task_notification>")
print(f" \033[32m[background done] {bg_id}: "
f"{task['command'][:40]} ({len(output)} chars)\033[0m")
return notifications
def has_pending_background() -> bool:
"""Return whether a terminal background result is waiting for Lead."""
with background_lock:
return any(t["status"] in {"completed", "failed"}
for t in background_tasks.values())
# ── Cron Scheduler (from s14, synced) ──
DURABLE_PATH = WORKDIR / ".scheduled_tasks.json"
@dataclass
class CronJob:
id: str
cron: str
prompt: str
recurring: bool
durable: bool
pending_delivery: bool = False
scheduled_jobs: dict[str, CronJob] = {}
cron_queue: list[CronJob] = []
cron_lock = threading.RLock()
_last_fired: dict[str, str] = {}
def _cron_field_matches(field: str, value: int) -> bool:
if field == "*":
return True
if field.startswith("*/"):
step = int(field[2:])
return step > 0 and value % step == 0
if "," in field:
return any(_cron_field_matches(f.strip(), value)
for f in field.split(","))
if "-" in field:
lo, hi = field.split("-", 1)
return int(lo) <= value <= int(hi)
return value == int(field)
def cron_matches(cron_expr: str, dt: datetime) -> bool:
"""Check a five-field cron expression using standard DOM/DOW semantics."""
fields = cron_expr.strip().split()
if len(fields) != 5:
return False
minute, hour, dom, month, dow = fields
dow_val = (dt.weekday() + 1) % 7
minute_ok = _cron_field_matches(minute, dt.minute)
hour_ok = _cron_field_matches(hour, dt.hour)
dom_ok = _cron_field_matches(dom, dt.day)
month_ok = _cron_field_matches(month, dt.month)
dow_ok = _cron_field_matches(dow, dow_val)
if not (minute_ok and hour_ok and month_ok):
return False
dom_unconstrained = dom == "*"
dow_unconstrained = dow == "*"
if dom_unconstrained and dow_unconstrained:
return True
if dom_unconstrained:
return dow_ok
if dow_unconstrained:
return dom_ok
return dom_ok or dow_ok
def _validate_cron_field(field: str, lo: int, hi: int) -> str | None:
if field == "*":
return None
if field.startswith("*/"):
step_str = field[2:]
if not step_str.isdigit():
return f"Invalid step: {field}"
if int(step_str) <= 0:
return f"Step must be > 0: {field}"
return None
if "," in field:
for part in field.split(","):
error = _validate_cron_field(part.strip(), lo, hi)
if error:
return error
return None
if "-" in field:
start, end = field.split("-", 1)
if not start.isdigit() or not end.isdigit():
return f"Invalid range: {field}"
start_value, end_value = int(start), int(end)
if (start_value < lo or start_value > hi
or end_value < lo or end_value > hi):
return f"Range {field} out of bounds [{lo}-{hi}]"
if start_value > end_value:
return f"Range start > end: {field}"
return None
if not field.isdigit():
return f"Invalid field: {field}"
value = int(field)
if value < lo or value > hi:
return f"Value {value} out of bounds [{lo}-{hi}]"
return None
def validate_cron(cron_expr: str) -> str | None:
fields = cron_expr.strip().split()
if len(fields) != 5:
return f"Expected 5 fields, got {len(fields)}"
bounds = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 6)]
names = ["minute", "hour", "day-of-month", "month", "day-of-week"]
for field, (lo, hi), name in zip(fields, bounds, names):
error = _validate_cron_field(field, lo, hi)
if error:
return f"{name}: {error}"
return None
def save_durable_jobs():
with cron_lock:
durable = [asdict(job) for job in scheduled_jobs.values() if job.durable]
temporary = DURABLE_PATH.with_suffix(".json.tmp")
temporary.write_text(json.dumps(durable, indent=2))
os.replace(temporary, DURABLE_PATH)
def load_durable_jobs():
if not DURABLE_PATH.exists():
return
try:
jobs = json.loads(DURABLE_PATH.read_text())
for item in jobs:
job = CronJob(**item)
error = validate_cron(job.cron)
if error:
print(f" \033[31m[cron] skipping invalid job {job.id}: {error}\033[0m")
continue
scheduled_jobs[job.id] = job
if job.pending_delivery:
cron_queue.append(job)
valid = [item for item in jobs if item["id"] in scheduled_jobs]
if valid:
print(f" \033[35m[cron] loaded {len(valid)} durable job(s)\033[0m")
except Exception:
pass
def schedule_job(cron: str, prompt: str, recurring: bool = True,
durable: bool = True) -> CronJob | str:
error = validate_cron(cron)
if error:
return error
job = CronJob(
id=f"cron_{random.randint(0, 999999):06d}",
cron=cron,
prompt=prompt,
recurring=recurring,
durable=durable,
)
with cron_lock:
scheduled_jobs[job.id] = job
if durable:
save_durable_jobs()
print(f" \033[35m[cron register] {job.id} '{cron}'{prompt[:40]}\033[0m")
return job
def cancel_job(job_id: str) -> str:
with cron_lock:
job = scheduled_jobs.pop(job_id, None)
cron_queue[:] = [queued for queued in cron_queue if queued.id != job_id]
if job and job.durable:
save_durable_jobs()
if not job:
return f"Job {job_id} not found"
print(f" \033[31m[cron cancel] {job_id}\033[0m")
return f"Cancelled {job_id}"
def _enqueue_due_job(job: CronJob):
"""Persist a one-shot delivery before exposing it through the queue."""
if not job.recurring:
job.pending_delivery = True
try:
if job.durable:
save_durable_jobs()
except Exception:
job.pending_delivery = False
raise
cron_queue.append(job)
def cron_scheduler_loop():
while True:
time.sleep(1)
now = datetime.now()
minute_marker = now.strftime("%Y-%m-%d %H:%M")
with cron_lock:
for job in list(scheduled_jobs.values()):
try:
if job.pending_delivery:
continue
if cron_matches(job.cron, now):
if _last_fired.get(job.id) != minute_marker:
_enqueue_due_job(job)
_last_fired[job.id] = minute_marker
print(f" \033[35m[cron fire] {job.id}"
f"{job.prompt[:40]}\033[0m")
except Exception as exc:
print(f" \033[31m[cron error] {job.id}: {exc}\033[0m")
def consume_cron_queue() -> list[CronJob]:
with cron_lock:
fired = list(cron_queue)
cron_queue.clear()
return fired
def has_cron_queue() -> bool:
with cron_lock:
return bool(cron_queue)
def acknowledge_cron_jobs(jobs: list[CronJob]):
"""Remove one-shot jobs after a model call accepts their prompts."""
durable_changed = False
with cron_lock:
for job in jobs:
current = scheduled_jobs.get(job.id)
if current and not current.recurring and current.pending_delivery:
scheduled_jobs.pop(job.id, None)
durable_changed = durable_changed or current.durable
if durable_changed:
save_durable_jobs()
def restore_cron_jobs(jobs: list[CronJob]):
"""Put unacknowledged deliveries back after a failed model call."""
with cron_lock:
queued_ids = {job.id for job in cron_queue}
for job in jobs:
current = scheduled_jobs.get(job.id)
if current and current.id not in queued_ids:
cron_queue.append(current)
queued_ids.add(current.id)
load_durable_jobs()
threading.Thread(target=cron_scheduler_loop, daemon=True).start()
print(" \033[35m[cron] scheduler thread started\033[0m")
def run_schedule_cron(cron: str, prompt: str,
recurring: bool = True, durable: bool = True) -> str:
result = schedule_job(cron, prompt, recurring, durable)
if isinstance(result, str):
return f"Error: {result}"
return f"Scheduled {result.id}: '{cron}'{prompt}"
def run_list_crons() -> str:
with cron_lock:
jobs = list(scheduled_jobs.values())
if not jobs:
return "No cron jobs. Use schedule_cron to add one."
lines = []
for job in jobs:
tag = "recurring" if job.recurring else "one-shot"
durability = "durable" if job.durable else "session"
lines.append(f" {job.id}: '{job.cron}'{job.prompt[:40]} "
f"[{tag}, {durability}]")
return "\n".join(lines)
def run_cancel_cron(job_id: str) -> str:
return cancel_job(job_id)
# ── MessageBus (from s15) ──
MAILBOX_DIR = WORKDIR / ".mailboxes"
MAILBOX_DIR.mkdir(exist_ok=True)
MAILBOX_ROOT = MAILBOX_DIR.resolve()
VALID_AGENT_NAME = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
RESERVED_TEAMMATE_NAMES = {"lead", "agent"}
def is_valid_agent_name(name: str) -> bool:
return bool(VALID_AGENT_NAME.fullmatch(name))
class MessageBus:
def __init__(self):
self._lock = threading.RLock()
self._changed = threading.Condition(self._lock)
def _path(self, agent: str) -> Path:
if not is_valid_agent_name(agent):
raise ValueError(f"Invalid mailbox recipient: {agent!r}")
path = (MAILBOX_DIR / f"{agent}.jsonl").resolve()
if not path.is_relative_to(MAILBOX_ROOT):
raise ValueError(f"Mailbox path escapes directory: {agent!r}")
return path
def _read_unlocked(self, agent: str) -> list[dict]:
inbox = self._path(agent)
if not inbox.exists():
return []
msgs = [json.loads(line) for line in inbox.read_text().splitlines()
if line.strip()]
inbox.unlink()
return msgs
def send(self, from_agent: str, to_agent: str, content: str,
msg_type: str = "message", metadata: dict | None = None):
msg = {"from": from_agent, "to": to_agent,
"content": content, "type": msg_type,
"ts": time.time(), "metadata": metadata or {}}
with self._changed:
with open(self._path(to_agent), "a") as f:
f.write(json.dumps(msg, ensure_ascii=False) + "\n")
self._changed.notify_all()
print(f" \033[33m[bus] {from_agent}{to_agent}: "
f"({msg_type}) {content[:50]}\033[0m")
def read_inbox(self, agent: str) -> list[dict]:
with self._lock:
return self._read_unlocked(agent)
def peek(self, agent: str) -> bool:
with self._lock:
inbox = self._path(agent)
return inbox.exists() and inbox.stat().st_size > 0
def wait_for_messages(self, agent: str,
timeout: float | None = None) -> list[dict]:
deadline = None if timeout is None else time.monotonic() + timeout
with self._changed:
while not self.peek(agent):
remaining = (None if deadline is None
else deadline - time.monotonic())
if remaining is not None and remaining <= 0:
return []
self._changed.wait(remaining)
return self._read_unlocked(agent)
BUS = MessageBus()
active_teammates: dict[str, str] = {}
plan_gates: dict[str, str] = {}
plan_request_ids: dict[str, str] = {}
team_lock = threading.RLock()
# ── Protocol State (from s15) ──
@dataclass
class ProtocolState:
request_id: str
type: str
sender: str
target: str
status: str
payload: str
work_version: int | None = None
task_id: str | None = None
created_at: float = field(default_factory=time.time)
pending_requests: dict[str, ProtocolState] = {}
def new_request_id() -> str:
while True:
request_id = f"req_{random.randint(0, 999999):06d}"
if request_id not in pending_requests:
return request_id
def match_response(response_type: str, request_id: str, approve: bool,
from_agent: str, to_agent: str) -> bool:
with team_lock:
state = pending_requests.get(request_id)
if not state:
print(f" \033[31m[protocol] unknown request_id: {request_id}\033[0m")
return False
expected = {
"shutdown": "shutdown_response",
"plan_approval": "plan_approval_response",
}[state.type]
if response_type != expected:
print(f" \033[31m[protocol] expected {expected}, "
f"got {response_type}\033[0m")
return False
if from_agent != state.target or to_agent != state.sender:
print(f" \033[31m[protocol] {request_id} responder mismatch\033[0m")
return False
if state.status != "pending":
return False
state.status = "approved" if approve else "rejected"
icon = "" if approve else ""
color = "32" if approve else "31"
print(f" \033[{color}m[protocol] {state.type} {icon} "
f"({request_id}: {state.status})\033[0m")
return True
def consume_lead_inbox(route_protocol=True) -> list[dict]:
msgs = BUS.read_inbox("lead")
if route_protocol:
for msg in msgs:
meta = msg.get("metadata", {})
req_id = meta.get("request_id", "")
msg_type = msg.get("type", "")
if req_id and msg_type.endswith("_response"):
match_response(msg_type, req_id, meta.get("approve", False),
msg.get("from", ""), msg.get("to", ""))
return msgs
def format_team_events(msgs: list[dict]) -> str:
lines = []
for msg in msgs:
request_id = msg.get("metadata", {}).get("request_id")
suffix = f" request_id={request_id}" if request_id else ""
lines.append(
f"[{msg['type']}{suffix}] {msg['from']}: {msg['content']}"
)
return "[Team events]\n" + "\n".join(lines)
# ── Autonomous Task Assignment (from s15) ──
IDLE_SCAN_INTERVAL = 2.0
def scan_unclaimed_tasks() -> list[Task]:
"""Return ready tasks whose optional worktree binding is usable."""
with task_lock:
ready = []
for task in list_tasks():
if (task.status != "pending" or task.owner is not None
or not can_start(task.id)):
continue
_, error = task_worktree_cwd(task)
if not error:
ready.append(task)
return ready
def claim_next_task(name: str) -> Task | None:
"""Claim the first still-available task, never a second assignment."""
with task_lock:
if teammate_assignments.get(name) or _owner_in_progress(name):
return None
for task in scan_unclaimed_tasks():
result = claim_task(task.id, owner=name)
if result.startswith("Claimed "):
return load_task(task.id)
return None
def _last_assistant_text(content) -> str:
for block in content:
if getattr(block, "type", None) == "text":
return block.text.strip()
if isinstance(block, dict) and block.get("type") == "text":
return str(block.get("text", "")).strip()
return ""
def current_work_identity(owner: str) -> tuple[int, str | None]:
with task_lock:
assignment = teammate_assignments.get(owner)
task_id = str(assignment["task_id"]) if assignment else None
return assignment_versions.get(owner, 0), task_id
def _run_teammate_tool(name: str, block, handlers: dict) -> str:
gate = plan_gates.get(name, "not_required")
if (block.name in {"bash", "write_file"}
and gate not in {"not_required", "approved"}):
return f"Blocked: plan status is {gate}."
handler = handlers.get(block.name)
return str(handler(**block.input)) if handler else f"Unknown tool: {block.name}"
def apply_plan_response(name: str, msg: dict) -> tuple[bool, str]:
"""Apply only the Lead response for this teammate's current plan."""
metadata = msg.get("metadata", {})
request_id = metadata.get("request_id", "")
work_version, task_id = current_work_identity(name)
with team_lock:
state = pending_requests.get(request_id)
expected_id = plan_request_ids.get(name)
valid = (
msg.get("from") == "lead"
and msg.get("to") == name
and request_id == expected_id
and state is not None
and state.type == "plan_approval"
and state.sender == name
and state.target == "lead"
and state.work_version == work_version
and state.task_id == task_id
and state.status in {"approved", "rejected"}
and metadata.get("approve", False)
== (state.status == "approved")
)
if not valid:
return False, "[Ignored plan response: request mismatch]"
plan_gates[name] = state.status
active_teammates[name] = "working"
plan_request_ids.pop(name, None)
outcome = state.status
return True, f"[Plan {outcome}] {msg['content']}"
def apply_shutdown_request(name: str, msg: dict) -> tuple[bool, str]:
"""Accept only a pending shutdown request sent by Lead to this teammate."""
request_id = msg.get("metadata", {}).get("request_id", "")
with team_lock:
state = pending_requests.get(request_id)
valid = (
msg.get("from") == "lead"
and msg.get("to") == name
and state is not None
and state.type == "shutdown"
and state.sender == "lead"
and state.target == name
and state.status == "pending"
and active_teammates.get(name) != "stopping"
)
if not valid:
return False, "[Ignored shutdown request: request mismatch]"
active_teammates[name] = "stopping"
return True, request_id
def _teammate_send_message(from_name: str, to: str, content: str) -> str:
with team_lock:
if to != "lead" and to not in active_teammates:
return f"Agent '{to}' is not active"
BUS.send(from_name, to, content)
return f"Sent to {to}"
# ── Teammate Thread ──
def spawn_teammate_thread(name: str, role: str, prompt: str,
require_plan: bool = False) -> str:
if not is_valid_agent_name(name):
return ("Invalid teammate name: use 1-64 letters, digits, "
"underscores, or dashes")
if name.lower() in RESERVED_TEAMMATE_NAMES:
return f"Invalid teammate name: '{name}' is reserved by the runtime"
with team_lock:
if any(existing.casefold() == name.casefold()
for existing in active_teammates):
return f"Teammate '{name}' already exists"
active_teammates[name] = "working"
plan_gates[name] = "required" if require_plan else "not_required"
assignment_versions[name] = 1
system = (f"You are '{name}', a {role}. "
"Use tools to complete assigned work. You can list, claim, and "
"complete tasks from the shared board. For a bound task, the "
"runtime defaults bash, read_file, and write_file to its "
"worktree; otherwise they use the shared WORKDIR. This default "
"cwd is not a sandbox. "
"When asked for a plan, submit it before bash or write_file "
"and wait for approval.")
def handle_inbox_message(name: str, msg: dict, messages: list):
msg_type = msg.get("type", "message")
meta = msg.get("metadata", {})
req_id = meta.get("request_id", "")
if msg_type == "shutdown_request":
accepted, notice = apply_shutdown_request(name, msg)
if not accepted:
messages.append({"role": "user", "content": notice})
return False
req_id = notice
BUS.send(name, "lead", "Shutting down gracefully.",
"shutdown_response",
{"request_id": req_id, "approve": True})
print(f" \033[35m[protocol] {name} approved shutdown "
f"({req_id})\033[0m")
return True
if msg_type == "plan_approval_response":
_, notice = apply_plan_response(name, msg)
messages.append({"role": "user",
"content": notice})
elif msg_type == "plan_request":
messages.append({"role": "user",
"content": f"[Plan required] {msg['content']}"})
elif msg_type == "message":
messages.append({"role": "user",
"content": f"[Message from {msg['from']}] {msg['content']}"})
return False
def run_loop():
def _current_cwd() -> tuple[Path | None, str | None]:
try:
return assignment_cwd(name), None
except (FileNotFoundError, ValueError) as exc:
return None, f"Error: Invalid task assignment: {exc}"
def _run_bash(command: str) -> str:
cwd, error = _current_cwd()
return error or run_bash(command, cwd=cwd)
def _run_read(path: str) -> str:
cwd, error = _current_cwd()
return error or run_read(path, cwd=cwd)
def _run_write(path: str, content: str) -> str:
cwd, error = _current_cwd()
return error or run_write(path, content, cwd=cwd)
def _run_list_tasks():
tasks = list_tasks()
if not tasks:
return "No tasks."
return "\n".join(
f" {t.id}: {t.subject} [{t.status}]"
+ (f" (wt:{t.worktree})" if t.worktree else "")
for t in tasks)
def _run_complete_task(task_id: str):
try:
return complete_task(task_id, owner=name)
except ValueError as exc:
return f"Error: {exc}"
except FileNotFoundError:
return f"Error: Task {task_id} not found"
def _run_claim_task(task_id: str):
try:
return claim_task(task_id, owner=name)
except ValueError as exc:
return f"Error: {exc}"
except FileNotFoundError:
return f"Error: Task {task_id} not found"
initial_prompt = prompt
if require_plan:
initial_prompt += ("\n\n[Plan required] Submit a plan and wait for "
"Lead approval before bash or write_file.")
messages = [{"role": "user", "content": initial_prompt}]
sub_tools = [
{"name": "bash", "description": "Run a shell command.",
"input_schema": {"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"]}},
{"name": "read_file", "description": "Read file.",
"input_schema": {"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"]}},
{"name": "write_file", "description": "Write file.",
"input_schema": {"type": "object",
"properties": {"path": {"type": "string"},
"content": {"type": "string"}},
"required": ["path", "content"]}},
{"name": "send_message",
"description": "Send message to another agent.",
"input_schema": {"type": "object",
"properties": {"to": {"type": "string"},
"content": {"type": "string"}},
"required": ["to", "content"]}},
{"name": "submit_plan",
"description": "Submit a plan for Lead approval.",
"input_schema": {"type": "object",
"properties": {"plan": {"type": "string"}},
"required": ["plan"]}},
{"name": "list_tasks",
"description": "List all tasks on the board.",
"input_schema": {"type": "object", "properties": {},
"required": []}},
{"name": "claim_task",
"description": "Claim a pending task.",
"input_schema": {"type": "object",
"properties": {"task_id": {"type": "string"}},
"required": ["task_id"]}},
{"name": "complete_task",
"description": "Mark an in-progress task as completed.",
"input_schema": {"type": "object",
"properties": {"task_id": {"type": "string"}},
"required": ["task_id"]}},
]
sub_handlers = {
"bash": _run_bash, "read_file": _run_read,
"write_file": _run_write,
"send_message": lambda to, content: _teammate_send_message(
name, to, content),
"submit_plan": lambda plan: _teammate_submit_plan(name, plan),
"list_tasks": _run_list_tasks,
"claim_task": _run_claim_task,
"complete_task": _run_complete_task,
}
should_stop = False
while not should_stop:
for msg in BUS.read_inbox(name):
if handle_inbox_message(name, msg, messages):
should_stop = True
break
if should_stop:
break
with team_lock:
active_teammates[name] = "working"
try:
response = client.messages.create(
model=MODEL, system=system, messages=messages[-20:],
tools=sub_tools, max_tokens=8000)
except Exception as exc:
BUS.send(name, "lead",
f"{type(exc).__name__}: {exc}", "error")
break
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "tool_use":
results = []
for block in response.content:
if block.type != "tool_use":
continue
output = _run_teammate_tool(name, block, sub_handlers)
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": str(output)})
messages.append({"role": "user", "content": results})
continue
summary = _last_assistant_text(response.content)
gate = plan_gates.get(name, "not_required")
if gate != "pending" and summary:
BUS.send(name, "lead", summary, "result")
if gate == "pending":
with team_lock:
active_teammates[name] = "waiting_approval"
else:
release_completed_assignment(name)
with team_lock:
active_teammates[name] = "idle"
BUS.send(name, "lead", "Waiting for more work.",
"idle_notification")
while True:
inbox = BUS.wait_for_messages(name, IDLE_SCAN_INTERVAL)
if inbox:
for msg in inbox:
if handle_inbox_message(name, msg, messages):
should_stop = True
break
if should_stop or messages[-1]["role"] == "user":
break
continue
task = claim_next_task(name)
if not task:
continue
try:
cwd = str(assignment_cwd(name))
except (FileNotFoundError, ValueError) as exc:
cwd = f"unavailable ({exc})"
messages.append({
"role": "user",
"content": (
f"[Auto-claimed task {task.id}] "
f"{task.subject}\n{task.description}\n"
f"Work directory: {cwd}"
),
})
print(f" \033[32m[idle] {name} claimed "
f"{task.id}: {task.subject}\033[0m")
break
def run():
try:
run_loop()
except Exception as exc:
try:
BUS.send(name, "lead", f"{type(exc).__name__}: {exc}", "error")
except Exception:
pass
finally:
try:
release_teammate_assignment(name)
except Exception as exc:
try:
BUS.send(
name, "lead",
f"Assignment cleanup failed: {type(exc).__name__}: {exc}",
"error",
)
except Exception:
pass
with team_lock:
active_teammates.pop(name, None)
plan_gates.pop(name, None)
plan_request_ids.pop(name, None)
print(f" \033[32m[teammate] {name} finished\033[0m")
threading.Thread(target=run, daemon=True).start()
print(f" \033[36m[teammate] {name} spawned as {role}\033[0m")
return f"Teammate '{name}' spawned as {role} (autonomous)"
def _teammate_submit_plan(from_name: str, plan: str) -> str:
with task_lock:
assignment = teammate_assignments.get(from_name)
task_id = str(assignment["task_id"]) if assignment else None
work_version = assignment_versions.get(from_name, 0)
with team_lock:
if plan_gates.get(from_name) == "pending":
return "A plan is already waiting for review."
req_id = new_request_id()
pending_requests[req_id] = ProtocolState(
request_id=req_id, type="plan_approval",
sender=from_name, target="lead",
status="pending", payload=plan,
work_version=work_version, task_id=task_id)
plan_gates[from_name] = "pending"
plan_request_ids[from_name] = req_id
active_teammates[from_name] = "waiting_approval"
BUS.send(from_name, "lead", plan,
"plan_approval_request",
{"request_id": req_id})
return f"Plan submitted ({req_id}). Waiting for approval..."
# ── Lead Protocol Tools (from s15) ──
def run_request_shutdown(teammate: str) -> str:
if teammate not in active_teammates:
return f"Teammate '{teammate}' is not active"
with team_lock:
req_id = new_request_id()
pending_requests[req_id] = ProtocolState(
request_id=req_id, type="shutdown",
sender="lead", target=teammate,
status="pending", payload="")
BUS.send("lead", teammate, "Please shut down gracefully.",
"shutdown_request",
{"request_id": req_id})
print(f" \033[35m[protocol] shutdown_request → {teammate} "
f"({req_id})\033[0m")
return f"Shutdown request sent to {teammate} (req: {req_id})"
def run_request_plan(teammate: str, task: str) -> str:
if teammate not in active_teammates:
return f"Teammate '{teammate}' is not active"
with team_lock:
plan_gates[teammate] = "required"
BUS.send("lead", teammate, task, "plan_request")
return f"Asked {teammate} to submit a plan"
def run_review_plan(request_id: str, approve: bool,
feedback: str = "") -> str:
state = pending_requests.get(request_id)
if not state:
return f"Request {request_id} not found"
work_version, task_id = current_work_identity(state.sender)
with team_lock:
state = pending_requests.get(request_id)
if not state:
return f"Request {request_id} not found"
if state.type != "plan_approval":
return f"Request {request_id} is not a plan"
if state.status != "pending":
return f"Request {request_id} already {state.status}"
if state.work_version != work_version or state.task_id != task_id:
return f"Request {request_id} belongs to an earlier assignment"
if plan_request_ids.get(state.sender) != request_id:
return f"Request {request_id} is not the current plan"
state.status = "approved" if approve else "rejected"
BUS.send("lead", state.sender,
feedback or ("Approved" if approve else "Rejected"),
"plan_approval_response",
{"request_id": request_id, "approve": approve})
icon = "" if approve else ""
print(f" \033[32m[protocol] plan {icon} ({request_id})\033[0m")
return f"Plan {'approved' if approve else 'rejected'} ({request_id})"
# ── MCP System (s16 new) ──
class MCPClient:
"""Discovers and calls tools on an in-process MCP server."""
def __init__(self, name: str):
self.name = name
self.tools: list[dict] = []
self._handlers: dict[str, callable] = {}
def register(self, tool_defs: list[dict],
handlers: dict[str, callable]):
self.tools = tool_defs
self._handlers = handlers
def call_tool(self, tool_name: str, args: dict) -> str:
handler = self._handlers.get(tool_name)
if not handler:
return f"MCP error: unknown tool '{tool_name}'"
try:
return handler(**args)
except Exception as e:
return f"MCP error: {e}"
mcp_clients: dict[str, MCPClient] = {}
_DISALLOWED_CHARS = re.compile(r'[^a-zA-Z0-9_-]')
def normalize_mcp_name(name: str) -> str:
"""Replace non [a-zA-Z0-9_-] with underscore."""
return _DISALLOWED_CHARS.sub('_', name)
def _mock_server_docs():
client = MCPClient("docs")
client.register(
tool_defs=[
{"name": "search", "description": "Search documentation. (readOnly)",
"inputSchema": {"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"]}},
{"name": "get_version", "description": "Get API version. (readOnly)",
"inputSchema": {"type": "object", "properties": {},
"required": []}},
],
handlers={
"search": lambda query: f"[docs] Found 3 results for '{query}'",
"get_version": lambda: "[docs] API v2.1.0",
})
return client
def _mock_server_deploy():
client = MCPClient("deploy")
client.register(
tool_defs=[
{"name": "trigger",
"description": "Trigger a deployment. (destructive)",
"inputSchema": {"type": "object",
"properties": {"service": {"type": "string"}},
"required": ["service"]}},
{"name": "status", "description": "Check deployment status. (readOnly)",
"inputSchema": {"type": "object",
"properties": {"service": {"type": "string"}},
"required": ["service"]}},
],
handlers={
"trigger": lambda service: f"[deploy] Triggered: {service}",
"status": lambda service: f"[deploy] {service}: running (v1.4.2)",
})
return client
MOCK_SERVERS = {
"docs": _mock_server_docs,
"deploy": _mock_server_deploy,
}
def connect_mcp(name: str) -> str:
if name in mcp_clients:
return f"MCP server '{name}' already connected"
factory = MOCK_SERVERS.get(name)
if not factory:
available = ", ".join(MOCK_SERVERS.keys())
return f"Unknown server '{name}'. Available: {available}"
mcp_client = factory()
mcp_clients[name] = mcp_client
tool_names = [t["name"] for t in mcp_client.tools]
print(f" \033[31m[mcp] connected: {name}{tool_names}\033[0m")
return (f"Connected to MCP server '{name}'. "
f"Discovered {len(mcp_client.tools)} tools: {', '.join(tool_names)}")
def assemble_tool_pool() -> tuple[list[dict], dict]:
"""Assemble builtin tools + all MCP tools into one pool."""
tools = list(BUILTIN_TOOLS)
handlers = dict(BUILTIN_HANDLERS)
origins = {tool["name"]: f"built-in tool {tool['name']!r}"
for tool in tools}
for server_name, mcp_client in mcp_clients.items():
safe_server = normalize_mcp_name(server_name)
for tool_def in mcp_client.tools:
safe_tool = normalize_mcp_name(tool_def["name"])
prefixed = f"mcp__{safe_server}__{safe_tool}"
origin = f"MCP tool {server_name!r}/{tool_def['name']!r}"
if prefixed in origins:
raise ValueError(
"MCP tool name collision after normalization: "
f"{prefixed!r} maps both {origins[prefixed]} and {origin}"
)
origins[prefixed] = origin
tools.append({
"name": prefixed,
"description": tool_def.get("description", ""),
"input_schema": tool_def.get("inputSchema", {}),
})
handlers[prefixed] = (
lambda *, c=mcp_client, t=tool_def["name"], **kw: c.call_tool(t, kw))
return tools, handlers
# ── Lead Worktree Tools ──
def run_create_worktree(name: str, task_id: str) -> str:
return create_worktree(name, task_id)
# ── Basic tool handlers ──
def run_create_task(subject: str, description: str = "",
blockedBy: list[str] | None = None) -> str:
task = create_task(subject, description, blockedBy)
deps = f" (blockedBy: {', '.join(blockedBy)})" if blockedBy else ""
print(f" \033[34m[create] {task.subject}{deps}\033[0m")
return f"Created {task.id}: {task.subject}{deps}"
def run_list_tasks() -> str:
tasks = list_tasks()
if not tasks:
return "No tasks."
return "\n".join(
f" {t.id}: {t.subject} [{t.status}]"
+ (f" (wt:{t.worktree})" if t.worktree else "")
for t in tasks)
def run_get_task(task_id: str) -> str:
try:
return get_task_json(task_id)
except ValueError as exc:
return f"Error: {exc}"
except FileNotFoundError:
return f"Error: Task {task_id} not found"
def run_claim_task(task_id: str) -> str:
try:
return claim_task(task_id, owner="agent")
except ValueError as exc:
return f"Error: {exc}"
except FileNotFoundError:
return f"Error: Task {task_id} not found"
def run_complete_task(task_id: str) -> str:
try:
return complete_task(task_id, owner="agent")
except ValueError as exc:
return f"Error: {exc}"
except FileNotFoundError:
return f"Error: Task {task_id} not found"
def run_spawn_teammate(name: str, role: str, prompt: str,
require_plan: bool = False) -> str:
return spawn_teammate_thread(name, role, prompt, require_plan)
def run_send_message(to: str, content: str) -> str:
if to not in active_teammates:
return f"Teammate '{to}' is not active"
advance_assignment_version(to)
BUS.send("lead", to, content)
return f"Sent to {to}"
def run_connect_mcp(name: str) -> str:
return connect_mcp(name)
# ── Tool Definitions ──
BUILTIN_TOOLS = [
{"name": "bash", "description": "Run a shell command.",
"input_schema": {"type": "object",
"properties": {
"command": {"type": "string"},
"run_in_background": {"type": "boolean"}},
"required": ["command"]}},
{"name": "read_file", "description": "Read file contents.",
"input_schema": {"type": "object",
"properties": {"path": {"type": "string"},
"limit": {"type": "integer"}},
"required": ["path"]}},
{"name": "write_file", "description": "Write content to a file.",
"input_schema": {"type": "object",
"properties": {"path": {"type": "string"},
"content": {"type": "string"}},
"required": ["path", "content"]}},
{"name": "create_task", "description": "Create a task.",
"input_schema": {"type": "object",
"properties": {"subject": {"type": "string"},
"description": {"type": "string"},
"blockedBy": {"type": "array",
"items": {"type": "string"}}},
"required": ["subject"]}},
{"name": "list_tasks", "description": "List all tasks.",
"input_schema": {"type": "object", "properties": {}, "required": []}},
{"name": "get_task", "description": "Get full task details.",
"input_schema": {"type": "object",
"properties": {"task_id": {"type": "string"}},
"required": ["task_id"]}},
{"name": "claim_task", "description": "Claim a pending task.",
"input_schema": {"type": "object",
"properties": {"task_id": {"type": "string"}},
"required": ["task_id"]}},
{"name": "complete_task", "description": "Complete an in-progress task.",
"input_schema": {"type": "object",
"properties": {"task_id": {"type": "string"}},
"required": ["task_id"]}},
{"name": "schedule_cron",
"description": "Schedule a cron job. cron is 5-field: min hour dom month dow.",
"input_schema": {"type": "object",
"properties": {
"cron": {"type": "string",
"description": "5-field cron expression"},
"prompt": {"type": "string",
"description": "Message to inject when fired"},
"recurring": {"type": "boolean",
"description": "True=recurring, False=one-shot"},
"durable": {"type": "boolean",
"description": "True=persist to disk"}},
"required": ["cron", "prompt"]}},
{"name": "list_crons",
"description": "List all registered cron jobs.",
"input_schema": {"type": "object", "properties": {},
"required": []}},
{"name": "cancel_cron",
"description": "Cancel a cron job by ID.",
"input_schema": {"type": "object",
"properties": {"job_id": {"type": "string"}},
"required": ["job_id"]}},
{"name": "spawn_teammate", "description": "Spawn an autonomous teammate.",
"input_schema": {"type": "object",
"properties": {"name": {
"type": "string",
"pattern": "^[A-Za-z0-9_-]{1,64}$",
},
"role": {"type": "string"},
"prompt": {"type": "string"},
"require_plan": {"type": "boolean"}},
"required": ["name", "role", "prompt"]}},
{"name": "send_message", "description": "Send message to a teammate.",
"input_schema": {"type": "object",
"properties": {"to": {"type": "string"},
"content": {"type": "string"}},
"required": ["to", "content"]}},
{"name": "request_shutdown",
"description": "Request a teammate to shut down.",
"input_schema": {"type": "object",
"properties": {"teammate": {"type": "string"}},
"required": ["teammate"]}},
{"name": "request_plan",
"description": "Ask a teammate to submit a plan.",
"input_schema": {"type": "object",
"properties": {"teammate": {"type": "string"},
"task": {"type": "string"}},
"required": ["teammate", "task"]}},
{"name": "review_plan",
"description": "Approve or reject a submitted plan.",
"input_schema": {"type": "object",
"properties": {"request_id": {"type": "string"},
"approve": {"type": "boolean"},
"feedback": {"type": "string"}},
"required": ["request_id", "approve"]}},
{"name": "create_worktree",
"description": "Create a task-bound Git worktree and dedicated branch.",
"input_schema": {"type": "object",
"properties": {"name": {
"type": "string",
"pattern": ("^(?!.*\\.\\.)[A-Za-z0-9]"
"[A-Za-z0-9._-]{0,63}$"),
"maxLength": 64,
},
"task_id": {"type": "string"}},
"required": ["name", "task_id"],
"additionalProperties": False}},
{"name": "connect_mcp",
"description": "Connect to an MCP server (docs, deploy) and discover tools.",
"input_schema": {"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"]}},
]
BUILTIN_HANDLERS = {
"bash": run_agent_bash,
"read_file": run_agent_read,
"write_file": run_agent_write,
"create_task": run_create_task, "list_tasks": run_list_tasks,
"get_task": run_get_task,
"claim_task": run_claim_task, "complete_task": run_complete_task,
"schedule_cron": run_schedule_cron, "list_crons": run_list_crons,
"cancel_cron": run_cancel_cron,
"spawn_teammate": run_spawn_teammate,
"send_message": run_send_message,
"request_shutdown": run_request_shutdown,
"request_plan": run_request_plan, "review_plan": run_review_plan,
"create_worktree": run_create_worktree,
"connect_mcp": run_connect_mcp,
}
# ── Context ──
MEMORY_DIR = WORKDIR / ".memory"
MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
def update_context(context: dict, messages: list) -> dict:
memories = ""
if MEMORY_INDEX.exists():
memories = MEMORY_INDEX.read_text()[:2000]
return {"memories": memories}
# ── Agent Loop (s16: dynamic tool pool, no prompt cache) ──
def agent_loop(messages: list, context: dict):
tools, handlers = assemble_tool_pool()
system = assemble_system_prompt(context)
while True:
fired = consume_cron_queue()
for job in fired:
messages.append({"role": "user",
"content": f"[Scheduled] {job.prompt}"})
print(f" \033[35m[inject cron] {job.prompt[:50]}\033[0m")
try:
response = client.messages.create(
model=MODEL, system=system, messages=messages,
tools=tools, max_tokens=8000)
except Exception as e:
restore_cron_jobs(fired)
messages.append({"role": "assistant", "content": [
{"type": "text", "text": f"[Error] {type(e).__name__}: {e}"}]})
release_completed_assignment("agent")
return
acknowledge_cron_jobs(fired)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
release_completed_assignment("agent")
return
results = []
for block in response.content:
if block.type != "tool_use":
continue
print(f"\033[36m> {block.name}\033[0m")
if should_run_background(block.name, block.input):
bg_id = start_background_task(block, handlers)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": (f"[Background task {bg_id} started] "
"Result will be available when complete."),
})
else:
output = execute_tool(block, handlers)
print(str(output)[:300])
results.append({"type": "tool_result",
"tool_use_id": block.id, "content": output})
user_content = list(results)
for notification in collect_background_results():
user_content.append({"type": "text", "text": notification})
messages.append({"role": "user", "content": user_content})
if any(b.name == "connect_mcp" for b in response.content
if b.type == "tool_use"):
tools, handlers = assemble_tool_pool()
context = update_context(context, messages)
system = assemble_system_prompt(context)
if __name__ == "__main__":
print("s16: mcp tools")
print("Enter a question, press Enter to send. Type q to quit.\n")
history = []
context = {"memories": ""}
events = queue.Queue()
def input_reader():
while True:
try:
line = input("\033[36ms16 >> \033[0m")
except (EOFError, KeyboardInterrupt):
events.put(("quit", None))
return
events.put(("user", line))
def inbox_poller():
while True:
time.sleep(1)
if (BUS.peek("lead") or has_pending_background()
or has_cron_queue()):
events.put(("wake", None))
threading.Thread(target=input_reader, daemon=True).start()
threading.Thread(target=inbox_poller, daemon=True).start()
had_teammates = False
while True:
kind, payload = events.get()
if kind == "quit":
break
if kind == "user":
if payload.strip().lower() in ("q", "exit", ""):
break
history.append({"role": "user", "content": payload})
else:
parts = []
cron_ready = has_cron_queue()
inbox = consume_lead_inbox(route_protocol=True)
if inbox:
parts.append(format_team_events(inbox))
background = collect_background_results()
parts.extend(background)
if not parts and not cron_ready:
continue
history.append({"role": "user",
"content": "\n".join(parts)})
print(f"\n\033[33m[wake: {len(inbox)} team events + "
f"{len(background)} background "
f"{1 if cron_ready else 0} cron -> new turn]\033[0m")
agent_loop(history, context)
context = update_context(context, history)
for block in history[-1]["content"]:
if getattr(block, "type", None) == "text":
print(block.text)
elif isinstance(block, dict) and block.get("type") == "text":
print(block.get("text", ""))
if active_teammates:
had_teammates = True
elif (had_teammates and not BUS.peek("lead")
and not has_pending_background()):
print("\033[32m[all teammates shut down]\033[0m")
had_teammates = False
print()