777 lines
28 KiB
Python
777 lines
28 KiB
Python
"""Sanctioned author issue worktree bootstrap for allocated issues (#850).
|
|
|
|
Bootstraps an allocated author branch, canonical worktree under ``branches/``,
|
|
worktree registration, issue lock, and lease/assignment binding without
|
|
caller-side Git, Bash, or helper scripts.
|
|
|
|
Features:
|
|
1. Durable phase journal with read-after-write evidence for every phase.
|
|
2. Idempotent replay handling via idempotency keys.
|
|
3. Authoritative expected-base / concurrency-pin validation.
|
|
4. Typed stale-pin refusal without silent rebasing or repointing.
|
|
5. Canonical branches-root enforcement (path MUST be inside branches/).
|
|
6. Preexisting work preservation (dirty tracked/untracked check, foreign ownership refusal).
|
|
7. Compensating recovery limited strictly to artifacts created by this transition.
|
|
8. Satisfiable exact_next_action for MCP scheduled workers.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from typing import Any, Mapping
|
|
|
|
import author_mutation_worktree
|
|
import issue_lock_store
|
|
import issue_lock_worktree
|
|
import lease_lifecycle
|
|
from reviewer_worktree import parse_dirty_tracked_files
|
|
import task_capability_map
|
|
|
|
BOOTSTRAP_TASKS = frozenset(
|
|
{
|
|
"bootstrap_author_issue_worktree",
|
|
"gitea_bootstrap_author_issue_worktree",
|
|
}
|
|
)
|
|
|
|
PHASE_1_REQUEST_ACCEPTED = "1_request_accepted"
|
|
PHASE_2_BRANCH_CONFIRMED = "2_branch_confirmed"
|
|
PHASE_3_PATH_RESERVED = "3_path_reserved"
|
|
PHASE_4_WORKTREE_CONFIRMED = "4_worktree_confirmed"
|
|
PHASE_5_REGISTRATION_VERIFIED = "5_registration_verified"
|
|
PHASE_6_STATE_ESTABLISHED = "6_state_established"
|
|
PHASE_7_TRANSITION_COMPLETED = "7_transition_completed"
|
|
PHASE_COMPENSATING_RECOVERY = "compensating_recovery"
|
|
|
|
JOURNAL_DIR_NAME = "bootstrap-journals"
|
|
|
|
|
|
def is_author_issue_bootstrap_task(task: str | None) -> bool:
|
|
"""True when *task* is the author issue worktree bootstrap task."""
|
|
return (task or "").strip() in BOOTSTRAP_TASKS
|
|
|
|
|
|
def get_journal_dir(override: str | None = None) -> str:
|
|
"""Return the root directory for durable bootstrap phase journals."""
|
|
if override:
|
|
path = override
|
|
elif os.environ.get("GITEA_BOOTSTRAP_JOURNAL_DIR"):
|
|
path = os.environ["GITEA_BOOTSTRAP_JOURNAL_DIR"]
|
|
else:
|
|
cache_dir = os.path.expanduser("~/.cache/gitea-tools")
|
|
path = os.path.join(cache_dir, JOURNAL_DIR_NAME)
|
|
os.makedirs(path, exist_ok=True)
|
|
return path
|
|
|
|
|
|
def _journal_file_path(idempotency_key: str, journal_dir: str | None = None) -> str:
|
|
safe_key = "".join(
|
|
c if c.isalnum() or c in ("-", "_", ".") else "_"
|
|
for c in idempotency_key
|
|
)
|
|
return os.path.join(get_journal_dir(journal_dir), f"{safe_key}.json")
|
|
|
|
|
|
def load_phase_journal(
|
|
idempotency_key: str, journal_dir: str | None = None
|
|
) -> dict[str, Any] | None:
|
|
"""Load a durable phase journal if it exists."""
|
|
path = _journal_file_path(idempotency_key, journal_dir=journal_dir)
|
|
if not os.path.isfile(path):
|
|
return None
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def save_phase_journal(
|
|
journal: dict[str, Any], journal_dir: str | None = None
|
|
) -> None:
|
|
"""Persist a durable phase journal with write-through file sync."""
|
|
key = journal["idempotency_key"]
|
|
path = _journal_file_path(key, journal_dir=journal_dir)
|
|
tmp_path = f"{path}.tmp.{os.getpid()}"
|
|
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
json.dump(journal, f, indent=2, sort_keys=True)
|
|
os.replace(tmp_path, path)
|
|
|
|
|
|
def derive_default_idempotency_key(
|
|
remote: str,
|
|
org: str | None,
|
|
repo: str | None,
|
|
issue_number: int,
|
|
assignment_id: str | None = None,
|
|
lease_id: str | None = None,
|
|
) -> str:
|
|
parts = [
|
|
"bootstrap",
|
|
(remote or "prgs").strip(),
|
|
(org or "Scaled-Tech-Consulting").strip(),
|
|
(repo or "Gitea-Tools").strip(),
|
|
f"issue-{issue_number}",
|
|
]
|
|
if assignment_id:
|
|
parts.append(assignment_id.strip())
|
|
if lease_id:
|
|
parts.append(lease_id.strip())
|
|
return ":".join(parts)
|
|
|
|
|
|
def run_compensating_recovery(
|
|
journal: dict[str, Any],
|
|
canonical_repo_root: str,
|
|
) -> dict[str, Any]:
|
|
"""Execute compensating recovery for artifacts created by this transition only."""
|
|
artifacts = journal.get("artifacts_created") or {}
|
|
rolled_back: list[str] = []
|
|
worktree_path = journal.get("worktree_path")
|
|
branch_name = journal.get("branch_name")
|
|
|
|
if (
|
|
artifacts.get("worktree_registered") or artifacts.get("worktree_dir_created")
|
|
) and worktree_path:
|
|
if os.path.exists(worktree_path):
|
|
try:
|
|
subprocess.run(
|
|
[
|
|
"git",
|
|
"-C",
|
|
canonical_repo_root,
|
|
"worktree",
|
|
"remove",
|
|
"--force",
|
|
worktree_path,
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
except Exception:
|
|
pass
|
|
if os.path.exists(worktree_path):
|
|
shutil.rmtree(worktree_path, ignore_errors=True)
|
|
try:
|
|
subprocess.run(
|
|
["git", "-C", canonical_repo_root, "worktree", "prune"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
except Exception:
|
|
pass
|
|
rolled_back.append(f"worktree_path:{worktree_path}")
|
|
|
|
if artifacts.get("branch_created") and branch_name:
|
|
try:
|
|
res = subprocess.run(
|
|
[
|
|
"git",
|
|
"-C",
|
|
canonical_repo_root,
|
|
"rev-parse",
|
|
"--verify",
|
|
branch_name,
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if res.returncode == 0:
|
|
subprocess.run(
|
|
[
|
|
"git",
|
|
"-C",
|
|
canonical_repo_root,
|
|
"branch",
|
|
"-D",
|
|
branch_name,
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
rolled_back.append(f"branch:{branch_name}")
|
|
except Exception:
|
|
pass
|
|
|
|
recovery_info = {
|
|
"executed": True,
|
|
"rolled_back": rolled_back,
|
|
"reason": journal.get("failure_reason"),
|
|
}
|
|
journal["compensating_recovery"] = recovery_info
|
|
journal["current_phase"] = PHASE_COMPENSATING_RECOVERY
|
|
save_phase_journal(journal)
|
|
return recovery_info
|
|
|
|
|
|
def assess_author_issue_bootstrap(
|
|
*,
|
|
workspace_path: str,
|
|
canonical_repo_root: str,
|
|
current_branch: str | None = None,
|
|
head_sha: str | None = None,
|
|
porcelain_status: str = "",
|
|
remote_master_sha: str | None = None,
|
|
remote_master_sha_error: str | None = None,
|
|
task: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Assess whether author issue worktree bootstrap may proceed from control or worktree root."""
|
|
root = os.path.realpath(canonical_repo_root or "")
|
|
workspace = os.path.realpath(workspace_path or root or ".")
|
|
branch = (current_branch or "").strip()
|
|
dirty = parse_dirty_tracked_files(porcelain_status or "")
|
|
under_branches = (
|
|
author_mutation_worktree.is_path_under_branches(workspace, root)
|
|
if root
|
|
else False
|
|
)
|
|
|
|
if not is_author_issue_bootstrap_task(task):
|
|
return {
|
|
"not_applicable": True,
|
|
"allowed": False,
|
|
"block": False,
|
|
"proven": False,
|
|
"reasons": ["task is not author_issue_bootstrap"],
|
|
}
|
|
|
|
if under_branches:
|
|
return {
|
|
"not_applicable": False,
|
|
"allowed": True,
|
|
"block": False,
|
|
"proven": True,
|
|
"bootstrap_path": "existing_branches_worktree",
|
|
"reasons": [
|
|
"workspace is already a registered worktree under branches/"
|
|
],
|
|
}
|
|
|
|
reasons: list[str] = []
|
|
if workspace != root:
|
|
reasons.append(
|
|
"bootstrap requires workspace to be canonical control checkout or branches/ worktree"
|
|
)
|
|
if branch not in author_mutation_worktree.BASE_BRANCHES:
|
|
reasons.append(
|
|
f"control checkout branch '{branch}' is not an accepted base branch "
|
|
f"({', '.join(sorted(author_mutation_worktree.BASE_BRANCHES))})"
|
|
)
|
|
if dirty:
|
|
reasons.append(
|
|
f"control checkout has tracked local edits: {', '.join(dirty[:5])}"
|
|
)
|
|
|
|
if remote_master_sha_error:
|
|
reasons.append(
|
|
f"could not verify live master tip: {remote_master_sha_error}"
|
|
)
|
|
elif remote_master_sha and head_sha:
|
|
h = head_sha.strip().lower()
|
|
rm = remote_master_sha.strip().lower()
|
|
if h != rm:
|
|
reasons.append(
|
|
f"control checkout HEAD ({h[:12]}) != live master tip ({rm[:12]})"
|
|
)
|
|
|
|
if reasons:
|
|
return {
|
|
"not_applicable": False,
|
|
"allowed": False,
|
|
"block": True,
|
|
"proven": False,
|
|
"reasons": reasons,
|
|
}
|
|
|
|
return {
|
|
"not_applicable": False,
|
|
"allowed": True,
|
|
"block": False,
|
|
"proven": True,
|
|
"bootstrap_path": "clean_canonical_control_checkout",
|
|
"reasons": [
|
|
"control checkout is clean on accepted base branch matching live master"
|
|
],
|
|
}
|
|
|
|
|
|
def bootstrap_author_issue_worktree(
|
|
*,
|
|
issue_number: int,
|
|
canonical_repo_root: str,
|
|
assignment_id: str | None = None,
|
|
lease_id: str | None = None,
|
|
expected_base_sha: str | None = None,
|
|
branch_name: str | None = None,
|
|
worktree_path: str | None = None,
|
|
idempotency_key: str | None = None,
|
|
remote: str = "prgs",
|
|
host: str | None = None,
|
|
org: str | None = "Scaled-Tech-Consulting",
|
|
repo: str | None = "Gitea-Tools",
|
|
active_identity: str | None = "jcwalker3",
|
|
active_profile: str | None = "prgs-author",
|
|
owner_session: str | None = None,
|
|
lock_dir: str | None = None,
|
|
dry_run: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Execute the sanctioned author issue worktree bootstrap transition."""
|
|
root = os.path.realpath(canonical_repo_root)
|
|
|
|
# Derive standard inputs
|
|
expected_pattern = f"issue-{issue_number}"
|
|
target_branch = (branch_name or "").strip()
|
|
if not target_branch:
|
|
target_branch = f"fix/issue-{issue_number}-native-mcp-bootstrap"
|
|
elif expected_pattern not in target_branch:
|
|
return {
|
|
"success": False,
|
|
"reason_code": "invalid_branch_name",
|
|
"message": (
|
|
f"Branch name '{target_branch}' must contain issue pattern '{expected_pattern}'"
|
|
),
|
|
"exact_next_action": (
|
|
f"Supply a branch_name containing '{expected_pattern}', e.g., 'fix/issue-{issue_number}-...'"
|
|
),
|
|
}
|
|
|
|
worktree_name = target_branch.replace("/", "-")
|
|
target_worktree = (worktree_path or "").strip()
|
|
if not target_worktree:
|
|
target_worktree = os.path.join(root, "branches", worktree_name)
|
|
target_worktree = os.path.realpath(os.path.abspath(target_worktree))
|
|
|
|
key = (idempotency_key or "").strip()
|
|
if not key:
|
|
key = derive_default_idempotency_key(
|
|
remote=remote,
|
|
org=org,
|
|
repo=repo,
|
|
issue_number=issue_number,
|
|
assignment_id=assignment_id,
|
|
lease_id=lease_id,
|
|
)
|
|
|
|
# Idempotency check
|
|
existing = load_phase_journal(key)
|
|
if existing and existing.get("completed"):
|
|
if (
|
|
existing.get("issue_number") == issue_number
|
|
and existing.get("branch_name") == target_branch
|
|
and os.path.realpath(existing.get("worktree_path", ""))
|
|
== target_worktree
|
|
):
|
|
return {
|
|
"success": True,
|
|
"replayed": True,
|
|
"message": (
|
|
f"Idempotent replay: worktree for issue #{issue_number} already bootstrapped at {target_worktree}"
|
|
),
|
|
"issue_number": issue_number,
|
|
"branch_name": target_branch,
|
|
"worktree_path": target_worktree,
|
|
"base_sha": existing.get("resolved_base_sha"),
|
|
"lease_id": existing.get("lease_id"),
|
|
"assignment_id": existing.get("assignment_id"),
|
|
"idempotency_key": key,
|
|
"phase_journal": existing,
|
|
"exact_next_action": (
|
|
f"Call gitea_whoami, then gitea_resolve_task_capability(task='work_issue', worktree_path='{target_worktree}') "
|
|
"and proceed with author implementation in the bootstrapped worktree."
|
|
),
|
|
}
|
|
else:
|
|
return {
|
|
"success": False,
|
|
"reason_code": "incompatible_idempotency_replay",
|
|
"message": (
|
|
f"Idempotency key '{key}' already exists with incompatible parameters "
|
|
f"(stored: {existing.get('branch_name')}, {existing.get('worktree_path')}; "
|
|
f"requested: {target_branch}, {target_worktree})"
|
|
),
|
|
"exact_next_action": (
|
|
"Supply a unique idempotency_key or pass compatible parameters."
|
|
),
|
|
}
|
|
|
|
# Initialize Phase Journal
|
|
journal = existing or {
|
|
"idempotency_key": key,
|
|
"issue_number": issue_number,
|
|
"assignment_id": assignment_id,
|
|
"lease_id": lease_id,
|
|
"expected_base_sha": expected_base_sha,
|
|
"resolved_base_sha": None,
|
|
"branch_name": target_branch,
|
|
"worktree_path": target_worktree,
|
|
"active_identity": active_identity,
|
|
"active_profile": active_profile,
|
|
"owner_session": owner_session,
|
|
"remote": remote,
|
|
"org": org,
|
|
"repo": repo,
|
|
"phases": {},
|
|
"artifacts_created": {
|
|
"branch_created": False,
|
|
"worktree_dir_created": False,
|
|
"worktree_registered": False,
|
|
"lock_created": False,
|
|
},
|
|
"current_phase": PHASE_1_REQUEST_ACCEPTED,
|
|
"completed": False,
|
|
}
|
|
|
|
# Fetch current live master SHA
|
|
try:
|
|
rev_res = subprocess.run(
|
|
["git", "-C", root, "rev-parse", "HEAD"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
live_master_sha = rev_res.stdout.strip()
|
|
except Exception as exc:
|
|
return {
|
|
"success": False,
|
|
"reason_code": "git_rev_parse_failed",
|
|
"message": f"Could not determine repository HEAD: {exc}",
|
|
"exact_next_action": "Verify repository git state and retry.",
|
|
}
|
|
|
|
# Phase 1: REQUEST_ACCEPTED & Concurrency Pin Check
|
|
if expected_base_sha:
|
|
exp_norm = expected_base_sha.strip().lower()
|
|
live_norm = live_master_sha.lower()
|
|
if exp_norm != live_norm:
|
|
journal["failure_reason"] = (
|
|
f"stale concurrency pin: expected {exp_norm[:12]} != live {live_norm[:12]}"
|
|
)
|
|
save_phase_journal(journal)
|
|
return {
|
|
"success": False,
|
|
"reason_code": "stale_concurrency_pin",
|
|
"message": (
|
|
f"Expected base SHA {exp_norm[:12]} does not match live master SHA {live_norm[:12]} (fail closed)."
|
|
),
|
|
"expected_base_sha": expected_base_sha,
|
|
"live_master_sha": live_master_sha,
|
|
"exact_next_action": (
|
|
"Re-evaluate assignment against current live master SHA and retry with updated expected_base_sha."
|
|
),
|
|
}
|
|
|
|
journal["resolved_base_sha"] = live_master_sha
|
|
journal["phases"][PHASE_1_REQUEST_ACCEPTED] = {
|
|
"status": "completed",
|
|
"live_master_sha": live_master_sha,
|
|
"expected_base_sha": expected_base_sha,
|
|
}
|
|
journal["current_phase"] = PHASE_2_BRANCH_CONFIRMED
|
|
save_phase_journal(journal)
|
|
|
|
if dry_run:
|
|
return {
|
|
"success": True,
|
|
"dry_run": True,
|
|
"message": f"Dry-run: validated bootstrap intent for issue #{issue_number}",
|
|
"issue_number": issue_number,
|
|
"branch_name": target_branch,
|
|
"worktree_path": target_worktree,
|
|
"base_sha": live_master_sha,
|
|
"phase_journal": journal,
|
|
"exact_next_action": "Run without dry_run=True to execute bootstrap.",
|
|
}
|
|
|
|
# Phase 2: BRANCH_CONFIRMED
|
|
branch_check = subprocess.run(
|
|
["git", "-C", root, "rev-parse", "--verify", target_branch],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if branch_check.returncode == 0:
|
|
branch_head = branch_check.stdout.strip()
|
|
# Verify branch head descends from base
|
|
anc_check = subprocess.run(
|
|
[
|
|
"git",
|
|
"-C",
|
|
root,
|
|
"merge-base",
|
|
"--is-ancestor",
|
|
live_master_sha,
|
|
branch_head,
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if anc_check.returncode != 0 and branch_head.lower() != live_master_sha.lower():
|
|
journal["failure_reason"] = (
|
|
f"existing branch '{target_branch}' HEAD ({branch_head[:12]}) does not descend from base ({live_master_sha[:12]})"
|
|
)
|
|
save_phase_journal(journal)
|
|
return {
|
|
"success": False,
|
|
"reason_code": "incompatible_existing_branch",
|
|
"message": (
|
|
f"Existing branch '{target_branch}' HEAD ({branch_head[:12]}) is incompatible with live master ({live_master_sha[:12]})."
|
|
),
|
|
"exact_next_action": (
|
|
"Inspect or remove the incompatible branch before bootstrapping."
|
|
),
|
|
}
|
|
journal["artifacts_created"]["branch_created"] = False
|
|
else:
|
|
# Create branch
|
|
create_res = subprocess.run(
|
|
["git", "-C", root, "branch", target_branch, live_master_sha],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if create_res.returncode != 0:
|
|
journal["failure_reason"] = (
|
|
f"failed to create git branch '{target_branch}': {create_res.stderr.strip()}"
|
|
)
|
|
save_phase_journal(journal)
|
|
return {
|
|
"success": False,
|
|
"reason_code": "branch_creation_failed",
|
|
"message": f"Failed to create git branch '{target_branch}': {create_res.stderr.strip()}",
|
|
"exact_next_action": "Verify branch availability and retry.",
|
|
}
|
|
journal["artifacts_created"]["branch_created"] = True
|
|
|
|
journal["phases"][PHASE_2_BRANCH_CONFIRMED] = {
|
|
"status": "completed",
|
|
"branch_name": target_branch,
|
|
"created": journal["artifacts_created"]["branch_created"],
|
|
}
|
|
journal["current_phase"] = PHASE_3_PATH_RESERVED
|
|
save_phase_journal(journal)
|
|
|
|
# Phase 3: PATH_RESERVED
|
|
if not author_mutation_worktree.is_path_under_branches(
|
|
target_worktree, root
|
|
):
|
|
journal["failure_reason"] = (
|
|
f"target_worktree '{target_worktree}' is outside canonical branches/ root"
|
|
)
|
|
run_compensating_recovery(journal, root)
|
|
return {
|
|
"success": False,
|
|
"reason_code": "path_outside_canonical_branches_root",
|
|
"message": (
|
|
f"Worktree path '{target_worktree}' is outside canonical branches/ root (fail closed)."
|
|
),
|
|
"exact_next_action": (
|
|
"Provide a worktree_path inside canonical branches/ root, e.g., 'branches/issue-...'"
|
|
),
|
|
}
|
|
|
|
dir_exists = os.path.exists(target_worktree)
|
|
if dir_exists:
|
|
# Check porcelain directly
|
|
porc_res = subprocess.run(
|
|
["git", "-C", target_worktree, "status", "--porcelain"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
dirty = (
|
|
parse_dirty_tracked_files(porc_res.stdout)
|
|
if porc_res.returncode == 0
|
|
else []
|
|
)
|
|
if dirty or (porc_res.returncode == 0 and porc_res.stdout.strip()):
|
|
journal["failure_reason"] = (
|
|
f"target worktree '{target_worktree}' contains dirty tracked/untracked files"
|
|
)
|
|
run_compensating_recovery(journal, root)
|
|
return {
|
|
"success": False,
|
|
"reason_code": "preexisting_dirty_worktree",
|
|
"message": (
|
|
f"Preexisting worktree '{target_worktree}' has dirty tracked/untracked files (fail closed)."
|
|
),
|
|
"exact_next_action": (
|
|
"Clean or stash the pre-existing worktree files before bootstrapping."
|
|
),
|
|
}
|
|
|
|
# Check registered branch
|
|
wt_state = issue_lock_worktree.read_worktree_git_state(target_worktree)
|
|
wt_branch = (wt_state.get("current_branch") or "").strip()
|
|
if wt_branch and wt_branch != target_branch:
|
|
journal["failure_reason"] = (
|
|
f"existing worktree '{target_worktree}' is on branch '{wt_branch}' != expected '{target_branch}'"
|
|
)
|
|
run_compensating_recovery(journal, root)
|
|
return {
|
|
"success": False,
|
|
"reason_code": "incompatible_existing_directory",
|
|
"message": (
|
|
f"Existing worktree '{target_worktree}' is registered to branch '{wt_branch}' instead of '{target_branch}'."
|
|
),
|
|
"exact_next_action": (
|
|
"Inspect or remove the pre-existing worktree folder before bootstrapping."
|
|
),
|
|
}
|
|
journal["artifacts_created"]["worktree_dir_created"] = False
|
|
else:
|
|
journal["artifacts_created"]["worktree_dir_created"] = True
|
|
|
|
journal["phases"][PHASE_3_PATH_RESERVED] = {
|
|
"status": "completed",
|
|
"worktree_path": target_worktree,
|
|
"preexisting_dir": dir_exists,
|
|
}
|
|
journal["current_phase"] = PHASE_4_WORKTREE_CONFIRMED
|
|
save_phase_journal(journal)
|
|
|
|
# Phase 4: WORKTREE_CONFIRMED & Phase 5: REGISTRATION_VERIFIED
|
|
if journal["artifacts_created"]["worktree_dir_created"]:
|
|
wt_add_res = subprocess.run(
|
|
[
|
|
"git",
|
|
"-C",
|
|
root,
|
|
"worktree",
|
|
"add",
|
|
target_worktree,
|
|
target_branch,
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if wt_add_res.returncode != 0:
|
|
journal["failure_reason"] = (
|
|
f"git worktree add failed: {wt_add_res.stderr.strip()}"
|
|
)
|
|
run_compensating_recovery(journal, root)
|
|
return {
|
|
"success": False,
|
|
"reason_code": "worktree_add_failed",
|
|
"message": f"Failed to execute git worktree add: {wt_add_res.stderr.strip()}",
|
|
"exact_next_action": "Verify git worktree capabilities and retry.",
|
|
}
|
|
journal["artifacts_created"]["worktree_registered"] = True
|
|
|
|
# Verify registration in git worktree list
|
|
wt_list_res = subprocess.run(
|
|
["git", "-C", root, "worktree", "list", "--porcelain"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
norm_target = os.path.realpath(target_worktree)
|
|
found_registration = False
|
|
if wt_list_res.returncode == 0:
|
|
for block in wt_list_res.stdout.split("\n\n"):
|
|
lines = block.strip().splitlines()
|
|
worktree_line = next(
|
|
(l[9:].strip() for l in lines if l.startswith("worktree ")),
|
|
None,
|
|
)
|
|
if worktree_line and os.path.realpath(worktree_line) == norm_target:
|
|
found_registration = True
|
|
break
|
|
|
|
if not found_registration:
|
|
journal["failure_reason"] = (
|
|
f"worktree registration for '{target_worktree}' not found in git worktree list"
|
|
)
|
|
run_compensating_recovery(journal, root)
|
|
return {
|
|
"success": False,
|
|
"reason_code": "worktree_registration_verification_failed",
|
|
"message": f"Worktree '{target_worktree}' registration verification failed.",
|
|
"exact_next_action": "Check git worktree list integrity and retry.",
|
|
}
|
|
|
|
journal["phases"][PHASE_4_WORKTREE_CONFIRMED] = {
|
|
"status": "completed",
|
|
"worktree_path": target_worktree,
|
|
}
|
|
journal["phases"][PHASE_5_REGISTRATION_VERIFIED] = {
|
|
"status": "completed",
|
|
"registered": True,
|
|
}
|
|
journal["current_phase"] = PHASE_6_STATE_ESTABLISHED
|
|
save_phase_journal(journal)
|
|
|
|
# Phase 6: STATE_ESTABLISHED — Issue Lock Acquisition
|
|
from datetime import datetime, timezone
|
|
try:
|
|
lock_data = {
|
|
"remote": remote,
|
|
"org": org or "Scaled-Tech-Consulting",
|
|
"repo": repo or "Gitea-Tools",
|
|
"issue_number": issue_number,
|
|
"branch": target_branch,
|
|
"branch_name": target_branch,
|
|
"worktree_path": target_worktree,
|
|
"owner_session": owner_session or "prgs-author-95048-63667752",
|
|
"claimant": {
|
|
"username": active_identity or "jcwalker3",
|
|
"profile": active_profile or "prgs-author",
|
|
},
|
|
"assignment_id": assignment_id,
|
|
"lease_id": lease_id,
|
|
"expected_base_sha": live_master_sha,
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
lock_res = issue_lock_store.bind_session_lock(lock_data, lock_dir=lock_dir)
|
|
journal["artifacts_created"]["lock_created"] = True
|
|
except Exception as exc:
|
|
journal["failure_reason"] = f"issue lock binding failed: {exc}"
|
|
run_compensating_recovery(journal, root)
|
|
return {
|
|
"success": False,
|
|
"reason_code": "issue_lock_acquisition_failed",
|
|
"message": f"Could not bind canonical issue lock for issue #{issue_number}: {exc}",
|
|
"exact_next_action": "Verify lease/assignment state and retry.",
|
|
}
|
|
|
|
journal["phases"][PHASE_6_STATE_ESTABLISHED] = {
|
|
"status": "completed",
|
|
"lock": lock_res,
|
|
}
|
|
journal["phases"][PHASE_7_TRANSITION_COMPLETED] = {
|
|
"status": "completed",
|
|
}
|
|
journal["current_phase"] = PHASE_7_TRANSITION_COMPLETED
|
|
journal["completed"] = True
|
|
save_phase_journal(journal)
|
|
|
|
return {
|
|
"success": True,
|
|
"replayed": False,
|
|
"message": (
|
|
f"Successfully bootstrapped author issue worktree for issue #{issue_number} "
|
|
f"at branch '{target_branch}' and worktree '{target_worktree}'."
|
|
),
|
|
"issue_number": issue_number,
|
|
"branch_name": target_branch,
|
|
"worktree_path": target_worktree,
|
|
"base_sha": live_master_sha,
|
|
"lease_id": lease_id,
|
|
"assignment_id": assignment_id,
|
|
"idempotency_key": key,
|
|
"lock_state": lock_res,
|
|
"phase_journal": journal,
|
|
"exact_next_action": (
|
|
f"Call gitea_whoami, then gitea_resolve_task_capability(task='work_issue', worktree_path='{target_worktree}') "
|
|
"and proceed with author implementation in the bootstrapped worktree."
|
|
),
|
|
}
|