@cyperx/clawforge 1.2.0
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/LICENSE +21 -0
- package/README.md +312 -0
- package/VERSION +1 -0
- package/bin/attach.sh +98 -0
- package/bin/check-agents.sh +343 -0
- package/bin/clawforge +257 -0
- package/bin/clawforge-dashboard +0 -0
- package/bin/clean.sh +257 -0
- package/bin/config.sh +111 -0
- package/bin/conflicts.sh +224 -0
- package/bin/cost.sh +273 -0
- package/bin/dashboard.sh +557 -0
- package/bin/diff.sh +109 -0
- package/bin/doctor.sh +196 -0
- package/bin/eval.sh +217 -0
- package/bin/history.sh +91 -0
- package/bin/init.sh +182 -0
- package/bin/learn.sh +230 -0
- package/bin/logs.sh +126 -0
- package/bin/memory.sh +207 -0
- package/bin/merge-helper.sh +174 -0
- package/bin/multi-review.sh +215 -0
- package/bin/notify.sh +93 -0
- package/bin/on-complete.sh +149 -0
- package/bin/parse-cost.sh +205 -0
- package/bin/pr.sh +167 -0
- package/bin/resume.sh +183 -0
- package/bin/review-mode.sh +163 -0
- package/bin/review-pr.sh +145 -0
- package/bin/routing.sh +88 -0
- package/bin/scope-task.sh +169 -0
- package/bin/spawn-agent.sh +190 -0
- package/bin/sprint.sh +320 -0
- package/bin/steer.sh +107 -0
- package/bin/stop.sh +136 -0
- package/bin/summary.sh +182 -0
- package/bin/swarm.sh +525 -0
- package/bin/templates.sh +244 -0
- package/lib/common.sh +302 -0
- package/lib/templates/bugfix.json +6 -0
- package/lib/templates/migration.json +7 -0
- package/lib/templates/refactor.json +6 -0
- package/lib/templates/security-audit.json +5 -0
- package/lib/templates/test-coverage.json +6 -0
- package/package.json +31 -0
- package/registry/conflicts.jsonl +0 -0
- package/registry/costs.jsonl +0 -0
- package/tui/PRD.md +106 -0
- package/tui/agent.go +266 -0
- package/tui/animation.go +192 -0
- package/tui/dashboard.go +219 -0
- package/tui/filter.go +68 -0
- package/tui/go.mod +25 -0
- package/tui/go.sum +46 -0
- package/tui/keybindings.go +229 -0
- package/tui/main.go +61 -0
- package/tui/model.go +166 -0
- package/tui/steer.go +69 -0
- package/tui/styles.go +69 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# merge-helper.sh — Module 7: PR merge helper with safety checks
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
source "${SCRIPT_DIR}/../lib/common.sh"
|
|
7
|
+
|
|
8
|
+
# ── Help ───────────────────────────────────────────────────────────────
|
|
9
|
+
usage() {
|
|
10
|
+
cat <<EOF
|
|
11
|
+
Usage: merge-helper.sh --repo <path> --pr <number> [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--repo <path> Path to the git repository (required)
|
|
15
|
+
--pr <number> PR number (required)
|
|
16
|
+
--auto Auto-merge if CI passing and reviews approved
|
|
17
|
+
--squash Use squash merge
|
|
18
|
+
--task-id <id> Task ID to update in registry
|
|
19
|
+
--dry-run Show what would happen without executing
|
|
20
|
+
--help Show this help
|
|
21
|
+
EOF
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# ── Parse args ─────────────────────────────────────────────────────────
|
|
25
|
+
REPO="" PR_NUMBER="" AUTO=false SQUASH=false TASK_ID="" DRY_RUN=false
|
|
26
|
+
|
|
27
|
+
while [[ $# -gt 0 ]]; do
|
|
28
|
+
case "$1" in
|
|
29
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
30
|
+
--pr) PR_NUMBER="$2"; shift 2 ;;
|
|
31
|
+
--auto) AUTO=true; shift ;;
|
|
32
|
+
--squash) SQUASH=true; shift ;;
|
|
33
|
+
--task-id) TASK_ID="$2"; shift 2 ;;
|
|
34
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
35
|
+
--help|-h) usage; exit 0 ;;
|
|
36
|
+
*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
37
|
+
esac
|
|
38
|
+
done
|
|
39
|
+
|
|
40
|
+
[[ -z "$REPO" ]] && { log_error "--repo is required"; usage; exit 1; }
|
|
41
|
+
[[ -z "$PR_NUMBER" ]] && { log_error "--pr is required"; usage; exit 1; }
|
|
42
|
+
|
|
43
|
+
REPO_ABS=$(cd "$REPO" && pwd)
|
|
44
|
+
|
|
45
|
+
# ── Resolve task ID from registry if not given ────────────────────────
|
|
46
|
+
if [[ -z "$TASK_ID" ]]; then
|
|
47
|
+
_ensure_registry
|
|
48
|
+
TASK_ID=$(jq -r --argjson pr "$PR_NUMBER" '.tasks[] | select(.pr == $pr) | .id' "$REGISTRY_FILE" 2>/dev/null || true)
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# ── Fetch PR info ────────────────────────────────────────────────────
|
|
52
|
+
log_info "Fetching PR #${PR_NUMBER} info..."
|
|
53
|
+
PR_INFO=$(gh pr view "$PR_NUMBER" --repo "$REPO_ABS" --json title,body,state,mergeable,reviewDecision,statusCheckRollup,additions,deletions,changedFiles 2>/dev/null || echo '{}')
|
|
54
|
+
|
|
55
|
+
PR_TITLE=$(echo "$PR_INFO" | jq -r '.title // "unknown"')
|
|
56
|
+
PR_STATE=$(echo "$PR_INFO" | jq -r '.state // "unknown"')
|
|
57
|
+
PR_MERGEABLE=$(echo "$PR_INFO" | jq -r '.mergeable // "unknown"')
|
|
58
|
+
PR_REVIEW=$(echo "$PR_INFO" | jq -r '.reviewDecision // "none"')
|
|
59
|
+
ADDITIONS=$(echo "$PR_INFO" | jq -r '.additions // 0')
|
|
60
|
+
DELETIONS=$(echo "$PR_INFO" | jq -r '.deletions // 0')
|
|
61
|
+
CHANGED=$(echo "$PR_INFO" | jq -r '.changedFiles // 0')
|
|
62
|
+
|
|
63
|
+
# Check CI status
|
|
64
|
+
CI_STATUS="unknown"
|
|
65
|
+
CI_CHECKS=$(echo "$PR_INFO" | jq '.statusCheckRollup // []')
|
|
66
|
+
if [[ "$CI_CHECKS" != "null" && "$CI_CHECKS" != "[]" ]]; then
|
|
67
|
+
FAILING=$(echo "$CI_CHECKS" | jq '[.[] | select(.conclusion != "SUCCESS" and .conclusion != null)] | length')
|
|
68
|
+
PENDING=$(echo "$CI_CHECKS" | jq '[.[] | select(.conclusion == null)] | length')
|
|
69
|
+
if [[ "$FAILING" -gt 0 ]]; then
|
|
70
|
+
CI_STATUS="failing"
|
|
71
|
+
elif [[ "$PENDING" -gt 0 ]]; then
|
|
72
|
+
CI_STATUS="pending"
|
|
73
|
+
else
|
|
74
|
+
CI_STATUS="passing"
|
|
75
|
+
fi
|
|
76
|
+
else
|
|
77
|
+
CI_STATUS="no-checks"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# ── Summary ──────────────────────────────────────────────────────────
|
|
81
|
+
echo "=== PR #${PR_NUMBER} Summary ==="
|
|
82
|
+
echo "Title: $PR_TITLE"
|
|
83
|
+
echo "State: $PR_STATE"
|
|
84
|
+
echo "Mergeable: $PR_MERGEABLE"
|
|
85
|
+
echo "Reviews: $PR_REVIEW"
|
|
86
|
+
echo "CI: $CI_STATUS"
|
|
87
|
+
echo "Diff: +${ADDITIONS} -${DELETIONS} (${CHANGED} files)"
|
|
88
|
+
echo ""
|
|
89
|
+
|
|
90
|
+
# ── Merge decision ───────────────────────────────────────────────────
|
|
91
|
+
MERGE_CMD="gh pr merge $PR_NUMBER --repo $REPO_ABS"
|
|
92
|
+
if $SQUASH; then
|
|
93
|
+
MERGE_CMD+=" --squash"
|
|
94
|
+
else
|
|
95
|
+
MERGE_CMD+=" --merge"
|
|
96
|
+
fi
|
|
97
|
+
MERGE_CMD+=" --delete-branch"
|
|
98
|
+
|
|
99
|
+
CAN_AUTO=true
|
|
100
|
+
REASONS=()
|
|
101
|
+
|
|
102
|
+
if [[ "$PR_STATE" != "OPEN" ]]; then
|
|
103
|
+
CAN_AUTO=false
|
|
104
|
+
REASONS+=("PR is not open (state: $PR_STATE)")
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
if [[ "$CI_STATUS" == "failing" ]]; then
|
|
108
|
+
CAN_AUTO=false
|
|
109
|
+
REASONS+=("CI checks are failing")
|
|
110
|
+
elif [[ "$CI_STATUS" == "pending" ]]; then
|
|
111
|
+
CAN_AUTO=false
|
|
112
|
+
REASONS+=("CI checks still pending")
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
if [[ "$PR_REVIEW" != "APPROVED" ]]; then
|
|
116
|
+
CAN_AUTO=false
|
|
117
|
+
REASONS+=("Not all reviews approved (status: $PR_REVIEW)")
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
if [[ "$PR_MERGEABLE" == "CONFLICTING" ]]; then
|
|
121
|
+
CAN_AUTO=false
|
|
122
|
+
REASONS+=("PR has merge conflicts")
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
if $AUTO; then
|
|
126
|
+
if $CAN_AUTO; then
|
|
127
|
+
log_info "All checks pass, proceeding with auto-merge"
|
|
128
|
+
if $DRY_RUN; then
|
|
129
|
+
echo "[dry-run] Would execute: $MERGE_CMD"
|
|
130
|
+
else
|
|
131
|
+
eval "$MERGE_CMD" || { log_error "Merge failed"; exit 1; }
|
|
132
|
+
log_info "PR #${PR_NUMBER} merged successfully"
|
|
133
|
+
|
|
134
|
+
# Update registry
|
|
135
|
+
if [[ -n "$TASK_ID" ]]; then
|
|
136
|
+
registry_update "$TASK_ID" "status" '"done"'
|
|
137
|
+
registry_update "$TASK_ID" "completedAt" "$(epoch_ms)"
|
|
138
|
+
log_info "Registry updated: $TASK_ID → done"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# Trigger cleanup
|
|
142
|
+
CLEAN_SCRIPT="${SCRIPT_DIR}/clean.sh"
|
|
143
|
+
if [[ -n "$TASK_ID" && -x "$CLEAN_SCRIPT" ]]; then
|
|
144
|
+
log_info "Triggering cleanup for task $TASK_ID"
|
|
145
|
+
"$CLEAN_SCRIPT" --task-id "$TASK_ID" 2>/dev/null || log_warn "Cleanup had issues"
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Trigger notification
|
|
149
|
+
NOTIFY_SCRIPT="${SCRIPT_DIR}/notify.sh"
|
|
150
|
+
if [[ -x "$NOTIFY_SCRIPT" ]]; then
|
|
151
|
+
"$NOTIFY_SCRIPT" --type task-done --task-id "${TASK_ID:-}" --description "$PR_TITLE" 2>/dev/null || log_warn "Notification had issues"
|
|
152
|
+
fi
|
|
153
|
+
fi
|
|
154
|
+
else
|
|
155
|
+
log_warn "Cannot auto-merge:"
|
|
156
|
+
for reason in "${REASONS[@]}"; do
|
|
157
|
+
echo " • $reason"
|
|
158
|
+
done
|
|
159
|
+
exit 1
|
|
160
|
+
fi
|
|
161
|
+
else
|
|
162
|
+
if $CAN_AUTO; then
|
|
163
|
+
echo "Ready to merge. Run:"
|
|
164
|
+
echo " $MERGE_CMD"
|
|
165
|
+
else
|
|
166
|
+
echo "Not ready to merge:"
|
|
167
|
+
for reason in "${REASONS[@]}"; do
|
|
168
|
+
echo " • $reason"
|
|
169
|
+
done
|
|
170
|
+
echo ""
|
|
171
|
+
echo "Manual merge command (when ready):"
|
|
172
|
+
echo " $MERGE_CMD"
|
|
173
|
+
fi
|
|
174
|
+
fi
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# multi-review.sh — Run a PR through multiple models and compare feedback
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
source "${SCRIPT_DIR}/../lib/common.sh"
|
|
7
|
+
|
|
8
|
+
usage() {
|
|
9
|
+
cat <<EOF
|
|
10
|
+
Usage: clawforge multi-review --pr <number> [options]
|
|
11
|
+
|
|
12
|
+
Run a PR through multiple AI models and compare their review feedback.
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--pr <number> PR number (required)
|
|
16
|
+
--repo <path> Repository path (default: auto-detect)
|
|
17
|
+
--models <list> Comma-separated model list (default: from config review_models)
|
|
18
|
+
--output <dir> Save individual reviews to directory
|
|
19
|
+
--diff-only Show only where models disagree
|
|
20
|
+
--json Output as JSON
|
|
21
|
+
--dry-run Show what would run
|
|
22
|
+
--help Show this help
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
clawforge multi-review --pr 42
|
|
26
|
+
clawforge multi-review --pr 42 --models "claude-sonnet-4-5,gpt-5.2-codex,claude-opus-4"
|
|
27
|
+
clawforge multi-review --pr 42 --output /tmp/reviews --diff-only
|
|
28
|
+
EOF
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
PR="" REPO="" MODELS="" OUTPUT_DIR="" DIFF_ONLY=false JSON_OUTPUT=false DRY_RUN=false
|
|
32
|
+
|
|
33
|
+
while [[ $# -gt 0 ]]; do
|
|
34
|
+
case "$1" in
|
|
35
|
+
--pr) PR="$2"; shift 2 ;;
|
|
36
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
37
|
+
--models) MODELS="$2"; shift 2 ;;
|
|
38
|
+
--output) OUTPUT_DIR="$2"; shift 2 ;;
|
|
39
|
+
--diff-only) DIFF_ONLY=true; shift ;;
|
|
40
|
+
--json) JSON_OUTPUT=true; shift ;;
|
|
41
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
42
|
+
--help|-h) usage; exit 0 ;;
|
|
43
|
+
--*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
44
|
+
*) shift ;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
[[ -z "$PR" ]] && { log_error "--pr required"; usage; exit 1; }
|
|
49
|
+
|
|
50
|
+
# Resolve repo
|
|
51
|
+
if [[ -z "$REPO" ]]; then
|
|
52
|
+
REPO=$(detect_repo) || { log_error "No repo found"; exit 1; }
|
|
53
|
+
fi
|
|
54
|
+
REPO_ABS=$(cd "$REPO" && pwd)
|
|
55
|
+
|
|
56
|
+
# Resolve models
|
|
57
|
+
if [[ -z "$MODELS" ]]; then
|
|
58
|
+
MODELS=$(config_get review_models "claude-sonnet-4-5,gpt-5.2-codex")
|
|
59
|
+
fi
|
|
60
|
+
IFS=',' read -ra MODEL_LIST <<< "$MODELS"
|
|
61
|
+
MODEL_COUNT=${#MODEL_LIST[@]}
|
|
62
|
+
|
|
63
|
+
log_info "Multi-model review: PR #${PR} with ${MODEL_COUNT} models"
|
|
64
|
+
|
|
65
|
+
# Get PR diff
|
|
66
|
+
PR_DIFF=$(gh pr diff "$PR" --repo "$REPO_ABS" 2>/dev/null || true)
|
|
67
|
+
if [[ -z "$PR_DIFF" ]]; then
|
|
68
|
+
log_error "Could not fetch diff for PR #${PR}"
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
PR_TITLE=$(gh pr view "$PR" --repo "$REPO_ABS" --json title -q '.title' 2>/dev/null || echo "PR #${PR}")
|
|
73
|
+
PR_BODY=$(gh pr view "$PR" --repo "$REPO_ABS" --json body -q '.body' 2>/dev/null || echo "")
|
|
74
|
+
|
|
75
|
+
# Build review prompt
|
|
76
|
+
REVIEW_PROMPT="Review this pull request. Focus on:
|
|
77
|
+
1. Bugs or logic errors
|
|
78
|
+
2. Security issues
|
|
79
|
+
3. Performance concerns
|
|
80
|
+
4. Code style and best practices
|
|
81
|
+
5. Missing edge cases or tests
|
|
82
|
+
|
|
83
|
+
PR: ${PR_TITLE}
|
|
84
|
+
${PR_BODY:+Description: ${PR_BODY}}
|
|
85
|
+
|
|
86
|
+
Diff:
|
|
87
|
+
\`\`\`diff
|
|
88
|
+
$(echo "$PR_DIFF" | head -500)
|
|
89
|
+
\`\`\`
|
|
90
|
+
|
|
91
|
+
Provide a structured review with severity levels (critical/warning/info) for each finding."
|
|
92
|
+
|
|
93
|
+
# Dry run
|
|
94
|
+
if $DRY_RUN; then
|
|
95
|
+
echo "=== Multi-Review Dry Run ==="
|
|
96
|
+
echo " PR: #${PR} — ${PR_TITLE}"
|
|
97
|
+
echo " Repo: $REPO_ABS"
|
|
98
|
+
echo " Models: ${MODELS}"
|
|
99
|
+
echo " Count: $MODEL_COUNT"
|
|
100
|
+
[[ -n "$OUTPUT_DIR" ]] && echo " Output: $OUTPUT_DIR"
|
|
101
|
+
echo ""
|
|
102
|
+
echo "Would run review with each model in parallel."
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Create output dir
|
|
107
|
+
REVIEW_DIR="${OUTPUT_DIR:-$(mktemp -d)}"
|
|
108
|
+
mkdir -p "$REVIEW_DIR"
|
|
109
|
+
|
|
110
|
+
# Run reviews in parallel
|
|
111
|
+
PIDS=()
|
|
112
|
+
for model in "${MODEL_LIST[@]}"; do
|
|
113
|
+
model=$(echo "$model" | xargs) # trim whitespace
|
|
114
|
+
SAFE_MODEL=$(echo "$model" | tr '/' '-' | tr '.' '-')
|
|
115
|
+
OUT_FILE="${REVIEW_DIR}/review-${SAFE_MODEL}.md"
|
|
116
|
+
|
|
117
|
+
log_info "Starting review with $model..."
|
|
118
|
+
|
|
119
|
+
(
|
|
120
|
+
if command -v claude &>/dev/null; then
|
|
121
|
+
claude --model "$model" -p "$REVIEW_PROMPT" > "$OUT_FILE" 2>/dev/null
|
|
122
|
+
else
|
|
123
|
+
echo "Model $model: claude CLI not available" > "$OUT_FILE"
|
|
124
|
+
fi
|
|
125
|
+
) &
|
|
126
|
+
PIDS+=($!)
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
# Wait for all reviews
|
|
130
|
+
FAILED=0
|
|
131
|
+
for i in "${!PIDS[@]}"; do
|
|
132
|
+
if ! wait "${PIDS[$i]}" 2>/dev/null; then
|
|
133
|
+
FAILED=$((FAILED + 1))
|
|
134
|
+
log_warn "Review with ${MODEL_LIST[$i]} failed"
|
|
135
|
+
fi
|
|
136
|
+
done
|
|
137
|
+
|
|
138
|
+
log_info "All reviews complete ($((MODEL_COUNT - FAILED))/$MODEL_COUNT succeeded)"
|
|
139
|
+
|
|
140
|
+
# Collect results
|
|
141
|
+
REVIEWS=()
|
|
142
|
+
for model in "${MODEL_LIST[@]}"; do
|
|
143
|
+
model=$(echo "$model" | xargs)
|
|
144
|
+
SAFE_MODEL=$(echo "$model" | tr '/' '-' | tr '.' '-')
|
|
145
|
+
OUT_FILE="${REVIEW_DIR}/review-${SAFE_MODEL}.md"
|
|
146
|
+
if [[ -f "$OUT_FILE" ]]; then
|
|
147
|
+
REVIEWS+=("$OUT_FILE")
|
|
148
|
+
fi
|
|
149
|
+
done
|
|
150
|
+
|
|
151
|
+
# Generate comparison
|
|
152
|
+
if [[ ${#REVIEWS[@]} -gt 1 ]]; then
|
|
153
|
+
COMPARE_FILE="${REVIEW_DIR}/comparison.md"
|
|
154
|
+
|
|
155
|
+
{
|
|
156
|
+
echo "# Multi-Model Review Comparison"
|
|
157
|
+
echo "PR #${PR}: ${PR_TITLE}"
|
|
158
|
+
echo "Models: ${MODELS}"
|
|
159
|
+
echo "Date: $(date)"
|
|
160
|
+
echo ""
|
|
161
|
+
|
|
162
|
+
for model in "${MODEL_LIST[@]}"; do
|
|
163
|
+
model=$(echo "$model" | xargs)
|
|
164
|
+
SAFE_MODEL=$(echo "$model" | tr '/' '-' | tr '.' '-')
|
|
165
|
+
OUT_FILE="${REVIEW_DIR}/review-${SAFE_MODEL}.md"
|
|
166
|
+
if [[ -f "$OUT_FILE" ]]; then
|
|
167
|
+
echo "---"
|
|
168
|
+
echo "## ${model}"
|
|
169
|
+
echo ""
|
|
170
|
+
cat "$OUT_FILE"
|
|
171
|
+
echo ""
|
|
172
|
+
fi
|
|
173
|
+
done
|
|
174
|
+
|
|
175
|
+
echo "---"
|
|
176
|
+
echo "## Summary"
|
|
177
|
+
echo ""
|
|
178
|
+
echo "| Model | Findings |"
|
|
179
|
+
echo "|-------|----------|"
|
|
180
|
+
for model in "${MODEL_LIST[@]}"; do
|
|
181
|
+
model=$(echo "$model" | xargs)
|
|
182
|
+
SAFE_MODEL=$(echo "$model" | tr '/' '-' | tr '.' '-')
|
|
183
|
+
OUT_FILE="${REVIEW_DIR}/review-${SAFE_MODEL}.md"
|
|
184
|
+
if [[ -f "$OUT_FILE" ]]; then
|
|
185
|
+
FINDING_COUNT=$(grep -ciE "critical|warning|bug|issue|error|concern" "$OUT_FILE" 2>/dev/null || echo "0")
|
|
186
|
+
echo "| $model | ~${FINDING_COUNT} findings |"
|
|
187
|
+
fi
|
|
188
|
+
done
|
|
189
|
+
} > "$COMPARE_FILE"
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
# Output
|
|
193
|
+
if $JSON_OUTPUT; then
|
|
194
|
+
jq -n \
|
|
195
|
+
--arg pr "$PR" \
|
|
196
|
+
--arg title "$PR_TITLE" \
|
|
197
|
+
--arg models "$MODELS" \
|
|
198
|
+
--argjson count "$MODEL_COUNT" \
|
|
199
|
+
--argjson failed "$FAILED" \
|
|
200
|
+
--arg dir "$REVIEW_DIR" \
|
|
201
|
+
'{pr:$pr, title:$title, models:$models, modelCount:$count, failed:$failed, outputDir:$dir}'
|
|
202
|
+
else
|
|
203
|
+
echo ""
|
|
204
|
+
echo " Multi-Model Review: PR #${PR}"
|
|
205
|
+
echo " Models: ${MODELS}"
|
|
206
|
+
echo " Results: $((MODEL_COUNT - FAILED))/$MODEL_COUNT succeeded"
|
|
207
|
+
echo ""
|
|
208
|
+
echo " Reviews saved to: $REVIEW_DIR"
|
|
209
|
+
[[ -f "${REVIEW_DIR}/comparison.md" ]] && echo " Comparison: ${REVIEW_DIR}/comparison.md"
|
|
210
|
+
echo ""
|
|
211
|
+
|
|
212
|
+
if ! $DIFF_ONLY && [[ -f "${REVIEW_DIR}/comparison.md" ]]; then
|
|
213
|
+
cat "${REVIEW_DIR}/comparison.md"
|
|
214
|
+
fi
|
|
215
|
+
fi
|
package/bin/notify.sh
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# notify.sh — Module 6: Send Discord notifications via openclaw
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
source "${SCRIPT_DIR}/../lib/common.sh"
|
|
7
|
+
|
|
8
|
+
# ── Help ───────────────────────────────────────────────────────────────
|
|
9
|
+
usage() {
|
|
10
|
+
cat <<EOF
|
|
11
|
+
Usage: notify.sh [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--channel <id> Discord channel target (default: from config)
|
|
15
|
+
--message <text> Raw message text
|
|
16
|
+
--type <type> Notification type: task-started, pr-ready, task-failed, task-done
|
|
17
|
+
--task-id <id> Task ID to look up details from registry
|
|
18
|
+
--description <d> Description (used with --type if no --task-id)
|
|
19
|
+
--pr <number> PR number (used with pr-ready type)
|
|
20
|
+
--retry <n/m> Retry count (used with task-failed type)
|
|
21
|
+
--dry-run Show command without executing
|
|
22
|
+
--help Show this help
|
|
23
|
+
EOF
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# ── Parse args ─────────────────────────────────────────────────────────
|
|
27
|
+
CHANNEL="" MESSAGE="" TYPE="" TASK_ID="" DESCRIPTION="" PR_NUM="" RETRY="" DRY_RUN=false
|
|
28
|
+
|
|
29
|
+
while [[ $# -gt 0 ]]; do
|
|
30
|
+
case "$1" in
|
|
31
|
+
--channel) CHANNEL="$2"; shift 2 ;;
|
|
32
|
+
--message) MESSAGE="$2"; shift 2 ;;
|
|
33
|
+
--type) TYPE="$2"; shift 2 ;;
|
|
34
|
+
--task-id) TASK_ID="$2"; shift 2 ;;
|
|
35
|
+
--description) DESCRIPTION="$2"; shift 2 ;;
|
|
36
|
+
--pr) PR_NUM="$2"; shift 2 ;;
|
|
37
|
+
--retry) RETRY="$2"; shift 2 ;;
|
|
38
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
39
|
+
--help|-h) usage; exit 0 ;;
|
|
40
|
+
*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
41
|
+
esac
|
|
42
|
+
done
|
|
43
|
+
|
|
44
|
+
# ── Resolve channel ──────────────────────────────────────────────────
|
|
45
|
+
if [[ -z "$CHANNEL" ]]; then
|
|
46
|
+
CHANNEL=$(config_get "notify.defaultChannel" "channel:1476433491452498000")
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# ── Resolve task details from registry ────────────────────────────────
|
|
50
|
+
if [[ -n "$TASK_ID" ]]; then
|
|
51
|
+
TASK_DATA=$(registry_get "$TASK_ID")
|
|
52
|
+
if [[ -n "$TASK_DATA" ]]; then
|
|
53
|
+
[[ -z "$DESCRIPTION" ]] && DESCRIPTION=$(echo "$TASK_DATA" | jq -r '.description // ""')
|
|
54
|
+
[[ -z "$PR_NUM" ]] && PR_NUM=$(echo "$TASK_DATA" | jq -r '.pr // empty' 2>/dev/null || true)
|
|
55
|
+
else
|
|
56
|
+
log_warn "Task '$TASK_ID' not found in registry"
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# ── Build message from type ──────────────────────────────────────────
|
|
61
|
+
if [[ -n "$TYPE" && -z "$MESSAGE" ]]; then
|
|
62
|
+
DESC="${DESCRIPTION:-unknown task}"
|
|
63
|
+
case "$TYPE" in
|
|
64
|
+
task-started) MESSAGE="🔧 Agent spawned for: ${DESC}" ;;
|
|
65
|
+
pr-ready) MESSAGE="✅ PR #${PR_NUM:-?} ready for review: ${DESC}" ;;
|
|
66
|
+
task-failed) MESSAGE="❌ Task failed: ${DESC} (retry ${RETRY:-?/?})" ;;
|
|
67
|
+
task-done) MESSAGE="🎉 Task complete: ${DESC}" ;;
|
|
68
|
+
*) log_error "Unknown notification type: $TYPE"; exit 1 ;;
|
|
69
|
+
esac
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
if [[ -z "$MESSAGE" ]]; then
|
|
73
|
+
log_error "No message to send. Use --message or --type"
|
|
74
|
+
usage
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# ── Send ─────────────────────────────────────────────────────────────
|
|
79
|
+
CMD="openclaw message send --channel discord --target ${CHANNEL} --message \"${MESSAGE}\""
|
|
80
|
+
|
|
81
|
+
if $DRY_RUN; then
|
|
82
|
+
echo "[dry-run] Would execute:"
|
|
83
|
+
echo " $CMD"
|
|
84
|
+
exit 0
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
log_info "Sending notification to $CHANNEL"
|
|
88
|
+
openclaw message send --channel discord --target "$CHANNEL" --message "$MESSAGE" 2>/dev/null || {
|
|
89
|
+
log_error "Failed to send notification"
|
|
90
|
+
log_error "Command was: $CMD"
|
|
91
|
+
exit 1
|
|
92
|
+
}
|
|
93
|
+
log_info "Notification sent"
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# on-complete.sh — Fire completion hooks when a task finishes
|
|
3
|
+
# Called by watch daemon or manually after task reaches done/failed/timeout
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
source "${SCRIPT_DIR}/../lib/common.sh"
|
|
8
|
+
|
|
9
|
+
usage() {
|
|
10
|
+
cat <<EOF
|
|
11
|
+
Usage: clawforge on-complete <id> [options]
|
|
12
|
+
|
|
13
|
+
Fire completion hooks for a finished task. Typically called by watch --daemon.
|
|
14
|
+
|
|
15
|
+
Arguments:
|
|
16
|
+
<id> Task ID or short ID
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--dry-run Show what would fire without executing
|
|
20
|
+
--help Show this help
|
|
21
|
+
|
|
22
|
+
Hooks fired:
|
|
23
|
+
1. OpenClaw event notification (if --notify was set on spawn)
|
|
24
|
+
2. Webhook POST (if --webhook was set on spawn)
|
|
25
|
+
3. Auto-clean (if --auto-clean was set on spawn)
|
|
26
|
+
4. Completion log entry
|
|
27
|
+
EOF
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
TASK_REF="" DRY_RUN=false
|
|
31
|
+
|
|
32
|
+
while [[ $# -gt 0 ]]; do
|
|
33
|
+
case "$1" in
|
|
34
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
35
|
+
--help|-h) usage; exit 0 ;;
|
|
36
|
+
--*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
37
|
+
*) TASK_REF="$1"; shift ;;
|
|
38
|
+
esac
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
[[ -z "$TASK_REF" ]] && { log_error "Task ID required"; usage; exit 1; }
|
|
42
|
+
|
|
43
|
+
_ensure_registry
|
|
44
|
+
|
|
45
|
+
# Resolve task
|
|
46
|
+
TASK_DATA=""
|
|
47
|
+
if [[ "$TASK_REF" =~ ^[0-9]+$ ]]; then
|
|
48
|
+
TASK_DATA=$(jq -r --argjson sid "$TASK_REF" '.tasks[] | select(.short_id == $sid)' "$REGISTRY_FILE" 2>/dev/null || true)
|
|
49
|
+
fi
|
|
50
|
+
if [[ -z "$TASK_DATA" ]]; then
|
|
51
|
+
TASK_DATA=$(registry_get "$TASK_REF" 2>/dev/null || true)
|
|
52
|
+
fi
|
|
53
|
+
if [[ -z "$TASK_DATA" ]]; then
|
|
54
|
+
log_error "Task '$TASK_REF' not found"
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
TASK_ID=$(echo "$TASK_DATA" | jq -r '.id')
|
|
59
|
+
STATUS=$(echo "$TASK_DATA" | jq -r '.status')
|
|
60
|
+
DESC=$(echo "$TASK_DATA" | jq -r '.description // "—"')
|
|
61
|
+
MODE=$(echo "$TASK_DATA" | jq -r '.mode // "—"')
|
|
62
|
+
SHORT_ID=$(echo "$TASK_DATA" | jq -r '.short_id // 0')
|
|
63
|
+
WEBHOOK=$(echo "$TASK_DATA" | jq -r '.webhook // empty')
|
|
64
|
+
NOTIFY=$(echo "$TASK_DATA" | jq -r '.notify // false')
|
|
65
|
+
AUTO_CLEAN=$(echo "$TASK_DATA" | jq -r '.auto_clean // false')
|
|
66
|
+
REPO=$(echo "$TASK_DATA" | jq -r '.repo // ""')
|
|
67
|
+
|
|
68
|
+
# Check task is actually complete
|
|
69
|
+
case "$STATUS" in
|
|
70
|
+
done|failed|timeout|cancelled) ;;
|
|
71
|
+
*)
|
|
72
|
+
log_warn "Task #${SHORT_ID} status is '$STATUS' — not a terminal state. Skipping hooks."
|
|
73
|
+
exit 0
|
|
74
|
+
;;
|
|
75
|
+
esac
|
|
76
|
+
|
|
77
|
+
# Check if hooks already fired
|
|
78
|
+
HOOKS_FIRED=$(echo "$TASK_DATA" | jq -r '.hooks_fired // false')
|
|
79
|
+
if [[ "$HOOKS_FIRED" == "true" ]]; then
|
|
80
|
+
log_info "Hooks already fired for #${SHORT_ID}. Skipping."
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
log_info "Firing completion hooks for #${SHORT_ID} ($STATUS)"
|
|
85
|
+
|
|
86
|
+
# 1. OpenClaw notification
|
|
87
|
+
if [[ "$NOTIFY" == "true" ]]; then
|
|
88
|
+
EMOJI="✅"
|
|
89
|
+
[[ "$STATUS" == "failed" ]] && EMOJI="❌"
|
|
90
|
+
[[ "$STATUS" == "timeout" ]] && EMOJI="⏰"
|
|
91
|
+
[[ "$STATUS" == "cancelled" ]] && EMOJI="🚫"
|
|
92
|
+
MSG="${EMOJI} ClawForge: ${MODE} #${SHORT_ID} ${STATUS} — ${DESC}"
|
|
93
|
+
if $DRY_RUN; then
|
|
94
|
+
echo "[dry-run] Would send OpenClaw event: $MSG"
|
|
95
|
+
else
|
|
96
|
+
openclaw system event --text "$MSG" --mode now 2>/dev/null || log_warn "OpenClaw notify failed"
|
|
97
|
+
log_info "Sent OpenClaw event"
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# 2. Webhook POST
|
|
102
|
+
if [[ -n "$WEBHOOK" ]]; then
|
|
103
|
+
PAYLOAD=$(jq -cn \
|
|
104
|
+
--arg taskId "$TASK_ID" \
|
|
105
|
+
--argjson shortId "$SHORT_ID" \
|
|
106
|
+
--arg mode "$MODE" \
|
|
107
|
+
--arg status "$STATUS" \
|
|
108
|
+
--arg description "$DESC" \
|
|
109
|
+
--arg repo "$REPO" \
|
|
110
|
+
'{event:"task_complete", taskId:$taskId, shortId:$shortId, mode:$mode, status:$status, description:$description, repo:$repo}')
|
|
111
|
+
if $DRY_RUN; then
|
|
112
|
+
echo "[dry-run] Would POST to $WEBHOOK"
|
|
113
|
+
echo " Payload: $PAYLOAD"
|
|
114
|
+
else
|
|
115
|
+
curl -s -X POST -H "Content-Type: application/json" -d "$PAYLOAD" "$WEBHOOK" >/dev/null 2>&1 || log_warn "Webhook POST failed"
|
|
116
|
+
log_info "Sent webhook to $WEBHOOK"
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# 3. Auto-clean
|
|
121
|
+
if [[ "$AUTO_CLEAN" == "true" ]]; then
|
|
122
|
+
if $DRY_RUN; then
|
|
123
|
+
echo "[dry-run] Would auto-clean task #${SHORT_ID}"
|
|
124
|
+
else
|
|
125
|
+
"${SCRIPT_DIR}/clean.sh" --task-id "$TASK_ID" 2>/dev/null || log_warn "Auto-clean failed"
|
|
126
|
+
log_info "Auto-cleaned task #${SHORT_ID}"
|
|
127
|
+
fi
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# 4. Mark hooks as fired
|
|
131
|
+
if ! $DRY_RUN; then
|
|
132
|
+
registry_update "$TASK_ID" "hooks_fired" 'true' 2>/dev/null || true
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# 5. Log completion
|
|
136
|
+
COMPLETION_LOG="${CLAWFORGE_DIR}/registry/completions.jsonl"
|
|
137
|
+
if ! $DRY_RUN; then
|
|
138
|
+
ENTRY=$(jq -cn \
|
|
139
|
+
--arg id "$TASK_ID" \
|
|
140
|
+
--argjson sid "$SHORT_ID" \
|
|
141
|
+
--arg mode "$MODE" \
|
|
142
|
+
--arg status "$STATUS" \
|
|
143
|
+
--arg desc "$DESC" \
|
|
144
|
+
--argjson ts "$(epoch_ms)" \
|
|
145
|
+
'{timestamp:$ts, taskId:$id, shortId:$sid, mode:$mode, status:$status, description:$desc}')
|
|
146
|
+
echo "$ENTRY" >> "$COMPLETION_LOG"
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
echo "Hooks fired for #${SHORT_ID} ($STATUS)"
|