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.
- package/.claude/commands/dev.md +345 -340
- package/.claude/commands/plan.md +566 -521
- package/.claude/commands/premerge.md +186 -176
- package/.claude/commands/research.md +42 -42
- package/.claude/commands/review.md +448 -442
- package/.claude/commands/rollback.md +721 -721
- package/.claude/commands/ship.md +212 -164
- package/.claude/commands/sonarcloud.md +152 -152
- package/.claude/commands/status.md +90 -48
- package/.claude/commands/validate.md +288 -282
- package/.claude/commands/verify.md +269 -221
- package/.claude/rules/greptile-review-process.md +285 -285
- package/.claude/rules/workflow.md +121 -105
- package/.claude/scripts/greptile-resolve.sh +558 -526
- package/.claude/scripts/load-env.sh +32 -32
- package/.cline/workflows/dev.md +342 -337
- package/.cline/workflows/plan.md +563 -518
- package/.cline/workflows/premerge.md +183 -173
- package/.cline/workflows/research.md +39 -39
- package/.cline/workflows/review.md +445 -439
- package/.cline/workflows/rollback.md +718 -718
- package/.cline/workflows/ship.md +209 -161
- package/.cline/workflows/sonarcloud.md +146 -146
- package/.cline/workflows/status.md +87 -45
- package/.cline/workflows/validate.md +285 -279
- package/.cline/workflows/verify.md +266 -218
- package/.codex/config.toml +11 -11
- package/.codex/skills/dev/SKILL.md +345 -340
- package/.codex/skills/plan/SKILL.md +566 -521
- package/.codex/skills/premerge/SKILL.md +186 -176
- package/.codex/skills/research/SKILL.md +42 -42
- package/.codex/skills/review/SKILL.md +448 -442
- package/.codex/skills/rollback/SKILL.md +721 -721
- package/.codex/skills/ship/SKILL.md +212 -164
- package/.codex/skills/sonarcloud/SKILL.md +149 -149
- package/.codex/skills/status/SKILL.md +90 -48
- package/.codex/skills/validate/SKILL.md +288 -282
- package/.codex/skills/verify/SKILL.md +269 -221
- package/.cursor/commands/dev.md +342 -337
- package/.cursor/commands/plan.md +563 -518
- package/.cursor/commands/premerge.md +183 -173
- package/.cursor/commands/research.md +39 -39
- package/.cursor/commands/review.md +445 -439
- package/.cursor/commands/rollback.md +718 -718
- package/.cursor/commands/ship.md +209 -161
- package/.cursor/commands/sonarcloud.md +146 -146
- package/.cursor/commands/status.md +87 -45
- package/.cursor/commands/validate.md +285 -279
- package/.cursor/commands/verify.md +266 -218
- package/.cursor/rules/permissions-guidance.mdc +37 -37
- package/.forge/hooks/check-tdd.js +240 -240
- package/.github/PLUGIN_TEMPLATE.json +32 -32
- package/.github/prompts/dev.prompt.md +347 -342
- package/.github/prompts/plan.prompt.md +568 -523
- package/.github/prompts/premerge.prompt.md +188 -178
- package/.github/prompts/research.prompt.md +44 -44
- package/.github/prompts/review.prompt.md +450 -444
- package/.github/prompts/rollback.prompt.md +723 -723
- package/.github/prompts/ship.prompt.md +214 -166
- package/.github/prompts/sonarcloud.prompt.md +151 -151
- package/.github/prompts/status.prompt.md +92 -50
- package/.github/prompts/validate.prompt.md +290 -284
- package/.github/prompts/verify.prompt.md +271 -223
- package/.github/workflows/beads-to-github.yml +56 -0
- package/.github/workflows/github-to-beads.yml +97 -0
- package/.kilocode/workflows/dev.md +346 -341
- package/.kilocode/workflows/plan.md +567 -522
- package/.kilocode/workflows/premerge.md +187 -177
- package/.kilocode/workflows/research.md +43 -43
- package/.kilocode/workflows/review.md +449 -443
- package/.kilocode/workflows/rollback.md +722 -722
- package/.kilocode/workflows/ship.md +213 -165
- package/.kilocode/workflows/sonarcloud.md +150 -150
- package/.kilocode/workflows/status.md +91 -49
- package/.kilocode/workflows/validate.md +289 -283
- package/.kilocode/workflows/verify.md +270 -222
- package/.mcp.json.example +12 -12
- package/.opencode/commands/dev.md +345 -340
- package/.opencode/commands/plan.md +566 -521
- package/.opencode/commands/premerge.md +186 -176
- package/.opencode/commands/research.md +42 -42
- package/.opencode/commands/review.md +448 -442
- package/.opencode/commands/rollback.md +721 -721
- package/.opencode/commands/ship.md +212 -164
- package/.opencode/commands/sonarcloud.md +149 -149
- package/.opencode/commands/status.md +90 -48
- package/.opencode/commands/validate.md +288 -282
- package/.opencode/commands/verify.md +269 -221
- package/.roo/commands/dev.md +346 -341
- package/.roo/commands/plan.md +567 -522
- package/.roo/commands/premerge.md +187 -177
- package/.roo/commands/research.md +43 -43
- package/.roo/commands/review.md +449 -443
- package/.roo/commands/rollback.md +722 -722
- package/.roo/commands/ship.md +213 -165
- package/.roo/commands/sonarcloud.md +150 -150
- package/.roo/commands/status.md +91 -49
- package/.roo/commands/validate.md +289 -283
- package/.roo/commands/verify.md +270 -222
- package/AGENTS.md +272 -175
- package/CLAUDE.md +110 -100
- package/README.md +429 -416
- package/bin/forge-cmd.js +317 -313
- package/bin/forge-preflight.js +322 -309
- package/bin/forge.js +4765 -4303
- package/docs/AGENT_INSTALL_PROMPT.md +342 -342
- package/docs/BEADS_GITHUB_SYNC.md +251 -251
- package/docs/ENHANCED_ONBOARDING.md +612 -602
- package/docs/EXAMPLES.md +482 -482
- package/docs/GREPTILE_SETUP.md +400 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
- package/docs/ROADMAP.md +359 -359
- package/docs/SETUP.md +663 -631
- package/docs/TOOLCHAIN.md +653 -630
- package/docs/VALIDATION.md +363 -363
- package/install.sh +40 -1056
- package/lefthook.yml +50 -39
- package/lib/agents/README.md +198 -198
- package/lib/agents/claude.plugin.json +28 -28
- package/lib/agents/cline.plugin.json +22 -22
- package/lib/agents/codex.plugin.json +19 -19
- package/lib/agents/copilot.plugin.json +24 -24
- package/lib/agents/cursor.plugin.json +25 -25
- package/lib/agents/kilocode.plugin.json +22 -22
- package/lib/agents/opencode.plugin.json +20 -20
- package/lib/agents/roo.plugin.json +23 -23
- package/lib/agents-config.js +2112 -2112
- package/lib/beads-health-check.js +143 -0
- package/lib/beads-setup.js +341 -0
- package/lib/beads-sync-scaffold.js +260 -0
- package/lib/commands/_registry.js +134 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/dev.js +571 -513
- package/lib/commands/plan.js +692 -692
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +119 -119
- package/lib/commands/ship.js +377 -377
- package/lib/commands/status.js +378 -378
- package/lib/commands/sync.js +55 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +602 -602
- package/lib/commands/worktree.js +310 -0
- package/lib/context-merge.js +359 -359
- package/lib/dep-guard/analyzer.js +294 -294
- package/lib/dep-guard/behavior-detector.js +98 -98
- package/lib/dep-guard/contract-detector.js +162 -162
- package/lib/dep-guard/import-detector.js +498 -498
- package/lib/dep-guard/path-utils.js +13 -13
- package/lib/dep-guard/rubric.js +120 -120
- package/lib/dep-guard/task-parser.js +318 -318
- package/lib/detect-agent.js +191 -191
- package/lib/detect-worktree.js +47 -47
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-hash.js +26 -26
- package/lib/freshness-token.js +148 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +450 -0
- package/lib/lefthook-check.js +65 -0
- package/lib/pat-setup.js +207 -0
- package/lib/plugin-catalog.js +350 -350
- package/lib/plugin-manager.js +166 -166
- package/lib/plugin-recommender.js +141 -141
- package/lib/project-discovery.js +491 -491
- package/lib/reset.js +309 -0
- package/lib/setup-action-log.js +139 -139
- package/lib/setup-summary-renderer.js +106 -106
- package/lib/setup-utils.js +96 -0
- package/lib/setup.js +192 -192
- package/lib/smart-merge.js +64 -0
- package/lib/symlink-utils.js +81 -0
- package/lib/task-ownership.js +117 -0
- package/lib/workflow-profiles.js +197 -197
- package/package.json +131 -128
- package/scripts/beads-context.sh +426 -0
- package/scripts/beads-context.test.js +567 -0
- package/scripts/behavioral-judge.sh +378 -0
- package/scripts/benchmark.js +85 -0
- package/scripts/branch-protection.js +183 -0
- package/scripts/check-agents.js +172 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/commitlint.js +42 -0
- package/scripts/conflict-detect.sh +323 -0
- package/scripts/dep-guard-analyze.js +71 -0
- package/scripts/dep-guard.sh +789 -0
- package/scripts/eval_win.py +249 -0
- package/scripts/file-index.sh +493 -0
- 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/github-beads-sync/comment.mjs +64 -0
- package/scripts/github-beads-sync/config.mjs +148 -0
- package/scripts/github-beads-sync/github-api.mjs +131 -0
- package/scripts/github-beads-sync/index.mjs +332 -0
- package/scripts/github-beads-sync/label-mapper.mjs +54 -0
- package/scripts/github-beads-sync/mapping.mjs +78 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
- package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
- package/scripts/github-beads-sync/run-bd.mjs +159 -0
- package/scripts/github-beads-sync/sanitize.mjs +121 -0
- package/scripts/github-beads-sync.config.json +26 -0
- package/scripts/improve-command.js +375 -0
- package/scripts/lib/eval-runner.js +268 -0
- package/scripts/lib/eval-schema.js +135 -0
- package/scripts/lib/eval-storage.js +78 -0
- package/scripts/lib/grading.js +203 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/lib/transcript-parser.js +63 -0
- package/scripts/lint.js +47 -0
- package/scripts/migrate-to-bun-test.js +412 -0
- package/scripts/pr-coordinator.sh +706 -0
- package/scripts/run-command-eval.js +236 -0
- package/scripts/smart-status.sh +809 -0
- package/scripts/sync-commands.js +571 -0
- package/scripts/sync-utils.sh +455 -0
- package/scripts/test-dashboard.js +123 -0
- package/scripts/test.js +46 -0
- package/scripts/validate.sh +94 -0
- package/skills/parallel-deep-research/SKILL.md +108 -108
- package/skills/parallel-deep-research/evals/README.md +27 -27
- package/skills/parallel-deep-research/evals/evals.json +62 -62
- package/skills/sonarcloud-analysis/SKILL.md +171 -171
- package/skills/sonarcloud-analysis/evals/README.md +27 -27
- package/skills/sonarcloud-analysis/evals/evals.json +50 -50
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
- package/.cursor/hooks/state/continual-learning-index.json +0 -19
- 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
|
+
}
|