feat: Guildhouse MCP server + multi-agent governed dev pipeline
MCP server connecting Claude Code to the Guildhouse ecosystem. Tools (23): Forgejo: repos, files, branches, PRs, CI status Chronicle: events, epochs, verify, emit GSAP proxy: request_ac, check_operation, get_posture, delegate Capstone: agents, tenants Tasks: list, get, create, update_status, submit_for_review, submit_review Multi-agent topology: Lead Agent — plans work, delegates, reviews proposals Worker Agents — implement in parallel, submit PRs with confidence Reviewer Agent — QA's worker output, submits review confidence Confidence Gate — auto-merge/flag/propose/reject based on combined score Confidence thresholds (configurable): >= 85: auto-merge (no human needed) >= 70: flag-merge (merged, flagged for post-review) >= 50: proposal (Tyler reviews) < 50: reject (worker revises) Reviewer can override: 'reject' always rejects, 'request_changes' caps at propose. Task definitions (TOML) with phased prompts, success criteria, and delegation scopes. Every tool call emits Chronicle MCP_TOOL_CALL (0x3020). Every gate decision emits Chronicle events (0x4010-0x4013). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
dffa821c38
18 changed files with 1076 additions and 0 deletions
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.eggs/
|
||||||
|
*.egg
|
||||||
|
.pytest_cache/
|
||||||
|
.status.json
|
||||||
34
CLAUDE.md
Normal file
34
CLAUDE.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# CLAUDE.md — Guildhouse MCP Server
|
||||||
|
|
||||||
|
## What is this?
|
||||||
|
|
||||||
|
MCP server connecting Claude Code to the Guildhouse ecosystem: Forgejo, Capstone, GSAP governance, Chronicle audit, and K8s/Flux.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- `src/guildhouse_mcp/server.py` — FastMCP server with 25+ tools
|
||||||
|
- `src/guildhouse_mcp/tools/` — Backend implementations (forgejo, chronicle, capstone, gsap_proxy, tasks)
|
||||||
|
- `src/guildhouse_mcp/agents/` — Multi-agent orchestration (confidence gate, topology)
|
||||||
|
- `tasks/` — TOML task definitions with phased prompts
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
python -m guildhouse_mcp # Run MCP server (stdio)
|
||||||
|
python -m pytest tests/ # Run tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Concepts
|
||||||
|
|
||||||
|
- **Confidence gate**: Combined worker (40%) + reviewer (60%) score decides auto-merge/flag/propose/reject
|
||||||
|
- **Chronicle recording**: Every tool call emits MCP_TOOL_CALL (0x3020) event
|
||||||
|
- **GSAP proxy**: Forwards governance tool calls to existing GSAP MCP broker
|
||||||
|
- **Task definitions**: TOML files with phases, success criteria, delegation scopes
|
||||||
|
|
||||||
|
## Thresholds (configurable via env)
|
||||||
|
|
||||||
|
- `GUILDHOUSE_AUTO_MERGE_THRESHOLD=85` — No human needed
|
||||||
|
- `GUILDHOUSE_FLAG_MERGE_THRESHOLD=70` — Merged, flagged for post-review
|
||||||
|
- `GUILDHOUSE_PROPOSAL_THRESHOLD=50` — Tyler reviews
|
||||||
|
- Below 50 — Rejected, worker revises
|
||||||
25
pyproject.toml
Normal file
25
pyproject.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "guildhouse-mcp"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Guildhouse governed development MCP server"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"mcp>=1.27.0",
|
||||||
|
"httpx>=0.27.0",
|
||||||
|
"pydantic>=2.0",
|
||||||
|
"pydantic-settings>=2.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
k8s = ["kubernetes>=28.0"]
|
||||||
|
dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
guildhouse-mcp = "guildhouse_mcp.__main__:main"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/guildhouse_mcp"]
|
||||||
0
src/guildhouse_mcp/__init__.py
Normal file
0
src/guildhouse_mcp/__init__.py
Normal file
12
src/guildhouse_mcp/__main__.py
Normal file
12
src/guildhouse_mcp/__main__.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
"""Entry point for guildhouse-mcp server."""
|
||||||
|
|
||||||
|
from .server import mcp
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run the MCP server via stdio (for Claude Code)."""
|
||||||
|
mcp.run(transport="stdio")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
0
src/guildhouse_mcp/agents/__init__.py
Normal file
0
src/guildhouse_mcp/agents/__init__.py
Normal file
95
src/guildhouse_mcp/agents/confidence.py
Normal file
95
src/guildhouse_mcp/agents/confidence.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
"""
|
||||||
|
Confidence-based approval gate.
|
||||||
|
|
||||||
|
Combines worker and reviewer confidence scores to decide:
|
||||||
|
auto-merge, flag-merge, propose-to-human, or reject.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from ..config import settings
|
||||||
|
|
||||||
|
|
||||||
|
class GateDecision(str, Enum):
|
||||||
|
AUTO_MERGE = "auto_merge"
|
||||||
|
FLAG_MERGE = "flag_merge"
|
||||||
|
PROPOSE = "propose_to_human"
|
||||||
|
REJECT = "reject"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GateResult:
|
||||||
|
decision: GateDecision
|
||||||
|
combined_confidence: float
|
||||||
|
worker_confidence: int
|
||||||
|
reviewer_confidence: int
|
||||||
|
reason: str
|
||||||
|
auto_approved: bool
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_gate(
|
||||||
|
worker_confidence: int,
|
||||||
|
reviewer_confidence: int,
|
||||||
|
reviewer_recommendation: str = "approve",
|
||||||
|
) -> GateResult:
|
||||||
|
"""
|
||||||
|
Evaluate the confidence gate.
|
||||||
|
|
||||||
|
Combined = (worker * 0.4) + (reviewer * 0.6)
|
||||||
|
Reviewer weighted higher because QA is the verification step.
|
||||||
|
|
||||||
|
If reviewer recommends "reject" -> always reject regardless of score.
|
||||||
|
If reviewer recommends "request_changes" -> cap at PROPOSE level.
|
||||||
|
"""
|
||||||
|
combined = (worker_confidence * 0.4) + (reviewer_confidence * 0.6)
|
||||||
|
|
||||||
|
if reviewer_recommendation == "reject":
|
||||||
|
return GateResult(
|
||||||
|
decision=GateDecision.REJECT,
|
||||||
|
combined_confidence=combined,
|
||||||
|
worker_confidence=worker_confidence,
|
||||||
|
reviewer_confidence=reviewer_confidence,
|
||||||
|
reason=f"Reviewer rejected (combined: {combined:.0f})",
|
||||||
|
auto_approved=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if reviewer_recommendation == "request_changes":
|
||||||
|
decision = (
|
||||||
|
GateDecision.PROPOSE
|
||||||
|
if combined >= settings.proposal_threshold
|
||||||
|
else GateDecision.REJECT
|
||||||
|
)
|
||||||
|
return GateResult(
|
||||||
|
decision=decision,
|
||||||
|
combined_confidence=combined,
|
||||||
|
worker_confidence=worker_confidence,
|
||||||
|
reviewer_confidence=reviewer_confidence,
|
||||||
|
reason=f"Reviewer requested changes (combined: {combined:.0f})",
|
||||||
|
auto_approved=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if combined >= settings.auto_merge_threshold:
|
||||||
|
decision = GateDecision.AUTO_MERGE
|
||||||
|
reason = f"High confidence ({combined:.0f} >= {settings.auto_merge_threshold})"
|
||||||
|
auto = True
|
||||||
|
elif combined >= settings.flag_merge_threshold:
|
||||||
|
decision = GateDecision.FLAG_MERGE
|
||||||
|
reason = f"Moderate confidence ({combined:.0f} >= {settings.flag_merge_threshold}), flagged for post-review"
|
||||||
|
auto = True
|
||||||
|
elif combined >= settings.proposal_threshold:
|
||||||
|
decision = GateDecision.PROPOSE
|
||||||
|
reason = f"Below auto-merge ({combined:.0f} < {settings.flag_merge_threshold}), creating proposal"
|
||||||
|
auto = False
|
||||||
|
else:
|
||||||
|
decision = GateDecision.REJECT
|
||||||
|
reason = f"Low confidence ({combined:.0f} < {settings.proposal_threshold})"
|
||||||
|
auto = False
|
||||||
|
|
||||||
|
return GateResult(
|
||||||
|
decision=decision,
|
||||||
|
combined_confidence=combined,
|
||||||
|
worker_confidence=worker_confidence,
|
||||||
|
reviewer_confidence=reviewer_confidence,
|
||||||
|
reason=reason,
|
||||||
|
auto_approved=auto,
|
||||||
|
)
|
||||||
37
src/guildhouse_mcp/config.py
Normal file
37
src/guildhouse_mcp/config.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
# Forgejo
|
||||||
|
forgejo_url: str = "https://git.guildhouse.dev"
|
||||||
|
forgejo_token: str = ""
|
||||||
|
|
||||||
|
# Capstone (engine API)
|
||||||
|
capstone_url: str = "http://localhost:8000"
|
||||||
|
capstone_token: str = ""
|
||||||
|
|
||||||
|
# GSAP Broker
|
||||||
|
gsap_url: str = "http://localhost:8090"
|
||||||
|
|
||||||
|
# Guildhouse Platform API (Rust)
|
||||||
|
platform_url: str = "http://localhost:8080"
|
||||||
|
|
||||||
|
# Chronicle identity
|
||||||
|
chronicle_source_id: str = "did:web:guildhouse.dev/agent/claude-code"
|
||||||
|
|
||||||
|
# Agent identity
|
||||||
|
agent_did: str = "did:web:guildhouse.dev/agent/lead"
|
||||||
|
|
||||||
|
# Confidence thresholds
|
||||||
|
auto_merge_threshold: int = 85
|
||||||
|
flag_merge_threshold: int = 70
|
||||||
|
proposal_threshold: int = 50
|
||||||
|
|
||||||
|
# K8s (optional)
|
||||||
|
kubeconfig: str = ""
|
||||||
|
k8s_context: str = ""
|
||||||
|
|
||||||
|
model_config = {"env_prefix": "GUILDHOUSE_"}
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
26
src/guildhouse_mcp/identity.py
Normal file
26
src/guildhouse_mcp/identity.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
"""Agent identity and delegation management."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from .config import Settings
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AgentIdentity:
|
||||||
|
"""Tracks this agent's identity and active delegation."""
|
||||||
|
|
||||||
|
settings: Settings
|
||||||
|
delegation_id: str = ""
|
||||||
|
ac_id: str = ""
|
||||||
|
capabilities: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def did(self) -> str:
|
||||||
|
return self.settings.agent_did
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source_id(self) -> str:
|
||||||
|
return self.settings.chronicle_source_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_delegation(self) -> bool:
|
||||||
|
return bool(self.delegation_id)
|
||||||
264
src/guildhouse_mcp/server.py
Normal file
264
src/guildhouse_mcp/server.py
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
"""
|
||||||
|
Guildhouse MCP Server — governed development tools.
|
||||||
|
|
||||||
|
Connects Claude Code to Forgejo, Capstone, GSAP, Chronicle,
|
||||||
|
and K8s via MCP protocol. All actions are governed by GSAP ACs
|
||||||
|
and recorded in Chronicle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
from .config import settings
|
||||||
|
from .tools import forgejo, chronicle, capstone, gsap_proxy, tasks
|
||||||
|
|
||||||
|
mcp = FastMCP("guildhouse-mcp")
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# FORGEJO TOOLS
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def forgejo_list_repos(org: str = "", limit: int = 20) -> str:
|
||||||
|
"""List repositories accessible to this agent. Filter by org if specified."""
|
||||||
|
result = await forgejo.list_repos(org=org, limit=limit)
|
||||||
|
await chronicle.emit_tool_call("forgejo_list_repos", {"org": org})
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def forgejo_read_file(repo: str, path: str, ref: str = "main") -> str:
|
||||||
|
"""Read a file from a Forgejo repository."""
|
||||||
|
result = await forgejo.read_file(repo=repo, path=path, ref=ref)
|
||||||
|
await chronicle.emit_tool_call("forgejo_read_file", {"repo": repo, "path": path})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def forgejo_create_branch(repo: str, branch: str, from_ref: str = "main") -> str:
|
||||||
|
"""Create a new branch in a repository for agent work."""
|
||||||
|
result = await forgejo.create_branch(repo=repo, branch=branch, from_ref=from_ref)
|
||||||
|
await chronicle.emit_tool_call("forgejo_create_branch", {"repo": repo, "branch": branch})
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def forgejo_create_pr(
|
||||||
|
repo: str, title: str, body: str,
|
||||||
|
head: str, base: str = "main", confidence: int = 0,
|
||||||
|
) -> str:
|
||||||
|
"""Create a pull request. Include confidence score (0-100) in metadata."""
|
||||||
|
result = await forgejo.create_pr(
|
||||||
|
repo=repo, title=title, body=body,
|
||||||
|
head=head, base=base,
|
||||||
|
labels=["agent-created"],
|
||||||
|
metadata={"confidence": confidence, "agent": settings.agent_did},
|
||||||
|
)
|
||||||
|
await chronicle.emit_tool_call("forgejo_create_pr", {
|
||||||
|
"repo": repo, "title": title, "confidence": confidence,
|
||||||
|
})
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def forgejo_get_pr(repo: str, pr_number: int) -> str:
|
||||||
|
"""Get pull request details including CI status and review comments."""
|
||||||
|
result = await forgejo.get_pr(repo=repo, pr_number=pr_number)
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def forgejo_ci_status(repo: str, ref: str = "main") -> str:
|
||||||
|
"""Check CI pipeline status for a branch or commit."""
|
||||||
|
result = await forgejo.ci_status(repo=repo, ref=ref)
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# CHRONICLE TOOLS
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def chronicle_query_events(
|
||||||
|
source: str = "", event_type: str = "",
|
||||||
|
principal: str = "", limit: int = 20,
|
||||||
|
) -> str:
|
||||||
|
"""Query Chronicle events. Filter by source, type, or principal."""
|
||||||
|
result = await chronicle.query_events(
|
||||||
|
source=source, event_type=event_type,
|
||||||
|
principal=principal, limit=limit,
|
||||||
|
)
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def chronicle_query_epochs(source: str = "", limit: int = 10) -> str:
|
||||||
|
"""Query sealed Chronicle epochs. Shows merkle roots and chain integrity."""
|
||||||
|
result = await chronicle.query_epochs(source=source, limit=limit)
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def chronicle_verify_epoch(epoch_id: str) -> str:
|
||||||
|
"""Verify a Chronicle epoch's integrity — recomputes and checks all merkle roots."""
|
||||||
|
result = await chronicle.verify_epoch(epoch_id=epoch_id)
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def chronicle_emit(event_type: str, event_code: str, payload: str = "{}") -> str:
|
||||||
|
"""Emit a Chronicle event for this agent's action. Payload is JSON string."""
|
||||||
|
result = await chronicle.emit_event(
|
||||||
|
event_type=event_type,
|
||||||
|
event_code=event_code,
|
||||||
|
principal=settings.agent_did,
|
||||||
|
payload=json.loads(payload),
|
||||||
|
)
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# GSAP GOVERNANCE TOOLS
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def gsap_request_ac(scope: str, duration_minutes: int = 60) -> str:
|
||||||
|
"""Request an Authorization Context for a governed operation."""
|
||||||
|
result = await gsap_proxy.request_ac(scope=scope, duration=duration_minutes)
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def gsap_check_operation(operation: str, target: str = "") -> str:
|
||||||
|
"""Dry-run: check if an operation is allowed at the current DEFCON posture."""
|
||||||
|
result = await gsap_proxy.check_operation(operation=operation, target=target)
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def gsap_get_posture() -> str:
|
||||||
|
"""Get current DEFCON posture level and any restrictions."""
|
||||||
|
result = await gsap_proxy.get_posture()
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def gsap_request_delegation(
|
||||||
|
agent_name: str, scope: str,
|
||||||
|
capabilities: str = "code:read,code:write",
|
||||||
|
max_ttl_minutes: int = 120,
|
||||||
|
) -> str:
|
||||||
|
"""Create a delegation for a sub-agent with scoped capabilities."""
|
||||||
|
result = await gsap_proxy.request_delegation(
|
||||||
|
agent_name=agent_name,
|
||||||
|
scope=scope,
|
||||||
|
capabilities=capabilities.split(","),
|
||||||
|
max_ttl_minutes=max_ttl_minutes,
|
||||||
|
)
|
||||||
|
await chronicle.emit_tool_call("gsap_request_delegation", {
|
||||||
|
"agent_name": agent_name, "scope": scope,
|
||||||
|
})
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# TASK MANAGEMENT TOOLS
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def task_list() -> str:
|
||||||
|
"""List available task definitions from the tasks/ directory."""
|
||||||
|
result = await tasks.list_tasks()
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def task_get(task_id: str) -> str:
|
||||||
|
"""Get a task definition including all phases and success criteria."""
|
||||||
|
result = await tasks.get_task(task_id)
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def task_create(
|
||||||
|
task_id: str, repo: str, description: str, phases: str,
|
||||||
|
) -> str:
|
||||||
|
"""Create a new task definition. phases is a JSON array of phase objects."""
|
||||||
|
result = await tasks.create_task(
|
||||||
|
task_id=task_id, repo=repo,
|
||||||
|
description=description,
|
||||||
|
phases=json.loads(phases),
|
||||||
|
)
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def task_update_status(
|
||||||
|
task_id: str, phase: int, status: str,
|
||||||
|
confidence: int = 0, notes: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""Update task phase status. Triggers confidence gate if phase is complete."""
|
||||||
|
result = await tasks.update_status(
|
||||||
|
task_id=task_id, phase=phase,
|
||||||
|
status=status, confidence=confidence, notes=notes,
|
||||||
|
)
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def submit_for_review(
|
||||||
|
task_id: str, phase: int, pr_number: int,
|
||||||
|
worker_confidence: int, notes: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""Submit completed work for reviewer agent evaluation."""
|
||||||
|
result = await tasks.submit_for_review(
|
||||||
|
task_id=task_id, phase=phase,
|
||||||
|
pr_number=pr_number,
|
||||||
|
worker_confidence=worker_confidence,
|
||||||
|
notes=notes,
|
||||||
|
)
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def submit_review(
|
||||||
|
task_id: str, phase: int, pr_number: int,
|
||||||
|
reviewer_confidence: int,
|
||||||
|
findings: str = "[]",
|
||||||
|
recommendation: str = "approve",
|
||||||
|
) -> str:
|
||||||
|
"""Submit a review evaluation. Triggers confidence gate for merge decision."""
|
||||||
|
result = await tasks.submit_review(
|
||||||
|
task_id=task_id, phase=phase,
|
||||||
|
pr_number=pr_number,
|
||||||
|
reviewer_confidence=reviewer_confidence,
|
||||||
|
findings=json.loads(findings),
|
||||||
|
recommendation=recommendation,
|
||||||
|
)
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# CAPSTONE TOOLS
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def capstone_list_agents() -> str:
|
||||||
|
"""List agent profiles from Capstone."""
|
||||||
|
result = await capstone.list_agents()
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def capstone_run_agent(agent_slug: str, input_text: str) -> str:
|
||||||
|
"""Trigger an agent run in Capstone."""
|
||||||
|
result = await capstone.run_agent(slug=agent_slug, input_text=input_text)
|
||||||
|
await chronicle.emit_tool_call("capstone_run_agent", {"agent": agent_slug})
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def capstone_list_tenants() -> str:
|
||||||
|
"""List tenants from the platform API."""
|
||||||
|
result = await capstone.list_tenants()
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
0
src/guildhouse_mcp/tools/__init__.py
Normal file
0
src/guildhouse_mcp/tools/__init__.py
Normal file
44
src/guildhouse_mcp/tools/capstone.py
Normal file
44
src/guildhouse_mcp/tools/capstone.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
"""Capstone API tools — agents, tenants."""
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from ..config import settings
|
||||||
|
|
||||||
|
_client: httpx.AsyncClient | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client() -> httpx.AsyncClient:
|
||||||
|
global _client
|
||||||
|
if _client is None:
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
if settings.capstone_token:
|
||||||
|
headers["Authorization"] = f"Bearer {settings.capstone_token}"
|
||||||
|
_client = httpx.AsyncClient(
|
||||||
|
base_url=settings.capstone_url,
|
||||||
|
headers=headers,
|
||||||
|
timeout=30.0,
|
||||||
|
)
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
async def list_agents() -> dict:
|
||||||
|
client = _get_client()
|
||||||
|
resp = await client.get("/api/v1/agents/profiles/")
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def run_agent(slug: str, input_text: str) -> dict:
|
||||||
|
client = _get_client()
|
||||||
|
resp = await client.post(
|
||||||
|
f"/api/v1/agents/profiles/{slug}/chat/",
|
||||||
|
json={"message": input_text},
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def list_tenants() -> dict:
|
||||||
|
client = _get_client()
|
||||||
|
resp = await client.get("/api/v1/tenancy/tenants/")
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
90
src/guildhouse_mcp/tools/chronicle.py
Normal file
90
src/guildhouse_mcp/tools/chronicle.py
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
"""Chronicle query + emit tools via Capstone API."""
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from ..config import settings
|
||||||
|
|
||||||
|
_client: httpx.AsyncClient | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client() -> httpx.AsyncClient:
|
||||||
|
global _client
|
||||||
|
if _client is None:
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
if settings.capstone_token:
|
||||||
|
headers["Authorization"] = f"Bearer {settings.capstone_token}"
|
||||||
|
_client = httpx.AsyncClient(
|
||||||
|
base_url=settings.capstone_url,
|
||||||
|
headers=headers,
|
||||||
|
timeout=15.0,
|
||||||
|
)
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
async def query_events(
|
||||||
|
source: str = "", event_type: str = "",
|
||||||
|
principal: str = "", limit: int = 20,
|
||||||
|
) -> dict:
|
||||||
|
client = _get_client()
|
||||||
|
params: dict = {}
|
||||||
|
if source:
|
||||||
|
params["source"] = source
|
||||||
|
if event_type:
|
||||||
|
params["type"] = event_type
|
||||||
|
if principal:
|
||||||
|
params["principal"] = principal
|
||||||
|
|
||||||
|
resp = await client.get("/api/v1/chronicle/events/", params=params)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def query_epochs(source: str = "", limit: int = 10) -> dict:
|
||||||
|
client = _get_client()
|
||||||
|
params: dict = {}
|
||||||
|
if source:
|
||||||
|
params["source"] = source
|
||||||
|
|
||||||
|
resp = await client.get("/api/v1/chronicle/epochs/", params=params)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def verify_epoch(epoch_id: str) -> dict:
|
||||||
|
client = _get_client()
|
||||||
|
resp = await client.get(f"/api/v1/chronicle/epochs/{epoch_id}/verify/")
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def emit_event(
|
||||||
|
event_type: str, event_code: str,
|
||||||
|
principal: str, payload: dict | None = None,
|
||||||
|
) -> dict:
|
||||||
|
client = _get_client()
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/v1/chronicle/events/",
|
||||||
|
json={
|
||||||
|
"event_type": event_type,
|
||||||
|
"event_code": event_code,
|
||||||
|
"principal": principal,
|
||||||
|
"source_id": settings.chronicle_source_id,
|
||||||
|
"source_type": "agent",
|
||||||
|
"payload": payload or {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if resp.status_code in (200, 201):
|
||||||
|
return resp.json()
|
||||||
|
return {"status": "failed", "code": resp.status_code}
|
||||||
|
|
||||||
|
|
||||||
|
async def emit_tool_call(tool_name: str, params: dict) -> None:
|
||||||
|
"""Convenience: emit MCP_TOOL_CALL event for this agent's action."""
|
||||||
|
try:
|
||||||
|
await emit_event(
|
||||||
|
event_type="MCP_TOOL_CALL",
|
||||||
|
event_code="0x3020",
|
||||||
|
principal=settings.agent_did,
|
||||||
|
payload={"tool": tool_name, "params": params},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass # Chronicle emission is best-effort
|
||||||
143
src/guildhouse_mcp/tools/forgejo.py
Normal file
143
src/guildhouse_mcp/tools/forgejo.py
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
"""Forgejo REST API tools."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import httpx
|
||||||
|
from ..config import settings
|
||||||
|
|
||||||
|
_client: httpx.AsyncClient | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client() -> httpx.AsyncClient:
|
||||||
|
global _client
|
||||||
|
if _client is None:
|
||||||
|
_client = httpx.AsyncClient(
|
||||||
|
base_url=settings.forgejo_url,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"token {settings.forgejo_token}",
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
timeout=30.0,
|
||||||
|
)
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
async def list_repos(org: str = "", limit: int = 20) -> dict:
|
||||||
|
client = _get_client()
|
||||||
|
if org:
|
||||||
|
resp = await client.get(f"/api/v1/orgs/{org}/repos", params={"limit": limit})
|
||||||
|
else:
|
||||||
|
resp = await client.get("/api/v1/repos/search", params={"limit": limit})
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
repos = data if isinstance(data, list) else data.get("data", [])
|
||||||
|
return {
|
||||||
|
"repos": [
|
||||||
|
{
|
||||||
|
"full_name": r["full_name"],
|
||||||
|
"description": r.get("description", ""),
|
||||||
|
"default_branch": r.get("default_branch", "main"),
|
||||||
|
"updated_at": r.get("updated_at", ""),
|
||||||
|
"stars": r.get("stars_count", 0),
|
||||||
|
"open_issues": r.get("open_issues_count", 0),
|
||||||
|
}
|
||||||
|
for r in repos
|
||||||
|
],
|
||||||
|
"count": len(repos),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def read_file(repo: str, path: str, ref: str = "main") -> str:
|
||||||
|
client = _get_client()
|
||||||
|
resp = await client.get(f"/api/v1/repos/{repo}/raw/{path}", params={"ref": ref})
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.text
|
||||||
|
|
||||||
|
|
||||||
|
async def create_branch(repo: str, branch: str, from_ref: str = "main") -> dict:
|
||||||
|
client = _get_client()
|
||||||
|
resp = await client.post(
|
||||||
|
f"/api/v1/repos/{repo}/branches",
|
||||||
|
json={"new_branch_name": branch, "old_branch_name": from_ref},
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return {"branch": branch, "repo": repo, "status": "created"}
|
||||||
|
|
||||||
|
|
||||||
|
async def create_pr(
|
||||||
|
repo: str, title: str, body: str,
|
||||||
|
head: str, base: str = "main",
|
||||||
|
labels: list[str] | None = None,
|
||||||
|
metadata: dict | None = None,
|
||||||
|
) -> dict:
|
||||||
|
client = _get_client()
|
||||||
|
|
||||||
|
full_body = body
|
||||||
|
if metadata:
|
||||||
|
full_body += f"\n\n<!-- agent-metadata: {json.dumps(metadata)} -->"
|
||||||
|
|
||||||
|
resp = await client.post(
|
||||||
|
f"/api/v1/repos/{repo}/pulls",
|
||||||
|
json={
|
||||||
|
"title": title,
|
||||||
|
"body": full_body,
|
||||||
|
"head": head,
|
||||||
|
"base": base,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
pr = resp.json()
|
||||||
|
return {
|
||||||
|
"number": pr["number"],
|
||||||
|
"url": pr.get("html_url", ""),
|
||||||
|
"state": pr["state"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_pr(repo: str, pr_number: int) -> dict:
|
||||||
|
client = _get_client()
|
||||||
|
resp = await client.get(f"/api/v1/repos/{repo}/pulls/{pr_number}")
|
||||||
|
resp.raise_for_status()
|
||||||
|
pr = resp.json()
|
||||||
|
|
||||||
|
ci = await ci_status(repo, ref=pr.get("head", {}).get("sha", ""))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"number": pr["number"],
|
||||||
|
"title": pr["title"],
|
||||||
|
"state": pr["state"],
|
||||||
|
"mergeable": pr.get("mergeable"),
|
||||||
|
"body": pr.get("body", ""),
|
||||||
|
"ci": ci,
|
||||||
|
"created_at": pr.get("created_at"),
|
||||||
|
"updated_at": pr.get("updated_at"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def ci_status(repo: str, ref: str = "main") -> dict:
|
||||||
|
client = _get_client()
|
||||||
|
try:
|
||||||
|
resp = await client.get(f"/api/v1/repos/{repo}/commits/{ref}/status")
|
||||||
|
if resp.status_code == 200:
|
||||||
|
data = resp.json()
|
||||||
|
return {
|
||||||
|
"state": data.get("state", "unknown"),
|
||||||
|
"total": data.get("total_count", 0),
|
||||||
|
"statuses": [
|
||||||
|
{"context": s.get("context"), "state": s.get("status")}
|
||||||
|
for s in data.get("statuses", [])
|
||||||
|
],
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"state": "unknown", "total": 0, "statuses": []}
|
||||||
|
|
||||||
|
|
||||||
|
async def merge_pr(repo: str, pr_number: int, method: str = "merge") -> dict:
|
||||||
|
"""Merge a PR. Only called by the confidence gate on auto-merge."""
|
||||||
|
client = _get_client()
|
||||||
|
resp = await client.post(
|
||||||
|
f"/api/v1/repos/{repo}/pulls/{pr_number}/merge",
|
||||||
|
json={"Do": method, "merge_message_field": ""},
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return {"merged": True, "pr_number": pr_number}
|
||||||
79
src/guildhouse_mcp/tools/gsap_proxy.py
Normal file
79
src/guildhouse_mcp/tools/gsap_proxy.py
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
"""Proxy to existing GSAP MCP broker tools."""
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from ..config import settings
|
||||||
|
|
||||||
|
_client: httpx.AsyncClient | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client() -> httpx.AsyncClient:
|
||||||
|
global _client
|
||||||
|
if _client is None:
|
||||||
|
_client = httpx.AsyncClient(
|
||||||
|
base_url=settings.gsap_url,
|
||||||
|
headers={"Accept": "application/json"},
|
||||||
|
timeout=15.0,
|
||||||
|
)
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
async def _call_gsap_tool(tool_name: str, arguments: dict) -> dict:
|
||||||
|
"""Call a GSAP MCP tool via JSON-RPC 2.0."""
|
||||||
|
client = _get_client()
|
||||||
|
resp = await client.post(
|
||||||
|
"/governance/mcp",
|
||||||
|
json={
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": tool_name,
|
||||||
|
"arguments": arguments,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
result = resp.json()
|
||||||
|
if "error" in result:
|
||||||
|
return {"error": result["error"]}
|
||||||
|
content = result.get("result", {}).get("content", [])
|
||||||
|
if content and content[0].get("type") == "text":
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
return json.loads(content[0]["text"])
|
||||||
|
except (json.JSONDecodeError, KeyError):
|
||||||
|
return {"text": content[0].get("text", "")}
|
||||||
|
return result.get("result", {})
|
||||||
|
|
||||||
|
|
||||||
|
async def request_ac(scope: str, duration: int = 60) -> dict:
|
||||||
|
return await _call_gsap_tool("request_ac", {
|
||||||
|
"principal": settings.agent_did,
|
||||||
|
"accord_template": "dev-operations",
|
||||||
|
"playbook": scope,
|
||||||
|
"session_mode": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async def check_operation(operation: str, target: str = "") -> dict:
|
||||||
|
return await _call_gsap_tool("check_operation", {
|
||||||
|
"operation_type": operation,
|
||||||
|
"target_layer": target or "applications",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async def get_posture() -> dict:
|
||||||
|
return await _call_gsap_tool("get_posture", {})
|
||||||
|
|
||||||
|
|
||||||
|
async def request_delegation(
|
||||||
|
agent_name: str, scope: str,
|
||||||
|
capabilities: list[str] | None = None,
|
||||||
|
max_ttl_minutes: int = 120,
|
||||||
|
) -> dict:
|
||||||
|
return await _call_gsap_tool("request_delegation", {
|
||||||
|
"delegator_ac_id": "",
|
||||||
|
"agent_type": agent_name,
|
||||||
|
"capability_ceiling": ",".join(capabilities or ["code:read"]),
|
||||||
|
"max_ttl_minutes": max_ttl_minutes,
|
||||||
|
})
|
||||||
178
src/guildhouse_mcp/tools/tasks.py
Normal file
178
src/guildhouse_mcp/tools/tasks.py
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
"""Task definition management — stored as TOML files."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
from ..config import settings
|
||||||
|
from ..agents.confidence import evaluate_gate, GateDecision
|
||||||
|
|
||||||
|
TASKS_DIR = Path(__file__).parent.parent.parent.parent / "tasks"
|
||||||
|
|
||||||
|
|
||||||
|
async def list_tasks() -> dict:
|
||||||
|
tasks = []
|
||||||
|
if TASKS_DIR.exists():
|
||||||
|
for f in sorted(TASKS_DIR.glob("**/*.toml")):
|
||||||
|
with open(f, "rb") as fh:
|
||||||
|
data = tomllib.load(fh)
|
||||||
|
task = data.get("task", {})
|
||||||
|
tasks.append({
|
||||||
|
"id": task.get("id", f.stem),
|
||||||
|
"repo": task.get("repo", ""),
|
||||||
|
"description": task.get("description", ""),
|
||||||
|
"phases": len(data.get("phases", {})),
|
||||||
|
"file": str(f.relative_to(TASKS_DIR)),
|
||||||
|
})
|
||||||
|
return {"tasks": tasks, "count": len(tasks)}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_task(task_id: str) -> dict:
|
||||||
|
if not TASKS_DIR.exists():
|
||||||
|
return {"error": f"Task '{task_id}' not found"}
|
||||||
|
for f in TASKS_DIR.glob("**/*.toml"):
|
||||||
|
with open(f, "rb") as fh:
|
||||||
|
data = tomllib.load(fh)
|
||||||
|
if data.get("task", {}).get("id") == task_id:
|
||||||
|
return data
|
||||||
|
return {"error": f"Task '{task_id}' not found"}
|
||||||
|
|
||||||
|
|
||||||
|
async def create_task(
|
||||||
|
task_id: str, repo: str,
|
||||||
|
description: str, phases: list[dict],
|
||||||
|
) -> dict:
|
||||||
|
TASKS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
task_file = TASKS_DIR / f"{task_id}.toml"
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
'[task]',
|
||||||
|
f'id = "{task_id}"',
|
||||||
|
f'repo = "{repo}"',
|
||||||
|
f'description = "{description}"',
|
||||||
|
'',
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, phase in enumerate(phases, 1):
|
||||||
|
lines.append(f'[phases.{i}]')
|
||||||
|
for key, value in phase.items():
|
||||||
|
if isinstance(value, str):
|
||||||
|
lines.append(f'{key} = "{value}"')
|
||||||
|
elif isinstance(value, (int, float)):
|
||||||
|
lines.append(f'{key} = {value}')
|
||||||
|
lines.append('')
|
||||||
|
|
||||||
|
task_file.write_text('\n'.join(lines))
|
||||||
|
return {"task_id": task_id, "file": str(task_file), "phases": len(phases)}
|
||||||
|
|
||||||
|
|
||||||
|
async def update_status(
|
||||||
|
task_id: str, phase: int,
|
||||||
|
status: str, confidence: int = 0, notes: str = "",
|
||||||
|
) -> dict:
|
||||||
|
TASKS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
status_file = TASKS_DIR / f".{task_id}.status.json"
|
||||||
|
|
||||||
|
if status_file.exists():
|
||||||
|
current = json.loads(status_file.read_text())
|
||||||
|
else:
|
||||||
|
current = {"task_id": task_id, "phases": {}}
|
||||||
|
|
||||||
|
current["phases"][str(phase)] = {
|
||||||
|
"status": status,
|
||||||
|
"confidence": confidence,
|
||||||
|
"notes": notes,
|
||||||
|
}
|
||||||
|
|
||||||
|
status_file.write_text(json.dumps(current, indent=2))
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
async def submit_for_review(
|
||||||
|
task_id: str, phase: int,
|
||||||
|
pr_number: int, worker_confidence: int, notes: str = "",
|
||||||
|
) -> dict:
|
||||||
|
return await update_status(
|
||||||
|
task_id=task_id, phase=phase,
|
||||||
|
status="awaiting_review",
|
||||||
|
confidence=worker_confidence,
|
||||||
|
notes=f"PR #{pr_number}: {notes}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def submit_review(
|
||||||
|
task_id: str, phase: int,
|
||||||
|
pr_number: int, reviewer_confidence: int,
|
||||||
|
findings: list | None = None, recommendation: str = "approve",
|
||||||
|
) -> dict:
|
||||||
|
from . import chronicle, forgejo
|
||||||
|
|
||||||
|
# Get worker confidence from status
|
||||||
|
status_file = TASKS_DIR / f".{task_id}.status.json"
|
||||||
|
current = json.loads(status_file.read_text()) if status_file.exists() else {}
|
||||||
|
phase_data = current.get("phases", {}).get(str(phase), {})
|
||||||
|
worker_confidence = phase_data.get("confidence", 50)
|
||||||
|
|
||||||
|
gate = evaluate_gate(
|
||||||
|
worker_confidence=worker_confidence,
|
||||||
|
reviewer_confidence=reviewer_confidence,
|
||||||
|
reviewer_recommendation=recommendation,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"gate_decision": gate.decision.value,
|
||||||
|
"combined_confidence": gate.combined_confidence,
|
||||||
|
"worker_confidence": gate.worker_confidence,
|
||||||
|
"reviewer_confidence": gate.reviewer_confidence,
|
||||||
|
"reason": gate.reason,
|
||||||
|
"auto_approved": gate.auto_approved,
|
||||||
|
"findings": findings or [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if gate.decision in (GateDecision.AUTO_MERGE, GateDecision.FLAG_MERGE):
|
||||||
|
task = await get_task(task_id)
|
||||||
|
repo = task.get("task", {}).get("repo", "")
|
||||||
|
if repo:
|
||||||
|
merge_result = await forgejo.merge_pr(repo, pr_number)
|
||||||
|
result["merged"] = True
|
||||||
|
result["merge_result"] = merge_result
|
||||||
|
|
||||||
|
event_type = "PR_AUTO_MERGED" if gate.decision == GateDecision.AUTO_MERGE else "PR_FLAG_MERGED"
|
||||||
|
event_code = "0x4010" if gate.decision == GateDecision.AUTO_MERGE else "0x4011"
|
||||||
|
if gate.decision == GateDecision.FLAG_MERGE:
|
||||||
|
result["flagged_for_review"] = True
|
||||||
|
|
||||||
|
await chronicle.emit_event(
|
||||||
|
event_type=event_type,
|
||||||
|
event_code=event_code,
|
||||||
|
principal=settings.agent_did,
|
||||||
|
payload=result,
|
||||||
|
)
|
||||||
|
|
||||||
|
elif gate.decision == GateDecision.PROPOSE:
|
||||||
|
result["proposal_created"] = True
|
||||||
|
result["message"] = "Below auto-merge threshold. Proposal created for Tyler."
|
||||||
|
await chronicle.emit_event(
|
||||||
|
event_type="PR_PROPOSAL_CREATED",
|
||||||
|
event_code="0x4012",
|
||||||
|
principal=settings.agent_did,
|
||||||
|
payload=result,
|
||||||
|
)
|
||||||
|
|
||||||
|
elif gate.decision == GateDecision.REJECT:
|
||||||
|
result["rejected"] = True
|
||||||
|
result["message"] = "Confidence too low. Work needs revision."
|
||||||
|
await chronicle.emit_event(
|
||||||
|
event_type="PR_REJECTED",
|
||||||
|
event_code="0x4013",
|
||||||
|
principal=settings.agent_did,
|
||||||
|
payload=result,
|
||||||
|
)
|
||||||
|
|
||||||
|
await update_status(
|
||||||
|
task_id=task_id, phase=phase,
|
||||||
|
status=gate.decision.value,
|
||||||
|
confidence=int(gate.combined_confidence),
|
||||||
|
notes=gate.reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
20
tasks/examples/bascule-tui.toml
Normal file
20
tasks/examples/bascule-tui.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
[task]
|
||||||
|
id = "bascule-dashboard-tui"
|
||||||
|
repo = "tking/bascule-oss"
|
||||||
|
description = "Build TUI dashboard using dioxus-ratatui bridge"
|
||||||
|
branch = "feat/dashboard-tui"
|
||||||
|
delegation_scope = "code:write,test:run"
|
||||||
|
chronicle_source = "did:web:guildhouse.dev/agent/worker-a"
|
||||||
|
|
||||||
|
[phases.1]
|
||||||
|
description = "Implement TUI components using dioxus-ratatui"
|
||||||
|
prompt = "Build TUI versions of SessionTable, AuthStats, and StatusBar using the dioxus-ratatui bridge crate"
|
||||||
|
success_criteria = "cargo build -p bascule-dashboard-tui compiles"
|
||||||
|
estimated_confidence = 75
|
||||||
|
|
||||||
|
[phases.2]
|
||||||
|
description = "Wire to management API"
|
||||||
|
prompt = "Connect TUI components to bascule-server management API on port 9090"
|
||||||
|
depends_on = 1
|
||||||
|
success_criteria = "TUI shows live session data from running bascule-server"
|
||||||
|
estimated_confidence = 70
|
||||||
20
tasks/examples/capstone-cleanup.toml
Normal file
20
tasks/examples/capstone-cleanup.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
[task]
|
||||||
|
id = "capstone-test-cleanup"
|
||||||
|
repo = "tking/capstone"
|
||||||
|
description = "Fix pre-existing Keycloak JWT auth test failures"
|
||||||
|
branch = "fix/auth-tests"
|
||||||
|
delegation_scope = "code:write,test:run"
|
||||||
|
chronicle_source = "did:web:guildhouse.dev/agent/worker-b"
|
||||||
|
|
||||||
|
[phases.1]
|
||||||
|
description = "Diagnose failing auth tests"
|
||||||
|
prompt = "Run pytest on apps/accounts/tests/ and identify the 4 pre-existing Keycloak JWT failures. Document root cause."
|
||||||
|
success_criteria = "Root cause documented for each failure"
|
||||||
|
estimated_confidence = 80
|
||||||
|
|
||||||
|
[phases.2]
|
||||||
|
description = "Fix test fixtures"
|
||||||
|
prompt = "Update test fixtures to match current Keycloak JWT format. Do not change production code."
|
||||||
|
depends_on = 1
|
||||||
|
success_criteria = "All 4 previously-failing tests pass, no regressions"
|
||||||
|
estimated_confidence = 70
|
||||||
Loading…
Reference in a new issue