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,256 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/forge-team/lib/claim.sh — Claim locking with pre-claim check
3
+ #
4
+ # Before allowing issue claim, checks GitHub assignee and uses flock/mkdir
5
+ # to prevent race conditions between concurrent agents.
6
+ #
7
+ # Functions:
8
+ # pre_claim_check — Check if issue is claimable
9
+ # claim_with_lock — Atomic claim with file locking
10
+ # forge_team_claim — Top-level dispatcher entry point
11
+ #
12
+ # Env overrides (for testing):
13
+ # GH_CMD — Path to gh binary
14
+ # BD_CMD — Path to bd binary
15
+ #
16
+ # This file does NOT set errexit/pipefail — callers manage their own shell options.
17
+
18
+ # Guard against double-sourcing
19
+ if [[ -n "${_CLAIM_LIB_LOADED:-}" ]]; then
20
+ return 0 2>/dev/null || true
21
+ fi
22
+ _CLAIM_LIB_LOADED=1
23
+
24
+ _CLAIM_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+
26
+ # Source dependencies
27
+ if [[ -f "$_CLAIM_DIR/agent-prompt.sh" ]]; then
28
+ source "$_CLAIM_DIR/agent-prompt.sh"
29
+ fi
30
+
31
+ if [[ -f "$_CLAIM_DIR/identity.sh" ]]; then
32
+ source "$_CLAIM_DIR/identity.sh"
33
+ fi
34
+
35
+ # ── Internal helpers ─────────────────────────────────────────────────────
36
+
37
+ # _claim_error <message>
38
+ _claim_error() {
39
+ if declare -f agent_error &>/dev/null; then
40
+ agent_error "$1"
41
+ else
42
+ echo "ERROR: $1" >&2
43
+ fi
44
+ }
45
+
46
+ # _claim_info <message>
47
+ _claim_info() {
48
+ if declare -f agent_info &>/dev/null; then
49
+ agent_info "$1"
50
+ else
51
+ echo "INFO: $1" >&2
52
+ fi
53
+ }
54
+
55
+ # _claim_prompt <message>
56
+ _claim_prompt() {
57
+ if declare -f agent_prompt &>/dev/null; then
58
+ agent_prompt "$1"
59
+ else
60
+ echo "PROMPT: $1" >&2
61
+ fi
62
+ }
63
+
64
+ # _get_github_issue_number <beads-id>
65
+ # Extracts github_issue:N from bd show output. Returns number or empty.
66
+ _get_github_issue_number() {
67
+ local beads_id="$1"
68
+ local bd_cmd="${BD_CMD:-bd}"
69
+ local bd_output
70
+ bd_output="$("$bd_cmd" show "$beads_id" 2>/dev/null)" || return 1
71
+
72
+ local issue_num
73
+ issue_num="$(printf '%s' "$bd_output" | grep -oP 'github_issue:\K[0-9]+' || true)"
74
+ if [[ -z "$issue_num" ]]; then
75
+ return 1
76
+ fi
77
+ printf '%s' "$issue_num"
78
+ }
79
+
80
+ # _get_github_assignee <issue-number>
81
+ # Returns the GitHub login of the assignee, or empty if unassigned.
82
+ _get_github_assignee() {
83
+ local issue_num="$1"
84
+ local gh_cmd="${GH_CMD:-gh}"
85
+ local assignee
86
+ assignee="$("$gh_cmd" issue view "$issue_num" --json assignees --jq '.assignees[0].login // empty' 2>/dev/null)" || return 1
87
+ printf '%s' "$assignee"
88
+ }
89
+
90
+ # _claim_lock_dir — Returns the path to the claim lock directory
91
+ _claim_lock_dir() {
92
+ local root="${TEAM_MAP_ROOT:-.}"
93
+ printf '%s' "$root/.beads"
94
+ }
95
+
96
+ # ── Public API ───────────────────────────────────────────────────────────
97
+
98
+ # pre_claim_check <beads-id>
99
+ # Checks if issue is claimable. Returns 0 if clear, 1 if blocked.
100
+ pre_claim_check() {
101
+ local beads_id="${1:-}"
102
+ if [[ -z "$beads_id" ]]; then
103
+ _claim_error "Usage: pre_claim_check <beads-id>"
104
+ return 1
105
+ fi
106
+
107
+ # Get current user first (validates gh auth)
108
+ local current_user
109
+ if ! current_user="$(get_github_user)"; then
110
+ _claim_error "GitHub CLI not authenticated. Run 'gh auth login' first."
111
+ return 1
112
+ fi
113
+
114
+ # Get GitHub issue number from beads
115
+ local issue_num
116
+ if ! issue_num="$(_get_github_issue_number "$beads_id")"; then
117
+ _claim_error "No github_issue number found for $beads_id"
118
+ return 1
119
+ fi
120
+
121
+ if [[ -z "$issue_num" ]]; then
122
+ _claim_error "No github_issue number found for $beads_id"
123
+ return 1
124
+ fi
125
+
126
+ # Query GitHub assignee
127
+ local assignee
128
+ assignee="$(_get_github_assignee "$issue_num")" || true
129
+
130
+ # If unassigned or assigned to current user: proceed
131
+ if [[ -z "$assignee" ]] || [[ "$assignee" == "$current_user" ]]; then
132
+ return 0
133
+ fi
134
+
135
+ # Assigned to someone else: prompt
136
+ _claim_prompt "$beads_id is claimed by $assignee. Override? Run: forge team claim $beads_id --force"
137
+ return 1
138
+ }
139
+
140
+ # claim_with_lock <beads-id> [--force]
141
+ # Atomic claim using flock/mkdir locking pattern.
142
+ # With --force, skips the re-check of assignee inside the lock.
143
+ claim_with_lock() {
144
+ local beads_id="${1:-}"
145
+ local force="${2:-}"
146
+ if [[ -z "$beads_id" ]]; then
147
+ _claim_error "Usage: claim_with_lock <beads-id>"
148
+ return 1
149
+ fi
150
+
151
+ local gh_cmd="${GH_CMD:-gh}"
152
+ local bd_cmd="${BD_CMD:-bd}"
153
+ local lock_dir
154
+ lock_dir="$(_claim_lock_dir)"
155
+ mkdir -p "$lock_dir"
156
+
157
+ local lock_file="$lock_dir/claim.lock"
158
+
159
+ # Get current user
160
+ local current_user
161
+ if ! current_user="$(get_github_user)"; then
162
+ _claim_error "GitHub CLI not authenticated."
163
+ return 1
164
+ fi
165
+
166
+ # Get GitHub issue number
167
+ local issue_num
168
+ if ! issue_num="$(_get_github_issue_number "$beads_id")"; then
169
+ _claim_error "No github_issue number found for $beads_id"
170
+ return 1
171
+ fi
172
+
173
+ if command -v flock &>/dev/null; then
174
+ # flock-based locking
175
+ (
176
+ flock -w 5 200 || {
177
+ _claim_error "Claim lock timeout after 5s"
178
+ exit 1
179
+ }
180
+ # Re-check assignee inside lock (skip if --force)
181
+ if [[ "$force" != "--force" ]]; then
182
+ local assignee
183
+ assignee="$(_get_github_assignee "$issue_num")" || true
184
+ if [[ -n "$assignee" ]] && [[ "$assignee" != "$current_user" ]]; then
185
+ _claim_error "$beads_id was claimed by $assignee while waiting for lock"
186
+ exit 1
187
+ fi
188
+ fi
189
+ # Claim on both Beads and GitHub
190
+ "$bd_cmd" update "$beads_id" --claim >/dev/null 2>&1 || true
191
+ "$gh_cmd" issue edit "$issue_num" --add-assignee "$current_user" >/dev/null 2>&1 || true
192
+ ) 200>"$lock_file"
193
+ local subshell_rc=$?
194
+ [[ $subshell_rc -ne 0 ]] && return $subshell_rc
195
+ else
196
+ # mkdir-based fallback
197
+ local lock_mkdir="$lock_dir/claim.lock.d"
198
+ local attempts=0
199
+ while ! mkdir "$lock_mkdir" 2>/dev/null; do
200
+ attempts=$((attempts + 1))
201
+ if [[ $attempts -ge 50 ]]; then
202
+ _claim_error "Claim lock timeout after 5s"
203
+ return 1
204
+ fi
205
+ sleep 0.1
206
+ done
207
+ trap 'rmdir "$lock_mkdir" 2>/dev/null; trap - RETURN' RETURN
208
+
209
+ # Re-check assignee inside lock (skip if --force)
210
+ if [[ "$force" != "--force" ]]; then
211
+ local assignee
212
+ assignee="$(_get_github_assignee "$issue_num")" || true
213
+ if [[ -n "$assignee" ]] && [[ "$assignee" != "$current_user" ]]; then
214
+ _claim_error "$beads_id was claimed by $assignee while waiting for lock"
215
+ return 1
216
+ fi
217
+ fi
218
+ # Claim on both Beads and GitHub
219
+ "$bd_cmd" update "$beads_id" --claim >/dev/null 2>&1 || true
220
+ "$gh_cmd" issue edit "$issue_num" --add-assignee "$current_user" >/dev/null 2>&1 || true
221
+ fi
222
+
223
+ return 0
224
+ }
225
+
226
+ # forge_team_claim <beads-id> [--force]
227
+ # Top-level function called by the dispatcher.
228
+ forge_team_claim() {
229
+ local beads_id="${1:-}"
230
+ local force_flag="${2:-}"
231
+
232
+ if [[ -z "$beads_id" ]]; then
233
+ _claim_error "Usage: forge team claim <beads-id> [--force]"
234
+ return 1
235
+ fi
236
+
237
+ local bd_cmd="${BD_CMD:-bd}"
238
+
239
+ if [[ "$force_flag" == "--force" ]]; then
240
+ # Skip pre-claim check, claim directly (pass --force to skip lock re-check too)
241
+ claim_with_lock "$beads_id" "--force" || return 1
242
+ # Log override via bd comments
243
+ "$bd_cmd" comments add "$beads_id" "Force-claimed by $(get_github_user 2>/dev/null || echo 'unknown')" >/dev/null 2>&1 || true
244
+ _claim_info "$beads_id force-claimed successfully"
245
+ return 0
246
+ fi
247
+
248
+ # Normal flow: pre-claim check then lock-claim
249
+ if ! pre_claim_check "$beads_id"; then
250
+ return 1
251
+ fi
252
+
253
+ claim_with_lock "$beads_id" || return 1
254
+ _claim_info "$beads_id claimed successfully"
255
+ return 0
256
+ }
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env bash
2
+ # dashboard.sh — Team health dashboard for forge-team.
3
+ #
4
+ # Provides cmd_dashboard [--format=json] which aggregates team issue data
5
+ # from Beads and displays per-developer stats, stale assignments, and
6
+ # blocked issues.
7
+
8
+ # ── Dependencies ─────────────────────────────────────────────────────────
9
+ # BD_CMD — path to bd binary (default: bd)
10
+ BD_CMD="${BD_CMD:-bd}"
11
+
12
+ # FORGE_NOW — override current epoch for testing (default: real time)
13
+ _now() {
14
+ if [[ -n "${FORGE_NOW:-}" ]]; then
15
+ echo "$FORGE_NOW"
16
+ else
17
+ date +%s
18
+ fi
19
+ }
20
+
21
+ # Convert ISO8601 timestamp to epoch seconds
22
+ _iso_to_epoch() {
23
+ local ts="$1"
24
+ # Remove trailing Z and replace T with space for date parsing
25
+ local clean="${ts%Z}"
26
+ clean="${clean//T/ }"
27
+ # Use date -d for GNU date (Linux/Git Bash)
28
+ if date -d "$clean UTC" +%s 2>/dev/null; then
29
+ return
30
+ fi
31
+ # Fallback: manual parse YYYY-MM-DD HH:MM:SS
32
+ # This handles the common case in Git Bash on Windows
33
+ local year month day hour min sec
34
+ IFS='-: ' read -r year month day hour min sec <<< "$clean"
35
+ # Use python if available
36
+ if command -v python3 &>/dev/null; then
37
+ python3 -c "import calendar,datetime; print(int(calendar.timegm(datetime.datetime($year,$month,$day,$hour,$min,$sec).timetuple())))"
38
+ elif command -v python &>/dev/null; then
39
+ python -c "import calendar,datetime; print(int(calendar.timegm(datetime.datetime($year,$month,$day,$hour,$min,$sec).timetuple())))"
40
+ else
41
+ # Last resort: approximate with GNU date
42
+ echo 0
43
+ fi
44
+ }
45
+
46
+ # ── Data Collection ──────────────────────────────────────────────────────
47
+
48
+ # Parse bd list output into issue IDs and titles.
49
+ # Each line: "◐ forge-aaa · Feature A" or "○ forge-bbb · Feature B"
50
+ _parse_issue_list() {
51
+ local line
52
+ while IFS= read -r line; do
53
+ [[ -z "$line" ]] && continue
54
+ # Extract issue ID: between status icon and " · "
55
+ local id title
56
+ id="$(echo "$line" | sed -E 's/^[^ ]+ +([^ ]+) +·.*/\1/')"
57
+ title="$(echo "$line" | sed -E 's/^[^ ]+ +[^ ]+ +· +(.*)/\1/')"
58
+ echo "$id|$title"
59
+ done
60
+ }
61
+
62
+ # Get issue details: status, owner, updated timestamp, blocked-by list
63
+ # Output: id|title|status|owner|updated_epoch|blocked_by
64
+ _get_issue_details() {
65
+ local issue_id="$1"
66
+ local show_output
67
+ show_output="$($BD_CMD show "$issue_id" 2>/dev/null)" || return 1
68
+
69
+ local status="OPEN" owner="" updated="" blocked_by="" title=""
70
+
71
+ local line
72
+ local in_depends=0
73
+ while IFS= read -r line; do
74
+ # First line has status: "◐ forge-aaa · Feature A [● P2 · IN_PROGRESS]"
75
+ if [[ "$line" =~ \[.*·[[:space:]]*(OPEN|IN_PROGRESS|BLOCKED)\] ]]; then
76
+ status="${BASH_REMATCH[1]}"
77
+ # Extract title from first line
78
+ title="$(echo "$line" | sed -E 's/^[^ ]+ +[^ ]+ +· +(.+) +\[.*/\1/')"
79
+ fi
80
+
81
+ # Owner line
82
+ if [[ "$line" =~ ^Owner:[[:space:]]*(.+) ]]; then
83
+ owner="${BASH_REMATCH[1]}"
84
+ fi
85
+
86
+ # Updated line
87
+ if [[ "$line" =~ ^Updated:[[:space:]]*(.+) ]]; then
88
+ updated="${BASH_REMATCH[1]}"
89
+ fi
90
+
91
+ # DEPENDS ON section
92
+ if [[ "$line" == "DEPENDS ON" ]]; then
93
+ in_depends=1
94
+ continue
95
+ fi
96
+
97
+ if [[ $in_depends -eq 1 && "$line" =~ →.*([a-z]+-[a-z]+):.* ]]; then
98
+ # Extract dependency issue ID
99
+ local dep_id
100
+ dep_id="$(echo "$line" | sed -E 's/.*→ +[^ ]+ +([a-z]+-[a-z0-9]+):.*/\1/')"
101
+ if [[ -n "$blocked_by" ]]; then
102
+ blocked_by="$blocked_by,$dep_id"
103
+ else
104
+ blocked_by="$dep_id"
105
+ fi
106
+ fi
107
+ done <<< "$show_output"
108
+
109
+ local updated_epoch=0
110
+ if [[ -n "$updated" ]]; then
111
+ updated_epoch="$(_iso_to_epoch "$updated")"
112
+ fi
113
+
114
+ echo "$issue_id|$title|$status|$owner|$updated_epoch|$blocked_by"
115
+ }
116
+
117
+ # ── Dashboard Command ────────────────────────────────────────────────────
118
+
119
+ cmd_dashboard() {
120
+ local format="text"
121
+
122
+ # Parse args
123
+ local arg
124
+ for arg in "$@"; do
125
+ case "$arg" in
126
+ --format=json) format="json" ;;
127
+ --format=*) echo "Error: unsupported format '${arg#--format=}'" >&2; return 1 ;;
128
+ esac
129
+ done
130
+
131
+ # 1. Get all open/in_progress issues
132
+ local issue_list
133
+ issue_list="$($BD_CMD list --status=open,in_progress 2>/dev/null)" || issue_list=""
134
+
135
+ # Check for empty
136
+ if [[ -z "$issue_list" ]]; then
137
+ if [[ "$format" == "json" ]]; then
138
+ echo '{"developers":{},"total":0,"stale":[],"blocked":[],"message":"No active issues — team is clear"}'
139
+ else
140
+ echo "No active issues — team is clear"
141
+ fi
142
+ return 0
143
+ fi
144
+
145
+ # 2. Parse issue IDs
146
+ local issues=()
147
+ while IFS= read -r entry; do
148
+ [[ -z "$entry" ]] && continue
149
+ issues+=("$entry")
150
+ done <<< "$(echo "$issue_list" | _parse_issue_list)"
151
+
152
+ # 3. Collect details for each issue
153
+ local now_epoch
154
+ now_epoch="$(_now)"
155
+ local stale_threshold=172800 # 48h in seconds
156
+
157
+ # Associative arrays for per-developer aggregation
158
+ declare -A dev_open dev_inprogress dev_blocked dev_stale
159
+
160
+ # Lists for stale and blocked sections
161
+ local stale_entries=()
162
+ local blocked_entries=()
163
+ local total_open=0 total_inprogress=0 total_blocked=0 total_stale=0
164
+
165
+ for entry in "${issues[@]}"; do
166
+ local issue_id="${entry%%|*}"
167
+
168
+ local details
169
+ details="$(_get_issue_details "$issue_id")" || continue
170
+
171
+ IFS='|' read -r _id _title _status _owner _updated_epoch _blocked_by <<< "$details"
172
+
173
+ # Default owner if empty
174
+ [[ -z "$_owner" ]] && _owner="unassigned"
175
+
176
+ # Initialize developer counters
177
+ [[ -z "${dev_open[$_owner]:-}" ]] && dev_open[$_owner]=0
178
+ [[ -z "${dev_inprogress[$_owner]:-}" ]] && dev_inprogress[$_owner]=0
179
+ [[ -z "${dev_blocked[$_owner]:-}" ]] && dev_blocked[$_owner]=0
180
+ [[ -z "${dev_stale[$_owner]:-}" ]] && dev_stale[$_owner]=0
181
+
182
+ # Count by status
183
+ case "$_status" in
184
+ OPEN)
185
+ dev_open[$_owner]=$(( ${dev_open[$_owner]} + 1 ))
186
+ total_open=$((total_open + 1))
187
+ ;;
188
+ IN_PROGRESS)
189
+ dev_inprogress[$_owner]=$(( ${dev_inprogress[$_owner]} + 1 ))
190
+ total_inprogress=$((total_inprogress + 1))
191
+ ;;
192
+ BLOCKED)
193
+ dev_blocked[$_owner]=$(( ${dev_blocked[$_owner]} + 1 ))
194
+ total_blocked=$((total_blocked + 1))
195
+ ;;
196
+ esac
197
+
198
+ # Check for blocked (has DEPENDS ON with open deps)
199
+ if [[ -n "$_blocked_by" ]]; then
200
+ dev_blocked[$_owner]=$(( ${dev_blocked[$_owner]} + 1 ))
201
+ total_blocked=$((total_blocked + 1))
202
+ blocked_entries+=("$_id|$_owner|$_title|$_blocked_by")
203
+ fi
204
+
205
+ # Check for stale (>48h since last update)
206
+ if [[ "$_updated_epoch" -gt 0 ]]; then
207
+ local age=$(( now_epoch - _updated_epoch ))
208
+ if [[ $age -gt $stale_threshold ]]; then
209
+ local hours=$(( age / 3600 ))
210
+ dev_stale[$_owner]=$(( ${dev_stale[$_owner]} + 1 ))
211
+ total_stale=$((total_stale + 1))
212
+ stale_entries+=("$_id|$_owner|$_title|${hours}h since last update")
213
+ fi
214
+ fi
215
+ done
216
+
217
+ local total_issues=${#issues[@]}
218
+
219
+ # 4. Output
220
+ if [[ "$format" == "json" ]]; then
221
+ _dashboard_json "$total_issues"
222
+ else
223
+ _dashboard_text "$total_issues"
224
+ fi
225
+ }
226
+
227
+ # ── Text Output ──────────────────────────────────────────────────────────
228
+
229
+ _dashboard_text() {
230
+ local total_issues="$1"
231
+
232
+ echo "Team Dashboard"
233
+ echo "══════════════"
234
+ echo ""
235
+
236
+ # Header
237
+ printf "%-16s %4s %11s %7s %5s\n" "Developer" "Open" "In Progress" "Blocked" "Stale"
238
+
239
+ # Get sorted list of developers
240
+ local devs=()
241
+ local dev
242
+ for dev in "${!dev_open[@]}"; do
243
+ devs+=("$dev")
244
+ done
245
+ # Sort developers
246
+ IFS=$'\n' devs=($(sort <<< "${devs[*]}")); unset IFS
247
+
248
+ for dev in "${devs[@]}"; do
249
+ printf "%-16s %4d %11d %7d %5d\n" \
250
+ "$dev" \
251
+ "${dev_open[$dev]:-0}" \
252
+ "${dev_inprogress[$dev]:-0}" \
253
+ "${dev_blocked[$dev]:-0}" \
254
+ "${dev_stale[$dev]:-0}"
255
+ done
256
+
257
+ echo ""
258
+
259
+ # Build total summary parts
260
+ local parts=()
261
+ [[ $total_inprogress -gt 0 ]] && parts+=("$total_inprogress in progress")
262
+ [[ $total_blocked -gt 0 ]] && parts+=("$total_blocked blocked")
263
+ [[ $total_stale -gt 0 ]] && parts+=("$total_stale stale")
264
+ [[ $total_open -gt 0 ]] && parts+=("$total_open open")
265
+
266
+ local summary=""
267
+ if [[ ${#parts[@]} -gt 0 ]]; then
268
+ summary=" ($(IFS=', '; echo "${parts[*]}"))"
269
+ fi
270
+ echo "Total: $total_issues issues${summary}"
271
+
272
+ # Stale section
273
+ if [[ ${#stale_entries[@]} -gt 0 ]]; then
274
+ echo ""
275
+ echo "Stale assignments (>48h):"
276
+ local entry
277
+ for entry in "${stale_entries[@]}"; do
278
+ IFS='|' read -r _id _owner _title _age <<< "$entry"
279
+ echo " ⚠ $_id [$_owner] — $_title ($_age)"
280
+ done
281
+ fi
282
+
283
+ # Blocked section
284
+ if [[ ${#blocked_entries[@]} -gt 0 ]]; then
285
+ echo ""
286
+ echo "Blocked issues:"
287
+ local entry
288
+ for entry in "${blocked_entries[@]}"; do
289
+ IFS='|' read -r _id _owner _title _deps <<< "$entry"
290
+ echo " ⚠ $_id [$_owner] — $_title (blocked by $_deps)"
291
+ done
292
+ fi
293
+ }
294
+
295
+ # ── JSON Output ──────────────────────────────────────────────────────────
296
+
297
+ _dashboard_json() {
298
+ local total_issues="$1"
299
+
300
+ # Build developers JSON
301
+ local dev_json="{"
302
+ local first=1
303
+ local devs=()
304
+ local dev
305
+ for dev in "${!dev_open[@]}"; do
306
+ devs+=("$dev")
307
+ done
308
+ IFS=$'\n' devs=($(sort <<< "${devs[*]}")); unset IFS
309
+
310
+ for dev in "${devs[@]}"; do
311
+ [[ $first -eq 0 ]] && dev_json+=","
312
+ first=0
313
+ dev_json+="\"$dev\":{\"open\":${dev_open[$dev]:-0},\"in_progress\":${dev_inprogress[$dev]:-0},\"blocked\":${dev_blocked[$dev]:-0},\"stale\":${dev_stale[$dev]:-0}}"
314
+ done
315
+ dev_json+="}"
316
+
317
+ # Build stale JSON array
318
+ local stale_json="["
319
+ first=1
320
+ local entry
321
+ for entry in "${stale_entries[@]}"; do
322
+ [[ $first -eq 0 ]] && stale_json+=","
323
+ first=0
324
+ IFS='|' read -r _id _owner _title _age <<< "$entry"
325
+ stale_json+="$(jq -n -c --arg id "$_id" --arg owner "$_owner" --arg title "$_title" --arg age "$_age" '{id:$id,owner:$owner,title:$title,age:$age}')"
326
+ done
327
+ stale_json+="]"
328
+
329
+ # Build blocked JSON array
330
+ local blocked_json="["
331
+ first=1
332
+ for entry in "${blocked_entries[@]}"; do
333
+ [[ $first -eq 0 ]] && blocked_json+=","
334
+ first=0
335
+ IFS='|' read -r _id _owner _title _deps <<< "$entry"
336
+ blocked_json+="$(jq -n -c --arg id "$_id" --arg owner "$_owner" --arg title "$_title" --arg deps "$_deps" '{id:$id,owner:$owner,title:$title,blocked_by:$deps}')"
337
+ done
338
+ blocked_json+="]"
339
+
340
+ echo "{\"developers\":$dev_json,\"total\":$total_issues,\"in_progress\":$total_inprogress,\"blocked\":$total_blocked,\"stale\":$total_stale,\"stale_entries\":$stale_json,\"blocked_entries\":$blocked_json}"
341
+ }