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
1010REPO_URL=" ${REPO_URL:- https:// github.com/ gHashTag/ trinity.git} "
1111ISSUE=" ${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/&/\&/g; s/</\</g; s/>/\>/g'
105111}
106112
107113send_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+
158172start_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}"
256291git config --global user.name " Trinity Agent"
257292git 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
266325fi
267- cd /workspace/trinity
326+ cd " ${WORKTREE_PATH} "
327+
328+ log " Worktree created at ${WORKTREE_PATH} "
268329
269330# === 3. Prepare SOUL.md ===
270331log " 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 ===
274335report_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"
0 commit comments