claude-dev-env 2.13.0 → 2.14.1

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 (263) hide show
  1. package/AGENTS.md +135 -0
  2. package/CLAUDE.md +1 -57
  3. package/_shared/AGENTS.md +15 -0
  4. package/_shared/CLAUDE.md +1 -15
  5. package/_shared/advisor/AGENTS.md +22 -0
  6. package/_shared/advisor/CLAUDE.md +1 -22
  7. package/_shared/pr-loop/AGENTS.md +27 -0
  8. package/_shared/pr-loop/CLAUDE.md +1 -27
  9. package/_shared/pr-loop/scripts/AGENTS.md +35 -0
  10. package/_shared/pr-loop/scripts/CLAUDE.md +1 -35
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +42 -0
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +1 -42
  13. package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +26 -0
  14. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -26
  15. package/_shared/pr-loop/scripts/tests/AGENTS.md +44 -0
  16. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -44
  17. package/_shared/process-tree/AGENTS.md +41 -0
  18. package/_shared/process-tree/CLAUDE.md +1 -41
  19. package/agents/AGENTS.md +26 -0
  20. package/agents/CLAUDE.md +1 -31
  21. package/agents/clean-coder.md +6 -6
  22. package/agents/test_agent_frontmatter.py +11 -11
  23. package/audit-rubrics/AGENTS.md +43 -0
  24. package/audit-rubrics/CLAUDE.md +1 -43
  25. package/audit-rubrics/category_rubrics/AGENTS.md +37 -0
  26. package/audit-rubrics/category_rubrics/CLAUDE.md +1 -37
  27. package/audit-rubrics/prompts/AGENTS.md +37 -0
  28. package/audit-rubrics/prompts/CLAUDE.md +1 -37
  29. package/bin/AGENTS.md +112 -0
  30. package/bin/CLAUDE.md +1 -112
  31. package/bin/ever-shipped-skills.mjs +1 -0
  32. package/bin/install-constants.mjs +6 -7
  33. package/bin/install.mjs +24 -5
  34. package/bin/install.test.mjs +34 -0
  35. package/commands/AGENTS.md +13 -0
  36. package/commands/CLAUDE.md +1 -23
  37. package/docs/AGENTS.md +32 -0
  38. package/docs/CLAUDE.md +1 -32
  39. package/docs/CODE_RULES.md +1 -1
  40. package/docs/imagegen.md +22 -0
  41. package/docs/references/AGENTS.md +17 -0
  42. package/docs/references/CLAUDE.md +1 -17
  43. package/hooks/AGENTS.md +31 -0
  44. package/hooks/CLAUDE.md +1 -31
  45. package/hooks/_gh_pr_author_swap_utils.py +14 -0
  46. package/hooks/advisory/AGENTS.md +16 -0
  47. package/hooks/advisory/CLAUDE.md +1 -16
  48. package/hooks/blocking/AGENTS.md +123 -0
  49. package/hooks/blocking/CLAUDE.md +1 -123
  50. package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +28 -0
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +1 -28
  52. package/hooks/blocking/code_rules_banned_identifiers.py +1 -1
  53. package/hooks/blocking/code_rules_dead_module_constant.py +149 -37
  54. package/hooks/blocking/code_rules_probe_chains.py +6 -2
  55. package/hooks/blocking/config/AGENTS.md +10 -0
  56. package/hooks/blocking/config/CLAUDE.md +1 -10
  57. package/hooks/blocking/gh_pr_author_restore.py +7 -2
  58. package/hooks/blocking/intent_only_ending_blocker.py +11 -16
  59. package/hooks/blocking/inventory_intent_records/AGENTS.md +26 -0
  60. package/hooks/blocking/inventory_intent_records/CLAUDE.md +1 -26
  61. package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +26 -0
  62. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +1 -26
  63. package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +24 -0
  64. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +1 -24
  65. package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +30 -0
  66. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +1 -30
  67. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +1 -1
  68. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +1 -1
  69. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +171 -0
  70. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  71. package/hooks/blocking/test_gh_pr_author_restore.py +2 -1
  72. package/hooks/blocking/test_intent_only_ending_blocker.py +4 -0
  73. package/hooks/diagnostic/AGENTS.md +75 -0
  74. package/hooks/diagnostic/CLAUDE.md +1 -43
  75. package/hooks/diagnostic/hook_log_extractor.py +2 -2
  76. package/hooks/diagnostic/hook_log_init.py +2 -2
  77. package/hooks/diagnostic/migrations/AGENTS.md +16 -0
  78. package/hooks/diagnostic/migrations/CLAUDE.md +1 -16
  79. package/hooks/diagnostic/queries/AGENTS.md +19 -0
  80. package/hooks/diagnostic/queries/CLAUDE.md +1 -19
  81. package/hooks/diagnostic/test_hook_log_extractor.py +2 -2
  82. package/hooks/git-hooks/AGENTS.md +31 -0
  83. package/hooks/git-hooks/CLAUDE.md +1 -31
  84. package/hooks/git-hooks/git_hooks_constants/AGENTS.md +21 -0
  85. package/hooks/git-hooks/git_hooks_constants/CLAUDE.md +1 -21
  86. package/hooks/git-hooks/git_hooks_constants/__init__.py +4 -6
  87. package/hooks/git-hooks/pre_push.py +47 -34
  88. package/hooks/git-hooks/test_pre_push.py +111 -7
  89. package/hooks/hooks_constants/AGENTS.md +104 -0
  90. package/hooks/hooks_constants/CLAUDE.md +1 -104
  91. package/hooks/hooks_constants/hook_log_extractor_constants.py +1 -2
  92. package/hooks/hooks_constants/task_list_loop_starter_constants.py +4 -3
  93. package/hooks/hooks_constants/working_style_prompt_constants.py +29 -20
  94. package/hooks/lifecycle/AGENTS.md +18 -0
  95. package/hooks/lifecycle/CLAUDE.md +1 -18
  96. package/hooks/observability/AGENTS.md +20 -0
  97. package/hooks/observability/CLAUDE.md +1 -20
  98. package/hooks/session/AGENTS.md +33 -0
  99. package/hooks/session/CLAUDE.md +1 -33
  100. package/hooks/session/gh_pr_author_session_cleanup.py +7 -2
  101. package/hooks/session/task_list_loop_starter.py +3 -3
  102. package/hooks/session/test_gh_pr_author_session_cleanup.py +2 -1
  103. package/hooks/session/test_task_list_loop_starter.py +10 -2
  104. package/hooks/session/test_working_style_prompt.py +9 -4
  105. package/hooks/validation/AGENTS.md +20 -0
  106. package/hooks/validation/CLAUDE.md +1 -20
  107. package/hooks/validators/AGENTS.md +51 -0
  108. package/hooks/validators/CLAUDE.md +1 -51
  109. package/hooks/validators/health_check.py +52 -19
  110. package/hooks/validators/python_style_checks.py +1 -1
  111. package/hooks/validators/test_health_check.py +112 -27
  112. package/hooks/workflow/AGENTS.md +16 -0
  113. package/hooks/workflow/CLAUDE.md +1 -16
  114. package/installable-surfaces.manifest.json +1 -0
  115. package/output-styles/AGENTS.md +15 -0
  116. package/output-styles/CLAUDE.md +1 -15
  117. package/package.json +2 -1
  118. package/rules/AGENTS.md +61 -0
  119. package/rules/CLAUDE.md +1 -61
  120. package/rules/code-standards.md +5 -9
  121. package/rules/doc-prose-cuts.md +1 -1
  122. package/scripts/AGENTS.md +62 -0
  123. package/scripts/CLAUDE.md +1 -62
  124. package/scripts/codex_compat_materializer.py +4 -0
  125. package/scripts/dev_env_scripts_constants/AGENTS.md +21 -0
  126. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -21
  127. package/scripts/sync_to_cursor/AGENTS.md +23 -0
  128. package/scripts/sync_to_cursor/CLAUDE.md +1 -23
  129. package/scripts/test_active_capability_references.py +2 -2
  130. package/scripts/test_validate_instruction_pairs.py +90 -0
  131. package/scripts/test_verify_installable_package.py +5 -1
  132. package/scripts/tests/AGENTS.md +33 -0
  133. package/scripts/tests/CLAUDE.md +1 -33
  134. package/scripts/tests/test_codex_compat_materializer.py +36 -1
  135. package/scripts/validate_instruction_pairs.py +183 -0
  136. package/skills/AGENTS.md +72 -0
  137. package/skills/CLAUDE.md +1 -72
  138. package/skills/_shared/AGENTS.md +44 -0
  139. package/skills/_shared/CLAUDE.md +1 -44
  140. package/skills/_shared/advisor/AGENTS.md +9 -0
  141. package/skills/_shared/advisor/CLAUDE.md +1 -9
  142. package/skills/_shared/pr-loop/AGENTS.md +50 -0
  143. package/skills/_shared/pr-loop/CLAUDE.md +1 -50
  144. package/skills/_shared/pr-loop/prompts/AGENTS.md +9 -0
  145. package/skills/_shared/pr-loop/prompts/CLAUDE.md +1 -9
  146. package/skills/_shared/pr-loop/scripts/AGENTS.md +35 -0
  147. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -35
  148. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +24 -0
  149. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -24
  150. package/skills/anthropic-plan/AGENTS.md +34 -0
  151. package/skills/anthropic-plan/CLAUDE.md +1 -34
  152. package/skills/anthropic-plan/SKILL.md +1 -1
  153. package/skills/anthropic-plan/scripts/AGENTS.md +11 -0
  154. package/skills/anthropic-plan/scripts/CLAUDE.md +1 -11
  155. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +16 -0
  156. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/CLAUDE.md +1 -16
  157. package/skills/anthropic-plan/templates/AGENTS.md +13 -0
  158. package/skills/anthropic-plan/templates/CLAUDE.md +1 -13
  159. package/skills/anthropic-plan/test_skill_contract.py +8 -6
  160. package/skills/anthropic-plan/workflow/AGENTS.md +14 -0
  161. package/skills/anthropic-plan/workflow/CLAUDE.md +1 -14
  162. package/skills/auditing-claude-config/AGENTS.md +21 -0
  163. package/skills/auditing-claude-config/CLAUDE.md +1 -21
  164. package/skills/autoconverge/AGENTS.md +36 -0
  165. package/skills/autoconverge/CLAUDE.md +1 -36
  166. package/skills/autoconverge/reference/AGENTS.md +16 -0
  167. package/skills/autoconverge/reference/CLAUDE.md +1 -16
  168. package/skills/autoconverge/workflow/AGENTS.md +26 -0
  169. package/skills/autoconverge/workflow/CLAUDE.md +1 -26
  170. package/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +16 -0
  171. package/skills/autoconverge/workflow/autoconverge_report_constants/CLAUDE.md +1 -16
  172. package/skills/bugteam/AGENTS.md +30 -0
  173. package/skills/bugteam/CLAUDE.md +1 -30
  174. package/skills/bugteam/reference/AGENTS.md +20 -0
  175. package/skills/bugteam/reference/CLAUDE.md +1 -20
  176. package/skills/bugteam/reference/obstacles/AGENTS.md +24 -0
  177. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -24
  178. package/skills/bugteam/scripts/AGENTS.md +30 -0
  179. package/skills/bugteam/scripts/CLAUDE.md +1 -30
  180. package/skills/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +18 -0
  181. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +1 -18
  182. package/skills/closeout/reference/handoff-prompt-template.md +1 -1
  183. package/skills/codex-review/AGENTS.md +46 -0
  184. package/skills/codex-review/CLAUDE.md +1 -46
  185. package/skills/codex-review/reference/AGENTS.md +15 -0
  186. package/skills/codex-review/reference/CLAUDE.md +1 -15
  187. package/skills/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +18 -0
  188. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +1 -18
  189. package/skills/codex-review/test_skill_scaffold.py +3 -3
  190. package/skills/copilot-review/AGENTS.md +18 -0
  191. package/skills/copilot-review/CLAUDE.md +1 -18
  192. package/skills/everything-search/AGENTS.md +17 -0
  193. package/skills/everything-search/CLAUDE.md +1 -17
  194. package/skills/fresh-branch/AGENTS.md +14 -0
  195. package/skills/fresh-branch/CLAUDE.md +1 -14
  196. package/skills/grok-spawn/AGENTS.md +28 -0
  197. package/skills/grok-spawn/CLAUDE.md +1 -28
  198. package/skills/pr-converge/AGENTS.md +32 -0
  199. package/skills/pr-converge/CLAUDE.md +1 -32
  200. package/skills/pr-converge/pr_converge_skill_constants/AGENTS.md +26 -0
  201. package/skills/pr-converge/pr_converge_skill_constants/CLAUDE.md +1 -26
  202. package/skills/pr-converge/reference/AGENTS.md +28 -0
  203. package/skills/pr-converge/reference/CLAUDE.md +1 -28
  204. package/skills/pr-converge/reference/obstacles/AGENTS.md +23 -0
  205. package/skills/pr-converge/reference/obstacles/CLAUDE.md +1 -23
  206. package/skills/pr-converge/scripts/AGENTS.md +46 -0
  207. package/skills/pr-converge/scripts/CLAUDE.md +1 -46
  208. package/skills/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +18 -0
  209. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -18
  210. package/skills/pr-converge/workflows/AGENTS.md +16 -0
  211. package/skills/pr-converge/workflows/CLAUDE.md +1 -16
  212. package/skills/prototype/SKILL.md +1 -2
  213. package/skills/prototype/reference/honest-limitations.md +1 -1
  214. package/skills/prototype/reference/promotion-tasks.md +1 -1
  215. package/skills/prototype/workflows/promotion.md +1 -1
  216. package/skills/rebase/AGENTS.md +32 -0
  217. package/skills/rebase/CLAUDE.md +1 -32
  218. package/skills/recall/AGENTS.md +30 -0
  219. package/skills/recall/CLAUDE.md +1 -30
  220. package/skills/remember/AGENTS.md +31 -0
  221. package/skills/remember/CLAUDE.md +1 -31
  222. package/skills/reviews/SKILL.md +1 -1
  223. package/skills/session-log/AGENTS.md +32 -0
  224. package/skills/session-log/CLAUDE.md +1 -32
  225. package/skills/session-tidy/AGENTS.md +36 -0
  226. package/skills/session-tidy/CLAUDE.md +1 -36
  227. package/skills/skill-builder/AGENTS.md +49 -0
  228. package/skills/skill-builder/CLAUDE.md +1 -49
  229. package/skills/skill-builder/references/AGENTS.md +25 -0
  230. package/skills/skill-builder/references/CLAUDE.md +1 -25
  231. package/skills/skill-builder/references/deterministic-elements.md +1 -1
  232. package/skills/skill-builder/templates/AGENTS.md +14 -0
  233. package/skills/skill-builder/templates/CLAUDE.md +1 -14
  234. package/skills/skill-builder/workflows/AGENTS.md +19 -0
  235. package/skills/skill-builder/workflows/CLAUDE.md +1 -19
  236. package/skills/task-build/AGENTS.md +29 -0
  237. package/skills/task-build/CLAUDE.md +1 -29
  238. package/skills/update/AGENTS.md +38 -0
  239. package/skills/update/CLAUDE.md +1 -38
  240. package/system-prompts/AGENTS.md +17 -0
  241. package/system-prompts/CLAUDE.md +1 -17
  242. package/system-prompts/software-engineer.xml +3 -3
  243. package/agents/caveman.md +0 -73
  244. package/agents/clasp-deployment-orchestrator.md +0 -608
  245. package/agents/code-advisor.md +0 -23
  246. package/agents/deep-research.md +0 -152
  247. package/agents/docs-agent.md +0 -85
  248. package/commands/commit.md +0 -28
  249. package/commands/docupdate.md +0 -322
  250. package/commands/hook-log-extract.md +0 -70
  251. package/commands/hook-log-init.md +0 -76
  252. package/commands/implement.md +0 -102
  253. package/commands/plan.md +0 -14
  254. package/commands/pr-comments.md +0 -47
  255. package/commands/review-plan.md +0 -5
  256. package/commands/right-size.md +0 -15
  257. package/commands/sum.md +0 -30
  258. package/skills/imagegen/SKILL.md +0 -21
  259. package/skills/imagegen/scripts/config/__init__.py +0 -1
  260. package/skills/imagegen/scripts/config/constants.py +0 -28
  261. package/skills/imagegen/scripts/imagegen.py +0 -45
  262. package/skills/imagegen/scripts/imagegen_core.py +0 -528
  263. package/skills/imagegen/scripts/test_imagegen.py +0 -204
@@ -1,528 +0,0 @@
1
- """Exact-resolution image generation with secret-safe provider boundaries."""
2
-
3
- from __future__ import annotations
4
-
5
- import base64
6
- import binascii
7
- import hashlib
8
- import json
9
- import os
10
- import secrets
11
- import shutil
12
- import subprocess
13
- import tempfile
14
- import urllib.error
15
- import urllib.request
16
- from collections.abc import Callable, Mapping, Sequence
17
- from dataclasses import dataclass
18
- from io import BytesIO
19
- from pathlib import Path
20
-
21
- from PIL import Image
22
-
23
- from config.constants import (
24
- ALL_SUPPORTED_BACKENDS,
25
- ALL_SUPPORTED_RESIZE_POLICIES,
26
- CODEX_TIMEOUT_SECONDS,
27
- CODEX_TOOL_NAME,
28
- DEFAULT_PROVIDER_EDGE,
29
- JSON_INDENT_LEVEL,
30
- MAXIMUM_CODEX_ARTIFACT_COUNT,
31
- MAXIMUM_CODEX_OUTPUT_BYTES,
32
- MAXIMUM_IMAGE_EDGE,
33
- MAXIMUM_PIXEL_COUNT,
34
- MAXIMUM_PROVIDER_BYTES,
35
- MINIMUM_DIMENSION,
36
- MINIMUM_PIXEL_COUNT,
37
- OPENAI_API_KEY_NAME,
38
- OPENAI_API_URL,
39
- OPENAI_MODEL,
40
- OPENAI_OUTPUT_FORMAT,
41
- OPENAI_TIMEOUT_SECONDS,
42
- PNG_FORMAT,
43
- PNG_SUFFIX,
44
- RECEIPT_SUFFIX,
45
- SIZE_COMPONENT_COUNT,
46
- SIZE_MULTIPLE,
47
- SIZE_SEPARATOR,
48
- TEMPORARY_DIRECTORY_PREFIX,
49
- TEMPORARY_FILE_TOKEN_BYTES,
50
- )
51
-
52
-
53
- class ImagegenError(RuntimeError):
54
- """Raised when generation cannot produce a truthful receipt."""
55
-
56
-
57
- @dataclass(frozen=True)
58
- class ImageSize:
59
- """A validated square image size."""
60
-
61
- width: int
62
- height: int
63
-
64
- def as_text(self) -> str:
65
- return f"{self.width}{SIZE_SEPARATOR}{self.height}"
66
-
67
-
68
- @dataclass(frozen=True)
69
- class ProviderArtifact:
70
- """Provider bytes, requested dimensions, and observed source dimensions."""
71
-
72
- image_bytes: bytes
73
- source_size: ImageSize
74
- provider_requested_size: ImageSize | None
75
- model_or_tool: str
76
- credential_source: str
77
-
78
-
79
- Transport = Callable[[str, bytes, Mapping[str, str]], bytes]
80
- Runner = Callable[[Sequence[str], Path, Mapping[str, str]], None]
81
-
82
-
83
- def parse_size(size_text: str) -> ImageSize:
84
- """Parse a square final artifact size within the output safety limits.
85
-
86
- Args:
87
- size_text: Width and height separated by ``x``.
88
- Returns:
89
- The parsed final artifact size.
90
- Raises:
91
- ImagegenError: If the size is malformed or exceeds the safety limits.
92
- """
93
- pieces = size_text.lower().split(SIZE_SEPARATOR)
94
- if len(pieces) != SIZE_COMPONENT_COUNT or not all(piece.isdigit() for piece in pieces):
95
- raise ImagegenError("size must use WIDTHxHEIGHT notation")
96
- image_size = ImageSize(int(pieces[0]), int(pieces[1]))
97
- validate_final_size(image_size)
98
- return image_size
99
-
100
-
101
- def validate_final_size(image_size: ImageSize) -> None:
102
- """Validate square final dimensions for both CLI and library callers.
103
-
104
- Args:
105
- image_size: Candidate final dimensions.
106
- Raises:
107
- ImagegenError: If dimensions are non-square or exceed output limits.
108
- """
109
- if (
110
- image_size.width < MINIMUM_DIMENSION
111
- or image_size.height < MINIMUM_DIMENSION
112
- or image_size.width != image_size.height
113
- or image_size.width > MAXIMUM_IMAGE_EDGE
114
- or image_size.width * image_size.height > MAXIMUM_PIXEL_COUNT
115
- ):
116
- raise ImagegenError("size exceeds output limits")
117
-
118
-
119
- def is_native_openai_size(image_size: ImageSize) -> bool:
120
- """Return whether GPT Image 2 accepts the requested provider size.
121
-
122
- Args:
123
- image_size: Candidate provider dimensions.
124
- Returns:
125
- True when the dimensions satisfy the GPT Image 2 contract.
126
- """
127
- return (
128
- image_size.width == image_size.height
129
- and image_size.width % SIZE_MULTIPLE == 0
130
- and image_size.height % SIZE_MULTIPLE == 0
131
- and image_size.width <= MAXIMUM_IMAGE_EDGE
132
- and image_size.width * image_size.height >= MINIMUM_PIXEL_COUNT
133
- and image_size.width * image_size.height <= MAXIMUM_PIXEL_COUNT
134
- )
135
-
136
-
137
- def select_provider_size(final_size: ImageSize) -> ImageSize:
138
- """Select a valid GPT Image 2 size with the final aspect ratio.
139
-
140
- Args:
141
- final_size: Requested final artifact dimensions.
142
- Returns:
143
- A valid native provider size with the same aspect ratio.
144
- """
145
- if is_native_openai_size(final_size):
146
- return final_size
147
- return ImageSize(DEFAULT_PROVIDER_EDGE, DEFAULT_PROVIDER_EDGE)
148
-
149
-
150
- def require_png_within_limits(image: Image.Image) -> None:
151
- """Require a PNG whose pixel count stays within the safety limit.
152
-
153
- Args:
154
- image: Open Pillow image.
155
- Raises:
156
- ImagegenError: If the format is not PNG or the pixel count is too high.
157
- """
158
- if image.format != PNG_FORMAT:
159
- raise ImagegenError("provider image must be PNG")
160
- if image.width * image.height > MAXIMUM_PIXEL_COUNT:
161
- raise ImagegenError("provider image exceeds the pixel limit")
162
-
163
-
164
- def require_provider_byte_limit(image_bytes: bytes) -> None:
165
- """Require encoded image bytes stay within the provider byte limit.
166
-
167
- Args:
168
- image_bytes: Encoded image payload.
169
- Raises:
170
- ImagegenError: If the payload is larger than the allowed byte limit.
171
- """
172
- if len(image_bytes) > MAXIMUM_PROVIDER_BYTES:
173
- raise ImagegenError("provider image exceeds the byte limit")
174
-
175
-
176
- def decode_image(image_bytes: bytes) -> ImageSize:
177
- """Decode image bytes with Pillow and return observed dimensions.
178
-
179
- Args:
180
- image_bytes: Encoded image bytes from a provider or transformation.
181
- Returns:
182
- Decoded image dimensions.
183
- Raises:
184
- ImagegenError: If bytes are oversized, malformed, or exceed pixel limits.
185
- """
186
- require_provider_byte_limit(image_bytes)
187
- try:
188
- with Image.open(BytesIO(image_bytes)) as image:
189
- require_png_within_limits(image)
190
- image.verify()
191
- with Image.open(BytesIO(image_bytes)) as image:
192
- image.load()
193
- return ImageSize(*image.size)
194
- except ImagegenError:
195
- raise
196
- except (OSError, SyntaxError, ValueError, Image.DecompressionBombError) as error:
197
- raise ImagegenError("provider returned an invalid image") from error
198
-
199
-
200
- def build_openai_transport() -> Transport:
201
- """Build the standard-library HTTPS transport.
202
-
203
- Returns:
204
- A transport callable for the OpenAI image-generation endpoint.
205
- """
206
- def send_request(url: str, body: bytes, all_headers: Mapping[str, str]) -> bytes:
207
- """Send one authenticated request.
208
-
209
- Args:
210
- url: HTTPS endpoint.
211
- body: JSON request bytes.
212
- all_headers: Request headers, including authentication.
213
- Returns:
214
- Raw endpoint bytes.
215
- Raises:
216
- ImagegenError: If the HTTPS request fails.
217
- """
218
- request = urllib.request.Request(url, data=body, headers=dict(all_headers), method="POST")
219
- try:
220
- with urllib.request.urlopen(request, timeout=OPENAI_TIMEOUT_SECONDS) as http_reply:
221
- return http_reply.read()
222
- except (OSError, TimeoutError, urllib.error.URLError) as error:
223
- raise ImagegenError("OpenAI image request failed") from error
224
-
225
- return send_request
226
-
227
-
228
- def request_openai_image(prompt: str, requested_size: ImageSize, transport: Transport) -> ProviderArtifact:
229
- """Request a GPT Image 2 artifact using the environment API key.
230
-
231
- Args:
232
- prompt: Image-generation prompt.
233
- requested_size: Native provider dimensions.
234
- transport: Injected HTTPS transport.
235
- Returns:
236
- Provider artifact with observed source dimensions.
237
- Raises:
238
- ImagegenError: If credentials, transport, or provider bytes are invalid.
239
- """
240
- api_key = os.environ.get(OPENAI_API_KEY_NAME)
241
- if not api_key:
242
- raise ImagegenError("OPENAI_API_KEY is required for openai-api")
243
- request_body = json.dumps({"model": OPENAI_MODEL, "prompt": prompt, "size": requested_size.as_text(), "output_format": OPENAI_OUTPUT_FORMAT}).encode()
244
- all_headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
245
- try:
246
- api_payload = json.loads(transport(OPENAI_API_URL, request_body, all_headers))
247
- image_records = api_payload.get("data", [])
248
- image_record = image_records[0] if image_records else {}
249
- if image_record.get("b64_json"):
250
- image_bytes = base64.b64decode(image_record["b64_json"], validate=True)
251
- elif image_record.get("url"):
252
- image_bytes = download_https_image(image_record["url"])
253
- else:
254
- raise ImagegenError("OpenAI returned no image artifact")
255
- except ImagegenError:
256
- raise
257
- except (AttributeError, binascii.Error, IndexError, KeyError, OSError, TypeError, ValueError, urllib.error.URLError, json.JSONDecodeError) as error:
258
- raise ImagegenError("OpenAI returned an invalid image response") from error
259
- return ProviderArtifact(image_bytes, decode_image(image_bytes), requested_size, OPENAI_MODEL, OPENAI_API_KEY_NAME)
260
-
261
-
262
- class _RejectRedirectHandler(urllib.request.HTTPRedirectHandler):
263
- """Refuse HTTP redirects so an HTTPS URL cannot land on a weaker scheme."""
264
-
265
- def redirect_request(
266
- self,
267
- request: urllib.request.Request,
268
- file_pointer: object,
269
- status_code: int,
270
- message: str,
271
- all_headers: Mapping[str, str],
272
- new_url: str,
273
- ) -> None:
274
- del request, file_pointer, status_code, message, all_headers, new_url
275
- raise ImagegenError("OpenAI image URL redirects are not allowed")
276
-
277
-
278
- def download_https_image(image_url: str) -> bytes:
279
- """Download provider image bytes from an HTTPS URL only.
280
-
281
- Args:
282
- image_url: Provider-returned image location.
283
- Returns:
284
- Raw image bytes capped by the provider byte limit.
285
- Raises:
286
- ImagegenError: If the URL is not HTTPS, redirects, download fails, or bytes exceed the limit.
287
- """
288
- if not image_url.casefold().startswith("https://"):
289
- raise ImagegenError("OpenAI image URL must use HTTPS")
290
- request = urllib.request.Request(image_url, method="GET")
291
- opener = urllib.request.build_opener(_RejectRedirectHandler)
292
- try:
293
- with opener.open(request, timeout=OPENAI_TIMEOUT_SECONDS) as image_download:
294
- if not image_download.geturl().casefold().startswith("https://"):
295
- raise ImagegenError("OpenAI image URL must use HTTPS")
296
- image_bytes = image_download.read(MAXIMUM_PROVIDER_BYTES + 1)
297
- except ImagegenError:
298
- raise
299
- except (OSError, TimeoutError, urllib.error.URLError) as error:
300
- raise ImagegenError("OpenAI image download failed") from error
301
- require_provider_byte_limit(image_bytes)
302
- return image_bytes
303
-
304
-
305
- def build_oauth_environment() -> dict[str, str]:
306
- """Copy the child environment while removing the API credential."""
307
- child_environment = dict(os.environ)
308
- child_environment.pop(OPENAI_API_KEY_NAME, None)
309
- return child_environment
310
-
311
-
312
- def discover_oauth_artifact(work_directory: Path, expected_path: Path) -> bytes:
313
- """Find exactly one PNG emitted by the Codex image tool.
314
-
315
- Args:
316
- work_directory: Isolated directory scanned for provider artifacts.
317
- expected_path: Exact PNG path declared in the Codex prompt.
318
- Returns:
319
- The single PNG artifact bytes.
320
- Raises:
321
- ImagegenError: If the provider emits zero, multiple, or unreadable PNGs.
322
- """
323
- all_images = sorted(work_directory.rglob(f"*{PNG_SUFFIX}"))
324
- if len(all_images) != MAXIMUM_CODEX_ARTIFACT_COUNT:
325
- raise ImagegenError("Codex must produce exactly one PNG artifact")
326
- if all_images[0] != expected_path:
327
- raise ImagegenError("Codex artifact path does not match the declared path")
328
- try:
329
- return all_images[0].read_bytes()
330
- except OSError as error:
331
- raise ImagegenError("Codex artifact could not be read") from error
332
-
333
-
334
- def request_oauth_image(prompt: str, runner: Runner) -> ProviderArtifact:
335
- """Run Codex in an isolated directory and measure its PNG artifact.
336
-
337
- Args:
338
- prompt: Image-generation prompt.
339
- runner: Injected Codex process runner.
340
- Returns:
341
- Provider artifact with runtime-observed dimensions.
342
- Raises:
343
- ImagegenError: If Codex or its artifact contract fails.
344
- """
345
- with tempfile.TemporaryDirectory(prefix=TEMPORARY_DIRECTORY_PREFIX) as temporary_directory:
346
- work_directory = Path(temporary_directory)
347
- artifact_path = work_directory / f"generated{PNG_SUFFIX}"
348
- contract = f"Generate exactly one PNG image for this prompt: {prompt}. Save it at {artifact_path}."
349
- codex_command = shutil.which("codex") or "codex"
350
- runner((codex_command, "exec", "--skip-git-repo-check", "-s", "workspace-write", "-C", str(work_directory), contract), work_directory, build_oauth_environment())
351
- image_bytes = discover_oauth_artifact(work_directory, artifact_path)
352
- return ProviderArtifact(image_bytes, decode_image(image_bytes), None, CODEX_TOOL_NAME, "codex OAuth")
353
-
354
-
355
- def resize_image(image_bytes: bytes, target_size: ImageSize) -> bytes:
356
- """Resize decoded image bytes to the requested dimensions as PNG.
357
-
358
- Args:
359
- image_bytes: Valid source image bytes.
360
- target_size: Final dimensions with the same aspect ratio.
361
- Returns:
362
- Resampled PNG bytes.
363
- Raises:
364
- ImagegenError: If the source is invalid or has a mismatched aspect ratio.
365
- """
366
- require_provider_byte_limit(image_bytes)
367
- try:
368
- with Image.open(BytesIO(image_bytes)) as source_image:
369
- require_png_within_limits(source_image)
370
- source_image.load()
371
- if source_image.width != source_image.height:
372
- raise ImagegenError("provider image must be square")
373
- resized_image = source_image.resize((target_size.width, target_size.height), Image.Resampling.LANCZOS)
374
- destination = BytesIO()
375
- resized_image.save(destination, format=PNG_FORMAT)
376
- return destination.getvalue()
377
- except ImagegenError:
378
- raise
379
- except (OSError, TypeError, ValueError, Image.DecompressionBombError) as error:
380
- raise ImagegenError("image resize failed") from error
381
-
382
-
383
- def build_receipt(prompt: str, backend: str, artifact: ProviderArtifact, requested_size: ImageSize, final_bytes: bytes) -> dict[str, object]:
384
- """Build receipt fields from observed provider and final bytes.
385
-
386
- Args:
387
- prompt: Image-generation prompt.
388
- backend: Selected provider backend.
389
- artifact: Verified provider artifact.
390
- requested_size: Requested final dimensions.
391
- final_bytes: Verified final PNG bytes.
392
- Returns:
393
- JSON-compatible receipt fields.
394
- Raises:
395
- ImagegenError: If final bytes do not match the requested dimensions.
396
- """
397
- final_size = decode_image(final_bytes)
398
- if final_size != requested_size:
399
- raise ImagegenError("final image dimensions do not match requested size")
400
- transformation = "native" if artifact.source_size == requested_size else "resized"
401
- transformations = [] if transformation == "native" else ["resize:lanczos"]
402
- provider_size = artifact.provider_requested_size.as_text() if artifact.provider_requested_size else None
403
- source_digest = hashlib.sha256(artifact.image_bytes).hexdigest()
404
- final_digest = source_digest if transformation == "native" else hashlib.sha256(final_bytes).hexdigest()
405
- return {"prompt_sha256": hashlib.sha256(prompt.encode()).hexdigest(), "backend": backend, "model_or_tool": artifact.model_or_tool, "requested_size": requested_size.as_text(), "provider_size": provider_size, "source_size": artifact.source_size.as_text(), "final_size": final_size.as_text(), "transformation": transformation, "transformations": transformations, "source_sha256": source_digest, "final_sha256": final_digest, "credential_source": artifact.credential_source}
406
-
407
-
408
- def require_png_destination(destination_path: Path) -> None:
409
- """Require a destination path that ends with ``.png``.
410
-
411
- Args:
412
- destination_path: Candidate final image path.
413
- Raises:
414
- ImagegenError: If the path does not use a ``.png`` suffix.
415
- """
416
- if destination_path.suffix.casefold() != PNG_SUFFIX:
417
- raise ImagegenError("destination must use a .png suffix")
418
-
419
-
420
- def publish_artifact(destination_path: Path, image_bytes: bytes, all_receipt: dict[str, object], should_overwrite: bool) -> None:
421
- """Publish image and receipt atomically after overwrite checks.
422
-
423
- Args:
424
- destination_path: Final PNG path.
425
- image_bytes: Verified final PNG bytes.
426
- all_receipt: Receipt fields describing the published bytes.
427
- should_overwrite: Whether existing destinations may be replaced.
428
- Raises:
429
- ImagegenError: If publication cannot proceed safely.
430
- """
431
- require_png_destination(destination_path)
432
- receipt_path = destination_path.with_suffix(RECEIPT_SUFFIX)
433
- if receipt_path == destination_path:
434
- raise ImagegenError("destination and receipt paths must differ")
435
- if not should_overwrite and (destination_path.exists() or receipt_path.exists()):
436
- raise ImagegenError("output or receipt already exists; use --overwrite")
437
- destination_path.parent.mkdir(parents=True, exist_ok=True)
438
- previous_image = destination_path.read_bytes() if destination_path.exists() else None
439
- previous_receipt = receipt_path.read_bytes() if receipt_path.exists() else None
440
- temporary_paths: list[Path] = []
441
- was_image_replaced = False
442
- was_receipt_replaced = False
443
- try:
444
- for each_destination, each_content in ((destination_path, image_bytes), (receipt_path, json.dumps(all_receipt, indent=JSON_INDENT_LEVEL).encode())):
445
- temporary_path = each_destination.with_name(f".{each_destination.name}.{secrets.token_hex(TEMPORARY_FILE_TOKEN_BYTES)}.tmp")
446
- temporary_paths.append(temporary_path)
447
- temporary_path.write_bytes(each_content)
448
- os.replace(temporary_paths[0], destination_path)
449
- was_image_replaced = True
450
- os.replace(temporary_paths[1], receipt_path)
451
- was_receipt_replaced = True
452
- except (OSError, TypeError, ValueError) as error:
453
- if was_receipt_replaced:
454
- _restore_path(receipt_path, previous_receipt)
455
- if was_image_replaced:
456
- _restore_path(destination_path, previous_image)
457
- raise ImagegenError("artifact publication failed") from error
458
- finally:
459
- for each_temporary_path in temporary_paths:
460
- try:
461
- each_temporary_path.unlink(missing_ok=True)
462
- except OSError:
463
- pass
464
-
465
-
466
- def _restore_path(path: Path, previous_bytes: bytes | None) -> None:
467
- """Restore one path during publication rollback."""
468
- try:
469
- if previous_bytes is None:
470
- path.unlink(missing_ok=True)
471
- else:
472
- path.write_bytes(previous_bytes)
473
- except OSError:
474
- pass
475
-
476
-
477
- def generate_image(prompt: str, backend: str, requested_size: ImageSize, destination_path: Path, resize_policy: str, should_overwrite: bool, transport: Transport | None = None, runner: Runner | None = None) -> dict[str, object]:
478
- """Generate, verify, optionally resize, and publish one image.
479
-
480
- Args:
481
- prompt: Image-generation prompt.
482
- backend: ``openai-api`` or ``codex-oauth``.
483
- requested_size: Final artifact dimensions.
484
- destination_path: Final PNG path.
485
- resize_policy: ``forbid`` or ``allow``.
486
- should_overwrite: Whether existing destinations may be replaced.
487
- transport: Optional injected OpenAI transport.
488
- runner: Optional injected Codex runner.
489
- Returns:
490
- Published receipt fields.
491
- Raises:
492
- ImagegenError: If provider, validation, transformation, or publication fails.
493
- """
494
- validate_final_size(requested_size)
495
- require_png_destination(destination_path)
496
- if backend not in ALL_SUPPORTED_BACKENDS or resize_policy not in ALL_SUPPORTED_RESIZE_POLICIES:
497
- raise ImagegenError("unsupported backend or resize policy")
498
- if backend == "openai-api":
499
- provider_size = select_provider_size(requested_size)
500
- if provider_size != requested_size and resize_policy == "forbid":
501
- raise ImagegenError("requested size is not supported natively; use --resize-policy allow")
502
- artifact = request_openai_image(prompt, provider_size, transport or build_openai_transport())
503
- else:
504
- artifact = request_oauth_image(prompt, runner or run_codex)
505
- if artifact.source_size != requested_size and resize_policy == "forbid":
506
- raise ImagegenError("provider dimensions differ from requested size")
507
- final_bytes = artifact.image_bytes if artifact.source_size == requested_size else resize_image(artifact.image_bytes, requested_size)
508
- receipt = build_receipt(prompt, backend, artifact, requested_size, final_bytes)
509
- publish_artifact(destination_path, final_bytes, receipt, should_overwrite)
510
- return receipt
511
-
512
-
513
- def run_codex(all_arguments: Sequence[str], work_directory: Path, all_environment: Mapping[str, str]) -> None:
514
- """Invoke Codex without exposing credentials in arguments.
515
-
516
- Args:
517
- all_arguments: Executable and arguments for the Codex process.
518
- work_directory: Isolated temporary working directory.
519
- all_environment: Sanitized child environment.
520
- Raises:
521
- ImagegenError: If Codex cannot complete within the bounded process contract.
522
- """
523
- try:
524
- completed_process = subprocess.run(all_arguments, cwd=work_directory, env=all_environment, shell=False, check=True, capture_output=True, text=False, timeout=CODEX_TIMEOUT_SECONDS)
525
- if len(completed_process.stdout) > MAXIMUM_CODEX_OUTPUT_BYTES or len(completed_process.stderr) > MAXIMUM_CODEX_OUTPUT_BYTES:
526
- raise ImagegenError("Codex output exceeds the limit")
527
- except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired) as error:
528
- raise ImagegenError("Codex image generation failed") from error