Skip to content

Commit d2df293

Browse files
Add tasks CLI, pubspec repair, and progress public API.
Export progress/pubspec from cecli.spec; bright-vision-tasks subcommands (materialize, progress, sync-agent, repair-pubspec); inject missing pub deps into implement workspace snapshot. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5b8fc42 commit d2df293

8 files changed

Lines changed: 473 additions & 11 deletions

File tree

cecli/spec/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,35 @@
22

33
from cecli.spec.ears import analyze_requirements, analyze_traceability, build_spec_index
44
from cecli.spec.jobs import SpecGenerationJob, spec_gen_timeout_s
5+
from cecli.spec.progress import (
6+
ImplementationStep,
7+
implementation_steps,
8+
mark_implementation_step_done,
9+
materialize_checklist_from_tasks_md,
10+
merge_agent_progress_into_tasks_md,
11+
next_open_implementation_step,
12+
try_mark_focus_step_complete,
13+
)
14+
from cecli.spec.pubspec_repair import (
15+
PubspecRepairResult,
16+
find_missing_pubspec_dependencies,
17+
repair_pubspec_dependencies,
18+
)
519

620
__all__ = [
21+
"ImplementationStep",
22+
"PubspecRepairResult",
723
"SpecGenerationJob",
824
"analyze_requirements",
925
"analyze_traceability",
1026
"build_spec_index",
27+
"find_missing_pubspec_dependencies",
28+
"implementation_steps",
29+
"mark_implementation_step_done",
30+
"materialize_checklist_from_tasks_md",
31+
"merge_agent_progress_into_tasks_md",
32+
"next_open_implementation_step",
33+
"repair_pubspec_dependencies",
1134
"spec_gen_timeout_s",
35+
"try_mark_focus_step_complete",
1236
]

cecli/spec/implement.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ def resolve_flutter_executable() -> str | None:
261261
def build_workspace_snapshot_lines(workspace: str | Path) -> list[str]:
262262
root = Path(workspace).resolve()
263263
lines = ["## Workspace snapshot (verified on disk — do **not** ls to rediscover)"]
264+
from cecli.spec.pubspec_repair import pubspec_repair_snapshot_lines
265+
264266
pubspec = root / "pubspec.yaml"
265267
if pubspec.is_file():
266268
lines.append("- `pubspec.yaml` — present")
@@ -275,6 +277,7 @@ def build_workspace_snapshot_lines(workspace: str | Path) -> list[str]:
275277
preview = ", ".join(f"`{f}`" for f in files[:8])
276278
extra = f" (+{len(files) - 8} more)" if len(files) > 8 else ""
277279
lines.append(f"- `{sub}/` — {len(files)} file(s): {preview}{extra}")
280+
lines.extend(pubspec_repair_snapshot_lines(root))
278281
return lines
279282

280283

cecli/spec/progress.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def merge_agent_progress_into_tasks_md(tasks_md: str, rows: list[AgentTodoRow])
7676
if not m:
7777
out.append(line)
7878
continue
79-
indent, _mark, body = m.group(1), m.group(2), m.group(3).strip()
79+
indent, body = m.group(1), m.group(3).strip()
8080
step = checklist_step_prefix(body)
8181
new_done = None
8282
if step and step in done_by_step:
@@ -127,9 +127,7 @@ def materialize_checklist_from_tasks_md(item: TodoItem) -> list[ChecklistItem]:
127127
parsed = rows_from_tasks_md(item.tasks_md or "")
128128
if not parsed:
129129
return list(item.checklist or [])
130-
rows = [
131-
AgentTodoRow(text=row.text, done=row.done, current=row.current) for row in parsed
132-
]
130+
rows = [AgentTodoRow(text=row.text, done=row.done, current=row.current) for row in parsed]
133131
return checklist_from_agent_rows(rows, prior=item.checklist or [])
134132

135133

cecli/spec/pubspec_repair.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
"""Detect and repair missing Dart package dependencies in pubspec.yaml."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
import re
7+
import subprocess
8+
from dataclasses import dataclass
9+
from pathlib import Path
10+
11+
_DART_PKG_IMPORT = re.compile(
12+
r"""^\s*import\s+['"]package:([a-zA-Z_][\w]*)/""",
13+
re.MULTILINE,
14+
)
15+
16+
_BUILTIN_PACKAGES = frozenset(
17+
{
18+
"flutter",
19+
"flutter_test",
20+
"integration_test",
21+
"flutter_localizations",
22+
"flutter_web_plugins",
23+
}
24+
)
25+
26+
_DEP_SECTION = re.compile(r"^(\s*)(dependencies|dev_dependencies):\s*$", re.MULTILINE)
27+
28+
29+
@dataclass(frozen=True)
30+
class PubspecRepairResult:
31+
missing: tuple[str, ...]
32+
added: tuple[str, ...]
33+
applied: bool
34+
message: str
35+
36+
37+
def collect_dart_package_imports(workspace: str | Path) -> set[str]:
38+
"""Package names imported from ``lib/`` and ``test/`` Dart sources."""
39+
root = Path(workspace).resolve()
40+
packages: set[str] = set()
41+
for sub in ("lib", "test"):
42+
base = root / sub
43+
if not base.is_dir():
44+
continue
45+
for path in base.rglob("*.dart"):
46+
try:
47+
text = path.read_text(encoding="utf-8", errors="replace")
48+
except OSError:
49+
continue
50+
for match in _DART_PKG_IMPORT.finditer(text):
51+
name = match.group(1)
52+
if name not in _BUILTIN_PACKAGES:
53+
packages.add(name)
54+
return packages
55+
56+
57+
def parse_pubspec_dependencies(pubspec_text: str) -> set[str]:
58+
"""Declared package names under dependencies / dev_dependencies."""
59+
declared: set[str] = set()
60+
section: str | None = None
61+
for line in pubspec_text.splitlines():
62+
stripped = line.strip()
63+
if stripped in ("dependencies:", "dev_dependencies:"):
64+
section = stripped[:-1]
65+
continue
66+
if section and stripped and not stripped.startswith("#"):
67+
if re.match(r"^[a-zA-Z_][\w]*:", stripped) and not stripped.startswith("sdk:"):
68+
key = stripped.split(":", 1)[0].strip()
69+
if key not in ("flutter", "sdk"):
70+
declared.add(key)
71+
elif line and not line[0].isspace():
72+
section = None
73+
return declared
74+
75+
76+
def find_missing_pubspec_dependencies(workspace: str | Path) -> list[str]:
77+
root = Path(workspace).resolve()
78+
pubspec = root / "pubspec.yaml"
79+
if not pubspec.is_file():
80+
return []
81+
try:
82+
text = pubspec.read_text(encoding="utf-8")
83+
except OSError:
84+
return []
85+
used = collect_dart_package_imports(root)
86+
declared = parse_pubspec_dependencies(text)
87+
return sorted(used - declared)
88+
89+
90+
def _append_dependencies(pubspec_text: str, packages: list[str]) -> str:
91+
if not packages:
92+
return pubspec_text
93+
lines = pubspec_text.splitlines()
94+
insert_at: int | None = None
95+
for idx, line in enumerate(lines):
96+
if line.strip() == "dependencies:":
97+
insert_at = idx + 1
98+
break
99+
if insert_at is None:
100+
if lines and lines[-1].strip():
101+
lines.append("")
102+
lines.append("dependencies:")
103+
insert_at = len(lines)
104+
indent = " "
105+
for pkg in packages:
106+
lines.insert(insert_at, f"{indent}{pkg}: any")
107+
insert_at += 1
108+
return "\n".join(lines) + ("\n" if pubspec_text.endswith("\n") else "")
109+
110+
111+
def _run_flutter_pub_add(workspace: Path, packages: list[str]) -> tuple[bool, str]:
112+
from cecli.spec.implement import resolve_flutter_executable
113+
114+
flutter = resolve_flutter_executable()
115+
if not flutter:
116+
return False, "flutter not found on PATH"
117+
cmd = [flutter, "pub", "add", *packages]
118+
try:
119+
proc = subprocess.run(
120+
cmd,
121+
cwd=str(workspace),
122+
capture_output=True,
123+
text=True,
124+
timeout=120,
125+
env={**os.environ, "PATH": os.environ.get("PATH", "")},
126+
check=False,
127+
)
128+
except (subprocess.TimeoutExpired, OSError) as exc:
129+
return False, str(exc)
130+
out = ((proc.stdout or "") + (proc.stderr or "")).strip()
131+
return proc.returncode == 0, out[-2000:] if out else f"exit {proc.returncode}"
132+
133+
134+
def repair_pubspec_dependencies(
135+
workspace: str | Path,
136+
packages: list[str] | None = None,
137+
*,
138+
apply: bool = False,
139+
) -> PubspecRepairResult:
140+
"""Detect or add missing pub dependencies (flutter pub add when possible)."""
141+
root = Path(workspace).resolve()
142+
pubspec = root / "pubspec.yaml"
143+
if not pubspec.is_file():
144+
return PubspecRepairResult((), (), False, "pubspec.yaml missing")
145+
146+
missing = list(packages or find_missing_pubspec_dependencies(root))
147+
if not missing:
148+
return PubspecRepairResult((), (), False, "No missing package dependencies detected.")
149+
150+
if not apply:
151+
return PubspecRepairResult(
152+
tuple(missing),
153+
(),
154+
False,
155+
f"Missing dependencies: {', '.join(missing)}. Re-run with --apply.",
156+
)
157+
158+
ok, output = _run_flutter_pub_add(root, missing)
159+
if ok:
160+
return PubspecRepairResult(
161+
tuple(missing),
162+
tuple(missing),
163+
True,
164+
output or f"Added: {', '.join(missing)}",
165+
)
166+
167+
try:
168+
original = pubspec.read_text(encoding="utf-8")
169+
updated = _append_dependencies(original, missing)
170+
pubspec.write_text(updated, encoding="utf-8")
171+
except OSError as exc:
172+
return PubspecRepairResult(tuple(missing), (), False, f"Failed to edit pubspec.yaml: {exc}")
173+
174+
return PubspecRepairResult(
175+
tuple(missing),
176+
tuple(missing),
177+
True,
178+
f"flutter pub add failed ({output}); appended {', '.join(missing)} under dependencies:",
179+
)
180+
181+
182+
def pubspec_repair_snapshot_lines(workspace: str | Path) -> list[str]:
183+
"""Optional implement-snapshot lines when imports lack pubspec entries."""
184+
missing = find_missing_pubspec_dependencies(workspace)
185+
if not missing:
186+
return []
187+
preview = ", ".join(f"`{p}`" for p in missing[:6])
188+
extra = f" (+{len(missing) - 6} more)" if len(missing) > 6 else ""
189+
return [
190+
f"- **pubspec.yaml** — missing dependencies: {preview}{extra}. "
191+
"Add with **EditText** on `pubspec.yaml` or run "
192+
"`bright-vision-tasks repair-pubspec --apply`."
193+
]

0 commit comments

Comments
 (0)