Skip to content
20 changes: 13 additions & 7 deletions src/agent_comms/commander/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,9 +664,9 @@ def deadline_lines(self, pending: list[Todo], read: dict[str, OrderProgress],
continue
answer = self.reply(t.holder, ordered)
due = f'due {hhmm(t.deadline)}, order {t.order.id}'
late.append(f'{t.holder} ({due}, {work.line}{answer})')
alarm = f'OVERDUE: {", ".join(late)} -> push or merge unfinished work now'
return [alarm] * bool(late)
late.append(f'OVERDUE: {t.id} {t.holder} ({due}, {work.line}{answer}) '
'-> push or merge unfinished work now')
return late

def install_lines(self, held: tuple[Todo, ...], read: dict[str, OrderProgress], now: float) -> list[str]:
"""One INSTALL line naming every held order that waits on the fleet's install."""
Expand Down Expand Up @@ -965,16 +965,22 @@ def run(self, ctx: Commander) -> None:

@dataclass(frozen=True, kw_only=True)
class PullRequestVerb(OrderVerb, declared_name="pr"):
help = "open the problem's PR with its required immutable order identity"
help = ("open the problem's PR from this owned source checkout "
"with its immutable order identity")
pid: str = option("pid")
title: str = option("--title")
body_file: str = option("--body-file")

def run(self, ctx: Commander) -> None:
todo = ctx.comms.goals.todos.get(self.pid)
checkout = ctx.comms.registry.storage_rule.order_checkout(todo.holder, todo.id)
print(todo.order.open_pull_request(
todo, checkout, self.title, Path(self.body_file).read_text()))
source = Repo.of(Path.cwd())
holder = todo.held().owner
if not ctx.comms.registry.storage_rule.owns_checkout(holder, source.checkout):
raise ValueError(f"Run order pr from a source checkout owned by {holder}.")
if source.github != todo.repo:
raise ValueError(f"This source checkout is not the problem repository {todo.repo}.")
print(todo.order.checked().open_pull_request(
todo, source.checkout, self.title, Path(self.body_file).read_text()))


@dataclass(frozen=True, kw_only=True)
Expand Down
28 changes: 17 additions & 11 deletions src/agent_comms/storage_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,27 @@ def initial_roots(self, name):
return StorageRoots(self.scratch_base / name)

def order_checkout(self, name: str, problem: str) -> Path:
"""The checkout thread ``name`` holds for one ordered problem."""
"""Allocation path for a new problem checkout; an existing source may be reused."""
return self.checkout_base.resolve() / f'{name}--{problem}'

def owns_checkout(self, thread: Thread, path: Path) -> bool:
problem = path.name.removeprefix(f'{thread.name}--')
return (re.fullmatch(r'P[1-9][0-9]*', problem) is not None
and path == self.order_checkout(thread.name, problem))
def owns_checkout(self, name: str, path: Path) -> bool:
problem = path.name.removeprefix(f'{name}--')
return (path == self.checkout_base.resolve() / name
or (bool(re.fullmatch(r'P[1-9][0-9]*', problem))
and path == self.order_checkout(name, problem)))

def checkout_paths(self, name: str) -> tuple[Path, ...]:
"""Candidate locations; Git worktree metadata supplies their branches."""
ordered = self.order_checkout(name, 'P*')
return tuple(path for path in (self.checkout_base.resolve() / name,
*sorted(ordered.parent.glob(ordered.name)))
if path.is_dir() and self.owns_checkout(name, path))

def checkouts_of(self, thread: Thread) -> tuple[Path, ...]:
"""Read each repository now; directories and names do not register ownership."""
home = self.checkout_base / thread.name
home = (home if home.exists() else Path(thread.worktree)).expanduser().resolve()
ordered = self.order_checkout(thread.name, 'P*')
candidates = (home, *(path for path in ordered.parent.glob(ordered.name)
if path.is_dir() and self.owns_checkout(thread, path)))
candidates = (home, *self.checkout_paths(thread.name))
checkouts = {home}
listed = set()
for repository in candidates:
Expand All @@ -118,7 +124,7 @@ def checkouts_of(self, thread: Thread) -> tuple[Path, ...]:
for field in output.split('\0') if field.startswith('worktree '))
listed.update(entries)
checkouts.update(path for path in entries
if path.is_dir() and self.owns_checkout(thread, path))
if path.is_dir() and self.owns_checkout(thread.name, path))
return (home, *sorted(checkouts - {home}))

def deny(self, reason):
Expand Down Expand Up @@ -193,7 +199,7 @@ def require_path(self, thread: Thread, path):
base = self.checkout_base.resolve()
if path.is_relative_to(base) and path != base:
checkout = base / path.relative_to(base).parts[0]
if not (checkout.name == thread.name or self.owns_checkout(thread, checkout)):
if not self.owns_checkout(thread.name, checkout):
self.deny(f'{checkout} is not a checkout of {thread.name}')

def render(self, roots):
Expand Down Expand Up @@ -320,7 +326,7 @@ class CheckoutStorageEffect(AllocateStorageEffect):

def check(self, rule, thread):
rule.require_path(thread, self.path)
if not rule.owns_checkout(thread, self.path):
if not rule.owns_checkout(thread.name, self.path):
destination = rule.order_checkout(thread.name, "P<number>")
rule.deny(f'checkout destination must be {destination}')
rule.require_allocation(thread, self.path)
Expand Down
Loading
Loading