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,591 @@
1
+ """
2
+ Spec resources for foundry-mcp.
3
+
4
+ Provides MCP resources for accessing specs, journals, and templates.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from mcp.server.fastmcp import FastMCP
13
+
14
+ from foundry_mcp.config import ServerConfig
15
+ from foundry_mcp.core.spec import (
16
+ load_spec,
17
+ list_specs,
18
+ find_specs_directory,
19
+ find_spec_file,
20
+ )
21
+ from foundry_mcp.core.journal import get_journal_entries
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ # Schema version for resource responses
27
+ SCHEMA_VERSION = "1.0.0"
28
+
29
+
30
+ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
31
+ """
32
+ Register spec resources with the FastMCP server.
33
+
34
+ Args:
35
+ mcp: FastMCP server instance
36
+ config: Server configuration
37
+ """
38
+
39
+ def _get_specs_dir(workspace: Optional[str] = None) -> Optional[Path]:
40
+ """Get the specs directory for the given workspace."""
41
+ if workspace:
42
+ ws_path = Path(workspace)
43
+ if ws_path.is_dir():
44
+ specs_path = ws_path / "specs"
45
+ if specs_path.is_dir():
46
+ return specs_path
47
+ return find_specs_directory(workspace)
48
+ return config.specs_dir or find_specs_directory()
49
+
50
+ def _validate_sandbox(path: Path, workspace: Optional[Path] = None) -> bool:
51
+ """
52
+ Validate that a path is within the workspace sandbox.
53
+
54
+ Args:
55
+ path: Path to validate
56
+ workspace: Workspace root (defaults to specs_dir parent)
57
+
58
+ Returns:
59
+ True if path is within sandbox, False otherwise
60
+ """
61
+ if workspace is None:
62
+ specs_dir = _get_specs_dir()
63
+ if specs_dir:
64
+ workspace = specs_dir.parent
65
+ else:
66
+ return False
67
+
68
+ try:
69
+ path.resolve().relative_to(workspace.resolve())
70
+ return True
71
+ except ValueError:
72
+ return False
73
+
74
+ # Resource: foundry://specs/ - List all specs
75
+ @mcp.resource("foundry://specs/")
76
+ def resource_specs_list() -> str:
77
+ """
78
+ List all specifications.
79
+
80
+ Returns JSON with all specs across all status folders.
81
+ """
82
+ specs_dir = _get_specs_dir()
83
+ if not specs_dir:
84
+ return json.dumps({
85
+ "success": False,
86
+ "schema_version": SCHEMA_VERSION,
87
+ "error": "No specs directory found",
88
+ }, separators=(",", ":"))
89
+
90
+ specs = list_specs(specs_dir=specs_dir)
91
+
92
+ return json.dumps({
93
+ "success": True,
94
+ "schema_version": SCHEMA_VERSION,
95
+ "specs": specs,
96
+ "count": len(specs),
97
+ }, separators=(",", ":"))
98
+
99
+ # Resource: foundry://specs/{status}/ - List specs by status
100
+ @mcp.resource("foundry://specs/{status}/")
101
+ def resource_specs_by_status(status: str) -> str:
102
+ """
103
+ List specifications filtered by status.
104
+
105
+ Args:
106
+ status: Status folder (active, pending, completed, archived)
107
+
108
+ Returns JSON with specs in the specified status folder.
109
+ """
110
+ valid_statuses = {"active", "pending", "completed", "archived"}
111
+ if status not in valid_statuses:
112
+ return json.dumps({
113
+ "success": False,
114
+ "schema_version": SCHEMA_VERSION,
115
+ "error": f"Invalid status: {status}. Must be one of: {', '.join(sorted(valid_statuses))}",
116
+ }, separators=(",", ":"))
117
+
118
+ specs_dir = _get_specs_dir()
119
+ if not specs_dir:
120
+ return json.dumps({
121
+ "success": False,
122
+ "schema_version": SCHEMA_VERSION,
123
+ "error": "No specs directory found",
124
+ }, separators=(",", ":"))
125
+
126
+ specs = list_specs(specs_dir=specs_dir, status=status)
127
+
128
+ return json.dumps({
129
+ "success": True,
130
+ "schema_version": SCHEMA_VERSION,
131
+ "status": status,
132
+ "specs": specs,
133
+ "count": len(specs),
134
+ }, separators=(",", ":"))
135
+
136
+ # Resource: foundry://specs/{status}/{spec_id} - Get specific spec
137
+ @mcp.resource("foundry://specs/{status}/{spec_id}")
138
+ def resource_spec_by_status(status: str, spec_id: str) -> str:
139
+ """
140
+ Get a specification by status and ID.
141
+
142
+ Args:
143
+ status: Status folder (active, pending, completed, archived)
144
+ spec_id: Specification ID
145
+
146
+ Returns JSON with full spec data.
147
+ """
148
+ valid_statuses = {"active", "pending", "completed", "archived"}
149
+ if status not in valid_statuses:
150
+ return json.dumps({
151
+ "success": False,
152
+ "schema_version": SCHEMA_VERSION,
153
+ "error": f"Invalid status: {status}. Must be one of: {', '.join(sorted(valid_statuses))}",
154
+ }, separators=(",", ":"))
155
+
156
+ specs_dir = _get_specs_dir()
157
+ if not specs_dir:
158
+ return json.dumps({
159
+ "success": False,
160
+ "schema_version": SCHEMA_VERSION,
161
+ "error": "No specs directory found",
162
+ }, separators=(",", ":"))
163
+
164
+ # Verify spec is in the specified status folder
165
+ spec_file = specs_dir / status / f"{spec_id}.json"
166
+ if not spec_file.exists():
167
+ return json.dumps({
168
+ "success": False,
169
+ "schema_version": SCHEMA_VERSION,
170
+ "error": f"Spec not found in {status}: {spec_id}",
171
+ }, separators=(",", ":"))
172
+
173
+ # Validate sandbox
174
+ if not _validate_sandbox(spec_file):
175
+ return json.dumps({
176
+ "success": False,
177
+ "schema_version": SCHEMA_VERSION,
178
+ "error": "Access denied: path outside workspace sandbox",
179
+ }, separators=(",", ":"))
180
+
181
+ spec_data = load_spec(spec_id, specs_dir)
182
+ if spec_data is None:
183
+ return json.dumps({
184
+ "success": False,
185
+ "schema_version": SCHEMA_VERSION,
186
+ "error": f"Failed to load spec: {spec_id}",
187
+ }, separators=(",", ":"))
188
+
189
+ # Calculate progress
190
+ hierarchy = spec_data.get("hierarchy", {})
191
+ total_tasks = len(hierarchy)
192
+ completed_tasks = sum(
193
+ 1 for task in hierarchy.values()
194
+ if task.get("status") == "completed"
195
+ )
196
+
197
+ return json.dumps({
198
+ "success": True,
199
+ "schema_version": SCHEMA_VERSION,
200
+ "spec_id": spec_id,
201
+ "status": status,
202
+ "title": spec_data.get("metadata", {}).get("title", spec_data.get("title", "Untitled")),
203
+ "total_tasks": total_tasks,
204
+ "completed_tasks": completed_tasks,
205
+ "progress_percentage": int((completed_tasks / total_tasks * 100)) if total_tasks > 0 else 0,
206
+ "hierarchy": hierarchy,
207
+ "metadata": spec_data.get("metadata", {}),
208
+ "journal": spec_data.get("journal", []),
209
+ }, separators=(",", ":"))
210
+
211
+ # Resource: foundry://specs/{spec_id}/journal - Get spec journal
212
+ @mcp.resource("foundry://specs/{spec_id}/journal")
213
+ def resource_spec_journal(spec_id: str) -> str:
214
+ """
215
+ Get journal entries for a specification.
216
+
217
+ Args:
218
+ spec_id: Specification ID
219
+
220
+ Returns JSON with journal entries.
221
+ """
222
+ specs_dir = _get_specs_dir()
223
+ if not specs_dir:
224
+ return json.dumps({
225
+ "success": False,
226
+ "schema_version": SCHEMA_VERSION,
227
+ "error": "No specs directory found",
228
+ }, separators=(",", ":"))
229
+
230
+ # Find spec file (in any status folder)
231
+ spec_file = find_spec_file(spec_id, specs_dir)
232
+ if not spec_file:
233
+ return json.dumps({
234
+ "success": False,
235
+ "schema_version": SCHEMA_VERSION,
236
+ "error": f"Spec not found: {spec_id}",
237
+ }, separators=(",", ":"))
238
+
239
+ # Validate sandbox
240
+ if not _validate_sandbox(spec_file):
241
+ return json.dumps({
242
+ "success": False,
243
+ "schema_version": SCHEMA_VERSION,
244
+ "error": "Access denied: path outside workspace sandbox",
245
+ }, separators=(",", ":"))
246
+
247
+ spec_data = load_spec(spec_id, specs_dir)
248
+ if spec_data is None:
249
+ return json.dumps({
250
+ "success": False,
251
+ "schema_version": SCHEMA_VERSION,
252
+ "error": f"Failed to load spec: {spec_id}",
253
+ }, separators=(",", ":"))
254
+
255
+ # Get journal entries
256
+ entries = get_journal_entries(spec_data)
257
+
258
+ # Convert to serializable format
259
+ journal_data = [
260
+ {
261
+ "timestamp": entry.timestamp,
262
+ "entry_type": entry.entry_type,
263
+ "title": entry.title,
264
+ "content": entry.content,
265
+ "author": entry.author,
266
+ "task_id": entry.task_id,
267
+ "metadata": entry.metadata,
268
+ }
269
+ for entry in entries
270
+ ]
271
+
272
+ return json.dumps({
273
+ "success": True,
274
+ "schema_version": SCHEMA_VERSION,
275
+ "spec_id": spec_id,
276
+ "journal": journal_data,
277
+ "count": len(journal_data),
278
+ }, separators=(",", ":"))
279
+
280
+ # Resource: foundry://templates/ - List available templates
281
+ @mcp.resource("foundry://templates/")
282
+ def resource_templates_list() -> str:
283
+ """
284
+ List available spec templates.
285
+
286
+ Returns JSON with template information.
287
+ """
288
+ specs_dir = _get_specs_dir()
289
+ if not specs_dir:
290
+ return json.dumps({
291
+ "success": False,
292
+ "schema_version": SCHEMA_VERSION,
293
+ "error": "No specs directory found",
294
+ }, separators=(",", ":"))
295
+
296
+ # Look for templates in specs/templates/ directory
297
+ templates_dir = specs_dir / "templates"
298
+ templates = []
299
+
300
+ if templates_dir.is_dir():
301
+ for template_file in sorted(templates_dir.glob("*.json")):
302
+ try:
303
+ with open(template_file, "r") as f:
304
+ template_data = json.load(f)
305
+
306
+ templates.append({
307
+ "template_id": template_file.stem,
308
+ "title": template_data.get("metadata", {}).get("title", template_data.get("title", template_file.stem)),
309
+ "description": template_data.get("metadata", {}).get("description", ""),
310
+ "file": str(template_file.name),
311
+ })
312
+ except (json.JSONDecodeError, IOError):
313
+ # Skip invalid templates
314
+ continue
315
+
316
+ # Add built-in templates info
317
+ builtin_templates = [
318
+ {
319
+ "template_id": "basic",
320
+ "title": "Basic Spec",
321
+ "description": "Minimal spec with a single phase",
322
+ "builtin": True,
323
+ },
324
+ {
325
+ "template_id": "feature",
326
+ "title": "Feature Development",
327
+ "description": "Standard feature with design, implementation, and verification phases",
328
+ "builtin": True,
329
+ },
330
+ {
331
+ "template_id": "bugfix",
332
+ "title": "Bug Fix",
333
+ "description": "Bug investigation, fix, and verification",
334
+ "builtin": True,
335
+ },
336
+ ]
337
+
338
+ return json.dumps({
339
+ "success": True,
340
+ "schema_version": SCHEMA_VERSION,
341
+ "templates": templates,
342
+ "builtin_templates": builtin_templates,
343
+ "count": len(templates),
344
+ "builtin_count": len(builtin_templates),
345
+ }, separators=(",", ":"))
346
+
347
+ # Resource: foundry://templates/{template_id} - Get specific template
348
+ @mcp.resource("foundry://templates/{template_id}")
349
+ def resource_template(template_id: str) -> str:
350
+ """
351
+ Get a specific template by ID.
352
+
353
+ Args:
354
+ template_id: Template ID
355
+
356
+ Returns JSON with template data.
357
+ """
358
+ specs_dir = _get_specs_dir()
359
+ if not specs_dir:
360
+ return json.dumps({
361
+ "success": False,
362
+ "schema_version": SCHEMA_VERSION,
363
+ "error": "No specs directory found",
364
+ }, separators=(",", ":"))
365
+
366
+ # Check for custom template
367
+ templates_dir = specs_dir / "templates"
368
+ template_file = templates_dir / f"{template_id}.json"
369
+
370
+ if template_file.exists():
371
+ # Validate sandbox
372
+ if not _validate_sandbox(template_file):
373
+ return json.dumps({
374
+ "success": False,
375
+ "schema_version": SCHEMA_VERSION,
376
+ "error": "Access denied: path outside workspace sandbox",
377
+ }, separators=(",", ":"))
378
+
379
+ try:
380
+ with open(template_file, "r") as f:
381
+ template_data = json.load(f)
382
+
383
+ return json.dumps({
384
+ "success": True,
385
+ "schema_version": SCHEMA_VERSION,
386
+ "template_id": template_id,
387
+ "template": template_data,
388
+ "builtin": False,
389
+ }, separators=(",", ":"))
390
+ except (json.JSONDecodeError, IOError) as e:
391
+ return json.dumps({
392
+ "success": False,
393
+ "schema_version": SCHEMA_VERSION,
394
+ "error": f"Failed to load template: {e}",
395
+ }, separators=(",", ":"))
396
+
397
+ # Check for builtin template
398
+ builtin_templates = {
399
+ "basic": _get_basic_template(),
400
+ "feature": _get_feature_template(),
401
+ "bugfix": _get_bugfix_template(),
402
+ }
403
+
404
+ if template_id in builtin_templates:
405
+ return json.dumps({
406
+ "success": True,
407
+ "schema_version": SCHEMA_VERSION,
408
+ "template_id": template_id,
409
+ "template": builtin_templates[template_id],
410
+ "builtin": True,
411
+ }, separators=(",", ":"))
412
+
413
+ return json.dumps({
414
+ "success": False,
415
+ "schema_version": SCHEMA_VERSION,
416
+ "error": f"Template not found: {template_id}",
417
+ }, separators=(",", ":"))
418
+
419
+ logger.debug("Registered spec resources: foundry://specs/, foundry://specs/{status}/, "
420
+ "foundry://specs/{status}/{spec_id}, foundry://specs/{spec_id}/journal, "
421
+ "foundry://templates/, foundry://templates/{template_id}")
422
+
423
+
424
+ def _get_basic_template() -> dict:
425
+ """Get the basic builtin template."""
426
+ return {
427
+ "spec_id": "{{spec_id}}",
428
+ "title": "{{title}}",
429
+ "metadata": {
430
+ "title": "{{title}}",
431
+ "description": "{{description}}",
432
+ "created_at": "{{timestamp}}",
433
+ },
434
+ "hierarchy": {
435
+ "spec-root": {
436
+ "type": "spec",
437
+ "title": "{{title}}",
438
+ "status": "pending",
439
+ "children": ["phase-1"],
440
+ },
441
+ "phase-1": {
442
+ "type": "phase",
443
+ "title": "Implementation",
444
+ "status": "pending",
445
+ "parent": "spec-root",
446
+ "children": ["task-1-1"],
447
+ },
448
+ "task-1-1": {
449
+ "type": "task",
450
+ "title": "Initial task",
451
+ "status": "pending",
452
+ "parent": "phase-1",
453
+ "children": [],
454
+ },
455
+ },
456
+ "journal": [],
457
+ }
458
+
459
+
460
+ def _get_feature_template() -> dict:
461
+ """Get the feature development builtin template."""
462
+ return {
463
+ "spec_id": "{{spec_id}}",
464
+ "title": "{{title}}",
465
+ "metadata": {
466
+ "title": "{{title}}",
467
+ "description": "{{description}}",
468
+ "created_at": "{{timestamp}}",
469
+ },
470
+ "hierarchy": {
471
+ "spec-root": {
472
+ "type": "spec",
473
+ "title": "{{title}}",
474
+ "status": "pending",
475
+ "children": ["phase-1", "phase-2", "phase-3"],
476
+ },
477
+ "phase-1": {
478
+ "type": "phase",
479
+ "title": "Design",
480
+ "status": "pending",
481
+ "parent": "spec-root",
482
+ "children": ["task-1-1"],
483
+ },
484
+ "task-1-1": {
485
+ "type": "task",
486
+ "title": "Design document",
487
+ "status": "pending",
488
+ "parent": "phase-1",
489
+ "children": [],
490
+ },
491
+ "phase-2": {
492
+ "type": "phase",
493
+ "title": "Implementation",
494
+ "status": "pending",
495
+ "parent": "spec-root",
496
+ "children": ["task-2-1"],
497
+ },
498
+ "task-2-1": {
499
+ "type": "task",
500
+ "title": "Core implementation",
501
+ "status": "pending",
502
+ "parent": "phase-2",
503
+ "children": [],
504
+ },
505
+ "phase-3": {
506
+ "type": "phase",
507
+ "title": "Verification",
508
+ "status": "pending",
509
+ "parent": "spec-root",
510
+ "children": ["verify-3-1"],
511
+ },
512
+ "verify-3-1": {
513
+ "type": "verify",
514
+ "title": "All tests pass",
515
+ "status": "pending",
516
+ "parent": "phase-3",
517
+ "children": [],
518
+ "metadata": {
519
+ "verification_type": "run-tests",
520
+ },
521
+ },
522
+ },
523
+ "journal": [],
524
+ }
525
+
526
+
527
+ def _get_bugfix_template() -> dict:
528
+ """Get the bugfix builtin template."""
529
+ return {
530
+ "spec_id": "{{spec_id}}",
531
+ "title": "{{title}}",
532
+ "metadata": {
533
+ "title": "{{title}}",
534
+ "description": "{{description}}",
535
+ "created_at": "{{timestamp}}",
536
+ },
537
+ "hierarchy": {
538
+ "spec-root": {
539
+ "type": "spec",
540
+ "title": "{{title}}",
541
+ "status": "pending",
542
+ "children": ["phase-1", "phase-2"],
543
+ },
544
+ "phase-1": {
545
+ "type": "phase",
546
+ "title": "Investigation",
547
+ "status": "pending",
548
+ "parent": "spec-root",
549
+ "children": ["task-1-1", "task-1-2"],
550
+ },
551
+ "task-1-1": {
552
+ "type": "task",
553
+ "title": "Reproduce bug",
554
+ "status": "pending",
555
+ "parent": "phase-1",
556
+ "children": [],
557
+ },
558
+ "task-1-2": {
559
+ "type": "task",
560
+ "title": "Root cause analysis",
561
+ "status": "pending",
562
+ "parent": "phase-1",
563
+ "children": [],
564
+ },
565
+ "phase-2": {
566
+ "type": "phase",
567
+ "title": "Fix & Verify",
568
+ "status": "pending",
569
+ "parent": "spec-root",
570
+ "children": ["task-2-1", "verify-2-1"],
571
+ },
572
+ "task-2-1": {
573
+ "type": "task",
574
+ "title": "Implement fix",
575
+ "status": "pending",
576
+ "parent": "phase-2",
577
+ "children": [],
578
+ },
579
+ "verify-2-1": {
580
+ "type": "verify",
581
+ "title": "Bug no longer reproduces",
582
+ "status": "pending",
583
+ "parent": "phase-2",
584
+ "children": [],
585
+ "metadata": {
586
+ "verification_type": "manual",
587
+ },
588
+ },
589
+ },
590
+ "journal": [],
591
+ }
@@ -0,0 +1,38 @@
1
+ """Bundled JSON schemas for SDD specifications."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Optional, Tuple
6
+
7
+
8
+ def load_schema(name: str = "sdd-spec-schema.json") -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
9
+ """Load a bundled JSON schema by name.
10
+
11
+ Args:
12
+ name: Schema filename (default: sdd-spec-schema.json).
13
+
14
+ Returns:
15
+ Tuple of (schema_dict, error_message). On success, error is None.
16
+ On failure, schema is None and error contains the reason.
17
+ """
18
+ schema_path = Path(__file__).parent / name
19
+
20
+ if not schema_path.exists():
21
+ return None, f"Schema file not found: {name}"
22
+
23
+ try:
24
+ with open(schema_path, "r") as f:
25
+ return json.load(f), None
26
+ except json.JSONDecodeError as e:
27
+ return None, f"Invalid JSON in schema: {e}"
28
+ except Exception as e:
29
+ return None, f"Error loading schema: {e}"
30
+
31
+
32
+ def get_spec_schema() -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
33
+ """Load the SDD spec JSON schema.
34
+
35
+ Returns:
36
+ Tuple of (schema_dict, error_message).
37
+ """
38
+ return load_schema("sdd-spec-schema.json")