fix(drone): protect dev on merge + live-remote branches + scope footer (#625, #623)

#625 (HIGH): drone @git merge passed --delete-branch to gh pr merge
unconditionally, so merging a dev->main PR DELETED the persistent dev branch
on the remote and left the tree on main (next commit silently on main). Now:
- merge looks up the PR head ref and only appends --delete-branch for
  non-protected branches; dev/main are never deleted. Unknown head ref fails
  SAFE (no delete) — devpulse hardening on top of @drone's protected-branch set.
- after merge, return the working tree to dev (loud warning if it can't).
- branches_handler runs git fetch --prune before git branch -r (no more
  'cached lies' reporting deleted branches as live).
- new drone @git prune-temp cleans merged temp PR branches (citizen/*).

#623: status/diff append a '(showing <branch> scope — use --all for full repo)'
footer when scoped, so an empty scoped view isn't mistaken for a clean repo.
Blank-output sub-item not reproducible — documented.

@drone built fixes 1-5 (FPLAN-0236); devpulse added the unknown-head-ref
fail-safe + test and verified independently. drone suite 716 pass, seedgo 99%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AIOSAI
2026-06-02 17:39:01 -07:00
co-authored by Claude Opus 4.8
parent bedf58e7b5
commit 895b8f04fd
5 changed files with 544 additions and 50 deletions
+45 -47
View File
@@ -61,6 +61,7 @@ _COMMANDS = (
"smart-sync",
"fix",
"pr",
"prune-temp",
)
_GH_PASSTHROUGH_COMMANDS = ("issue", "run", "workflow")
@@ -167,6 +168,8 @@ def handle_command(command: str | None = None, args: list[str] | None = None) ->
return _handle_fix(args, caller)
if command == "pr":
return _handle_pr(args)
if command == "prune-temp":
return _handle_prune_temp()
available = ", ".join(_COMMANDS)
return {
@@ -219,6 +222,15 @@ def _handle_branches() -> dict:
return {"stdout": result["message"], "stderr": "", "exit_code": 0}
def _handle_prune_temp() -> dict:
"""Handle the prune-temp subcommand — delete merged citizen/* branches."""
result = branches_handler.prune_temp_branches()
lines = [result["message"]]
for name in result.get("pruned", []):
lines.append(f" deleted: {name}")
return {"stdout": "\n".join(lines), "stderr": "", "exit_code": 0}
def _handle_pr(args: list[str]) -> dict:
"""Handle the pr subcommand — push current branch and create PR to main."""
if not args:
@@ -356,6 +368,8 @@ def _handle_status(args: list[str] | None = None) -> dict:
_, branch_dir = detected
branch_name = detected[0]
if show_all:
repo_root = lock_handler.find_repo_root()
result = status_handler.get_branch_status(repo_root)
@@ -367,6 +381,9 @@ def _handle_status(args: list[str] | None = None) -> dict:
for f in result["files"]:
lines.append(f" {f['status']:>2} {f['path']}")
if not show_all:
lines.append(f"(showing {branch_name} scope — use --all for full repo)")
return {
"stdout": "\n".join(lines),
"stderr": "",
@@ -384,18 +401,18 @@ def _handle_diff(args: list[str]) -> dict:
"exit_code": 1,
}
_, branch_dir = detected
branch_name, branch_dir = detected
staged = "--staged" in args
show_all = "--all" in args
target_dir = lock_handler.find_repo_root() if show_all else branch_dir
result = diff_handler.get_branch_diff(target_dir, staged=staged)
return {
"stdout": result["diff"] if result["diff"] else result["message"],
"stderr": "",
"exit_code": 0,
}
output = result["diff"] if result["diff"] else result["message"]
if not show_all:
output += f"\n(showing {branch_name} scope — use --all for full repo)"
return {"stdout": output, "stderr": "", "exit_code": 0}
def _handle_log(args: list[str]) -> dict:
@@ -610,17 +627,16 @@ def get_help(command: str | None = None) -> str:
return (
"git — Tier-based git workflow (dev branch model)\n"
"\n"
"Global (all branches):\n"
" status Show git status for your branch\n"
" diff [--staged] Show git diff for your branch\n"
" log [count] Show recent git log (default: 10)\n"
" lock Check lock status\n"
" branches List remote branches\n"
" prune-temp Delete merged citizen/* temp branches\n"
" issue [args] Passthrough to gh issue\n"
" run [args] Passthrough to gh run\n"
" workflow [args] Passthrough to gh workflow\n"
"\n"
"Owner (devpulse only):\n"
" commit <msg> [--all | files] Commit changes (selective or --all)\n"
" checkout <main|dev> Switch branches\n"
@@ -642,52 +658,34 @@ def get_introspective() -> str:
"@git — Tier-based git workflow, dev branch model (v3.0.0)\n"
"Connected Handlers:\n"
" handlers/git/\n"
" - lock_handler.py (acquire_lock, release_lock, check_lock_status, force_unlock)\n"
" - status_handler.py (get_branch_status — scoped git status)\n"
" - diff_handler.py (get_branch_diff — scoped git diff)\n"
" - log_handler.py (get_git_log — recent log entries)\n"
" - commit_handler.py (commit_changes — selective files, --all, or pre-staged)\n"
" - checkout_handler.py (checkout_branch — main/dev only)\n"
" - sync_handler.py (sync_main — safe main synchronization)\n"
" - dev_pr_handler.py (create_branch_pr, create_dev_pr — PR to main)\n"
" - branches_handler.py (list_remote_branches)\n"
" - delete_branch_handler.py (delete_remote_branch — protected: main/dev)\n"
" - close_pr_handler.py (close_pr — close PR by number)\n"
"\n"
" - lock_handler.py, status_handler.py, diff_handler.py, log_handler.py\n"
" - commit_handler.py, checkout_handler.py, sync_handler.py\n"
" - dev_pr_handler.py, branches_handler.py, delete_branch_handler.py, close_pr_handler.py\n"
" plugins/devpulse_ops/\n"
" - auth.py (verify_git_access — tier-based authorization)\n"
" - merge_plugin.py (merge_pr — merge PR + sync)\n"
" - sync_plugin.py (smart_sync — fetch + rebase if behind)\n"
" - fix_plugin.py (fix_git_state — detect/fix broken states)\n"
"\n"
" gh passthrough:\n"
" - issue, run, workflow → subprocess gh <cmd> [args]\n"
"\n"
"Access Tiers: global (status, diff, log, lock, branches, issue, run, workflow) | owner (pr, commit, checkout, dev-pr, delete-branch, close-pr, sync, unlock, merge, smart-sync, fix)\n"
" - auth.py, merge_plugin.py, sync_plugin.py, fix_plugin.py\n"
" gh passthrough: issue, run, workflow\n"
"Tiers: global (status,diff,log,lock,branches,prune-temp,issue,run,workflow)"
" | owner (pr,commit,checkout,dev-pr,delete-branch,close-pr,sync,unlock,merge,smart-sync,fix)\n"
)
def _get_console():
try:
from aipass.cli.apps.modules.display import console
return console
except ImportError:
logger.warning("CLI console not available, using fallback")
from rich.console import Console
return Console()
def print_introspection() -> None:
"""Print introspection (seedgo compliance)."""
try:
from aipass.cli.apps.modules.display import console
except ImportError:
logger.warning("CLI console not available, using fallback")
from rich.console import Console
console = Console()
console.print(get_introspective())
_get_console().print(get_introspective())
def print_help() -> None:
"""Print help (seedgo compliance)."""
try:
from aipass.cli.apps.modules.display import console
except ImportError:
logger.warning("CLI console not available, using fallback")
from rich.console import Console
console = Console()
console.print(get_help())
_get_console().print(get_help())