foundry-mcp 0.3.3__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.
- foundry_mcp/__init__.py +7 -0
- foundry_mcp/cli/__init__.py +80 -0
- foundry_mcp/cli/__main__.py +9 -0
- foundry_mcp/cli/agent.py +96 -0
- foundry_mcp/cli/commands/__init__.py +37 -0
- foundry_mcp/cli/commands/cache.py +137 -0
- foundry_mcp/cli/commands/dashboard.py +148 -0
- foundry_mcp/cli/commands/dev.py +446 -0
- foundry_mcp/cli/commands/journal.py +377 -0
- foundry_mcp/cli/commands/lifecycle.py +274 -0
- foundry_mcp/cli/commands/modify.py +824 -0
- foundry_mcp/cli/commands/plan.py +633 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +652 -0
- foundry_mcp/cli/commands/session.py +479 -0
- foundry_mcp/cli/commands/specs.py +856 -0
- foundry_mcp/cli/commands/tasks.py +807 -0
- foundry_mcp/cli/commands/testing.py +676 -0
- foundry_mcp/cli/commands/validate.py +982 -0
- foundry_mcp/cli/config.py +98 -0
- foundry_mcp/cli/context.py +259 -0
- foundry_mcp/cli/flags.py +266 -0
- foundry_mcp/cli/logging.py +212 -0
- foundry_mcp/cli/main.py +44 -0
- foundry_mcp/cli/output.py +122 -0
- foundry_mcp/cli/registry.py +110 -0
- foundry_mcp/cli/resilience.py +178 -0
- foundry_mcp/cli/transcript.py +217 -0
- foundry_mcp/config.py +850 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1636 -0
- foundry_mcp/core/cache.py +195 -0
- foundry_mcp/core/capabilities.py +446 -0
- foundry_mcp/core/concurrency.py +898 -0
- foundry_mcp/core/context.py +540 -0
- foundry_mcp/core/discovery.py +1603 -0
- foundry_mcp/core/error_collection.py +728 -0
- foundry_mcp/core/error_store.py +592 -0
- foundry_mcp/core/feature_flags.py +592 -0
- foundry_mcp/core/health.py +749 -0
- foundry_mcp/core/journal.py +694 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1350 -0
- foundry_mcp/core/llm_patterns.py +510 -0
- foundry_mcp/core/llm_provider.py +1569 -0
- foundry_mcp/core/logging_config.py +374 -0
- foundry_mcp/core/metrics_persistence.py +584 -0
- foundry_mcp/core/metrics_registry.py +327 -0
- foundry_mcp/core/metrics_store.py +641 -0
- foundry_mcp/core/modifications.py +224 -0
- foundry_mcp/core/naming.py +123 -0
- foundry_mcp/core/observability.py +1216 -0
- foundry_mcp/core/otel.py +452 -0
- foundry_mcp/core/otel_stubs.py +264 -0
- foundry_mcp/core/pagination.py +255 -0
- foundry_mcp/core/progress.py +317 -0
- foundry_mcp/core/prometheus.py +577 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +546 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +511 -0
- foundry_mcp/core/prompts/plan_review.py +623 -0
- foundry_mcp/core/providers/__init__.py +225 -0
- foundry_mcp/core/providers/base.py +476 -0
- foundry_mcp/core/providers/claude.py +460 -0
- foundry_mcp/core/providers/codex.py +619 -0
- foundry_mcp/core/providers/cursor_agent.py +642 -0
- foundry_mcp/core/providers/detectors.py +488 -0
- foundry_mcp/core/providers/gemini.py +405 -0
- foundry_mcp/core/providers/opencode.py +616 -0
- foundry_mcp/core/providers/opencode_wrapper.js +302 -0
- foundry_mcp/core/providers/package-lock.json +24 -0
- foundry_mcp/core/providers/package.json +25 -0
- foundry_mcp/core/providers/registry.py +607 -0
- foundry_mcp/core/providers/test_provider.py +171 -0
- foundry_mcp/core/providers/validation.py +729 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +934 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +1650 -0
- foundry_mcp/core/task.py +1289 -0
- foundry_mcp/core/testing.py +450 -0
- foundry_mcp/core/validation.py +2081 -0
- foundry_mcp/dashboard/__init__.py +32 -0
- foundry_mcp/dashboard/app.py +119 -0
- foundry_mcp/dashboard/components/__init__.py +17 -0
- foundry_mcp/dashboard/components/cards.py +88 -0
- foundry_mcp/dashboard/components/charts.py +234 -0
- foundry_mcp/dashboard/components/filters.py +136 -0
- foundry_mcp/dashboard/components/tables.py +195 -0
- foundry_mcp/dashboard/data/__init__.py +11 -0
- foundry_mcp/dashboard/data/stores.py +433 -0
- foundry_mcp/dashboard/launcher.py +289 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +174 -0
- foundry_mcp/dashboard/views/overview.py +160 -0
- foundry_mcp/dashboard/views/providers.py +83 -0
- foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
- foundry_mcp/dashboard/views/tool_usage.py +139 -0
- foundry_mcp/prompts/__init__.py +9 -0
- foundry_mcp/prompts/workflows.py +525 -0
- foundry_mcp/resources/__init__.py +9 -0
- foundry_mcp/resources/specs.py +591 -0
- foundry_mcp/schemas/__init__.py +38 -0
- foundry_mcp/schemas/sdd-spec-schema.json +386 -0
- foundry_mcp/server.py +164 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +71 -0
- foundry_mcp/tools/unified/authoring.py +1487 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +198 -0
- foundry_mcp/tools/unified/environment.py +939 -0
- foundry_mcp/tools/unified/error.py +462 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +632 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +745 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +629 -0
- foundry_mcp/tools/unified/review.py +685 -0
- foundry_mcp/tools/unified/review_helpers.py +299 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +580 -0
- foundry_mcp/tools/unified/spec.py +808 -0
- foundry_mcp/tools/unified/task.py +2202 -0
- foundry_mcp/tools/unified/test.py +370 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.3.3.dist-info/METADATA +337 -0
- foundry_mcp-0.3.3.dist-info/RECORD +135 -0
- foundry_mcp-0.3.3.dist-info/WHEEL +4 -0
- foundry_mcp-0.3.3.dist-info/entry_points.txt +3 -0
- foundry_mcp-0.3.3.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
|
+
)
|