Skip to content
Merged
5 changes: 4 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
{
"name": "workflows-env",
"image": "mcr.microsoft.com/devcontainers/python:3.11",
"onCreateCommand": "sudo apt-get update && sudo apt-get install -y python3-venv curl tar && sudo mkdir -p /usr/local/bin && curl -sSL 'https://github.com/rhysd/actionlint/releases/download/v1.7.3/actionlint_1.7.3_linux_amd64.tar.gz' | sudo tar -xz -C /usr/local/bin actionlint && sudo chmod +x /usr/local/bin/actionlint",
"onCreateCommand": "sudo apt-get update && sudo apt-get install -y python3-venv curl tar && sudo mkdir -p /usr/local/bin && curl -sSL 'https://github.com/rhysd/actionlint/releases/download/v1.7.3/actionlint_1.7.3_linux_amd64.tar.gz' | sudo tar -xz -C /usr/local/bin actionlint && sudo chmod +x /usr/local/bin/actionlint && sudo rm -f /usr/local/py-utils/bin/black /usr/local/py-utils/bin/ruff /usr/local/py-utils/bin/isort /usr/local/py-utils/bin/mypy",
Comment thread
stranske marked this conversation as resolved.
"postCreateCommand": "pip install -e '.[dev]' && pre-commit install --install-hooks --hook-type pre-commit --hook-type pre-push",
"postStartCommand": "pre-commit install --install-hooks --hook-type pre-commit --hook-type pre-push",
"containerEnv": {
"PATH": "/home/vscode/.local/bin:${containerEnv:PATH}"
},
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/node:1": {
Expand Down
41 changes: 33 additions & 8 deletions scripts/langchain/followup_issue_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
SECTION_ALIASES = {
"why": ["why", "motivation", "summary", "goals"],
"scope": ["scope", "context", "background", "overview"],
"non_goals": ["non-goals", "nongoals", "out of scope", "constraints", "exclusions"],
Comment thread
stranske marked this conversation as resolved.
"tasks": ["tasks", "task list", "tasklist", "todo", "to do", "implementation"],
"acceptance": [
"acceptance criteria",
Expand All @@ -55,6 +56,7 @@
SECTION_TITLES = {
"why": "Why",
"scope": "Scope",
"non_goals": "Non-Goals",
"tasks": "Tasks",
"acceptance": "Acceptance Criteria",
"implementation": "Implementation Notes",
Expand Down Expand Up @@ -510,12 +512,14 @@ def extract_original_issue_data(


def _normalize_heading(text: str) -> str:
"""Normalize heading text for comparison (lowercase, stripped of markdown)."""
cleaned = re.sub(r"[#*_:]+", " ", text).strip().lower()
cleaned = re.sub(r"\s+", " ", cleaned)
return cleaned


def _resolve_section(label: str) -> str | None:
"""Map a heading label to a known section key, or None if unrecognized."""
normalized = _normalize_heading(label)
for key, aliases in SECTION_ALIASES.items():
for alias in aliases:
Expand All @@ -525,23 +529,39 @@ def _resolve_section(label: str) -> str | None:


def _parse_sections(body: str) -> dict[str, list[str]]:
"""Parse issue body into recognized sections.

Splits the body by top-level headings (# or ##) and maps content to known section keys.
Unrecognized top-level headings terminate the current section.
Subheadings (###, ####, etc.) within a section are preserved as content.
"""
sections: dict[str, list[str]] = {key: [] for key in SECTION_TITLES}
current: str | None = None
for line in body.splitlines():
heading_match = re.match(r"^\s*#{1,6}\s+(.*)$", line)
# Only match top-level section headings (# or ## but not ### or deeper)
# Subheadings (###, ####) are kept as content within the current section
heading_match = re.match(r"^\s*#{1,2}\s+(.*)$", line)
Comment thread
stranske marked this conversation as resolved.
Outdated
if heading_match:
section_key = _resolve_section(heading_match.group(1))
if section_key:
current = section_key
continue
# Update current - set to None for unrecognized top-level headings
# This prevents content under "## Random Notes" etc. from being
# appended to the previous recognized section
current = section_key
continue
if current:
sections[current].append(line)
return sections
Comment thread
stranske marked this conversation as resolved.


def _strip_checkbox(line: str) -> str:
def _strip_checkbox(line: str, list_match: re.Match[str] | None = None) -> str:
"""Extract text content from a list item, stripping bullet and checkbox markers.

Args:
line: The line to process.
list_match: Optional pre-computed LIST_ITEM_REGEX match to avoid re-matching.
"""
stripped = line.strip()
match = LIST_ITEM_REGEX.match(stripped)
match = list_match or LIST_ITEM_REGEX.match(stripped)
if not match:
return stripped
content = match.group(2).strip()
Expand All @@ -552,19 +572,24 @@ def _strip_checkbox(line: str) -> str:


def _parse_checklist(lines: list[str]) -> list[str]:
"""Extract checklist items from lines, handling both checkbox and plain list formats."""
items: list[str] = []
for line in lines:
stripped = line.strip()
if not stripped:
continue
# First try direct checkbox at start of line (rare but possible)
checkbox_match = CHECKBOX_REGEX.match(stripped)
if checkbox_match:
value = checkbox_match.group(2).strip()
if value and len(value) > 3:
items.append(value)
continue
if LIST_ITEM_REGEX.match(stripped):
value = _strip_checkbox(line)
# Then try list item (with optional checkbox inside)
list_match = LIST_ITEM_REGEX.match(stripped)
if list_match:
# Pass the match to avoid re-matching in _strip_checkbox
value = _strip_checkbox(line, list_match)
if value and len(value) > 3:
items.append(value)
return items
Comment thread
stranske marked this conversation as resolved.
Expand Down
29 changes: 23 additions & 6 deletions scripts/sync_tool_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

PIN_FILE = Path(".github/workflows/autofix-versions.env")
PYPROJECT_FILE = Path("pyproject.toml")
TEMPLATE_FILE = Path("templates/consumer-repo/.github/workflows/autofix-versions.env")


@dataclasses.dataclass(frozen=True)
Expand Down Expand Up @@ -167,18 +168,34 @@ def main(argv: Iterable[str]) -> int:
pyproject_content, TOOL_CONFIGS, env_values, apply_changes
)

if project_mismatches and not apply_changes:
for package, message in project_mismatches.items():
# Check template file is in sync with source
template_mismatches: dict[str, str] = {}
if TEMPLATE_FILE.exists():
template_content = TEMPLATE_FILE.read_text(encoding="utf-8")
source_content = PIN_FILE.read_text(encoding="utf-8")
if template_content != source_content:
template_mismatches["template"] = (
"templates/consumer-repo autofix-versions.env differs from source"
)
if apply_changes:
TEMPLATE_FILE.write_text(source_content, encoding="utf-8")

all_mismatches = {**project_mismatches, **template_mismatches}
if all_mismatches and not apply_changes:
for package, message in all_mismatches.items():
print(f"✗ {package}: {message}", file=sys.stderr)
print(
"Use --apply to rewrite pyproject.toml with the pinned versions.",
"Use --apply to sync tool versions to pyproject.toml and template.",
file=sys.stderr,
)
return 1

if apply_changes and pyproject_updated != pyproject_content:
PYPROJECT_FILE.write_text(pyproject_updated, encoding="utf-8")
print("✓ tool pins synced to pyproject.toml")
if apply_changes:
if pyproject_updated != pyproject_content:
PYPROJECT_FILE.write_text(pyproject_updated, encoding="utf-8")
print("✓ tool pins synced to pyproject.toml")
if template_mismatches:
print("✓ template autofix-versions.env synced from source")

return 0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
# NOTE: Only include dev tools here (linters, formatters, test runners).
# Runtime dependencies (PyYAML, Pydantic, Hypothesis) should be managed via Dependabot
# in each consumer repo's pyproject.toml directly, NOT synced from this file.
BLACK_VERSION=25.12.0
RUFF_VERSION=0.14.11
BLACK_VERSION=26.1.0
RUFF_VERSION=0.14.13
ISORT_VERSION=7.0.0
DOCFORMATTER_VERSION=1.7.7
MYPY_VERSION=1.19.1
PYTEST_VERSION=9.0.2
PYTEST_COV_VERSION=7.0.0
PYTEST_XDIST_VERSION=3.8.0
COVERAGE_VERSION=7.13.1

Loading