Files
xjxandgd-ops 72c66a67fb Fix Agent Pet Companion 0.4.3 release blockers (#21)
## Summary

- restore release-preparation context on `main` pushes only when typed
validation scope reports an exact `CHANGELOG.md` change
- make primary cold launch converge on the shared control-center
presenter and retain fronting until the SwiftUI window is registered
- add control-plane, lifecycle, and race-regression coverage for both
release blockers

## Validation

- `python3 script/tests/test_validation_tooling.py` (53 passed)
- `python3 script/development_domains.py validate`
- `./script/validate_build_scripts_safety.sh --static-only`
- `./script/validate_app_lifecycle_contract.sh` (12/12 passed)
- `./script/validate_swift_tests.sh --scope all` (745 passed)
- `./script/validate_pre_push.sh`
- `./script/changelog_fragments.py policy --base-ref origin/main
--release-preparation true`

## Release note

This amends the still-unpublished v0.4.3 release preparation. The exact
merged `main` commit will receive the mandatory host UI acceptance
before release dispatch.

## Development claim / 开发声明

- Domain: `release-control-plane`
- Claim: `control-plane`
- Base commit: `7c57bbdb16b24074df16dd64ec51d3fc28782a7f`
- Release preparation: `yes`
- Focused validation:
  - `python3 script/tests/test_validation_tooling.py`
  - `./script/validate_build_scripts_safety.sh --static-only`

<!-- apc-engineering-coordination-v1
{"claim_overlap_rejections":0,"merge_tree_conflicts":0,"schema_version":"apc.engineering-coordination.v1","task_created_at":"2026-08-12T16:56:32Z","train_conflict_resolutions":0}
-->

---------

Co-authored-by: gd-ops <gd-ops@xmfunny.com>
2026-08-13 01:13:01 +08:00

1172 lines
43 KiB
Python
Executable File

#!/usr/bin/env python3
"""Coordinate direct and train-based multi-session development safely.
State is stored under Git's common directory so every worktree created from the
same checkout observes the same active train. Mutating commands are dry-run by
default and require ``--apply``.
"""
from __future__ import annotations
import argparse
import fcntl
import json
import os
import pathlib
import re
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from dataclasses import asdict, dataclass
from typing import Any, NoReturn
SCRIPT_DIR = pathlib.Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from development_domains import ( # noqa: E402
DOMAIN_ID,
Manifest,
load_manifest,
path_matches,
patterns_overlap,
validate_relative_pattern,
)
SCHEMA_VERSION = "apc.development-flow.v3"
PREVIOUS_SCHEMA_VERSION = "apc.development-flow.v2"
LEGACY_SCHEMA_VERSION = "apc.development-flow.v1"
COORDINATION_SCHEMA_VERSION = "apc.engineering-coordination.v1"
COORDINATION_FIELDS = (
"claim_overlap_rejections",
"merge_tree_conflicts",
"train_conflict_resolutions",
)
MAIN_BRANCH = "main"
TRAIN_PREFIX = "gd-ops/train/"
TASK_PREFIX = "gd-ops/task/"
FIX_PREFIX = "gd-ops/fix/"
RELEASE_PREFIX = "gd-ops/release/"
TRAIN_PATTERN = re.compile(r"gd-ops/train/[a-z0-9][a-z0-9._-]{0,63}")
WORK_PATTERN = re.compile(r"gd-ops/(?:task|fix|release)/[A-Za-z0-9._-]+-[a-z0-9][a-z0-9._-]{0,63}")
RELEASE_PATTERN = re.compile(r"gd-ops/release/[A-Za-z0-9._-]+-[a-z0-9][a-z0-9._-]{0,63}")
TOKEN_PATTERN = re.compile(r"[A-Za-z0-9._-]+")
SLUG_PATTERN = re.compile(r"[a-z0-9][a-z0-9._-]{0,63}")
def fail(message: str) -> NoReturn:
raise ValueError(message)
class ClaimOverlapError(ValueError):
"""An exclusive/domain/Red claim collision rejected before mutation."""
def run(
command: list[str],
*,
cwd: pathlib.Path,
check: bool = True,
input_text: str | None = None,
) -> subprocess.CompletedProcess[str]:
result = subprocess.run(
command,
cwd=cwd,
check=False,
text=True,
input=input_text,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if check and result.returncode != 0:
detail = result.stderr.strip() or result.stdout.strip()
fail(f"command failed ({' '.join(command)}): {detail}")
return result
def repository_root(root: pathlib.Path) -> pathlib.Path:
value = run(
["git", "rev-parse", "--show-toplevel"], cwd=root.resolve()
).stdout.strip()
resolved = pathlib.Path(value).resolve()
if not resolved.is_dir():
fail("Git repository root is unavailable")
return resolved
def common_state_paths(root: pathlib.Path) -> tuple[pathlib.Path, pathlib.Path]:
common = run(
["git", "rev-parse", "--path-format=absolute", "--git-common-dir"],
cwd=root,
).stdout.strip()
common_path = pathlib.Path(common)
if not common_path.is_absolute():
common_path = (root / common_path).resolve()
state_dir = common_path / "apc-development"
return state_dir / "state.json", state_dir / "state.lock"
def empty_state() -> dict[str, Any]:
return {
"schema_version": SCHEMA_VERSION,
"active_train": None,
"branches": {},
"coordination": {field: 0 for field in COORDINATION_FIELDS},
}
def validate_state(payload: dict[str, Any]) -> dict[str, Any]:
if set(payload) != {"schema_version", "active_train", "branches", "coordination"}:
fail("development-flow state field inventory is invalid")
if payload.get("schema_version") != SCHEMA_VERSION:
fail("development-flow state schema is unsupported")
active_train = payload.get("active_train")
if active_train is not None and TRAIN_PATTERN.fullmatch(active_train) is None:
fail("development-flow active train is invalid")
branches = payload.get("branches")
if not isinstance(branches, dict) or len(branches) > 256:
fail("development-flow branch context inventory is invalid")
context_fields = {
"base",
"base_commit",
"domain",
"claim",
"claimed_paths",
"approved_shared_paths",
"created_at",
"worktree",
"release_preparation",
"control_plane_owner",
}
for branch, context in branches.items():
if (
not isinstance(branch, str)
or (WORK_PATTERN.fullmatch(branch) is None and TRAIN_PATTERN.fullmatch(branch) is None)
or not isinstance(context, dict)
or set(context) != context_fields
):
fail("development-flow branch context entry is invalid")
base = context["base"]
if not isinstance(base, str) or (
base != MAIN_BRANCH and TRAIN_PATTERN.fullmatch(base) is None
):
fail("development-flow branch base is invalid")
base_commit = context["base_commit"]
if base_commit is not None and (
not isinstance(base_commit, str)
or re.fullmatch(r"[0-9a-f]{40}", base_commit) is None
):
fail("development-flow branch base commit is invalid")
domain = context["domain"]
claim = context["claim"]
if (domain is None) != (claim is None):
fail("development-flow legacy domain and claim must both be absent")
if domain is not None and (
not isinstance(domain, str)
or DOMAIN_ID.fullmatch(domain) is None
or not isinstance(claim, str)
or DOMAIN_ID.fullmatch(claim) is None
):
fail("development-flow domain claim is invalid")
for field in ("claimed_paths", "approved_shared_paths"):
patterns = context[field]
if (
not isinstance(patterns, list)
or len(patterns) > 256
or len(patterns) != len(set(patterns))
):
fail(f"development-flow {field} is invalid")
for pattern in patterns:
validate_relative_pattern(pattern, field=f"development-flow {field}")
if domain is None and (context["claimed_paths"] or context["approved_shared_paths"]):
fail("legacy development-flow context cannot contain path claims")
created_at = context["created_at"]
if created_at is not None and (
not isinstance(created_at, str)
or re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z", created_at) is None
):
fail("development-flow claim timestamp is invalid")
worktree = context["worktree"]
if worktree is not None and (
not isinstance(worktree, str)
or not pathlib.Path(worktree).is_absolute()
or len(worktree) > 4096
or "\x00" in worktree
):
fail("development-flow worktree path is invalid")
if not isinstance(context["release_preparation"], bool) or not isinstance(
context["control_plane_owner"], bool
):
fail("development-flow branch policy flags are invalid")
coordination = payload.get("coordination")
if not isinstance(coordination, dict) or set(coordination) != set(COORDINATION_FIELDS):
fail("development-flow coordination field inventory is invalid")
if any(
not isinstance(coordination[field], int)
or not 0 <= coordination[field] <= 1_000_000
for field in COORDINATION_FIELDS
):
fail("development-flow coordination count is invalid")
return payload
def migrate_previous_state(payload: dict[str, Any]) -> dict[str, Any]:
if set(payload) != {"schema_version", "active_train", "branches"}:
fail("previous development-flow state field inventory is invalid")
if payload.get("schema_version") != PREVIOUS_SCHEMA_VERSION:
fail("development-flow state schema is unsupported")
migrated = dict(payload)
migrated["schema_version"] = SCHEMA_VERSION
migrated["coordination"] = {field: 0 for field in COORDINATION_FIELDS}
return validate_state(migrated)
def migrate_legacy_state(payload: dict[str, Any]) -> dict[str, Any]:
if set(payload) != {"schema_version", "active_train", "branch_bases"}:
fail("legacy development-flow state field inventory is invalid")
if payload.get("schema_version") != LEGACY_SCHEMA_VERSION:
fail("development-flow state schema is unsupported")
branch_bases = payload.get("branch_bases")
if not isinstance(branch_bases, dict) or len(branch_bases) > 256:
fail("legacy development-flow branch inventory is invalid")
migrated = empty_state()
migrated["active_train"] = payload.get("active_train")
for branch, base in branch_bases.items():
migrated["branches"][branch] = {
"base": base,
"base_commit": None,
"domain": None,
"claim": None,
"claimed_paths": [],
"approved_shared_paths": [],
"created_at": None,
"worktree": None,
"release_preparation": False,
"control_plane_owner": False,
}
return validate_state(migrated)
def read_state(path: pathlib.Path) -> dict[str, Any]:
if not path.exists():
return empty_state()
metadata = path.lstat()
if not path.is_file() or path.is_symlink() or metadata.st_size > 256 * 1024:
fail("development-flow state must be a bounded regular file")
payload = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(payload, dict):
fail("development-flow state must contain one JSON object")
if payload.get("schema_version") == LEGACY_SCHEMA_VERSION:
return migrate_legacy_state(payload)
if payload.get("schema_version") == PREVIOUS_SCHEMA_VERSION:
return migrate_previous_state(payload)
return validate_state(payload)
def update_state(root: pathlib.Path, transform: Any) -> dict[str, Any]:
state_path, lock_path = common_state_paths(root)
state_path.parent.mkdir(parents=True, exist_ok=True)
with lock_path.open("a+", encoding="utf-8") as lock:
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
state = transform(read_state(state_path))
validate_state(state)
descriptor, temporary = tempfile.mkstemp(
prefix=".state.", dir=state_path.parent
)
try:
os.fchmod(descriptor, 0o600)
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
json.dump(state, handle, sort_keys=True, separators=(",", ":"))
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, state_path)
finally:
try:
os.unlink(temporary)
except FileNotFoundError:
pass
return state
def record_coordination(root: pathlib.Path, event: str, count: int) -> dict[str, Any]:
if event not in COORDINATION_FIELDS or not 1 <= count <= 10_000:
fail("coordination event or count is invalid")
def transform(state: dict[str, Any]) -> dict[str, Any]:
updated = state["coordination"][event] + count
if updated > 1_000_000:
fail("development-flow coordination count exceeds its bound")
state["coordination"][event] = updated
return state
return update_state(root, transform)
def coordination_marker(context: dict[str, Any], state: dict[str, Any]) -> str:
created_at = context.get("created_at")
if not isinstance(created_at, str):
fail("branch claim timestamp is required for engineering metrics")
payload = {
"schema_version": COORDINATION_SCHEMA_VERSION,
"task_created_at": created_at,
**{field: state["coordination"][field] for field in COORDINATION_FIELDS},
}
return (
"<!-- apc-engineering-coordination-v1 "
+ json.dumps(payload, sort_keys=True, separators=(",", ":"))
+ " -->"
)
@dataclass(frozen=True)
class LaneDecision:
lane: str
base_branch: str | None
reason: str
active_train: str | None
def choose_lane(
requested: str,
*,
hotfix: bool,
small: bool,
agents: int,
cross_component: bool,
active_train: str | None,
explicit_train: str | None = None,
) -> LaneDecision:
if agents <= 0:
fail("agent count must be positive")
train = explicit_train or active_train
if train is not None and TRAIN_PATTERN.fullmatch(train) is None:
fail("train branch must use gd-ops/train/<name>")
if requested == "direct":
return LaneDecision("direct", MAIN_BRANCH, "agent explicitly selected direct", train)
if requested == "train":
if train is None:
fail("train lane requires --train or one active shared train")
return LaneDecision("train", train, "agent explicitly selected train", train)
if requested != "auto":
fail("lane must be auto, direct, or train")
if hotfix:
return LaneDecision("direct", MAIN_BRANCH, "hotfix uses the direct lane", train)
if train is not None:
return LaneDecision("train", train, "shared active train is available", train)
if agents > 1 or cross_component:
return LaneDecision(
"train",
None,
"parallel or cross-component work requires the main agent to start a train",
None,
)
if small:
return LaneDecision("direct", MAIN_BRANCH, "small isolated change", None)
return LaneDecision(
"direct",
MAIN_BRANCH,
"single-agent isolated work defaults to direct; the agent may choose train",
None,
)
@dataclass(frozen=True)
class CIContext:
lane: str
full_candidate: bool
release_source: bool
release_preparation: bool
def ci_context(
event: str,
base: str,
head: str,
draft: bool,
*,
release_preparation: bool = False,
) -> CIContext:
if event == "push":
if base != MAIN_BRANCH:
fail("authoritative push CI is restricted to main")
return CIContext("main-push", True, True, release_preparation)
if event != "pull_request":
fail("CI development lane supports only pull_request and push")
if base == MAIN_BRANCH:
if TRAIN_PATTERN.fullmatch(head):
lane = "train-to-main"
elif RELEASE_PATTERN.fullmatch(head):
lane = "release-preparation-to-main"
else:
lane = "direct-to-main"
return CIContext(lane, not draft, False, RELEASE_PATTERN.fullmatch(head) is not None)
if TRAIN_PATTERN.fullmatch(base):
if WORK_PATTERN.fullmatch(head) is None:
fail("a train accepts only gd-ops/task/* or gd-ops/fix/* pull requests")
if RELEASE_PATTERN.fullmatch(head):
fail("release preparation branches must target main")
return CIContext("task-to-train", False, False, False)
fail("pull request base must be main or gd-ops/train/<name>")
def ensure_new_branch(root: pathlib.Path, branch: str, worktree: pathlib.Path) -> None:
if worktree.exists():
fail(f"worktree path already exists: {worktree}")
local = run(
["git", "show-ref", "--verify", "--quiet", f"refs/heads/{branch}"],
cwd=root,
check=False,
)
if local.returncode == 0:
fail(f"local branch already exists: {branch}")
remote = run(
["git", "ls-remote", "--exit-code", "--heads", "origin", branch],
cwd=root,
check=False,
)
if remote.returncode == 0:
fail(f"remote branch already exists: {branch}")
if remote.returncode not in (0, 2):
fail(f"could not inspect remote branch {branch}: {remote.stderr.strip()}")
def command_plan(commands: list[list[str]], **metadata: Any) -> None:
print(
json.dumps(
{"apply": False, "commands": commands, **metadata},
indent=2,
sort_keys=True,
)
)
def claim_timestamp() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def remote_base_commit(root: pathlib.Path, base: str) -> str:
commit = run(
["git", "rev-parse", "--verify", f"refs/remotes/origin/{base}^{{commit}}"],
cwd=root,
).stdout.strip()
if re.fullmatch(r"[0-9a-f]{40}", commit) is None:
fail("remote base commit is invalid")
return commit
def selected_claim_context(
manifest: Manifest,
*,
domain_id: str,
claim_id: str,
approved_shared_paths: list[str],
control_plane_owner: bool,
release_preparation: bool,
base: str,
base_commit: str,
worktree: pathlib.Path,
) -> dict[str, Any]:
domain = manifest.domain(domain_id)
claim = domain.claim(claim_id)
if domain.control_plane and not control_plane_owner:
fail(
f"domain {domain.id!r} is restricted to the declared control-plane owner"
)
approved: list[str] = []
for pattern in approved_shared_paths:
shared = domain.shared(pattern)
if shared.risk == "red" and not control_plane_owner:
fail(f"Red path {pattern!r} requires the control-plane owner")
approved.append(pattern)
if len(approved) != len(set(approved)):
fail("approved shared paths contain duplicates")
return {
"base": base,
"base_commit": base_commit,
"domain": domain.id,
"claim": claim.id,
"claimed_paths": sorted(claim.paths),
"approved_shared_paths": sorted(approved),
"created_at": claim_timestamp(),
"worktree": str(worktree),
"release_preparation": release_preparation,
"control_plane_owner": control_plane_owner,
}
def context_pattern_risk(manifest: Manifest, context: dict[str, Any], pattern: str) -> str:
domain_id = context.get("domain")
if domain_id is None:
return "legacy"
domain = manifest.domain(domain_id)
for shared in domain.shared_paths:
if shared.path == pattern:
return shared.risk
return "owned"
def ensure_claim_available(
manifest: Manifest,
state: dict[str, Any],
branch: str,
context: dict[str, Any],
) -> None:
domain = manifest.domain(context["domain"])
for other_branch, other in state["branches"].items():
if other_branch == branch or other["domain"] is None:
continue
other_domain = manifest.domain(other["domain"])
overlaps = [
(path, other_path)
for path in context["claimed_paths"]
for other_path in other["claimed_paths"]
if patterns_overlap(path, other_path)
]
if overlaps:
raise ClaimOverlapError(
f"exclusive path claim overlaps active branch {other_branch}: {overlaps[0]}"
)
if domain.id == other_domain.id and (
not domain.allow_parallel_claims or not other_domain.allow_parallel_claims
):
raise ClaimOverlapError(
f"domain {domain.id!r} permits only one active claim; "
f"branch {other_branch} already owns it"
)
for pattern in context["approved_shared_paths"]:
for other_pattern in other["approved_shared_paths"]:
if not patterns_overlap(pattern, other_pattern):
continue
risks = {
context_pattern_risk(manifest, context, pattern),
context_pattern_risk(manifest, other, other_pattern),
}
if "red" in risks:
raise ClaimOverlapError(
f"Red path approval overlaps active branch {other_branch}: "
f"{pattern!r} and {other_pattern!r}"
)
def ensure_claim_available_recorded(
root: pathlib.Path,
manifest: Manifest,
state: dict[str, Any],
branch: str,
context: dict[str, Any],
) -> None:
try:
ensure_claim_available(manifest, state, branch, context)
except ClaimOverlapError:
record_coordination(root, "claim_overlap_rejections", 1)
raise
def context_allows_path(context: dict[str, Any], path: str) -> bool:
return any(
path_matches(path, pattern)
for pattern in [*context["claimed_paths"], *context["approved_shared_paths"]]
)
def changed_paths(root: pathlib.Path, base_commit: str) -> list[str]:
result = run(
["git", "diff", "--name-only", "-z", base_commit, "HEAD", "--"],
cwd=root,
)
return sorted(
{
os.fsdecode(item)
for item in result.stdout.encode("utf-8", "surrogateescape").split(b"\0")
if item
}
)
def focused_validation(manifest: Manifest, context: dict[str, Any]) -> list[str]:
if context["domain"] is None:
return []
return [
" ".join(command)
for command in manifest.domain(context["domain"]).focused_tests
]
def repository_name(root: pathlib.Path) -> str:
payload = json.loads(
run(["gh", "repo", "view", "--json", "nameWithOwner"], cwd=root).stdout
)
value = payload.get("nameWithOwner")
if not isinstance(value, str) or value.count("/") != 1:
fail("GitHub repository identity is unavailable")
return value
def train_start(args: argparse.Namespace, root: pathlib.Path) -> None:
if SLUG_PATTERN.fullmatch(args.name) is None:
fail("train name must be a lowercase slug")
branch = f"{TRAIN_PREFIX}{args.name}"
worktree = args.worktree.resolve()
state_path, _ = common_state_paths(root)
current_state = read_state(state_path)
if current_state["active_train"] not in (None, branch):
fail(f"another train is already active: {current_state['active_train']}")
ensure_new_branch(root, branch, worktree)
commands: list[list[str]] = [
["git", "fetch", "origin", MAIN_BRANCH],
[
"git",
"worktree",
"add",
"-b",
branch,
str(worktree),
f"refs/remotes/origin/{MAIN_BRANCH}",
],
["git", "-C", str(worktree), "push", "-u", "origin", branch],
]
if not args.apply:
command_plan(
commands,
lane="train",
branch=branch,
base=MAIN_BRANCH,
domain=args.domain,
claim=args.claim,
)
return
run(commands[0], cwd=root)
manifest = load_manifest(root)
context = selected_claim_context(
manifest,
domain_id=args.domain,
claim_id=args.claim,
approved_shared_paths=args.approve_shared,
control_plane_owner=args.control_plane_owner,
release_preparation=False,
base=MAIN_BRANCH,
base_commit=remote_base_commit(root, MAIN_BRANCH),
worktree=worktree,
)
ensure_claim_available_recorded(root, manifest, current_state, branch, context)
run(commands[1], cwd=root)
run(commands[2], cwd=root)
def transform(state: dict[str, Any]) -> dict[str, Any]:
if state["active_train"] not in (None, branch):
fail(f"another train is already active: {state['active_train']}")
state["active_train"] = branch
ensure_claim_available(manifest, state, branch, context)
state["branches"][branch] = context
return state
state = update_state(root, transform)
print(json.dumps(state, indent=2, sort_keys=True))
def task_start(args: argparse.Namespace, root: pathlib.Path) -> None:
if TOKEN_PATTERN.fullmatch(args.task) is None:
fail("task identifier is invalid")
if SLUG_PATTERN.fullmatch(args.slug) is None:
fail("task slug must be lowercase")
state_path, _ = common_state_paths(root)
state = read_state(state_path)
decision = choose_lane(
args.lane,
hotfix=args.hotfix,
small=args.small,
agents=args.agents,
cross_component=args.cross_component,
active_train=state["active_train"],
explicit_train=args.train,
)
if decision.base_branch is None:
fail("start a shared train before creating this parallel task worktree")
if args.release_preparation and decision.base_branch != MAIN_BRANCH:
fail("release preparation must use the direct lane to main")
kind = "release" if args.release_preparation else "fix" if args.hotfix else "task"
branch = f"gd-ops/{kind}/{args.task}-{args.slug}"
if WORK_PATTERN.fullmatch(branch) is None:
fail("generated work branch is invalid")
worktree = args.worktree.resolve()
ensure_new_branch(root, branch, worktree)
base = decision.base_branch
commands = [
["git", "fetch", "origin", base],
[
"git",
"worktree",
"add",
"-b",
branch,
str(worktree),
f"refs/remotes/origin/{base}",
],
]
if not args.apply:
command_plan(
commands,
lane=decision.lane,
reason=decision.reason,
branch=branch,
base=base,
domain=args.domain,
claim=args.claim,
release_preparation=args.release_preparation,
)
return
run(commands[0], cwd=root)
manifest = load_manifest(root)
context = selected_claim_context(
manifest,
domain_id=args.domain,
claim_id=args.claim,
approved_shared_paths=args.approve_shared,
control_plane_owner=args.control_plane_owner,
release_preparation=args.release_preparation,
base=base,
base_commit=remote_base_commit(root, base),
worktree=worktree,
)
ensure_claim_available_recorded(root, manifest, state, branch, context)
run(commands[1], cwd=root)
def transform(current: dict[str, Any]) -> dict[str, Any]:
ensure_claim_available(manifest, current, branch, context)
current["branches"][branch] = context
return current
update_state(root, transform)
print(
json.dumps(
{"lane": decision.lane, "reason": decision.reason, "branch": branch, "base": base},
indent=2,
sort_keys=True,
)
)
def branch_claim(args: argparse.Namespace, root: pathlib.Path) -> None:
worktree = args.worktree.resolve()
branch = run(["git", "branch", "--show-current"], cwd=worktree).stdout.strip()
if WORK_PATTERN.fullmatch(branch) is None and TRAIN_PATTERN.fullmatch(branch) is None:
fail("claim target must be a managed task, fix, release, or train branch")
state_path, _ = common_state_paths(root)
state = read_state(state_path)
existing = state["branches"].get(branch)
if existing is None:
fail("branch base metadata is missing; recreate the worktree with this tool")
base = existing["base"]
run(["git", "fetch", "origin", base], cwd=root)
manifest = load_manifest(root)
context = selected_claim_context(
manifest,
domain_id=args.domain,
claim_id=args.claim,
approved_shared_paths=args.approve_shared,
control_plane_owner=args.control_plane_owner,
release_preparation=args.release_preparation,
base=base,
base_commit=existing["base_commit"] or remote_base_commit(root, base),
worktree=worktree,
)
if not args.apply:
ensure_claim_available(manifest, state, branch, context)
print(
json.dumps(
{"apply": False, "branch": branch, "context": context},
indent=2,
sort_keys=True,
)
)
return
ensure_claim_available_recorded(root, manifest, state, branch, context)
def transform(current: dict[str, Any]) -> dict[str, Any]:
if branch not in current["branches"]:
fail("branch context disappeared while acquiring the state lock")
ensure_claim_available(manifest, current, branch, context)
current["branches"][branch] = context
return current
update_state(root, transform)
print(json.dumps({"branch": branch, "context": context}, indent=2, sort_keys=True))
def branch_ref(root: pathlib.Path, branch: str) -> str | None:
for ref in (branch, f"refs/remotes/origin/{branch}"):
if run(["git", "rev-parse", "--verify", f"{ref}^{{commit}}"], cwd=root, check=False).returncode == 0:
return ref
return None
def conflicts(root: pathlib.Path, *, record: bool = False) -> None:
state_path, _ = common_state_paths(root)
state = read_state(state_path)
manifest = load_manifest(root)
branches = sorted(state["branches"])
reports: list[dict[str, Any]] = []
for index, first in enumerate(branches):
first_context = state["branches"][first]
for second in branches[index + 1 :]:
second_context = state["branches"][second]
claim_overlaps = sorted(
{
f"{left} <> {right}"
for left in first_context["claimed_paths"]
for right in second_context["claimed_paths"]
if patterns_overlap(left, right)
}
)
shared_overlaps = sorted(
{
f"{left} <> {right}"
for left in first_context["approved_shared_paths"]
for right in second_context["approved_shared_paths"]
if patterns_overlap(left, right)
}
)
order_dependency = False
if first_context["domain"] and second_context["domain"]:
first_domain = manifest.domain(first_context["domain"])
second_domain = manifest.domain(second_context["domain"])
order_dependency = (
second_domain.id in first_domain.dependencies
or first_domain.id in second_domain.dependencies
or bool(shared_overlaps)
)
first_ref = branch_ref(root, first)
second_ref = branch_ref(root, second)
textual_conflict: bool | None = None
merge_detail = "branch ref unavailable"
if first_ref is not None and second_ref is not None:
result = run(
["git", "merge-tree", "--write-tree", first_ref, second_ref],
cwd=root,
check=False,
)
textual_conflict = result.returncode != 0
merge_detail = (result.stderr.strip() or result.stdout.strip())[:4000]
if claim_overlaps or shared_overlaps or order_dependency or textual_conflict:
reports.append(
{
"branches": [first, second],
"claim_overlaps": claim_overlaps,
"shared_interface_overlaps": shared_overlaps,
"shared_interface_order_dependency": order_dependency,
"textual_conflict": textual_conflict,
"merge_tree": merge_detail,
}
)
merge_tree_conflicts = sum(
report["textual_conflict"] is True for report in reports
)
if record and merge_tree_conflicts:
record_coordination(root, "merge_tree_conflicts", merge_tree_conflicts)
print(
json.dumps(
{
"active_branches": branches,
"conflicts": reports,
"recorded_merge_tree_conflicts": merge_tree_conflicts if record else 0,
},
indent=2,
sort_keys=True,
)
)
def pr_open(args: argparse.Namespace, root: pathlib.Path) -> None:
worktree = args.worktree.resolve()
branch = run(
["git", "branch", "--show-current"], cwd=worktree
).stdout.strip()
if WORK_PATTERN.fullmatch(branch) is None and TRAIN_PATTERN.fullmatch(branch) is None:
fail("PR head must be a managed task, fix, or train branch")
state_path, _ = common_state_paths(root)
state = read_state(state_path)
context = state["branches"].get(branch)
if context is None:
fail("branch base metadata is missing; recreate the worktree with this tool")
if context["domain"] is None or context["base_commit"] is None:
fail("branch has no typed path claim; run branch-claim before opening a PR")
base = context["base"]
manifest = load_manifest(root)
manifest.domain(context["domain"]).claim(context["claim"])
if context["release_preparation"] != (RELEASE_PATTERN.fullmatch(branch) is not None):
fail("release preparation identity disagrees with the managed branch name")
repository = repository_name(root)
validation = focused_validation(manifest, context)
metadata_body = "\n".join(
[
"## Development claim / 开发声明",
"",
f"- Domain: `{context['domain']}`",
f"- Claim: `{context['claim']}`",
f"- Base commit: `{context['base_commit']}`",
f"- Release preparation: `{'yes' if context['release_preparation'] else 'no'}`",
"- Focused validation:",
*[f" - `{command}`" for command in validation],
"",
coordination_marker(context, state),
]
)
if args.body_file:
body_path = args.body_file.resolve()
metadata = body_path.lstat()
if not body_path.is_file() or body_path.is_symlink() or metadata.st_size > 256 * 1024:
fail("PR body must be a bounded regular file")
body = body_path.read_text(encoding="utf-8").rstrip() + "\n\n" + metadata_body
else:
body = metadata_body
commands = [
["git", "fetch", "origin", base],
["git", "-C", str(worktree), "push", "-u", "origin", branch],
[
"gh",
"pr",
"create",
"--repo",
repository,
"--head",
branch,
"--base",
base,
"--title",
args.title,
"--body",
body,
*([] if args.ready else ["--draft"]),
],
]
if not args.apply:
command_plan(
commands,
branch=branch,
base=base,
ready=args.ready,
domain=context["domain"],
claim=context["claim"],
focused_validation=validation,
)
return
if run(["git", "status", "--porcelain"], cwd=worktree).stdout.strip():
fail("PR worktree must be clean before push")
run(commands[0], cwd=root)
if run(
["git", "merge-base", "--is-ancestor", context["base_commit"], "HEAD"],
cwd=worktree,
check=False,
).returncode != 0:
fail("recorded branch base is not an ancestor of the PR head")
paths = changed_paths(worktree, context["base_commit"])
outside = [path for path in paths if not context_allows_path(context, path)]
if outside:
fail("PR diff exceeds the declared path claim: " + ", ".join(outside))
run(commands[1], cwd=root)
local_head = run(["git", "rev-parse", "HEAD"], cwd=worktree).stdout.strip()
remote = run(["git", "ls-remote", "--heads", "origin", branch], cwd=root).stdout.strip().split()
if len(remote) != 2 or remote[0] != local_head:
fail("remote PR branch SHA does not match the clean local head")
created = run(commands[2], cwd=root).stdout.strip()
if not created.startswith("https://github.com/"):
fail("gh did not return a pull request URL")
print(
json.dumps(
{
"url": created,
"branch": branch,
"base": base,
"head": local_head,
"domain": context["domain"],
"claim": context["claim"],
"changed_paths": paths,
"focused_validation": validation,
},
sort_keys=True,
)
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--root", type=pathlib.Path, default=pathlib.Path.cwd())
subparsers = parser.add_subparsers(dest="command", required=True)
decide = subparsers.add_parser("decide")
decide.add_argument("--lane", choices=("auto", "direct", "train"), default="auto")
decide.add_argument("--hotfix", action="store_true")
decide.add_argument("--small", action="store_true")
decide.add_argument("--agents", type=int, default=1)
decide.add_argument("--cross-component", action="store_true")
decide.add_argument("--train")
ci = subparsers.add_parser("ci-context")
ci.add_argument("--event", choices=("pull_request", "push"), required=True)
ci.add_argument("--base", required=True)
ci.add_argument("--head", default="")
ci.add_argument("--draft", choices=("true", "false"), default="false")
ci.add_argument(
"--release-preparation",
choices=("true", "false", "1", "0"),
default="false",
)
ci.add_argument("--format", choices=("json", "github"), default="json")
status = subparsers.add_parser("status")
status.set_defaults()
train = subparsers.add_parser("train-start")
train.add_argument("--name", required=True)
train.add_argument("--worktree", type=pathlib.Path, required=True)
train.add_argument("--domain", required=True)
train.add_argument("--claim", required=True)
train.add_argument("--approve-shared", action="append", default=[])
train.add_argument("--control-plane-owner", action="store_true")
train.add_argument("--apply", action="store_true")
clear = subparsers.add_parser("train-clear")
clear.add_argument("--train", required=True)
clear.add_argument("--apply", action="store_true")
task = subparsers.add_parser("task-start")
task.add_argument("--task", required=True)
task.add_argument("--slug", required=True)
task.add_argument("--worktree", type=pathlib.Path, required=True)
task.add_argument("--lane", choices=("auto", "direct", "train"), default="auto")
task.add_argument("--train")
task.add_argument("--hotfix", action="store_true")
task.add_argument("--small", action="store_true")
task.add_argument("--agents", type=int, default=1)
task.add_argument("--cross-component", action="store_true")
task.add_argument("--domain", required=True)
task.add_argument("--claim", required=True)
task.add_argument("--approve-shared", action="append", default=[])
task.add_argument("--control-plane-owner", action="store_true")
task.add_argument("--release-preparation", action="store_true")
task.add_argument("--apply", action="store_true")
claim = subparsers.add_parser("branch-claim")
claim.add_argument("--worktree", type=pathlib.Path, required=True)
claim.add_argument("--domain", required=True)
claim.add_argument("--claim", required=True)
claim.add_argument("--approve-shared", action="append", default=[])
claim.add_argument("--control-plane-owner", action="store_true")
claim.add_argument("--release-preparation", action="store_true")
claim.add_argument("--apply", action="store_true")
conflicts_parser = subparsers.add_parser("conflicts")
conflicts_parser.add_argument("--record", action="store_true")
coordination = subparsers.add_parser("coordination-record")
coordination.add_argument("--event", choices=COORDINATION_FIELDS, required=True)
coordination.add_argument("--count", type=int, default=1)
coordination.add_argument("--apply", action="store_true")
pr = subparsers.add_parser("pr-open")
pr.add_argument("--worktree", type=pathlib.Path, required=True)
pr.add_argument("--title", required=True)
pr.add_argument("--body-file", type=pathlib.Path)
pr.add_argument("--ready", action="store_true")
pr.add_argument("--apply", action="store_true")
return parser.parse_args()
def main() -> int:
args = parse_args()
root = repository_root(args.root)
state_path, _ = common_state_paths(root)
if args.command == "decide":
state = read_state(state_path)
decision = choose_lane(
args.lane,
hotfix=args.hotfix,
small=args.small,
agents=args.agents,
cross_component=args.cross_component,
active_train=state["active_train"],
explicit_train=args.train,
)
print(json.dumps(asdict(decision), sort_keys=True))
elif args.command == "ci-context":
context = ci_context(
args.event,
args.base,
args.head,
args.draft == "true",
release_preparation=args.release_preparation in ("true", "1"),
)
if args.format == "github":
for key, value in asdict(context).items():
rendered = "1" if value is True else "0" if value is False else value
print(f"{key}={rendered}")
else:
print(json.dumps(asdict(context), sort_keys=True))
elif args.command == "status":
print(json.dumps(read_state(state_path), indent=2, sort_keys=True))
elif args.command == "coordination-record":
if not args.apply:
command_plan(
[], action="record-coordination", event=args.event, count=args.count
)
else:
print(
json.dumps(
record_coordination(root, args.event, args.count),
indent=2,
sort_keys=True,
)
)
elif args.command == "train-start":
train_start(args, root)
elif args.command == "train-clear":
if TRAIN_PATTERN.fullmatch(args.train) is None:
fail("train branch is invalid")
if not args.apply:
command_plan([], action="clear-active-train", train=args.train)
else:
def transform(state: dict[str, Any]) -> dict[str, Any]:
if state["active_train"] != args.train:
fail("requested train is not the active train")
related = [
branch
for branch, context in state["branches"].items()
if branch == args.train or context["base"] == args.train
]
dirty: list[str] = []
for branch in related:
worktree = state["branches"][branch]["worktree"]
if worktree and pathlib.Path(worktree).is_dir() and run(
["git", "status", "--porcelain"],
cwd=pathlib.Path(worktree),
).stdout.strip():
dirty.append(branch)
if dirty:
fail("train clear refuses dirty worktrees: " + ", ".join(dirty))
state["active_train"] = None
for branch in related:
del state["branches"][branch]
return state
print(json.dumps(update_state(root, transform), indent=2, sort_keys=True))
elif args.command == "task-start":
task_start(args, root)
elif args.command == "branch-claim":
branch_claim(args, root)
elif args.command == "conflicts":
conflicts(root, record=args.record)
else:
pr_open(args, root)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError) as error:
print(f"development-flow: {error}", file=sys.stderr)
raise SystemExit(1)