Skip to content

Commit 5082b01

Browse files
author
Antigravity Agent
committed
Merge branch 'main' into feat/issue-140
2 parents 98c19fb + b470c5a commit 5082b01

5 files changed

Lines changed: 300 additions & 35 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,6 @@ hslm-train/
258258
--output
259259
--output/
260260
data/checkpoints_v3/
261+
262+
# Claude agent config (local)
263+
CLAUDE.md.agent

deploy/Dockerfile.agent

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,15 @@ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
3030
&& npm install -g @anthropic-ai/claude-code \
3131
&& rm -rf /var/lib/apt/lists/*
3232

33-
# Pre-clone and pre-build (cached — saves ~2min on spawn)
34-
RUN git clone --depth=50 https://github.com/gHashTag/trinity.git /prebuild \
35-
&& cd /prebuild && zig build || true
33+
# Pre-clone as bare repo + pre-build (cached — saves ~2min on spawn)
34+
# Bare repo enables fast git worktree add for agent containers
35+
RUN git clone --bare --depth=50 https://github.com/gHashTag/trinity.git /bare-repo.git \
36+
&& git clone --local /bare-repo.git /tmp/build \
37+
&& cd /tmp/build && zig build || true \
38+
&& rm -rf /tmp/build
39+
40+
# Shared bare repo volume (fast worktree creation for each agent)
41+
VOLUME ["/bare-repo.git"]
3642

3743
# Stage 2: Runtime (fast start)
3844
FROM prebuild AS runtime

deploy/agent-entrypoint.sh

Lines changed: 87 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
#!/bin/sh
1+
#!/bin/bash
22
# Trinity Cloud Agent Entrypoint
33
# Solves a single GitHub issue using Claude Code
44
# Required env: ISSUE_NUMBER, GITHUB_TOKEN, ANTHROPIC_API_KEY
55
#
66
# P0 hardened: timeout, SIGTERM handler, heartbeat loop, retry wrapper
77

8-
set -e
8+
set -eo pipefail
99

1010
REPO_URL="${REPO_URL:-https://github.com/gHashTag/trinity.git}"
1111
ISSUE="${ISSUE_NUMBER:?ISSUE_NUMBER is required}"
@@ -54,7 +54,10 @@ report_status() {
5454

5555
log "Status: ${CURRENT_STATUS}${CURRENT_DETAIL}"
5656

57-
# 1. HTTP POST to monitor (existing)
57+
# Update heartbeat file so background heartbeat reads current state
58+
echo "${CURRENT_STATUS}|${CURRENT_DETAIL}" > "${HEARTBEAT_FILE}"
59+
60+
# 1. HTTP POST to monitor
5861
if [ -n "${WS_MONITOR_URL}" ]; then
5962
curl -s -X POST "${WS_MONITOR_URL}/api/status" \
6063
-H "Content-Type: application/json" \
@@ -64,7 +67,14 @@ report_status() {
6467
2>/dev/null || log "Warning: monitor unreachable"
6568
fi
6669

67-
# 2. GitHub issue comment on status change (skip duplicates)
70+
# 2. Telegram notification on status change (BEFORE updating LAST_STATUS)
71+
if [ "${CURRENT_STATUS}" != "${LAST_STATUS}" ] || echo "${CURRENT_STATUS}" | grep -qE "STUCK|ERROR|FAILED|KILLED|DONE"; then
72+
send_telegram "${EMOJI} <b>Agent #${ISSUE}</b>: ${CURRENT_STATUS}
73+
<i>${ISSUE_TITLE:-issue #${ISSUE}}</i>
74+
${CURRENT_DETAIL} (${ELAPSED}s)"
75+
fi
76+
77+
# 3. GitHub issue comment on status change (skip duplicates)
6878
if [ "${CURRENT_STATUS}" != "${LAST_STATUS}" ]; then
6979
gh issue comment "${ISSUE}" --body "${EMOJI} **Trinity Agent** | ${TIMESTAMP}
7080
📋 **Step**: ${STEP_NUM}/${TOTAL_STEPS}${CURRENT_DETAIL}
@@ -73,7 +83,7 @@ report_status() {
7383
fi
7484
LAST_STATUS="${CURRENT_STATUS}"
7585

76-
# 3. Dashboard comment (create or update)
86+
# 4. Dashboard comment (create or update)
7787
DASHBOARD_BODY="${EMOJI} **Trinity Agent Dashboard** — Issue #${ISSUE}
7888
7989
| Field | Value |
@@ -87,30 +97,32 @@ report_status() {
8797

8898
if [ -z "${DASHBOARD_COMMENT_ID}" ]; then
8999
DASHBOARD_COMMENT_ID=$(gh issue comment "${ISSUE}" --body "${DASHBOARD_BODY}" 2>/dev/null | grep -o '/[0-9]*$' | tr -d '/' || true)
90-
# Fallback: fetch last comment ID
91100
if [ -z "${DASHBOARD_COMMENT_ID}" ]; then
92101
DASHBOARD_COMMENT_ID=$(gh api "repos/{owner}/{repo}/issues/${ISSUE}/comments" --jq '.[-1].id' 2>/dev/null || true)
93102
fi
94103
elif [ -n "${DASHBOARD_COMMENT_ID}" ]; then
95104
gh api "repos/{owner}/{repo}/issues/comments/${DASHBOARD_COMMENT_ID}" \
96105
-X PATCH -f body="${DASHBOARD_BODY}" 2>/dev/null || log "Warning: Dashboard update failed"
97106
fi
107+
}
98108

99-
# 4. Telegram notification on status change (with issue title for context)
100-
if [ "${CURRENT_STATUS}" != "${LAST_STATUS}" ] || echo "${CURRENT_STATUS}" | grep -qE "STUCK|ERROR|FAILED|KILLED|DONE"; then
101-
send_telegram "${EMOJI} <b>Agent #${ISSUE}</b>: ${CURRENT_STATUS}
102-
<i>${ISSUE_TITLE:-issue #${ISSUE}}</i>
103-
${CURRENT_DETAIL} (${ELAPSED}s)"
104-
fi
109+
escape_html() {
110+
echo "$1" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g'
105111
}
106112

107113
send_telegram() {
108114
if [ -n "${TELEGRAM_BOT_TOKEN}" ] && [ -n "${TELEGRAM_CHAT_ID}" ]; then
115+
# Write message to temp file to avoid JSON escaping issues with special chars
116+
local msg_file="/tmp/tg_msg_$$.json"
117+
printf '{"chat_id":"%s","text":"%s","parse_mode":"HTML"}' \
118+
"${TELEGRAM_CHAT_ID}" "$(echo "$1" | sed 's/"/\\"/g; s/$/\\n/' | tr -d '\n' | sed 's/\\n$//')" \
119+
> "${msg_file}"
109120
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
110121
-H "Content-Type: application/json" \
111-
-d "{\"chat_id\":\"${TELEGRAM_CHAT_ID}\",\"text\":\"$1\",\"parse_mode\":\"HTML\"}" \
122+
-d "@${msg_file}" \
112123
--connect-timeout 5 --max-time 10 \
113124
2>/dev/null || log "Warning: Telegram send failed"
125+
rm -f "${msg_file}"
114126
fi
115127
}
116128

@@ -155,11 +167,25 @@ emit_event() {
155167
# HEARTBEAT LOOP (P0.6) — background process sends status every 30s
156168
# ═══════════════════════════════════════════════════════════════════════════════
157169

170+
HEARTBEAT_FILE="/tmp/agent_heartbeat_state"
171+
158172
start_heartbeat() {
173+
echo "STARTING|Initializing" > "${HEARTBEAT_FILE}"
159174
(
160175
while true; do
161176
sleep "${HEARTBEAT_INTERVAL}"
162-
report_status "${CURRENT_STATUS}" "${CURRENT_DETAIL}"
177+
if [ -f "${HEARTBEAT_FILE}" ]; then
178+
HB_STATUS=$(cut -d'|' -f1 "${HEARTBEAT_FILE}")
179+
HB_DETAIL=$(cut -d'|' -f2 "${HEARTBEAT_FILE}")
180+
ELAPSED=$(( $(date +%s) - ${START_TIME:-0} ))
181+
if [ -n "${WS_MONITOR_URL}" ]; then
182+
curl -s -X POST "${WS_MONITOR_URL}/api/status" \
183+
-H "Content-Type: application/json" \
184+
-H "Authorization: Bearer ${MONITOR_TOKEN:-trinity}" \
185+
-d "{\"issue\":${ISSUE},\"status\":\"${HB_STATUS}\",\"detail\":\"heartbeat: ${HB_DETAIL} (${ELAPSED}s)\"}" \
186+
--connect-timeout 5 --max-time 10 2>/dev/null || true
187+
fi
188+
fi
163189
done
164190
) &
165191
HEARTBEAT_PID=$!
@@ -208,6 +234,15 @@ cleanup() {
208234
log "Shutting down (signal received)..."
209235
stop_heartbeat
210236
report_status "KILLED" "Container terminated by signal"
237+
238+
# Cleanup worktree if it exists
239+
if [ -n "${WORKTREE_PATH}" ] && [ -d "${WORKTREE_PATH}" ]; then
240+
log "Cleaning up worktree on exit..."
241+
cd /bare-repo.git 2>/dev/null || true
242+
git worktree remove "${WORKTREE_PATH}" --force 2>/dev/null || true
243+
log "Worktree removed: ${WORKTREE_PATH}"
244+
fi
245+
211246
rm -f /tmp/agent-alive
212247
exit 1
213248
}
@@ -256,19 +291,45 @@ log "gh auth status: ${GH_STATUS}"
256291
git config --global user.name "Trinity Agent"
257292
git config --global user.email "trinity-agent@users.noreply.github.com"
258293

259-
# === 2. Clone (with retry) ===
260-
report_status "AWAKENING" "Cloning repository"
261-
if ! retry "gh repo clone '${REPO_URL}' /workspace/trinity -- --depth=50 2>/dev/null"; then
262-
report_status "FAILED" "Git clone failed after 3 attempts"
294+
# === 2. Setup worktree from shared bare repo ===
295+
report_status "AWAKENING" "Creating worktree from bare repository"
296+
297+
# Check if bare repo needs to be created or updated
298+
if [ ! -d /bare-repo.git/objects ]; then
299+
log "Bare repo not found, creating from remote..."
300+
if ! retry "git clone --bare --depth=50 '${REPO_URL}' /bare-repo.git 2>/dev/null"; then
301+
report_status "FAILED" "Git bare clone failed after 3 attempts"
302+
stop_heartbeat
303+
rm -f /tmp/agent-alive
304+
exit 1
305+
fi
306+
else
307+
log "Updating bare repo from remote..."
308+
cd /bare-repo.git
309+
retry "git fetch origin main --depth=50 2>/dev/null" || log "Warning: bare repo update failed"
310+
fi
311+
312+
# Create worktree for this agent (fast! ~5-10s vs ~60s for full clone)
313+
WORKTREE_PATH="/workspace/trinity-${ISSUE}"
314+
if [ -d "${WORKTREE_PATH}" ]; then
315+
log "Removing existing worktree..."
316+
rm -rf "${WORKTREE_PATH}"
317+
fi
318+
319+
cd /bare-repo.git
320+
if ! retry "git worktree add '${WORKTREE_PATH}' main 2>/dev/null"; then
321+
report_status "FAILED" "Git worktree add failed after 3 attempts"
263322
stop_heartbeat
264323
rm -f /tmp/agent-alive
265324
exit 1
266325
fi
267-
cd /workspace/trinity
326+
cd "${WORKTREE_PATH}"
327+
328+
log "Worktree created at ${WORKTREE_PATH}"
268329

269330
# === 3. Prepare SOUL.md ===
270331
log "Injecting soul..."
271-
sed "s/{ISSUE_NUMBER}/${ISSUE}/g" /etc/trinity/SOUL.md > /workspace/trinity/CLAUDE.md.agent
332+
sed "s/{ISSUE_NUMBER}/${ISSUE}/g" /etc/trinity/SOUL.md > "${WORKTREE_PATH}/CLAUDE.md.agent"
272333

273334
# === 4. Read issue ===
274335
report_status "READING" "Reading issue #${ISSUE}"
@@ -381,6 +442,12 @@ Commits: ${COMMIT_COUNT}" \
381442
\`\`\`
382443
${DIFF_STAT}
383444
\`\`\`" 2>/dev/null || true
445+
446+
# Cleanup worktree after PR creation (keeps shared bare repo intact)
447+
log "Cleaning up worktree..."
448+
cd /bare-repo.git
449+
git worktree remove "${WORKTREE_PATH}" --force 2>/dev/null || true
450+
log "Worktree removed: ${WORKTREE_PATH}"
384451
fi
385452
else
386453
report_status "FAILED" "No commits produced — agent could not solve issue"

src/tri/tri_cloud.zig

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ fn cloudCleanup(allocator: Allocator) !void {
602602
print("{s}✓ Cleaned {d} inactive agent(s){s}\n", .{ GREEN, cleaned, RESET });
603603
}
604604

605-
/// tri cloud history [issue] — Show event history from JSONL
605+
/// tri cloud history [issue] [--format=json] — Show event history from JSONL
606606
fn cloudHistory(_: Allocator, args: []const []const u8) !void {
607607
const events_path = ".trinity/cloud_events.jsonl";
608608
const file = std.fs.cwd().openFile(events_path, .{}) catch {
@@ -611,22 +611,70 @@ fn cloudHistory(_: Allocator, args: []const []const u8) !void {
611611
};
612612
defer file.close();
613613

614-
// Optional issue filter
614+
// Parse args: [issue] [--format=json]
615615
var filter_issue: ?u32 = null;
616-
if (args.len >= 1) {
617-
filter_issue = std.fmt.parseInt(u32, args[0], 10) catch null;
616+
var json_output: bool = false;
617+
for (args) |arg| {
618+
if (eql(u8, arg, "--format=json")) {
619+
json_output = true;
620+
} else if (eql(u8, arg, "--format=human")) {
621+
json_output = false;
622+
} else if (filter_issue == null) {
623+
// Try to parse as issue number
624+
filter_issue = std.fmt.parseInt(u32, arg, 10) catch null;
625+
}
618626
}
619627

620-
print("\n{s}{s}", .{ GOLDEN, BOLD });
621-
print("═══════════════════════════════════════════════════\n", .{});
622-
print(" CLOUD EVENTS — History\n", .{});
623-
print("═══════════════════════════════════════════════════{s}\n", .{RESET});
624-
625628
// Read entire file (cloud events are small)
626629
var buf: [32768]u8 = undefined;
627630
const len = file.readAll(&buf) catch 0;
628631
const content = buf[0..len];
629632

633+
if (json_output) {
634+
// JSON output for machine consumption
635+
var json_buf: [65536]u8 = undefined;
636+
var fbs = std.io.fixedBufferStream(&json_buf);
637+
const w = fbs.writer();
638+
639+
w.writeAll("{\"events\":[") catch return;
640+
641+
var first = true;
642+
var count: u32 = 0;
643+
var offset: usize = 0;
644+
645+
while (offset < content.len) {
646+
const line_end = std.mem.indexOfPos(u8, content, offset, "\n") orelse content.len;
647+
const line = content[offset..line_end];
648+
offset = line_end + 1;
649+
650+
if (line.len == 0) continue;
651+
652+
// Filter by issue if specified
653+
if (filter_issue) |fi| {
654+
var needle_buf: [32]u8 = undefined;
655+
const needle = std.fmt.bufPrint(&needle_buf, "\"issue\":{d}", .{fi}) catch continue;
656+
if (std.mem.indexOf(u8, line, needle) == null) continue;
657+
}
658+
659+
if (!first) w.writeAll(",") catch {};
660+
first = false;
661+
w.writeAll(line) catch break;
662+
count += 1;
663+
}
664+
665+
w.writeAll("],\"count\":") catch {};
666+
std.fmt.format(w, "{d}}}", .{count}) catch {};
667+
668+
print("{s}\n", .{fbs.getWritten()});
669+
return;
670+
}
671+
672+
// Human-readable output
673+
print("\n{s}{s}", .{ GOLDEN, BOLD });
674+
print("═══════════════════════════════════════════════════\n", .{});
675+
print(" CLOUD EVENTS — History\n", .{});
676+
print("═══════════════════════════════════════════════════{s}\n", .{RESET});
677+
630678
var count: u32 = 0;
631679
var offset: usize = 0;
632680

0 commit comments

Comments
 (0)