claude-mpm 5.4.96__py3-none-any.whl → 5.6.17__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.

Files changed (214) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +44 -10
  4. claude_mpm/agents/WORKFLOW.md +2 -0
  5. claude_mpm/agents/templates/circuit-breakers.md +26 -17
  6. claude_mpm/cli/commands/autotodos.py +45 -5
  7. claude_mpm/cli/commands/commander.py +216 -0
  8. claude_mpm/cli/commands/hook_errors.py +60 -60
  9. claude_mpm/cli/commands/run.py +35 -3
  10. claude_mpm/cli/commands/skill_source.py +51 -2
  11. claude_mpm/cli/commands/skills.py +5 -3
  12. claude_mpm/cli/executor.py +32 -17
  13. claude_mpm/cli/parsers/base_parser.py +17 -0
  14. claude_mpm/cli/parsers/commander_parser.py +116 -0
  15. claude_mpm/cli/parsers/run_parser.py +10 -0
  16. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  17. claude_mpm/cli/parsers/skills_parser.py +5 -0
  18. claude_mpm/cli/startup.py +124 -3
  19. claude_mpm/cli/startup_display.py +2 -1
  20. claude_mpm/cli/utils.py +7 -3
  21. claude_mpm/commander/__init__.py +78 -0
  22. claude_mpm/commander/adapters/__init__.py +60 -0
  23. claude_mpm/commander/adapters/auggie.py +260 -0
  24. claude_mpm/commander/adapters/base.py +288 -0
  25. claude_mpm/commander/adapters/claude_code.py +392 -0
  26. claude_mpm/commander/adapters/codex.py +237 -0
  27. claude_mpm/commander/adapters/communication.py +366 -0
  28. claude_mpm/commander/adapters/example_usage.py +310 -0
  29. claude_mpm/commander/adapters/mpm.py +389 -0
  30. claude_mpm/commander/adapters/registry.py +204 -0
  31. claude_mpm/commander/api/__init__.py +16 -0
  32. claude_mpm/commander/api/app.py +121 -0
  33. claude_mpm/commander/api/errors.py +133 -0
  34. claude_mpm/commander/api/routes/__init__.py +8 -0
  35. claude_mpm/commander/api/routes/events.py +184 -0
  36. claude_mpm/commander/api/routes/inbox.py +171 -0
  37. claude_mpm/commander/api/routes/messages.py +148 -0
  38. claude_mpm/commander/api/routes/projects.py +271 -0
  39. claude_mpm/commander/api/routes/sessions.py +226 -0
  40. claude_mpm/commander/api/routes/work.py +296 -0
  41. claude_mpm/commander/api/schemas.py +186 -0
  42. claude_mpm/commander/chat/__init__.py +7 -0
  43. claude_mpm/commander/chat/cli.py +111 -0
  44. claude_mpm/commander/chat/commands.py +96 -0
  45. claude_mpm/commander/chat/repl.py +310 -0
  46. claude_mpm/commander/config.py +49 -0
  47. claude_mpm/commander/config_loader.py +115 -0
  48. claude_mpm/commander/core/__init__.py +10 -0
  49. claude_mpm/commander/core/block_manager.py +325 -0
  50. claude_mpm/commander/core/response_manager.py +323 -0
  51. claude_mpm/commander/daemon.py +594 -0
  52. claude_mpm/commander/env_loader.py +59 -0
  53. claude_mpm/commander/events/__init__.py +26 -0
  54. claude_mpm/commander/events/manager.py +332 -0
  55. claude_mpm/commander/frameworks/__init__.py +12 -0
  56. claude_mpm/commander/frameworks/base.py +143 -0
  57. claude_mpm/commander/frameworks/claude_code.py +58 -0
  58. claude_mpm/commander/frameworks/mpm.py +62 -0
  59. claude_mpm/commander/inbox/__init__.py +16 -0
  60. claude_mpm/commander/inbox/dedup.py +128 -0
  61. claude_mpm/commander/inbox/inbox.py +224 -0
  62. claude_mpm/commander/inbox/models.py +70 -0
  63. claude_mpm/commander/instance_manager.py +337 -0
  64. claude_mpm/commander/llm/__init__.py +6 -0
  65. claude_mpm/commander/llm/openrouter_client.py +167 -0
  66. claude_mpm/commander/llm/summarizer.py +70 -0
  67. claude_mpm/commander/memory/__init__.py +45 -0
  68. claude_mpm/commander/memory/compression.py +347 -0
  69. claude_mpm/commander/memory/embeddings.py +230 -0
  70. claude_mpm/commander/memory/entities.py +310 -0
  71. claude_mpm/commander/memory/example_usage.py +290 -0
  72. claude_mpm/commander/memory/integration.py +325 -0
  73. claude_mpm/commander/memory/search.py +381 -0
  74. claude_mpm/commander/memory/store.py +657 -0
  75. claude_mpm/commander/models/__init__.py +18 -0
  76. claude_mpm/commander/models/events.py +121 -0
  77. claude_mpm/commander/models/project.py +162 -0
  78. claude_mpm/commander/models/work.py +214 -0
  79. claude_mpm/commander/parsing/__init__.py +20 -0
  80. claude_mpm/commander/parsing/extractor.py +132 -0
  81. claude_mpm/commander/parsing/output_parser.py +270 -0
  82. claude_mpm/commander/parsing/patterns.py +100 -0
  83. claude_mpm/commander/persistence/__init__.py +11 -0
  84. claude_mpm/commander/persistence/event_store.py +274 -0
  85. claude_mpm/commander/persistence/state_store.py +309 -0
  86. claude_mpm/commander/persistence/work_store.py +164 -0
  87. claude_mpm/commander/polling/__init__.py +13 -0
  88. claude_mpm/commander/polling/event_detector.py +104 -0
  89. claude_mpm/commander/polling/output_buffer.py +49 -0
  90. claude_mpm/commander/polling/output_poller.py +153 -0
  91. claude_mpm/commander/project_session.py +268 -0
  92. claude_mpm/commander/proxy/__init__.py +12 -0
  93. claude_mpm/commander/proxy/formatter.py +89 -0
  94. claude_mpm/commander/proxy/output_handler.py +191 -0
  95. claude_mpm/commander/proxy/relay.py +155 -0
  96. claude_mpm/commander/registry.py +410 -0
  97. claude_mpm/commander/runtime/__init__.py +10 -0
  98. claude_mpm/commander/runtime/executor.py +191 -0
  99. claude_mpm/commander/runtime/monitor.py +346 -0
  100. claude_mpm/commander/session/__init__.py +6 -0
  101. claude_mpm/commander/session/context.py +81 -0
  102. claude_mpm/commander/session/manager.py +59 -0
  103. claude_mpm/commander/tmux_orchestrator.py +361 -0
  104. claude_mpm/commander/web/__init__.py +1 -0
  105. claude_mpm/commander/work/__init__.py +30 -0
  106. claude_mpm/commander/work/executor.py +207 -0
  107. claude_mpm/commander/work/queue.py +405 -0
  108. claude_mpm/commander/workflow/__init__.py +27 -0
  109. claude_mpm/commander/workflow/event_handler.py +241 -0
  110. claude_mpm/commander/workflow/notifier.py +146 -0
  111. claude_mpm/commands/mpm-config.md +8 -0
  112. claude_mpm/commands/mpm-doctor.md +8 -0
  113. claude_mpm/commands/mpm-help.md +8 -0
  114. claude_mpm/commands/mpm-init.md +8 -0
  115. claude_mpm/commands/mpm-monitor.md +8 -0
  116. claude_mpm/commands/mpm-organize.md +8 -0
  117. claude_mpm/commands/mpm-postmortem.md +8 -0
  118. claude_mpm/commands/mpm-session-resume.md +8 -0
  119. claude_mpm/commands/mpm-status.md +8 -0
  120. claude_mpm/commands/mpm-ticket-view.md +8 -0
  121. claude_mpm/commands/mpm-version.md +8 -0
  122. claude_mpm/commands/mpm.md +8 -0
  123. claude_mpm/config/agent_presets.py +8 -7
  124. claude_mpm/config/skill_sources.py +16 -0
  125. claude_mpm/core/claude_runner.py +143 -0
  126. claude_mpm/core/config.py +32 -19
  127. claude_mpm/core/logger.py +26 -9
  128. claude_mpm/core/logging_utils.py +35 -11
  129. claude_mpm/core/output_style_manager.py +49 -12
  130. claude_mpm/core/unified_config.py +10 -6
  131. claude_mpm/core/unified_paths.py +68 -80
  132. claude_mpm/experimental/cli_enhancements.py +2 -1
  133. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
  134. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  135. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  136. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
  137. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
  138. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  139. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
  140. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
  141. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  142. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
  143. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
  144. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  145. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
  146. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  147. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
  148. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
  149. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  150. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
  151. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
  152. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
  153. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
  154. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +29 -30
  155. claude_mpm/hooks/claude_hooks/event_handlers.py +112 -99
  156. claude_mpm/hooks/claude_hooks/hook_handler.py +81 -88
  157. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
  158. claude_mpm/hooks/claude_hooks/installer.py +116 -8
  159. claude_mpm/hooks/claude_hooks/memory_integration.py +51 -31
  160. claude_mpm/hooks/claude_hooks/response_tracking.py +39 -58
  161. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
  162. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
  163. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  164. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  165. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
  166. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
  167. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
  168. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
  169. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  170. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
  171. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
  172. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  173. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
  174. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
  175. claude_mpm/hooks/claude_hooks/services/connection_manager.py +23 -28
  176. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +36 -103
  177. claude_mpm/hooks/claude_hooks/services/state_manager.py +23 -36
  178. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +47 -73
  179. claude_mpm/hooks/session_resume_hook.py +22 -18
  180. claude_mpm/hooks/templates/pre_tool_use_template.py +10 -2
  181. claude_mpm/scripts/claude-hook-handler.sh +43 -16
  182. claude_mpm/scripts/start_activity_logging.py +0 -0
  183. claude_mpm/services/agents/agent_recommendation_service.py +8 -8
  184. claude_mpm/services/agents/agent_selection_service.py +2 -2
  185. claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
  186. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  187. claude_mpm/services/event_log.py +8 -0
  188. claude_mpm/services/pm_skills_deployer.py +84 -6
  189. claude_mpm/services/skills/git_skill_source_manager.py +130 -10
  190. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  191. claude_mpm/services/skills/skill_discovery_service.py +74 -4
  192. claude_mpm/services/skills_deployer.py +31 -5
  193. claude_mpm/skills/__init__.py +2 -1
  194. claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
  195. claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
  196. claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
  197. claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
  198. claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
  199. claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
  200. claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
  201. claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
  202. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  203. claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
  204. claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
  205. claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
  206. claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
  207. claude_mpm/skills/registry.py +295 -90
  208. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/METADATA +22 -6
  209. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/RECORD +213 -83
  210. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/WHEEL +0 -0
  211. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/entry_points.txt +0 -0
  212. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE +0 -0
  213. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  214. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,226 @@
1
+ """Session management endpoints for MPM Commander API.
2
+
3
+ This module implements REST endpoints for creating and managing tool sessions
4
+ (Claude Code, Aider, etc.) within projects.
5
+ """
6
+
7
+ import logging
8
+ import subprocess # nosec B404 - needed for tmux error handling
9
+ import uuid
10
+ from typing import List
11
+
12
+ from fastapi import APIRouter, Request, Response
13
+
14
+ from ...models import ToolSession
15
+ from ..errors import (
16
+ InvalidRuntimeError,
17
+ ProjectNotFoundError,
18
+ SessionNotFoundError,
19
+ TmuxNoSpaceError,
20
+ )
21
+ from ..schemas import CreateSessionRequest, SessionResponse
22
+
23
+ router = APIRouter()
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Valid runtime adapters (Phase 1: claude-code only)
27
+ VALID_RUNTIMES = {"claude-code"}
28
+
29
+
30
+ def _get_registry(request: Request):
31
+ """Get registry instance from app.state."""
32
+ if not hasattr(request.app.state, "registry") or request.app.state.registry is None:
33
+ raise RuntimeError("Registry not initialized")
34
+ return request.app.state.registry
35
+
36
+
37
+ def _get_tmux(request: Request):
38
+ """Get tmux orchestrator instance from app.state."""
39
+ if not hasattr(request.app.state, "tmux") or request.app.state.tmux is None:
40
+ raise RuntimeError("Tmux orchestrator not initialized")
41
+ return request.app.state.tmux
42
+
43
+
44
+ def _session_to_response(session: ToolSession) -> SessionResponse:
45
+ """Convert ToolSession model to SessionResponse schema.
46
+
47
+ Args:
48
+ session: ToolSession instance
49
+
50
+ Returns:
51
+ SessionResponse with session data
52
+ """
53
+ return SessionResponse(
54
+ id=session.id,
55
+ project_id=session.project_id,
56
+ runtime=session.runtime,
57
+ tmux_target=session.tmux_target,
58
+ status=session.status,
59
+ created_at=session.created_at,
60
+ )
61
+
62
+
63
+ @router.get("/projects/{project_id}/sessions", response_model=List[SessionResponse])
64
+ async def list_sessions(request: Request, project_id: str) -> List[SessionResponse]:
65
+ """List all sessions for a project.
66
+
67
+ Args:
68
+ project_id: Unique project identifier
69
+
70
+ Returns:
71
+ List of session information (may be empty)
72
+
73
+ Raises:
74
+ ProjectNotFoundError: If project_id doesn't exist
75
+
76
+ Example:
77
+ GET /api/projects/abc-123/sessions
78
+ Response: [
79
+ {
80
+ "id": "sess-456",
81
+ "project_id": "abc-123",
82
+ "runtime": "claude-code",
83
+ "tmux_target": "%1",
84
+ "status": "running",
85
+ "created_at": "2025-01-12T10:00:00Z"
86
+ }
87
+ ]
88
+ """
89
+ registry = _get_registry(request)
90
+ project = registry.get(project_id)
91
+
92
+ if project is None:
93
+ raise ProjectNotFoundError(project_id)
94
+
95
+ # Convert sessions dict to list of responses
96
+ return [_session_to_response(s) for s in project.sessions.values()]
97
+
98
+
99
+ @router.post(
100
+ "/projects/{project_id}/sessions", response_model=SessionResponse, status_code=201
101
+ )
102
+ async def create_session(
103
+ request: Request, project_id: str, req: CreateSessionRequest
104
+ ) -> SessionResponse:
105
+ """Create a new session for a project.
106
+
107
+ Creates a new tmux pane and initializes the specified runtime adapter.
108
+
109
+ Args:
110
+ project_id: Unique project identifier
111
+ req: Session creation request
112
+
113
+ Returns:
114
+ Newly created session information
115
+
116
+ Raises:
117
+ ProjectNotFoundError: If project_id doesn't exist
118
+ InvalidRuntimeError: If runtime is not supported
119
+ TmuxNoSpaceError: If tmux has no space for new pane
120
+
121
+ Example:
122
+ POST /api/projects/abc-123/sessions
123
+ Body: {
124
+ "runtime": "claude-code",
125
+ "agent_prompt": "You are a helpful coding assistant"
126
+ }
127
+ Response: {
128
+ "id": "sess-456",
129
+ "project_id": "abc-123",
130
+ "runtime": "claude-code",
131
+ "tmux_target": "%1",
132
+ "status": "initializing",
133
+ "created_at": "2025-01-12T10:00:00Z"
134
+ }
135
+ """
136
+ registry = _get_registry(request)
137
+ tmux_orch = _get_tmux(request)
138
+
139
+ # Validate project exists
140
+ project = registry.get(project_id)
141
+ if project is None:
142
+ raise ProjectNotFoundError(project_id)
143
+
144
+ # Validate runtime
145
+ if req.runtime not in VALID_RUNTIMES:
146
+ raise InvalidRuntimeError(req.runtime)
147
+
148
+ # Generate session ID
149
+ session_id = str(uuid.uuid4())
150
+
151
+ # Create tmux pane for session
152
+ try:
153
+ tmux_target = tmux_orch.create_pane(
154
+ pane_id=f"{project.name}-{req.runtime}",
155
+ working_dir=project.path,
156
+ )
157
+ except subprocess.CalledProcessError as e:
158
+ stderr = e.stderr.decode() if e.stderr else ""
159
+ if "no space for new pane" in stderr.lower():
160
+ raise TmuxNoSpaceError() from None
161
+ raise # Re-raise other subprocess errors
162
+
163
+ # Create session object
164
+ session = ToolSession(
165
+ id=session_id,
166
+ project_id=project_id,
167
+ runtime=req.runtime,
168
+ tmux_target=tmux_target,
169
+ status="initializing",
170
+ )
171
+
172
+ # Add session to project
173
+ registry.add_session(project_id, session)
174
+
175
+ # TODO: Start runtime adapter in pane (Phase 2)
176
+ # For Phase 1, session stays in "initializing" state
177
+
178
+ return _session_to_response(session)
179
+
180
+
181
+ @router.delete("/sessions/{session_id}", status_code=204)
182
+ async def stop_session(request: Request, session_id: str) -> Response:
183
+ """Stop and remove a session.
184
+
185
+ Kills the tmux pane and removes the session from its project.
186
+
187
+ Args:
188
+ session_id: Unique session identifier
189
+
190
+ Returns:
191
+ Empty response with 204 status
192
+
193
+ Raises:
194
+ SessionNotFoundError: If session_id doesn't exist
195
+
196
+ Example:
197
+ DELETE /api/sessions/sess-456
198
+ Response: 204 No Content
199
+ """
200
+ registry = _get_registry(request)
201
+ tmux_orch = _get_tmux(request)
202
+
203
+ # Find session across all projects
204
+ session = None
205
+ parent_project_id = None
206
+
207
+ for project in registry.list_all():
208
+ if session_id in project.sessions:
209
+ session = project.sessions[session_id]
210
+ parent_project_id = project.id
211
+ break
212
+
213
+ if session is None or parent_project_id is None:
214
+ raise SessionNotFoundError(session_id)
215
+
216
+ # Kill tmux pane
217
+ try:
218
+ tmux_orch.kill_pane(session.tmux_target)
219
+ except Exception as e:
220
+ # Pane may already be dead, continue with cleanup
221
+ logger.debug("Failed to kill pane (may already be dead): %s", e)
222
+
223
+ # Remove session from project
224
+ registry.remove_session(parent_project_id, session_id)
225
+
226
+ return Response(status_code=204)
@@ -0,0 +1,296 @@
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 Dict, List, Optional
8
+
9
+ from fastapi import APIRouter, HTTPException, Query, Request
10
+
11
+ from ...models.work import WorkPriority, WorkState
12
+ from ...work import WorkQueue
13
+ from ..schemas import CreateWorkRequest, WorkItemResponse
14
+
15
+ router = APIRouter()
16
+
17
+
18
+ def _get_registry(request: Request):
19
+ """Get registry instance from app.state."""
20
+ if not hasattr(request.app.state, "registry") or request.app.state.registry is None:
21
+ raise RuntimeError("Registry not initialized")
22
+ return request.app.state.registry
23
+
24
+
25
+ def _get_work_queues(request: Request) -> Dict:
26
+ """Get work queues dict from app.state (shared with daemon)."""
27
+ if (
28
+ not hasattr(request.app.state, "work_queues")
29
+ or request.app.state.work_queues is None
30
+ ):
31
+ raise RuntimeError("Work queues not initialized")
32
+ return request.app.state.work_queues
33
+
34
+
35
+ def _get_daemon(request: Request):
36
+ """Get daemon instance from app.state."""
37
+ if not hasattr(request.app.state, "daemon_instance"):
38
+ return None
39
+ return request.app.state.daemon_instance
40
+
41
+
42
+ def _work_item_to_response(work_item) -> WorkItemResponse:
43
+ """Convert WorkItem model to WorkItemResponse schema.
44
+
45
+ Args:
46
+ work_item: WorkItem instance
47
+
48
+ Returns:
49
+ WorkItemResponse with all work item data
50
+ """
51
+ return WorkItemResponse(
52
+ id=work_item.id,
53
+ project_id=work_item.project_id,
54
+ content=work_item.content,
55
+ state=work_item.state.value,
56
+ priority=work_item.priority.value,
57
+ created_at=work_item.created_at,
58
+ started_at=work_item.started_at,
59
+ completed_at=work_item.completed_at,
60
+ result=work_item.result,
61
+ error=work_item.error,
62
+ depends_on=work_item.depends_on,
63
+ metadata=work_item.metadata,
64
+ )
65
+
66
+
67
+ @router.post("/projects/{project_id}/work", response_model=WorkItemResponse)
68
+ async def add_work(
69
+ request: Request, project_id: str, work: CreateWorkRequest
70
+ ) -> WorkItemResponse:
71
+ """Add work item to project queue.
72
+
73
+ Args:
74
+ request: FastAPI request (for accessing app.state)
75
+ project_id: Project identifier
76
+ work: Work item creation request
77
+
78
+ Returns:
79
+ Created work item
80
+
81
+ Raises:
82
+ HTTPException: 404 if project not found
83
+
84
+ Example:
85
+ POST /api/projects/proj-123/work
86
+ Request: {
87
+ "content": "Implement OAuth2 authentication",
88
+ "priority": 3,
89
+ "depends_on": ["work-abc"]
90
+ }
91
+ Response: {
92
+ "id": "work-xyz",
93
+ "project_id": "proj-123",
94
+ "content": "Implement OAuth2 authentication",
95
+ "state": "queued",
96
+ "priority": 3,
97
+ ...
98
+ }
99
+ """
100
+ registry = _get_registry(request)
101
+ work_queues = _get_work_queues(request)
102
+ daemon = _get_daemon(request)
103
+
104
+ # Get project
105
+ project = registry.get(project_id)
106
+ if project is None:
107
+ raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
108
+
109
+ # Get or create work queue (shared with daemon)
110
+ import logging
111
+
112
+ logger = logging.getLogger(__name__)
113
+ logger.info(
114
+ f"work_queues dict id: {id(work_queues)}, keys: {list(work_queues.keys())}"
115
+ )
116
+
117
+ if project_id not in work_queues:
118
+ logger.info(f"Creating new work queue for {project_id}")
119
+ work_queues[project_id] = WorkQueue(project_id)
120
+ logger.info(f"After creation, work_queues keys: {list(work_queues.keys())}")
121
+
122
+ queue = work_queues[project_id]
123
+
124
+ # Convert priority int to enum
125
+ priority = WorkPriority(work.priority)
126
+
127
+ # Add work item
128
+ work_item = queue.add(
129
+ content=work.content, priority=priority, depends_on=work.depends_on
130
+ )
131
+
132
+ # Ensure daemon has a session for this project (creates if needed)
133
+ if daemon and not daemon.sessions.get(project_id):
134
+ # Session creation will be handled by daemon's main loop
135
+ # when it detects work in the queue
136
+ pass
137
+
138
+ return _work_item_to_response(work_item)
139
+
140
+
141
+ @router.get("/projects/{project_id}/work", response_model=List[WorkItemResponse])
142
+ async def list_work(
143
+ request: Request,
144
+ project_id: str,
145
+ state: Optional[str] = Query(None, description="Filter by state"),
146
+ ) -> List[WorkItemResponse]:
147
+ """List work items for project.
148
+
149
+ Args:
150
+ request: FastAPI request (for accessing app.state)
151
+ project_id: Project identifier
152
+ state: Optional state filter (pending, queued, in_progress, etc.)
153
+
154
+ Returns:
155
+ List of work items (may be empty)
156
+
157
+ Raises:
158
+ HTTPException: 404 if project not found, 400 if invalid state
159
+
160
+ Example:
161
+ GET /api/projects/proj-123/work
162
+ Response: [
163
+ {"id": "work-1", "state": "queued", ...},
164
+ {"id": "work-2", "state": "in_progress", ...}
165
+ ]
166
+
167
+ GET /api/projects/proj-123/work?state=queued
168
+ Response: [
169
+ {"id": "work-1", "state": "queued", ...}
170
+ ]
171
+ """
172
+ registry = _get_registry(request)
173
+ work_queues = _get_work_queues(request)
174
+
175
+ # Get project
176
+ project = registry.get(project_id)
177
+ if project is None:
178
+ raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
179
+
180
+ # Get work queue (shared with daemon)
181
+ if project_id not in work_queues:
182
+ # Return empty list if no work queue exists yet
183
+ return []
184
+
185
+ queue = work_queues[project_id]
186
+
187
+ # Parse state filter
188
+ state_filter = None
189
+ if state:
190
+ try:
191
+ state_filter = WorkState(state)
192
+ except ValueError as e:
193
+ raise HTTPException(
194
+ status_code=400,
195
+ detail=f"Invalid state: {state}. "
196
+ f"Valid states: {[s.value for s in WorkState]}",
197
+ ) from e
198
+
199
+ # List work items
200
+ items = queue.list(state=state_filter)
201
+
202
+ return [_work_item_to_response(item) for item in items]
203
+
204
+
205
+ @router.get("/projects/{project_id}/work/{work_id}", response_model=WorkItemResponse)
206
+ async def get_work(request: Request, project_id: str, work_id: str) -> WorkItemResponse:
207
+ """Get work item details.
208
+
209
+ Args:
210
+ request: FastAPI request (for accessing app.state)
211
+ project_id: Project identifier
212
+ work_id: Work item identifier
213
+
214
+ Returns:
215
+ Work item details
216
+
217
+ Raises:
218
+ HTTPException: 404 if project or work item not found
219
+
220
+ Example:
221
+ GET /api/projects/proj-123/work/work-xyz
222
+ Response: {
223
+ "id": "work-xyz",
224
+ "project_id": "proj-123",
225
+ "state": "in_progress",
226
+ ...
227
+ }
228
+ """
229
+ registry = _get_registry(request)
230
+ work_queues = _get_work_queues(request)
231
+
232
+ # Get project
233
+ project = registry.get(project_id)
234
+ if project is None:
235
+ raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
236
+
237
+ # Get work queue (shared with daemon)
238
+ if project_id not in work_queues:
239
+ raise HTTPException(status_code=404, detail="Work queue not found")
240
+
241
+ queue = work_queues[project_id]
242
+
243
+ # Get work item
244
+ work_item = queue.get(work_id)
245
+ if not work_item:
246
+ raise HTTPException(status_code=404, detail=f"Work item {work_id} not found")
247
+
248
+ return _work_item_to_response(work_item)
249
+
250
+
251
+ @router.post("/projects/{project_id}/work/{work_id}/cancel")
252
+ async def cancel_work(request: Request, project_id: str, work_id: str) -> dict:
253
+ """Cancel pending work item.
254
+
255
+ Args:
256
+ request: FastAPI request (for accessing app.state)
257
+ project_id: Project identifier
258
+ work_id: Work item identifier
259
+
260
+ Returns:
261
+ Success message
262
+
263
+ Raises:
264
+ HTTPException: 404 if project/work not found, 400 if invalid state
265
+
266
+ Example:
267
+ POST /api/projects/proj-123/work/work-xyz/cancel
268
+ Response: {"status": "cancelled", "id": "work-xyz"}
269
+ """
270
+ registry = _get_registry(request)
271
+ work_queues = _get_work_queues(request)
272
+
273
+ # Get project
274
+ project = registry.get(project_id)
275
+ if project is None:
276
+ raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
277
+
278
+ # Get work queue (shared with daemon)
279
+ if project_id not in work_queues:
280
+ raise HTTPException(status_code=404, detail="Work queue not found")
281
+
282
+ queue = work_queues[project_id]
283
+
284
+ # Cancel work item
285
+ if not queue.cancel(work_id):
286
+ work_item = queue.get(work_id)
287
+ if not work_item:
288
+ raise HTTPException(
289
+ status_code=404, detail=f"Work item {work_id} not found"
290
+ )
291
+ raise HTTPException(
292
+ status_code=400,
293
+ detail=f"Cannot cancel work item in state {work_item.state.value}",
294
+ )
295
+
296
+ return {"status": "cancelled", "id": work_id}
@@ -0,0 +1,186 @@
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
+ project_id: Optional project identifier (UUID generated if omitted)
21
+ name: Optional display name (derived from path if omitted)
22
+ """
23
+
24
+ path: str = Field(..., description="Filesystem path to project")
25
+ project_id: Optional[str] = Field(
26
+ None, description="Project identifier (UUID generated if omitted)"
27
+ )
28
+ name: Optional[str] = Field(
29
+ None, description="Display name (derived from path if omitted)"
30
+ )
31
+
32
+
33
+ class CreateSessionRequest(BaseModel):
34
+ """Request to create a new tool session.
35
+
36
+ Attributes:
37
+ runtime: Runtime adapter to use (e.g., "claude-code")
38
+ agent_prompt: Optional custom system prompt for the agent
39
+ """
40
+
41
+ runtime: str = Field("claude-code", description="Runtime adapter to use")
42
+ agent_prompt: Optional[str] = Field(None, description="Custom system prompt")
43
+
44
+
45
+ class SendMessageRequest(BaseModel):
46
+ """Request to send a message to a project.
47
+
48
+ Attributes:
49
+ content: Message content
50
+ session_id: Target session ID (uses active session if omitted)
51
+ """
52
+
53
+ content: str = Field(..., description="Message content")
54
+ session_id: Optional[str] = Field(
55
+ None, description="Target session (uses active if omitted)"
56
+ )
57
+
58
+
59
+ # Response Models
60
+
61
+
62
+ class SessionResponse(BaseModel):
63
+ """Session information response.
64
+
65
+ Attributes:
66
+ id: Unique session identifier
67
+ project_id: Parent project ID
68
+ runtime: Runtime adapter name
69
+ tmux_target: Tmux pane target identifier
70
+ status: Current session status
71
+ created_at: Session creation timestamp
72
+ """
73
+
74
+ id: str
75
+ project_id: str
76
+ runtime: str
77
+ tmux_target: str
78
+ status: str
79
+ created_at: datetime
80
+
81
+
82
+ class ProjectResponse(BaseModel):
83
+ """Project information response.
84
+
85
+ Attributes:
86
+ id: Unique project identifier
87
+ path: Absolute filesystem path
88
+ name: Display name
89
+ state: Current project state
90
+ state_reason: Optional state reason (e.g., error message)
91
+ sessions: List of active sessions
92
+ pending_events_count: Number of pending events
93
+ last_activity: Last activity timestamp
94
+ created_at: Project registration timestamp
95
+ """
96
+
97
+ id: str
98
+ path: str
99
+ name: str
100
+ state: str
101
+ state_reason: Optional[str]
102
+ sessions: List[SessionResponse]
103
+ pending_events_count: int
104
+ last_activity: datetime
105
+ created_at: datetime
106
+
107
+
108
+ class MessageResponse(BaseModel):
109
+ """Conversation message response.
110
+
111
+ Attributes:
112
+ id: Unique message identifier
113
+ role: Message sender role (user, assistant, system, tool)
114
+ content: Message content
115
+ session_id: Associated session ID (if from tool)
116
+ timestamp: Message creation timestamp
117
+ """
118
+
119
+ id: str
120
+ role: str
121
+ content: str
122
+ session_id: Optional[str]
123
+ timestamp: datetime
124
+
125
+
126
+ class CreateWorkRequest(BaseModel):
127
+ """Request to create a work item.
128
+
129
+ Attributes:
130
+ content: Task/message to execute
131
+ priority: Priority level (1-4, where 4 is CRITICAL)
132
+ depends_on: Optional list of work item IDs that must complete first
133
+ """
134
+
135
+ content: str = Field(..., description="Task/message to execute")
136
+ priority: int = Field(
137
+ 2,
138
+ description="Priority level (1=LOW, 2=MEDIUM, 3=HIGH, 4=CRITICAL)",
139
+ ge=1,
140
+ le=4,
141
+ )
142
+ depends_on: Optional[List[str]] = Field(
143
+ None, description="Work item IDs that must complete first"
144
+ )
145
+
146
+
147
+ class WorkItemResponse(BaseModel):
148
+ """Work item information response.
149
+
150
+ Attributes:
151
+ id: Unique work item identifier
152
+ project_id: Parent project ID
153
+ content: Task/message content
154
+ state: Current state (pending, queued, in_progress, blocked, completed, failed, cancelled)
155
+ priority: Priority level (1-4)
156
+ created_at: Creation timestamp
157
+ started_at: Execution start timestamp (if started)
158
+ completed_at: Completion timestamp (if completed/failed)
159
+ result: Result message (if completed)
160
+ error: Error message (if failed)
161
+ depends_on: List of dependency work item IDs
162
+ metadata: Additional structured data
163
+ """
164
+
165
+ id: str
166
+ project_id: str
167
+ content: str
168
+ state: str
169
+ priority: int
170
+ created_at: datetime
171
+ started_at: Optional[datetime]
172
+ completed_at: Optional[datetime]
173
+ result: Optional[str]
174
+ error: Optional[str]
175
+ depends_on: List[str]
176
+ metadata: dict
177
+
178
+
179
+ class ErrorResponse(BaseModel):
180
+ """Error response with structured error information.
181
+
182
+ Attributes:
183
+ error: Error details with code and message
184
+ """
185
+
186
+ error: dict # {code: str, message: str}
@@ -0,0 +1,7 @@
1
+ """Commander chat interface."""
2
+
3
+ from .cli import run_commander
4
+ from .commands import Command, CommandParser, CommandType
5
+ from .repl import CommanderREPL
6
+
7
+ __all__ = ["Command", "CommandParser", "CommandType", "CommanderREPL", "run_commander"]