sdd-parallel 0.1.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.
- sdd_parallel/__init__.py +28 -0
- sdd_parallel/adapters/__init__.py +168 -0
- sdd_parallel/adapters/beads.py +324 -0
- sdd_parallel/adapters/bmad.py +637 -0
- sdd_parallel/adapters/gsd_hijack.py +403 -0
- sdd_parallel/adapters/openspec.py +419 -0
- sdd_parallel/adapters/spec_kit.py +483 -0
- sdd_parallel/ast_scanner.py +209 -0
- sdd_parallel/autopilot.py +167 -0
- sdd_parallel/cli.py +740 -0
- sdd_parallel/crg_sidecar.py +137 -0
- sdd_parallel/demo.py +897 -0
- sdd_parallel/hooks/__init__.py +12 -0
- sdd_parallel/hooks/blast_radius_guard.py +339 -0
- sdd_parallel/models.py +230 -0
- sdd_parallel/plugin_installer.py +162 -0
- sdd_parallel/py.typed +0 -0
- sdd_parallel/resources/SKILL.md +130 -0
- sdd_parallel/resources/__init__.py +1 -0
- sdd_parallel/scheduler.py +378 -0
- sdd_parallel/status_merger.py +284 -0
- sdd_parallel/worktree.py +1346 -0
- sdd_parallel-0.1.0.dist-info/METADATA +320 -0
- sdd_parallel-0.1.0.dist-info/RECORD +27 -0
- sdd_parallel-0.1.0.dist-info/WHEEL +4 -0
- sdd_parallel-0.1.0.dist-info/entry_points.txt +4 -0
- sdd_parallel-0.1.0.dist-info/licenses/LICENSE +21 -0
sdd_parallel/cli.py
ADDED
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
"""Command-line interface for sdd-parallel.
|
|
2
|
+
|
|
3
|
+
Subcommands: plan, prepare, claim-track, verify-wave, merge, merge-status,
|
|
4
|
+
check-lock, install-plugin, autopilot, abort-wave, rollback-wave, run-guide, demo.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from sdd_parallel.adapters import diagnose_multi_kit_coexistence, get_adapter
|
|
17
|
+
from sdd_parallel.ast_scanner import ASTHotspotScanner
|
|
18
|
+
from sdd_parallel.autopilot import AutopilotLauncher
|
|
19
|
+
from sdd_parallel.crg_sidecar import CRGSidecarAdapter
|
|
20
|
+
from sdd_parallel.demo import run_demo
|
|
21
|
+
from sdd_parallel.hooks.blast_radius_guard import evaluate_tool_access
|
|
22
|
+
from sdd_parallel.models import TaskNode, WavePlan
|
|
23
|
+
from sdd_parallel.plugin_installer import install_plugin
|
|
24
|
+
from sdd_parallel.scheduler import BSPWaveScheduler
|
|
25
|
+
from sdd_parallel.status_merger import merge_sprint_status_files
|
|
26
|
+
from sdd_parallel.worktree import (
|
|
27
|
+
LIFORollbackViolationError,
|
|
28
|
+
WorktreeManager,
|
|
29
|
+
WorktreeRollbackManager,
|
|
30
|
+
default_antigravity_worktrees_dir,
|
|
31
|
+
read_parallel_state,
|
|
32
|
+
write_parallel_state,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
37
|
+
parser = argparse.ArgumentParser(
|
|
38
|
+
prog="sdd-parallel",
|
|
39
|
+
description="SDD-agnostic multi-track parallel roadmap orchestrator and worktree manager",
|
|
40
|
+
)
|
|
41
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
42
|
+
|
|
43
|
+
# plan
|
|
44
|
+
p_plan = sub.add_parser("plan", help="Analyze SDD roadmap, AST/CRG graph, and build WavePlan")
|
|
45
|
+
p_plan.add_argument("--repo", type=Path, default=Path.cwd(), help="Target repository root")
|
|
46
|
+
p_plan.add_argument("--max-tracks", type=int, default=3, help="Max parallel tracks per wave")
|
|
47
|
+
p_plan.add_argument(
|
|
48
|
+
"--kit",
|
|
49
|
+
type=str,
|
|
50
|
+
default="auto",
|
|
51
|
+
help="SDD kit adapter (auto, bmad, spec-kit, gsd, openspec, beads)",
|
|
52
|
+
)
|
|
53
|
+
p_plan.add_argument(
|
|
54
|
+
"--include-done",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help="Include already completed stories in the schedule analysis",
|
|
57
|
+
)
|
|
58
|
+
p_plan.add_argument(
|
|
59
|
+
"--dry-run",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help="Print execution plan without writing artifacts into the repository",
|
|
62
|
+
)
|
|
63
|
+
p_plan.add_argument(
|
|
64
|
+
"--plan-source",
|
|
65
|
+
type=str,
|
|
66
|
+
default="auto",
|
|
67
|
+
choices=["auto", "plan", "specs"],
|
|
68
|
+
help="Source of tasks and file footprints (auto, plan, specs)",
|
|
69
|
+
)
|
|
70
|
+
p_plan.add_argument(
|
|
71
|
+
"--output-dir",
|
|
72
|
+
type=Path,
|
|
73
|
+
default=None,
|
|
74
|
+
help="Optional custom output directory for execution-plan-parallel.md and wave-plan.json",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# prepare
|
|
78
|
+
p_prep = sub.add_parser("prepare", help="Provision git worktrees and inject boundary locks")
|
|
79
|
+
p_prep.add_argument("--repo", type=Path, default=Path.cwd(), help="Target repository root")
|
|
80
|
+
p_prep.add_argument("--wave", type=int, required=True, help="Wave number to prepare")
|
|
81
|
+
p_prep.add_argument("--max-tracks", type=int, default=3, help="Max parallel tracks per wave")
|
|
82
|
+
p_prep.add_argument("--kit", type=str, default="auto", help="SDD kit adapter")
|
|
83
|
+
p_prep.add_argument("--headless", action="store_true", help="Enable Silent File-State Handoff")
|
|
84
|
+
p_prep.add_argument(
|
|
85
|
+
"--antigravity-layout",
|
|
86
|
+
action="store_true",
|
|
87
|
+
help="Place or link worktrees in ~/.gemini/antigravity/worktrees/<Project>/<track>",
|
|
88
|
+
)
|
|
89
|
+
p_prep.add_argument(
|
|
90
|
+
"--dynamic-worktrees",
|
|
91
|
+
action="store_true",
|
|
92
|
+
help="Use dynamic wave-specific worktrees (wave-{W}-track-{id}) instead of permanent slots",
|
|
93
|
+
)
|
|
94
|
+
p_prep.add_argument(
|
|
95
|
+
"--dry-run", action="store_true", help="Print worktree paths without git add"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# claim-track
|
|
99
|
+
p_claim = sub.add_parser(
|
|
100
|
+
"claim-track",
|
|
101
|
+
help="Attach boundary locks to an existing worktree (e.g. Antigravity UI worktree)",
|
|
102
|
+
)
|
|
103
|
+
p_claim.add_argument("--repo", type=Path, default=Path.cwd(), help="Target repository root")
|
|
104
|
+
p_claim.add_argument("--wave", type=int, required=True, help="Wave number")
|
|
105
|
+
p_claim.add_argument("--track", type=str, required=True, help="Track ID (e.g. track-a)")
|
|
106
|
+
p_claim.add_argument("--worktree", type=Path, required=True, help="Existing worktree path")
|
|
107
|
+
p_claim.add_argument("--max-tracks", type=int, default=3, help="Max parallel tracks per wave")
|
|
108
|
+
p_claim.add_argument("--kit", type=str, default="auto", help="SDD kit adapter")
|
|
109
|
+
p_claim.add_argument("--headless", action="store_true", help="Enable Silent File-State Handoff")
|
|
110
|
+
|
|
111
|
+
# verify-wave
|
|
112
|
+
p_ver = sub.add_parser(
|
|
113
|
+
"verify-wave",
|
|
114
|
+
help="Verify that generated story specs in Wave K do not violate .parallel-lock.json",
|
|
115
|
+
)
|
|
116
|
+
p_ver.add_argument("--repo", type=Path, default=Path.cwd(), help="Target repository root")
|
|
117
|
+
p_ver.add_argument("--wave", type=int, required=True, help="Wave number to verify")
|
|
118
|
+
p_ver.add_argument("--max-tracks", type=int, default=3, help="Max parallel tracks per wave")
|
|
119
|
+
p_ver.add_argument("--kit", type=str, default="auto", help="SDD kit adapter")
|
|
120
|
+
p_ver.add_argument(
|
|
121
|
+
"--dynamic-worktrees",
|
|
122
|
+
action="store_true",
|
|
123
|
+
help="Use dynamic wave-specific worktrees (wave-{W}-track-{id}) instead of permanent slots",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# merge
|
|
127
|
+
p_merge = sub.add_parser(
|
|
128
|
+
"merge",
|
|
129
|
+
help="Barrier checkpoint merge for Wave K (3-way status merge + git merge --no-ff + gates)",
|
|
130
|
+
)
|
|
131
|
+
p_merge.add_argument("--repo", type=Path, default=Path.cwd(), help="Target repository root")
|
|
132
|
+
p_merge.add_argument("--wave", type=int, required=True, help="Wave number to merge")
|
|
133
|
+
p_merge.add_argument("--max-tracks", type=int, default=3, help="Max parallel tracks per wave")
|
|
134
|
+
p_merge.add_argument("--kit", type=str, default="auto", help="SDD kit adapter")
|
|
135
|
+
p_merge.add_argument(
|
|
136
|
+
"--dynamic-worktrees",
|
|
137
|
+
action="store_true",
|
|
138
|
+
help="Use dynamic wave-specific worktrees (wave-{W}-track-{id}) instead of permanent slots",
|
|
139
|
+
)
|
|
140
|
+
p_merge.add_argument(
|
|
141
|
+
"--skip-gates",
|
|
142
|
+
action="store_true",
|
|
143
|
+
help="Skip running checkpoint quality gates after merge",
|
|
144
|
+
)
|
|
145
|
+
p_merge.add_argument(
|
|
146
|
+
"--strict-gates",
|
|
147
|
+
action="store_true",
|
|
148
|
+
help="Fail checkpoint gate even when 0 tests are collected in the repository",
|
|
149
|
+
)
|
|
150
|
+
p_merge.add_argument(
|
|
151
|
+
"--cleanup",
|
|
152
|
+
action="store_true",
|
|
153
|
+
help="Remove finished track worktrees after successful merge",
|
|
154
|
+
)
|
|
155
|
+
p_merge.add_argument(
|
|
156
|
+
"--dry-run",
|
|
157
|
+
action="store_true",
|
|
158
|
+
help="Print branches to be merged without modifying Git state",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# merge-status
|
|
162
|
+
p_mstat = sub.add_parser(
|
|
163
|
+
"merge-status", help="3-way deterministic merge for sprint-status.yaml"
|
|
164
|
+
)
|
|
165
|
+
p_mstat.add_argument("--base", type=Path, required=True, help="Base sprint-status.yaml")
|
|
166
|
+
p_mstat.add_argument("--ours", type=Path, required=True, help="Ours sprint-status.yaml")
|
|
167
|
+
p_mstat.add_argument("--theirs", type=Path, required=True, help="Theirs sprint-status.yaml")
|
|
168
|
+
p_mstat.add_argument("--out", type=Path, required=True, help="Output merged YAML path")
|
|
169
|
+
|
|
170
|
+
# check-lock
|
|
171
|
+
p_lock = sub.add_parser("check-lock", help="Check if modifying a file is allowed in worktree")
|
|
172
|
+
p_lock.add_argument("--file", type=str, required=True, help="Target file path")
|
|
173
|
+
p_lock.add_argument("--tool", type=str, default="write_to_file", help="Tool name")
|
|
174
|
+
|
|
175
|
+
# install-plugin
|
|
176
|
+
p_inst = sub.add_parser(
|
|
177
|
+
"install-plugin",
|
|
178
|
+
help="Package and install portable sdd-parallel plugin for Google Antigravity",
|
|
179
|
+
)
|
|
180
|
+
p_inst.add_argument(
|
|
181
|
+
"--target-dir",
|
|
182
|
+
type=Path,
|
|
183
|
+
default=None,
|
|
184
|
+
help="Plugin output directory (defaults to ~/.gemini/config/plugins/sdd-parallel)",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# autopilot
|
|
188
|
+
p_auto = sub.add_parser(
|
|
189
|
+
"autopilot",
|
|
190
|
+
help="Generate or execute Multi-Conversation Autopilot launch commands for Wave K",
|
|
191
|
+
)
|
|
192
|
+
p_auto.add_argument("--repo", type=Path, default=Path.cwd(), help="Target repository root")
|
|
193
|
+
p_auto.add_argument("--wave", type=int, required=True, help="Wave number to launch")
|
|
194
|
+
p_auto.add_argument("--max-tracks", type=int, default=3, help="Max parallel tracks per wave")
|
|
195
|
+
p_auto.add_argument("--kit", type=str, default="auto", help="SDD kit adapter")
|
|
196
|
+
p_auto.add_argument(
|
|
197
|
+
"--harness",
|
|
198
|
+
type=str,
|
|
199
|
+
default="antigravity",
|
|
200
|
+
choices=["antigravity", "claude-code", "codex"],
|
|
201
|
+
help="Target AI coding harness",
|
|
202
|
+
)
|
|
203
|
+
p_auto.add_argument(
|
|
204
|
+
"--antigravity-layout",
|
|
205
|
+
action="store_true",
|
|
206
|
+
help="Target worktrees in ~/.gemini/antigravity/worktrees/<Project>/<track>",
|
|
207
|
+
)
|
|
208
|
+
p_auto.add_argument(
|
|
209
|
+
"--execute",
|
|
210
|
+
action="store_true",
|
|
211
|
+
help="Execute the generated launch commands via subprocess",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# abort-wave
|
|
215
|
+
p_abort = sub.add_parser(
|
|
216
|
+
"abort-wave",
|
|
217
|
+
help="Abort wave before merge: remove worktrees, prune, and delete parallel branches",
|
|
218
|
+
)
|
|
219
|
+
p_abort.add_argument("--repo", type=Path, default=Path.cwd(), help="Target repository root")
|
|
220
|
+
p_abort.add_argument("--wave", type=int, required=True, help="Wave number to abort")
|
|
221
|
+
p_abort.add_argument(
|
|
222
|
+
"--dynamic-worktrees",
|
|
223
|
+
action="store_true",
|
|
224
|
+
help="Use dynamic wave-specific worktrees (wave-{W}-track-{id}) instead of permanent slots",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# rollback-wave
|
|
228
|
+
p_rb = sub.add_parser(
|
|
229
|
+
"rollback-wave",
|
|
230
|
+
help="Rollback merged wave in LIFO order with safety backup and status reset",
|
|
231
|
+
)
|
|
232
|
+
p_rb.add_argument("--repo", type=Path, default=Path.cwd(), help="Target repository root")
|
|
233
|
+
p_rb.add_argument("--wave", type=int, required=True, help="Wave number to rollback")
|
|
234
|
+
p_rb.add_argument(
|
|
235
|
+
"--dynamic-worktrees",
|
|
236
|
+
action="store_true",
|
|
237
|
+
help="Use dynamic wave-specific worktrees (wave-{W}-track-{id}) instead of permanent slots",
|
|
238
|
+
)
|
|
239
|
+
p_rb.add_argument("--to-commit", type=str, default=None, help="Target commit hash to reset to")
|
|
240
|
+
p_rb.add_argument(
|
|
241
|
+
"--save-backup",
|
|
242
|
+
action="store_true",
|
|
243
|
+
default=True,
|
|
244
|
+
help="Create safety backup branch before rollback",
|
|
245
|
+
)
|
|
246
|
+
p_rb.add_argument(
|
|
247
|
+
"--no-backup",
|
|
248
|
+
action="store_false",
|
|
249
|
+
dest="save_backup",
|
|
250
|
+
help="Skip creating safety backup branch",
|
|
251
|
+
)
|
|
252
|
+
p_rb.add_argument("--kit", type=str, default="auto", help="SDD kit adapter")
|
|
253
|
+
|
|
254
|
+
# run-guide
|
|
255
|
+
p_guide = sub.add_parser(
|
|
256
|
+
"run-guide",
|
|
257
|
+
help="Display operator run-guide with worktree paths and commands for Wave K",
|
|
258
|
+
)
|
|
259
|
+
p_guide.add_argument("--repo", type=Path, default=Path.cwd(), help="Target repository root")
|
|
260
|
+
p_guide.add_argument("--wave", type=int, required=True, help="Wave number")
|
|
261
|
+
p_guide.add_argument("--max-tracks", type=int, default=3, help="Max parallel tracks per wave")
|
|
262
|
+
p_guide.add_argument("--kit", type=str, default="auto", help="SDD kit adapter")
|
|
263
|
+
p_guide.add_argument(
|
|
264
|
+
"--antigravity-layout",
|
|
265
|
+
action="store_true",
|
|
266
|
+
help="Show paths in ~/.gemini/antigravity/worktrees/<Project>/<track>",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# demo
|
|
270
|
+
p_demo = sub.add_parser(
|
|
271
|
+
"demo",
|
|
272
|
+
help="Run standalone 5-second Merge-Bomb demonstration in a temporary repository",
|
|
273
|
+
)
|
|
274
|
+
p_demo.add_argument(
|
|
275
|
+
"--kit",
|
|
276
|
+
type=str,
|
|
277
|
+
choices=["bmad", "spec-kit", "gsd", "openspec", "beads", "all"],
|
|
278
|
+
default="bmad",
|
|
279
|
+
help="Target SDD kit framework to demonstrate (or 'all' for complete 5-kit validation)",
|
|
280
|
+
)
|
|
281
|
+
p_demo.add_argument(
|
|
282
|
+
"--keep-temp",
|
|
283
|
+
action="store_true",
|
|
284
|
+
help="Keep the temporary demonstration repository after completion",
|
|
285
|
+
)
|
|
286
|
+
p_demo.add_argument(
|
|
287
|
+
"--scaffold-only",
|
|
288
|
+
action="store_true",
|
|
289
|
+
help=(
|
|
290
|
+
"Generate the synthetic Git repository with SDD artifacts and hidden Merge-Bomb, "
|
|
291
|
+
"then stop before Step 1 for manual E2E walkthrough"
|
|
292
|
+
),
|
|
293
|
+
)
|
|
294
|
+
p_demo.add_argument(
|
|
295
|
+
"--output-dir",
|
|
296
|
+
type=Path,
|
|
297
|
+
default=None,
|
|
298
|
+
help="Target directory for the demo repository (implies --keep-temp)",
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return parser
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def render_run_guide(
|
|
305
|
+
repo_root: Path,
|
|
306
|
+
plan: WavePlan,
|
|
307
|
+
wave_number: int,
|
|
308
|
+
antigravity_layout: bool = False,
|
|
309
|
+
) -> str:
|
|
310
|
+
"""Render operator instructions for Two-Stage Assisted wave execution."""
|
|
311
|
+
target_wave = next((w for w in plan.waves if w.wave_number == wave_number), None)
|
|
312
|
+
if target_wave is None:
|
|
313
|
+
return f"Error: Wave {wave_number} not found in plan."
|
|
314
|
+
|
|
315
|
+
lines: list[str] = [
|
|
316
|
+
f"# 🧭 Two-Stage Assisted Operator Guide — Wave {wave_number} ({plan.sdd_kit.upper()})",
|
|
317
|
+
"",
|
|
318
|
+
"## 1. Worktree Tracks Execution Matrix",
|
|
319
|
+
"",
|
|
320
|
+
"| Track | Stories / Tasks | Effort | Worktree Directory | Worker Command |",
|
|
321
|
+
"| :--- | :--- | :--- | :--- | :--- |",
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
_, adapter = get_adapter(repo_root, kit=plan.sdd_kit)
|
|
325
|
+
for track in target_wave.tracks:
|
|
326
|
+
tasks_str = ", ".join(track.task_ids)
|
|
327
|
+
if antigravity_layout:
|
|
328
|
+
wt_path = (
|
|
329
|
+
default_antigravity_worktrees_dir(repo_root.name)
|
|
330
|
+
/ f"wave-{wave_number}-{track.track_id}"
|
|
331
|
+
)
|
|
332
|
+
else:
|
|
333
|
+
wt_path = repo_root / ".worktrees" / f"wave-{wave_number}-{track.track_id}"
|
|
334
|
+
|
|
335
|
+
worker_cmd = adapter.render_worker_command(track)
|
|
336
|
+
|
|
337
|
+
lines.append(
|
|
338
|
+
f"| `{track.track_id}` | `{tasks_str}` | `{track.effort.value}` | "
|
|
339
|
+
f"`{wt_path.as_posix()}` | {worker_cmd} |"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
lines.extend(
|
|
343
|
+
[
|
|
344
|
+
"",
|
|
345
|
+
"## 2. Operator Workflow Instructions",
|
|
346
|
+
"",
|
|
347
|
+
"1. **Open Windows:** Open each worktree in a separate Antigravity IDE window.",
|
|
348
|
+
"2. **Launch Workers (Depth 0):** Run worker command in each window.",
|
|
349
|
+
"3. **Verify Boundaries:** Run verification before barrier merge:",
|
|
350
|
+
" ```powershell",
|
|
351
|
+
f" sdd-parallel verify-wave --wave {wave_number}",
|
|
352
|
+
" ```",
|
|
353
|
+
"4. **Barrier Checkpoint Merge:** After all workers pass quality gates:",
|
|
354
|
+
" ```powershell",
|
|
355
|
+
f" sdd-parallel merge --wave {wave_number} --cleanup",
|
|
356
|
+
" ```",
|
|
357
|
+
"5. **Rollback & Safety (If Needed):**",
|
|
358
|
+
f" - Abort before merge: `sdd-parallel abort-wave --wave {wave_number}`",
|
|
359
|
+
(
|
|
360
|
+
" - Rollback after merge: `sdd-parallel rollback-wave "
|
|
361
|
+
f"--wave {wave_number} --save-backup`"
|
|
362
|
+
),
|
|
363
|
+
]
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return "\n".join(lines)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _compute_repo_plan(
|
|
370
|
+
repo_root: Path,
|
|
371
|
+
max_tracks: int,
|
|
372
|
+
include_done: bool = False,
|
|
373
|
+
kit: str = "auto",
|
|
374
|
+
plan_source: str = "auto",
|
|
375
|
+
) -> tuple[BSPWaveScheduler, list[TaskNode], WavePlan]:
|
|
376
|
+
kit_name, adapter = get_adapter(repo_root, kit=kit)
|
|
377
|
+
scanner = ASTHotspotScanner(repo_root)
|
|
378
|
+
meta_map = scanner.scan()
|
|
379
|
+
file_roles = {rel: meta.role for rel, meta in meta_map.items()}
|
|
380
|
+
crg = CRGSidecarAdapter(repo_root, ast_scanner=scanner)
|
|
381
|
+
|
|
382
|
+
tasks = adapter.load_tasks(include_done=include_done, plan_source=plan_source)
|
|
383
|
+
for task in tasks:
|
|
384
|
+
if task.write_files and not task.impact_files:
|
|
385
|
+
task.impact_files = sorted(crg.compute_impact_files(task.write_files))
|
|
386
|
+
|
|
387
|
+
blockers = adapter.check_wave_zero_blockers()
|
|
388
|
+
scheduler = BSPWaveScheduler(max_tracks=max_tracks, file_roles=file_roles)
|
|
389
|
+
plan = scheduler.schedule(tasks, sdd_kit=kit_name, wave_zero_blockers=blockers)
|
|
390
|
+
return scheduler, tasks, plan
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
394
|
+
parser = _build_parser()
|
|
395
|
+
args = parser.parse_args(argv)
|
|
396
|
+
|
|
397
|
+
if args.command == "plan":
|
|
398
|
+
repo_root: Path = args.repo.resolve()
|
|
399
|
+
scheduler, tasks, plan = _compute_repo_plan(
|
|
400
|
+
repo_root=repo_root,
|
|
401
|
+
max_tracks=args.max_tracks,
|
|
402
|
+
include_done=args.include_done,
|
|
403
|
+
kit=args.kit,
|
|
404
|
+
plan_source=args.plan_source,
|
|
405
|
+
)
|
|
406
|
+
md_text = scheduler.render_markdown_plan(plan, tasks)
|
|
407
|
+
if not args.dry_run:
|
|
408
|
+
out_dir = (
|
|
409
|
+
args.output_dir.resolve()
|
|
410
|
+
if args.output_dir is not None
|
|
411
|
+
else (repo_root / "_bmad-output" / "parallel-artifacts")
|
|
412
|
+
)
|
|
413
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
414
|
+
(out_dir / "execution-plan-parallel.md").write_text(md_text, encoding="utf-8")
|
|
415
|
+
plan_dict = plan.to_dict()
|
|
416
|
+
(out_dir / "wave-plan.json").write_text(
|
|
417
|
+
json.dumps(plan_dict, indent=2, ensure_ascii=False) + "\n",
|
|
418
|
+
encoding="utf-8",
|
|
419
|
+
)
|
|
420
|
+
# Monotonic history archive: .parallel/history/run-NNN-<timestamp>/wave-plan.json
|
|
421
|
+
p_state = read_parallel_state(repo_root)
|
|
422
|
+
run_id = int(p_state.get("run_counter", 0)) + 1
|
|
423
|
+
p_state["run_counter"] = run_id
|
|
424
|
+
write_parallel_state(repo_root, p_state)
|
|
425
|
+
ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
|
426
|
+
hist_dir = repo_root / ".parallel" / "history" / f"run-{run_id:03d}-{ts}"
|
|
427
|
+
hist_dir.mkdir(parents=True, exist_ok=True)
|
|
428
|
+
(hist_dir / "wave-plan.json").write_text(
|
|
429
|
+
json.dumps(plan_dict, indent=2, ensure_ascii=False) + "\n",
|
|
430
|
+
encoding="utf-8",
|
|
431
|
+
)
|
|
432
|
+
for warn in diagnose_multi_kit_coexistence(repo_root, plan.sdd_kit):
|
|
433
|
+
print(f"[NOTE] {warn}", file=sys.stderr)
|
|
434
|
+
print(md_text)
|
|
435
|
+
return 0
|
|
436
|
+
|
|
437
|
+
if args.command == "prepare":
|
|
438
|
+
repo_root = args.repo.resolve()
|
|
439
|
+
_scheduler, _tasks, plan = _compute_repo_plan(
|
|
440
|
+
repo_root=repo_root,
|
|
441
|
+
max_tracks=args.max_tracks,
|
|
442
|
+
kit=args.kit,
|
|
443
|
+
)
|
|
444
|
+
target_wave = next((w for w in plan.waves if w.wave_number == args.wave), None)
|
|
445
|
+
if target_wave is None:
|
|
446
|
+
print(f"Error: Wave {args.wave} not found in plan", file=sys.stderr)
|
|
447
|
+
return 1
|
|
448
|
+
|
|
449
|
+
# Pre-Flight Context Gate
|
|
450
|
+
_, adapter = get_adapter(repo_root, kit=plan.sdd_kit)
|
|
451
|
+
missing = adapter.check_preflight_context_gate(target_wave)
|
|
452
|
+
if missing:
|
|
453
|
+
print(
|
|
454
|
+
"PRE-FLIGHT CONTEXT GATE BLOCKED: Missing pre-compiled epic context(s):",
|
|
455
|
+
file=sys.stderr,
|
|
456
|
+
)
|
|
457
|
+
for path in missing:
|
|
458
|
+
print(f" - {path}", file=sys.stderr)
|
|
459
|
+
print(
|
|
460
|
+
"\nRecommendation: Compile the missing epic context file(s) on master "
|
|
461
|
+
"(e.g. using compile-epic-context.md) before spawning parallel tracks "
|
|
462
|
+
"to prevent fatal add/add git merge conflicts.",
|
|
463
|
+
file=sys.stderr,
|
|
464
|
+
)
|
|
465
|
+
return 1
|
|
466
|
+
|
|
467
|
+
layout = "antigravity" if args.antigravity_layout else "local"
|
|
468
|
+
wm = WorktreeManager(
|
|
469
|
+
repo_root,
|
|
470
|
+
layout=layout,
|
|
471
|
+
fixed_slots=not getattr(args, "dynamic_worktrees", False),
|
|
472
|
+
)
|
|
473
|
+
wt_paths = wm.prepare_wave(
|
|
474
|
+
plan=plan,
|
|
475
|
+
wave_number=args.wave,
|
|
476
|
+
headless=args.headless,
|
|
477
|
+
dry_run=args.dry_run,
|
|
478
|
+
)
|
|
479
|
+
print(
|
|
480
|
+
json.dumps(
|
|
481
|
+
{
|
|
482
|
+
"wave": args.wave,
|
|
483
|
+
"dry_run": args.dry_run,
|
|
484
|
+
"layout": layout,
|
|
485
|
+
"worktrees": [p.as_posix() for p in wt_paths],
|
|
486
|
+
},
|
|
487
|
+
indent=2,
|
|
488
|
+
)
|
|
489
|
+
)
|
|
490
|
+
return 0
|
|
491
|
+
|
|
492
|
+
if args.command == "claim-track":
|
|
493
|
+
repo_root = args.repo.resolve()
|
|
494
|
+
_scheduler, _tasks, plan = _compute_repo_plan(
|
|
495
|
+
repo_root=repo_root,
|
|
496
|
+
max_tracks=args.max_tracks,
|
|
497
|
+
kit=args.kit,
|
|
498
|
+
)
|
|
499
|
+
wm = WorktreeManager(repo_root)
|
|
500
|
+
lock = wm.claim_track(
|
|
501
|
+
plan=plan,
|
|
502
|
+
wave_number=args.wave,
|
|
503
|
+
track_id=args.track,
|
|
504
|
+
worktree_path=args.worktree.resolve(),
|
|
505
|
+
headless=args.headless,
|
|
506
|
+
)
|
|
507
|
+
print(json.dumps(lock.to_dict(), indent=2, ensure_ascii=False))
|
|
508
|
+
return 0
|
|
509
|
+
|
|
510
|
+
if args.command == "verify-wave":
|
|
511
|
+
repo_root = args.repo.resolve()
|
|
512
|
+
_scheduler, _tasks, plan = _compute_repo_plan(
|
|
513
|
+
repo_root=repo_root,
|
|
514
|
+
max_tracks=args.max_tracks,
|
|
515
|
+
kit=args.kit,
|
|
516
|
+
)
|
|
517
|
+
wm = WorktreeManager(
|
|
518
|
+
repo_root,
|
|
519
|
+
fixed_slots=not getattr(args, "dynamic_worktrees", False),
|
|
520
|
+
)
|
|
521
|
+
try:
|
|
522
|
+
violations = wm.verify_wave(plan=plan, wave_number=args.wave)
|
|
523
|
+
except (RuntimeError, ValueError, LIFORollbackViolationError) as exc:
|
|
524
|
+
err_payload = {
|
|
525
|
+
"status": "error",
|
|
526
|
+
"command": args.command,
|
|
527
|
+
"wave": args.wave,
|
|
528
|
+
"error": str(exc),
|
|
529
|
+
}
|
|
530
|
+
print(
|
|
531
|
+
json.dumps(err_payload, indent=2, ensure_ascii=False),
|
|
532
|
+
file=sys.stderr,
|
|
533
|
+
)
|
|
534
|
+
return 2
|
|
535
|
+
print(
|
|
536
|
+
json.dumps(
|
|
537
|
+
{
|
|
538
|
+
"wave": args.wave,
|
|
539
|
+
"compliant": len(violations) == 0,
|
|
540
|
+
"violations": violations,
|
|
541
|
+
},
|
|
542
|
+
indent=2,
|
|
543
|
+
ensure_ascii=False,
|
|
544
|
+
)
|
|
545
|
+
)
|
|
546
|
+
return 0 if not violations else 2
|
|
547
|
+
|
|
548
|
+
if args.command == "merge":
|
|
549
|
+
repo_root = args.repo.resolve()
|
|
550
|
+
_scheduler, _tasks, plan = _compute_repo_plan(
|
|
551
|
+
repo_root=repo_root,
|
|
552
|
+
max_tracks=args.max_tracks,
|
|
553
|
+
kit=args.kit,
|
|
554
|
+
)
|
|
555
|
+
wm = WorktreeManager(
|
|
556
|
+
repo_root,
|
|
557
|
+
fixed_slots=not getattr(args, "dynamic_worktrees", False),
|
|
558
|
+
)
|
|
559
|
+
try:
|
|
560
|
+
merged = wm.merge_wave(
|
|
561
|
+
plan=plan,
|
|
562
|
+
wave_number=args.wave,
|
|
563
|
+
run_quality_gates=not args.skip_gates,
|
|
564
|
+
cleanup_worktrees=args.cleanup,
|
|
565
|
+
dry_run=args.dry_run,
|
|
566
|
+
strict_gates=args.strict_gates,
|
|
567
|
+
)
|
|
568
|
+
except (RuntimeError, ValueError, LIFORollbackViolationError) as exc:
|
|
569
|
+
err_payload = {
|
|
570
|
+
"status": "error",
|
|
571
|
+
"command": args.command,
|
|
572
|
+
"wave": args.wave,
|
|
573
|
+
"error": str(exc),
|
|
574
|
+
}
|
|
575
|
+
print(
|
|
576
|
+
json.dumps(err_payload, indent=2, ensure_ascii=False),
|
|
577
|
+
file=sys.stderr,
|
|
578
|
+
)
|
|
579
|
+
return 2
|
|
580
|
+
print(
|
|
581
|
+
json.dumps(
|
|
582
|
+
{
|
|
583
|
+
"wave": args.wave,
|
|
584
|
+
"dry_run": args.dry_run,
|
|
585
|
+
"merged_branches": merged,
|
|
586
|
+
},
|
|
587
|
+
indent=2,
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
return 0
|
|
591
|
+
|
|
592
|
+
if args.command == "merge-status":
|
|
593
|
+
merge_sprint_status_files(
|
|
594
|
+
base_path=args.base,
|
|
595
|
+
ours_path=args.ours,
|
|
596
|
+
theirs_path=args.theirs,
|
|
597
|
+
output_path=args.out,
|
|
598
|
+
)
|
|
599
|
+
print(f"Merged sprint-status.yaml written to {args.out}")
|
|
600
|
+
return 0
|
|
601
|
+
|
|
602
|
+
if args.command == "check-lock":
|
|
603
|
+
decision = evaluate_tool_access(
|
|
604
|
+
tool_name=args.tool,
|
|
605
|
+
tool_input={"TargetFile": args.file},
|
|
606
|
+
)
|
|
607
|
+
print(json.dumps(decision.to_dict(), indent=2, ensure_ascii=False))
|
|
608
|
+
return 0 if decision.decision == "allow" else 2
|
|
609
|
+
|
|
610
|
+
if args.command == "install-plugin":
|
|
611
|
+
manifest_paths = install_plugin(target_dir=args.target_dir)
|
|
612
|
+
print(json.dumps(manifest_paths, indent=2, ensure_ascii=False))
|
|
613
|
+
return 0
|
|
614
|
+
|
|
615
|
+
if args.command == "autopilot":
|
|
616
|
+
repo_root = args.repo.resolve()
|
|
617
|
+
_scheduler, _tasks, plan = _compute_repo_plan(
|
|
618
|
+
repo_root=repo_root,
|
|
619
|
+
max_tracks=args.max_tracks,
|
|
620
|
+
kit=args.kit,
|
|
621
|
+
)
|
|
622
|
+
if not any(w.wave_number == args.wave for w in plan.waves):
|
|
623
|
+
available = [w.wave_number for w in plan.waves]
|
|
624
|
+
err_msg = f"Wave {args.wave} not found in plan for repository {repo_root.as_posix()}."
|
|
625
|
+
if not available:
|
|
626
|
+
err_msg += (
|
|
627
|
+
" No active SDD tasks found in repository. "
|
|
628
|
+
"Use --repo <path> targeting an SDD repository (e.g. --repo /path/to/project)."
|
|
629
|
+
)
|
|
630
|
+
print(
|
|
631
|
+
json.dumps(
|
|
632
|
+
{"error": err_msg, "available_waves": available},
|
|
633
|
+
indent=2,
|
|
634
|
+
ensure_ascii=False,
|
|
635
|
+
)
|
|
636
|
+
)
|
|
637
|
+
return 1
|
|
638
|
+
launcher = AutopilotLauncher(repo_root)
|
|
639
|
+
if getattr(args, "antigravity_layout", False):
|
|
640
|
+
launcher.worktrees_dir = default_antigravity_worktrees_dir(repo_root.name)
|
|
641
|
+
results = launcher.launch_wave(
|
|
642
|
+
plan=plan,
|
|
643
|
+
wave_number=args.wave,
|
|
644
|
+
harness=args.harness,
|
|
645
|
+
execute=args.execute,
|
|
646
|
+
)
|
|
647
|
+
print(json.dumps(results, indent=2, ensure_ascii=False))
|
|
648
|
+
return 0
|
|
649
|
+
|
|
650
|
+
if args.command == "abort-wave":
|
|
651
|
+
repo_root = args.repo.resolve()
|
|
652
|
+
rm = WorktreeRollbackManager(
|
|
653
|
+
repo_root,
|
|
654
|
+
fixed_slots=not getattr(args, "dynamic_worktrees", False),
|
|
655
|
+
)
|
|
656
|
+
deleted = rm.abort_wave(wave_id=args.wave)
|
|
657
|
+
print(
|
|
658
|
+
json.dumps(
|
|
659
|
+
{
|
|
660
|
+
"action": "abort-wave",
|
|
661
|
+
"wave": args.wave,
|
|
662
|
+
"deleted_branches": deleted,
|
|
663
|
+
"status": "aborted",
|
|
664
|
+
},
|
|
665
|
+
indent=2,
|
|
666
|
+
ensure_ascii=False,
|
|
667
|
+
)
|
|
668
|
+
)
|
|
669
|
+
return 0
|
|
670
|
+
|
|
671
|
+
if args.command == "rollback-wave":
|
|
672
|
+
repo_root = args.repo.resolve()
|
|
673
|
+
rm = WorktreeRollbackManager(
|
|
674
|
+
repo_root,
|
|
675
|
+
fixed_slots=not getattr(args, "dynamic_worktrees", False),
|
|
676
|
+
)
|
|
677
|
+
try:
|
|
678
|
+
backup_branch = rm.rollback_wave(
|
|
679
|
+
wave_id=args.wave,
|
|
680
|
+
save_backup=args.save_backup,
|
|
681
|
+
to_commit=args.to_commit,
|
|
682
|
+
kit=args.kit,
|
|
683
|
+
)
|
|
684
|
+
except (RuntimeError, ValueError, LIFORollbackViolationError) as exc:
|
|
685
|
+
err_payload = {
|
|
686
|
+
"status": "error",
|
|
687
|
+
"command": args.command,
|
|
688
|
+
"wave": args.wave,
|
|
689
|
+
"error": str(exc),
|
|
690
|
+
}
|
|
691
|
+
print(
|
|
692
|
+
json.dumps(err_payload, indent=2, ensure_ascii=False),
|
|
693
|
+
file=sys.stderr,
|
|
694
|
+
)
|
|
695
|
+
return 2
|
|
696
|
+
|
|
697
|
+
print(
|
|
698
|
+
json.dumps(
|
|
699
|
+
{
|
|
700
|
+
"action": "rollback-wave",
|
|
701
|
+
"wave": args.wave,
|
|
702
|
+
"backup_branch": backup_branch if args.save_backup else None,
|
|
703
|
+
"status": "rolled_back",
|
|
704
|
+
},
|
|
705
|
+
indent=2,
|
|
706
|
+
ensure_ascii=False,
|
|
707
|
+
)
|
|
708
|
+
)
|
|
709
|
+
return 0
|
|
710
|
+
|
|
711
|
+
if args.command == "run-guide":
|
|
712
|
+
repo_root = args.repo.resolve()
|
|
713
|
+
_scheduler, _tasks, plan = _compute_repo_plan(
|
|
714
|
+
repo_root=repo_root,
|
|
715
|
+
max_tracks=args.max_tracks,
|
|
716
|
+
kit=args.kit,
|
|
717
|
+
)
|
|
718
|
+
guide_md = render_run_guide(
|
|
719
|
+
repo_root=repo_root,
|
|
720
|
+
plan=plan,
|
|
721
|
+
wave_number=args.wave,
|
|
722
|
+
antigravity_layout=args.antigravity_layout,
|
|
723
|
+
)
|
|
724
|
+
print(guide_md)
|
|
725
|
+
return 0
|
|
726
|
+
|
|
727
|
+
if args.command == "demo":
|
|
728
|
+
run_demo(
|
|
729
|
+
keep_temp=args.keep_temp,
|
|
730
|
+
kit=args.kit,
|
|
731
|
+
scaffold_only=args.scaffold_only,
|
|
732
|
+
output_dir=args.output_dir,
|
|
733
|
+
)
|
|
734
|
+
return 0
|
|
735
|
+
|
|
736
|
+
return 1
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
if __name__ == "__main__":
|
|
740
|
+
raise SystemExit(main())
|