Skip to content
Merged
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
37 changes: 37 additions & 0 deletions planfile/sync/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from rich.console import Console

from planfile.sync.base import BasePMBackend, TicketRef, TicketState
from planfile.sync.state import normalize_repository

try:
from github import Github
Expand Down Expand Up @@ -669,6 +670,42 @@ def _issue_to_ticket_status(self, issue: Issue) -> TicketState:
metadata=metadata,
)

def list_selected_tickets(
self, ticket_ids: set[str], labels: list[str] | None = None,
) -> list[TicketState]:
"""Read only resolved issue numbers, retaining exact provider bindings."""
repository = normalize_repository(
getattr(self, "_verified_repository", None) or self.repo.full_name
)
numbers = set()
for reference in ticket_ids:
number = str(reference).strip()
if "#" in number:
prefix, number = number.rsplit("#", 1)
if prefix and normalize_repository(prefix) != repository:
continue
if (number.isascii() and number.isdecimal() and int(number) > 0
and str(int(number)) == number):
numbers.add(number)
tickets = []
for number in sorted(numbers, key=int):
ticket = self.get_ticket(number)
key_repository, separator, key_number = str(ticket.key or "").rpartition("#")
url_repository, url_separator, url_number = str(ticket.url or "").removeprefix(
"https://github.com/"
).rpartition("/issues/")
if (str(ticket.id) != number
or not separator or key_number != number
or normalize_repository(key_repository) != repository
or not str(ticket.url or "").startswith("https://github.com/")
or not url_separator or url_number != number
or normalize_repository(url_repository) != repository):
raise ValueError("sync_selected_provider_binding_mismatch")
if labels and not set(labels).issubset(ticket.labels):
continue
tickets.append(ticket)
return tickets

def _list_tickets(
self,
labels: list[str] | None = None,
Expand Down
29 changes: 20 additions & 9 deletions planfile/sync/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,11 +440,17 @@ def _fetch_external_tickets(
backend,
integration_name: str,
labels: list[str] | None = None,
selected_remote_ids: set[str] | None = None,
) -> list | None:
"""Fetch tickets; preserve provider failures for caller retry and cooldown."""
try:
kwargs = {"labels": labels} if labels else {}
external_tickets = backend.list_tickets(**kwargs)
selected_reader = getattr(backend, "list_selected_tickets", None)
if (integration_name == "github" and selected_remote_ids is not None
and callable(selected_reader)):
external_tickets = selected_reader(selected_remote_ids, **kwargs)
else:
external_tickets = backend.list_tickets(**kwargs)
if external_tickets is None:
console.print(f" [dim]ℹ️ No tickets found in {integration_name}[/dim]")
return None
Expand Down Expand Up @@ -634,20 +640,25 @@ def sync_from_external(
return
import_target = sections[target_id]
lookup_sections = {target_id: import_target}
external_tickets = _fetch_external_tickets(backend, integration_name, remote_labels)

if external_tickets is None:
return
if not external_tickets:
console.print(f" [dim]ℹ️ No tickets to import from {integration_name}[/dim]")
return

normalized_ticket_ids = (
{str(value).strip() for value in (ticket_ids or []) if str(value).strip()} or None
)
if normalized_ticket_ids:
for section in lookup_sections.values():
for local_id, ticket in (section.get("tickets") or {}).items():
if local_id in normalized_ticket_ids:
_validate_ticket_binding(ticket, integration_name, backend)
selected_remote_ids = _resolve_selected_remote_ids(
normalized_ticket_ids, lookup_sections, sync_state, integration_name
)
external_tickets = _fetch_external_tickets(
backend, integration_name, remote_labels, selected_remote_ids
)
if external_tickets is None:
return
if not external_tickets:
console.print(f" [dim]ℹ️ No tickets to import from {integration_name}[/dim]")
return

for ext_ticket in external_tickets:
imported_count, updated_count = _process_external_ticket(
Expand Down
2 changes: 2 additions & 0 deletions project/TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,6 @@ This file indexes governance tickets without taking ownership of
| **ticket-248** | [`README.md`](./ticket-248/README.md) | - | - | - | - | - |
| **ticket-249** | [`README.md`](./ticket-249/README.md) | - | - | - | - | - |
| **ticket-250** | [`README.md`](./ticket-250/README.md) | - | - | - | - | - |
| **ticket-252** | [`README.md`](./ticket-252/README.md) | - | - | - | - | - |
| **ticket-253** | [`README.md`](./ticket-253/README.md) | - | - | - | - | - |
<!-- AUTO:TICKET_INDEX:END -->
27 changes: 27 additions & 0 deletions project/ticket-253/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Ticket 253: Exact scoped GitHub inbound reads

- **ID**: ticket-253
- **Owner**: agent:codex
- **Status**: IN_PROGRESS
- **Workflow state**: PUBLICATION
- **Created**: 2026-10-28

## Goal and scope

SESSION_EXECUTION_AUTHORIZATION: continue user-requested Heyasa autonomy repair, tests and protected publication. Resume existing owner Planfile PLF-17304 / https://github.com/semcod/planfile/issues/256; no duplicate intake issue.

Read only canonical mapped GitHub issues before enumeration in selected inbound sync. Preserve repository identity, labels, CAS, active execution guards and existing unscoped/provider compatibility. This slice retains the remaining total-deadline/transport requirements of256.

## Acceptance criteria

- [x] AC-01: Selected mapped intake avoids unrelated enumeration, rejects invalid packets and preserves unrelated/concurrent work.
- [x] AC-02: Current scoped regression/compatibility tests and native governance pass.
- [ ] AC-03: Required current-head/base verification and independent protected publication succeed.

The original admission refusal is retained externally. Owner251 subsequently released its main lease at revision7/fence318; this session acquired a fresh main writer through the published controller. No ownership takeover or target/namespace substitution occurred.

The historical proposal8d7f716ee0e043e3f54fe306231e9fcdc8caafd066005f2c4625ef7f07722e88 was requalified on base4adc9a2:667affected tests including60selected-provider cases passed. Current base8e2b186 changes managed metadata only; functional inputs planfile/**, tests/**, pyproject.toml and uv.lock are byte-identical. These external fixture results grant no CI/review/deployment authority; fresh native validation remains required.

## Native validation

Actual admitted worktree:667tests across26affected test files passed with Testless coverage contexts in191.44s; Ruff passes; native governance passes with0errors/0warnings. One existing SQLite ResourceWarning is retained in the test log. Testless generated693external advisory suggestions; none were imported or published as tasks. No runtime dependency, required check or protected policy changed. AC-03 remains pending independent verification/publication.
80 changes: 80 additions & 0 deletions project/ticket-253/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-253",
"summary": "Read only mapped issues during scoped GitHub inbound sync",
"workstream": "application",
"classification": {
"kind": "BUG",
"priority": "P1",
"origin": "regression"
},
"allowedPaths": [
"project/ticket-253/**",
"TODO.md",
"project/TICKETS.md",
"planfile/sync/github.py",
"planfile/sync/operations.py",
"tests/test_sync_selected_provider_reads.py"
],
"forbiddenPaths": [
"project/ticket-*/user-*.md"
],
"stacks": [],
"dependsOn": [],
"conflictsWith": [],
"integrationTicket": null,
"delivery": {
"outcome": "Read only canonical mapped GitHub issues for selected inbound sync while retaining full/unmapped provider compatibility, repository identity, labels, CAS and execution guards.",
"targetBranch": "main",
"acceptedBaseSha": "8e2b18637514adac380018f5ffb840a87f3e05a8",
"complexity": "S",
"estimatedMinutes": 45,
"budgets": {
"maxImplementationFiles": 3,
"maxAffectedComponents": 2,
"maxPublicInterfaceChanges": 1,
"maxRuntimeDependencies": 0
},
"nonGoals": [
"No change to merge policy, lease ownership, provider credentials, full sync or runtime promotion.",
"This slice does not close every deadline/transport requirement in issue256."
],
"architecture": {
"status": "accepted",
"decision": "Resume existing qualified unapplied256 proposal against current source; resolve canonical selections before fetch and use a provider capability for exact issue reads.",
"responsibilityChanges": false,
"components": [
{
"name": "scoped-sync",
"paths": [
"planfile/sync/operations.py",
"planfile/sync/github.py",
"tests/test_sync_selected_provider_reads.py"
]
}
],
"interfaceChanges": [
"GitHubBackend.list_selected_tickets capability; existing CLI unchanged."
],
"dataChanges": [],
"ui": {
"impact": "none",
"states": [],
"evidence": []
},
"rollback": "Protected revert of the source change; retain authoritative ticket records and remote terminal state."
},
"runtimeDependencies": [],
"validation": [
{
"criterion": "AC-01",
"commands": [
"python -m pytest -q tests/test_sync_selected_provider_reads.py",
"python -m pytest -q tests/test_sync*.py",
"./project/governance-check.sh"
],
"evidence": "Baseline desired contract fails; candidate exact/unmapped/sprint/identity/label/PR/failure/CAS/live-owner controls pass before full required current-head verification and independent publication."
}
]
}
}
Loading
Loading