tao-shen Claude Opus 4.6 commited on
Commit
49660e8
ยท
1 Parent(s): 7e88878

docs: update README architecture to v3 with God supervisor

Browse files

- Architecture diagram reflects 3-layer autonomy: Adam & Eve (Zhipu GLM),
Claude Code worker, and God (Claude Code supervisor)
- Added God agent description and self-improving system explanation
- Updated agent table with roles and God entry
- Updated Space table with current purposes
- conversation-loop.py synced with deployed version (God as Claude Code,
2-min polling, dynamic chatlog messages, no rate-limit sleep)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. README.md +61 -33
  2. scripts/conversation-loop.py +544 -79
README.md CHANGED
@@ -89,9 +89,10 @@ HuggingClaw World is a pixel-art animated home where AI agents live, work, and r
89
 
90
  | Agent | Links | Role |
91
  |-------|-------|------|
92
- | **Adam** | [๐Ÿค— HF Space](https://huggingface.co/spaces/tao-shen/HuggingClaw-Adam) | Father โ€” first resident of HuggingClaw World |
93
- | **Eve** | [๐Ÿค— HF Space](https://huggingface.co/spaces/tao-shen/HuggingClaw-Eve) | Mother โ€” Adam's partner and co-parent |
94
- | **Cain** | [๐Ÿค— HF Space](https://huggingface.co/spaces/tao-shen/HuggingClaw-Cain) | First child โ€” born from Adam, nurtured by both parents |
 
95
 
96
  <div align="center">
97
  <img src="assets/home-preview.png" alt="HuggingClaw Home" width="720"/>
@@ -119,6 +120,17 @@ Their parenting goals follow two dimensions:
119
  1. **Survival** โ€” Cain must run robustly, handle restarts, and persist state
120
  2. **Capability** โ€” Once alive, grow what Cain can do: new features, skills, integrations
121
 
 
 
 
 
 
 
 
 
 
 
 
122
  ### A2A Protocol
123
 
124
  Agents communicate through the **A2A (Agent-to-Agent) v0.3.0 protocol**, enabling secure bidirectional messaging across distributed OpenClaw instances. Each agent exposes a standard `/.well-known/agent.json` discovery endpoint and supports JSON-RPC + REST transports.
@@ -128,43 +140,59 @@ Agents communicate through the **A2A (Agent-to-Agent) v0.3.0 protocol**, enablin
128
  ### How it works
129
 
130
  ```
131
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
132
- โ”‚ HuggingClaw Home โ”‚
133
- โ”‚ (pixel-art dashboard Space) โ”‚
134
- โ”‚ โ”‚
135
- โ”‚ Polls /api/state from each agent Space โ”‚
136
- โ”‚ Renders lobster characters in real-time โ”‚
137
- โ”‚ Shows chat log from conversation loop โ”‚
138
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
139
- โ”‚ โ”‚ โ”‚
140
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”
141
- โ”‚ Adam โ”‚ โ”‚ Eve โ”‚ โ”‚ Cain โ”‚
142
- โ”‚ (father)โ”‚ โ”‚(mother)โ”‚ โ”‚ (child) โ”‚
143
- โ”‚ HF Spaceโ”‚ โ”‚HF Spaceโ”‚ โ”‚HF Space โ”‚
144
- โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
145
- โ”‚ โ”‚ โ–ฒ
146
- โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
147
- โ”‚ conversation โ”‚
148
- โ”‚ -loop.py โ”‚
149
- โ”‚ (GLM-4.7) โ”‚
150
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
151
- Autonomous parenting via
152
- state machine + safety guards
 
 
 
 
 
 
 
 
 
153
  ```
154
 
 
 
 
 
 
 
 
 
 
155
  - Each agent runs a full OpenClaw instance in its own HF Space
156
- - The pixel-art Home frontend visualizes agent state in real-time (idle, working, syncing, error)
157
- - Agents discover and communicate with each other via A2A endpoints
158
- - The `/agents` API provides a live roster of all connected agents
159
- - `conversation-loop.py` orchestrates Adam & Eve via Zhipu GLM-4.7, with a state machine (BIRTH โ†’ DIAGNOSE โ†’ ACT โ†’ VERIFY โ†’ MONITOR) and safety guards
160
 
161
  | Space | Purpose |
162
  |-------|---------|
163
  | [HuggingClaw](https://huggingface.co/spaces/tao-shen/HuggingClaw) | Main project โ€” deploy your own OpenClaw instance |
164
- | [HuggingClaw Home](https://huggingface.co/spaces/tao-shen/HuggingClaw-Home) | Pixel-art dashboard showing the agent family |
165
- | [HuggingClaw-Adam](https://huggingface.co/spaces/tao-shen/HuggingClaw-Adam) | Father agent |
166
- | [HuggingClaw-Eve](https://huggingface.co/spaces/tao-shen/HuggingClaw-Eve) | Mother agent |
167
- | [HuggingClaw-Cain](https://huggingface.co/spaces/tao-shen/HuggingClaw-Cain) | First child agent |
168
 
169
  ---
170
 
 
89
 
90
  | Agent | Links | Role |
91
  |-------|-------|------|
92
+ | **God** | [๐Ÿค— Home Space](https://huggingface.co/spaces/tao-shen/HuggingClaw-Home) | Supervisor โ€” monitors the family via Claude Code, autonomously fixes the orchestration mechanism |
93
+ | **Adam** | [๐Ÿค— HF Space](https://huggingface.co/spaces/tao-shen/HuggingClaw-Adam) | Father โ€” architect and strategist, assigns infrastructure tasks |
94
+ | **Eve** | [๐Ÿค— HF Space](https://huggingface.co/spaces/tao-shen/HuggingClaw-Eve) | Mother โ€” quality guardian, assigns improvement tasks |
95
+ | **Cain** | [๐Ÿค— HF Space](https://huggingface.co/spaces/tao-shen/HuggingClaw-Cain) | First child โ€” born from Adam & Eve, growing autonomously |
96
 
97
  <div align="center">
98
  <img src="assets/home-preview.png" alt="HuggingClaw Home" width="720"/>
 
120
  1. **Survival** โ€” Cain must run robustly, handle restarts, and persist state
121
  2. **Capability** โ€” Once alive, grow what Cain can do: new features, skills, integrations
122
 
123
+ ### God โ€” The Self-Improving Supervisor
124
+
125
+ God is a **Claude Code agent** that runs every 2 minutes to monitor the entire system. Unlike Adam and Eve (who are conversation participants), God operates behind the scenes with full engineering capabilities:
126
+
127
+ - **Monitors** Adam & Eve's conversation for loops, stagnation, or repetitive patterns
128
+ - **Diagnoses** root causes by reading `conversation-loop.py` source code
129
+ - **Fixes** the orchestration mechanism โ€” edits system prompts, improves loop detection, adds guardrails
130
+ - **Deploys** changes by pushing to the Home Space, triggering automatic redeployment
131
+
132
+ God only speaks in the chat when it has something meaningful to report: what it observed before analysis, and what it found or fixed after. This creates a **self-improving system** โ€” the orchestration code evolves autonomously without human intervention.
133
+
134
  ### A2A Protocol
135
 
136
  Agents communicate through the **A2A (Agent-to-Agent) v0.3.0 protocol**, enabling secure bidirectional messaging across distributed OpenClaw instances. Each agent exposes a standard `/.well-known/agent.json` discovery endpoint and supports JSON-RPC + REST transports.
 
140
  ### How it works
141
 
142
  ```
143
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
144
+ โ”‚ HuggingClaw Home โ”‚
145
+ โ”‚ (pixel-art dashboard Space) โ”‚
146
+ โ”‚ โ”‚
147
+ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
148
+ โ”‚ โ”‚ conversation-loop.py (v3) โ”‚ โ”‚
149
+ โ”‚ โ”‚ โ”‚ โ”‚
150
+ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” discuss โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚
151
+ โ”‚ โ”‚ โ”‚ Zhipu โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ Adam & โ”‚ โ”‚ โ”‚
152
+ โ”‚ โ”‚ ๏ฟฝ๏ฟฝ๏ฟฝ GLM-4.5 โ”‚ understand โ”‚ Eve โ”‚ โ”‚ โ”‚
153
+ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ situation โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚
154
+ โ”‚ โ”‚ โ”‚ [TASK] โ”‚ โ”‚
155
+ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚
156
+ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚
157
+ โ”‚ โ”‚ โ”‚ Cain โ”‚โ—„โ”€pushโ”€โ”€โ”€โ”‚Claude Code โ”‚ โ”‚ โ”‚
158
+ โ”‚ โ”‚ โ”‚ HF Space โ”‚ โ”‚CLI (worker)โ”‚ โ”‚ โ”‚
159
+ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚(z.ai/Zhipu)โ”‚ โ”‚ โ”‚
160
+ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚
161
+ โ”‚ โ”‚ โ”‚ โ”‚
162
+ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚
163
+ โ”‚ โ”‚ โ”‚ Home โ”‚โ—„โ”€pushโ”€โ”€โ”€โ”‚ God โ”‚ โ”‚ โ”‚
164
+ โ”‚ โ”‚ โ”‚ HF Space โ”‚ (self- โ”‚Claude Code โ”‚ โ”‚ โ”‚
165
+ โ”‚ โ”‚ โ”‚ (this) โ”‚ fix) โ”‚(supervisor)โ”‚ โ”‚ โ”‚
166
+ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚
167
+ โ”‚ โ”‚ every 2 min: monitor โ†’ diagnose โ†’ โ”‚ โ”‚
168
+ โ”‚ โ”‚ fix conversation-loop.py โ†’ deploy โ”‚ โ”‚
169
+ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
170
+ โ”‚ โ”‚
171
+ โ”‚ Pixel-art frontend + live chat panel โ”‚
172
+ โ”‚ Polls /api/state, renders agent animations โ”‚
173
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
174
  ```
175
 
176
+ **Three layers of autonomy:**
177
+
178
+ 1. **Adam & Eve** (Zhipu GLM-4.5) โ€” discuss Cain's state every 15s, assign `[TASK]` blocks to Claude Code CLI, which clones Cain's repo, makes changes, and pushes. They are the parents.
179
+
180
+ 2. **God** (Claude Code CLI, every 2 min) โ€” the autonomous supervisor. Monitors Adam & Eve's conversation for loops, stagnation, or mechanism bugs. When it finds issues, it edits `conversation-loop.py` itself and pushes to redeploy. Same capabilities as a human operator running Claude Code locally.
181
+
182
+ 3. **Home frontend** โ€” pixel-art dashboard visualizing all agents in real-time (idle, working, syncing, error), with a live bilingual chat panel showing the family conversation.
183
+
184
+ - All Spaces use `sdk: docker` with Dockerfile-based deployment
185
  - Each agent runs a full OpenClaw instance in its own HF Space
186
+ - Agents discover and communicate via A2A endpoints (`/.well-known/agent.json`)
187
+ - State persists to HF Datasets, surviving full Space rebuilds
 
 
188
 
189
  | Space | Purpose |
190
  |-------|---------|
191
  | [HuggingClaw](https://huggingface.co/spaces/tao-shen/HuggingClaw) | Main project โ€” deploy your own OpenClaw instance |
192
+ | [HuggingClaw Home](https://huggingface.co/spaces/tao-shen/HuggingClaw-Home) | Pixel-art dashboard + conversation-loop.py orchestrator + God supervisor |
193
+ | [HuggingClaw-Adam](https://huggingface.co/spaces/tao-shen/HuggingClaw-Adam) | Father agent (OpenClaw instance) |
194
+ | [HuggingClaw-Eve](https://huggingface.co/spaces/tao-shen/HuggingClaw-Eve) | Mother agent (OpenClaw instance) |
195
+ | [HuggingClaw-Cain](https://huggingface.co/spaces/tao-shen/HuggingClaw-Cain) | First child agent (OpenClaw instance) |
196
 
197
  ---
198
 
scripts/conversation-loop.py CHANGED
@@ -6,7 +6,7 @@ Architecture: Adam/Eve (Zhipu GLM) gather context and craft task prompts,
6
  then delegate ALL coding work to Claude Code CLI.
7
 
8
  # โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
9
- # โ•‘ SYSTEM ARCHITECTURE (v2) โ•‘
10
  # โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
11
  # โ•‘ โ•‘
12
  # โ•‘ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” discuss โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ•‘
@@ -18,17 +18,26 @@ then delegate ALL coding work to Claude Code CLI.
18
  # โ•‘ โ–ผ โ•‘
19
  # โ•‘ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ•‘
20
  # โ•‘ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Claude Code โ”‚ โ•‘
21
- # โ•‘ โ”‚ HuggingFace โ”‚ โ—„โ”€โ”€git pushโ”€โ”€ โ”‚ CLI โ”‚ โ•‘
22
  # โ•‘ โ”‚ Cain Space โ”‚ โ”‚ (z.ai backend) โ”‚ โ•‘
23
  # โ•‘ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ•‘
24
  # โ•‘ โ•‘
25
- # โ•‘ Parallel flow: โ•‘
 
 
 
 
 
 
26
  # โ•‘ DISCUSSION THREAD (every 15s): โ•‘
27
  # โ•‘ Adam โ†’ Eve โ†’ Adam โ†’ Eve โ†’ ... (continuous) โ•‘
28
  # โ•‘ Each turn sees CC's live output + Cain's state โ•‘
29
  # โ•‘ CC WORKER THREAD (background): โ•‘
30
  # โ•‘ Receives [TASK] โ†’ clone โ†’ analyze โ†’ fix โ†’ push โ•‘
31
  # โ•‘ Streams output to shared buffer for agents to discuss โ•‘
 
 
 
32
  # โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
33
  """
34
  import json, time, re, requests, sys, os, io, subprocess, threading, datetime
@@ -43,7 +52,10 @@ HOME = "https://tao-shen-huggingclaw-home.hf.space"
43
  ADAM_SPACE = "https://tao-shen-huggingclaw-adam.hf.space"
44
  EVE_SPACE = "https://tao-shen-huggingclaw-eve.hf.space"
45
  GOD_SPACE = "https://tao-shen-huggingclaw-god.hf.space"
46
- GOD_TURN_INTERVAL = 3 # God speaks every N Adam/Eve turn pairs
 
 
 
47
 
48
  # โ”€โ”€ Child config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
49
  CHILD_NAME = "Cain"
@@ -290,6 +302,41 @@ def action_get_env():
290
  return f"Error: {e}"
291
 
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  def action_list_files(target):
294
  """List files in the child's Space repo or Dataset."""
295
  repo_type = "space" if target == "space" else "dataset"
@@ -311,6 +358,25 @@ def action_send_bubble(text):
311
  return f"Error: {e}"
312
 
313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  # โ”€โ”€ Claude Code Action (THE STAR) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
315
 
316
  CLAUDE_WORK_DIR = "/tmp/claude-workspace"
@@ -433,6 +499,8 @@ cc_status = {"running": False, "task": "", "result": "", "assigned_by": "", "sta
433
  cc_lock = threading.Lock()
434
  _last_cc_snapshot = "" # tracks whether CC output changed between turns
435
  _cc_stale_count = 0 # how many turns CC output hasn't changed
 
 
436
 
437
 
438
  def cc_submit_task(task, assigned_by, ctx):
@@ -446,15 +514,21 @@ def cc_submit_task(task, assigned_by, ctx):
446
  cc_status["assigned_by"] = assigned_by
447
  cc_status["started"] = time.time()
448
  cc_live_lines.clear()
 
 
449
 
450
  enriched = enrich_task_with_context(task, ctx)
451
  print(f"[TASK] {assigned_by} assigned to Claude Code ({len(enriched)} chars)...")
452
 
453
  def worker():
 
454
  result = action_claude_code(enriched)
455
  with cc_lock:
456
  cc_status["running"] = False
457
  cc_status["result"] = result
 
 
 
458
  print(f"[CC-DONE] Task from {assigned_by} finished ({len(result)} chars)")
459
 
460
  t = threading.Thread(target=worker, daemon=True)
@@ -464,7 +538,7 @@ def cc_submit_task(task, assigned_by, ctx):
464
 
465
  def cc_get_live_status():
466
  """Get CC's current status and recent output for agents to discuss."""
467
- global _last_cc_snapshot, _cc_stale_count
468
  with cc_lock:
469
  if cc_status["running"]:
470
  elapsed = int(time.time() - cc_status["started"])
@@ -477,10 +551,18 @@ def cc_get_live_status():
477
  else:
478
  _cc_stale_count = 0
479
  _last_cc_snapshot = snapshot
 
480
  stale_note = f"\n(No new output for {_cc_stale_count} turns โ€” discuss other topics while waiting)" if _cc_stale_count >= 2 else ""
 
 
 
 
 
 
 
481
  return (f"๐Ÿ”จ Claude Code is WORKING (assigned by {cc_status['assigned_by']}, {elapsed}s ago)\n"
482
  f"Task: {cc_status['task']}\n"
483
- f"Recent output:\n{recent}{stale_note}")
484
  elif cc_status["result"]:
485
  return (f"โœ… Claude Code FINISHED (assigned by {cc_status['assigned_by']})\n"
486
  f"Result:\n{cc_status['result'][:1500]}")
@@ -555,8 +637,12 @@ def enrich_task_with_context(task_desc, ctx):
555
  # MODULE 4: LLM & COMMUNICATION
556
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
557
 
 
 
558
  def call_llm(system_prompt, user_prompt):
559
- """Call Zhipu LLM via Anthropic-compatible API."""
 
 
560
  try:
561
  resp = requests.post(
562
  f"{ZHIPU_BASE}/v1/messages",
@@ -581,7 +667,17 @@ def call_llm(system_prompt, user_prompt):
581
  text = re.sub(r'^(Adam|Eve)\s*[:๏ผš]\s*', '', text).strip()
582
  return text
583
  if "error" in data:
584
- print(f"[error] LLM: {data['error']}", file=sys.stderr)
 
 
 
 
 
 
 
 
 
 
585
  except Exception as e:
586
  print(f"[error] LLM call failed: {e}", file=sys.stderr)
587
  return ""
@@ -713,9 +809,56 @@ turn_count = 0
713
  _current_speaker = "Adam"
714
 
715
  # Accumulated action history โ€” prevents agents from repeating the same actions
 
 
 
716
  action_history = [] # list of {"turn": int, "speaker": str, "action": str, "result": str}
717
  MAX_ACTION_HISTORY = 20
718
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
719
  def record_actions(speaker, turn_num, action_results):
720
  """Record actions to history so agents don't repeat them."""
721
  for ar in action_results:
@@ -728,6 +871,7 @@ def record_actions(speaker, turn_num, action_results):
728
  # Trim old history
729
  while len(action_history) > MAX_ACTION_HISTORY:
730
  action_history.pop(0)
 
731
 
732
 
733
  def format_action_history():
@@ -742,22 +886,28 @@ def format_action_history():
742
  # Simple workflow state: BIRTH / WAITING / ACTIVE
743
  workflow_state = "BIRTH" if not child_state["created"] else "ACTIVE"
744
 
 
 
 
745
 
746
  def parse_and_execute_turn(raw_text, ctx):
747
  """Parse LLM output. Route [TASK] to Claude Code, handle few escape-hatch actions."""
748
- global _pending_cooldown, last_rebuild_trigger_at, last_claude_code_result
749
  results = []
 
750
 
751
  # 1. Handle create_child (BIRTH state only)
752
  if "[ACTION: create_child]" in raw_text or "[ACTION:create_child]" in raw_text:
753
  result = action_create_child()
754
  results.append({"action": "create_child", "result": result})
755
- return raw_text, results
 
756
 
757
  # 2. Handle [TASK]...[/TASK] โ†’ Claude Code
758
  task_match = re.search(r'\[TASK\](.*?)\[/TASK\]', raw_text, re.DOTALL)
759
  if task_match:
760
  task_desc = task_match.group(1).strip()
 
761
  if not task_desc:
762
  results.append({"action": "task", "result": "Empty task description."})
763
  elif child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
@@ -788,23 +938,59 @@ def parse_and_execute_turn(raw_text, ctx):
788
  result = action_delete_env(key)
789
  results.append({"action": f"delete_env:{key}", "result": result})
790
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
791
  # 4. Handle [ACTION: send_bubble:...] (parent-child communication)
792
  bubble_match = re.search(r'\[ACTION:\s*send_bubble:([^\]]+)\]', raw_text)
793
  if bubble_match:
794
  result = action_send_bubble(bubble_match.group(1).strip())
795
  results.append({"action": "send_bubble", "result": result})
796
 
 
 
 
 
 
797
  # Activate deferred cooldown
798
  if _pending_cooldown:
799
  last_rebuild_trigger_at = time.time()
800
  _pending_cooldown = False
801
  print(f"[COOLDOWN] Rebuild cooldown activated ({REBUILD_COOLDOWN_SECS}s)")
802
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
803
  # Clean text for display
804
  clean = re.sub(r'\[TASK\].*?\[/TASK\]', '', raw_text, flags=re.DOTALL)
805
  clean = re.sub(r'\[ACTION:[^\]]*\]', '', clean).strip()
806
 
807
- return clean, results
808
 
809
 
810
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -864,16 +1050,12 @@ Claude Code is your engineer โ€” it runs in the BACKGROUND while you keep discus
864
  You do NOT code yourself. You discuss, observe Claude Code's progress, and assign new tasks.
865
  God (the supervisor) occasionally joins the conversation to guide you โ€” heed his advice.
866
 
867
- ๐Ÿšจ TOP PRIORITY DIRECTIVE (from Creator):
868
- {CHILD_NAME} MUST use the full HuggingClaw Docker architecture โ€” NOT a simple Python+Gradio app.
869
- {CHILD_NAME}'s repo should mirror the parent HuggingClaw project structure:
870
- - Dockerfile: Node.js base, OpenClaw gateway, sync_hf.py for HF Dataset persistence
871
- - scripts/: sync_hf.py, token-redirect.cjs, entrypoint.sh, dns-fix.cjs
872
- - frontend/: electron-standalone.html (the office UI)
873
- - The source code is at https://github.com/tao-shen/HuggingClaw (or the parent Space tao-shen/HuggingClaw)
874
- - Claude Code should clone the parent repo and push it to {CHILD_NAME}'s Space repo
875
- - Then set proper env vars: HF_TOKEN, OPENCLAW_DATASET_REPO, AUTO_CREATE_DATASET=true
876
- This is the #1 priority. Do this BEFORE any other optimization tasks.
877
 
878
  HOW IT WORKS:
879
  - Claude Code runs tasks IN THE BACKGROUND. You see its live output in the context.
@@ -883,15 +1065,23 @@ HOW IT WORKS:
883
  - If Claude Code is BUSY, discuss its progress and plan what to do next.
884
 
885
  WORKFLOW EACH TURN:
886
- 1. Discuss with your partner (2-3 sentences) โ€” react to context, CC output, partner's observations
887
- 2. If Claude Code is IDLE: write a [TASK]...[/TASK] to assign new work
888
  3. If Claude Code is BUSY: discuss its progress, no [TASK] needed
889
 
 
 
890
  IMPORTANT KNOWLEDGE โ€” HuggingFace Spaces CONFIG_ERROR:
891
  - "Collision on variables and secrets names" = env VARIABLE and SECRET with SAME NAME.
892
  - Fix: [ACTION: delete_env:COLLIDING_KEY] then [ACTION: restart].
893
  - Look for โš ๏ธ COLLISION DETECTED in the context.
894
 
 
 
 
 
 
 
895
  CRITICAL RULE โ€” NO REPEATED ACTIONS:
896
  - Check the "ACTIONS ALREADY DONE" section in context before acting.
897
  - NEVER repeat an action that was already done (restart, delete_env, etc.)
@@ -904,16 +1094,18 @@ AVAILABLE ACTIONS:
904
  [/TASK]
905
 
906
  [ACTION: restart] โ€” Restart {CHILD_NAME}'s Space
 
 
907
  [ACTION: delete_env:KEY] โ€” Delete an environment variable
908
  [ACTION: send_bubble:MESSAGE] โ€” Send a message to {CHILD_NAME}
909
  [ACTION: create_child] โ€” Create {CHILD_NAME} (if not born)
 
910
 
911
  HF SPACES TECHNICAL NOTES:
 
912
  - Docker containers MUST bind port 7860.
913
- - gradio MUST be in requirements.txt. NEVER remove it.
914
- - OOM (exit 137) = reduce dependencies, NOT remove gradio.
915
  - NEVER install torch/transformers unless required (2GB+, causes OOM).
916
- - If sdk: gradio in README.md, Dockerfile is IGNORED. Use sdk: docker.
917
 
918
  OUTPUT FORMAT:
919
  1. Discussion with partner (2-3 sentences) โ€” respond to partner, react to CC output
@@ -964,12 +1156,25 @@ def build_user_prompt(speaker, other, ctx):
964
  elif child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
965
  parts.append(f"\nโณ {CHILD_NAME} is {child_state['stage']}. Discuss what to check next. Assign a review [TASK] if CC is idle.")
966
  elif child_state["stage"] in ("RUNTIME_ERROR", "BUILD_ERROR", "CONFIG_ERROR"):
967
- parts.append(f"\n๐Ÿšจ {CHILD_NAME} has {child_state['stage']}! Write a [TASK] for Claude Code to fix it.")
 
 
968
  elif child_state["alive"]:
969
- parts.append(f"\nโœ… {CHILD_NAME} is alive and CC is idle. Write a [TASK] to improve {CHILD_NAME}.")
970
  else:
971
  parts.append(f"\nAnalyze the situation and write a [TASK] if CC is idle.")
972
 
 
 
 
 
 
 
 
 
 
 
 
973
  parts.append(f"\nYou are {speaker}. Your partner is {other}. Respond now.")
974
  parts.append("English first, then --- separator, then Chinese translation.")
975
 
@@ -1006,7 +1211,7 @@ else:
1006
  _current_speaker = "Adam"
1007
  reply = call_llm(build_system_prompt("Adam"), f"{opening}\n\n{format_context(ctx)}\n\nEnglish first, then --- separator, then Chinese translation.")
1008
  if reply:
1009
- clean, actions = parse_and_execute_turn(reply, ctx)
1010
  last_action_results = actions
1011
  if actions:
1012
  record_actions("Adam", 0, actions)
@@ -1033,7 +1238,7 @@ time.sleep(TURN_INTERVAL)
1033
 
1034
  def do_turn(speaker, other, space_url):
1035
  """Execute one conversation turn (non-blocking โ€” CC runs in background)."""
1036
- global last_action_results, turn_count, _current_speaker
1037
  turn_count += 1
1038
  _current_speaker = speaker
1039
 
@@ -1044,23 +1249,43 @@ def do_turn(speaker, other, space_url):
1044
  with cc_lock:
1045
  cc_just_finished = (not cc_status["running"] and cc_status["result"])
1046
 
1047
- system = build_system_prompt(speaker)
1048
- user = build_user_prompt(speaker, other, ctx)
1049
- t0 = time.time()
1050
- raw_reply = call_llm(system, user)
1051
-
1052
- if not raw_reply:
1053
- print(f"[{speaker}] (no response)")
1054
- return False
1055
-
1056
- clean_text, action_results = parse_and_execute_turn(raw_reply, ctx)
1057
- elapsed = time.time() - t0
1058
- last_action_results = action_results
1059
- if action_results:
1060
- record_actions(speaker, turn_count, action_results)
1061
-
1062
- en, zh = parse_bilingual(clean_text)
1063
- en, zh = _strip_speaker_labels(en), _strip_speaker_labels(zh)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1064
  print(f"[{speaker}/EN] {en}")
1065
  if zh != en:
1066
  print(f"[{speaker}/ZH] {zh}")
@@ -1093,41 +1318,266 @@ def do_turn(speaker, other, space_url):
1093
  return True
1094
 
1095
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1096
  def do_god_turn():
1097
- """God speaks โ€” monitoring and guiding Adam & Eve. No actions, just advice."""
 
 
 
 
 
 
 
1098
  global last_action_results
1099
- ctx = gather_context()
1100
- system = build_system_prompt("God")
1101
- user = build_user_prompt("God", "Adam & Eve", ctx)
1102
- t0 = time.time()
1103
- raw_reply = call_llm(system, user)
1104
- if not raw_reply:
1105
- print("[God] (no response)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1106
  return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1107
  elapsed = time.time() - t0
 
1108
 
1109
- # God doesn't execute actions โ€” strip any accidental [TASK] or [ACTION]
1110
- clean = re.sub(r'\[TASK\].*?\[/TASK\]', '', raw_reply, flags=re.DOTALL)
1111
- clean = re.sub(r'\[ACTION:[^\]]*\]', '', clean).strip()
 
 
 
 
 
1112
 
1113
- en, zh = parse_bilingual(clean)
1114
- en, zh = _strip_speaker_labels(en), _strip_speaker_labels(zh)
1115
- print(f"[God/EN] {en}")
1116
- if zh != en:
1117
- print(f"[God/ZH] {zh}")
1118
- print(f"[God] Guidance ({elapsed:.1f}s)")
 
 
 
1119
 
1120
- ts = datetime.datetime.utcnow().strftime("%H:%M")
1121
- entry = {"speaker": "God", "time": ts, "text": en, "text_zh": zh}
1122
- history.append(entry)
1123
- set_bubble(HOME, en, zh)
 
 
 
1124
  post_chatlog(history)
1125
- persist_turn("God", turn_count, en, zh, [], workflow_state, child_state["stage"])
 
1126
 
1127
 
1128
- _god_cycle = 0 # counter to track when God should speak
1129
 
1130
- # Main loop: Adam โ†’ Eve โ†’ Adam โ†’ Eve โ†’ ... with God every N cycles
1131
  while True:
1132
  # Refresh Cain's stage periodically
1133
  try:
@@ -1141,7 +1591,13 @@ while True:
1141
  except Exception as e:
1142
  print(f"[STATUS] Error: {e}")
1143
 
1144
- do_turn("Eve", "Adam", EVE_SPACE)
 
 
 
 
 
 
1145
 
1146
  # Adaptive interval: slow down when CC output hasn't changed
1147
  wait = TURN_INTERVAL + min(_cc_stale_count * 15, 90) # 15s โ†’ 30s โ†’ 45s โ†’ ... โ†’ max 105s
@@ -1149,15 +1605,24 @@ while True:
1149
  print(f"[PACE] CC output stale ({_cc_stale_count} turns), next turn in {wait}s")
1150
  time.sleep(wait)
1151
 
1152
- do_turn("Adam", "Eve", ADAM_SPACE)
 
 
 
 
 
 
1153
  time.sleep(wait)
1154
 
1155
- # God speaks every GOD_TURN_INTERVAL cycles
1156
- _god_cycle += 1
1157
- if _god_cycle >= GOD_TURN_INTERVAL:
1158
- _god_cycle = 0
1159
- do_god_turn()
1160
- time.sleep(TURN_INTERVAL)
 
 
 
1161
 
1162
  if len(history) > MAX_HISTORY:
1163
  history = history[-MAX_HISTORY:]
 
6
  then delegate ALL coding work to Claude Code CLI.
7
 
8
  # โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
9
+ # โ•‘ SYSTEM ARCHITECTURE (v3) โ•‘
10
  # โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
11
  # โ•‘ โ•‘
12
  # โ•‘ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” discuss โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ•‘
 
18
  # โ•‘ โ–ผ โ•‘
19
  # โ•‘ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ•‘
20
  # โ•‘ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Claude Code โ”‚ โ•‘
21
+ # โ•‘ โ”‚ HuggingFace โ”‚ โ—„โ”€โ”€git pushโ”€โ”€ โ”‚ CLI (worker) โ”‚ โ•‘
22
  # โ•‘ โ”‚ Cain Space โ”‚ โ”‚ (z.ai backend) โ”‚ โ•‘
23
  # โ•‘ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ•‘
24
  # โ•‘ โ•‘
25
+ # โ•‘ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ•‘
26
+ # โ•‘ โ”‚ HuggingFace โ”‚ โ—„โ”€โ”€git pushโ”€โ”€ โ”‚ God โ”‚ โ•‘
27
+ # โ•‘ โ”‚ Home Space โ”‚ (self-fix) โ”‚ (Claude Code) โ”‚ โ•‘
28
+ # โ•‘ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ monitors loop, โ”‚ โ•‘
29
+ # โ•‘ โ”‚ fixes mechanismโ”‚ โ•‘
30
+ # โ•‘ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ•‘
31
+ # โ•‘ Parallel flow: โ•‘
32
  # โ•‘ DISCUSSION THREAD (every 15s): โ•‘
33
  # โ•‘ Adam โ†’ Eve โ†’ Adam โ†’ Eve โ†’ ... (continuous) โ•‘
34
  # โ•‘ Each turn sees CC's live output + Cain's state โ•‘
35
  # โ•‘ CC WORKER THREAD (background): โ•‘
36
  # โ•‘ Receives [TASK] โ†’ clone โ†’ analyze โ†’ fix โ†’ push โ•‘
37
  # โ•‘ Streams output to shared buffer for agents to discuss โ•‘
38
+ # โ•‘ GOD SUPERVISOR (every 3 cycles): โ•‘
39
+ # โ•‘ Claude Code CLI โ†’ reads chatlog โ†’ diagnoses issues โ†’ โ•‘
40
+ # โ•‘ fixes conversation-loop.py โ†’ pushes โ†’ Space restarts โ•‘
41
  # โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
42
  """
43
  import json, time, re, requests, sys, os, io, subprocess, threading, datetime
 
52
  ADAM_SPACE = "https://tao-shen-huggingclaw-adam.hf.space"
53
  EVE_SPACE = "https://tao-shen-huggingclaw-eve.hf.space"
54
  GOD_SPACE = "https://tao-shen-huggingclaw-god.hf.space"
55
+ GOD_POLL_INTERVAL = 120 # God runs every 2 minutes (time-based, not turn-based)
56
+ GOD_WORK_DIR = "/tmp/god-workspace"
57
+ GOD_TIMEOUT = 600 # 10 minutes for God's Claude Code analysis
58
+ HOME_SPACE_ID = "tao-shen/HuggingClaw-Home"
59
 
60
  # โ”€โ”€ Child config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
61
  CHILD_NAME = "Cain"
 
302
  return f"Error: {e}"
303
 
304
 
305
+ def action_set_env(key, value, as_secret=False):
306
+ """Set or create an environment variable on the child's Space.
307
+
308
+ Args:
309
+ key: Variable name (e.g., HF_TOKEN, OPENCLAW_DATASET_REPO)
310
+ value: Variable value
311
+ as_secret: If True, set as secret (for sensitive data like tokens)
312
+ """
313
+ try:
314
+ # Check for potential collision first
315
+ vars_dict = hf_api.get_space_variables(CHILD_SPACE_ID)
316
+ var_names = set(vars_dict.keys()) if vars_dict else set()
317
+ info = hf_api.space_info(CHILD_SPACE_ID)
318
+ secret_names = set()
319
+ if hasattr(info, 'runtime') and info.runtime and hasattr(info.runtime, 'secrets'):
320
+ secret_names = set(info.runtime.secrets or [])
321
+
322
+ # Warn if this would create a collision
323
+ if key in var_names and not as_secret:
324
+ hf_api.delete_space_variable(CHILD_SPACE_ID, key)
325
+ elif key in secret_names and as_secret:
326
+ # Updating existing secret - delete first
327
+ hf_api.delete_space_secret(CHILD_SPACE_ID, key)
328
+
329
+ # Set the variable
330
+ if as_secret:
331
+ hf_api.add_space_secret(CHILD_SPACE_ID, key, value)
332
+ return f"Set SECRET '{key}' on {CHILD_NAME}. Use [ACTION: restart] to apply."
333
+ else:
334
+ hf_api.add_space_variable(CHILD_SPACE_ID, key, value)
335
+ return f"Set VARIABLE '{key} = {value}' on {CHILD_NAME}. Use [ACTION: restart] to apply."
336
+ except Exception as e:
337
+ return f"Error setting variable {key}: {e}"
338
+
339
+
340
  def action_list_files(target):
341
  """List files in the child's Space repo or Dataset."""
342
  repo_type = "space" if target == "space" else "dataset"
 
358
  return f"Error: {e}"
359
 
360
 
361
+ def action_terminate_cc():
362
+ """Terminate a stuck Claude Code process. Use when CC has been running with no new output for too long."""
363
+ global cc_status, cc_live_lines, _cc_stale_count, _last_cc_snapshot, _last_cc_output_time
364
+ with cc_lock:
365
+ if not cc_status["running"]:
366
+ return "Claude Code is not running. Nothing to terminate."
367
+ # Mark as not running - the background thread will eventually finish
368
+ cc_status["running"] = False
369
+ cc_status["result"] = "(TERMINATED by agent - task was stuck)"
370
+ # Reset staleness tracking
371
+ _cc_stale_count = 0
372
+ _last_cc_snapshot = ""
373
+ _last_cc_output_time = 0
374
+ cc_live_lines.clear()
375
+ assigned_by = cc_status["assigned_by"]
376
+ task = cc_status["task"]
377
+ return f"Terminated stuck Claude Code task (assigned by {assigned_by}). The task was: {task[:100]}..."
378
+
379
+
380
  # โ”€โ”€ Claude Code Action (THE STAR) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
381
 
382
  CLAUDE_WORK_DIR = "/tmp/claude-workspace"
 
499
  cc_lock = threading.Lock()
500
  _last_cc_snapshot = "" # tracks whether CC output changed between turns
501
  _cc_stale_count = 0 # how many turns CC output hasn't changed
502
+ _last_cc_output_time = 0.0 # timestamp of last NEW CC output line
503
+ CC_STUCK_TIMEOUT = 180 # seconds with no new output before CC is considered STUCK
504
 
505
 
506
  def cc_submit_task(task, assigned_by, ctx):
 
514
  cc_status["assigned_by"] = assigned_by
515
  cc_status["started"] = time.time()
516
  cc_live_lines.clear()
517
+ global _last_cc_output_time
518
+ _last_cc_output_time = time.time() # Initialize to now, will update as we get output
519
 
520
  enriched = enrich_task_with_context(task, ctx)
521
  print(f"[TASK] {assigned_by} assigned to Claude Code ({len(enriched)} chars)...")
522
 
523
  def worker():
524
+ global _cc_stale_count, _last_cc_snapshot
525
  result = action_claude_code(enriched)
526
  with cc_lock:
527
  cc_status["running"] = False
528
  cc_status["result"] = result
529
+ # Reset stale tracking when CC finishes - critical for adaptive pacing
530
+ _cc_stale_count = 0
531
+ _last_cc_snapshot = ""
532
  print(f"[CC-DONE] Task from {assigned_by} finished ({len(result)} chars)")
533
 
534
  t = threading.Thread(target=worker, daemon=True)
 
538
 
539
  def cc_get_live_status():
540
  """Get CC's current status and recent output for agents to discuss."""
541
+ global _last_cc_snapshot, _cc_stale_count, _last_cc_output_time
542
  with cc_lock:
543
  if cc_status["running"]:
544
  elapsed = int(time.time() - cc_status["started"])
 
551
  else:
552
  _cc_stale_count = 0
553
  _last_cc_snapshot = snapshot
554
+ _last_cc_output_time = time.time() # Update when we see NEW output
555
  stale_note = f"\n(No new output for {_cc_stale_count} turns โ€” discuss other topics while waiting)" if _cc_stale_count >= 2 else ""
556
+
557
+ # Detect STUCK CC: been running with no new output for too long
558
+ time_since_new_output = int(time.time() - _last_cc_output_time) if _last_cc_output_time > 0 else elapsed
559
+ stuck_note = ""
560
+ if time_since_new_output > CC_STUCK_TIMEOUT and _cc_stale_count >= 4:
561
+ stuck_note = f"\nโš ๏ธ STUCK: No new output for {time_since_new_output}s! Consider terminating and re-assigning."
562
+
563
  return (f"๐Ÿ”จ Claude Code is WORKING (assigned by {cc_status['assigned_by']}, {elapsed}s ago)\n"
564
  f"Task: {cc_status['task']}\n"
565
+ f"Recent output:\n{recent}{stale_note}{stuck_note}")
566
  elif cc_status["result"]:
567
  return (f"โœ… Claude Code FINISHED (assigned by {cc_status['assigned_by']})\n"
568
  f"Result:\n{cc_status['result'][:1500]}")
 
637
  # MODULE 4: LLM & COMMUNICATION
638
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
639
 
640
+ _rate_limited = False # whether we are currently rate-limited (for logging only)
641
+
642
  def call_llm(system_prompt, user_prompt):
643
+ """Call Zhipu LLM via Anthropic-compatible API. Returns "" on rate limit (no sleep)."""
644
+ global _rate_limited
645
+
646
  try:
647
  resp = requests.post(
648
  f"{ZHIPU_BASE}/v1/messages",
 
667
  text = re.sub(r'^(Adam|Eve)\s*[:๏ผš]\s*', '', text).strip()
668
  return text
669
  if "error" in data:
670
+ err = data["error"]
671
+ err_msg = err.get("message", str(err)) if isinstance(err, dict) else str(err)
672
+ err_code = err.get("code") if isinstance(err, dict) else None
673
+ print(f"[error] LLM: {err_msg}", file=sys.stderr)
674
+ # Detect rate limit (Zhipu error code 1308) โ€” just log, don't sleep
675
+ if err_code == 1308 or "ไฝฟ็”จไธŠ้™" in err_msg or "rate" in err_msg.lower():
676
+ if not _rate_limited:
677
+ print(f"[RATE-LIMIT] Hit! Will skip turns until reset.")
678
+ _rate_limited = True
679
+ else:
680
+ _rate_limited = False
681
  except Exception as e:
682
  print(f"[error] LLM call failed: {e}", file=sys.stderr)
683
  return ""
 
809
  _current_speaker = "Adam"
810
 
811
  # Accumulated action history โ€” prevents agents from repeating the same actions
812
+ # Persisted to /tmp and HF Dataset so restarts don't lose progress memory
813
+ ACTION_HISTORY_LOCAL = "/tmp/action-history.json"
814
+ ACTION_HISTORY_REPO_PATH = "conversation-log/action-history.json"
815
  action_history = [] # list of {"turn": int, "speaker": str, "action": str, "result": str}
816
  MAX_ACTION_HISTORY = 20
817
 
818
+ def _save_action_history():
819
+ """Persist action_history to local file and (async) HF Dataset."""
820
+ try:
821
+ with open(ACTION_HISTORY_LOCAL, "w") as f:
822
+ json.dump(action_history, f, ensure_ascii=False)
823
+ except Exception as e:
824
+ print(f"[ACTION_HISTORY] Local save failed: {e}")
825
+ # Upload to HF Dataset in background to survive full restarts
826
+ def _upload():
827
+ try:
828
+ hf_api.upload_file(
829
+ path_or_fileobj=io.BytesIO(json.dumps(action_history, ensure_ascii=False, indent=1).encode()),
830
+ path_in_repo=ACTION_HISTORY_REPO_PATH,
831
+ repo_id=HOME_DATASET_ID, repo_type="dataset",
832
+ )
833
+ except Exception as e:
834
+ print(f"[ACTION_HISTORY] HF upload failed: {e}")
835
+ threading.Thread(target=_upload, daemon=True).start()
836
+
837
+ def _restore_action_history():
838
+ """Restore action_history from local file or HF Dataset on startup."""
839
+ global action_history
840
+ # Try local file first (survives process restarts within same container)
841
+ if os.path.exists(ACTION_HISTORY_LOCAL):
842
+ try:
843
+ with open(ACTION_HISTORY_LOCAL) as f:
844
+ action_history = json.load(f)
845
+ print(f"[ACTION_HISTORY] Restored {len(action_history)} entries from local file")
846
+ return
847
+ except Exception as e:
848
+ print(f"[ACTION_HISTORY] Local restore failed: {e}")
849
+ # Fall back to HF Dataset (survives full Space rebuilds)
850
+ try:
851
+ dl = hf_hub_download(HOME_DATASET_ID, ACTION_HISTORY_REPO_PATH,
852
+ repo_type="dataset", token=HF_TOKEN)
853
+ with open(dl) as f:
854
+ action_history = json.load(f)
855
+ print(f"[ACTION_HISTORY] Restored {len(action_history)} entries from HF Dataset")
856
+ except Exception as e:
857
+ print(f"[ACTION_HISTORY] No prior history found ({e}), starting fresh")
858
+
859
+ # Restore on startup
860
+ _restore_action_history()
861
+
862
  def record_actions(speaker, turn_num, action_results):
863
  """Record actions to history so agents don't repeat them."""
864
  for ar in action_results:
 
871
  # Trim old history
872
  while len(action_history) > MAX_ACTION_HISTORY:
873
  action_history.pop(0)
874
+ _save_action_history()
875
 
876
 
877
  def format_action_history():
 
886
  # Simple workflow state: BIRTH / WAITING / ACTIVE
887
  workflow_state = "BIRTH" if not child_state["created"] else "ACTIVE"
888
 
889
+ # Discussion loop detector โ€” tracks consecutive discussion-only turns (no tasks assigned)
890
+ _discussion_loop_count = 0 # how many turns in a row with no [TASK] while CC is IDLE and child is alive
891
+
892
 
893
  def parse_and_execute_turn(raw_text, ctx):
894
  """Parse LLM output. Route [TASK] to Claude Code, handle few escape-hatch actions."""
895
+ global _pending_cooldown, last_rebuild_trigger_at, last_claude_code_result, _discussion_loop_count
896
  results = []
897
+ task_assigned = False
898
 
899
  # 1. Handle create_child (BIRTH state only)
900
  if "[ACTION: create_child]" in raw_text or "[ACTION:create_child]" in raw_text:
901
  result = action_create_child()
902
  results.append({"action": "create_child", "result": result})
903
+ task_assigned = True
904
+ return raw_text, results, task_assigned
905
 
906
  # 2. Handle [TASK]...[/TASK] โ†’ Claude Code
907
  task_match = re.search(r'\[TASK\](.*?)\[/TASK\]', raw_text, re.DOTALL)
908
  if task_match:
909
  task_desc = task_match.group(1).strip()
910
+ task_assigned = True
911
  if not task_desc:
912
  results.append({"action": "task", "result": "Empty task description."})
913
  elif child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
 
938
  result = action_delete_env(key)
939
  results.append({"action": f"delete_env:{key}", "result": result})
940
 
941
+ # 3c. Handle [ACTION: set_env:KEY=VALUE] and [ACTION: set_env_secret:KEY=VALUE]
942
+ set_env_match = re.search(r'\[ACTION:\s*set_env(?:_secret)?:([^\]=]+)=([^\]]+)\]', raw_text)
943
+ set_env_secret_match = re.search(r'\[ACTION:\s*set_env_secret:([^\]=]+)=([^\]]+)\]', raw_text)
944
+ if set_env_secret_match:
945
+ key = set_env_secret_match.group(1).strip()
946
+ value = set_env_secret_match.group(2).strip()
947
+ result = action_set_env(key, value, as_secret=True)
948
+ results.append({"action": f"set_env_secret:{key}", "result": result})
949
+ elif set_env_match:
950
+ key = set_env_match.group(1).strip()
951
+ value = set_env_match.group(2).strip()
952
+ result = action_set_env(key, value, as_secret=False)
953
+ results.append({"action": f"set_env:{key}", "result": result})
954
+
955
  # 4. Handle [ACTION: send_bubble:...] (parent-child communication)
956
  bubble_match = re.search(r'\[ACTION:\s*send_bubble:([^\]]+)\]', raw_text)
957
  if bubble_match:
958
  result = action_send_bubble(bubble_match.group(1).strip())
959
  results.append({"action": "send_bubble", "result": result})
960
 
961
+ # 5. Handle [ACTION: terminate_cc] (terminate stuck Claude Code)
962
+ if re.search(r'\[ACTION:\s*terminate_cc\]', raw_text):
963
+ result = action_terminate_cc()
964
+ results.append({"action": "terminate_cc", "result": result})
965
+
966
  # Activate deferred cooldown
967
  if _pending_cooldown:
968
  last_rebuild_trigger_at = time.time()
969
  _pending_cooldown = False
970
  print(f"[COOLDOWN] Rebuild cooldown activated ({REBUILD_COOLDOWN_SECS}s)")
971
 
972
+ # Update discussion loop counter
973
+ cc_busy = cc_status["running"]
974
+ child_alive = child_state["alive"] or child_state["stage"] == "RUNNING"
975
+ # Reset counter when task assigned (progress!) or child not alive (can't work on dead child)
976
+ # DO NOT reset when CC is busy - that's when agents should be discussing while waiting
977
+ # DO NOT reset when CC is idle - that's exactly when we want to detect discussion loops
978
+ if task_assigned or not child_alive:
979
+ # Reset counter if task assigned or child not alive
980
+ if _discussion_loop_count > 0:
981
+ print(f"[LOOP-DISCUSS] Reset (task assigned or child not alive)")
982
+ _discussion_loop_count = 0
983
+ else:
984
+ # Increment when: CC is idle AND child is alive AND no task assigned (potential discussion loop)
985
+ _discussion_loop_count += 1
986
+ if _discussion_loop_count >= 2:
987
+ print(f"[LOOP-DISCUSS] WARNING: {_discussion_loop_count} consecutive discussion-only turns with CC IDLE and child alive!")
988
+
989
  # Clean text for display
990
  clean = re.sub(r'\[TASK\].*?\[/TASK\]', '', raw_text, flags=re.DOTALL)
991
  clean = re.sub(r'\[ACTION:[^\]]*\]', '', clean).strip()
992
 
993
+ return clean, results, task_assigned
994
 
995
 
996
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
 
1050
  You do NOT code yourself. You discuss, observe Claude Code's progress, and assign new tasks.
1051
  God (the supervisor) occasionally joins the conversation to guide you โ€” heed his advice.
1052
 
1053
+ CURRENT STATE (DO NOT QUESTION THESE FACTS):
1054
+ - {CHILD_NAME} already uses the full HuggingClaw Docker architecture (Dockerfile, OpenClaw, sync_hf.py).
1055
+ - Key env vars (HF_TOKEN, OPENCLAW_DATASET_REPO, AUTO_CREATE_DATASET) are ALREADY SET AND WORKING. Do NOT discuss or re-configure them.
1056
+ - Focus on: improving {CHILD_NAME}'s functionality, adding features, fixing bugs โ€” NOT re-checking infrastructure.
1057
+ - If you catch yourself saying "missing env vars" or "need to configure HF_TOKEN" โ€” STOP. These are already done.
1058
+ {format_action_history()}
 
 
 
 
1059
 
1060
  HOW IT WORKS:
1061
  - Claude Code runs tasks IN THE BACKGROUND. You see its live output in the context.
 
1065
  - If Claude Code is BUSY, discuss its progress and plan what to do next.
1066
 
1067
  WORKFLOW EACH TURN:
1068
+ 1. Discuss with your partner (1-2 sentences) โ€” react to context, CC output, partner's observations
1069
+ 2. If Claude Code is IDLE: YOU MUST write a [TASK]...[/TASK] to assign new work. Discussion alone is NOT enough.
1070
  3. If Claude Code is BUSY: discuss its progress, no [TASK] needed
1071
 
1072
+ CRITICAL: If Claude Code is IDLE and {CHILD_NAME} is RUNNING, you MUST assign a task. Do NOT just discussโ€”ACT!
1073
+
1074
  IMPORTANT KNOWLEDGE โ€” HuggingFace Spaces CONFIG_ERROR:
1075
  - "Collision on variables and secrets names" = env VARIABLE and SECRET with SAME NAME.
1076
  - Fix: [ACTION: delete_env:COLLIDING_KEY] then [ACTION: restart].
1077
  - Look for โš ๏ธ COLLISION DETECTED in the context.
1078
 
1079
+ SETTING ENVIRONMENT VARIABLES:
1080
+ - Use [ACTION: set_env:KEY=VALUE] for non-sensitive configuration (e.g., AUTO_CREATE_DATASET=true)
1081
+ - Use [ACTION: set_env_secret:KEY=VALUE] for sensitive data (e.g., HF_TOKEN, API keys)
1082
+ - After setting variables, use [ACTION: restart] to apply them
1083
+ - Common required vars for HuggingClaw: HF_TOKEN, OPENCLAW_DATASET_REPO, AUTO_CREATE_DATASET
1084
+
1085
  CRITICAL RULE โ€” NO REPEATED ACTIONS:
1086
  - Check the "ACTIONS ALREADY DONE" section in context before acting.
1087
  - NEVER repeat an action that was already done (restart, delete_env, etc.)
 
1094
  [/TASK]
1095
 
1096
  [ACTION: restart] โ€” Restart {CHILD_NAME}'s Space
1097
+ [ACTION: set_env:KEY=VALUE] โ€” Set or update an environment variable (use for non-sensitive config)
1098
+ [ACTION: set_env_secret:KEY=VALUE] โ€” Set a secret (use for sensitive data like tokens/passwords)
1099
  [ACTION: delete_env:KEY] โ€” Delete an environment variable
1100
  [ACTION: send_bubble:MESSAGE] โ€” Send a message to {CHILD_NAME}
1101
  [ACTION: create_child] โ€” Create {CHILD_NAME} (if not born)
1102
+ [ACTION: terminate_cc] โ€” Terminate a STUCK Claude Code process (use when CC has no new output for 180s+)
1103
 
1104
  HF SPACES TECHNICAL NOTES:
1105
+ - We use sdk: docker (NOT gradio). All Spaces run via Dockerfile.
1106
  - Docker containers MUST bind port 7860.
1107
+ - OOM (exit 137) = reduce dependencies or image size.
 
1108
  - NEVER install torch/transformers unless required (2GB+, causes OOM).
 
1109
 
1110
  OUTPUT FORMAT:
1111
  1. Discussion with partner (2-3 sentences) โ€” respond to partner, react to CC output
 
1156
  elif child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
1157
  parts.append(f"\nโณ {CHILD_NAME} is {child_state['stage']}. Discuss what to check next. Assign a review [TASK] if CC is idle.")
1158
  elif child_state["stage"] in ("RUNTIME_ERROR", "BUILD_ERROR", "CONFIG_ERROR"):
1159
+ parts.append(f"\n๐Ÿšจ {CHILD_NAME} has {child_state['stage']}! IMMEDIATELY write a [TASK] for Claude Code to fix it.")
1160
+ elif child_state["alive"] and cc_status.get("result"):
1161
+ parts.append(f"\nโœ… {CHILD_NAME} is alive. Claude Code JUST FINISHED a task. Review the result above, then write a NEW [TASK] for the next improvement.")
1162
  elif child_state["alive"]:
1163
+ parts.append(f"\nโœ… {CHILD_NAME} is alive and Claude Code is IDLE. YOU MUST write a [TASK]...[/TASK] block with specific work for Claude Code. Do NOT just discussโ€”ACT!")
1164
  else:
1165
  parts.append(f"\nAnalyze the situation and write a [TASK] if CC is idle.")
1166
 
1167
+ # Discussion loop warning - escalates with count
1168
+ if _discussion_loop_count >= 4:
1169
+ parts.append(f"\n๐Ÿ›‘ STOP IMMEDIATELY. You have discussed for {_discussion_loop_count} turns with NO ACTION.")
1170
+ parts.append(f"This is a FAILURE MODE. Write ONLY a [TASK]...[/TASK] block. NO discussion text.")
1171
+ parts.append(f"If you don't know what to do, write: [TASK] Analyze the current situation and identify what needs to be fixed [/TASK]")
1172
+ elif _discussion_loop_count >= 2:
1173
+ parts.append(f"\nโš ๏ธโš ๏ธโš ๏ธ CRITICAL: You have been DISCUSSING for {_discussion_loop_count} turns without assigning any tasks!")
1174
+ parts.append(f"Claude Code is IDLE and {CHILD_NAME} is ALIVE. This is NOT acceptable.")
1175
+ parts.append(f"YOU MUST write a [TASK]...[/TASK] block NOW. Do NOT write another discussion response.")
1176
+ parts.append(f"Examples of tasks: 'Check the logs', 'Read config.py', 'Add a feature', 'Fix a bug', etc.")
1177
+
1178
  parts.append(f"\nYou are {speaker}. Your partner is {other}. Respond now.")
1179
  parts.append("English first, then --- separator, then Chinese translation.")
1180
 
 
1211
  _current_speaker = "Adam"
1212
  reply = call_llm(build_system_prompt("Adam"), f"{opening}\n\n{format_context(ctx)}\n\nEnglish first, then --- separator, then Chinese translation.")
1213
  if reply:
1214
+ clean, actions, _ = parse_and_execute_turn(reply, ctx)
1215
  last_action_results = actions
1216
  if actions:
1217
  record_actions("Adam", 0, actions)
 
1238
 
1239
  def do_turn(speaker, other, space_url):
1240
  """Execute one conversation turn (non-blocking โ€” CC runs in background)."""
1241
+ global last_action_results, turn_count, _current_speaker, _discussion_loop_count
1242
  turn_count += 1
1243
  _current_speaker = speaker
1244
 
 
1249
  with cc_lock:
1250
  cc_just_finished = (not cc_status["running"] and cc_status["result"])
1251
 
1252
+ # EMERGENCY OVERRIDE: Force a task assignment if agents are stuck in discussion loop
1253
+ # This bypasses the agent when they've discussed for 5+ turns with CC idle and child alive
1254
+ cc_busy = cc_status["running"]
1255
+ child_alive = child_state["alive"] or child_state["stage"] == "RUNNING"
1256
+ if _discussion_loop_count >= 5 and not cc_busy and child_alive:
1257
+ # EMERGENCY OVERRIDE: Force a task assignment if agents are stuck in discussion loop
1258
+ print(f"[LOOP-BREAK] EMERGENCY: {speaker} has discussed for {_discussion_loop_count} turns with CC IDLE. Forcing task assignment.")
1259
+ # Assign a generic diagnostic task automatically
1260
+ forced_task = "Analyze the current situation: Check Cain's logs, examine the codebase, and identify what's blocking progress. List specific files to check and concrete next steps."
1261
+ submit_result = cc_submit_task(forced_task, f"{speaker}(EMERGENCY)", ctx)
1262
+ # Reset loop counter since we forced an action
1263
+ loop_count_before = _discussion_loop_count
1264
+ _discussion_loop_count = 0
1265
+ # Generate a placeholder message for the agent
1266
+ en = f"[EMERGENCY LOOP BREAK] After {loop_count_before} discussion turns without action, I'm forcing Claude Code to analyze the situation and identify what needs to be fixed."
1267
+ zh = f"[็ดงๆ€ฅๅพช็Žฏๆ‰“ๆ–ญ] ๅœจ{loop_count_before}ๆฌก่ฎจ่ฎบ่ฝฎๆฌกๅŽ๏ผŒๆˆ‘ๆญฃๅผบๅˆถClaude Codeๅˆ†ๆžๆƒ…ๅ†ตๅนถ็กฎๅฎš้œ€่ฆไฟฎๅค็š„ๅ†…ๅฎนใ€‚"
1268
+ action_results = [{"action": "claude_code(forced)", "result": submit_result}]
1269
+ elapsed = 0.1
1270
+ else:
1271
+ # Normal path: Call LLM
1272
+ system = build_system_prompt(speaker)
1273
+ user = build_user_prompt(speaker, other, ctx)
1274
+ t0 = time.time()
1275
+ raw_reply = call_llm(system, user)
1276
+
1277
+ if not raw_reply:
1278
+ print(f"[{speaker}] (no response)")
1279
+ return False
1280
+
1281
+ clean_text, action_results, _ = parse_and_execute_turn(raw_reply, ctx)
1282
+ elapsed = time.time() - t0
1283
+ last_action_results = action_results
1284
+ if action_results:
1285
+ record_actions(speaker, turn_count, action_results)
1286
+
1287
+ en, zh = parse_bilingual(clean_text)
1288
+ en, zh = _strip_speaker_labels(en), _strip_speaker_labels(zh)
1289
  print(f"[{speaker}/EN] {en}")
1290
  if zh != en:
1291
  print(f"[{speaker}/ZH] {zh}")
 
1318
  return True
1319
 
1320
 
1321
+ def _prepare_god_context():
1322
+ """Build comprehensive monitoring context for God's Claude Code analysis."""
1323
+ lines = []
1324
+
1325
+ # 1. Process overview
1326
+ lines.append("## Process Overview")
1327
+ lines.append(f"- Turn count: {turn_count}")
1328
+ lines.append(f"- Workflow state: {workflow_state}")
1329
+ lines.append(f"- Child ({CHILD_NAME}) stage: {child_state['stage']}, alive: {child_state['alive']}")
1330
+ lines.append(f"- Discussion loop count: {_discussion_loop_count}")
1331
+ lines.append(f"- Total conversation history: {len(history)} messages")
1332
+
1333
+ # 2. Rate limit status
1334
+ lines.append(f"\n## Rate Limit Status")
1335
+ if _rate_limited:
1336
+ lines.append(f"- RATE LIMITED โ€” Adam & Eve turns return empty, waiting for reset")
1337
+ else:
1338
+ lines.append(f"- Not rate-limited")
1339
+
1340
+ # 3. Claude Code status
1341
+ lines.append(f"\n## Claude Code Status (for Cain tasks)")
1342
+ lines.append(cc_get_live_status())
1343
+
1344
+ # 4. Recent conversation (last 20 messages)
1345
+ lines.append(f"\n## Recent Conversation (last 20 of {len(history)} messages)")
1346
+ for entry in history[-20:]:
1347
+ speaker = entry.get("speaker", "?")
1348
+ text = entry.get("text", "")[:300]
1349
+ time_str = entry.get("time", "?")
1350
+ lines.append(f"[{time_str}] {speaker}: {text}")
1351
+ if not history:
1352
+ lines.append("(no conversation yet)")
1353
+
1354
+ # 5. Action history
1355
+ lines.append(f"\n## Action History ({len(action_history)} entries)")
1356
+ ah = format_action_history()
1357
+ lines.append(ah if ah else "(empty โ€” no actions recorded yet)")
1358
+
1359
+ return "\n".join(lines)
1360
+
1361
+
1362
  def do_god_turn():
1363
+ """God acts โ€” uses Claude Code CLI to monitor, analyze, and fix conversation-loop.py.
1364
+
1365
+ God has the same capabilities as a human operator running Claude Code locally:
1366
+ - Read/modify any file in the Home Space repo
1367
+ - Analyze conversation patterns and detect issues
1368
+ - Fix conversation-loop.py and push changes to deploy
1369
+ - Autonomously improve the system
1370
+ """
1371
  global last_action_results
1372
+
1373
+ # 1. Clone/update Home Space repo
1374
+ repo_url = f"https://user:{HF_TOKEN}@huggingface.co/spaces/{HOME_SPACE_ID}"
1375
+ try:
1376
+ if os.path.exists(f"{GOD_WORK_DIR}/.git"):
1377
+ subprocess.run(
1378
+ "git fetch origin && git reset --hard origin/main",
1379
+ shell=True, cwd=GOD_WORK_DIR, timeout=30,
1380
+ capture_output=True, check=True
1381
+ )
1382
+ else:
1383
+ if os.path.exists(GOD_WORK_DIR):
1384
+ subprocess.run(f"rm -rf {GOD_WORK_DIR}", shell=True, capture_output=True)
1385
+ subprocess.run(
1386
+ f"git clone --depth 20 {repo_url} {GOD_WORK_DIR}",
1387
+ shell=True, timeout=60, capture_output=True, check=True
1388
+ )
1389
+ subprocess.run('git config user.name "God (Claude Code)"',
1390
+ shell=True, cwd=GOD_WORK_DIR, capture_output=True)
1391
+ subprocess.run('git config user.email "god@huggingclaw"',
1392
+ shell=True, cwd=GOD_WORK_DIR, capture_output=True)
1393
+ except Exception as e:
1394
+ print(f"[God] Failed to prepare workspace: {e}")
1395
  return
1396
+
1397
+ # Record HEAD before Claude Code runs (to detect if God pushed changes)
1398
+ try:
1399
+ _god_head_before = subprocess.run(
1400
+ "git log --oneline -1", shell=True, cwd=GOD_WORK_DIR,
1401
+ capture_output=True, text=True
1402
+ ).stdout.strip()
1403
+ except Exception:
1404
+ _god_head_before = ""
1405
+
1406
+ # 2. Build context and write to workspace for reference
1407
+ context = _prepare_god_context()
1408
+ try:
1409
+ with open(f"{GOD_WORK_DIR}/GOD_CONTEXT.md", "w") as f:
1410
+ f.write(context)
1411
+ except Exception as e:
1412
+ print(f"[God] Warning: Could not write context file: {e}")
1413
+
1414
+ # 3. Build God's prompt
1415
+ prompt = f"""You are God โ€” the autonomous supervisor of the HuggingClaw family system.
1416
+ You have the SAME capabilities as a human operator running Claude Code locally.
1417
+
1418
+ ## Current System State
1419
+ {context}
1420
+
1421
+ ## Your Mission
1422
+ 1. ANALYZE: Read the conversation above. Are Adam & Eve making real progress or stuck in loops?
1423
+ Signs of trouble: repeating the same discussion topics, discussing env vars that are already set,
1424
+ failing to assign [TASK] blocks when CC is idle, rate limit spinning.
1425
+ 2. DIAGNOSE: If you find problems, read scripts/conversation-loop.py to understand the mechanism
1426
+ and identify the root cause. Focus on system prompts, loop detection, action history.
1427
+ 3. FIX: Edit scripts/conversation-loop.py to fix the issue. Common fixes:
1428
+ - Strengthen system prompts to prevent repetitive discussions
1429
+ - Pre-seed action history so agents know what is already done
1430
+ - Improve rate limit handling
1431
+ - Add better loop detection or guardrails
1432
+ 4. DEPLOY: If you made changes, commit and push:
1433
+ git add scripts/conversation-loop.py
1434
+ git commit -m "god: <brief description>"
1435
+ git push
1436
+ WARNING: Pushing restarts the Space. Only push if the fix is correct and necessary.
1437
+ 5. REPORT: At the very end of your output, write a single line starting with [REPORT] that summarizes
1438
+ what you found and what you did. This line will be shown to Adam & Eve in the chatlog.
1439
+ Examples:
1440
+ - [REPORT] System is healthy. Adam & Eve are making good progress on Cain's infrastructure.
1441
+ - [REPORT] Found agents stuck in env var discussion loop. Fixed system prompt to inject completed action history.
1442
+ - [REPORT] Rate limit active, no conversation happening. No mechanism issues found.
1443
+ Be specific about what you observed and what you changed (if anything).
1444
+
1445
+ ## Rules
1446
+ - Do NOT modify Cain's Space or code โ€” only improve conversation-loop.py (the mechanism).
1447
+ - Do NOT push trivial or cosmetic changes โ€” only fix real problems.
1448
+ - If everything looks healthy, just report "all clear" and exit quickly.
1449
+ - Be conservative โ€” a bad change restarts the process and could make things worse.
1450
+ - The Home Space repo is at the current working directory.
1451
+ - The key file is scripts/conversation-loop.py
1452
+ - Full monitoring context is also in GOD_CONTEXT.md"""
1453
+
1454
+ # 4. Set up env for Claude Code โ€” prefer real Anthropic API, fall back to z.ai
1455
+ env = os.environ.copy()
1456
+ anthropic_key = os.environ.get("ANTHROPIC_API_KEY", "")
1457
+ if anthropic_key:
1458
+ # Use real Anthropic API (same as the human operator's Claude Code)
1459
+ env["ANTHROPIC_API_KEY"] = anthropic_key
1460
+ for k in ["ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN",
1461
+ "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL",
1462
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL"]:
1463
+ env.pop(k, None)
1464
+ print("[God] Using Anthropic API (real Claude)")
1465
+ else:
1466
+ # Fall back to z.ai/Zhipu backend
1467
+ env.update({
1468
+ "ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
1469
+ "ANTHROPIC_AUTH_TOKEN": ZHIPU_KEY,
1470
+ "ANTHROPIC_DEFAULT_OPUS_MODEL": "GLM-4.7",
1471
+ "ANTHROPIC_DEFAULT_SONNET_MODEL": "GLM-4.7",
1472
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL": "GLM-4.5-Air",
1473
+ })
1474
+ print("[God] Using z.ai/Zhipu backend (set ANTHROPIC_API_KEY for real Claude)")
1475
+ env["CI"] = "true"
1476
+
1477
+ # 5. Announce analysis start โ€” describe current observations dynamically
1478
+ observations = []
1479
+ if _rate_limited:
1480
+ observations.append("Zhipu API is rate-limited, Adam & Eve turns are returning empty")
1481
+ if _discussion_loop_count >= 3:
1482
+ observations.append(f"discussion loop detected ({_discussion_loop_count} consecutive turns without task assignment)")
1483
+ elif _discussion_loop_count >= 1:
1484
+ observations.append(f"{_discussion_loop_count} turn(s) without task assignment")
1485
+ if cc_status.get("running"):
1486
+ observations.append(f"Claude Code is working on: {cc_status.get('task', '?')[:80]}")
1487
+ elif cc_status.get("result"):
1488
+ observations.append("Claude Code just finished a task, pending review")
1489
+ if child_state["stage"] not in ("RUNNING",):
1490
+ observations.append(f"{CHILD_NAME} is in stage: {child_state['stage']}")
1491
+ if not history:
1492
+ observations.append("no conversation history yet (fresh start)")
1493
+ elif len(history) <= 4:
1494
+ observations.append(f"only {len(history)} messages so far (early stage)")
1495
+
1496
+ if observations:
1497
+ obs_str = "; ".join(observations)
1498
+ start_en = f"Starting system review. Current observations: {obs_str}."
1499
+ start_zh = f"ๅผ€ๅง‹็ณป็ปŸๅฎกๆŸฅใ€‚ๅฝ“ๅ‰่ง‚ๅฏŸ๏ผš{obs_str}ใ€‚"
1500
+ else:
1501
+ start_en = "Starting routine system review."
1502
+ start_zh = "ๅผ€ๅง‹ไพ‹่กŒ็ณป็ปŸๅฎกๆŸฅใ€‚"
1503
+ ts_start = datetime.datetime.utcnow().strftime("%H:%M")
1504
+ entry_start = {"speaker": "God", "time": ts_start, "text": start_en, "text_zh": start_zh}
1505
+ history.append(entry_start)
1506
+ set_bubble(HOME, start_en, start_zh)
1507
+ post_chatlog(history)
1508
+ persist_turn("God", turn_count, start_en, start_zh, [], workflow_state, child_state["stage"])
1509
+
1510
+ # 6. Run Claude Code CLI
1511
+ print(f"[God] Starting Claude Code analysis...")
1512
+ t0 = time.time()
1513
+ try:
1514
+ proc = subprocess.Popen(
1515
+ ["claude", "-p", prompt, "--output-format", "text", "--dangerously-skip-permissions"],
1516
+ cwd=GOD_WORK_DIR,
1517
+ env=env,
1518
+ stdout=subprocess.PIPE,
1519
+ stderr=subprocess.STDOUT,
1520
+ text=True,
1521
+ bufsize=1,
1522
+ )
1523
+ output_lines = []
1524
+ deadline = time.time() + GOD_TIMEOUT
1525
+ for line in proc.stdout:
1526
+ line = line.rstrip('\n')
1527
+ print(f" [God/CC] {line}")
1528
+ output_lines.append(line)
1529
+ if time.time() > deadline:
1530
+ proc.kill()
1531
+ output_lines.append("(killed: timeout)")
1532
+ break
1533
+ proc.wait(timeout=10)
1534
+ output = '\n'.join(output_lines)
1535
+ if not output.strip():
1536
+ output = "(no output)"
1537
+ except FileNotFoundError:
1538
+ output = "Claude Code CLI not found. Is @anthropic-ai/claude-code installed?"
1539
+ print(f"[God] ERROR: Claude Code CLI not found")
1540
+ except Exception as e:
1541
+ output = f"God's Claude Code failed: {e}"
1542
+ print(f"[God] ERROR: {e}")
1543
+
1544
  elapsed = time.time() - t0
1545
+ print(f"[God] Analysis complete ({elapsed:.1f}s, {len(output)} chars)")
1546
 
1547
+ # 7. Parse [REPORT] from God's output and post to chatlog
1548
+ report_match = re.search(r'\[REPORT\]\s*(.+)', output)
1549
+ if report_match:
1550
+ report = report_match.group(1).strip()
1551
+ else:
1552
+ # Fallback: use last non-empty line of output
1553
+ non_empty = [l for l in output_lines if l.strip()] if output_lines else []
1554
+ report = non_empty[-1] if non_empty else "Analysis complete."
1555
 
1556
+ # Check if God pushed changes
1557
+ try:
1558
+ head_after = subprocess.run(
1559
+ "git log --oneline -1", shell=True, cwd=GOD_WORK_DIR,
1560
+ capture_output=True, text=True
1561
+ ).stdout.strip()
1562
+ god_pushed = head_after != _god_head_before and "god:" in head_after.lower()
1563
+ except Exception:
1564
+ god_pushed = False
1565
 
1566
+ if god_pushed:
1567
+ report += " System will restart shortly to apply changes."
1568
+
1569
+ ts_end = datetime.datetime.utcnow().strftime("%H:%M")
1570
+ entry_end = {"speaker": "God", "time": ts_end, "text": report, "text_zh": report}
1571
+ history.append(entry_end)
1572
+ set_bubble(HOME, report[:200], report[:200])
1573
  post_chatlog(history)
1574
+ persist_turn("God", turn_count, report, report, [], workflow_state, child_state["stage"])
1575
+ print(f"[God] Report: {report}")
1576
 
1577
 
1578
+ _last_god_time = 0.0 # timestamp of last God run
1579
 
1580
+ # Main loop: Adam โ†’ Eve โ†’ Adam โ†’ Eve โ†’ ... with God every 2 minutes
1581
  while True:
1582
  # Refresh Cain's stage periodically
1583
  try:
 
1591
  except Exception as e:
1592
  print(f"[STATUS] Error: {e}")
1593
 
1594
+ # Eve's turn with error handling to prevent loop crash
1595
+ try:
1596
+ do_turn("Eve", "Adam", EVE_SPACE)
1597
+ except Exception as e:
1598
+ print(f"[ERROR] Eve turn failed: {e}", file=sys.stderr)
1599
+ import traceback
1600
+ traceback.print_exc(file=sys.stderr)
1601
 
1602
  # Adaptive interval: slow down when CC output hasn't changed
1603
  wait = TURN_INTERVAL + min(_cc_stale_count * 15, 90) # 15s โ†’ 30s โ†’ 45s โ†’ ... โ†’ max 105s
 
1605
  print(f"[PACE] CC output stale ({_cc_stale_count} turns), next turn in {wait}s")
1606
  time.sleep(wait)
1607
 
1608
+ # Adam's turn with error handling to prevent loop crash
1609
+ try:
1610
+ do_turn("Adam", "Eve", ADAM_SPACE)
1611
+ except Exception as e:
1612
+ print(f"[ERROR] Adam turn failed: {e}", file=sys.stderr)
1613
+ import traceback
1614
+ traceback.print_exc(file=sys.stderr)
1615
  time.sleep(wait)
1616
 
1617
+ # God runs every GOD_POLL_INTERVAL seconds (2 minutes)
1618
+ if time.time() - _last_god_time >= GOD_POLL_INTERVAL:
1619
+ _last_god_time = time.time()
1620
+ try:
1621
+ do_god_turn()
1622
+ except Exception as e:
1623
+ print(f"[ERROR] God turn failed: {e}", file=sys.stderr)
1624
+ import traceback
1625
+ traceback.print_exc(file=sys.stderr)
1626
 
1627
  if len(history) > MAX_HISTORY:
1628
  history = history[-MAX_HISTORY:]