Summary
BUNDLE_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,39}$") (sweep.py:37) is applied with
.match(), and $ matches before a trailing newline. So a bundle name of "fix-it\n" passes
validation:
>>> BUNDLE_NAME_RE.match("fix-it\n") # truthy — accepted
>>> BUNDLE_NAME_RE.fullmatch("fix-it\n") # None
Confirmed end to end: validate_triage with bundles=[{"name": "fix-it\n", ...}] returns
errors == [] and plan.bundles[0].name == "fix-it\n".
Consequence
Bundle.name is used raw downstream:
- as a filesystem segment —
_run_bundle → _write_intent, self.run_dir / "bundles" / dirname / "intent.md" (sweep.py:637, :1207), with no safe_segment
- as
story_key via _bundle_key (dw-fix-it\n), which lands in run state and the journal
On Linux this silently creates a directory whose name ends in a newline. On the native-Windows
target a path segment containing a control character is invalid, so the sweep fails at bundle start.
Branch names are safe — workspace.py:99-100 runs safe_ref_segment — but the directory path and
the story key are not.
Fix shape
Anchor with \Z (or switch the call sites to re.fullmatch):
BUNDLE_NAME_RE = re.compile(r"[a-z0-9][a-z0-9-]{1,39}\Z")
and add a negative case asserting plan is None for a trailing break in both bundles[].name and an
option's bundle_name. Worth auditing the other .match()-with-$ patterns in the same module at
the same time.
Note this is a validation hole, not a ledger-injection one — #305's sanitizer covers the ledger
route (_close_bundle_ledger_when_spec_status builds its note from task.story_key); this is the
path/story-key axis.
Found during the #305 review.
Summary
BUNDLE_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,39}$")(sweep.py:37) is applied with.match(), and$matches before a trailing newline. So a bundle name of"fix-it\n"passesvalidation:
Confirmed end to end:
validate_triagewithbundles=[{"name": "fix-it\n", ...}]returnserrors == []andplan.bundles[0].name == "fix-it\n".Consequence
Bundle.nameis used raw downstream:_run_bundle→_write_intent,self.run_dir / "bundles" / dirname / "intent.md"(sweep.py:637,:1207), with nosafe_segmentstory_keyvia_bundle_key(dw-fix-it\n), which lands in run state and the journalOn Linux this silently creates a directory whose name ends in a newline. On the native-Windows
target a path segment containing a control character is invalid, so the sweep fails at bundle start.
Branch names are safe —
workspace.py:99-100runssafe_ref_segment— but the directory path andthe story key are not.
Fix shape
Anchor with
\Z(or switch the call sites tore.fullmatch):and add a negative case asserting
plan is Nonefor a trailing break in bothbundles[].nameand anoption's
bundle_name. Worth auditing the other.match()-with-$patterns in the same module atthe same time.
Note this is a validation hole, not a ledger-injection one — #305's sanitizer covers the ledger
route (
_close_bundle_ledger_when_spec_statusbuilds its note fromtask.story_key); this is thepath/story-key axis.
Found during the #305 review.