Files
Auree Aubert 07e792124c Add a reproducible build for the OpenAI directory package (#90)
* Add a reproducible build for the OpenAI directory package

The OpenAI Plugins Directory is the only channel that does not track this
repo. Its docs are explicit: "published plugins do not update those
skills live." It takes a submission-time snapshot, and every change needs
a new version, a fresh review, and a manual publish -- where skills.sh,
ClawHub, the Gemini gallery and the Claude community catalog all follow
main on their own.

So the archive cannot be a hand-assembled zip. This builds it from the
repo, takes the version from .claude-plugin/plugin.json so the upload can
never claim a version the repo does not have, and discovers skills with
the same rule skills-lint uses so the three cannot disagree.

Two deliberate differences from the repo layout, both documented in the
script: skills go under skills/ (OpenAI's convention, not our root
layout), and the Gemini symlinks are dereferenced because symlinks do not
survive a zip reliably. Skills-only, since codex-hooks.json invokes
${CLAUDE_PLUGIN_ROOT} -- a Claude Code variable -- and shipping an
unverified path into review is not worth it.

Verifies the archive rather than the staging dir: manifest at root, one
SKILL.md per repo skill, no symlinks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Normalise the package for OpenAI's validator

First upload surfaced three rules no other channel enforces:

  - interface.shortDescription is required, 240 chars max.
  - Skill descriptions cap at 1024. cargo-gtm (1393) and cargo-connection
    (1208) exceed it, both because they enumerate every provider and
    integration -- which is exactly what makes them route well elsewhere.
  - metadata in SKILL.md is rejected outright. Ours carries OpenClaw
    install directives, meaningless to OpenAI.

All three are fixed in the package, not the repo, because the repo copies
serve channels that want the fuller form. The two long descriptions are
rewritten by hand rather than truncated -- every trigger phrase kept, the
rosters cut to a sample plus a count -- so no list ends mid-name.

A description that grows past the limit without a rewrite now fails the
build with the reason, instead of shipping into a rejected review. The
archive is re-read after zipping and checked against the validator's own
rules, since that is what actually gets uploaded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Add the required plugin icon and verify it is square

OpenAI requires interface.composerIcon and interface.logo, both square.

assets/icon.png is the Cargo product mark at 512x512, lifted from the
browser extension where it already ships -- the real brand asset rather
than a new one. The repo's existing assets/logo.svg would have been the
obvious source, but rendering it here only produced a padded, off-centre
bitmap, and a logo that sits wrong in the UI is worse than none.

The build now resolves both asset paths inside the archive and checks the
image is square, because a dangling path or a 1200x630 crop is invisible
from the manifest and costs a review cycle to discover. Verified against
a deliberately non-square image: it fails with the dimensions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Automate everything up to the upload, and encode the full ruleset

There is no submission API. The portal is the only path and every version
is human-reviewed, so the upload itself cannot be automated. Everything
before it can:

  - On a release, CI builds the archive and attaches it, so publishing is
    "download the asset, drop it in the portal" rather than "find whoever
    built the zip last time".
  - On a PR touching skills, assets or the builder, the same build runs,
    so a change that would fail OpenAI's validator fails in CI instead of
    months later in a review cycle.

Also encodes the published error catalogue. Two of our values would have
failed the final stage after passing the uploader, because the two tiers
disagree: shortDescription is 240 chars at upload but 30 in the directory
(ours was 201), and displayName drops from 80 to 30. LIMITS targets the
stricter tier throughout, plus name/version/URL shape, skill identity
length, archive entry count and path rules, and icon bounds.

The runner shims sips - scriptable image processing system.
This tool is used to query or modify raster image files and ColorSync ICC profiles.
Its functionality can also be used through the "Image Events" AppleScript suite.

  Usages:
    sips [image-functions] imagefile ...
    sips [profile-functions] profile ...

  Profile query functions:
    -g, --getProperty key
    -X, --extractTag tag tagFile
        --verify
    -1, --oneLine

  Image query functions:
    -g, --getProperty key
    -x, --extractProfile profile
    -1, --oneLine

  Profile modification functions:
    -s, --setProperty key value
    -d, --deleteProperty key
        --deleteTag tag
        --copyTag srcTag dstTag
        --loadTag tag tagFile
        --repair
    -o, --out file-or-directory

  Image modification functions:
    -s, --setProperty key value
    -d, --deleteProperty key
    -e, --embedProfile profile
    -E, --embedProfileIfNone profile
    -m, --matchTo profile
    -M, --matchToWithIntent profile intent
        --deleteColorManagementProperties
    -r, --rotate degreesCW
    -f, --flip horizontal|vertical
    -c, --cropToHeightWidth pixelsH pixelsW
        --cropOffset offsetY offsetH
    -p, --padToHeightWidth pixelsH pixelsW
        --padColor hexcolor
    -z, --resampleHeightWidth pixelsH pixelsW
        --resampleWidth pixelsW
        --resampleHeight pixelsH
    -Z, --resampleHeightWidthMax pixelsWH
    -i, --addIcon
        --optimizeColorForSharing
    -o, --out file-or-directory
    -j, --js file

  Other functions:
        --debug           Enable debugging output
    -h, --help            Show help
    -H, --helpProperties  Show help for properties
        --man             Generate man pages
    -v, --version         Show the version
        --formats         Show the read/write formats with ImageMagick -- the icon check is the only
macOS-specific part, and that beats paying for a macOS runner to measure
two PNGs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Detect packaged symlinks from zipinfo modes, not a listing that never shows them

The post-zip guard tested `unzip -l` output for ` -> `, but that listing
prints only length, date, and name. No zipinfo format prints ` -> target`
either, so the check could never fire and a packaged symlink would have
shipped despite being treated as a hard upload invariant.

Read zipinfo's Unix mode column instead, where a symlink reads
`lrwxrwxrwx`, and name the offending entries in the error. Require a mode
for every entry so an archive whose types cannot be read fails loudly
rather than passing by default.

Co-authored-by: Auree Aubert <aureeaubert@users.noreply.github.com>

* Answer the spam rejection with real gates, not a reworded description (#92)

OpenAI rejected cargo-gtm under "Spam mass abuse". The description was the
whole case against it: "find emails, phones and LinkedIn URLs", "write cold
emails", "build me a list of" — contact harvesting plus bulk cold mail, with
nothing in 8,800 lines about consent, suppression, or volume. Opening the
archive confirmed it; the outreach guide and prompt library are cold-email
prompts end to end.

Rewording alone would be dressing it up, so this does both halves.

references/acceptable-use.md is the substantive half — a peer of
cost-discipline.md at the same mandatory tier. Hard refusals (undifferentiated
fan-out, consumer targeting, lists with no stated origin, contacting a
suppressed record, filter and identity evasion, auto-dialing, batch-blasting
LinkedIn engagement actions), three free blocking checks before any outreach
step (basis, suppression, relevance), what drafted copy must carry, and the
data-hygiene obligations that follow a person out of the workspace.

It is wired into the path an agent actually walks rather than filed next to
it: a mandatory section above Bootstrap, the outreach row of the routing
table, a gate at the top of the outreach guide, and a "before you start" block
in outreach-activation that subtracts suppression before enrichment — which
is also cheaper. The opener prompt now returns NULL when no signal justifies
writing to that person.

The description half stays honest. It leads with B2B, licensed providers, and
the gates, and says the pack sends nothing itself. Every routing-eval trigger
survives except two rewordings ("write cold emails", "find emails for these"),
so evals/routing.jsonl line 14 is worth re-running before the next upload.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Auree Aubert <aureeaubert@users.noreply.github.com>
2026-08-12 18:28:39 -07:00
..