@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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/VERSION +1 -0
  4. package/bin/attach.sh +98 -0
  5. package/bin/check-agents.sh +343 -0
  6. package/bin/clawforge +257 -0
  7. package/bin/clawforge-dashboard +0 -0
  8. package/bin/clean.sh +257 -0
  9. package/bin/config.sh +111 -0
  10. package/bin/conflicts.sh +224 -0
  11. package/bin/cost.sh +273 -0
  12. package/bin/dashboard.sh +557 -0
  13. package/bin/diff.sh +109 -0
  14. package/bin/doctor.sh +196 -0
  15. package/bin/eval.sh +217 -0
  16. package/bin/history.sh +91 -0
  17. package/bin/init.sh +182 -0
  18. package/bin/learn.sh +230 -0
  19. package/bin/logs.sh +126 -0
  20. package/bin/memory.sh +207 -0
  21. package/bin/merge-helper.sh +174 -0
  22. package/bin/multi-review.sh +215 -0
  23. package/bin/notify.sh +93 -0
  24. package/bin/on-complete.sh +149 -0
  25. package/bin/parse-cost.sh +205 -0
  26. package/bin/pr.sh +167 -0
  27. package/bin/resume.sh +183 -0
  28. package/bin/review-mode.sh +163 -0
  29. package/bin/review-pr.sh +145 -0
  30. package/bin/routing.sh +88 -0
  31. package/bin/scope-task.sh +169 -0
  32. package/bin/spawn-agent.sh +190 -0
  33. package/bin/sprint.sh +320 -0
  34. package/bin/steer.sh +107 -0
  35. package/bin/stop.sh +136 -0
  36. package/bin/summary.sh +182 -0
  37. package/bin/swarm.sh +525 -0
  38. package/bin/templates.sh +244 -0
  39. package/lib/common.sh +302 -0
  40. package/lib/templates/bugfix.json +6 -0
  41. package/lib/templates/migration.json +7 -0
  42. package/lib/templates/refactor.json +6 -0
  43. package/lib/templates/security-audit.json +5 -0
  44. package/lib/templates/test-coverage.json +6 -0
  45. package/package.json +31 -0
  46. package/registry/conflicts.jsonl +0 -0
  47. package/registry/costs.jsonl +0 -0
  48. package/tui/PRD.md +106 -0
  49. package/tui/agent.go +266 -0
  50. package/tui/animation.go +192 -0
  51. package/tui/dashboard.go +219 -0
  52. package/tui/filter.go +68 -0
  53. package/tui/go.mod +25 -0
  54. package/tui/go.sum +46 -0
  55. package/tui/keybindings.go +229 -0
  56. package/tui/main.go +61 -0
  57. package/tui/model.go +166 -0
  58. package/tui/steer.go +69 -0
  59. 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)"