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.
- 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/.cursorrules +149 -0
- 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 +211 -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/README.md +46 -1
- package/lib/agents/cline.plugin.json +11 -4
- package/lib/agents/codex.plugin.json +2 -2
- package/lib/agents/copilot.plugin.json +5 -5
- package/lib/agents/cursor.plugin.json +1 -1
- package/lib/agents/kilocode.plugin.json +1 -1
- package/lib/agents/opencode.plugin.json +7 -4
- package/lib/agents/roo.plugin.json +10 -3
- package/lib/agents-config.js +129 -81
- package/lib/codex-skills.js +50 -0
- package/lib/commands/_registry.js +173 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/commands-reset.js +147 -0
- package/lib/commands/dev.js +84 -0
- package/lib/commands/plan.js +18 -0
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +1 -1
- package/lib/commands/setup.js +4295 -0
- package/lib/commands/ship.js +20 -0
- package/lib/commands/status.js +210 -44
- package/lib/commands/sync.js +71 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +13 -0
- package/lib/commands/worktree.js +310 -0
- package/lib/detect-agent.js +38 -8
- package/lib/detection-utils.js +405 -0
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-utils.js +260 -0
- package/lib/forge-context.js +42 -0
- package/lib/freshness-token.js +148 -0
- package/lib/frontmatter.js +79 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +113 -12
- package/lib/lefthook-check.js +27 -6
- package/lib/plugin-manager.js +225 -72
- package/lib/project-discovery.js +39 -5
- package/lib/reset.js +309 -0
- package/lib/runtime-health.js +305 -0
- package/lib/shell-utils.js +50 -0
- package/lib/task-ownership.js +117 -0
- package/lib/ui-utils.js +43 -0
- package/lib/validation-utils.js +163 -0
- package/lib/workflow/enforce-stage.js +179 -0
- package/lib/workflow/stages.js +201 -0
- package/lib/workflow/state.js +332 -0
- package/opencode.json +67 -0
- package/package.json +16 -6
- package/scripts/beads-context.sh +165 -22
- package/scripts/beads-context.test.js +5 -1
- package/scripts/check-agents.js +103 -0
- 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 +756 -0
- package/scripts/smart-status.sh +58 -21
- package/scripts/sync-commands.js +49 -20
- package/scripts/sync-utils.sh +24 -29
- package/scripts/test.js +18 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/forge-team/lib/hooks.sh — Hook-based sync (pre-push + stage transitions)
|
|
3
|
+
#
|
|
4
|
+
# Functions:
|
|
5
|
+
# forge_team_sync [--quiet] — Top-level sync for hooks/manual
|
|
6
|
+
# forge_team_sync_on_stage <beads-id> <from> <to> — Stage transition sync
|
|
7
|
+
# _check_auto_sync_enabled — Check config for auto-sync
|
|
8
|
+
#
|
|
9
|
+
# Env overrides (for testing):
|
|
10
|
+
# GH_CMD — Path to gh binary (default: gh)
|
|
11
|
+
# BD_CMD — Path to bd binary (default: bd)
|
|
12
|
+
# BEADS_ROOT — Path to .beads/ parent directory (default: .)
|
|
13
|
+
#
|
|
14
|
+
# This file does NOT set errexit/pipefail — callers manage their own shell options.
|
|
15
|
+
|
|
16
|
+
# Guard against double-sourcing
|
|
17
|
+
if [[ -n "${_HOOKS_LIB_LOADED:-}" ]]; then
|
|
18
|
+
return 0 2>/dev/null || true
|
|
19
|
+
fi
|
|
20
|
+
_HOOKS_LIB_LOADED=1
|
|
21
|
+
|
|
22
|
+
_HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
|
|
24
|
+
# Source dependencies
|
|
25
|
+
if [[ -f "$_HOOKS_DIR/agent-prompt.sh" ]]; then
|
|
26
|
+
source "$_HOOKS_DIR/agent-prompt.sh"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [[ -f "$_HOOKS_DIR/sync-github.sh" ]]; then
|
|
30
|
+
source "$_HOOKS_DIR/sync-github.sh"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# ── Internal helpers ─────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
# _hooks_error <message>
|
|
36
|
+
_hooks_error() {
|
|
37
|
+
if declare -f agent_error &>/dev/null; then
|
|
38
|
+
agent_error "$1"
|
|
39
|
+
else
|
|
40
|
+
echo "ERROR: $1" >&2
|
|
41
|
+
fi
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# _hooks_info <message>
|
|
45
|
+
_hooks_info() {
|
|
46
|
+
if declare -f agent_info &>/dev/null; then
|
|
47
|
+
agent_info "$1"
|
|
48
|
+
else
|
|
49
|
+
echo "INFO: $1" >&2
|
|
50
|
+
fi
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# _hooks_warn <message>
|
|
54
|
+
_hooks_warn() {
|
|
55
|
+
echo "FORGE_AGENT_7f3a:WARN: $1" >&2
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ── _check_auto_sync_enabled ─────────────────────────────────────────────
|
|
59
|
+
# Reads .beads/config.yaml for team.auto-sync setting.
|
|
60
|
+
# Returns 0 if enabled (default), 1 if disabled.
|
|
61
|
+
_check_auto_sync_enabled() {
|
|
62
|
+
local beads_root="${BEADS_ROOT:-.}"
|
|
63
|
+
local config_file="$beads_root/.beads/config.yaml"
|
|
64
|
+
|
|
65
|
+
if [[ ! -f "$config_file" ]]; then
|
|
66
|
+
# Default: enabled
|
|
67
|
+
return 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Parse auto-sync value from YAML (simple grep, no yq dependency)
|
|
71
|
+
local auto_sync_line
|
|
72
|
+
auto_sync_line="$(grep -E 'auto-sync:' "$config_file" 2>/dev/null || true)"
|
|
73
|
+
|
|
74
|
+
if [[ -z "$auto_sync_line" ]]; then
|
|
75
|
+
# Not set → default enabled
|
|
76
|
+
return 0
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
local value
|
|
80
|
+
value="$(printf '%s' "$auto_sync_line" | sed -E 's/.*auto-sync:[[:space:]]*//' | tr -d '[:space:]')"
|
|
81
|
+
|
|
82
|
+
if [[ "$value" == "false" ]]; then
|
|
83
|
+
return 1
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
return 0
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# ── Public API ───────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
# forge_team_sync [--quiet]
|
|
92
|
+
# Top-level sync function called by hooks and manual `forge team sync`.
|
|
93
|
+
# 1. Check gh auth status
|
|
94
|
+
# 2. Check auto-sync config
|
|
95
|
+
# 3. Get in_progress issues with github_issue state
|
|
96
|
+
# 4. Sync each to GitHub
|
|
97
|
+
# 5. Report count (unless --quiet)
|
|
98
|
+
# Non-blocking: warns on failure, doesn't prevent push.
|
|
99
|
+
forge_team_sync() {
|
|
100
|
+
local quiet=false
|
|
101
|
+
|
|
102
|
+
# Parse arguments
|
|
103
|
+
while [[ $# -gt 0 ]]; do
|
|
104
|
+
case "$1" in
|
|
105
|
+
--quiet) quiet=true; shift ;;
|
|
106
|
+
*) shift ;;
|
|
107
|
+
esac
|
|
108
|
+
done
|
|
109
|
+
|
|
110
|
+
local gh_cmd="${GH_CMD:-gh}"
|
|
111
|
+
local bd_cmd="${BD_CMD:-bd}"
|
|
112
|
+
|
|
113
|
+
# 1. Check gh auth status
|
|
114
|
+
if ! "$gh_cmd" auth status >/dev/null 2>&1; then
|
|
115
|
+
_hooks_error "GitHub CLI not authenticated. Run 'gh auth login'."
|
|
116
|
+
return 1
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# 2. Check auto-sync config
|
|
120
|
+
if ! _check_auto_sync_enabled; then
|
|
121
|
+
# auto-sync disabled, skip silently
|
|
122
|
+
return 0
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# 3. Get all in_progress issues
|
|
126
|
+
local list_output
|
|
127
|
+
list_output="$("$bd_cmd" list --status=in_progress 2>/dev/null)" || {
|
|
128
|
+
if [[ "$quiet" == "false" ]]; then
|
|
129
|
+
_hooks_warn "Failed to list issues, sync skipped"
|
|
130
|
+
fi
|
|
131
|
+
return 0
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Filter empty lines
|
|
135
|
+
if [[ -z "$list_output" ]] || [[ -z "$(echo "$list_output" | tr -d '[:space:]')" ]]; then
|
|
136
|
+
if [[ "$quiet" == "false" ]]; then
|
|
137
|
+
_hooks_info "Nothing to sync"
|
|
138
|
+
fi
|
|
139
|
+
return 0
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# 4. For each issue, check for github_issue state and sync
|
|
143
|
+
local synced=0
|
|
144
|
+
local warnings=0
|
|
145
|
+
|
|
146
|
+
while IFS= read -r line; do
|
|
147
|
+
[[ -z "$line" ]] && continue
|
|
148
|
+
|
|
149
|
+
# Extract issue id
|
|
150
|
+
local issue_id
|
|
151
|
+
issue_id="$(echo "$line" | grep -oE 'forge-[a-zA-Z0-9]+' | head -1)"
|
|
152
|
+
[[ -z "$issue_id" ]] && continue
|
|
153
|
+
|
|
154
|
+
# Get details to check for github_issue state
|
|
155
|
+
local show_output
|
|
156
|
+
show_output="$("$bd_cmd" show "$issue_id" 2>/dev/null)" || continue
|
|
157
|
+
|
|
158
|
+
# Check if it has github_issue:N state
|
|
159
|
+
local gh_issue_num
|
|
160
|
+
gh_issue_num="$(printf '%s' "$show_output" | grep -oP 'github_issue:\K[0-9]+' | head -1 || true)"
|
|
161
|
+
|
|
162
|
+
if [[ -z "$gh_issue_num" ]]; then
|
|
163
|
+
# No GitHub issue linked, skip but warn
|
|
164
|
+
warnings=$((warnings + 1))
|
|
165
|
+
if [[ "$quiet" == "false" ]]; then
|
|
166
|
+
_hooks_warn "No github_issue for $issue_id, sync skipped"
|
|
167
|
+
fi
|
|
168
|
+
continue
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# Sync status to in_progress on GitHub
|
|
172
|
+
if sync_issue_status "$issue_id" "in_progress" 2>/dev/null; then
|
|
173
|
+
synced=$((synced + 1))
|
|
174
|
+
else
|
|
175
|
+
warnings=$((warnings + 1))
|
|
176
|
+
if [[ "$quiet" == "false" ]]; then
|
|
177
|
+
_hooks_warn "Failed to sync $issue_id to GitHub"
|
|
178
|
+
fi
|
|
179
|
+
fi
|
|
180
|
+
done <<< "$list_output"
|
|
181
|
+
|
|
182
|
+
# 5. Report
|
|
183
|
+
if [[ "$quiet" == "false" ]]; then
|
|
184
|
+
if [[ "$synced" -gt 0 ]]; then
|
|
185
|
+
_hooks_info "Synced $synced issues to GitHub"
|
|
186
|
+
elif [[ "$warnings" -gt 0 ]]; then
|
|
187
|
+
_hooks_warn "Sync completed with $warnings warnings"
|
|
188
|
+
fi
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
return 0
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# forge_team_sync_on_stage <beads-id> <from-stage> <to-stage>
|
|
195
|
+
# Called after stage transitions. Syncs the specific issue.
|
|
196
|
+
# Stages: plan → dev → validate → ship → review → premerge → verify
|
|
197
|
+
forge_team_sync_on_stage() {
|
|
198
|
+
local beads_id="${1:-}"
|
|
199
|
+
local from_stage="${2:-}"
|
|
200
|
+
local to_stage="${3:-}"
|
|
201
|
+
|
|
202
|
+
if [[ -z "$beads_id" ]] || [[ -z "$from_stage" ]] || [[ -z "$to_stage" ]]; then
|
|
203
|
+
_hooks_error "Usage: forge_team_sync_on_stage <beads-id> <from-stage> <to-stage>"
|
|
204
|
+
return 1
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
# Check auto-sync
|
|
208
|
+
if ! _check_auto_sync_enabled; then
|
|
209
|
+
return 0
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
local status=""
|
|
213
|
+
case "$to_stage" in
|
|
214
|
+
dev)
|
|
215
|
+
status="in_progress"
|
|
216
|
+
;;
|
|
217
|
+
ship)
|
|
218
|
+
status="in_progress"
|
|
219
|
+
;;
|
|
220
|
+
verify)
|
|
221
|
+
# Will be closed by /verify, skip sync
|
|
222
|
+
_hooks_info "Stage verify: sync skipped (closed by /verify)"
|
|
223
|
+
return 0
|
|
224
|
+
;;
|
|
225
|
+
*)
|
|
226
|
+
# Other stages: sync as in_progress if moving forward
|
|
227
|
+
status="in_progress"
|
|
228
|
+
;;
|
|
229
|
+
esac
|
|
230
|
+
|
|
231
|
+
if [[ -n "$status" ]]; then
|
|
232
|
+
if ! sync_issue_status "$beads_id" "$status" 2>/dev/null; then
|
|
233
|
+
_hooks_warn "Failed to sync $beads_id status on stage transition $from_stage → $to_stage"
|
|
234
|
+
fi
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
return 0
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# ── CLI entrypoint (for lefthook) ────────────────────────────────────────
|
|
241
|
+
# When called directly: bash hooks.sh sync [--quiet]
|
|
242
|
+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
243
|
+
case "${1:-}" in
|
|
244
|
+
sync)
|
|
245
|
+
shift
|
|
246
|
+
forge_team_sync "$@"
|
|
247
|
+
;;
|
|
248
|
+
*)
|
|
249
|
+
echo "Usage: hooks.sh sync [--quiet]" >&2
|
|
250
|
+
exit 1
|
|
251
|
+
;;
|
|
252
|
+
esac
|
|
253
|
+
fi
|
|
@@ -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
|
+
}
|