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,377 @@
1
+ """Journal management commands for SDD CLI.
2
+
3
+ Provides commands for managing journal entries in specifications.
4
+ """
5
+
6
+ from typing import Optional
7
+
8
+ import click
9
+
10
+ from foundry_mcp.cli.logging import cli_command, get_cli_logger
11
+ from foundry_mcp.cli.output import emit_error, emit_success
12
+ from foundry_mcp.cli.registry import get_context
13
+ from foundry_mcp.cli.resilience import (
14
+ FAST_TIMEOUT,
15
+ MEDIUM_TIMEOUT,
16
+ handle_keyboard_interrupt,
17
+ with_sync_timeout,
18
+ )
19
+ from foundry_mcp.core.spec import load_spec, find_spec_file
20
+ from foundry_mcp.core.journal import (
21
+ add_journal_entry,
22
+ find_unjournaled_tasks,
23
+ get_journal_entries,
24
+ save_journal,
25
+ )
26
+
27
+ logger = get_cli_logger()
28
+
29
+
30
+ @click.group("journal")
31
+ def journal() -> None:
32
+ """Journal entry management."""
33
+ pass
34
+
35
+
36
+ @journal.command("add")
37
+ @click.argument("spec_id")
38
+ @click.option("--title", "-t", required=True, help="Entry title.")
39
+ @click.option("--content", "-c", required=True, help="Entry content.")
40
+ @click.option(
41
+ "--type",
42
+ "-T",
43
+ "entry_type",
44
+ type=click.Choice(["note", "decision", "blocker", "deviation", "status_change"]),
45
+ default="note",
46
+ help="Type of journal entry.",
47
+ )
48
+ @click.option("--task-id", help="Associated task ID (optional).")
49
+ @click.pass_context
50
+ @cli_command("add")
51
+ @handle_keyboard_interrupt()
52
+ @with_sync_timeout(MEDIUM_TIMEOUT, "Journal add timed out")
53
+ def journal_add_cmd(
54
+ ctx: click.Context,
55
+ spec_id: str,
56
+ title: str,
57
+ content: str,
58
+ entry_type: str,
59
+ task_id: Optional[str],
60
+ ) -> None:
61
+ """Add a journal entry to a specification.
62
+
63
+ SPEC_ID is the specification identifier.
64
+
65
+ Examples:
66
+ sdd journal add my-spec --title "Progress update" --content "Completed phase 1"
67
+ sdd journal add my-spec -t "Decision" -c "Using Redis for cache" --type decision
68
+ sdd journal add my-spec -t "Task note" -c "Found edge case" --task-id task-2-1
69
+ """
70
+ cli_ctx = get_context(ctx)
71
+ specs_dir = cli_ctx.specs_dir
72
+
73
+ if specs_dir is None:
74
+ emit_error(
75
+ "No specs directory found",
76
+ code="VALIDATION_ERROR",
77
+ error_type="validation",
78
+ remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
79
+ details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
80
+ )
81
+ return
82
+
83
+ # Find and load spec
84
+ spec_path = find_spec_file(spec_id, specs_dir)
85
+ if spec_path is None:
86
+ emit_error(
87
+ f"Specification not found: {spec_id}",
88
+ code="SPEC_NOT_FOUND",
89
+ error_type="not_found",
90
+ remediation="Verify the spec ID exists using: sdd specs list",
91
+ details={"spec_id": spec_id, "specs_dir": str(specs_dir)},
92
+ )
93
+ return
94
+
95
+ spec_data = load_spec(spec_id, specs_dir)
96
+ if spec_data is None:
97
+ emit_error(
98
+ f"Failed to load specification: {spec_id}",
99
+ code="INTERNAL_ERROR",
100
+ error_type="internal",
101
+ remediation="Check that the spec file is valid JSON",
102
+ details={"spec_id": spec_id},
103
+ )
104
+ return
105
+
106
+ # Validate task_id if provided
107
+ if task_id:
108
+ hierarchy = spec_data.get("hierarchy", {})
109
+ if task_id not in hierarchy:
110
+ emit_error(
111
+ f"Task not found: {task_id}",
112
+ code="TASK_NOT_FOUND",
113
+ error_type="not_found",
114
+ remediation="Verify the task ID exists using: sdd task-info <spec_id> <task_id>",
115
+ details={"spec_id": spec_id, "task_id": task_id},
116
+ )
117
+ return
118
+
119
+ # Add journal entry
120
+ entry = add_journal_entry(
121
+ spec_data,
122
+ title=title,
123
+ content=content,
124
+ entry_type=entry_type,
125
+ task_id=task_id,
126
+ )
127
+
128
+ # Save changes
129
+ if not save_journal(spec_data, str(spec_path), create_backup=True):
130
+ emit_error(
131
+ "Failed to save spec file",
132
+ code="INTERNAL_ERROR",
133
+ error_type="internal",
134
+ remediation="Check file permissions and disk space",
135
+ details={"path": str(spec_path)},
136
+ )
137
+ return
138
+
139
+ emit_success(
140
+ {
141
+ "spec_id": spec_id,
142
+ "entry": {
143
+ "timestamp": entry.timestamp,
144
+ "entry_type": entry.entry_type,
145
+ "title": entry.title,
146
+ "content": entry.content,
147
+ "task_id": entry.task_id,
148
+ },
149
+ }
150
+ )
151
+
152
+
153
+ @journal.command("list")
154
+ @click.argument("spec_id")
155
+ @click.option("--task-id", help="Filter by task ID.")
156
+ @click.option(
157
+ "--type",
158
+ "-T",
159
+ "entry_type",
160
+ type=click.Choice(["note", "decision", "blocker", "deviation", "status_change"]),
161
+ help="Filter by entry type.",
162
+ )
163
+ @click.option("--limit", "-l", type=int, help="Limit number of entries returned.")
164
+ @click.pass_context
165
+ @cli_command("list")
166
+ @handle_keyboard_interrupt()
167
+ @with_sync_timeout(MEDIUM_TIMEOUT, "Journal list timed out")
168
+ def journal_list_cmd(
169
+ ctx: click.Context,
170
+ spec_id: str,
171
+ task_id: Optional[str],
172
+ entry_type: Optional[str],
173
+ limit: Optional[int],
174
+ ) -> None:
175
+ """List journal entries from a specification.
176
+
177
+ SPEC_ID is the specification identifier.
178
+
179
+ Returns entries sorted by timestamp (most recent first).
180
+
181
+ Examples:
182
+ sdd journal list my-spec
183
+ sdd journal list my-spec --task-id task-2-1
184
+ sdd journal list my-spec --type decision --limit 5
185
+ """
186
+ cli_ctx = get_context(ctx)
187
+ specs_dir = cli_ctx.specs_dir
188
+
189
+ if specs_dir is None:
190
+ emit_error(
191
+ "No specs directory found",
192
+ code="VALIDATION_ERROR",
193
+ error_type="validation",
194
+ remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
195
+ details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
196
+ )
197
+ return
198
+
199
+ # Load spec
200
+ spec_data = load_spec(spec_id, specs_dir)
201
+ if spec_data is None:
202
+ emit_error(
203
+ f"Specification not found: {spec_id}",
204
+ code="SPEC_NOT_FOUND",
205
+ error_type="not_found",
206
+ remediation="Verify the spec ID exists using: sdd specs list",
207
+ details={"spec_id": spec_id, "specs_dir": str(specs_dir)},
208
+ )
209
+ return
210
+
211
+ # Get journal entries
212
+ entries = get_journal_entries(
213
+ spec_data,
214
+ task_id=task_id,
215
+ entry_type=entry_type,
216
+ limit=limit,
217
+ )
218
+
219
+ emit_success(
220
+ {
221
+ "spec_id": spec_id,
222
+ "entry_count": len(entries),
223
+ "filters": {
224
+ "task_id": task_id,
225
+ "entry_type": entry_type,
226
+ "limit": limit,
227
+ },
228
+ "entries": [
229
+ {
230
+ "timestamp": e.timestamp,
231
+ "entry_type": e.entry_type,
232
+ "title": e.title,
233
+ "content": e.content,
234
+ "task_id": e.task_id,
235
+ "author": e.author,
236
+ }
237
+ for e in entries
238
+ ],
239
+ }
240
+ )
241
+
242
+
243
+ @journal.command("unjournaled")
244
+ @click.argument("spec_id")
245
+ @click.pass_context
246
+ @cli_command("unjournaled")
247
+ @handle_keyboard_interrupt()
248
+ @with_sync_timeout(FAST_TIMEOUT, "Journal unjournaled lookup timed out")
249
+ def journal_unjournaled_cmd(
250
+ ctx: click.Context,
251
+ spec_id: str,
252
+ ) -> None:
253
+ """List completed tasks that need journal entries.
254
+
255
+ SPEC_ID is the specification identifier.
256
+
257
+ Returns tasks that are marked as completed but have the
258
+ needs_journaling flag set to true.
259
+
260
+ Examples:
261
+ sdd journal unjournaled my-spec
262
+ """
263
+ cli_ctx = get_context(ctx)
264
+ specs_dir = cli_ctx.specs_dir
265
+
266
+ if specs_dir is None:
267
+ emit_error(
268
+ "No specs directory found",
269
+ code="VALIDATION_ERROR",
270
+ error_type="validation",
271
+ remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
272
+ details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
273
+ )
274
+ return
275
+
276
+ # Load spec
277
+ spec_data = load_spec(spec_id, specs_dir)
278
+ if spec_data is None:
279
+ emit_error(
280
+ f"Specification not found: {spec_id}",
281
+ code="SPEC_NOT_FOUND",
282
+ error_type="not_found",
283
+ remediation="Verify the spec ID exists using: sdd specs list",
284
+ details={"spec_id": spec_id, "specs_dir": str(specs_dir)},
285
+ )
286
+ return
287
+
288
+ # Find unjournaled tasks
289
+ unjournaled = find_unjournaled_tasks(spec_data)
290
+
291
+ emit_success(
292
+ {
293
+ "spec_id": spec_id,
294
+ "count": len(unjournaled),
295
+ "tasks": unjournaled,
296
+ }
297
+ )
298
+
299
+
300
+ @journal.command("get")
301
+ @click.argument("spec_id")
302
+ @click.option("--task-id", help="Filter by task ID.")
303
+ @click.option(
304
+ "--last", "-n", "last_n", type=int, help="Get last N entries (most recent)."
305
+ )
306
+ @click.pass_context
307
+ @cli_command("get")
308
+ @handle_keyboard_interrupt()
309
+ @with_sync_timeout(FAST_TIMEOUT, "Journal get timed out")
310
+ def journal_get_cmd(
311
+ ctx: click.Context,
312
+ spec_id: str,
313
+ task_id: Optional[str],
314
+ last_n: Optional[int],
315
+ ) -> None:
316
+ """Get journal entries for a specification or task.
317
+
318
+ SPEC_ID is the specification identifier.
319
+
320
+ Retrieves journal entries, optionally filtered by task.
321
+ Use --last to limit to the N most recent entries.
322
+
323
+ Examples:
324
+ sdd get-journal my-spec
325
+ sdd get-journal my-spec --task-id task-2-1
326
+ sdd get-journal my-spec --last 5
327
+ """
328
+ cli_ctx = get_context(ctx)
329
+ specs_dir = cli_ctx.specs_dir
330
+
331
+ if specs_dir is None:
332
+ emit_error(
333
+ "No specs directory found",
334
+ code="VALIDATION_ERROR",
335
+ error_type="validation",
336
+ remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
337
+ details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
338
+ )
339
+ return
340
+
341
+ # Load spec
342
+ spec_data = load_spec(spec_id, specs_dir)
343
+ if spec_data is None:
344
+ emit_error(
345
+ f"Specification not found: {spec_id}",
346
+ code="SPEC_NOT_FOUND",
347
+ error_type="not_found",
348
+ remediation="Verify the spec ID exists using: sdd specs list",
349
+ details={"spec_id": spec_id, "specs_dir": str(specs_dir)},
350
+ )
351
+ return
352
+
353
+ # Get journal entries
354
+ entries = get_journal_entries(
355
+ spec_data,
356
+ task_id=task_id,
357
+ limit=last_n,
358
+ )
359
+
360
+ emit_success(
361
+ {
362
+ "spec_id": spec_id,
363
+ "task_id": task_id,
364
+ "entry_count": len(entries),
365
+ "entries": [
366
+ {
367
+ "timestamp": e.timestamp,
368
+ "entry_type": e.entry_type,
369
+ "title": e.title,
370
+ "content": e.content,
371
+ "task_id": e.task_id,
372
+ "author": e.author,
373
+ }
374
+ for e in entries
375
+ ],
376
+ }
377
+ )
@@ -0,0 +1,274 @@
1
+ """Spec lifecycle commands for SDD CLI.
2
+
3
+ Provides commands for spec lifecycle transitions: activate, complete, archive.
4
+ """
5
+
6
+
7
+ import click
8
+
9
+ from foundry_mcp.cli.logging import cli_command, get_cli_logger
10
+ from foundry_mcp.cli.output import emit_error, emit_success
11
+ from foundry_mcp.cli.resilience import (
12
+ handle_keyboard_interrupt,
13
+ MEDIUM_TIMEOUT,
14
+ with_sync_timeout,
15
+ )
16
+ from foundry_mcp.cli.registry import get_context
17
+
18
+ logger = get_cli_logger()
19
+ from foundry_mcp.core.lifecycle import (
20
+ activate_spec,
21
+ archive_spec,
22
+ complete_spec,
23
+ get_lifecycle_state,
24
+ move_spec,
25
+ )
26
+
27
+
28
+ @click.group("lifecycle")
29
+ def lifecycle() -> None:
30
+ """Spec lifecycle management commands."""
31
+ pass
32
+
33
+
34
+ @lifecycle.command("activate")
35
+ @click.argument("spec_id")
36
+ @click.pass_context
37
+ @cli_command("activate")
38
+ @handle_keyboard_interrupt()
39
+ @with_sync_timeout(MEDIUM_TIMEOUT, "Spec activation timed out")
40
+ def activate_spec_cmd(ctx: click.Context, spec_id: str) -> None:
41
+ """Activate a specification (move from pending to active).
42
+
43
+ SPEC_ID is the specification identifier.
44
+ """
45
+ cli_ctx = get_context(ctx)
46
+ specs_dir = cli_ctx.specs_dir
47
+
48
+ if specs_dir is None:
49
+ emit_error(
50
+ "No specs directory found",
51
+ code="VALIDATION_ERROR",
52
+ error_type="validation",
53
+ remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
54
+ details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
55
+ )
56
+
57
+ result = activate_spec(spec_id, specs_dir)
58
+
59
+ if not result.success:
60
+ emit_error(
61
+ result.error or "Failed to activate spec",
62
+ code="CONFLICT",
63
+ error_type="conflict",
64
+ remediation="Verify the spec exists in pending folder and is ready for activation",
65
+ details={"spec_id": spec_id, "from_folder": result.from_folder},
66
+ )
67
+
68
+ emit_success(
69
+ {
70
+ "spec_id": spec_id,
71
+ "from_folder": result.from_folder,
72
+ "to_folder": result.to_folder,
73
+ "old_path": result.old_path,
74
+ "new_path": result.new_path,
75
+ }
76
+ )
77
+
78
+
79
+ @lifecycle.command("complete")
80
+ @click.argument("spec_id")
81
+ @click.option(
82
+ "--force", "-f", is_flag=True, help="Force completion even with incomplete tasks."
83
+ )
84
+ @click.pass_context
85
+ @cli_command("complete")
86
+ @handle_keyboard_interrupt()
87
+ @with_sync_timeout(MEDIUM_TIMEOUT, "Spec completion timed out")
88
+ def complete_spec_cmd(ctx: click.Context, spec_id: str, force: bool) -> None:
89
+ """Mark a specification as completed.
90
+
91
+ SPEC_ID is the specification identifier.
92
+ """
93
+ cli_ctx = get_context(ctx)
94
+ specs_dir = cli_ctx.specs_dir
95
+
96
+ if specs_dir is None:
97
+ emit_error(
98
+ "No specs directory found",
99
+ code="VALIDATION_ERROR",
100
+ error_type="validation",
101
+ remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
102
+ details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
103
+ )
104
+
105
+ result = complete_spec(spec_id, specs_dir, force=force)
106
+
107
+ if not result.success:
108
+ emit_error(
109
+ result.error or "Failed to complete spec",
110
+ code="CONFLICT",
111
+ error_type="conflict",
112
+ remediation="Verify all tasks are completed, or use --force to complete anyway",
113
+ details={
114
+ "spec_id": spec_id,
115
+ "from_folder": result.from_folder,
116
+ "force": force,
117
+ },
118
+ )
119
+
120
+ emit_success(
121
+ {
122
+ "spec_id": spec_id,
123
+ "from_folder": result.from_folder,
124
+ "to_folder": result.to_folder,
125
+ "old_path": result.old_path,
126
+ "new_path": result.new_path,
127
+ }
128
+ )
129
+
130
+
131
+ @lifecycle.command("archive")
132
+ @click.argument("spec_id")
133
+ @click.pass_context
134
+ @cli_command("archive")
135
+ @handle_keyboard_interrupt()
136
+ @with_sync_timeout(MEDIUM_TIMEOUT, "Spec archival timed out")
137
+ def archive_spec_cmd(ctx: click.Context, spec_id: str) -> None:
138
+ """Archive a specification.
139
+
140
+ SPEC_ID is the specification identifier.
141
+ """
142
+ cli_ctx = get_context(ctx)
143
+ specs_dir = cli_ctx.specs_dir
144
+
145
+ if specs_dir is None:
146
+ emit_error(
147
+ "No specs directory found",
148
+ code="VALIDATION_ERROR",
149
+ error_type="validation",
150
+ remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
151
+ details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
152
+ )
153
+
154
+ result = archive_spec(spec_id, specs_dir)
155
+
156
+ if not result.success:
157
+ emit_error(
158
+ result.error or "Failed to archive spec",
159
+ code="CONFLICT",
160
+ error_type="conflict",
161
+ remediation="Verify the spec exists and is in a state that can be archived",
162
+ details={"spec_id": spec_id, "from_folder": result.from_folder},
163
+ )
164
+
165
+ emit_success(
166
+ {
167
+ "spec_id": spec_id,
168
+ "from_folder": result.from_folder,
169
+ "to_folder": result.to_folder,
170
+ "old_path": result.old_path,
171
+ "new_path": result.new_path,
172
+ }
173
+ )
174
+
175
+
176
+ @lifecycle.command("move")
177
+ @click.argument("spec_id")
178
+ @click.argument(
179
+ "to_folder", type=click.Choice(["pending", "active", "completed", "archived"])
180
+ )
181
+ @click.pass_context
182
+ @cli_command("move")
183
+ @handle_keyboard_interrupt()
184
+ @with_sync_timeout(MEDIUM_TIMEOUT, "Spec move timed out")
185
+ def move_spec_cmd(ctx: click.Context, spec_id: str, to_folder: str) -> None:
186
+ """Move a specification between status folders.
187
+
188
+ SPEC_ID is the specification identifier.
189
+ TO_FOLDER is one of: pending, active, completed, archived.
190
+ """
191
+ cli_ctx = get_context(ctx)
192
+ specs_dir = cli_ctx.specs_dir
193
+
194
+ if specs_dir is None:
195
+ emit_error(
196
+ "No specs directory found",
197
+ code="VALIDATION_ERROR",
198
+ error_type="validation",
199
+ remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
200
+ details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
201
+ )
202
+
203
+ result = move_spec(spec_id, to_folder, specs_dir)
204
+
205
+ if not result.success:
206
+ emit_error(
207
+ result.error or "Failed to move spec",
208
+ code="CONFLICT",
209
+ error_type="conflict",
210
+ remediation="Verify the spec exists and the transition is valid",
211
+ details={
212
+ "spec_id": spec_id,
213
+ "from_folder": result.from_folder,
214
+ "to_folder": to_folder,
215
+ },
216
+ )
217
+
218
+ emit_success(
219
+ {
220
+ "spec_id": spec_id,
221
+ "from_folder": result.from_folder,
222
+ "to_folder": result.to_folder,
223
+ "old_path": result.old_path,
224
+ "new_path": result.new_path,
225
+ }
226
+ )
227
+
228
+
229
+ @lifecycle.command("state")
230
+ @click.argument("spec_id")
231
+ @click.pass_context
232
+ @cli_command("state")
233
+ @handle_keyboard_interrupt()
234
+ @with_sync_timeout(MEDIUM_TIMEOUT, "Lifecycle state lookup timed out")
235
+ def lifecycle_state_cmd(ctx: click.Context, spec_id: str) -> None:
236
+ """Get the current lifecycle state of a specification.
237
+
238
+ SPEC_ID is the specification identifier.
239
+ """
240
+ cli_ctx = get_context(ctx)
241
+ specs_dir = cli_ctx.specs_dir
242
+
243
+ if specs_dir is None:
244
+ emit_error(
245
+ "No specs directory found",
246
+ code="VALIDATION_ERROR",
247
+ error_type="validation",
248
+ remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
249
+ details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
250
+ )
251
+
252
+ state = get_lifecycle_state(spec_id, specs_dir)
253
+
254
+ if state is None:
255
+ emit_error(
256
+ f"Specification not found: {spec_id}",
257
+ code="SPEC_NOT_FOUND",
258
+ error_type="not_found",
259
+ remediation="Verify the spec ID exists using: sdd specs list",
260
+ details={"spec_id": spec_id},
261
+ )
262
+
263
+ emit_success(
264
+ {
265
+ "spec_id": state.spec_id,
266
+ "folder": state.folder,
267
+ "status": state.status,
268
+ "progress_percentage": state.progress_percentage,
269
+ "total_tasks": state.total_tasks,
270
+ "completed_tasks": state.completed_tasks,
271
+ "can_complete": state.can_complete,
272
+ "can_archive": state.can_archive,
273
+ }
274
+ )