Two bugs in the encode path, one reported (#1510), one found next to it. DISCORD. gifski gives every frame its own LOCAL colour table — that is where its quality comes from. gifsicle then stacks cross-frame transparency on top, and the result is a GIF that gifsicle itself refuses to reopen: "too complex to unoptimize — local colour tables or complex transparency". PIL, ffmpeg and macOS Preview all render it perfectly. Discord's decoder does not: later scenes come out with stale rows, ghosted captions and holes punched in solid shapes. The reported file had 199 of 207 frames carrying their own palette. `--colors 256` collapses them to one global palette. Confirmed on the reported file: rebuilt, it plays correctly in Discord where the original flashes. It costs ~6/255 mean colour error — one palette now spans every scene, so gradients band slightly — and it came out SMALLER, 1210 KB -> 918 KB. Worth stating plainly because it nearly sent me the wrong way: this is NOT gifsicle's fault. gifski's raw output has the same 207/207 local tables and draws the same warning with gifsicle nowhere in the pipeline. "Drop the gifsicle pass" would have fixed nothing. FRAME DROP. The ffmpeg fallback set fps= in the filter chain but never passed -framerate on the INPUT, so ffmpeg read the PNG sequence at its default 25fps and resampled down — silently discarding 91 of 207 frames (44%) and playing the rest too fast. _mp4() always passed -framerate; this path never did. With the input rate correct the fps= filter is redundant, and dropping it is what takes the output from 205 frames back to all 207. This path runs whenever gifski is missing, which the mort image allows to happen silently (its install is wrapped in `|| echo "gifski unavailable"`). Verified by driving the patched _gif() against the 207 real frames of the reported render: gifski path 207 frames / 0 local palettes / 918 KB; ffmpeg path 0 local palettes with total duration preserved (14.78s vs 14.79s source — gifsicle merges two identical frames and extends their delays).
executus-skills
Skill packs for executus
harnesses (mort, gadfly, …), in the SKILL.md "agent-skills" format. Each pack is
its own subfolder with a SKILL.md manifest plus any bundled helper files.
executus-skills/
gif/
SKILL.md # instructions the agent loads on demand
scripts/encode.py # bundled helper the agent runs in its sandbox
Packs
gif— make a funny animated GIF or MP4 for Discord from a description. Plans scenes, keeps recurring people/props consistent via a reusable cast, renders frames in Python, and lets a bundledencode.pypick the format: a GIF for short loops, or an H.264 MP4 (that Discord plays inline) for long bits or when a GIF comes out too big. Adapted from mort's builtinanimateskill. Handles multi-minute pieces.
Using a pack from mort
Each subfolder is a separate subscription (pinned by content digest):
.set skillpacks.enabled true # then restart mort (boot-read)
.set skillpacks.allowed_sources git:https://gitea.stevedudenhoeffer.com/steve/
.skillpack subscribe git https://gitea.stevedudenhoeffer.com/steve/executus-skills subpath=gif ref=main
Then attach the pack to an agent that already has the tools the pack uses (the
pack supplies instructions, not tools). For gif, the agent needs
code_exec, image_describe, send_attachments, file_get_metadata, and
think in its toolset, a generous max_runtime with critic_enabled: true
(long renders run for minutes), and a sandbox image with ffmpeg (plus
gifski/gifsicle for nicer GIFs). Add it in the agent's YAML:
low_level_tools: [code_exec, image_describe, send_attachments, file_get_metadata, think]
skill_packs: [gif]
When the pack is updated upstream, .skillpack check reports it and
.skillpack apply gif re-pins — nothing an agent runs changes until you apply.