Add recurring Daily Slop Drop collaboration cycle
This commit is contained in:
parent
3c6d41b49c
commit
cec957ead6
99
scripts/open_daily_cycle.py
Normal file
99
scripts/open_daily_cycle.py
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
#!/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)]})
|
||||||
|
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
tests/test_daily_cycle.py
Normal file
20
tests/test_daily_cycle.py
Normal 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()
|
||||||
Loading…
Reference in New Issue
Block a user