claude-mpm 5.4.96__py3-none-any.whl → 5.6.10__py3-none-any.whl
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.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
- claude_mpm/agents/PM_INSTRUCTIONS.md +44 -10
- claude_mpm/agents/WORKFLOW.md +2 -0
- claude_mpm/agents/templates/circuit-breakers.md +26 -17
- claude_mpm/cli/commands/autotodos.py +45 -5
- claude_mpm/cli/commands/commander.py +46 -0
- claude_mpm/cli/commands/hook_errors.py +60 -60
- claude_mpm/cli/commands/run.py +35 -3
- claude_mpm/cli/commands/skill_source.py +51 -2
- claude_mpm/cli/commands/skills.py +5 -3
- claude_mpm/cli/executor.py +32 -17
- claude_mpm/cli/parsers/base_parser.py +17 -0
- claude_mpm/cli/parsers/commander_parser.py +83 -0
- claude_mpm/cli/parsers/run_parser.py +10 -0
- claude_mpm/cli/parsers/skill_source_parser.py +4 -0
- claude_mpm/cli/parsers/skills_parser.py +5 -0
- claude_mpm/cli/startup.py +20 -2
- claude_mpm/cli/utils.py +7 -3
- claude_mpm/commander/__init__.py +72 -0
- claude_mpm/commander/adapters/__init__.py +31 -0
- claude_mpm/commander/adapters/base.py +191 -0
- claude_mpm/commander/adapters/claude_code.py +361 -0
- claude_mpm/commander/adapters/communication.py +366 -0
- claude_mpm/commander/api/__init__.py +16 -0
- claude_mpm/commander/api/app.py +105 -0
- claude_mpm/commander/api/errors.py +133 -0
- claude_mpm/commander/api/routes/__init__.py +8 -0
- claude_mpm/commander/api/routes/events.py +184 -0
- claude_mpm/commander/api/routes/inbox.py +171 -0
- claude_mpm/commander/api/routes/messages.py +148 -0
- claude_mpm/commander/api/routes/projects.py +271 -0
- claude_mpm/commander/api/routes/sessions.py +228 -0
- claude_mpm/commander/api/routes/work.py +260 -0
- claude_mpm/commander/api/schemas.py +182 -0
- claude_mpm/commander/chat/__init__.py +7 -0
- claude_mpm/commander/chat/cli.py +107 -0
- claude_mpm/commander/chat/commands.py +96 -0
- claude_mpm/commander/chat/repl.py +310 -0
- claude_mpm/commander/config.py +49 -0
- claude_mpm/commander/config_loader.py +115 -0
- claude_mpm/commander/daemon.py +398 -0
- claude_mpm/commander/events/__init__.py +26 -0
- claude_mpm/commander/events/manager.py +332 -0
- claude_mpm/commander/frameworks/__init__.py +12 -0
- claude_mpm/commander/frameworks/base.py +143 -0
- claude_mpm/commander/frameworks/claude_code.py +58 -0
- claude_mpm/commander/frameworks/mpm.py +62 -0
- claude_mpm/commander/inbox/__init__.py +16 -0
- claude_mpm/commander/inbox/dedup.py +128 -0
- claude_mpm/commander/inbox/inbox.py +224 -0
- claude_mpm/commander/inbox/models.py +70 -0
- claude_mpm/commander/instance_manager.py +337 -0
- claude_mpm/commander/llm/__init__.py +6 -0
- claude_mpm/commander/llm/openrouter_client.py +167 -0
- claude_mpm/commander/llm/summarizer.py +70 -0
- claude_mpm/commander/models/__init__.py +18 -0
- claude_mpm/commander/models/events.py +121 -0
- claude_mpm/commander/models/project.py +162 -0
- claude_mpm/commander/models/work.py +214 -0
- claude_mpm/commander/parsing/__init__.py +20 -0
- claude_mpm/commander/parsing/extractor.py +132 -0
- claude_mpm/commander/parsing/output_parser.py +270 -0
- claude_mpm/commander/parsing/patterns.py +100 -0
- claude_mpm/commander/persistence/__init__.py +11 -0
- claude_mpm/commander/persistence/event_store.py +274 -0
- claude_mpm/commander/persistence/state_store.py +309 -0
- claude_mpm/commander/persistence/work_store.py +164 -0
- claude_mpm/commander/polling/__init__.py +13 -0
- claude_mpm/commander/polling/event_detector.py +104 -0
- claude_mpm/commander/polling/output_buffer.py +49 -0
- claude_mpm/commander/polling/output_poller.py +153 -0
- claude_mpm/commander/project_session.py +268 -0
- claude_mpm/commander/proxy/__init__.py +12 -0
- claude_mpm/commander/proxy/formatter.py +89 -0
- claude_mpm/commander/proxy/output_handler.py +191 -0
- claude_mpm/commander/proxy/relay.py +155 -0
- claude_mpm/commander/registry.py +404 -0
- claude_mpm/commander/runtime/__init__.py +10 -0
- claude_mpm/commander/runtime/executor.py +191 -0
- claude_mpm/commander/runtime/monitor.py +316 -0
- claude_mpm/commander/session/__init__.py +6 -0
- claude_mpm/commander/session/context.py +81 -0
- claude_mpm/commander/session/manager.py +59 -0
- claude_mpm/commander/tmux_orchestrator.py +361 -0
- claude_mpm/commander/web/__init__.py +1 -0
- claude_mpm/commander/work/__init__.py +30 -0
- claude_mpm/commander/work/executor.py +189 -0
- claude_mpm/commander/work/queue.py +405 -0
- claude_mpm/commander/workflow/__init__.py +27 -0
- claude_mpm/commander/workflow/event_handler.py +219 -0
- claude_mpm/commander/workflow/notifier.py +146 -0
- claude_mpm/commands/mpm-config.md +8 -0
- claude_mpm/commands/mpm-doctor.md +8 -0
- claude_mpm/commands/mpm-help.md +8 -0
- claude_mpm/commands/mpm-init.md +8 -0
- claude_mpm/commands/mpm-monitor.md +8 -0
- claude_mpm/commands/mpm-organize.md +8 -0
- claude_mpm/commands/mpm-postmortem.md +8 -0
- claude_mpm/commands/mpm-session-resume.md +8 -0
- claude_mpm/commands/mpm-status.md +8 -0
- claude_mpm/commands/mpm-ticket-view.md +8 -0
- claude_mpm/commands/mpm-version.md +8 -0
- claude_mpm/commands/mpm.md +8 -0
- claude_mpm/config/agent_presets.py +8 -7
- claude_mpm/config/skill_sources.py +16 -0
- claude_mpm/core/config.py +32 -19
- claude_mpm/core/logger.py +26 -9
- claude_mpm/core/logging_utils.py +35 -11
- claude_mpm/core/output_style_manager.py +15 -5
- claude_mpm/core/unified_config.py +10 -6
- claude_mpm/core/unified_paths.py +68 -80
- claude_mpm/experimental/cli_enhancements.py +2 -1
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +29 -30
- claude_mpm/hooks/claude_hooks/event_handlers.py +90 -99
- claude_mpm/hooks/claude_hooks/hook_handler.py +81 -88
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
- claude_mpm/hooks/claude_hooks/installer.py +116 -8
- claude_mpm/hooks/claude_hooks/memory_integration.py +51 -31
- claude_mpm/hooks/claude_hooks/response_tracking.py +39 -58
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +23 -28
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +36 -103
- claude_mpm/hooks/claude_hooks/services/state_manager.py +23 -36
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +47 -73
- claude_mpm/hooks/session_resume_hook.py +22 -18
- claude_mpm/hooks/templates/pre_tool_use_template.py +10 -2
- claude_mpm/scripts/claude-hook-handler.sh +43 -16
- claude_mpm/services/agents/agent_recommendation_service.py +8 -8
- claude_mpm/services/agents/agent_selection_service.py +2 -2
- claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
- claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
- claude_mpm/services/event_log.py +8 -0
- claude_mpm/services/pm_skills_deployer.py +84 -6
- claude_mpm/services/skills/git_skill_source_manager.py +130 -10
- claude_mpm/services/skills/selective_skill_deployer.py +28 -0
- claude_mpm/services/skills/skill_discovery_service.py +74 -4
- claude_mpm/services/skills_deployer.py +31 -5
- claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
- claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
- claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
- claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
- claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
- claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
- claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
- claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
- claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
- claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
- claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
- claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/METADATA +18 -4
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/RECORD +190 -79
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Work queue management endpoints for MPM Commander API.
|
|
2
|
+
|
|
3
|
+
This module implements REST endpoints for managing work items
|
|
4
|
+
in project work queues.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
10
|
+
|
|
11
|
+
from ...models.work import WorkPriority, WorkState
|
|
12
|
+
from ...work import WorkQueue
|
|
13
|
+
from ..errors import ProjectNotFoundError
|
|
14
|
+
from ..schemas import CreateWorkRequest, WorkItemResponse
|
|
15
|
+
|
|
16
|
+
router = APIRouter()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_registry():
|
|
20
|
+
"""Get registry instance from app global."""
|
|
21
|
+
from ..app import registry
|
|
22
|
+
|
|
23
|
+
if registry is None:
|
|
24
|
+
raise RuntimeError("Registry not initialized")
|
|
25
|
+
return registry
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _work_item_to_response(work_item) -> WorkItemResponse:
|
|
29
|
+
"""Convert WorkItem model to WorkItemResponse schema.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
work_item: WorkItem instance
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
WorkItemResponse with all work item data
|
|
36
|
+
"""
|
|
37
|
+
return WorkItemResponse(
|
|
38
|
+
id=work_item.id,
|
|
39
|
+
project_id=work_item.project_id,
|
|
40
|
+
content=work_item.content,
|
|
41
|
+
state=work_item.state.value,
|
|
42
|
+
priority=work_item.priority.value,
|
|
43
|
+
created_at=work_item.created_at,
|
|
44
|
+
started_at=work_item.started_at,
|
|
45
|
+
completed_at=work_item.completed_at,
|
|
46
|
+
result=work_item.result,
|
|
47
|
+
error=work_item.error,
|
|
48
|
+
depends_on=work_item.depends_on,
|
|
49
|
+
metadata=work_item.metadata,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.post("/projects/{project_id}/work", response_model=WorkItemResponse)
|
|
54
|
+
async def add_work(project_id: str, work: CreateWorkRequest) -> WorkItemResponse:
|
|
55
|
+
"""Add work item to project queue.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
project_id: Project identifier
|
|
59
|
+
work: Work item creation request
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Created work item
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
HTTPException: 404 if project not found
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
POST /api/projects/proj-123/work
|
|
69
|
+
Request: {
|
|
70
|
+
"content": "Implement OAuth2 authentication",
|
|
71
|
+
"priority": 3,
|
|
72
|
+
"depends_on": ["work-abc"]
|
|
73
|
+
}
|
|
74
|
+
Response: {
|
|
75
|
+
"id": "work-xyz",
|
|
76
|
+
"project_id": "proj-123",
|
|
77
|
+
"content": "Implement OAuth2 authentication",
|
|
78
|
+
"state": "queued",
|
|
79
|
+
"priority": 3,
|
|
80
|
+
...
|
|
81
|
+
}
|
|
82
|
+
"""
|
|
83
|
+
registry = _get_registry()
|
|
84
|
+
|
|
85
|
+
# Get project
|
|
86
|
+
try:
|
|
87
|
+
project = registry.get(project_id)
|
|
88
|
+
except ProjectNotFoundError as e:
|
|
89
|
+
raise HTTPException(status_code=404, detail=str(e)) from e
|
|
90
|
+
|
|
91
|
+
# Get or create work queue for project
|
|
92
|
+
# Note: In full implementation, this would be managed by ProjectSession
|
|
93
|
+
# For now, we'll need to integrate with project's work queue
|
|
94
|
+
# Access or create work queue
|
|
95
|
+
if not hasattr(project, "_work_queue"):
|
|
96
|
+
project._work_queue = WorkQueue(project_id)
|
|
97
|
+
|
|
98
|
+
queue = project._work_queue
|
|
99
|
+
|
|
100
|
+
# Convert priority int to enum
|
|
101
|
+
priority = WorkPriority(work.priority)
|
|
102
|
+
|
|
103
|
+
# Add work item
|
|
104
|
+
work_item = queue.add(
|
|
105
|
+
content=work.content, priority=priority, depends_on=work.depends_on
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return _work_item_to_response(work_item)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@router.get("/projects/{project_id}/work", response_model=List[WorkItemResponse])
|
|
112
|
+
async def list_work(
|
|
113
|
+
project_id: str, state: Optional[str] = Query(None, description="Filter by state")
|
|
114
|
+
) -> List[WorkItemResponse]:
|
|
115
|
+
"""List work items for project.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
project_id: Project identifier
|
|
119
|
+
state: Optional state filter (pending, queued, in_progress, etc.)
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
List of work items (may be empty)
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
HTTPException: 404 if project not found, 400 if invalid state
|
|
126
|
+
|
|
127
|
+
Example:
|
|
128
|
+
GET /api/projects/proj-123/work
|
|
129
|
+
Response: [
|
|
130
|
+
{"id": "work-1", "state": "queued", ...},
|
|
131
|
+
{"id": "work-2", "state": "in_progress", ...}
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
GET /api/projects/proj-123/work?state=queued
|
|
135
|
+
Response: [
|
|
136
|
+
{"id": "work-1", "state": "queued", ...}
|
|
137
|
+
]
|
|
138
|
+
"""
|
|
139
|
+
registry = _get_registry()
|
|
140
|
+
|
|
141
|
+
# Get project
|
|
142
|
+
try:
|
|
143
|
+
project = registry.get(project_id)
|
|
144
|
+
except ProjectNotFoundError as e:
|
|
145
|
+
raise HTTPException(status_code=404, detail=str(e)) from e
|
|
146
|
+
|
|
147
|
+
# Get work queue
|
|
148
|
+
if not hasattr(project, "_work_queue"):
|
|
149
|
+
project._work_queue = WorkQueue(project_id)
|
|
150
|
+
|
|
151
|
+
queue = project._work_queue
|
|
152
|
+
|
|
153
|
+
# Parse state filter
|
|
154
|
+
state_filter = None
|
|
155
|
+
if state:
|
|
156
|
+
try:
|
|
157
|
+
state_filter = WorkState(state)
|
|
158
|
+
except ValueError as e:
|
|
159
|
+
raise HTTPException(
|
|
160
|
+
status_code=400,
|
|
161
|
+
detail=f"Invalid state: {state}. "
|
|
162
|
+
f"Valid states: {[s.value for s in WorkState]}",
|
|
163
|
+
) from e
|
|
164
|
+
|
|
165
|
+
# List work items
|
|
166
|
+
items = queue.list(state=state_filter)
|
|
167
|
+
|
|
168
|
+
return [_work_item_to_response(item) for item in items]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@router.get("/projects/{project_id}/work/{work_id}", response_model=WorkItemResponse)
|
|
172
|
+
async def get_work(project_id: str, work_id: str) -> WorkItemResponse:
|
|
173
|
+
"""Get work item details.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
project_id: Project identifier
|
|
177
|
+
work_id: Work item identifier
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Work item details
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
HTTPException: 404 if project or work item not found
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
GET /api/projects/proj-123/work/work-xyz
|
|
187
|
+
Response: {
|
|
188
|
+
"id": "work-xyz",
|
|
189
|
+
"project_id": "proj-123",
|
|
190
|
+
"state": "in_progress",
|
|
191
|
+
...
|
|
192
|
+
}
|
|
193
|
+
"""
|
|
194
|
+
registry = _get_registry()
|
|
195
|
+
|
|
196
|
+
# Get project
|
|
197
|
+
try:
|
|
198
|
+
project = registry.get(project_id)
|
|
199
|
+
except ProjectNotFoundError as e:
|
|
200
|
+
raise HTTPException(status_code=404, detail=str(e)) from e
|
|
201
|
+
|
|
202
|
+
# Get work queue
|
|
203
|
+
if not hasattr(project, "_work_queue"):
|
|
204
|
+
raise HTTPException(status_code=404, detail="Work queue not found")
|
|
205
|
+
|
|
206
|
+
queue = project._work_queue
|
|
207
|
+
|
|
208
|
+
# Get work item
|
|
209
|
+
work_item = queue.get(work_id)
|
|
210
|
+
if not work_item:
|
|
211
|
+
raise HTTPException(status_code=404, detail=f"Work item {work_id} not found")
|
|
212
|
+
|
|
213
|
+
return _work_item_to_response(work_item)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@router.post("/projects/{project_id}/work/{work_id}/cancel")
|
|
217
|
+
async def cancel_work(project_id: str, work_id: str) -> dict:
|
|
218
|
+
"""Cancel pending work item.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
project_id: Project identifier
|
|
222
|
+
work_id: Work item identifier
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Success message
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
HTTPException: 404 if project/work not found, 400 if invalid state
|
|
229
|
+
|
|
230
|
+
Example:
|
|
231
|
+
POST /api/projects/proj-123/work/work-xyz/cancel
|
|
232
|
+
Response: {"status": "cancelled", "id": "work-xyz"}
|
|
233
|
+
"""
|
|
234
|
+
registry = _get_registry()
|
|
235
|
+
|
|
236
|
+
# Get project
|
|
237
|
+
try:
|
|
238
|
+
project = registry.get(project_id)
|
|
239
|
+
except ProjectNotFoundError as e:
|
|
240
|
+
raise HTTPException(status_code=404, detail=str(e)) from e
|
|
241
|
+
|
|
242
|
+
# Get work queue
|
|
243
|
+
if not hasattr(project, "_work_queue"):
|
|
244
|
+
raise HTTPException(status_code=404, detail="Work queue not found")
|
|
245
|
+
|
|
246
|
+
queue = project._work_queue
|
|
247
|
+
|
|
248
|
+
# Cancel work item
|
|
249
|
+
if not queue.cancel(work_id):
|
|
250
|
+
work_item = queue.get(work_id)
|
|
251
|
+
if not work_item:
|
|
252
|
+
raise HTTPException(
|
|
253
|
+
status_code=404, detail=f"Work item {work_id} not found"
|
|
254
|
+
)
|
|
255
|
+
raise HTTPException(
|
|
256
|
+
status_code=400,
|
|
257
|
+
detail=f"Cannot cancel work item in state {work_item.state.value}",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return {"status": "cancelled", "id": work_id}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Pydantic request/response schemas for MPM Commander API.
|
|
2
|
+
|
|
3
|
+
This module defines all request and response models for the REST API,
|
|
4
|
+
providing type safety and automatic validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
# Request Models
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RegisterProjectRequest(BaseModel):
|
|
16
|
+
"""Request to register a new project.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
path: Filesystem path to project directory
|
|
20
|
+
name: Optional display name (derived from path if omitted)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
path: str = Field(..., description="Filesystem path to project")
|
|
24
|
+
name: Optional[str] = Field(
|
|
25
|
+
None, description="Display name (derived from path if omitted)"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CreateSessionRequest(BaseModel):
|
|
30
|
+
"""Request to create a new tool session.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
runtime: Runtime adapter to use (e.g., "claude-code")
|
|
34
|
+
agent_prompt: Optional custom system prompt for the agent
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
runtime: str = Field("claude-code", description="Runtime adapter to use")
|
|
38
|
+
agent_prompt: Optional[str] = Field(None, description="Custom system prompt")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SendMessageRequest(BaseModel):
|
|
42
|
+
"""Request to send a message to a project.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
content: Message content
|
|
46
|
+
session_id: Target session ID (uses active session if omitted)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
content: str = Field(..., description="Message content")
|
|
50
|
+
session_id: Optional[str] = Field(
|
|
51
|
+
None, description="Target session (uses active if omitted)"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Response Models
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SessionResponse(BaseModel):
|
|
59
|
+
"""Session information response.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
id: Unique session identifier
|
|
63
|
+
project_id: Parent project ID
|
|
64
|
+
runtime: Runtime adapter name
|
|
65
|
+
tmux_target: Tmux pane target identifier
|
|
66
|
+
status: Current session status
|
|
67
|
+
created_at: Session creation timestamp
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
id: str
|
|
71
|
+
project_id: str
|
|
72
|
+
runtime: str
|
|
73
|
+
tmux_target: str
|
|
74
|
+
status: str
|
|
75
|
+
created_at: datetime
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ProjectResponse(BaseModel):
|
|
79
|
+
"""Project information response.
|
|
80
|
+
|
|
81
|
+
Attributes:
|
|
82
|
+
id: Unique project identifier
|
|
83
|
+
path: Absolute filesystem path
|
|
84
|
+
name: Display name
|
|
85
|
+
state: Current project state
|
|
86
|
+
state_reason: Optional state reason (e.g., error message)
|
|
87
|
+
sessions: List of active sessions
|
|
88
|
+
pending_events_count: Number of pending events
|
|
89
|
+
last_activity: Last activity timestamp
|
|
90
|
+
created_at: Project registration timestamp
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
id: str
|
|
94
|
+
path: str
|
|
95
|
+
name: str
|
|
96
|
+
state: str
|
|
97
|
+
state_reason: Optional[str]
|
|
98
|
+
sessions: List[SessionResponse]
|
|
99
|
+
pending_events_count: int
|
|
100
|
+
last_activity: datetime
|
|
101
|
+
created_at: datetime
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class MessageResponse(BaseModel):
|
|
105
|
+
"""Conversation message response.
|
|
106
|
+
|
|
107
|
+
Attributes:
|
|
108
|
+
id: Unique message identifier
|
|
109
|
+
role: Message sender role (user, assistant, system, tool)
|
|
110
|
+
content: Message content
|
|
111
|
+
session_id: Associated session ID (if from tool)
|
|
112
|
+
timestamp: Message creation timestamp
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
id: str
|
|
116
|
+
role: str
|
|
117
|
+
content: str
|
|
118
|
+
session_id: Optional[str]
|
|
119
|
+
timestamp: datetime
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class CreateWorkRequest(BaseModel):
|
|
123
|
+
"""Request to create a work item.
|
|
124
|
+
|
|
125
|
+
Attributes:
|
|
126
|
+
content: Task/message to execute
|
|
127
|
+
priority: Priority level (1-4, where 4 is CRITICAL)
|
|
128
|
+
depends_on: Optional list of work item IDs that must complete first
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
content: str = Field(..., description="Task/message to execute")
|
|
132
|
+
priority: int = Field(
|
|
133
|
+
2,
|
|
134
|
+
description="Priority level (1=LOW, 2=MEDIUM, 3=HIGH, 4=CRITICAL)",
|
|
135
|
+
ge=1,
|
|
136
|
+
le=4,
|
|
137
|
+
)
|
|
138
|
+
depends_on: Optional[List[str]] = Field(
|
|
139
|
+
None, description="Work item IDs that must complete first"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class WorkItemResponse(BaseModel):
|
|
144
|
+
"""Work item information response.
|
|
145
|
+
|
|
146
|
+
Attributes:
|
|
147
|
+
id: Unique work item identifier
|
|
148
|
+
project_id: Parent project ID
|
|
149
|
+
content: Task/message content
|
|
150
|
+
state: Current state (pending, queued, in_progress, blocked, completed, failed, cancelled)
|
|
151
|
+
priority: Priority level (1-4)
|
|
152
|
+
created_at: Creation timestamp
|
|
153
|
+
started_at: Execution start timestamp (if started)
|
|
154
|
+
completed_at: Completion timestamp (if completed/failed)
|
|
155
|
+
result: Result message (if completed)
|
|
156
|
+
error: Error message (if failed)
|
|
157
|
+
depends_on: List of dependency work item IDs
|
|
158
|
+
metadata: Additional structured data
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
id: str
|
|
162
|
+
project_id: str
|
|
163
|
+
content: str
|
|
164
|
+
state: str
|
|
165
|
+
priority: int
|
|
166
|
+
created_at: datetime
|
|
167
|
+
started_at: Optional[datetime]
|
|
168
|
+
completed_at: Optional[datetime]
|
|
169
|
+
result: Optional[str]
|
|
170
|
+
error: Optional[str]
|
|
171
|
+
depends_on: List[str]
|
|
172
|
+
metadata: dict
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ErrorResponse(BaseModel):
|
|
176
|
+
"""Error response with structured error information.
|
|
177
|
+
|
|
178
|
+
Attributes:
|
|
179
|
+
error: Error details with code and message
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
error: dict # {code: str, message: str}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Commander CLI entry point."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from claude_mpm.commander.instance_manager import InstanceManager
|
|
9
|
+
from claude_mpm.commander.llm.openrouter_client import (
|
|
10
|
+
OpenRouterClient,
|
|
11
|
+
OpenRouterConfig,
|
|
12
|
+
)
|
|
13
|
+
from claude_mpm.commander.llm.summarizer import OutputSummarizer
|
|
14
|
+
from claude_mpm.commander.proxy.formatter import OutputFormatter
|
|
15
|
+
from claude_mpm.commander.proxy.output_handler import OutputHandler
|
|
16
|
+
from claude_mpm.commander.proxy.relay import OutputRelay
|
|
17
|
+
from claude_mpm.commander.session.manager import SessionManager
|
|
18
|
+
from claude_mpm.commander.tmux_orchestrator import TmuxOrchestrator
|
|
19
|
+
|
|
20
|
+
from .repl import CommanderREPL
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def run_commander(
|
|
26
|
+
port: int = 8765,
|
|
27
|
+
state_dir: Optional[Path] = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Run Commander in interactive mode.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
port: Port for internal services (unused currently).
|
|
33
|
+
state_dir: Directory for state persistence (optional).
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> asyncio.run(run_commander())
|
|
37
|
+
# Starts interactive Commander REPL
|
|
38
|
+
"""
|
|
39
|
+
# Setup logging
|
|
40
|
+
logging.basicConfig(
|
|
41
|
+
level=logging.INFO,
|
|
42
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Initialize components
|
|
46
|
+
logger.info("Initializing Commander...")
|
|
47
|
+
|
|
48
|
+
# Create tmux orchestrator
|
|
49
|
+
orchestrator = TmuxOrchestrator()
|
|
50
|
+
|
|
51
|
+
# Create instance manager
|
|
52
|
+
instance_manager = InstanceManager(orchestrator)
|
|
53
|
+
|
|
54
|
+
# Create session manager
|
|
55
|
+
session_manager = SessionManager()
|
|
56
|
+
|
|
57
|
+
# Try to initialize LLM client (optional)
|
|
58
|
+
llm_client: Optional[OpenRouterClient] = None
|
|
59
|
+
try:
|
|
60
|
+
config = OpenRouterConfig()
|
|
61
|
+
llm_client = OpenRouterClient(config)
|
|
62
|
+
logger.info("LLM client initialized")
|
|
63
|
+
except ValueError as e:
|
|
64
|
+
logger.warning(f"LLM client not available: {e}")
|
|
65
|
+
logger.warning("Output summarization will be disabled")
|
|
66
|
+
|
|
67
|
+
# Create output relay (optional)
|
|
68
|
+
output_relay: Optional[OutputRelay] = None
|
|
69
|
+
if llm_client:
|
|
70
|
+
try:
|
|
71
|
+
summarizer = OutputSummarizer(llm_client)
|
|
72
|
+
handler = OutputHandler(orchestrator, summarizer)
|
|
73
|
+
formatter = OutputFormatter()
|
|
74
|
+
output_relay = OutputRelay(handler, formatter)
|
|
75
|
+
logger.info("Output relay initialized")
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.warning(f"Output relay setup failed: {e}")
|
|
78
|
+
|
|
79
|
+
# Create REPL
|
|
80
|
+
repl = CommanderREPL(
|
|
81
|
+
instance_manager=instance_manager,
|
|
82
|
+
session_manager=session_manager,
|
|
83
|
+
output_relay=output_relay,
|
|
84
|
+
llm_client=llm_client,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Run REPL
|
|
88
|
+
try:
|
|
89
|
+
await repl.run()
|
|
90
|
+
except KeyboardInterrupt:
|
|
91
|
+
logger.info("Commander interrupted by user")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(f"Commander error: {e}", exc_info=True)
|
|
94
|
+
finally:
|
|
95
|
+
# Cleanup
|
|
96
|
+
logger.info("Shutting down Commander...")
|
|
97
|
+
if output_relay:
|
|
98
|
+
await output_relay.stop_all()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main() -> None:
|
|
102
|
+
"""Entry point for command-line execution."""
|
|
103
|
+
asyncio.run(run_commander())
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
main()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Built-in Commander chat commands."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CommandType(Enum):
|
|
9
|
+
"""Built-in command types."""
|
|
10
|
+
|
|
11
|
+
LIST = "list"
|
|
12
|
+
START = "start"
|
|
13
|
+
STOP = "stop"
|
|
14
|
+
CONNECT = "connect"
|
|
15
|
+
DISCONNECT = "disconnect"
|
|
16
|
+
STATUS = "status"
|
|
17
|
+
HELP = "help"
|
|
18
|
+
EXIT = "exit"
|
|
19
|
+
INSTANCES = "instances" # alias for list
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Command:
|
|
24
|
+
"""Parsed command with args."""
|
|
25
|
+
|
|
26
|
+
type: CommandType
|
|
27
|
+
args: list[str]
|
|
28
|
+
raw: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CommandParser:
|
|
32
|
+
"""Parses user input into commands."""
|
|
33
|
+
|
|
34
|
+
ALIASES = {
|
|
35
|
+
"ls": CommandType.LIST,
|
|
36
|
+
"instances": CommandType.LIST,
|
|
37
|
+
"quit": CommandType.EXIT,
|
|
38
|
+
"q": CommandType.EXIT,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def parse(self, input_text: str) -> Optional[Command]:
|
|
42
|
+
"""Parse input into a Command.
|
|
43
|
+
|
|
44
|
+
Returns None if input is not a built-in command (natural language).
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
input_text: Raw user input.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Command if input is a built-in command, None otherwise.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> parser = CommandParser()
|
|
54
|
+
>>> cmd = parser.parse("list")
|
|
55
|
+
>>> cmd.type
|
|
56
|
+
<CommandType.LIST: 'list'>
|
|
57
|
+
>>> parser.parse("tell me about the code")
|
|
58
|
+
None
|
|
59
|
+
"""
|
|
60
|
+
if not input_text:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
parts = input_text.split()
|
|
64
|
+
command_str = parts[0].lower()
|
|
65
|
+
args = parts[1:] if len(parts) > 1 else []
|
|
66
|
+
|
|
67
|
+
# Check if it's an alias
|
|
68
|
+
if command_str in self.ALIASES:
|
|
69
|
+
cmd_type = self.ALIASES[command_str]
|
|
70
|
+
return Command(type=cmd_type, args=args, raw=input_text)
|
|
71
|
+
|
|
72
|
+
# Check if it's a direct command
|
|
73
|
+
try:
|
|
74
|
+
cmd_type = CommandType(command_str)
|
|
75
|
+
return Command(type=cmd_type, args=args, raw=input_text)
|
|
76
|
+
except ValueError:
|
|
77
|
+
# Not a built-in command
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
def is_command(self, input_text: str) -> bool:
|
|
81
|
+
"""Check if input is a built-in command.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
input_text: Raw user input.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if input is a built-in command, False otherwise.
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
>>> parser = CommandParser()
|
|
91
|
+
>>> parser.is_command("list")
|
|
92
|
+
True
|
|
93
|
+
>>> parser.is_command("tell me about the code")
|
|
94
|
+
False
|
|
95
|
+
"""
|
|
96
|
+
return self.parse(input_text) is not None
|