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.
- Users/david/AppData/Local/Programs/Python/Python313/Lib/site-packages/boris_prompt.md +191 -0
- boris.py +871 -130
- {borisxdave-0.2.0.dist-info → borisxdave-0.3.1.dist-info}/METADATA +1 -1
- borisxdave-0.3.1.dist-info/RECORD +14 -0
- {borisxdave-0.2.0.dist-info → borisxdave-0.3.1.dist-info}/entry_points.txt +1 -0
- {borisxdave-0.2.0.dist-info → borisxdave-0.3.1.dist-info}/top_level.txt +1 -0
- engine.py +229 -4
- file_lock.py +123 -0
- planner.py +9 -1
- prompts.py +191 -12
- state.py +82 -1
- borisxdave-0.2.0.dist-info/RECORD +0 -12
- {borisxdave-0.2.0.dist-info → borisxdave-0.3.1.dist-info}/WHEEL +0 -0
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
"
|
|
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
|
|
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,,
|
|
File without changes
|