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,235 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/forge-team/lib/identity.sh — GitHub-username-only identity mapping
|
|
3
|
+
#
|
|
4
|
+
# Auto-detects developer via `gh api user`. Stores identity in
|
|
5
|
+
# .beads/team-map.jsonl (append-only, LWW per GitHub username).
|
|
6
|
+
#
|
|
7
|
+
# Functions:
|
|
8
|
+
# get_github_user — Current dev's GitHub username (cached)
|
|
9
|
+
# team_map_add — Add/update entry in team-map.jsonl
|
|
10
|
+
# team_map_read — Read all entries with LWW resolution
|
|
11
|
+
# team_map_get — Get single entry by GitHub username
|
|
12
|
+
# is_bot — Check if username is a bot
|
|
13
|
+
# auto_detect_identity — Auto-detect and register identity
|
|
14
|
+
#
|
|
15
|
+
# Env overrides:
|
|
16
|
+
# GH_CMD — Path to gh binary (for testing)
|
|
17
|
+
# TEAM_MAP_ROOT — Root dir for .beads/team-map.jsonl (default: .)
|
|
18
|
+
#
|
|
19
|
+
# This file does NOT set errexit/pipefail — callers manage their own shell options.
|
|
20
|
+
|
|
21
|
+
# Guard against double-sourcing
|
|
22
|
+
if [[ -n "${_IDENTITY_LIB_LOADED:-}" ]]; then
|
|
23
|
+
return 0 2>/dev/null || true
|
|
24
|
+
fi
|
|
25
|
+
_IDENTITY_LIB_LOADED=1
|
|
26
|
+
|
|
27
|
+
_IDENTITY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
|
|
29
|
+
# Source shared libraries (graceful fallback if missing)
|
|
30
|
+
# sanitize.sh — from scripts/lib/
|
|
31
|
+
_SANITIZE_PATH="$_IDENTITY_DIR/../../lib/sanitize.sh"
|
|
32
|
+
if [[ -f "$_SANITIZE_PATH" ]]; then
|
|
33
|
+
source "$_SANITIZE_PATH"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# jsonl-lock.sh — from scripts/lib/
|
|
37
|
+
_JSONL_LOCK_PATH="$_IDENTITY_DIR/../../lib/jsonl-lock.sh"
|
|
38
|
+
if [[ -f "$_JSONL_LOCK_PATH" ]]; then
|
|
39
|
+
source "$_JSONL_LOCK_PATH"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# agent-prompt.sh — from forge-team/lib/ (Task 1, may not exist yet)
|
|
43
|
+
if [[ -f "$_IDENTITY_DIR/agent-prompt.sh" ]]; then
|
|
44
|
+
source "$_IDENTITY_DIR/agent-prompt.sh"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# ── Internal helpers ─────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
# _identity_error <message>
|
|
50
|
+
# Output error via agent_error if available, otherwise stderr
|
|
51
|
+
_identity_error() {
|
|
52
|
+
if declare -f agent_error &>/dev/null; then
|
|
53
|
+
agent_error "$1"
|
|
54
|
+
else
|
|
55
|
+
echo "ERROR: $1" >&2
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# _identity_prompt <message>
|
|
60
|
+
# Output prompt via agent_prompt if available, otherwise stderr
|
|
61
|
+
_identity_prompt() {
|
|
62
|
+
if declare -f agent_prompt &>/dev/null; then
|
|
63
|
+
agent_prompt "$1"
|
|
64
|
+
else
|
|
65
|
+
echo "PROMPT: $1" >&2
|
|
66
|
+
fi
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# _team_map_file — Returns the path to team-map.jsonl
|
|
70
|
+
_team_map_file() {
|
|
71
|
+
local root="${TEAM_MAP_ROOT:-.}"
|
|
72
|
+
printf '%s' "$root/.beads/team-map.jsonl"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# _validate_github_username <username>
|
|
76
|
+
# GitHub usernames: alphanumeric + hyphens, cannot start with hyphen
|
|
77
|
+
_validate_github_username() {
|
|
78
|
+
local name="${1:-}"
|
|
79
|
+
if [[ -z "$name" ]]; then
|
|
80
|
+
_identity_error "GitHub username cannot be empty"
|
|
81
|
+
return 1
|
|
82
|
+
fi
|
|
83
|
+
# Allow [bot] suffix for bot accounts
|
|
84
|
+
local check_name="$name"
|
|
85
|
+
check_name="${check_name%\[bot\]}"
|
|
86
|
+
if [[ -z "$check_name" ]]; then
|
|
87
|
+
_identity_error "Invalid GitHub username: $name"
|
|
88
|
+
return 1
|
|
89
|
+
fi
|
|
90
|
+
if [[ ! "$check_name" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]*$ ]]; then
|
|
91
|
+
_identity_error "Invalid GitHub username format: $name"
|
|
92
|
+
return 1
|
|
93
|
+
fi
|
|
94
|
+
return 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# ── Public API ───────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
# get_github_user — Returns current developer's GitHub username.
|
|
100
|
+
# Caches in _GITHUB_USER_CACHE for session. Uses GH_CMD env override for testing.
|
|
101
|
+
# Exit: 0 on success, 1 if gh not authenticated.
|
|
102
|
+
get_github_user() {
|
|
103
|
+
# Return cached value if available
|
|
104
|
+
if [[ -n "${_GITHUB_USER_CACHE:-}" ]]; then
|
|
105
|
+
printf '%s' "$_GITHUB_USER_CACHE"
|
|
106
|
+
return 0
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
local gh_cmd="${GH_CMD:-gh}"
|
|
110
|
+
local user
|
|
111
|
+
if ! user="$("$gh_cmd" api user --jq .login 2>/dev/null)"; then
|
|
112
|
+
_identity_error "GitHub CLI not authenticated. Run 'gh auth login' first."
|
|
113
|
+
return 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
if [[ -z "$user" ]]; then
|
|
117
|
+
_identity_error "GitHub CLI returned empty username."
|
|
118
|
+
return 1
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
_GITHUB_USER_CACHE="$user"
|
|
122
|
+
printf '%s' "$user"
|
|
123
|
+
return 0
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# team_map_add <github-user> [display-name]
|
|
127
|
+
# Adds/updates entry in .beads/team-map.jsonl.
|
|
128
|
+
# Schema: {"github":"<user>","display_name":"<name>","updated_at":"<ts>","is_bot":false}
|
|
129
|
+
team_map_add() {
|
|
130
|
+
local github_user="${1:-}"
|
|
131
|
+
local display_name="${2:-$github_user}"
|
|
132
|
+
|
|
133
|
+
# Validate username
|
|
134
|
+
if ! _validate_github_username "$github_user"; then
|
|
135
|
+
return 1
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
local mapfile
|
|
139
|
+
mapfile="$(_team_map_file)"
|
|
140
|
+
|
|
141
|
+
# Determine bot status
|
|
142
|
+
local bot_flag="false"
|
|
143
|
+
if is_bot "$github_user"; then
|
|
144
|
+
bot_flag="true"
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# Build JSON entry using jq
|
|
148
|
+
local ts
|
|
149
|
+
ts="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
|
150
|
+
local json_line
|
|
151
|
+
json_line="$(jq -n -c \
|
|
152
|
+
--arg gh "$github_user" \
|
|
153
|
+
--arg dn "$display_name" \
|
|
154
|
+
--arg ts "$ts" \
|
|
155
|
+
--argjson bot "$bot_flag" \
|
|
156
|
+
'{github: $gh, display_name: $dn, updated_at: $ts, is_bot: $bot}')"
|
|
157
|
+
|
|
158
|
+
# Append atomically
|
|
159
|
+
if declare -f atomic_jsonl_append &>/dev/null; then
|
|
160
|
+
atomic_jsonl_append "$mapfile" "$json_line"
|
|
161
|
+
else
|
|
162
|
+
mkdir -p "$(dirname "$mapfile")"
|
|
163
|
+
printf '%s\n' "$json_line" >> "$mapfile"
|
|
164
|
+
fi
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# team_map_read — Read all entries with LWW resolution.
|
|
168
|
+
# Groups by github username, sorts by updated_at, takes last per user.
|
|
169
|
+
# Returns JSON array. Returns [] if file missing/empty.
|
|
170
|
+
team_map_read() {
|
|
171
|
+
local mapfile
|
|
172
|
+
mapfile="$(_team_map_file)"
|
|
173
|
+
|
|
174
|
+
if [[ ! -f "$mapfile" ]] || [[ ! -s "$mapfile" ]]; then
|
|
175
|
+
echo "[]"
|
|
176
|
+
return 0
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# LWW resolution: group by github, sort by updated_at, take last per user
|
|
180
|
+
# Filter out tombstones (entries with "tombstone":true)
|
|
181
|
+
jq -s '
|
|
182
|
+
[ .[] | select(.tombstone != true) ]
|
|
183
|
+
| group_by(.github)
|
|
184
|
+
| map(sort_by(.updated_at) | last)
|
|
185
|
+
' "$mapfile"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# team_map_get <github-username>
|
|
189
|
+
# Gets single entry by GitHub username. Returns JSON object or "null".
|
|
190
|
+
team_map_get() {
|
|
191
|
+
local username="${1:-}"
|
|
192
|
+
if [[ -z "$username" ]]; then
|
|
193
|
+
echo "null"
|
|
194
|
+
return 0
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
local all
|
|
198
|
+
all="$(team_map_read)"
|
|
199
|
+
local result
|
|
200
|
+
result="$(echo "$all" | jq -c --arg u "$username" '[.[] | select(.github == $u)] | if length > 0 then .[0] else null end')"
|
|
201
|
+
printf '%s' "${result:-null}"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# is_bot <username>
|
|
205
|
+
# Returns 0 if username ends with [bot], 1 otherwise.
|
|
206
|
+
is_bot() {
|
|
207
|
+
local username="${1:-}"
|
|
208
|
+
if [[ "$username" == *"[bot]" ]]; then
|
|
209
|
+
return 0
|
|
210
|
+
fi
|
|
211
|
+
return 1
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# auto_detect_identity — Runs get_github_user(), checks if already in
|
|
215
|
+
# team-map, adds if missing. Silent on success (exit 0).
|
|
216
|
+
# On failure, outputs agent prompt.
|
|
217
|
+
auto_detect_identity() {
|
|
218
|
+
local user
|
|
219
|
+
if ! user="$(get_github_user)"; then
|
|
220
|
+
_identity_prompt "Could not detect GitHub identity. Run 'gh auth login'."
|
|
221
|
+
return 1
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
# Check if already in team map
|
|
225
|
+
local existing
|
|
226
|
+
existing="$(team_map_get "$user")"
|
|
227
|
+
if [[ "$existing" != "null" ]] && [[ -n "$existing" ]]; then
|
|
228
|
+
# Already registered, silent success
|
|
229
|
+
return 0
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
# Add to team map
|
|
233
|
+
team_map_add "$user" "$user"
|
|
234
|
+
return 0
|
|
235
|
+
}
|
|
@@ -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
|
+
}
|