delimit-cli 3.15.6 → 3.15.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gateway/ai/server.py +249 -0
- package/gateway/ai/swarm.py +397 -0
- package/package.json +1 -1
package/gateway/ai/server.py
CHANGED
|
@@ -3631,6 +3631,255 @@ STANDARD_WORKFLOWS = [
|
|
|
3631
3631
|
]
|
|
3632
3632
|
|
|
3633
3633
|
|
|
3634
|
+
@mcp.tool()
|
|
3635
|
+
def delimit_swarm(action: str = "status", venture: str = "",
|
|
3636
|
+
agent_id: str = "", repo_path: str = "",
|
|
3637
|
+
deploy_target: str = "", target_path: str = "",
|
|
3638
|
+
access_action: str = "read") -> Dict[str, Any]:
|
|
3639
|
+
"""Manage the agent swarm — ventures, personas, namespace isolation.
|
|
3640
|
+
|
|
3641
|
+
Each venture gets 5 AI agent roles (Architect, Senior Dev, Reviewer, QA, Ops)
|
|
3642
|
+
with namespace isolation and model binding per Agent Swarm Standard v1.2.
|
|
3643
|
+
|
|
3644
|
+
Actions:
|
|
3645
|
+
status: Full swarm overview (ventures, agents, health)
|
|
3646
|
+
register: Register a new venture with agent team
|
|
3647
|
+
venture: Get venture details + its agents
|
|
3648
|
+
agent: Get agent details
|
|
3649
|
+
check: Check namespace access for an agent
|
|
3650
|
+
approve: Check approval tier for an action
|
|
3651
|
+
guide: Get usage documentation
|
|
3652
|
+
rules: Get escalation rules
|
|
3653
|
+
|
|
3654
|
+
Args:
|
|
3655
|
+
action: "status", "register", "venture", "agent", "check", "approve", "guide", or "rules".
|
|
3656
|
+
venture: Venture name (for register/venture).
|
|
3657
|
+
agent_id: Agent ID like "delimit-architect-01" (for agent/check).
|
|
3658
|
+
repo_path: Repo path for venture registration.
|
|
3659
|
+
deploy_target: Deploy target for venture registration.
|
|
3660
|
+
target_path: File path to check access for (check action).
|
|
3661
|
+
access_action: Action name — for check: "read"/"write"/"deploy". For approve: "deploy_production"/"deploy_staging"/"social_post" etc.
|
|
3662
|
+
"""
|
|
3663
|
+
from ai.swarm import (register_venture, get_venture, get_agent,
|
|
3664
|
+
check_namespace_access, get_swarm_status,
|
|
3665
|
+
check_approval, get_escalation_rules, get_usage_guide)
|
|
3666
|
+
|
|
3667
|
+
if action == "register":
|
|
3668
|
+
return _with_next_steps("swarm", _safe_call(
|
|
3669
|
+
register_venture, name=venture, repo_path=repo_path, deploy_target=deploy_target,
|
|
3670
|
+
))
|
|
3671
|
+
if action == "venture":
|
|
3672
|
+
return _with_next_steps("swarm", _safe_call(get_venture, name=venture))
|
|
3673
|
+
if action == "agent":
|
|
3674
|
+
return _with_next_steps("swarm", _safe_call(get_agent, agent_id=agent_id))
|
|
3675
|
+
if action == "approve":
|
|
3676
|
+
return _with_next_steps("swarm", _safe_call(
|
|
3677
|
+
check_approval, action=access_action, venture=venture, agent_id=agent_id,
|
|
3678
|
+
))
|
|
3679
|
+
if action == "guide":
|
|
3680
|
+
return _with_next_steps("swarm", _safe_call(get_usage_guide))
|
|
3681
|
+
if action == "rules":
|
|
3682
|
+
return _with_next_steps("swarm", _safe_call(get_escalation_rules))
|
|
3683
|
+
if action == "check":
|
|
3684
|
+
return _with_next_steps("swarm", _safe_call(
|
|
3685
|
+
check_namespace_access, agent_id=agent_id, target_path=target_path, action=access_action,
|
|
3686
|
+
))
|
|
3687
|
+
return _with_next_steps("swarm", _safe_call(get_swarm_status))
|
|
3688
|
+
|
|
3689
|
+
|
|
3690
|
+
@mcp.tool()
|
|
3691
|
+
def delimit_review(diff: str = "", file_path: str = "",
|
|
3692
|
+
context: str = "", pr_url: str = "") -> Dict[str, Any]:
|
|
3693
|
+
"""Run a multi-model code review on a diff or file.
|
|
3694
|
+
|
|
3695
|
+
Sends the code change to multiple AI models and consolidates their
|
|
3696
|
+
feedback into a single structured review. The output can be posted
|
|
3697
|
+
as a GitHub PR comment.
|
|
3698
|
+
|
|
3699
|
+
Provide either a diff string or a file path to review.
|
|
3700
|
+
|
|
3701
|
+
Args:
|
|
3702
|
+
diff: Git diff or code to review. Takes priority over file_path.
|
|
3703
|
+
file_path: Path to file to review (reads current content).
|
|
3704
|
+
context: Additional context about the change (what it does, why).
|
|
3705
|
+
pr_url: GitHub PR URL for linking the review.
|
|
3706
|
+
"""
|
|
3707
|
+
from ai.multi_review import generate_review_prompt, consolidate_reviews, save_review
|
|
3708
|
+
|
|
3709
|
+
# Get the diff content
|
|
3710
|
+
if not diff and file_path:
|
|
3711
|
+
try:
|
|
3712
|
+
from subprocess import run as _run
|
|
3713
|
+
result = _run(
|
|
3714
|
+
["git", "diff", "HEAD", "--", file_path],
|
|
3715
|
+
capture_output=True, text=True, timeout=10,
|
|
3716
|
+
)
|
|
3717
|
+
diff = result.stdout or Path(file_path).read_text()[:5000]
|
|
3718
|
+
except Exception:
|
|
3719
|
+
try:
|
|
3720
|
+
diff = Path(file_path).read_text()[:5000]
|
|
3721
|
+
except Exception as e:
|
|
3722
|
+
return {"error": f"Could not read {file_path}: {e}"}
|
|
3723
|
+
|
|
3724
|
+
if not diff:
|
|
3725
|
+
return {"error": "Provide either diff text or file_path to review"}
|
|
3726
|
+
|
|
3727
|
+
prompt = generate_review_prompt(diff, context)
|
|
3728
|
+
|
|
3729
|
+
# Run through deliberation engine for multi-model feedback
|
|
3730
|
+
try:
|
|
3731
|
+
from ai.deliberation import get_models_config
|
|
3732
|
+
config = get_models_config()
|
|
3733
|
+
enabled = {k: v for k, v in config.items() if v.get("enabled")}
|
|
3734
|
+
|
|
3735
|
+
if len(enabled) < 2:
|
|
3736
|
+
return {
|
|
3737
|
+
"error": "Need at least 2 AI models for multi-model review",
|
|
3738
|
+
"tip": "Configure models in ~/.delimit/models.json",
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
reviews = []
|
|
3742
|
+
from ai.deliberation import _call_model
|
|
3743
|
+
import time as _time
|
|
3744
|
+
|
|
3745
|
+
for model_id, model_config in list(enabled.items())[:3]:
|
|
3746
|
+
start = _time.time()
|
|
3747
|
+
try:
|
|
3748
|
+
response = _call_model(model_id, model_config, prompt,
|
|
3749
|
+
system_prompt="You are a senior code reviewer. Be concise and actionable.")
|
|
3750
|
+
duration = int((_time.time() - start) * 1000)
|
|
3751
|
+
reviews.append({
|
|
3752
|
+
"model": model_config.get("name", model_id),
|
|
3753
|
+
"content": response,
|
|
3754
|
+
"duration_ms": duration,
|
|
3755
|
+
})
|
|
3756
|
+
except Exception as e:
|
|
3757
|
+
reviews.append({
|
|
3758
|
+
"model": model_config.get("name", model_id),
|
|
3759
|
+
"content": f"Review failed: {e}",
|
|
3760
|
+
"duration_ms": 0,
|
|
3761
|
+
})
|
|
3762
|
+
|
|
3763
|
+
report = consolidate_reviews(reviews)
|
|
3764
|
+
result = save_review(diff, report, pr_url)
|
|
3765
|
+
|
|
3766
|
+
return _with_next_steps("review", {
|
|
3767
|
+
"status": "complete",
|
|
3768
|
+
"models_used": report["models_used"],
|
|
3769
|
+
"review_count": len(reviews),
|
|
3770
|
+
"pr_comment": result["pr_comment"],
|
|
3771
|
+
"review_id": result["review_id"],
|
|
3772
|
+
})
|
|
3773
|
+
|
|
3774
|
+
except ImportError:
|
|
3775
|
+
return {"error": "Deliberation engine required for multi-model review"}
|
|
3776
|
+
|
|
3777
|
+
|
|
3778
|
+
@mcp.tool()
|
|
3779
|
+
def delimit_redact(action: str = "scan", text: str = "",
|
|
3780
|
+
categories: str = "") -> Dict[str, Any]:
|
|
3781
|
+
"""Scan or redact sensitive data (API keys, secrets, PII) from text.
|
|
3782
|
+
|
|
3783
|
+
Use before sending prompts to external LLMs to prevent data leakage.
|
|
3784
|
+
Detects: API keys (OpenAI, xAI, Google, GitHub, npm), passwords,
|
|
3785
|
+
bearer tokens, emails, phone numbers, SSNs, credit cards, IPs, DB URLs.
|
|
3786
|
+
|
|
3787
|
+
Actions:
|
|
3788
|
+
scan: Preview what would be redacted (non-destructive)
|
|
3789
|
+
redact: Replace sensitive data with [REDACTED_TYPE_N] tokens
|
|
3790
|
+
|
|
3791
|
+
Args:
|
|
3792
|
+
action: "scan" or "redact".
|
|
3793
|
+
text: Text to scan/redact.
|
|
3794
|
+
categories: Comma-separated categories (api_key, secret, pii, infra). Empty = all.
|
|
3795
|
+
"""
|
|
3796
|
+
from ai.pii_redact import scan as pii_scan, redact as pii_redact
|
|
3797
|
+
|
|
3798
|
+
cat_list = [c.strip() for c in categories.split(",") if c.strip()] if categories else None
|
|
3799
|
+
|
|
3800
|
+
if action == "redact":
|
|
3801
|
+
result = pii_redact(text, categories=cat_list)
|
|
3802
|
+
# Never expose token_map through MCP — keep it local
|
|
3803
|
+
return _with_next_steps("redact", {
|
|
3804
|
+
"redacted": result["redacted"],
|
|
3805
|
+
"findings": result["findings"],
|
|
3806
|
+
"token_count": result["token_count"],
|
|
3807
|
+
})
|
|
3808
|
+
|
|
3809
|
+
return _with_next_steps("redact", _safe_call(pii_scan, text=text))
|
|
3810
|
+
|
|
3811
|
+
|
|
3812
|
+
@mcp.tool()
|
|
3813
|
+
def delimit_prompt_drift(action: str = "check", prompt: str = "",
|
|
3814
|
+
model: str = "", result_summary: str = "",
|
|
3815
|
+
success: str = "true", task_type: str = "") -> Dict[str, Any]:
|
|
3816
|
+
"""Detect prompt drift — when the same task behaves differently across models.
|
|
3817
|
+
|
|
3818
|
+
Track how prompts perform across Claude, Codex, and Gemini.
|
|
3819
|
+
Find which model is best for each task type on YOUR codebase.
|
|
3820
|
+
|
|
3821
|
+
Actions:
|
|
3822
|
+
record: Log a prompt result (model, success, duration)
|
|
3823
|
+
check: Detect drift across models for a prompt or task type
|
|
3824
|
+
rank: Rank models by success rate and speed
|
|
3825
|
+
|
|
3826
|
+
Args:
|
|
3827
|
+
action: "record", "check", or "rank".
|
|
3828
|
+
prompt: The prompt text (for record/check).
|
|
3829
|
+
model: AI model name (for record).
|
|
3830
|
+
result_summary: Brief description of the result (for record).
|
|
3831
|
+
success: Whether the result was good ("true"/"false").
|
|
3832
|
+
task_type: Task category (refactoring/testing/debugging/docs).
|
|
3833
|
+
"""
|
|
3834
|
+
from ai.prompt_drift import record_result, check_drift, get_model_rankings
|
|
3835
|
+
|
|
3836
|
+
if action == "record":
|
|
3837
|
+
return _with_next_steps("prompt_drift", _safe_call(
|
|
3838
|
+
record_result, prompt=prompt, model=model,
|
|
3839
|
+
result_summary=result_summary,
|
|
3840
|
+
success=success.lower().strip() in ("true", "1", "yes"),
|
|
3841
|
+
task_type=task_type,
|
|
3842
|
+
))
|
|
3843
|
+
if action == "rank":
|
|
3844
|
+
return _with_next_steps("prompt_drift", _safe_call(
|
|
3845
|
+
get_model_rankings, task_type=task_type,
|
|
3846
|
+
))
|
|
3847
|
+
return _with_next_steps("prompt_drift", _safe_call(
|
|
3848
|
+
check_drift, prompt=prompt, task_type=task_type,
|
|
3849
|
+
))
|
|
3850
|
+
|
|
3851
|
+
|
|
3852
|
+
@mcp.tool()
|
|
3853
|
+
def delimit_collision_check(action: str = "check", file_path: str = "",
|
|
3854
|
+
model: str = "", task_id: str = "") -> Dict[str, Any]:
|
|
3855
|
+
"""Detect and prevent two AI models from editing the same file.
|
|
3856
|
+
|
|
3857
|
+
Call before editing a file to check if another model is already working on it.
|
|
3858
|
+
|
|
3859
|
+
Actions:
|
|
3860
|
+
check: Show all active file locks and hotspots
|
|
3861
|
+
claim: Claim a file before editing (returns collision if held)
|
|
3862
|
+
release: Release a file lock after done editing
|
|
3863
|
+
|
|
3864
|
+
Args:
|
|
3865
|
+
action: "check", "claim", or "release".
|
|
3866
|
+
file_path: File to claim/release (required for claim/release).
|
|
3867
|
+
model: AI model name (claude/codex/gemini).
|
|
3868
|
+
task_id: Optional task ID for tracking.
|
|
3869
|
+
"""
|
|
3870
|
+
from ai.collision_detect import claim_file, release_file, check_collisions
|
|
3871
|
+
|
|
3872
|
+
if action == "claim":
|
|
3873
|
+
return _with_next_steps("collision", _safe_call(
|
|
3874
|
+
claim_file, file_path=file_path, model=model, task_id=task_id,
|
|
3875
|
+
))
|
|
3876
|
+
if action == "release":
|
|
3877
|
+
return _with_next_steps("collision", _safe_call(
|
|
3878
|
+
release_file, file_path=file_path, model=model,
|
|
3879
|
+
))
|
|
3880
|
+
return _with_next_steps("collision", _safe_call(check_collisions, model=model))
|
|
3881
|
+
|
|
3882
|
+
|
|
3634
3883
|
@mcp.tool()
|
|
3635
3884
|
def delimit_project_config(action: str = "load", project_path: str = ".",
|
|
3636
3885
|
mode: str = "advisory", preset: str = "default",
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Agent Swarm — persona registry, namespace isolation, and venture management.
|
|
2
|
+
|
|
3
|
+
Implements Agent Swarm Standard v1.2 (4-party consent achieved 2026-03-30).
|
|
4
|
+
Each venture gets 5 agent roles bound to AI models with namespace isolation.
|
|
5
|
+
|
|
6
|
+
Config: ~/.delimit/swarm/config.yml
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
SWARM_DIR = Path.home() / ".delimit" / "swarm"
|
|
15
|
+
REGISTRY_FILE = SWARM_DIR / "agent_registry.json"
|
|
16
|
+
VENTURES_FILE = SWARM_DIR / "ventures.json"
|
|
17
|
+
SWARM_LOG = SWARM_DIR / "swarm_log.jsonl"
|
|
18
|
+
|
|
19
|
+
# Default roster from Agent Swarm Standard v1.2
|
|
20
|
+
DEFAULT_ROSTER = {
|
|
21
|
+
"architect": {
|
|
22
|
+
"role": "System Design, Architecture, Complex Problem Solving",
|
|
23
|
+
"default_model": "claude-opus-4.6",
|
|
24
|
+
"fallback_model": "grok-4",
|
|
25
|
+
},
|
|
26
|
+
"senior_dev": {
|
|
27
|
+
"role": "Implementation, Code Generation, Feature Building",
|
|
28
|
+
"default_model": "claude-opus-4.6",
|
|
29
|
+
"fallback_model": "codex-gpt-5.4",
|
|
30
|
+
},
|
|
31
|
+
"reviewer": {
|
|
32
|
+
"role": "Code Review, PR Analysis, Bug Detection",
|
|
33
|
+
"default_model": "gemini-3.1-pro-preview",
|
|
34
|
+
"fallback_model": "grok-4",
|
|
35
|
+
},
|
|
36
|
+
"qa": {
|
|
37
|
+
"role": "Quality Assurance, Testing, CI Verification",
|
|
38
|
+
"default_model": "gemini-3.1-pro-preview",
|
|
39
|
+
"fallback_model": "codex-gpt-5.4",
|
|
40
|
+
},
|
|
41
|
+
"ops": {
|
|
42
|
+
"role": "Strategy, Deliberation, Outreach, Competitive Intel",
|
|
43
|
+
"default_model": "grok-4",
|
|
44
|
+
"fallback_model": "gemini-3.1-pro-preview",
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _ensure_dir():
|
|
50
|
+
SWARM_DIR.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load_registry() -> Dict[str, Any]:
|
|
54
|
+
if not REGISTRY_FILE.exists():
|
|
55
|
+
return {"agents": {}, "version": "1.2"}
|
|
56
|
+
try:
|
|
57
|
+
return json.loads(REGISTRY_FILE.read_text())
|
|
58
|
+
except (json.JSONDecodeError, OSError):
|
|
59
|
+
return {"agents": {}, "version": "1.2"}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _save_registry(registry: Dict[str, Any]):
|
|
63
|
+
_ensure_dir()
|
|
64
|
+
REGISTRY_FILE.write_text(json.dumps(registry, indent=2))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _load_ventures() -> Dict[str, Any]:
|
|
68
|
+
if not VENTURES_FILE.exists():
|
|
69
|
+
return {}
|
|
70
|
+
try:
|
|
71
|
+
return json.loads(VENTURES_FILE.read_text())
|
|
72
|
+
except (json.JSONDecodeError, OSError):
|
|
73
|
+
return {}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _save_ventures(ventures: Dict[str, Any]):
|
|
77
|
+
_ensure_dir()
|
|
78
|
+
VENTURES_FILE.write_text(json.dumps(ventures, indent=2))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _log(entry: Dict[str, Any]):
|
|
82
|
+
_ensure_dir()
|
|
83
|
+
entry["ts"] = time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
84
|
+
with open(SWARM_LOG, "a") as f:
|
|
85
|
+
f.write(json.dumps(entry) + "\n")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def register_venture(
|
|
89
|
+
name: str,
|
|
90
|
+
namespace: str = "",
|
|
91
|
+
repo_path: str = "",
|
|
92
|
+
deploy_target: str = "",
|
|
93
|
+
custom_tools: Optional[List[str]] = None,
|
|
94
|
+
special_rules: Optional[List[str]] = None,
|
|
95
|
+
) -> Dict[str, Any]:
|
|
96
|
+
"""Register a venture in the swarm with its own namespace and agent team."""
|
|
97
|
+
if not name:
|
|
98
|
+
return {"error": "name is required"}
|
|
99
|
+
|
|
100
|
+
name = name.strip().lower()
|
|
101
|
+
namespace = namespace or name.replace(" ", "_").replace("-", "_")
|
|
102
|
+
|
|
103
|
+
ventures = _load_ventures()
|
|
104
|
+
ventures[name] = {
|
|
105
|
+
"name": name,
|
|
106
|
+
"namespace": namespace,
|
|
107
|
+
"repo_path": repo_path,
|
|
108
|
+
"deploy_target": deploy_target,
|
|
109
|
+
"custom_tools": custom_tools or [],
|
|
110
|
+
"special_rules": special_rules or [],
|
|
111
|
+
"created_at": ventures.get(name, {}).get("created_at", time.strftime("%Y-%m-%dT%H:%M:%SZ")),
|
|
112
|
+
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
113
|
+
}
|
|
114
|
+
_save_ventures(ventures)
|
|
115
|
+
|
|
116
|
+
# Auto-create agents for this venture
|
|
117
|
+
registry = _load_registry()
|
|
118
|
+
agents_created = 0
|
|
119
|
+
for role_key, role_config in DEFAULT_ROSTER.items():
|
|
120
|
+
agent_id = f"{namespace}-{role_key}-01"
|
|
121
|
+
if agent_id not in registry["agents"]:
|
|
122
|
+
registry["agents"][agent_id] = {
|
|
123
|
+
"id": agent_id,
|
|
124
|
+
"venture": name,
|
|
125
|
+
"namespace": namespace,
|
|
126
|
+
"role": role_key,
|
|
127
|
+
"role_description": role_config["role"],
|
|
128
|
+
"model": role_config["default_model"],
|
|
129
|
+
"fallback_model": role_config["fallback_model"],
|
|
130
|
+
"permissions": {
|
|
131
|
+
"read": f"{namespace}/*",
|
|
132
|
+
"write": f"{namespace}/src/*",
|
|
133
|
+
"deploy": False,
|
|
134
|
+
},
|
|
135
|
+
"status": "active",
|
|
136
|
+
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
137
|
+
}
|
|
138
|
+
agents_created += 1
|
|
139
|
+
|
|
140
|
+
_save_registry(registry)
|
|
141
|
+
_log({"action": "register_venture", "venture": name, "agents_created": agents_created})
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
"status": "registered",
|
|
145
|
+
"venture": name,
|
|
146
|
+
"namespace": namespace,
|
|
147
|
+
"agents_created": agents_created,
|
|
148
|
+
"total_agents": len([a for a in registry["agents"].values() if a["venture"] == name]),
|
|
149
|
+
"message": f"Venture '{name}' registered with {agents_created} new agent(s)",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_venture(name: str = "") -> Dict[str, Any]:
|
|
154
|
+
"""Get venture details, or list all ventures."""
|
|
155
|
+
ventures = _load_ventures()
|
|
156
|
+
|
|
157
|
+
if not name:
|
|
158
|
+
return {
|
|
159
|
+
"status": "ok",
|
|
160
|
+
"ventures": list(ventures.values()),
|
|
161
|
+
"total": len(ventures),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
name = name.strip().lower()
|
|
165
|
+
if name not in ventures:
|
|
166
|
+
return {"error": f"Venture '{name}' not found"}
|
|
167
|
+
|
|
168
|
+
venture = ventures[name]
|
|
169
|
+
registry = _load_registry()
|
|
170
|
+
agents = [a for a in registry["agents"].values() if a["venture"] == name]
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
"status": "ok",
|
|
174
|
+
"venture": venture,
|
|
175
|
+
"agents": agents,
|
|
176
|
+
"agent_count": len(agents),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_agent(agent_id: str = "") -> Dict[str, Any]:
|
|
181
|
+
"""Get agent details, or list all agents across ventures."""
|
|
182
|
+
registry = _load_registry()
|
|
183
|
+
|
|
184
|
+
if not agent_id:
|
|
185
|
+
agents = list(registry["agents"].values())
|
|
186
|
+
by_venture = {}
|
|
187
|
+
for a in agents:
|
|
188
|
+
by_venture.setdefault(a["venture"], []).append({
|
|
189
|
+
"id": a["id"],
|
|
190
|
+
"role": a["role"],
|
|
191
|
+
"model": a["model"],
|
|
192
|
+
"status": a["status"],
|
|
193
|
+
})
|
|
194
|
+
return {
|
|
195
|
+
"status": "ok",
|
|
196
|
+
"total_agents": len(agents),
|
|
197
|
+
"by_venture": by_venture,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
agent = registry["agents"].get(agent_id)
|
|
201
|
+
if not agent:
|
|
202
|
+
return {"error": f"Agent '{agent_id}' not found"}
|
|
203
|
+
return {"status": "ok", "agent": agent}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def check_namespace_access(
|
|
207
|
+
agent_id: str,
|
|
208
|
+
target_path: str,
|
|
209
|
+
action: str = "read",
|
|
210
|
+
) -> Dict[str, Any]:
|
|
211
|
+
"""Check if an agent has access to a path within namespace isolation rules."""
|
|
212
|
+
registry = _load_registry()
|
|
213
|
+
agent = registry["agents"].get(agent_id)
|
|
214
|
+
|
|
215
|
+
if not agent:
|
|
216
|
+
return {"allowed": False, "reason": f"Agent '{agent_id}' not found"}
|
|
217
|
+
|
|
218
|
+
namespace = agent["namespace"]
|
|
219
|
+
ventures = _load_ventures()
|
|
220
|
+
venture = ventures.get(agent["venture"], {})
|
|
221
|
+
repo_path = venture.get("repo_path", "")
|
|
222
|
+
|
|
223
|
+
# Check if target is within the venture's namespace
|
|
224
|
+
if repo_path and target_path.startswith(repo_path):
|
|
225
|
+
if action == "read":
|
|
226
|
+
return {"allowed": True, "agent": agent_id, "reason": "Within venture namespace"}
|
|
227
|
+
if action == "write":
|
|
228
|
+
return {"allowed": True, "agent": agent_id, "reason": "Write within namespace"}
|
|
229
|
+
if action == "deploy":
|
|
230
|
+
if agent["permissions"].get("deploy", False):
|
|
231
|
+
return {"allowed": True, "agent": agent_id, "reason": "Deploy permitted"}
|
|
232
|
+
return {"allowed": False, "agent": agent_id, "reason": "Deploy requires founder approval"}
|
|
233
|
+
|
|
234
|
+
# Cross-venture access blocked by default
|
|
235
|
+
return {
|
|
236
|
+
"allowed": False,
|
|
237
|
+
"agent": agent_id,
|
|
238
|
+
"target": target_path,
|
|
239
|
+
"namespace": namespace,
|
|
240
|
+
"reason": f"Cross-venture access blocked. Agent '{agent_id}' can only access {namespace}/* paths.",
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def get_swarm_status() -> Dict[str, Any]:
|
|
245
|
+
"""Get the full swarm status — ventures, agents, health."""
|
|
246
|
+
ventures = _load_ventures()
|
|
247
|
+
registry = _load_registry()
|
|
248
|
+
agents = list(registry["agents"].values())
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"status": "ok",
|
|
252
|
+
"version": registry.get("version", "1.2"),
|
|
253
|
+
"ventures": len(ventures),
|
|
254
|
+
"total_agents": len(agents),
|
|
255
|
+
"active_agents": len([a for a in agents if a["status"] == "active"]),
|
|
256
|
+
"by_venture": {
|
|
257
|
+
v: {
|
|
258
|
+
"agents": len([a for a in agents if a["venture"] == v]),
|
|
259
|
+
"namespace": ventures[v].get("namespace", v),
|
|
260
|
+
"repo": ventures[v].get("repo_path", ""),
|
|
261
|
+
}
|
|
262
|
+
for v in ventures
|
|
263
|
+
},
|
|
264
|
+
"roster": list(DEFAULT_ROSTER.keys()),
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
269
|
+
# LED-276: Central Governor — tiered approvals + auto-escalation
|
|
270
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
271
|
+
|
|
272
|
+
APPROVAL_TIERS = {
|
|
273
|
+
"deploy_production": "founder_required",
|
|
274
|
+
"deploy_staging": "auto_approved",
|
|
275
|
+
"social_post": "founder_email",
|
|
276
|
+
"social_low_risk": "auto_after_consensus",
|
|
277
|
+
"outreach_issue": "founder_email",
|
|
278
|
+
"ledger_update": "auto_approved",
|
|
279
|
+
"code_commit": "auto_approved",
|
|
280
|
+
"security_audit": "auto_approved",
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
ESCALATION_RULES = [
|
|
284
|
+
{"trigger": "collision_detected", "action": "halt_and_notify", "severity": "high"},
|
|
285
|
+
{"trigger": "prompt_drift_exceeded", "action": "pause_agent", "severity": "medium"},
|
|
286
|
+
{"trigger": "unauthorized_deploy", "action": "block_and_alert", "severity": "critical"},
|
|
287
|
+
{"trigger": "pii_detected_outbound", "action": "redact_and_log", "severity": "high"},
|
|
288
|
+
{"trigger": "xai_credits_exhausted", "action": "pause_posting", "severity": "medium"},
|
|
289
|
+
{"trigger": "model_error_rate_high", "action": "switch_to_fallback", "severity": "medium"},
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def check_approval(action: str, venture: str = "", agent_id: str = "") -> Dict[str, Any]:
|
|
294
|
+
"""Check if an action requires approval or is auto-approved.
|
|
295
|
+
|
|
296
|
+
Tiered approval system:
|
|
297
|
+
- deploy_production: always requires founder approval
|
|
298
|
+
- deploy_staging: auto-approved
|
|
299
|
+
- social_post: founder email approval
|
|
300
|
+
- social_low_risk: auto after multi-model consensus
|
|
301
|
+
- code_commit, ledger_update: auto-approved
|
|
302
|
+
"""
|
|
303
|
+
tier = APPROVAL_TIERS.get(action, "founder_required")
|
|
304
|
+
|
|
305
|
+
result = {
|
|
306
|
+
"action": action,
|
|
307
|
+
"tier": tier,
|
|
308
|
+
"venture": venture,
|
|
309
|
+
"agent_id": agent_id,
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if tier == "auto_approved":
|
|
313
|
+
result["approved"] = True
|
|
314
|
+
result["message"] = f"'{action}' is auto-approved"
|
|
315
|
+
elif tier == "auto_after_consensus":
|
|
316
|
+
result["approved"] = False
|
|
317
|
+
result["message"] = f"'{action}' requires multi-model consensus before auto-approval"
|
|
318
|
+
result["next_step"] = "Run delimit_deliberate on the proposed action"
|
|
319
|
+
elif tier == "founder_email":
|
|
320
|
+
result["approved"] = False
|
|
321
|
+
result["message"] = f"'{action}' requires founder email approval"
|
|
322
|
+
result["next_step"] = "Send via delimit_notify for founder review"
|
|
323
|
+
else:
|
|
324
|
+
result["approved"] = False
|
|
325
|
+
result["message"] = f"'{action}' requires founder approval"
|
|
326
|
+
result["next_step"] = "Submit for founder review via dashboard or email"
|
|
327
|
+
|
|
328
|
+
_log({"action": "approval_check", "requested": action, "result": tier, "venture": venture, "agent": agent_id})
|
|
329
|
+
return result
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def get_escalation_rules() -> Dict[str, Any]:
|
|
333
|
+
"""Get the current escalation rules for the central governor."""
|
|
334
|
+
return {
|
|
335
|
+
"status": "ok",
|
|
336
|
+
"rules": ESCALATION_RULES,
|
|
337
|
+
"approval_tiers": APPROVAL_TIERS,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
342
|
+
# Usage Guide
|
|
343
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
344
|
+
|
|
345
|
+
USAGE_GUIDE = """
|
|
346
|
+
# Delimit Agent Swarm — Usage Guide
|
|
347
|
+
|
|
348
|
+
## Quick Start
|
|
349
|
+
|
|
350
|
+
1. Register your ventures:
|
|
351
|
+
delimit_swarm(action="register", venture="my-project", repo_path="/path/to/repo")
|
|
352
|
+
|
|
353
|
+
2. View your swarm:
|
|
354
|
+
delimit_swarm(action="status")
|
|
355
|
+
|
|
356
|
+
3. Check agent permissions:
|
|
357
|
+
delimit_swarm(action="check", agent_id="my_project-architect-01", target_path="/path/to/file")
|
|
358
|
+
|
|
359
|
+
4. Check approval tier:
|
|
360
|
+
delimit_swarm(action="approve", action_name="deploy_production")
|
|
361
|
+
|
|
362
|
+
## Agent Roles
|
|
363
|
+
|
|
364
|
+
Each venture gets 5 agent roles:
|
|
365
|
+
- architect: System design (default: Claude Opus)
|
|
366
|
+
- senior_dev: Implementation (default: Claude Opus)
|
|
367
|
+
- reviewer: Code review (default: Gemini)
|
|
368
|
+
- qa: Testing (default: Gemini)
|
|
369
|
+
- ops: Strategy & outreach (default: Grok)
|
|
370
|
+
|
|
371
|
+
## Namespace Isolation
|
|
372
|
+
|
|
373
|
+
- Agents can only access files within their venture's repo path
|
|
374
|
+
- Cross-venture access is blocked by default
|
|
375
|
+
- Deploy requires founder approval (auto for staging)
|
|
376
|
+
- All actions are logged to ~/.delimit/swarm/swarm_log.jsonl
|
|
377
|
+
|
|
378
|
+
## Approval Tiers
|
|
379
|
+
|
|
380
|
+
| Action | Tier |
|
|
381
|
+
|--------|------|
|
|
382
|
+
| deploy_production | Founder approval required |
|
|
383
|
+
| deploy_staging | Auto-approved |
|
|
384
|
+
| social_post | Founder email approval |
|
|
385
|
+
| code_commit | Auto-approved |
|
|
386
|
+
| ledger_update | Auto-approved |
|
|
387
|
+
|
|
388
|
+
## Escalation
|
|
389
|
+
|
|
390
|
+
Critical alerts auto-escalate: collision, unauthorized deploy, PII detected.
|
|
391
|
+
Medium alerts: prompt drift, model errors, credit exhaustion.
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def get_usage_guide() -> Dict[str, Any]:
|
|
396
|
+
"""Get the swarm usage guide."""
|
|
397
|
+
return {"guide": USAGE_GUIDE, "version": "1.2"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "delimit-cli",
|
|
3
3
|
"mcpName": "io.github.delimit-ai/delimit-mcp-server",
|
|
4
|
-
"version": "3.15.
|
|
4
|
+
"version": "3.15.7",
|
|
5
5
|
"description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|