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/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())