@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
package/bin/clean.sh
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# clean.sh — Module 8: Clean up completed tasks (worktrees, tmux, registry)
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
source "${SCRIPT_DIR}/../lib/common.sh"
|
|
7
|
+
|
|
8
|
+
CLEANUP_LOG="${CLAWFORGE_DIR}/registry/cleanup-log.jsonl"
|
|
9
|
+
COMPLETED_TASKS="${CLAWFORGE_DIR}/registry/completed-tasks.jsonl"
|
|
10
|
+
|
|
11
|
+
# ── Help ───────────────────────────────────────────────────────────────
|
|
12
|
+
usage() {
|
|
13
|
+
cat <<EOF
|
|
14
|
+
Usage: clean.sh [options]
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--task-id <id> Clean a specific task
|
|
18
|
+
--all-done Clean all tasks with status "done"
|
|
19
|
+
--stale-days <n> Clean tasks older than N days
|
|
20
|
+
--force Allow cleaning running tasks
|
|
21
|
+
--dry-run Show what would be cleaned without doing it
|
|
22
|
+
--prune-days <n> Remove archived tasks older than N days from registry
|
|
23
|
+
--keep-branch Skip branch deletion after cleaning
|
|
24
|
+
--help Show this help
|
|
25
|
+
EOF
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# ── Parse args ─────────────────────────────────────────────────────────
|
|
29
|
+
TASK_ID="" ALL_DONE=false STALE_DAYS="" FORCE=false DRY_RUN=false PRUNE_DAYS="" KEEP_BRANCH=false
|
|
30
|
+
|
|
31
|
+
while [[ $# -gt 0 ]]; do
|
|
32
|
+
case "$1" in
|
|
33
|
+
--task-id) TASK_ID="$2"; shift 2 ;;
|
|
34
|
+
--all-done) ALL_DONE=true; shift ;;
|
|
35
|
+
--stale-days) STALE_DAYS="$2"; shift 2 ;;
|
|
36
|
+
--force) FORCE=true; shift ;;
|
|
37
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
38
|
+
--prune-days) PRUNE_DAYS="$2"; shift 2 ;;
|
|
39
|
+
--keep-branch) KEEP_BRANCH=true; shift ;;
|
|
40
|
+
--help|-h) usage; exit 0 ;;
|
|
41
|
+
*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
if [[ -z "$TASK_ID" ]] && ! $ALL_DONE && [[ -z "$STALE_DAYS" ]] && [[ -z "$PRUNE_DAYS" ]]; then
|
|
46
|
+
log_error "Specify --task-id, --all-done, or --stale-days"
|
|
47
|
+
usage
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
_ensure_registry
|
|
52
|
+
mkdir -p "$(dirname "$CLEANUP_LOG")"
|
|
53
|
+
|
|
54
|
+
# ── Clean one task ────────────────────────────────────────────────────
|
|
55
|
+
clean_task() {
|
|
56
|
+
local id="$1"
|
|
57
|
+
local task_data
|
|
58
|
+
task_data=$(registry_get "$id")
|
|
59
|
+
|
|
60
|
+
if [[ -z "$task_data" ]]; then
|
|
61
|
+
log_warn "Task '$id' not found in registry"
|
|
62
|
+
return 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
local status worktree tmux_session repo
|
|
66
|
+
status=$(echo "$task_data" | jq -r '.status')
|
|
67
|
+
worktree=$(echo "$task_data" | jq -r '.worktree // empty')
|
|
68
|
+
tmux_session=$(echo "$task_data" | jq -r '.tmuxSession // empty')
|
|
69
|
+
repo=$(echo "$task_data" | jq -r '.repo // empty')
|
|
70
|
+
|
|
71
|
+
# Safety check
|
|
72
|
+
if [[ "$status" == "running" || "$status" == "spawned" ]] && ! $FORCE; then
|
|
73
|
+
log_warn "Skipping '$id' — status is '$status' (use --force to override)"
|
|
74
|
+
return 1
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
local cleaned_items=()
|
|
78
|
+
|
|
79
|
+
# Kill tmux session
|
|
80
|
+
if [[ -n "$tmux_session" ]]; then
|
|
81
|
+
if tmux has-session -t "$tmux_session" 2>/dev/null; then
|
|
82
|
+
if $DRY_RUN; then
|
|
83
|
+
echo "[dry-run] Would kill tmux session: $tmux_session"
|
|
84
|
+
else
|
|
85
|
+
tmux kill-session -t "$tmux_session" 2>/dev/null || true
|
|
86
|
+
log_info "Killed tmux session: $tmux_session"
|
|
87
|
+
fi
|
|
88
|
+
cleaned_items+=("tmux:$tmux_session")
|
|
89
|
+
fi
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Remove worktree
|
|
93
|
+
if [[ -n "$worktree" && -d "$worktree" ]]; then
|
|
94
|
+
if $DRY_RUN; then
|
|
95
|
+
echo "[dry-run] Would remove worktree: $worktree"
|
|
96
|
+
else
|
|
97
|
+
if [[ -n "$repo" && -d "$repo" ]]; then
|
|
98
|
+
git -C "$repo" worktree remove "$worktree" --force 2>/dev/null || {
|
|
99
|
+
log_warn "git worktree remove failed, removing directory"
|
|
100
|
+
rm -rf "$worktree"
|
|
101
|
+
}
|
|
102
|
+
else
|
|
103
|
+
rm -rf "$worktree"
|
|
104
|
+
fi
|
|
105
|
+
log_info "Removed worktree: $worktree"
|
|
106
|
+
fi
|
|
107
|
+
cleaned_items+=("worktree:$worktree")
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Delete merged branch
|
|
112
|
+
if ! $KEEP_BRANCH; then
|
|
113
|
+
local branch
|
|
114
|
+
branch=$(echo "$task_data" | jq -r '.branch // empty')
|
|
115
|
+
if [[ -n "$branch" && -n "$repo" && -d "$repo" ]]; then
|
|
116
|
+
# Only delete if merged into main/master
|
|
117
|
+
local main_branch
|
|
118
|
+
main_branch=$(git -C "$repo" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||' || echo "main")
|
|
119
|
+
if git -C "$repo" branch --merged "$main_branch" 2>/dev/null | grep -qF "$branch"; then
|
|
120
|
+
if $DRY_RUN; then
|
|
121
|
+
echo "[dry-run] Would delete merged branch: $branch"
|
|
122
|
+
else
|
|
123
|
+
git -C "$repo" branch -d "$branch" 2>/dev/null || true
|
|
124
|
+
log_info "Deleted merged branch: $branch"
|
|
125
|
+
fi
|
|
126
|
+
cleaned_items+=("branch:$branch")
|
|
127
|
+
fi
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# Update registry (archive instead of remove)
|
|
132
|
+
if $DRY_RUN; then
|
|
133
|
+
echo "[dry-run] Would archive task: $id"
|
|
134
|
+
else
|
|
135
|
+
registry_update "$id" "status" '"archived"'
|
|
136
|
+
registry_update "$id" "cleanedAt" "$(epoch_ms)"
|
|
137
|
+
log_info "Archived task: $id"
|
|
138
|
+
fi
|
|
139
|
+
cleaned_items+=("registry:$id")
|
|
140
|
+
|
|
141
|
+
# Log cleanup
|
|
142
|
+
if ! $DRY_RUN; then
|
|
143
|
+
local log_entry
|
|
144
|
+
log_entry=$(jq -cn \
|
|
145
|
+
--arg id "$id" \
|
|
146
|
+
--arg status "$status" \
|
|
147
|
+
--argjson timestamp "$(epoch_ms)" \
|
|
148
|
+
--argjson items "$(printf '%s\n' "${cleaned_items[@]}" | jq -R . | jq -s .)" \
|
|
149
|
+
'{timestamp: $timestamp, taskId: $id, previousStatus: $status, cleaned: $items}')
|
|
150
|
+
echo "$log_entry" >> "$CLEANUP_LOG"
|
|
151
|
+
|
|
152
|
+
# Append to completed-tasks history
|
|
153
|
+
local desc mode agent model started_at completed_at dur_min cost pr
|
|
154
|
+
desc=$(echo "$task_data" | jq -r '.description // "—"')
|
|
155
|
+
mode=$(echo "$task_data" | jq -r '.mode // "—"')
|
|
156
|
+
agent=$(echo "$task_data" | jq -r '.agent // "—"')
|
|
157
|
+
model=$(echo "$task_data" | jq -r '.model // "—"')
|
|
158
|
+
started_at=$(echo "$task_data" | jq -r '.startedAt // 0')
|
|
159
|
+
completed_at=$(echo "$task_data" | jq -r '.completedAt // 0')
|
|
160
|
+
dur_min=0
|
|
161
|
+
if [[ "$started_at" -gt 0 && "$completed_at" -gt 0 ]]; then
|
|
162
|
+
dur_min=$(( (completed_at - started_at) / 60000 ))
|
|
163
|
+
fi
|
|
164
|
+
cost=$(echo "$task_data" | jq -r '.cost // null')
|
|
165
|
+
pr=$(echo "$task_data" | jq -r '.pr // null')
|
|
166
|
+
|
|
167
|
+
local hist_entry
|
|
168
|
+
hist_entry=$(jq -cn \
|
|
169
|
+
--arg id "$id" \
|
|
170
|
+
--arg description "$desc" \
|
|
171
|
+
--arg mode "$mode" \
|
|
172
|
+
--arg status "$status" \
|
|
173
|
+
--arg agent "$agent" \
|
|
174
|
+
--arg model "$model" \
|
|
175
|
+
--arg repo "$(echo "$task_data" | jq -r '.repo // ""')" \
|
|
176
|
+
--argjson duration_minutes "$dur_min" \
|
|
177
|
+
--argjson completedAt "$(epoch_ms)" \
|
|
178
|
+
--argjson timestamp "$(epoch_ms)" \
|
|
179
|
+
'{id:$id, description:$description, mode:$mode, status:$status, agent:$agent, model:$model, repo:$repo, duration_minutes:$duration_minutes, completedAt:$completedAt, timestamp:$timestamp}')
|
|
180
|
+
# Add cost and pr only if they exist
|
|
181
|
+
[[ "$cost" != "null" ]] && hist_entry=$(echo "$hist_entry" | jq --arg c "$cost" '. + {cost:$c}')
|
|
182
|
+
[[ "$pr" != "null" ]] && hist_entry=$(echo "$hist_entry" | jq --arg p "$pr" '. + {pr:$p}')
|
|
183
|
+
echo "$hist_entry" >> "$COMPLETED_TASKS"
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
echo "Cleaned: $id (${cleaned_items[*]})"
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# ── Execute ──────────────────────────────────────────────────────────
|
|
190
|
+
CLEANED=0
|
|
191
|
+
|
|
192
|
+
if [[ -n "$TASK_ID" ]]; then
|
|
193
|
+
clean_task "$TASK_ID" && CLEANED=$((CLEANED + 1))
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
if $ALL_DONE; then
|
|
197
|
+
DONE_IDS=$(jq -r '.tasks[] | select(.status == "done") | .id' "$REGISTRY_FILE" 2>/dev/null || true)
|
|
198
|
+
if [[ -n "$DONE_IDS" ]]; then
|
|
199
|
+
while IFS= read -r id; do
|
|
200
|
+
clean_task "$id" && CLEANED=$((CLEANED + 1)) || true
|
|
201
|
+
done <<< "$DONE_IDS"
|
|
202
|
+
else
|
|
203
|
+
log_info "No tasks with status 'done'"
|
|
204
|
+
fi
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
if [[ -n "$STALE_DAYS" ]]; then
|
|
208
|
+
NOW_MS=$(epoch_ms)
|
|
209
|
+
STALE_MS=$((STALE_DAYS * 86400 * 1000))
|
|
210
|
+
CUTOFF=$((NOW_MS - STALE_MS))
|
|
211
|
+
|
|
212
|
+
STALE_IDS=$(jq -r --argjson cutoff "$CUTOFF" \
|
|
213
|
+
'.tasks[] | select(.startedAt < $cutoff and .status != "running" and .status != "spawned") | .id' \
|
|
214
|
+
"$REGISTRY_FILE" 2>/dev/null || true)
|
|
215
|
+
|
|
216
|
+
if $FORCE; then
|
|
217
|
+
STALE_IDS=$(jq -r --argjson cutoff "$CUTOFF" \
|
|
218
|
+
'.tasks[] | select(.startedAt < $cutoff) | .id' \
|
|
219
|
+
"$REGISTRY_FILE" 2>/dev/null || true)
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
if [[ -n "$STALE_IDS" ]]; then
|
|
223
|
+
while IFS= read -r id; do
|
|
224
|
+
clean_task "$id" && CLEANED=$((CLEANED + 1)) || true
|
|
225
|
+
done <<< "$STALE_IDS"
|
|
226
|
+
else
|
|
227
|
+
log_info "No stale tasks older than $STALE_DAYS days"
|
|
228
|
+
fi
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
echo ""
|
|
232
|
+
echo "Total cleaned: $CLEANED"
|
|
233
|
+
|
|
234
|
+
# ── Registry pruning ────────────────────────────────────────────────
|
|
235
|
+
if [[ -n "$PRUNE_DAYS" ]]; then
|
|
236
|
+
NOW_MS=$(epoch_ms)
|
|
237
|
+
PRUNE_MS=$((PRUNE_DAYS * 86400 * 1000))
|
|
238
|
+
CUTOFF=$((NOW_MS - PRUNE_MS))
|
|
239
|
+
|
|
240
|
+
ARCHIVED_IDS=$(jq -r --argjson cutoff "$CUTOFF" '.tasks[] | select(.status == "archived" and (.cleanedAt // 0) < $cutoff) | .id' "$REGISTRY_FILE" 2>/dev/null || true)
|
|
241
|
+
|
|
242
|
+
if [[ -n "$ARCHIVED_IDS" ]]; then
|
|
243
|
+
local pruned=0
|
|
244
|
+
while IFS= read -r id; do
|
|
245
|
+
if $DRY_RUN; then
|
|
246
|
+
echo "[dry-run] Would prune archived task: $id"
|
|
247
|
+
else
|
|
248
|
+
registry_remove "$id"
|
|
249
|
+
log_info "Pruned archived task: $id"
|
|
250
|
+
fi
|
|
251
|
+
pruned=$((pruned + 1))
|
|
252
|
+
done <<< "$ARCHIVED_IDS"
|
|
253
|
+
echo "Pruned: $pruned archived tasks older than $PRUNE_DAYS days"
|
|
254
|
+
else
|
|
255
|
+
log_info "No archived tasks older than $PRUNE_DAYS days to prune"
|
|
256
|
+
fi
|
|
257
|
+
fi
|
package/bin/config.sh
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# config.sh — Manage ClawForge user configuration
|
|
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 config <subcommand> [args]
|
|
11
|
+
|
|
12
|
+
Manage user configuration at ~/.clawforge/config.json.
|
|
13
|
+
User config overrides project defaults.
|
|
14
|
+
|
|
15
|
+
Subcommands:
|
|
16
|
+
show Show all config (user + defaults)
|
|
17
|
+
get <key> Get a config value
|
|
18
|
+
set <key> <value> Set a config value
|
|
19
|
+
unset <key> Remove a config key
|
|
20
|
+
init Create default user config with common settings
|
|
21
|
+
path Show config file path
|
|
22
|
+
--help Show this help
|
|
23
|
+
|
|
24
|
+
Common config keys:
|
|
25
|
+
default_agent Default agent: claude or codex
|
|
26
|
+
default_model_claude Default Claude model
|
|
27
|
+
default_model_codex Default Codex model
|
|
28
|
+
auto_clean Auto-clean on completion (true/false)
|
|
29
|
+
default_timeout Default timeout in minutes
|
|
30
|
+
max_agents Default max parallel agents for swarm
|
|
31
|
+
routing Default routing strategy (auto/cheap/quality)
|
|
32
|
+
review_models Comma-separated models for multi-model review
|
|
33
|
+
disk_warn_gb Disk space warning threshold
|
|
34
|
+
disk_error_gb Disk space error threshold
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
clawforge config show
|
|
38
|
+
clawforge config set default_agent claude
|
|
39
|
+
clawforge config set auto_clean true
|
|
40
|
+
clawforge config set default_timeout 30
|
|
41
|
+
clawforge config set review_models "claude-sonnet-4-5,gpt-5.2-codex,claude-opus-4"
|
|
42
|
+
clawforge config init
|
|
43
|
+
EOF
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
[[ $# -eq 0 ]] && { usage; exit 0; }
|
|
47
|
+
|
|
48
|
+
case "$1" in
|
|
49
|
+
show)
|
|
50
|
+
config_list
|
|
51
|
+
;;
|
|
52
|
+
get)
|
|
53
|
+
[[ -z "${2:-}" ]] && { log_error "Key required"; exit 1; }
|
|
54
|
+
val=$(config_get "$2" "")
|
|
55
|
+
if [[ -n "$val" ]]; then
|
|
56
|
+
echo "$val"
|
|
57
|
+
else
|
|
58
|
+
echo "(not set)"
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
;;
|
|
62
|
+
set)
|
|
63
|
+
[[ -z "${2:-}" || -z "${3:-}" ]] && { log_error "Key and value required"; exit 1; }
|
|
64
|
+
config_set "$2" "$3"
|
|
65
|
+
echo "Set $2 = $3"
|
|
66
|
+
;;
|
|
67
|
+
unset)
|
|
68
|
+
[[ -z "${2:-}" ]] && { log_error "Key required"; exit 1; }
|
|
69
|
+
mkdir -p "$(dirname "$USER_CONFIG_FILE")"
|
|
70
|
+
if [[ -f "$USER_CONFIG_FILE" ]]; then
|
|
71
|
+
tmp=$(mktemp)
|
|
72
|
+
jq --arg k "$2" 'del(.[$k])' "$USER_CONFIG_FILE" > "$tmp" && mv "$tmp" "$USER_CONFIG_FILE"
|
|
73
|
+
echo "Unset $2"
|
|
74
|
+
fi
|
|
75
|
+
;;
|
|
76
|
+
init)
|
|
77
|
+
mkdir -p "$(dirname "$USER_CONFIG_FILE")"
|
|
78
|
+
if [[ -f "$USER_CONFIG_FILE" ]]; then
|
|
79
|
+
echo "Config already exists at $USER_CONFIG_FILE"
|
|
80
|
+
echo "Use 'clawforge config set' to modify individual values."
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
cat > "$USER_CONFIG_FILE" << 'INITJSON'
|
|
84
|
+
{
|
|
85
|
+
"default_agent": "claude",
|
|
86
|
+
"default_model_claude": "claude-sonnet-4-5",
|
|
87
|
+
"default_model_codex": "gpt-5.3-codex",
|
|
88
|
+
"auto_clean": "false",
|
|
89
|
+
"default_timeout": "",
|
|
90
|
+
"max_agents": "3",
|
|
91
|
+
"routing": "",
|
|
92
|
+
"review_models": "claude-sonnet-4-5,gpt-5.2-codex",
|
|
93
|
+
"disk_warn_gb": "5",
|
|
94
|
+
"disk_error_gb": "1"
|
|
95
|
+
}
|
|
96
|
+
INITJSON
|
|
97
|
+
echo "Created config at $USER_CONFIG_FILE"
|
|
98
|
+
echo "Edit with: clawforge config set <key> <value>"
|
|
99
|
+
;;
|
|
100
|
+
path)
|
|
101
|
+
echo "$USER_CONFIG_FILE"
|
|
102
|
+
;;
|
|
103
|
+
--help|-h)
|
|
104
|
+
usage
|
|
105
|
+
;;
|
|
106
|
+
*)
|
|
107
|
+
log_error "Unknown subcommand: $1"
|
|
108
|
+
usage
|
|
109
|
+
exit 1
|
|
110
|
+
;;
|
|
111
|
+
esac
|
package/bin/conflicts.sh
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# conflicts.sh — Swarm conflict resolution: detect overlapping file changes
|
|
3
|
+
# Usage: clawforge conflicts [--json]
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
source "${SCRIPT_DIR}/../lib/common.sh"
|
|
8
|
+
|
|
9
|
+
CONFLICTS_FILE="${CLAWFORGE_DIR}/registry/conflicts.jsonl"
|
|
10
|
+
|
|
11
|
+
# ── Help ───────────────────────────────────────────────────────────────
|
|
12
|
+
usage() {
|
|
13
|
+
cat <<EOF
|
|
14
|
+
Usage: clawforge conflicts [options]
|
|
15
|
+
|
|
16
|
+
Show and manage swarm file conflicts.
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
clawforge conflicts Show current/recent conflicts
|
|
20
|
+
clawforge conflicts --check Run conflict detection now
|
|
21
|
+
clawforge conflicts --resolve Spawn coordinator to resolve conflicts
|
|
22
|
+
|
|
23
|
+
Flags:
|
|
24
|
+
--check Run conflict detection across active worktrees
|
|
25
|
+
--resolve Spawn a coordinator agent to merge conflicting changes
|
|
26
|
+
--json Output as JSON
|
|
27
|
+
--help Show this help
|
|
28
|
+
EOF
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# ── Parse args ─────────────────────────────────────────────────────────
|
|
32
|
+
CHECK=false RESOLVE=false JSON_OUTPUT=false
|
|
33
|
+
|
|
34
|
+
while [[ $# -gt 0 ]]; do
|
|
35
|
+
case "$1" in
|
|
36
|
+
--check) CHECK=true; shift ;;
|
|
37
|
+
--resolve) RESOLVE=true; shift ;;
|
|
38
|
+
--json) JSON_OUTPUT=true; shift ;;
|
|
39
|
+
--help|-h) usage; exit 0 ;;
|
|
40
|
+
--*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
41
|
+
*) shift ;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
mkdir -p "$(dirname "$CONFLICTS_FILE")"
|
|
46
|
+
touch "$CONFLICTS_FILE"
|
|
47
|
+
|
|
48
|
+
# ── Conflict detection ─────────────────────────────────────────────────
|
|
49
|
+
_detect_conflicts() {
|
|
50
|
+
_ensure_registry
|
|
51
|
+
local running_tasks
|
|
52
|
+
running_tasks=$(jq -c '[.tasks[] | select(
|
|
53
|
+
(.status == "running" or .status == "spawned" or .status == "pr-created") and
|
|
54
|
+
.worktree != "" and .worktree != null
|
|
55
|
+
)]' "$REGISTRY_FILE" 2>/dev/null || echo "[]")
|
|
56
|
+
|
|
57
|
+
local task_count
|
|
58
|
+
task_count=$(echo "$running_tasks" | jq 'length' 2>/dev/null || echo 0)
|
|
59
|
+
|
|
60
|
+
if [[ "$task_count" -lt 2 ]]; then
|
|
61
|
+
if $JSON_OUTPUT; then
|
|
62
|
+
echo '{"conflicts":[],"message":"Need at least 2 active agents for conflict detection"}'
|
|
63
|
+
else
|
|
64
|
+
echo "No conflicts possible (fewer than 2 active agents)."
|
|
65
|
+
fi
|
|
66
|
+
return
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Collect changed files per agent
|
|
70
|
+
local agent_files=()
|
|
71
|
+
local agent_ids=()
|
|
72
|
+
local i=0
|
|
73
|
+
while IFS= read -r task; do
|
|
74
|
+
local id worktree branch repo
|
|
75
|
+
id=$(echo "$task" | jq -r '.id')
|
|
76
|
+
worktree=$(echo "$task" | jq -r '.worktree')
|
|
77
|
+
branch=$(echo "$task" | jq -r '.branch')
|
|
78
|
+
repo=$(echo "$task" | jq -r '.repo')
|
|
79
|
+
|
|
80
|
+
local files=""
|
|
81
|
+
if [[ -d "$worktree" ]]; then
|
|
82
|
+
# Get changed files in worktree relative to base
|
|
83
|
+
files=$(git -C "$worktree" diff --name-only HEAD~1 2>/dev/null || \
|
|
84
|
+
git -C "$worktree" diff --name-only 2>/dev/null || true)
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Also check files_touched from registry
|
|
88
|
+
local reg_files
|
|
89
|
+
reg_files=$(echo "$task" | jq -r '.files_touched // [] | .[]' 2>/dev/null || true)
|
|
90
|
+
if [[ -n "$reg_files" ]]; then
|
|
91
|
+
files=$(printf "%s\n%s" "$files" "$reg_files" | sort -u)
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
agent_files[i]="$files"
|
|
95
|
+
agent_ids[i]="$id"
|
|
96
|
+
((i++)) || true
|
|
97
|
+
done < <(echo "$running_tasks" | jq -c '.[]' 2>/dev/null)
|
|
98
|
+
|
|
99
|
+
# Compare file lists between all agent pairs
|
|
100
|
+
local conflict_count=0
|
|
101
|
+
local now
|
|
102
|
+
now=$(epoch_ms)
|
|
103
|
+
|
|
104
|
+
for ((a=0; a<i; a++)); do
|
|
105
|
+
for ((b=a+1; b<i; b++)); do
|
|
106
|
+
local overlap
|
|
107
|
+
overlap=$(comm -12 <(echo "${agent_files[a]}" | sort) <(echo "${agent_files[b]}" | sort) 2>/dev/null || true)
|
|
108
|
+
if [[ -n "$overlap" ]]; then
|
|
109
|
+
local overlap_json
|
|
110
|
+
overlap_json=$(echo "$overlap" | jq -R . | jq -s .)
|
|
111
|
+
local conflict_entry
|
|
112
|
+
conflict_entry=$(jq -cn \
|
|
113
|
+
--arg agent1 "${agent_ids[a]}" \
|
|
114
|
+
--arg agent2 "${agent_ids[b]}" \
|
|
115
|
+
--argjson files "$overlap_json" \
|
|
116
|
+
--argjson timestamp "$now" \
|
|
117
|
+
--arg status "detected" \
|
|
118
|
+
'{
|
|
119
|
+
agent1: $agent1,
|
|
120
|
+
agent2: $agent2,
|
|
121
|
+
overlapping_files: $files,
|
|
122
|
+
timestamp: $timestamp,
|
|
123
|
+
status: $status
|
|
124
|
+
}')
|
|
125
|
+
echo "$conflict_entry" >> "$CONFLICTS_FILE"
|
|
126
|
+
((conflict_count++)) || true
|
|
127
|
+
|
|
128
|
+
if ! $JSON_OUTPUT; then
|
|
129
|
+
echo " ⚠ Conflict: ${agent_ids[a]} ↔ ${agent_ids[b]}"
|
|
130
|
+
echo "$overlap" | sed 's/^/ /'
|
|
131
|
+
echo ""
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# Update registry with conflict info
|
|
135
|
+
registry_update "${agent_ids[a]}" "has_conflict" 'true' 2>/dev/null || true
|
|
136
|
+
registry_update "${agent_ids[b]}" "has_conflict" 'true' 2>/dev/null || true
|
|
137
|
+
fi
|
|
138
|
+
done
|
|
139
|
+
done
|
|
140
|
+
|
|
141
|
+
if $JSON_OUTPUT; then
|
|
142
|
+
local recent
|
|
143
|
+
recent=$(tail -20 "$CONFLICTS_FILE" | jq -s '.' 2>/dev/null || echo "[]")
|
|
144
|
+
echo "$recent" | jq --argjson count "$conflict_count" '{conflicts: ., new_conflicts: $count}'
|
|
145
|
+
elif [[ "$conflict_count" -eq 0 ]]; then
|
|
146
|
+
echo "No file conflicts detected."
|
|
147
|
+
else
|
|
148
|
+
echo "Detected $conflict_count conflict(s)."
|
|
149
|
+
fi
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# ── Show conflicts ─────────────────────────────────────────────────────
|
|
153
|
+
_show_conflicts() {
|
|
154
|
+
if [[ ! -s "$CONFLICTS_FILE" ]]; then
|
|
155
|
+
if $JSON_OUTPUT; then
|
|
156
|
+
echo '{"conflicts":[]}'
|
|
157
|
+
else
|
|
158
|
+
echo "No conflicts recorded."
|
|
159
|
+
fi
|
|
160
|
+
return
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
if $JSON_OUTPUT; then
|
|
164
|
+
cat "$CONFLICTS_FILE" | jq -s '{
|
|
165
|
+
conflicts: .,
|
|
166
|
+
total: length,
|
|
167
|
+
detected: [.[] | select(.status == "detected")] | length,
|
|
168
|
+
resolved: [.[] | select(.status == "resolved")] | length
|
|
169
|
+
}'
|
|
170
|
+
return
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
echo "=== Swarm Conflicts ==="
|
|
174
|
+
echo ""
|
|
175
|
+
|
|
176
|
+
local total detected resolved
|
|
177
|
+
total=$(wc -l < "$CONFLICTS_FILE" | tr -d ' ')
|
|
178
|
+
detected=$(grep -c '"detected"' "$CONFLICTS_FILE" 2>/dev/null || echo 0)
|
|
179
|
+
resolved=$(grep -c '"resolved"' "$CONFLICTS_FILE" 2>/dev/null || echo 0)
|
|
180
|
+
|
|
181
|
+
echo " Total: $total | Detected: $detected | Resolved: $resolved"
|
|
182
|
+
echo ""
|
|
183
|
+
|
|
184
|
+
echo " Recent conflicts:"
|
|
185
|
+
tail -10 "$CONFLICTS_FILE" | jq -r '" [\(.timestamp | . / 1000 | strftime("%H:%M:%S"))] \(.agent1) ↔ \(.agent2) [\(.status)] — \(.overlapping_files | length) files"' 2>/dev/null || true
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# ── Resolve conflicts ──────────────────────────────────────────────────
|
|
189
|
+
_resolve_conflicts() {
|
|
190
|
+
local unresolved
|
|
191
|
+
unresolved=$(grep '"detected"' "$CONFLICTS_FILE" 2>/dev/null | tail -1 || true)
|
|
192
|
+
|
|
193
|
+
if [[ -z "$unresolved" ]]; then
|
|
194
|
+
echo "No unresolved conflicts to resolve."
|
|
195
|
+
return
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
local agent1 agent2
|
|
199
|
+
agent1=$(echo "$unresolved" | jq -r '.agent1')
|
|
200
|
+
agent2=$(echo "$unresolved" | jq -r '.agent2')
|
|
201
|
+
local files
|
|
202
|
+
files=$(echo "$unresolved" | jq -r '.overlapping_files | join(", ")')
|
|
203
|
+
|
|
204
|
+
echo "Resolving conflict between $agent1 and $agent2"
|
|
205
|
+
echo "Overlapping files: $files"
|
|
206
|
+
echo ""
|
|
207
|
+
echo "Note: Automatic coordinator agent spawning requires both agents to be completed."
|
|
208
|
+
echo "Use 'clawforge steer' to manually coordinate or wait for completion."
|
|
209
|
+
|
|
210
|
+
# Mark as being resolved
|
|
211
|
+
local tmp
|
|
212
|
+
tmp=$(mktemp)
|
|
213
|
+
sed "s/\"agent1\":\"${agent1}\",\"agent2\":\"${agent2}\".*\"status\":\"detected\"/&/" "$CONFLICTS_FILE" > "$tmp"
|
|
214
|
+
mv "$tmp" "$CONFLICTS_FILE"
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# ── Route ──────────────────────────────────────────────────────────────
|
|
218
|
+
if $CHECK; then
|
|
219
|
+
_detect_conflicts
|
|
220
|
+
elif $RESOLVE; then
|
|
221
|
+
_resolve_conflicts
|
|
222
|
+
else
|
|
223
|
+
_show_conflicts
|
|
224
|
+
fi
|