OUTPUT #687
PARÇA 1 / 2
TOPLAM: 61543 karakter | 1283 satır
BU PARÇA: 40000 karakter
========== YENİ KOMUT | 16.09.2026 22:38:17 ==========


========== YENİ KOMUT | 16.09.2026 22:38:17 ==========


===== /home/hermes/.hermes/skills/media/shorts-core/SKILL.md =====
82-
83-- Pan/zoom verilmiş fotoğrafı gerçek footage yerine koyma. Fotoğraf zorunluysa destekleyici ve kısa kullan; annotation, belge detayı veya gerçek footage ile bağlantı kur.
84-- Aynı görsel kompozisyonunu yaklaşık **2–3 saniyeden uzun** kesintisiz tutmayı QA uyarısı say; fakat otomatik kesme reçetesi yapma. Uzun cümleyi anlamlı alt beat’lere böl ve gerçek hareket varsa planı yalnız süre doldu diye kesme.
85-- Rastgele hızlı kesme yapma. Kesme noktaları cümle, yan cümle, vurgu veya anlatı beat’iyle eşleşmeli.
86-- Aynı kaynak görsel yeniden kullanılacaksa farklı crop, focal point, zoom yönü veya renk tonu ile yeni bir kompozisyon oluştur.
87:- Hareketin gerçekten var olduğunu `freezedetect` ve farklı zamanlardan kareler ile doğrula.
88-
89-### 6. Anlamlı grafik ve animasyon
90-
91-- Anlatımda **süreç, sıralama, karşılaştırma, mekanizma veya neden-sonuç** varsa yalnızca fotoğraf gösterme; sesle senkron açıklayıcı grafik/animasyon üret.
92-- Grafik yalnızca estetik süs değil, cümlenin anlamını görünür kılmalı.
--
100-
101-### 7. Müzik ve ses dengesi
102-
103-- Anlatım ana unsur olmalı. Müzik varsayılan olarak düşük seviyede (`0.10`–`0.18` lineer gain) ve konuşma altında ducking ile kullanılmalı.
104-- Başlangıç/bitiş fade’i ekle; müziği video süresine eksiksiz uzat veya kontrollü döngüle.
105:- Final karışım için pratik hedef: yaklaşık **−18 ila −14 LUFS**, clipping yok, true peak tercihen **≤ −1 dBFS**.
106-- TTS cümle başlarını dinamik müzik veya geçiş efektiyle maskeleme.
107-- Her beat için tek bir ses odağı seç: konuşma, müzik veya efekt. Katmanların tamamını aynı anda tepeye çıkarma.
108-- Görsel geçişi her zaman sesle aynı karede kesmek zorunda değilsin; anlamlı J/L-cut ve ses köprüleri akışı yumuşatabilir.
109-
110-### 8. Hook, payoff ve bilişsel yük
--
125-
126-### 10. Yayın sonrası öğrenme
127-
128-- Her video için üretimden önce tek bir test hipotezi kaydet: hook, süre, format, görsel kanıt veya payoff gibi.
129-- YouTube teşhis sırası: **feed gösterimi → viewed/swiped → engaged view → retention/AVD/% viewed → memnuniyet/etkileşim → abone/geri dönen izleyici**.
130:- Instagram teşhis sırası: **recommendation uygunluğu → views/viewers → skip/average watch time → tam izleme → paylaşım/kaydetme → follows**.
131-- Ham görüntülemeyi tek başına kalite ölçüsü sayma. Videoyu benzer süre ve aynı formatın tipik performansıyla karşılaştır.
132-- Retention diplerini otomatik olarak “daha hızlı kes” diye yorumlama; vaat uyuşmazlığı, tekrar, konu sapması, kafa karışıklığı veya bilişsel yükü ayrı teşhis et.
133-- Tek viral sonucu kalıcı format sayma; kazanan hipotezi iki-üç kontrollü tekrar ile doğrula.
134-
135-## Faz Kapılı Hızlı Üretim Prosedürü
136-
137-Amaç yalnız render süresini değil, ana model agent-loop sayısını da azaltmaktır. Aynı üretimde `ara → oku → yaz → çalıştır → tekrar ara` biçimindeki mikro turları çoğaltma.
138-
139-### Faz 1 — Tek planlama turu
140-
141:Üretime başlamadan önce tam **bir kez** `delegate_task` çağır. `goal` mutlaka `HERMES_CREATIVE_DIRECTOR` ile başlasın. Bu delegation tamamlanıp creative contract geri dönmeden asset arama, terminal, pipeline, TTS, render veya başka production tool çağrısı başlatma.
142-
143-Creative Director'a şu sınırlar ve hedefler ver:
144-
145-**IMMUTABLE TECHNICAL BASELINE**
146-- Ana executor `gpt-5.6-sol` olarak kalır.
--
158-
159-**CREATIVE AUTHORITY**
160-- konuya özgü hook ve visual thesis
161-- real-footage-first omurga
162-- yaklaşık 6–8 anlamlı shot ile güçlü progression
163:- asset çeşitliliği ve batch asset discovery hedefleri
164-- yalnız görünmeyen mekanizma için kısa grafik/annotation
165-- net, görünür payoff
166-- narration'ın içerik/dramatik akışı
167-
168:Astra çözüm yönteminde yaratıcı olarak serbesttir; yukarıdaki teknik baseline'a müdahale edemez. Web/terminal/browser/TTS/Vision veya production tool kullanmaz. Uzun manifesto, alternatif A/B/C planları, temporary helper, micro-loop veya ikinci re-plan üretmez.
169-
170-Tek çıktı kısa ve uygulanabilir bir contract olsun:
171:`creative_thesis -> hook -> narration_arc -> shot progression -> mechanism -> payoff -> batch_asset_targets -> creative_donts`
172-
173-Bu production request içinde ikinci Astra delegation yapma. Sol, dönen contract'ı yaratıcı yön olarak kullanır ve teknik execution'ı mevcut Golden kurallarıyla kendisi tamamlar.
174-
175-Tek ana-model turunda mümkün olduğunca birlikte çöz:
176-
177-- izleyici vaadi, hook, payoff ve kısa Türkçe narration;
178-- seçilmiş TTS sesi ve hız/pitch değerleri;
179-- VTT zamanlamasını kullanacak altyazı yaklaşımı;
180:- her beat için shot manifesti ve mevcut yerel asset yolu;
181:- final çıktı yolu ve kabul kriterleri.
182-
183:Mevcut workspace ve asset yolları biliniyorsa `/home/hermes` genelinde tekrar `search_files` taraması yapma. Bilinen proje dizinini ve mevcut manifest/cache bilgisini kullan.
184-
185-TTS sesi henüz seçilmemişse iki ücretsiz Türkçe sesi kısa aynı örnekle karşılaştır; seçim yapıldıktan sonra üretim fazında tekrar ses keşfi yapma.
186-
187:### ASSET-001 — Pre-casting visual inspection diagnostic
188-
189:Bu tanı koşusunda asset/casting kararını yalnız metadata, dosya adı veya arama açıklamasından verme.
190:Gerekli olduğunda aday gerçek footage/source/range/assetleri `vision_analyze` ile gerçekten gör.
191-
192-Değerlendir:
193-- gerçek hareket ve görüntü kalitesi;
194-- shot'ın anlatısal işlevini gerçekten kanıtlayıp kanıtlamadığı;
195-- aynı video içinde adayların görsel olarak birbirini gereksiz tekrar edip etmediği;
196-- kadraj, viewpoint, scale, environment ve motion bakımından yeterli progression olup olmadığı.
197-
198-Güzel fakat anlatısal işlevi karşılamayan adayı reddet ve gerekirse yeni aday ara.
199:Bu tanı koşusunda yapay Vision çağrı limiti yoktur ve batch zorunlu değildir.
200-Aynı kanıtı aynı soruyla gereksiz tekrar inceleme.
201:Bu izin yalnız asset/casting görsel değerlendirmesidir; model/provider, TTS, pipeline,
202-config, servis veya teknik mimariyi değiştirme.
203-
204-### Faz 2 — Deterministic fast pipeline
205-
206-Narration, seçilmiş voice ve shot manifest kesinleşince tek seferlik medya işlemleri için:
--
215-
216-Helper şu işleri tek deterministic zincirde yürütür:
217-
218-- seçilmiş Türkçe TTS + padding;
219-- VTT tabanlı en fazla iki satır ASS altyazı;
220:- full final encode öncesi gerçek glyph bbox guardrail;
221-- seçilmiş shot'lardan tek 1080×1920 visual master;
222-- hazır master + ses için FFmpeg direct mux ve `-c:v copy`;
223-- video stream hash kontrolü;
224:- probe, decode, black/freeze ve loudness QA;
225:- tek final contact sheet;
226:- TTS, bbox, visual master, final mux ve deterministic QA cache.
227-
228-Değişmeyen manifest ve girdilerde cache'i yeniden kullan. Aynı deterministic adımı doğrulama amacıyla tekrar üretme.
229-
230-Helper için geçici tek-seferlik bbox scripti, yeni Python ortamı veya yeniden keşfedilmiş FFmpeg/Montaj yolları oluşturma. Helper teknik olarak başarısız olursa yalnız hatalı bileşeni teşhis et.
231-
232-### Faz 3 — Tek görsel QA geçidi
233-
234:Deterministic pipeline PASS verdikten sonra yalnız final contact sheet'i `vision_analyze` ile değerlendir.
235-
236-Kontrol et:
237-
238-- gerçek 9:16 tam ekran;
239-- siyah/boş bant veya yanlış crop;
--
247-
248-FAIL ise yalnız doğrulanmış kusuru etkileyen shot/overlay/altyazıyı değiştir ve pipeline'ı yeniden çalıştır. Cache sağlam kalan aşamaları yeniden üretmemelidir.
249-
250-### Faz 4 — Teslim
251-
252:Tek final MP4 teslim et. Son raporu kısa tut ve yalnız doğrulanmış metrikleri yaz.
253-
254:Ana GPT için hedef, standart mevcut-asset Shorts üretiminde yaklaşık **12–18 agent-loop çağrısı veya daha azıdır**. Bu sayı kalite kriterlerini atlamak için değil, bağımsız tool çağrılarını aynı fazda toplamak ve deterministic işleri script'e taşımak için bir verimlilik guardrail'idir.
255-
256-## Montaj Kullanım Kararı
257-
258-- **Hazır görsel master + yalnız ses/müzik:** FFmpeg direct mux varsayılandır. Video üzerinde değişiklik gerekmiyorsa `-c:v copy` ile video stream’i yeniden encode etme.
259-- **Kısa, düzenlenebilir, zamanlı katman:** Montaj JSX overlay kullan.
260:- **Gerçek timeline edit/composite:** Montaj kullan; validate sonrası yalnız gerekli final render’ı yap.
261-- **Uzun ve her karede değişen JSX:** Puppeteer maliyetlidir; önceden render edilmiş görsel master daha az ajan turu ve yeniden render üretir.
262-- **Şema bilgisi:** Önce `references/montaj-fast-path.md`; yalnızca eksik alan varsa yerel `docs/schemas/project.md` dosyasına bak.
263-
264-## Kaçınılacak Hatalar
265-
--
267-- Altyazıyı üç satıra zorlamak ya da kenara kadar uzatmak.
268-- Whisper’ın yanlış Türkçe kelimelerini altyazıya aynen taşımak.
269-- TTS hızını agresif artırmak veya ilk heceyi fade/padding olmadan kesmek.
270-- Tek fotoğrafı 5–10 saniye küçük bir zoom bile olmadan göstermek.
271-- Anlamlı bir mekanizmayı yalnızca dekoratif parçacıklarla geçiştirmek.
272:- Bbox/preflight guardrail geçmeden pahalı final render’a girmek.
273-- Hazır video master’ı yalnız ses eklemek için Montaj’da yeniden encode etmek.
274-- Değişmeyen source-sheet, shot, annotation veya QA çıktısını cache varken yeniden üretmek.
275:- Bilinen workspace varken tüm home dizininde tekrar asset aramak veya üretim içinde tek-seferlik bbox/QA scriptleri yeniden yazmak.
276-- Background süreçte PATH’e güvenip `montaj: command not found` hatasına düşmek.
277:- Başarılı tool çıkışını teslim başarısı saymak; final MP4’ü yeniden probe ve görsel QA ile doğrulamak.
278-
279-## Doğrulama Geçidi
280-
281-Teslimden önce bunların tamamı doğru olmalı:
282-
--
289-- [ ] Fotoğraf yalnız destekleyici görevde; gereksiz statik sahne veya yalnız kayan kartlardan oluşan bölüm yok.
290-- [ ] Sahne grameri konuya uygun çeşitleniyor; grid/kart/easing/kompozisyon mekanik biçimde tekrarlanmıyor.
291-- [ ] Grafik ve animasyon görünmeyen mekanizmayı açıklıyor; gerçek görüntünün yerine geçmiyor ve ona anlamlı geçişle bağlanıyor.
292-- [ ] Büyük vurgu metni altyazıyı gereksiz yere tekrar etmiyor.
293-- [ ] TTS metni konuşma dilinde; en az iki ücretsiz Türkçe ses karşılaştırılmış, ritim/durak/vurgu doğal ve ilk/son hece kesilmiyor.
294:- [ ] İlk kare/cümle anlaşılır vaat ve bilgi açığı kuruyor; final payoff bu vaadi karşılıyor.
295-- [ ] Her kesmenin anlatısal görevi var; aşırı bilişsel yük veya rastgele hızlandırma yok.
296-- [ ] Sahne değişimleri ve grafikler anlatım zamanlarıyla eşleşiyor; bakış yönü korunuyor.
297-- [ ] Süreç/sıralama/karşılaştırma/neden-sonuç anlatımı anlamlı animasyonla destekleniyor.
298-- [ ] Müzik konuşmayı bastırmıyor; her beat’te ses odağı belirgin ve clipping yok.
299-- [ ] Assembly yolu doğru seçildi: direct mux mümkünse video stream yeniden encode edilmedi; Montaj gerekiyorsa validate başarılı.
300:- [ ] Final MP4 probe, decode, black/freeze, loudness ve contact-sheet QA’dan geçti.
301-- [ ] Kullanıcıya kısa Türkçe rapor ve gerçek MP4 dosyası gönderiliyor.

===== /home/hermes/.hermes/plugins/shorts-production-guard/__init__.py =====
9-from pathlib import Path
10-from typing import Any, Optional
11-
12-_DB = Path.home() / ".hermes" / "state.db"
13-_LOCK = threading.Lock()
14:_VISION_SEEN: set[str] = set()
15-_CANONICAL_READS: set[tuple[str, str]] = set()
16-_CREATIVE_CHILDREN: dict[str, str] = {}
17-
18-_CREATIVE_CONTRACT_DIR = Path.home() / ".hermes" / "cache" / "shorts-production-guard" / "creative-contracts"
19-_CREATIVE_CONTRACT_SCHEMA = {
--
31-        "payoff": {"type": "string", "minLength": 1},
32-        "avoid": {
33-            "type": "array", "maxItems": 4,
34-            "items": {"type": "string", "minLength": 1},
35-        },
36:        "fallback": {"type": "string"},
37-    },
38-    "required": [
39-        "contract_version", "creative_thesis", "visual_strategy",
40-        "source_strategy", "visual_arc", "payoff",
41-    ],
--
67-_EXPLICIT_SYSTEM_WORDS = (
68-    "paket kur", "paket yükle", "paket yukle", "pip install",
69-    "apt install", "npm install", "bağımlılık kur", "bagimlilik kur",
70-    "hermes'i güncelle", "hermesi güncelle", "hermes'i değiştir",
71-    "hermesi değiştir", "plugin kur", "eklenti kur", "vlm kur",
72:    # English system-maintenance requests must not be mistaken for a normal
73:    # production job. These phrases authorize only the mutation policy below;
74-    # all ordinary safety and tool approval rules still apply.
75-    "hermes self-improvement", "hermes self improvement",
76-    "video-production system", "video production system",
77-    "improve hermes", "modify hermes", "change hermes"
78-)
--
101-    sid = str(session_id or "").strip()
102-    if not sid or not _DB.exists():
103-        return sid
104-    try:
105-        from hermes_state import SessionDB
106:        with SessionDB(_DB, read_only=True) as db:
107-            lineage = db.get_compression_lineage(sid)
108-        return str(lineage[0]) if lineage else sid
109-    except Exception:
110-        return sid
111-
--
149-def _recent_real_user_text(session_id: str) -> str:
150-    if not session_id or not _DB.exists():
151-        return ""
152-    try:
153-        from hermes_state import SessionDB
154:        with SessionDB(_DB, read_only=True) as db:
155-            lineage = db.get_compression_lineage(str(session_id))
156-        session_ids = [str(x) for x in lineage if x] or [str(session_id)]
157-
158-        placeholders = ",".join("?" for _ in session_ids)
159-        con = sqlite3.connect(str(_DB), timeout=1)
--
161-            f"""
162-            SELECT content
163-            FROM messages
164-            WHERE session_id IN ({placeholders}) AND role='user'
165-            ORDER BY id DESC
166:            LIMIT 12
167-            """,
168-            tuple(session_ids),
169-        ).fetchall()
170-        con.close()
171-    except Exception:
--
185-    low = str(text or "").lower()
186-    is_video = any(x in low for x in _VIDEO_WORDS)
187-    is_production = any(x in low for x in _PRODUCTION_WORDS)
188-    alternate_method = any(x in low for x in _ALT_METHOD_WORDS)
189-    explicit_system_change = any(x in low for x in _EXPLICIT_SYSTEM_WORDS)
190:    # Turkish/English engineering briefs must retain source, test and dependency
191-    # access. Requiring one engineering phrase plus one system-quality phrase
192-    # avoids turning an ordinary "make the video better" prompt into a bypass.
193-    engineering = any(x in low for x in (
194-        "engineering", "mühendislik", "regression harness", "case-vid",
195-        "regresyon harness", "regresyon", "kök neden", "root cause", "runtime/architecture",
196-        "system-change", "sistem değişikliği",
197-    ))
198-    system_scope = any(x in low for x in (
199-        "hermes shorts sistem", "video-production system", "video production system",
200-        "canonical pipeline", "production guard", "shorts-production-guard",
201:        "renderer architecture", "renderer mimarisi", "fallback mimarisi",
202-        "video hattı", "video hatti", "production hattı", "production hatti",
203-    ))
204-    explicit_system_change = explicit_system_change or (engineering and system_scope)
205-    return is_video and is_production, alternate_method, explicit_system_change
206-
--
224-        low = str(args.get("code") or "").lower()
225-        if any(marker in low for marker in ("importlib", "open(", "read_text", "read_file", "spec_from_file")):
226-            return target
227-    if tool_name == "terminal":
228-        low = str(args.get("command") or "").lower()
229:        # Normal preflight/proof-check/segment-build/pipeline runs are the public
230-        # interface and remain available. Introspection is rediscovery.
231-        if any(marker in low for marker in ("--help", "grep ", "rg ", "cat ", "sed ", "python -c")):
232-            return target
233-    return None
234-
235-
236:def _canonical_rediscovery_block(tool_name: str, args: dict, session_id: str) -> Optional[dict]:
237-    target = _canonical_rediscovery_target(tool_name, args)
238-    if not target:
239-        return None
240-    key = (session_id, target)
241-    # Implementation/test internals are never needed for normal production. A
242:    # documented reference may be opened once for a genuine ambiguity, but not
243-    # revisited as an agent loop. Engineering/system-change context bypasses
244-    # this function at its caller.
245-    if target in _CANONICAL_IMPLEMENTATION_NAMES or key in _CANONICAL_READS:
246:        return _block(
247:            "Shorts production guard: normal production must use the documented interface "
248-            f"instead of re-reading {target} to rediscover schema. Run the canonical "
249:            "preflight/proof-check/pipeline command. Source and tests remain readable in an "
250-            "explicit engineering/system-change task."
251-        )
252-    _CANONICAL_READS.add(key)
253-    return None
254-
255:def _block(message: str) -> dict[str, str]:
256:    return {"action": "block", "message": message}
257-
258-
259:def _runtime_proof_fingerprint(data: dict) -> str:
260:    """Mirror the canonical proof fingerprint so accepted proof can be frozen at runtime."""
261-    creative = data.get("creative_intent") or {}
262:    gate = data.get("proof_gate") or {}
263:    candidates = gate.get("candidates") or []
264-
265-    normalized = []
266:    for c in candidates:
267-        if not isinstance(c, dict):
268-            normalized.append(c)
269-            continue
270-        item = dict(c)
271-        src = item.get("src")
--
280-                }
281-            else:
282-                item["_source_identity"] = {"path": str(path), "missing": True}
283-        normalized.append(item)
284-
285:    selected_casting = []
286-    for index, shot in enumerate(data.get("shots") or []):
287-        if not isinstance(shot, dict):
288-            continue
289-        kind = str(shot.get("type") or "").lower()
290-        origin = str(shot.get("source_origin") or "").strip().lower()
--
306-            "src": shot.get("src"),
307-            "source_origin": shot.get("source_origin"),
308-            "visual_goal": shot.get("visual_goal"),
309-            "in": shot.get("in", 0),
310-            "duration": shot.get("duration"),
311:            "casting": shot.get("casting"),
312-        }
313-        src = item.get("src")
314-        if isinstance(src, str) and src.strip():
315-            path = Path(src).expanduser()
316-            if path.exists():
--
320-                    "size": st.st_size,
321-                    "mtime_ns": st.st_mtime_ns,
322-                }
323-            else:
324-                item["_source_identity"] = {"path": str(path), "missing": True}
325:        selected_casting.append(item)
326-
327-    payload = {
328-        "creative_intent": {
329-            "promise": creative.get("promise"),
330:            "primary_visual_proof": creative.get("primary_visual_proof"),
331-            "hook_visual_goal": creative.get("hook_visual_goal"),
332-            "payoff": creative.get("payoff"),
333-        },
334:        "proof_gate": {
335-            "mode": gate.get("mode"),
336:            "candidates": normalized,
337-        },
338:        "selected_casting": selected_casting,
339-    }
340-    return hashlib.sha256(
341-        json.dumps(payload, sort_keys=True, ensure_ascii=False).encode("utf-8")
342-    ).hexdigest()
343-
344-
345:def _proof_freeze_block(tool_name: str, args: dict) -> Optional[dict]:
346:    """After a valid proof PASS, prevent edits that invalidate the accepted proof."""
347-    if tool_name not in {"patch", "write_file", "execute_code"}:
348-        return None
349-
350-    if tool_name == "execute_code":
351-        code = str(args.get("code") or "")
352-        low = code.lower()
353-        if "manifest.json" in low and any(
354-            marker in low
355-            for marker in ("write_file(", "write_text(", ".write(", "json.dump(", "open(")
356-        ):
357:            return _block(
358:                "Shorts production guard: PROOF_FREEZE — do not mutate manifest.json through "
359:                "execute_code after an accepted proof. Use patch/write_file instead so the runtime "
360:                "can compare the prospective proof fingerprint and allow proof-irrelevant edits "
361:                "while blocking changes to accepted creative proof/casting."
362-            )
363-        return None
364-
365-    raw_path = str(args.get("path") or args.get("file_path") or "")
366-    if not raw_path:
--
373-        return None
374-
375-    try:
376-        current_text = path.read_text(encoding="utf-8")
377-        current = json.loads(current_text)
378:        gate = current.get("proof_gate") or {}
379-        receipt_value = gate.get("receipt")
380-        receipt = (
381-            Path(str(receipt_value)).expanduser().resolve()
382-            if receipt_value
383:            else (path.parent / "proof_gate" / "proof_receipt.json").resolve()
384-        )
385-        if not receipt.exists():
386-            return None
387-        accepted = json.loads(receipt.read_text(encoding="utf-8"))
388-        if accepted.get("pass") is not True:
389-            return None
390-
391:        current_fp = _runtime_proof_fingerprint(current)
392-        if accepted.get("fingerprint") != current_fp:
393-            # Already stale: do not create a new policy trap here.
394-            return None
395-
396-        if tool_name == "write_file":
--
410-                if args.get("replace_all") is True
411-                else current_text.replace(old, new, 1)
412-            )
413-
414-        prospective = json.loads(prospective_text)
415:        if _runtime_proof_fingerprint(prospective) != current_fp:
416:            return _block(
417:                "Shorts production guard: PROOF_FREEZE — accepted proof is locked. "
418:                "Do not change creative_intent, proof candidates, or selected footage/casting "
419:                "after Vision PASS. Continue with TTS/render/final QA using the accepted plan. "
420:                "A genuine proof failure must be handled before acceptance, not by reopening "
421:                "the proof loop after PASS."
422-            )
423-    except Exception:
424-        return None
425-    return None
426-
427-
428:def _proof_attempt_state_path(image_path: Path, pending: Optional[dict] = None) -> Path:
429:    """Use one attempt ledger per canonical proof receipt, not per proofN workdir."""
430-    if isinstance(pending, dict):
431-        receipt = pending.get("receipt")
432-        if isinstance(receipt, str) and receipt.strip():
433-            try:
434:                return Path(receipt).expanduser().resolve().parent / "proof_attempt_state.json"
435-            except Exception:
436-                pass
437:    return image_path.parent / "proof_attempt_state.json"
438-
439-
440:def _read_proof_attempt_state(image_path: Path, pending: Optional[dict] = None) -> dict:
441:    path = _proof_attempt_state_path(image_path, pending)
442-    try:
443-        if path.exists():
444-            value = json.loads(path.read_text(encoding="utf-8"))
445-            return value if isinstance(value, dict) else {}
446-    except Exception:
447-        pass
448-    return {}
449-
450-
451:def _write_proof_attempt_state(
452-    image_path: Path, state: dict, pending: Optional[dict] = None
453-) -> None:
454-    try:
455:        path = _proof_attempt_state_path(image_path, pending)
456-        path.parent.mkdir(parents=True, exist_ok=True)
457-        tmp = path.with_suffix(path.suffix + ".tmp")
458-        tmp.write_text(json.dumps(state, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
459-        tmp.replace(path)
460-    except Exception:
461-        pass
462-
463-
464-def _direct_render_bypass(tool_name: str, args: dict) -> bool:
465:    """Block manual TTS/final assembly during normal Shorts production.
466-
467:    Asset inspection, proof generation and deterministic QA remain available.
468:    Final production must go through the canonical shorts_fast_pipeline.
469-    """
470-    if tool_name == "terminal":
471-        cmd = str(args.get("command") or "")
472-        low = cmd.lower()
473-        if "shorts_fast_pipeline.py" in low:
474-            return False
475-        if "google-tts" in low or "voice_raw_google" in low:
476-            return True
477-        if "ffmpeg" in low:
478:            for marker in ("final.mp4", "visual_master.mp4"):
479-                if marker not in low:
480-                    continue
481-                inputs = re.findall(r"-i\\s+['\"]?([^\\s'\";|]+)", low)
482-                input_hits = sum(x.endswith(marker) for x in inputs)
483-                if low.count(marker) > input_hits:
--
496-            and any(x in low for x in ("subprocess", "terminal(", "os.system", "run("))
497-        ):
498-            return True
499-        if (
500-            "ffmpeg" in low
501:            and any(x in low for x in ("final.mp4", "visual_master.mp4"))
502-            and any(x in low for x in ("subprocess", "terminal(", "os.system", "run("))
503-        ):
504-            return True
505-    return False
506-
--
533-        except Exception:
534-            continue
535-    return None
536-
537-
538:def _normalized_proof_token(value: Any) -> str:
539:    """Accept the harmless exact-prefix variation repeatedly emitted by Vision."""
540-    token = str(value or "").strip()
541:    prefix = "HERMES_PROOF_GATE::"
542-    return token[len(prefix):] if token.startswith(prefix) else token
543-
544-
545:def _proof_verdict_accepted(verdict: Any, token: str) -> bool:
546:    """Accept proof only when every selected footage role is unambiguous."""
547-    return bool(
548-        isinstance(verdict, dict)
549-        and verdict.get("verdict") == "PASS"
550:        and verdict.get("verifier") == "vision_analyze"
551:        and verdict.get("primary_visual_proof") is True
552-        and verdict.get("hook_visual_goal") is True
553:        and verdict.get("proof_visible_and_unmistakable") is True
554:        and verdict.get("casting_subjects_clear") is True
555:        and _normalized_proof_token(verdict.get("proof_token")) == token
556-    )
557-
558-
559-def _on_pre_tool_call(
560-    tool_name: str = "",
--
567-    if not active:
568-        return None
569-
570-    args = args if isinstance(args, dict) else {}
571-
572:    # Normal Shorts may delegate only the bounded Astra creative-director plan/re-plan.
573:    # All other production delegation remains blocked.
574-    if tool_name == "delegate_task" and not alternate_method and not explicit_system_change:
575-        tasks = args.get("tasks") if isinstance(args.get("tasks"), list) else []
576-        goals = [
577-            str(t.get("goal") or "")
578-            for t in tasks
--
581-        astra_planner = (
582-            len(goals) == 1
583-            and goals[0].startswith("HERMES_CREATIVE_DIRECTOR")
584-        )
585-        if not astra_planner:
586:            return _block(
587-                "Shorts production guard: normal production delegation is restricted to the "
588-                "bounded Astra creative director. Use one delegate_task whose goal starts with "
589:                "HERMES_CREATIVE_DIRECTOR; all other delegation remains blocked."
590-            )
591-        typed_tasks = [dict(tasks[0])]
592-        typed_tasks[0]["goal"] = (
593-            str(typed_tasks[0].get("goal") or "")
594-            + "\nKeep the creative contract compact and concise. Use short phrases/sentences; "
595:              "include only information needed by the production executor."
596-        )
597-        typed_tasks[0]["output_schema"] = _CREATIVE_CONTRACT_SCHEMA
598-        return {"action": "modify", "args": {"tasks": typed_tasks}}
599-
600-    if not alternate_method and not explicit_system_change:
601:        rediscovery_block = _canonical_rediscovery_block(tool_name, args, session_id)
602:        if rediscovery_block:
603:            return rediscovery_block
604-
605:        freeze_block = _proof_freeze_block(tool_name, args)
606:        if freeze_block:
607:            return freeze_block
608-
609-    if not alternate_method and not explicit_system_change and _direct_render_bypass(tool_name, args):
610:        return _block(
611:            "Shorts production guard: direct TTS/final assembly outside the canonical pipeline "
612:            "is blocked during normal production. Use shorts_fast_pipeline.py. If proof Vision "
613:            "cannot issue a valid receipt, stop with QA_UNAVAILABLE; do not build a fallback final."
614-        )
615-
616-    # Explicitly requested alternate/code-driven workflows remain available.
617-    if tool_name == "skill_view" and not alternate_method:
618-        name = str(args.get("name") or "")
619-        if name and name != "shorts-core":
620:            return _block(
621-                "Shorts production guard: shorts-core is the sole production authority "
622-                "for this footage-based Shorts/Reels turn. Do not load another production, "
623-                "debug, installation, or QA skill. Continue with shorts-core and the canonical pipeline."
624-            )
625-
626:    # Vision policy: do not impose arbitrary call counts.
627-    # The agent may re-evaluate when the evidence or question genuinely changes.
628:    # Only exact no-progress repetition of the same visual evidence + question is blocked.
629:    if tool_name in {"browser_vision", "computer_use"} and not alternate_method:
630:        return _block(
631:            "Shorts production guard: this QA fallback is disabled during normal production. "
632:            "Use vision_analyze as the primary visual QA path; if Vision is unavailable after "
633:            "the permitted retry, continue with controlled QA_UNAVAILABLE rather than inventing a fallback."
634-        )
635-
636:    if tool_name == "vision_analyze":
637-        image_url = str(args.get("image_url") or "")
638-        question = str(args.get("question") or "").strip()
639-        raw = image_url[7:] if image_url.startswith("file://") else image_url
640-        image_path = Path(raw).expanduser()
641-
642:        # ASSET-001 diagnostic:
643:        # Pre-casting source/range/asset Vision inspection is intentionally OPEN.
644:        # No artificial Vision-call cap and no mandatory batching in this diagnostic.
645:        # Use visual inspection only when it helps choose footage that actually fulfills
646:        # the creative contract. Existing duplicate/no-progress and other guards remain active.
647-
648:        if image_path.name == "proof_contact_sheet.jpg":
649:            pending_path = image_path.parent / "proof_pending.json"
650-            current_fingerprint = None
651-            pending = {}
652-            try:
653-                if pending_path.exists():
654-                    pending = json.loads(pending_path.read_text(encoding="utf-8"))
655-                    current_fingerprint = pending.get("fingerprint")
656-            except Exception:
657-                pending = {}
658:            proof_state = _read_proof_attempt_state(image_path, pending)
659-
660-            if (
661:                int(proof_state.get("failed_attempts") or 0) >= 2
662-                and current_fingerprint
663:                and proof_state.get("last_fingerprint") == current_fingerprint
664-            ):
665:                return _block(
666:                    "Shorts production guard: PROOF_STRATEGY_EXHAUSTED — this proof strategy "
667:                    "already failed initial review plus one repair. Do not retry Vision on the "
668:                    "same accepted candidates/casting. Materially change the proof footage or "
669:                    "casting, rerun proof-check, and continue the production autonomously."
670-                )
671-
672:        # Content identity matters more than filename: a repaired final_contact_sheet
673:        # at the same path is new evidence and must be allowed to be judged again.
674-        try:
675-            image_identity = _sha_file(image_path.resolve())
676-        except Exception:
677-            image_identity = str(image_path)
678-
--
687-            sort_keys=True,
688-        )
689-        signature = hashlib.sha256(sig_raw.encode("utf-8")).hexdigest()
690-
691-        with _LOCK:
692:            if signature in _VISION_SEEN:
693:                return _block(
694-                    "Shorts production guard: this exact visual evidence has already been "
695:                    "asked the same question. Reuse the existing result. If evidence was repaired "
696-                    "or the verification question genuinely needs correction, change the evidence "
697-                    "or question rather than repeating the same call."
698-                )
699-        return None
700-
701-    # During a production turn the agent may USE the installed toolchain, but may not redesign it.
702-    if not explicit_system_change:
703-        if tool_name == "skill_manage":
704:            return _block(
705-                "Shorts production guard: do not modify skills during a production job. "
706-                "Use the current canonical pipeline and finish the artifact."
707-            )
708-
709-        if tool_name in {"write_file", "patch"}:
710-            path = str(
711-                args.get("path")
712-                or args.get("file_path")
713-                or ""
714-            )
715:            if path.endswith("proof_receipt.json") or path.endswith("proof_pending.json"):
716:                return _block(
717:                    "Shorts production guard: proof state is runtime-owned. "
718:                    "Do not write or patch proof receipts/pending attestations manually; "
719:                    "only a real vision_analyze PASS may issue the receipt."
720-                )
721-            if any(path.startswith(prefix) for prefix in _PROTECTED_PREFIXES):
722:                return _block(
723-                    "Shorts production guard: production-time mutation of Hermes core/tools/skills "
724:                    "is blocked. Use the installed production stack as-is and finish the video."
725-                )
726-
727-        if tool_name == "terminal":
728-            cmd = str(args.get("command") or "")
729-            low = cmd.lower()
730:            if "proof_receipt.json" in low or "proof_pending.json" in low:
731:                return _block(
732:                    "Shorts production guard: proof state is runtime-owned. "
733:                    "Do not create, edit, copy, delete, or inspect proof receipt/pending files "
734-                    "through terminal commands during production."
735-                )
736-            if (
737-                _INSTALL_RE.search(cmd)
738-                or "local-vlm" in low
739-                or ("transformers" in low and "install" in low)
740-                or ("torch" in low and "install" in low)
741-            ):
742:                return _block(
743-                    "Shorts production guard: installing packages/models or creating a local VLM "
744:                    "during video production is blocked. Do not build a new QA/tool stack; continue "
745-                    "with the canonical installed pipeline."
746-                )
747-            if _PROTECTED_MUTATION_RE.search(cmd):
748:                return _block(
749-                    "Shorts production guard: production-time mutation of the Hermes runtime/toolchain "
750:                    "is blocked. Use existing tools without modifying them."
751-                )
752-
753-    return None
754-
755:def _expanded_vision_question_for_receipt(image_path: Path, question: str) -> str:
756:    """Resolve only canonical sibling QA prompts for receipt token checks."""
757-    prefix = "@file:"
758-    text = str(question or "")
759-    if not text.startswith(prefix):
760-        return text
761-    prompt = Path(text[len(prefix):].strip()).expanduser()
--
763-        return text
764-    try:
765-        prompt = prompt.resolve()
766-        if prompt.parent != image_path.parent:
767-            return text
768:        if not prompt.name.endswith("_vision_question.txt"):
769-            return text
770-        if not prompt.is_file() or prompt.stat().st_size > 128 * 1024:
771-            return text
772-        expanded = prompt.read_text(encoding="utf-8").strip()
773-        return expanded or text
774-    except Exception:
775-        return text
776-
777-
778:def _update_repair_state_from_vision(
779-    image_path: Path,
780-    question: str,
781-    result: Any,
782-    session_id: str,
783-    turn_id: str,
784-    tool_call_id: str,
785-) -> None:
786:    if image_path.name != "final_contact_sheet.jpg":
787-        return
788:    state_path = image_path.parent / "repair_state.json"
789-    if not state_path.exists():
790-        return
791-    try:
792-        state = json.loads(state_path.read_text(encoding="utf-8"))
793-    except Exception:
794-        return
795:    if state.get("state") not in {"repair_attempted", "verification_pending"}:
796-        return
797:    token = str(state.get("repair_token") or "")
798:    if not token or f"HERMES_REPAIR_VERIFY::{token}" not in question:
799-        return
800-    try:
801-        if _sha_file(image_path) != state.get("contact_sheet_sha256"):
802-            return
803-        artifact = Path(str(state.get("artifact") or "")).expanduser().resolve()
--
812-        state["verification_pending"] = False
813-        state["claim_fixed_allowed"] = False
814-    else:
815-        accepted = (
816-            verdict.get("verdict") == "PASS"
817:            and verdict.get("verifier") == "vision_analyze"
818:            and verdict.get("post_repair_semantic_verified") is True
819:            and _normalized_proof_token(verdict.get("repair_token")) == token
820-        )
821-        failed = verdict.get("verdict") == "FAIL"
822-        state["state"] = "verified" if accepted else ("failed" if failed else "unverified")
823-        state["verification_pending"] = False
824-        state["claim_fixed_allowed"] = accepted
825-        state["verification"] = {
826:            "verifier": "vision_analyze",
827-            "verdict": verdict.get("verdict"),
828-            "artifact_sha256": state.get("artifact_sha256"),
829-            "contact_sheet_sha256": state.get("contact_sheet_sha256"),
830-            "session_id": session_id,
831-            "turn_id": turn_id,
832-            "tool_call_id": tool_call_id,
833-            "accepted_at": time.time(),
834-            "raw": verdict,
835:            "vision_route": outer.get("vision_route") if isinstance(outer.get("vision_route"), dict) else None,
836-        }
837-    try:
838-        tmp = state_path.with_suffix(state_path.suffix + ".tmp")
839-        tmp.write_text(json.dumps(state, ensure_ascii=False, indent=2) + "\n", enc