Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env fish
2# 🐜 Aesthetic Ant Colony
3# A dumb ant wakes every N minutes, reads the score, does one small thing.
4#
5# Usage: fish ants/colony.fish [--once] [--interval MINUTES] [--provider PROVIDER] [--model MODEL]
6#
7# Options:
8# --once Run one ant and exit (for testing)
9# --interval N Minutes between runs (default: 30)
10# --provider PROVIDER LLM provider (default: gh-models). See brain.fish.
11# --model MODEL Model name (default: per provider)
12
13set -g COLONY_DIR (realpath (dirname (status filename)))
14set -g REPO_DIR (realpath "$COLONY_DIR/..")
15set -g MAIN_SCORE_FILE "$REPO_DIR/SCORE.md"
16set -g ANT_MINDSET_FILE "$COLONY_DIR/mindset-and-rules.md"
17set -g LOG_FILE "$COLONY_DIR/colony.log"
18set -g PHEROMONE_FILE "$COLONY_DIR/pheromones.log"
19set -g BRAIN "$COLONY_DIR/brain.fish"
20set -g INTERVAL 30
21set -g PROVIDER "gh-models"
22set -g MODEL ""
23set -g ONCE false
24
25# Parse args
26set -l i 1
27while test $i -le (count $argv)
28 switch $argv[$i]
29 case --once
30 set ONCE true
31 case --interval
32 set i (math $i + 1)
33 set INTERVAL $argv[$i]
34 case --provider
35 set i (math $i + 1)
36 set PROVIDER $argv[$i]
37 case --model
38 set i (math $i + 1)
39 set MODEL $argv[$i]
40 end
41 set i (math $i + 1)
42end
43
44function log_msg
45 set -l msg (date "+%Y-%m-%d %H:%M:%S")" 🐜 $argv"
46 echo $msg
47 echo $msg >> $LOG_FILE
48end
49
50function log_pheromone
51 set -l msg (date "+%Y-%m-%d %H:%M:%S")" $argv"
52 echo $msg >> $PHEROMONE_FILE
53end
54
55function call_brain --argument-names system_prompt user_prompt
56 set -l args --provider $PROVIDER
57 if test -n "$MODEL"
58 set args $args --model $MODEL
59 end
60 fish $BRAIN $args --system "$system_prompt" --prompt "$user_prompt" 2>&1
61end
62
63function run_ant
64 set -l run_id (date "+%Y%m%d-%H%M%S")
65 log_msg "Ant $run_id waking up..."
66
67 cd $REPO_DIR
68
69 # Check for dirty tracked files (queen might be working)
70 set -l dirty (git diff --name-only HEAD 2>/dev/null | head -1)
71 set -l staged (git diff --cached --name-only 2>/dev/null | head -1)
72 if test -n "$dirty" -o -n "$staged"
73 log_msg "Tracked files modified/staged — queen is working. Sleeping."
74 log_pheromone "IDLE: ant $run_id — dirty tree, deferred to queen"
75 return 1
76 end
77
78 # Read the score from both sources:
79 # 1) ant-local mindset/rules, 2) main score/tasks
80 if not test -f $ANT_MINDSET_FILE
81 log_msg "ERROR: Ant mindset/rules not found at $ANT_MINDSET_FILE"
82 return 1
83 end
84 if not test -f $MAIN_SCORE_FILE
85 log_msg "ERROR: Main score not found at $MAIN_SCORE_FILE"
86 return 1
87 end
88 set -l ant_mindset_content (cat $ANT_MINDSET_FILE | string collect)
89 set -l main_score_content (cat $MAIN_SCORE_FILE | string collect)
90 set -l score_content (string join "\n\n" "$ant_mindset_content" "$main_score_content")
91
92 # Read recent pheromones
93 set -l recent_pheromones "(none yet)"
94 if test -f $PHEROMONE_FILE
95 set -l trail (tail -20 $PHEROMONE_FILE)
96 if test -n "$trail"
97 set recent_pheromones (string join \n $trail)
98 end
99 end
100
101 # Gather context
102 set -l test_output (cd $REPO_DIR; npm test 2>&1 | tail -30)
103 set -l recent_commits (cd $REPO_DIR; git log --oneline -10 2>/dev/null)
104
105 # --- Phase 1: SCOUT — pick a task and target file ---
106 log_msg "Phase 1: Scouting..."
107
108 set -l scout_system "You are a dumb but careful ant. Respond with exactly one line starting with SCOUT:"
109 set -l scout_prompt "You are aesthetic ant $run_id. You follow the score.
110
111## The Score
112$score_content
113
114## Recent Pheromones (what other ants did)
115$recent_pheromones
116
117## Current Test Output (last 30 lines)
118$test_output
119
120## Recent Git History
121$recent_commits
122
123---
124
125Pick ONE small task from the Current Tasks in the score.
126Based on the test output and context, decide what specific file to look at.
127
128Respond with EXACTLY one line in this format:
129SCOUT: <task> | <file path relative to repo root> | <plan in one sentence>
130
131If nothing to do:
132SCOUT: IDLE | none | <reason>
133
134Respond with ONLY the SCOUT line. Nothing else."
135
136 set -l scout_output (call_brain "$scout_system" "$scout_prompt")
137
138 mkdir -p "$COLONY_DIR/runs"
139 echo "=== SCOUT ===" > "$COLONY_DIR/runs/$run_id.log"
140 echo "$scout_output" >> "$COLONY_DIR/runs/$run_id.log"
141
142 set -l scout_line (echo "$scout_output" | grep "^SCOUT:" | tail -1)
143 if test -z "$scout_line"
144 log_msg "Scout returned no SCOUT line. Raw output saved to runs/$run_id.log"
145 log_pheromone "ERROR: ant $run_id — scout failed (no SCOUT line)"
146 return 1
147 end
148
149 log_msg "Scout: $scout_line"
150
151 if string match -q "*IDLE*" "$scout_line"
152 set -l reason (echo $scout_line | sed 's/.*| *//')
153 log_msg "Nothing to do: $reason"
154 log_pheromone "IDLE: ant $run_id — $reason"
155 return 0
156 end
157
158 # Parse: SCOUT: task | path | plan
159 set -l parts (string split "|" (string replace "SCOUT:" "" "$scout_line"))
160 set -l task_name (string trim $parts[1])
161 set -l target_file (string trim $parts[2])
162 set -l plan (string trim $parts[3])
163
164 if test -z "$target_file" -o "$target_file" = "none"
165 log_msg "No target file."
166 log_pheromone "IDLE: ant $run_id — no target"
167 return 0
168 end
169
170 log_msg "Target: $target_file"
171 log_msg "Plan: $plan"
172
173 # Resolve the file
174 set -l full_path "$REPO_DIR/$target_file"
175 if not test -f "$full_path"
176 log_msg "File not found: $full_path"
177 log_pheromone "FAILURE: ant $run_id — file not found: $target_file"
178 return 1
179 end
180
181 # --- Phase 2: WORK — read the file and produce a diff ---
182 log_msg "Phase 2: Working on $target_file..."
183
184 set -l file_content (head -200 "$full_path")
185 set -l line_count (wc -l < "$full_path" | string trim)
186
187 set -l work_system "You output precise unified diffs. No preamble, no explanation outside the required format."
188 set -l work_prompt "You are aesthetic ant $run_id editing: $target_file ($line_count lines, showing first 200)
189Plan: $plan
190
191## File Content
192$file_content
193
194## Test Output
195$test_output
196
197## Rules
198- Make the SMALLEST change that accomplishes your plan.
199- You must be 98% confident your change is correct.
200- Do NOT change anything unrelated to your plan.
201- Include 3 lines of context before and after each hunk.
202
203## Response Format
204If you have a change, respond:
205WORK: CHANGE | <one-line description>
206\`\`\`diff
207--- a/$target_file
208+++ b/$target_file
209@@ <hunk header> @@
210 context
211-old line
212+new line
213 context
214\`\`\`
215WORK_END
216
217If not confident enough:
218WORK: ABORT | <reason>"
219
220 set -l work_output (call_brain "$work_system" "$work_prompt")
221
222 echo "" >> "$COLONY_DIR/runs/$run_id.log"
223 echo "=== WORK ===" >> "$COLONY_DIR/runs/$run_id.log"
224 echo "$work_output" >> "$COLONY_DIR/runs/$run_id.log"
225
226 if string match -q "*ABORT*" "$work_output"
227 set -l reason (echo "$work_output" | grep "ABORT" | sed 's/.*ABORT *| *//')
228 log_msg "Aborted: $reason"
229 log_pheromone "IDLE: ant $run_id — aborted: $reason"
230 return 0
231 end
232
233 # Extract the diff block
234 set -l diff_content (echo "$work_output" | sed -n '/^```diff/,/^```/{/^```/d;p}')
235 if test -z "$diff_content"
236 # Try without fenced code block — raw diff
237 set diff_content (echo "$work_output" | sed -n '/^--- a\//,/WORK_END/{/WORK_END/d;p}')
238 end
239
240 if test -z "$diff_content"
241 log_msg "No valid diff produced."
242 log_pheromone "FAILURE: ant $run_id — no valid diff"
243 return 1
244 end
245
246 set -l description (echo "$work_output" | grep "^WORK: CHANGE" | sed 's/WORK: CHANGE *| *//')
247 if test -z "$description"
248 set description "$plan"
249 end
250
251 # --- Phase 3: APPLY & VERIFY ---
252 log_msg "Phase 3: Applying..."
253
254 set -l diff_file (mktemp /tmp/ant-XXXXXX.patch)
255 printf '%s\n' $diff_content > $diff_file
256
257 cd $REPO_DIR
258
259 # Dry run
260 git apply --check $diff_file 2>/dev/null
261 if test $status -ne 0
262 log_msg "Diff doesn't apply cleanly."
263 echo "" >> "$COLONY_DIR/runs/$run_id.log"
264 echo "=== FAILED DIFF ===" >> "$COLONY_DIR/runs/$run_id.log"
265 cat $diff_file >> "$COLONY_DIR/runs/$run_id.log"
266 rm -f $diff_file
267 log_pheromone "FAILURE: ant $run_id — diff didn't apply cleanly"
268 return 1
269 end
270
271 # Apply for real
272 git apply $diff_file 2>&1
273 set -l apply_exit $status
274 rm -f $diff_file
275
276 if test $apply_exit -ne 0
277 log_msg "Apply failed."
278 git checkout . 2>/dev/null
279 log_pheromone "FAILURE: ant $run_id — apply failed"
280 return 1
281 end
282
283 # Check we actually changed something
284 set -l changes (git diff --name-only 2>/dev/null)
285 if test -z "$changes"
286 log_msg "No changes after apply (diff was a no-op)."
287 log_pheromone "IDLE: ant $run_id — no-op diff"
288 return 0
289 end
290
291 # Verify tests pass
292 log_msg "Verifying tests..."
293 set -l verify_output (npm test 2>&1)
294 set -l verify_exit $status
295
296 echo "" >> "$COLONY_DIR/runs/$run_id.log"
297 echo "=== TEST VERIFY ===" >> "$COLONY_DIR/runs/$run_id.log"
298 echo "Exit: $verify_exit" >> "$COLONY_DIR/runs/$run_id.log"
299 echo "$verify_output" | tail -10 >> "$COLONY_DIR/runs/$run_id.log"
300
301 if test $verify_exit -ne 0
302 log_msg "Tests FAILED after apply. Reverting."
303 git checkout . 2>/dev/null
304 log_pheromone "REVERTED: ant $run_id — tests failed after change"
305 return 1
306 end
307
308 log_msg "Tests pass. Changed: $changes"
309
310 # Commit
311 git add -A
312 git commit -m "ant: $description
313
314Ant-ID: $run_id
315Provider: $PROVIDER
316Model: $MODEL
317Verified: tests pass" --no-verify 2>&1
318
319 if test $status -eq 0
320 log_msg "Committed! 🐜✅"
321 log_pheromone "SUCCESS: ant $run_id ($PROVIDER/$MODEL) — $description"
322 else
323 log_msg "Commit failed. Reverting."
324 git checkout . 2>/dev/null
325 git reset HEAD . 2>/dev/null
326 log_pheromone "ERROR: ant $run_id — commit failed"
327 return 1
328 end
329
330 return 0
331end
332
333# --- Main Loop ---
334
335log_msg "Colony starting. Interval: "$INTERVAL"m | Provider: $PROVIDER | Model: $MODEL | Once: $ONCE"
336log_msg "Score: $SCORE_FILE"
337log_msg "Repo: $REPO_DIR"
338
339if test "$ONCE" = true
340 run_ant
341 set -l result $status
342 log_msg "Single run complete. Exit: $result"
343 exit $result
344end
345
346while true
347 run_ant
348 log_msg "Sleeping for $INTERVAL minutes..."
349 sleep (math "$INTERVAL * 60")
350end