Skip to content

Commit 1f32e91

Browse files
fix(spec): generic implement workspace from checklist paths
Top-level disk listing plus checklist-named paths only — no layout classifiers. Resume injects open implementation tasks; pathless steps get explicit next-action guidance. Tests cover no-path steps and resume. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f034058 commit 1f32e91

6 files changed

Lines changed: 239 additions & 112 deletions

File tree

cecli/spec/focus.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@
88
from cecli.spec.implement import build_implement_workspace_block
99
from cecli.spec.steering import (
1010
IMPLEMENTATION_TOOL_HINTS,
11-
SCAFFOLD_MISSING_HINT,
11+
SCAFFOLD_HINT,
1212
SPEC_FOCUS_INSTRUCTIONS,
1313
build_spec_focus_preamble,
14-
workspace_lib_missing,
1514
)
1615
from cecli.spec.todos import (
1716
TodoItem,
@@ -85,9 +84,15 @@ def should_inject_task_context(
8584
focus_requested: bool,
8685
item: TodoItem | None,
8786
inject_todo_spec: bool,
87+
agent_continuation: bool = False,
88+
message: str | None = None,
8889
) -> bool:
8990
if item is None:
9091
return False
92+
if agent_continuation:
93+
return False
94+
if message and _is_resume_implement_message(message):
95+
return False
9196
if inject_todo_spec:
9297
return True
9398
if not focus_requested:
@@ -137,6 +142,8 @@ def build_user_message_with_spec_context(
137142
focus_requested=focus_requested,
138143
item=item,
139144
inject_todo_spec=inject_todo_spec,
145+
agent_continuation=agent_continuation,
146+
message=message,
140147
):
141148
assert item is not None
142149
turn_todo_id = item.id
@@ -154,8 +161,14 @@ def build_user_message_with_spec_context(
154161
if not agent_continuation:
155162
blocks.append(build_spec_focus_preamble(workspace))
156163
blocks.append(IMPLEMENTATION_TOOL_HINTS.strip())
157-
if workspace_lib_missing(workspace):
158-
blocks.append(SCAFFOLD_MISSING_HINT.strip())
164+
blocks.append(SCAFFOLD_HINT.strip())
165+
if _is_resume_implement_message(message) and item is not None:
166+
from cecli.spec.todos import _implementation_tasks_for_inject
167+
168+
blocks.append(
169+
"## Open implementation tasks (resume)\n"
170+
+ _implementation_tasks_for_inject(item, max_open=4)
171+
)
159172
checklist = item.checklist if item is not None else []
160173
blocks.append(
161174
build_implement_workspace_block(

cecli/spec/implement.py

Lines changed: 79 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,25 @@
88
import subprocess
99
from pathlib import Path
1010

11-
from cecli.spec.steering import workspace_lib_missing
1211
from cecli.spec.todos import ChecklistItem
1312

1413
_PATH_IN_CHECKLIST = re.compile(
15-
r"(?:`((?:lib|test)/[\w./-]+)`|((?:lib|test)/[\w./-]+))",
16-
re.IGNORECASE,
14+
r"`([^`\n]+)`|(?<![`\w])((?:[\w.-]+/)+[\w./-]+)",
1715
)
1816

19-
_SNAPSHOT_DIRS = ("lib", "test")
20-
_MAX_LIST_FILES = 24
21-
22-
23-
def list_workspace_test_files(workspace: str | Path, *, limit: int = _MAX_LIST_FILES) -> list[str]:
24-
return _list_tree_files(Path(workspace).resolve(), "test", limit=limit)
17+
_MAX_TOP_LEVEL = 20
2518

19+
_SNAPSHOT_ORIENTATION = (
20+
"- **Note:** Top-level listing is orientation only — do **not** ContextManager-add "
21+
"these entries unless the checklist or implementation tasks name them."
22+
)
2623

27-
def _list_tree_files(root: Path, subdir: str, *, limit: int = _MAX_LIST_FILES) -> list[str]:
28-
base = root / subdir
29-
if not base.is_dir():
30-
return []
31-
out: list[str] = []
32-
for path in sorted(base.rglob("*")):
33-
if not path.is_file():
34-
continue
35-
if path.name.startswith("."):
36-
continue
37-
rel = path.relative_to(root).as_posix()
38-
out.append(rel)
39-
if len(out) >= limit:
40-
break
41-
return out
24+
_NO_PATH_NEXT_ACTION = (
25+
"This checklist item names **no file paths**. Use **## Implementation tasks** "
26+
"(injected above) for deliverable paths for this step — pick **one missing file**, "
27+
"then **ContextManager create** → **ReadRange** → **EditText**. "
28+
"**Do not** skip to later numbered tasks."
29+
)
4230

4331

4432
def paths_from_checklist_text(text: str) -> list[str]:
@@ -48,6 +36,8 @@ def paths_from_checklist_text(text: str) -> list[str]:
4836
raw = (match.group(1) or match.group(2) or "").strip().rstrip("/")
4937
if not raw or raw in seen:
5038
continue
39+
if "/" not in raw and not re.search(r"\.[a-z]{2,5}$", raw, re.IGNORECASE):
40+
continue
5141
seen.add(raw)
5242
found.append(raw)
5343
return found
@@ -211,28 +201,19 @@ def resolve_implement_focus(
211201

212202

213203
def dart_test_paths_for_focus(workspace: str | Path, focus: ChecklistItem) -> list[str]:
214-
"""Best-effort test file paths for a checklist item mentioning tests."""
215-
paths = paths_from_checklist_text(focus.text)
216-
test_paths = [p for p in paths if p.startswith("test/") and p.endswith(".dart")]
217-
if test_paths:
218-
return test_paths[:4]
219-
all_tests = list_workspace_test_files(workspace)
220-
lower = focus.text.lower()
221-
tokens = [
222-
t
223-
for t in re.split(r"[\W_]+", lower)
224-
if len(t) > 3 and t not in {"write", "unit", "tests", "test", "for"}
225-
]
226-
scored: list[tuple[int, str]] = []
227-
for path in all_tests:
228-
path_lower = path.lower()
229-
score = sum(1 for tok in tokens if tok in path_lower)
230-
if score:
231-
scored.append((score, path))
232-
if scored:
233-
scored.sort(key=lambda x: (-x[0], x[1]))
234-
return [p for _, p in scored[:4]]
235-
return [f for f in all_tests if f.endswith("_test.dart")][:4]
204+
"""Dart test files explicitly named in the checklist and present on disk."""
205+
root = Path(workspace).resolve()
206+
if not (root / "pubspec.yaml").is_file():
207+
return []
208+
out: list[str] = []
209+
for rel in paths_from_checklist_text(focus.text):
210+
if not rel.endswith(".dart"):
211+
continue
212+
if deliverable_paths_exist(root, [rel]):
213+
out.append(rel)
214+
if len(out) >= 4:
215+
break
216+
return out
236217

237218

238219
def resolve_flutter_executable() -> str | None:
@@ -263,24 +244,29 @@ def build_workspace_snapshot_lines(workspace: str | Path) -> list[str]:
263244
lines = ["## Workspace snapshot (verified on disk — do **not** ls to rediscover)"]
264245
from cecli.spec.pubspec_repair import pubspec_repair_snapshot_lines
265246

266-
pubspec = root / "pubspec.yaml"
267-
if pubspec.is_file():
268-
lines.append("- `pubspec.yaml` — present")
269-
else:
270-
lines.append("- `pubspec.yaml` — **missing**")
247+
try:
248+
entries = sorted(root.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower()))
249+
except OSError:
250+
entries = []
251+
top_names = [
252+
e.name + ("/" if e.is_dir() else "")
253+
for e in entries[:_MAX_TOP_LEVEL]
254+
if not e.name.startswith(".")
255+
]
256+
if top_names:
257+
preview = ", ".join(f"`{name}`" for name in top_names)
258+
extra = f" (+{len(entries) - _MAX_TOP_LEVEL} more)" if len(entries) > _MAX_TOP_LEVEL else ""
259+
lines.append(f"- **Top level:** {preview}{extra}")
271260

272-
for sub in _SNAPSHOT_DIRS:
273-
files = _list_tree_files(root, sub)
274-
if not files:
275-
lines.append(f"- `{sub}/` — **empty or missing**")
276-
continue
277-
preview = ", ".join(f"`{f}`" for f in files[:8])
278-
extra = f" (+{len(files) - 8} more)" if len(files) > 8 else ""
279-
lines.append(f"- `{sub}/` — {len(files)} file(s): {preview}{extra}")
261+
lines.append(_SNAPSHOT_ORIENTATION)
280262
lines.extend(pubspec_repair_snapshot_lines(root))
281263
return lines
282264

283265

266+
def _flutter_project(workspace: str | Path) -> bool:
267+
return (Path(workspace).resolve() / "pubspec.yaml").is_file()
268+
269+
284270
def build_implement_next_action_lines(
285271
workspace: str | Path,
286272
checklist: list[ChecklistItem],
@@ -314,51 +300,57 @@ def build_implement_next_action_lines(
314300
)
315301
paths = paths_from_checklist_text(focus.text)
316302
on_disk = deliverable_paths_exist(workspace, paths) if paths else False
317-
test_files = _list_tree_files(Path(workspace).resolve(), "test")
303+
named_dart_tests = [
304+
p for p in paths if p.endswith(".dart") and deliverable_paths_exist(workspace, [p])
305+
]
318306

319-
if is_test_related_checklist_text(focus.text) and test_files:
320-
target = next((f for f in test_files if "test" in f), test_files[0])
321-
lines.append(f"Focus checklist: **{focus.text.strip()}** — test file(s) already on disk.")
307+
if is_test_related_checklist_text(focus.text) and named_dart_tests:
308+
target = named_dart_tests[0]
309+
lines.append(f"Focus checklist: **{focus.text.strip()}** — `{target}` is on disk.")
322310
lines.append(
323311
f"1. **ReadRange** `{target}` with `@000` / `000@` once\n"
324-
f"2. **EditText** only if tests need fixes\n"
325-
f"3. BrightVision runs **`flutter test`** at end of this turn — **do not** run flutter via Command\n"
326-
f"4. Mark this checklist item done **only after** BrightVision reports tests passed\n"
327-
f"**Do not** call ls, Grep, GitStatus, or repeat ReadRange on the same file."
312+
f"2. **EditText** only if tests need fixes"
328313
)
329-
elif on_disk and is_test_related_checklist_text(focus.text):
330-
test_files = _list_tree_files(Path(workspace).resolve(), "test")
331-
target = next((f for f in test_files if "test" in f), test_files[0] if test_files else None)
332-
if target:
314+
if _flutter_project(workspace):
333315
lines.append(
334-
f"Focus checklist: **{focus.text.strip()}** — deliverable files already exist."
335-
)
336-
lines.append(
337-
f"1. **ReadRange** `{target}` with `@000` / `000@` once\n"
338-
f"2. **EditText** only if tests need fixes\n"
339-
f"3. BrightVision runs **`flutter test`** at end of this turn — **do not** run flutter via Command\n"
340-
f"4. Mark this checklist item done **only after** BrightVision reports tests passed\n"
341-
f"**Do not** call ls, Grep, GitStatus, or repeat ReadRange on the same file."
316+
"3. BrightVision runs **`flutter test`** at end of this turn — **do not** run flutter via Command\n"
317+
"4. Mark this checklist item done **only after** BrightVision reports tests passed"
342318
)
343319
else:
344-
lines.append(
345-
f"Focus: **{focus.text.strip()}** — create tests with **ContextManager** + "
346-
"**ReadRange** + **EditText** (one file). **No ls.**"
347-
)
320+
lines.append("3. Mark this checklist item done **only after** edits succeed")
321+
lines.append("**Do not** call ls, Grep, GitStatus, or repeat ReadRange on the same file.")
322+
elif is_test_related_checklist_text(focus.text) and paths and not on_disk:
323+
lines.append(f"Focus checklist: **{focus.text.strip()}**")
324+
lines.append(
325+
"Create the test file(s) **named in this item** with **ContextManager create**, "
326+
"then **ReadRange** + **EditText**. **No ls.**"
327+
)
328+
elif is_test_related_checklist_text(focus.text):
329+
lines.append(f"Focus checklist: **{focus.text.strip()}**")
330+
lines.append(
331+
"Name target file path(s) in the checklist, then **ContextManager** / **ReadRange** / "
332+
"**EditText** on **one** file. **No ls.**"
333+
)
348334
elif on_disk:
349-
target = paths[0] if paths else "lib/"
335+
target = paths[0]
350336
lines.append(
351337
f"Focus checklist: **{focus.text.strip()}** — paths exist on disk (`{target}`)."
352338
)
353339
lines.append(
354340
"**ReadRange** the target source file, then **EditText** to finish. **No ls.**"
355341
)
356-
elif workspace_lib_missing(workspace):
342+
elif paths and not on_disk:
357343
lines.append(f"Focus checklist: **{focus.text.strip()}**")
358344
lines.append(
359-
"**ContextManager** to scaffold `lib/` (and `test/` if needed), then **ReadRange** + "
360-
"**EditText** on one file. **Do not ls** empty directories."
345+
"Paths **named in this item** are not on disk yet — use **ContextManager create** "
346+
"(not `add` on missing files), then **ReadRange** + **EditText** on **one** target file."
361347
)
348+
lines.append(
349+
"**Do not** ls, Grep, or ReadRange paths **not named** in this checklist item."
350+
)
351+
elif not paths:
352+
lines.append(f"Focus checklist: **{focus.text.strip()}**")
353+
lines.append(_NO_PATH_NEXT_ACTION)
362354
elif resume:
363355
lines.append(f"Focus checklist: **{focus.text.strip()}**")
364356
lines.append(

cecli/spec/steering.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,15 @@
3838
- Do not mark implementation done until requirements pass EARS lint (WHEN/SHALL, no duplicate REQ ids).
3939
"""
4040

41-
SCAFFOLD_MISSING_HINT = """\
42-
## Workspace state (read before exploring)
41+
SCAFFOLD_HINT = """\
42+
## Scaffolding (this turn)
4343
44-
`lib/` does **not** exist yet — scaffolding tasks (e.g. **1.1**, **1.2**) are incomplete.
45-
Do **not** repeat `ls` on `lib/` or `test/`. Use **ContextManager** to create directories/files,
46-
then **ReadRange** + **EditText**. If the active task is **1.3+**, complete **1.1** first.
44+
Use **ContextManager create** for **missing** paths **named in the checklist** (add on missing files is upgraded to create).
45+
Then **ReadRange** + **EditText** on **one** target file. **No ls.**
46+
Do not edit or ReadRange paths **not named** in the current checklist item.
4747
"""
4848

4949

50-
def workspace_lib_missing(workspace: str | Path) -> bool:
51-
return not (Path(workspace).resolve() / "lib").is_dir()
52-
53-
5450
IMPLEMENTATION_TOOL_HINTS = """\
5551
## Implementation turn (tools)
5652

cecli/spec/todos.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,29 @@ def format_todo_context_light(item: TodoItem, *, store: TodoStore | None = None)
251251

252252

253253
_IMPLEMENT_DESIGN_MAX_CHARS = int(os.environ.get("BV_IMPLEMENT_DESIGN_MAX_CHARS", "4000"))
254+
_IMPLEMENT_TASKS_MAX_OPEN = int(os.environ.get("BV_IMPLEMENT_TASKS_MAX_OPEN", "12"))
255+
256+
257+
def _implementation_tasks_for_inject(item: TodoItem, *, max_open: int | None = None) -> str:
258+
"""Open checklist steps only — full tasks_md can be 20k+ tokens on local models."""
259+
from cecli.spec.progress import implementation_steps
260+
261+
limit = max_open if max_open is not None else _IMPLEMENT_TASKS_MAX_OPEN
262+
steps = implementation_steps(item)
263+
open_steps = [s for s in steps if not s.done and s.step_id]
264+
if not open_steps:
265+
return _layer_or_placeholder(item.tasks_md, "(No implementation tasks yet.)")
266+
shown = open_steps[: max(1, limit)]
267+
lines: list[str] = []
268+
for step in shown:
269+
cur = " **(current)**" if step.current else ""
270+
lines.append(f"- [ ] {step.step_id} {step.text.strip()}{cur}")
271+
hidden = len(open_steps) - len(shown)
272+
if hidden:
273+
lines.append(
274+
f"\n{hidden} more open implementation step(s) — full list in Tasks / `.cecli/specs/`."
275+
)
276+
return "\n".join(lines)
254277

255278

256279
def _truncate_spec_layer(text: str, *, max_chars: int, label: str) -> str:
@@ -317,7 +340,7 @@ def format_todo_context_implement(item: TodoItem, *, store: TodoStore | None = N
317340
_truncate_spec_layer(item.design, max_chars=_IMPLEMENT_DESIGN_MAX_CHARS, label="design"),
318341
"",
319342
"## Implementation tasks",
320-
_layer_or_placeholder(item.tasks_md, "(No implementation tasks yet.)"),
343+
_implementation_tasks_for_inject(item),
321344
]
322345
if item.checklist:
323346
_append_checklist_block(lines, item.checklist)

0 commit comments

Comments
 (0)