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.
- foundry_mcp/__init__.py +13 -0
- foundry_mcp/cli/__init__.py +67 -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 +640 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +667 -0
- foundry_mcp/cli/commands/session.py +472 -0
- foundry_mcp/cli/commands/specs.py +686 -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 +298 -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 +1454 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1773 -0
- foundry_mcp/core/batch_operations.py +1202 -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/health.py +749 -0
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/journal.py +700 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1376 -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 +146 -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 +387 -0
- foundry_mcp/core/prometheus.py +564 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +691 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
- foundry_mcp/core/prompts/plan_review.py +627 -0
- foundry_mcp/core/providers/__init__.py +237 -0
- foundry_mcp/core/providers/base.py +515 -0
- foundry_mcp/core/providers/claude.py +472 -0
- foundry_mcp/core/providers/codex.py +637 -0
- foundry_mcp/core/providers/cursor_agent.py +630 -0
- foundry_mcp/core/providers/detectors.py +515 -0
- foundry_mcp/core/providers/gemini.py +426 -0
- foundry_mcp/core/providers/opencode.py +718 -0
- foundry_mcp/core/providers/opencode_wrapper.js +308 -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 +857 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +528 -0
- foundry_mcp/core/research/models.py +1234 -0
- foundry_mcp/core/research/providers/__init__.py +40 -0
- foundry_mcp/core/research/providers/base.py +242 -0
- foundry_mcp/core/research/providers/google.py +507 -0
- foundry_mcp/core/research/providers/perplexity.py +442 -0
- foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
- foundry_mcp/core/research/providers/tavily.py +383 -0
- foundry_mcp/core/research/workflows/__init__.py +25 -0
- foundry_mcp/core/research/workflows/base.py +298 -0
- foundry_mcp/core/research/workflows/chat.py +271 -0
- foundry_mcp/core/research/workflows/consensus.py +539 -0
- foundry_mcp/core/research/workflows/deep_research.py +4142 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +1624 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +4119 -0
- foundry_mcp/core/task.py +2463 -0
- foundry_mcp/core/testing.py +839 -0
- foundry_mcp/core/validation.py +2357 -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 +177 -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 +300 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +164 -0
- foundry_mcp/dashboard/views/overview.py +96 -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/intake-schema.json +89 -0
- foundry_mcp/schemas/sdd-spec-schema.json +414 -0
- foundry_mcp/server.py +150 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +92 -0
- foundry_mcp/tools/unified/authoring.py +3620 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +268 -0
- foundry_mcp/tools/unified/environment.py +1341 -0
- foundry_mcp/tools/unified/error.py +479 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +640 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +876 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +589 -0
- foundry_mcp/tools/unified/research.py +1283 -0
- foundry_mcp/tools/unified/review.py +1042 -0
- foundry_mcp/tools/unified/review_helpers.py +314 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +565 -0
- foundry_mcp/tools/unified/spec.py +1283 -0
- foundry_mcp/tools/unified/task.py +3846 -0
- foundry_mcp/tools/unified/test.py +431 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.8.22.dist-info/METADATA +344 -0
- foundry_mcp-0.8.22.dist-info/RECORD +153 -0
- foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
- foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
- foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
"""Modification commands for SDD CLI.
|
|
2
|
+
|
|
3
|
+
Provides commands for modifying SDD specifications including:
|
|
4
|
+
- Applying bulk modifications from JSON files
|
|
5
|
+
- Adding/removing tasks
|
|
6
|
+
- Managing assumptions and revisions
|
|
7
|
+
- Updating frontmatter metadata
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import time
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from foundry_mcp.cli.logging import cli_command, get_cli_logger
|
|
17
|
+
from foundry_mcp.cli.output import emit_error, emit_success
|
|
18
|
+
from foundry_mcp.cli.registry import get_context
|
|
19
|
+
from foundry_mcp.cli.resilience import (
|
|
20
|
+
MEDIUM_TIMEOUT,
|
|
21
|
+
with_sync_timeout,
|
|
22
|
+
handle_keyboard_interrupt,
|
|
23
|
+
)
|
|
24
|
+
from foundry_mcp.core.modifications import apply_modifications, load_modifications_file
|
|
25
|
+
from foundry_mcp.core.task import add_task, remove_task
|
|
26
|
+
from foundry_mcp.core.spec import (
|
|
27
|
+
add_assumption,
|
|
28
|
+
add_phase,
|
|
29
|
+
add_revision,
|
|
30
|
+
update_frontmatter,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
logger = get_cli_logger()
|
|
34
|
+
|
|
35
|
+
# Default timeout for modification operations
|
|
36
|
+
MODIFY_TIMEOUT = 60
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@click.group("modify")
|
|
40
|
+
def modify_group() -> None:
|
|
41
|
+
"""Spec modification commands."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@modify_group.command("apply")
|
|
46
|
+
@click.argument("spec_id")
|
|
47
|
+
@click.argument("modifications_file", type=click.Path(exists=True))
|
|
48
|
+
@click.option(
|
|
49
|
+
"--dry-run",
|
|
50
|
+
is_flag=True,
|
|
51
|
+
help="Preview changes without applying.",
|
|
52
|
+
)
|
|
53
|
+
@click.option(
|
|
54
|
+
"--output",
|
|
55
|
+
"output_file",
|
|
56
|
+
type=click.Path(),
|
|
57
|
+
help="Output path for modified spec (default: overwrite original).",
|
|
58
|
+
)
|
|
59
|
+
@click.pass_context
|
|
60
|
+
@cli_command("apply")
|
|
61
|
+
@handle_keyboard_interrupt()
|
|
62
|
+
@with_sync_timeout(MODIFY_TIMEOUT, "Apply modifications timed out")
|
|
63
|
+
def modify_apply_cmd(
|
|
64
|
+
ctx: click.Context,
|
|
65
|
+
spec_id: str,
|
|
66
|
+
modifications_file: str,
|
|
67
|
+
dry_run: bool,
|
|
68
|
+
output_file: Optional[str],
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Apply bulk modifications from a JSON file.
|
|
71
|
+
|
|
72
|
+
SPEC_ID is the specification identifier.
|
|
73
|
+
MODIFICATIONS_FILE is the path to the JSON modifications file.
|
|
74
|
+
|
|
75
|
+
The modifications file should contain structured changes like
|
|
76
|
+
task additions, updates, removals, and metadata modifications.
|
|
77
|
+
"""
|
|
78
|
+
start_time = time.perf_counter()
|
|
79
|
+
cli_ctx = get_context(ctx)
|
|
80
|
+
specs_dir = cli_ctx.specs_dir
|
|
81
|
+
|
|
82
|
+
if specs_dir is None:
|
|
83
|
+
emit_error(
|
|
84
|
+
"No specs directory found",
|
|
85
|
+
code="VALIDATION_ERROR",
|
|
86
|
+
error_type="validation",
|
|
87
|
+
remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
|
|
88
|
+
details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# Load modifications from file
|
|
94
|
+
modifications = load_modifications_file(modifications_file)
|
|
95
|
+
|
|
96
|
+
# Apply modifications using native Python API
|
|
97
|
+
applied, skipped, changes = apply_modifications(
|
|
98
|
+
spec_id=spec_id,
|
|
99
|
+
modifications=modifications,
|
|
100
|
+
specs_dir=specs_dir,
|
|
101
|
+
dry_run=dry_run,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
105
|
+
|
|
106
|
+
emit_success(
|
|
107
|
+
{
|
|
108
|
+
"spec_id": spec_id,
|
|
109
|
+
"dry_run": dry_run,
|
|
110
|
+
"modifications_applied": applied,
|
|
111
|
+
"modifications_skipped": skipped,
|
|
112
|
+
"changes": changes,
|
|
113
|
+
"output_path": output_file if output_file else str(specs_dir),
|
|
114
|
+
"telemetry": {"duration_ms": round(duration_ms, 2)},
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
except FileNotFoundError as e:
|
|
119
|
+
emit_error(
|
|
120
|
+
str(e),
|
|
121
|
+
code="FILE_NOT_FOUND",
|
|
122
|
+
error_type="validation",
|
|
123
|
+
remediation="Check that the modifications file and spec exist",
|
|
124
|
+
details={
|
|
125
|
+
"spec_id": spec_id,
|
|
126
|
+
"modifications_file": modifications_file,
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
except json.JSONDecodeError as e:
|
|
130
|
+
emit_error(
|
|
131
|
+
f"Invalid JSON in modifications file: {e}",
|
|
132
|
+
code="INVALID_JSON",
|
|
133
|
+
error_type="validation",
|
|
134
|
+
remediation="Check that the modifications file is valid JSON",
|
|
135
|
+
details={
|
|
136
|
+
"spec_id": spec_id,
|
|
137
|
+
"modifications_file": modifications_file,
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
except ValueError as e:
|
|
141
|
+
emit_error(
|
|
142
|
+
str(e),
|
|
143
|
+
code="APPLY_FAILED",
|
|
144
|
+
error_type="internal",
|
|
145
|
+
remediation="Verify spec exists and modifications are valid",
|
|
146
|
+
details={
|
|
147
|
+
"spec_id": spec_id,
|
|
148
|
+
"modifications_file": modifications_file,
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# Phase subgroup
|
|
154
|
+
@modify_group.group("phase")
|
|
155
|
+
def modify_phase_group() -> None:
|
|
156
|
+
"""Phase modification commands."""
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@modify_phase_group.command("add")
|
|
161
|
+
@click.argument("spec_id")
|
|
162
|
+
@click.option(
|
|
163
|
+
"--title",
|
|
164
|
+
required=True,
|
|
165
|
+
help="Phase title.",
|
|
166
|
+
)
|
|
167
|
+
@click.option(
|
|
168
|
+
"--description",
|
|
169
|
+
help="Phase description or scope.",
|
|
170
|
+
)
|
|
171
|
+
@click.option(
|
|
172
|
+
"--purpose",
|
|
173
|
+
help="Phase purpose metadata.",
|
|
174
|
+
)
|
|
175
|
+
@click.option(
|
|
176
|
+
"--estimated-hours",
|
|
177
|
+
"estimated_hours",
|
|
178
|
+
type=float,
|
|
179
|
+
help="Estimated hours for this phase.",
|
|
180
|
+
)
|
|
181
|
+
@click.option(
|
|
182
|
+
"--position",
|
|
183
|
+
type=int,
|
|
184
|
+
help="Insertion index under spec-root children (0-based).",
|
|
185
|
+
)
|
|
186
|
+
@click.option(
|
|
187
|
+
"--link-previous/--no-link-previous",
|
|
188
|
+
default=True,
|
|
189
|
+
show_default=True,
|
|
190
|
+
help="Automatically block on the previous phase when appending.",
|
|
191
|
+
)
|
|
192
|
+
@click.option(
|
|
193
|
+
"--dry-run",
|
|
194
|
+
is_flag=True,
|
|
195
|
+
help="Preview changes without applying.",
|
|
196
|
+
)
|
|
197
|
+
@click.pass_context
|
|
198
|
+
@cli_command("phase-add")
|
|
199
|
+
@handle_keyboard_interrupt()
|
|
200
|
+
@with_sync_timeout(MEDIUM_TIMEOUT, "Add phase timed out")
|
|
201
|
+
def modify_phase_add_cmd(
|
|
202
|
+
ctx: click.Context,
|
|
203
|
+
spec_id: str,
|
|
204
|
+
title: str,
|
|
205
|
+
description: Optional[str],
|
|
206
|
+
purpose: Optional[str],
|
|
207
|
+
estimated_hours: Optional[float],
|
|
208
|
+
position: Optional[int],
|
|
209
|
+
link_previous: bool,
|
|
210
|
+
dry_run: bool,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Add a new phase to a specification."""
|
|
213
|
+
start_time = time.perf_counter()
|
|
214
|
+
cli_ctx = get_context(ctx)
|
|
215
|
+
specs_dir = cli_ctx.specs_dir
|
|
216
|
+
|
|
217
|
+
if specs_dir is None:
|
|
218
|
+
emit_error(
|
|
219
|
+
"No specs directory found",
|
|
220
|
+
code="VALIDATION_ERROR",
|
|
221
|
+
error_type="validation",
|
|
222
|
+
remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
|
|
223
|
+
details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
|
|
224
|
+
)
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
if dry_run:
|
|
228
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
229
|
+
emit_success(
|
|
230
|
+
{
|
|
231
|
+
"spec_id": spec_id,
|
|
232
|
+
"title": title,
|
|
233
|
+
"dry_run": True,
|
|
234
|
+
"preview": {
|
|
235
|
+
"action": "add_phase",
|
|
236
|
+
"description": description,
|
|
237
|
+
"purpose": purpose,
|
|
238
|
+
"estimated_hours": estimated_hours,
|
|
239
|
+
"position": position,
|
|
240
|
+
"link_previous": link_previous,
|
|
241
|
+
},
|
|
242
|
+
"telemetry": {"duration_ms": round(duration_ms, 2)},
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
result, error = add_phase(
|
|
248
|
+
spec_id=spec_id,
|
|
249
|
+
title=title,
|
|
250
|
+
description=description,
|
|
251
|
+
purpose=purpose,
|
|
252
|
+
estimated_hours=estimated_hours,
|
|
253
|
+
position=position,
|
|
254
|
+
link_previous=link_previous,
|
|
255
|
+
specs_dir=specs_dir,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
259
|
+
|
|
260
|
+
if error:
|
|
261
|
+
emit_error(
|
|
262
|
+
f"Add phase failed: {error}",
|
|
263
|
+
code="ADD_FAILED",
|
|
264
|
+
error_type="internal",
|
|
265
|
+
remediation="Check that the spec exists and parameters are valid",
|
|
266
|
+
details={
|
|
267
|
+
"spec_id": spec_id,
|
|
268
|
+
"title": title,
|
|
269
|
+
},
|
|
270
|
+
)
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
emit_success(
|
|
274
|
+
{
|
|
275
|
+
"spec_id": spec_id,
|
|
276
|
+
"dry_run": False,
|
|
277
|
+
**result,
|
|
278
|
+
"telemetry": {"duration_ms": round(duration_ms, 2)},
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# Task subgroup
|
|
284
|
+
@modify_group.group("task")
|
|
285
|
+
def modify_task_group() -> None:
|
|
286
|
+
"""Task modification commands."""
|
|
287
|
+
pass
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@modify_task_group.command("add")
|
|
291
|
+
@click.argument("spec_id")
|
|
292
|
+
@click.option(
|
|
293
|
+
"--parent",
|
|
294
|
+
required=True,
|
|
295
|
+
help="Parent node ID (e.g., phase-1, task-2-1).",
|
|
296
|
+
)
|
|
297
|
+
@click.option(
|
|
298
|
+
"--title",
|
|
299
|
+
required=True,
|
|
300
|
+
help="Task title.",
|
|
301
|
+
)
|
|
302
|
+
@click.option(
|
|
303
|
+
"--description",
|
|
304
|
+
help="Task description.",
|
|
305
|
+
)
|
|
306
|
+
@click.option(
|
|
307
|
+
"--type",
|
|
308
|
+
"task_type",
|
|
309
|
+
type=click.Choice(["task", "subtask", "verify"]),
|
|
310
|
+
default="task",
|
|
311
|
+
help="Task type.",
|
|
312
|
+
)
|
|
313
|
+
@click.option(
|
|
314
|
+
"--hours",
|
|
315
|
+
type=float,
|
|
316
|
+
help="Estimated hours.",
|
|
317
|
+
)
|
|
318
|
+
@click.option(
|
|
319
|
+
"--position",
|
|
320
|
+
type=int,
|
|
321
|
+
help="Position in parent's children list (0-based).",
|
|
322
|
+
)
|
|
323
|
+
@click.option(
|
|
324
|
+
"--dry-run",
|
|
325
|
+
is_flag=True,
|
|
326
|
+
help="Preview changes without applying.",
|
|
327
|
+
)
|
|
328
|
+
@click.pass_context
|
|
329
|
+
@cli_command("task-add")
|
|
330
|
+
@handle_keyboard_interrupt()
|
|
331
|
+
@with_sync_timeout(MEDIUM_TIMEOUT, "Add task timed out")
|
|
332
|
+
def modify_task_add_cmd(
|
|
333
|
+
ctx: click.Context,
|
|
334
|
+
spec_id: str,
|
|
335
|
+
parent: str,
|
|
336
|
+
title: str,
|
|
337
|
+
description: Optional[str],
|
|
338
|
+
task_type: str,
|
|
339
|
+
hours: Optional[float],
|
|
340
|
+
position: Optional[int],
|
|
341
|
+
dry_run: bool,
|
|
342
|
+
) -> None:
|
|
343
|
+
"""Add a new task to a specification.
|
|
344
|
+
|
|
345
|
+
SPEC_ID is the specification identifier.
|
|
346
|
+
"""
|
|
347
|
+
start_time = time.perf_counter()
|
|
348
|
+
cli_ctx = get_context(ctx)
|
|
349
|
+
specs_dir = cli_ctx.specs_dir
|
|
350
|
+
|
|
351
|
+
if specs_dir is None:
|
|
352
|
+
emit_error(
|
|
353
|
+
"No specs directory found",
|
|
354
|
+
code="VALIDATION_ERROR",
|
|
355
|
+
error_type="validation",
|
|
356
|
+
remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
|
|
357
|
+
details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
|
|
358
|
+
)
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
if dry_run:
|
|
362
|
+
# For dry_run, we just validate and preview without saving
|
|
363
|
+
# The native add_task function doesn't support dry_run directly,
|
|
364
|
+
# so we emit a preview response
|
|
365
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
366
|
+
emit_success(
|
|
367
|
+
{
|
|
368
|
+
"spec_id": spec_id,
|
|
369
|
+
"parent": parent,
|
|
370
|
+
"title": title,
|
|
371
|
+
"type": task_type,
|
|
372
|
+
"dry_run": True,
|
|
373
|
+
"preview": {
|
|
374
|
+
"action": "add_task",
|
|
375
|
+
"description": description,
|
|
376
|
+
"estimated_hours": hours,
|
|
377
|
+
"position": position,
|
|
378
|
+
},
|
|
379
|
+
"telemetry": {"duration_ms": round(duration_ms, 2)},
|
|
380
|
+
}
|
|
381
|
+
)
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
# Use native add_task function
|
|
385
|
+
result, error = add_task(
|
|
386
|
+
spec_id=spec_id,
|
|
387
|
+
parent_id=parent,
|
|
388
|
+
title=title,
|
|
389
|
+
description=description,
|
|
390
|
+
task_type=task_type,
|
|
391
|
+
estimated_hours=hours,
|
|
392
|
+
position=position,
|
|
393
|
+
specs_dir=specs_dir,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
397
|
+
|
|
398
|
+
if error:
|
|
399
|
+
emit_error(
|
|
400
|
+
f"Add task failed: {error}",
|
|
401
|
+
code="ADD_FAILED",
|
|
402
|
+
error_type="internal",
|
|
403
|
+
remediation="Check that the parent node exists and parameters are valid",
|
|
404
|
+
details={
|
|
405
|
+
"spec_id": spec_id,
|
|
406
|
+
"parent": parent,
|
|
407
|
+
},
|
|
408
|
+
)
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
emit_success(
|
|
412
|
+
{
|
|
413
|
+
"spec_id": spec_id,
|
|
414
|
+
"parent": parent,
|
|
415
|
+
"title": title,
|
|
416
|
+
"type": task_type,
|
|
417
|
+
"dry_run": False,
|
|
418
|
+
**result,
|
|
419
|
+
"telemetry": {"duration_ms": round(duration_ms, 2)},
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@modify_task_group.command("remove")
|
|
425
|
+
@click.argument("spec_id")
|
|
426
|
+
@click.argument("task_id")
|
|
427
|
+
@click.option(
|
|
428
|
+
"--cascade",
|
|
429
|
+
is_flag=True,
|
|
430
|
+
help="Also remove all child tasks recursively.",
|
|
431
|
+
)
|
|
432
|
+
@click.option(
|
|
433
|
+
"--dry-run",
|
|
434
|
+
is_flag=True,
|
|
435
|
+
help="Preview changes without applying.",
|
|
436
|
+
)
|
|
437
|
+
@click.pass_context
|
|
438
|
+
@cli_command("task-remove")
|
|
439
|
+
@handle_keyboard_interrupt()
|
|
440
|
+
@with_sync_timeout(MEDIUM_TIMEOUT, "Remove task timed out")
|
|
441
|
+
def modify_task_remove_cmd(
|
|
442
|
+
ctx: click.Context,
|
|
443
|
+
spec_id: str,
|
|
444
|
+
task_id: str,
|
|
445
|
+
cascade: bool,
|
|
446
|
+
dry_run: bool,
|
|
447
|
+
) -> None:
|
|
448
|
+
"""Remove a task from a specification.
|
|
449
|
+
|
|
450
|
+
SPEC_ID is the specification identifier.
|
|
451
|
+
TASK_ID is the task to remove.
|
|
452
|
+
"""
|
|
453
|
+
start_time = time.perf_counter()
|
|
454
|
+
cli_ctx = get_context(ctx)
|
|
455
|
+
specs_dir = cli_ctx.specs_dir
|
|
456
|
+
|
|
457
|
+
if specs_dir is None:
|
|
458
|
+
emit_error(
|
|
459
|
+
"No specs directory found",
|
|
460
|
+
code="VALIDATION_ERROR",
|
|
461
|
+
error_type="validation",
|
|
462
|
+
remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
|
|
463
|
+
details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
|
|
464
|
+
)
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
if dry_run:
|
|
468
|
+
# For dry_run, emit a preview response
|
|
469
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
470
|
+
emit_success(
|
|
471
|
+
{
|
|
472
|
+
"spec_id": spec_id,
|
|
473
|
+
"task_id": task_id,
|
|
474
|
+
"cascade": cascade,
|
|
475
|
+
"dry_run": True,
|
|
476
|
+
"preview": {
|
|
477
|
+
"action": "remove_task",
|
|
478
|
+
"cascade": cascade,
|
|
479
|
+
},
|
|
480
|
+
"telemetry": {"duration_ms": round(duration_ms, 2)},
|
|
481
|
+
}
|
|
482
|
+
)
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
# Use native remove_task function
|
|
486
|
+
result, error = remove_task(
|
|
487
|
+
spec_id=spec_id,
|
|
488
|
+
task_id=task_id,
|
|
489
|
+
cascade=cascade,
|
|
490
|
+
specs_dir=specs_dir,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
494
|
+
|
|
495
|
+
if error:
|
|
496
|
+
emit_error(
|
|
497
|
+
f"Remove task failed: {error}",
|
|
498
|
+
code="REMOVE_FAILED",
|
|
499
|
+
error_type="internal",
|
|
500
|
+
remediation="Verify task exists and spec structure is valid",
|
|
501
|
+
details={
|
|
502
|
+
"spec_id": spec_id,
|
|
503
|
+
"task_id": task_id,
|
|
504
|
+
},
|
|
505
|
+
)
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
emit_success(
|
|
509
|
+
{
|
|
510
|
+
"spec_id": spec_id,
|
|
511
|
+
"task_id": task_id,
|
|
512
|
+
"cascade": cascade,
|
|
513
|
+
"dry_run": False,
|
|
514
|
+
**result,
|
|
515
|
+
"telemetry": {"duration_ms": round(duration_ms, 2)},
|
|
516
|
+
}
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@modify_group.command("assumption")
|
|
521
|
+
@click.argument("spec_id")
|
|
522
|
+
@click.option(
|
|
523
|
+
"--text",
|
|
524
|
+
required=True,
|
|
525
|
+
help="Assumption text/description.",
|
|
526
|
+
)
|
|
527
|
+
@click.option(
|
|
528
|
+
"--type",
|
|
529
|
+
"assumption_type",
|
|
530
|
+
type=click.Choice(["constraint", "requirement"]),
|
|
531
|
+
help="Type of assumption.",
|
|
532
|
+
)
|
|
533
|
+
@click.option(
|
|
534
|
+
"--author",
|
|
535
|
+
help="Author who added the assumption.",
|
|
536
|
+
)
|
|
537
|
+
@click.option(
|
|
538
|
+
"--dry-run",
|
|
539
|
+
is_flag=True,
|
|
540
|
+
help="Preview changes without applying.",
|
|
541
|
+
)
|
|
542
|
+
@click.pass_context
|
|
543
|
+
@cli_command("assumption")
|
|
544
|
+
@handle_keyboard_interrupt()
|
|
545
|
+
@with_sync_timeout(MEDIUM_TIMEOUT, "Add assumption timed out")
|
|
546
|
+
def modify_assumption_cmd(
|
|
547
|
+
ctx: click.Context,
|
|
548
|
+
spec_id: str,
|
|
549
|
+
text: str,
|
|
550
|
+
assumption_type: Optional[str],
|
|
551
|
+
author: Optional[str],
|
|
552
|
+
dry_run: bool,
|
|
553
|
+
) -> None:
|
|
554
|
+
"""Add an assumption to a specification.
|
|
555
|
+
|
|
556
|
+
SPEC_ID is the specification identifier.
|
|
557
|
+
"""
|
|
558
|
+
start_time = time.perf_counter()
|
|
559
|
+
cli_ctx = get_context(ctx)
|
|
560
|
+
specs_dir = cli_ctx.specs_dir
|
|
561
|
+
|
|
562
|
+
if specs_dir is None:
|
|
563
|
+
emit_error(
|
|
564
|
+
"No specs directory found",
|
|
565
|
+
code="VALIDATION_ERROR",
|
|
566
|
+
error_type="validation",
|
|
567
|
+
remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
|
|
568
|
+
details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
|
|
569
|
+
)
|
|
570
|
+
return
|
|
571
|
+
|
|
572
|
+
if dry_run:
|
|
573
|
+
# For dry_run, emit a preview response
|
|
574
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
575
|
+
emit_success(
|
|
576
|
+
{
|
|
577
|
+
"spec_id": spec_id,
|
|
578
|
+
"text": text,
|
|
579
|
+
"type": assumption_type or "constraint",
|
|
580
|
+
"dry_run": True,
|
|
581
|
+
"preview": {
|
|
582
|
+
"action": "add_assumption",
|
|
583
|
+
"author": author,
|
|
584
|
+
},
|
|
585
|
+
"telemetry": {"duration_ms": round(duration_ms, 2)},
|
|
586
|
+
}
|
|
587
|
+
)
|
|
588
|
+
return
|
|
589
|
+
|
|
590
|
+
# Use native add_assumption function
|
|
591
|
+
result, error = add_assumption(
|
|
592
|
+
spec_id=spec_id,
|
|
593
|
+
text=text,
|
|
594
|
+
assumption_type=assumption_type or "constraint",
|
|
595
|
+
author=author,
|
|
596
|
+
specs_dir=specs_dir,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
600
|
+
|
|
601
|
+
if error:
|
|
602
|
+
emit_error(
|
|
603
|
+
f"Add assumption failed: {error}",
|
|
604
|
+
code="ADD_FAILED",
|
|
605
|
+
error_type="internal",
|
|
606
|
+
remediation="Verify spec exists and assumption format is valid",
|
|
607
|
+
details={
|
|
608
|
+
"spec_id": spec_id,
|
|
609
|
+
},
|
|
610
|
+
)
|
|
611
|
+
return
|
|
612
|
+
|
|
613
|
+
emit_success(
|
|
614
|
+
{
|
|
615
|
+
"spec_id": spec_id,
|
|
616
|
+
"text": text,
|
|
617
|
+
"type": assumption_type or "constraint",
|
|
618
|
+
"dry_run": False,
|
|
619
|
+
**result,
|
|
620
|
+
"telemetry": {"duration_ms": round(duration_ms, 2)},
|
|
621
|
+
}
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
@modify_group.command("revision")
|
|
626
|
+
@click.argument("spec_id")
|
|
627
|
+
@click.option(
|
|
628
|
+
"--version",
|
|
629
|
+
required=True,
|
|
630
|
+
help="Revision version (e.g., 1.1, 2.0).",
|
|
631
|
+
)
|
|
632
|
+
@click.option(
|
|
633
|
+
"--changes",
|
|
634
|
+
required=True,
|
|
635
|
+
help="Summary of changes.",
|
|
636
|
+
)
|
|
637
|
+
@click.option(
|
|
638
|
+
"--author",
|
|
639
|
+
help="Revision author.",
|
|
640
|
+
)
|
|
641
|
+
@click.option(
|
|
642
|
+
"--dry-run",
|
|
643
|
+
is_flag=True,
|
|
644
|
+
help="Preview changes without applying.",
|
|
645
|
+
)
|
|
646
|
+
@click.pass_context
|
|
647
|
+
@cli_command("revision")
|
|
648
|
+
@handle_keyboard_interrupt()
|
|
649
|
+
@with_sync_timeout(MEDIUM_TIMEOUT, "Add revision timed out")
|
|
650
|
+
def modify_revision_cmd(
|
|
651
|
+
ctx: click.Context,
|
|
652
|
+
spec_id: str,
|
|
653
|
+
version: str,
|
|
654
|
+
changes: str,
|
|
655
|
+
author: Optional[str],
|
|
656
|
+
dry_run: bool,
|
|
657
|
+
) -> None:
|
|
658
|
+
"""Add a revision history entry to a specification.
|
|
659
|
+
|
|
660
|
+
SPEC_ID is the specification identifier.
|
|
661
|
+
"""
|
|
662
|
+
start_time = time.perf_counter()
|
|
663
|
+
cli_ctx = get_context(ctx)
|
|
664
|
+
specs_dir = cli_ctx.specs_dir
|
|
665
|
+
|
|
666
|
+
if specs_dir is None:
|
|
667
|
+
emit_error(
|
|
668
|
+
"No specs directory found",
|
|
669
|
+
code="VALIDATION_ERROR",
|
|
670
|
+
error_type="validation",
|
|
671
|
+
remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
|
|
672
|
+
details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
|
|
673
|
+
)
|
|
674
|
+
return
|
|
675
|
+
|
|
676
|
+
if dry_run:
|
|
677
|
+
# For dry_run, emit a preview response
|
|
678
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
679
|
+
emit_success(
|
|
680
|
+
{
|
|
681
|
+
"spec_id": spec_id,
|
|
682
|
+
"version": version,
|
|
683
|
+
"changes": changes,
|
|
684
|
+
"dry_run": True,
|
|
685
|
+
"preview": {
|
|
686
|
+
"action": "add_revision",
|
|
687
|
+
"author": author,
|
|
688
|
+
},
|
|
689
|
+
"telemetry": {"duration_ms": round(duration_ms, 2)},
|
|
690
|
+
}
|
|
691
|
+
)
|
|
692
|
+
return
|
|
693
|
+
|
|
694
|
+
# Use native add_revision function
|
|
695
|
+
result, error = add_revision(
|
|
696
|
+
spec_id=spec_id,
|
|
697
|
+
version=version,
|
|
698
|
+
changelog=changes,
|
|
699
|
+
author=author,
|
|
700
|
+
specs_dir=specs_dir,
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
704
|
+
|
|
705
|
+
if error:
|
|
706
|
+
emit_error(
|
|
707
|
+
f"Add revision failed: {error}",
|
|
708
|
+
code="ADD_FAILED",
|
|
709
|
+
error_type="internal",
|
|
710
|
+
remediation="Verify spec exists and revision format is valid",
|
|
711
|
+
details={
|
|
712
|
+
"spec_id": spec_id,
|
|
713
|
+
},
|
|
714
|
+
)
|
|
715
|
+
return
|
|
716
|
+
|
|
717
|
+
emit_success(
|
|
718
|
+
{
|
|
719
|
+
"spec_id": spec_id,
|
|
720
|
+
"version": version,
|
|
721
|
+
"changes": changes,
|
|
722
|
+
"dry_run": False,
|
|
723
|
+
**result,
|
|
724
|
+
"telemetry": {"duration_ms": round(duration_ms, 2)},
|
|
725
|
+
}
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
@modify_group.command("frontmatter")
|
|
730
|
+
@click.argument("spec_id")
|
|
731
|
+
@click.option(
|
|
732
|
+
"--key",
|
|
733
|
+
required=True,
|
|
734
|
+
help="Frontmatter key to update (e.g., title, status, version).",
|
|
735
|
+
)
|
|
736
|
+
@click.option(
|
|
737
|
+
"--value",
|
|
738
|
+
required=True,
|
|
739
|
+
help="New value for the key.",
|
|
740
|
+
)
|
|
741
|
+
@click.option(
|
|
742
|
+
"--dry-run",
|
|
743
|
+
is_flag=True,
|
|
744
|
+
help="Preview changes without applying.",
|
|
745
|
+
)
|
|
746
|
+
@click.pass_context
|
|
747
|
+
@cli_command("frontmatter")
|
|
748
|
+
@handle_keyboard_interrupt()
|
|
749
|
+
@with_sync_timeout(MEDIUM_TIMEOUT, "Update frontmatter timed out")
|
|
750
|
+
def modify_frontmatter_cmd(
|
|
751
|
+
ctx: click.Context,
|
|
752
|
+
spec_id: str,
|
|
753
|
+
key: str,
|
|
754
|
+
value: str,
|
|
755
|
+
dry_run: bool,
|
|
756
|
+
) -> None:
|
|
757
|
+
"""Update frontmatter metadata in a specification.
|
|
758
|
+
|
|
759
|
+
SPEC_ID is the specification identifier.
|
|
760
|
+
"""
|
|
761
|
+
start_time = time.perf_counter()
|
|
762
|
+
cli_ctx = get_context(ctx)
|
|
763
|
+
specs_dir = cli_ctx.specs_dir
|
|
764
|
+
|
|
765
|
+
if specs_dir is None:
|
|
766
|
+
emit_error(
|
|
767
|
+
"No specs directory found",
|
|
768
|
+
code="VALIDATION_ERROR",
|
|
769
|
+
error_type="validation",
|
|
770
|
+
remediation="Use --specs-dir option or set SDD_SPECS_DIR environment variable",
|
|
771
|
+
details={"hint": "Use --specs-dir or set SDD_SPECS_DIR"},
|
|
772
|
+
)
|
|
773
|
+
return
|
|
774
|
+
|
|
775
|
+
if dry_run:
|
|
776
|
+
# For dry_run, emit a preview response
|
|
777
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
778
|
+
emit_success(
|
|
779
|
+
{
|
|
780
|
+
"spec_id": spec_id,
|
|
781
|
+
"key": key,
|
|
782
|
+
"value": value,
|
|
783
|
+
"dry_run": True,
|
|
784
|
+
"preview": {
|
|
785
|
+
"action": "update_frontmatter",
|
|
786
|
+
},
|
|
787
|
+
"telemetry": {"duration_ms": round(duration_ms, 2)},
|
|
788
|
+
}
|
|
789
|
+
)
|
|
790
|
+
return
|
|
791
|
+
|
|
792
|
+
# Use native update_frontmatter function
|
|
793
|
+
result, error = update_frontmatter(
|
|
794
|
+
spec_id=spec_id,
|
|
795
|
+
key=key,
|
|
796
|
+
value=value,
|
|
797
|
+
specs_dir=specs_dir,
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
801
|
+
|
|
802
|
+
if error:
|
|
803
|
+
emit_error(
|
|
804
|
+
f"Update frontmatter failed: {error}",
|
|
805
|
+
code="UPDATE_FAILED",
|
|
806
|
+
error_type="internal",
|
|
807
|
+
remediation="Verify spec exists and frontmatter key is valid",
|
|
808
|
+
details={
|
|
809
|
+
"spec_id": spec_id,
|
|
810
|
+
"key": key,
|
|
811
|
+
},
|
|
812
|
+
)
|
|
813
|
+
return
|
|
814
|
+
|
|
815
|
+
emit_success(
|
|
816
|
+
{
|
|
817
|
+
"spec_id": spec_id,
|
|
818
|
+
"key": key,
|
|
819
|
+
"value": value,
|
|
820
|
+
"dry_run": False,
|
|
821
|
+
**result,
|
|
822
|
+
"telemetry": {"duration_ms": round(duration_ms, 2)},
|
|
823
|
+
}
|
|
824
|
+
)
|