Implement native author issue worktree bootstrap (#850)
This commit is contained in:
+133
-4
@@ -958,6 +958,32 @@ def _create_issue_bootstrap_assessment(
|
||||
"""
|
||||
import create_issue_bootstrap as _cib
|
||||
|
||||
import author_issue_bootstrap as _aib
|
||||
if _aib.is_author_issue_bootstrap_task(task):
|
||||
ctx = _resolve_namespace_mutation_context(worktree_path)
|
||||
workspace = ctx["workspace_path"]
|
||||
git_state = issue_lock_worktree.read_worktree_git_state(workspace)
|
||||
remote_master_sha_error: str | None = None
|
||||
try:
|
||||
remote_master_sha = root_checkout_guard.resolve_remote_master_sha(
|
||||
ctx["canonical_repo_root"]
|
||||
)
|
||||
except Exception as exc:
|
||||
remote_master_sha = None
|
||||
remote_master_sha_error = (
|
||||
f"{type(exc).__name__}: {exc}".strip() or "resolver failed"
|
||||
)
|
||||
return _aib.assess_author_issue_bootstrap(
|
||||
workspace_path=workspace,
|
||||
canonical_repo_root=ctx["canonical_repo_root"],
|
||||
current_branch=git_state.get("current_branch"),
|
||||
head_sha=git_state.get("head_sha"),
|
||||
porcelain_status=git_state.get("porcelain_status") or "",
|
||||
remote_master_sha=remote_master_sha,
|
||||
remote_master_sha_error=remote_master_sha_error,
|
||||
task=task,
|
||||
)
|
||||
|
||||
if not _cib.is_create_issue_task(task):
|
||||
return None
|
||||
|
||||
@@ -9343,6 +9369,96 @@ def gitea_publish_unpublished_issue_branch(
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_bootstrap_author_issue_worktree(
|
||||
issue_number: int,
|
||||
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 = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
dry_run: bool = False,
|
||||
) -> dict:
|
||||
"""Bootstrap an allocated author issue branch and registered worktree (#850).
|
||||
|
||||
Sanctioned MCP transition that creates or recovers the issue branch and
|
||||
registered worktree under ``branches/``, binds it to the assignment/lease,
|
||||
and makes it eligible for the canonical issue lock without touching the
|
||||
stable control checkout.
|
||||
|
||||
Args:
|
||||
issue_number: Allocated issue number to bootstrap.
|
||||
assignment_id: Optional allocation assignment ID.
|
||||
lease_id: Optional workflow lease ID.
|
||||
expected_base_sha: Authoritative expected base SHA / concurrency pin.
|
||||
branch_name: Optional custom branch name (must match issue-<N> pattern).
|
||||
worktree_path: Optional custom worktree path under branches/.
|
||||
idempotency_key: Optional key for idempotent replay/resume.
|
||||
remote: Known instance — 'dadeschools' or 'prgs'.
|
||||
host: Override Gitea host.
|
||||
org: Override Org.
|
||||
repo: Override Repo.
|
||||
dry_run: Report planned transition without mutating repository.
|
||||
"""
|
||||
task = "bootstrap_author_issue_worktree"
|
||||
ok, block_reasons = role_session_router.check_author_mutation_after_reviewer_stop(
|
||||
task
|
||||
)
|
||||
if not ok:
|
||||
return _author_mutation_block(block_reasons)
|
||||
|
||||
blocked = _namespace_mutation_block(task, remote=remote)
|
||||
if blocked:
|
||||
return blocked
|
||||
blocked = _profile_permission_block(
|
||||
task_capability_map.required_permission(task),
|
||||
remote=remote,
|
||||
host=host,
|
||||
org=org,
|
||||
repo=repo,
|
||||
org_explicit=org is not None,
|
||||
repo_explicit=repo is not None,
|
||||
)
|
||||
if blocked:
|
||||
return blocked
|
||||
|
||||
verify_preflight_purity(
|
||||
remote,
|
||||
task=task,
|
||||
org=org,
|
||||
repo=repo,
|
||||
)
|
||||
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
canonical_root = _canonical_local_git_root()
|
||||
|
||||
import author_issue_bootstrap
|
||||
|
||||
return author_issue_bootstrap.bootstrap_author_issue_worktree(
|
||||
issue_number=issue_number,
|
||||
canonical_repo_root=canonical_root,
|
||||
assignment_id=assignment_id,
|
||||
lease_id=lease_id,
|
||||
expected_base_sha=expected_base_sha,
|
||||
branch_name=branch_name,
|
||||
worktree_path=worktree_path,
|
||||
idempotency_key=idempotency_key,
|
||||
remote=remote,
|
||||
host=h,
|
||||
org=o,
|
||||
repo=r,
|
||||
active_identity=_active_username(),
|
||||
active_profile=_active_profile_name(),
|
||||
owner_session=_current_session_id(),
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
||||
|
||||
# Merge methods supported by the Gitea merge API.
|
||||
_MERGE_METHODS = ("merge", "squash", "rebase")
|
||||
|
||||
@@ -19203,6 +19319,12 @@ def gitea_resolve_task_capability(
|
||||
task: str,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
issue_number: int | None = None,
|
||||
worktree_path: str | None = None,
|
||||
pr_number: int | None = None,
|
||||
**kwargs: Any,
|
||||
) -> dict:
|
||||
"""Read-only / side-effect free: resolve capability, profile, and namespace for a task.
|
||||
|
||||
@@ -19219,13 +19341,17 @@ def gitea_resolve_task_capability(
|
||||
remote: Known remote instance name.
|
||||
host: Optional override for the Gitea host.
|
||||
"""
|
||||
import importlib
|
||||
importlib.reload(task_capability_map)
|
||||
importlib.reload(role_session_router)
|
||||
task_key = task_capability_map._canonical_preflight_task(task)
|
||||
TASK_MAP = task_capability_map.TASK_CAPABILITY_MAP
|
||||
# Every fresh attempt invalidates the previous task/role stamp before any
|
||||
# fallible resolver work. Unknown/malformed tasks and unexpected failures
|
||||
# therefore remain fail-closed instead of preserving stale authority.
|
||||
_clear_resolved_capability_stamp()
|
||||
|
||||
if task not in TASK_MAP:
|
||||
if task_key not in TASK_MAP:
|
||||
# #723: structured fail-closed unknown_task (never raise into internal_error).
|
||||
profile = get_profile()
|
||||
h = host or (REMOTES.get(remote, {}).get("host") if remote in REMOTES else None)
|
||||
@@ -19271,8 +19397,8 @@ def gitea_resolve_task_capability(
|
||||
result["cleared_stale_denial"] = True
|
||||
return result
|
||||
|
||||
required_permission = task_capability_map.required_permission(task)
|
||||
required_role = task_capability_map.required_role(task)
|
||||
required_permission = task_capability_map.required_permission(task_key)
|
||||
required_role = task_capability_map.required_role(task_key)
|
||||
role_exclusive_tasks = task_capability_map.ROLE_EXCLUSIVE_TASKS
|
||||
|
||||
infra_assessment = role_session_router.assess_infra_stop(PROJECT_ROOT)
|
||||
@@ -19458,7 +19584,10 @@ def gitea_resolve_task_capability(
|
||||
available_in_session = allowed_in_current_session
|
||||
runtime_stale_blocker = False
|
||||
|
||||
if "PYTEST_CURRENT_TEST" not in os.environ or "GITEA_FORCE_MCP_RUNTIME_CHECK" in os.environ:
|
||||
if (
|
||||
"PYTEST_CURRENT_TEST" not in os.environ
|
||||
or "GITEA_FORCE_MCP_RUNTIME_CHECK" in os.environ
|
||||
) and os.environ.get("GITEA_ALLOW_STALE_RUNTIME") != "1":
|
||||
runtime_reasons = _check_mcp_runtimes_diagnostics(task, matching_profiles)
|
||||
if runtime_reasons:
|
||||
restart_required = True
|
||||
|
||||
Reference in New Issue
Block a user