This file is for any Claude Code agent working on this repository. Read it before making changes. Conventions here are mandatory unless the user asks otherwise.
- Single source of truth:
.claude-plugin/plugin.json,.claude-plugin/marketplace.json,.codex-plugin/plugin.json, and.agents/plugins/marketplace.json. Plugin manifests must always match on package name and version; marketplace entries must point to the same package; author, license, homepage, and the public skill-bundle description must stay aligned. - Keep
CLAUDE.mdandAGENTS.mdaligned when changing shared project rules. Claude-specific workflow details belong here; Codex-specific details belong inAGENTS.md. - Codex marketplace install uses
.codex-marketplace/threads-skills/. Do not edit that generated package by hand. Update the root files first, then runpython3 scripts/sync_codex_marketplace.py. - Default: bump the PATCH segment (3rd level,
0.0.X). This is the automatic behavior for every shippable commit, regardless of how large the diff feels. Skill renames, lib API breaks, new features: still PATCH by default. - Only bump MINOR or MAJOR when the user explicitly asks for a higher rank ("this is minor", "make it 2.0", "bump major"). Do not promote on your own initiative even if semver textbook says so.
- After bumping, two steps are required:
- Tag the commit:
git tag -a v<X.Y.Z> -m "..."+git push origin v<X.Y.Z> - Publish a GitHub Release for the tag:
gh release create v<X.Y.Z> --title "v<X.Y.Z>" --notes "<changelog>" --latestA tag alone does NOT update the README release badge. The shields.io badge reads from the Releases API, not from raw tags. Skipping step 2 leaves it stale.
- Tag the commit:
- Primary author must be Sergey: every
git commitneeds--author="Sergey Bulaev <s@bulaev.org>". The harness defaults to the Claude identity if you forget; verify withgit log -1 --format='%an <%ae>'before pushing. - Co-author trailer (
Co-Authored-By: Claude ...) is fine and welcomed. - Verify locally before push: build never breaks, no broken refs in
SKILL.md, library smoke import passes.
- Exactly 8 skills. Adding requires merging or splitting elsewhere to stay at 7. The number is announced in plugin manifests and the README.
- Frontmatter
description:target <= 400 chars (some bundle-heavy skills land slightly higher when their scope is genuinely broad; keep under 510). Always include a "Not for X (use Y)" disambiguation sentinel when a skill overlaps with a sibling. - No em dashes anywhere in
description:fields. Em dashes in body prose are allowed for table separators and list dividers only. No em dashes inside the literal fill-in lines of any hook skeleton (they would leak into generated posts). - Skill names are public surface. Renaming a skill is a major version bump and
requires updating:
.codex-plugin/plugin.json,.agents/plugins/marketplace.json,.claude-plugin/plugin.json,.claude-plugin/marketplace.json, rootSKILL.mdbundle list, README skill table, and everythreads-<name>cross-reference in sibling SKILL.md files.
- Canonical voice rules live at root
references/voice-rules.md. Skill-local "Hard rules" sections must only contain skill-specific overrides (char ranges, thread rules, format constraints) and start with:Global voice rules: see root SKILL.md Voice rules. - Other root-level references shared across skills:
references/hook-formulas.md(10 Threads formulas) andreferences/algorithm-heuristics.md. - Skill-local references live in
skills/<skill>/references/. Cite from the skill with barereferences/X.md. Cite root from skills with../../references/X.md. threads-humanizerhassub-skills/for the folded-in post-audit workflow. Don't duplicate this pattern in other skills without a clear reason.
- Write layer (Publora):
lib/publora_client.py. Two methods used by the bundle:create_post(single post or thread; pass long content and Publora auto-splits into a connected multi-post thread, no(1/N)markers) andlist_connections/threads_connections(GET /platform-connections). Skills should calllib.publish(kind, draft_text, target_url, ...)rather than inline the publora / manual / diy dispatch. Real endpoint:POST /create-postwithplatforms: ["threads-<id>"](an array of STRING ids), headerx-publora-key. Threads-specificplatformSettings.threads.replyControlis passed throughplatform_settings. - Threads has no comment/reply/reaction endpoint on Publora (those are
LinkedIn only), and
create-postcannot target another user's post. Sokind="reply"always routes to a manual copy-paste block inlib/backend_selector.py. - No read layer ships by default. There is no cheap, documented Threads
post-read actor wired in.
threads-hook-extractorandthreads-reply-drafterask the user to paste the post text. If a post-read actor is added later, gate it behindAPIFY_TOKENand keep the paste fallback. - Don't name competing third-party schedulers in committed files. The bundle is positioned around the Publora write integration.
- Codex requires marketplace entries to point at a nested plugin directory. The root remains the Claude-facing source layout.
.agents/plugins/marketplace.jsonpoints to.codex-marketplace/threads-skills.scripts/sync_codex_marketplace.pycopies the root Codex manifest,SKILL.md,skills/,references/,lib/,scripts/,requirements.txt,.env.example, andLICENSEinto the hidden package.- After editing any copied file, run the sync script before testing or committing.
testing/is the local scratch directory: API keys, sample API responses, validation reports, integration scripts.- Never write secrets above
testing/(the rest of the repo is public). - The
.gitignorerule fortesting/is load-bearing; do not change it.
Run from repo root:
python3 -c "from lib import publish, parse_threads_url, PubloraClient; print('OK')"
python3 scripts/sync_codex_marketplace.py
ls skills/ | wc -l # must equal 8
grep -rnP '\x{2014}|\x{2013}' skills/*/SKILL.md SKILL.md | grep -i '^.*description:' # must be empty
python3 -m json.tool .codex-plugin/plugin.json >/dev/null
python3 -m json.tool .agents/plugins/marketplace.json >/dev/null
python3 -m json.tool .claude-plugin/plugin.json >/dev/null
python3 -m json.tool .claude-plugin/marketplace.json >/dev/nullIf any of these fail, do not push.