Files
playbook/skills/cook-it-through/scripts/main_loop.py
T

2397 lines
85 KiB
Python

#!/usr/bin/env python3
import argparse
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import json
import os
import re
import shlex
import subprocess
import sys
import tempfile
import threading
import time
from pathlib import Path
from typing import Any, Iterator, Optional
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from main_loop_scheduler import ( # noqa: E402
Dependency,
FeatureId,
FeatureIntegrationId,
FeatureRecord,
Scheduler,
SchedulerError,
TicketId,
TicketRecord,
parse_dependencies,
)
try:
import fcntl
except ImportError: # pragma: no cover
fcntl = None
try:
import msvcrt
except ImportError: # pragma: no cover
msvcrt = None
QUEUE_START = "<!-- main-loop:queue:start -->"
QUEUE_END = "<!-- main-loop:queue:end -->"
TICKET_STATE_START = "<!-- main-loop:ticket-state:start -->"
TICKET_STATE_END = "<!-- main-loop:ticket-state:end -->"
FEATURE_STATE_FILENAME = ".main-loop.json"
CLAIM_STALE_AFTER_SECONDS = 30 * 60
TICKET_FILE_RE = re.compile(r"^(?P<number>\d{2,})-(?P<slug>[a-z0-9][a-z0-9-]*)\.md$")
TITLE_RE = re.compile(
r"^#\s+(?P<number>\d{2,})\s+[-\N{EN DASH}\N{EM DASH}]\s+(?P<title>\S.*)$",
re.MULTILINE,
)
STATUS_RE = re.compile(
r"^\*\*Status:\*\*[ \t]*(?P<status>[^\s\r\n]+)[ \t]*$",
re.MULTILINE,
)
BLOCKED_BY_RE = re.compile(
r"^\*\*Blocked by:\*\*[ \t]*(?P<blockers>[^\r\n]*?)[ \t]*$",
re.MULTILINE,
)
ACCEPTANCE_RE = re.compile(r"^-\s+\[[ xX]\]\s+\S", re.MULTILINE)
QUEUE_ENTRY_RE = re.compile(r"^-\s+`(?P<slug>[a-z0-9][a-z0-9-]*)`\s*$")
ALLOWED_STATUSES = {
"ready-for-agent",
"claimed",
"blocked",
"resolved",
"skipped",
}
SATISFIED_STATUSES = {"resolved", "skipped"}
THREAD_LOCKS: dict[str, threading.Lock] = {}
THREAD_LOCKS_GUARD = threading.Lock()
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
GIT_OBJECT_ID_RE = re.compile(r"^(?:[0-9a-f]{40}|[0-9a-f]{64})$")
class StateError(ValueError):
pass
@dataclass(frozen=True)
class Ticket:
id: TicketId
slug: str
title: str
dependencies: tuple[Dependency, ...]
status: str
path: Path
metadata: dict[str, Any]
@property
def number(self) -> str:
return self.id.number
@dataclass(frozen=True)
class Feature:
id: FeatureId
path: Path
tickets: dict[TicketId, Ticket]
@property
def slug(self) -> str:
return str(self.id)
@property
def partial(self) -> bool:
return any(ticket.status == "skipped" for ticket in self.tickets.values())
@dataclass(frozen=True)
class EvidenceArtifact:
data: dict[str, Any]
digest: str
source: Path
def _thread_lock(lock_path: Path) -> threading.Lock:
key = str(lock_path.resolve())
with THREAD_LOCKS_GUARD:
lock = THREAD_LOCKS.get(key)
if lock is None:
lock = threading.Lock()
THREAD_LOCKS[key] = lock
return lock
@contextmanager
def locked_state(state_root: Path) -> Iterator[None]:
state_root.mkdir(parents=True, exist_ok=True)
lock_path = state_root / ".main-loop.lock"
with _thread_lock(lock_path):
with lock_path.open("a+b") as lock_file:
if fcntl is not None:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
elif msvcrt is not None: # pragma: no cover
while True:
try:
lock_file.seek(0)
msvcrt.locking(lock_file.fileno(), msvcrt.LK_LOCK, 1)
break
except OSError:
time.sleep(0.05)
try:
yield
finally:
if fcntl is not None:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
elif msvcrt is not None: # pragma: no cover
lock_file.seek(0)
msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
def atomic_write_text(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fd, raw_temp_path = tempfile.mkstemp(
dir=path.parent, prefix=f".{path.name}.", suffix=".tmp"
)
temp_path = Path(raw_temp_path)
try:
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
handle.write(text)
handle.flush()
os.fsync(handle.fileno())
os.replace(temp_path, path)
finally:
if temp_path.exists():
temp_path.unlink()
def parse_blockers(raw: str, ticket_id: TicketId) -> tuple[Dependency, ...]:
return parse_dependencies(raw, ticket_id)
def parse_ticket_metadata(
text: str,
ticket_path: Path,
ticket_id: Optional[TicketId] = None,
) -> dict[str, Any]:
"""Read the scheduler-owned JSON block, if present.
The block is deliberately ordinary Markdown so a ticket remains useful when
opened without the scheduler. Malformed machine state is rejected instead
of silently losing ownership information.
"""
label = str(ticket_id) if ticket_id is not None else ticket_path.name
start_count = text.count(TICKET_STATE_START)
end_count = text.count(TICKET_STATE_END)
if start_count == 0 and end_count == 0:
return {}
if start_count > 1 or end_count > 1:
raise StateError(f"{label}: multiple ticket state blocks")
if start_count != 1 or end_count != 1:
raise StateError(f"{label}: malformed ticket state markers")
marker = re.compile(
rf"{re.escape(TICKET_STATE_START)}\s*\n(?P<body>.*?)\n"
rf"{re.escape(TICKET_STATE_END)}",
re.DOTALL,
)
match = marker.search(text)
if not match:
raise StateError(f"{label}: malformed ticket state")
body = match.group("body").strip()
if not body:
return {}
try:
value = json.loads(body)
except json.JSONDecodeError as exc:
raise StateError(f"{label}: malformed ticket state") from exc
if not isinstance(value, dict):
raise StateError(f"{label}: ticket state must be an object")
return value
def render_ticket_state(text: str, metadata: dict[str, Any]) -> str:
body = json.dumps(metadata, ensure_ascii=True, indent=2, sort_keys=True)
block = f"{TICKET_STATE_START}\n{body}\n{TICKET_STATE_END}"
marker = re.compile(
rf"{re.escape(TICKET_STATE_START)}\s*\n.*?\n"
rf"{re.escape(TICKET_STATE_END)}",
re.DOTALL,
)
if marker.search(text):
updated = marker.sub(lambda _match: block, text, count=1)
else:
separator = "" if not text or text.endswith("\n\n") else "\n"
if not text.endswith("\n"):
separator = "\n\n"
updated = f"{text}{separator}{block}\n"
return updated
def update_ticket_state(
ticket: Ticket,
*,
status: Optional[str] = None,
metadata: Optional[dict[str, Any]] = None,
) -> None:
if status is not None and status not in ALLOWED_STATUSES:
raise StateError(f"invalid status {status}")
text = ticket.path.read_text(encoding="utf-8")
if status is not None:
status_pattern = re.compile(r"(?m)^\*\*Status:\*\*\s*[^\r\n]*$")
if not status_pattern.search(text):
raise StateError(f"{ticket.id}: missing Status")
text = status_pattern.sub(f"**Status:** {status}", text, count=1)
if metadata is not None:
text = render_ticket_state(text, metadata)
atomic_write_text(ticket.path, text)
def utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace(
"+00:00", "Z"
)
def parse_timestamp(raw: str) -> datetime:
try:
value = datetime.fromisoformat(raw.replace("Z", "+00:00"))
except ValueError as exc:
raise StateError(f"invalid timestamp: {raw}") from exc
if value.tzinfo is None:
raise StateError(f"timestamp must include a timezone: {raw}")
return value.astimezone(timezone.utc)
def format_timestamp(value: datetime) -> str:
return value.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace(
"+00:00", "Z"
)
def claim_is_stale(ticket: Ticket, now: datetime) -> bool:
heartbeat = ticket.metadata.get("heartbeat_at")
if not heartbeat:
raise StateError(f"{ticket.id}: claimed ticket has no heartbeat")
return (
now - parse_timestamp(str(heartbeat))
).total_seconds() > CLAIM_STALE_AFTER_SECONDS
def git_run(repo_root: Path, *args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["git", *args],
cwd=repo_root,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
def git_output(repo_root: Path, *args: str) -> str:
result = git_run(repo_root, *args)
if result.returncode != 0:
detail = (result.stderr or result.stdout).strip()
raise StateError(f"git {' '.join(args)} failed: {detail}")
return result.stdout.strip()
def resolve_repo_root(repo_root: Path) -> Path:
candidate = repo_root.resolve()
if not candidate.exists():
raise StateError(f"repo root not found: {repo_root}")
raw = git_output(candidate, "rev-parse", "--show-toplevel")
return Path(raw).resolve()
def branch_exists(repo_root: Path, branch: str) -> bool:
return git_run(repo_root, "show-ref", "--verify", "--quiet", f"refs/heads/{branch}").returncode == 0
def current_branch(repo_root: Path) -> str:
return git_output(repo_root, "branch", "--show-current")
def worktree_branch_paths(repo_root: Path) -> dict[str, Path]:
output = git_output(repo_root, "worktree", "list", "--porcelain")
paths: dict[str, Path] = {}
current_path: Optional[Path] = None
for line in output.splitlines():
if line.startswith("worktree "):
current_path = Path(line[len("worktree ") :]).resolve()
elif line.startswith("branch refs/heads/") and current_path is not None:
branch = line[len("branch refs/heads/") :]
paths[branch] = current_path
return paths
def checkout_is_dirty(repo_root: Path) -> Optional[str]:
output = git_output(repo_root, "status", "--porcelain", "--untracked-files=all")
for line in output.splitlines():
if len(line) < 4:
continue
raw_path = line[3:].strip().strip('"')
normalized = raw_path.replace("\\", "/")
if normalized == ".scratch" or normalized.startswith(".scratch/"):
continue
return raw_path
return None
def ensure_git_repository(repo_root: Path) -> None:
result = git_run(repo_root, "rev-parse", "--git-dir")
if result.returncode != 0:
raise StateError(f"not a Git repository: {repo_root}")
def feature_state_path(feature: Feature) -> Path:
return feature.path / FEATURE_STATE_FILENAME
def load_feature_state(feature: Feature) -> dict[str, Any]:
path = feature_state_path(feature)
if not path.exists():
return {}
try:
value = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise StateError(
f"{FeatureIntegrationId(feature.id)}: malformed feature state"
) from exc
if not isinstance(value, dict):
raise StateError(
f"{FeatureIntegrationId(feature.id)}: feature state must be an object"
)
return value
def write_feature_state(feature: Feature, state: dict[str, Any]) -> None:
atomic_write_text(
feature_state_path(feature),
json.dumps(state, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
)
def parse_ticket(path: Path, feature_id: FeatureId) -> Ticket:
file_match = TICKET_FILE_RE.fullmatch(path.name)
if not file_match:
raise StateError(
f"{feature_id}: invalid ticket filename: {path.name}"
)
ticket_id = TicketId(feature_id, file_match.group("number"))
text = path.read_text(encoding="utf-8")
title_match = TITLE_RE.search(text)
if not title_match:
raise StateError(f"{ticket_id}: invalid title")
number = file_match.group("number")
if title_match.group("number") != number:
raise StateError(f"{ticket_id}: title number does not match filename")
status_matches = tuple(STATUS_RE.finditer(text))
if not status_matches:
raise StateError(f"{ticket_id}: missing Status")
if len(status_matches) != 1:
raise StateError(f"{ticket_id}: multiple Status fields")
status_match = status_matches[0]
status = status_match.group("status")
if status not in ALLOWED_STATUSES:
raise StateError(f"{ticket_id}: invalid status {status}")
blocked_by_matches = tuple(BLOCKED_BY_RE.finditer(text))
if not blocked_by_matches:
raise StateError(f"{ticket_id}: missing Blocked by")
if len(blocked_by_matches) != 1:
raise StateError(f"{ticket_id}: multiple Blocked by fields")
blocked_by_match = blocked_by_matches[0]
if not ACCEPTANCE_RE.search(text):
raise StateError(f"{ticket_id}: missing acceptance criterion")
metadata = parse_ticket_metadata(text, path, ticket_id)
return Ticket(
id=ticket_id,
slug=file_match.group("slug"),
title=title_match.group("title").strip(),
dependencies=parse_blockers(
blocked_by_match.group("blockers"),
ticket_id,
),
status=status,
path=path,
metadata=metadata,
)
def load_feature(state_root: Path, feature_id: FeatureId) -> Feature:
feature_path = state_root / str(feature_id)
if not (feature_path / "spec.md").is_file():
raise StateError(f"{feature_id}: spec.md not found")
issues_path = feature_path / "issues"
if not issues_path.is_dir():
raise StateError(f"{feature_id}: issues directory not found")
ticket_paths = sorted(
(path for path in issues_path.iterdir() if path.is_file()),
key=lambda path: path.name,
)
if not ticket_paths:
raise StateError(f"{feature_id}: no ticket files found")
tickets: dict[TicketId, Ticket] = {}
for path in ticket_paths:
ticket = parse_ticket(path, feature_id)
if ticket.id in tickets:
raise StateError(f"{feature_id}: duplicate ticket identity {ticket.id}")
tickets[ticket.id] = ticket
return Feature(id=feature_id, path=feature_path, tickets=tickets)
def load_queue(queue_path: Path) -> list[FeatureId]:
if not queue_path.exists():
return []
lines = queue_path.read_text(encoding="utf-8").splitlines()
if lines.count(QUEUE_START) != 1 or lines.count(QUEUE_END) != 1:
raise StateError("queue.md has an invalid managed block")
try:
start = lines.index(QUEUE_START)
end = lines.index(QUEUE_END, start + 1)
except ValueError as exc:
raise StateError("queue.md has an invalid managed block") from exc
feature_ids: list[FeatureId] = []
for line in lines[start + 1 : end]:
if not line.strip():
continue
match = QUEUE_ENTRY_RE.fullmatch(line.strip())
if not match:
raise StateError(f"queue.md has an invalid entry: {line.strip()}")
feature_id = FeatureId.parse(match.group("slug"))
if feature_id in feature_ids:
raise StateError(f"queue.md has a duplicate feature: {feature_id}")
feature_ids.append(feature_id)
return feature_ids
def render_queue(feature_ids: list[FeatureId]) -> str:
entries = [f"- `{feature_id}`" for feature_id in feature_ids]
return "\n".join(
["# Feature Queue", "", QUEUE_START, "", *entries, "", QUEUE_END, ""]
)
@dataclass(frozen=True)
class QueueSnapshot:
feature_ids: tuple[FeatureId, ...]
features: dict[FeatureId, Feature]
feature_states: dict[FeatureId, dict[str, Any]]
scheduler: Scheduler
def feature(self, feature_id: FeatureId) -> Feature:
feature = self.features.get(feature_id)
if feature is None:
raise StateError(f"feature is not queued: {feature_id}")
return feature
def ticket(self, ticket_id: TicketId) -> Ticket:
feature = self.feature(ticket_id.feature)
ticket = feature.tickets.get(ticket_id)
if ticket is None:
raise StateError(f"ticket not found: {ticket_id}")
return ticket
def validated_integration_commit(
feature_id: FeatureId,
state: dict[str, Any],
) -> Optional[str]:
value = state.get("integration_commit")
if value is None:
return None
if (
not isinstance(value, str)
or not GIT_OBJECT_ID_RE.fullmatch(value.strip())
):
raise StateError(
f"{FeatureIntegrationId(feature_id)}: invalid integration_commit"
)
return value.strip()
def load_queue_snapshot(
state_root: Path,
feature_ids: Optional[list[FeatureId]] = None,
) -> QueueSnapshot:
ordered_ids = tuple(
feature_ids
if feature_ids is not None
else load_queue(state_root / "queue.md")
)
features: dict[FeatureId, Feature] = {}
states: dict[FeatureId, dict[str, Any]] = {}
records: list[FeatureRecord] = []
for feature_id in ordered_ids:
feature = load_feature(state_root, feature_id)
state = load_feature_state(feature)
features[feature_id] = feature
states[feature_id] = state
tickets = tuple(
TicketRecord(
id=ticket.id,
slug=ticket.slug,
status=ticket.status,
dependencies=ticket.dependencies,
)
for ticket in sorted(
feature.tickets.values(),
key=lambda item: (int(item.number), item.number, item.slug),
)
)
records.append(
FeatureRecord(
id=feature_id,
tickets=tickets,
integrated=validated_integration_commit(feature_id, state) is not None,
integration_blocked=bool(state.get("integration_blocked_reason")),
)
)
return QueueSnapshot(
feature_ids=ordered_ids,
features=features,
feature_states=states,
scheduler=Scheduler(tuple(records)),
)
def enqueue_features(
state_root: Path,
requested_ids: tuple[FeatureId, ...],
) -> str:
if len(requested_ids) != len(set(requested_ids)):
duplicate = next(
feature_id
for feature_id in requested_ids
if requested_ids.count(feature_id) > 1
)
raise StateError(f"duplicate enqueue feature: {duplicate}")
queue_path = state_root / "queue.md"
with locked_state(state_root):
feature_ids = load_queue(queue_path)
existing = set(feature_ids)
candidate_ids = list(feature_ids)
output: list[str] = []
for feature_id in requested_ids:
if feature_id in existing:
output.append(f"EXISTS={feature_id}")
continue
candidate_ids.append(feature_id)
output.append(f"ENQUEUED={feature_id}")
load_queue_snapshot(state_root, candidate_ids)
if candidate_ids != feature_ids:
atomic_write_text(queue_path, render_queue(candidate_ids))
return "\n".join(output)
def status_report(
state_root: Path,
now: Optional[datetime] = None,
) -> str:
observed_at = now or datetime.now(timezone.utc)
with locked_state(state_root):
snapshot = load_queue_snapshot(state_root)
if not snapshot.feature_ids:
return "NO FEATURES"
scheduler = snapshot.scheduler
integration_frontier = scheduler.integration_frontier
output: list[str] = []
if integration_frontier is None:
output.append("INTEGRATION_FRONTIER=- STATE=complete")
else:
frontier_state = snapshot.feature_states[integration_frontier.feature]
if frontier_state.get("integration_blocked_reason"):
integration_state = "blocked"
elif scheduler.integration_frontier_ready:
integration_state = "ready"
else:
integration_state = "waiting"
waiting_on = scheduler.unsatisfied_dependencies(integration_frontier)
output.append(
f"INTEGRATION_FRONTIER={integration_frontier} "
f"STATE={integration_state} "
f"WAITING_ON={';'.join(map(str, waiting_on)) or '-'}"
)
global_frontier = set(scheduler.ticket_frontier)
for feature_id in snapshot.feature_ids:
feature = snapshot.feature(feature_id)
feature_metadata = snapshot.feature_states[feature_id]
feature_status = scheduler.feature_state(feature_id)
output.append(
f"FEATURE={feature_id} STATE={feature_status} "
f"PARTIAL={'yes' if feature.partial else 'no'}"
)
frontier = ",".join(
str(ticket.id)
for ticket in sorted(
feature.tickets.values(),
key=lambda item: (int(item.number), item.number, item.slug),
)
if ticket.id in global_frontier
)
output.append(f"TICKET_FRONTIER={frontier or '-'}")
integration_commit = validated_integration_commit(
feature_id,
feature_metadata,
)
if integration_commit:
output.append(
f"MAIN_INTEGRATION_COMMIT={integration_commit}"
)
elif feature_metadata.get("integration_blocked_reason"):
output.append(
f"FEATURE_BLOCKED={feature_id} "
f"REASON={feature_metadata['integration_blocked_reason']}"
)
for ticket in sorted(
feature.tickets.values(),
key=lambda item: (int(item.number), item.number, item.slug),
):
if ticket.status == "claimed":
stale = claim_is_stale(ticket, observed_at)
output.append(
f"CLAIM={ticket.id} "
f"OWNER={ticket.metadata.get('claimed_by')} "
f"HEARTBEAT={ticket.metadata.get('heartbeat_at')} "
f"STALE={'yes' if stale else 'no'} "
f"ISOLATION={ticket.metadata.get('isolation')} "
f"WORKSPACE={ticket.metadata.get('workspace')}"
)
if ticket.metadata.get("last_error"):
output.append(
f"TICKET_ERROR={ticket.id} "
f"REASON={ticket.metadata['last_error']}"
)
elif ticket.status == "blocked":
output.append(
f"BLOCKED={ticket.id} "
f"REASON={ticket.metadata.get('blocked_reason', 'unspecified')}"
)
elif ticket.status == "ready-for-agent":
waiting_on = scheduler.unsatisfied_dependencies(ticket.id)
if waiting_on:
output.append(
f"BLOCKED_DEPENDENCY={ticket.id} "
f"WAITING_ON={';'.join(map(str, waiting_on))}"
)
return "\n".join(output)
def render_claim_context(feature: Feature, ticket: Ticket, metadata: dict[str, Any]) -> str:
values = [
("FEATURE", feature.slug),
("TICKET", str(ticket.id)),
("CONTROL_ROOT", str(metadata["control_root"])),
("STATE_ROOT", str(metadata["state_root"])),
("WORKSPACE", str(metadata["workspace"])),
("BRANCH", str(metadata["ticket_branch"])),
("BASE", str(metadata["base_commit"])),
("ISOLATION", str(metadata["isolation"])),
]
return "\n".join(f"{key}={value}" for key, value in values)
def active_claims(features: list[Feature]) -> list[tuple[Feature, Ticket]]:
claims: list[tuple[Feature, Ticket]] = []
for feature in features:
for ticket in feature.tickets.values():
if ticket.status == "claimed":
if not ticket.metadata.get("claimed_by"):
raise StateError(f"{ticket.id}: claimed ticket has no owner")
claims.append((feature, ticket))
return claims
def integration_visibility_retry(
snapshot: QueueSnapshot,
repo_root: Path,
feature: Feature,
ticket: Ticket,
main_branch: str,
) -> Optional[str]:
"""Return an actionable retry when an integrated dependency is not visible.
A feature branch that predates ``main`` must be synchronized explicitly. We
do not merge behind an agent's back because that would invalidate its claim
base and could overwrite uncommitted work.
"""
integration_dependencies = snapshot.scheduler.integration_dependencies(ticket.id)
if not integration_dependencies:
return None
if not branch_exists(repo_root, main_branch):
raise StateError(f"main branch not found: {main_branch}")
main_head = git_output(repo_root, "rev-parse", main_branch)
feature_state = snapshot.feature_states[feature.id]
feature_branch = str(
feature_state.get("feature_branch", f"feature/{feature.id}")
)
feature_branch_exists = branch_exists(repo_root, feature_branch)
branch_head = (
git_output(repo_root, "rev-parse", feature_branch)
if feature_branch_exists
else "<not-created>"
)
expected_ticket_branch = f"ticket/{feature.slug}/{ticket.number}-{ticket.slug}"
ticket_branch = str(
ticket.metadata.get("ticket_branch", expected_ticket_branch)
)
if ticket_branch != expected_ticket_branch:
raise StateError(
f"{ticket.id}: claim branch mismatch: expected "
f"{expected_ticket_branch}, got {ticket_branch}"
)
ticket_branch_exists = branch_exists(repo_root, ticket_branch)
ticket_branch_head = (
git_output(repo_root, "rev-parse", ticket_branch)
if ticket_branch_exists
else "<not-created>"
)
branch_workspaces = worktree_branch_paths(repo_root)
if ticket_branch_exists:
configured_workspace = Path(
str(
ticket.metadata.get(
"workspace",
snapshot.features[feature.id].path.parent
/ "worktrees"
/ feature.slug
/ f"{ticket.number}-{ticket.slug}",
)
)
).resolve()
sync_branch = ticket_branch
occupied_workspace = branch_workspaces.get(ticket_branch)
else:
configured_workspace = Path(
str(
feature_state.get(
"integration_workspace",
snapshot.features[feature.id].path.parent
/ "worktrees"
/ "_integration"
/ str(feature.id),
)
)
).resolve()
sync_branch = feature_branch
occupied_workspace = branch_workspaces.get(feature_branch)
workspace = occupied_workspace or configured_workspace
if occupied_workspace is None:
prepare_workspace = " && ".join(
(
shlex.join(("mkdir", "-p", str(workspace.parent))),
shlex.join(
(
"git",
"-C",
str(repo_root.resolve()),
"worktree",
"add",
str(workspace),
sync_branch,
)
),
)
)
else:
prepare_workspace = ""
merge_main = shlex.join(
("git", "-C", str(workspace), "merge", main_branch)
)
sync_command = (
f"{prepare_workspace} && {merge_main}"
if prepare_workspace
else merge_main
)
for dependency in integration_dependencies:
dependency_state = snapshot.feature_states[dependency.feature]
integration_commit = validated_integration_commit(
dependency.feature,
dependency_state,
)
if integration_commit is None:
# The graph should already have filtered this out; keep the error
# explicit if a caller supplies an inconsistent snapshot.
raise StateError(f"{dependency}: integration commit is missing")
integration_commit = normalize_commit(
repo_root,
integration_commit,
str(dependency),
)
if ticket_branch_exists:
visible_head = ticket_branch_head
elif feature_branch_exists:
visible_head = branch_head
else:
visible_head = main_head
visible = is_ancestor(
repo_root,
integration_commit,
main_head,
) and is_ancestor(repo_root, integration_commit, visible_head)
if visible:
continue
return "\n".join(
[
"RETRY: dependency integration is not visible",
f"TICKET={ticket.id}",
f"DEPENDENCY={dependency}",
f"INTEGRATION_COMMIT={integration_commit}",
f"WORKSPACE={workspace}",
f"BRANCH={feature_branch}",
f"BRANCH_HEAD={branch_head}",
f"TICKET_BRANCH={ticket_branch}",
f"TICKET_BRANCH_HEAD={ticket_branch_head}",
f"SYNC_BRANCH={sync_branch}",
f"MAIN_BRANCH={main_branch}",
f"MAIN_HEAD={main_head}",
f"SYNC_COMMAND={sync_command}",
]
)
return None
def checkout_branch(repo_root: Path, branch: str) -> None:
occupied = worktree_branch_paths(repo_root).get(branch)
if occupied is not None and os.path.normcase(str(occupied)) != os.path.normcase(
str(repo_root.resolve())
):
raise StateError(f"branch {branch} is checked out at {occupied}")
result = git_run(repo_root, "checkout", branch)
if result.returncode != 0:
detail = (result.stderr or result.stdout).strip()
raise StateError(f"cannot checkout {branch}: {detail}")
def prepare_in_place_claim(
repo_root: Path,
feature: Feature,
ticket: Ticket,
main_branch: str,
prior_metadata: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
ensure_git_repository(repo_root)
branch = current_branch(repo_root)
if not branch:
raise StateError("in-place claim requires an attached HEAD")
feature_branch = f"feature/{feature.slug}"
ticket_branch = f"ticket/{feature.slug}/{ticket.number}-{ticket.slug}"
prior_workspace = prior_metadata.get("workspace") if prior_metadata else None
restores_released_workspace = bool(
prior_metadata
and prior_metadata.get("isolation") == "in-place"
and prior_workspace
and Path(str(prior_workspace)).resolve() == repo_root.resolve()
and prior_metadata.get("ticket_branch") == ticket_branch
and branch == ticket_branch
)
dirty_path = checkout_is_dirty(repo_root)
if dirty_path and not restores_released_workspace:
raise StateError(f"in-place checkout is dirty: {dirty_path}")
if not branch_exists(repo_root, main_branch):
raise StateError(f"main branch not found: {main_branch}")
if not branch_exists(repo_root, feature_branch):
git_output(repo_root, "branch", feature_branch, main_branch)
feature_head = git_output(repo_root, "rev-parse", feature_branch)
if not branch_exists(repo_root, ticket_branch):
git_output(repo_root, "branch", ticket_branch, feature_branch)
if branch != ticket_branch:
checkout_branch(repo_root, ticket_branch)
control_root = repo_root.resolve()
return {
"isolation": "in-place",
"control_root": str(control_root),
"workspace": str(control_root),
"feature_branch": feature_branch,
"ticket_branch": ticket_branch,
"base_commit": feature_head,
}
def add_worktree(
repo_root: Path,
workspace: Path,
branch: str,
start_point: str,
) -> None:
registered = worktree_branch_paths(repo_root).get(branch)
if registered is not None:
if os.path.normcase(str(registered)) == os.path.normcase(str(workspace.resolve())):
if not workspace.is_dir():
raise StateError(f"registered worktree is missing: {workspace}")
return
raise StateError(f"branch {branch} is checked out at {registered}")
if workspace.exists():
if any(workspace.iterdir()):
raise StateError(f"worktree path is not empty: {workspace}")
else:
workspace.parent.mkdir(parents=True, exist_ok=True)
if branch_exists(repo_root, branch):
command = ("worktree", "add", str(workspace), branch)
else:
command = ("worktree", "add", "-b", branch, str(workspace), start_point)
result = git_run(repo_root, *command)
if result.returncode != 0:
detail = (result.stderr or result.stdout).strip()
raise StateError(f"cannot create worktree for {branch}: {detail}")
def prepare_worktree_claim(
repo_root: Path,
state_root: Path,
feature: Feature,
ticket: Ticket,
main_branch: str,
prior_metadata: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
ensure_git_repository(repo_root)
if not branch_exists(repo_root, main_branch):
raise StateError(f"main branch not found: {main_branch}")
feature_branch = f"feature/{feature.slug}"
ticket_branch = f"ticket/{feature.slug}/{ticket.number}-{ticket.slug}"
if not branch_exists(repo_root, feature_branch):
git_output(repo_root, "branch", feature_branch, main_branch)
worktree_root = state_root / "worktrees"
feature_state = load_feature_state(feature)
integration_workspace = Path(
str(
feature_state.get(
"integration_workspace",
worktree_root / "_integration" / feature.slug,
)
)
).resolve()
occupied = worktree_branch_paths(repo_root).get(feature_branch)
if occupied is not None and os.path.normcase(str(occupied)) == os.path.normcase(
str(repo_root.resolve())
):
dirty_path = checkout_is_dirty(repo_root)
if dirty_path:
raise StateError(
f"cannot free feature branch from dirty control checkout: {dirty_path}"
)
checkout_branch(repo_root, main_branch)
add_worktree(repo_root, integration_workspace, feature_branch, main_branch)
prior_workspace = prior_metadata.get("workspace") if prior_metadata else None
ticket_workspace = Path(
str(
prior_workspace
or worktree_root / feature.slug / f"{ticket.number}-{ticket.slug}"
)
).resolve()
feature_head = git_output(repo_root, "rev-parse", feature_branch)
add_worktree(
repo_root,
ticket_workspace,
ticket_branch,
feature_branch,
)
feature_state.update(
{
"feature_branch": feature_branch,
"integration_workspace": str(integration_workspace),
}
)
write_feature_state(feature, feature_state)
return {
"isolation": "worktree",
"control_root": str(repo_root.resolve()),
"workspace": str(ticket_workspace),
"feature_branch": feature_branch,
"ticket_branch": ticket_branch,
"base_commit": feature_head,
"integration_workspace": str(integration_workspace),
}
def resume_claim(
repo_root: Path,
feature: Feature,
ticket: Ticket,
owner: str,
) -> str:
metadata = dict(ticket.metadata)
isolation = metadata.get("isolation")
workspace = Path(str(metadata.get("workspace", ""))).resolve()
if isolation == "in-place":
if workspace != repo_root.resolve():
raise StateError(
f"claim workspace mismatch: expected {workspace}, got {repo_root.resolve()}"
)
branch = current_branch(repo_root)
if not branch:
raise StateError("in-place claim requires an attached HEAD")
ticket_branch = str(metadata.get("ticket_branch", ""))
if branch != ticket_branch:
dirty_path = checkout_is_dirty(repo_root)
if dirty_path:
raise StateError(f"in-place checkout is dirty: {dirty_path}")
checkout_branch(repo_root, ticket_branch)
elif isolation == "worktree":
if not workspace.is_dir():
raise StateError(f"claim workspace not found: {workspace}")
ticket_branch = str(metadata.get("ticket_branch", ""))
if current_branch(workspace) != ticket_branch:
raise StateError(f"claim workspace is not on {ticket_branch}: {workspace}")
else:
raise StateError(f"{ticket.id}: invalid claim isolation")
metadata["heartbeat_at"] = utc_now()
update_ticket_state(ticket, metadata=metadata)
return render_claim_context(feature, ticket, metadata)
def claim_ticket(
state_root: Path,
repo_root: Path,
owner: str,
isolation: str,
main_branch: str,
) -> str:
if not owner.strip():
raise StateError("owner must not be empty")
state_root = state_root.resolve()
repo_root = resolve_repo_root(repo_root)
with locked_state(state_root):
snapshot = load_queue_snapshot(state_root)
if not snapshot.feature_ids:
return "NO FEATURES"
features = [
snapshot.feature(feature_id) for feature_id in snapshot.feature_ids
]
claims = active_claims(features)
for feature, ticket in claims:
if ticket.metadata.get("claimed_by") == owner:
return resume_claim(repo_root, feature, ticket, owner)
if claims and isolation == "in-place":
return "BUSY"
if any(ticket.metadata.get("isolation") == "in-place" for _, ticket in claims):
return "BUSY"
frontier = snapshot.scheduler.ticket_frontier
if not frontier:
integration_frontier = snapshot.scheduler.integration_frontier
if (
integration_frontier is not None
and snapshot.scheduler.integration_frontier_ready
):
return f"INTEGRATION_REQUIRED={integration_frontier}"
if claims:
return "BUSY"
return "NOOP: no claimable tickets"
selected_id = frontier[0]
feature = snapshot.feature(selected_id.feature)
ticket = snapshot.ticket(selected_id)
visibility_retry = integration_visibility_retry(
snapshot,
repo_root,
feature,
ticket,
main_branch,
)
if visibility_retry is not None:
return visibility_retry
selected_isolation = isolation
if isolation == "auto":
selected_isolation = "in-place" if not claims else "worktree"
try:
if selected_isolation == "in-place":
execution = prepare_in_place_claim(
repo_root, feature, ticket, main_branch, ticket.metadata
)
else:
execution = prepare_worktree_claim(
repo_root,
state_root,
feature,
ticket,
main_branch,
ticket.metadata,
)
except (OSError, StateError) as exc:
now = utc_now()
reason = f"claim preparation failed: {exc}"
metadata = dict(ticket.metadata)
append_history(
metadata,
"claim-blocked",
now,
owner=owner,
reason=reason,
)
metadata.update(
{
"claimed_by": owner,
"claimed_at": now,
"blocked_at": now,
"blocked_reason": reason,
"requested_isolation": selected_isolation,
}
)
metadata.pop("heartbeat_at", None)
update_ticket_state(ticket, status="blocked", metadata=metadata)
raise StateError(reason) from exc
now = utc_now()
metadata = dict(ticket.metadata)
execution["state_root"] = str(state_root)
metadata.update(execution)
metadata.update(
{
"claimed_by": owner,
"claimed_at": now,
"heartbeat_at": now,
}
)
update_ticket_state(ticket, status="claimed", metadata=metadata)
return render_claim_context(feature, ticket, metadata)
def canonical_json_bytes(value: dict[str, Any]) -> bytes:
return (
json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
+ "\n"
).encode("utf-8")
def artifact_string(data: dict[str, Any], key: str, label: str) -> str:
value = data.get(key)
if not isinstance(value, str) or not value.strip():
raise StateError(f"{label} evidence artifact field {key} must be a non-empty string")
return value.strip()
def artifact_pass(data: dict[str, Any], key: str, label: str) -> None:
value = artifact_string(data, key, label)
if value != "pass":
raise StateError(f"{label} evidence artifact field {key} must equal pass")
def load_evidence_artifact(
raw_path: str,
label: str,
expected_kind: str,
) -> EvidenceArtifact:
if not raw_path.strip():
raise StateError(f"{label} evidence artifact is required")
source = Path(raw_path).expanduser().resolve()
if not source.is_file():
raise StateError(f"{label} evidence artifact not found: {source}")
try:
data = json.loads(source.read_text(encoding="utf-8"))
except (UnicodeError, json.JSONDecodeError) as exc:
raise StateError(f"{label} evidence artifact must be valid UTF-8 JSON") from exc
if not isinstance(data, dict):
raise StateError(f"{label} evidence artifact must be a JSON object")
if data.get("version") != 1:
raise StateError(f"{label} evidence artifact version must equal 1")
if data.get("kind") != expected_kind:
raise StateError(
f"{label} evidence artifact kind must equal {expected_kind}"
)
artifact_string(data, "commit", label)
if expected_kind == "verification":
artifact_pass(data, "result", label)
artifact_string(data, "command", label)
if data.get("exit_code") != 0 or isinstance(data.get("exit_code"), bool):
raise StateError(f"{label} evidence artifact exit_code must equal 0")
output = artifact_string(data, "output", label)
output_sha256 = artifact_string(data, "output_sha256", label)
if not SHA256_RE.fullmatch(output_sha256):
raise StateError(
f"{label} evidence artifact output_sha256 must be a SHA-256 digest"
)
if hashlib.sha256(output.encode("utf-8")).hexdigest() != output_sha256:
raise StateError(f"{label} evidence artifact output_sha256 mismatch")
else:
artifact_string(data, "base", label)
artifact_pass(data, "standards", label)
artifact_pass(data, "spec", label)
report = artifact_string(data, "report", label)
report_sha256 = artifact_string(data, "report_sha256", label)
if not SHA256_RE.fullmatch(report_sha256):
raise StateError(
f"{label} evidence artifact report_sha256 must be a SHA-256 digest"
)
if hashlib.sha256(report.encode("utf-8")).hexdigest() != report_sha256:
raise StateError(f"{label} evidence artifact report_sha256 mismatch")
canonical = canonical_json_bytes(data)
return EvidenceArtifact(
data=data,
digest=hashlib.sha256(canonical).hexdigest(),
source=source,
)
def snapshot_evidence(
feature: Feature,
purpose: str,
artifact: EvidenceArtifact,
) -> dict[str, str]:
filename = f"{purpose}-{artifact.digest[:16]}.json"
relative_path = Path("evidence") / filename
atomic_write_text(
feature.path / relative_path,
json.dumps(
artifact.data,
ensure_ascii=True,
indent=2,
sort_keys=True,
)
+ "\n",
)
return {
"path": relative_path.as_posix(),
"sha256": artifact.digest,
}
def require_review_passed(reviewed: str) -> tuple[str, str, EvidenceArtifact]:
artifact = load_evidence_artifact(reviewed, "review", "review")
return (
artifact_string(artifact.data, "commit", "review"),
artifact_string(artifact.data, "base", "review"),
artifact,
)
def require_verification_passed(
evidence: str, label: str
) -> tuple[str, EvidenceArtifact]:
artifact = load_evidence_artifact(evidence, label, "verification")
return artifact_string(artifact.data, "commit", label), artifact
def normalize_commit(repo_root: Path, commit: str, label: str) -> str:
result = git_run(repo_root, "cat-file", "-e", f"{commit}^{{commit}}")
if result.returncode != 0:
raise StateError(f"{label} commit not found: {commit}")
return git_output(repo_root, "rev-parse", f"{commit}^{{commit}}")
def is_ancestor(repo_root: Path, ancestor: str, descendant: str) -> bool:
return git_run(repo_root, "merge-base", "--is-ancestor", ancestor, descendant).returncode == 0
def find_no_ff_merge(
repo_root: Path,
head: str,
merged_head: str,
label: str,
) -> tuple[str, str]:
merges = git_run(repo_root, "rev-list", "--first-parent", "--merges", head)
if merges.returncode != 0:
detail = (merges.stderr or merges.stdout).strip()
raise StateError(f"cannot inspect {label} integration history: {detail}")
for merge_commit in merges.stdout.splitlines():
parents = git_output(
repo_root,
"rev-list",
"--parents",
"-n",
"1",
merge_commit,
).split()
if len(parents) == 3 and parents[2] == merged_head:
return merge_commit, parents[1]
raise StateError(f"cannot recover {label} integration merge")
def append_history(
metadata: dict[str, Any], event: str, at: str, **details: str
) -> None:
raw_history = metadata.get("history", [])
history = list(raw_history) if isinstance(raw_history, list) else []
history.append({"event": event, "at": at, **details})
metadata["history"] = history
def heartbeat_ticket(
state_root: Path,
ticket_id: TicketId,
owner: str,
at: datetime,
) -> str:
state_root = state_root.resolve()
with locked_state(state_root):
snapshot = load_queue_snapshot(state_root)
ticket = snapshot.ticket(ticket_id)
if ticket.status != "claimed":
raise StateError(f"ticket {ticket_id} is not claimed")
if ticket.metadata.get("claimed_by") != owner:
raise StateError(f"ticket {ticket_id} is owned by another session")
metadata = dict(ticket.metadata)
timestamp = format_timestamp(at)
metadata["heartbeat_at"] = timestamp
update_ticket_state(ticket, metadata=metadata)
return f"HEARTBEAT={ticket_id}\nAT={timestamp}"
def validate_reclaim_workspace(repo_root: Path, ticket: Ticket) -> None:
metadata = ticket.metadata
workspace = Path(str(metadata.get("workspace", ""))).resolve()
if not workspace.is_dir():
raise StateError(f"claim workspace not found: {workspace}")
isolation = metadata.get("isolation")
if isolation == "in-place" and workspace != repo_root.resolve():
raise StateError(
f"claim workspace mismatch: expected {workspace}, got {repo_root.resolve()}"
)
ticket_branch = str(metadata.get("ticket_branch", ""))
if current_branch(workspace) != ticket_branch:
raise StateError(f"claim workspace is not on {ticket_branch}: {workspace}")
def reclaim_ticket(
state_root: Path,
repo_root: Path,
ticket_id: TicketId,
owner: str,
at: datetime,
) -> str:
state_root = state_root.resolve()
repo_root = resolve_repo_root(repo_root)
with locked_state(state_root):
snapshot = load_queue_snapshot(state_root)
feature = snapshot.feature(ticket_id.feature)
ticket = snapshot.ticket(ticket_id)
if ticket.status != "claimed":
raise StateError(f"ticket {ticket_id} is not claimed")
previous_owner = str(ticket.metadata.get("claimed_by", ""))
if not previous_owner:
raise StateError(f"ticket {ticket_id} has no owner")
if previous_owner == owner:
raise StateError("reclaim requires a different owner")
if not claim_is_stale(ticket, at):
raise StateError(f"ticket {ticket_id} is not stale")
validate_reclaim_workspace(repo_root, ticket)
metadata = dict(ticket.metadata)
timestamp = format_timestamp(at)
append_history(
metadata,
"reclaim",
timestamp,
previous_owner=previous_owner,
owner=owner,
)
metadata.update(
{
"claimed_by": owner,
"claimed_at": timestamp,
"heartbeat_at": timestamp,
}
)
update_ticket_state(ticket, metadata=metadata)
return render_claim_context(feature, ticket, metadata)
def finish_nonresolved_ticket(
state_root: Path,
ticket_id: TicketId,
owner: str,
result: str,
reason: str,
) -> str:
state_root = state_root.resolve()
with locked_state(state_root):
snapshot = load_queue_snapshot(state_root)
ticket = snapshot.ticket(ticket_id)
allowed_source = ticket.status == "claimed" or (
result == "released" and ticket.status == "blocked"
)
if not allowed_source:
raise StateError(
f"ticket {ticket_id} cannot become {result} "
f"from {ticket.status}"
)
metadata = dict(ticket.metadata)
if metadata.get("claimed_by") != owner:
raise StateError(f"ticket {ticket_id} is owned by another session")
if result in {"blocked", "skipped"} and not reason.strip():
raise StateError(f"{result} requires a reason")
timestamp = utc_now()
append_history(metadata, result, timestamp, owner=owner, reason=reason.strip())
if result == "blocked":
status = "blocked"
metadata["blocked_reason"] = reason.strip()
metadata["blocked_at"] = timestamp
elif result == "released":
status = "ready-for-agent"
metadata["last_owner"] = owner
metadata["released_at"] = timestamp
metadata.pop("blocked_reason", None)
metadata.pop("blocked_at", None)
metadata.pop("claimed_by", None)
metadata.pop("claimed_at", None)
metadata.pop("heartbeat_at", None)
else:
status = "skipped"
metadata["last_owner"] = owner
metadata["skipped_reason"] = reason.strip()
metadata["skipped_at"] = timestamp
metadata.pop("claimed_by", None)
metadata.pop("claimed_at", None)
metadata.pop("heartbeat_at", None)
update_ticket_state(ticket, status=status, metadata=metadata)
return f"{result.upper()}={ticket_id}"
def release_blocked_ticket(
state_root: Path,
ticket_id: TicketId,
reason: str,
) -> str:
"""Return a blocked ticket to the queue without its original owner.
``finish --result released`` is owner-scoped and ``reclaim`` only accepts
claimed tickets, so a blocked ticket whose session is gone would otherwise
have no owner left that could unblock it. Branch, worktree and uncommitted
changes are preserved exactly as ``finish --result released`` leaves them.
"""
if not reason.strip():
raise StateError("ticket release requires a reason")
state_root = state_root.resolve()
with locked_state(state_root):
snapshot = load_queue_snapshot(state_root)
ticket = snapshot.ticket(ticket_id)
if ticket.status != "blocked":
raise StateError(
f"ticket {ticket_id} is not blocked: {ticket.status}"
)
metadata = dict(ticket.metadata)
previous_owner = str(metadata.get("claimed_by", ""))
timestamp = utc_now()
append_history(
metadata,
"ticket-released",
timestamp,
reason=reason.strip(),
previous_owner=previous_owner,
)
if previous_owner:
metadata["last_owner"] = previous_owner
metadata["released_at"] = timestamp
metadata["released_reason"] = reason.strip()
for key in (
"blocked_reason",
"blocked_at",
"claimed_by",
"claimed_at",
"heartbeat_at",
):
metadata.pop(key, None)
update_ticket_state(ticket, status="ready-for-agent", metadata=metadata)
return f"TICKET_RELEASED={ticket_id}"
def finish_resolved_ticket(
state_root: Path,
repo_root: Path,
ticket_id: TicketId,
owner: str,
implementation_commit: str,
feature_head: str,
review_base: str,
verified: str,
reviewed: str,
) -> str:
if not implementation_commit:
raise StateError("implementation commit is required")
if not feature_head:
raise StateError("feature head is required")
if not review_base:
raise StateError("review base is required")
verification_commit, verification_artifact = require_verification_passed(
verified, "verification"
)
review_commit, reviewed_base, review_artifact = require_review_passed(reviewed)
state_root = state_root.resolve()
repo_root = resolve_repo_root(repo_root)
with locked_state(state_root):
snapshot = load_queue_snapshot(state_root)
feature = snapshot.feature(ticket_id.feature)
ticket = snapshot.ticket(ticket_id)
metadata = dict(ticket.metadata)
if ticket.status == "resolved" and metadata.get("integration_commit"):
return (
f"RESOLVED={ticket_id}\n"
f"INTEGRATION_COMMIT={metadata['integration_commit']}"
)
if ticket.status != "claimed":
raise StateError(f"ticket {ticket_id} is not claimed")
if metadata.get("claimed_by") != owner:
raise StateError(f"ticket {ticket_id} is owned by another session")
isolation = metadata.get("isolation")
expected_workspace = Path(str(metadata.get("workspace", ""))).resolve()
if isolation == "in-place":
if expected_workspace != repo_root.resolve():
raise StateError(
f"claim workspace mismatch: expected {expected_workspace}, "
f"got {repo_root.resolve()}"
)
ticket_workspace = repo_root
integration_workspace = repo_root
elif isolation == "worktree":
if not expected_workspace.is_dir():
raise StateError(f"ticket workspace not found: {expected_workspace}")
ticket_workspace = expected_workspace
feature_state = load_feature_state(feature)
integration_value = feature_state.get("integration_workspace")
if not integration_value:
raise StateError(
f"{ticket_id.feature}: integration workspace is not recorded"
)
integration_workspace = Path(str(integration_value)).resolve()
if not integration_workspace.is_dir():
raise StateError(
f"integration workspace not found: {integration_workspace}"
)
else:
raise StateError(f"{ticket.id}: invalid claim isolation")
dirty_path = checkout_is_dirty(ticket_workspace)
if dirty_path:
raise StateError(f"ticket checkout is dirty: {dirty_path}")
ticket_branch = str(metadata.get("ticket_branch", ""))
feature_branch = str(metadata.get("feature_branch", ""))
if not branch_exists(repo_root, ticket_branch):
raise StateError(f"ticket branch not found: {ticket_branch}")
if not branch_exists(repo_root, feature_branch):
raise StateError(f"feature branch not found: {feature_branch}")
ticket_tip = git_output(repo_root, "rev-parse", ticket_branch)
implementation_commit = normalize_commit(
repo_root, implementation_commit, "implementation"
)
feature_head = normalize_commit(repo_root, feature_head, "feature head")
review_base = normalize_commit(repo_root, review_base, "review base")
verification_commit = normalize_commit(
repo_root, verification_commit, "verification"
)
review_commit = normalize_commit(repo_root, review_commit, "review")
reviewed_base = normalize_commit(
repo_root, reviewed_base, "review base evidence"
)
if implementation_commit != ticket_tip:
raise StateError(
"implementation commit must equal ticket branch HEAD"
)
if verification_commit != ticket_tip:
raise StateError(
"verification commit must equal ticket branch HEAD"
)
if review_commit != ticket_tip:
raise StateError("review commit must equal ticket branch HEAD")
if review_base != feature_head:
raise StateError(
"review base must equal the verified feature head"
)
if reviewed_base != review_base:
raise StateError(
"review evidence base must equal --review-base"
)
if current_branch(ticket_workspace) != ticket_branch:
raise StateError(
f"ticket workspace is not on its branch: {ticket_workspace}"
)
current_feature_head = git_output(repo_root, "rev-parse", feature_branch)
already_integrated = is_ancestor(repo_root, ticket_tip, current_feature_head)
recovered_integration_commit: Optional[str] = None
if already_integrated:
recovered_integration_commit, integration_base = find_no_ff_merge(
repo_root,
current_feature_head,
ticket_tip,
"ticket",
)
if feature_head != integration_base:
raise StateError(
"verified feature head must equal the ticket integration base"
)
else:
if current_feature_head != feature_head:
return f"RETRY: feature advanced\nFEATURE_HEAD={current_feature_head}"
if not is_ancestor(repo_root, feature_head, ticket_tip):
raise StateError(
"ticket branch must include the verified feature head"
)
if not already_integrated:
integration_dirty = checkout_is_dirty(integration_workspace)
if integration_dirty:
raise StateError(
f"integration checkout is dirty: {integration_dirty}"
)
if current_branch(integration_workspace) != feature_branch:
checkout_branch(integration_workspace, feature_branch)
merge = git_run(
integration_workspace,
"merge",
"--no-ff",
"-m",
f"Integrate {ticket_id}",
ticket_branch,
)
if merge.returncode != 0:
detail = (merge.stderr or merge.stdout).strip()
metadata["last_error"] = f"merge failed: {detail}"
update_ticket_state(ticket, metadata=metadata)
raise StateError(f"ticket merge failed: {detail}")
elif current_branch(integration_workspace) != feature_branch:
checkout_branch(integration_workspace, feature_branch)
integration_commit = (
recovered_integration_commit
if recovered_integration_commit is not None
else git_output(repo_root, "rev-parse", feature_branch)
)
evidence = {
"verification": snapshot_evidence(
feature,
f"ticket-{ticket_id.number}-verification",
verification_artifact,
),
"review": snapshot_evidence(
feature,
f"ticket-{ticket_id.number}-review",
review_artifact,
),
}
metadata.update(
{
"implementation_commit": implementation_commit,
"ticket_head": ticket_tip,
"integration_commit": integration_commit,
"evidence": evidence,
"resolved_at": utc_now(),
}
)
metadata.pop("last_error", None)
update_ticket_state(ticket, status="resolved", metadata=metadata)
return (
f"RESOLVED={ticket_id}\n"
f"INTEGRATION_COMMIT={integration_commit}"
)
def branch_workspace(repo_root: Path, branch: str) -> Optional[Path]:
return worktree_branch_paths(repo_root).get(branch)
def prepare_main_workspace(repo_root: Path, main_branch: str) -> Path:
workspace = branch_workspace(repo_root, main_branch)
if workspace is not None:
dirty_path = checkout_is_dirty(workspace)
if dirty_path:
raise StateError(f"main checkout is dirty: {dirty_path}")
return workspace
dirty_path = checkout_is_dirty(repo_root)
if dirty_path:
raise StateError(f"control checkout is dirty: {dirty_path}")
if not current_branch(repo_root):
raise StateError("control checkout has detached HEAD")
checkout_branch(repo_root, main_branch)
return repo_root
def path_is_within(path: Path, parent: Path) -> bool:
try:
return os.path.commonpath((str(path.resolve()), str(parent.resolve()))) == str(
parent.resolve()
)
except ValueError:
return False
def remove_clean_worktree(
repo_root: Path, workspace: Path, allowed_root: Path
) -> Optional[str]:
workspace = workspace.resolve()
if not path_is_within(workspace, allowed_root):
return f"preserved unexpected worktree path {workspace}"
if not workspace.exists():
return None
dirty_path = checkout_is_dirty(workspace)
if dirty_path:
return f"preserved dirty worktree {workspace}: {dirty_path}"
result = git_run(repo_root, "worktree", "remove", str(workspace))
if result.returncode != 0:
detail = (result.stderr or result.stdout).strip()
return f"could not remove worktree {workspace}: {detail}"
return None
def cleanup_integrated_feature_worktrees(
repo_root: Path, state_root: Path, feature: Feature
) -> list[str]:
warnings: list[str] = []
allowed_root = (state_root / "worktrees").resolve()
for ticket in feature.tickets.values():
if ticket.status not in SATISFIED_STATUSES:
continue
if ticket.metadata.get("isolation") != "worktree":
continue
workspace_value = ticket.metadata.get("workspace")
if not workspace_value:
continue
warning = remove_clean_worktree(
repo_root, Path(str(workspace_value)), allowed_root
)
if warning:
warnings.append(warning)
feature_metadata = load_feature_state(feature)
integration_value = feature_metadata.get("integration_workspace")
if integration_value:
warning = remove_clean_worktree(
repo_root, Path(str(integration_value)), allowed_root
)
if warning:
warnings.append(warning)
return warnings
def block_feature_integration(
state_root: Path,
feature_id: FeatureId,
reason: str,
) -> str:
if not reason.strip():
raise StateError("feature block requires a reason")
state_root = state_root.resolve()
with locked_state(state_root):
snapshot = load_queue_snapshot(state_root)
feature = snapshot.feature(feature_id)
state = snapshot.feature_states[feature_id]
if validated_integration_commit(feature_id, state):
raise StateError(f"feature is already integrated: {feature_id}")
feature_status = snapshot.scheduler.feature_state(feature_id)
if feature_status != "ready-to-integrate":
raise StateError(
f"feature {feature_id} is not ready to integrate: {feature_status}"
)
timestamp = utc_now()
state["integration_blocked_reason"] = reason.strip()
state["integration_blocked_at"] = timestamp
append_history(
state,
"feature-blocked",
timestamp,
reason=reason.strip(),
)
write_feature_state(feature, state)
return f"FEATURE_BLOCKED={feature_id}"
def release_feature_integration(state_root: Path, feature_id: FeatureId) -> str:
state_root = state_root.resolve()
with locked_state(state_root):
snapshot = load_queue_snapshot(state_root)
feature = snapshot.feature(feature_id)
state = snapshot.feature_states[feature_id]
if not state.get("integration_blocked_reason"):
raise StateError(f"feature is not blocked: {feature_id}")
timestamp = utc_now()
append_history(state, "feature-released", timestamp)
state.pop("integration_blocked_reason", None)
state.pop("integration_blocked_at", None)
write_feature_state(feature, state)
return f"FEATURE_RELEASED={feature_id}"
def integrate_feature(
state_root: Path,
repo_root: Path,
feature_id: FeatureId,
feature_head: str,
verified: str,
main_verified: str,
reviewed: str,
main_branch: str,
allow_partial: bool,
) -> str:
if not feature_head:
raise StateError("feature head is required")
feature_verification_commit, feature_verification_artifact = require_verification_passed(
verified, "feature verification"
)
main_verification_commit, main_verification_artifact = require_verification_passed(
main_verified, "main candidate verification"
)
review_commit, reviewed_base, review_artifact = require_review_passed(reviewed)
state_root = state_root.resolve()
repo_root = resolve_repo_root(repo_root)
with locked_state(state_root):
snapshot = load_queue_snapshot(state_root)
requested = snapshot.feature(feature_id)
requested_state = snapshot.feature_states[feature_id]
integration_commit = validated_integration_commit(
feature_id,
requested_state,
)
if integration_commit:
integration_commit = normalize_commit(
repo_root,
integration_commit,
str(FeatureIntegrationId(feature_id)),
)
warnings = cleanup_integrated_feature_worktrees(
repo_root, state_root, requested
)
output = [
f"INTEGRATED={feature_id}",
f"MAIN_INTEGRATION_COMMIT={integration_commit}",
]
output.extend(f"WARNING={warning}" for warning in warnings)
return "\n".join(output)
if requested_state.get("integration_blocked_reason"):
raise StateError(
f"feature {feature_id} is blocked: "
f"{requested_state['integration_blocked_reason']}"
)
integration_frontier = snapshot.scheduler.integration_frontier
if integration_frontier is None:
return "NOOP: all features integrated"
if integration_frontier.feature != feature_id:
raise StateError(
f"cannot integrate {feature_id}: earlier feature "
f"{integration_frontier.feature} is not integrated"
)
requested_status = snapshot.scheduler.feature_state(feature_id)
if requested_status != "ready-to-integrate":
raise StateError(
f"feature {feature_id} is not ready to integrate: "
f"{requested_status}"
)
if requested.partial and not allow_partial:
raise StateError(
f"feature {feature_id} is partial; pass --allow-partial explicitly"
)
claims = active_claims(
[
snapshot.feature(queued_feature_id)
for queued_feature_id in snapshot.feature_ids
]
)
if any(
ticket.metadata.get("isolation") == "in-place"
for _, ticket in claims
):
return "BUSY"
feature_branch = str(
requested_state.get("feature_branch", f"feature/{feature_id}")
)
if not branch_exists(repo_root, feature_branch):
raise StateError(f"feature branch not found: {feature_branch}")
if not branch_exists(repo_root, main_branch):
raise StateError(f"main branch not found: {main_branch}")
current_feature_head = git_output(repo_root, "rev-parse", feature_branch)
feature_head = normalize_commit(repo_root, feature_head, "feature head")
if current_feature_head != feature_head:
return f"RETRY: feature advanced\nFEATURE_HEAD={current_feature_head}"
main_head = git_output(repo_root, "rev-parse", main_branch)
already_integrated = is_ancestor(
repo_root, current_feature_head, main_head
)
if not already_integrated and not is_ancestor(
repo_root, main_head, current_feature_head
):
return f"RETRY: feature needs main sync\nMAIN_HEAD={main_head}"
feature_verification_commit = normalize_commit(
repo_root,
feature_verification_commit,
"feature verification",
)
main_verification_commit = normalize_commit(
repo_root,
main_verification_commit,
"main candidate verification",
)
review_commit = normalize_commit(repo_root, review_commit, "review")
reviewed_base = normalize_commit(
repo_root, reviewed_base, "review base evidence"
)
if feature_verification_commit != current_feature_head:
raise StateError(
"feature verification commit must equal feature HEAD"
)
if main_verification_commit != current_feature_head:
raise StateError(
"main candidate verification commit must equal feature HEAD"
)
if review_commit != current_feature_head:
raise StateError("review commit must equal feature HEAD")
recovered_integration_commit: Optional[str] = None
if already_integrated:
if current_feature_head == main_head:
expected_review_base = main_head
recovered_integration_commit = main_head
else:
(
recovered_integration_commit,
expected_review_base,
) = find_no_ff_merge(
repo_root,
main_head,
current_feature_head,
"feature",
)
if reviewed_base != expected_review_base:
raise StateError(
"review base must equal the pre-integration main HEAD"
)
elif reviewed_base != main_head:
raise StateError("review base must equal the latest main HEAD")
if already_integrated:
if recovered_integration_commit is None:
raise StateError("feature integration commit was not recovered")
integration_commit = recovered_integration_commit
else:
main_workspace = prepare_main_workspace(repo_root, main_branch)
latest_main = git_output(repo_root, "rev-parse", main_branch)
if latest_main != main_head:
return f"RETRY: feature needs main sync\nMAIN_HEAD={latest_main}"
merge = git_run(
main_workspace,
"merge",
"--no-ff",
"-m",
f"Integrate feature {feature_id}",
feature_branch,
)
if merge.returncode != 0:
detail = (merge.stderr or merge.stdout).strip()
requested_state["last_error"] = f"main merge failed: {detail}"
requested_state["integration_blocked_reason"] = (
f"main merge failed: {detail}"
)
requested_state["integration_blocked_at"] = utc_now()
write_feature_state(requested, requested_state)
raise StateError(f"feature merge failed: {detail}")
integration_commit = git_output(repo_root, "rev-parse", main_branch)
evidence = {
"feature_verification": snapshot_evidence(
requested,
"feature-verification",
feature_verification_artifact,
),
"main_candidate_verification": snapshot_evidence(
requested,
"main-candidate-verification",
main_verification_artifact,
),
"review": snapshot_evidence(
requested,
"feature-review",
review_artifact,
),
}
requested_state.update(
{
"feature_branch": feature_branch,
"feature_head": current_feature_head,
"integration_commit": integration_commit,
"evidence": evidence,
"partial_authorized": bool(allow_partial),
"integrated_at": utc_now(),
}
)
requested_state.pop("last_error", None)
write_feature_state(requested, requested_state)
warnings = cleanup_integrated_feature_worktrees(
repo_root, state_root, requested
)
output = [
f"INTEGRATED={feature_id}",
f"MAIN_INTEGRATION_COMMIT={integration_commit}",
]
output.extend(f"WARNING={warning}" for warning in warnings)
return "\n".join(output)
VERIFICATION_JSON_HELP = (
"verification JSON fields: version=1, kind=verification, commit, "
"result=pass, command, exit_code=0, output, output_sha256."
)
REVIEW_JSON_HELP = (
"review JSON fields: version=1, kind=review, commit, base, "
"standards=pass, spec=pass, report, report_sha256."
)
EVIDENCE_HELP = f"{VERIFICATION_JSON_HELP} {REVIEW_JSON_HELP}"
STATUS_OUTPUT_HELP = (
"stdout/0 outcome NO FEATURES, or records: INTEGRATION_FRONTIER with STATE and "
"WAITING_ON; FEATURE with STATE and PARTIAL; TICKET_FRONTIER; CLAIM with OWNER, "
"HEARTBEAT, STALE, ISOLATION, and WORKSPACE; BLOCKED and TICKET_ERROR with REASON; "
"BLOCKED_DEPENDENCY with WAITING_ON; FEATURE_BLOCKED with REASON; "
"MAIN_INTEGRATION_COMMIT."
)
CLAIM_OUTPUT_HELP = (
"successful assignment keys: FEATURE, TICKET, CONTROL_ROOT, STATE_ROOT, "
"WORKSPACE, BRANCH, BASE, ISOLATION. Other stdout/0 outcomes: NO FEATURES, "
"NOOP, BUSY, RETRY, INTEGRATION_REQUIRED."
)
INTEGRATE_OUTPUT_HELP = (
"successful stdout keys: INTEGRATED, MAIN_INTEGRATION_COMMIT, followed by zero or "
"more WARNING lines for retained worktrees. Other stdout/0 outcomes: NOOP, RETRY."
)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="main_loop.py",
description=(
"Operate the locked global ticket DAG and its serial feature "
"integration queue."
),
epilog=(
"Normal scheduler outcomes are written to stdout with exit code 0; "
"invalid input or state is written to stderr with exit code 2."
),
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
subparsers = parser.add_subparsers(dest="command", required=True)
def command_parser(
name: str,
summary: str,
*,
description: Optional[str] = None,
epilog: Optional[str] = None,
) -> argparse.ArgumentParser:
return subparsers.add_parser(
name,
help=summary,
description=description or summary,
epilog=epilog,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
def add_state_root(command: argparse.ArgumentParser) -> None:
command.add_argument(
"--state-root",
default=".scratch",
help=(
"shared scheduler state; use <PROJECT_ROOT>/.scratch before "
"claim and the returned absolute STATE_ROOT afterwards"
),
)
def add_repo_root(command: argparse.ArgumentParser) -> None:
command.add_argument(
"--repo-root",
default=".",
help="control Git checkout containing the shared repository",
)
enqueue = command_parser(
"enqueue",
"atomically validate and append one or more queued features",
)
add_state_root(enqueue)
enqueue.add_argument(
"--feature",
required=True,
action="append",
help="FeatureId to append; repeat for one atomic dependency batch",
)
status = command_parser(
"status",
"report qualified ticket and integration frontiers",
epilog=STATUS_OUTPUT_HELP,
)
add_state_root(status)
claim = command_parser(
"claim",
"resume this owner or claim the first globally runnable ticket",
description=(
"Resume this owner's active claim or select the first globally "
"runnable ticket. In-place isolation is queue-global; worktrees can "
"run across features."
),
epilog=CLAIM_OUTPUT_HELP,
)
add_state_root(claim)
add_repo_root(claim)
claim.add_argument(
"--owner",
required=True,
help="globally unique stable owner for this session",
)
claim.add_argument(
"--isolation",
choices=("auto", "in-place", "worktree"),
default="auto",
help="workspace isolation; auto uses global active claims",
)
claim.add_argument(
"--main-branch",
default="main",
help="branch containing integrated features",
)
finish = command_parser(
"finish",
"resolve, block, release, or skip an owned ticket",
description=(
"Transition an owned ticket. Resolved requires commit-bound "
"verification and review evidence; blocked and skipped require a "
"reason."
),
epilog=EVIDENCE_HELP,
)
add_state_root(finish)
add_repo_root(finish)
finish.add_argument(
"--ticket",
required=True,
help="qualified <feature>/<NN> TicketId",
)
finish.add_argument("--owner", required=True, help="current claim owner")
finish.add_argument(
"--result",
choices=("resolved", "blocked", "released", "skipped"),
required=True,
help="target ticket status transition",
)
finish.add_argument(
"--implementation-commit",
default="",
help="ticket branch HEAD for a resolved result",
)
finish.add_argument(
"--feature-head",
default="",
help="verified feature branch head used as the merge target",
)
finish.add_argument(
"--review-base",
default="",
help="fixed review base, equal to the verified feature head",
)
finish.add_argument(
"--verified",
default="",
help="path to ticket verification JSON for a resolved result",
)
finish.add_argument(
"--reviewed",
default="",
help="path to ticket review JSON for a resolved result",
)
finish.add_argument(
"--reason",
default="",
help="required explanation for blocked or skipped",
)
heartbeat = command_parser(
"heartbeat",
"renew an active claim lease for its current owner",
)
add_state_root(heartbeat)
heartbeat.add_argument("--ticket", required=True, help="qualified TicketId")
heartbeat.add_argument("--owner", required=True, help="current claim owner")
reclaim = command_parser(
"reclaim",
"transfer a stale claim while preserving its workspace and base",
)
add_state_root(reclaim)
add_repo_root(reclaim)
reclaim.add_argument("--ticket", required=True, help="qualified TicketId")
reclaim.add_argument("--owner", required=True, help="new unique owner")
block_feature = command_parser(
"block-feature",
"block only a feature integration frontier, not later development",
)
add_state_root(block_feature)
block_feature.add_argument("--feature", required=True, help="FeatureId")
block_feature.add_argument("--reason", required=True, help="blocking reason")
release_feature = command_parser(
"release-feature",
"clear a feature integration block",
)
add_state_root(release_feature)
release_feature.add_argument("--feature", required=True, help="FeatureId")
release_ticket = command_parser(
"release-ticket",
"return a blocked ticket to ready without its lost owner",
)
add_state_root(release_ticket)
release_ticket.add_argument("--ticket", required=True, help="qualified TicketId")
release_ticket.add_argument("--reason", required=True, help="recovery reason")
integrate = command_parser(
"integrate",
"merge only the strict integration frontier under the global lock",
description=(
"Integrate only the strict integration frontier. The feature must "
"be ready, synchronized with main, and backed by fresh verification "
"and review evidence."
),
epilog=f"{EVIDENCE_HELP} {INTEGRATE_OUTPUT_HELP}",
)
add_state_root(integrate)
add_repo_root(integrate)
integrate.add_argument("--feature", required=True, help="frontier FeatureId")
integrate.add_argument(
"--feature-head",
required=True,
help="verified feature branch HEAD",
)
integrate.add_argument(
"--verified",
required=True,
help="path to feature verification JSON",
)
integrate.add_argument(
"--main-verified",
required=True,
help="path to main-candidate verification JSON",
)
integrate.add_argument(
"--reviewed",
required=True,
help="path to feature review JSON",
)
integrate.add_argument(
"--main-branch",
default="main",
help="serial integration target branch",
)
integrate.add_argument(
"--allow-partial",
action="store_true",
help="explicitly authorize integration when tickets were skipped",
)
return parser
def main(argv: list[str]) -> int:
args = build_parser().parse_args(argv)
state_root = Path(args.state_root)
try:
if args.command == "enqueue":
message = enqueue_features(
state_root,
tuple(FeatureId.parse(raw) for raw in args.feature),
)
elif args.command == "status":
message = status_report(state_root)
elif args.command == "claim":
message = claim_ticket(
state_root,
Path(args.repo_root),
args.owner,
args.isolation,
args.main_branch,
)
elif args.command == "heartbeat":
message = heartbeat_ticket(
state_root,
TicketId.parse(args.ticket),
args.owner,
datetime.now(timezone.utc),
)
elif args.command == "reclaim":
message = reclaim_ticket(
state_root,
Path(args.repo_root),
TicketId.parse(args.ticket),
args.owner,
datetime.now(timezone.utc),
)
elif args.command == "block-feature":
message = block_feature_integration(
state_root,
FeatureId.parse(args.feature),
args.reason,
)
elif args.command == "release-feature":
message = release_feature_integration(
state_root,
FeatureId.parse(args.feature),
)
elif args.command == "release-ticket":
message = release_blocked_ticket(
state_root,
TicketId.parse(args.ticket),
args.reason,
)
elif args.command == "integrate":
message = integrate_feature(
state_root,
Path(args.repo_root),
FeatureId.parse(args.feature),
args.feature_head,
args.verified,
args.main_verified,
args.reviewed,
args.main_branch,
args.allow_partial,
)
elif args.command == "finish" and args.result == "resolved":
message = finish_resolved_ticket(
state_root,
Path(args.repo_root),
TicketId.parse(args.ticket),
args.owner,
args.implementation_commit,
args.feature_head,
args.review_base,
args.verified,
args.reviewed,
)
elif args.command == "finish":
message = finish_nonresolved_ticket(
state_root,
TicketId.parse(args.ticket),
args.owner,
args.result,
args.reason,
)
else:
raise StateError(f"unhandled command: {args.command}")
except (OSError, SchedulerError, StateError, UnicodeError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
print(message)
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))