forge-workflow 0.0.5 → 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 +6 -1
- package/.claude/commands/plan.md +59 -14
- package/.claude/commands/premerge.md +10 -0
- package/.claude/commands/review.md +7 -1
- package/.claude/commands/ship.md +95 -47
- package/.claude/commands/status.md +42 -0
- package/.claude/commands/validate.md +7 -1
- package/.claude/commands/verify.md +52 -4
- package/.claude/rules/workflow.md +16 -0
- package/.claude/scripts/greptile-resolve.sh +32 -0
- package/.cline/workflows/dev.md +6 -1
- package/.cline/workflows/plan.md +59 -14
- package/.cline/workflows/premerge.md +10 -0
- package/.cline/workflows/review.md +7 -1
- package/.cline/workflows/ship.md +95 -47
- package/.cline/workflows/status.md +42 -0
- package/.cline/workflows/validate.md +7 -1
- package/.cline/workflows/verify.md +52 -4
- package/.codex/skills/dev/SKILL.md +6 -1
- package/.codex/skills/plan/SKILL.md +59 -14
- package/.codex/skills/premerge/SKILL.md +10 -0
- package/.codex/skills/review/SKILL.md +7 -1
- package/.codex/skills/ship/SKILL.md +95 -47
- package/.codex/skills/status/SKILL.md +42 -0
- package/.codex/skills/validate/SKILL.md +7 -1
- package/.codex/skills/verify/SKILL.md +52 -4
- package/.cursor/commands/dev.md +6 -1
- package/.cursor/commands/plan.md +59 -14
- package/.cursor/commands/premerge.md +10 -0
- package/.cursor/commands/review.md +7 -1
- package/.cursor/commands/ship.md +95 -47
- package/.cursor/commands/status.md +42 -0
- package/.cursor/commands/validate.md +7 -1
- package/.cursor/commands/verify.md +52 -4
- package/.github/prompts/dev.prompt.md +6 -1
- package/.github/prompts/plan.prompt.md +59 -14
- package/.github/prompts/premerge.prompt.md +10 -0
- package/.github/prompts/review.prompt.md +7 -1
- package/.github/prompts/ship.prompt.md +95 -47
- package/.github/prompts/status.prompt.md +42 -0
- package/.github/prompts/validate.prompt.md +7 -1
- package/.github/prompts/verify.prompt.md +52 -4
- package/.kilocode/workflows/dev.md +6 -1
- package/.kilocode/workflows/plan.md +59 -14
- package/.kilocode/workflows/premerge.md +10 -0
- package/.kilocode/workflows/review.md +7 -1
- package/.kilocode/workflows/ship.md +95 -47
- package/.kilocode/workflows/status.md +42 -0
- package/.kilocode/workflows/validate.md +7 -1
- package/.kilocode/workflows/verify.md +52 -4
- package/.opencode/commands/dev.md +6 -1
- package/.opencode/commands/plan.md +59 -14
- package/.opencode/commands/premerge.md +10 -0
- package/.opencode/commands/review.md +7 -1
- package/.opencode/commands/ship.md +95 -47
- package/.opencode/commands/status.md +42 -0
- package/.opencode/commands/validate.md +7 -1
- package/.opencode/commands/verify.md +52 -4
- package/.roo/commands/dev.md +6 -1
- package/.roo/commands/plan.md +59 -14
- package/.roo/commands/premerge.md +10 -0
- package/.roo/commands/review.md +7 -1
- package/.roo/commands/ship.md +95 -47
- package/.roo/commands/status.md +42 -0
- package/.roo/commands/validate.md +7 -1
- package/.roo/commands/verify.md +52 -4
- package/AGENTS.md +97 -0
- package/CLAUDE.md +10 -0
- package/README.md +2 -2
- package/bin/forge-cmd.js +5 -1
- package/bin/forge-preflight.js +15 -2
- package/bin/forge.js +178 -9
- package/docs/ENHANCED_ONBOARDING.md +96 -86
- package/docs/ROADMAP.md +2 -2
- package/docs/TOOLCHAIN.md +23 -0
- package/docs/VALIDATION.md +1 -1
- package/lefthook.yml +11 -0
- package/lib/agents-config.js +2 -2
- package/lib/commands/_registry.js +134 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/dev.js +58 -0
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +1 -1
- package/lib/commands/sync.js +55 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/worktree.js +310 -0
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/freshness-token.js +148 -0
- package/lib/greptile-match.js +80 -0
- package/lib/reset.js +309 -0
- package/lib/task-ownership.js +117 -0
- package/package.json +2 -2
- package/scripts/beads-context.sh +157 -22
- package/scripts/beads-context.test.js +5 -1
- package/scripts/check-forge-token.js +98 -0
- package/scripts/conflict-detect.sh +2 -2
- package/scripts/dep-guard.sh +6 -28
- package/scripts/file-index.sh +117 -23
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -0
- package/scripts/lib/eval-runner.js +39 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/pr-coordinator.sh +706 -0
- package/scripts/smart-status.sh +37 -10
- package/scripts/sync-utils.sh +24 -29
- package/scripts/test.js +3 -1
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/forge-team/lib/sync-github.sh — Bidirectional sync between Beads and GitHub
|
|
3
|
+
#
|
|
4
|
+
# Functions:
|
|
5
|
+
# sync_issue_create <beads-id> — Create GitHub issue from Beads
|
|
6
|
+
# sync_issue_claim <beads-id> — Assign current dev on GitHub
|
|
7
|
+
# sync_issue_status <beads-id> <status> — Update status label on GitHub
|
|
8
|
+
# sync_issue_close <beads-id> — Close GitHub issue
|
|
9
|
+
# sync_issue_deps <beads-id-a> <beads-id-b> — Add "Blocked by" comment
|
|
10
|
+
# _get_github_issue_number <beads-id> — Extract github_issue from bd show
|
|
11
|
+
#
|
|
12
|
+
# Env overrides (for testing):
|
|
13
|
+
# GH_CMD — Path to gh binary (default: gh)
|
|
14
|
+
# BD_CMD — Path to bd binary (default: bd)
|
|
15
|
+
#
|
|
16
|
+
# All GitHub-sourced strings are sanitized via sanitize_for_agent() before
|
|
17
|
+
# use in shell commands (OWASP A03).
|
|
18
|
+
#
|
|
19
|
+
# This file does NOT set errexit/pipefail — callers manage their own shell options.
|
|
20
|
+
|
|
21
|
+
# Guard against double-sourcing
|
|
22
|
+
if [[ -n "${_SYNC_GITHUB_LIB_LOADED:-}" ]]; then
|
|
23
|
+
return 0 2>/dev/null || true
|
|
24
|
+
fi
|
|
25
|
+
_SYNC_GITHUB_LIB_LOADED=1
|
|
26
|
+
|
|
27
|
+
_SYNC_GITHUB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
|
|
29
|
+
# Source shared libraries (graceful fallback if missing)
|
|
30
|
+
# sanitize.sh — from scripts/lib/
|
|
31
|
+
_SYNC_SANITIZE_PATH="$_SYNC_GITHUB_DIR/../../lib/sanitize.sh"
|
|
32
|
+
if [[ -f "$_SYNC_SANITIZE_PATH" ]]; then
|
|
33
|
+
source "$_SYNC_SANITIZE_PATH"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# agent-prompt.sh — from forge-team/lib/ (provides sanitize_for_agent)
|
|
37
|
+
if [[ -f "$_SYNC_GITHUB_DIR/agent-prompt.sh" ]]; then
|
|
38
|
+
source "$_SYNC_GITHUB_DIR/agent-prompt.sh"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# identity.sh — from forge-team/lib/ (provides get_github_user)
|
|
42
|
+
if [[ -f "$_SYNC_GITHUB_DIR/identity.sh" ]]; then
|
|
43
|
+
source "$_SYNC_GITHUB_DIR/identity.sh"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# ── Internal helpers ─────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
# _sync_error <message>
|
|
49
|
+
# Output error via agent_error if available, otherwise stderr
|
|
50
|
+
_sync_error() {
|
|
51
|
+
if declare -f agent_error &>/dev/null; then
|
|
52
|
+
agent_error "$1"
|
|
53
|
+
else
|
|
54
|
+
echo "ERROR: $1" >&2
|
|
55
|
+
fi
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# _safe_sanitize <string>
|
|
59
|
+
# Sanitize using both sanitize_for_agent (strip agent prefix) and sanitize
|
|
60
|
+
# (strip shell injection patterns). Either or both may be available.
|
|
61
|
+
_safe_sanitize() {
|
|
62
|
+
local input="${1:-}"
|
|
63
|
+
# Step 1: strip agent prefix injection if sanitize_for_agent is available
|
|
64
|
+
if declare -f sanitize_for_agent &>/dev/null; then
|
|
65
|
+
input="$(sanitize_for_agent "$input")"
|
|
66
|
+
fi
|
|
67
|
+
# Step 2: strip shell injection patterns (OWASP A03) if sanitize is available
|
|
68
|
+
if declare -f sanitize &>/dev/null; then
|
|
69
|
+
input="$(sanitize "$input")"
|
|
70
|
+
fi
|
|
71
|
+
printf '%s' "$input"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# ── _get_github_issue_number ─────────────────────────────────────────────
|
|
75
|
+
# Extracts github_issue number from `bd show <id>` output.
|
|
76
|
+
# Looks for `github_issue:N` pattern.
|
|
77
|
+
# Returns the number or empty string (exit 1 if not found).
|
|
78
|
+
_get_github_issue_number() {
|
|
79
|
+
local beads_id="${1:-}"
|
|
80
|
+
if [[ -z "$beads_id" ]]; then
|
|
81
|
+
_sync_error "beads-id required"
|
|
82
|
+
return 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
local bd_cmd="${BD_CMD:-bd}"
|
|
86
|
+
local show_output
|
|
87
|
+
show_output="$("$bd_cmd" show "$beads_id" 2>/dev/null)" || {
|
|
88
|
+
_sync_error "Failed to get beads info for $beads_id"
|
|
89
|
+
return 1
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
local issue_num
|
|
93
|
+
issue_num="$(printf '%s' "$show_output" | grep -oP 'github_issue:\K[0-9]+' | head -1)"
|
|
94
|
+
|
|
95
|
+
if [[ -z "$issue_num" ]]; then
|
|
96
|
+
_sync_error "No github_issue found for $beads_id"
|
|
97
|
+
return 1
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
printf '%s' "$issue_num"
|
|
101
|
+
return 0
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# ── _get_issue_title ─────────────────────────────────────────────────────
|
|
105
|
+
# Extracts issue title from `bd show <id>` output.
|
|
106
|
+
# Looks for "Title: <text>" line or falls back to the summary line.
|
|
107
|
+
_get_issue_title() {
|
|
108
|
+
local beads_id="${1:-}"
|
|
109
|
+
local bd_cmd="${BD_CMD:-bd}"
|
|
110
|
+
local show_output
|
|
111
|
+
show_output="$("$bd_cmd" show "$beads_id" 2>/dev/null)" || return 1
|
|
112
|
+
|
|
113
|
+
local title
|
|
114
|
+
# Try "Title: ..." line first
|
|
115
|
+
title="$(printf '%s' "$show_output" | grep -oP '^Title:\s*\K.*' | head -1)"
|
|
116
|
+
|
|
117
|
+
if [[ -z "$title" ]]; then
|
|
118
|
+
# Fallback: extract from summary line "○ <id> · <title> [...]"
|
|
119
|
+
title="$(printf '%s' "$show_output" | grep -oP '·\s*\K[^\[]+' | head -1 | sed 's/[[:space:]]*$//')"
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
if [[ -z "$title" ]]; then
|
|
123
|
+
title="$beads_id"
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
printf '%s' "$title"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# ── Public API ───────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
# sync_issue_create <beads-id>
|
|
132
|
+
# Creates GitHub issue from Beads issue data.
|
|
133
|
+
# Stores GitHub issue number back via `bd set-state`.
|
|
134
|
+
# Returns 0 on success, 1 on failure.
|
|
135
|
+
sync_issue_create() {
|
|
136
|
+
local beads_id="${1:-}"
|
|
137
|
+
if [[ -z "$beads_id" ]]; then
|
|
138
|
+
_sync_error "beads-id required"
|
|
139
|
+
return 1
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
local gh_cmd="${GH_CMD:-gh}"
|
|
143
|
+
local bd_cmd="${BD_CMD:-bd}"
|
|
144
|
+
|
|
145
|
+
# Get title from bd show
|
|
146
|
+
local raw_title
|
|
147
|
+
raw_title="$(_get_issue_title "$beads_id")" || {
|
|
148
|
+
_sync_error "Failed to get title for $beads_id"
|
|
149
|
+
return 1
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Sanitize title (OWASP A03)
|
|
153
|
+
local title
|
|
154
|
+
title="$(_safe_sanitize "$raw_title")"
|
|
155
|
+
|
|
156
|
+
# Create GitHub issue
|
|
157
|
+
local gh_output
|
|
158
|
+
gh_output="$("$gh_cmd" issue create --title "$title" --body "Beads: $beads_id" 2>/dev/null)" || {
|
|
159
|
+
_sync_error "Failed to create GitHub issue for $beads_id"
|
|
160
|
+
return 1
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Extract issue number from URL (e.g., https://github.com/.../issues/42)
|
|
164
|
+
local issue_num
|
|
165
|
+
issue_num="$(printf '%s' "$gh_output" | grep -oP '/issues/\K[0-9]+' | head -1)"
|
|
166
|
+
|
|
167
|
+
if [[ -z "$issue_num" ]]; then
|
|
168
|
+
_sync_error "Could not extract issue number from: $gh_output"
|
|
169
|
+
return 1
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# Store issue number in Beads
|
|
173
|
+
"$bd_cmd" set-state "$beads_id" "github_issue=$issue_num" >/dev/null 2>&1 || {
|
|
174
|
+
_sync_error "Failed to store github_issue=$issue_num for $beads_id"
|
|
175
|
+
return 1
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# Update beads-mapping.json for verify Check 4
|
|
179
|
+
local mapping_file=".github/beads-mapping.json"
|
|
180
|
+
if [[ -f "$mapping_file" ]]; then
|
|
181
|
+
local updated
|
|
182
|
+
updated="$(jq --arg num "$issue_num" --arg id "$beads_id" '.[$num] = $id' "$mapping_file")"
|
|
183
|
+
printf '%s\n' "$updated" > "$mapping_file"
|
|
184
|
+
else
|
|
185
|
+
mkdir -p .github
|
|
186
|
+
printf '%s\n' "{\"$issue_num\":\"$beads_id\"}" > "$mapping_file"
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
return 0
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# sync_issue_claim <beads-id>
|
|
193
|
+
# Assigns the current developer to the GitHub issue.
|
|
194
|
+
# Returns 0 on success.
|
|
195
|
+
sync_issue_claim() {
|
|
196
|
+
local beads_id="${1:-}"
|
|
197
|
+
if [[ -z "$beads_id" ]]; then
|
|
198
|
+
_sync_error "beads-id required"
|
|
199
|
+
return 1
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
local gh_cmd="${GH_CMD:-gh}"
|
|
203
|
+
|
|
204
|
+
# Get GitHub issue number
|
|
205
|
+
local issue_num
|
|
206
|
+
issue_num="$(_get_github_issue_number "$beads_id")" || return 1
|
|
207
|
+
|
|
208
|
+
# Get current developer's GitHub username
|
|
209
|
+
local user
|
|
210
|
+
user="$(get_github_user)" || {
|
|
211
|
+
_sync_error "Failed to get GitHub username"
|
|
212
|
+
return 1
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Update assignee
|
|
216
|
+
"$gh_cmd" issue edit "$issue_num" --add-assignee "$user" >/dev/null 2>&1 || {
|
|
217
|
+
_sync_error "Failed to assign $user to issue #$issue_num"
|
|
218
|
+
return 1
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return 0
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# sync_issue_status <beads-id> <status>
|
|
225
|
+
# Updates status label on the GitHub issue.
|
|
226
|
+
# Maps: open → status/open, in_progress → status/in-progress, blocked → status/blocked
|
|
227
|
+
# Returns 0 on success.
|
|
228
|
+
sync_issue_status() {
|
|
229
|
+
local beads_id="${1:-}"
|
|
230
|
+
local status="${2:-}"
|
|
231
|
+
if [[ -z "$beads_id" ]] || [[ -z "$status" ]]; then
|
|
232
|
+
_sync_error "beads-id and status required"
|
|
233
|
+
return 1
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
local gh_cmd="${GH_CMD:-gh}"
|
|
237
|
+
|
|
238
|
+
# Get GitHub issue number
|
|
239
|
+
local issue_num
|
|
240
|
+
issue_num="$(_get_github_issue_number "$beads_id")" || return 1
|
|
241
|
+
|
|
242
|
+
# Map status to label
|
|
243
|
+
local label
|
|
244
|
+
case "$status" in
|
|
245
|
+
open) label="status/open" ;;
|
|
246
|
+
in_progress) label="status/in-progress" ;;
|
|
247
|
+
blocked) label="status/blocked" ;;
|
|
248
|
+
*)
|
|
249
|
+
_sync_error "Unknown status: $status (expected: open, in_progress, blocked)"
|
|
250
|
+
return 1
|
|
251
|
+
;;
|
|
252
|
+
esac
|
|
253
|
+
|
|
254
|
+
# Remove old status labels, add new one
|
|
255
|
+
"$gh_cmd" issue edit "$issue_num" \
|
|
256
|
+
--remove-label "status/open" \
|
|
257
|
+
--remove-label "status/in-progress" \
|
|
258
|
+
--remove-label "status/blocked" \
|
|
259
|
+
--add-label "$label" >/dev/null 2>&1 || {
|
|
260
|
+
_sync_error "Failed to update status label on issue #$issue_num"
|
|
261
|
+
return 1
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return 0
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# sync_issue_close <beads-id>
|
|
268
|
+
# Closes the GitHub issue.
|
|
269
|
+
# Returns 0 on success.
|
|
270
|
+
sync_issue_close() {
|
|
271
|
+
local beads_id="${1:-}"
|
|
272
|
+
if [[ -z "$beads_id" ]]; then
|
|
273
|
+
_sync_error "beads-id required"
|
|
274
|
+
return 1
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
local gh_cmd="${GH_CMD:-gh}"
|
|
278
|
+
|
|
279
|
+
# Get GitHub issue number
|
|
280
|
+
local issue_num
|
|
281
|
+
issue_num="$(_get_github_issue_number "$beads_id")" || return 1
|
|
282
|
+
|
|
283
|
+
# Close the issue
|
|
284
|
+
"$gh_cmd" issue close "$issue_num" >/dev/null 2>&1 || {
|
|
285
|
+
_sync_error "Failed to close issue #$issue_num"
|
|
286
|
+
return 1
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return 0
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# sync_issue_deps <beads-id-a> <beads-id-b>
|
|
293
|
+
# Adds "Blocked by #N-B" comment on issue A.
|
|
294
|
+
# Returns 0 on success.
|
|
295
|
+
sync_issue_deps() {
|
|
296
|
+
local beads_id_a="${1:-}"
|
|
297
|
+
local beads_id_b="${2:-}"
|
|
298
|
+
if [[ -z "$beads_id_a" ]] || [[ -z "$beads_id_b" ]]; then
|
|
299
|
+
_sync_error "Two beads-ids required (A blocked by B)"
|
|
300
|
+
return 1
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
local gh_cmd="${GH_CMD:-gh}"
|
|
304
|
+
|
|
305
|
+
# Get both GitHub issue numbers
|
|
306
|
+
local issue_num_a issue_num_b
|
|
307
|
+
issue_num_a="$(_get_github_issue_number "$beads_id_a")" || return 1
|
|
308
|
+
issue_num_b="$(_get_github_issue_number "$beads_id_b")" || return 1
|
|
309
|
+
|
|
310
|
+
# Add comment on A referencing B
|
|
311
|
+
"$gh_cmd" issue comment "$issue_num_a" --body "Blocked by #$issue_num_b" >/dev/null 2>&1 || {
|
|
312
|
+
_sync_error "Failed to add dependency comment on issue #$issue_num_a"
|
|
313
|
+
return 1
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return 0
|
|
317
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/forge-team/lib/verify.sh — 1:1 enforcement and orphan detection
|
|
3
|
+
#
|
|
4
|
+
# Functions:
|
|
5
|
+
# cmd_verify — Check 5 health conditions and report results
|
|
6
|
+
#
|
|
7
|
+
# Checks:
|
|
8
|
+
# 1. gh auth status (GitHub CLI authenticated)
|
|
9
|
+
# 2. Identity mapped in team-map.jsonl
|
|
10
|
+
# 3. Orphan Beads issues (Beads without GitHub counterpart)
|
|
11
|
+
# 4. Orphan GitHub issues (GitHub without Beads counterpart)
|
|
12
|
+
# 5. Assignee consistency between Beads and GitHub
|
|
13
|
+
#
|
|
14
|
+
# Env overrides (for testing):
|
|
15
|
+
# GH_CMD — Path to gh binary (default: gh)
|
|
16
|
+
# BD_CMD — Path to bd binary (default: bd)
|
|
17
|
+
# TEAM_MAP_ROOT — Root dir for .beads/ and .github/ (default: .)
|
|
18
|
+
# VERIFY_MAPPING_FILE — Path to beads-mapping.json (default: $TEAM_MAP_ROOT/.github/beads-mapping.json)
|
|
19
|
+
#
|
|
20
|
+
# This file does NOT set errexit/pipefail — callers manage their own shell options.
|
|
21
|
+
|
|
22
|
+
# Guard against double-sourcing
|
|
23
|
+
if [[ -n "${_VERIFY_LIB_LOADED:-}" ]]; then
|
|
24
|
+
return 0 2>/dev/null || true
|
|
25
|
+
fi
|
|
26
|
+
_VERIFY_LIB_LOADED=1
|
|
27
|
+
|
|
28
|
+
_VERIFY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
29
|
+
|
|
30
|
+
# Source shared libraries (graceful fallback if missing)
|
|
31
|
+
if [[ -f "$_VERIFY_DIR/agent-prompt.sh" ]]; then
|
|
32
|
+
source "$_VERIFY_DIR/agent-prompt.sh"
|
|
33
|
+
fi
|
|
34
|
+
if [[ -f "$_VERIFY_DIR/identity.sh" ]]; then
|
|
35
|
+
source "$_VERIFY_DIR/identity.sh"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# ── Internal helpers ─────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
_verify_error() {
|
|
41
|
+
if declare -f agent_error &>/dev/null; then
|
|
42
|
+
agent_error "$1"
|
|
43
|
+
else
|
|
44
|
+
echo "ERROR: $1" >&2
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_verify_prompt() {
|
|
49
|
+
if declare -f agent_prompt &>/dev/null; then
|
|
50
|
+
agent_prompt "$1"
|
|
51
|
+
else
|
|
52
|
+
echo "PROMPT: $1" >&2
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_verify_info() {
|
|
57
|
+
if declare -f agent_info &>/dev/null; then
|
|
58
|
+
agent_info "$1"
|
|
59
|
+
else
|
|
60
|
+
echo "INFO: $1" >&2
|
|
61
|
+
fi
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_mapping_file() {
|
|
65
|
+
local root="${TEAM_MAP_ROOT:-.}"
|
|
66
|
+
printf '%s' "${VERIFY_MAPPING_FILE:-$root/.github/beads-mapping.json}"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# ── _extract_beads_ids ───────────────────────────────────────────────────
|
|
70
|
+
# Parse bd list output to extract beads IDs.
|
|
71
|
+
# Input format: "◐ forge-aaa · Feature A" or "○ forge-bbb · Feature B"
|
|
72
|
+
# Output: one ID per line
|
|
73
|
+
_extract_beads_ids() {
|
|
74
|
+
local input="${1:-}"
|
|
75
|
+
if [[ -z "$input" ]]; then
|
|
76
|
+
return 0
|
|
77
|
+
fi
|
|
78
|
+
printf '%s\n' "$input" | grep -oP '(forge-[a-z0-9]+)' || true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# ── Public API ───────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
# cmd_verify — Run all 5 health checks and report results.
|
|
84
|
+
# Exit 0 if all clean, exit 1 if any issues found.
|
|
85
|
+
cmd_verify() {
|
|
86
|
+
local gh_cmd="${GH_CMD:-gh}"
|
|
87
|
+
local bd_cmd="${BD_CMD:-bd}"
|
|
88
|
+
local issues_found=0
|
|
89
|
+
|
|
90
|
+
echo "forge team verify"
|
|
91
|
+
echo "═════════════════"
|
|
92
|
+
echo ""
|
|
93
|
+
|
|
94
|
+
# ── Check 1: gh auth status ──────────────────────────────────────────
|
|
95
|
+
local gh_auth_output
|
|
96
|
+
if gh_auth_output="$("$gh_cmd" auth status 2>&1)"; then
|
|
97
|
+
# Extract username from auth status output
|
|
98
|
+
local gh_user
|
|
99
|
+
gh_user="$(printf '%s' "$gh_auth_output" | grep -oP 'account\s+\K\S+' | head -1)"
|
|
100
|
+
if [[ -z "$gh_user" ]]; then
|
|
101
|
+
gh_user="$(printf '%s' "$gh_auth_output" | grep -oP 'Logged in to \S+ account \K\S+' | head -1)"
|
|
102
|
+
fi
|
|
103
|
+
echo "✓ GitHub CLI: authenticated${gh_user:+ as $gh_user}"
|
|
104
|
+
else
|
|
105
|
+
echo "✗ GitHub CLI: not authenticated"
|
|
106
|
+
_verify_error "GitHub CLI not authenticated. Run: gh auth login"
|
|
107
|
+
issues_found=$((issues_found + 1))
|
|
108
|
+
# Cannot proceed with other checks without auth
|
|
109
|
+
echo ""
|
|
110
|
+
echo "Result: $issues_found issue found — action needed"
|
|
111
|
+
return 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# ── Check 2: Identity mapped ────────────────────────────────────────
|
|
115
|
+
unset _GITHUB_USER_CACHE
|
|
116
|
+
local identity_user
|
|
117
|
+
if identity_user="$(get_github_user 2>/dev/null)"; then
|
|
118
|
+
local existing
|
|
119
|
+
existing="$(team_map_get "$identity_user" 2>/dev/null)"
|
|
120
|
+
if [[ "$existing" != "null" ]] && [[ -n "$existing" ]]; then
|
|
121
|
+
echo "✓ Identity: mapped in team-map.jsonl"
|
|
122
|
+
else
|
|
123
|
+
echo "✗ Identity: not found in team-map.jsonl"
|
|
124
|
+
_verify_prompt "Run: forge team add"
|
|
125
|
+
issues_found=$((issues_found + 1))
|
|
126
|
+
fi
|
|
127
|
+
else
|
|
128
|
+
echo "✗ Identity: could not detect"
|
|
129
|
+
_verify_prompt "Run: forge team add"
|
|
130
|
+
issues_found=$((issues_found + 1))
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# ── Check 3: Orphan Beads issues (Beads without GitHub) ─────────────
|
|
134
|
+
local bd_output
|
|
135
|
+
bd_output="$("$bd_cmd" list --status=open,in_progress 2>/dev/null)" || bd_output=""
|
|
136
|
+
|
|
137
|
+
local beads_ids
|
|
138
|
+
beads_ids="$(_extract_beads_ids "$bd_output")"
|
|
139
|
+
|
|
140
|
+
local beads_total=0
|
|
141
|
+
local beads_synced=0
|
|
142
|
+
local beads_orphans=()
|
|
143
|
+
|
|
144
|
+
if [[ -n "$beads_ids" ]]; then
|
|
145
|
+
while IFS= read -r bid; do
|
|
146
|
+
[[ -z "$bid" ]] && continue
|
|
147
|
+
beads_total=$((beads_total + 1))
|
|
148
|
+
|
|
149
|
+
# Check if this beads issue has github_issue state
|
|
150
|
+
local show_output
|
|
151
|
+
show_output="$("$bd_cmd" show "$bid" 2>/dev/null)" || show_output=""
|
|
152
|
+
if printf '%s' "$show_output" | grep -qP 'github_issue:\d+'; then
|
|
153
|
+
beads_synced=$((beads_synced + 1))
|
|
154
|
+
else
|
|
155
|
+
beads_orphans+=("$bid")
|
|
156
|
+
fi
|
|
157
|
+
done <<< "$beads_ids"
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
if [[ ${#beads_orphans[@]} -eq 0 ]]; then
|
|
161
|
+
echo "✓ Beads→GitHub: ${beads_synced}/${beads_total} issues synced"
|
|
162
|
+
else
|
|
163
|
+
echo "✗ Beads→GitHub: ${#beads_orphans[@]} orphan issues found"
|
|
164
|
+
for orphan in "${beads_orphans[@]}"; do
|
|
165
|
+
_verify_prompt "${orphan} has no GitHub issue. Run: forge team sync-issue ${orphan}"
|
|
166
|
+
done
|
|
167
|
+
issues_found=$((issues_found + ${#beads_orphans[@]}))
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
# ── Check 4: Orphan GitHub issues (GitHub without Beads) ────────────
|
|
171
|
+
local mapping_file
|
|
172
|
+
mapping_file="$(_mapping_file)"
|
|
173
|
+
|
|
174
|
+
local gh_issues_json
|
|
175
|
+
gh_issues_json="$("$gh_cmd" issue list --state open --json number,title --limit 100 2>/dev/null)" || gh_issues_json="[]"
|
|
176
|
+
|
|
177
|
+
local gh_issue_count
|
|
178
|
+
gh_issue_count="$(printf '%s' "$gh_issues_json" | jq 'length')"
|
|
179
|
+
|
|
180
|
+
local gh_orphans=()
|
|
181
|
+
|
|
182
|
+
if [[ "$gh_issue_count" -gt 0 ]] && [[ -f "$mapping_file" ]]; then
|
|
183
|
+
local mapping_content
|
|
184
|
+
mapping_content="$(cat "$mapping_file")"
|
|
185
|
+
|
|
186
|
+
local i=0
|
|
187
|
+
while [[ $i -lt $gh_issue_count ]]; do
|
|
188
|
+
local issue_num
|
|
189
|
+
issue_num="$(printf '%s' "$gh_issues_json" | jq -r ".[$i].number")"
|
|
190
|
+
|
|
191
|
+
# Check if this GitHub issue number is a key in the mapping
|
|
192
|
+
local mapped
|
|
193
|
+
mapped="$(printf '%s' "$mapping_content" | jq -r --arg n "$issue_num" '.[$n] // empty')"
|
|
194
|
+
|
|
195
|
+
if [[ -z "$mapped" ]]; then
|
|
196
|
+
gh_orphans+=("$issue_num")
|
|
197
|
+
fi
|
|
198
|
+
i=$((i + 1))
|
|
199
|
+
done
|
|
200
|
+
elif [[ "$gh_issue_count" -gt 0 ]] && [[ ! -f "$mapping_file" ]]; then
|
|
201
|
+
# No mapping file — all GitHub issues are orphans
|
|
202
|
+
local i=0
|
|
203
|
+
while [[ $i -lt $gh_issue_count ]]; do
|
|
204
|
+
local issue_num
|
|
205
|
+
issue_num="$(printf '%s' "$gh_issues_json" | jq -r ".[$i].number")"
|
|
206
|
+
gh_orphans+=("$issue_num")
|
|
207
|
+
i=$((i + 1))
|
|
208
|
+
done
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
if [[ ${#gh_orphans[@]} -eq 0 ]]; then
|
|
212
|
+
local mapped_count=$((gh_issue_count))
|
|
213
|
+
echo "✓ GitHub→Beads: ${mapped_count}/${mapped_count} issues mapped"
|
|
214
|
+
else
|
|
215
|
+
echo "✗ GitHub→Beads: ${#gh_orphans[@]} orphan issues found"
|
|
216
|
+
for orphan_num in "${gh_orphans[@]}"; do
|
|
217
|
+
_verify_prompt "GitHub #${orphan_num} has no Beads issue. Run: forge team import #${orphan_num}"
|
|
218
|
+
done
|
|
219
|
+
issues_found=$((issues_found + ${#gh_orphans[@]}))
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
# ── Check 5: Assignee consistency ───────────────────────────────────
|
|
223
|
+
local assignee_mismatches=()
|
|
224
|
+
|
|
225
|
+
if [[ -f "$mapping_file" ]] && [[ "$gh_issue_count" -gt 0 ]]; then
|
|
226
|
+
local mapping_content
|
|
227
|
+
mapping_content="$(cat "$mapping_file")"
|
|
228
|
+
|
|
229
|
+
local i=0
|
|
230
|
+
while [[ $i -lt $gh_issue_count ]]; do
|
|
231
|
+
local issue_num
|
|
232
|
+
issue_num="$(printf '%s' "$gh_issues_json" | jq -r ".[$i].number")"
|
|
233
|
+
|
|
234
|
+
# Check if issue is in mapping
|
|
235
|
+
local mapped_entry
|
|
236
|
+
mapped_entry="$(printf '%s' "$mapping_content" | jq -r --arg n "$issue_num" '.[$n] // empty')"
|
|
237
|
+
|
|
238
|
+
if [[ -n "$mapped_entry" ]]; then
|
|
239
|
+
local beads_assignee beads_id
|
|
240
|
+
beads_id="$(printf '%s' "$mapping_content" | jq -r --arg n "$issue_num" '.[$n].beads_id // empty')"
|
|
241
|
+
beads_assignee="$(printf '%s' "$mapping_content" | jq -r --arg n "$issue_num" '.[$n].assignee // empty')"
|
|
242
|
+
|
|
243
|
+
if [[ -n "$beads_assignee" ]]; then
|
|
244
|
+
# Get GitHub assignee
|
|
245
|
+
local gh_assignee_json
|
|
246
|
+
gh_assignee_json="$("$gh_cmd" issue view "$issue_num" --json assignees 2>/dev/null)" || gh_assignee_json="{}"
|
|
247
|
+
local gh_assignee
|
|
248
|
+
gh_assignee="$(printf '%s' "$gh_assignee_json" | jq -r '.assignees[0].login // empty')"
|
|
249
|
+
|
|
250
|
+
if [[ -n "$gh_assignee" ]] && [[ "$beads_assignee" != "$gh_assignee" ]]; then
|
|
251
|
+
assignee_mismatches+=("${beads_id:-issue-$issue_num}|$beads_assignee|$gh_assignee")
|
|
252
|
+
fi
|
|
253
|
+
fi
|
|
254
|
+
fi
|
|
255
|
+
i=$((i + 1))
|
|
256
|
+
done
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
if [[ ${#assignee_mismatches[@]} -eq 0 ]]; then
|
|
260
|
+
echo "✓ Assignees: all consistent"
|
|
261
|
+
else
|
|
262
|
+
echo "✗ Assignees: ${#assignee_mismatches[@]} mismatches found"
|
|
263
|
+
for mismatch in "${assignee_mismatches[@]}"; do
|
|
264
|
+
local m_id m_beads m_github
|
|
265
|
+
m_id="$(printf '%s' "$mismatch" | cut -d'|' -f1)"
|
|
266
|
+
m_beads="$(printf '%s' "$mismatch" | cut -d'|' -f2)"
|
|
267
|
+
m_github="$(printf '%s' "$mismatch" | cut -d'|' -f3)"
|
|
268
|
+
_verify_info "${m_id} assignee mismatch: Beads=${m_beads}, GitHub=${m_github}"
|
|
269
|
+
done
|
|
270
|
+
issues_found=$((issues_found + ${#assignee_mismatches[@]}))
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
# ── Summary ─────────────────────────────────────────────────────────
|
|
274
|
+
echo ""
|
|
275
|
+
if [[ $issues_found -eq 0 ]]; then
|
|
276
|
+
echo "Result: all checks passed"
|
|
277
|
+
return 0
|
|
278
|
+
else
|
|
279
|
+
local noun="issue"
|
|
280
|
+
[[ $issues_found -gt 1 ]] && noun="issues"
|
|
281
|
+
echo "Result: $issues_found $noun found — action needed"
|
|
282
|
+
return 1
|
|
283
|
+
fi
|
|
284
|
+
}
|