@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
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
@@ -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