@leejungkiin/awkit 1.7.1 → 1.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/bin/awk.js +576 -84
  2. package/core/CLAUDE.md +1 -1
  3. package/core/GEMINI.md +148 -167
  4. package/core/GEMINI.md.bak +149 -116
  5. package/core/skill-runtime-manifest.json +3 -0
  6. package/docs/Claude Fable 5.md +3826 -0
  7. package/docs/android_kotlin_system_instruction.md +210 -0
  8. package/docs/brainstorm_ponytail_integration.md +146 -0
  9. package/docs/brainstorm_smart_setup.md +113 -0
  10. package/docs/deep-research-report (1).md +293 -0
  11. package/docs/history/GEMINI.v1.md +135 -0
  12. package/docs/history/brainstorm_antigravity_unified_architecture.v1.md +105 -0
  13. package/docs/history/implementation_plan.v1.md +58 -0
  14. package/package.json +4 -1
  15. package/scripts/artifact-storage.js +130 -0
  16. package/scripts/automation-gate.js +35 -2
  17. package/scripts/claude-plan.js +76 -0
  18. package/scripts/dependency-manager.js +210 -0
  19. package/scripts/exec-rtk.js +11 -5
  20. package/scripts/i18n-helper.js +381 -0
  21. package/scripts/multi-model-pipeline.js +144 -0
  22. package/skill-packs/mobile-ios/pack.json +4 -2
  23. package/skill-packs/reverse-engineering/pack.json +1 -0
  24. package/skills/CATALOG.md +20 -0
  25. package/skills/GEMINI.md +9 -1
  26. package/skills/TRIGGER_INDEX.md +10 -0
  27. package/skills/ai-music/SKILL.md +275 -0
  28. package/skills/android-re-analyzer/SKILL.md +238 -0
  29. package/skills/android-re-analyzer/references/api-extraction-patterns.md +119 -0
  30. package/skills/android-re-analyzer/references/call-flow-analysis.md +176 -0
  31. package/skills/android-re-analyzer/references/fernflower-usage.md +115 -0
  32. package/skills/android-re-analyzer/references/jadx-usage.md +116 -0
  33. package/skills/android-re-analyzer/references/setup-guide.md +221 -0
  34. package/skills/android-re-analyzer/scripts/check-deps.sh +129 -0
  35. package/skills/android-re-analyzer/scripts/decompile.sh +375 -0
  36. package/skills/android-re-analyzer/scripts/find-api-calls.sh +118 -0
  37. package/skills/android-re-analyzer/scripts/install-dep.sh +448 -0
  38. package/skills/animal-island-ui-style/SKILL.md +1450 -0
  39. package/skills/app-store-review-agent/SKILL.md +164 -0
  40. package/skills/app-store-review-agent/references/guidelines/README.md +154 -0
  41. package/skills/app-store-review-agent/references/guidelines/by-app-type/ai_apps.md +37 -0
  42. package/skills/app-store-review-agent/references/guidelines/by-app-type/all_apps.md +50 -0
  43. package/skills/app-store-review-agent/references/guidelines/by-app-type/crypto_finance.md +31 -0
  44. package/skills/app-store-review-agent/references/guidelines/by-app-type/games.md +31 -0
  45. package/skills/app-store-review-agent/references/guidelines/by-app-type/health_fitness.md +31 -0
  46. package/skills/app-store-review-agent/references/guidelines/by-app-type/kids.md +27 -0
  47. package/skills/app-store-review-agent/references/guidelines/by-app-type/macos.md +38 -0
  48. package/skills/app-store-review-agent/references/guidelines/by-app-type/social_ugc.md +32 -0
  49. package/skills/app-store-review-agent/references/guidelines/by-app-type/subscription_iap.md +34 -0
  50. package/skills/app-store-review-agent/references/guidelines/by-app-type/vpn.md +18 -0
  51. package/skills/app-store-review-agent/references/rules/design/minimum_functionality.md +96 -0
  52. package/skills/app-store-review-agent/references/rules/design/sign_in_with_apple.md +54 -0
  53. package/skills/app-store-review-agent/references/rules/entitlements/unused_entitlements.md +83 -0
  54. package/skills/app-store-review-agent/references/rules/metadata/accurate_metadata.md +54 -0
  55. package/skills/app-store-review-agent/references/rules/metadata/apple_trademark.md +99 -0
  56. package/skills/app-store-review-agent/references/rules/metadata/china_storefront.md +72 -0
  57. package/skills/app-store-review-agent/references/rules/metadata/competitor_terms.md +56 -0
  58. package/skills/app-store-review-agent/references/rules/metadata/subscription_metadata.md +81 -0
  59. package/skills/app-store-review-agent/references/rules/privacy/privacy_manifest.md +84 -0
  60. package/skills/app-store-review-agent/references/rules/privacy/unnecessary_data.md +60 -0
  61. package/skills/app-store-review-agent/references/rules/subscription/misleading_pricing.md +63 -0
  62. package/skills/app-store-review-agent/references/rules/subscription/missing_tos_pp.md +54 -0
  63. package/skills/awf-ponytail/SKILL.md +91 -0
  64. package/skills/awf-ponytail-review/SKILL.md +67 -0
  65. package/skills/awf-session-restore/SKILL.md +3 -3
  66. package/skills/brainstorm-agent/SKILL.md +11 -2
  67. package/skills/brainstorm-agent/templates/brief-template.md +8 -0
  68. package/skills/claude-planner/SKILL.md +47 -0
  69. package/skills/code-review/SKILL.md +87 -0
  70. package/skills/expo-game-development/SKILL.md +163 -0
  71. package/skills/flutter/LICENSE.txt +202 -0
  72. package/skills/flutter/SKILL.md +127 -0
  73. package/skills/flutter-project-creater/LICENSE.txt +202 -0
  74. package/skills/flutter-project-creater/SKILL.md +106 -0
  75. package/skills/game-developer/SKILL.md +163 -0
  76. package/skills/game-developer/references/ecs-patterns.md +501 -0
  77. package/skills/game-developer/references/multiplayer-networking.md +475 -0
  78. package/skills/game-developer/references/performance-optimization.md +422 -0
  79. package/skills/game-developer/references/unity-patterns.md +271 -0
  80. package/skills/game-developer/references/unreal-cpp.md +352 -0
  81. package/skills/generate-gui-assets/SKILL.md +305 -0
  82. package/skills/generate-gui-assets/agents/openai.yaml +4 -0
  83. package/skills/generate-gui-assets/references/catalog-schema.md +58 -0
  84. package/skills/generate-gui-assets/references/extraction-techniques.md +21 -0
  85. package/skills/generate-gui-assets/references/prompt-patterns.md +58 -0
  86. package/skills/generate-gui-assets/scripts/__pycache__/clean_chroma_edges.cpython-311.pyc +0 -0
  87. package/skills/generate-gui-assets/scripts/build_gui_contact_sheet.py +51 -0
  88. package/skills/generate-gui-assets/scripts/clean_chroma_edges.py +262 -0
  89. package/skills/generate-gui-assets/scripts/copy_approved_icons.py +64 -0
  90. package/skills/generate-gui-assets/scripts/prepare_gui_asset_run.py +91 -0
  91. package/skills/generate-gui-assets/scripts/suggest_grid_options.py +63 -0
  92. package/skills/generate-gui-assets/scripts/validate_gui_catalog.py +50 -0
  93. package/skills/godot-game-development/SKILL.md +142 -0
  94. package/skills/hatch-pet/LICENSE.txt +201 -0
  95. package/skills/hatch-pet/SKILL.md +420 -0
  96. package/skills/hatch-pet/agents/openai.yaml +4 -0
  97. package/skills/hatch-pet/references/animation-rows.md +29 -0
  98. package/skills/hatch-pet/references/codex-pet-contract.md +35 -0
  99. package/skills/hatch-pet/references/qa-rubric.md +60 -0
  100. package/skills/hatch-pet/scripts/__pycache__/clean_chroma_edges.cpython-311.pyc +0 -0
  101. package/skills/hatch-pet/scripts/clean_chroma_edges.py +262 -0
  102. package/skills/hatch-pet/scripts/compose_atlas.py +150 -0
  103. package/skills/hatch-pet/scripts/derive_running_left_from_running_right.py +143 -0
  104. package/skills/hatch-pet/scripts/extract_strip_frames.py +323 -0
  105. package/skills/hatch-pet/scripts/finalize_pet_run.py +382 -0
  106. package/skills/hatch-pet/scripts/generate_pet_images.py +287 -0
  107. package/skills/hatch-pet/scripts/inspect_frames.py +246 -0
  108. package/skills/hatch-pet/scripts/make_contact_sheet.py +96 -0
  109. package/skills/hatch-pet/scripts/package_custom_pet.py +108 -0
  110. package/skills/hatch-pet/scripts/pet_job_status.py +117 -0
  111. package/skills/hatch-pet/scripts/prepare_pet_run.py +673 -0
  112. package/skills/hatch-pet/scripts/queue_pet_repairs.py +172 -0
  113. package/skills/hatch-pet/scripts/record_imagegen_result.py +250 -0
  114. package/skills/hatch-pet/scripts/render_animation_videos.py +134 -0
  115. package/skills/hatch-pet/scripts/render_animation_videos.sh +5 -0
  116. package/skills/hatch-pet/scripts/validate_atlas.py +139 -0
  117. package/skills/i18n-orchestrator/SKILL.md +37 -0
  118. package/skills/ios-simulator-skill/SKILL.md +390 -0
  119. package/skills/ios-simulator-skill/scripts/accessibility_audit.py +300 -0
  120. package/skills/ios-simulator-skill/scripts/app_launcher.py +326 -0
  121. package/skills/ios-simulator-skill/scripts/app_state_capture.py +400 -0
  122. package/skills/ios-simulator-skill/scripts/appearance.py +385 -0
  123. package/skills/ios-simulator-skill/scripts/build_and_test.py +348 -0
  124. package/skills/ios-simulator-skill/scripts/clipboard.py +103 -0
  125. package/skills/ios-simulator-skill/scripts/common/__init__.py +61 -0
  126. package/skills/ios-simulator-skill/scripts/common/cache_utils.py +289 -0
  127. package/skills/ios-simulator-skill/scripts/common/device_utils.py +462 -0
  128. package/skills/ios-simulator-skill/scripts/common/env_config.py +35 -0
  129. package/skills/ios-simulator-skill/scripts/common/hang_pipeline.py +862 -0
  130. package/skills/ios-simulator-skill/scripts/common/hang_sessions.py +490 -0
  131. package/skills/ios-simulator-skill/scripts/common/idb_utils.py +180 -0
  132. package/skills/ios-simulator-skill/scripts/common/screenshot_utils.py +338 -0
  133. package/skills/ios-simulator-skill/scripts/container.py +668 -0
  134. package/skills/ios-simulator-skill/scripts/gesture.py +394 -0
  135. package/skills/ios-simulator-skill/scripts/hang_watcher.py +1533 -0
  136. package/skills/ios-simulator-skill/scripts/keyboard.py +391 -0
  137. package/skills/ios-simulator-skill/scripts/localization_audit.py +483 -0
  138. package/skills/ios-simulator-skill/scripts/location.py +467 -0
  139. package/skills/ios-simulator-skill/scripts/log_monitor.py +493 -0
  140. package/skills/ios-simulator-skill/scripts/model_inspector.py +645 -0
  141. package/skills/ios-simulator-skill/scripts/navigator.py +461 -0
  142. package/skills/ios-simulator-skill/scripts/privacy_manager.py +310 -0
  143. package/skills/ios-simulator-skill/scripts/push_notification.py +240 -0
  144. package/skills/ios-simulator-skill/scripts/screen_mapper.py +296 -0
  145. package/skills/ios-simulator-skill/scripts/sim_health_check.sh +245 -0
  146. package/skills/ios-simulator-skill/scripts/sim_list.py +299 -0
  147. package/skills/ios-simulator-skill/scripts/simctl_boot.py +312 -0
  148. package/skills/ios-simulator-skill/scripts/simctl_create.py +316 -0
  149. package/skills/ios-simulator-skill/scripts/simctl_delete.py +357 -0
  150. package/skills/ios-simulator-skill/scripts/simctl_erase.py +351 -0
  151. package/skills/ios-simulator-skill/scripts/simctl_shutdown.py +290 -0
  152. package/skills/ios-simulator-skill/scripts/simulator_selector.py +375 -0
  153. package/skills/ios-simulator-skill/scripts/status_bar.py +250 -0
  154. package/skills/ios-simulator-skill/scripts/test_recorder.py +323 -0
  155. package/skills/ios-simulator-skill/scripts/visual_diff.py +235 -0
  156. package/skills/ios-simulator-skill/scripts/xcode/__init__.py +13 -0
  157. package/skills/ios-simulator-skill/scripts/xcode/builder.py +397 -0
  158. package/skills/ios-simulator-skill/scripts/xcode/cache.py +204 -0
  159. package/skills/ios-simulator-skill/scripts/xcode/config.py +178 -0
  160. package/skills/ios-simulator-skill/scripts/xcode/reporter.py +343 -0
  161. package/skills/ios-simulator-skill/scripts/xcode/xcresult.py +451 -0
  162. package/skills/ios-visual-qa-strategist/SKILL.md +111 -0
  163. package/skills/ios-visual-qa-strategist/agents/openai.yaml +4 -0
  164. package/skills/ios-visual-qa-strategist/references/ios-tool-selection.md +61 -0
  165. package/skills/ios-visual-qa-strategist/references/minimal-capture-policy.md +56 -0
  166. package/skills/ios-visual-qa-strategist/references/visual-reasoning-heuristics.md +53 -0
  167. package/skills/orchestrator/SKILL.md +0 -20
  168. package/skills/persistent-storage/SKILL.md +55 -0
  169. package/skills/short-maker/SKILL.md +23 -0
  170. package/skills/short-maker/scripts/effects.js +56 -0
  171. package/skills/short-maker/scripts/shortmaker-bridge.js +332 -0
  172. package/skills/short-maker/scripts/videomix.js +601 -0
  173. package/skills/short-maker/templates/hyperframes/cinematic-character.template.html +172 -0
  174. package/skills/short-maker/templates/hyperframes/index.template.html +194 -0
  175. package/skills/smali-to-kotlin/SKILL.md +128 -0
  176. package/skills/smali-to-kotlin/examples/getting-started/tech-stack.md +58 -0
  177. package/skills/smali-to-kotlin/examples/pipeline/data-ui-parity.md +118 -0
  178. package/skills/smali-to-kotlin/examples/pipeline/scanner-and-bootstrap.md +106 -0
  179. package/skills/smali-to-kotlin/library-patterns.md +189 -0
  180. package/skills/smali-to-kotlin/phase-0-discovery.md +128 -0
  181. package/skills/smali-to-kotlin/phase-1-architecture.md +166 -0
  182. package/skills/smali-to-kotlin/phase-2-blueprint-ui.md +347 -0
  183. package/skills/smali-to-kotlin/phase-2-blueprint.md +228 -0
  184. package/skills/smali-to-kotlin/phase-3-build.md +248 -0
  185. package/skills/smali-to-kotlin/phase-3-logic-build.md +268 -0
  186. package/skills/smali-to-kotlin/smali-reading-guide.md +310 -0
  187. package/skills/smali-to-kotlin/templates/app-map.md +101 -0
  188. package/skills/smali-to-kotlin/templates/architecture.md +142 -0
  189. package/skills/smali-to-kotlin/templates/blueprint.md +145 -0
  190. package/skills/spec-gate/SKILL.md +6 -2
  191. package/skills/symphony-enforcer/SKILL.md +8 -0
  192. package/skills/symphony-enforcer/examples/mindful-stop.md +2 -0
  193. package/skills/symphony-enforcer/examples/three-phase.md +16 -0
  194. package/skills/symphony-enforcer/examples/trigger-points.md +7 -1
  195. package/skills/unity-game-development/SKILL.md +231 -0
  196. package/skills/video-edit/SKILL.md +36 -0
  197. package/skills/video-edit/scripts/video_edit.py +324 -0
  198. package/templates/project-identity/android.json +2 -2
  199. package/templates/project-identity/backend-nestjs.json +2 -2
  200. package/templates/project-identity/expo.json +2 -2
  201. package/templates/project-identity/ios.json +2 -2
  202. package/templates/project-identity/web-nextjs.json +2 -2
  203. package/templates/setup-mapping.json +48 -0
  204. package/templates/specs/design-template.md +161 -71
  205. package/templates/specs/requirements-template.md +65 -133
  206. package/templates/specs/task-spec-template.xml +3 -0
  207. package/workflows/_uncategorized/critic.md +40 -0
  208. package/workflows/_uncategorized/git-rebase-flow.md +81 -0
  209. package/workflows/_uncategorized/image-gen.md +118 -0
  210. package/workflows/_uncategorized/multi-model-pipeline.md +60 -0
  211. package/workflows/_uncategorized/pixel-gen.md +86 -0
  212. package/workflows/_uncategorized/pixel-setup.md +90 -0
  213. package/workflows/_uncategorized/ponytail-review.md +59 -0
  214. package/workflows/_uncategorized/reverse-android-build.md +222 -0
  215. package/workflows/_uncategorized/reverse-android-design.md +139 -0
  216. package/workflows/_uncategorized/reverse-android-discover.md +150 -0
  217. package/workflows/_uncategorized/reverse-android-scan.md +158 -0
  218. package/workflows/_uncategorized/reverse-android.md +143 -0
  219. package/workflows/_uncategorized/reverse-ios-build.md +240 -0
  220. package/workflows/_uncategorized/reverse-ios-design.md +112 -0
  221. package/workflows/_uncategorized/reverse-ios-discover.md +120 -0
  222. package/workflows/_uncategorized/reverse-ios-scan.md +155 -0
  223. package/workflows/_uncategorized/reverse-ios.md +152 -0
  224. package/workflows/_uncategorized/safety-router.md +34 -0
  225. package/workflows/_uncategorized/teach.md +89 -0
  226. package/workflows/_uncategorized/verify-ui.md +53 -0
  227. package/workflows/_uncategorized/visualize-screenshots.md +34 -0
  228. package/workflows/ads/ads-analyst.md +201 -0
  229. package/workflows/ads/ads-audit.md +106 -0
  230. package/workflows/ads/ads-optimize.md +97 -0
  231. package/workflows/ads/ads-targeting.md +241 -0
  232. package/workflows/ads/adsExpert.md +160 -0
  233. package/workflows/ads/smali-ads-config.md +400 -0
  234. package/workflows/ads/smali-ads-flow.md +331 -0
  235. package/workflows/ads/smali-ads-interstitial.md +377 -0
  236. package/workflows/ads/smali-ads-native.md +382 -0
  237. package/workflows/context/teach.md +89 -0
  238. package/workflows/gitnexus.md +8 -8
  239. package/workflows/lifecycle/brainstorm.md +43 -0
  240. package/workflows/lifecycle/code.md +5 -0
  241. package/workflows/lifecycle/init.md +23 -5
  242. package/workflows/lifecycle/multi-model-pipeline.md +60 -0
  243. package/workflows/quality/ponytail-review.md +59 -0
  244. package/workflows/roles/critic.md +40 -0
  245. package/workflows/roles/safety-router.md +34 -0
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env python3
2
+ """Reopen failed Codex pet row jobs after frame QA."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import shutil
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+
13
+ def load_json(path: Path) -> dict[str, object]:
14
+ if not path.exists():
15
+ raise SystemExit(f"file not found: {path}")
16
+ return json.loads(path.read_text(encoding="utf-8"))
17
+
18
+
19
+ def rows_to_repair(
20
+ review: dict[str, object], *, repair_on_warnings: bool
21
+ ) -> list[dict[str, object]]:
22
+ rows = review.get("rows")
23
+ if not isinstance(rows, list):
24
+ raise SystemExit("review does not contain row-level results")
25
+
26
+ repairs: list[dict[str, object]] = []
27
+ for row in rows:
28
+ if not isinstance(row, dict) or not isinstance(row.get("state"), str):
29
+ continue
30
+ errors = row.get("errors") if isinstance(row.get("errors"), list) else []
31
+ warnings = row.get("warnings") if isinstance(row.get("warnings"), list) else []
32
+ if errors or (repair_on_warnings and warnings):
33
+ repairs.append(
34
+ {
35
+ "state": row["state"],
36
+ "reason": "; ".join(str(item) for item in [*errors, *warnings])
37
+ or "the row did not pass visual QA",
38
+ }
39
+ )
40
+ return repairs
41
+
42
+
43
+ def append_repair_note(run_dir: Path, state: str, attempt: int, reason: str) -> None:
44
+ prompt_path = run_dir / "prompts" / "rows" / f"{state}.md"
45
+ if not prompt_path.exists():
46
+ raise SystemExit(f"row prompt not found: {prompt_path}")
47
+ existing = prompt_path.read_text(encoding="utf-8")
48
+ note = f"""
49
+
50
+ Repair attempt {attempt}:
51
+ - The previous `{state}` strip failed QA: {reason}
52
+ - Regenerate the entire row, not just one pose.
53
+ - Fill every requested frame slot with one complete centered full-body pet pose.
54
+ - Keep large gaps of pure chroma key only between slots; do not leave a requested slot empty.
55
+ - Avoid pose overlap, clipping, edge slivers, extra partial sprites, and detached fragments from neighboring poses.
56
+ - Use the canonical base image and any original references listed in `imagegen-jobs.json` as grounding inputs.
57
+ - Do not redesign the pet. Keep the exact same head shape, face design, markings, body proportions, palette, outline weight, materials, and props as the approved base pet.
58
+ - If the contact sheet shows identity drift, repair only this row while preserving the canonical base identity.
59
+ """
60
+ prompt_path.write_text(existing.rstrip() + note.rstrip() + "\n", encoding="utf-8")
61
+
62
+
63
+ def job_list(manifest: dict[str, object]) -> list[dict[str, object]]:
64
+ jobs = manifest.get("jobs")
65
+ if not isinstance(jobs, list):
66
+ raise SystemExit("invalid imagegen-jobs.json: jobs must be a list")
67
+ return [job for job in jobs if isinstance(job, dict)]
68
+
69
+
70
+ def next_archive_path(archive_dir: Path, state: str, attempt: int, suffix: str) -> Path:
71
+ candidate = archive_dir / f"{state}-attempt-{attempt}-previous{suffix}"
72
+ if not candidate.exists():
73
+ return candidate
74
+ counter = 2
75
+ while True:
76
+ candidate = archive_dir / f"{state}-attempt-{attempt}-previous-{counter}{suffix}"
77
+ if not candidate.exists():
78
+ return candidate
79
+ counter += 1
80
+
81
+
82
+ def archive_decoded_output(run_dir: Path, job: dict[str, object], state: str, attempt: int) -> str | None:
83
+ output_raw = job.get("output_path")
84
+ output = (
85
+ run_dir / output_raw
86
+ if isinstance(output_raw, str) and output_raw
87
+ else run_dir / "decoded" / f"{state}.png"
88
+ )
89
+ if not output.exists():
90
+ return None
91
+ archive_dir = run_dir / "decoded" / "repair-archive"
92
+ archive_dir.mkdir(parents=True, exist_ok=True)
93
+ archived = next_archive_path(archive_dir, state, attempt, output.suffix or ".png")
94
+ shutil.move(str(output), archived)
95
+ return str(archived.relative_to(run_dir))
96
+
97
+
98
+ def queue_repair(manifest: dict[str, object], run_dir: Path, state: str, reason: str) -> dict[str, object]:
99
+ for job in job_list(manifest):
100
+ if job.get("id") != state:
101
+ continue
102
+ attempt = int(job.get("repair_attempt", 0)) + 1
103
+ archived_output = archive_decoded_output(run_dir, job, state, attempt)
104
+ job["status"] = "pending"
105
+ job["repair_attempt"] = attempt
106
+ job["repair_reason"] = reason
107
+ job["queued_at"] = datetime.now(timezone.utc).isoformat()
108
+ if archived_output is not None:
109
+ previous_outputs = job.setdefault("previous_outputs", [])
110
+ if not isinstance(previous_outputs, list):
111
+ previous_outputs = []
112
+ job["previous_outputs"] = previous_outputs
113
+ previous_outputs.append(
114
+ {
115
+ "attempt": attempt,
116
+ "path": archived_output,
117
+ "archived_at": job["queued_at"],
118
+ }
119
+ )
120
+ for key in [
121
+ "source_path",
122
+ "source_provenance",
123
+ "source_sha256",
124
+ "output_sha256",
125
+ "completed_at",
126
+ "metadata",
127
+ "synthetic_test_source",
128
+ "secondary_fallback",
129
+ "derived_from",
130
+ "mirror_decision",
131
+ ]:
132
+ job.pop(key, None)
133
+ result: dict[str, object] = {"attempt": attempt}
134
+ if archived_output is not None:
135
+ result["archived_output"] = archived_output
136
+ return result
137
+ raise SystemExit(f"unknown row job id: {state}")
138
+
139
+
140
+ def main() -> None:
141
+ parser = argparse.ArgumentParser(description=__doc__)
142
+ parser.add_argument("--run-dir", required=True)
143
+ parser.add_argument("--review", default="")
144
+ parser.add_argument("--repair-on-warnings", action="store_true")
145
+ args = parser.parse_args()
146
+
147
+ run_dir = Path(args.run_dir).expanduser().resolve()
148
+ review_path = (
149
+ Path(args.review).expanduser().resolve()
150
+ if args.review
151
+ else run_dir / "qa" / "review.json"
152
+ )
153
+ manifest_path = run_dir / "imagegen-jobs.json"
154
+ review = load_json(review_path)
155
+ manifest = load_json(manifest_path)
156
+
157
+ repairs = rows_to_repair(review, repair_on_warnings=args.repair_on_warnings)
158
+ queued: list[dict[str, object]] = []
159
+ for repair in repairs:
160
+ state = str(repair["state"])
161
+ reason = str(repair["reason"])
162
+ queued_repair = queue_repair(manifest, run_dir, state, reason)
163
+ attempt = int(queued_repair["attempt"])
164
+ append_repair_note(run_dir, state, attempt, reason)
165
+ queued.append({"state": state, "reason": reason, **queued_repair})
166
+
167
+ manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
168
+ print(json.dumps({"ok": True, "queued": queued}, indent=2))
169
+
170
+
171
+ if __name__ == "__main__":
172
+ main()
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env python3
2
+ """Record a selected $imagegen output for a Codex pet generation job."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import shutil
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+
14
+ from PIL import Image
15
+
16
+ CANONICAL_BASE_PATH = "references/canonical-base.png"
17
+
18
+
19
+ def load_jobs(path: Path) -> dict[str, object]:
20
+ if not path.exists():
21
+ raise SystemExit(f"job manifest not found: {path}")
22
+ return json.loads(path.read_text(encoding="utf-8"))
23
+
24
+
25
+ def job_list(manifest: dict[str, object]) -> list[dict[str, object]]:
26
+ jobs = manifest.get("jobs")
27
+ if not isinstance(jobs, list):
28
+ raise SystemExit("invalid imagegen-jobs.json: jobs must be a list")
29
+ return [job for job in jobs if isinstance(job, dict)]
30
+
31
+
32
+ def find_job(manifest: dict[str, object], job_id: str) -> dict[str, object]:
33
+ for job in job_list(manifest):
34
+ if job.get("id") == job_id:
35
+ return job
36
+ raise SystemExit(f"unknown job id: {job_id}")
37
+
38
+
39
+ def image_metadata(path: Path) -> dict[str, object]:
40
+ with Image.open(path) as image:
41
+ image.verify()
42
+ with Image.open(path) as image:
43
+ return {
44
+ "width": image.width,
45
+ "height": image.height,
46
+ "mode": image.mode,
47
+ "format": image.format,
48
+ }
49
+
50
+
51
+ def file_sha256(path: Path) -> str:
52
+ digest = hashlib.sha256()
53
+ with path.open("rb") as file:
54
+ for chunk in iter(lambda: file.read(1024 * 1024), b""):
55
+ digest.update(chunk)
56
+ return digest.hexdigest()
57
+
58
+
59
+ def manifest_relative(path: Path, run_dir: Path) -> str:
60
+ return str(path.resolve().relative_to(run_dir.resolve()))
61
+
62
+
63
+ def completed_job_ids(manifest: dict[str, object]) -> set[str]:
64
+ return {
65
+ str(job["id"])
66
+ for job in job_list(manifest)
67
+ if job.get("status") == "complete" and isinstance(job.get("id"), str)
68
+ }
69
+
70
+
71
+ def is_relative_to(path: Path, root: Path) -> bool:
72
+ try:
73
+ path.relative_to(root)
74
+ except ValueError:
75
+ return False
76
+ return True
77
+
78
+
79
+ def default_generated_images_root() -> Path:
80
+ codex_home = Path(os.environ.get("CODEX_HOME") or "~/.codex").expanduser().resolve()
81
+ return codex_home / "generated_images"
82
+
83
+
84
+ def validate_source_path(
85
+ *,
86
+ source: Path,
87
+ run_dir: Path,
88
+ allow_synthetic_test_source: bool,
89
+ ) -> str:
90
+ if allow_synthetic_test_source:
91
+ return "synthetic-test"
92
+ if is_relative_to(source, run_dir):
93
+ raise SystemExit(
94
+ "source image is inside the pet run directory; record the original "
95
+ "$imagegen output from $CODEX_HOME/generated_images/.../ig_*.png instead"
96
+ )
97
+ generated_root = default_generated_images_root()
98
+ if not is_relative_to(source, generated_root) or not source.name.startswith("ig_"):
99
+ raise SystemExit(
100
+ "source image does not look like a built-in $imagegen output; expected "
101
+ f"{generated_root}/.../ig_*.png. Do not ingest locally drawn or "
102
+ "post-processed row strips as visual job outputs."
103
+ )
104
+ return "built-in-imagegen"
105
+
106
+
107
+ def validate_required_grounding(job: dict[str, object], run_dir: Path) -> None:
108
+ if job.get("allow_prompt_only_generation") is not False:
109
+ return
110
+ inputs = job.get("input_images")
111
+ if not isinstance(inputs, list) or not inputs:
112
+ raise SystemExit(
113
+ f"job {job.get('id')} does not list input_images; grounded row jobs must attach references"
114
+ )
115
+ missing = []
116
+ for item in inputs:
117
+ if not isinstance(item, dict) or not isinstance(item.get("path"), str):
118
+ raise SystemExit(f"job {job.get('id')} has an invalid input image entry")
119
+ path = run_dir / item["path"]
120
+ if not path.is_file():
121
+ missing.append(str(path))
122
+ if missing:
123
+ raise SystemExit(
124
+ f"job {job.get('id')} is missing required grounding image(s): "
125
+ + ", ".join(missing)
126
+ )
127
+
128
+
129
+ def update_base_canonical_reference(
130
+ *,
131
+ run_dir: Path,
132
+ output: Path,
133
+ manifest: dict[str, object],
134
+ job: dict[str, object],
135
+ metadata: dict[str, object],
136
+ ) -> None:
137
+ if job.get("id") != "base":
138
+ return
139
+
140
+ canonical = run_dir / CANONICAL_BASE_PATH
141
+ canonical.parent.mkdir(parents=True, exist_ok=True)
142
+ shutil.copy2(output, canonical)
143
+ canonical_sha = file_sha256(canonical)
144
+ reference = {
145
+ "path": manifest_relative(canonical, run_dir),
146
+ "source_job": "base",
147
+ "sha256": canonical_sha,
148
+ "metadata": metadata,
149
+ }
150
+ job["canonical_reference_path"] = reference["path"]
151
+ manifest["canonical_identity_reference"] = reference
152
+
153
+ request_path = run_dir / "pet_request.json"
154
+ if request_path.exists():
155
+ request = json.loads(request_path.read_text(encoding="utf-8"))
156
+ request["canonical_identity_reference"] = reference
157
+ request_path.write_text(json.dumps(request, indent=2) + "\n", encoding="utf-8")
158
+
159
+
160
+ def main() -> None:
161
+ parser = argparse.ArgumentParser(description=__doc__)
162
+ parser.add_argument("--run-dir", required=True)
163
+ parser.add_argument("--job-id", required=True)
164
+ parser.add_argument("--source", required=True)
165
+ parser.add_argument("--force", action="store_true")
166
+ parser.add_argument(
167
+ "--allow-synthetic-test-source", action="store_true", help=argparse.SUPPRESS
168
+ )
169
+ args = parser.parse_args()
170
+
171
+ run_dir = Path(args.run_dir).expanduser().resolve()
172
+ source = Path(args.source).expanduser().resolve()
173
+ if not source.is_file():
174
+ raise SystemExit(f"source image not found: {source}")
175
+ source_provenance = validate_source_path(
176
+ source=source,
177
+ run_dir=run_dir,
178
+ allow_synthetic_test_source=args.allow_synthetic_test_source,
179
+ )
180
+
181
+ manifest_path = run_dir / "imagegen-jobs.json"
182
+ manifest = load_jobs(manifest_path)
183
+ job = find_job(manifest, args.job_id)
184
+
185
+ missing_deps = [
186
+ dep
187
+ for dep in job.get("depends_on", [])
188
+ if isinstance(dep, str) and dep not in completed_job_ids(manifest)
189
+ ]
190
+ if missing_deps:
191
+ raise SystemExit(
192
+ f"job {args.job_id} is not ready; missing dependency result(s): {', '.join(missing_deps)}"
193
+ )
194
+ validate_required_grounding(job, run_dir)
195
+
196
+ output_raw = job.get("output_path")
197
+ if not isinstance(output_raw, str):
198
+ raise SystemExit(f"job {args.job_id} has no output_path")
199
+ output = run_dir / output_raw
200
+ if output.exists() and not args.force:
201
+ raise SystemExit(f"{output} already exists; pass --force to replace it")
202
+
203
+ output.parent.mkdir(parents=True, exist_ok=True)
204
+ shutil.copy2(source, output)
205
+ metadata = image_metadata(output)
206
+
207
+ job["status"] = "complete"
208
+ job["source_path"] = str(source)
209
+ job["source_provenance"] = source_provenance
210
+ job["source_sha256"] = file_sha256(source)
211
+ job["output_sha256"] = file_sha256(output)
212
+ if source_provenance == "synthetic-test":
213
+ job["synthetic_test_source"] = True
214
+ else:
215
+ job.pop("synthetic_test_source", None)
216
+ job["completed_at"] = datetime.now(timezone.utc).isoformat()
217
+ job["metadata"] = metadata
218
+ for key in [
219
+ "last_error",
220
+ "secondary_fallback",
221
+ "derived_from",
222
+ "mirror_decision",
223
+ "repair_reason",
224
+ "queued_at",
225
+ ]:
226
+ job.pop(key, None)
227
+ update_base_canonical_reference(
228
+ run_dir=run_dir,
229
+ output=output,
230
+ manifest=manifest,
231
+ job=job,
232
+ metadata=metadata,
233
+ )
234
+
235
+ manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
236
+ print(
237
+ json.dumps(
238
+ {
239
+ "ok": True,
240
+ "job_id": args.job_id,
241
+ "output": str(output),
242
+ "metadata": metadata,
243
+ },
244
+ indent=2,
245
+ )
246
+ )
247
+
248
+
249
+ if __name__ == "__main__":
250
+ main()
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env python3
2
+ """Render Codex pet state videos from an atlas using ffmpeg."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import shutil
8
+ import subprocess
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ from PIL import Image, ImageDraw
13
+
14
+ CELL_WIDTH = 192
15
+ CELL_HEIGHT = 208
16
+ STATES = {
17
+ "idle": (0, [280, 110, 110, 140, 140, 320]),
18
+ "running-right": (1, [120, 120, 120, 120, 120, 120, 120, 220]),
19
+ "running-left": (2, [120, 120, 120, 120, 120, 120, 120, 220]),
20
+ "waving": (3, [140, 140, 140, 280]),
21
+ "jumping": (4, [140, 140, 140, 140, 280]),
22
+ "failed": (5, [140, 140, 140, 140, 140, 140, 140, 240]),
23
+ "waiting": (6, [150, 150, 150, 150, 150, 260]),
24
+ "running": (7, [120, 120, 120, 120, 120, 220]),
25
+ "review": (8, [150, 150, 150, 150, 150, 280]),
26
+ }
27
+
28
+
29
+ def checker(size: tuple[int, int], square: int = 16) -> Image.Image:
30
+ image = Image.new("RGB", size, "#ffffff")
31
+ draw = ImageDraw.Draw(image)
32
+ for y in range(0, size[1], square):
33
+ for x in range(0, size[0], square):
34
+ if (x // square + y // square) % 2:
35
+ draw.rectangle((x, y, x + square - 1, y + square - 1), fill="#e8e8e8")
36
+ return image
37
+
38
+
39
+ def shell_quote_for_concat(path: Path) -> str:
40
+ return "'" + str(path).replace("'", "'\\''") + "'"
41
+
42
+
43
+ def render_state(
44
+ atlas: Image.Image,
45
+ state: str,
46
+ row: int,
47
+ durations: list[int],
48
+ output_dir: Path,
49
+ loops: int,
50
+ scale: int,
51
+ ffmpeg: str,
52
+ ) -> None:
53
+ with tempfile.TemporaryDirectory(prefix=f"codex-pet-{state}-") as temp_raw:
54
+ temp = Path(temp_raw)
55
+ frame_paths: list[Path] = []
56
+ for column in range(len(durations)):
57
+ crop = atlas.crop(
58
+ (
59
+ column * CELL_WIDTH,
60
+ row * CELL_HEIGHT,
61
+ (column + 1) * CELL_WIDTH,
62
+ (row + 1) * CELL_HEIGHT,
63
+ )
64
+ ).convert("RGBA")
65
+ bg = checker((CELL_WIDTH, CELL_HEIGHT))
66
+ bg.paste(crop, (0, 0), crop)
67
+ frame_path = temp / f"{state}-{column:02d}.png"
68
+ bg.save(frame_path)
69
+ frame_paths.append(frame_path)
70
+
71
+ concat_path = temp / f"{state}.ffconcat"
72
+ lines = ["ffconcat version 1.0"]
73
+ sequence: list[tuple[Path, int]] = []
74
+ for _ in range(loops):
75
+ sequence.extend(zip(frame_paths, durations, strict=True))
76
+ for frame_path, duration_ms in sequence:
77
+ lines.append(f"file {shell_quote_for_concat(frame_path)}")
78
+ lines.append(f"duration {duration_ms / 1000:.3f}")
79
+ lines.append(f"file {shell_quote_for_concat(sequence[-1][0])}")
80
+ concat_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
81
+
82
+ output = output_dir / f"{state}.mp4"
83
+ command = [
84
+ ffmpeg,
85
+ "-y",
86
+ "-hide_banner",
87
+ "-loglevel",
88
+ "error",
89
+ "-f",
90
+ "concat",
91
+ "-safe",
92
+ "0",
93
+ "-i",
94
+ str(concat_path),
95
+ "-vf",
96
+ f"scale={CELL_WIDTH * scale}:{CELL_HEIGHT * scale}:flags=lanczos,format=yuv420p",
97
+ "-movflags",
98
+ "+faststart",
99
+ str(output),
100
+ ]
101
+ subprocess.run(command, check=True)
102
+
103
+
104
+ def main() -> None:
105
+ parser = argparse.ArgumentParser(description=__doc__)
106
+ parser.add_argument("atlas")
107
+ parser.add_argument("--output-dir", required=True)
108
+ parser.add_argument("--loops", type=int, default=4)
109
+ parser.add_argument("--scale", type=int, default=2)
110
+ parser.add_argument("--ffmpeg", default=shutil.which("ffmpeg") or "ffmpeg")
111
+ args = parser.parse_args()
112
+
113
+ output_dir = Path(args.output_dir).expanduser().resolve()
114
+ output_dir.mkdir(parents=True, exist_ok=True)
115
+
116
+ with Image.open(Path(args.atlas).expanduser().resolve()) as opened:
117
+ atlas = opened.convert("RGBA")
118
+
119
+ for state, (row, durations) in STATES.items():
120
+ render_state(
121
+ atlas,
122
+ state,
123
+ row,
124
+ durations,
125
+ output_dir,
126
+ args.loops,
127
+ args.scale,
128
+ args.ffmpeg,
129
+ )
130
+ print(f"wrote videos to {output_dir}")
131
+
132
+
133
+ if __name__ == "__main__":
134
+ main()
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ python3 "$SCRIPT_DIR/render_animation_videos.py" "$@"
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ """Validate a Codex pet spritesheet atlas."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from collections import defaultdict
9
+ from pathlib import Path
10
+
11
+ from PIL import Image
12
+
13
+ COLUMNS = 8
14
+ ROWS = 9
15
+ CELL_WIDTH = 192
16
+ CELL_HEIGHT = 208
17
+ ATLAS_WIDTH = COLUMNS * CELL_WIDTH
18
+ ATLAS_HEIGHT = ROWS * CELL_HEIGHT
19
+ ROW_BY_INDEX = {
20
+ 0: ("idle", 6),
21
+ 1: ("running-right", 8),
22
+ 2: ("running-left", 8),
23
+ 3: ("waving", 4),
24
+ 4: ("jumping", 5),
25
+ 5: ("failed", 8),
26
+ 6: ("waiting", 6),
27
+ 7: ("running", 6),
28
+ 8: ("review", 6),
29
+ }
30
+
31
+
32
+ def alpha_nonzero_count(image: Image.Image) -> int:
33
+ alpha = image.getchannel("A")
34
+ return sum(alpha.histogram()[1:])
35
+
36
+
37
+ def main() -> None:
38
+ parser = argparse.ArgumentParser(description=__doc__)
39
+ parser.add_argument("atlas")
40
+ parser.add_argument("--json-out")
41
+ parser.add_argument("--min-used-pixels", type=int, default=50)
42
+ parser.add_argument("--near-opaque-threshold", type=float, default=0.95)
43
+ parser.add_argument("--allow-opaque", action="store_true")
44
+ parser.add_argument("--allow-near-opaque-used-cells", action="store_true")
45
+ args = parser.parse_args()
46
+
47
+ atlas_path = Path(args.atlas).expanduser().resolve()
48
+ errors: list[str] = []
49
+ warnings: list[str] = []
50
+ near_opaque_used_cells: dict[str, list[int]] = defaultdict(list)
51
+ cells: list[dict[str, object]] = []
52
+
53
+ try:
54
+ with Image.open(atlas_path) as opened:
55
+ source_mode = opened.mode
56
+ source_format = opened.format
57
+ image = opened.convert("RGBA")
58
+ except Exception as exc: # noqa: BLE001
59
+ result = {"ok": False, "errors": [f"could not open atlas: {exc}"], "warnings": []}
60
+ print(json.dumps(result, indent=2))
61
+ raise SystemExit(1)
62
+
63
+ if image.size != (ATLAS_WIDTH, ATLAS_HEIGHT):
64
+ errors.append(f"expected {ATLAS_WIDTH}x{ATLAS_HEIGHT}, got {image.width}x{image.height}")
65
+
66
+ if source_format not in {"PNG", "WEBP"}:
67
+ errors.append(f"expected PNG or WebP, got {source_format}")
68
+
69
+ if "A" not in source_mode and not args.allow_opaque:
70
+ errors.append("atlas does not have an alpha channel")
71
+
72
+ for row_index in range(ROWS):
73
+ state, frame_count = ROW_BY_INDEX[row_index]
74
+ for column_index in range(COLUMNS):
75
+ left = column_index * CELL_WIDTH
76
+ top = row_index * CELL_HEIGHT
77
+ cell = image.crop((left, top, left + CELL_WIDTH, top + CELL_HEIGHT))
78
+ nontransparent = alpha_nonzero_count(cell)
79
+ used = column_index < frame_count
80
+ cell_info = {
81
+ "state": state,
82
+ "row": row_index,
83
+ "column": column_index,
84
+ "used": used,
85
+ "nontransparent_pixels": nontransparent,
86
+ }
87
+ cells.append(cell_info)
88
+ if used and nontransparent < args.min_used_pixels:
89
+ errors.append(
90
+ f"{state} row {row_index} column {column_index} is empty or too sparse ({nontransparent} pixels)"
91
+ )
92
+ if used and nontransparent > CELL_WIDTH * CELL_HEIGHT * args.near_opaque_threshold:
93
+ near_opaque_used_cells[f"{state} row {row_index}"].append(column_index)
94
+ if not used and nontransparent != 0:
95
+ errors.append(
96
+ f"{state} row {row_index} unused column {column_index} is not transparent ({nontransparent} pixels)"
97
+ )
98
+
99
+ for row_label, columns in near_opaque_used_cells.items():
100
+ message = (
101
+ f"{row_label} has {len(columns)} nearly opaque used cells; "
102
+ "this usually means the sprite has a non-transparent background"
103
+ )
104
+ if args.allow_near_opaque_used_cells:
105
+ warnings.append(message)
106
+ else:
107
+ errors.append(message)
108
+
109
+ alpha_count = alpha_nonzero_count(image)
110
+ if alpha_count == ATLAS_WIDTH * ATLAS_HEIGHT:
111
+ message = "atlas is fully opaque; custom pets require a transparent sprite background"
112
+ if args.allow_opaque:
113
+ warnings.append(message)
114
+ else:
115
+ errors.append(message)
116
+
117
+ result = {
118
+ "ok": not errors,
119
+ "file": str(atlas_path),
120
+ "format": source_format,
121
+ "mode": source_mode,
122
+ "width": image.width,
123
+ "height": image.height,
124
+ "errors": errors,
125
+ "warnings": warnings,
126
+ "cells": cells,
127
+ }
128
+
129
+ if args.json_out:
130
+ Path(args.json_out).expanduser().resolve().write_text(
131
+ json.dumps(result, indent=2) + "\n", encoding="utf-8"
132
+ )
133
+
134
+ print(json.dumps({k: v for k, v in result.items() if k != "cells"}, indent=2))
135
+ raise SystemExit(0 if result["ok"] else 1)
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()