foundry-mcp 0.8.22__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 foundry-mcp might be problematic. Click here for more details.

Files changed (153) hide show
  1. foundry_mcp/__init__.py +13 -0
  2. foundry_mcp/cli/__init__.py +67 -0
  3. foundry_mcp/cli/__main__.py +9 -0
  4. foundry_mcp/cli/agent.py +96 -0
  5. foundry_mcp/cli/commands/__init__.py +37 -0
  6. foundry_mcp/cli/commands/cache.py +137 -0
  7. foundry_mcp/cli/commands/dashboard.py +148 -0
  8. foundry_mcp/cli/commands/dev.py +446 -0
  9. foundry_mcp/cli/commands/journal.py +377 -0
  10. foundry_mcp/cli/commands/lifecycle.py +274 -0
  11. foundry_mcp/cli/commands/modify.py +824 -0
  12. foundry_mcp/cli/commands/plan.py +640 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +667 -0
  15. foundry_mcp/cli/commands/session.py +472 -0
  16. foundry_mcp/cli/commands/specs.py +686 -0
  17. foundry_mcp/cli/commands/tasks.py +807 -0
  18. foundry_mcp/cli/commands/testing.py +676 -0
  19. foundry_mcp/cli/commands/validate.py +982 -0
  20. foundry_mcp/cli/config.py +98 -0
  21. foundry_mcp/cli/context.py +298 -0
  22. foundry_mcp/cli/logging.py +212 -0
  23. foundry_mcp/cli/main.py +44 -0
  24. foundry_mcp/cli/output.py +122 -0
  25. foundry_mcp/cli/registry.py +110 -0
  26. foundry_mcp/cli/resilience.py +178 -0
  27. foundry_mcp/cli/transcript.py +217 -0
  28. foundry_mcp/config.py +1454 -0
  29. foundry_mcp/core/__init__.py +144 -0
  30. foundry_mcp/core/ai_consultation.py +1773 -0
  31. foundry_mcp/core/batch_operations.py +1202 -0
  32. foundry_mcp/core/cache.py +195 -0
  33. foundry_mcp/core/capabilities.py +446 -0
  34. foundry_mcp/core/concurrency.py +898 -0
  35. foundry_mcp/core/context.py +540 -0
  36. foundry_mcp/core/discovery.py +1603 -0
  37. foundry_mcp/core/error_collection.py +728 -0
  38. foundry_mcp/core/error_store.py +592 -0
  39. foundry_mcp/core/health.py +749 -0
  40. foundry_mcp/core/intake.py +933 -0
  41. foundry_mcp/core/journal.py +700 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1376 -0
  44. foundry_mcp/core/llm_patterns.py +510 -0
  45. foundry_mcp/core/llm_provider.py +1569 -0
  46. foundry_mcp/core/logging_config.py +374 -0
  47. foundry_mcp/core/metrics_persistence.py +584 -0
  48. foundry_mcp/core/metrics_registry.py +327 -0
  49. foundry_mcp/core/metrics_store.py +641 -0
  50. foundry_mcp/core/modifications.py +224 -0
  51. foundry_mcp/core/naming.py +146 -0
  52. foundry_mcp/core/observability.py +1216 -0
  53. foundry_mcp/core/otel.py +452 -0
  54. foundry_mcp/core/otel_stubs.py +264 -0
  55. foundry_mcp/core/pagination.py +255 -0
  56. foundry_mcp/core/progress.py +387 -0
  57. foundry_mcp/core/prometheus.py +564 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +691 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
  61. foundry_mcp/core/prompts/plan_review.py +627 -0
  62. foundry_mcp/core/providers/__init__.py +237 -0
  63. foundry_mcp/core/providers/base.py +515 -0
  64. foundry_mcp/core/providers/claude.py +472 -0
  65. foundry_mcp/core/providers/codex.py +637 -0
  66. foundry_mcp/core/providers/cursor_agent.py +630 -0
  67. foundry_mcp/core/providers/detectors.py +515 -0
  68. foundry_mcp/core/providers/gemini.py +426 -0
  69. foundry_mcp/core/providers/opencode.py +718 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +308 -0
  71. foundry_mcp/core/providers/package-lock.json +24 -0
  72. foundry_mcp/core/providers/package.json +25 -0
  73. foundry_mcp/core/providers/registry.py +607 -0
  74. foundry_mcp/core/providers/test_provider.py +171 -0
  75. foundry_mcp/core/providers/validation.py +857 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/research/__init__.py +68 -0
  78. foundry_mcp/core/research/memory.py +528 -0
  79. foundry_mcp/core/research/models.py +1234 -0
  80. foundry_mcp/core/research/providers/__init__.py +40 -0
  81. foundry_mcp/core/research/providers/base.py +242 -0
  82. foundry_mcp/core/research/providers/google.py +507 -0
  83. foundry_mcp/core/research/providers/perplexity.py +442 -0
  84. foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
  85. foundry_mcp/core/research/providers/tavily.py +383 -0
  86. foundry_mcp/core/research/workflows/__init__.py +25 -0
  87. foundry_mcp/core/research/workflows/base.py +298 -0
  88. foundry_mcp/core/research/workflows/chat.py +271 -0
  89. foundry_mcp/core/research/workflows/consensus.py +539 -0
  90. foundry_mcp/core/research/workflows/deep_research.py +4142 -0
  91. foundry_mcp/core/research/workflows/ideate.py +682 -0
  92. foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
  93. foundry_mcp/core/resilience.py +600 -0
  94. foundry_mcp/core/responses.py +1624 -0
  95. foundry_mcp/core/review.py +366 -0
  96. foundry_mcp/core/security.py +438 -0
  97. foundry_mcp/core/spec.py +4119 -0
  98. foundry_mcp/core/task.py +2463 -0
  99. foundry_mcp/core/testing.py +839 -0
  100. foundry_mcp/core/validation.py +2357 -0
  101. foundry_mcp/dashboard/__init__.py +32 -0
  102. foundry_mcp/dashboard/app.py +119 -0
  103. foundry_mcp/dashboard/components/__init__.py +17 -0
  104. foundry_mcp/dashboard/components/cards.py +88 -0
  105. foundry_mcp/dashboard/components/charts.py +177 -0
  106. foundry_mcp/dashboard/components/filters.py +136 -0
  107. foundry_mcp/dashboard/components/tables.py +195 -0
  108. foundry_mcp/dashboard/data/__init__.py +11 -0
  109. foundry_mcp/dashboard/data/stores.py +433 -0
  110. foundry_mcp/dashboard/launcher.py +300 -0
  111. foundry_mcp/dashboard/views/__init__.py +12 -0
  112. foundry_mcp/dashboard/views/errors.py +217 -0
  113. foundry_mcp/dashboard/views/metrics.py +164 -0
  114. foundry_mcp/dashboard/views/overview.py +96 -0
  115. foundry_mcp/dashboard/views/providers.py +83 -0
  116. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  117. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  118. foundry_mcp/prompts/__init__.py +9 -0
  119. foundry_mcp/prompts/workflows.py +525 -0
  120. foundry_mcp/resources/__init__.py +9 -0
  121. foundry_mcp/resources/specs.py +591 -0
  122. foundry_mcp/schemas/__init__.py +38 -0
  123. foundry_mcp/schemas/intake-schema.json +89 -0
  124. foundry_mcp/schemas/sdd-spec-schema.json +414 -0
  125. foundry_mcp/server.py +150 -0
  126. foundry_mcp/tools/__init__.py +10 -0
  127. foundry_mcp/tools/unified/__init__.py +92 -0
  128. foundry_mcp/tools/unified/authoring.py +3620 -0
  129. foundry_mcp/tools/unified/context_helpers.py +98 -0
  130. foundry_mcp/tools/unified/documentation_helpers.py +268 -0
  131. foundry_mcp/tools/unified/environment.py +1341 -0
  132. foundry_mcp/tools/unified/error.py +479 -0
  133. foundry_mcp/tools/unified/health.py +225 -0
  134. foundry_mcp/tools/unified/journal.py +841 -0
  135. foundry_mcp/tools/unified/lifecycle.py +640 -0
  136. foundry_mcp/tools/unified/metrics.py +777 -0
  137. foundry_mcp/tools/unified/plan.py +876 -0
  138. foundry_mcp/tools/unified/pr.py +294 -0
  139. foundry_mcp/tools/unified/provider.py +589 -0
  140. foundry_mcp/tools/unified/research.py +1283 -0
  141. foundry_mcp/tools/unified/review.py +1042 -0
  142. foundry_mcp/tools/unified/review_helpers.py +314 -0
  143. foundry_mcp/tools/unified/router.py +102 -0
  144. foundry_mcp/tools/unified/server.py +565 -0
  145. foundry_mcp/tools/unified/spec.py +1283 -0
  146. foundry_mcp/tools/unified/task.py +3846 -0
  147. foundry_mcp/tools/unified/test.py +431 -0
  148. foundry_mcp/tools/unified/verification.py +520 -0
  149. foundry_mcp-0.8.22.dist-info/METADATA +344 -0
  150. foundry_mcp-0.8.22.dist-info/RECORD +153 -0
  151. foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
  152. foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
  153. foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,412 @@
1
+ """
2
+ Lifecycle operations for SDD spec files.
3
+ Provides spec status transitions: move, activate, complete, archive.
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional
10
+ import json
11
+ import shutil
12
+
13
+
14
+ # Data structures
15
+
16
+ @dataclass
17
+ class MoveResult:
18
+ """
19
+ Result of moving a spec between status folders.
20
+ """
21
+ success: bool
22
+ spec_id: str
23
+ from_folder: str
24
+ to_folder: str
25
+ old_path: Optional[str] = None
26
+ new_path: Optional[str] = None
27
+ error: Optional[str] = None
28
+
29
+
30
+ @dataclass
31
+ class LifecycleState:
32
+ """
33
+ Current lifecycle state of a spec.
34
+ """
35
+ spec_id: str
36
+ folder: str # pending, active, completed, archived
37
+ status: str # from spec-root
38
+ progress_percentage: float
39
+ total_tasks: int
40
+ completed_tasks: int
41
+ can_complete: bool
42
+ can_archive: bool
43
+
44
+
45
+ # Constants
46
+
47
+ VALID_FOLDERS = {"pending", "active", "completed", "archived"}
48
+
49
+ FOLDER_TRANSITIONS = {
50
+ "pending": ["active", "archived"],
51
+ "active": ["pending", "completed", "archived"],
52
+ "completed": ["active", "archived"],
53
+ "archived": ["pending", "active"],
54
+ }
55
+
56
+
57
+ # Main lifecycle functions
58
+
59
+ def move_spec(
60
+ spec_id: str,
61
+ to_folder: str,
62
+ specs_dir: Path,
63
+ update_status: bool = True,
64
+ ) -> MoveResult:
65
+ """
66
+ Move a spec between status folders.
67
+
68
+ Args:
69
+ spec_id: Specification ID
70
+ to_folder: Target folder (pending, active, completed, archived)
71
+ specs_dir: Path to specs directory
72
+ update_status: Whether to update spec-root status to match folder
73
+
74
+ Returns:
75
+ MoveResult with operation outcome
76
+ """
77
+ if to_folder not in VALID_FOLDERS:
78
+ return MoveResult(
79
+ success=False,
80
+ spec_id=spec_id,
81
+ from_folder="",
82
+ to_folder=to_folder,
83
+ error=f"Invalid folder: {to_folder}. Must be one of: {VALID_FOLDERS}",
84
+ )
85
+
86
+ # Find current location
87
+ current_folder, current_path = _find_spec_location(spec_id, specs_dir)
88
+
89
+ if not current_path:
90
+ return MoveResult(
91
+ success=False,
92
+ spec_id=spec_id,
93
+ from_folder="",
94
+ to_folder=to_folder,
95
+ error=f"Spec not found: {spec_id}",
96
+ )
97
+
98
+ if current_folder == to_folder:
99
+ return MoveResult(
100
+ success=True,
101
+ spec_id=spec_id,
102
+ from_folder=current_folder,
103
+ to_folder=to_folder,
104
+ old_path=str(current_path),
105
+ new_path=str(current_path),
106
+ )
107
+
108
+ # Validate transition
109
+ allowed = FOLDER_TRANSITIONS.get(current_folder, [])
110
+ if to_folder not in allowed:
111
+ return MoveResult(
112
+ success=False,
113
+ spec_id=spec_id,
114
+ from_folder=current_folder,
115
+ to_folder=to_folder,
116
+ error=f"Cannot move from {current_folder} to {to_folder}. Allowed: {allowed}",
117
+ )
118
+
119
+ # Ensure target folder exists
120
+ target_dir = specs_dir / to_folder
121
+ target_dir.mkdir(parents=True, exist_ok=True)
122
+
123
+ # Calculate new path
124
+ new_path = target_dir / current_path.name
125
+
126
+ # Check for conflicts
127
+ if new_path.exists():
128
+ return MoveResult(
129
+ success=False,
130
+ spec_id=spec_id,
131
+ from_folder=current_folder,
132
+ to_folder=to_folder,
133
+ error=f"Target already exists: {new_path}",
134
+ )
135
+
136
+ try:
137
+ # Move the file
138
+ shutil.move(str(current_path), str(new_path))
139
+
140
+ # Update spec status if requested
141
+ if update_status:
142
+ _update_spec_folder_status(new_path, to_folder)
143
+
144
+ return MoveResult(
145
+ success=True,
146
+ spec_id=spec_id,
147
+ from_folder=current_folder,
148
+ to_folder=to_folder,
149
+ old_path=str(current_path),
150
+ new_path=str(new_path),
151
+ )
152
+
153
+ except OSError as e:
154
+ return MoveResult(
155
+ success=False,
156
+ spec_id=spec_id,
157
+ from_folder=current_folder,
158
+ to_folder=to_folder,
159
+ error=f"Failed to move file: {e}",
160
+ )
161
+
162
+
163
+ def activate_spec(spec_id: str, specs_dir: Path) -> MoveResult:
164
+ """
165
+ Activate a spec (move from pending to active).
166
+
167
+ Args:
168
+ spec_id: Specification ID
169
+ specs_dir: Path to specs directory
170
+
171
+ Returns:
172
+ MoveResult with operation outcome
173
+ """
174
+ return move_spec(spec_id, "active", specs_dir)
175
+
176
+
177
+ def complete_spec(
178
+ spec_id: str,
179
+ specs_dir: Path,
180
+ force: bool = False,
181
+ ) -> MoveResult:
182
+ """
183
+ Mark a spec as completed (move to completed folder).
184
+
185
+ Args:
186
+ spec_id: Specification ID
187
+ specs_dir: Path to specs directory
188
+ force: Force completion even if tasks are incomplete
189
+
190
+ Returns:
191
+ MoveResult with operation outcome
192
+ """
193
+ # Check if spec can be completed
194
+ if not force:
195
+ state = get_lifecycle_state(spec_id, specs_dir)
196
+ if state and not state.can_complete:
197
+ return MoveResult(
198
+ success=False,
199
+ spec_id=spec_id,
200
+ from_folder=state.folder,
201
+ to_folder="completed",
202
+ error=f"Cannot complete spec: {state.completed_tasks}/{state.total_tasks} tasks done ({state.progress_percentage:.0f}%)",
203
+ )
204
+
205
+ return move_spec(spec_id, "completed", specs_dir)
206
+
207
+
208
+ def archive_spec(spec_id: str, specs_dir: Path) -> MoveResult:
209
+ """
210
+ Archive a spec (move to archived folder).
211
+
212
+ Args:
213
+ spec_id: Specification ID
214
+ specs_dir: Path to specs directory
215
+
216
+ Returns:
217
+ MoveResult with operation outcome
218
+ """
219
+ return move_spec(spec_id, "archived", specs_dir)
220
+
221
+
222
+ def get_lifecycle_state(spec_id: str, specs_dir: Path) -> Optional[LifecycleState]:
223
+ """
224
+ Get the current lifecycle state of a spec.
225
+
226
+ Args:
227
+ spec_id: Specification ID
228
+ specs_dir: Path to specs directory
229
+
230
+ Returns:
231
+ LifecycleState or None if spec not found
232
+ """
233
+ folder, path = _find_spec_location(spec_id, specs_dir)
234
+
235
+ if not path:
236
+ return None
237
+
238
+ try:
239
+ with open(path, "r") as f:
240
+ spec_data = json.load(f)
241
+ except (OSError, json.JSONDecodeError):
242
+ return None
243
+
244
+ hierarchy = spec_data.get("hierarchy", {})
245
+ root = hierarchy.get("spec-root", {})
246
+
247
+ total_tasks = root.get("total_tasks", 0)
248
+ completed_tasks = root.get("completed_tasks", 0)
249
+ status = root.get("status", "pending")
250
+
251
+ progress = (completed_tasks / total_tasks * 100) if total_tasks > 0 else 0
252
+
253
+ # Can complete if all tasks done or in active folder with 100% progress
254
+ can_complete = progress >= 100 or status == "completed"
255
+
256
+ # Can archive from any folder
257
+ can_archive = True
258
+
259
+ return LifecycleState(
260
+ spec_id=spec_id,
261
+ folder=folder,
262
+ status=status,
263
+ progress_percentage=progress,
264
+ total_tasks=total_tasks,
265
+ completed_tasks=completed_tasks,
266
+ can_complete=can_complete,
267
+ can_archive=can_archive,
268
+ )
269
+
270
+
271
+ def list_specs_by_folder(
272
+ specs_dir: Path,
273
+ folder: Optional[str] = None,
274
+ ) -> Dict[str, List[Dict[str, Any]]]:
275
+ """
276
+ List specs organized by folder.
277
+
278
+ Args:
279
+ specs_dir: Path to specs directory
280
+ folder: Optional filter to specific folder
281
+
282
+ Returns:
283
+ Dict mapping folder names to lists of spec summaries
284
+ """
285
+ result: Dict[str, List[Dict[str, Any]]] = {}
286
+
287
+ folders = [folder] if folder else list(VALID_FOLDERS)
288
+
289
+ for f in folders:
290
+ folder_path = specs_dir / f
291
+ if not folder_path.exists():
292
+ result[f] = []
293
+ continue
294
+
295
+ specs = []
296
+ for spec_file in folder_path.glob("*.json"):
297
+ try:
298
+ with open(spec_file, "r") as file:
299
+ data = json.load(file)
300
+
301
+ hierarchy = data.get("hierarchy", {})
302
+ root = hierarchy.get("spec-root", {})
303
+ metadata = data.get("metadata", {})
304
+
305
+ total = root.get("total_tasks", 0)
306
+ completed = root.get("completed_tasks", 0)
307
+
308
+ specs.append({
309
+ "spec_id": data.get("spec_id", spec_file.stem),
310
+ "title": metadata.get("title") or root.get("title", "Untitled"),
311
+ "status": root.get("status", "pending"),
312
+ "total_tasks": total,
313
+ "completed_tasks": completed,
314
+ "progress": (completed / total * 100) if total > 0 else 0,
315
+ "path": str(spec_file),
316
+ })
317
+ except (OSError, json.JSONDecodeError):
318
+ continue
319
+
320
+ result[f] = specs
321
+
322
+ return result
323
+
324
+
325
+ def get_folder_for_spec(spec_id: str, specs_dir: Path) -> Optional[str]:
326
+ """
327
+ Get the folder where a spec is located.
328
+
329
+ Args:
330
+ spec_id: Specification ID
331
+ specs_dir: Path to specs directory
332
+
333
+ Returns:
334
+ Folder name or None if not found
335
+ """
336
+ folder, _ = _find_spec_location(spec_id, specs_dir)
337
+ return folder
338
+
339
+
340
+ # Helper functions
341
+
342
+ def _find_spec_location(
343
+ spec_id: str,
344
+ specs_dir: Path,
345
+ ) -> tuple[Optional[str], Optional[Path]]:
346
+ """
347
+ Find which folder contains a spec.
348
+
349
+ Returns:
350
+ Tuple of (folder_name, path) or (None, None) if not found
351
+ """
352
+ for folder in VALID_FOLDERS:
353
+ folder_path = specs_dir / folder
354
+ if not folder_path.exists():
355
+ continue
356
+
357
+ # Check for exact match
358
+ spec_path = folder_path / f"{spec_id}.json"
359
+ if spec_path.exists():
360
+ return folder, spec_path
361
+
362
+ # Check for files that contain the spec_id
363
+ for f in folder_path.glob("*.json"):
364
+ try:
365
+ with open(f, "r") as file:
366
+ data = json.load(file)
367
+ if data.get("spec_id") == spec_id:
368
+ return folder, f
369
+ except (OSError, json.JSONDecodeError):
370
+ continue
371
+
372
+ return None, None
373
+
374
+
375
+ def _update_spec_folder_status(path: Path, folder: str) -> bool:
376
+ """
377
+ Update spec-root status to match folder.
378
+
379
+ Args:
380
+ path: Path to spec file
381
+ folder: Folder name (maps to status)
382
+
383
+ Returns:
384
+ True if update successful
385
+ """
386
+ folder_to_status = {
387
+ "pending": "pending",
388
+ "active": "in_progress",
389
+ "completed": "completed",
390
+ "archived": "completed",
391
+ }
392
+
393
+ new_status = folder_to_status.get(folder, "pending")
394
+
395
+ try:
396
+ with open(path, "r") as f:
397
+ data = json.load(f)
398
+
399
+ hierarchy = data.get("hierarchy", {})
400
+ if "spec-root" in hierarchy:
401
+ hierarchy["spec-root"]["status"] = new_status
402
+
403
+ # Update last_updated
404
+ data["last_updated"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
405
+
406
+ with open(path, "w") as f:
407
+ json.dump(data, f, indent=2)
408
+
409
+ return True
410
+
411
+ except (OSError, json.JSONDecodeError):
412
+ return False