buildmind 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.
Files changed (43) hide show
  1. buildmind/__init__.py +3 -0
  2. buildmind/cli.py +686 -0
  3. buildmind/config/__init__.py +12 -0
  4. buildmind/config/settings.py +146 -0
  5. buildmind/core/__init__.py +7 -0
  6. buildmind/core/decision_classifier.py +136 -0
  7. buildmind/core/decision_engine.py +292 -0
  8. buildmind/core/executor.py +97 -0
  9. buildmind/core/explanation_engine.py +74 -0
  10. buildmind/core/export_engine.py +69 -0
  11. buildmind/core/file_writer.py +31 -0
  12. buildmind/core/task_decomposer.py +127 -0
  13. buildmind/llm/__init__.py +2 -0
  14. buildmind/llm/client.py +378 -0
  15. buildmind/models/__init__.py +10 -0
  16. buildmind/models/decision.py +87 -0
  17. buildmind/models/project.py +63 -0
  18. buildmind/models/task.py +90 -0
  19. buildmind/prompts/__init__.py +2 -0
  20. buildmind/prompts/classifier_system.txt +29 -0
  21. buildmind/prompts/classifier_user.txt +7 -0
  22. buildmind/prompts/decision_card_system.txt +30 -0
  23. buildmind/prompts/decision_card_user.txt +13 -0
  24. buildmind/prompts/decomposer_system.txt +20 -0
  25. buildmind/prompts/decomposer_user.txt +10 -0
  26. buildmind/prompts/executor_system.txt +15 -0
  27. buildmind/prompts/executor_user.txt +13 -0
  28. buildmind/prompts/explainer_system.txt +29 -0
  29. buildmind/prompts/explainer_user.txt +17 -0
  30. buildmind/prompts/loader.py +50 -0
  31. buildmind/server/mcp_server.py +452 -0
  32. buildmind/storage/__init__.py +18 -0
  33. buildmind/storage/audit_log.py +103 -0
  34. buildmind/storage/project_store.py +180 -0
  35. buildmind/ui/__init__.py +15 -0
  36. buildmind/ui/decision_ui.py +318 -0
  37. buildmind/ui/graph_ui.py +78 -0
  38. buildmind/ui/terminal.py +313 -0
  39. buildmind-0.1.0.dist-info/METADATA +182 -0
  40. buildmind-0.1.0.dist-info/RECORD +43 -0
  41. buildmind-0.1.0.dist-info/WHEEL +5 -0
  42. buildmind-0.1.0.dist-info/entry_points.txt +2 -0
  43. buildmind-0.1.0.dist-info/top_level.txt +1 -0
buildmind/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ # BuildMind — AI Thinking Infrastructure
2
+ __version__ = "0.1.0"
3
+ __author__ = "Mukul Prasad"
buildmind/cli.py ADDED
@@ -0,0 +1,686 @@
1
+ """
2
+ BuildMind CLI -- entry point.
3
+
4
+ Commands:
5
+ buildmind init Initialize BuildMind in current directory
6
+ buildmind start "..." Start a new project from intent
7
+ buildmind status Show current project status
8
+ buildmind continue Resume a paused project
9
+ buildmind graph Show ASCII task dependency graph
10
+ buildmind decisions Show all recorded decisions
11
+ buildmind export Export summary or spec
12
+ buildmind retry <tid> Re-run a specific task
13
+ buildmind override <tid> Re-decide + cascade re-run
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ from pathlib import Path
19
+ from typing import Optional, List, Set
20
+
21
+ from buildmind.models.task import Task, TaskStatus
22
+
23
+ # Auto-load .env before anything else
24
+ try:
25
+ from dotenv import load_dotenv
26
+ _env_path = Path.cwd() / ".env"
27
+ if not _env_path.exists():
28
+ for parent in Path.cwd().parents:
29
+ candidate = parent / ".env"
30
+ if candidate.exists():
31
+ _env_path = candidate
32
+ break
33
+ load_dotenv(_env_path, override=False)
34
+ except ImportError:
35
+ pass
36
+
37
+ import typer
38
+ from rich.panel import Panel
39
+
40
+ from buildmind import __version__
41
+ from buildmind.config.settings import (
42
+ is_initialized, load_config, write_default_config, get_buildmind_dir,
43
+ )
44
+ from buildmind.storage.project_store import (
45
+ load_project, load_tasks, load_decisions, load_spec, initialize_storage,
46
+ save_tasks,
47
+ )
48
+ from buildmind.storage.audit_log import log_project_created
49
+ from buildmind.ui.terminal import (
50
+ console, print_header, print_success, print_error, print_warning,
51
+ print_info, print_step, print_project_status, print_spec, print_task_table,
52
+ make_spinner,
53
+ )
54
+
55
+ # ── App setup ─────────────────────────────────────────────────────────────────
56
+
57
+ app = typer.Typer(
58
+ name="buildmind",
59
+ help="BuildMind -- AI Thinking Infrastructure for human-AI collaborative engineering.",
60
+ add_completion=False,
61
+ rich_markup_mode="rich",
62
+ )
63
+
64
+
65
+ def _require_init() -> bool:
66
+ if not is_initialized():
67
+ print_error("No BuildMind project found in this directory.")
68
+ print_info("Run [bold]buildmind init[/bold] to initialize one.")
69
+ raise typer.Exit(1)
70
+ return True
71
+
72
+
73
+ # ── Commands ──────────────────────────────────────────────────────────────────
74
+
75
+ @app.command()
76
+ def init(
77
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Project name"),
78
+ cwd: Optional[Path] = typer.Option(None, "--cwd", help="Project directory (default: current)"),
79
+ ) -> None:
80
+ """Initialize BuildMind in the current directory. No API keys required."""
81
+ project_dir = cwd or Path.cwd()
82
+
83
+ if is_initialized(project_dir):
84
+ print_warning("BuildMind is already initialized in this directory.")
85
+ print_info(f"Config: [bold]{get_buildmind_dir(project_dir) / 'config.yaml'}[/bold]")
86
+ raise typer.Exit(0)
87
+
88
+ print_header("Initializing BuildMind")
89
+
90
+ bm_dir = initialize_storage(project_dir)
91
+ print_step("Created", str(bm_dir))
92
+
93
+ project_name = name or project_dir.resolve().name
94
+ write_default_config(project_name, project_dir)
95
+ print_step("Config written", str(bm_dir / "config.yaml"))
96
+
97
+ console.print()
98
+ console.print(Panel(
99
+ f"[bold white]Project:[/bold white] {project_name}\n"
100
+ f"[bold white]Models:[/bold white] IDE models (Antigravity) -- no API keys needed\n"
101
+ f"[bold white]Storage:[/bold white] [cyan]{bm_dir}[/cyan]\n\n"
102
+ f"[muted]Run:[/muted] [bold cyan]buildmind start \"What do you want to build?\"[/bold cyan]",
103
+ title="[brand]BuildMind Ready[/brand]",
104
+ border_style="green",
105
+ padding=(1, 2),
106
+ ))
107
+
108
+
109
+ @app.command()
110
+ def start(
111
+ intent: str = typer.Argument(..., help="What you want to build -- in plain English"),
112
+ dry_run: bool = typer.Option(False, "--dry-run", help="Decompose + classify only, no execution"),
113
+ interactive: bool = typer.Option(False, "--interactive", "-i", help="Approve each step manually"),
114
+ mock: bool = typer.Option(False, "--mock", help="Use sample tasks -- no API key needed (for testing)"),
115
+ ) -> None:
116
+ """
117
+ Start a new BuildMind project from your intent.
118
+
119
+ Examples:
120
+ buildmind start "Build a REST API with authentication"
121
+ buildmind start "Build a Stripe payment integration"
122
+ buildmind start "Build a REST API" --mock (test without API key)
123
+ """
124
+ _require_init()
125
+ config = load_config()
126
+
127
+ print_header(f"Starting: {intent[:60]}...")
128
+
129
+ from buildmind.models.project import Project, ProjectMode
130
+ from buildmind.models.task import Task, TaskType, TaskSubType, TaskStatus, TaskComplexity
131
+ from buildmind.storage.project_store import save_project, save_tasks
132
+ from buildmind.storage.audit_log import log_tasks_decomposed, log_task_classified
133
+ from buildmind.core.task_decomposer import TaskDecomposer
134
+ from buildmind.core.decision_classifier import DecisionClassifier
135
+
136
+ # Create project
137
+ project_dir = Path.cwd()
138
+ project = Project.from_intent(intent, project_dir)
139
+ try:
140
+ project.mode = ProjectMode(config.mode)
141
+ except (ValueError, AttributeError):
142
+ pass
143
+ save_project(project)
144
+ log_project_created(project.id, intent)
145
+ print_step("Project created", project.id)
146
+
147
+ if mock:
148
+ # ── Mock path: no LLM, fixed sample tasks ─────────────────────────
149
+ print_warning("MOCK mode -- sample tasks, no model called")
150
+ tasks = [
151
+ Task(id="t1", project_id=project.id,
152
+ title="Choose tech stack and architecture pattern",
153
+ description="Decide language, framework, and architecture pattern.",
154
+ type=TaskType.HUMAN_REQUIRED, sub_type=TaskSubType.UNKNOWN,
155
+ status=TaskStatus.AWAITING_HUMAN, complexity=TaskComplexity.HIGH,
156
+ dependencies=[],
157
+ classification_reason="Architecture tradeoff requires human judgment"),
158
+ Task(id="t2", project_id=project.id,
159
+ title="Choose authentication strategy",
160
+ description="Decide between JWT, session-based, OAuth2, or API key auth.",
161
+ type=TaskType.HUMAN_REQUIRED, sub_type=TaskSubType.UNKNOWN,
162
+ status=TaskStatus.AWAITING_HUMAN, complexity=TaskComplexity.HIGH,
163
+ dependencies=["t1"],
164
+ classification_reason="Auth strategy has security and UX tradeoffs"),
165
+ Task(id="t3", project_id=project.id,
166
+ title="Choose database and ORM",
167
+ description="Select database and whether to use ORM or raw queries.",
168
+ type=TaskType.HUMAN_REQUIRED, sub_type=TaskSubType.UNKNOWN,
169
+ status=TaskStatus.AWAITING_HUMAN, complexity=TaskComplexity.MEDIUM,
170
+ dependencies=["t1"],
171
+ classification_reason="DB choice depends on scale and team preferences"),
172
+ Task(id="t4", project_id=project.id,
173
+ title="Implement data models and schema",
174
+ description="Create core data models: users, tasks, sessions.",
175
+ type=TaskType.AI_EXECUTABLE, sub_type=TaskSubType.CODE_PYTHON,
176
+ status=TaskStatus.PENDING, complexity=TaskComplexity.MEDIUM,
177
+ dependencies=["t1", "t3"],
178
+ classification_reason="Standard once stack is decided"),
179
+ Task(id="t5", project_id=project.id,
180
+ title="Implement authentication endpoints",
181
+ description="Build register, login, logout, token refresh endpoints.",
182
+ type=TaskType.AI_EXECUTABLE, sub_type=TaskSubType.CODE_PYTHON,
183
+ status=TaskStatus.PENDING, complexity=TaskComplexity.HIGH,
184
+ dependencies=["t2", "t4"],
185
+ classification_reason="Clear spec once auth strategy chosen"),
186
+ Task(id="t6", project_id=project.id,
187
+ title="Implement task CRUD API endpoints",
188
+ description="Build create, read, update, delete endpoints.",
189
+ type=TaskType.AI_EXECUTABLE, sub_type=TaskSubType.CODE_PYTHON,
190
+ status=TaskStatus.PENDING, complexity=TaskComplexity.MEDIUM,
191
+ dependencies=["t4", "t5"],
192
+ classification_reason="Standard CRUD with auth middleware"),
193
+ Task(id="t7", project_id=project.id,
194
+ title="Implement input validation and error handling",
195
+ description="Add request validation and meaningful HTTP error codes.",
196
+ type=TaskType.AI_EXECUTABLE, sub_type=TaskSubType.CODE_PYTHON,
197
+ status=TaskStatus.PENDING, complexity=TaskComplexity.MEDIUM,
198
+ dependencies=["t5", "t6"],
199
+ classification_reason="Standard validation patterns"),
200
+ Task(id="t8", project_id=project.id,
201
+ title="Write API documentation",
202
+ description="Generate OpenAPI/Swagger docs or README with usage examples.",
203
+ type=TaskType.AI_EXECUTABLE, sub_type=TaskSubType.DOCUMENTATION,
204
+ status=TaskStatus.PENDING, complexity=TaskComplexity.LOW,
205
+ dependencies=["t6", "t7"],
206
+ classification_reason="Documentation of existing endpoints"),
207
+ ]
208
+ save_tasks(tasks)
209
+ log_tasks_decomposed(project.id, len(tasks))
210
+ for t in tasks:
211
+ log_task_classified(project.id, t.id, t.type.value, t.classification_reason or "")
212
+ human_count = sum(1 for t in tasks if t.is_human)
213
+ ai_count = sum(1 for t in tasks if t.is_ai)
214
+ print_success(f"Created {len(tasks)} sample tasks -- [H] {human_count} human [A] {ai_count} AI")
215
+
216
+ else:
217
+ # ── Real path: LLM decompose + classify ───────────────────────────
218
+ from buildmind.ui.terminal import make_spinner
219
+
220
+ with make_spinner(f"Decomposing with {config.models.decomposer}...") as progress:
221
+ task_id = progress.add_task(f"Decomposing with {config.models.decomposer}...", total=None)
222
+ try:
223
+ decomposer = TaskDecomposer(config)
224
+ tasks = decomposer.decompose(project)
225
+ except EnvironmentError as e:
226
+ print_error(str(e))
227
+ raise typer.Exit(1)
228
+ except Exception as e:
229
+ print_error(f"Decomposition failed: {e}")
230
+ raise typer.Exit(1)
231
+ progress.update(task_id, completed=100)
232
+
233
+ print_success(f"Decomposed into {len(tasks)} tasks")
234
+
235
+ with make_spinner(f"Classifying with {config.models.classifier}...") as progress:
236
+ task_id = progress.add_task(f"Classifying with {config.models.classifier}...", total=None)
237
+ try:
238
+ classifier = DecisionClassifier(config)
239
+ tasks = classifier.classify(project, tasks)
240
+ except Exception as e:
241
+ print_error(f"Classification failed: {e}")
242
+ raise typer.Exit(1)
243
+ progress.update(task_id, completed=100)
244
+
245
+ human_count = sum(1 for t in tasks if t.is_human)
246
+ ai_count = sum(1 for t in tasks if t.is_ai)
247
+ print_success(f"Classified -- [H] {human_count} human decisions [A] {ai_count} AI tasks")
248
+
249
+ # ── Show results ──────────────────────────────────────────────────────────
250
+ console.print()
251
+ print_task_table(tasks, title=f"Task Plan -- {project.id}")
252
+
253
+ if dry_run:
254
+ print_info("Dry run complete. No code written.")
255
+ return
256
+
257
+ print_info("Run [bold]buildmind continue[/bold] to work through decisions and execute tasks.")
258
+
259
+
260
+ @app.command()
261
+ def status() -> None:
262
+ """Show the current project status -- tasks, decisions, and build progress."""
263
+ _require_init()
264
+
265
+ project = load_project()
266
+ if not project:
267
+ print_warning("No project started yet.")
268
+ print_info("Run [bold]buildmind start \"...\"[/bold] to begin.")
269
+ raise typer.Exit(0)
270
+
271
+ tasks = load_tasks()
272
+ decisions = load_decisions()
273
+ print_project_status(project, tasks, decisions)
274
+
275
+
276
+ @app.command(name="continue")
277
+ def continue_cmd(
278
+ mock: bool = typer.Option(False, "--mock", help="Use mock decision cards (no API key needed)"),
279
+ ) -> None:
280
+ """Alias for resume."""
281
+ resume(mock=mock)
282
+
283
+
284
+ @app.command()
285
+ def resume(
286
+ mock: bool = typer.Option(False, "--mock", help="Use mock decision cards (no API key needed)"),
287
+ ) -> None:
288
+ """
289
+ Resume a project -- work through decision gates interactively.
290
+
291
+ For each HUMAN_REQUIRED task (in dependency order):
292
+ - Shows a decision card with options, AI suggestion, and impact areas
293
+ - Accepts: number, explain <n>, compare <a> <b>, why, custom, spec, skip
294
+ """
295
+ _require_init()
296
+
297
+ project = load_project()
298
+ if not project:
299
+ print_warning("No project started yet.")
300
+ print_info("Run [bold]buildmind start \"...\"[/bold] first.")
301
+ raise typer.Exit(0)
302
+
303
+ tasks = load_tasks()
304
+ if not tasks:
305
+ print_warning("No tasks found. Run [bold]buildmind start \"...\"[/bold] first.")
306
+ raise typer.Exit(0)
307
+
308
+ from buildmind.core.decision_engine import DecisionEngine
309
+ from buildmind.ui.decision_ui import run_decision_card
310
+
311
+ engine = DecisionEngine(config=load_config())
312
+ pending = engine.get_pending_tasks(tasks)
313
+
314
+ if not pending:
315
+ # Check if all human tasks are already done
316
+ human_pending = [t for t in tasks if t.is_human and not t.is_done]
317
+ if not human_pending:
318
+ print_success("All decision gates resolved.")
319
+ print_info("Run [bold]buildmind status[/bold] to see the full task plan.")
320
+ else:
321
+ print_info("Waiting on dependent decisions first.")
322
+ print_task_table(human_pending, title="Blocked Decisions")
323
+ raise typer.Exit(0)
324
+
325
+ total = len(pending)
326
+ print_header(f"Decision Mode -- {total} decision(s) to make")
327
+ console.print(
328
+ f" [muted]Project:[/muted] [white]{project.title[:70]}[/white]\n"
329
+ f" [muted]Working through:[/muted] [bold cyan]{total} HUMAN_REQUIRED task(s)[/bold cyan]\n"
330
+ )
331
+
332
+ for i, task in enumerate(pending, start=1):
333
+ # Generate decision card
334
+ print_info(f"Generating decision card for: [bold]{task.title}[/bold]...")
335
+ try:
336
+ card = engine.generate_card(project, task, use_mock=mock)
337
+ except Exception as e:
338
+ print_error(f"Could not generate card for {task.id}: {e}")
339
+ print_info("Skipping. Run again or use --mock to test without API key.")
340
+ continue
341
+
342
+ gate = engine.create_gate(project, task, card)
343
+
344
+ # Run interactive decision loop
345
+ run_decision_card(
346
+ project_id=project.id,
347
+ task=task,
348
+ gate=gate,
349
+ card=card,
350
+ decision_num=i,
351
+ total_decisions=total,
352
+ )
353
+
354
+ # Reload tasks to reflect status updates
355
+ tasks = load_tasks()
356
+ # Refresh pending list
357
+ pending_remaining = engine.get_pending_tasks(tasks)
358
+ if not pending_remaining and i < total:
359
+ print_info("Remaining decisions are blocked by dependencies. Run again after executing AI tasks.")
360
+ break
361
+
362
+ # Final summary
363
+ console.print()
364
+ all_tasks = load_tasks()
365
+ all_decisions = load_decisions()
366
+ print_project_status(project, all_tasks, all_decisions)
367
+
368
+ ai_ready = [t for t in all_tasks if t.is_ai and t.status.value == "PENDING"]
369
+ if ai_ready:
370
+ print_info(f"[bold]{len(ai_ready)} AI tasks[/bold] ready to execute.")
371
+
372
+
373
+ @app.command()
374
+ def execute(
375
+ mock: bool = typer.Option(False, "--mock", help="Use mock code generation (no API key needed)"),
376
+ ) -> None:
377
+ """Execute AI tasks -- generate and write code for PENDING tasks."""
378
+ _require_init()
379
+
380
+ project = load_project()
381
+ if not project:
382
+ print_warning("No project started yet.")
383
+ raise typer.Exit(0)
384
+
385
+ tasks = load_tasks()
386
+ if not tasks:
387
+ print_warning("No tasks found.")
388
+ raise typer.Exit(0)
389
+
390
+ from buildmind.core.executor import Executor
391
+ from buildmind.core.explanation_engine import ExplanationEngine
392
+ from buildmind.core.file_writer import write_files
393
+ from buildmind.storage.project_store import update_task, save_tasks, load_spec
394
+ from buildmind.models.task import TaskStatus
395
+ from buildmind.ui.terminal import print_explanation_card
396
+
397
+ config = load_config()
398
+ executor = Executor(config)
399
+ explainer = ExplanationEngine(config)
400
+
401
+ executed_any = False
402
+ skip_explanations = False
403
+
404
+ while True:
405
+ ready_tasks = executor.get_ready_tasks(load_tasks())
406
+
407
+ if not ready_tasks:
408
+ break
409
+
410
+ total = len(ready_tasks)
411
+ print_header(f"Execution Mode -- {total} task(s) ready")
412
+ spec = load_spec()
413
+
414
+ for i, task in enumerate(ready_tasks, start=1):
415
+ print_step(f"Executing [{i}/{total}]: {task.title}...")
416
+ try:
417
+ from buildmind.ui.terminal import make_spinner
418
+ with make_spinner("Writing code...") as progress:
419
+ pid = progress.add_task(f"Generating for {task.id}", total=None)
420
+ file_actions = executor.execute_task(project, task, use_mock=mock)
421
+ write_files(Path.cwd(), file_actions)
422
+ progress.update(pid, completed=100)
423
+
424
+ task.status = TaskStatus.COMPLETED
425
+ update_task(task)
426
+ executed_any = True
427
+ print_success(f"Task {task.id} complete.")
428
+
429
+ # Explanation Engine
430
+ if not skip_explanations:
431
+ with make_spinner("Explaining component...") as progress:
432
+ pid = progress.add_task(f"Analyzing {task.id}...", total=None)
433
+ explanation_json = explainer.generate_component_explanation(
434
+ project, task, file_actions, spec, use_mock=mock
435
+ )
436
+ progress.update(pid, completed=100)
437
+
438
+ user_input = print_explanation_card(explanation_json)
439
+ if user_input and user_input.startswith("s"):
440
+ skip_explanations = True
441
+
442
+ except Exception as e:
443
+ print_error(f"Failed to execute {task.id}: {e}")
444
+ print_info("Skipping to next task...")
445
+ continue
446
+
447
+ if not executed_any:
448
+ tasks = load_tasks()
449
+ pending_ai = [t for t in tasks if t.is_ai and not t.is_done]
450
+ if pending_ai:
451
+ print_warning("AI tasks exist but are blocked by dependencies (e.g. AWAITING_HUMAN gates).")
452
+ print_info("Run [bold]buildmind resume[/bold] to clear decisions first.")
453
+ else:
454
+ print_success("All AI tasks are complete.")
455
+ raise typer.Exit(0)
456
+
457
+ console.print()
458
+ all_tasks = load_tasks()
459
+ print_project_status(project, all_tasks, load_decisions())
460
+
461
+
462
+
463
+ @app.command()
464
+ def graph() -> None:
465
+ """Show the task dependency graph as ASCII in your terminal."""
466
+ _require_init()
467
+
468
+ tasks = load_tasks()
469
+ if not tasks:
470
+ print_warning("No tasks found. Start a project first.")
471
+ raise typer.Exit(0)
472
+
473
+ project = load_project()
474
+ print_header(f"Task Graph -- {project.title[:50]}...")
475
+
476
+ from buildmind.ui.graph_ui import print_task_graph
477
+ print_task_graph(tasks)
478
+
479
+
480
+ @app.command()
481
+ def decisions() -> None:
482
+ """Show all decisions you have made in this project."""
483
+ _require_init()
484
+
485
+ all_decisions = load_decisions()
486
+ spec = load_spec()
487
+
488
+ if not all_decisions:
489
+ print_info("No decisions recorded yet.")
490
+ return
491
+
492
+ print_header("Decisions Made")
493
+ print_spec(spec)
494
+
495
+ console.print(f"\n [muted]Total:[/muted] [white]{len(all_decisions)} decision(s)[/white]")
496
+ for d in all_decisions:
497
+ accepted = " [suggestion](AI suggestion)[/suggestion]" if d.accepted_ai_suggestion else ""
498
+ console.print(
499
+ f" [bold white]{d.task_id}[/bold white] "
500
+ f"--> [cyan]{d.chosen_value}[/cyan]{accepted}"
501
+ )
502
+
503
+
504
+ def _get_descendants(tasks: List[Task], target_id: str) -> Set[str]:
505
+ """Find all task IDs that depend on target_id recursively."""
506
+ descendants = set()
507
+ queue = [target_id]
508
+ while queue:
509
+ curr = queue.pop(0)
510
+ for t in tasks:
511
+ if curr in t.dependencies and t.id not in descendants:
512
+ descendants.add(t.id)
513
+ queue.append(t.id)
514
+ return descendants
515
+
516
+
517
+ @app.command()
518
+ def retry(
519
+ task_id: str = typer.Argument(..., help="Task ID to retry, e.g. t3"),
520
+ ) -> None:
521
+ """Re-run a specific AI task and cascade reset its dependents."""
522
+ _require_init()
523
+
524
+ tasks = load_tasks()
525
+ target = next((t for t in tasks if t.id == task_id), None)
526
+
527
+ if not target:
528
+ print_error(f"Task '{task_id}' not found.")
529
+ raise typer.Exit(1)
530
+
531
+ if target.is_human:
532
+ print_error(f"Task {task_id} is a human decision task. Use 'buildmind override {task_id}' instead.")
533
+ raise typer.Exit(1)
534
+
535
+ descendants = _get_descendants(tasks, task_id)
536
+
537
+ target.status = TaskStatus.PENDING
538
+ reset_count = 1
539
+
540
+ for t in tasks:
541
+ if t.id in descendants and t.is_ai:
542
+ t.status = TaskStatus.PENDING
543
+ reset_count += 1
544
+
545
+ save_tasks(tasks)
546
+ print_success(f"Reset {reset_count} AI task(s) to PENDING (including descendants).")
547
+ print_info("Run [bold]buildmind execute[/bold] to process them.")
548
+
549
+
550
+ @app.command()
551
+ def override(
552
+ task_id: str = typer.Argument(..., help="Task ID to re-decide, e.g. t1"),
553
+ ) -> None:
554
+ """Re-decide a HUMAN task and cascade reset all affected tasks."""
555
+ _require_init()
556
+
557
+ tasks = load_tasks()
558
+ target = next((t for t in tasks if t.id == task_id), None)
559
+
560
+ if not target:
561
+ print_error(f"Task '{task_id}' not found.")
562
+ raise typer.Exit(1)
563
+
564
+ if target.is_ai:
565
+ print_error(f"Task {task_id} is an AI task. Use 'buildmind retry {task_id}' instead.")
566
+ raise typer.Exit(1)
567
+
568
+ descendants = _get_descendants(tasks, task_id)
569
+
570
+ # 1. Reset Tasks
571
+ target.status = TaskStatus.AWAITING_HUMAN
572
+ reset_human = 1
573
+ reset_ai = 0
574
+
575
+ for t in tasks:
576
+ if t.id in descendants:
577
+ if t.is_human:
578
+ t.status = TaskStatus.AWAITING_HUMAN
579
+ reset_human += 1
580
+ else:
581
+ t.status = TaskStatus.PENDING
582
+ reset_ai += 1
583
+ save_tasks(tasks)
584
+
585
+ # 2. Clear Decisions & Spec
586
+ decisions = load_decisions()
587
+ to_delete_ids = descendants.copy()
588
+ to_delete_ids.add(task_id)
589
+
590
+ new_decisions = [d for d in decisions if d.task_id not in to_delete_ids]
591
+
592
+ from buildmind.storage.project_store import save_decisions, save_spec
593
+ save_decisions(new_decisions)
594
+
595
+ # Rebuild spec from remaining decisions
596
+ new_spec = {}
597
+ task_map = {t.id: t for t in load_tasks()}
598
+ for d in new_decisions:
599
+ t = task_map.get(d.task_id)
600
+ if t:
601
+ # Need to match the same logic as _record_decision / _skip_decision
602
+ if d.is_skipped:
603
+ new_spec[d.task_id] = d.chosen_value
604
+ else:
605
+ skey = t.title.lower().replace(" ", "_").replace("/", "_")[:40]
606
+ new_spec[skey] = d.chosen_value
607
+
608
+ save_spec(new_spec)
609
+
610
+ print_success(f"Overridden {task_id}. Cascade reset {reset_human} Human task(s) and {reset_ai} AI task(s).")
611
+ print_info("Run [bold]buildmind resume[/bold] to re-make decisions, then [bold]buildmind execute[/bold].")
612
+
613
+
614
+ @app.command()
615
+ def serve(
616
+ mcp: bool = typer.Option(False, "--mcp", help="Run as an MCP standard server for IDEs"),
617
+ ) -> None:
618
+ """Start the BuildMind server (for IDE MCP Integration)."""
619
+ if not mcp:
620
+ print_warning("Server mode currently requires the --mcp flag.")
621
+ print_info("Use: buildmind serve --mcp")
622
+ raise typer.Exit(1)
623
+
624
+ try:
625
+ from buildmind.server.mcp_server import start_mcp_server
626
+ start_mcp_server()
627
+ except ImportError:
628
+ print_error("Failed to load the MCP server modules. Did you install with 'pip install buildmind[mcp]'?")
629
+ raise typer.Exit(1)
630
+
631
+
632
+ @app.command()
633
+ def export(
634
+ what: str = typer.Argument("summary", help="What to export: 'summary' or 'spec'"),
635
+ ) -> None:
636
+ """Export project summary or spec."""
637
+ _require_init()
638
+ if what == "spec":
639
+ spec = load_spec()
640
+ if not spec:
641
+ print_info("No spec recorded yet.")
642
+ else:
643
+ print_spec(spec)
644
+ elif what == "summary":
645
+ project = load_project()
646
+ if not project:
647
+ print_warning("No project started yet.")
648
+ raise typer.Exit(0)
649
+
650
+ tasks = load_tasks()
651
+ decisions = load_decisions()
652
+
653
+ from buildmind.core.export_engine import ExportEngine
654
+ engine = ExportEngine(load_config())
655
+
656
+ output_path = Path("buildmind_report.md")
657
+ engine.export_summary(project, tasks, decisions, output_path)
658
+
659
+ print_success(f"Project summary exported successfully to [bold]{output_path}[/bold]")
660
+ else:
661
+ print_error(f"Unknown export option: '{what}'. Use 'summary' or 'spec'.")
662
+
663
+
664
+ # ── Version / root callback ───────────────────────────────────────────────────
665
+
666
+ @app.callback(invoke_without_command=True)
667
+ def main(
668
+ ctx: typer.Context,
669
+ version: bool = typer.Option(False, "--version", "-v", help="Show version and exit"),
670
+ ) -> None:
671
+ """
672
+ BuildMind -- AI Thinking Infrastructure
673
+
674
+ Orchestrate human decisions and AI execution in your IDE terminal.
675
+ Uses your IDE's AI models (no API keys required).
676
+ """
677
+ if version:
678
+ console.print(f"[brand]BuildMind[/brand] [white]v{__version__}[/white]")
679
+ raise typer.Exit()
680
+ if ctx.invoked_subcommand is None:
681
+ print_header()
682
+ console.print(ctx.get_help())
683
+
684
+
685
+ if __name__ == "__main__":
686
+ app()