@team-agent/installer 0.1.10 → 0.2.0
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/crates/team-agent-core/src/lib.rs +50 -5
- package/package.json +1 -1
- package/schemas/team.schema.json +1 -0
- package/skills/team-agent/SKILL.md +1 -1
- package/src/team_agent/approvals/__init__.py +65 -0
- package/src/team_agent/approvals/constants.py +6 -0
- package/src/team_agent/approvals/parsing.py +176 -0
- package/src/team_agent/approvals/runtime_prompts.py +171 -0
- package/src/team_agent/approvals/status.py +165 -0
- package/src/team_agent/cli/__init__.py +135 -0
- package/src/team_agent/cli/commands.py +335 -0
- package/src/team_agent/cli/e2e.py +202 -0
- package/src/team_agent/cli/helpers.py +137 -0
- package/src/team_agent/cli/parser.py +470 -0
- package/src/team_agent/compiler.py +98 -33
- package/src/team_agent/coordinator/__init__.py +53 -0
- package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
- package/src/team_agent/coordinator/lifecycle.py +319 -0
- package/src/team_agent/coordinator/metadata.py +61 -0
- package/src/team_agent/coordinator/paths.py +17 -0
- package/src/team_agent/diagnose/__init__.py +48 -0
- package/src/team_agent/diagnose/checks.py +101 -0
- package/src/team_agent/diagnose/health.py +241 -0
- package/src/team_agent/diagnose/preflight.py +194 -0
- package/src/team_agent/diagnose/quick_start.py +233 -0
- package/src/team_agent/display/__init__.py +61 -0
- package/src/team_agent/display/close.py +147 -0
- package/src/team_agent/display/ghostty.py +77 -0
- package/src/team_agent/display/worker_window.py +110 -0
- package/src/team_agent/display/workspace.py +473 -0
- package/src/team_agent/launch/__init__.py +41 -0
- package/src/team_agent/launch/bootstrap.py +85 -0
- package/src/team_agent/launch/config.py +106 -0
- package/src/team_agent/launch/core.py +291 -0
- package/src/team_agent/launch/requirements.py +57 -0
- package/src/team_agent/leader/__init__.py +320 -0
- package/src/team_agent/lifecycle/__init__.py +5 -0
- package/src/team_agent/lifecycle/agents.py +226 -0
- package/src/team_agent/lifecycle/operations.py +321 -0
- package/src/team_agent/lifecycle/start.py +360 -0
- package/src/team_agent/mcp_server/__init__.py +42 -0
- package/src/team_agent/mcp_server/__main__.py +7 -0
- package/src/team_agent/mcp_server/contracts.py +148 -0
- package/src/team_agent/mcp_server/normalize.py +257 -0
- package/src/team_agent/mcp_server/server.py +150 -0
- package/src/team_agent/mcp_server/tools.py +205 -0
- package/src/team_agent/message_store/__init__.py +23 -0
- package/src/team_agent/message_store/agent_health.py +109 -0
- package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
- package/src/team_agent/message_store/result_watchers.py +102 -0
- package/src/team_agent/message_store/schema.py +266 -0
- package/src/team_agent/messaging/__init__.py +1 -0
- package/src/team_agent/messaging/activity_detector.py +190 -0
- package/src/team_agent/messaging/delivery.py +128 -0
- package/src/team_agent/messaging/deps.py +263 -0
- package/src/team_agent/messaging/idle_alerts.py +217 -0
- package/src/team_agent/messaging/internal_delivery.py +46 -0
- package/src/team_agent/messaging/leader.py +317 -0
- package/src/team_agent/messaging/leader_panes.py +343 -0
- package/src/team_agent/messaging/result_delivery.py +300 -0
- package/src/team_agent/messaging/results.py +456 -0
- package/src/team_agent/messaging/scheduler.py +418 -0
- package/src/team_agent/messaging/send.py +493 -0
- package/src/team_agent/messaging/tmux_io.py +337 -0
- package/src/team_agent/messaging/tmux_prompt.py +229 -0
- package/src/team_agent/orchestrator/__init__.py +376 -0
- package/src/team_agent/orchestrator/plan.py +122 -0
- package/src/team_agent/orchestrator/state.py +128 -0
- package/src/team_agent/profiles/__init__.py +82 -0
- package/src/team_agent/profiles/constants.py +19 -0
- package/src/team_agent/profiles/core.py +407 -0
- package/src/team_agent/profiles/helpers.py +69 -0
- package/src/team_agent/profiles/provider_env.py +188 -0
- package/src/team_agent/profiles/smoke.py +201 -0
- package/src/team_agent/provider_cli/__init__.py +43 -0
- package/src/team_agent/provider_cli/adapter.py +167 -0
- package/src/team_agent/provider_cli/base.py +48 -0
- package/src/team_agent/provider_cli/claude.py +457 -0
- package/src/team_agent/provider_cli/codex.py +319 -0
- package/src/team_agent/provider_cli/copilot.py +8 -0
- package/src/team_agent/provider_cli/fake.py +39 -0
- package/src/team_agent/provider_cli/gemini.py +95 -0
- package/src/team_agent/provider_cli/opencode.py +8 -0
- package/src/team_agent/provider_cli/prompt.py +62 -0
- package/src/team_agent/provider_cli/registry.py +18 -0
- package/src/team_agent/provider_cli/unsupported.py +32 -0
- package/src/team_agent/providers.py +67 -949
- package/src/team_agent/quality_gates.py +104 -0
- package/src/team_agent/restart/__init__.py +34 -0
- package/src/team_agent/restart/orchestration.py +328 -0
- package/src/team_agent/restart/selection.py +89 -0
- package/src/team_agent/restart/snapshot.py +70 -0
- package/src/team_agent/runtime.py +802 -5740
- package/src/team_agent/rust_core.py +22 -5
- package/src/team_agent/sessions/__init__.py +25 -0
- package/src/team_agent/sessions/capture.py +93 -0
- package/src/team_agent/sessions/inventory.py +44 -0
- package/src/team_agent/sessions/resume.py +135 -0
- package/src/team_agent/spec.py +3 -1
- package/src/team_agent/state.py +204 -4
- package/src/team_agent/status/__init__.py +63 -0
- package/src/team_agent/status/approvals.py +52 -0
- package/src/team_agent/status/compact.py +158 -0
- package/src/team_agent/status/constants.py +18 -0
- package/src/team_agent/status/inbox.py +28 -0
- package/src/team_agent/status/peek.py +117 -0
- package/src/team_agent/status/queries.py +168 -0
- package/src/team_agent/terminal.py +57 -0
- package/src/team_agent/cli.py +0 -857
- package/src/team_agent/mcp_server.py +0 -579
- package/src/team_agent/profiles.py +0 -882
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from team_agent import runtime
|
|
4
|
+
from team_agent.mcp_server.contracts import TOOLS
|
|
5
|
+
from team_agent.mcp_server.normalize import (
|
|
6
|
+
_compact_tool_result,
|
|
7
|
+
_normalize_report_envelope,
|
|
8
|
+
_items,
|
|
9
|
+
_text,
|
|
10
|
+
_first_text,
|
|
11
|
+
_normalize_result_status,
|
|
12
|
+
_normalize_changes,
|
|
13
|
+
_normalize_change_kind,
|
|
14
|
+
_normalize_tests,
|
|
15
|
+
_normalize_test_status,
|
|
16
|
+
_normalize_risks,
|
|
17
|
+
_normalize_artifacts,
|
|
18
|
+
_normalize_next_actions,
|
|
19
|
+
)
|
|
20
|
+
from team_agent.mcp_server.server import dispatch, handle_mcp, main
|
|
21
|
+
from team_agent.mcp_server.tools import TeamOrchestratorTools
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
'TeamOrchestratorTools',
|
|
25
|
+
'TOOLS',
|
|
26
|
+
'dispatch',
|
|
27
|
+
'handle_mcp',
|
|
28
|
+
'main',
|
|
29
|
+
'_compact_tool_result',
|
|
30
|
+
'_normalize_report_envelope',
|
|
31
|
+
'_items',
|
|
32
|
+
'_text',
|
|
33
|
+
'_first_text',
|
|
34
|
+
'_normalize_result_status',
|
|
35
|
+
'_normalize_changes',
|
|
36
|
+
'_normalize_change_kind',
|
|
37
|
+
'_normalize_tests',
|
|
38
|
+
'_normalize_test_status',
|
|
39
|
+
'_normalize_risks',
|
|
40
|
+
'_normalize_artifacts',
|
|
41
|
+
'_normalize_next_actions',
|
|
42
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
TOOLS = [
|
|
5
|
+
{
|
|
6
|
+
"name": "assign_task",
|
|
7
|
+
"description": "Add or update a task in the team graph and deliver it to its assignee.",
|
|
8
|
+
"inputSchema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"required": ["task"],
|
|
11
|
+
"properties": {
|
|
12
|
+
"task": {"type": "object"},
|
|
13
|
+
"message": {"type": "string"},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "send_message",
|
|
19
|
+
"description": "Send a message to a teammate, the leader, or '*' for all other team members. Provide only target and content; Team Agent fills sender, task id, ack policy, and delivery metadata.",
|
|
20
|
+
"inputSchema": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"required": ["to", "content"],
|
|
23
|
+
"properties": {
|
|
24
|
+
"to": {
|
|
25
|
+
"oneOf": [
|
|
26
|
+
{"type": "string"},
|
|
27
|
+
{"type": "array", "items": {"type": "string"}, "minItems": 1},
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"content": {"type": "string"},
|
|
31
|
+
},
|
|
32
|
+
"additionalProperties": False,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "report_result",
|
|
37
|
+
"description": "Report task completion. Provide a short summary and optional status/details; Team Agent fills schema_version, task_id, and agent_id, and normalizes common change/test field aliases.",
|
|
38
|
+
"inputSchema": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"required": ["summary"],
|
|
41
|
+
"properties": {
|
|
42
|
+
"summary": {"type": "string"},
|
|
43
|
+
"status": {"type": "string", "enum": ["success", "blocked", "failed", "partial"]},
|
|
44
|
+
"changes": {"type": "array", "items": {"type": "object"}},
|
|
45
|
+
"tests": {"type": "array", "items": {"type": "object"}},
|
|
46
|
+
"risks": {"type": "array", "items": {"type": "object"}},
|
|
47
|
+
"artifacts": {"type": "array", "items": {"type": "object"}},
|
|
48
|
+
"next_actions": {"type": "array", "items": {"type": "object"}},
|
|
49
|
+
},
|
|
50
|
+
"additionalProperties": False,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "update_state",
|
|
55
|
+
"description": "Append a note to team state and rewrite team_state.md.",
|
|
56
|
+
"inputSchema": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"required": ["note"],
|
|
59
|
+
"properties": {"note": {"type": "string"}},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "get_team_status",
|
|
64
|
+
"description": "Return machine-readable team status.",
|
|
65
|
+
"inputSchema": {"type": "object", "properties": {}},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "stop_agent",
|
|
69
|
+
"description": "Hard-stop one running worker while preserving its session for start_agent resume.",
|
|
70
|
+
"inputSchema": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"required": ["agent_id"],
|
|
73
|
+
"properties": {"agent_id": {"type": "string"}},
|
|
74
|
+
"additionalProperties": False,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "reset_agent",
|
|
79
|
+
"description": "Reset one worker to a fresh session. discard_session must be true.",
|
|
80
|
+
"inputSchema": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"required": ["agent_id", "discard_session"],
|
|
83
|
+
"properties": {
|
|
84
|
+
"agent_id": {"type": "string"},
|
|
85
|
+
"discard_session": {"type": "boolean"},
|
|
86
|
+
},
|
|
87
|
+
"additionalProperties": False,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "add_agent",
|
|
92
|
+
"description": "Add a first-class worker from a workspace-relative role file.",
|
|
93
|
+
"inputSchema": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"required": ["new_agent_id", "role_file_path"],
|
|
96
|
+
"properties": {
|
|
97
|
+
"new_agent_id": {"type": "string"},
|
|
98
|
+
"role_file_path": {"type": "string"},
|
|
99
|
+
},
|
|
100
|
+
"additionalProperties": False,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "fork_agent",
|
|
105
|
+
"description": "Fork a running worker using the provider's native branch/fork support.",
|
|
106
|
+
"inputSchema": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"required": ["source_agent_id", "as_agent_id"],
|
|
109
|
+
"properties": {
|
|
110
|
+
"source_agent_id": {"type": "string"},
|
|
111
|
+
"as_agent_id": {"type": "string"},
|
|
112
|
+
"label": {"type": "string"},
|
|
113
|
+
},
|
|
114
|
+
"additionalProperties": False,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "request_human",
|
|
119
|
+
"description": "Ask the leader/user for human input.",
|
|
120
|
+
"inputSchema": {
|
|
121
|
+
"type": "object",
|
|
122
|
+
"required": ["question"],
|
|
123
|
+
"properties": {
|
|
124
|
+
"question": {"type": "string"},
|
|
125
|
+
"task_id": {"type": "string"},
|
|
126
|
+
"agent_id": {"type": "string"},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "stuck_list",
|
|
132
|
+
"description": "List manually suppressed idle-triggered alerts for this team.",
|
|
133
|
+
"inputSchema": {"type": "object", "properties": {}, "additionalProperties": False},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "stuck_cancel",
|
|
137
|
+
"description": "Suppress repeated stuck/idle alerts for one agent until meaningful agent state changes.",
|
|
138
|
+
"inputSchema": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"required": ["agent_id"],
|
|
141
|
+
"properties": {
|
|
142
|
+
"agent_id": {"type": "string"},
|
|
143
|
+
"alert_type": {"type": "string", "enum": ["stuck", "idle_fallback", "all"]},
|
|
144
|
+
},
|
|
145
|
+
"additionalProperties": False,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
]
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _compact_tool_result(result: dict[str, Any]) -> dict[str, Any]:
|
|
7
|
+
if result.get("ok") is False:
|
|
8
|
+
keys = [
|
|
9
|
+
"ok",
|
|
10
|
+
"status",
|
|
11
|
+
"reason",
|
|
12
|
+
"error",
|
|
13
|
+
"message_id",
|
|
14
|
+
"agent_id",
|
|
15
|
+
"new_agent_id",
|
|
16
|
+
"source_agent_id",
|
|
17
|
+
"role_file_sha",
|
|
18
|
+
"session_id",
|
|
19
|
+
"to",
|
|
20
|
+
"targets",
|
|
21
|
+
"delivered_count",
|
|
22
|
+
"failed_count",
|
|
23
|
+
"fallback_path",
|
|
24
|
+
"suggestion",
|
|
25
|
+
]
|
|
26
|
+
compact = {key: result[key] for key in keys if key in result}
|
|
27
|
+
if str(result.get("status") or "").startswith("fanout_"):
|
|
28
|
+
for key in ("deliveries", "recipients"):
|
|
29
|
+
if key in result:
|
|
30
|
+
compact[key] = result[key]
|
|
31
|
+
return compact
|
|
32
|
+
keys = [
|
|
33
|
+
"ok",
|
|
34
|
+
"status",
|
|
35
|
+
"message_id",
|
|
36
|
+
"to",
|
|
37
|
+
"targets",
|
|
38
|
+
"delivered_count",
|
|
39
|
+
"failed_count",
|
|
40
|
+
"submitted",
|
|
41
|
+
"visible",
|
|
42
|
+
"queued",
|
|
43
|
+
"durably_stored",
|
|
44
|
+
"result_id",
|
|
45
|
+
"task_id",
|
|
46
|
+
"agent_id",
|
|
47
|
+
"new_agent_id",
|
|
48
|
+
"source_agent_id",
|
|
49
|
+
"role_file_sha",
|
|
50
|
+
"session_id",
|
|
51
|
+
"leader_notified",
|
|
52
|
+
"notification_message_id",
|
|
53
|
+
"notification_status",
|
|
54
|
+
"notification_channel",
|
|
55
|
+
"notification_event_id",
|
|
56
|
+
]
|
|
57
|
+
compact = {key: result[key] for key in keys if key in result}
|
|
58
|
+
if str(result.get("status") or "").startswith("fanout_"):
|
|
59
|
+
for key in ("deliveries", "recipients"):
|
|
60
|
+
if key in result:
|
|
61
|
+
compact[key] = result[key]
|
|
62
|
+
if "acknowledged_messages" in result:
|
|
63
|
+
compact["acknowledged_count"] = len(result.get("acknowledged_messages") or [])
|
|
64
|
+
return compact or {"ok": True}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _normalize_report_envelope(env: dict[str, Any]) -> dict[str, Any]:
|
|
68
|
+
summary = _text(env.get("summary")) or "completed"
|
|
69
|
+
return {
|
|
70
|
+
"schema_version": "result_envelope_v1",
|
|
71
|
+
"task_id": _text(env.get("task_id")) or "manual",
|
|
72
|
+
"agent_id": _text(env.get("agent_id")) or "unknown",
|
|
73
|
+
"status": _normalize_result_status(env.get("status")),
|
|
74
|
+
"summary": summary,
|
|
75
|
+
"changes": _normalize_changes(env.get("changes"), summary),
|
|
76
|
+
"tests": _normalize_tests(env.get("tests")),
|
|
77
|
+
"risks": _normalize_risks(env.get("risks")),
|
|
78
|
+
"artifacts": _normalize_artifacts(env.get("artifacts")),
|
|
79
|
+
"next_actions": _normalize_next_actions(env.get("next_actions")),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _items(value: Any) -> list[Any]:
|
|
84
|
+
if value is None:
|
|
85
|
+
return []
|
|
86
|
+
if isinstance(value, list):
|
|
87
|
+
return value
|
|
88
|
+
return [value]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _text(value: Any) -> str | None:
|
|
92
|
+
if value is None:
|
|
93
|
+
return None
|
|
94
|
+
text = str(value).strip()
|
|
95
|
+
return text or None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _first_text(item: dict[str, Any], *keys: str) -> str | None:
|
|
99
|
+
for key in keys:
|
|
100
|
+
text = _text(item.get(key))
|
|
101
|
+
if text:
|
|
102
|
+
return text
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _normalize_result_status(value: Any) -> str:
|
|
107
|
+
text = (_text(value) or "success").lower().replace("-", "_").replace(" ", "_")
|
|
108
|
+
mapping = {
|
|
109
|
+
"ok": "success",
|
|
110
|
+
"done": "success",
|
|
111
|
+
"complete": "success",
|
|
112
|
+
"completed": "success",
|
|
113
|
+
"passed": "success",
|
|
114
|
+
"pass": "success",
|
|
115
|
+
"blocked": "blocked",
|
|
116
|
+
"block": "blocked",
|
|
117
|
+
"failed": "failed",
|
|
118
|
+
"fail": "failed",
|
|
119
|
+
"error": "failed",
|
|
120
|
+
"partial": "partial",
|
|
121
|
+
"partially_done": "partial",
|
|
122
|
+
}
|
|
123
|
+
return mapping.get(text, text if text in {"success", "blocked", "failed", "partial"} else "success")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _normalize_changes(value: Any, fallback_summary: str) -> list[dict[str, str]]:
|
|
127
|
+
changes: list[dict[str, str]] = []
|
|
128
|
+
for item in _items(value):
|
|
129
|
+
if not isinstance(item, dict):
|
|
130
|
+
continue
|
|
131
|
+
path = _first_text(item, "path", "file", "filepath", "filename")
|
|
132
|
+
if not path:
|
|
133
|
+
continue
|
|
134
|
+
description = _first_text(item, "description", "summary", "detail", "details", "message") or fallback_summary
|
|
135
|
+
changes.append(
|
|
136
|
+
{
|
|
137
|
+
"path": path,
|
|
138
|
+
"kind": _normalize_change_kind(_first_text(item, "kind", "type", "action"), description),
|
|
139
|
+
"description": description,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
return changes
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _normalize_change_kind(value: str | None, description: str) -> str:
|
|
146
|
+
text = (value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
|
147
|
+
if text in {"created", "modified", "deleted", "observed"}:
|
|
148
|
+
return text
|
|
149
|
+
mapping = {
|
|
150
|
+
"create": "created",
|
|
151
|
+
"added": "created",
|
|
152
|
+
"add": "created",
|
|
153
|
+
"new": "created",
|
|
154
|
+
"changed": "modified",
|
|
155
|
+
"change": "modified",
|
|
156
|
+
"updated": "modified",
|
|
157
|
+
"update": "modified",
|
|
158
|
+
"edited": "modified",
|
|
159
|
+
"edit": "modified",
|
|
160
|
+
"removed": "deleted",
|
|
161
|
+
"remove": "deleted",
|
|
162
|
+
"delete": "deleted",
|
|
163
|
+
"observe": "observed",
|
|
164
|
+
"observed": "observed",
|
|
165
|
+
"inspected": "observed",
|
|
166
|
+
"inspect": "observed",
|
|
167
|
+
}
|
|
168
|
+
if text in mapping:
|
|
169
|
+
return mapping[text]
|
|
170
|
+
description_text = description.lower()
|
|
171
|
+
if any(word in description_text for word in ["created", "added", "new file"]):
|
|
172
|
+
return "created"
|
|
173
|
+
if any(word in description_text for word in ["deleted", "removed"]):
|
|
174
|
+
return "deleted"
|
|
175
|
+
if any(word in description_text for word in ["observed", "inspected", "verified"]):
|
|
176
|
+
return "observed"
|
|
177
|
+
return "modified"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _normalize_tests(value: Any) -> list[dict[str, str]]:
|
|
181
|
+
tests: list[dict[str, str]] = []
|
|
182
|
+
for item in _items(value):
|
|
183
|
+
if not isinstance(item, dict):
|
|
184
|
+
command = _text(item)
|
|
185
|
+
if command:
|
|
186
|
+
tests.append({"command": command, "status": "not_run"})
|
|
187
|
+
continue
|
|
188
|
+
command = _first_text(item, "command", "cmd", "name", "test")
|
|
189
|
+
if not command:
|
|
190
|
+
continue
|
|
191
|
+
test = {"command": command, "status": _normalize_test_status(item.get("status"))}
|
|
192
|
+
detail = _first_text(item, "detail", "output", "stdout", "stderr", "summary", "message")
|
|
193
|
+
if detail:
|
|
194
|
+
test["detail"] = detail
|
|
195
|
+
tests.append(test)
|
|
196
|
+
return tests
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _normalize_test_status(value: Any) -> str:
|
|
200
|
+
text = (_text(value) or "not_run").lower().replace("-", "_").replace(" ", "_")
|
|
201
|
+
mapping = {
|
|
202
|
+
"pass": "passed",
|
|
203
|
+
"ok": "passed",
|
|
204
|
+
"success": "passed",
|
|
205
|
+
"fail": "failed",
|
|
206
|
+
"error": "failed",
|
|
207
|
+
"notrun": "not_run",
|
|
208
|
+
"not_run": "not_run",
|
|
209
|
+
"notrun_yet": "not_run",
|
|
210
|
+
"skip": "skipped",
|
|
211
|
+
}
|
|
212
|
+
return mapping.get(text, text if text in {"passed", "failed", "not_run", "skipped"} else "not_run")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _normalize_risks(value: Any) -> list[dict[str, str]]:
|
|
216
|
+
risks: list[dict[str, str]] = []
|
|
217
|
+
for item in _items(value):
|
|
218
|
+
if not isinstance(item, dict):
|
|
219
|
+
description = _text(item)
|
|
220
|
+
if description:
|
|
221
|
+
risks.append({"severity": "low", "description": description})
|
|
222
|
+
continue
|
|
223
|
+
description = _first_text(item, "description", "summary", "detail", "message")
|
|
224
|
+
if not description:
|
|
225
|
+
continue
|
|
226
|
+
severity = (_first_text(item, "severity", "level") or "low").lower()
|
|
227
|
+
if severity not in {"low", "medium", "high"}:
|
|
228
|
+
severity = "low"
|
|
229
|
+
risks.append({"severity": severity, "description": description})
|
|
230
|
+
return risks
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _normalize_artifacts(value: Any) -> list[dict[str, str]]:
|
|
234
|
+
artifacts: list[dict[str, str]] = []
|
|
235
|
+
for item in _items(value):
|
|
236
|
+
if not isinstance(item, dict):
|
|
237
|
+
path = _text(item)
|
|
238
|
+
if path:
|
|
239
|
+
artifacts.append({"path": path, "description": path})
|
|
240
|
+
continue
|
|
241
|
+
path = _first_text(item, "path", "file", "filepath", "filename")
|
|
242
|
+
if not path:
|
|
243
|
+
continue
|
|
244
|
+
artifacts.append({"path": path, "description": _first_text(item, "description", "summary", "detail") or path})
|
|
245
|
+
return artifacts
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _normalize_next_actions(value: Any) -> list[dict[str, str]]:
|
|
249
|
+
actions: list[dict[str, str]] = []
|
|
250
|
+
for item in _items(value):
|
|
251
|
+
if isinstance(item, dict):
|
|
252
|
+
description = _first_text(item, "description", "summary", "action", "todo", "message")
|
|
253
|
+
else:
|
|
254
|
+
description = _text(item)
|
|
255
|
+
if description:
|
|
256
|
+
actions.append({"description": description})
|
|
257
|
+
return actions
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from team_agent.mcp_server.contracts import TOOLS
|
|
10
|
+
from team_agent.mcp_server.tools import TeamOrchestratorTools
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
ARGUMENT_ERROR_TYPES = (TypeError, ValueError, KeyError, AttributeError)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def dispatch(tools: TeamOrchestratorTools, request: dict[str, Any]) -> dict[str, Any]:
|
|
17
|
+
tool = request.get("tool") or request.get("method")
|
|
18
|
+
args = request.get("arguments") or request.get("params") or {}
|
|
19
|
+
if tool == "assign_task":
|
|
20
|
+
return tools.assign_task(**args)
|
|
21
|
+
if tool == "send_message":
|
|
22
|
+
return tools.send_message(**args)
|
|
23
|
+
if tool == "report_result":
|
|
24
|
+
return tools.report_result(**args)
|
|
25
|
+
if tool == "update_state":
|
|
26
|
+
return tools.update_state(**args)
|
|
27
|
+
if tool == "get_team_status":
|
|
28
|
+
return tools.get_team_status()
|
|
29
|
+
if tool == "stop_agent":
|
|
30
|
+
return tools.stop_agent(**args)
|
|
31
|
+
if tool == "reset_agent":
|
|
32
|
+
return tools.reset_agent(**args)
|
|
33
|
+
if tool == "add_agent":
|
|
34
|
+
return tools.add_agent(**args)
|
|
35
|
+
if tool == "fork_agent":
|
|
36
|
+
return tools.fork_agent(**args)
|
|
37
|
+
if tool == "request_human":
|
|
38
|
+
return tools.request_human(**args)
|
|
39
|
+
if tool == "stuck_list":
|
|
40
|
+
return tools.stuck_list()
|
|
41
|
+
if tool == "stuck_cancel":
|
|
42
|
+
return tools.stuck_cancel(**args)
|
|
43
|
+
return _tool_error_result("unknown_tool", f"unknown tool {tool!r}", exc_type="UnknownTool")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def handle_mcp(tools: TeamOrchestratorTools, request: dict[str, Any]) -> dict[str, Any] | None:
|
|
47
|
+
method = request.get("method")
|
|
48
|
+
msg_id = request.get("id")
|
|
49
|
+
if method and method.startswith("notifications/"):
|
|
50
|
+
return None
|
|
51
|
+
if method == "initialize":
|
|
52
|
+
return {
|
|
53
|
+
"jsonrpc": "2.0",
|
|
54
|
+
"id": msg_id,
|
|
55
|
+
"result": {
|
|
56
|
+
"protocolVersion": request.get("params", {}).get("protocolVersion", "2024-11-05"),
|
|
57
|
+
"capabilities": {"tools": {}},
|
|
58
|
+
"serverInfo": {"name": "team_orchestrator", "version": "0.1.4"},
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
if method == "tools/list":
|
|
62
|
+
return {"jsonrpc": "2.0", "id": msg_id, "result": {"tools": TOOLS}}
|
|
63
|
+
if method == "tools/call":
|
|
64
|
+
params = request.get("params", {})
|
|
65
|
+
name = params.get("name")
|
|
66
|
+
arguments = params.get("arguments") or {}
|
|
67
|
+
try:
|
|
68
|
+
result = dispatch(tools, {"tool": name, "arguments": arguments})
|
|
69
|
+
except ARGUMENT_ERROR_TYPES as exc:
|
|
70
|
+
result = _tool_exception_result(exc, "invalid_tool_arguments")
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
result = _tool_exception_result(exc, "internal_runtime_error")
|
|
73
|
+
is_error = result.get("ok") is False
|
|
74
|
+
return {
|
|
75
|
+
"jsonrpc": "2.0",
|
|
76
|
+
"id": msg_id,
|
|
77
|
+
"result": {
|
|
78
|
+
"content": [
|
|
79
|
+
{
|
|
80
|
+
"type": "text",
|
|
81
|
+
"text": json.dumps(result, ensure_ascii=False),
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"isError": is_error,
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
"jsonrpc": "2.0",
|
|
89
|
+
"id": msg_id,
|
|
90
|
+
"error": {"code": -32601, "message": f"unknown method {method!r}"},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _tool_exception_result(exc: Exception, reason: str) -> dict[str, str | bool]:
|
|
95
|
+
return _tool_error_result(reason, _public_exception_message(exc), exc_type=type(exc).__name__)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _tool_error_result(reason: str, message: str, exc_type: str) -> dict[str, str | bool]:
|
|
99
|
+
return {
|
|
100
|
+
"ok": False,
|
|
101
|
+
"reason": reason,
|
|
102
|
+
"error_code": reason,
|
|
103
|
+
"exc_type": exc_type,
|
|
104
|
+
"message": message,
|
|
105
|
+
"error": message,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _public_exception_message(exc: Exception) -> str:
|
|
110
|
+
message = str(exc).replace("\n", " ").strip()
|
|
111
|
+
return message[:200] if message else type(exc).__name__
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main(argv: list[str] | None = None) -> None:
|
|
115
|
+
parser = argparse.ArgumentParser(description="TeamSpec team_orchestrator MCP stdio server")
|
|
116
|
+
parser.add_argument("--workspace", default=".", help="Workspace containing .team/runtime")
|
|
117
|
+
args = parser.parse_args(argv)
|
|
118
|
+
tools = TeamOrchestratorTools(Path(args.workspace))
|
|
119
|
+
for line in sys.stdin:
|
|
120
|
+
line = line.strip()
|
|
121
|
+
if not line:
|
|
122
|
+
continue
|
|
123
|
+
try:
|
|
124
|
+
request = json.loads(line)
|
|
125
|
+
if request.get("jsonrpc") == "2.0":
|
|
126
|
+
response = handle_mcp(tools, request)
|
|
127
|
+
if response is None:
|
|
128
|
+
continue
|
|
129
|
+
sys.stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
|
|
130
|
+
sys.stdout.flush()
|
|
131
|
+
continue
|
|
132
|
+
result = dispatch(tools, request)
|
|
133
|
+
sys.stdout.write(json.dumps({"ok": result.get("ok", True), "result": result}, ensure_ascii=False) + "\n")
|
|
134
|
+
sys.stdout.flush()
|
|
135
|
+
except Exception as exc: # MCP transports need errors surfaced on stdout.
|
|
136
|
+
if "request" in locals() and isinstance(request, dict) and request.get("jsonrpc") == "2.0":
|
|
137
|
+
sys.stdout.write(
|
|
138
|
+
json.dumps(
|
|
139
|
+
{
|
|
140
|
+
"jsonrpc": "2.0",
|
|
141
|
+
"id": request.get("id"),
|
|
142
|
+
"error": {"code": -32000, "message": str(exc)},
|
|
143
|
+
},
|
|
144
|
+
ensure_ascii=False,
|
|
145
|
+
)
|
|
146
|
+
+ "\n"
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
sys.stdout.write(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) + "\n")
|
|
150
|
+
sys.stdout.flush()
|