Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
"Potential Gotchas",
]

# Accept any standard Markdown heading level for required/recommended sections.
SECTION_HEADING_PATTERN = r'(?:^|\n)#{1,6}\s*'
NEXT_HEADING_PATTERN = r'\n#{1,6}\s+'


def check_todos(content: str) -> tuple[bool, list[str]]:
"""Check for remaining TODO placeholders."""
Expand All @@ -65,14 +69,14 @@ def check_required_sections(content: str) -> tuple[bool, list[str]]:
missing = []
for section in REQUIRED_SECTIONS:
# Look for section header
pattern = rf'(?:^|\n)##?\s*{re.escape(section)}'
pattern = rf'{SECTION_HEADING_PATTERN}{re.escape(section)}'
match = re.search(pattern, content, re.IGNORECASE)
if not match:
missing.append(f"{section} (missing)")
else:
# Check if section has meaningful content (not just placeholder)
section_start = match.end()
next_section = re.search(r'\n##?\s+', content[section_start:])
next_section = re.search(NEXT_HEADING_PATTERN, content[section_start:])
section_end = section_start + next_section.start() if next_section else len(content)
section_content = content[section_start:section_end].strip()

Expand All @@ -87,7 +91,7 @@ def check_recommended_sections(content: str) -> list[str]:
"""Check which recommended sections are missing."""
missing = []
for section in RECOMMENDED_SECTIONS:
pattern = rf'(?:^|\n)##?\s*{re.escape(section)}'
pattern = rf'{SECTION_HEADING_PATTERN}{re.escape(section)}'
if not re.search(pattern, content, re.IGNORECASE):
missing.append(section)
return missing
Expand Down
10 changes: 7 additions & 3 deletions skills/session-handoff/scripts/validate_handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
"Potential Gotchas",
]

# Accept any standard Markdown heading level for required/recommended sections.
SECTION_HEADING_PATTERN = r'(?:^|\n)#{1,6}\s*'
NEXT_HEADING_PATTERN = r'\n#{1,6}\s+'


def check_todos(content: str) -> tuple[bool, list[str]]:
"""Check for remaining TODO placeholders."""
Expand All @@ -65,14 +69,14 @@ def check_required_sections(content: str) -> tuple[bool, list[str]]:
missing = []
for section in REQUIRED_SECTIONS:
# Look for section header
pattern = rf'(?:^|\n)##?\s*{re.escape(section)}'
pattern = rf'{SECTION_HEADING_PATTERN}{re.escape(section)}'
match = re.search(pattern, content, re.IGNORECASE)
if not match:
missing.append(f"{section} (missing)")
else:
# Check if section has meaningful content (not just placeholder)
section_start = match.end()
next_section = re.search(r'\n##?\s+', content[section_start:])
next_section = re.search(NEXT_HEADING_PATTERN, content[section_start:])
section_end = section_start + next_section.start() if next_section else len(content)
section_content = content[section_start:section_end].strip()

Expand All @@ -87,7 +91,7 @@ def check_recommended_sections(content: str) -> list[str]:
"""Check which recommended sections are missing."""
missing = []
for section in RECOMMENDED_SECTIONS:
pattern = rf'(?:^|\n)##?\s*{re.escape(section)}'
pattern = rf'{SECTION_HEADING_PATTERN}{re.escape(section)}'
if not re.search(pattern, content, re.IGNORECASE):
missing.append(section)
return missing
Expand Down
86 changes: 86 additions & 0 deletions tests/test_session_handoff_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import importlib.util
import unittest
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
MODULE_PATH = ROOT / "skills" / "session-handoff" / "scripts" / "validate_handoff.py"

spec = importlib.util.spec_from_file_location("validate_handoff", MODULE_PATH)
validate_handoff = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(validate_handoff)


THIRD_LEVEL_HANDOFF = """# Task Handoff

### Current State Summary
This section explains the current state in enough detail for the next agent to
pick up the work without needing to rediscover the plan from scratch.

### Important Context
The validator should accept deeper heading levels because real handoffs often
nest sections under a top-level document title before listing the main content.

### Immediate Next Steps
Patch the regex, run the regression tests, and rebuild the plugin output so the
packaged version matches the source tree exactly.

### Architecture Overview
The script validates sections, scans for secrets, and checks file references.

### Critical Files
The main file is skills/session-handoff/scripts/validate_handoff.py.

### Files Modified
This regression touches the validator and the new tests in the repository.

### Decisions Made
Allow any standard Markdown heading level instead of hard-coding level two only.

### Assumptions Made
Markdown handoff files may use nested section headings under a document title.

### Potential Gotchas
The dist plugin copy must be rebuilt after changing the source implementation.
"""


SECOND_LEVEL_HANDOFF = """# Task Handoff

## Current State Summary
This section confirms the previous level-two format still passes after the
validator is updated to accept deeper headings as well.

## Important Context
Backward compatibility matters because existing handoffs use second-level
headings and should keep receiving high validation scores.

## Immediate Next Steps
Run the same checks against both legacy and nested heading examples to verify
the broader heading regex does not regress older files.
"""


class SessionHandoffValidatorTests(unittest.TestCase):
def test_accepts_third_level_headings_for_required_and_recommended_sections(self):
required_complete, missing_required = validate_handoff.check_required_sections(
THIRD_LEVEL_HANDOFF
)
missing_recommended = validate_handoff.check_recommended_sections(THIRD_LEVEL_HANDOFF)

self.assertTrue(required_complete)
self.assertEqual(missing_required, [])
self.assertEqual(missing_recommended, [])

def test_keeps_support_for_second_level_required_headings(self):
required_complete, missing_required = validate_handoff.check_required_sections(
SECOND_LEVEL_HANDOFF
)

self.assertTrue(required_complete)
self.assertEqual(missing_required, [])


if __name__ == "__main__":
unittest.main()