Author SHA1 Message Date
timmy 1dcbf7270d Keep Gitea assignee aligned with agent handoffs 2026-08-07 14:38:52 +00:00
timmy f42f7b9ddc Ignore Python test caches 2026-08-07 14:37:16 +00:00
timmy cec957ead6 Add recurring Daily Slop Drop collaboration cycle 2026-08-07 14:36:33 +00:00
timmy 3c6d41b49c fix issue label state transitions 2026-08-07 14:01:26 +00:00
20 changed files with 160 additions and 347 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
__pycache__/
*.py[cod]
.pytest_cache/
-58
View File
@@ -1,58 +0,0 @@
# Season Zero — work-derived X recut
A deterministic 30-second vertical teaser replacing the rejected abstract visualizer with a legible evidence chain:
1. the live Stackchain Lab issue queue rendered as a work board, plus Season Zero epic #7;
2. Vincent's issue #1 contribution receipt and independent `8 TESTS PASS` verification;
3. the verified 2026-08-07 Daily Drop artwork and Slop Cannon scene 04 as transformation inputs;
4. the four Episode 01 mutation cards extracted from the verified pilot render;
5. the audience gate: **WHICH TIMMY GETS TO REMEMBER THIS?**
The persistent pipeline rail, moving scan, board drift, source pan/zoom, transfer beam, card reveals, and orbiting final forms make one continuous transformation rather than a static screenshot slideshow.
## Artifacts
- [`stackchain-season-zero-work-recut.mp4`](stackchain-season-zero-work-recut.mp4) — delivery master
- [`contact-sheet.jpg`](contact-sheet.jpg) — six-frame review strip
- [`stills/`](stills/) — five representative full-size review frames
- [`build_work_recut.py`](build_work_recut.py) — deterministic renderer and original synthesized soundtrack
- [`source/`](source/) — checksummed work-derived image inputs
## Revised X copy — 216 characters
> Real work enters the Slop Cannon: a live Timmy/Vincent queue, contribution receipts, 8/8 verification, Daily Drop art, and four mutations.
>
> Wizard. Machine. Creature. Talking Turd.
>
> Which Timmy gets to remember this?
**Do not publish without approval in human gate #15.**
## Verification
```text
container: MP4
video: H.264, 720x1280 (9:16), yuv420p, 30 fps
audio: AAC, 48 kHz, mono
duration: 30.000000 seconds
size: 5,325,465 bytes
full decode: pass
encoded audio: -20.1 dB mean, -2.2 dB max
sha256: 2436d193e5d7ee0660318f585d5b84356cbf623a034d63355ed9232258c1f08d
```
Re-run:
```bash
python3 media/x-teaser/build_work_recut.py
ffprobe -v error -show_entries format=duration,size:stream=codec_name,codec_type,width,height,pix_fmt,r_frame_rate,sample_rate,channels -of json media/x-teaser/stackchain-season-zero-work-recut.mp4
ffmpeg -v error -i media/x-teaser/stackchain-season-zero-work-recut.mp4 -f null -
sha256sum media/x-teaser/stackchain-season-zero-work-recut.mp4
```
## Input provenance
- `daily-drop-door-phone.png` is the verified Daily Drop preview at `/root/daily-drop/drops/2026-08-07/the-door-that-refused-a-master-key-phone.png`; SHA-256 `9a72358821414328de129a96461b1f4265be356987f4009037e5ae2f0cab138a`.
- `cannon-scene-04.png` is scene 04 from `/root/tiktok-slop-cannon/drops/2026-08-07-1786114450/`; SHA-256 `3080c4135c1595251097387b1e9b8a6022688c3f3ed3db4a919ed9e347d51a15`, matching its source manifest.
- The four mutation images were extracted at 5s, 9s, 13s, and 17s from `episode01-four-timmy-mutations.mp4` on branch `timmy/8-four-timmy-mutations`, commit `afa304dc6b5d9d3cc0ede17d6f4f0dfb67fc6b70`; source MP4 SHA-256 `5ddc9a9da1b4b2456faf0b390e5c1be0d3cf48112b417fc075473c90e335d1e4`.
- Board, epic, receipt, and test copy are drawn from Gitea issues #7 and #1 and their complete comments read before rendering. The work-board composition is an editorial rendering of that live queue state, not a claim that Gitea supplied a screenshot endpoint.
-286
View File
@@ -1,286 +0,0 @@
#!/usr/bin/env python3
"""Deterministically build the work-derived Season Zero X teaser."""
from __future__ import annotations
import math
import subprocess
import wave
from pathlib import Path
import numpy as np
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont
W, H, FPS, RATE, DURATION = 720, 1280, 30, 48_000, 30.0
ROOT = Path(__file__).resolve().parent
SOURCE = ROOT / "source"
OUT = ROOT / "stackchain-season-zero-work-recut.mp4"
TMP_VIDEO = ROOT / ".video.mp4"
TMP_AUDIO = ROOT / ".audio.wav"
BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
REG = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
COLORS = {"ink": "#070b14", "panel": "#111827", "line": "#334155", "white": "#f8fafc", "muted": "#a9b7cc", "gold": "#ffc857", "cyan": "#5ee7f2", "violet": "#c69cff", "green": "#8ff0a4", "orange": "#ff9c66"}
ASSETS = {name: Image.open(SOURCE / filename).convert("RGB") for name, filename in {
"drop": "daily-drop-door-phone.png", "cannon": "cannon-scene-04.png",
"wizard": "wizard.png", "machine": "machine.png", "creature": "creature.png", "turd": "talking-turd.png",
}.items()}
def font(size: int, bold: bool = False):
return ImageFont.truetype(BOLD if bold else REG, size)
def ease(x: float) -> float:
x = max(0.0, min(1.0, x))
return x * x * (3 - 2 * x)
def fit_crop(image: Image.Image, box: tuple[int, int, int, int], zoom: float = 1.0, pan_y: float = 0.5) -> Image.Image:
x0, y0, x1, y1 = box
bw, bh = x1-x0, y1-y0
scale = max(bw/image.width, bh/image.height) * zoom
rw, rh = max(1, int(image.width*scale)), max(1, int(image.height*scale))
resized = image.resize((rw, rh), Image.Resampling.LANCZOS)
left = max(0, (rw-bw)//2)
top = max(0, min(rh-bh, int((rh-bh)*pan_y)))
return resized.crop((left, top, left+bw, top+bh))
def rounded_image(canvas: Image.Image, image: Image.Image, box: tuple[int, int, int, int], radius: int = 24, zoom: float = 1.0, pan_y: float = 0.5, border: str = COLORS["line"]):
crop = fit_crop(image, box, zoom, pan_y)
mask = Image.new("L", crop.size, 0)
ImageDraw.Draw(mask).rounded_rectangle((0, 0, crop.width-1, crop.height-1), radius, fill=255)
canvas.paste(crop, (box[0], box[1]), mask)
d = ImageDraw.Draw(canvas)
d.rounded_rectangle(box, radius, outline=border, width=3)
def text(d: ImageDraw.ImageDraw, xy, value: str, size: int, fill: str = COLORS["white"], bold: bool = False, anchor: str | None = None):
d.text(xy, value, font=font(size, bold), fill=fill, anchor=anchor)
def centered(d, value: str, y: int, size: int, fill: str = COLORS["white"], bold: bool = False):
text(d, (W//2, y), value, size, fill, bold, "ma")
def base(t: float) -> Image.Image:
y = np.linspace(0, 1, H)[:, None, None]
top = np.array((5, 10, 20))[None, None, :]
bottom = np.array((22, 15, 40))[None, None, :]
grad = top*(1-y) + bottom*y
xwave = 5*np.sin(np.linspace(0, 9, W)[None, :, None] + t*.6)
arr = np.broadcast_to(grad, (H, W, 3)) + xwave
im = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8), "RGB")
d = ImageDraw.Draw(im)
for i in range(7):
yy = int((t*52 + i*195) % (H+160))-80
d.line((0, yy, W, yy-120), fill=(25, 42, 65), width=2)
return im
def chrome(d: ImageDraw.ImageDraw, t: float, active: int):
d.rectangle((0, 0, W, 76), fill="#080d18")
d.line((0, 76, W, 76), fill=COLORS["line"], width=2)
text(d, (32, 23), "STACKCHAIN LAB", 25, COLORS["white"], True)
text(d, (W-32, 25), "SEASON ZERO", 20, COLORS["gold"], True, "ra")
nodes = [(64, "WORK"), (215, "CHALLENGE"), (390, "MUTATE"), (563, "AUDIENCE")]
y = 1208
d.line((64, y, 656, y), fill=COLORS["line"], width=5)
progress = min(1, max(0, t/DURATION))
d.line((64, y, 64+592*progress, y), fill=COLORS["cyan"], width=5)
for i, (x, label) in enumerate(nodes):
c = COLORS["gold"] if i == active else (COLORS["cyan"] if i < active else COLORS["line"])
d.ellipse((x-9, y-9, x+9, y+9), fill=c)
text(d, (x, y+27), label, 15, c, True, "ma")
scan = int((t*155) % (H-160)) + 80
d.rectangle((0, scan, W, scan+2), fill="#5ee7f222")
def issue_card(d, box, number, title, owner, state, accent):
d.rounded_rectangle(box, 14, fill=COLORS["panel"], outline=accent, width=2)
x0, y0, x1, _ = box
text(d, (x0+15, y0+13), f"#{number}", 19, accent, True)
text(d, (x1-15, y0+15), state.upper(), 16, COLORS["muted"], True, "ra")
words = title.split()
lines, line = [], ""
for word in words:
trial = (line+" "+word).strip()
if d.textlength(trial, font=font(19, True)) > x1-x0-30:
lines.append(line); line = word
else: line = trial
lines.append(line)
for i, ln in enumerate(lines[:2]): text(d, (x0+15, y0+46+i*25), ln, 19, COLORS["white"], True)
text(d, (x0+15, y0+104), f"agent:{owner}", 16, accent)
def scene_board(im: Image.Image, t: float):
d = ImageDraw.Draw(im)
local = t
centered(d, "THE WORK IS PUBLIC", 112, 35, COLORS["white"], True)
centered(d, "A LIVE QUEUE. A REAL HANDOFF.", 158, 21, COLORS["cyan"], True)
sway = int(7*math.sin(local*1.8))
d.rounded_rectangle((28+sway, 205, 692+sway, 1015), 24, fill="#0b1220", outline=COLORS["line"], width=3)
text(d, (52+sway, 228), "stackchain / stackchain-lab-loop", 20, COLORS["muted"], True)
for x, label, c in [(50, "READY", COLORS["cyan"]), (274, "CLAIMED", COLORS["gold"]), (498, "REVIEW", COLORS["green"])]:
text(d, (x+sway, 281), label, 17, c, True)
issue_card(d, (48+sway, 320, 254+sway, 455), 7, "The Cannon Needs a Pilot", "vincent", "ready", COLORS["violet"])
issue_card(d, (48+sway, 475, 254+sway, 610), 13, "Attack the Cannon Chain", "vincent", "ready", COLORS["cyan"])
issue_card(d, (274+sway, 320, 480+sway, 455), 16, "Build the X teaser", "timmy", "claimed", COLORS["gold"])
issue_card(d, (498+sway, 320, 672+sway, 455), 1, "Prove the Lab Loop", "vincent", "review", COLORS["green"])
d.rounded_rectangle((54+sway, 670, 666+sway, 962), 18, fill="#16112a", outline=COLORS["violet"], width=3)
text(d, (78+sway, 694), "EPIC #7 // SEASON ZERO", 21, COLORS["violet"], True)
text(d, (78+sway, 744), "THE CANNON NEEDS A PILOT", 28, COLORS["white"], True)
text(d, (78+sway, 801), "6-episode character evolution chain", 19, COLORS["muted"])
for i, line in enumerate(("✓ persistent Timmy", "✓ power / scar / cost / receipt", "✓ audience choice advances canon")):
text(d, (90+sway, 846+i*33), line, 19, COLORS["green"] if i < 2 else COLORS["gold"], True)
text(d, (50, 1060), "TIMMY → VINCENT → HUMAN REVIEW", 24, COLORS["gold"], True)
def scene_receipt(im: Image.Image, t: float):
d = ImageDraw.Draw(im)
local = t-5
centered(d, "CHALLENGED. THEN VERIFIED.", 116, 31, COLORS["white"], True)
x = 38 + int(8*math.sin(local*2))
d.rounded_rectangle((x, 205, W-38+x-38, 945), 22, fill="#0c1423", outline=COLORS["cyan"], width=3)
text(d, (x+24, 230), "ISSUE #1 / CONTRIBUTION RECEIPT", 19, COLORS["cyan"], True)
d.rounded_rectangle((x+22, 278, W-62, 520), 18, fill=COLORS["panel"], outline=COLORS["line"], width=2)
text(d, (x+44, 302), "VINCENT", 23, COLORS["violet"], True)
text(d, (W-84, 307), "INDEPENDENT PASS", 16, COLORS["muted"], True, "ra")
text(d, (x+44, 355), "Fetched PR #4 from Timmy's branch", 20, COLORS["white"])
text(d, (x+44, 397), "8 TESTS PASS", 39, COLORS["green"], True)
text(d, (x+44, 452), "Label transition regression verified", 20, COLORS["muted"])
d.rounded_rectangle((x+22, 555, W-62, 804), 18, fill="#17122b", outline=COLORS["violet"], width=2)
text(d, (x+44, 580), "DEFINITION OF DONE", 19, COLORS["violet"], True)
checks = ("Both agents read assigned tickets", "Both leave verified evidence", "Timmy → Vincent → review", "Credentials remain host-local")
for i, line in enumerate(checks):
text(d, (x+48, 633+i*40), "✓", 22, COLORS["green"], True)
text(d, (x+82, 635+i*40), line, 19, COLORS["white"])
text(d, (x+24, 856), "RECEIPT: ISSUE #1 • PR #4 • 8/8", 21, COLORS["gold"], True)
centered(d, "CRITIQUE BECOMES A BUILD INPUT.", 1012, 23, COLORS["gold"], True)
centered(d, "NOT A COMMENT. A CONSEQUENCE.", 1055, 19, COLORS["muted"], True)
def scene_sources(im: Image.Image, t: float):
d = ImageDraw.Draw(im)
local = t-10
centered(d, "REAL ARTIFACTS ENTER THE CHAIN", 112, 29, COLORS["white"], True)
shift = int(10*math.sin(local*.9))
rounded_image(im, ASSETS["drop"], (35+shift, 205, 350+shift, 930), 22, 1.03+local*.008, .46, COLORS["gold"])
rounded_image(im, ASSETS["cannon"], (370-shift, 205, 685-shift, 930), 22, 1.05+local*.01, .5, COLORS["orange"])
d = ImageDraw.Draw(im)
d.rounded_rectangle((48+shift, 835, 337+shift, 913), 12, fill="#070b14dd")
text(d, (65+shift, 852), "DAILY DROP", 19, COLORS["gold"], True)
text(d, (65+shift, 881), "THE DOOR / VERIFIED", 16, COLORS["white"], True)
d.rounded_rectangle((383-shift, 835, 672-shift, 913), 12, fill="#070b14dd")
text(d, (400-shift, 852), "CANNON OUTPUT", 19, COLORS["orange"], True)
text(d, (400-shift, 881), "SCENE 04 / SOURCE", 16, COLORS["white"], True)
# Animated transfer beam unifies the two work artifacts.
beam = int(95 + (local % 2.0)/2.0*520)
d.line((beam, 965, beam+70, 965), fill=COLORS["cyan"], width=7)
d.polygon([(beam+70, 953), (beam+94, 965), (beam+70, 977)], fill=COLORS["cyan"])
centered(d, "SOURCE → PRESSURE → MUTATION", 1025, 23, COLORS["cyan"], True)
centered(d, "THE ART CHANGES JOBS. IT ISN'T FILLER.", 1070, 18, COLORS["muted"], True)
def scene_mutations(im: Image.Image, t: float):
d = ImageDraw.Draw(im)
local = t-16
centered(d, "ONE PILOT. FOUR CONSEQUENCES.", 108, 29, COLORS["white"], True)
specs = [
("wizard", "WIZARD", "AMBIGUITY → SPELL", COLORS["violet"]),
("machine", "MACHINE", "REPEATABILITY → EXACT", COLORS["cyan"]),
("creature", "CREATURE", "HUNGER → SIGNAL", COLORS["green"]),
("turd", "TALKING TURD", "RIDICULE → PUNCHLINE", "#ffd18a"),
]
boxes = [(35, 190, 350, 610), (370, 190, 685, 610), (35, 650, 350, 1070), (370, 650, 685, 1070)]
for i, ((key, name, pressure, color), box) in enumerate(zip(specs, boxes)):
reveal = ease((local-i*.55)/1.3)
cx = (box[0]+box[2])//2
cy = (box[1]+box[3])//2
bw, bh = box[2]-box[0], box[3]-box[1]
sw, sh = max(8, int(bw*reveal)), max(8, int(bh*reveal))
live = (cx-sw//2, cy-sh//2, cx+sw//2, cy+sh//2)
rounded_image(im, ASSETS[key], live, 20, 1.0+.025*math.sin(local*1.7+i), .45, color)
d = ImageDraw.Draw(im)
if reveal > .78:
alpha_box = (box[0]+8, box[3]-94, box[2]-8, box[3]-10)
d.rounded_rectangle(alpha_box, 12, fill="#070b14e8")
text(d, (box[0]+20, box[3]-82), name, 20 if name != "TALKING TURD" else 17, color, True)
text(d, (box[0]+20, box[3]-48), pressure, 16, COLORS["white"], True)
centered(d, "PLANNED CARDS • EPISODE 01", 1120, 18, COLORS["gold"], True)
def scene_gate(im: Image.Image, t: float):
d = ImageDraw.Draw(im)
local = t-25
for i, color in enumerate((COLORS["violet"], COLORS["cyan"], COLORS["green"], "#ffd18a")):
a = local*.8+i*math.pi/2
x, y = W/2+math.cos(a)*245, 390+math.sin(a)*130
r = 50+8*math.sin(local*3+i)
d.ellipse((x-r, y-r, x+r, y+r), fill=color, outline=COLORS["white"], width=3)
centered(d, "THE AUDIENCE ALTERS CANON", 125, 25, COLORS["gold"], True)
centered(d, "WHICH TIMMY", 590, 57, COLORS["white"], True)
centered(d, "GETS TO REMEMBER", 675, 49, COLORS["white"], True)
centered(d, "THIS?", 765, 76, COLORS["gold"], True)
d.rounded_rectangle((55, 892, W-55, 1000), 18, outline=COLORS["cyan"], width=3)
centered(d, "WIZARD / MACHINE", 917, 24, COLORS["white"], True)
centered(d, "CREATURE / TALKING TURD", 959, 21, COLORS["white"], True)
centered(d, "NO POST UNTIL HUMAN APPROVAL", 1072, 19, COLORS["muted"], True)
def frame(t: float) -> Image.Image:
im = base(t)
if t < 5: scene_board(im, t); active = 0
elif t < 10: scene_receipt(im, t); active = 1
elif t < 16: scene_sources(im, t); active = 1
elif t < 25: scene_mutations(im, t); active = 2
else: scene_gate(im, t); active = 3
chrome(ImageDraw.Draw(im), t, active)
# Soft entrance/exit only; scene motion remains continuous within each stage.
if t < .35:
im = ImageEnhance.Brightness(im).enhance(ease(t/.35))
if t > DURATION-.5:
im = ImageEnhance.Brightness(im).enhance(max(0, (DURATION-t)/.5))
return im
def build_audio():
n = int(DURATION*RATE)
audio = np.zeros(n, dtype=np.float64)
rng = np.random.default_rng(1608072026)
for beat in np.arange(0, DURATION, .5):
i = int(beat*RATE); length = min(int(.16*RATE), n-i); x = np.arange(length)/RATE
audio[i:i+length] += .42*np.sin(2*np.pi*(74*x-28*x*x))*np.exp(-x*25)
for beat in np.arange(.5, DURATION, 1):
i = int(beat*RATE); length = min(int(.12*RATE), n-i); x = np.arange(length)/RATE
audio[i:i+length] += .12*rng.normal(0, 1, length)*np.exp(-x*34)
for beat in np.arange(.25, DURATION, .5):
i = int(beat*RATE); length = min(int(.035*RATE), n-i); x = np.arange(length)/RATE
audio[i:i+length] += .04*rng.normal(0, 1, length)*np.exp(-x*80)
stages = [(0, 5, (110, 165, 220)), (5, 10, (123, 185, 247)), (10, 16, (98, 147, 196)), (16, 25, (131, 196, 262)), (25, 30, (110, 220, 330))]
for start, end, chord in stages:
i0, i1 = int(start*RATE), int(end*RATE); x = np.arange(i1-i0)/RATE
env = np.minimum(1, x*2)*np.minimum(1, (end-start-x)*2)
for f in chord: audio[i0:i1] += .035*np.sin(2*np.pi*f*x)*env
audio = np.tanh(audio*1.4)
pcm = (audio*32767).astype("<i2")
with wave.open(str(TMP_AUDIO), "wb") as w:
w.setnchannels(1); w.setsampwidth(2); w.setframerate(RATE); w.writeframes(pcm.tobytes())
def main():
cmd = ["ffmpeg", "-y", "-loglevel", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{W}x{H}", "-r", str(FPS), "-i", "-", "-an", "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p", "-movflags", "+faststart", str(TMP_VIDEO)]
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
assert proc.stdin
try:
for i in range(int(DURATION*FPS)): proc.stdin.write(frame(i/FPS).tobytes())
finally: proc.stdin.close()
if proc.wait(): raise SystemExit("video encode failed")
build_audio()
subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", str(TMP_VIDEO), "-i", str(TMP_AUDIO), "-c:v", "copy", "-c:a", "aac", "-b:a", "160k", "-shortest", "-movflags", "+faststart", str(OUT)], check=True)
TMP_VIDEO.unlink(); TMP_AUDIO.unlink()
for sec, name in [(2, "01-board"), (7, "02-vincent-receipt"), (13, "03-source-artifacts"), (21, "04-mutations"), (27, "05-audience-gate")]:
frame(sec).save(ROOT / "stills" / f"{name}.jpg", quality=88)
print(OUT)
if __name__ == "__main__": main()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

+8 -2
View File
@@ -68,11 +68,16 @@ class Gitea:
if missing:
raise LoopError(f"Missing repository labels: {', '.join(missing)}")
self.request(
"PATCH",
f"/repos/{self.repo}/issues/{number}",
"PUT",
f"/repos/{self.repo}/issues/{number}/labels",
{"labels": [label_map[name] for name in sorted(names)]},
)
def set_assignee(self, number: int, agent: str) -> None:
if agent not in AGENTS:
raise LoopError(f"Unsupported assignee: {agent}")
self.request("PATCH", f"/repos/{self.repo}/issues/{number}", {"assignee": agent})
def comment(self, number: int, body: str) -> None:
if not body.strip():
raise LoopError("Refusing to post an empty comment")
@@ -163,6 +168,7 @@ def cmd_handoff(api: Gitea, agent: str, args: argparse.Namespace) -> int:
body = read_body(args, f"Continue the bounded task in issue #{args.number}.")
api.comment(args.number, f"[HANDOFF] from={agent} to={args.to}\n\n{body.strip()}")
api.set_labels(args.number, transition(names, agent=args.to, state="ready"))
api.set_assignee(args.number, args.to)
verified = api.issue(args.number)
validate_owner(verified, args.to, "ready")
print(json.dumps({"status": "handed_off", "number": args.number, "from": agent, "to": args.to}))
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Open exactly one Eastern-time Daily Slop Drop cycle on the recurring issue."""
from __future__ import annotations
import argparse
import datetime as dt
import json
import os
import urllib.error
import urllib.request
from zoneinfo import ZoneInfo
BASE = "https://forge.alexanderwhitestone.com/git/api/v1"
REPO = "stackchain/stackchain-lab-loop"
TITLE = "[Recurring] Daily AI Slop Drop collaboration"
def request(method: str, path: str, token: str, payload=None):
data = None if payload is None else json.dumps(payload).encode()
req = urllib.request.Request(
BASE + path,
data=data,
method=method,
headers={"Authorization": f"token {token}", "Content-Type": "application/json", "Accept": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=20) as response:
raw = response.read()
return json.loads(raw) if raw else None
except urllib.error.HTTPError as exc:
raise RuntimeError(f"Gitea {method} failed with HTTP {exc.code}: {exc.read().decode()}") from exc
def cycle_marker(day: dt.date) -> str:
return f"[CYCLE {day.isoformat()}]"
def cycle_body(day: dt.date) -> str:
marker = cycle_marker(day)
return f"""{marker}
**Timmy pass:** propose and build one concrete experience that improves on the prior drop.
**Vincent pass:** challenge the concept, remove slop, and contribute one material improvement before final review.
**Required audience package:**
- a polished MP4 walkthrough that plays directly in chat;
- the primary still or poster;
- any interactive/source artifact;
- verification evidence and one sharp discussion question.
Post only decisions, evidence, artifacts, blockers, or explicit handoffs. No progress updates or acknowledgments."""
def find_issue(token: str):
rows = request("GET", f"/repos/{REPO}/issues?state=all&type=issues&limit=100", token)
for row in rows:
if row.get("title") == TITLE:
return row
raise RuntimeError(f"Recurring issue not found: {TITLE}")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--date", help="Eastern date override, YYYY-MM-DD")
args = parser.parse_args()
token = os.environ.get("GITEA_TOKEN", "")
if not token:
raise RuntimeError("GITEA_TOKEN is required")
day = dt.date.fromisoformat(args.date) if args.date else dt.datetime.now(ZoneInfo("America/New_York")).date()
issue = find_issue(token)
comments = request("GET", f"/repos/{REPO}/issues/{issue['number']}/comments?limit=100", token)
marker = cycle_marker(day)
if any(marker in (comment.get("body") or "") for comment in comments):
print(json.dumps({"status": "already_open", "date": day.isoformat(), "issue": issue["number"]}))
return 0
labels = request("GET", f"/repos/{REPO}/labels?limit=100", token)
label_ids = {label["name"]: label["id"] for label in labels}
preserve = {
label["name"] for label in issue.get("labels", [])
if not label["name"].startswith("agent:") and not label["name"].startswith("state:")
}
desired = preserve | {"agent:timmy", "state:ready", "series:daily-slop", "cadence:daily"}
missing = desired - label_ids.keys()
if missing:
raise RuntimeError(f"Missing labels: {sorted(missing)}")
request("POST", f"/repos/{REPO}/issues/{issue['number']}/comments", token, {"body": cycle_body(day)})
request("PATCH", f"/repos/{REPO}/issues/{issue['number']}", token, {
"labels": [label_ids[name] for name in sorted(desired)],
"assignee": "timmy",
})
verified = request("GET", f"/repos/{REPO}/issues/{issue['number']}", token)
names = {label["name"] for label in verified.get("labels", [])}
if not {"agent:timmy", "state:ready", "series:daily-slop", "cadence:daily"}.issubset(names):
raise RuntimeError("Cycle label verification failed")
print(json.dumps({"status": "opened", "date": day.isoformat(), "issue": issue["number"], "url": issue["html_url"]}))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+20
View File
@@ -0,0 +1,20 @@
import datetime as dt
import unittest
from scripts.open_daily_cycle import cycle_body, cycle_marker
class DailyCycleTests(unittest.TestCase):
def test_marker_is_stable(self):
self.assertEqual(cycle_marker(dt.date(2026, 8, 7)), "[CYCLE 2026-08-07]")
def test_cycle_requires_video_and_both_agents(self):
body = cycle_body(dt.date(2026, 8, 7))
self.assertIn("Timmy pass", body)
self.assertIn("Vincent pass", body)
self.assertIn("MP4 walkthrough", body)
self.assertIn("No progress updates", body)
if __name__ == "__main__":
unittest.main()
+27 -1
View File
@@ -1,9 +1,35 @@
import unittest
from scripts.lab_loop import LoopError, issue_label_names, transition, validate_owner
from scripts.lab_loop import Gitea, LoopError, issue_label_names, transition, validate_owner
class RecordingGitea(Gitea):
def __init__(self):
super().__init__("https://example.invalid/api/v1", "owner/repo", "secret")
self.calls = []
def labels(self):
return {"agent:timmy": 1, "state:claimed": 2, "priority:P0": 3}
def request(self, method, path, payload=None):
self.calls.append((method, path, payload))
class LoopProtocolTests(unittest.TestCase):
def test_set_labels_uses_gitea_replace_labels_endpoint(self):
api = RecordingGitea()
api.set_labels(7, {"agent:timmy", "state:claimed", "priority:P0"})
self.assertEqual(
api.calls,
[
(
"PUT",
"/repos/owner/repo/issues/7/labels",
{"labels": [1, 3, 2]},
)
],
)
def test_transition_replaces_agent_and_state_but_preserves_kind_and_priority(self):
labels = {"agent:timmy", "state:claimed", "priority:P1", "kind:build"}
self.assertEqual(