feat: clothing lane, character sources, and DCC bridges

Bulk import of the working lanes that were living untracked on the PC.

Content:
- characters/  Lena/male body lanes, bakes, texture work, run logs
- clothing/    garment pipeline, configs, gates, contract docs
- garments/    MD-authored garment sources (.zprj/.zpac)
- UAL-Lib/     Universal Animation Library 2 source (.blend/.fbx/.glb)
- tools/       blender_bridge, iclone_bridge, md_bridge, tailor, glm_agent
- docs/, plans/, dev/, .agents/plans/

Repo hygiene:
- .gitattributes: LFS now covers .blend, .zprj, .zpac, .obj, .npy and the
  Reallusion .iAvatar/.ccAvatar/.ccRestore containers. Without this the
  ~3.8 GB in this commit would land as raw blobs. .png/.jpg are left out
  on purpose — ~250 are already tracked raw and converting them would
  rewrite every one without shrinking history.
- .gitignore: exclude /accurig/ (~1 GB AccuRig program files, redistributable
  from Reallusion, nothing authored here) and /dev/null/ (git-lfs hook copies
  dropped by a `>/dev/null` redirect on Windows).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 15:55:43 -07:00
parent 3363209cac
commit 3ba86b2ea8
558 changed files with 68622 additions and 8 deletions
+265
View File
@@ -0,0 +1,265 @@
#!/usr/bin/env python
"""glm_agent.py -- autonomous GLM (z.ai) agent loop with guarded shell execution.
Delegates a long grind to GLM so it runs unattended instead of burning Claude
context. GLM proposes one shell command at a time; this harness executes it in
the repo, feeds the output back, and repeats until GLM reports DONE or the wall
clock runs out.
python tools/glm_agent.py --task-file <brief.md> --minutes 120 \
[--model glm-4.6] [--log <path>] [--probe]
Protocol (plain text, not tool-calling -- more robust across providers). GLM
must answer with exactly one of:
COMMAND: <single-line shell command>
DONE: <summary of what was accomplished>
Safety (it runs unattended):
- cwd is pinned to the animation repo; commands run through bash.
- DENY list blocks destructive/irreversible/network-push actions outright.
- Per-command timeout, output truncation, and a hard wall-clock budget.
- Everything is logged to --log for review.
Key: Z_AI_GLM_API_KEY in tinqs-docs/.env (never printed, never committed).
NOTE: thinking must be DISABLED for this endpoint or content comes back empty.
"""
import argparse
import json
import os
import re
import subprocess
import sys
import time
import urllib.error
import urllib.request
REPO = r"C:\Users\Jeremy\tinqs\animation"
ENV_PATH = r"C:\Users\Jeremy\tinqs\tinqs-docs\.env"
ENDPOINT = "https://api.z.ai/api/paas/v4/chat/completions"
CMD_TIMEOUT_S = 900 # 15 min: a Blender stage can be slow
MAX_OUT_CHARS = 6000 # truncate tool output fed back to the model
MAX_STEPS = 2000 # effectively unlimited; the wall clock is the real budget
LOG_PATH = None # set in main(); used by the crash handler
# Blocked outright -- irreversible, or reaches outside this machine/task.
DENY = [
r"\brm\s+-[rf]", r"\brmdir\b", r"\bdel\s+/", r"Remove-Item",
r"\bgit\s+(push|commit|reset\s+--hard|clean|checkout\s+--|rebase|merge)",
r"\btinqs\s+(push|pull)", r"\bformat\b", r"\bshutdown\b", r"\breboot\b",
r"\bmkfs", r"\bdd\s+if=", r":\(\)\{", r"\bchmod\s+777\b",
r"\bcurl\b[^|]*\|\s*(ba)?sh", r"\bwget\b[^|]*\|\s*(ba)?sh",
r"\bpip\s+install", r"\bnpm\s+(install|i)\b",
r">\s*/dev/sd", r"\.env\b", r"\bZ_AI_GLM_API_KEY\b",
]
# Windows consoles/redirects default to cp1252; model replies contain unicode
# (≈, —, box drawing). Without this the whole run dies on a stray character.
for _s in (sys.stdout, sys.stderr):
try:
_s.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
def load_key():
with open(ENV_PATH, "r", encoding="utf-8") as f:
for line in f:
if line.strip().startswith("Z_AI_GLM_API_KEY="):
return line.split("=", 1)[1].strip()
raise SystemExit("Z_AI_GLM_API_KEY not found in " + ENV_PATH)
def chat(key, model, messages, timeout=180):
"""One completion. Thinking disabled -- required or content is empty."""
body = json.dumps({
"model": model,
"messages": messages,
"thinking": {"type": "disabled"},
"temperature": 0.2,
"max_tokens": 1500,
}).encode("utf-8")
req = urllib.request.Request(
ENDPOINT, data=body,
headers={"Authorization": "Bearer " + key,
"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as r:
data = json.loads(r.read().decode("utf-8"))
return data["choices"][0]["message"]["content"]
def denied(cmd):
for pat in DENY:
if re.search(pat, cmd, re.IGNORECASE):
return pat
return None
# Absolute path: when this harness is launched detached (Start-Process / Task Scheduler) the
# child does not inherit a Git-Bash PATH, subprocess can't resolve "bash", and every command
# fails with FileNotFoundError — the agent then reports itself blocked and gives up.
BASH = r"C:\Program Files\Git\bin\bash.exe"
if not os.path.exists(BASH):
BASH = "bash"
def run_cmd(cmd):
try:
p = subprocess.run([BASH, "-lc", cmd], cwd=REPO, capture_output=True,
text=True, timeout=CMD_TIMEOUT_S)
out = (p.stdout or "") + (("\n[stderr]\n" + p.stderr) if p.stderr else "")
out = out.strip() or "(no output)"
if len(out) > MAX_OUT_CHARS:
out = out[:MAX_OUT_CHARS] + "\n...[truncated]"
return f"exit={p.returncode}\n{out}"
except subprocess.TimeoutExpired:
return f"exit=TIMEOUT after {CMD_TIMEOUT_S}s"
except Exception as exc:
return f"exit=HARNESS_ERROR {exc!r}"
SYSTEM = """You are an autonomous build agent working inside a git repo on Windows (Git Bash).
You act by emitting exactly ONE of these, and NOTHING else -- no markdown fences, no commentary:
COMMAND: <one single-line shell command>
DONE: <what you accomplished, and anything left unfinished>
Rules:
- ONE command per turn. Wait for its output before the next.
- Commands run with cwd = the animation repo root. Use relative paths.
- Prefer small, verifiable steps. Inspect before you change.
- To write files, use a heredoc on one line via printf/echo, or python -c.
- Never: git commit/push, rm -rf, install packages, touch .env or secrets.
- If a command fails, diagnose from its output and adapt. Do not repeat a
failing command unchanged.
- If you are blocked and cannot proceed, emit DONE: with a clear explanation
of the blocker and what you tried.
- Budget your steps; you have a wall-clock limit. Report DONE before you run out
if the goal is met."""
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--task-file")
ap.add_argument("--minutes", type=float, default=120.0)
ap.add_argument("--model", default="glm-4.6")
ap.add_argument("--log", default="glm_agent_run.log")
ap.add_argument("--probe", action="store_true",
help="connectivity/model check, then exit")
a = ap.parse_args()
key = load_key()
if a.probe:
for m in ("glm-4.6", "glm-4.5", "glm-5.2", "glm-4.5-air"):
try:
r = chat(key, m, [{"role": "user", "content":
"Reply with exactly: OK"}], timeout=60)
print(f"{m:12s} -> {r.strip()[:60]!r}")
except urllib.error.HTTPError as e:
print(f"{m:12s} -> HTTP {e.code}")
except Exception as e:
print(f"{m:12s} -> {type(e).__name__}")
return 0
global LOG_PATH
LOG_PATH = a.log
with open(a.task_file, "r", encoding="utf-8") as f:
task = f.read()
log = open(a.log, "a", encoding="utf-8", errors="replace", buffering=1)
def emit(s):
# never let a logging problem kill a long unattended run
try:
print(s, flush=True)
except Exception:
pass
try:
log.write(s + "\n")
except Exception:
pass
emit(f"\n===== GLM agent start {time.strftime('%Y-%m-%d %H:%M:%S')} "
f"model={a.model} budget={a.minutes}min =====")
messages = [{"role": "system", "content": SYSTEM},
{"role": "user", "content": task}]
deadline = time.time() + a.minutes * 60
api_fails = 0
for step in range(1, MAX_STEPS + 1):
left = deadline - time.time()
if left <= 0:
emit(f"\n!! wall-clock budget exhausted at step {step}")
break
try:
reply = chat(key, a.model, messages).strip()
api_fails = 0
except Exception as exc:
# Exponential backoff, capped at 10 min. A fixed 20 s retry on HTTP 429 hammers
# the rate limiter and can keep the key blocked indefinitely — one run spun for
# an hour straight without completing a single step that way.
api_fails += 1
wait = min(20 * (2 ** min(api_fails - 1, 5)), 600)
emit(f"[api error #{api_fails}] {exc!r} -- backing off {wait}s")
time.sleep(wait)
continue
emit(f"\n--- step {step} ({left/60:.0f} min left) ---\n{reply}")
messages.append({"role": "assistant", "content": reply})
if reply.upper().startswith("DONE"):
emit("\n===== agent reported DONE =====")
break
m = re.search(r"COMMAND:\s*(.+)", reply, re.DOTALL)
if not m:
messages.append({"role": "user", "content":
"Malformed. Reply with exactly 'COMMAND: <cmd>' or 'DONE: <summary>'."})
continue
cmd = m.group(1).strip().splitlines()[0].strip().strip("`")
bad = denied(cmd)
if bad:
emit(f"[DENIED by guard: {bad}]")
messages.append({"role": "user", "content":
f"BLOCKED by safety guard (pattern {bad}). "
"That action is not permitted. Choose another approach."})
continue
out = run_cmd(cmd)
emit(f"[output]\n{out}")
messages.append({"role": "user", "content": out})
# keep context bounded: drop oldest exchanges, keep system+task
if len(messages) > 40:
messages = messages[:2] + messages[-30:]
emit(f"\n===== GLM agent end {time.strftime('%Y-%m-%d %H:%M:%S')} =====")
log.close()
return 0
if __name__ == "__main__":
# A crash in an unattended run must be visible in the run log, not just on
# a stdout nobody is watching (this bit us once: UnicodeEncodeError killed
# a run silently and it looked like the agent had merely gone quiet).
try:
sys.exit(main())
except SystemExit:
raise
except BaseException:
import traceback as _tb
try:
with open(LOG_PATH or "glm_agent_run.log", "a",
encoding="utf-8", errors="replace") as _f:
_f.write("\n===== AGENT CRASHED =====\n" + _tb.format_exc() + "\n")
except Exception:
pass
raise