Commit Graph

7 Commits

Author SHA1 Message Date
Auree Aubert 55d2eaf660 Ship the OpenAI package without the surfaces its review rejected (#120)
* Hold the sending skill out of the OpenAI package

The directory review rejected two skills under "Spam mass abuse".

cargo-mailbox-management is a true positive on category: it provisions
sending mailboxes and runs a 5-to-40/day warm-up ramp, which is the shape
of cold-email infrastructure whatever the gates around it. So the package
now ships without it — EXCLUDED_SKILLS — and says so by shipping eighteen
skills rather than by rewording the nineteenth. The alternative, deleting
the refusal lists and ramp documentation until the keywords stop matching,
would ship a less guarded skill in order to pass a safety review.

cargo-gtm looks like a false positive on negation: its description ended
"bulk unsolicited messaging, purchased or scraped lists, and consumer
targeting are refused", which hands a keyword classifier four spam nouns
in one clause. The packaged description now states the same policy as what
the skill does — documented lawful basis, suppression screening,
per-recipient relevance, sends nothing itself — and keeps every routing
trigger. That did not clear review — gtm was rejected again with the new
card live, so the trigger is the skill body or the capability itself, not
the description. The reframed card is kept because it is the more accurate
of the two, not because it fixed anything.

Excluding a skill is not just skipping a directory. Fifteen of the
remaining skills link to it: a router table row, an ASCII diagram box, a
recap section, the domain list, defineMailbox/defineDomain in cargo-cdk,
a routing row in cargo-mcp, three passages in cargo-gtm's acceptable-use.
PACKAGE_EDITS removes each one in the staged copy, and every find must
match exactly once or the build fails — they are anchored to prose that
will drift, and a silent no-op would ship a dangling link. Skill counts
renumber from the real packaged total, so the router reads "18 skills ...
fifteen capability skills" without anyone maintaining the number.

Backstopping all of that, zipgrep asserts after zipping that no packaged
file names an excluded skill anywhere. That check does not care whether
the edits above were written correctly.

Every edit applies to the staging copy under dist/. The repo files are
read-only inputs, so Claude Code, Cursor, skills.sh and Hermes still
install all nineteen skills with every mailbox reference intact.

Also fixes the packaged manifest description, which claimed 17 skills —
stale since two were added. It derives from the packaged count now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YL6YdzRFUf27A2WChKxr1C

* Narrow cargo-gtm's person-level surface for the OpenAI package

Second rejection, with the reframed description already live in the
listing — so the card was never the trigger. Reading the skill body as a
policy reviewer would, five capabilities carry most of the risk, and on
those the reviewer is not wrong:

  snitcher              de-anonymising website visitors
  aiArk.analyzePersonality  OCEAN/DISC inference from a profile
  forager               personal email and personal phone from a URL
  linkedin.extractEventAttendees   harvesting a platform's attendee list
  x                     reading a social platform's posts and profiles

"We gate it" is a weak answer to visitor de-anonymisation and personality
inference. So the packaged skill no longer offers them: three playbooks
leave the tree, and the actions are removed from the stack table, the
cost table, the stage map, the alternatives table, the finding guide's
decision tree, two recipes, the aiArk playbook and the router's glossary.
Thirty-four edits, each asserted to match exactly once.

What remains is the same skill minus a person-level edge: company
research, work-email enrichment from licensed providers, verification,
scoring, signal monitoring, CRM sync. That is a real narrowing of what
the package can do rather than a rewording of what it says it does, which
is also what makes the appeal behind it checkable.

REMOVED_TERMS asserts against the built archive that nothing survived,
scoped by path — extractEventAttendees stays in cargo-connection, which
documents the catalog rather than recommending the action, and which the
directory already approved.

Metadata is regenerated with the repo's own skills-metadata.mjs over the
staged tree: three files left and a dozen changed, so every packaged
skill-metadata.json was stale by construction, content hash included.

Repo copies are untouched, as before. Every other channel still ships all
nineteen skills with the full provider stack.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YL6YdzRFUf27A2WChKxr1C

* Give the packaged quickstart a company-level demo

Third rejection, same category: the quickstart's headline pull is 25 named
people with titles and a "recently hired" flag. Sixteen skills that never
touch a person passed on the first try; the three that were rejected are
the three that do.

Rather than drop the onboarding skill, the package now ships a variant
that runs the same two-minute arc against company-level hiring intent —
theirStack.searchCompanies on the persona's role, postings inside 30 days.
This is the skill's own fallback rung 2, which it already describes as
"same wow, different angle". The receipt is unchanged at ~0.5 credits,
because searchCompanies bills per call rather than per row, so `limit` is
free to set. The demo names no individuals.

It ships as a whole file rather than as edits. The spine is rewritten —
action, filter, output fields, headline, fallback ladder, next steps — and
a dozen brittle anchors through a rewritten section would fail on the next
upstream wording change with nothing useful to say about it.

The copy is tied to the repo skill's frontmatter version instead. Bump
cargo-quickstart and this build fails, naming both versions and asking for
the port. That is the maintenance cost of an override, made loud on
purpose.

Step 3 reads the first row's keys and renders from them rather than
hardcoding a shape: the payload of this action is not something I have run,
and a demo that dies on line 3 is worse than one that spends a second
looking. Worth one 0.5-credit run against a live workspace before the
package is resubmitted.

Repo copies untouched, as with the two commits before this. Every other
channel still ships the leads demo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YL6YdzRFUf27A2WChKxr1C

* Rebuild the quickstart demo on aiArk, against a live run

theirStack was the wrong pick and the reason is not taste: it needs its
own API-key connector, so on the fresh workspace a quickstart exists for
it dead-ends on rung 1. It got chosen because the fallback ladder named
it, not because anything checked it.

aiArk.searchCompanies is better on every axis that matters here. It runs
on cargo's managed connection, so a workspace with nothing wired up can
still complete the demo. At 0.01 per returned record, 25 rows cost 0.25
credits — half what the leads demo cost. And it answers the persona
question with accounts rather than people.

The demo also opens on countCompanies, which is fixed-cost 0. Sizing the
whole addressable market for free and then sampling it for a quarter of a
credit is a better first beat than 25 rows with no denominator, and it
puts the pack's own count-first rule in the first thing a new user sees.

Everything here is verified against a real run rather than a playbook:

  countCompanies      free, returns {"count": N} — 1,904 on the example
  searchCompanies     25 rows for 0.25 credits
  employeeRole        employee_title_or / employee_seniority_or /
                      employee_department_or
  industry            industry_or takes plain lowercase strings
                      ("software development") — no autocomplete needed,
                      and it halved the pool to 840
  row fields          name, industry, employee_count, city, country,
                      domain, linkedin_url, revenue, founded_year,
                      technologies

So step 4 renders named columns now instead of printing the keys and
hoping. theirStack keeps a place as rung 3, where its own-key connector
is a known cost rather than a surprise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YL6YdzRFUf27A2WChKxr1C

* Remove only the mailbox bullet from the CDK critical rules

The drop_block helper takes the blank-line-delimited block around a
marker, which is right for a standalone diagram and wrong for a markdown
bullet list: consecutive bullets have no blank line between them, so the
whole list is one block. The cargo-cdk edit anchored on the defineMailbox
bullet and took all of "## 6) Critical rules" with it — 71 lines instead
of 12. The packaged CDK skill shipped an empty heading, losing the
state-file, secrets, handle-wiring, --yes, paid-play sample and alert
cost-gate rules.

Caught by Bugbot on #120, not by the build: every assertion here checks
that a find matched exactly once and that removed things left no trace.
Nothing checked that what remained was still there.

A repo-vs-package line-count diff over the whole archive now reconciles:
every SKILL.md is -12 for the dropped metadata block, and each further
delta matches a specific edit. cargo-cdk is the only file that was wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YL6YdzRFUf27A2WChKxr1C

* Document aiArk's three undocumented actions

Bugbot flagged the quickstart override for calling aiArk.countCompanies,
an action absent from the packaged playbook. The action is real — the demo
was verified against a live run — so the gap is in the documentation, and
it is a repo gap rather than a packaging one.

The playbook described six actions and asserted "all six are credits-based".
The integration exposes nine:

  enrichCompany    0.01   the cheapest company enrich in the catalog
  countCompanies   free   {"count": N} for a searchCompanies filter
  countPeople      free   same, for searchPeople

The count pair is the more useful omission. Cost discipline tells an agent
to size a pool before paying per record, and on this provider that check
costs nothing — but only if the agent knows the actions exist. Added
alongside the per-record billing warning, where the question actually comes
up.

Also corrects the override's claim that industry_or takes ordinary
lowercase strings, which contradicted the same playbook two files away.
Both are true and neither said so: the enum members are LinkedIn-style
lowercase names, identical to the strings each row returns in its own
industry field, so a value copied off a result matches while a guess
empties the pool. The override now says that, and points at listIndustries
for anything less obvious.

Repo change, so every channel gets it, not just the OpenAI package.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YL6YdzRFUf27A2WChKxr1C

* Remove the personality pitch, not just its slug

REMOVED_TERMS asserted on "analyzePersonality" and passed, while the
packaged playbook went on selling the capability in prose that named no
action: an intro pitching "a personality-analysis action nothing else in
the catalog has", and a reason-to-reach-for-it clause offering
"personality/selling guidance". A slug-only check waved both through.

The counts were wrong too. The repo playbook says nine actions, seven
credits-based; the package removes one, so it ships eight and six. Now
stated as such, and the packaged action table has exactly eight rows to
match.

The assertion now looks for "personality" itself. It cannot collide with
"personalization", which is a different word, and the sweep it flagged on
the first run is what found the second clause — the fix caught its own
gap before the archive shipped.

Also adds .github/package-overrides/** to the Codex package workflow's
path filter. The package depends on that file now, so a change touching
only the override needs to run the build and the version guard, and did
not.

Both found by Bugbot on #120.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YL6YdzRFUf27A2WChKxr1C

* Fetch the count, drop the attendee pitch, fail a broken sweep

Three from Bugbot on #120, all correct.

The quickstart's headline number was never fetched. Step 1 wrote the
countCompanies run object to a file and step 4 told the agent to lead with
the pool size, but the run object holds no results — the same skill says so
two paragraphs earlier — and nothing ever called download-outputs for that
run. The demo would have opened on an empty variable. Both steps now go
through one fetch_output helper, so the retrieval that was already correct
for the search is correct for the count too, and the headline reads $POOL.

The linkedin playbook still pitched event-attendee harvesting in prose
after the action row was removed: an engagement-sourcing bullet and the
per-item billing note. Same failure as the personality pitch — the slug
left, the capability stayed. The new assertion is scoped to that one file
on purpose: "event attendees" appears four more times in cargo-gtm as a
lawful BASIS for contacting someone, and that must stay exactly where it is.

And the sweeps themselves could not fail. Every throw was read as "nothing
matched", so a missing binary or an unreadable archive reported a clean
package. grep-family exit codes distinguish these: 1 is no match, anything
else is an error. Verified by putting a zipgrep that exits 2 on PATH — the
build now stops with "could not verify ... zipgrep exited 2" where it
previously passed.

That last one is the pattern worth naming: three of the five findings on
this PR were assertions that were true and conclusions that were wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YL6YdzRFUf27A2WChKxr1C

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 13:26:47 -07:00
dependabot[bot] cda2ee39cb CI: bump actions/checkout from 4.4.0 to 7.0.1 (#103)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.4.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/11d5960a326750d5838078e36cf38b85af677262...3d3c42e5aac5ba805825da76410c181273ba90b1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 23:53:47 -07:00
dependabot[bot] 842e9940ba CI: bump actions/setup-node from 4.4.0 to 7.0.0 (#106)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/49933ea5288caeca8642d1e84afbd3f7d6820020...820762786026740c76f36085b0efc47a31fe5020)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 23:53:39 -07:00
dependabot[bot] 144be3ea00 CI: bump actions/download-artifact from 4.3.0 to 8.0.1 (#105)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.3.0 to 8.0.1.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/d3f86a106a0bac45b974a628896c90dbdf5c8093...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 23:53:31 -07:00
dependabot[bot] 08ec7127de CI: bump actions/upload-artifact from 4.6.2 to 7.0.1 (#104)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 7.0.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 23:53:22 -07:00
Auree Aubert 6683645b00 CI: run the HOL AI plugin scanner (#102)
* CI: run the HOL AI plugin scanner

hashgraph-online/awesome-ai-plugins requires every listed plugin's source
repo to invoke `ai-plugin-scanner-action` on push/pull_request before the
listing PR can merge (getcargohq/cargo-skills#83 → their PR #72).

Pinned to a full commit SHA instead of the floating `v1` tag, with a
read-only token and no secrets in scope, so a third-party action in our
workflow surface can't reach anything it shouldn't.

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

* CI: publish the scanner report when the scan fails

The action's built-in summary step is `if: success()`, so a failing scan
prints counts and no findings. Render markdown to a file and cat it into
the step summary unconditionally.

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

* CI: tee the scanner report to the job log as well

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

* CI: clear the plugin scanner's 80-point / no-high gate

The scanner job this PR added failed on its first run: 71/100 against a
`min_score` of 80, one critical finding and one high. The awesome-ai-plugins
contribution gate re-scans this repo with the same two thresholds, so the
findings had to be fixed rather than configured around. Now 92/100 (A), zero
critical, zero high.

- Pin every `uses:` in .github/workflows/ to a full commit SHA, and add
  .github/dependabot.yml so the pins move weekly instead of never.
- Add SECURITY.md: private reporting via the Security tab, and an explicit
  boundary between this bundle and the cargo-ai CLI / Cargo platform.
- Stop inlining `curl … install.sh` in cargo/SKILL.md. The command now lives
  only in README.md, where a human runs it; the router keeps the guidance
  (never run it on the user's behalf) and points there.
- Use execFileSync, not execSync, for `npm root -g` in the QA script library —
  no shell to interpret a command string. Windows gets the npm.cmd shim.
- Fill in repository/license/keywords in .codex-plugin/plugin.json, a category
  on the .agents marketplace entry, and add .codexignore.
- Set write_step_summary: false on the scanner action and publish the report
  ourselves, so it lands once whether the gate passes or fails.

Two medium findings are left standing and documented in CHANGELOG.md: the
`anthropic` provider slug in cargo-gtm's description (correct, and CI-synced
with the CLI) and `fetch(` in the run-output downloader (the point of the
function). Neither is a gate failure.

Co-Authored-By: Auree Aubert <16732193+aureeaubert@users.noreply.github.com>
Co-Authored-By: replicas-connector[bot] <replicas-connector[bot]@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: replicas-connector[bot] <replicas-connector[bot]@users.noreply.github.com>
Co-authored-by: Auree Aubert <16732193+aureeaubert@users.noreply.github.com>
2026-08-14 19:00:18 -07:00
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