feat(system): test
Co-Authored-By: @devpulse <devpulse@aipass>
This commit is contained in:
@@ -20,15 +20,37 @@ from pathlib import Path
|
||||
|
||||
from aipass.prax import logger
|
||||
from aipass.drone.apps.handlers.json import json_handler
|
||||
from aipass.drone.apps.handlers.git import lock_handler, status_handler, sync_handler, pr_handler
|
||||
from aipass.drone.apps.handlers.git import (
|
||||
lock_handler,
|
||||
status_handler,
|
||||
sync_handler,
|
||||
diff_handler,
|
||||
log_handler,
|
||||
commit_handler,
|
||||
checkout_handler,
|
||||
)
|
||||
|
||||
DRONE_MODULE = {
|
||||
"name": "git",
|
||||
"version": "1.0.0",
|
||||
"description": "Git workflow — PR, status, sync, lock management",
|
||||
"version": "2.0.0",
|
||||
"description": "Git workflow — tier-based access, status, diff, log, commit, checkout, sync, lock",
|
||||
}
|
||||
|
||||
_COMMANDS = ("pr", "status", "sync", "lock", "unlock", "system-pr", "merge", "smart-sync", "fix")
|
||||
_COMMANDS = (
|
||||
"status",
|
||||
"diff",
|
||||
"log",
|
||||
"lock",
|
||||
"commit",
|
||||
"checkout",
|
||||
"sync",
|
||||
"unlock",
|
||||
"system-pr",
|
||||
"merge",
|
||||
"smart-sync",
|
||||
"fix",
|
||||
"pr",
|
||||
)
|
||||
|
||||
|
||||
def _detect_branch_dir() -> tuple[str, Path] | None:
|
||||
@@ -63,42 +85,64 @@ def _detect_branch_dir() -> tuple[str, Path] | None:
|
||||
def handle_command(command: str | None = None, args: list[str] | None = None) -> dict:
|
||||
"""Route a git command to the appropriate handler.
|
||||
|
||||
Auth is centralized: verify_git_access() is called once at the top,
|
||||
before any routing. Global-tier commands pass for all callers;
|
||||
owner-tier commands require devpulse.
|
||||
|
||||
Args:
|
||||
command: The subcommand (pr, status, sync, lock, unlock).
|
||||
command: The subcommand (status, diff, log, commit, checkout, etc.).
|
||||
args: Optional list of arguments.
|
||||
|
||||
Returns:
|
||||
Dict with stdout, stderr, and exit_code.
|
||||
"""
|
||||
if not args:
|
||||
if command is None:
|
||||
print_introspection()
|
||||
return {"stdout": "", "stderr": "", "exit_code": 0}
|
||||
args = []
|
||||
if command in ("--help", "-h") or (args and args[0] in ("--help", "-h")):
|
||||
print_help()
|
||||
return {"stdout": "", "stderr": "", "exit_code": 0}
|
||||
|
||||
json_handler.log_operation("git_handle_command", {"command": command, "args": args})
|
||||
if command is None:
|
||||
print_introspection()
|
||||
return {"stdout": "", "stderr": "", "exit_code": 0}
|
||||
|
||||
cmd: str = command
|
||||
try:
|
||||
from aipass.drone.apps.plugins.devpulse_ops.auth import verify_git_access
|
||||
|
||||
caller = verify_git_access(cmd)
|
||||
except PermissionError as exc:
|
||||
logger.error("git access denied: %s", exc)
|
||||
return {"stdout": "", "stderr": str(exc), "exit_code": 1}
|
||||
|
||||
json_handler.log_operation("git_handle_command", {"command": command, "args": args, "caller": caller})
|
||||
|
||||
if command == "system-pr":
|
||||
return _handle_system_pr(args)
|
||||
if command == "merge":
|
||||
return _handle_merge(args)
|
||||
if command == "smart-sync":
|
||||
return _handle_smart_sync(args)
|
||||
if command == "fix":
|
||||
return _handle_fix(args)
|
||||
if command == "pr":
|
||||
return _handle_pr(args)
|
||||
if command == "status":
|
||||
return _handle_status()
|
||||
if command == "sync":
|
||||
return _handle_sync(args)
|
||||
if command == "diff":
|
||||
return _handle_diff(args)
|
||||
if command == "log":
|
||||
return _handle_log(args)
|
||||
if command == "lock":
|
||||
return _handle_lock()
|
||||
if command == "commit":
|
||||
return _handle_commit(args)
|
||||
if command == "checkout":
|
||||
return _handle_checkout(args)
|
||||
if command == "sync":
|
||||
return _handle_sync(args)
|
||||
if command == "unlock":
|
||||
return _handle_unlock(args)
|
||||
if command == "system-pr":
|
||||
return _handle_system_pr(args, caller)
|
||||
if command == "merge":
|
||||
return _handle_merge(args, caller)
|
||||
if command == "smart-sync":
|
||||
return _handle_smart_sync(caller)
|
||||
if command == "fix":
|
||||
return _handle_fix(args, caller)
|
||||
if command == "pr":
|
||||
return {"stdout": "", "stderr": "Agent PRs are deprecated.", "exit_code": 1}
|
||||
|
||||
available = ", ".join(_COMMANDS)
|
||||
return {
|
||||
@@ -108,8 +152,8 @@ def handle_command(command: str | None = None, args: list[str] | None = None) ->
|
||||
}
|
||||
|
||||
|
||||
def _handle_system_pr(args: list[str]) -> dict:
|
||||
"""Handle the system-pr subcommand (devpulse-only)."""
|
||||
def _handle_system_pr(args: list[str], caller: str) -> dict:
|
||||
"""Handle the system-pr subcommand (owner-tier, auth pre-checked)."""
|
||||
if not args:
|
||||
return {
|
||||
"stdout": "",
|
||||
@@ -120,7 +164,6 @@ def _handle_system_pr(args: list[str]) -> dict:
|
||||
description = " ".join(args)
|
||||
|
||||
try:
|
||||
from aipass.drone.apps.plugins.devpulse_ops.auth import verify_caller
|
||||
from aipass.drone.apps.plugins.devpulse_ops.pr_plugin import create_system_pr
|
||||
except ImportError as exc:
|
||||
logger.error("Failed to import devpulse_ops plugin: %s", exc)
|
||||
@@ -130,16 +173,6 @@ def _handle_system_pr(args: list[str]) -> dict:
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
try:
|
||||
caller = verify_caller()
|
||||
except PermissionError as exc:
|
||||
logger.error("system-pr authorization failed: %s", exc)
|
||||
return {
|
||||
"stdout": "",
|
||||
"stderr": str(exc),
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
result = create_system_pr(description, caller)
|
||||
|
||||
if result["success"]:
|
||||
@@ -155,8 +188,8 @@ def _handle_system_pr(args: list[str]) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _handle_merge(args: list[str]) -> dict:
|
||||
"""Handle the merge subcommand (devpulse-only)."""
|
||||
def _handle_merge(args: list[str], caller: str) -> dict:
|
||||
"""Handle the merge subcommand (owner-tier, auth pre-checked)."""
|
||||
if not args:
|
||||
return {
|
||||
"stdout": "",
|
||||
@@ -167,7 +200,6 @@ def _handle_merge(args: list[str]) -> dict:
|
||||
pr_number = args[0]
|
||||
|
||||
try:
|
||||
from aipass.drone.apps.plugins.devpulse_ops.auth import verify_caller
|
||||
from aipass.drone.apps.plugins.devpulse_ops.merge_plugin import merge_pr
|
||||
except ImportError as exc:
|
||||
logger.error("Failed to import devpulse_ops merge plugin: %s", exc)
|
||||
@@ -177,16 +209,6 @@ def _handle_merge(args: list[str]) -> dict:
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
try:
|
||||
caller = verify_caller()
|
||||
except PermissionError as exc:
|
||||
logger.error("merge authorization failed: %s", exc)
|
||||
return {
|
||||
"stdout": "",
|
||||
"stderr": str(exc),
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
result = merge_pr(pr_number, caller)
|
||||
|
||||
if result["success"]:
|
||||
@@ -202,10 +224,9 @@ def _handle_merge(args: list[str]) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _handle_smart_sync(args: list[str]) -> dict:
|
||||
"""Handle the smart-sync subcommand (devpulse-only)."""
|
||||
def _handle_smart_sync(caller: str) -> dict:
|
||||
"""Handle the smart-sync subcommand (owner-tier, auth pre-checked)."""
|
||||
try:
|
||||
from aipass.drone.apps.plugins.devpulse_ops.auth import verify_caller
|
||||
from aipass.drone.apps.plugins.devpulse_ops.sync_plugin import smart_sync
|
||||
except ImportError as exc:
|
||||
logger.error("Failed to import devpulse_ops sync plugin: %s", exc)
|
||||
@@ -215,16 +236,6 @@ def _handle_smart_sync(args: list[str]) -> dict:
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
try:
|
||||
caller = verify_caller()
|
||||
except PermissionError as exc:
|
||||
logger.error("smart-sync authorization failed: %s", exc)
|
||||
return {
|
||||
"stdout": "",
|
||||
"stderr": str(exc),
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
result = smart_sync(caller)
|
||||
|
||||
if result["success"]:
|
||||
@@ -240,10 +251,9 @@ def _handle_smart_sync(args: list[str]) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _handle_fix(args: list[str]) -> dict:
|
||||
"""Handle the fix subcommand (devpulse-only)."""
|
||||
def _handle_fix(args: list[str], caller: str) -> dict:
|
||||
"""Handle the fix subcommand (owner-tier, auth pre-checked)."""
|
||||
try:
|
||||
from aipass.drone.apps.plugins.devpulse_ops.auth import verify_caller
|
||||
from aipass.drone.apps.plugins.devpulse_ops.fix_plugin import fix_git_state
|
||||
except ImportError as exc:
|
||||
logger.error("Failed to import devpulse_ops fix plugin: %s", exc)
|
||||
@@ -253,16 +263,6 @@ def _handle_fix(args: list[str]) -> dict:
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
try:
|
||||
caller = verify_caller()
|
||||
except PermissionError as exc:
|
||||
logger.error("fix authorization failed: %s", exc)
|
||||
return {
|
||||
"stdout": "",
|
||||
"stderr": str(exc),
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
dry_run = "--dry-run" in (args or [])
|
||||
result = fix_git_state(caller, dry_run=dry_run)
|
||||
|
||||
@@ -279,42 +279,8 @@ def _handle_fix(args: list[str]) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _handle_pr(args: list[str]) -> dict:
|
||||
"""Handle the PR subcommand."""
|
||||
if not args:
|
||||
return {
|
||||
"stdout": "",
|
||||
"stderr": "Usage: drone @git pr <description>",
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
description = " ".join(args)
|
||||
detected = _detect_branch_dir()
|
||||
if detected is None:
|
||||
return {
|
||||
"stdout": "",
|
||||
"stderr": "Cannot detect branch directory from CWD. Run from within src/aipass/<branch>/",
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
branch_name, branch_dir = detected
|
||||
result = pr_handler.create_pr(branch_name, description, branch_dir)
|
||||
|
||||
if result["success"]:
|
||||
return {
|
||||
"stdout": f"PR created: {result['pr_url']}\nBranch: {result['feature_branch']}",
|
||||
"stderr": "",
|
||||
"exit_code": 0,
|
||||
}
|
||||
return {
|
||||
"stdout": "",
|
||||
"stderr": result["message"],
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
|
||||
def _handle_status() -> dict:
|
||||
"""Handle the status subcommand."""
|
||||
"""Handle the status subcommand (global tier)."""
|
||||
detected = _detect_branch_dir()
|
||||
if detected is None:
|
||||
return {
|
||||
@@ -337,8 +303,96 @@ def _handle_status() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _handle_diff(args: list[str]) -> dict:
|
||||
"""Handle the diff subcommand (global tier)."""
|
||||
detected = _detect_branch_dir()
|
||||
if detected is None:
|
||||
return {
|
||||
"stdout": "",
|
||||
"stderr": "Cannot detect branch directory from CWD. Run from within src/aipass/<branch>/",
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
_, branch_dir = detected
|
||||
staged = "--staged" in args
|
||||
result = diff_handler.get_branch_diff(branch_dir, staged=staged)
|
||||
|
||||
return {
|
||||
"stdout": result["diff"] if result["diff"] else result["message"],
|
||||
"stderr": "",
|
||||
"exit_code": 0,
|
||||
}
|
||||
|
||||
|
||||
def _handle_log(args: list[str]) -> dict:
|
||||
"""Handle the log subcommand (global tier)."""
|
||||
count = 10
|
||||
for arg in args:
|
||||
try:
|
||||
count = int(arg)
|
||||
break
|
||||
except ValueError as exc:
|
||||
logger.warning("Invalid log count argument '%s': %s", arg, exc)
|
||||
continue
|
||||
|
||||
result = log_handler.get_git_log(count=count)
|
||||
|
||||
if result["entries"]:
|
||||
return {
|
||||
"stdout": "\n".join(result["entries"]),
|
||||
"stderr": "",
|
||||
"exit_code": 0,
|
||||
}
|
||||
return {
|
||||
"stdout": result["message"],
|
||||
"stderr": "",
|
||||
"exit_code": 0,
|
||||
}
|
||||
|
||||
|
||||
def _handle_commit(args: list[str]) -> dict:
|
||||
"""Handle the commit subcommand (owner tier)."""
|
||||
if not args:
|
||||
return {
|
||||
"stdout": "",
|
||||
"stderr": "Usage: drone @git commit <message> [--all]",
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
all_files = "--all" in args
|
||||
msg_parts = [a for a in args if a != "--all"]
|
||||
message = " ".join(msg_parts)
|
||||
|
||||
if not message:
|
||||
return {
|
||||
"stdout": "",
|
||||
"stderr": "Commit message cannot be empty",
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
branch_dir = None
|
||||
if all_files:
|
||||
detected = _detect_branch_dir()
|
||||
if detected:
|
||||
_, branch_dir = detected
|
||||
|
||||
return commit_handler.commit_changes(message, branch_dir=branch_dir, all_files=all_files)
|
||||
|
||||
|
||||
def _handle_checkout(args: list[str]) -> dict:
|
||||
"""Handle the checkout subcommand (owner tier)."""
|
||||
if not args:
|
||||
return {
|
||||
"stdout": "",
|
||||
"stderr": "Usage: drone @git checkout <main|dev>",
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
return checkout_handler.checkout_branch(args[0])
|
||||
|
||||
|
||||
def _handle_sync(args: list[str]) -> dict:
|
||||
"""Handle the sync subcommand."""
|
||||
"""Handle the sync subcommand (owner tier)."""
|
||||
autostash = "--autostash" in args
|
||||
result = sync_handler.sync_main(autostash=autostash)
|
||||
|
||||
@@ -356,7 +410,7 @@ def _handle_sync(args: list[str]) -> dict:
|
||||
|
||||
|
||||
def _handle_lock() -> dict:
|
||||
"""Handle the lock subcommand (check status)."""
|
||||
"""Handle the lock subcommand (global tier)."""
|
||||
result = lock_handler.check_lock_status()
|
||||
return {
|
||||
"stdout": json.dumps(result, indent=2),
|
||||
@@ -366,7 +420,7 @@ def _handle_lock() -> dict:
|
||||
|
||||
|
||||
def _handle_unlock(args: list[str]) -> dict:
|
||||
"""Handle the unlock subcommand (force only)."""
|
||||
"""Handle the unlock subcommand (owner tier)."""
|
||||
if "--force" not in args:
|
||||
return {
|
||||
"stdout": "",
|
||||
@@ -398,100 +452,109 @@ def get_help(command: str | None = None) -> str:
|
||||
Help text string.
|
||||
"""
|
||||
if command == "pr":
|
||||
return (
|
||||
"git pr <description> — Create a PR with scoped changes\n"
|
||||
" Stages only files under the caller's branch directory,\n"
|
||||
" creates a feature branch, commits, pushes, and opens a PR.\n"
|
||||
)
|
||||
return "git pr — DEPRECATED. Agent PRs are no longer supported. Devpulse handles git.\n"
|
||||
if command == "status":
|
||||
return "git status — Show git status filtered to your branch directory\n"
|
||||
return "git status — Show git status filtered to your branch directory [global]\n"
|
||||
if command == "diff":
|
||||
return (
|
||||
"git diff [--staged] — Show git diff filtered to your branch directory [global]\n"
|
||||
" Options:\n"
|
||||
" --staged Show staged changes only.\n"
|
||||
)
|
||||
if command == "log":
|
||||
return "git log [count] — Show recent git log entries (default: 10) [global]\n"
|
||||
if command == "lock":
|
||||
return "git lock — Check current lock status [global]\n Shows lock holder, age, stale/orphan detection.\n"
|
||||
if command == "commit":
|
||||
return (
|
||||
"git commit <message> [--all] — Commit staged changes [owner]\n"
|
||||
" Options:\n"
|
||||
" --all Stage all changes under your branch directory first.\n"
|
||||
)
|
||||
if command == "checkout":
|
||||
return "git checkout <main|dev> — Switch branches (main or dev only) [owner]\n"
|
||||
if command == "sync":
|
||||
return (
|
||||
"git sync [--autostash] — Checkout main and pull latest changes\n"
|
||||
"git sync [--autostash] — Checkout main and pull latest changes [owner]\n"
|
||||
" Options:\n"
|
||||
" --autostash Stash local changes before pull and restore after.\n"
|
||||
" Use when sync fails with 'unstaged changes' error.\n"
|
||||
)
|
||||
if command == "lock":
|
||||
return "git lock — Check current lock status\n Shows lock holder, age, stale/orphan detection.\n"
|
||||
if command == "unlock":
|
||||
return "git unlock --force — Force-release the PR lock\n Removes .git_pr.lock regardless of holder.\n"
|
||||
return "git unlock --force — Force-release the PR lock [owner]\n"
|
||||
if command == "system-pr":
|
||||
return (
|
||||
"git system-pr <description> — Create a system-wide PR (devpulse only)\n"
|
||||
" Stages all tracked changes, creates a disposable feature branch,\n"
|
||||
" and opens a PR. Requires devpulse passport authorization.\n"
|
||||
"git system-pr <description> — Create a system-wide PR [owner]\n"
|
||||
" Stages all tracked changes, creates a feature branch, and opens a PR.\n"
|
||||
)
|
||||
if command == "merge":
|
||||
return (
|
||||
"git merge <PR#> — Merge a PR and sync local main (devpulse only)\n"
|
||||
"git merge <PR#> — Merge a PR and sync local main [owner]\n"
|
||||
" Runs gh pr merge --merge --delete-branch, then git pull --rebase.\n"
|
||||
)
|
||||
if command == "smart-sync":
|
||||
return (
|
||||
"git smart-sync — Fetch origin and rebase if behind (devpulse only)\n"
|
||||
" Detects divergence and rebases safely; aborts on conflict.\n"
|
||||
)
|
||||
return "git smart-sync — Fetch origin and rebase if behind [owner]\n"
|
||||
if command == "fix":
|
||||
return (
|
||||
"git fix [--dry-run] — Detect and fix common broken git states (devpulse only)\n"
|
||||
"git fix [--dry-run] — Detect and fix broken git states [owner]\n"
|
||||
"\n"
|
||||
"Detected states and actions:\n"
|
||||
" Stuck rebase .git/rebase-merge or rebase-apply exists\n"
|
||||
" → git rebase --abort\n"
|
||||
" Detached HEAD HEAD is not on a named branch\n"
|
||||
" → git checkout main\n"
|
||||
" Diverged local main and origin/main have diverged\n"
|
||||
" → git fetch + git merge origin/main --no-edit\n"
|
||||
" Dirty index files are staged but not committed\n"
|
||||
" → git reset HEAD (unstages all)\n"
|
||||
" Stuck rebase → git rebase --abort\n"
|
||||
" Detached HEAD → git checkout main\n"
|
||||
" Diverged → git fetch + git merge origin/main\n"
|
||||
" Dirty index → git reset HEAD\n"
|
||||
"\n"
|
||||
"Options:\n"
|
||||
" --dry-run Report detected states and proposed actions without\n"
|
||||
" executing any fixes. Safe to run anytime.\n"
|
||||
" --dry-run Report without executing fixes.\n"
|
||||
)
|
||||
|
||||
return (
|
||||
"git — Git workflow: PR, status, sync, lock management\n"
|
||||
"git — Tier-based git workflow\n"
|
||||
"\n"
|
||||
"Commands:\n"
|
||||
" pr <description> Create a PR with scoped changes\n"
|
||||
" system-pr <desc> Create a system-wide PR (devpulse only)\n"
|
||||
" merge <PR#> Merge a PR (devpulse only)\n"
|
||||
" smart-sync Fetch + rebase if behind (devpulse only)\n"
|
||||
" fix Fix broken git states (devpulse only)\n"
|
||||
"Global (all branches):\n"
|
||||
" status Show git status for your branch\n"
|
||||
" sync Checkout main and pull\n"
|
||||
" diff [--staged] Show git diff for your branch\n"
|
||||
" log [count] Show recent git log (default: 10)\n"
|
||||
" lock Check lock status\n"
|
||||
" unlock --force Force-release the PR lock\n"
|
||||
"\n"
|
||||
"Policy: raw git commands are blocked for agents via .claude/settings.json:\n"
|
||||
" - git checkout* (any form) — use `drone @git sync` instead\n"
|
||||
" - git add -f* / --force* — use scoped `drone @git pr` instead\n"
|
||||
"Culturally also avoid: raw git commit/push, gh pr create. Go through drone.\n"
|
||||
"Owner (devpulse only):\n"
|
||||
" commit <msg> [--all] Commit staged changes\n"
|
||||
" checkout <main|dev> Switch branches\n"
|
||||
" sync [--autostash] Checkout main and pull\n"
|
||||
" unlock --force Force-release the PR lock\n"
|
||||
" system-pr <desc> Create a system-wide PR\n"
|
||||
" merge <PR#> Merge a PR\n"
|
||||
" smart-sync Fetch + rebase if behind\n"
|
||||
" fix [--dry-run] Fix broken git states\n"
|
||||
"\n"
|
||||
"Deprecated:\n"
|
||||
" pr Agent PRs removed — devpulse handles git\n"
|
||||
)
|
||||
|
||||
|
||||
def get_introspective() -> str:
|
||||
"""Return introspection text showing connected handlers."""
|
||||
return (
|
||||
"@git — Git workflow: PR, status, sync, lock management\n"
|
||||
"@git — Tier-based git workflow (v2.0.0)\n"
|
||||
"\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, stage_branch_dir)\n"
|
||||
" - checkout_handler.py (checkout_branch — main/dev only)\n"
|
||||
" - sync_handler.py (sync_main — safe main synchronization)\n"
|
||||
" - pr_handler.py (create_pr — full PR workflow with lockfile)\n"
|
||||
" - pr_handler.py (create_pr — DEPRECATED)\n"
|
||||
"\n"
|
||||
" plugins/devpulse_ops/\n"
|
||||
" - auth.py (verify_caller — passport-based authorization)\n"
|
||||
" - auth.py (verify_git_access — tier-based authorization)\n"
|
||||
" - pr_plugin.py (create_system_pr — system-wide PR workflow)\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"
|
||||
"Commands: pr, system-pr, merge, smart-sync, fix, status, sync, lock, unlock\n"
|
||||
"Access Tiers: global (status, diff, log, lock) | owner (commit, checkout, sync, unlock, system-pr, merge, smart-sync, fix)\n"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user