[object Object]

← back to Crazy News Channel Shadowman

P24 cartoon batch: 6:00 ET start (3:00 PT), build all briefs in parallel to post by 6:30 ET; parent registers manifest/stories + commits (keeps story_id + tags rules)

7b23cb12c9e4328fe7be80bf1d01fa3ba2b3b73c · 2026-09-24 15:24:27 -0700 · Steve Abrams

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

Files touched

Diff

commit 7b23cb12c9e4328fe7be80bf1d01fa3ba2b3b73c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 24 15:24:27 2026 -0700

    P24 cartoon batch: 6:00 ET start (3:00 PT), build all briefs in parallel to post by 6:30 ET; parent registers manifest/stories + commits (keeps story_id + tags rules)
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
 scripts/build-cartoon-batch.sh | 168 ++++++++++++++++++++++-------------------
 1 file changed, 90 insertions(+), 78 deletions(-)

diff --git a/scripts/build-cartoon-batch.sh b/scripts/build-cartoon-batch.sh
index 60b9852..31c65ef 100644
--- a/scripts/build-cartoon-batch.sh
+++ b/scripts/build-cartoon-batch.sh
@@ -68,16 +68,25 @@ QFILE="$DIR/cartoons/queue/$DATE.json"
 [ -f "$QFILE" ] || { echo "$TS no queue file for $DATE" >>"$LOG"; exit 1; }
 
 python3 - "$QFILE" "$DRY" "$LOG" "$TS" "$DIR" <<'PYEOF'
-import json, sys, subprocess, os
+# PARALLEL build (2026-09-24, Steve: start 6:00 ET, finish + post by 6:30 ET).
+# Sequential took ~48 min for 6; all briefs now build concurrently with a hard
+# 25-min timeout. Builders write ONLY their HTML + a meta sidecar; this parent
+# alone appends manifest.js / stories-data.js and commits, so parallel sessions
+# can't race on shared files or git's index.lock. Keeps the TK-12158 real-article
+# story_id rule and the TK-12165 non-empty tags rule, now checked on the sidecar.
+import json, sys, subprocess, os, datetime, re
+from concurrent.futures import ThreadPoolExecutor
 qfile, dry, log, ts, root = sys.argv[1:6]
 q = json.load(open(qfile))
-for b in q['briefs']:
-    if b.get('status') != 'queued':
-        continue
+todo = [b for b in q['briefs'] if b.get('status') == 'queued']
+tools = "Read Write Edit Glob Grep Bash(node:*) Bash(tk:*) Bash(/Users/macstudio3/Projects/ticket-system/tk:*)"
+
+def build(b):
+    meta = os.path.join(root, "cartoons", "queue", b["slug"] + ".meta.json")
     prompt = (
-        f"Build one P24 (PANDEMONIUM-24) Political Cartoon Desk strip, ticket-tracked "
+        f"Build one P24 (PANDEMONIUM-24) Cartoon Desk strip, ticket-tracked "
         f"(export TK_AGENT=p24-cartoon-batch; tk new for this brief under project crazy-news-channel). "
-        f"TITLE: {b['title']}\nBRIEF: {b['brief']}\n\n"
+        f"TITLE: {b['title']}\nBRIEF: {b['brief']}\nSECTION: {b.get('category','politics')}\n\n"
         f"Follow the exact style, acceptance bar, and file structure of "
         f"~/Projects/crazy-news-channel/cartoons/20260924-commission-turtles.html "
         f"(read it first as the reference implementation): 3-panel click-to-advance SVG strip, "
@@ -86,87 +95,90 @@ for b in q['briefs']:
         f"respected, dark-mode toggle, zero console errors. Save to "
         f"~/Projects/crazy-news-channel/cartoons/{b['slug']}.html. Playwright-test at 1440/1024/390 "
         f"+ reduced-motion + dark (require playwright via ~/Projects/animals/node_modules/playwright, "
-        f"launch chromium with channel:'chrome'). Fix any real defects found, then add an entry to "
-        f"~/Projects/crazy-news-channel/cartoons/manifest.js (as a new object appended to the window.P24_CARTOONS array: id, title, file, created_at ISO now, "
-        f"category 'Political Cartoon', blurb, thumb:null, tags) and commit with author "
-        f"steve@designerwallcoverings.com. HARD RULE — Steve's standing instruction is 'always link "
-        f"images cartoons to the real article': the manifest entry's story_id field must be a real, "
-        f"non-null id. If no existing entry in ~/Projects/crazy-news-channel/stories-data.js or "
-        f"real-news-data.js already covers this brief's topic, write ONE new satirical story object "
-        f"matching the exact schema of an existing window.P24_EXTRA_STORIES entry (id, category, "
-        f"categoryLabel, location, byline, image, intervalSec, offsetSec, stageIndex, article.dek, "
-        f"article.photoCaption, article.paragraphs, stages, tags) and append it to stories-data.js, then set "
-        f"this cartoon's story_id to that new id. Never leave story_id: null. HARD RULE (TK-12165) — "
-        f"every post carries tags: the new (or reused) story object's tags array must be non-empty (3-6 "
-        f"short, lowercase-ish, human-readable tags reusing the shared vocabulary already in "
-        f"stories-data.js where it fits, e.g. 'city council', 'bureaucracy', 'parking', 'small town "
-        f"news' — so tags actually recur across posts), and this cartoon's own manifest.js entry must "
-        f"set tags to that story's tags PLUS at most 1-2 cartoon-specific tags (e.g. 'editorial "
-        f"cartoon', and 'paul conrad style' only if drawn in that manner). Never leave tags: [] or "
-        f"omit the field on either object. Do not deploy, push, or touch anything outside this repo."
+        f"launch chromium with channel:'chrome'); keep test scripts/screenshots in /tmp, NOT the repo. "
+        f"Fix any real defects found.\n\n"
+        f"Then write {meta} as JSON: {{\"blurb\": one sentence, \"story_id\": id, \"tags\": [...], "
+        f"\"new_story\": object-or-null}}. HARD RULE — every cartoon links to a real article: story_id "
+        f"must be non-null. Reuse an existing id from ~/Projects/crazy-news-channel/stories-data.js or "
+        f"real-news-data.js if one covers this topic (new_story: null); otherwise put ONE new satirical "
+        f"story in new_story matching the exact schema of an existing window.P24_EXTRA_STORIES entry "
+        f"(id, category, categoryLabel, location, byline, image, intervalSec, offsetSec, stageIndex, "
+        f"article.dek, article.photoCaption, article.paragraphs, stages, tags) with new_story.id == story_id. "
+        f"HARD RULE (TK-12165) — the story's tags must be non-empty (3-6 short lowercase-ish tags reusing "
+        f"the shared vocabulary in stories-data.js, e.g. 'city council', 'bureaucracy', 'small town news'), "
+        f"and the sidecar tags = that story's tags plus at most 1-2 cartoon tags (e.g. 'editorial cartoon'). "
+        f"HARD LIMIT: finish within 20 minutes. Do NOT edit cartoons/manifest.js or stories-data.js and do "
+        f"NOT git commit — the batch process registers and commits your work. Do not deploy, push, or "
+        f"touch anything else."
     )
-    if dry == "1":
-        print(f"[DRY RUN] would build: {b['id']} — {b['title']}")
-        continue
-    print(f"Building {b['id']} — {b['title']} ...")
-    tools = "Read Write Edit Glob Grep Bash(node:*) Bash(git add:*) Bash(git commit:*) Bash(git status:*) Bash(git diff:*) Bash(tk:*) Bash(/Users/macstudio3/Projects/ticket-system/tk:*)"
     try:
         r = subprocess.run(["claude", "-p", prompt, "--permission-mode", "acceptEdits", "--allowedTools", tools],
-                           cwd=root, capture_output=True, text=True, timeout=1800)
+                           cwd=root, capture_output=True, text=True, timeout=1500)
         rc = r.returncode
     except subprocess.TimeoutExpired:
         rc = 124
-    # exit 0 is not proof: require the file on disk AND a manifest entry for it.
-    out = os.path.join(root, "cartoons", b["slug"] + ".html")
-    man = open(os.path.join(root, "cartoons", "manifest.js")).read()
-    has_file, in_manifest = os.path.isfile(out), (b["slug"] + ".html") in man
-    # HARD RULE check (TK-12158): a cartoon with story_id: null is not a valid
-    # end state (Steve, verbatim: "always link images cartoons to the real
-    # article"). Cheap substring check, not a full JSON parse — find this
-    # entry's object body between its "file" key and the object's closing
-    # "  }" line, and require a non-null "story_id" inside it.
-    has_story_id = False
-    # HARD RULE (TK-12165): a cartoon with tags: [] (or missing) is not a
-    # valid end state either — same substring-check approach as story_id
-    # above (this is a cheap FAIL gate, not a full JSON parse). A non-empty
-    # tags array always contains at least one quoted string between the
-    # brackets, so "tags": [] (empty) or a missing key both fail the regex.
-    has_tags = False
-    story_tags_ok = False
-    marker = f'"file": "{b["slug"]}.html"'
-    mi = man.find(marker)
-    if mi != -1:
-        obj_end = man.find("\n  }", mi)
-        obj_body = man[mi: obj_end if obj_end != -1 else mi + 2000]
-        m = __import__("re").search(r'"story_id"\s*:\s*"([^"]+)"', obj_body)
-        has_story_id = bool(m)
-        tm = __import__("re").search(r'"tags"\s*:\s*\[\s*"[^\]]*\]', obj_body)
-        has_tags = bool(tm)
-        # If a NEW story object was appended to stories-data.js for this
-        # brief, it must also carry non-empty tags — check the story's own
-        # object body there the same way (find its id, then its own tags
-        # field before the next top-level "},\n {" or the array's closing).
-        if has_story_id:
-            story_id_val = m.group(1)
-            try:
-                sd = open(os.path.join(root, "stories-data.js")).read()
-                sid_marker = f'"id": "{story_id_val}"'
-                si = sd.find(sid_marker)
-                if si != -1:
-                    s_end = sd.find('\n }', si)
-                    s_body = sd[si: s_end if s_end != -1 else si + 4000]
-                    story_tags_ok = bool(__import__("re").search(r'"tags"\s*:\s*\[\s*"', s_body))
-                else:
-                    story_tags_ok = True  # story_id resolves to real-news-data.js instead — out of this script's scope, don't false-FAIL
-            except FileNotFoundError:
-                story_tags_ok = True
-    ok = rc == 0 and has_file and in_manifest and has_story_id and has_tags and story_tags_ok
+    return b, rc, meta
+
+if dry == "1":
+    for b in todo: print(f"[DRY RUN] would build: {b['id']} — {b['title']}")
+    sys.exit(0)
+
+with ThreadPoolExecutor(max_workers=len(todo) or 1) as ex:
+    results = list(ex.map(build, todo))
+
+def load_js_array(path, var):
+    head, arr = open(path).read().split(f"window.{var} = ", 1)
+    return head, json.loads(arr.rstrip().rstrip(";"))
+
+def save_js_array(path, var, head, data, indent):
+    tmp = path + ".tmp"
+    with open(tmp, "w") as f:
+        f.write(head + f"window.{var} = " + json.dumps(data, indent=indent, ensure_ascii=False) + ";\n")
+    os.replace(tmp, path)
+
+man_path = os.path.join(root, "cartoons", "manifest.js")
+sd_path = os.path.join(root, "stories-data.js")
+m_head, entries = load_js_array(man_path, "P24_CARTOONS")
+s_head, stories = load_js_array(sd_path, "P24_EXTRA_STORIES")
+try: real_news = open(os.path.join(root, "real-news-data.js")).read()
+except OSError: real_news = ""
+now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
+added, new_stories = [], 0
+for b, rc, meta_path in results:
+    fname = b["slug"] + ".html"
+    has_file = os.path.isfile(os.path.join(root, "cartoons", fname))
+    try: meta = json.load(open(meta_path))
+    except Exception: meta = {}
+    sid, tags, ns = meta.get("story_id"), meta.get("tags") or [], meta.get("new_story")
+    story_ok = False
+    if sid and ns:
+        story_ok = ns.get("id") == sid and bool(ns.get("tags")) and not any(s.get("id") == sid for s in stories)
+    elif sid:
+        story_ok = any(s.get("id") == sid for s in stories) or f'"{sid}"' in real_news
+    ok = (rc == 0 and has_file and bool(meta.get("blurb")) and story_ok and bool(tags)
+          and not any(e.get("file") == fname for e in entries))
+    if ok:
+        if ns: stories.append(ns); new_stories += 1
+        sec = b.get("category", "politics")
+        entries.append({"id": b["id"], "title": b["title"], "file": fname, "created_at": now,
+                        "category": "Political Cartoon" if sec == "politics" else "Editorial Cartoon",
+                        "section": sec, "blurb": meta["blurb"], "thumb": None,
+                        "story_id": sid, "tags": tags})
+        added.append(fname)
     with open(log, "a") as f:
         f.write(json.dumps({"ts": ts, "id": b["id"], "title": b["title"], "exit": rc,
-                            "file": has_file, "manifest": in_manifest, "story_id": has_story_id,
-                            "tags": has_tags, "story_tags": story_tags_ok,
-                            "ok": ok, "verdict": "PASS" if ok else "FAIL"}) + "\n")
+                            "file": has_file, "blurb": bool(meta.get("blurb")), "story_id": story_ok,
+                            "tags": bool(tags), "ok": ok, "verdict": "PASS" if ok else "FAIL"}) + "\n")
     b['status'] = 'built' if ok else 'failed'
+if added:
+    save_js_array(man_path, "P24_CARTOONS", m_head, entries, 2)
+    paths = ["cartoons/manifest.js"] + ["cartoons/" + a for a in added]
+    if new_stories:
+        save_js_array(sd_path, "P24_EXTRA_STORIES", s_head, stories, 1)
+        paths.append("stories-data.js")
+    subprocess.run(["git", "add", "--"] + paths, cwd=root)
+    subprocess.run(["git", "-c", "user.email=steve@designerwallcoverings.com", "commit", "-q", "-m",
+                    f"P24 daily cartoons {q['date']}: {len(added)} built + registered"], cwd=root)
 json.dump(q, open(qfile, 'w'), indent=2)
 sys.exit(1 if any(b.get('status') == 'failed' for b in q['briefs']) else 0)
 PYEOF

← 2a7fdfb auto-data-snapshot: 2026-09-24T15:23:04 (2 data files) — scr  ·  back to Crazy News Channel Shadowman  ·  P24 cartoon batch: build one at a time (no parallel rate-lim bd41b0b →