forge-workflow 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/dev.md +6 -1
- package/.claude/commands/plan.md +59 -14
- package/.claude/commands/premerge.md +10 -0
- package/.claude/commands/review.md +7 -1
- package/.claude/commands/ship.md +95 -47
- package/.claude/commands/status.md +42 -0
- package/.claude/commands/validate.md +7 -1
- package/.claude/commands/verify.md +52 -4
- package/.claude/rules/workflow.md +16 -0
- package/.claude/scripts/greptile-resolve.sh +32 -0
- package/.cline/workflows/dev.md +6 -1
- package/.cline/workflows/plan.md +59 -14
- package/.cline/workflows/premerge.md +10 -0
- package/.cline/workflows/review.md +7 -1
- package/.cline/workflows/ship.md +95 -47
- package/.cline/workflows/status.md +42 -0
- package/.cline/workflows/validate.md +7 -1
- package/.cline/workflows/verify.md +52 -4
- package/.codex/skills/dev/SKILL.md +6 -1
- package/.codex/skills/plan/SKILL.md +59 -14
- package/.codex/skills/premerge/SKILL.md +10 -0
- package/.codex/skills/review/SKILL.md +7 -1
- package/.codex/skills/ship/SKILL.md +95 -47
- package/.codex/skills/status/SKILL.md +42 -0
- package/.codex/skills/validate/SKILL.md +7 -1
- package/.codex/skills/verify/SKILL.md +52 -4
- package/.cursor/commands/dev.md +6 -1
- package/.cursor/commands/plan.md +59 -14
- package/.cursor/commands/premerge.md +10 -0
- package/.cursor/commands/review.md +7 -1
- package/.cursor/commands/ship.md +95 -47
- package/.cursor/commands/status.md +42 -0
- package/.cursor/commands/validate.md +7 -1
- package/.cursor/commands/verify.md +52 -4
- package/.cursorrules +149 -0
- package/.github/prompts/dev.prompt.md +6 -1
- package/.github/prompts/plan.prompt.md +59 -14
- package/.github/prompts/premerge.prompt.md +10 -0
- package/.github/prompts/review.prompt.md +7 -1
- package/.github/prompts/ship.prompt.md +95 -47
- package/.github/prompts/status.prompt.md +42 -0
- package/.github/prompts/validate.prompt.md +7 -1
- package/.github/prompts/verify.prompt.md +52 -4
- package/.kilocode/workflows/dev.md +6 -1
- package/.kilocode/workflows/plan.md +59 -14
- package/.kilocode/workflows/premerge.md +10 -0
- package/.kilocode/workflows/review.md +7 -1
- package/.kilocode/workflows/ship.md +95 -47
- package/.kilocode/workflows/status.md +42 -0
- package/.kilocode/workflows/validate.md +7 -1
- package/.kilocode/workflows/verify.md +52 -4
- package/.opencode/commands/dev.md +6 -1
- package/.opencode/commands/plan.md +59 -14
- package/.opencode/commands/premerge.md +10 -0
- package/.opencode/commands/review.md +7 -1
- package/.opencode/commands/ship.md +95 -47
- package/.opencode/commands/status.md +42 -0
- package/.opencode/commands/validate.md +7 -1
- package/.opencode/commands/verify.md +52 -4
- package/.roo/commands/dev.md +6 -1
- package/.roo/commands/plan.md +59 -14
- package/.roo/commands/premerge.md +10 -0
- package/.roo/commands/review.md +7 -1
- package/.roo/commands/ship.md +95 -47
- package/.roo/commands/status.md +42 -0
- package/.roo/commands/validate.md +7 -1
- package/.roo/commands/verify.md +52 -4
- package/AGENTS.md +97 -0
- package/CLAUDE.md +10 -0
- package/README.md +2 -2
- package/bin/forge-cmd.js +5 -1
- package/bin/forge-preflight.js +15 -2
- package/bin/forge.js +211 -9
- package/docs/ENHANCED_ONBOARDING.md +96 -86
- package/docs/ROADMAP.md +2 -2
- package/docs/TOOLCHAIN.md +23 -0
- package/docs/VALIDATION.md +1 -1
- package/lefthook.yml +11 -0
- package/lib/agents/README.md +46 -1
- package/lib/agents/cline.plugin.json +11 -4
- package/lib/agents/codex.plugin.json +2 -2
- package/lib/agents/copilot.plugin.json +5 -5
- package/lib/agents/cursor.plugin.json +1 -1
- package/lib/agents/kilocode.plugin.json +1 -1
- package/lib/agents/opencode.plugin.json +7 -4
- package/lib/agents/roo.plugin.json +10 -3
- package/lib/agents-config.js +129 -81
- package/lib/codex-skills.js +50 -0
- package/lib/commands/_registry.js +173 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/commands-reset.js +147 -0
- package/lib/commands/dev.js +84 -0
- package/lib/commands/plan.js +18 -0
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +1 -1
- package/lib/commands/setup.js +4295 -0
- package/lib/commands/ship.js +20 -0
- package/lib/commands/status.js +210 -44
- package/lib/commands/sync.js +71 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +13 -0
- package/lib/commands/worktree.js +310 -0
- package/lib/detect-agent.js +38 -8
- package/lib/detection-utils.js +405 -0
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-utils.js +260 -0
- package/lib/forge-context.js +42 -0
- package/lib/freshness-token.js +148 -0
- package/lib/frontmatter.js +79 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +113 -12
- package/lib/lefthook-check.js +27 -6
- package/lib/plugin-manager.js +225 -72
- package/lib/project-discovery.js +39 -5
- package/lib/reset.js +309 -0
- package/lib/runtime-health.js +305 -0
- package/lib/shell-utils.js +50 -0
- package/lib/task-ownership.js +117 -0
- package/lib/ui-utils.js +43 -0
- package/lib/validation-utils.js +163 -0
- package/lib/workflow/enforce-stage.js +179 -0
- package/lib/workflow/stages.js +201 -0
- package/lib/workflow/state.js +332 -0
- package/opencode.json +67 -0
- package/package.json +16 -6
- package/scripts/beads-context.sh +165 -22
- package/scripts/beads-context.test.js +5 -1
- package/scripts/check-agents.js +103 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/conflict-detect.sh +2 -2
- package/scripts/dep-guard.sh +6 -28
- package/scripts/file-index.sh +117 -23
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -0
- package/scripts/lib/eval-runner.js +39 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/pr-coordinator.sh +756 -0
- package/scripts/smart-status.sh +58 -21
- package/scripts/sync-commands.js +49 -20
- package/scripts/sync-utils.sh +24 -29
- package/scripts/test.js +18 -1
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# pr-coordinator.sh — Parallel PR coordination: dependencies, merge order, simulation.
|
|
3
|
+
#
|
|
4
|
+
# Subcommands:
|
|
5
|
+
# dep add|remove|list|list-all PR dependency management
|
|
6
|
+
# merge-sim <branch> Merge simulation dry-run
|
|
7
|
+
# merge-order Dependency-aware merge sequence
|
|
8
|
+
# rebase-check Rebase guidance for open branches
|
|
9
|
+
# auto-label <issue-id> PR label auto-tagging
|
|
10
|
+
# stale-worktrees Abandoned worktree detection
|
|
11
|
+
# help Show usage
|
|
12
|
+
#
|
|
13
|
+
# Exit codes: 0=success, 1=error, 2=validation error
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
|
+
source "$SCRIPT_DIR/lib/sanitize.sh" || { echo "FATAL: failed to source sanitize.sh" >&2; exit 2; }
|
|
19
|
+
|
|
20
|
+
# Source these but don't fail if missing (tests may not have them)
|
|
21
|
+
[[ -f "$SCRIPT_DIR/file-index.sh" ]] && source "$SCRIPT_DIR/file-index.sh"
|
|
22
|
+
[[ -f "$SCRIPT_DIR/sync-utils.sh" ]] && source "$SCRIPT_DIR/sync-utils.sh"
|
|
23
|
+
|
|
24
|
+
# ── Stub implementations (replaced in Tasks 6-11) ────────────────────
|
|
25
|
+
|
|
26
|
+
cmd_dep() {
|
|
27
|
+
if [[ $# -lt 1 ]]; then
|
|
28
|
+
echo "Usage: pr-coordinator.sh dep <add|remove|list|list-all|set-pr> [args...]" >&2
|
|
29
|
+
return 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
local action="$1"
|
|
33
|
+
shift
|
|
34
|
+
|
|
35
|
+
case "$action" in
|
|
36
|
+
add)
|
|
37
|
+
# dep add <issue-a> <issue-b> — issue-a depends on issue-b
|
|
38
|
+
if [[ $# -lt 2 ]]; then
|
|
39
|
+
echo "Usage: pr-coordinator.sh dep add <issue-a> <issue-b>" >&2
|
|
40
|
+
return 1
|
|
41
|
+
fi
|
|
42
|
+
local issue_a="$1" issue_b="$2"
|
|
43
|
+
|
|
44
|
+
# Validate issue IDs
|
|
45
|
+
[[ "$issue_a" =~ ^[a-zA-Z0-9-]+$ ]] || { echo "Error: invalid issue-id: $issue_a" >&2; return 2; }
|
|
46
|
+
[[ "$issue_b" =~ ^[a-zA-Z0-9-]+$ ]] || { echo "Error: invalid issue-id: $issue_b" >&2; return 2; }
|
|
47
|
+
|
|
48
|
+
# Add dependency
|
|
49
|
+
local add_output
|
|
50
|
+
add_output="$(${BD_CMD:-bd} dep add "$issue_a" "$issue_b" 2>&1)" || {
|
|
51
|
+
echo "Error: failed to add dependency: $add_output" >&2
|
|
52
|
+
return 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Check for cycles — use exit code as primary signal (exit 0 = no cycles)
|
|
56
|
+
if ! ${BD_CMD:-bd} dep cycles &>/dev/null; then
|
|
57
|
+
# Cycle detected — rollback
|
|
58
|
+
${BD_CMD:-bd} dep remove "$issue_a" "$issue_b" 2>/dev/null || true
|
|
59
|
+
echo "Error: circular dependency detected — rolled back" >&2
|
|
60
|
+
return 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
echo "Dependency added: $issue_a depends on $issue_b"
|
|
64
|
+
;;
|
|
65
|
+
|
|
66
|
+
remove)
|
|
67
|
+
if [[ $# -lt 2 ]]; then
|
|
68
|
+
echo "Usage: pr-coordinator.sh dep remove <issue-a> <issue-b>" >&2
|
|
69
|
+
return 1
|
|
70
|
+
fi
|
|
71
|
+
local issue_a="$1" issue_b="$2"
|
|
72
|
+
[[ "$issue_a" =~ ^[a-zA-Z0-9-]+$ ]] || { echo "Error: invalid issue-id: $issue_a" >&2; return 2; }
|
|
73
|
+
[[ "$issue_b" =~ ^[a-zA-Z0-9-]+$ ]] || { echo "Error: invalid issue-id: $issue_b" >&2; return 2; }
|
|
74
|
+
|
|
75
|
+
${BD_CMD:-bd} dep remove "$issue_a" "$issue_b" 2>&1 || {
|
|
76
|
+
echo "Error: failed to remove dependency" >&2
|
|
77
|
+
return 1
|
|
78
|
+
}
|
|
79
|
+
echo "Dependency removed: $issue_a no longer depends on $issue_b"
|
|
80
|
+
;;
|
|
81
|
+
|
|
82
|
+
list)
|
|
83
|
+
if [[ $# -lt 1 ]]; then
|
|
84
|
+
echo "Usage: pr-coordinator.sh dep list <issue-id>" >&2
|
|
85
|
+
return 1
|
|
86
|
+
fi
|
|
87
|
+
local issue_id="$1"
|
|
88
|
+
[[ "$issue_id" =~ ^[a-zA-Z0-9-]+$ ]] || { echo "Error: invalid issue-id: $issue_id" >&2; return 2; }
|
|
89
|
+
|
|
90
|
+
local show_output
|
|
91
|
+
show_output="$(${BD_CMD:-bd} show "$issue_id" 2>&1)" || {
|
|
92
|
+
echo "Error: failed to show issue $issue_id" >&2
|
|
93
|
+
return 1
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Parse DEPENDS ON section
|
|
97
|
+
local deps
|
|
98
|
+
deps="$(printf '%s' "$show_output" | sed -n '/^DEPENDS ON/,/^$/p' | grep -E '^\s+' | grep -v '^DEPENDS ON' || true)"
|
|
99
|
+
|
|
100
|
+
if [[ -z "$deps" ]]; then
|
|
101
|
+
echo "No dependencies for $issue_id"
|
|
102
|
+
else
|
|
103
|
+
echo "Dependencies for $issue_id:"
|
|
104
|
+
printf '%s\n' "$deps"
|
|
105
|
+
fi
|
|
106
|
+
;;
|
|
107
|
+
|
|
108
|
+
list-all)
|
|
109
|
+
local list_output
|
|
110
|
+
list_output="$(${BD_CMD:-bd} list --status=open,in_progress 2>&1)" || {
|
|
111
|
+
echo "Error: failed to list issues" >&2
|
|
112
|
+
return 1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if [[ -z "$list_output" ]]; then
|
|
116
|
+
echo "No open issues"
|
|
117
|
+
return 0
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# For each issue, show its dependencies
|
|
121
|
+
local issue_id
|
|
122
|
+
while IFS= read -r line; do
|
|
123
|
+
issue_id="$(printf '%s' "$line" | grep -oE '[A-Za-z]+-[A-Za-z0-9]+' | head -1)" || continue
|
|
124
|
+
[[ -z "$issue_id" ]] && continue
|
|
125
|
+
|
|
126
|
+
local show_out
|
|
127
|
+
show_out="$(${BD_CMD:-bd} show "$issue_id" 2>&1)" || continue
|
|
128
|
+
local deps
|
|
129
|
+
deps="$(printf '%s' "$show_out" | sed -n '/^DEPENDS ON/,/^$/p' | grep -E '^\s+' | grep -v '^DEPENDS ON' || true)"
|
|
130
|
+
|
|
131
|
+
if [[ -n "$deps" ]]; then
|
|
132
|
+
echo "$issue_id:"
|
|
133
|
+
printf '%s\n' "$deps"
|
|
134
|
+
echo ""
|
|
135
|
+
fi
|
|
136
|
+
done <<< "$list_output"
|
|
137
|
+
;;
|
|
138
|
+
|
|
139
|
+
set-pr)
|
|
140
|
+
# Store PR number on issue: dep set-pr <issue-id> <pr-number>
|
|
141
|
+
if [[ $# -lt 2 ]]; then
|
|
142
|
+
echo "Usage: pr-coordinator.sh dep set-pr <issue-id> <pr-number>" >&2
|
|
143
|
+
return 1
|
|
144
|
+
fi
|
|
145
|
+
local issue_id="$1" pr_number="$2"
|
|
146
|
+
[[ "$issue_id" =~ ^[a-zA-Z0-9-]+$ ]] || { echo "Error: invalid issue-id: $issue_id" >&2; return 2; }
|
|
147
|
+
validate_pr_number "$pr_number" || return 2
|
|
148
|
+
|
|
149
|
+
${BD_CMD:-bd} set-state "$issue_id" "pr_number=$pr_number" --reason "PR created by /ship" 2>&1 || {
|
|
150
|
+
echo "Error: failed to store PR number" >&2
|
|
151
|
+
return 1
|
|
152
|
+
}
|
|
153
|
+
echo "PR #$pr_number linked to $issue_id"
|
|
154
|
+
;;
|
|
155
|
+
|
|
156
|
+
*)
|
|
157
|
+
echo "Error: unknown dep action '$action'" >&2
|
|
158
|
+
echo "Usage: pr-coordinator.sh dep <add|remove|list|list-all|set-pr>" >&2
|
|
159
|
+
return 1
|
|
160
|
+
;;
|
|
161
|
+
esac
|
|
162
|
+
}
|
|
163
|
+
cmd_merge_sim() {
|
|
164
|
+
if [[ $# -lt 1 ]]; then
|
|
165
|
+
echo "Usage: pr-coordinator.sh merge-sim <branch> [--base=<base>]" >&2
|
|
166
|
+
return 1
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
local branch="$1"
|
|
170
|
+
shift
|
|
171
|
+
local base
|
|
172
|
+
base="$(get_sync_branch 2>/dev/null || echo "master")"
|
|
173
|
+
|
|
174
|
+
# Parse optional --base flag
|
|
175
|
+
while [[ $# -gt 0 ]]; do
|
|
176
|
+
case "$1" in
|
|
177
|
+
--base=*) base="${1#--base=}"; shift ;;
|
|
178
|
+
*) echo "Error: unknown flag '$1'" >&2; return 1 ;;
|
|
179
|
+
esac
|
|
180
|
+
done
|
|
181
|
+
|
|
182
|
+
# Validate branch names (defense in depth — dispatcher also validates)
|
|
183
|
+
validate_branch_name "$branch" || return 2
|
|
184
|
+
validate_branch_name "$base" || return 2
|
|
185
|
+
|
|
186
|
+
# Verify branches exist
|
|
187
|
+
if ! git rev-parse --verify "$branch" &>/dev/null; then
|
|
188
|
+
echo "Error: branch '$branch' does not exist" >&2
|
|
189
|
+
return 1
|
|
190
|
+
fi
|
|
191
|
+
if ! git rev-parse --verify "$base" &>/dev/null; then
|
|
192
|
+
echo "Error: base branch '$base' does not exist" >&2
|
|
193
|
+
return 1
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
# Save current state
|
|
197
|
+
local original_branch
|
|
198
|
+
original_branch="$(git rev-parse --abbrev-ref HEAD)"
|
|
199
|
+
local original_head
|
|
200
|
+
original_head="$(git rev-parse HEAD)"
|
|
201
|
+
|
|
202
|
+
# Set up trap for crash recovery — guard flag prevents double-cleanup when
|
|
203
|
+
# ERR fires followed by EXIT (both invoke the trap in bash)
|
|
204
|
+
_merge_sim_cleanup_done=0
|
|
205
|
+
_merge_sim_cleanup_once() {
|
|
206
|
+
[[ "$_merge_sim_cleanup_done" -eq 1 ]] && return
|
|
207
|
+
_merge_sim_cleanup_done=1
|
|
208
|
+
_merge_sim_cleanup "$original_branch" "$original_head"
|
|
209
|
+
}
|
|
210
|
+
trap '_merge_sim_cleanup_once' EXIT ERR INT TERM
|
|
211
|
+
|
|
212
|
+
# Checkout base branch (detached to avoid moving branch pointer)
|
|
213
|
+
git checkout --detach "$base" 2>/dev/null || {
|
|
214
|
+
echo "Error: failed to checkout base '$base'" >&2
|
|
215
|
+
trap - EXIT ERR INT TERM
|
|
216
|
+
return 1
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# Attempt merge
|
|
220
|
+
local merge_output
|
|
221
|
+
if merge_output="$(git merge --no-commit --no-ff "$branch" 2>&1)"; then
|
|
222
|
+
# Clean merge — no conflicts
|
|
223
|
+
git merge --abort 2>/dev/null || true
|
|
224
|
+
git checkout "$original_branch" 2>/dev/null || git checkout "$original_head" 2>/dev/null
|
|
225
|
+
trap - EXIT ERR INT TERM
|
|
226
|
+
echo "No conflicts detected with $base"
|
|
227
|
+
return 0
|
|
228
|
+
else
|
|
229
|
+
# Conflicts detected
|
|
230
|
+
local conflicted_files
|
|
231
|
+
conflicted_files="$(git diff --name-only --diff-filter=U 2>/dev/null || true)"
|
|
232
|
+
git merge --abort 2>/dev/null || true
|
|
233
|
+
git checkout "$original_branch" 2>/dev/null || git checkout "$original_head" 2>/dev/null
|
|
234
|
+
trap - EXIT ERR INT TERM
|
|
235
|
+
|
|
236
|
+
if [[ -n "$conflicted_files" ]]; then
|
|
237
|
+
echo "Conflicts detected with $base:"
|
|
238
|
+
printf '%s\n' "$conflicted_files"
|
|
239
|
+
else
|
|
240
|
+
echo "Merge failed (non-conflict reason):"
|
|
241
|
+
printf '%s\n' "$merge_output"
|
|
242
|
+
fi
|
|
243
|
+
return 1
|
|
244
|
+
fi
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
_merge_sim_cleanup() {
|
|
248
|
+
local original_branch="$1"
|
|
249
|
+
local original_head="$2"
|
|
250
|
+
git merge --abort 2>/dev/null || true
|
|
251
|
+
git checkout "$original_branch" 2>/dev/null || git checkout "$original_head" 2>/dev/null || true
|
|
252
|
+
}
|
|
253
|
+
cmd_merge_order() {
|
|
254
|
+
local format="text"
|
|
255
|
+
|
|
256
|
+
# Parse flags
|
|
257
|
+
while [[ $# -gt 0 ]]; do
|
|
258
|
+
case "$1" in
|
|
259
|
+
--format=*) format="${1#--format=}"; shift ;;
|
|
260
|
+
*) echo "Error: unknown flag '$1'" >&2; return 1 ;;
|
|
261
|
+
esac
|
|
262
|
+
done
|
|
263
|
+
|
|
264
|
+
# Check for cycles first — use exit code as primary signal
|
|
265
|
+
if ! ${BD_CMD:-bd} dep cycles &>/dev/null; then
|
|
266
|
+
echo "Error: dependency cycle detected — cannot compute merge order" >&2
|
|
267
|
+
return 1
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
# Get all open/in_progress issues
|
|
271
|
+
local issues_output
|
|
272
|
+
issues_output="$(${BD_CMD:-bd} list --status=open,in_progress 2>&1)" || {
|
|
273
|
+
echo "Error: failed to list issues" >&2
|
|
274
|
+
return 1
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if [[ -z "$issues_output" ]]; then
|
|
278
|
+
echo "Nothing to merge — no open issues"
|
|
279
|
+
return 0
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
# Extract issue IDs
|
|
283
|
+
local -a all_issues=()
|
|
284
|
+
while IFS= read -r line; do
|
|
285
|
+
local id
|
|
286
|
+
id="$(printf '%s' "$line" | grep -oE '[A-Za-z]+-[A-Za-z0-9]+' | head -1)" || continue
|
|
287
|
+
[[ -n "$id" ]] && all_issues+=("$id")
|
|
288
|
+
done <<< "$issues_output"
|
|
289
|
+
|
|
290
|
+
if [[ ${#all_issues[@]} -eq 0 ]]; then
|
|
291
|
+
echo "Nothing to merge — no open issues"
|
|
292
|
+
return 0
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
# Build adjacency list and in-degree count
|
|
296
|
+
# dependency: A depends on B means B must merge before A
|
|
297
|
+
# So edge is B -> A (B blocks A)
|
|
298
|
+
local -A in_degree=()
|
|
299
|
+
local -A adj=() # adj[B] = "A1 A2 A3" (B blocks these)
|
|
300
|
+
|
|
301
|
+
for id in "${all_issues[@]}"; do
|
|
302
|
+
in_degree[$id]=0
|
|
303
|
+
adj[$id]=""
|
|
304
|
+
done
|
|
305
|
+
|
|
306
|
+
# For each issue, get its dependencies
|
|
307
|
+
for id in "${all_issues[@]}"; do
|
|
308
|
+
local show_out
|
|
309
|
+
show_out="$(${BD_CMD:-bd} show "$id" 2>&1)" || continue
|
|
310
|
+
|
|
311
|
+
# Parse DEPENDS ON section — extract issue IDs
|
|
312
|
+
local deps
|
|
313
|
+
deps="$(printf '%s' "$show_out" | sed -n '/^DEPENDS ON/,/^$/p' | grep -oE '[A-Za-z]+-[A-Za-z0-9]+' || true)"
|
|
314
|
+
|
|
315
|
+
while IFS= read -r dep_id; do
|
|
316
|
+
[[ -z "$dep_id" ]] && continue
|
|
317
|
+
# Only count deps that are in our open issues list
|
|
318
|
+
local found=0
|
|
319
|
+
for oid in "${all_issues[@]}"; do
|
|
320
|
+
if [[ "$oid" == "$dep_id" ]]; then
|
|
321
|
+
found=1
|
|
322
|
+
break
|
|
323
|
+
fi
|
|
324
|
+
done
|
|
325
|
+
[[ "$found" -eq 0 ]] && continue
|
|
326
|
+
|
|
327
|
+
# dep_id blocks id (dep_id must merge first)
|
|
328
|
+
in_degree[$id]=$(( ${in_degree[$id]} + 1 ))
|
|
329
|
+
adj[$dep_id]="${adj[$dep_id]} $id"
|
|
330
|
+
done <<< "$deps"
|
|
331
|
+
done
|
|
332
|
+
|
|
333
|
+
# Kahn's algorithm
|
|
334
|
+
local -a queue=()
|
|
335
|
+
local -a result=()
|
|
336
|
+
|
|
337
|
+
# Find all nodes with in-degree 0
|
|
338
|
+
for id in "${all_issues[@]}"; do
|
|
339
|
+
if [[ "${in_degree[$id]}" -eq 0 ]]; then
|
|
340
|
+
queue+=("$id")
|
|
341
|
+
fi
|
|
342
|
+
done
|
|
343
|
+
|
|
344
|
+
while [[ ${#queue[@]} -gt 0 ]]; do
|
|
345
|
+
# Pop first element
|
|
346
|
+
local current="${queue[0]}"
|
|
347
|
+
queue=("${queue[@]:1}")
|
|
348
|
+
result+=("$current")
|
|
349
|
+
|
|
350
|
+
# Reduce in-degree of neighbors
|
|
351
|
+
for neighbor in ${adj[$current]}; do
|
|
352
|
+
in_degree[$neighbor]=$(( ${in_degree[$neighbor]} - 1 ))
|
|
353
|
+
if [[ "${in_degree[$neighbor]}" -eq 0 ]]; then
|
|
354
|
+
queue+=("$neighbor")
|
|
355
|
+
fi
|
|
356
|
+
done
|
|
357
|
+
done
|
|
358
|
+
|
|
359
|
+
# Output
|
|
360
|
+
if [[ "$format" == "json" ]]; then
|
|
361
|
+
printf '%s\n' "${result[@]}" | jq -R . | jq -s -c '.'
|
|
362
|
+
else
|
|
363
|
+
if [[ ${#result[@]} -eq 0 ]]; then
|
|
364
|
+
echo "Nothing to merge"
|
|
365
|
+
elif [[ ${#result[@]} -eq 1 ]]; then
|
|
366
|
+
echo "Ready to merge: ${result[0]}"
|
|
367
|
+
else
|
|
368
|
+
# Check if all are independent (all in-degree 0 initially)
|
|
369
|
+
local all_independent=1
|
|
370
|
+
for id in "${all_issues[@]}"; do
|
|
371
|
+
local show_out
|
|
372
|
+
show_out="$(${BD_CMD:-bd} show "$id" 2>&1)" || continue
|
|
373
|
+
local deps
|
|
374
|
+
deps="$(printf '%s' "$show_out" | sed -n '/^DEPENDS ON/,/^$/p' | grep -oE '[A-Za-z]+-[A-Za-z0-9]+' || true)"
|
|
375
|
+
# Check if any dep is in our open list
|
|
376
|
+
while IFS= read -r dep_id; do
|
|
377
|
+
[[ -z "$dep_id" ]] && continue
|
|
378
|
+
for oid in "${all_issues[@]}"; do
|
|
379
|
+
if [[ "$oid" == "$dep_id" ]]; then
|
|
380
|
+
all_independent=0
|
|
381
|
+
break 2
|
|
382
|
+
fi
|
|
383
|
+
done
|
|
384
|
+
done <<< "$deps"
|
|
385
|
+
[[ "$all_independent" -eq 0 ]] && break
|
|
386
|
+
done
|
|
387
|
+
|
|
388
|
+
if [[ "$all_independent" -eq 1 ]]; then
|
|
389
|
+
echo "Can merge in any order:"
|
|
390
|
+
local i=1
|
|
391
|
+
for id in "${result[@]}"; do
|
|
392
|
+
echo " $i. $id"
|
|
393
|
+
i=$((i + 1))
|
|
394
|
+
done
|
|
395
|
+
else
|
|
396
|
+
echo "Recommended merge order:"
|
|
397
|
+
local i=1
|
|
398
|
+
for id in "${result[@]}"; do
|
|
399
|
+
echo " $i. $id"
|
|
400
|
+
i=$((i + 1))
|
|
401
|
+
done
|
|
402
|
+
fi
|
|
403
|
+
fi
|
|
404
|
+
fi
|
|
405
|
+
}
|
|
406
|
+
cmd_rebase_check() {
|
|
407
|
+
local after_merge=""
|
|
408
|
+
local base
|
|
409
|
+
base="$(get_sync_branch 2>/dev/null || echo "master")"
|
|
410
|
+
|
|
411
|
+
while [[ $# -gt 0 ]]; do
|
|
412
|
+
case "$1" in
|
|
413
|
+
--after-merge=*) after_merge="${1#--after-merge=}"; shift ;;
|
|
414
|
+
--base=*) base="${1#--base=}"; shift ;;
|
|
415
|
+
*) echo "Error: unknown flag '$1'" >&2; return 1 ;;
|
|
416
|
+
esac
|
|
417
|
+
done
|
|
418
|
+
|
|
419
|
+
# Get list of feature branches (not merged into base)
|
|
420
|
+
local branches
|
|
421
|
+
branches="$(git branch --no-merged "$base" --format='%(refname:short)' 2>/dev/null | grep -v '^HEAD' || true)"
|
|
422
|
+
|
|
423
|
+
if [[ -z "$branches" ]]; then
|
|
424
|
+
echo "No branches need rebasing"
|
|
425
|
+
return 0
|
|
426
|
+
fi
|
|
427
|
+
|
|
428
|
+
local found_any=0
|
|
429
|
+
|
|
430
|
+
while IFS= read -r branch; do
|
|
431
|
+
[[ -z "$branch" ]] && continue
|
|
432
|
+
[[ "$branch" == "$base" ]] && continue
|
|
433
|
+
|
|
434
|
+
# Files changed on the branch (relative to base)
|
|
435
|
+
local branch_files
|
|
436
|
+
branch_files="$(git log --name-only --pretty=format: "$base".."$branch" 2>/dev/null | sort -u | grep -v '^$' || true)"
|
|
437
|
+
|
|
438
|
+
# Files changed on base since branch diverged
|
|
439
|
+
local base_files
|
|
440
|
+
if [[ -n "$after_merge" ]]; then
|
|
441
|
+
# Only check overlap with specific merged branch's changes
|
|
442
|
+
base_files="$(git log --name-only --pretty=format: "$branch".."$after_merge" 2>/dev/null | sort -u | grep -v '^$' || true)"
|
|
443
|
+
else
|
|
444
|
+
base_files="$(git log --name-only --pretty=format: "$branch".."$base" 2>/dev/null | sort -u | grep -v '^$' || true)"
|
|
445
|
+
fi
|
|
446
|
+
|
|
447
|
+
if [[ -z "$base_files" ]]; then
|
|
448
|
+
continue # Branch is up-to-date with base
|
|
449
|
+
fi
|
|
450
|
+
|
|
451
|
+
# Check for file overlap
|
|
452
|
+
local overlap
|
|
453
|
+
overlap="$(comm -12 <(printf '%s\n' "$branch_files" | sort) <(printf '%s\n' "$base_files" | sort) 2>/dev/null || true)"
|
|
454
|
+
|
|
455
|
+
if [[ -n "$overlap" ]]; then
|
|
456
|
+
found_any=1
|
|
457
|
+
echo "CONFLICT REBASE: $branch"
|
|
458
|
+
echo " Overlapping files:"
|
|
459
|
+
printf '%s\n' "$overlap" | while IFS= read -r f; do
|
|
460
|
+
[[ -z "$f" ]] && continue
|
|
461
|
+
echo " - $f"
|
|
462
|
+
done
|
|
463
|
+
else
|
|
464
|
+
found_any=1
|
|
465
|
+
echo "CLEAN REBASE: $branch (behind but no file overlap)"
|
|
466
|
+
fi
|
|
467
|
+
done <<< "$branches"
|
|
468
|
+
|
|
469
|
+
if [[ "$found_any" -eq 0 ]]; then
|
|
470
|
+
echo "No branches need rebasing"
|
|
471
|
+
fi
|
|
472
|
+
|
|
473
|
+
return 0
|
|
474
|
+
}
|
|
475
|
+
cmd_auto_label() {
|
|
476
|
+
if [[ $# -lt 1 ]]; then
|
|
477
|
+
echo "Usage: pr-coordinator.sh auto-label <issue-id>" >&2
|
|
478
|
+
return 1
|
|
479
|
+
fi
|
|
480
|
+
|
|
481
|
+
local issue_id="$1"
|
|
482
|
+
|
|
483
|
+
# Get issue details
|
|
484
|
+
local show_output
|
|
485
|
+
show_output="$(${BD_CMD:-bd} show "$issue_id" 2>&1)" || {
|
|
486
|
+
echo "Error: failed to show issue $issue_id" >&2
|
|
487
|
+
return 1
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
# Get PR number from bd state metadata (pr_number:NNN in show output)
|
|
491
|
+
local pr_number=""
|
|
492
|
+
pr_number="$(printf '%s' "$show_output" | grep -oE 'pr_number:[0-9]+' | head -1 | cut -d: -f2 || true)"
|
|
493
|
+
|
|
494
|
+
if [[ -z "$pr_number" ]]; then
|
|
495
|
+
echo "No PR found for $issue_id, skipping labels"
|
|
496
|
+
return 0
|
|
497
|
+
fi
|
|
498
|
+
|
|
499
|
+
# Validate PR number
|
|
500
|
+
validate_pr_number "$pr_number" || return 2
|
|
501
|
+
|
|
502
|
+
# Determine label state
|
|
503
|
+
local has_deps=0
|
|
504
|
+
local blocks_others=0
|
|
505
|
+
local needs_rebase=0
|
|
506
|
+
|
|
507
|
+
# Check DEPENDS ON section
|
|
508
|
+
if printf '%s' "$show_output" | grep -q '^DEPENDS ON'; then
|
|
509
|
+
local dep_lines
|
|
510
|
+
dep_lines="$(printf '%s' "$show_output" | sed -n '/^DEPENDS ON/,/^$/p' | grep -E '^\s+' || true)"
|
|
511
|
+
[[ -n "$dep_lines" ]] && has_deps=1
|
|
512
|
+
fi
|
|
513
|
+
|
|
514
|
+
# Check BLOCKS section
|
|
515
|
+
if printf '%s' "$show_output" | grep -q '^BLOCKS'; then
|
|
516
|
+
local block_lines
|
|
517
|
+
block_lines="$(printf '%s' "$show_output" | sed -n '/^BLOCKS/,/^$/p' | grep -E '^\s+' || true)"
|
|
518
|
+
[[ -n "$block_lines" ]] && blocks_others=1
|
|
519
|
+
fi
|
|
520
|
+
|
|
521
|
+
# Check if branch needs rebase (behind base)
|
|
522
|
+
local pr_branch=""
|
|
523
|
+
pr_branch="$(printf '%s' "$show_output" | grep -oE 'pr_branch:[a-zA-Z0-9./_@-]+' | head -1 | cut -d: -f2 || true)"
|
|
524
|
+
if [[ -n "$pr_branch" ]]; then
|
|
525
|
+
local base
|
|
526
|
+
base="$(get_sync_branch 2>/dev/null || echo "master")"
|
|
527
|
+
local behind_count
|
|
528
|
+
behind_count="$(git rev-list --count "$pr_branch".."$base" 2>/dev/null || echo 0)"
|
|
529
|
+
[[ "$behind_count" -gt 0 ]] && needs_rebase=1
|
|
530
|
+
fi
|
|
531
|
+
|
|
532
|
+
# Apply or remove labels
|
|
533
|
+
local GH_CMD="${GH_CMD:-gh}"
|
|
534
|
+
local labels_added=()
|
|
535
|
+
local labels_removed=()
|
|
536
|
+
|
|
537
|
+
if [[ "$has_deps" -eq 1 ]]; then
|
|
538
|
+
$GH_CMD pr edit "$pr_number" --add-label "forge/has-deps" 2>/dev/null && labels_added+=("forge/has-deps") || true
|
|
539
|
+
else
|
|
540
|
+
$GH_CMD pr edit "$pr_number" --remove-label "forge/has-deps" 2>/dev/null && labels_removed+=("forge/has-deps") || true
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
if [[ "$blocks_others" -eq 1 ]]; then
|
|
544
|
+
$GH_CMD pr edit "$pr_number" --add-label "forge/blocks-others" 2>/dev/null && labels_added+=("forge/blocks-others") || true
|
|
545
|
+
else
|
|
546
|
+
$GH_CMD pr edit "$pr_number" --remove-label "forge/blocks-others" 2>/dev/null && labels_removed+=("forge/blocks-others") || true
|
|
547
|
+
fi
|
|
548
|
+
|
|
549
|
+
if [[ "$needs_rebase" -eq 1 ]]; then
|
|
550
|
+
$GH_CMD pr edit "$pr_number" --add-label "forge/needs-rebase" 2>/dev/null && labels_added+=("forge/needs-rebase") || true
|
|
551
|
+
else
|
|
552
|
+
$GH_CMD pr edit "$pr_number" --remove-label "forge/needs-rebase" 2>/dev/null && labels_removed+=("forge/needs-rebase") || true
|
|
553
|
+
fi
|
|
554
|
+
|
|
555
|
+
# Report
|
|
556
|
+
if [[ ${#labels_added[@]} -gt 0 ]]; then
|
|
557
|
+
echo "Labels added: ${labels_added[*]}"
|
|
558
|
+
fi
|
|
559
|
+
if [[ ${#labels_removed[@]} -gt 0 ]]; then
|
|
560
|
+
echo "Labels removed: ${labels_removed[*]}"
|
|
561
|
+
fi
|
|
562
|
+
if [[ ${#labels_added[@]} -eq 0 ]] && [[ ${#labels_removed[@]} -eq 0 ]]; then
|
|
563
|
+
echo "No label changes needed for PR #$pr_number"
|
|
564
|
+
fi
|
|
565
|
+
}
|
|
566
|
+
collect_worktree_entries() {
|
|
567
|
+
local git_cmd porcelain current_path current_branch entries line separator
|
|
568
|
+
entries=""
|
|
569
|
+
current_path=""
|
|
570
|
+
current_branch=""
|
|
571
|
+
separator=$'\t'
|
|
572
|
+
git_cmd="${GIT_CMD:-git}"
|
|
573
|
+
porcelain="$("$git_cmd" worktree list --porcelain 2>/dev/null || true)"
|
|
574
|
+
|
|
575
|
+
while IFS= read -r line; do
|
|
576
|
+
case "$line" in
|
|
577
|
+
worktree\ *)
|
|
578
|
+
current_path="${line#worktree }"
|
|
579
|
+
;;
|
|
580
|
+
branch\ refs/heads/*)
|
|
581
|
+
current_branch="${line#branch refs/heads/}"
|
|
582
|
+
;;
|
|
583
|
+
"")
|
|
584
|
+
if [[ -n "$current_path" ]]; then
|
|
585
|
+
if [[ -n "$entries" ]]; then
|
|
586
|
+
entries+=$'\n'
|
|
587
|
+
fi
|
|
588
|
+
entries+="${current_path}${separator}${current_branch}"
|
|
589
|
+
fi
|
|
590
|
+
current_path=""
|
|
591
|
+
current_branch=""
|
|
592
|
+
;;
|
|
593
|
+
esac
|
|
594
|
+
done <<EOF
|
|
595
|
+
$porcelain
|
|
596
|
+
|
|
597
|
+
EOF
|
|
598
|
+
|
|
599
|
+
if [[ -n "$current_path" ]]; then
|
|
600
|
+
if [[ -n "$entries" ]]; then
|
|
601
|
+
entries+=$'\n'
|
|
602
|
+
fi
|
|
603
|
+
entries+="${current_path}${separator}${current_branch}"
|
|
604
|
+
fi
|
|
605
|
+
|
|
606
|
+
printf '%s' "$entries"
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
cmd_stale_worktrees() {
|
|
610
|
+
local threshold_hours=48
|
|
611
|
+
local worktree_filter=""
|
|
612
|
+
local git_cmd
|
|
613
|
+
git_cmd="${GIT_CMD:-git}"
|
|
614
|
+
|
|
615
|
+
while [[ $# -gt 0 ]]; do
|
|
616
|
+
case "$1" in
|
|
617
|
+
--threshold=*)
|
|
618
|
+
local val="${1#--threshold=}"
|
|
619
|
+
# Parse hours: "48h" -> 48, "72h" -> 72, just a number -> hours
|
|
620
|
+
threshold_hours="${val%h}"
|
|
621
|
+
if [[ ! "$threshold_hours" =~ ^[0-9]+$ ]]; then
|
|
622
|
+
echo "Error: invalid threshold '$val' (use e.g. 48h or 48)" >&2
|
|
623
|
+
return 1
|
|
624
|
+
fi
|
|
625
|
+
shift ;;
|
|
626
|
+
--dir=*) worktree_filter="${1#--dir=}"; shift ;;
|
|
627
|
+
*) echo "Error: unknown flag '$1'" >&2; return 1 ;;
|
|
628
|
+
esac
|
|
629
|
+
done
|
|
630
|
+
|
|
631
|
+
local repo_root
|
|
632
|
+
repo_root="$("$git_cmd" rev-parse --show-toplevel 2>/dev/null)" || repo_root="."
|
|
633
|
+
|
|
634
|
+
local worktree_entries
|
|
635
|
+
worktree_entries="$(collect_worktree_entries)"
|
|
636
|
+
|
|
637
|
+
if [[ -z "$worktree_entries" ]]; then
|
|
638
|
+
echo "No worktrees found"
|
|
639
|
+
return 0
|
|
640
|
+
fi
|
|
641
|
+
|
|
642
|
+
# OWASP A01: validate each worktree path with realpath
|
|
643
|
+
local found_any=0
|
|
644
|
+
local stale_count=0
|
|
645
|
+
|
|
646
|
+
while IFS=$'\t' read -r entry_path entry_branch; do
|
|
647
|
+
[[ -n "$entry_path" ]] || continue
|
|
648
|
+
|
|
649
|
+
if [[ "$entry_path" == "$repo_root" ]]; then
|
|
650
|
+
continue
|
|
651
|
+
fi
|
|
652
|
+
|
|
653
|
+
if [[ -n "$worktree_filter" ]] && [[ "$entry_path" != *"$worktree_filter"* ]]; then
|
|
654
|
+
continue
|
|
655
|
+
fi
|
|
656
|
+
|
|
657
|
+
found_any=1
|
|
658
|
+
|
|
659
|
+
# Get last commit date
|
|
660
|
+
local last_commit_date
|
|
661
|
+
last_commit_date="$("$git_cmd" -C "$entry_path" log -1 --format=%ci 2>/dev/null)" || continue
|
|
662
|
+
|
|
663
|
+
# Get branch name
|
|
664
|
+
local branch_name="${entry_branch:-}"
|
|
665
|
+
if [[ -z "$branch_name" ]]; then
|
|
666
|
+
branch_name="$("$git_cmd" -C "$entry_path" branch --show-current 2>/dev/null)" || branch_name="unknown"
|
|
667
|
+
fi
|
|
668
|
+
branch_name="${branch_name:-unknown}"
|
|
669
|
+
|
|
670
|
+
# Calculate age in hours
|
|
671
|
+
local last_epoch now_epoch
|
|
672
|
+
last_epoch="$(date -d "$last_commit_date" +%s 2>/dev/null || date -jf "%Y-%m-%d %H:%M:%S %z" "$last_commit_date" +%s 2>/dev/null || echo 0)"
|
|
673
|
+
now_epoch="$(date +%s)"
|
|
674
|
+
local age_hours=$(( (now_epoch - last_epoch) / 3600 ))
|
|
675
|
+
|
|
676
|
+
if [[ "$age_hours" -ge "$threshold_hours" ]]; then
|
|
677
|
+
stale_count=$((stale_count + 1))
|
|
678
|
+
local dir_name
|
|
679
|
+
dir_name="$(basename "$entry_path")"
|
|
680
|
+
echo "STALE: $dir_name (branch: $branch_name, last commit: ${age_hours}h ago)"
|
|
681
|
+
fi
|
|
682
|
+
done <<EOF
|
|
683
|
+
$worktree_entries
|
|
684
|
+
EOF
|
|
685
|
+
|
|
686
|
+
if [[ "$found_any" -eq 0 ]]; then
|
|
687
|
+
echo "No worktrees found"
|
|
688
|
+
elif [[ "$stale_count" -eq 0 ]]; then
|
|
689
|
+
echo "All worktrees are active"
|
|
690
|
+
else
|
|
691
|
+
echo ""
|
|
692
|
+
echo "$stale_count potentially abandoned worktree(s) found"
|
|
693
|
+
fi
|
|
694
|
+
|
|
695
|
+
return 0 # Always exit 0 (informational only)
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
cmd_help() {
|
|
699
|
+
cat <<'EOF'
|
|
700
|
+
Usage: pr-coordinator.sh <subcommand> [args...]
|
|
701
|
+
|
|
702
|
+
Subcommands:
|
|
703
|
+
dep add <issue-a> <issue-b> Record issue-a depends on issue-b
|
|
704
|
+
dep remove <issue-a> <issue-b> Remove dependency
|
|
705
|
+
dep list <issue-id> Show dependencies for issue
|
|
706
|
+
dep list-all Show all open issues with dependencies
|
|
707
|
+
merge-sim <branch> Dry-run merge simulation
|
|
708
|
+
merge-order [--format=text|json] Recommended merge sequence
|
|
709
|
+
rebase-check [--after-merge=<branch>] Rebase guidance
|
|
710
|
+
auto-label <issue-id> Auto-tag PR labels
|
|
711
|
+
stale-worktrees [--threshold=48h] Detect abandoned worktrees
|
|
712
|
+
help Show this message
|
|
713
|
+
EOF
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
# ── Main dispatcher ──────────────────────────────────────────────────
|
|
717
|
+
|
|
718
|
+
main() {
|
|
719
|
+
if [[ $# -lt 1 ]]; then
|
|
720
|
+
cmd_help >&2
|
|
721
|
+
exit 1
|
|
722
|
+
fi
|
|
723
|
+
|
|
724
|
+
local subcommand="$1"
|
|
725
|
+
shift
|
|
726
|
+
|
|
727
|
+
case "$subcommand" in
|
|
728
|
+
dep) cmd_dep "$@" ;;
|
|
729
|
+
merge-sim)
|
|
730
|
+
# Validate branch name if provided
|
|
731
|
+
if [[ $# -ge 1 ]]; then
|
|
732
|
+
validate_branch_name "$1" || exit 2
|
|
733
|
+
fi
|
|
734
|
+
cmd_merge_sim "$@"
|
|
735
|
+
;;
|
|
736
|
+
merge-order) cmd_merge_order "$@" ;;
|
|
737
|
+
rebase-check) cmd_rebase_check "$@" ;;
|
|
738
|
+
auto-label)
|
|
739
|
+
# Validate issue-id if provided
|
|
740
|
+
if [[ $# -ge 1 ]] && [[ ! "$1" =~ ^[a-zA-Z0-9-]+$ ]]; then
|
|
741
|
+
echo "Error: invalid issue-id format" >&2
|
|
742
|
+
exit 2
|
|
743
|
+
fi
|
|
744
|
+
cmd_auto_label "$@"
|
|
745
|
+
;;
|
|
746
|
+
stale-worktrees) cmd_stale_worktrees "$@" ;;
|
|
747
|
+
help) cmd_help ;;
|
|
748
|
+
*)
|
|
749
|
+
echo "Error: unknown subcommand '$subcommand'" >&2
|
|
750
|
+
cmd_help >&2
|
|
751
|
+
exit 1
|
|
752
|
+
;;
|
|
753
|
+
esac
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
main "$@"
|