forge-workflow 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/.claude/commands/dev.md +6 -1
  2. package/.claude/commands/plan.md +59 -14
  3. package/.claude/commands/premerge.md +10 -0
  4. package/.claude/commands/review.md +7 -1
  5. package/.claude/commands/ship.md +95 -47
  6. package/.claude/commands/status.md +42 -0
  7. package/.claude/commands/validate.md +7 -1
  8. package/.claude/commands/verify.md +52 -4
  9. package/.claude/rules/workflow.md +16 -0
  10. package/.claude/scripts/greptile-resolve.sh +32 -0
  11. package/.cline/workflows/dev.md +6 -1
  12. package/.cline/workflows/plan.md +59 -14
  13. package/.cline/workflows/premerge.md +10 -0
  14. package/.cline/workflows/review.md +7 -1
  15. package/.cline/workflows/ship.md +95 -47
  16. package/.cline/workflows/status.md +42 -0
  17. package/.cline/workflows/validate.md +7 -1
  18. package/.cline/workflows/verify.md +52 -4
  19. package/.codex/skills/dev/SKILL.md +6 -1
  20. package/.codex/skills/plan/SKILL.md +59 -14
  21. package/.codex/skills/premerge/SKILL.md +10 -0
  22. package/.codex/skills/review/SKILL.md +7 -1
  23. package/.codex/skills/ship/SKILL.md +95 -47
  24. package/.codex/skills/status/SKILL.md +42 -0
  25. package/.codex/skills/validate/SKILL.md +7 -1
  26. package/.codex/skills/verify/SKILL.md +52 -4
  27. package/.cursor/commands/dev.md +6 -1
  28. package/.cursor/commands/plan.md +59 -14
  29. package/.cursor/commands/premerge.md +10 -0
  30. package/.cursor/commands/review.md +7 -1
  31. package/.cursor/commands/ship.md +95 -47
  32. package/.cursor/commands/status.md +42 -0
  33. package/.cursor/commands/validate.md +7 -1
  34. package/.cursor/commands/verify.md +52 -4
  35. package/.cursorrules +149 -0
  36. package/.github/prompts/dev.prompt.md +6 -1
  37. package/.github/prompts/plan.prompt.md +59 -14
  38. package/.github/prompts/premerge.prompt.md +10 -0
  39. package/.github/prompts/review.prompt.md +7 -1
  40. package/.github/prompts/ship.prompt.md +95 -47
  41. package/.github/prompts/status.prompt.md +42 -0
  42. package/.github/prompts/validate.prompt.md +7 -1
  43. package/.github/prompts/verify.prompt.md +52 -4
  44. package/.kilocode/workflows/dev.md +6 -1
  45. package/.kilocode/workflows/plan.md +59 -14
  46. package/.kilocode/workflows/premerge.md +10 -0
  47. package/.kilocode/workflows/review.md +7 -1
  48. package/.kilocode/workflows/ship.md +95 -47
  49. package/.kilocode/workflows/status.md +42 -0
  50. package/.kilocode/workflows/validate.md +7 -1
  51. package/.kilocode/workflows/verify.md +52 -4
  52. package/.opencode/commands/dev.md +6 -1
  53. package/.opencode/commands/plan.md +59 -14
  54. package/.opencode/commands/premerge.md +10 -0
  55. package/.opencode/commands/review.md +7 -1
  56. package/.opencode/commands/ship.md +95 -47
  57. package/.opencode/commands/status.md +42 -0
  58. package/.opencode/commands/validate.md +7 -1
  59. package/.opencode/commands/verify.md +52 -4
  60. package/.roo/commands/dev.md +6 -1
  61. package/.roo/commands/plan.md +59 -14
  62. package/.roo/commands/premerge.md +10 -0
  63. package/.roo/commands/review.md +7 -1
  64. package/.roo/commands/ship.md +95 -47
  65. package/.roo/commands/status.md +42 -0
  66. package/.roo/commands/validate.md +7 -1
  67. package/.roo/commands/verify.md +52 -4
  68. package/AGENTS.md +97 -0
  69. package/CLAUDE.md +10 -0
  70. package/README.md +2 -2
  71. package/bin/forge-cmd.js +5 -1
  72. package/bin/forge-preflight.js +15 -2
  73. package/bin/forge.js +211 -9
  74. package/docs/ENHANCED_ONBOARDING.md +96 -86
  75. package/docs/ROADMAP.md +2 -2
  76. package/docs/TOOLCHAIN.md +23 -0
  77. package/docs/VALIDATION.md +1 -1
  78. package/lefthook.yml +11 -0
  79. package/lib/agents/README.md +46 -1
  80. package/lib/agents/cline.plugin.json +11 -4
  81. package/lib/agents/codex.plugin.json +2 -2
  82. package/lib/agents/copilot.plugin.json +5 -5
  83. package/lib/agents/cursor.plugin.json +1 -1
  84. package/lib/agents/kilocode.plugin.json +1 -1
  85. package/lib/agents/opencode.plugin.json +7 -4
  86. package/lib/agents/roo.plugin.json +10 -3
  87. package/lib/agents-config.js +129 -81
  88. package/lib/codex-skills.js +50 -0
  89. package/lib/commands/_registry.js +173 -0
  90. package/lib/commands/clean.js +181 -0
  91. package/lib/commands/commands-reset.js +147 -0
  92. package/lib/commands/dev.js +84 -0
  93. package/lib/commands/plan.js +18 -0
  94. package/lib/commands/push.js +196 -0
  95. package/lib/commands/recommend.js +1 -1
  96. package/lib/commands/setup.js +4295 -0
  97. package/lib/commands/ship.js +20 -0
  98. package/lib/commands/status.js +210 -44
  99. package/lib/commands/sync.js +71 -0
  100. package/lib/commands/team.js +37 -0
  101. package/lib/commands/test.js +207 -0
  102. package/lib/commands/validate.js +13 -0
  103. package/lib/commands/worktree.js +310 -0
  104. package/lib/detect-agent.js +38 -8
  105. package/lib/detection-utils.js +405 -0
  106. package/lib/docs-command.js +51 -0
  107. package/lib/docs-copy.js +50 -0
  108. package/lib/file-utils.js +260 -0
  109. package/lib/forge-context.js +42 -0
  110. package/lib/freshness-token.js +148 -0
  111. package/lib/frontmatter.js +79 -0
  112. package/lib/greptile-match.js +80 -0
  113. package/lib/husky-migration.js +113 -12
  114. package/lib/lefthook-check.js +27 -6
  115. package/lib/plugin-manager.js +225 -72
  116. package/lib/project-discovery.js +39 -5
  117. package/lib/reset.js +309 -0
  118. package/lib/runtime-health.js +305 -0
  119. package/lib/shell-utils.js +50 -0
  120. package/lib/task-ownership.js +117 -0
  121. package/lib/ui-utils.js +43 -0
  122. package/lib/validation-utils.js +163 -0
  123. package/lib/workflow/enforce-stage.js +179 -0
  124. package/lib/workflow/stages.js +201 -0
  125. package/lib/workflow/state.js +332 -0
  126. package/opencode.json +67 -0
  127. package/package.json +16 -6
  128. package/scripts/beads-context.sh +165 -22
  129. package/scripts/beads-context.test.js +5 -1
  130. package/scripts/check-agents.js +103 -0
  131. package/scripts/check-forge-token.js +98 -0
  132. package/scripts/conflict-detect.sh +2 -2
  133. package/scripts/dep-guard.sh +6 -28
  134. package/scripts/file-index.sh +117 -23
  135. package/scripts/forge-team/index.sh +86 -0
  136. package/scripts/forge-team/lib/agent-prompt.sh +52 -0
  137. package/scripts/forge-team/lib/claim.sh +256 -0
  138. package/scripts/forge-team/lib/dashboard.sh +341 -0
  139. package/scripts/forge-team/lib/epic.sh +332 -0
  140. package/scripts/forge-team/lib/hooks.sh +253 -0
  141. package/scripts/forge-team/lib/identity.sh +235 -0
  142. package/scripts/forge-team/lib/sync-github.sh +317 -0
  143. package/scripts/forge-team/lib/verify.sh +284 -0
  144. package/scripts/forge-team/lib/workload.sh +296 -0
  145. package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
  146. package/scripts/forge-team/tests/claim.test.sh +179 -0
  147. package/scripts/forge-team/tests/dashboard.test.sh +170 -0
  148. package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
  149. package/scripts/forge-team/tests/epic.test.sh +176 -0
  150. package/scripts/forge-team/tests/hooks.test.sh +239 -0
  151. package/scripts/forge-team/tests/identity.test.sh +176 -0
  152. package/scripts/forge-team/tests/integration.test.sh +371 -0
  153. package/scripts/forge-team/tests/sync-github.test.sh +209 -0
  154. package/scripts/forge-team/tests/verify.test.sh +314 -0
  155. package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
  156. package/scripts/forge-team/tests/workload.test.sh +209 -0
  157. package/scripts/lib/eval-runner.js +39 -0
  158. package/scripts/lib/jsonl-lock.sh +48 -0
  159. package/scripts/lib/sanitize.sh +116 -0
  160. package/scripts/pr-coordinator.sh +756 -0
  161. package/scripts/smart-status.sh +58 -21
  162. package/scripts/sync-commands.js +49 -20
  163. package/scripts/sync-utils.sh +24 -29
  164. package/scripts/test.js +18 -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
+ }