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.
- buildmind/__init__.py +3 -0
- buildmind/cli.py +686 -0
- buildmind/config/__init__.py +12 -0
- buildmind/config/settings.py +146 -0
- buildmind/core/__init__.py +7 -0
- buildmind/core/decision_classifier.py +136 -0
- buildmind/core/decision_engine.py +292 -0
- buildmind/core/executor.py +97 -0
- buildmind/core/explanation_engine.py +74 -0
- buildmind/core/export_engine.py +69 -0
- buildmind/core/file_writer.py +31 -0
- buildmind/core/task_decomposer.py +127 -0
- buildmind/llm/__init__.py +2 -0
- buildmind/llm/client.py +378 -0
- buildmind/models/__init__.py +10 -0
- buildmind/models/decision.py +87 -0
- buildmind/models/project.py +63 -0
- buildmind/models/task.py +90 -0
- buildmind/prompts/__init__.py +2 -0
- buildmind/prompts/classifier_system.txt +29 -0
- buildmind/prompts/classifier_user.txt +7 -0
- buildmind/prompts/decision_card_system.txt +30 -0
- buildmind/prompts/decision_card_user.txt +13 -0
- buildmind/prompts/decomposer_system.txt +20 -0
- buildmind/prompts/decomposer_user.txt +10 -0
- buildmind/prompts/executor_system.txt +15 -0
- buildmind/prompts/executor_user.txt +13 -0
- buildmind/prompts/explainer_system.txt +29 -0
- buildmind/prompts/explainer_user.txt +17 -0
- buildmind/prompts/loader.py +50 -0
- buildmind/server/mcp_server.py +452 -0
- buildmind/storage/__init__.py +18 -0
- buildmind/storage/audit_log.py +103 -0
- buildmind/storage/project_store.py +180 -0
- buildmind/ui/__init__.py +15 -0
- buildmind/ui/decision_ui.py +318 -0
- buildmind/ui/graph_ui.py +78 -0
- buildmind/ui/terminal.py +313 -0
- buildmind-0.1.0.dist-info/METADATA +182 -0
- buildmind-0.1.0.dist-info/RECORD +43 -0
- buildmind-0.1.0.dist-info/WHEEL +5 -0
- buildmind-0.1.0.dist-info/entry_points.txt +2 -0
- buildmind-0.1.0.dist-info/top_level.txt +1 -0
buildmind/__init__.py
ADDED
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()
|