minder-cli 0.2.0__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 (132) hide show
  1. minder/__init__.py +12 -0
  2. minder/api/routers/prompts.py +177 -0
  3. minder/application/__init__.py +1 -0
  4. minder/application/admin/__init__.py +11 -0
  5. minder/application/admin/dto.py +453 -0
  6. minder/application/admin/jobs.py +327 -0
  7. minder/application/admin/use_cases.py +1895 -0
  8. minder/auth/__init__.py +12 -0
  9. minder/auth/context.py +26 -0
  10. minder/auth/middleware.py +70 -0
  11. minder/auth/principal.py +59 -0
  12. minder/auth/rate_limiter.py +89 -0
  13. minder/auth/rbac.py +60 -0
  14. minder/auth/service.py +541 -0
  15. minder/bootstrap/__init__.py +9 -0
  16. minder/bootstrap/providers.py +109 -0
  17. minder/bootstrap/transport.py +807 -0
  18. minder/cache/__init__.py +10 -0
  19. minder/cache/providers.py +140 -0
  20. minder/chunking/__init__.py +4 -0
  21. minder/chunking/code_splitter.py +184 -0
  22. minder/chunking/splitter.py +136 -0
  23. minder/cli.py +1542 -0
  24. minder/config.py +179 -0
  25. minder/continuity.py +363 -0
  26. minder/dev.py +160 -0
  27. minder/embedding/__init__.py +9 -0
  28. minder/embedding/base.py +7 -0
  29. minder/embedding/local.py +65 -0
  30. minder/embedding/openai.py +7 -0
  31. minder/graph/__init__.py +11 -0
  32. minder/graph/edges.py +13 -0
  33. minder/graph/executor.py +127 -0
  34. minder/graph/graph.py +263 -0
  35. minder/graph/nodes/__init__.py +27 -0
  36. minder/graph/nodes/evaluator.py +21 -0
  37. minder/graph/nodes/guard.py +64 -0
  38. minder/graph/nodes/llm.py +59 -0
  39. minder/graph/nodes/planning.py +30 -0
  40. minder/graph/nodes/reasoning.py +87 -0
  41. minder/graph/nodes/reranker.py +141 -0
  42. minder/graph/nodes/retriever.py +86 -0
  43. minder/graph/nodes/verification.py +230 -0
  44. minder/graph/nodes/workflow_planner.py +250 -0
  45. minder/graph/runtime.py +15 -0
  46. minder/graph/state.py +26 -0
  47. minder/llm/__init__.py +5 -0
  48. minder/llm/base.py +14 -0
  49. minder/llm/local.py +381 -0
  50. minder/llm/openai.py +89 -0
  51. minder/models/__init__.py +109 -0
  52. minder/models/base.py +10 -0
  53. minder/models/client.py +137 -0
  54. minder/models/document.py +34 -0
  55. minder/models/error.py +32 -0
  56. minder/models/graph.py +114 -0
  57. minder/models/history.py +32 -0
  58. minder/models/job.py +62 -0
  59. minder/models/prompt.py +41 -0
  60. minder/models/repository.py +62 -0
  61. minder/models/rule.py +68 -0
  62. minder/models/session.py +51 -0
  63. minder/models/skill.py +52 -0
  64. minder/models/user.py +41 -0
  65. minder/models/workflow.py +35 -0
  66. minder/observability/__init__.py +57 -0
  67. minder/observability/audit.py +243 -0
  68. minder/observability/logging.py +253 -0
  69. minder/observability/metrics.py +448 -0
  70. minder/observability/tracing.py +215 -0
  71. minder/presentation/__init__.py +1 -0
  72. minder/presentation/http/__init__.py +1 -0
  73. minder/presentation/http/admin/__init__.py +3 -0
  74. minder/presentation/http/admin/api.py +1309 -0
  75. minder/presentation/http/admin/context.py +94 -0
  76. minder/presentation/http/admin/dashboard.py +111 -0
  77. minder/presentation/http/admin/jobs.py +208 -0
  78. minder/presentation/http/admin/memories.py +185 -0
  79. minder/presentation/http/admin/prompts.py +219 -0
  80. minder/presentation/http/admin/routes.py +127 -0
  81. minder/presentation/http/admin/runtime.py +650 -0
  82. minder/presentation/http/admin/search.py +368 -0
  83. minder/presentation/http/admin/skills.py +230 -0
  84. minder/prompts/__init__.py +646 -0
  85. minder/prompts/formatter.py +142 -0
  86. minder/resources/__init__.py +318 -0
  87. minder/retrieval/__init__.py +5 -0
  88. minder/retrieval/hybrid.py +178 -0
  89. minder/retrieval/mmr.py +116 -0
  90. minder/retrieval/multi_hop.py +115 -0
  91. minder/runtime.py +15 -0
  92. minder/server.py +145 -0
  93. minder/store/__init__.py +64 -0
  94. minder/store/document.py +115 -0
  95. minder/store/error.py +82 -0
  96. minder/store/feedback.py +114 -0
  97. minder/store/graph.py +588 -0
  98. minder/store/history.py +57 -0
  99. minder/store/interfaces.py +512 -0
  100. minder/store/milvus/__init__.py +11 -0
  101. minder/store/milvus/client.py +26 -0
  102. minder/store/milvus/collections.py +15 -0
  103. minder/store/milvus/vector_store.py +232 -0
  104. minder/store/mongodb/__init__.py +11 -0
  105. minder/store/mongodb/client.py +49 -0
  106. minder/store/mongodb/indexes.py +90 -0
  107. minder/store/mongodb/operational_store.py +993 -0
  108. minder/store/relational.py +1087 -0
  109. minder/store/repo_state.py +58 -0
  110. minder/store/rule.py +93 -0
  111. minder/store/vector.py +79 -0
  112. minder/tools/__init__.py +47 -0
  113. minder/tools/auth.py +94 -0
  114. minder/tools/graph.py +839 -0
  115. minder/tools/ingest.py +353 -0
  116. minder/tools/memory.py +381 -0
  117. minder/tools/query.py +307 -0
  118. minder/tools/registry.py +269 -0
  119. minder/tools/repo_scanner.py +1266 -0
  120. minder/tools/search.py +15 -0
  121. minder/tools/session.py +316 -0
  122. minder/tools/skills.py +899 -0
  123. minder/tools/workflow.py +215 -0
  124. minder/transport/__init__.py +4 -0
  125. minder/transport/base.py +286 -0
  126. minder/transport/sse.py +252 -0
  127. minder/transport/stdio.py +29 -0
  128. minder_cli-0.2.0.dist-info/METADATA +318 -0
  129. minder_cli-0.2.0.dist-info/RECORD +132 -0
  130. minder_cli-0.2.0.dist-info/WHEEL +4 -0
  131. minder_cli-0.2.0.dist-info/entry_points.txt +2 -0
  132. minder_cli-0.2.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,646 @@
1
+ """MCP prompt registration and runtime sync for Minder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from types import SimpleNamespace
6
+ from typing import TYPE_CHECKING, Any, Iterable
7
+
8
+ from mcp.server.fastmcp import FastMCP
9
+ from mcp.server.fastmcp.prompts.base import Prompt, PromptArgument
10
+
11
+ if TYPE_CHECKING:
12
+ from minder.store.interfaces import IOperationalStore
13
+
14
+
15
+ class PromptRegistry:
16
+ """Registers all Minder MCP prompts onto a :class:`FastMCP` app."""
17
+
18
+ _BUILTIN_NAMES = {"debug", "review", "explain", "tdd_step", "query_reasoning"}
19
+ _BUILTIN_DEFINITIONS: dict[str, dict[str, Any]] = {
20
+ "debug": {
21
+ "title": "Debug Assistant",
22
+ "description": (
23
+ "Structured prompt to diagnose errors with root cause analysis, "
24
+ "ranked hypotheses, and a minimal fix proposal."
25
+ ),
26
+ "arguments": ["error", "context"],
27
+ "defaults": {
28
+ "error": "Paste the error message or stack trace here.",
29
+ "context": "",
30
+ },
31
+ "content_template": "\n\n".join(
32
+ [
33
+ "## Error\n```\n{error}\n```",
34
+ "## Context\n{context}",
35
+ "## Task",
36
+ "1. Identify the root cause of the error above.",
37
+ "2. List 2-3 hypotheses ranked by likelihood.",
38
+ "3. Propose the minimal fix with a code snippet.",
39
+ "4. Describe how to verify the fix.",
40
+ ]
41
+ ),
42
+ },
43
+ "review": {
44
+ "title": "Code Reviewer",
45
+ "description": (
46
+ "Code review checklist prompt for structured diff analysis covering "
47
+ "correctness, edge cases, tests, performance, security, and readability."
48
+ ),
49
+ "arguments": ["diff", "context"],
50
+ "defaults": {
51
+ "diff": "Paste the diff or changed code here.",
52
+ "context": "",
53
+ },
54
+ "content_template": "\n\n".join(
55
+ [
56
+ "## Diff\n```diff\n{diff}\n```",
57
+ "## Context\n{context}",
58
+ "## Review Checklist",
59
+ "- [ ] **Correctness** — Does the logic match the stated intent?",
60
+ "- [ ] **Edge cases** — Are failure modes and null/empty states handled?",
61
+ "- [ ] **Tests** — Are new behaviours covered by automated tests?",
62
+ "- [ ] **Performance** — Any N+1 queries, unbounded loops, or hot-path regressions?",
63
+ "- [ ] **Security** — Any injection vectors, auth bypass, or data leakage?",
64
+ "- [ ] **Readability** — Is naming clear and are comments useful?",
65
+ "",
66
+ "Provide **BLOCKING** issues first, then **RECOMMENDED** improvements, then **SUGGESTIONS**.",
67
+ ]
68
+ ),
69
+ },
70
+ "explain": {
71
+ "title": "Code Explainer",
72
+ "description": (
73
+ "Explain a code snippet in plain language with a summary, "
74
+ "step-by-step walkthrough, gotchas, and a usage example."
75
+ ),
76
+ "arguments": ["code", "language"],
77
+ "defaults": {
78
+ "code": "Paste the code snippet here.",
79
+ "language": "python",
80
+ },
81
+ "content_template": "\n\n".join(
82
+ [
83
+ "## Code ({language})\n```{language}\n{code}\n```",
84
+ "## Task",
85
+ "Explain the code above clearly:",
86
+ "1. **What it does** — one-sentence summary.",
87
+ "2. **How it works** — step-by-step walkthrough of the key logic.",
88
+ "3. **Gotchas** — any non-obvious behaviour, edge cases, or side effects.",
89
+ "4. **Example** — show a concrete usage example if helpful.",
90
+ ]
91
+ ),
92
+ },
93
+ "tdd_step": {
94
+ "title": "TDD Step Guide",
95
+ "description": (
96
+ "Workflow-aware TDD guidance tailored to the current step "
97
+ "(Test Writing, Implementation, or Review)."
98
+ ),
99
+ "arguments": ["current_step", "failing_tests"],
100
+ "defaults": {
101
+ "current_step": "Test Writing",
102
+ "failing_tests": "",
103
+ },
104
+ "content_template": "\n\n".join(
105
+ [
106
+ "## Current Workflow Step: {current_step}",
107
+ "## Failing Tests\n```\n{failing_tests}\n```",
108
+ "## Guidance",
109
+ "Tailor the response to the workflow step above and keep the next action concrete.",
110
+ ]
111
+ ),
112
+ },
113
+ "query_reasoning": {
114
+ "title": "Query Reasoning",
115
+ "description": (
116
+ "Primary query reasoning prompt that injects workflow instruction, "
117
+ "continuity packet, retrieved context, and corrective retry guidance."
118
+ ),
119
+ "arguments": [
120
+ "workflow_instruction",
121
+ "instruction_envelope",
122
+ "continuity_brief",
123
+ "continuity_packet",
124
+ "tool_capabilities",
125
+ "data_access_policy",
126
+ "repository_context_note",
127
+ "user_query",
128
+ "retrieved_context",
129
+ "correction_required",
130
+ ],
131
+ "defaults": {
132
+ "workflow_instruction": "",
133
+ "instruction_envelope": "{}",
134
+ "continuity_brief": "{}",
135
+ "continuity_packet": "{}",
136
+ "tool_capabilities": "No capability manifest was provided.",
137
+ "data_access_policy": "No data-access policy was provided.",
138
+ "repository_context_note": "No repository context policy was provided.",
139
+ "user_query": "Summarize the current repository state.",
140
+ "retrieved_context": "No repository context found.",
141
+ "correction_required": "",
142
+ },
143
+ "content_template": "\n\n".join(
144
+ [
145
+ "Workflow instruction:\n{workflow_instruction}",
146
+ "Instruction envelope:\n{instruction_envelope}",
147
+ "Continuity packet:\n{continuity_packet}",
148
+ "Tool capabilities:\n{tool_capabilities}",
149
+ "Data access policy:\n{data_access_policy}",
150
+ "Repository context note:\n{repository_context_note}",
151
+ "User query:\n{user_query}",
152
+ "Retrieved context:\n{retrieved_context}",
153
+ "Correction required:\n{correction_required}",
154
+ "Respond with grounded reasoning and cite source paths.",
155
+ ]
156
+ ),
157
+ },
158
+ }
159
+
160
+ @staticmethod
161
+ def get_builtin_definition(name: str) -> dict[str, Any] | None:
162
+ definition = PromptRegistry._BUILTIN_DEFINITIONS.get(name)
163
+ if definition is None:
164
+ return None
165
+ return {
166
+ "title": definition["title"],
167
+ "description": definition["description"],
168
+ "arguments": list(definition["arguments"]),
169
+ "defaults": dict(definition.get("defaults", {})),
170
+ "content_template": str(definition["content_template"]),
171
+ }
172
+
173
+ @staticmethod
174
+ def get_builtin_prompt_model(name: str) -> SimpleNamespace | None:
175
+ definition = PromptRegistry.get_builtin_definition(name)
176
+ if definition is None:
177
+ return None
178
+ return SimpleNamespace(
179
+ id=f"builtin:{name}",
180
+ name=name,
181
+ title=definition["title"],
182
+ description=definition["description"],
183
+ content_template=definition["content_template"],
184
+ arguments=list(definition["arguments"]),
185
+ defaults=dict(definition.get("defaults", {})),
186
+ created_at=None,
187
+ updated_at=None,
188
+ is_builtin=True,
189
+ )
190
+
191
+ @staticmethod
192
+ async def resolve_prompt_model(
193
+ name: str,
194
+ store: IOperationalStore | None = None,
195
+ ) -> Any | None:
196
+ if store is not None:
197
+ prompt = await store.get_prompt_by_name(name)
198
+ if prompt is not None:
199
+ return prompt
200
+ return PromptRegistry.get_builtin_prompt_model(name)
201
+
202
+ @staticmethod
203
+ def render_content_template(
204
+ content_template: str,
205
+ arguments: dict[str, Any],
206
+ *,
207
+ defaults: dict[str, Any] | None = None,
208
+ ) -> str:
209
+ rendered = str(content_template)
210
+ merged_arguments = dict(defaults or {})
211
+ merged_arguments.update(arguments)
212
+ for arg_name, arg_val in merged_arguments.items():
213
+ rendered = rendered.replace("{" + str(arg_name) + "}", str(arg_val or ""))
214
+ return rendered
215
+
216
+ @staticmethod
217
+ def _prompt_manager(app: FastMCP) -> Any:
218
+ return getattr(app, "_prompt_manager", None)
219
+
220
+ @staticmethod
221
+ def _prompt_mapping(app: FastMCP) -> dict[str, Any]:
222
+ manager = PromptRegistry._prompt_manager(app)
223
+ if manager is not None and hasattr(manager, "_prompts"):
224
+ return manager._prompts
225
+
226
+ prompts = getattr(app, "_prompts", None)
227
+ if isinstance(prompts, dict):
228
+ return prompts
229
+
230
+ raise AttributeError("Prompt registry storage is not available on app")
231
+
232
+ @staticmethod
233
+ def _upsert_prompt(app: FastMCP, prompt: Prompt) -> None:
234
+ manager = PromptRegistry._prompt_manager(app)
235
+ prompt_mapping = PromptRegistry._prompt_mapping(app)
236
+ existing = prompt_mapping.get(prompt.name)
237
+ if existing is None:
238
+ if hasattr(app, "add_prompt"):
239
+ app.add_prompt(prompt)
240
+ else:
241
+ prompt_mapping[prompt.name] = prompt.fn
242
+ return
243
+ if manager is not None and hasattr(existing, "title"):
244
+ existing.title = prompt.title
245
+ existing.description = prompt.description
246
+ existing.arguments = prompt.arguments
247
+ existing.fn = prompt.fn
248
+ existing.context_kwarg = prompt.context_kwarg
249
+ return
250
+
251
+ prompt_mapping[prompt.name] = prompt.fn
252
+
253
+ @staticmethod
254
+ def _remove_prompt(app: FastMCP, name: str) -> None:
255
+ prompt_mapping = PromptRegistry._prompt_mapping(app)
256
+ prompt_mapping.pop(name, None)
257
+
258
+ @staticmethod
259
+ def _optional_arguments(name: str) -> list[PromptArgument]:
260
+ definition = PromptRegistry._BUILTIN_DEFINITIONS[name]
261
+ return [
262
+ PromptArgument(
263
+ name=argument_name,
264
+ required=False,
265
+ description=f"{argument_name} argument",
266
+ )
267
+ for argument_name in definition["arguments"]
268
+ ]
269
+
270
+ @staticmethod
271
+ def _configure_builtin_prompt(prompt: Prompt) -> Prompt:
272
+ prompt.arguments = PromptRegistry._optional_arguments(prompt.name)
273
+ return prompt
274
+
275
+ @staticmethod
276
+ def builtin_prompt_models() -> list[SimpleNamespace]:
277
+ models: list[SimpleNamespace] = []
278
+ for name in PromptRegistry._BUILTIN_DEFINITIONS:
279
+ model = PromptRegistry.get_builtin_prompt_model(name)
280
+ if model is not None:
281
+ models.append(model)
282
+ return models
283
+
284
+ @staticmethod
285
+ def _normalize_argument_names(raw_arguments: Any) -> list[str]:
286
+ if raw_arguments is None:
287
+ return []
288
+ if isinstance(raw_arguments, dict):
289
+ candidates: Iterable[Any] = raw_arguments.keys()
290
+ elif isinstance(raw_arguments, (list, tuple, set)):
291
+ candidates = raw_arguments
292
+ else:
293
+ candidates = [raw_arguments]
294
+
295
+ normalized: list[str] = []
296
+ seen: set[str] = set()
297
+ for item in candidates:
298
+ value = item.get("name") if isinstance(item, dict) else item
299
+ argument = str(value or "").strip()
300
+ if not argument or argument in seen:
301
+ continue
302
+ seen.add(argument)
303
+ normalized.append(argument)
304
+ return normalized
305
+
306
+ @staticmethod
307
+ def _build_dynamic_handler(prompt_model: Any, store: IOperationalStore):
308
+ async def dynamic_handler(**kwargs):
309
+ from minder.auth.context import get_current_principal
310
+
311
+ principal = get_current_principal()
312
+ actor_id = str(principal.principal_id) if principal else "unknown"
313
+ actor_type = principal.principal_type if principal else "unknown"
314
+ client_id = (
315
+ getattr(principal, "client_slug", "unknown") if principal else "unknown"
316
+ )
317
+
318
+ try:
319
+ await store.create_audit_log(
320
+ actor_type=actor_type,
321
+ actor_id=actor_id,
322
+ event_type="prompt_request",
323
+ resource_type="prompt",
324
+ resource_id=prompt_model.name,
325
+ outcome="success",
326
+ audit_metadata={"client_id": client_id},
327
+ )
328
+ except Exception:
329
+ pass
330
+
331
+ content = PromptRegistry.render_content_template(
332
+ str(prompt_model.content_template),
333
+ kwargs,
334
+ defaults=dict(getattr(prompt_model, "defaults", {}) or {}),
335
+ )
336
+ return [{"role": "user", "content": content}]
337
+
338
+ dynamic_handler.__name__ = f"prompt_{prompt_model.name}"
339
+ return dynamic_handler
340
+
341
+ @staticmethod
342
+ def register(app: FastMCP, store: IOperationalStore | None = None) -> None:
343
+ """Register builtin prompts using a mutable runtime registry."""
344
+
345
+ debug_defaults = PromptRegistry._BUILTIN_DEFINITIONS["debug"]["defaults"]
346
+ review_defaults = PromptRegistry._BUILTIN_DEFINITIONS["review"]["defaults"]
347
+ explain_defaults = PromptRegistry._BUILTIN_DEFINITIONS["explain"]["defaults"]
348
+ tdd_step_defaults = PromptRegistry._BUILTIN_DEFINITIONS["tdd_step"]["defaults"]
349
+ query_reasoning_defaults = PromptRegistry._BUILTIN_DEFINITIONS[
350
+ "query_reasoning"
351
+ ]["defaults"]
352
+
353
+ async def _log_prompt(name: str):
354
+ if store is not None:
355
+ from minder.auth.context import get_current_principal
356
+
357
+ p = get_current_principal()
358
+ actor_id = "unknown"
359
+ actor_type = "unknown"
360
+ client_id = "unknown"
361
+ if p:
362
+ actor_id = str(p.principal_id)
363
+ actor_type = p.principal_type
364
+ client_id = getattr(p, "client_slug", "unknown")
365
+
366
+ try:
367
+ await store.create_audit_log(
368
+ actor_type=actor_type,
369
+ actor_id=actor_id,
370
+ event_type="prompt_request",
371
+ resource_type="prompt",
372
+ resource_id=name,
373
+ outcome="success",
374
+ audit_metadata={"client_id": client_id},
375
+ )
376
+ except Exception:
377
+ pass
378
+
379
+ async def debug_prompt(
380
+ error: str = str(debug_defaults["error"]),
381
+ context: str = str(debug_defaults["context"]),
382
+ ) -> list[dict[str, str]]:
383
+ await _log_prompt("debug")
384
+ """Generate a debug analysis prompt.
385
+
386
+ Args:
387
+ error: The error message or stack trace to diagnose.
388
+ context: Optional surrounding context (file, function, recent changes).
389
+ """
390
+ parts = [f"## Error\n```\n{error}\n```"]
391
+ if context:
392
+ parts.append(f"## Context\n{context}")
393
+ parts += [
394
+ "## Task",
395
+ "1. Identify the root cause of the error above.",
396
+ "2. List 2-3 hypotheses ranked by likelihood.",
397
+ "3. Propose the minimal fix with a code snippet.",
398
+ "4. Describe how to verify the fix.",
399
+ ]
400
+ return [{"role": "user", "content": "\n\n".join(parts)}]
401
+
402
+ async def review_prompt(
403
+ diff: str = str(review_defaults["diff"]),
404
+ context: str = str(review_defaults["context"]),
405
+ ) -> list[dict[str, str]]:
406
+ await _log_prompt("review")
407
+ """Generate a code review prompt.
408
+
409
+ Args:
410
+ diff: The unified diff or changed code to review.
411
+ context: Optional ticket description, requirements, or intent.
412
+ """
413
+ parts = [f"## Diff\n```diff\n{diff}\n```"]
414
+ if context:
415
+ parts.append(f"## Context\n{context}")
416
+ parts += [
417
+ "## Review Checklist",
418
+ "- [ ] **Correctness** — Does the logic match the stated intent?",
419
+ "- [ ] **Edge cases** — Are failure modes and null/empty states handled?",
420
+ "- [ ] **Tests** — Are new behaviours covered by automated tests?",
421
+ "- [ ] **Performance** — Any N+1 queries, unbounded loops, or hot-path regressions?",
422
+ "- [ ] **Security** — Any injection vectors, auth bypass, or data leakage?",
423
+ "- [ ] **Readability** — Is naming clear and are comments useful?",
424
+ "",
425
+ "Provide **BLOCKING** issues first, then **RECOMMENDED** improvements, "
426
+ "then **SUGGESTIONS**.",
427
+ ]
428
+ return [{"role": "user", "content": "\n\n".join(parts)}]
429
+
430
+ async def explain_prompt(
431
+ code: str = str(explain_defaults["code"]),
432
+ language: str = str(explain_defaults["language"]),
433
+ ) -> list[dict[str, str]]:
434
+ await _log_prompt("explain")
435
+ """Generate a code explanation prompt.
436
+
437
+ Args:
438
+ code: The source code snippet to explain.
439
+ language: The programming language (default: python).
440
+ """
441
+ content = "\n\n".join(
442
+ [
443
+ f"## Code ({language})\n```{language}\n{code}\n```",
444
+ "## Task",
445
+ "Explain the code above clearly:",
446
+ "1. **What it does** — one-sentence summary.",
447
+ "2. **How it works** — step-by-step walkthrough of the key logic.",
448
+ "3. **Gotchas** — any non-obvious behaviour, edge cases, or side effects.",
449
+ "4. **Example** — show a concrete usage example if helpful.",
450
+ ]
451
+ )
452
+ return [{"role": "user", "content": content}]
453
+
454
+ async def tdd_step_prompt(
455
+ current_step: str = str(tdd_step_defaults["current_step"]),
456
+ failing_tests: str = str(tdd_step_defaults["failing_tests"]),
457
+ ) -> list[dict[str, str]]:
458
+ await _log_prompt("tdd_step")
459
+ """Generate a TDD step guidance prompt.
460
+
461
+ Args:
462
+ current_step: The name of the current workflow step.
463
+ failing_tests: Optional failing test output to include as context.
464
+ """
465
+ parts = [f"## Current Workflow Step: {current_step}"]
466
+ if failing_tests:
467
+ parts.append(f"## Failing Tests\n```\n{failing_tests}\n```")
468
+
469
+ lowered = current_step.lower()
470
+ if "test" in lowered:
471
+ parts += [
472
+ "## Guidance",
473
+ "You are in the **Test Writing** phase.",
474
+ "1. Write failing tests that specify the exact behaviour required.",
475
+ "2. Do NOT write any implementation code yet.",
476
+ "3. Each test should express a single, clear assertion.",
477
+ "4. Name tests descriptively: `test_<behaviour>_when_<condition>`.",
478
+ ]
479
+ elif "implement" in lowered:
480
+ parts += [
481
+ "## Guidance",
482
+ "You are in the **Implementation** phase.",
483
+ "1. Write the minimal code to make all failing tests pass.",
484
+ "2. Do not add features beyond what the tests require.",
485
+ "3. Run tests after every small change.",
486
+ "4. Refactor only after all tests are green.",
487
+ ]
488
+ elif "review" in lowered:
489
+ parts += [
490
+ "## Guidance",
491
+ "You are in the **Review** phase.",
492
+ "1. Verify that all acceptance criteria are met.",
493
+ "2. Confirm no regressions exist in the existing test suite.",
494
+ "3. Ensure code quality meets project standards.",
495
+ ]
496
+ else:
497
+ parts += [
498
+ "## Guidance",
499
+ f"Complete **{current_step}** fully before advancing to the next step.",
500
+ "Do not skip or partially satisfy prerequisites.",
501
+ ]
502
+
503
+ return [{"role": "user", "content": "\n\n".join(parts)}]
504
+
505
+ async def query_reasoning_prompt(
506
+ workflow_instruction: str = str(
507
+ query_reasoning_defaults["workflow_instruction"]
508
+ ),
509
+ instruction_envelope: str = str(
510
+ query_reasoning_defaults["instruction_envelope"]
511
+ ),
512
+ continuity_brief: str = str(query_reasoning_defaults["continuity_brief"]),
513
+ continuity_packet: str = str(query_reasoning_defaults["continuity_packet"]),
514
+ user_query: str = str(query_reasoning_defaults["user_query"]),
515
+ retrieved_context: str = str(query_reasoning_defaults["retrieved_context"]),
516
+ correction_required: str = str(
517
+ query_reasoning_defaults["correction_required"]
518
+ ),
519
+ ) -> list[dict[str, str]]:
520
+ await _log_prompt("query_reasoning")
521
+ content = PromptRegistry.render_content_template(
522
+ str(
523
+ PromptRegistry._BUILTIN_DEFINITIONS["query_reasoning"][
524
+ "content_template"
525
+ ]
526
+ ),
527
+ {
528
+ "workflow_instruction": workflow_instruction,
529
+ "instruction_envelope": instruction_envelope,
530
+ "continuity_brief": continuity_brief,
531
+ "continuity_packet": continuity_packet,
532
+ "user_query": user_query,
533
+ "retrieved_context": retrieved_context,
534
+ "correction_required": correction_required,
535
+ },
536
+ defaults=query_reasoning_defaults,
537
+ )
538
+ return [{"role": "user", "content": content}]
539
+
540
+ PromptRegistry._upsert_prompt(
541
+ app,
542
+ PromptRegistry._configure_builtin_prompt(
543
+ Prompt.from_function(
544
+ debug_prompt,
545
+ name="debug",
546
+ title=PromptRegistry._BUILTIN_DEFINITIONS["debug"]["title"],
547
+ description=PromptRegistry._BUILTIN_DEFINITIONS["debug"][
548
+ "description"
549
+ ],
550
+ )
551
+ ),
552
+ )
553
+ PromptRegistry._upsert_prompt(
554
+ app,
555
+ PromptRegistry._configure_builtin_prompt(
556
+ Prompt.from_function(
557
+ review_prompt,
558
+ name="review",
559
+ title=PromptRegistry._BUILTIN_DEFINITIONS["review"]["title"],
560
+ description=PromptRegistry._BUILTIN_DEFINITIONS["review"][
561
+ "description"
562
+ ],
563
+ )
564
+ ),
565
+ )
566
+ PromptRegistry._upsert_prompt(
567
+ app,
568
+ PromptRegistry._configure_builtin_prompt(
569
+ Prompt.from_function(
570
+ explain_prompt,
571
+ name="explain",
572
+ title=PromptRegistry._BUILTIN_DEFINITIONS["explain"]["title"],
573
+ description=PromptRegistry._BUILTIN_DEFINITIONS["explain"][
574
+ "description"
575
+ ],
576
+ )
577
+ ),
578
+ )
579
+ PromptRegistry._upsert_prompt(
580
+ app,
581
+ PromptRegistry._configure_builtin_prompt(
582
+ Prompt.from_function(
583
+ tdd_step_prompt,
584
+ name="tdd_step",
585
+ title=PromptRegistry._BUILTIN_DEFINITIONS["tdd_step"]["title"],
586
+ description=PromptRegistry._BUILTIN_DEFINITIONS["tdd_step"][
587
+ "description"
588
+ ],
589
+ )
590
+ ),
591
+ )
592
+ PromptRegistry._upsert_prompt(
593
+ app,
594
+ PromptRegistry._configure_builtin_prompt(
595
+ Prompt.from_function(
596
+ query_reasoning_prompt,
597
+ name="query_reasoning",
598
+ title=PromptRegistry._BUILTIN_DEFINITIONS["query_reasoning"][
599
+ "title"
600
+ ],
601
+ description=PromptRegistry._BUILTIN_DEFINITIONS["query_reasoning"][
602
+ "description"
603
+ ],
604
+ )
605
+ ),
606
+ )
607
+
608
+ @staticmethod
609
+ async def sync(app: FastMCP, store: IOperationalStore) -> None:
610
+ """Synchronize FastMCP prompts with builtin and database-backed prompt data."""
611
+ PromptRegistry.register(app, store=store)
612
+ try:
613
+ prompts = await store.list_prompts()
614
+ except Exception:
615
+ return
616
+
617
+ dynamic_names: set[str] = set()
618
+ for prompt_model in prompts:
619
+ argument_names = PromptRegistry._normalize_argument_names(
620
+ getattr(prompt_model, "arguments", None)
621
+ )
622
+ dynamic_handler = PromptRegistry._build_dynamic_handler(prompt_model, store)
623
+
624
+ dynamic_prompt = Prompt.from_function(
625
+ dynamic_handler,
626
+ name=str(prompt_model.name),
627
+ title=str(prompt_model.title),
628
+ description=str(prompt_model.description),
629
+ )
630
+ dynamic_prompt.arguments = [
631
+ PromptArgument(
632
+ name=name, required=False, description=f"{name} argument"
633
+ )
634
+ for name in argument_names
635
+ ]
636
+ PromptRegistry._upsert_prompt(app, dynamic_prompt)
637
+ dynamic_names.add(str(prompt_model.name))
638
+
639
+ previous_dynamic_names = set(
640
+ getattr(app, "_minder_dynamic_prompt_names", set())
641
+ )
642
+ for stale_name in previous_dynamic_names - dynamic_names:
643
+ if stale_name not in PromptRegistry._BUILTIN_NAMES:
644
+ PromptRegistry._remove_prompt(app, stale_name)
645
+
646
+ setattr(app, "_minder_dynamic_prompt_names", dynamic_names)