claude-mpm 5.6.13__py3-none-any.whl → 5.6.14__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.
Files changed (48) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/commander.py +173 -3
  3. claude_mpm/cli/parsers/commander_parser.py +41 -8
  4. claude_mpm/cli/startup.py +10 -1
  5. claude_mpm/cli/startup_display.py +2 -1
  6. claude_mpm/commander/__init__.py +6 -0
  7. claude_mpm/commander/adapters/__init__.py +32 -3
  8. claude_mpm/commander/adapters/auggie.py +260 -0
  9. claude_mpm/commander/adapters/base.py +98 -1
  10. claude_mpm/commander/adapters/claude_code.py +32 -1
  11. claude_mpm/commander/adapters/codex.py +237 -0
  12. claude_mpm/commander/adapters/example_usage.py +310 -0
  13. claude_mpm/commander/adapters/mpm.py +389 -0
  14. claude_mpm/commander/adapters/registry.py +204 -0
  15. claude_mpm/commander/api/app.py +32 -16
  16. claude_mpm/commander/api/routes/messages.py +11 -11
  17. claude_mpm/commander/api/routes/projects.py +20 -20
  18. claude_mpm/commander/api/routes/sessions.py +19 -21
  19. claude_mpm/commander/api/routes/work.py +86 -50
  20. claude_mpm/commander/api/schemas.py +4 -0
  21. claude_mpm/commander/chat/cli.py +4 -0
  22. claude_mpm/commander/daemon.py +139 -9
  23. claude_mpm/commander/env_loader.py +59 -0
  24. claude_mpm/commander/memory/__init__.py +45 -0
  25. claude_mpm/commander/memory/compression.py +347 -0
  26. claude_mpm/commander/memory/embeddings.py +230 -0
  27. claude_mpm/commander/memory/entities.py +310 -0
  28. claude_mpm/commander/memory/example_usage.py +290 -0
  29. claude_mpm/commander/memory/integration.py +325 -0
  30. claude_mpm/commander/memory/search.py +381 -0
  31. claude_mpm/commander/memory/store.py +657 -0
  32. claude_mpm/commander/registry.py +10 -4
  33. claude_mpm/commander/work/executor.py +22 -12
  34. claude_mpm/core/output_style_manager.py +34 -7
  35. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +0 -0
  36. claude_mpm/hooks/claude_hooks/event_handlers.py +0 -0
  37. claude_mpm/hooks/claude_hooks/hook_handler.py +0 -0
  38. claude_mpm/hooks/claude_hooks/memory_integration.py +0 -0
  39. claude_mpm/hooks/claude_hooks/response_tracking.py +0 -0
  40. claude_mpm/hooks/templates/pre_tool_use_template.py +0 -0
  41. claude_mpm/scripts/start_activity_logging.py +0 -0
  42. {claude_mpm-5.6.13.dist-info → claude_mpm-5.6.14.dist-info}/METADATA +2 -2
  43. {claude_mpm-5.6.13.dist-info → claude_mpm-5.6.14.dist-info}/RECORD +41 -27
  44. {claude_mpm-5.6.13.dist-info → claude_mpm-5.6.14.dist-info}/WHEEL +0 -0
  45. {claude_mpm-5.6.13.dist-info → claude_mpm-5.6.14.dist-info}/entry_points.txt +0 -0
  46. {claude_mpm-5.6.13.dist-info → claude_mpm-5.6.14.dist-info}/licenses/LICENSE +0 -0
  47. {claude_mpm-5.6.13.dist-info → claude_mpm-5.6.14.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  48. {claude_mpm-5.6.13.dist-info → claude_mpm-5.6.14.dist-info}/top_level.txt +0 -0
@@ -7,7 +7,7 @@ conversation threads for projects.
7
7
  import uuid
8
8
  from typing import List
9
9
 
10
- from fastapi import APIRouter
10
+ from fastapi import APIRouter, Request
11
11
 
12
12
  from ...models import ThreadMessage
13
13
  from ..errors import ProjectNotFoundError
@@ -16,13 +16,11 @@ from ..schemas import MessageResponse, SendMessageRequest
16
16
  router = APIRouter()
17
17
 
18
18
 
19
- def _get_registry():
20
- """Get registry instance from app global."""
21
- from ..app import registry
22
-
23
- if registry is None:
19
+ def _get_registry(request: Request):
20
+ """Get registry instance from app.state."""
21
+ if not hasattr(request.app.state, "registry") or request.app.state.registry is None:
24
22
  raise RuntimeError("Registry not initialized")
25
- return registry
23
+ return request.app.state.registry
26
24
 
27
25
 
28
26
  def _message_to_response(message: ThreadMessage) -> MessageResponse:
@@ -44,7 +42,7 @@ def _message_to_response(message: ThreadMessage) -> MessageResponse:
44
42
 
45
43
 
46
44
  @router.get("/projects/{project_id}/thread", response_model=List[MessageResponse])
47
- async def get_thread(project_id: str) -> List[MessageResponse]:
45
+ async def get_thread(request: Request, project_id: str) -> List[MessageResponse]:
48
46
  """Get conversation thread for a project.
49
47
 
50
48
  Returns all messages in chronological order.
@@ -77,7 +75,7 @@ async def get_thread(project_id: str) -> List[MessageResponse]:
77
75
  }
78
76
  ]
79
77
  """
80
- registry = _get_registry()
78
+ registry = _get_registry(request)
81
79
  project = registry.get(project_id)
82
80
 
83
81
  if project is None:
@@ -90,7 +88,9 @@ async def get_thread(project_id: str) -> List[MessageResponse]:
90
88
  @router.post(
91
89
  "/projects/{project_id}/messages", response_model=MessageResponse, status_code=201
92
90
  )
93
- async def send_message(project_id: str, req: SendMessageRequest) -> MessageResponse:
91
+ async def send_message(
92
+ request: Request, project_id: str, req: SendMessageRequest
93
+ ) -> MessageResponse:
94
94
  """Send a message to a project's active session.
95
95
 
96
96
  Adds message to conversation thread and sends to specified or active session.
@@ -119,7 +119,7 @@ async def send_message(project_id: str, req: SendMessageRequest) -> MessageRespo
119
119
  "timestamp": "2025-01-12T10:00:00Z"
120
120
  }
121
121
  """
122
- registry = _get_registry()
122
+ registry = _get_registry(request)
123
123
  project = registry.get(project_id)
124
124
 
125
125
  if project is None:
@@ -7,7 +7,7 @@ projects in the MPM Commander.
7
7
  from pathlib import Path
8
8
  from typing import List
9
9
 
10
- from fastapi import APIRouter, Response
10
+ from fastapi import APIRouter, Request, Response
11
11
 
12
12
  from ...models import ProjectState
13
13
  from ..errors import InvalidPathError, ProjectAlreadyExistsError, ProjectNotFoundError
@@ -16,13 +16,11 @@ from ..schemas import ProjectResponse, RegisterProjectRequest, SessionResponse
16
16
  router = APIRouter()
17
17
 
18
18
 
19
- def _get_registry():
20
- """Get registry instance from app global."""
21
- from ..app import registry
22
-
23
- if registry is None:
19
+ def _get_registry(request: Request):
20
+ """Get registry instance from app.state."""
21
+ if not hasattr(request.app.state, "registry") or request.app.state.registry is None:
24
22
  raise RuntimeError("Registry not initialized")
25
- return registry
23
+ return request.app.state.registry
26
24
 
27
25
 
28
26
  def _project_to_response(project) -> ProjectResponse:
@@ -61,7 +59,7 @@ def _project_to_response(project) -> ProjectResponse:
61
59
 
62
60
 
63
61
  @router.get("/projects", response_model=List[ProjectResponse])
64
- async def list_projects() -> List[ProjectResponse]:
62
+ async def list_projects(request: Request) -> List[ProjectResponse]:
65
63
  """List all registered projects.
66
64
 
67
65
  Returns:
@@ -83,13 +81,13 @@ async def list_projects() -> List[ProjectResponse]:
83
81
  }
84
82
  ]
85
83
  """
86
- registry = _get_registry()
84
+ registry = _get_registry(request)
87
85
  projects = registry.list_all()
88
86
  return [_project_to_response(p) for p in projects]
89
87
 
90
88
 
91
89
  @router.get("/projects/{project_id}", response_model=ProjectResponse)
92
- async def get_project(project_id: str) -> ProjectResponse:
90
+ async def get_project(request: Request, project_id: str) -> ProjectResponse:
93
91
  """Get project details by ID.
94
92
 
95
93
  Args:
@@ -111,7 +109,7 @@ async def get_project(project_id: str) -> ProjectResponse:
111
109
  ...
112
110
  }
113
111
  """
114
- registry = _get_registry()
112
+ registry = _get_registry(request)
115
113
  project = registry.get(project_id)
116
114
 
117
115
  if project is None:
@@ -121,7 +119,9 @@ async def get_project(project_id: str) -> ProjectResponse:
121
119
 
122
120
 
123
121
  @router.post("/projects", response_model=ProjectResponse, status_code=201)
124
- async def register_project(req: RegisterProjectRequest) -> ProjectResponse:
122
+ async def register_project(
123
+ request: Request, req: RegisterProjectRequest
124
+ ) -> ProjectResponse:
125
125
  """Register a new project.
126
126
 
127
127
  Args:
@@ -148,7 +148,7 @@ async def register_project(req: RegisterProjectRequest) -> ProjectResponse:
148
148
  ...
149
149
  }
150
150
  """
151
- registry = _get_registry()
151
+ registry = _get_registry(request)
152
152
 
153
153
  # Validate path exists and is directory
154
154
  path_obj = Path(req.path)
@@ -156,7 +156,7 @@ async def register_project(req: RegisterProjectRequest) -> ProjectResponse:
156
156
  raise InvalidPathError(req.path)
157
157
 
158
158
  try:
159
- project = registry.register(req.path, req.name)
159
+ project = registry.register(req.path, req.name, req.project_id)
160
160
  return _project_to_response(project)
161
161
  except ValueError as e:
162
162
  # Registry raises ValueError for duplicate registration
@@ -168,7 +168,7 @@ async def register_project(req: RegisterProjectRequest) -> ProjectResponse:
168
168
 
169
169
 
170
170
  @router.delete("/projects/{project_id}", status_code=204)
171
- async def unregister_project(project_id: str) -> Response:
171
+ async def unregister_project(request: Request, project_id: str) -> Response:
172
172
  """Unregister a project.
173
173
 
174
174
  Args:
@@ -184,7 +184,7 @@ async def unregister_project(project_id: str) -> Response:
184
184
  DELETE /api/projects/abc-123
185
185
  Response: 204 No Content
186
186
  """
187
- registry = _get_registry()
187
+ registry = _get_registry(request)
188
188
 
189
189
  try:
190
190
  registry.unregister(project_id)
@@ -194,7 +194,7 @@ async def unregister_project(project_id: str) -> Response:
194
194
 
195
195
 
196
196
  @router.post("/projects/{project_id}/pause", response_model=ProjectResponse)
197
- async def pause_project(project_id: str) -> ProjectResponse:
197
+ async def pause_project(request: Request, project_id: str) -> ProjectResponse:
198
198
  """Pause a project.
199
199
 
200
200
  Sets project state to PAUSED to prevent automatic work processing.
@@ -217,7 +217,7 @@ async def pause_project(project_id: str) -> ProjectResponse:
217
217
  ...
218
218
  }
219
219
  """
220
- registry = _get_registry()
220
+ registry = _get_registry(request)
221
221
  project = registry.get(project_id)
222
222
 
223
223
  if project is None:
@@ -233,7 +233,7 @@ async def pause_project(project_id: str) -> ProjectResponse:
233
233
 
234
234
 
235
235
  @router.post("/projects/{project_id}/resume", response_model=ProjectResponse)
236
- async def resume_project(project_id: str) -> ProjectResponse:
236
+ async def resume_project(request: Request, project_id: str) -> ProjectResponse:
237
237
  """Resume a paused project.
238
238
 
239
239
  Sets project state back to IDLE to allow work processing.
@@ -256,7 +256,7 @@ async def resume_project(project_id: str) -> ProjectResponse:
256
256
  ...
257
257
  }
258
258
  """
259
- registry = _get_registry()
259
+ registry = _get_registry(request)
260
260
  project = registry.get(project_id)
261
261
 
262
262
  if project is None:
@@ -9,7 +9,7 @@ import subprocess # nosec B404 - needed for tmux error handling
9
9
  import uuid
10
10
  from typing import List
11
11
 
12
- from fastapi import APIRouter, Response
12
+ from fastapi import APIRouter, Request, Response
13
13
 
14
14
  from ...models import ToolSession
15
15
  from ..errors import (
@@ -27,22 +27,18 @@ logger = logging.getLogger(__name__)
27
27
  VALID_RUNTIMES = {"claude-code"}
28
28
 
29
29
 
30
- def _get_registry():
31
- """Get registry instance from app global."""
32
- from ..app import registry
33
-
34
- if registry is None:
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:
35
33
  raise RuntimeError("Registry not initialized")
36
- return registry
37
-
34
+ return request.app.state.registry
38
35
 
39
- def _get_tmux():
40
- """Get tmux orchestrator instance from app global."""
41
- from ..app import tmux
42
36
 
43
- if tmux is None:
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:
44
40
  raise RuntimeError("Tmux orchestrator not initialized")
45
- return tmux
41
+ return request.app.state.tmux
46
42
 
47
43
 
48
44
  def _session_to_response(session: ToolSession) -> SessionResponse:
@@ -65,7 +61,7 @@ def _session_to_response(session: ToolSession) -> SessionResponse:
65
61
 
66
62
 
67
63
  @router.get("/projects/{project_id}/sessions", response_model=List[SessionResponse])
68
- async def list_sessions(project_id: str) -> List[SessionResponse]:
64
+ async def list_sessions(request: Request, project_id: str) -> List[SessionResponse]:
69
65
  """List all sessions for a project.
70
66
 
71
67
  Args:
@@ -90,7 +86,7 @@ async def list_sessions(project_id: str) -> List[SessionResponse]:
90
86
  }
91
87
  ]
92
88
  """
93
- registry = _get_registry()
89
+ registry = _get_registry(request)
94
90
  project = registry.get(project_id)
95
91
 
96
92
  if project is None:
@@ -103,7 +99,9 @@ async def list_sessions(project_id: str) -> List[SessionResponse]:
103
99
  @router.post(
104
100
  "/projects/{project_id}/sessions", response_model=SessionResponse, status_code=201
105
101
  )
106
- async def create_session(project_id: str, req: CreateSessionRequest) -> SessionResponse:
102
+ async def create_session(
103
+ request: Request, project_id: str, req: CreateSessionRequest
104
+ ) -> SessionResponse:
107
105
  """Create a new session for a project.
108
106
 
109
107
  Creates a new tmux pane and initializes the specified runtime adapter.
@@ -135,8 +133,8 @@ async def create_session(project_id: str, req: CreateSessionRequest) -> SessionR
135
133
  "created_at": "2025-01-12T10:00:00Z"
136
134
  }
137
135
  """
138
- registry = _get_registry()
139
- tmux_orch = _get_tmux()
136
+ registry = _get_registry(request)
137
+ tmux_orch = _get_tmux(request)
140
138
 
141
139
  # Validate project exists
142
140
  project = registry.get(project_id)
@@ -181,7 +179,7 @@ async def create_session(project_id: str, req: CreateSessionRequest) -> SessionR
181
179
 
182
180
 
183
181
  @router.delete("/sessions/{session_id}", status_code=204)
184
- async def stop_session(session_id: str) -> Response:
182
+ async def stop_session(request: Request, session_id: str) -> Response:
185
183
  """Stop and remove a session.
186
184
 
187
185
  Kills the tmux pane and removes the session from its project.
@@ -199,8 +197,8 @@ async def stop_session(session_id: str) -> Response:
199
197
  DELETE /api/sessions/sess-456
200
198
  Response: 204 No Content
201
199
  """
202
- registry = _get_registry()
203
- tmux_orch = _get_tmux()
200
+ registry = _get_registry(request)
201
+ tmux_orch = _get_tmux(request)
204
202
 
205
203
  # Find session across all projects
206
204
  session = None
@@ -4,25 +4,39 @@ This module implements REST endpoints for managing work items
4
4
  in project work queues.
5
5
  """
6
6
 
7
- from typing import List, Optional
7
+ from typing import Dict, List, Optional
8
8
 
9
- from fastapi import APIRouter, HTTPException, Query
9
+ from fastapi import APIRouter, HTTPException, Query, Request
10
10
 
11
11
  from ...models.work import WorkPriority, WorkState
12
12
  from ...work import WorkQueue
13
- from ..errors import ProjectNotFoundError
14
13
  from ..schemas import CreateWorkRequest, WorkItemResponse
15
14
 
16
15
  router = APIRouter()
17
16
 
18
17
 
19
- def _get_registry():
20
- """Get registry instance from app global."""
21
- from ..app import registry
22
-
23
- if registry is None:
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:
24
21
  raise RuntimeError("Registry not initialized")
25
- return registry
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
26
40
 
27
41
 
28
42
  def _work_item_to_response(work_item) -> WorkItemResponse:
@@ -51,10 +65,13 @@ def _work_item_to_response(work_item) -> WorkItemResponse:
51
65
 
52
66
 
53
67
  @router.post("/projects/{project_id}/work", response_model=WorkItemResponse)
54
- async def add_work(project_id: str, work: CreateWorkRequest) -> WorkItemResponse:
68
+ async def add_work(
69
+ request: Request, project_id: str, work: CreateWorkRequest
70
+ ) -> WorkItemResponse:
55
71
  """Add work item to project queue.
56
72
 
57
73
  Args:
74
+ request: FastAPI request (for accessing app.state)
58
75
  project_id: Project identifier
59
76
  work: Work item creation request
60
77
 
@@ -80,22 +97,29 @@ async def add_work(project_id: str, work: CreateWorkRequest) -> WorkItemResponse
80
97
  ...
81
98
  }
82
99
  """
83
- registry = _get_registry()
100
+ registry = _get_registry(request)
101
+ work_queues = _get_work_queues(request)
102
+ daemon = _get_daemon(request)
84
103
 
85
104
  # 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
105
+ project = registry.get(project_id)
106
+ if project is None:
107
+ raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
90
108
 
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)
109
+ # Get or create work queue (shared with daemon)
110
+ import logging
97
111
 
98
- queue = project._work_queue
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]
99
123
 
100
124
  # Convert priority int to enum
101
125
  priority = WorkPriority(work.priority)
@@ -105,16 +129,25 @@ async def add_work(project_id: str, work: CreateWorkRequest) -> WorkItemResponse
105
129
  content=work.content, priority=priority, depends_on=work.depends_on
106
130
  )
107
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
+
108
138
  return _work_item_to_response(work_item)
109
139
 
110
140
 
111
141
  @router.get("/projects/{project_id}/work", response_model=List[WorkItemResponse])
112
142
  async def list_work(
113
- project_id: str, state: Optional[str] = Query(None, description="Filter by state")
143
+ request: Request,
144
+ project_id: str,
145
+ state: Optional[str] = Query(None, description="Filter by state"),
114
146
  ) -> List[WorkItemResponse]:
115
147
  """List work items for project.
116
148
 
117
149
  Args:
150
+ request: FastAPI request (for accessing app.state)
118
151
  project_id: Project identifier
119
152
  state: Optional state filter (pending, queued, in_progress, etc.)
120
153
 
@@ -136,19 +169,20 @@ async def list_work(
136
169
  {"id": "work-1", "state": "queued", ...}
137
170
  ]
138
171
  """
139
- registry = _get_registry()
172
+ registry = _get_registry(request)
173
+ work_queues = _get_work_queues(request)
140
174
 
141
175
  # 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
176
+ project = registry.get(project_id)
177
+ if project is None:
178
+ raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
146
179
 
147
- # Get work queue
148
- if not hasattr(project, "_work_queue"):
149
- project._work_queue = WorkQueue(project_id)
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 []
150
184
 
151
- queue = project._work_queue
185
+ queue = work_queues[project_id]
152
186
 
153
187
  # Parse state filter
154
188
  state_filter = None
@@ -169,10 +203,11 @@ async def list_work(
169
203
 
170
204
 
171
205
  @router.get("/projects/{project_id}/work/{work_id}", response_model=WorkItemResponse)
172
- async def get_work(project_id: str, work_id: str) -> WorkItemResponse:
206
+ async def get_work(request: Request, project_id: str, work_id: str) -> WorkItemResponse:
173
207
  """Get work item details.
174
208
 
175
209
  Args:
210
+ request: FastAPI request (for accessing app.state)
176
211
  project_id: Project identifier
177
212
  work_id: Work item identifier
178
213
 
@@ -191,19 +226,19 @@ async def get_work(project_id: str, work_id: str) -> WorkItemResponse:
191
226
  ...
192
227
  }
193
228
  """
194
- registry = _get_registry()
229
+ registry = _get_registry(request)
230
+ work_queues = _get_work_queues(request)
195
231
 
196
232
  # 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
233
+ project = registry.get(project_id)
234
+ if project is None:
235
+ raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
201
236
 
202
- # Get work queue
203
- if not hasattr(project, "_work_queue"):
237
+ # Get work queue (shared with daemon)
238
+ if project_id not in work_queues:
204
239
  raise HTTPException(status_code=404, detail="Work queue not found")
205
240
 
206
- queue = project._work_queue
241
+ queue = work_queues[project_id]
207
242
 
208
243
  # Get work item
209
244
  work_item = queue.get(work_id)
@@ -214,10 +249,11 @@ async def get_work(project_id: str, work_id: str) -> WorkItemResponse:
214
249
 
215
250
 
216
251
  @router.post("/projects/{project_id}/work/{work_id}/cancel")
217
- async def cancel_work(project_id: str, work_id: str) -> dict:
252
+ async def cancel_work(request: Request, project_id: str, work_id: str) -> dict:
218
253
  """Cancel pending work item.
219
254
 
220
255
  Args:
256
+ request: FastAPI request (for accessing app.state)
221
257
  project_id: Project identifier
222
258
  work_id: Work item identifier
223
259
 
@@ -231,19 +267,19 @@ async def cancel_work(project_id: str, work_id: str) -> dict:
231
267
  POST /api/projects/proj-123/work/work-xyz/cancel
232
268
  Response: {"status": "cancelled", "id": "work-xyz"}
233
269
  """
234
- registry = _get_registry()
270
+ registry = _get_registry(request)
271
+ work_queues = _get_work_queues(request)
235
272
 
236
273
  # 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
274
+ project = registry.get(project_id)
275
+ if project is None:
276
+ raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
241
277
 
242
- # Get work queue
243
- if not hasattr(project, "_work_queue"):
278
+ # Get work queue (shared with daemon)
279
+ if project_id not in work_queues:
244
280
  raise HTTPException(status_code=404, detail="Work queue not found")
245
281
 
246
- queue = project._work_queue
282
+ queue = work_queues[project_id]
247
283
 
248
284
  # Cancel work item
249
285
  if not queue.cancel(work_id):
@@ -17,10 +17,14 @@ class RegisterProjectRequest(BaseModel):
17
17
 
18
18
  Attributes:
19
19
  path: Filesystem path to project directory
20
+ project_id: Optional project identifier (UUID generated if omitted)
20
21
  name: Optional display name (derived from path if omitted)
21
22
  """
22
23
 
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
+ )
24
28
  name: Optional[str] = Field(
25
29
  None, description="Display name (derived from path if omitted)"
26
30
  )
@@ -5,6 +5,7 @@ import logging
5
5
  from pathlib import Path
6
6
  from typing import Optional
7
7
 
8
+ from claude_mpm.commander.env_loader import load_env
8
9
  from claude_mpm.commander.instance_manager import InstanceManager
9
10
  from claude_mpm.commander.llm.openrouter_client import (
10
11
  OpenRouterClient,
@@ -19,6 +20,9 @@ from claude_mpm.commander.tmux_orchestrator import TmuxOrchestrator
19
20
 
20
21
  from .repl import CommanderREPL
21
22
 
23
+ # Load environment variables at module import
24
+ load_env()
25
+
22
26
  logger = logging.getLogger(__name__)
23
27
 
24
28