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
package/scripts/file-index.sh
CHANGED
|
@@ -19,6 +19,13 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
|
19
19
|
set -euo pipefail
|
|
20
20
|
fi
|
|
21
21
|
|
|
22
|
+
# Source shared libraries
|
|
23
|
+
_FI_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
source "$_FI_SCRIPT_DIR/lib/sanitize.sh"
|
|
25
|
+
if [[ -f "$_FI_SCRIPT_DIR/lib/jsonl-lock.sh" ]]; then
|
|
26
|
+
source "$_FI_SCRIPT_DIR/lib/jsonl-lock.sh"
|
|
27
|
+
fi
|
|
28
|
+
|
|
22
29
|
# ── Helpers ──────────────────────────────────────────────────────────────
|
|
23
30
|
|
|
24
31
|
# Resolve the JSONL file path.
|
|
@@ -28,24 +35,6 @@ _file_index_path() {
|
|
|
28
35
|
printf '%s' "$root/.beads/file-index.jsonl"
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
# Sanitize a string: strip shell-injection patterns (OWASP A03)
|
|
32
|
-
# Reused from dep-guard.sh pattern.
|
|
33
|
-
# Removes: double quotes, $(...), backticks, semicolons, and newlines
|
|
34
|
-
sanitize() {
|
|
35
|
-
local val="$1"
|
|
36
|
-
# Remove double quotes
|
|
37
|
-
val="${val//\"/}"
|
|
38
|
-
# Remove $(...) command substitution patterns (loop handles nested)
|
|
39
|
-
val="$(printf '%s' "$val" | sed -e ':loop' -e 's/\$([^()]*)//g' -e 't loop')"
|
|
40
|
-
# Remove backtick command substitution
|
|
41
|
-
val="${val//\`/}"
|
|
42
|
-
# Remove semicolons (command chaining)
|
|
43
|
-
val="${val//;/}"
|
|
44
|
-
# Replace newlines with spaces
|
|
45
|
-
val="$(printf '%s' "$val" | tr '\n' ' ')"
|
|
46
|
-
printf '%s' "$val"
|
|
47
|
-
}
|
|
48
|
-
|
|
49
38
|
# Validate issue_id: alphanumeric + hyphens only
|
|
50
39
|
_validate_issue_id() {
|
|
51
40
|
local id="$1"
|
|
@@ -127,7 +116,11 @@ file_index_add() {
|
|
|
127
116
|
# Ensure parent directory exists
|
|
128
117
|
mkdir -p "$(dirname "$jsonl_path")"
|
|
129
118
|
|
|
130
|
-
|
|
119
|
+
if command -v atomic_jsonl_append &>/dev/null; then
|
|
120
|
+
atomic_jsonl_append "$jsonl_path" "$json_line"
|
|
121
|
+
else
|
|
122
|
+
printf '%s\n' "$json_line" >> "$jsonl_path"
|
|
123
|
+
fi
|
|
131
124
|
}
|
|
132
125
|
|
|
133
126
|
# file_index_remove <issue_id>
|
|
@@ -163,7 +156,11 @@ file_index_remove() {
|
|
|
163
156
|
local jsonl_path
|
|
164
157
|
jsonl_path="$(_file_index_path)"
|
|
165
158
|
mkdir -p "$(dirname "$jsonl_path")"
|
|
166
|
-
|
|
159
|
+
if command -v atomic_jsonl_append &>/dev/null; then
|
|
160
|
+
atomic_jsonl_append "$jsonl_path" "$json_line"
|
|
161
|
+
else
|
|
162
|
+
printf '%s\n' "$json_line" >> "$jsonl_path"
|
|
163
|
+
fi
|
|
167
164
|
}
|
|
168
165
|
|
|
169
166
|
# file_index_read
|
|
@@ -320,12 +317,28 @@ file_index_update_from_tasks() {
|
|
|
320
317
|
# Build deduplicated JSON arrays using jq (no associative arrays needed)
|
|
321
318
|
local files_json modules_json
|
|
322
319
|
if [[ -n "$sanitized_paths" ]]; then
|
|
323
|
-
|
|
320
|
+
# Save and restore pipefail state to avoid side effects
|
|
321
|
+
local _prev_pipefail
|
|
322
|
+
_prev_pipefail="$(set +o | grep pipefail)" || true
|
|
323
|
+
set -o pipefail
|
|
324
|
+
|
|
325
|
+
files_json="$(printf '%s' "$sanitized_paths" | grep -v '^$' | jq -R . | jq -s -c 'unique')" || {
|
|
326
|
+
eval "$_prev_pipefail"
|
|
327
|
+
echo "Error: failed to build files JSON array" >&2
|
|
328
|
+
return 1
|
|
329
|
+
}
|
|
324
330
|
modules_json="$(printf '%s' "$sanitized_paths" | grep -v '^$' | while IFS= read -r p; do
|
|
325
331
|
local d
|
|
326
332
|
d="$(dirname "$p")"
|
|
327
333
|
if [[ "$d" == "." ]]; then printf '%s\n' "./"; else printf '%s\n' "${d}/"; fi
|
|
328
|
-
done | jq -R . | jq -s -c 'unique')"
|
|
334
|
+
done | jq -R . | jq -s -c 'unique')" || {
|
|
335
|
+
eval "$_prev_pipefail"
|
|
336
|
+
echo "Error: failed to build modules JSON array" >&2
|
|
337
|
+
return 1
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# Restore pipefail state
|
|
341
|
+
eval "$_prev_pipefail"
|
|
329
342
|
else
|
|
330
343
|
files_json="[]"
|
|
331
344
|
modules_json="[]"
|
|
@@ -358,13 +371,92 @@ file_index_update_from_tasks() {
|
|
|
358
371
|
tombstone: false,
|
|
359
372
|
confidence: $conf
|
|
360
373
|
}')"
|
|
361
|
-
|
|
374
|
+
if command -v atomic_jsonl_append &>/dev/null; then
|
|
375
|
+
atomic_jsonl_append "$jsonl_path" "$json_line"
|
|
376
|
+
else
|
|
377
|
+
printf '%s\n' "$json_line" >> "$jsonl_path"
|
|
378
|
+
fi
|
|
362
379
|
else
|
|
363
380
|
# Normal entry via file_index_add (no confidence field needed)
|
|
364
381
|
file_index_add "$issue_id" "$developer" "$files_json" "$modules_json"
|
|
365
382
|
fi
|
|
366
383
|
}
|
|
367
384
|
|
|
385
|
+
# file_index_auto_update <issue_id> [--from-git]
|
|
386
|
+
# Update the file index with the current set of changed files.
|
|
387
|
+
# Files come from: --from-git flag (git diff HEAD~1) or stdin.
|
|
388
|
+
file_index_auto_update() {
|
|
389
|
+
local raw_issue_id="$1"
|
|
390
|
+
shift
|
|
391
|
+
local from_git=0
|
|
392
|
+
|
|
393
|
+
# Parse flags
|
|
394
|
+
while [[ $# -gt 0 ]]; do
|
|
395
|
+
case "$1" in
|
|
396
|
+
--from-git) from_git=1; shift ;;
|
|
397
|
+
*) echo "Error: unknown flag '$1'" >&2; return 1 ;;
|
|
398
|
+
esac
|
|
399
|
+
done
|
|
400
|
+
|
|
401
|
+
# Sanitize and validate issue_id
|
|
402
|
+
local issue_id
|
|
403
|
+
issue_id="$(sanitize "$raw_issue_id")"
|
|
404
|
+
issue_id="$(printf '%s' "$issue_id" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
405
|
+
_validate_issue_id "$issue_id" || return 1
|
|
406
|
+
|
|
407
|
+
# Get file list
|
|
408
|
+
local -a files=()
|
|
409
|
+
if [[ "$from_git" -eq 1 ]]; then
|
|
410
|
+
while IFS= read -r f; do
|
|
411
|
+
[[ -n "$f" ]] && files+=("$f")
|
|
412
|
+
done < <(git diff --name-only HEAD~1 2>/dev/null || true)
|
|
413
|
+
else
|
|
414
|
+
# Read from stdin (non-blocking check)
|
|
415
|
+
if [[ ! -t 0 ]]; then
|
|
416
|
+
while IFS= read -r f; do
|
|
417
|
+
[[ -n "$f" ]] && files+=("$f")
|
|
418
|
+
done
|
|
419
|
+
else
|
|
420
|
+
echo "Error: no file list provided (use --from-git or pipe files via stdin)" >&2
|
|
421
|
+
return 1
|
|
422
|
+
fi
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
# Empty file list = nothing to do
|
|
426
|
+
if [[ ${#files[@]} -eq 0 ]]; then
|
|
427
|
+
echo "No files changed"
|
|
428
|
+
return 0
|
|
429
|
+
fi
|
|
430
|
+
|
|
431
|
+
# Get session identity
|
|
432
|
+
local developer
|
|
433
|
+
if ! command -v get_session_identity &>/dev/null; then
|
|
434
|
+
local script_dir
|
|
435
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
436
|
+
if [[ -f "$script_dir/sync-utils.sh" ]]; then
|
|
437
|
+
source "$script_dir/sync-utils.sh"
|
|
438
|
+
fi
|
|
439
|
+
fi
|
|
440
|
+
developer="$(get_session_identity)" || {
|
|
441
|
+
echo "Error: could not determine session identity" >&2
|
|
442
|
+
return 1
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# Build JSON arrays using jq
|
|
446
|
+
local files_json modules_json
|
|
447
|
+
files_json="$(printf '%s\n' "${files[@]}" | jq -R . | jq -s -c 'unique')"
|
|
448
|
+
modules_json="$(printf '%s\n' "${files[@]}" | while IFS= read -r p; do
|
|
449
|
+
local d
|
|
450
|
+
d="$(dirname "$p")"
|
|
451
|
+
if [[ "$d" == "." ]]; then printf '%s\n' "./"; else printf '%s\n' "${d}/"; fi
|
|
452
|
+
done | jq -R . | jq -s -c 'unique')"
|
|
453
|
+
|
|
454
|
+
# Append via file_index_add (which uses atomic locking)
|
|
455
|
+
file_index_add "$issue_id" "$developer" "$files_json" "$modules_json"
|
|
456
|
+
|
|
457
|
+
echo "File index updated for $issue_id (${#files[@]} files)"
|
|
458
|
+
}
|
|
459
|
+
|
|
368
460
|
# ── Main dispatcher (when run as a script) ────────────────────────────────
|
|
369
461
|
|
|
370
462
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
@@ -378,6 +470,7 @@ Subcommands:
|
|
|
378
470
|
read
|
|
379
471
|
get <issue_id>
|
|
380
472
|
update-from-tasks <issue_id> <task_file_path> [action]
|
|
473
|
+
auto-update <issue_id> [--from-git]
|
|
381
474
|
EOF
|
|
382
475
|
exit 1
|
|
383
476
|
fi
|
|
@@ -391,6 +484,7 @@ EOF
|
|
|
391
484
|
read) file_index_read ;;
|
|
392
485
|
get) file_index_get "$@" ;;
|
|
393
486
|
update-from-tasks) file_index_update_from_tasks "$@" ;;
|
|
487
|
+
auto-update) file_index_auto_update "$@" ;;
|
|
394
488
|
*)
|
|
395
489
|
echo "Error: Unknown subcommand '${subcommand}'" >&2
|
|
396
490
|
exit 1
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# forge-team — Team orchestration plugin for Forge.
|
|
3
|
+
#
|
|
4
|
+
# Subcommands:
|
|
5
|
+
# workload Show team workload by developer
|
|
6
|
+
# epic Epic progress rollup
|
|
7
|
+
# dashboard Team health dashboard
|
|
8
|
+
# add Add developer to team map
|
|
9
|
+
# verify Check 1:1 Beads<>GitHub enforcement
|
|
10
|
+
# sync Manual GitHub<>Beads sync
|
|
11
|
+
# claim Claim an issue with pre-check
|
|
12
|
+
# help Show usage
|
|
13
|
+
#
|
|
14
|
+
# Exit codes: 0=success, 1=error, 2=validation error
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
|
|
20
|
+
# Source libs (graceful if missing — stubs used in early dev)
|
|
21
|
+
[[ -f "$SCRIPT_DIR/lib/agent-prompt.sh" ]] && source "$SCRIPT_DIR/lib/agent-prompt.sh"
|
|
22
|
+
[[ -f "$SCRIPT_DIR/lib/identity.sh" ]] && source "$SCRIPT_DIR/lib/identity.sh"
|
|
23
|
+
[[ -f "$SCRIPT_DIR/lib/workload.sh" ]] && source "$SCRIPT_DIR/lib/workload.sh"
|
|
24
|
+
[[ -f "$SCRIPT_DIR/lib/epic.sh" ]] && source "$SCRIPT_DIR/lib/epic.sh"
|
|
25
|
+
[[ -f "$SCRIPT_DIR/lib/dashboard.sh" ]] && source "$SCRIPT_DIR/lib/dashboard.sh"
|
|
26
|
+
[[ -f "$SCRIPT_DIR/lib/hooks.sh" ]] && source "$SCRIPT_DIR/lib/hooks.sh"
|
|
27
|
+
[[ -f "$SCRIPT_DIR/lib/verify.sh" ]] && source "$SCRIPT_DIR/lib/verify.sh"
|
|
28
|
+
[[ -f "$SCRIPT_DIR/lib/claim.sh" ]] && source "$SCRIPT_DIR/lib/claim.sh"
|
|
29
|
+
|
|
30
|
+
# Source shared forge utilities
|
|
31
|
+
FORGE_SCRIPTS="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
32
|
+
[[ -f "$FORGE_SCRIPTS/lib/sanitize.sh" ]] && source "$FORGE_SCRIPTS/lib/sanitize.sh"
|
|
33
|
+
|
|
34
|
+
# ── Stub implementations ──
|
|
35
|
+
# cmd_workload provided by lib/workload.sh
|
|
36
|
+
# cmd_epic provided by lib/epic.sh
|
|
37
|
+
# cmd_dashboard provided by lib/dashboard.sh
|
|
38
|
+
cmd_add() { auto_detect_identity "$@"; }
|
|
39
|
+
# cmd_verify provided by lib/verify.sh
|
|
40
|
+
cmd_sync() { forge_team_sync "$@"; }
|
|
41
|
+
cmd_claim() { forge_team_claim "$@"; }
|
|
42
|
+
|
|
43
|
+
cmd_help() {
|
|
44
|
+
cat <<'EOF'
|
|
45
|
+
Usage: forge team <subcommand> [args...]
|
|
46
|
+
|
|
47
|
+
Subcommands:
|
|
48
|
+
workload [--developer=<user>] [--me] Show team workload
|
|
49
|
+
epic <issue-id> Epic progress rollup
|
|
50
|
+
dashboard Team health dashboard
|
|
51
|
+
add [--github=<user>] Add developer to team map
|
|
52
|
+
verify Check 1:1 Beads<>GitHub sync
|
|
53
|
+
sync Manual GitHub<>Beads sync
|
|
54
|
+
claim <issue-id> [--force] Claim issue with pre-check
|
|
55
|
+
help Show this message
|
|
56
|
+
EOF
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# ── Main dispatcher ──
|
|
60
|
+
main() {
|
|
61
|
+
if [[ $# -lt 1 ]]; then
|
|
62
|
+
cmd_help >&2
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
local subcommand="$1"
|
|
67
|
+
shift
|
|
68
|
+
|
|
69
|
+
case "$subcommand" in
|
|
70
|
+
workload) cmd_workload "$@" ;;
|
|
71
|
+
epic) cmd_epic "$@" ;;
|
|
72
|
+
dashboard) cmd_dashboard "$@" ;;
|
|
73
|
+
add) cmd_add "$@" ;;
|
|
74
|
+
verify) cmd_verify "$@" ;;
|
|
75
|
+
sync) cmd_sync "$@" ;;
|
|
76
|
+
claim) cmd_claim "$@" ;;
|
|
77
|
+
help) cmd_help ;;
|
|
78
|
+
*)
|
|
79
|
+
echo "Error: unknown subcommand '$subcommand'" >&2
|
|
80
|
+
cmd_help >&2
|
|
81
|
+
exit 1
|
|
82
|
+
;;
|
|
83
|
+
esac
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
main "$@"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# agent-prompt.sh — Shared library for structured agent communication
|
|
2
|
+
# All user-facing output from `forge team` goes through these helpers.
|
|
3
|
+
# Uses non-guessable prefix to prevent injection from GitHub data.
|
|
4
|
+
#
|
|
5
|
+
# Usage: source this file from other scripts.
|
|
6
|
+
# This library does NOT set errexit/pipefail.
|
|
7
|
+
|
|
8
|
+
# Guard against double-sourcing (readonly would error on second source)
|
|
9
|
+
if [[ -n "${_AGENT_PROMPT_LIB_LOADED:-}" ]]; then
|
|
10
|
+
return 0 2>/dev/null || true
|
|
11
|
+
fi
|
|
12
|
+
_AGENT_PROMPT_LIB_LOADED=1
|
|
13
|
+
|
|
14
|
+
readonly FORGE_AGENT_PREFIX="FORGE_AGENT_7f3a"
|
|
15
|
+
|
|
16
|
+
# Output a prompt for the AI agent to ask the user.
|
|
17
|
+
# Format: FORGE_AGENT_7f3a:PROMPT: <message> (to stderr)
|
|
18
|
+
agent_prompt() {
|
|
19
|
+
echo "${FORGE_AGENT_PREFIX}:PROMPT: $1" >&2
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Informational output.
|
|
23
|
+
# Format: FORGE_AGENT_7f3a:INFO: <message> (to stderr)
|
|
24
|
+
agent_info() {
|
|
25
|
+
echo "${FORGE_AGENT_PREFIX}:INFO: $1" >&2
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Error output.
|
|
29
|
+
# Format: FORGE_AGENT_7f3a:ERROR: <message> (to stderr)
|
|
30
|
+
agent_error() {
|
|
31
|
+
echo "${FORGE_AGENT_PREFIX}:ERROR: $1" >&2
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Strip any occurrence of the FORGE_AGENT_7f3a: prefix from input strings.
|
|
35
|
+
# Prevents injection from GitHub issue titles or other external data.
|
|
36
|
+
# Takes input via stdin or as $1 arg, outputs sanitized text to stdout.
|
|
37
|
+
sanitize_for_agent() {
|
|
38
|
+
local input
|
|
39
|
+
if [[ $# -gt 0 ]]; then
|
|
40
|
+
input="$1"
|
|
41
|
+
else
|
|
42
|
+
input="$(cat)"
|
|
43
|
+
fi
|
|
44
|
+
# Loop until no more prefix occurrences (prevents split-prefix bypass)
|
|
45
|
+
local prev
|
|
46
|
+
while true; do
|
|
47
|
+
prev="$input"
|
|
48
|
+
input="${input//${FORGE_AGENT_PREFIX}:/}"
|
|
49
|
+
[[ "$input" == "$prev" ]] && break
|
|
50
|
+
done
|
|
51
|
+
printf '%s' "$input"
|
|
52
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/forge-team/lib/claim.sh — Claim locking with pre-claim check
|
|
3
|
+
#
|
|
4
|
+
# Before allowing issue claim, checks GitHub assignee and uses flock/mkdir
|
|
5
|
+
# to prevent race conditions between concurrent agents.
|
|
6
|
+
#
|
|
7
|
+
# Functions:
|
|
8
|
+
# pre_claim_check — Check if issue is claimable
|
|
9
|
+
# claim_with_lock — Atomic claim with file locking
|
|
10
|
+
# forge_team_claim — Top-level dispatcher entry point
|
|
11
|
+
#
|
|
12
|
+
# Env overrides (for testing):
|
|
13
|
+
# GH_CMD — Path to gh binary
|
|
14
|
+
# BD_CMD — Path to bd binary
|
|
15
|
+
#
|
|
16
|
+
# This file does NOT set errexit/pipefail — callers manage their own shell options.
|
|
17
|
+
|
|
18
|
+
# Guard against double-sourcing
|
|
19
|
+
if [[ -n "${_CLAIM_LIB_LOADED:-}" ]]; then
|
|
20
|
+
return 0 2>/dev/null || true
|
|
21
|
+
fi
|
|
22
|
+
_CLAIM_LIB_LOADED=1
|
|
23
|
+
|
|
24
|
+
_CLAIM_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
25
|
+
|
|
26
|
+
# Source dependencies
|
|
27
|
+
if [[ -f "$_CLAIM_DIR/agent-prompt.sh" ]]; then
|
|
28
|
+
source "$_CLAIM_DIR/agent-prompt.sh"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
if [[ -f "$_CLAIM_DIR/identity.sh" ]]; then
|
|
32
|
+
source "$_CLAIM_DIR/identity.sh"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# ── Internal helpers ─────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
# _claim_error <message>
|
|
38
|
+
_claim_error() {
|
|
39
|
+
if declare -f agent_error &>/dev/null; then
|
|
40
|
+
agent_error "$1"
|
|
41
|
+
else
|
|
42
|
+
echo "ERROR: $1" >&2
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# _claim_info <message>
|
|
47
|
+
_claim_info() {
|
|
48
|
+
if declare -f agent_info &>/dev/null; then
|
|
49
|
+
agent_info "$1"
|
|
50
|
+
else
|
|
51
|
+
echo "INFO: $1" >&2
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# _claim_prompt <message>
|
|
56
|
+
_claim_prompt() {
|
|
57
|
+
if declare -f agent_prompt &>/dev/null; then
|
|
58
|
+
agent_prompt "$1"
|
|
59
|
+
else
|
|
60
|
+
echo "PROMPT: $1" >&2
|
|
61
|
+
fi
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# _get_github_issue_number <beads-id>
|
|
65
|
+
# Extracts github_issue:N from bd show output. Returns number or empty.
|
|
66
|
+
_get_github_issue_number() {
|
|
67
|
+
local beads_id="$1"
|
|
68
|
+
local bd_cmd="${BD_CMD:-bd}"
|
|
69
|
+
local bd_output
|
|
70
|
+
bd_output="$("$bd_cmd" show "$beads_id" 2>/dev/null)" || return 1
|
|
71
|
+
|
|
72
|
+
local issue_num
|
|
73
|
+
issue_num="$(printf '%s' "$bd_output" | grep -oP 'github_issue:\K[0-9]+' || true)"
|
|
74
|
+
if [[ -z "$issue_num" ]]; then
|
|
75
|
+
return 1
|
|
76
|
+
fi
|
|
77
|
+
printf '%s' "$issue_num"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# _get_github_assignee <issue-number>
|
|
81
|
+
# Returns the GitHub login of the assignee, or empty if unassigned.
|
|
82
|
+
_get_github_assignee() {
|
|
83
|
+
local issue_num="$1"
|
|
84
|
+
local gh_cmd="${GH_CMD:-gh}"
|
|
85
|
+
local assignee
|
|
86
|
+
assignee="$("$gh_cmd" issue view "$issue_num" --json assignees --jq '.assignees[0].login // empty' 2>/dev/null)" || return 1
|
|
87
|
+
printf '%s' "$assignee"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# _claim_lock_dir — Returns the path to the claim lock directory
|
|
91
|
+
_claim_lock_dir() {
|
|
92
|
+
local root="${TEAM_MAP_ROOT:-.}"
|
|
93
|
+
printf '%s' "$root/.beads"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# ── Public API ───────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
# pre_claim_check <beads-id>
|
|
99
|
+
# Checks if issue is claimable. Returns 0 if clear, 1 if blocked.
|
|
100
|
+
pre_claim_check() {
|
|
101
|
+
local beads_id="${1:-}"
|
|
102
|
+
if [[ -z "$beads_id" ]]; then
|
|
103
|
+
_claim_error "Usage: pre_claim_check <beads-id>"
|
|
104
|
+
return 1
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# Get current user first (validates gh auth)
|
|
108
|
+
local current_user
|
|
109
|
+
if ! current_user="$(get_github_user)"; then
|
|
110
|
+
_claim_error "GitHub CLI not authenticated. Run 'gh auth login' first."
|
|
111
|
+
return 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Get GitHub issue number from beads
|
|
115
|
+
local issue_num
|
|
116
|
+
if ! issue_num="$(_get_github_issue_number "$beads_id")"; then
|
|
117
|
+
_claim_error "No github_issue number found for $beads_id"
|
|
118
|
+
return 1
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
if [[ -z "$issue_num" ]]; then
|
|
122
|
+
_claim_error "No github_issue number found for $beads_id"
|
|
123
|
+
return 1
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# Query GitHub assignee
|
|
127
|
+
local assignee
|
|
128
|
+
assignee="$(_get_github_assignee "$issue_num")" || true
|
|
129
|
+
|
|
130
|
+
# If unassigned or assigned to current user: proceed
|
|
131
|
+
if [[ -z "$assignee" ]] || [[ "$assignee" == "$current_user" ]]; then
|
|
132
|
+
return 0
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# Assigned to someone else: prompt
|
|
136
|
+
_claim_prompt "$beads_id is claimed by $assignee. Override? Run: forge team claim $beads_id --force"
|
|
137
|
+
return 1
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# claim_with_lock <beads-id> [--force]
|
|
141
|
+
# Atomic claim using flock/mkdir locking pattern.
|
|
142
|
+
# With --force, skips the re-check of assignee inside the lock.
|
|
143
|
+
claim_with_lock() {
|
|
144
|
+
local beads_id="${1:-}"
|
|
145
|
+
local force="${2:-}"
|
|
146
|
+
if [[ -z "$beads_id" ]]; then
|
|
147
|
+
_claim_error "Usage: claim_with_lock <beads-id>"
|
|
148
|
+
return 1
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
local gh_cmd="${GH_CMD:-gh}"
|
|
152
|
+
local bd_cmd="${BD_CMD:-bd}"
|
|
153
|
+
local lock_dir
|
|
154
|
+
lock_dir="$(_claim_lock_dir)"
|
|
155
|
+
mkdir -p "$lock_dir"
|
|
156
|
+
|
|
157
|
+
local lock_file="$lock_dir/claim.lock"
|
|
158
|
+
|
|
159
|
+
# Get current user
|
|
160
|
+
local current_user
|
|
161
|
+
if ! current_user="$(get_github_user)"; then
|
|
162
|
+
_claim_error "GitHub CLI not authenticated."
|
|
163
|
+
return 1
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# Get GitHub issue number
|
|
167
|
+
local issue_num
|
|
168
|
+
if ! issue_num="$(_get_github_issue_number "$beads_id")"; then
|
|
169
|
+
_claim_error "No github_issue number found for $beads_id"
|
|
170
|
+
return 1
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
if command -v flock &>/dev/null; then
|
|
174
|
+
# flock-based locking
|
|
175
|
+
(
|
|
176
|
+
flock -w 5 200 || {
|
|
177
|
+
_claim_error "Claim lock timeout after 5s"
|
|
178
|
+
exit 1
|
|
179
|
+
}
|
|
180
|
+
# Re-check assignee inside lock (skip if --force)
|
|
181
|
+
if [[ "$force" != "--force" ]]; then
|
|
182
|
+
local assignee
|
|
183
|
+
assignee="$(_get_github_assignee "$issue_num")" || true
|
|
184
|
+
if [[ -n "$assignee" ]] && [[ "$assignee" != "$current_user" ]]; then
|
|
185
|
+
_claim_error "$beads_id was claimed by $assignee while waiting for lock"
|
|
186
|
+
exit 1
|
|
187
|
+
fi
|
|
188
|
+
fi
|
|
189
|
+
# Claim on both Beads and GitHub
|
|
190
|
+
"$bd_cmd" update "$beads_id" --claim >/dev/null 2>&1 || true
|
|
191
|
+
"$gh_cmd" issue edit "$issue_num" --add-assignee "$current_user" >/dev/null 2>&1 || true
|
|
192
|
+
) 200>"$lock_file"
|
|
193
|
+
local subshell_rc=$?
|
|
194
|
+
[[ $subshell_rc -ne 0 ]] && return $subshell_rc
|
|
195
|
+
else
|
|
196
|
+
# mkdir-based fallback
|
|
197
|
+
local lock_mkdir="$lock_dir/claim.lock.d"
|
|
198
|
+
local attempts=0
|
|
199
|
+
while ! mkdir "$lock_mkdir" 2>/dev/null; do
|
|
200
|
+
attempts=$((attempts + 1))
|
|
201
|
+
if [[ $attempts -ge 50 ]]; then
|
|
202
|
+
_claim_error "Claim lock timeout after 5s"
|
|
203
|
+
return 1
|
|
204
|
+
fi
|
|
205
|
+
sleep 0.1
|
|
206
|
+
done
|
|
207
|
+
trap 'rmdir "$lock_mkdir" 2>/dev/null; trap - RETURN' RETURN
|
|
208
|
+
|
|
209
|
+
# Re-check assignee inside lock (skip if --force)
|
|
210
|
+
if [[ "$force" != "--force" ]]; then
|
|
211
|
+
local assignee
|
|
212
|
+
assignee="$(_get_github_assignee "$issue_num")" || true
|
|
213
|
+
if [[ -n "$assignee" ]] && [[ "$assignee" != "$current_user" ]]; then
|
|
214
|
+
_claim_error "$beads_id was claimed by $assignee while waiting for lock"
|
|
215
|
+
return 1
|
|
216
|
+
fi
|
|
217
|
+
fi
|
|
218
|
+
# Claim on both Beads and GitHub
|
|
219
|
+
"$bd_cmd" update "$beads_id" --claim >/dev/null 2>&1 || true
|
|
220
|
+
"$gh_cmd" issue edit "$issue_num" --add-assignee "$current_user" >/dev/null 2>&1 || true
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
return 0
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# forge_team_claim <beads-id> [--force]
|
|
227
|
+
# Top-level function called by the dispatcher.
|
|
228
|
+
forge_team_claim() {
|
|
229
|
+
local beads_id="${1:-}"
|
|
230
|
+
local force_flag="${2:-}"
|
|
231
|
+
|
|
232
|
+
if [[ -z "$beads_id" ]]; then
|
|
233
|
+
_claim_error "Usage: forge team claim <beads-id> [--force]"
|
|
234
|
+
return 1
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
local bd_cmd="${BD_CMD:-bd}"
|
|
238
|
+
|
|
239
|
+
if [[ "$force_flag" == "--force" ]]; then
|
|
240
|
+
# Skip pre-claim check, claim directly (pass --force to skip lock re-check too)
|
|
241
|
+
claim_with_lock "$beads_id" "--force" || return 1
|
|
242
|
+
# Log override via bd comments
|
|
243
|
+
"$bd_cmd" comments add "$beads_id" "Force-claimed by $(get_github_user 2>/dev/null || echo 'unknown')" >/dev/null 2>&1 || true
|
|
244
|
+
_claim_info "$beads_id force-claimed successfully"
|
|
245
|
+
return 0
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
# Normal flow: pre-claim check then lock-claim
|
|
249
|
+
if ! pre_claim_check "$beads_id"; then
|
|
250
|
+
return 1
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
claim_with_lock "$beads_id" || return 1
|
|
254
|
+
_claim_info "$beads_id claimed successfully"
|
|
255
|
+
return 0
|
|
256
|
+
}
|