borisxdave 0.2.0__py3-none-any.whl → 0.3.1__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.
prompts.py CHANGED
@@ -29,17 +29,32 @@ _boris_prompt_cache = None
29
29
 
30
30
 
31
31
  def _load_boris_prompt() -> str:
32
- """Load Boris's management prompt from boris_prompt.md. Cached after first load."""
32
+ """Load Boris's management prompt from boris_prompt.md. Cached after first load.
33
+
34
+ Searches multiple locations to handle both editable installs (source dir)
35
+ and regular pip installs (site-packages).
36
+ """
33
37
  global _boris_prompt_cache
34
38
  if _boris_prompt_cache is not None:
35
39
  return _boris_prompt_cache
36
- try:
37
- with open(_BORIS_PROMPT_PATH, "r", encoding="utf-8") as f:
38
- _boris_prompt_cache = f.read().strip()
39
- logger.debug("Loaded Boris prompt: %d chars", len(_boris_prompt_cache))
40
- except FileNotFoundError:
41
- logger.warning("boris_prompt.md not found at %s", _BORIS_PROMPT_PATH)
42
- _boris_prompt_cache = ""
40
+
41
+ search_paths = [
42
+ _BORIS_PROMPT_PATH, # Next to this .py file (editable install / source)
43
+ os.path.join(sys.prefix, "boris_prompt.md"), # sys.prefix (some data_files installs)
44
+ os.path.join(os.path.dirname(shutil.which("boris") or ""), "boris_prompt.md"), # Next to boris script
45
+ ]
46
+
47
+ for path in search_paths:
48
+ try:
49
+ with open(path, "r", encoding="utf-8") as f:
50
+ _boris_prompt_cache = f.read().strip()
51
+ logger.debug("Loaded Boris prompt from %s (%d chars)", path, len(_boris_prompt_cache))
52
+ return _boris_prompt_cache
53
+ except (FileNotFoundError, OSError, TypeError):
54
+ continue
55
+
56
+ logger.warning("boris_prompt.md not found in any search path: %s", search_paths)
57
+ _boris_prompt_cache = ""
43
58
  return _boris_prompt_cache
44
59
 
45
60
  # Regex to strip ANSI escape codes
@@ -87,14 +102,45 @@ def _list_project_files(project_dir: str, max_files: int = 500) -> str:
87
102
  # --- Planning (from planner.py) ---
88
103
 
89
104
 
90
- def create_plan(task: str, project_dir: str) -> Plan:
91
- """Break a task into milestones using Claude CLI."""
105
+ def create_plan(task: str, project_dir: str, turbo: bool = False) -> Plan:
106
+ """Break a task into milestones using Claude CLI.
107
+
108
+ Args:
109
+ task: The task description.
110
+ project_dir: Path to the project directory.
111
+ turbo: When True, emphasize minimal dependencies and maximum parallelism.
112
+ """
92
113
  file_listing = _list_project_files(project_dir)
93
114
 
115
+ if turbo:
116
+ dependency_instructions = (
117
+ "CRITICAL DEPENDENCY RULES (turbo/parallel mode):\n"
118
+ "- Only add a dependency if the milestone TRULY requires output from another milestone to function.\n"
119
+ "- Two milestones that share a common dependency should NOT depend on each other unless one genuinely needs the other's output.\n"
120
+ "- Maximize parallelism in the dependency graph. Independent features, pages, or modules that share the same foundation should run in parallel.\n"
121
+ "- Do NOT chain milestones linearly unless there is a real data/code dependency between them.\n\n"
122
+ "Example of WRONG dependencies (overly sequential):\n"
123
+ ' M1: Project Setup (depends_on: [])\n'
124
+ ' M2: Auth Backend (depends_on: ["M1"])\n'
125
+ ' M3: Login Page (depends_on: ["M2"])\n'
126
+ ' M4: Dashboard Page (depends_on: ["M3"]) <-- WRONG: only needs M2, not M3\n'
127
+ ' M5: Profile Page (depends_on: ["M4"]) <-- WRONG: only needs M2, not M4\n\n'
128
+ "Example of CORRECT dependencies (maximizes parallelism):\n"
129
+ ' M1: Project Setup (depends_on: [])\n'
130
+ ' M2: Auth Backend (depends_on: ["M1"])\n'
131
+ ' M3: Login Page (depends_on: ["M2"])\n'
132
+ ' M4: Dashboard Page (depends_on: ["M2"]) <-- parallel with M3\n'
133
+ ' M5: Profile Page (depends_on: ["M2"]) <-- parallel with M3, M4\n\n'
134
+ )
135
+ else:
136
+ dependency_instructions = (
137
+ "Order by dependency: foundation first, then features that depend on it.\n"
138
+ )
139
+
94
140
  prompt = (
95
141
  "You are a technical project planner. Break this task into ordered milestones.\n"
96
142
  "Each milestone must be a concrete, buildable unit that DaveLoop can execute independently.\n"
97
- "Order by dependency: foundation first, then features that depend on it.\n\n"
143
+ f"{dependency_instructions}\n"
98
144
  f"Task: {task}\n"
99
145
  f"Project directory: {project_dir}\n"
100
146
  f"Existing files:\n{file_listing}\n\n"
@@ -279,14 +325,116 @@ def _save_plan_markdown(plan: Plan, task: str) -> str:
279
325
  return filepath
280
326
 
281
327
 
328
+ def load_plan_from_file(filepath: str) -> Plan:
329
+ """Parse a saved plan markdown file back into a Plan object.
330
+
331
+ Reads the markdown format produced by _save_plan_markdown and reconstructs
332
+ the Plan with its Milestone objects.
333
+ """
334
+ if not os.path.exists(filepath):
335
+ raise FileNotFoundError(f"Plan file not found: {filepath}")
336
+
337
+ with open(filepath, "r", encoding="utf-8") as f:
338
+ content = f.read()
339
+
340
+ # Extract task from **Task:** line
341
+ task_match = re.search(r"\*\*Task:\*\*\s*(.+)", content)
342
+ task = task_match.group(1).strip() if task_match else "Unknown task"
343
+
344
+ # Extract created_at from **Created:** line
345
+ created_match = re.search(r"\*\*Created:\*\*\s*(.+)", content)
346
+ created_at = created_match.group(1).strip() if created_match else datetime.now().isoformat()
347
+
348
+ # Split into milestone sections by ## M<N>: headers
349
+ milestone_sections = re.split(r"(?=^## M\d+:)", content, flags=re.MULTILINE)
350
+
351
+ milestones = []
352
+ for section in milestone_sections:
353
+ # Only process sections that start with a milestone header
354
+ header_match = re.match(r"^## (M\d+):\s*(.+)", section)
355
+ if not header_match:
356
+ continue
357
+
358
+ m_id = header_match.group(1)
359
+ m_title = header_match.group(2).strip()
360
+
361
+ # Extract description: text between the header and the first **bold** field or ---
362
+ # Skip the header line, then collect lines until we hit a **field:** or ---
363
+ lines = section.split("\n")
364
+ desc_lines = []
365
+ in_desc = False
366
+ for line in lines[1:]: # skip header line
367
+ stripped = line.strip()
368
+ if not stripped:
369
+ if in_desc:
370
+ desc_lines.append("")
371
+ continue
372
+ if stripped.startswith("**") or stripped == "---":
373
+ break
374
+ desc_lines.append(stripped)
375
+ in_desc = True
376
+ description = "\n".join(desc_lines).strip()
377
+
378
+ # Extract depends_on
379
+ depends_match = re.search(r"\*\*Depends on:\*\*\s*(.+)", section)
380
+ depends_on = []
381
+ if depends_match:
382
+ deps_str = depends_match.group(1).strip()
383
+ depends_on = [d.strip() for d in deps_str.split(",") if d.strip()]
384
+
385
+ # Extract acceptance criteria (bulleted list after **Acceptance Criteria:**)
386
+ criteria = []
387
+ criteria_match = re.search(r"\*\*Acceptance Criteria:\*\*\s*\n((?:\s*- .+\n?)+)", section)
388
+ if criteria_match:
389
+ for line in criteria_match.group(1).strip().split("\n"):
390
+ line = line.strip()
391
+ if line.startswith("- "):
392
+ criteria.append(line[2:].strip())
393
+
394
+ # Extract files to create
395
+ files_create_match = re.search(r"\*\*Files to create:\*\*\s*(.+)", section)
396
+ files_to_create = []
397
+ if files_create_match:
398
+ files_to_create = [f.strip() for f in files_create_match.group(1).split(",") if f.strip()]
399
+
400
+ # Extract files to modify
401
+ files_modify_match = re.search(r"\*\*Files to modify:\*\*\s*(.+)", section)
402
+ files_to_modify = []
403
+ if files_modify_match:
404
+ files_to_modify = [f.strip() for f in files_modify_match.group(1).split(",") if f.strip()]
405
+
406
+ milestones.append(Milestone(
407
+ id=m_id,
408
+ title=m_title,
409
+ description=description,
410
+ depends_on=depends_on,
411
+ acceptance_criteria=criteria,
412
+ files_to_create=files_to_create,
413
+ files_to_modify=files_to_modify,
414
+ ))
415
+
416
+ if not milestones:
417
+ raise RuntimeError(f"No milestones found in plan file: {filepath}")
418
+
419
+ return Plan(task=task, milestones=milestones, created_at=created_at)
420
+
421
+
282
422
  # --- Prompt Crafting (from crafter.py) ---
283
423
 
284
424
 
285
- def craft_prompt(milestone: Milestone, plan: Plan, project_dir: str) -> str:
425
+ def craft_prompt(milestone: Milestone, plan: Plan, project_dir: str,
426
+ turbo: bool = False, sibling_milestones: list[Milestone] | None = None) -> str:
286
427
  """Build a detailed prompt for DaveLoop to execute a milestone.
287
428
 
288
429
  This is the most critical function in Boris. DaveLoop needs everything
289
430
  in ONE prompt - context, spec, integration points, acceptance criteria.
431
+
432
+ Args:
433
+ milestone: The milestone to build a prompt for.
434
+ plan: The full plan for context.
435
+ project_dir: Path to the project directory.
436
+ turbo: When True, adds parallel execution warnings.
437
+ sibling_milestones: Other milestones running concurrently in the same turbo batch.
290
438
  """
291
439
  sections = []
292
440
 
@@ -305,6 +453,37 @@ def craft_prompt(milestone: Milestone, plan: Plan, project_dir: str) -> str:
305
453
  sections.append("**Do NOT create or modify any other files.**")
306
454
  sections.append("")
307
455
 
456
+ # Turbo mode: parallel execution warning
457
+ if turbo and sibling_milestones:
458
+ sections.append("## PARALLEL EXECUTION WARNING")
459
+ sections.append("")
460
+ sections.append("**You are running in PARALLEL with other DaveLoops.** Other milestones")
461
+ sections.append("are being built AT THE SAME TIME by sibling DaveLoop processes.")
462
+ sections.append("Modifying files outside your scope WILL cause conflicts or silent overwrites.")
463
+ sections.append("")
464
+ sections.append("**Sibling milestones running concurrently:**")
465
+ for sib in sibling_milestones:
466
+ sib_files = (sib.files_to_create or []) + (sib.files_to_modify or [])
467
+ sib_files_str = ", ".join(sib_files) if sib_files else "(no specific files)"
468
+ sections.append(f"- **{sib.id}: {sib.title}** - Files: {sib_files_str}")
469
+ sections.append("")
470
+ # Collect all sibling files into a flat list for the explicit prohibition
471
+ all_sibling_files = []
472
+ for sib in sibling_milestones:
473
+ all_sibling_files.extend(sib.files_to_create or [])
474
+ all_sibling_files.extend(sib.files_to_modify or [])
475
+ if all_sibling_files:
476
+ unique_sibling_files = sorted(set(all_sibling_files))
477
+ sections.append("**DO NOT touch these files (owned by sibling milestones):**")
478
+ for sf in unique_sibling_files:
479
+ sections.append(f"- {sf}")
480
+ sections.append("")
481
+ sections.append("**Rules:**")
482
+ sections.append("1. ONLY modify files listed in YOUR files_to_create/files_to_modify scope above.")
483
+ sections.append("2. Do NOT modify shared config files (package.json, routing tables, etc.) unless they are explicitly in YOUR scope.")
484
+ sections.append("3. If you need a shared file changed, add a TODO comment instead of editing it.")
485
+ sections.append("")
486
+
308
487
  # What to build
309
488
  sections.append(f"## What to Build")
310
489
  sections.append(milestone.description)
state.py CHANGED
@@ -63,16 +63,96 @@ class State:
63
63
  updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
64
64
  phase: str = "structural" # structural | ui_testing
65
65
  ui_plan: Optional[UIPlan] = None
66
+ turbo: bool = False
66
67
 
67
68
 
68
69
  def save(state: State) -> None:
69
- """Save state to {project_dir}/.boris/state.json."""
70
+ """Save state to {project_dir}/.boris/state.json and plan.md."""
70
71
  state_dir = os.path.join(state.project_dir, config.STATE_DIR)
71
72
  os.makedirs(state_dir, exist_ok=True)
72
73
  state_path = os.path.join(state_dir, config.STATE_FILE)
73
74
  state.updated_at = datetime.now().isoformat()
74
75
  with open(state_path, "w", encoding="utf-8") as f:
75
76
  json.dump(asdict(state), f, indent=2, ensure_ascii=False)
77
+ # Also export a human-readable markdown plan
78
+ _save_plan_md(state)
79
+
80
+
81
+ def _save_plan_md(state: State) -> None:
82
+ """Export the plan as a readable markdown file in the project directory."""
83
+ plan = state.plan
84
+ if not plan or not plan.milestones:
85
+ return
86
+
87
+ status_icon = {
88
+ "completed": "[x]",
89
+ "in_progress": "[-]",
90
+ "skipped": "[~]",
91
+ "pending": "[ ]",
92
+ }
93
+
94
+ lines = []
95
+ lines.append(f"# Boris Plan")
96
+ lines.append("")
97
+ lines.append(f"**Task:** {plan.task}")
98
+ lines.append(f"**Created:** {plan.created_at}")
99
+ lines.append(f"**Updated:** {state.updated_at}")
100
+ lines.append(f"**Mode:** {'Turbo (parallel)' if state.turbo else 'Sequential'}")
101
+ lines.append("")
102
+
103
+ # Summary counts
104
+ counts = {}
105
+ for m in plan.milestones:
106
+ counts[m.status] = counts.get(m.status, 0) + 1
107
+ total = len(plan.milestones)
108
+ done = counts.get("completed", 0)
109
+ lines.append(f"**Progress:** {done}/{total} milestones completed")
110
+ if counts.get("skipped", 0):
111
+ lines.append(f"**Skipped:** {counts['skipped']}")
112
+ lines.append("")
113
+ lines.append("---")
114
+ lines.append("")
115
+
116
+ # Milestone list
117
+ lines.append("## Milestones")
118
+ lines.append("")
119
+ for m in plan.milestones:
120
+ icon = status_icon.get(m.status, "[ ]")
121
+ lines.append(f"### {icon} {m.id}: {m.title}")
122
+ lines.append("")
123
+ if m.depends_on:
124
+ lines.append(f"**Depends on:** {', '.join(m.depends_on)}")
125
+ lines.append(f"**Status:** {m.status}")
126
+ if m.completed_at:
127
+ lines.append(f"**Completed:** {m.completed_at}")
128
+ lines.append("")
129
+ lines.append(m.description)
130
+ lines.append("")
131
+
132
+ if m.acceptance_criteria:
133
+ lines.append("**Acceptance Criteria:**")
134
+ for ac in m.acceptance_criteria:
135
+ lines.append(f"- {ac}")
136
+ lines.append("")
137
+
138
+ if m.files_to_create:
139
+ lines.append(f"**Files to create:** {len(m.files_to_create)}")
140
+ for f_path in m.files_to_create:
141
+ lines.append(f"- `{f_path}`")
142
+ lines.append("")
143
+
144
+ if m.files_to_modify:
145
+ lines.append(f"**Files to modify:** {len(m.files_to_modify)}")
146
+ for f_path in m.files_to_modify:
147
+ lines.append(f"- `{f_path}`")
148
+ lines.append("")
149
+
150
+ lines.append("---")
151
+ lines.append("")
152
+
153
+ md_path = os.path.join(state.project_dir, config.STATE_DIR, "plan.md")
154
+ with open(md_path, "w", encoding="utf-8") as f:
155
+ f.write("\n".join(lines))
76
156
 
77
157
 
78
158
  def load(project_dir: str) -> Optional[State]:
@@ -98,6 +178,7 @@ def load(project_dir: str) -> Optional[State]:
98
178
  no_git=data.get("no_git", False),
99
179
  started_at=data.get("started_at", ""),
100
180
  updated_at=data.get("updated_at", ""),
181
+ turbo=data.get("turbo", False),
101
182
  )
102
183
  except (json.JSONDecodeError, KeyError, TypeError):
103
184
  return None
@@ -1,12 +0,0 @@
1
- boris.py,sha256=0AvcqWPhvfoR1U8zUGhcnCJImpcy8cITcD4zzJpx9Jk,28715
2
- config.py,sha256=KfFKyCGasdm1yBvIRFv-ykzA_oRo-zu1Euu9YC7V1Cg,324
3
- engine.py,sha256=PGRtbnsvr6egikqo0ENpA8WnXKdQ1kW2XWUFgJ-AXzk,26889
4
- git_manager.py,sha256=BuuTT4naPb5-jLhOik1xHM2ztzuKvJ_bnecZmlYgwFs,8493
5
- planner.py,sha256=HHrw2lpcZhRBzmCapk8Uw8ivUpB7fSz2V6_FaJLHlU4,5297
6
- prompts.py,sha256=6QHAftDjDqk_e2WCq5NkTH5Jd41PgFk68fkg4nMsy-Q,26228
7
- state.py,sha256=erggbuwJu-Ks6Y7hLQpFwIEN4LjqzBPpy8kTLZtk0ZM,3092
8
- borisxdave-0.2.0.dist-info/METADATA,sha256=adf_nOv8TymIA4ez-Hb7EEzzDXaFkAmVMJ512pY21Po,133
9
- borisxdave-0.2.0.dist-info/WHEEL,sha256=hPN0AlP2dZM_3ZJZWP4WooepkmU9wzjGgCLCeFjkHLA,92
10
- borisxdave-0.2.0.dist-info/entry_points.txt,sha256=fuO7JxKFLOm6xp6m3JHRA1UO_QW1dYU-F0IooA1NqQs,37
11
- borisxdave-0.2.0.dist-info/top_level.txt,sha256=zNzzkbLJWzWpTjJTQsnbdOBypcA4XpioE1dEgWZVBx4,54
12
- borisxdave-0.2.0.dist-info/RECORD,,