Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
18 changes: 14 additions & 4 deletions .github/scripts/issue_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ def _task_has_concrete_target(item: str) -> bool:
for match in re.finditer(rf"\b{_TASK_CATEGORY}\s+(`[^`]+`|[^\s]+)", item, re.I):
token = match.group(1)
span = token[1:-1] if token.startswith("`") and token.endswith("`") else token.strip("'\"")
# A quoted identifier immediately following an explicit target category
# is concrete even when its spelling is a normal lowercase word (for
# example, ``function `validate` `` or ``key `timeout` ``).
if (
token.startswith("`")
and token.endswith("`")
and re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", span)
and not re.fullmatch(_TASK_CATEGORY, span, re.I)
):
return True
if _concrete_span(span):
return True
for match in re.finditer(r"`([^`]+)`", item):
Expand Down Expand Up @@ -158,7 +168,7 @@ def _headings(body: str) -> list[tuple[str, int, int]]:
out: list[tuple[str, int, int]] = []
fence: tuple[str, int] | None = None
for i, line in enumerate(body.splitlines()):
fence_match = re.match(r"\s{0,3}(`{3,}|~{3,})", line)
fence_match = re.match(r"\s*(`{3,}|~{3,})", line)
if fence_match:
marker = fence_match.group(1)
if fence is None:
Expand All @@ -167,7 +177,7 @@ def _headings(body: str) -> list[tuple[str, int, int]]:
marker[0] == fence[0]
and len(marker) >= fence[1]
and re.fullmatch(
rf"\s{{0,3}}(?:`{{{fence[1]},}}|~{{{fence[1]},}})\s*",
rf"\s*(?:`{{{fence[1]},}}|~{{{fence[1]},}})\s*",
line,
)
):
Expand Down Expand Up @@ -204,7 +214,7 @@ def _without_fenced_code(text: str) -> str:
kept: list[str] = []
fence: tuple[str, int] | None = None
for line in text.splitlines():
match = re.match(r"\s{0,3}(`{3,}|~{3,})", line)
match = re.match(r"\s*(`{3,}|~{3,})", line)
if match:
marker = match.group(1)
if fence is None:
Expand All @@ -213,7 +223,7 @@ def _without_fenced_code(text: str) -> str:
marker[0] == fence[0]
and len(marker) >= fence[1]
and re.fullmatch(
rf"\s{{0,3}}(?:`{{{fence[1]},}}|~{{{fence[1]},}})\s*",
rf"\s*(?:`{{{fence[1]},}}|~{{{fence[1]},}})\s*",
line,
)
):
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/agents-issue-format-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
github.event_name != 'issues' ||
github.event.action == 'opened' || github.event.action == 'edited' ||
github.event.action == 'reopened' ||
(github.event.action == 'unlabeled' &&
github.event.label.name == 'agents:format') ||
((github.event.action == 'labeled' || github.event.action == 'unlabeled') &&
(github.event.label.name == 'agents:auto-pilot-pause' ||
github.event.label.name == 'needs-human' ||
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/agents-issue-optimizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -744,3 +744,7 @@ jobs:
# A guard retry may proceed only after the failed format run releases its lease.
gh issue edit "$ISSUE_NUMBER" --remove-label "agents:format" \
|| echo "::warning::could not release failed agents:format lease"
# GITHUB_TOKEN label edits do not emit issues:unlabeled workflows; dispatch explicitly.
gh workflow run agents-issue-format-guard.yml --repo "$GITHUB_REPOSITORY" \
-f issue_number="$ISSUE_NUMBER" \
|| echo "::warning::could not dispatch format guard retry after lease release"
Comment thread
stranske marked this conversation as resolved.
Resolved
18 changes: 14 additions & 4 deletions templates/consumer-repo/.github/scripts/issue_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ def _task_has_concrete_target(item: str) -> bool:
for match in re.finditer(rf"\b{_TASK_CATEGORY}\s+(`[^`]+`|[^\s]+)", item, re.I):
token = match.group(1)
span = token[1:-1] if token.startswith("`") and token.endswith("`") else token.strip("'\"")
# A quoted identifier immediately following an explicit target category
# is concrete even when its spelling is a normal lowercase word (for
# example, ``function `validate` `` or ``key `timeout` ``).
if (
token.startswith("`")
and token.endswith("`")
and re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", span)
and not re.fullmatch(_TASK_CATEGORY, span, re.I)
):
return True
if _concrete_span(span):
return True
for match in re.finditer(r"`([^`]+)`", item):
Expand Down Expand Up @@ -158,7 +168,7 @@ def _headings(body: str) -> list[tuple[str, int, int]]:
out: list[tuple[str, int, int]] = []
fence: tuple[str, int] | None = None
for i, line in enumerate(body.splitlines()):
fence_match = re.match(r"\s{0,3}(`{3,}|~{3,})", line)
fence_match = re.match(r"\s*(`{3,}|~{3,})", line)
if fence_match:
marker = fence_match.group(1)
if fence is None:
Expand All @@ -167,7 +177,7 @@ def _headings(body: str) -> list[tuple[str, int, int]]:
marker[0] == fence[0]
and len(marker) >= fence[1]
and re.fullmatch(
rf"\s{{0,3}}(?:`{{{fence[1]},}}|~{{{fence[1]},}})\s*",
rf"\s*(?:`{{{fence[1]},}}|~{{{fence[1]},}})\s*",
line,
)
):
Expand Down Expand Up @@ -204,7 +214,7 @@ def _without_fenced_code(text: str) -> str:
kept: list[str] = []
fence: tuple[str, int] | None = None
for line in text.splitlines():
match = re.match(r"\s{0,3}(`{3,}|~{3,})", line)
match = re.match(r"\s*(`{3,}|~{3,})", line)
if match:
marker = match.group(1)
if fence is None:
Expand All @@ -213,7 +223,7 @@ def _without_fenced_code(text: str) -> str:
marker[0] == fence[0]
and len(marker) >= fence[1]
and re.fullmatch(
rf"\s{{0,3}}(?:`{{{fence[1]},}}|~{{{fence[1]},}})\s*",
rf"\s*(?:`{{{fence[1]},}}|~{{{fence[1]},}})\s*",
line,
)
):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
github.event_name != 'issues' ||
github.event.action == 'opened' || github.event.action == 'edited' ||
github.event.action == 'reopened' ||
(github.event.action == 'unlabeled' &&
github.event.label.name == 'agents:format') ||
((github.event.action == 'labeled' || github.event.action == 'unlabeled') &&
(github.event.label.name == 'agents:auto-pilot-pause' ||
github.event.label.name == 'needs-human' ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -597,3 +597,7 @@ jobs:
# A guard retry may proceed only after the failed format run releases its lease.
gh issue edit "$ISSUE_NUMBER" --remove-label "agents:format" \
|| echo "::warning::could not release failed agents:format lease"
# GITHUB_TOKEN label edits do not emit issues:unlabeled workflows; dispatch explicitly.
gh workflow run agents-issue-format-guard.yml --repo "$GITHUB_REPOSITORY" \
-f issue_number="$ISSUE_NUMBER" \
|| echo "::warning::could not dispatch format guard retry after lease release"
40 changes: 40 additions & 0 deletions tests/scripts/test_issue_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,46 @@ def test_fence_with_language_marker_does_not_close_a_code_block() -> None:
assert "Acceptance Criteria" in report.missing_required


@pytest.mark.parametrize(
"validator_path",
[
Path(".github/scripts/issue_format.py"),
Path("templates/consumer-repo/.github/scripts/issue_format.py"),
],
)
def test_list_indented_fenced_acceptance_command_is_ignored(validator_path: Path) -> None:
"""A four-space list fence is an example, not an observable gate."""
validator = _validator(validator_path)
report = validator.validate(
VALID_CONTEXT
+ "## Tasks\n- [ ] Update `src/client.py`\n\n"
+ "## Acceptance Criteria\n- Example command:\n\n ```sh\n pytest tests/test_x.py\n ```\n"
)
assert not report.ok
assert any("names no test" in problem for problem in report.problems)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

@pytest.mark.parametrize(
"validator_path",
[
Path(".github/scripts/issue_format.py"),
Path("templates/consumer-repo/.github/scripts/issue_format.py"),
],
)
@pytest.mark.parametrize("target", ["validate", "timeout"])
def test_explicit_category_accepts_quoted_lowercase_identifier(
validator_path: Path, target: str
) -> None:
validator = _validator(validator_path)
category = "function" if target == "validate" else "key"
report = validator.validate(
VALID_CONTEXT
+ f"## Tasks\n- [ ] Update {category} `{target}`\n\n"
+ "## Acceptance Criteria\n- pytest tests/test_x.py passes\n"
)
assert report.ok


@pytest.mark.parametrize("marker", ["```", "~~~"])
def test_fence_close_requires_marker_only_line(marker: str) -> None:
"""Trailing text after fence markers must not end the fenced region."""
Expand Down
25 changes: 25 additions & 0 deletions tests/workflows/test_agents_issue_optimizer_format_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,31 @@ def test_format_guard_deduplicates_inflight_optimizer_dispatch() -> None:
)


def test_format_guard_rechecks_after_format_lease_release() -> None:
for path in (GUARD_PATH, CONSUMER_GUARD_PATH):
text = path.read_text(encoding="utf-8")
condition = text[text.index("if: >-") : text.index("runs-on: ubuntu-latest")]
assert "github.event.action == 'unlabeled'" in condition
assert "github.event.label.name == 'agents:format'" in condition
# Applying agents:format already starts the optimizer via issues:labeled;
# the guard must react only when the lease is removed.
labeled_format = "github.event.action == 'labeled'" in condition and condition.index(
"github.event.action == 'labeled'"
) < condition.index("github.event.label.name == 'agents:format'")
assert not labeled_format


def test_format_optimizer_dispatches_guard_after_failed_lease_release() -> None:
for text in (
WORKFLOW_PATH.read_text(encoding="utf-8"),
CONSUMER_WORKFLOW_PATH.read_text(encoding="utf-8"),
):
release_idx = text.index("Release failed format lease")
release_block = text[release_idx : release_idx + 1200]
assert "gh workflow run agents-issue-format-guard.yml" in release_block
assert '-f issue_number="$ISSUE_NUMBER"' in release_block
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved


def test_format_guard_attempt_marker_sequence_consumes_retry_budget() -> None:
"""Exercise the marker contract used by the workflow's routing step."""
fingerprint = "abc123def456"
Expand Down
Loading