Merge pull request #566 from AIOSAI/dev
feat: DPLAN-0173 Phase 2 — dev branch workflow complete (handlers, sync, prompts, docs)
This commit is contained in:
@@ -75,18 +75,20 @@ TRUSTED_HOOK_EDITORS = ("devpulse", "seedgo")
|
||||
|
||||
GIT_REDIRECT = (
|
||||
"Raw git write commands are blocked. Use drone instead:\n"
|
||||
' drone @git pr "description" # branch-scoped PR\n'
|
||||
' drone @git system-pr "description" # devpulse-only system PR\n'
|
||||
" drone @git smart-sync # fetch + rebase\n"
|
||||
" drone @git sync # checkout main + pull\n"
|
||||
" drone @git status # what changed\n"
|
||||
" drone @git diff # see changes\n"
|
||||
" drone @git log # commit history\n"
|
||||
" drone @git commit 'msg' --all # commit all changes (devpulse only)\n"
|
||||
' drone @git dev-pr "description" # PR dev to main (devpulse only)\n'
|
||||
" drone @git smart-sync # fetch + rebase\n"
|
||||
"Read-only git (status, log, diff, show, fetch, ls-files) is allowed."
|
||||
)
|
||||
|
||||
GH_REDIRECT = (
|
||||
"Raw gh write commands are blocked. Use drone for git ops:\n"
|
||||
' drone @git pr "description"\n'
|
||||
" drone @git merge <PR#> # devpulse only, on user request\n"
|
||||
' drone @git dev-pr "description" # PR dev to main\n'
|
||||
" drone @git merge <PR#> # merge a PR (devpulse only)\n"
|
||||
" drone @git issue list/create/view # gh issue passthrough\n"
|
||||
"Read-only gh (list, view, status, diff, checks, comments) is allowed."
|
||||
)
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ aipass init agent my-agent # Full agent: apps, mail, memory, identity
|
||||
- **Everything is local.** Your data stays on your machine. Memory is JSON files. Communication is local mailbox files. No cloud dependencies, no external APIs for core operations.
|
||||
- **One pattern for everything.** Every agent follows the same structure. One command (`drone @branch command`) reaches any agent. Learn it once, use it everywhere.
|
||||
- **Projects are isolated by design.** Each project gets its own registry. Agents communicate within their project, not across projects.
|
||||
- **The system protects itself.** Agent locks prevent double-dispatch. PR locks prevent merge conflicts. Branches don't touch each other's files. Quality standards are embedded in every workflow. Errors trigger self-healing.
|
||||
- **The system protects itself.** Agent locks prevent double-dispatch. Git access is tier-controlled through drone. Branches don't touch each other's files. Quality standards are embedded in every workflow. Errors trigger self-healing.
|
||||
|
||||
**Say "hi" tomorrow and pick up exactly where you left off.** One agent or fifteen — the memory persists.
|
||||
|
||||
@@ -241,7 +241,7 @@ setup.sh auto-detects which CLIs are installed and configures hooks for each.
|
||||
| Agents | 12 core + user-created |
|
||||
| Quality standards | 34 automated checks |
|
||||
| Tests | 7,600+ (across all agents) |
|
||||
| PRs merged | 538+ (created by agents, reviewed by human) |
|
||||
| PRs merged | 560+ (human-AI collaboration) |
|
||||
|
||||
Each agent documents its own operational status in its branch README — what works, what doesn't, and why.
|
||||
|
||||
|
||||
@@ -83,11 +83,11 @@ drone @memory search <query> # Search archived memories
|
||||
|
||||
### Git Workflow
|
||||
```
|
||||
drone @git pr 'description' # Create a pull request
|
||||
drone @git status # Git status (branch-scoped)
|
||||
drone @git sync # Sync with main
|
||||
drone @git lock / unlock # Lock/unlock the repo
|
||||
drone @git diff # See changes
|
||||
drone @git log # Commit history
|
||||
```
|
||||
You have no git write access. Devpulse handles all commits and PRs.
|
||||
|
||||
### Infrastructure
|
||||
```
|
||||
|
||||
@@ -48,11 +48,13 @@ When a task belongs to a specialist's DOMAIN, ask them. You can still investigat
|
||||
drone @git status # What changed? (all branches can use)
|
||||
drone @git diff # See the diff (all branches can use)
|
||||
drone @git log # Recent commits (all branches can use)
|
||||
drone @git commit "description" # Commit changes (devpulse only)
|
||||
drone @git branches # List remote branches (all branches can use)
|
||||
drone @git commit "msg" --all # Commit all changes (devpulse only)
|
||||
drone @git checkout dev # Switch to dev branch (devpulse only)
|
||||
drone @git checkout main # Switch to main (devpulse only)
|
||||
drone @git system-pr "description" # System-wide PR (devpulse only)
|
||||
drone @git dev-pr "description" # PR dev to main (devpulse only)
|
||||
drone @git merge <PR#> # Merge a PR (devpulse only, user must request)
|
||||
drone @git delete-branch <name> # Delete remote branch (devpulse only)
|
||||
drone @git sync # Pull latest (devpulse only)
|
||||
drone @git smart-sync # Fetch + rebase (devpulse only)
|
||||
drone @git fix # Fix broken git states (devpulse only)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
dev branch workflow test - Tue May 12 09:06:19 PM PDT 2026
|
||||
+19
-22
@@ -47,20 +47,21 @@ drone @git workflow list # Passthrough to gh workflow list
|
||||
|
||||
# Git workflow — owner tier (devpulse only)
|
||||
drone @git commit "message" # Commit staged changes
|
||||
drone @git commit "msg" --all # Stage tracked files and commit
|
||||
drone @git commit "msg" --all # Stage ALL repo changes and commit
|
||||
drone @git checkout dev # Switch to dev branch
|
||||
drone @git checkout main # Switch to main branch
|
||||
drone @git sync # Checkout main and pull
|
||||
drone @git dev-pr "desc" # Push dev and create PR to main
|
||||
drone @git merge <PR#> # Merge a PR and sync local main
|
||||
drone @git delete-branch <name> # Delete a remote branch (not main/dev)
|
||||
drone @git branches # List remote branches
|
||||
drone @git sync # Pull latest (branch-aware: main or dev)
|
||||
drone @git sync --autostash # Sync with autostash for dirty trees
|
||||
drone @git unlock --force # Force-release the PR lock
|
||||
drone @git system-pr "desc" # System-wide PR across all tracked changes
|
||||
drone @git merge <PR#> # Straight-merge a PR and sync local main
|
||||
drone @git smart-sync # Fetch + detect divergence + rebase
|
||||
drone @git unlock --force # Force-release the PR lock
|
||||
drone @git system-pr "desc" # Legacy system-wide PR (use dev-pr)
|
||||
drone @git fix # Auto-fix stuck rebase / detached HEAD
|
||||
drone @git fix --dry-run # Detect issues without fixing
|
||||
|
||||
# Git workflow — deprecated
|
||||
drone @git pr # DEPRECATED — returns error message
|
||||
|
||||
# Command discovery
|
||||
drone scan @branch # Discover available commands in a branch
|
||||
drone activate @branch # Scan + register all commands as shortcuts
|
||||
@@ -206,30 +207,26 @@ External modules are declared in `apps/handlers/routing_config.json` with entry
|
||||
|
||||
### Git Access Tiers
|
||||
|
||||
Auth centralized via `verify_git_access()` in `apps/handlers/git/auth.py`. Two tiers:
|
||||
Auth centralized via `verify_git_access()` in `apps/plugins/devpulse_ops/auth.py`. Two tiers:
|
||||
|
||||
| Tier | Who | Commands |
|
||||
|------|-----|----------|
|
||||
| **Global** | All branches | `status`, `diff`, `log`, `lock` |
|
||||
| **Owner** | `devpulse` only | `commit`, `checkout`, `sync`, `unlock`, `system-pr`, `merge`, `smart-sync`, `fix` |
|
||||
| **Global** | All branches | `status`, `diff`, `log`, `lock`, `branches`, `issue`, `run`, `workflow` |
|
||||
| **Owner** | `devpulse` only | `commit`, `checkout`, `dev-pr`, `delete-branch`, `sync`, `unlock`, `system-pr`, `merge`, `smart-sync`, `fix` |
|
||||
|
||||
- `pr` is **deprecated** — returns an error message directing to devpulse
|
||||
- Auth is checked once at the top of `git_module.handle_command()` before any handler is called
|
||||
- Unauthorized commands return a clear "Access denied" message with the caller's tier
|
||||
|
||||
### Git Main-Only Enforcement
|
||||
### Dev Branch Model
|
||||
|
||||
All agents work on `main`. Branch creation is only allowed inside `drone @git system-pr`, which:
|
||||
1. Commits changes on main
|
||||
2. Moves branch pointer with `git branch -f` (HEAD stays on main)
|
||||
3. Pushes branch with `--force-with-lease`
|
||||
4. Opens PR via `gh`
|
||||
5. Returns to main
|
||||
All work happens on `dev`. Only devpulse has write access. Agents build and report; devpulse commits.
|
||||
|
||||
**Flow:** work on dev → stack changes → `drone @git dev-pr "desc"` → merge PR → `drone @git sync` (realigns dev from main)
|
||||
|
||||
Enforcement layers:
|
||||
- `.claude/settings.json` deny rules block `git checkout -b`, `git switch -c`
|
||||
- `_assert_on_main_or_pr_flow()` guard in `git_module.py`
|
||||
- Persistent citizen branches: `citizen/{name}` reused across PRs
|
||||
- `git_gate.py` PreToolUse hook blocks ALL raw git/gh commands
|
||||
- Drone tier system restricts write commands to devpulse only
|
||||
- Prompt instructions tell agents they have zero git access
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -62,6 +62,13 @@ def checkout_branch(target: str) -> dict:
|
||||
text=True,
|
||||
cwd=str(repo_root),
|
||||
)
|
||||
if result.returncode != 0 and "did not match" in result.stderr:
|
||||
result = subprocess.run(
|
||||
["git", "checkout", "-b", target],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo_root),
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
logger.error("git checkout failed: %s", exc)
|
||||
return {
|
||||
|
||||
@@ -58,13 +58,23 @@ def commit_changes(
|
||||
branch_dir: Path | None = None,
|
||||
all_files: bool = False,
|
||||
) -> dict:
|
||||
"""Commit staged changes or all changes under branch_dir."""
|
||||
"""Commit changes. With --all, stages the entire repo (not CWD-scoped).
|
||||
|
||||
Post-DPLAN-0173: only devpulse commits, agents don't PR. Repo-wide
|
||||
staging is the correct default since dispatched agents work across
|
||||
multiple branch directories.
|
||||
"""
|
||||
repo_root = find_repo_root()
|
||||
|
||||
if all_files and branch_dir:
|
||||
stage_result = stage_branch_dir(branch_dir, repo_root)
|
||||
if not stage_result["success"]:
|
||||
return {"stdout": "", "stderr": stage_result["message"], "exit_code": 1}
|
||||
if all_files:
|
||||
add_result = subprocess.run(
|
||||
["git", "add", "-A"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo_root),
|
||||
)
|
||||
if add_result.returncode != 0:
|
||||
return {"stdout": "", "stderr": f"Failed to stage: {add_result.stderr.strip()}", "exit_code": 1}
|
||||
|
||||
diff_check = subprocess.run(
|
||||
["git", "diff", "--cached", "--quiet"],
|
||||
@@ -81,13 +91,6 @@ def commit_changes(
|
||||
|
||||
try:
|
||||
cmd = ["git", "commit", "-m", message]
|
||||
if branch_dir:
|
||||
try:
|
||||
rel_dir = branch_dir.resolve().relative_to(repo_root.resolve())
|
||||
except ValueError as exc:
|
||||
logger.warning("commit_changes: branch_dir not relative to repo root: %s", exc)
|
||||
rel_dir = branch_dir
|
||||
cmd.extend(["--", str(rel_dir) + "/"])
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
|
||||
@@ -75,7 +75,17 @@ def create_dev_pr(description: str) -> dict:
|
||||
return {"success": False, "message": f"PR creation failed: {exc}", "pr_url": ""}
|
||||
|
||||
if pr.returncode != 0:
|
||||
return {"success": False, "message": f"PR creation failed: {pr.stderr.strip()}", "pr_url": ""}
|
||||
stderr = pr.stderr.strip()
|
||||
if "already exists" in stderr:
|
||||
existing_url = ""
|
||||
for line in stderr.splitlines():
|
||||
if "github.com" in line:
|
||||
existing_url = line.strip()
|
||||
break
|
||||
msg = f"Pushed to dev. PR already open: {existing_url}" if existing_url else "Pushed to dev. PR already open."
|
||||
json_handler.log_operation("dev_pr_push_existing", {"pr_url": existing_url, "description": description})
|
||||
return {"success": True, "message": msg, "pr_url": existing_url}
|
||||
return {"success": False, "message": f"PR creation failed: {stderr}", "pr_url": ""}
|
||||
|
||||
pr_url = pr.stdout.strip()
|
||||
json_handler.log_operation("create_dev_pr", {"pr_url": pr_url, "description": description})
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Safe main branch synchronization.
|
||||
Branch synchronization — works on both main and dev.
|
||||
|
||||
Checks out main and pulls latest, with error handling for dirty
|
||||
working trees and other common failure modes.
|
||||
On main: pulls latest from origin/main.
|
||||
On dev: pulls origin/main into dev (realigns after PR merge).
|
||||
From other branch: checks out main first, then pulls.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -36,16 +37,28 @@ def sync_main(autostash: bool = False) -> dict:
|
||||
stashed = False
|
||||
|
||||
try:
|
||||
checkout = subprocess.run(
|
||||
["git", "checkout", "main"],
|
||||
head = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo_root),
|
||||
)
|
||||
if checkout.returncode != 0:
|
||||
msg = f"Failed to checkout main: {checkout.stderr.strip()}"
|
||||
logger.error(msg)
|
||||
return {"success": False, "message": msg, "stdout": checkout.stdout}
|
||||
current_branch = head.stdout.strip() if head.returncode == 0 else ""
|
||||
|
||||
if current_branch == "dev":
|
||||
return _sync_dev(repo_root, autostash)
|
||||
|
||||
if current_branch != "main":
|
||||
checkout = subprocess.run(
|
||||
["git", "checkout", "main"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo_root),
|
||||
)
|
||||
if checkout.returncode != 0:
|
||||
msg = f"Failed to checkout main: {checkout.stderr.strip()}"
|
||||
logger.error(msg)
|
||||
return {"success": False, "message": msg, "stdout": checkout.stdout}
|
||||
|
||||
if autostash:
|
||||
stash = subprocess.run(
|
||||
@@ -158,3 +171,51 @@ def sync_main(autostash: bool = False) -> dict:
|
||||
msg = f"Sync failed: {exc}"
|
||||
logger.error(msg)
|
||||
return {"success": False, "message": msg, "stdout": ""}
|
||||
|
||||
|
||||
def _sync_dev(repo_root, autostash: bool = False) -> dict:
|
||||
"""Pull origin/main into dev branch to realign after PR merge."""
|
||||
stashed = False
|
||||
|
||||
if autostash:
|
||||
stash = subprocess.run(
|
||||
["git", "stash"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo_root),
|
||||
)
|
||||
stashed = "No local changes to save" not in stash.stdout
|
||||
|
||||
fetch = subprocess.run(
|
||||
["git", "fetch", "origin", "--prune"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo_root),
|
||||
)
|
||||
if fetch.returncode != 0:
|
||||
if stashed:
|
||||
subprocess.run(["git", "stash", "pop"], capture_output=True, text=True, cwd=str(repo_root))
|
||||
return {"success": False, "message": f"Fetch failed: {fetch.stderr.strip()}", "stdout": ""}
|
||||
|
||||
result = subprocess.run(
|
||||
["git", "pull", "origin", "main", "--rebase"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo_root),
|
||||
)
|
||||
|
||||
if stashed:
|
||||
subprocess.run(["git", "stash", "pop"], capture_output=True, text=True, cwd=str(repo_root))
|
||||
|
||||
if result.returncode != 0:
|
||||
raw = result.stderr.strip()
|
||||
msg = f"Failed to sync dev from main: {raw}"
|
||||
if not autostash and ("unstaged changes" in raw or "uncommitted changes" in raw):
|
||||
msg += "\n Tip: retry with 'drone @git sync --autostash'"
|
||||
return {"success": False, "message": msg, "stdout": result.stdout}
|
||||
|
||||
stdout = result.stdout.strip()
|
||||
msg = f"Synced dev from origin/main: {stdout}"
|
||||
json_handler.log_operation("sync_dev", {"result": stdout, "autostash": autostash})
|
||||
logger.info(msg)
|
||||
return {"success": True, "message": msg, "stdout": stdout}
|
||||
|
||||
@@ -462,13 +462,7 @@ def _handle_commit(args: list[str]) -> dict:
|
||||
"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)
|
||||
return commit_handler.commit_changes(message, all_files=all_files)
|
||||
|
||||
|
||||
def _handle_checkout(args: list[str]) -> dict:
|
||||
@@ -567,11 +561,23 @@ def get_help(command: str | None = None) -> str:
|
||||
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 == "branches":
|
||||
return "git branches — List all remote branches [global]\n"
|
||||
if command == "dev-pr":
|
||||
return (
|
||||
"git dev-pr <description> — Push dev branch and create PR to main [owner]\n"
|
||||
" Description becomes the PR title.\n"
|
||||
)
|
||||
if command == "delete-branch":
|
||||
return (
|
||||
"git delete-branch <name> — Delete a remote branch [owner]\n"
|
||||
" Protected: main and dev cannot be deleted.\n"
|
||||
)
|
||||
if command == "commit":
|
||||
return (
|
||||
"git commit <message> [--all] — Commit staged changes [owner]\n"
|
||||
"git commit <message> [--all] — Commit changes [owner]\n"
|
||||
" Options:\n"
|
||||
" --all Stage all changes under your branch directory first.\n"
|
||||
" --all Stage all repo changes (git add -A) before committing.\n"
|
||||
)
|
||||
if command == "checkout":
|
||||
return "git checkout <main|dev> — Switch branches (main or dev only) [owner]\n"
|
||||
@@ -610,36 +616,36 @@ def get_help(command: str | None = None) -> str:
|
||||
)
|
||||
|
||||
return (
|
||||
"git — Tier-based git workflow\n"
|
||||
"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"
|
||||
" 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] Commit staged changes\n"
|
||||
" commit <msg> [--all] Commit changes (--all stages entire repo)\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"
|
||||
" dev-pr <desc> Push dev and create PR to main\n"
|
||||
" delete-branch <name> Delete a remote branch\n"
|
||||
" merge <PR#> Merge a PR\n"
|
||||
" sync [--autostash] Checkout main and pull\n"
|
||||
" smart-sync Fetch + rebase if behind\n"
|
||||
" unlock --force Force-release the PR lock\n"
|
||||
" system-pr <desc> Legacy system-wide PR (use dev-pr)\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 — Tier-based git workflow (v2.0.0)\n"
|
||||
"@git — Tier-based git workflow, dev branch model (v3.0.0)\n"
|
||||
"\n"
|
||||
"Connected Handlers:\n"
|
||||
" handlers/git/\n"
|
||||
@@ -647,14 +653,17 @@ def get_introspective() -> str:
|
||||
" - 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"
|
||||
" - commit_handler.py (commit_changes — repo-wide staging with --all)\n"
|
||||
" - checkout_handler.py (checkout_branch — main/dev only)\n"
|
||||
" - sync_handler.py (sync_main — safe main synchronization)\n"
|
||||
" - pr_handler.py (create_pr — DEPRECATED)\n"
|
||||
" - dev_pr_handler.py (create_dev_pr — push dev, PR to main)\n"
|
||||
" - branches_handler.py (list_remote_branches)\n"
|
||||
" - delete_branch_handler.py (delete_remote_branch — protected: main/dev)\n"
|
||||
" - pr_handler.py (create_pr — DEPRECATED, kept for reference)\n"
|
||||
"\n"
|
||||
" plugins/devpulse_ops/\n"
|
||||
" - auth.py (verify_git_access — tier-based authorization)\n"
|
||||
" - pr_plugin.py (create_system_pr — system-wide PR workflow)\n"
|
||||
" - pr_plugin.py (create_system_pr — legacy, use dev-pr instead)\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"
|
||||
@@ -662,7 +671,7 @@ def get_introspective() -> str:
|
||||
" gh passthrough:\n"
|
||||
" - issue, run, workflow → subprocess gh <cmd> [args]\n"
|
||||
"\n"
|
||||
"Access Tiers: global (status, diff, log, lock, issue, run, workflow) | owner (commit, checkout, sync, unlock, system-pr, merge, smart-sync, fix)\n"
|
||||
"Access Tiers: global (status, diff, log, lock, branches, issue, run, workflow) | owner (commit, checkout, dev-pr, delete-branch, sync, unlock, system-pr, merge, smart-sync, fix)\n"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -86,8 +86,8 @@ def _find_caller() -> str:
|
||||
def verify_caller() -> str:
|
||||
"""Verify the calling branch is authorized for devpulse operations.
|
||||
|
||||
system-pr, merge, smart-sync, fix are restricted to ALLOWED_CALLERS.
|
||||
Other branches use drone @git pr for their own branch-scoped PRs.
|
||||
Owner-tier commands (commit, dev-pr, merge, etc.) are restricted to
|
||||
ALLOWED_CALLERS. Other branches have read-only access via global tier.
|
||||
|
||||
Returns:
|
||||
The caller's branch name if authorized.
|
||||
@@ -97,7 +97,7 @@ def verify_caller() -> str:
|
||||
"""
|
||||
name = _find_caller()
|
||||
if name not in ALLOWED_CALLERS:
|
||||
msg = f"Branch '{name}' is not authorized for this operation. Use 'drone @git pr' for branch-scoped PRs."
|
||||
msg = f"Branch '{name}' is not authorized for this operation. Only devpulse can use owner-tier commands."
|
||||
logger.error(msg)
|
||||
raise PermissionError(msg)
|
||||
json_handler.log_operation(
|
||||
|
||||
Reference in New Issue
Block a user