forge-workflow 0.0.4 → 0.0.6

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.
Files changed (248) hide show
  1. package/.claude/commands/dev.md +345 -340
  2. package/.claude/commands/plan.md +566 -521
  3. package/.claude/commands/premerge.md +186 -176
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +448 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +212 -164
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +90 -48
  10. package/.claude/commands/validate.md +288 -282
  11. package/.claude/commands/verify.md +269 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +121 -105
  14. package/.claude/scripts/greptile-resolve.sh +558 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +342 -337
  17. package/.cline/workflows/plan.md +563 -518
  18. package/.cline/workflows/premerge.md +183 -173
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +445 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +209 -161
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +87 -45
  25. package/.cline/workflows/validate.md +285 -279
  26. package/.cline/workflows/verify.md +266 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +345 -340
  29. package/.codex/skills/plan/SKILL.md +566 -521
  30. package/.codex/skills/premerge/SKILL.md +186 -176
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +448 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +212 -164
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +90 -48
  37. package/.codex/skills/validate/SKILL.md +288 -282
  38. package/.codex/skills/verify/SKILL.md +269 -221
  39. package/.cursor/commands/dev.md +342 -337
  40. package/.cursor/commands/plan.md +563 -518
  41. package/.cursor/commands/premerge.md +183 -173
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +445 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +209 -161
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +87 -45
  48. package/.cursor/commands/validate.md +285 -279
  49. package/.cursor/commands/verify.md +266 -218
  50. package/.cursor/rules/permissions-guidance.mdc +37 -37
  51. package/.forge/hooks/check-tdd.js +240 -240
  52. package/.github/PLUGIN_TEMPLATE.json +32 -32
  53. package/.github/prompts/dev.prompt.md +347 -342
  54. package/.github/prompts/plan.prompt.md +568 -523
  55. package/.github/prompts/premerge.prompt.md +188 -178
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +450 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +214 -166
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +92 -50
  62. package/.github/prompts/validate.prompt.md +290 -284
  63. package/.github/prompts/verify.prompt.md +271 -223
  64. package/.github/workflows/beads-to-github.yml +56 -0
  65. package/.github/workflows/github-to-beads.yml +97 -0
  66. package/.kilocode/workflows/dev.md +346 -341
  67. package/.kilocode/workflows/plan.md +567 -522
  68. package/.kilocode/workflows/premerge.md +187 -177
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +449 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +213 -165
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +91 -49
  75. package/.kilocode/workflows/validate.md +289 -283
  76. package/.kilocode/workflows/verify.md +270 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +345 -340
  79. package/.opencode/commands/plan.md +566 -521
  80. package/.opencode/commands/premerge.md +186 -176
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +448 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +212 -164
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +90 -48
  87. package/.opencode/commands/validate.md +288 -282
  88. package/.opencode/commands/verify.md +269 -221
  89. package/.roo/commands/dev.md +346 -341
  90. package/.roo/commands/plan.md +567 -522
  91. package/.roo/commands/premerge.md +187 -177
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +449 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +213 -165
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +91 -49
  98. package/.roo/commands/validate.md +289 -283
  99. package/.roo/commands/verify.md +270 -222
  100. package/AGENTS.md +272 -175
  101. package/CLAUDE.md +110 -100
  102. package/README.md +429 -416
  103. package/bin/forge-cmd.js +317 -313
  104. package/bin/forge-preflight.js +322 -309
  105. package/bin/forge.js +4765 -4303
  106. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  107. package/docs/BEADS_GITHUB_SYNC.md +251 -251
  108. package/docs/ENHANCED_ONBOARDING.md +612 -602
  109. package/docs/EXAMPLES.md +482 -482
  110. package/docs/GREPTILE_SETUP.md +400 -400
  111. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  112. package/docs/ROADMAP.md +359 -359
  113. package/docs/SETUP.md +663 -631
  114. package/docs/TOOLCHAIN.md +653 -630
  115. package/docs/VALIDATION.md +363 -363
  116. package/install.sh +40 -1056
  117. package/lefthook.yml +50 -39
  118. package/lib/agents/README.md +198 -198
  119. package/lib/agents/claude.plugin.json +28 -28
  120. package/lib/agents/cline.plugin.json +22 -22
  121. package/lib/agents/codex.plugin.json +19 -19
  122. package/lib/agents/copilot.plugin.json +24 -24
  123. package/lib/agents/cursor.plugin.json +25 -25
  124. package/lib/agents/kilocode.plugin.json +22 -22
  125. package/lib/agents/opencode.plugin.json +20 -20
  126. package/lib/agents/roo.plugin.json +23 -23
  127. package/lib/agents-config.js +2112 -2112
  128. package/lib/beads-health-check.js +143 -0
  129. package/lib/beads-setup.js +341 -0
  130. package/lib/beads-sync-scaffold.js +260 -0
  131. package/lib/commands/_registry.js +134 -0
  132. package/lib/commands/clean.js +181 -0
  133. package/lib/commands/dev.js +571 -513
  134. package/lib/commands/plan.js +692 -692
  135. package/lib/commands/push.js +196 -0
  136. package/lib/commands/recommend.js +119 -119
  137. package/lib/commands/ship.js +377 -377
  138. package/lib/commands/status.js +378 -378
  139. package/lib/commands/sync.js +55 -0
  140. package/lib/commands/team.js +37 -0
  141. package/lib/commands/test.js +207 -0
  142. package/lib/commands/validate.js +602 -602
  143. package/lib/commands/worktree.js +310 -0
  144. package/lib/context-merge.js +359 -359
  145. package/lib/dep-guard/analyzer.js +294 -294
  146. package/lib/dep-guard/behavior-detector.js +98 -98
  147. package/lib/dep-guard/contract-detector.js +162 -162
  148. package/lib/dep-guard/import-detector.js +498 -498
  149. package/lib/dep-guard/path-utils.js +13 -13
  150. package/lib/dep-guard/rubric.js +120 -120
  151. package/lib/dep-guard/task-parser.js +318 -318
  152. package/lib/detect-agent.js +191 -191
  153. package/lib/detect-worktree.js +47 -47
  154. package/lib/docs-command.js +51 -0
  155. package/lib/docs-copy.js +50 -0
  156. package/lib/file-hash.js +26 -26
  157. package/lib/freshness-token.js +148 -0
  158. package/lib/greptile-match.js +80 -0
  159. package/lib/husky-migration.js +450 -0
  160. package/lib/lefthook-check.js +65 -0
  161. package/lib/pat-setup.js +207 -0
  162. package/lib/plugin-catalog.js +350 -350
  163. package/lib/plugin-manager.js +166 -166
  164. package/lib/plugin-recommender.js +141 -141
  165. package/lib/project-discovery.js +491 -491
  166. package/lib/reset.js +309 -0
  167. package/lib/setup-action-log.js +139 -139
  168. package/lib/setup-summary-renderer.js +106 -106
  169. package/lib/setup-utils.js +96 -0
  170. package/lib/setup.js +192 -192
  171. package/lib/smart-merge.js +64 -0
  172. package/lib/symlink-utils.js +81 -0
  173. package/lib/task-ownership.js +117 -0
  174. package/lib/workflow-profiles.js +197 -197
  175. package/package.json +131 -128
  176. package/scripts/beads-context.sh +426 -0
  177. package/scripts/beads-context.test.js +567 -0
  178. package/scripts/behavioral-judge.sh +378 -0
  179. package/scripts/benchmark.js +85 -0
  180. package/scripts/branch-protection.js +183 -0
  181. package/scripts/check-agents.js +172 -0
  182. package/scripts/check-forge-token.js +98 -0
  183. package/scripts/commitlint.js +42 -0
  184. package/scripts/conflict-detect.sh +323 -0
  185. package/scripts/dep-guard-analyze.js +71 -0
  186. package/scripts/dep-guard.sh +789 -0
  187. package/scripts/eval_win.py +249 -0
  188. package/scripts/file-index.sh +493 -0
  189. package/scripts/forge-team/index.sh +86 -0
  190. package/scripts/forge-team/lib/agent-prompt.sh +52 -0
  191. package/scripts/forge-team/lib/claim.sh +256 -0
  192. package/scripts/forge-team/lib/dashboard.sh +341 -0
  193. package/scripts/forge-team/lib/epic.sh +332 -0
  194. package/scripts/forge-team/lib/hooks.sh +253 -0
  195. package/scripts/forge-team/lib/identity.sh +235 -0
  196. package/scripts/forge-team/lib/sync-github.sh +317 -0
  197. package/scripts/forge-team/lib/verify.sh +284 -0
  198. package/scripts/forge-team/lib/workload.sh +296 -0
  199. package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
  200. package/scripts/forge-team/tests/claim.test.sh +179 -0
  201. package/scripts/forge-team/tests/dashboard.test.sh +170 -0
  202. package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
  203. package/scripts/forge-team/tests/epic.test.sh +176 -0
  204. package/scripts/forge-team/tests/hooks.test.sh +239 -0
  205. package/scripts/forge-team/tests/identity.test.sh +176 -0
  206. package/scripts/forge-team/tests/integration.test.sh +371 -0
  207. package/scripts/forge-team/tests/sync-github.test.sh +209 -0
  208. package/scripts/forge-team/tests/verify.test.sh +314 -0
  209. package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
  210. package/scripts/forge-team/tests/workload.test.sh +209 -0
  211. package/scripts/github-beads-sync/comment.mjs +64 -0
  212. package/scripts/github-beads-sync/config.mjs +148 -0
  213. package/scripts/github-beads-sync/github-api.mjs +131 -0
  214. package/scripts/github-beads-sync/index.mjs +332 -0
  215. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  216. package/scripts/github-beads-sync/mapping.mjs +78 -0
  217. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  218. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  219. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  220. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  221. package/scripts/github-beads-sync.config.json +26 -0
  222. package/scripts/improve-command.js +375 -0
  223. package/scripts/lib/eval-runner.js +268 -0
  224. package/scripts/lib/eval-schema.js +135 -0
  225. package/scripts/lib/eval-storage.js +78 -0
  226. package/scripts/lib/grading.js +203 -0
  227. package/scripts/lib/jsonl-lock.sh +48 -0
  228. package/scripts/lib/sanitize.sh +116 -0
  229. package/scripts/lib/transcript-parser.js +63 -0
  230. package/scripts/lint.js +47 -0
  231. package/scripts/migrate-to-bun-test.js +412 -0
  232. package/scripts/pr-coordinator.sh +706 -0
  233. package/scripts/run-command-eval.js +236 -0
  234. package/scripts/smart-status.sh +809 -0
  235. package/scripts/sync-commands.js +571 -0
  236. package/scripts/sync-utils.sh +455 -0
  237. package/scripts/test-dashboard.js +123 -0
  238. package/scripts/test.js +46 -0
  239. package/scripts/validate.sh +94 -0
  240. package/skills/parallel-deep-research/SKILL.md +108 -108
  241. package/skills/parallel-deep-research/evals/README.md +27 -27
  242. package/skills/parallel-deep-research/evals/evals.json +62 -62
  243. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  244. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  245. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  246. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  247. package/.cursor/hooks/state/continual-learning-index.json +0 -19
  248. package/.cursor/hooks/state/continual-learning.json +0 -8
@@ -0,0 +1,706 @@
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
+ cmd_stale_worktrees() {
567
+ local threshold_hours=48
568
+ local worktrees_dir=".worktrees"
569
+
570
+ while [[ $# -gt 0 ]]; do
571
+ case "$1" in
572
+ --threshold=*)
573
+ local val="${1#--threshold=}"
574
+ # Parse hours: "48h" -> 48, "72h" -> 72, just a number -> hours
575
+ threshold_hours="${val%h}"
576
+ if [[ ! "$threshold_hours" =~ ^[0-9]+$ ]]; then
577
+ echo "Error: invalid threshold '$val' (use e.g. 48h or 48)" >&2
578
+ return 1
579
+ fi
580
+ shift ;;
581
+ --dir=*) worktrees_dir="${1#--dir=}"; shift ;;
582
+ *) echo "Error: unknown flag '$1'" >&2; return 1 ;;
583
+ esac
584
+ done
585
+
586
+ local repo_root
587
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || repo_root="."
588
+ local wt_path="$repo_root/$worktrees_dir"
589
+
590
+ if [[ ! -d "$wt_path" ]]; then
591
+ echo "No worktrees found (directory $worktrees_dir does not exist)"
592
+ return 0
593
+ fi
594
+
595
+ # OWASP A01: validate each worktree path with realpath
596
+ local found_any=0
597
+ local stale_count=0
598
+
599
+ for entry in "$wt_path"/*/; do
600
+ [[ -d "$entry" ]] || continue
601
+ found_any=1
602
+
603
+ # Resolve real path and verify it's under repo root
604
+ local real_path
605
+ real_path="$(realpath "$entry" 2>/dev/null)" || continue
606
+ local real_root
607
+ real_root="$(realpath "$repo_root" 2>/dev/null)" || continue
608
+
609
+ if [[ "$real_path" != "$real_root/"* ]]; then
610
+ echo "WARNING: skipping symlink outside repo: $entry" >&2
611
+ continue
612
+ fi
613
+
614
+ # Get last commit date
615
+ local last_commit_date
616
+ last_commit_date="$(git -C "$entry" log -1 --format=%ci 2>/dev/null)" || continue
617
+
618
+ # Get branch name
619
+ local branch_name
620
+ branch_name="$(git -C "$entry" branch --show-current 2>/dev/null)" || branch_name="unknown"
621
+
622
+ # Calculate age in hours
623
+ local last_epoch now_epoch
624
+ 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)"
625
+ now_epoch="$(date +%s)"
626
+ local age_hours=$(( (now_epoch - last_epoch) / 3600 ))
627
+
628
+ if [[ "$age_hours" -ge "$threshold_hours" ]]; then
629
+ stale_count=$((stale_count + 1))
630
+ local dir_name
631
+ dir_name="$(basename "$entry")"
632
+ echo "STALE: $dir_name (branch: $branch_name, last commit: ${age_hours}h ago)"
633
+ fi
634
+ done
635
+
636
+ if [[ "$found_any" -eq 0 ]]; then
637
+ echo "No worktrees found"
638
+ elif [[ "$stale_count" -eq 0 ]]; then
639
+ echo "All worktrees are active"
640
+ else
641
+ echo ""
642
+ echo "$stale_count potentially abandoned worktree(s) found"
643
+ fi
644
+
645
+ return 0 # Always exit 0 (informational only)
646
+ }
647
+
648
+ cmd_help() {
649
+ cat <<'EOF'
650
+ Usage: pr-coordinator.sh <subcommand> [args...]
651
+
652
+ Subcommands:
653
+ dep add <issue-a> <issue-b> Record issue-a depends on issue-b
654
+ dep remove <issue-a> <issue-b> Remove dependency
655
+ dep list <issue-id> Show dependencies for issue
656
+ dep list-all Show all open issues with dependencies
657
+ merge-sim <branch> Dry-run merge simulation
658
+ merge-order [--format=text|json] Recommended merge sequence
659
+ rebase-check [--after-merge=<branch>] Rebase guidance
660
+ auto-label <issue-id> Auto-tag PR labels
661
+ stale-worktrees [--threshold=48h] Detect abandoned worktrees
662
+ help Show this message
663
+ EOF
664
+ }
665
+
666
+ # ── Main dispatcher ──────────────────────────────────────────────────
667
+
668
+ main() {
669
+ if [[ $# -lt 1 ]]; then
670
+ cmd_help >&2
671
+ exit 1
672
+ fi
673
+
674
+ local subcommand="$1"
675
+ shift
676
+
677
+ case "$subcommand" in
678
+ dep) cmd_dep "$@" ;;
679
+ merge-sim)
680
+ # Validate branch name if provided
681
+ if [[ $# -ge 1 ]]; then
682
+ validate_branch_name "$1" || exit 2
683
+ fi
684
+ cmd_merge_sim "$@"
685
+ ;;
686
+ merge-order) cmd_merge_order "$@" ;;
687
+ rebase-check) cmd_rebase_check "$@" ;;
688
+ auto-label)
689
+ # Validate issue-id if provided
690
+ if [[ $# -ge 1 ]] && [[ ! "$1" =~ ^[a-zA-Z0-9-]+$ ]]; then
691
+ echo "Error: invalid issue-id format" >&2
692
+ exit 2
693
+ fi
694
+ cmd_auto_label "$@"
695
+ ;;
696
+ stale-worktrees) cmd_stale_worktrees "$@" ;;
697
+ help) cmd_help ;;
698
+ *)
699
+ echo "Error: unknown subcommand '$subcommand'" >&2
700
+ cmd_help >&2
701
+ exit 1
702
+ ;;
703
+ esac
704
+ }
705
+
706
+ main "$@"