hanoman 0.9.38 → 0.9.40

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 (34) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli.js +95 -4
  3. package/dist/server.js +1461 -451
  4. package/docs/agent-integration.md +6 -0
  5. package/package.json +2 -1
  6. package/prisma/migrations/20261101090000_runtime_account/migration.sql +18 -0
  7. package/prisma/schema.prisma +19 -0
  8. package/skills/codex-image/SKILL.md +225 -0
  9. package/skills/codex-image/references/sprite-sheets.md +60 -0
  10. package/skills/codex-image/scripts/codex-image.sh +253 -0
  11. package/skills/codex-image/scripts/normalize-set.py +104 -0
  12. package/web/assets/{DalangHanomanScreen-wpAO0xvb.js → DalangHanomanScreen-WQ2B2G0F.js} +1 -1
  13. package/web/assets/{DocPreviewModal-CbhkDUJ2.js → DocPreviewModal-DkuSjeL9.js} +1 -1
  14. package/web/assets/{DocsWorkspace-CcB0LGFV.js → DocsWorkspace-BJ71xmQ_.js} +1 -1
  15. package/web/assets/{IdeScreen-DI4v3ZkU.js → IdeScreen-Bc4gR6md.js} +1 -1
  16. package/web/assets/{LeadScreen-BdS_xX_f.js → LeadScreen-BmRdOMoJ.js} +1 -1
  17. package/web/assets/{ReviewScreen-Ddw5U2sM.js → ReviewScreen-C83FuzRB.js} +1 -1
  18. package/web/assets/{SchedulerScreen-D6LGVrhM.js → SchedulerScreen-BKnqWV7j.js} +1 -1
  19. package/web/assets/{SettingsScreen-DQZ3K43g.css → SettingsScreen-B7CruwHq.css} +1 -1
  20. package/web/assets/SettingsScreen-FrgI5pzO.js +62 -0
  21. package/web/assets/{TeamScreen-BkxPDI5P.js → TeamScreen-Dgrvomt0.js} +1 -1
  22. package/web/assets/TerminalScreen-D2KSQ13m.js +2 -0
  23. package/web/assets/{TriageScreen-B-HdZsEt.js → TriageScreen-DrPERvDs.js} +1 -1
  24. package/web/assets/{VpsScreen-CWp29NL_.js → VpsScreen-7sDyg5MI.js} +2 -2
  25. package/web/assets/fields-BNhNDKOP.js +1 -0
  26. package/web/assets/{index-B4b6i2tH.css → index-B0VYqeq_.css} +1 -1
  27. package/web/assets/{index-C4Py9MJV.js → index-NoTUDPfb.js} +212 -212
  28. package/web/assets/{live-CBa_7-hn.js → live-B4ZwQ4kB.js} +1 -1
  29. package/web/assets/{sched-lead-shared-CQIoYIzG.js → sched-lead-shared-CZ0_L34F.js} +1 -1
  30. package/web/assets/{settings-rebase-C3611gs8.js → settings-rebase-DwTIHvhF.js} +1 -1
  31. package/web/index.html +2 -2
  32. package/web/assets/SettingsScreen-CFNcg1T8.js +0 -62
  33. package/web/assets/TerminalScreen-CEQFK9Rc.js +0 -2
  34. package/web/assets/fields-BY7SYerd.js +0 -1
@@ -318,6 +318,12 @@ Tool MCP-nya (semua tulis bermode `danger`, hanya tampil dengan `--danger`):
318
318
  | `GET /api/lead/decisions` | `lead:read` | jejak keputusan hanoman-lead. Menerima `page`/`limit`; `take`/`skip` lama tetap jalan. |
319
319
  | `GET /api/scheduler/queue` | `settings:read` | antrean scheduler (`?status=queued\|launched\|done\|failed`). `GET /api/scheduler/state` **tak lagi** memuat `queue` — ia memuat `queueCounts`. |
320
320
  | `POST /api/lead/decisions` | `lead:write` | minta putusan — baca **§8** dan **§11** dulu. |
321
+ | `GET /api/runtime-accounts` | `settings:read` | akun runtime per mesin (tool `hanoman_runtime_accounts_list`). Memuat email. |
322
+ | `PUT /api/runtime-accounts/default` | `settings:write` | default akun per runtime (tool `hanoman_runtime_account_default_set`). |
323
+ | `GET /api/limits/accounts` | — (GLOBAL_READ) | limit per akun, tanpa email (tool `hanoman_limits_accounts`). |
324
+
325
+ Akun runtime (ADR-0234): `hanoman_session_create` menerima `account` (id dari `hanoman_runtime_accounts_list`;
326
+ kosong = akun default runtime). Tambah/hapus/login akun hanya lewat dashboard (COOKIE_ONLY).
321
327
 
322
328
  Settings memakai tool existing `hanoman_settings_get`/`hanoman_settings_set` dan capability
323
329
  `settings:read|write` untuk runtime Antigravity (SPEC-1442/ADR-0221). Panggil `_get` tepat sebelum
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hanoman",
3
- "version": "0.9.38",
3
+ "version": "0.9.40",
4
4
  "description": "Orchestrator + dashboard workflow docs-driven untuk sesi Claude Code / Codex",
5
5
  "type": "module",
6
6
  "repository": {
@@ -24,6 +24,7 @@
24
24
  "prisma",
25
25
  "docs",
26
26
  "scripts",
27
+ "skills",
27
28
  "README.md",
28
29
  "LICENSE"
29
30
  ],
@@ -0,0 +1,18 @@
1
+ -- AlterTable
2
+ ALTER TABLE "SessionHistory" ADD COLUMN "accountId" TEXT;
3
+
4
+ -- CreateTable
5
+ CREATE TABLE "RuntimeAccount" (
6
+ "id" TEXT NOT NULL PRIMARY KEY,
7
+ "runtime" TEXT NOT NULL,
8
+ "label" TEXT NOT NULL,
9
+ "homeDir" TEXT NOT NULL,
10
+ "identity" JSONB,
11
+ "status" TEXT NOT NULL DEFAULT 'belum-login',
12
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
13
+ "lastLoginAt" DATETIME
14
+ );
15
+
16
+ -- CreateIndex
17
+ CREATE UNIQUE INDEX "RuntimeAccount_runtime_label_key" ON "RuntimeAccount"("runtime", "label");
18
+
@@ -478,6 +478,9 @@ model SessionHistory {
478
478
  reconciledAt DateTime?
479
479
  transcriptKey String?
480
480
  transcriptBytes Int?
481
+ // ADR-0234 · akun runtime saat sesi lahir (`RuntimeAccount.id` atau `default:<runtime>`). Tanpa FK:
482
+ // riwayat bertahan sesudah akunnya dihapus. null = baris lama / akun bawaan.
483
+ accountId String?
481
484
  createdAt DateTime @default(now())
482
485
  updatedAt DateTime @updatedAt
483
486
 
@@ -798,6 +801,22 @@ model RuntimeConfig {
798
801
  updatedAt DateTime @updatedAt
799
802
  }
800
803
 
804
+ // ADR-0234 · akun langganan runtime (claude/codex/antigravity) di mesin ini. LOCAL-only: tak masuk
805
+ // SYNCED/FIELDS sync. Kredensial TIDAK di sini — ia hidup di `homeDir` (atau Keychain macOS).
806
+ // Akun bawaan tak punya baris: id tetap `default:<runtime>`.
807
+ model RuntimeAccount {
808
+ id String @id
809
+ runtime String
810
+ label String
811
+ homeDir String
812
+ identity Json?
813
+ status String @default("belum-login")
814
+ createdAt DateTime @default(now())
815
+ lastLoginAt DateTime?
816
+
817
+ @@unique([runtime, label])
818
+ }
819
+
801
820
  // SPEC-476 · ADR-0096 · seluruh state gateway Telegram bersifat LOCAL-only. Telegram adalah kanal
802
821
  // menuju tmux di mesin ini, jadi offset/binding/memory/outbox tidak punya arti untuk sync antar-node.
803
822
  // Tujuh model ini sengaja tanpa FK: referensi project/spec/session dapat hilang sementara audit dan
@@ -0,0 +1,225 @@
1
+ ---
2
+ name: codex-image
3
+ description: Use when a task needs a raster image that does not exist yet — mascot or character art, illustrations, hero and banner images, sprite sheets, textures, product or UI mockups, concept art, transparent-background cutouts — or when an existing PNG/JPG needs an AI edit (background removal, object swap, style transfer, relighting). Also use when you are about to hand-write SVG, CSS shapes, or an emoji placeholder to stand in for artwork you cannot actually draw, or when asked to "generate an image", "buatkan ilustrasi", "bikin gambar".
4
+ ---
5
+
6
+ # Generating images with Codex
7
+
8
+ ## Overview
9
+
10
+ You cannot render bitmaps. Codex can: its built-in `image_gen` tool (GPT Image) needs no
11
+ `OPENAI_API_KEY` — it rides the user's ChatGPT login. So image generation is **delegated**,
12
+ not simulated.
13
+
14
+ **Core principle: a request for artwork is a request for pixels.** A hand-written SVG of a
15
+ fox is not a fox illustration. If the deliverable is a picture, spawn Codex and return a
16
+ verified PNG file path.
17
+
18
+ ## Pick the engine first
19
+
20
+ This skill ships with hanoman (bundled in the npm package, seeded into
21
+ `$HANOMAN_HOME/skills/codex-image`, injected into every agent session). Which engine you use
22
+ depends on the runtime you are:
23
+
24
+ | You are | Engine | How |
25
+ |---|---|---|
26
+ | **Codex** (you have the built-in `image_gen` tool) | `image_gen` directly, via Codex's own `imagegen` skill | Do **not** shell out to `codex exec` from inside Codex. Generate, then copy the chosen file from `$CODEX_HOME/generated_images/...` (default `~/.codex/generated_images/...`) to the destination path. |
27
+ | **Anything else** (Claude Code, Antigravity, other CLIs) | `scripts/codex-image.sh` (spawns `codex exec`) | Everything below. Needs the `codex` CLI installed and logged in (`codex login`) on this machine. |
28
+
29
+ The spec format, sizes, verification rules and the "no substitute" rule below apply to both.
30
+
31
+ ## When to use
32
+
33
+ Delegate to Codex when the deliverable is a **bitmap that has to be drawn**:
34
+
35
+ - mascots, characters, avatars, sprite sheets
36
+ - illustrations, hero/banner/OG images, backgrounds, textures
37
+ - photorealistic product shots, packaging and merch mockups
38
+ - concept art, storyboards, book/comic panels
39
+ - transparent-background cutouts and object isolation
40
+ - editing an existing raster: remove/replace an object, change lighting or season,
41
+ swap a background, apply a reference style, sketch → render
42
+
43
+ ## When NOT to use
44
+
45
+ Reach for code, not Codex, when the output is **structural rather than drawn**:
46
+
47
+ | Task | Do this instead |
48
+ |---|---|
49
+ | Charts, plots, dashboards | Load the `dataviz` skill and write chart code |
50
+ | Flowcharts, architecture diagrams | Inline SVG or mermaid |
51
+ | Icons matching an existing SVG icon set | Edit the vector set directly |
52
+ | Logo systems already defined in vector | Edit the source vector |
53
+ | UI built from real components | Build it in HTML/CSS |
54
+ | Screenshot of something that exists | Run it and capture it |
55
+
56
+ Generated bitmaps cannot be diffed, recolored by token, or scaled cleanly. If the asset
57
+ belongs to a design system, that is a reason to draw it in code.
58
+
59
+ ## Quick reference
60
+
61
+ ```bash
62
+ S="${HANOMAN_HOME:-$HOME/.hanoman}/skills/codex-image/scripts/codex-image.sh"
63
+
64
+ # one asset
65
+ "$S" --out assets/mascot.png --size 1024x1024 --transparent --fit <<'SPEC'
66
+ Use case: illustration-story
67
+ Asset type: dashboard empty-state mascot
68
+ Primary request: a small orange fox sitting, waving one paw
69
+ Style/medium: flat vector-style illustration, thick clean outlines
70
+ Composition/framing: full body, centered, generous margin
71
+ Color palette: warm orange, cream, one teal accent
72
+ Constraints: no text, no watermark, no drop shadow
73
+ SPEC
74
+
75
+ # several assets, with references
76
+ "$S" --out-dir assets/icons --name icon --count 4 --ref brand/palette.png <<'SPEC'
77
+ ...
78
+ SPEC
79
+ ```
80
+
81
+ Run `"$S" --help` for every flag.
82
+
83
+ **Budget 1–3 minutes per image.** A default shell call times out at two minutes, so run the
84
+ script as a background command and pick it up from the notification — or raise the timeout past
85
+ `--count x 180s`. Three concurrent jobs are safe; use them for independent assets rather than
86
+ raising `--count`, so one bad asset does not cost you the batch.
87
+
88
+ ## hanoman `Mockup` phase
89
+
90
+ In hanoman's `Mockup` phase (ADR-0233) the deliverable is a UI mockup that a human approves before
91
+ planning. One image per changed screen or state, at most 6, saved as
92
+ `docs/mockups/<spec-id>/<NN>-<slug>.png`:
93
+
94
+ ```bash
95
+ "$S" --out docs/mockups/spec-123/01-backlog-detail.png --size 1536x1024 --fit <<'SPEC'
96
+ Use case: ui-mockup
97
+ Asset type: product UI mockup for design approval (desktop, 1536x1024)
98
+ Primary request: backlog detail screen with a new "Mockup" section showing three thumbnails
99
+ Style/medium: crisp flat product UI screenshot, real-looking components, no device frame
100
+ Composition/framing: full screen, left sidebar, content column, generous whitespace
101
+ Color palette: <the project's design-system tokens, e.g. bone paper #F4EFE6, ink #1F1B16, brass #B08A3E>
102
+ Text (verbatim): "Mockup", "Lihat mockup (3)"
103
+ Constraints: follow the existing layout of the screen; only the new section changes
104
+ Avoid: lorem ipsum, watermark, browser chrome, illegible text
105
+ SPEC
106
+ ```
107
+
108
+ Add `--size 1024x1536` mobile variants when the change touches a responsive layout. A missing
109
+ `codex` or a failed login is a blocker to report and ask about — never a reason to hand-draw.
110
+
111
+ ## Writing the spec
112
+
113
+ Codex's `imagegen` skill expects a labeled spec. Fill only the lines that carry information —
114
+ an unused line is noise, and a vague line ("nice colors") is worse than an absent one.
115
+
116
+ ```text
117
+ Use case: <photorealistic-natural | product-mockup | ui-mockup | infographic-diagram |
118
+ ads-marketing | logo-brand | illustration-story | stylized-concept |
119
+ background-extraction | precise-object-edit | style-transfer | compositing>
120
+ Asset type: <where this will actually be used>
121
+ Primary request: <the subject, in one sentence>
122
+ Scene/backdrop: <environment>
123
+ Style/medium: <flat illustration | studio photo | 3D render | watercolor | ...>
124
+ Composition/framing: <wide/close/top-down, placement, margins>
125
+ Lighting/mood: <lighting + mood>
126
+ Color palette: <named colors or hex>
127
+ Text (verbatim): "<exact string, spelled out letter by letter if tricky>"
128
+ Constraints: <what must be kept>
129
+ Avoid: <what must not appear>
130
+ ```
131
+
132
+ **Editing an existing image:** pass it with `--ref`, set an edit `Use case`, and state the
133
+ invariants explicitly — `change only the background; keep the product, its edges and its
134
+ colors unchanged`. Repeat those invariants on every retry; they drift otherwise.
135
+
136
+ ## Sizes
137
+
138
+ `gpt-image-2` accepts `auto` or `WIDTHxHEIGHT` where both edges are multiples of 16, the
139
+ long edge is ≤ 3840, the ratio is ≤ 3:1, and total pixels fall between 655,360 and 8,294,400.
140
+
141
+ | Size | Use |
142
+ |---|---|
143
+ | `1024x1024` | square default, fastest |
144
+ | `1536x1024` / `1024x1536` | landscape / portrait |
145
+ | `2048x1152` | 2K landscape, hero images |
146
+ | `3840x2160` / `2160x3840` | 4K |
147
+
148
+ `image_gen` reads this as guidance, not a contract, and will return a nearby size. Pass `--fit`
149
+ whenever the layout depends on exact pixels.
150
+
151
+ ## Verifying the result
152
+
153
+ The script already does this, and its exit code is the honest signal:
154
+
155
+ | Exit | Meaning | Next step |
156
+ |---|---|---|
157
+ | 0 | every requested PNG exists and `file` confirms it | report the paths |
158
+ | 2 | usage error | fix the flags |
159
+ | 3 | Codex turn failed | read the reason; if auth, run `codex login` |
160
+ | 4 | Codex produced no image | retry with a more concrete spec |
161
+ | 5 | timeout | raise `--timeout`, or lower `--count` |
162
+ | 6 | `--transparent` asked, opaque result | the file is kept — look at it, then retry with `no checkerboard` in `Avoid` |
163
+
164
+ Never report an image as delivered without an `OK <path>` line. `ls` the file if you want a
165
+ second opinion — but do not describe what the picture looks like unless you read it with the
166
+ image-reading tool, and say so when you have not looked.
167
+
168
+ ## When generation fails
169
+
170
+ **A substitute is not the asset.** If Codex cannot generate the image, deliver **no file** and
171
+ report the blocker. Do not hand-author an SVG, CSS shape, or emoji composite, rasterise it, and
172
+ save it under the filename that was asked for — the caller cannot tell the difference from the
173
+ file, and the next person to touch it inherits a fake.
174
+
175
+ Measured baseline: an agent given a 6-minute budget hit the auth wall, wrote fox-shaped SVG
176
+ paths by hand, rasterised them to `mascot.png`, and shipped it. Its reasoning: *"the 6-minute
177
+ budget doesn't tolerate betting on a single path."*
178
+
179
+ | Rationalization | Reality |
180
+ |---|---|
181
+ | "I'll build a fallback in parallel to be safe" | The fallback becomes the deliverable. Fix the blocker instead; it is usually one `codex login`. |
182
+ | "It's still a real raster PNG with real alpha" | The file format was never the requirement. The artwork was. |
183
+ | "I disclosed it in my report" | The report is thrown away; the file is committed. Filename `mascot.png` claims to be the mascot. |
184
+ | "The deadline doesn't allow a retry" | A missing asset is a known gap. A hand-drawn stand-in is an unknown one. Report the gap. |
185
+ | "Close enough to unblock the layout" | Then name it `mascot.placeholder.svg` and say it is a placeholder — never the requested filename. |
186
+
187
+ Blocked on auth? Say so plainly and give the user the one command: `codex login`.
188
+
189
+ ## Common mistakes
190
+
191
+ | Mistake | Reality |
192
+ |---|---|
193
+ | Trusting `codex exec`'s exit code | It exits **0** on a failed turn. Only the JSONL event stream and the file on disk tell the truth — the script checks both. |
194
+ | Trusting `codex login status` | It reports "Logged in using ChatGPT" while the refresh token is revoked. A `turn.failed` mentioning `revoked` / 401 is the real answer: run `codex login`. |
195
+ | Letting Codex post-process | Its imagegen skill will happily run `remove_chroma_key.py --despill`, which **cuts red out of reds** — gold goes olive, red cloth goes dull. The script forbids it; do your own keying downstream. |
196
+ | Asking for "16-bit pixel art" | You get a high-resolution illustration that merely looks pixelated. Real pixels only come from a nearest-neighbour downscale afterwards. |
197
+ | Feeding a `.webp` reference | `--ref` takes PNG/JPG. Convert first: `dwebp in.webp -o out.png`. |
198
+ | Generating into the repo blindly | Raw outputs also land in `~/.codex/generated_images/<thread-id>/`. Put deliverables where the project expects them, and add large raws to `.gitignore`. |
199
+ | One prompt, `n` variants, many assets | Distinct assets need distinct calls. `--count` issues one `image_gen` call per file. |
200
+ | Correcting a retry with just the fix | A partial correction makes the model forget the constraints it already satisfied. Restate the **whole** spec plus the new constraint. |
201
+ | Assuming `--size` is honoured | `image_gen` treats size as a **hint**. Measured: `1024x1024` requested, `1254x1254` returned. The script prints a NOTE on any mismatch; pass `--fit` when the pixel dimensions are a real requirement. |
202
+ | Polling the destination while the job runs | The script copies into place only at the very end. An empty destination mid-run is not a failure — wait for the `OK <path>` line and the exit code. |
203
+ | Combining `--ref` with `--transparent` | A reference image routes the request through the **edit** path, which flattens alpha. Measured on one spec: 0.0% clear pixels with a reference (twice), ~59% without (twice). Anti-checkerboard wording does not rescue it. For a transparent asset, anchor style in **words**, not an attachment — and expect line weight to drift, so state it numerically ("uniform charcoal contour ~8px at 1024"). |
204
+ | Believing `--transparent` produced transparency | The model can **paint the checkerboard** — the visual convention for transparency — as opaque pixels. Measured: a retry came back 0.0% transparent while the first attempt was 58.9%, same flag, valid PNG, exit 0. The script now gates this (exit 6); always put `no checkerboard, no fake transparency grid` in `Avoid`. |
205
+ | Expecting a batch to be a matched *set* | One job gives you consistent style but not consistent **optical scale**. Measured on a 4-icon set: alpha bbox filled 81%, 82%, 95%, 85% of the frame. Normalize in post with `scripts/normalize-set.py` rather than regenerating. |
206
+ | Assuming the palette line is binding | It is a strong hint, not a constraint. In the same set one icon came back with red and blue ruled lines (1.0% off-palette pixels against 0.0% for the other three). Measure hue drift instead of eyeballing it. |
207
+ | Editing the script while a job is running | Bash reads a script incrementally from disk; patching it mid-run shifts the offsets and kills the job with a bogus syntax error. Finish the run first. |
208
+
209
+ ## Finishing an icon or asset set
210
+
211
+ One job gives a set consistent style but not consistent size. Even out the optical scale
212
+ deterministically instead of burning generations on it:
213
+
214
+ ```bash
215
+ python3 "${HANOMAN_HOME:-$HOME/.hanoman}/skills/codex-image/scripts/normalize-set.py" \
216
+ assets/icons/icon-*.png --dry-run # see the scale factors first
217
+ ```
218
+
219
+ It crops each icon to its alpha bounding box, rescales so the set matches on one metric, and
220
+ re-pads to the original canvas. `--metric bbox` (default) makes every icon fill the same box;
221
+ `--metric area` suits a set where one subject is mostly empty space. Look at a contact sheet
222
+ over a mid-grey ground before committing — transparency hides scale problems on white.
223
+
224
+ For sprite sheets and animation strips, read `references/sprite-sheets.md` — the grid, tail,
225
+ and registration traps there cost days to rediscover.
@@ -0,0 +1,60 @@
1
+ # Sprite sheets and animation strips
2
+
3
+ Generating an animated character is not "generate 8 images". It is one sheet, then a
4
+ cutting-and-registration pipeline you own. These traps were each paid for once.
5
+
6
+ ## Generating the sheet
7
+
8
+ Ask for **one sheet** (e.g. `1536x1024`, one row of 8 frames) rather than 8 separate images —
9
+ separate calls give you 8 different characters. Then:
10
+
11
+ - **Ask for the raw sheet only.** Codex's imagegen skill will otherwise run
12
+ `remove_chroma_key.py --soft-matte --despill` on its own. Despill against magenta **subtracts
13
+ red from red pixels**: gold turns olive, red cloth turns dull. Do the keying yourself.
14
+ - **Key on green `#00FF00`, not magenta**, for the same reason.
15
+ - Keep the standing character well under the cell height (a 192px character in a 192px cell
16
+ spills its tail into the neighbour; ~168px inside 192px is safe) and clip per cell. Treat
17
+ spill as a gate failure, not a rounding error.
18
+ - Floating ornaments (stars, sparkles) must **touch the character silhouette**. A detached blob
19
+ smaller than ~40×80px and ≥8 columns away gets dropped silently by blob detection.
20
+ - "16-bit pixel art" in the prompt yields a high-resolution illustration that looks pixelated.
21
+ Real pixels come from `resize(NEAREST)` to the logical size afterwards.
22
+
23
+ ## Cutting
24
+
25
+ **The model does not obey your grid.** Bounding boxes drift up to ~97px along a row and sprites
26
+ cross cell borders. Detect sprites from transparent gutters, never by slicing at fixed offsets.
27
+
28
+ ## Registration
29
+
30
+ Anchoring on the feet alone jitters ±15px, because the tail tip enters the foot bounding box.
31
+
32
+ - **Find feet by mass, not by side.** In the bottom band, a grazing tail is a thin line
33
+ (~80–100px of mass) next to real feet (~300–600px). A "rightmost run" rule is exactly wrong
34
+ for a walk-left row drawn with the tail on the right, and mid-stride feet are two runs.
35
+ - Register with an XOR-mask over the static region (rows ≥45%, columns ≥ feet−12) plus a ±8%
36
+ scale search. That gets residual to 0.01–0.07; the remainder is shape difference.
37
+ - **Pinning the static region freezes a rectangle** and will amputate a hand that moves through
38
+ it. Plain erosion cuts the arm while the palm survives — the hand reads as severed. Use
39
+ morphological reconstruction: grow the eroded seed inside the difference map until stable.
40
+ - Do not normalise the frame-1 bbox to a fixed height — anything that lengthens the bbox (a
41
+ dangling tail) silently shrinks the character. Gate on `body_ratio` (eroded bbox height ÷ full
42
+ bbox height); healthy rows sit at 0.84–0.91. For curled poses gate on body **mass** instead.
43
+
44
+ ## Reviewing
45
+
46
+ - Onion-skin (mean alpha across frames, pixel ratio 0.2–0.8) is the cheapest jitter measure.
47
+ - A QA GIF with a **per-frame adaptive palette** shifts colours every frame and reads as a
48
+ flicker that is not in the strip. Use one global palette.
49
+ - The **8→1 loop seam is invisible** in both contact sheets and onion-skins. Gate it as a
50
+ ratio of largest to smallest inter-frame step (threshold ~3.5), not an absolute value, and
51
+ guard it with "visible motion ≥ 0.10" — otherwise a barely-moving row passes vacuously.
52
+ - Calibrate residual gates across all rows, not on the idle row. A gate tuned to idle punishes
53
+ correct animation.
54
+
55
+ ## Retrying
56
+
57
+ Restate the **entire** constraint set on every retry, including the constraints already
58
+ satisfied. A partial correction makes the model forget what it had got right.
59
+
60
+ Cost of record: 1–3 attempts per row; ~1–3 minutes per sheet; 3 concurrent jobs are safe.
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env bash
2
+ # codex-image.sh — spawn Codex to generate raster images, land verified PNGs on disk.
3
+ # Codex's built-in image_gen tool needs no OPENAI_API_KEY; it uses the ChatGPT login.
4
+ set -euo pipefail
5
+
6
+ usage() {
7
+ cat <<'USAGE'
8
+ codex-image.sh — generate PNG assets via Codex's built-in image_gen tool.
9
+
10
+ Usage:
11
+ codex-image.sh --out FILE.png [options] < spec.txt
12
+ codex-image.sh --out-dir DIR --count N [options] < spec.txt
13
+
14
+ The image spec is read from stdin (use a heredoc or a file).
15
+
16
+ Options:
17
+ -o, --out FILE Destination PNG for a single asset. Implies --count 1.
18
+ --out-dir DIR Destination directory when generating several assets.
19
+ --name PREFIX Filename prefix under --out-dir (default: asset).
20
+ -n, --count N Number of distinct assets to generate (default: 1, max 8).
21
+ -r, --ref FILE Reference image passed to Codex (repeatable, PNG/JPG).
22
+ -s, --size WxH Pixel size (default: 1024x1024). Both edges multiples of 16,
23
+ max edge 3840, ratio <= 3:1, 655360..8294400 total pixels.
24
+ -t, --transparent Ask for a genuinely transparent background.
25
+ --fit Resize the result to exactly --size (image_gen treats size as a hint).
26
+ --raw Harvest from ~/.codex/generated_images instead of Codex's copies.
27
+ --effort LEVEL Codex reasoning effort: low|medium|high (default: low).
28
+ --timeout SEC Hard timeout (default: 900).
29
+ --keep Keep the staging directory for debugging.
30
+ -h, --help Show this help.
31
+
32
+ Exit codes:
33
+ 0 success (every requested PNG exists and is a real PNG)
34
+ 2 usage error
35
+ 3 Codex turn failed (see the printed reason)
36
+ 4 Codex produced no image
37
+ 5 timeout
38
+ 6 --transparent was asked for but the result is opaque (file kept for inspection)
39
+ USAGE
40
+ }
41
+
42
+ OUT_FILE=""; OUT_DIR=""; NAME="asset"; COUNT=1; SIZE="1024x1024"
43
+ TRANSPARENT=0; RAW=0; EFFORT="low"; TIMEOUT=900; KEEP=0; FIT=0
44
+ REFS=()
45
+
46
+ while [[ $# -gt 0 ]]; do
47
+ case "$1" in
48
+ -o|--out) OUT_FILE="$2"; shift 2 ;;
49
+ --out-dir) OUT_DIR="$2"; shift 2 ;;
50
+ --name) NAME="$2"; shift 2 ;;
51
+ -n|--count) COUNT="$2"; shift 2 ;;
52
+ -r|--ref) REFS+=("$2"); shift 2 ;;
53
+ -s|--size) SIZE="$2"; shift 2 ;;
54
+ -t|--transparent) TRANSPARENT=1; shift ;;
55
+ --fit) FIT=1; shift ;;
56
+ --raw) RAW=1; shift ;;
57
+ --effort) EFFORT="$2"; shift 2 ;;
58
+ --timeout) TIMEOUT="$2"; shift 2 ;;
59
+ --keep) KEEP=1; shift ;;
60
+ -h|--help) usage; exit 0 ;;
61
+ *) echo "codex-image: unknown option: $1" >&2; usage >&2; exit 2 ;;
62
+ esac
63
+ done
64
+
65
+ command -v codex >/dev/null 2>&1 || { echo "codex-image: codex CLI not found on PATH" >&2; exit 2; }
66
+
67
+ if [[ -n "$OUT_FILE" ]]; then
68
+ [[ -n "$OUT_DIR" ]] && { echo "codex-image: use --out or --out-dir, not both" >&2; exit 2; }
69
+ COUNT=1
70
+ OUT_DIR="$(cd "$(dirname "$OUT_FILE")" 2>/dev/null && pwd)" || {
71
+ echo "codex-image: destination directory does not exist: $(dirname "$OUT_FILE")" >&2; exit 2; }
72
+ elif [[ -n "$OUT_DIR" ]]; then
73
+ mkdir -p "$OUT_DIR"
74
+ OUT_DIR="$(cd "$OUT_DIR" && pwd)"
75
+ else
76
+ echo "codex-image: --out or --out-dir is required" >&2; usage >&2; exit 2
77
+ fi
78
+
79
+ [[ "$COUNT" =~ ^[1-8]$ ]] || { echo "codex-image: --count must be 1..8" >&2; exit 2; }
80
+ [[ "$SIZE" =~ ^[0-9]+x[0-9]+$ ]] || { echo "codex-image: --size must look like 1536x1024" >&2; exit 2; }
81
+ for r in "${REFS[@]+"${REFS[@]}"}"; do
82
+ [[ -f "$r" ]] || { echo "codex-image: reference image not found: $r" >&2; exit 2; }
83
+ done
84
+
85
+ if [[ "$TRANSPARENT" == "1" && ${#REFS[@]} -gt 0 ]]; then
86
+ echo "codex-image: WARNING --ref together with --transparent reliably returns an OPAQUE image" >&2
87
+ echo "codex-image: (measured: 0.0% clear with a reference, ~59% without, same spec). Reference" >&2
88
+ echo "codex-image: images route through the edit path, which flattens alpha and paints a fake" >&2
89
+ echo "codex-image: transparency checkerboard. Describe the style in words instead of attaching one." >&2
90
+ fi
91
+
92
+ SPEC="$(cat)"
93
+ [[ -n "${SPEC//[[:space:]]/}" ]] || { echo "codex-image: empty spec on stdin" >&2; exit 2; }
94
+
95
+ STAGE="$(mktemp -d "${TMPDIR:-/tmp}/codex-image.XXXXXX")"
96
+ cleanup() { [[ "$KEEP" == "1" ]] || rm -rf "$STAGE"; }
97
+ trap cleanup EXIT
98
+ [[ "$KEEP" == "1" ]] && echo "codex-image: staging dir $STAGE" >&2
99
+
100
+ # --- build the prompt: the caller's spec plus a hard output contract ---
101
+ {
102
+ echo "Generate raster image assets with your built-in image_gen tool (imagegen skill)."
103
+ echo
104
+ echo "=== IMAGE SPEC ==="
105
+ echo "$SPEC"
106
+ echo
107
+ echo "Size: ${SIZE}."
108
+ [[ "$TRANSPARENT" == "1" ]] && echo "Background: genuinely transparent; preserve the alpha channel."
109
+ if [[ ${#REFS[@]} -gt 0 ]]; then
110
+ echo "Attached images are references for style, subject and composition — not edit targets,"
111
+ echo "unless the spec above says otherwise."
112
+ fi
113
+ echo
114
+ echo "=== OUTPUT CONTRACT ==="
115
+ echo "Your deliverable is exactly ${COUNT} PNG file(s) in your current working directory:"
116
+ for i in $(seq 1 "$COUNT"); do printf ' %s-%02d.png\n' "$NAME" "$i"; done
117
+ if [[ "$COUNT" -gt 1 ]]; then
118
+ echo "If the spec above enumerates ${COUNT} distinct subjects, render them in the order listed,"
119
+ echo "one subject per file, sharing the style, palette and framing stated once at the top."
120
+ echo "If it describes a single subject, render ${COUNT} variants of that subject instead."
121
+ fi
122
+ echo "Each file is the image_gen output written to disk unchanged — same pixels, same alpha,"
123
+ echo "same dimensions as generated. Issue one image_gen call per file."
124
+ echo "Write nothing else: no README, no notes, no scripts, no edits to any other file."
125
+ echo "Do not run remove_chroma_key.py, do not despill, do not crop, resize or recolor."
126
+ echo "Finish with one line per file, and nothing after them:"
127
+ echo "SAVED: <absolute path>"
128
+ } > "$STAGE/prompt.txt"
129
+
130
+ GEN_DIR="${CODEX_HOME:-$HOME/.codex}/generated_images"
131
+ mkdir -p "$GEN_DIR"
132
+ CODEX_ARGS=(exec --skip-git-repo-check -s workspace-write -C "$STAGE" --add-dir "$GEN_DIR"
133
+ -c "model_reasoning_effort=\"$EFFORT\"" --json -o "$STAGE/last.txt")
134
+ for r in "${REFS[@]+"${REFS[@]}"}"; do CODEX_ARGS+=(-i "$r"); done
135
+ CODEX_ARGS+=(-)
136
+
137
+ echo "codex-image: generating ${COUNT} image(s) at ${SIZE} (timeout ${TIMEOUT}s)…" >&2
138
+ codex "${CODEX_ARGS[@]}" < "$STAGE/prompt.txt" > "$STAGE/events.jsonl" 2> "$STAGE/stderr.txt" &
139
+ CODEX_PID=$!
140
+ WAITED=0
141
+ while kill -0 "$CODEX_PID" 2>/dev/null; do
142
+ if [[ "$WAITED" -ge "$TIMEOUT" ]]; then
143
+ kill -TERM "$CODEX_PID" 2>/dev/null || true; sleep 2
144
+ kill -KILL "$CODEX_PID" 2>/dev/null || true
145
+ echo "codex-image: TIMEOUT after ${TIMEOUT}s" >&2
146
+ exit 5
147
+ fi
148
+ sleep 3; WAITED=$((WAITED + 3))
149
+ done
150
+ wait "$CODEX_PID" 2>/dev/null || true # codex exec exits 0 even when the turn failed
151
+
152
+ # --- codex exec's exit code lies; the events stream is the source of truth ---
153
+ FAIL="$(python3 - "$STAGE/events.jsonl" <<'PY'
154
+ import json, sys
155
+ reason = ""
156
+ for line in open(sys.argv[1], encoding="utf-8", errors="replace"):
157
+ line = line.strip()
158
+ if not line: continue
159
+ try: ev = json.loads(line)
160
+ except Exception: continue
161
+ if ev.get("type") == "turn.failed":
162
+ reason = (ev.get("error") or {}).get("message", "turn failed")
163
+ elif ev.get("type") == "error" and not reason:
164
+ reason = ev.get("message", "error")
165
+ print(reason)
166
+ PY
167
+ )"
168
+ if [[ -n "$FAIL" ]]; then
169
+ echo "codex-image: Codex turn FAILED: $FAIL" >&2
170
+ case "$FAIL" in
171
+ *revoked*|*"log in again"*|*[Uu]nauthorized*|*401*)
172
+ echo "codex-image: the ChatGPT login is stale. Run 'codex login' and retry." >&2 ;;
173
+ esac
174
+ exit 3
175
+ fi
176
+
177
+ THREAD="$(python3 - "$STAGE/events.jsonl" <<'PY'
178
+ import json, sys
179
+ for line in open(sys.argv[1], encoding="utf-8", errors="replace"):
180
+ try: ev = json.loads(line)
181
+ except Exception: continue
182
+ if ev.get("type") == "thread.started":
183
+ print(ev.get("thread_id", "")); break
184
+ PY
185
+ )"
186
+
187
+ # --- harvest: Codex's saved deliverables, else the raw generated_images drop ---
188
+ RAW_DIR="${CODEX_HOME:-$HOME/.codex}/generated_images/$THREAD"
189
+ FOUND=()
190
+ if [[ "$RAW" == "0" ]]; then
191
+ while IFS= read -r f; do FOUND+=("$f"); done < <(find "$STAGE" -maxdepth 1 -name '*.png' | sort)
192
+ fi
193
+ if [[ ${#FOUND[@]} -eq 0 && -d "$RAW_DIR" ]]; then
194
+ [[ "$RAW" == "0" ]] && echo "codex-image: no PNG in staging; falling back to raw $RAW_DIR" >&2
195
+ while IFS= read -r f; do FOUND+=("$f"); done < <(ls -t "$RAW_DIR"/*.png 2>/dev/null | head -n "$COUNT" | sort)
196
+ fi
197
+ if [[ ${#FOUND[@]} -eq 0 ]]; then
198
+ echo "codex-image: Codex produced no image. Its final message was:" >&2
199
+ sed 's/^/ | /' "$STAGE/last.txt" >&2 2>/dev/null || true
200
+ exit 4
201
+ fi
202
+ if [[ ${#FOUND[@]} -lt "$COUNT" ]]; then
203
+ echo "codex-image: WARNING wanted $COUNT image(s), got ${#FOUND[@]}" >&2
204
+ fi
205
+
206
+ # --- verify every file really is a PNG, then place it ---
207
+ STATUS=0; i=0
208
+ for src in "${FOUND[@]}"; do
209
+ i=$((i + 1)); [[ "$i" -gt "$COUNT" ]] && break
210
+ if [[ -n "$OUT_FILE" ]]; then dst="$OUT_FILE"; else dst="$(printf '%s/%s-%02d.png' "$OUT_DIR" "$NAME" "$i")"; fi
211
+ cp -f "$src" "$dst"
212
+ if ! file -b "$dst" | grep -qi '^PNG image data'; then
213
+ echo "codex-image: NOT a PNG: $dst ($(file -b "$dst"))" >&2; STATUS=4; continue
214
+ fi
215
+ read_dims() { file -b "$1" | sed -n 's/.*, \([0-9]*\) x \([0-9]*\),.*/\1x\2/p'; }
216
+ dims="$(read_dims "$dst")"
217
+ if [[ -n "$dims" && "$dims" != "$SIZE" ]]; then
218
+ if [[ "$FIT" == "1" ]] && command -v sips >/dev/null 2>&1; then
219
+ sips -z "${SIZE#*x}" "${SIZE%x*}" "$dst" --out "$dst" >/dev/null 2>&1
220
+ echo "codex-image: resized $dims -> $SIZE (--fit)" >&2
221
+ dims="$(read_dims "$dst")"
222
+ else
223
+ echo "codex-image: NOTE image_gen returned $dims, not the requested $SIZE — it treats size as a hint. Pass --fit to resize." >&2
224
+ fi
225
+ fi
226
+ if [[ "$TRANSPARENT" == "1" ]]; then
227
+ chk="$(python3 - "$dst" <<'PYCHK'
228
+ import sys
229
+ try:
230
+ from PIL import Image
231
+ except ImportError:
232
+ print("skip"); raise SystemExit
233
+ im = Image.open(sys.argv[1]).convert("RGBA")
234
+ clear = sum(im.getchannel("A").histogram()[:9]) / (im.width * im.height)
235
+ corners = [im.getpixel(p)[3] for p in
236
+ [(0, 0), (im.width - 1, 0), (0, im.height - 1), (im.width - 1, im.height - 1)]]
237
+ print("ok" if clear >= 0.05 and max(corners) < 16 else "opaque %.1f%%" % (clear * 100))
238
+ PYCHK
239
+ )"
240
+ case "$chk" in
241
+ ok) ;;
242
+ skip) echo "codex-image: NOTE Pillow missing, transparency not verified: $dst" >&2 ;;
243
+ *) echo "codex-image: TRANSPARENCY FAILED $dst (only $chk of pixels are clear)." >&2
244
+ echo "codex-image: the model most likely PAINTED a transparency checkerboard instead of" >&2
245
+ echo "codex-image: leaving alpha. Add 'no checkerboard, no fake transparency grid' to Avoid." >&2
246
+ printf 'BAD %s (%s) — opaque background, not delivered as transparent\n' \
247
+ "$dst" "${dims:-unknown size}"
248
+ STATUS=6; continue ;;
249
+ esac
250
+ fi
251
+ printf 'OK %s (%s, %s bytes)\n' "$dst" "${dims:-unknown size}" "$(wc -c < "$dst" | tr -d ' ')"
252
+ done
253
+ [[ "$STATUS" == "0" ]] || exit "$STATUS"