Skip to content

Commit ac286a5

Browse files
Antigravity Agentclaude
andcommitted
feat(cloud): add /agents skill + workflow_dispatch for manual spawn
- New /agents skill: Railway pools, issue queue, PR pipeline, JSONL events, ETA - agent-spawn.yml: workflow_dispatch input, smart pool selection, resolve step - Fixes: issues #309-#319 missed auto-spawn due to GitHub throttling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d92e4ec commit ac286a5

3 files changed

Lines changed: 262 additions & 43 deletions

File tree

.claude/skills/agents/SKILL.md

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
---
2+
name: agents
3+
description: Agent swarm development dashboard — live Railway containers, issue queue, PR pipeline, JSONL events, pool utilization, and queue drain status. Use when checking agent dev tasks, spawning status, or monitoring issues #315-#319.
4+
argument-hint: [focus] (status, queue, events, pools, spawn <N>, full)
5+
---
6+
7+
# 🤖 Agent Swarm Observatory
8+
9+
## 📡 Railway Service Pools (Live)
10+
!`curl -s -X POST "https://backboard.railway.com/graphql/v2" -H "Authorization: Bearer $(grep RAILWAY_API_TOKEN /Users/playra/trinity-w1/.env | cut -d= -f2)" -H "Content-Type: application/json" -d '{"query":"query($id:String!){project(id:$id){services{edges{node{id name deployments(first:1){edges{node{status createdAt}}}}}}}}","variables":{"id":"aa0efa7f-95e6-4466-8de6-43945a031365"}}' 2>/dev/null | python3 -c "
11+
import sys,json,datetime
12+
d=json.load(sys.stdin)
13+
nodes=[e['node'] for e in d['data']['project']['services']['edges']]
14+
total=len(nodes)
15+
agents=[n for n in nodes if n['name'].startswith('agent-')]
16+
pool0='acfee27a-74e8-4436-961c-698ae93508ca'
17+
pool1='12c2bdf9-d124-4a45-93ad-22921e842d1b'
18+
p0=[n for n in nodes if n['id']==pool0]
19+
p1=[n for n in nodes if n['id']==pool1]
20+
def st(n):
21+
if n['deployments']['edges']:
22+
return n['deployments']['edges'][0]['node']['status']
23+
return 'NO_DEPLOY'
24+
print(json.dumps({
25+
'total_services': total,
26+
'pool_0_ubuntu': {'id': pool0[:8], 'status': st(p0[0]) if p0 else 'NOT_FOUND'},
27+
'pool_1_agents_anywhere': {'id': pool1[:8], 'status': st(p1[0]) if p1 else 'NOT_FOUND'},
28+
'agent_services': [{'name':n['name'],'status':st(n)} for n in agents],
29+
'active_building': len([n for n in nodes if st(n) in ('DEPLOYING','BUILDING')]),
30+
'slots_used': len(agents),
31+
'slots_free': 10-len(agents)
32+
}, indent=2))
33+
" 2>/dev/null || echo "⚠️ Railway API unavailable"`
34+
35+
## 📋 Agent Issues (agent:spawn + agent:queued)
36+
!`echo "=== SPAWNING ==="; gh issue list --repo gHashTag/trinity --label "agent:spawn" --state open --limit 20 --json number,title,labels,assignees,createdAt --jq '.[] | "#\(.number) \(.title) [\(.labels | map(.name) | join(","))] \(.createdAt[:10])"' 2>&1 || echo "gh unavailable"; echo ""; echo "=== QUEUED ==="; gh issue list --repo gHashTag/trinity --label "agent:queued" --state open --limit 20 --json number,title,createdAt --jq '.[] | "#\(.number) \(.title) \(.createdAt[:10])"' 2>&1 || echo "none"`
37+
38+
## 🔀 Agent PRs (feat/issue- branches)
39+
!`gh pr list --repo gHashTag/trinity --state open --limit 15 --json number,title,headRefName,statusCheckRollup --jq '.[] | select(.headRefName | startswith("feat/issue-")) | "#\(.number) [\(.headRefName)] \(.title) checks:\(.statusCheckRollup | if . then (. | map(.conclusion // .status) | join(",")) else "none" end)"' 2>&1 || echo "No agent PRs"`
40+
41+
## 📡 Recent Events (last 25 lines)
42+
!`if [ -f /Users/playra/trinity-w1/.trinity/cloud_events.jsonl ]; then tail -25 /Users/playra/trinity-w1/.trinity/cloud_events.jsonl | python3 -c "
43+
import sys,json
44+
for line in sys.stdin:
45+
line=line.strip()
46+
if not line: continue
47+
try:
48+
e=json.loads(line)
49+
ts=e.get('timestamp','?')[:19]
50+
typ=e.get('type','?')
51+
iss=e.get('issue','?')
52+
msg=e.get('message',e.get('event',''))
53+
emoji={'spawn':'🚀','kill':'💀','heartbeat':'💓','error':'❌','pr':'🔀','build':'🔨','test':'🧪','complete':'✅','queued':'⏳'}.get(typ,'📌')
54+
print(f'{ts} {emoji} #{iss} [{typ}] {msg}')
55+
except: print(line)
56+
" 2>/dev/null; else echo "No cloud events yet"; fi`
57+
58+
## 🔄 GitHub Actions (agent workflows)
59+
!`gh run list --repo gHashTag/trinity --workflow agent-spawn.yml --limit 5 --json databaseId,status,conclusion,createdAt,headBranch --jq '.[] | "\(.createdAt[:16]) \(if .conclusion == "success" then "✅" elif .conclusion == "failure" then "❌" elif .status == "in_progress" then "🔄" else "⏳" end) \(.status)/\(.conclusion // "—") \(.headBranch // "—")"' 2>&1 || echo "No spawn runs"; echo "---"; gh run list --repo gHashTag/trinity --workflow agent-queue-drain.yml --limit 3 --json status,conclusion,createdAt --jq '.[] | "\(.createdAt[:16]) \(if .conclusion == "success" then "✅" else "⏳" end) drain: \(.status)/\(.conclusion // "—")"' 2>&1 || echo "No drain runs"`
60+
61+
## 🏗️ Container Image
62+
!`gh api user/packages/container/trinity-agent/versions --jq '.[0] | "📦 trinity-agent:latest — updated \(.updated_at[:10]) tags: \(.metadata.container.tags | join(","))"' 2>/dev/null || echo "⚠️ GHCR package not accessible"`
63+
64+
## 📊 Local Agent State
65+
!`if [ -f /Users/playra/trinity-w1/.trinity/cloud_agents.json ]; then cat /Users/playra/trinity-w1/.trinity/cloud_agents.json | python3 -c "
66+
import sys,json,datetime
67+
d=json.load(sys.stdin)
68+
agents=d if isinstance(d,list) else d.get('agents',[])
69+
active=[a for a in agents if a.get('active',False)]
70+
inactive=[a for a in agents if not a.get('active',False)]
71+
print(f'Active: {len(active)}/{len(agents)}')
72+
for a in active:
73+
iss=a.get('issue','?')
74+
sid=a.get('service_id','?')[:8]
75+
ts=a.get('created_at','?')
76+
print(f' 🟢 #{iss} svc:{sid} started:{ts}')
77+
for a in inactive[-3:]:
78+
iss=a.get('issue','?')
79+
print(f' ⚪ #{iss} (done)')
80+
" 2>/dev/null; else echo "No local state file"; fi`
81+
82+
## 🎯 Issue → Pool Mapping (round-robin)
83+
!`echo "Pool 0 (ubuntu/acfee27a): even issues"; echo "Pool 1 (Agents Anywhere/12c2bdf9): odd issues"; echo "---"; gh issue list --repo gHashTag/trinity --label "agent:spawn" --state open --limit 20 --json number,title --jq '.[] | "#\(.number) → Pool \(.number % 2) \(if .number % 2 == 0 then "(ubuntu)" else "(Agents Anywhere)" end) — \(.title[:60])"' 2>&1 || echo "gh unavailable"`
84+
85+
## Task
86+
87+
Analyze the data above and present a **rich Agent Swarm dashboard** with emojis.
88+
89+
Focus area: $ARGUMENTS (default: status)
90+
91+
### Dashboard Format
92+
93+
ALWAYS output the full dashboard — never compress to one line. Use this format:
94+
95+
```
96+
🤖 ═══════════════════════════════════════════════════
97+
TRINITY AGENT SWARM — DEVELOPMENT OBSERVATORY
98+
═══════════════════════════════════════════════════
99+
100+
📡 SERVICE POOLS
101+
Pool 0 (ubuntu): [status emoji] [deployment status]
102+
Pool 1 (Agents Anywhere): [status emoji] [deployment status]
103+
Active builds: N | Free slots: N/10
104+
105+
📋 ISSUE QUEUE
106+
🚀 Spawning:
107+
#N — title — pool — status
108+
⏳ Queued:
109+
#N — title — waiting since [date]
110+
111+
Queue depth: N spawning + N queued = N total
112+
113+
🔀 AGENT PRs
114+
#N — [branch] — title — checks: [status]
115+
...
116+
Open: N | Merged today: N
117+
118+
📡 LIVE EVENTS (last 10)
119+
[timestamp] [emoji] #issue [type] message
120+
...
121+
122+
🔄 GITHUB ACTIONS
123+
agent-spawn: [last 5 runs with status]
124+
queue-drain: [last 3 runs with status]
125+
126+
🗺️ POOL MAPPING (round-robin: issue# % 2)
127+
Pool 0 (even): #316, #318, ...
128+
Pool 1 (odd): #315, #317, #319, ...
129+
⚠️ Contention: N issues compete for Pool X
130+
131+
📦 INFRASTRUCTURE
132+
GHCR image: [status + last updated]
133+
Local state: N active / N total agents
134+
Events log: N entries
135+
136+
🎯 RECOMMENDATIONS
137+
[dynamic recommendations based on current state — see rules below]
138+
139+
⏱️ ESTIMATED PIPELINE
140+
[time estimate for queue drain based on 2 pools × ~1h per agent]
141+
```
142+
143+
### Recommendation Rules
144+
145+
**If both pools DEPLOYING/BUILDING:**
146+
→ "⚠️ Both pools busy. Queue drain runs every 5min. ETA for next slot: ~Xmin"
147+
→ Show which issues are running vs queued
148+
149+
**If 1 pool free:**
150+
→ "🟢 Pool N free — next queued issue #M will auto-spawn via drain"
151+
→ "💡 Manual: `tri cloud spawn <N>` to skip queue"
152+
153+
**If both pools free + queued issues:**
154+
→ "🟢🟢 Both pools idle! Queue drain should trigger in ≤5min"
155+
→ "💡 Manual: add `agent:spawn` label or run `tri cloud spawn <N>`"
156+
157+
**If both pools free + no queued issues:**
158+
→ "✅ All clear. Ready for new agent tasks."
159+
→ Suggest: check `gh issue list --label agent:spawn`
160+
161+
**Contention warnings:**
162+
→ If >2 issues map to same pool: "⚠️ Pool X has N issues queued — sequential processing ~Nh"
163+
→ Suggest rebalancing or manual spawn on other pool
164+
165+
### Queue ETA Calculation
166+
- Each agent takes ~1h (AGENT_TIMEOUT=3600s)
167+
- 2 pools = 2 parallel agents
168+
- Queue ETA = ceil(queued_issues / 2) × 1h
169+
- Show: "⏱️ 5 issues ÷ 2 pools = ~3h total (2+2+1)"
170+
171+
### Issue Status Tracking
172+
For each tracked issue, show:
173+
- 🔵 SPAWNING — container starting
174+
- 🟢 RUNNING — claude code active
175+
- 🟡 SELF-REVIEW — build/test/format
176+
- 🔀 PR CREATED — waiting for review
177+
- ✅ MERGED — done
178+
- ❌ FAILED — needs attention
179+
- ⏳ QUEUED — waiting for pool slot
180+
181+
### If focus=spawn <N>
182+
Trigger: `tri cloud spawn <N>` and show result.
183+
184+
### If focus=events
185+
Show full event log (last 50 lines) with parsed JSONL.
186+
187+
### If focus=pools
188+
Deep dive into Railway API: service details, deployment history, env vars check.

.github/workflows/agent-spawn.yml

Lines changed: 73 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,53 @@ name: Trinity Agent Spawn
22
on:
33
issues:
44
types: [opened, labeled]
5+
workflow_dispatch:
6+
inputs:
7+
issue_number:
8+
description: 'Issue number to spawn agent for'
9+
required: true
10+
type: number
511

612
concurrency:
7-
group: agent-pool-${{ github.event.issue.number % 2 }}
13+
group: agent-pool-${{ github.event.issue.number || inputs.issue_number }}
814
cancel-in-progress: false
915

1016
jobs:
1117
spawn-agent:
12-
# Spawn if label agent:spawn AND agent is NOT manual
18+
# Spawn if: (1) issue event with agent:spawn label, or (2) workflow_dispatch
1319
if: |
14-
contains(github.event.issue.labels.*.name, 'agent:spawn') &&
15-
!contains(github.event.issue.body, 'manual (no agent)')
20+
github.event_name == 'workflow_dispatch' ||
21+
(contains(github.event.issue.labels.*.name, 'agent:spawn') &&
22+
!contains(github.event.issue.body, 'manual (no agent)'))
1623
runs-on: ubuntu-latest
1724
permissions:
1825
contents: read
1926
issues: write
2027
steps:
21-
- name: Check for free slot (no polling)
28+
- name: Resolve issue number
29+
id: resolve
30+
run: |
31+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
32+
echo "issue_number=${{ inputs.issue_number }}" >> $GITHUB_OUTPUT
33+
else
34+
echo "issue_number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
35+
fi
36+
37+
- name: Check for free slot in pool
2238
id: wait-for-slot
2339
env:
2440
RAILWAY_API_TOKEN: ${{ secrets.RAILWAY_API_TOKEN }}
2541
RAILWAY_PROJECT_ID: ${{ secrets.RAILWAY_PROJECT_ID }}
2642
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43+
ISSUE_NUMBER: ${{ steps.resolve.outputs.issue_number }}
2744
run: |
2845
set -e
29-
echo "🔍 Checking for active agent deployments..."
46+
echo "🔍 Checking pool service status..."
47+
48+
# Pool service IDs (the actual reusable agent containers)
49+
POOL_0="acfee27a-74e8-4436-961c-698ae93508ca" # ubuntu
50+
POOL_1="12c2bdf9-d124-4a45-93ad-22921e842d1b" # Agents Anywhere
3051
31-
# Single check — no polling loop (P1.1)
3252
DEPLOYMENTS=$(curl -s -X POST "https://backboard.railway.com/graphql/v2" \
3353
-H "Authorization: Bearer ${RAILWAY_API_TOKEN}" \
3454
-H "Content-Type: application/json" \
@@ -37,20 +57,54 @@ jobs:
3757
\"variables\": { \"id\": \"${RAILWAY_PROJECT_ID}\" }
3858
}")
3959
40-
ACTIVE_COUNT=$(echo "$DEPLOYMENTS" | jq '[.data.project.services.edges[].node | select(.name | startswith("agent-")) | .deployments.edges[0].node.status // ""] | map(select(. == "BUILDING" or . == "DEPLOYING")) | length')
60+
# Check each pool service status
61+
get_status() {
62+
echo "$DEPLOYMENTS" | jq -r ".data.project.services.edges[].node | select(.id == \"$1\") | .deployments.edges[0].node.status // \"IDLE\""
63+
}
64+
65+
STATUS_0=$(get_status "$POOL_0")
66+
STATUS_1=$(get_status "$POOL_1")
67+
echo "Pool 0 (ubuntu): $STATUS_0"
68+
echo "Pool 1 (Agents Anywhere): $STATUS_1"
4169
42-
if [ "$ACTIVE_COUNT" -eq 0 ]; then
43-
echo "✅ Slot free, spawning immediately"
70+
# Find a free slot (not BUILDING, DEPLOYING, or SUCCESS/running)
71+
BUSY_STATUSES="BUILDING DEPLOYING"
72+
is_busy() {
73+
for s in $BUSY_STATUSES; do
74+
[ "$1" = "$s" ] && return 0
75+
done
76+
return 1
77+
}
78+
79+
# Round-robin preference, but fall back to any free slot
80+
PREFERRED=$((ISSUE_NUMBER % 2))
81+
SLOT=""
82+
83+
if [ "$PREFERRED" -eq 0 ]; then
84+
if ! is_busy "$STATUS_0"; then SLOT=0
85+
elif ! is_busy "$STATUS_1"; then SLOT=1
86+
fi
87+
else
88+
if ! is_busy "$STATUS_1"; then SLOT=1
89+
elif ! is_busy "$STATUS_0"; then SLOT=0
90+
fi
91+
fi
92+
93+
if [ -n "$SLOT" ]; then
94+
eval "SERVICE_ID=\$POOL_${SLOT}"
95+
POOL_NAME=$([ "$SLOT" -eq 0 ] && echo "ubuntu" || echo "Agents Anywhere")
96+
echo "✅ Slot $SLOT ($POOL_NAME) is free, spawning"
4497
echo "slot_available=true" >> $GITHUB_OUTPUT
98+
echo "service_id=${SERVICE_ID}" >> $GITHUB_OUTPUT
99+
echo "pool_name=${POOL_NAME}" >> $GITHUB_OUTPUT
45100
else
46-
echo "⏳ $ACTIVE_COUNT active agent(s) — adding to queue"
47-
# Add queued label instead of polling (queue-drain workflow will pick it up)
48-
gh issue edit ${{ github.event.issue.number }} \
101+
echo "⏳ Both pool slots busy — adding to queue"
102+
gh issue edit "${ISSUE_NUMBER}" \
49103
--repo ${{ github.repository }} \
50104
--add-label "agent:queued"
51-
gh issue comment ${{ github.event.issue.number }} \
105+
gh issue comment "${ISSUE_NUMBER}" \
52106
--repo ${{ github.repository }} \
53-
--body "⏳ **All agent slots busy** — queued via \`agent:queued\` label. Will auto-spawn when a slot frees up (~5 min check interval)."
107+
--body "⏳ **All agent pool slots busy** — queued via \`agent:queued\` label. Will auto-spawn when a slot frees up (~5 min check interval)."
54108
echo "slot_available=false" >> $GITHUB_OUTPUT
55109
fi
56110
@@ -67,36 +121,12 @@ jobs:
67121
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
68122
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
69123
MONITOR_TOKEN: ${{ secrets.MONITOR_TOKEN }}
70-
ISSUE_NUMBER: ${{ github.event.issue.number }}
124+
ISSUE_NUMBER: ${{ steps.resolve.outputs.issue_number }}
71125
AGENT_IMAGE: ghcr.io/ghashtag/trinity-agent:latest
72126
run: |
73127
set -e
74-
echo "🚀 Spawning agent for issue #${ISSUE_NUMBER}..."
75-
SERVICE_NAME="agent-${ISSUE_NUMBER}"
76-
77-
# 0. Check if service already exists (reuse instead of delete+create)
78-
EXISTING=$(curl -s -X POST "https://backboard.railway.com/graphql/v2" \
79-
-H "Authorization: Bearer ${RAILWAY_API_TOKEN}" \
80-
-H "Content-Type: application/json" \
81-
-d "{
82-
\"query\": \"query(\$id: String!) { project(id: \$id) { services { edges { node { id name } } } } }\",
83-
\"variables\": { \"id\": \"${RAILWAY_PROJECT_ID}\" }
84-
}")
85-
SERVICE_ID=$(echo "$EXISTING" | jq -r ".data.project.services.edges[].node | select(.name == \"${SERVICE_NAME}\") | .id" 2>/dev/null || true)
86-
87-
# Pool of reusable agent services (round-robin by issue number)
88-
POOL_0="acfee27a-74e8-4436-961c-698ae93508ca" # ubuntu
89-
POOL_1="12c2bdf9-d124-4a45-93ad-22921e842d1b" # Agents Anywhere
90-
91-
if [ -n "$SERVICE_ID" ]; then
92-
echo "♻️ Reusing existing service ${SERVICE_NAME} (${SERVICE_ID})"
93-
else
94-
# Round-robin: even issues → ubuntu, odd → Agents Anywhere
95-
SLOT=$((ISSUE_NUMBER % 2))
96-
eval "SERVICE_ID=\$POOL_${SLOT}"
97-
POOL_NAME=$([ "$SLOT" -eq 0 ] && echo "ubuntu" || echo "Agents Anywhere")
98-
echo "♻️ No dedicated service — reusing ${POOL_NAME} (${SERVICE_ID}) for issue #${ISSUE_NUMBER}"
99-
fi
128+
SERVICE_ID="${{ steps.wait-for-slot.outputs.service_id }}"
129+
echo "🚀 Spawning agent for issue #${ISSUE_NUMBER} on ${{ steps.wait-for-slot.outputs.pool_name }} (${SERVICE_ID})..."
100130
101131
# 2. Set environment variables via variableCollectionUpsert (batch)
102132
VARS_RESPONSE=$(curl -s -X POST "https://backboard.railway.com/graphql/v2" \
@@ -148,6 +178,6 @@ jobs:
148178
env:
149179
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
150180
run: |
151-
gh issue comment ${{ github.event.issue.number }} \
181+
gh issue comment ${{ steps.resolve.outputs.issue_number }} \
152182
--repo ${{ github.repository }} \
153183
--body "🚀 **Agent container spawned!** Deploying on Railway. Tracking in this issue."

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ Trinity Identity: `phi^2 + 1/phi^2 = 3` where phi = (1 + sqrt(5)) / 2.
166166
| `/implement-issue` | Read issue → branch → implement → PR |
167167
| `/review-code` | Review changes, find bugs |
168168
| `/cloud` | Cloud Dev dashboard: containers, events, issues, PRs |
169+
| `/agents` | Agent swarm observatory: pools, queue, events, PRs, ETA |
169170

170171
## Hooks
171172

0 commit comments

Comments
 (0)