open-design/.claude/commands
koki 4b7c018a9b
feat(contrib): add od-contribute skill for non-coder contributors (#3172)
* feat(contrib): add od-contribute skill for non-coder contributors

Adds a Claude Code skill at .claude/skills/od-contribute/ that walks any OD
user — including non-coders — through a first-PR contribution flow:

  - Ship a Skill / Design System made with OD
  - Translate README / QUICKSTART / CONTRIBUTING to a new language
  - Fix a typo / dead link / write a use-case blog post
  - Report a high-quality bug (issue path, no PR)

The skill replaces the test-driven dev-loop of auto-github-contributor with
type-specific no-code validators (frontmatter parse, markdown link check,
code-fence balance, structural overlap with reference DESIGN.md files), so
artifact-only contributions don't have to pretend to be code.

This commit only adds files under .claude/ — no product code, no build
config, no runtime dependencies. .gitignore is amended with three explicit
exceptions so the skill is tracked while personal Claude state (sessions,
settings, etc.) stays ignored as before.

Next steps (separate PRs):
  - Wire the OD app to mount this skill for its embedded agent
  - Add a "Ship to GitHub" UI button in OD that invokes /od-contribute

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

* feat(contrib): English-by-default skill + zip installer for non-coders

Two follow-ups to the initial od-contribute skill:

1. Skill content is now English with an explicit instruction at the top
   telling the agent to mirror the user's chat language for every user-
   facing prompt. Generated artifacts (PR titles, commit messages, PR/
   issue body) stay English regardless — GitHub convention.

2. tools/od-contribute-installer/ ships a cross-platform installer that
   drops the skill into every supported agent's home dir without the
   user opening a terminal:

     install.command  macOS double-click
     install.bat      Windows double-click
     install.sh       Linux

   Targets covered:
     ~/.claude/skills/od-contribute/        Claude Code (native)
     ~/.claude/commands/od-contribute.md    Claude Code slash command
     ~/.agents/skills/od-contribute/        Codex CLI (canonical)
     ~/.codex/skills/od-contribute/         Codex CLI (legacy, only
                                            written if ~/.codex/ exists)

   Verified Codex CLI reads the same SKILL.md frontmatter format as
   Claude Code (source: openai/codex codex-rs/core-skills/src/loader.rs).
   Added agents/openai.yaml sidecar inside the skill for Codex picker UX.

3. build-zip.sh produces od-contribute-installer.zip (~37KB) from the
   in-repo skill. The zip is meant to be hosted as a GitHub Release
   asset; the marketing site button points at:
     github.com/nexu-io/open-design/releases/latest/download/od-contribute-installer.zip
   (See tools/od-contribute-installer/HOSTING.md for the manual release
   recipe; CI workflow can come later.)

   The zip itself is gitignored — distribute via Releases, not source.

Still no product code touched, no build config changed.

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

* refactor(contrib): drop zip installer; ship single curl one-liner

Replace tools/od-contribute-installer/ (4 install scripts + zip build
machinery) with a single self-bootstrapping tools/install-od-contribute.sh.

User flow becomes:

  1. Click button on opendesign.so
  2. Modal shows: paste this into your AI agent's chat:

       curl -sSL https://raw.githubusercontent.com/nexu-io/open-design/main/tools/install-od-contribute.sh | bash

  3. Agent runs it via its Bash tool. User never touches a terminal.
  4. /od-contribute is live in their next chat.

Why this is better than the zip approach:

  * Zero downloads visible to the user — no .zip in their Downloads folder
  * Zero unzip step
  * Zero terminal window flash (the agent's Bash tool runs in-process)
  * Zero per-OS installer files (.command/.bat/.sh) to maintain
  * Auto-updates: re-running the one-liner pulls the latest skill from main

The script downloads only the skill subtree (.claude/skills/od-contribute/
and .claude/commands/od-contribute.md) from a GitHub tarball — no `git`
dependency, just curl + tar (universally available).

Targets remain the same:
  ~/.claude/skills/od-contribute/
  ~/.claude/commands/od-contribute.md
  ~/.agents/skills/od-contribute/
  ~/.codex/skills/od-contribute/  (only if ~/.codex/ exists)

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

* chore(contrib): remove leftover zip artifact

Build artifact accidentally committed in the previous commit.
Cleaning up so the binary doesn't live in git history.

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

* fix(contrib): make skill work in sandboxed agents (Codex.app, Cursor)

macOS App Sandbox apps like Codex.app cannot reach the system keychain
where `gh auth login` stores the GitHub token by default. Result: the
skill's check-prereqs.sh fails on `gh auth status` with a misleading
"not authenticated" error, even when gh works fine in the user's regular
shell.

Two changes:

1. config.sh: if GH_TOKEN isn't set in the env, fall back to reading a
   .gh-token file at the skill root. Lets a user (or the OD app, or a
   future OAuth Device Flow bootstrapper) drop a token there once and
   have every skill script pick it up automatically.

2. check-prereqs.sh: accept GH_TOKEN-from-env as a valid auth path
   alongside `gh auth status`. When neither works, the error hint now
   shows BOTH options:
     A) gh auth login from a regular terminal (any agent)
     B) gh auth token > <skill>/.gh-token (sandboxed agents)

Verified: in my local Claude Code (where gh has keychain access), the
keychain path still wins and nothing changes. With GH_TOKEN exported,
check-prereqs.sh succeeds without even consulting gh auth status.

Future: implement OAuth Device Flow inside the skill so non-coder users
hitting this in Codex.app can authenticate by clicking a link, no
terminal involved. That's a separate PR.

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

* chore(contrib): move install script into skill folder (CI policy fix)

The repo's tools/ directory has a strict allowlist policy enforced by
scripts/guard.ts — only AGENTS.md, dev/, pack/, and serve/ are permitted
top-level entries. Moving install-od-contribute.sh out of tools/ and into
.claude/skills/od-contribute/install.sh:

  - Satisfies the guard policy (no scripts/guard.ts edit needed)
  - Co-locates the install script with the skill it installs (cleaner
    mental model: skill folder is self-contained)
  - The install URL stays inside the gitignore exception we already
    established for .claude/skills/od-contribute/

Public install URL changes from
  raw.githubusercontent.com/.../main/tools/install-od-contribute.sh
to
  raw.githubusercontent.com/.../main/.claude/skills/od-contribute/install.sh

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

* fix(contrib): address @nettee/looper review feedback (3 blocking issues)

Three real bugs caught by the looper review bot, all fixed:

1) create-pr.sh:48 — git diff missed untracked files

   `git diff --quiet || git diff --cached --quiet` ignored untracked paths,
   so the most common contribution shape (a brand-new Skill folder, a new
   translation file, a new doc) hit the else branch and pushed an empty
   commit. Replaced with `git status --porcelain` which sees untracked,
   plus a post-stage sanity check via `git diff --cached --quiet` so we
   skip the commit cleanly if everything turned out to be in .gitignore.

2) validate-skill-submission.sh:34 — frontmatter parse too lenient

   The awk fence-counter accepted `---` anywhere in the file as the
   opening fence. A SKILL.md with prose before the YAML block parsed as
   "valid frontmatter" by this script while the actual loaders (Claude
   Code + codex-rs/core-skills) required the fence on line 1 and would
   reject it. Added an explicit head -n 1 check so leading prose is
   rejected with a clear error before awk runs.

3) check-prereqs.sh:87 — gh api user failure swallowed

   `GH_USER="$(gh api user --jq .login 2>/dev/null || echo '?')"` set
   GH_USER to literal "?" when the API call failed (revoked token,
   missing 'repo' scope, network), then the script exited READY=1.
   Downstream that propagated to TARGET_FORK="?/open-design" and
   blew up at push time.

   Dropped the `|| echo '?'` fallback. An empty GH_USER now triggers a
   structured error with three common causes and the recovery command,
   and exits 2.

   While here, also fixed a related bug: this script sources config.sh
   which has `set -euo pipefail`, so -e leaked in and aborted the
   script silently the moment any check failed (instead of accumulating
   diagnostics like the original auto-github-contributor design
   intended). Added explicit `set +e; set -uo pipefail` after sourcing
   to restore the "keep checking past failures" behavior the comment
   on line 7 promised.

Smoke-tested all four fixes locally:
  - create-pr.sh: git status --porcelain correctly sees untracked files
  - validator: rejects SKILL.md starting with prose, passes well-formed
  - check-prereqs.sh: with stubbed gh that fails `gh api user`, now
    exits 2 with the structured error (was: silent exit 1)
  - check-prereqs.sh: happy path on real machine unchanged

Thanks @nettee for the careful review.

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

* fix(contrib): macOS Bash 3.2 + over-strict link validator (review round 2)

Two more blocking issues from the looper review, plus one related bug I
caught while re-testing on real OD docs.

1) discover-i18n-gaps.sh: removed Bash 4 dep (declare -A)

   macOS still ships Bash 3.2.57 by default and most agent-spawned bash
   subprocesses inherit that. `declare -A SEEN_LANG=()` failed with
   `declare: -A: invalid option`, crashing Step 3b before any translation
   target could be shown.

   Replaced the associative array with a newline-delimited string set
   (\n<lang>\n bracket form to avoid prefix-overlap false matches like
   zh vs zh-CN). Verified end-to-end on /bin/bash 3.2.57 against the
   actual OD repo: returns the correct 28 stale-translation rows
   across the four English source docs.

   Also fixed a latent path-stripping bug in the same loop: `find`
   emits `./README.zh-CN.md` with leading `./`, so `${path#README.}`
   wasn't stripping the prefix at all. Switched to basename-first.

2) validate-markdown.sh: --reference flag for i18n / docs-edit flows

   The validator was treating every relative link target as a file path
   and failing on slugs like `skills/blog-post/` that are website
   router routes, not files in the checkout. A structure-preserving
   translation of README.md couldn't pass even when the user changed
   nothing except language.

   Added --reference <orig> flag. The validator now builds a "known
   already-broken" set of refs from the source file and excuses those
   in the new file. Newly-introduced broken refs still fail.

   Without --reference (e.g. brand-new blog file with no prior version),
   the relative-ref check is skipped entirely with a SKIP note — since
   we can't tell route slugs from file paths in isolation, failing
   would be wrong. Code-fence balance + external-link health still run.

   Updated SKILL.md so the i18n branch (3b.6) and the docs branch
   (3c.6) call validate-markdown.sh with --reference pointing at the
   English source / HEAD revision respectively.

3) (caught while testing) URL extraction regex too loose

   `grep -oE 'https?://[^) ]+'` was capturing trailing quotes from HTML
   <img src="..."> tags in OD's README, e.g.
     https://cms-assets.youmind.com/.../foo.jpg"
   The trailing `"` made the curl HEAD return 404. Tightened the
   character class to also stop at `"`, `'`, `<`, `>`, `[`, `]`.
   With this fix, README.md now passes all checks (20 external links
   verified 2xx/3xx).

Smoke-tested on macOS /bin/bash 3.2.57 with the actual nexu-io/open-design
working copy. All four scenarios behave correctly:
  - README.md without --reference → SKIP relative-ref check, PASS overall
  - README.md with --reference itself → 34 refs excused as pre-existing, PASS
  - Newly-introduced broken ref → FAIL (regression catch preserved)
  - Old test cases (skill validator, prereq check) → still pass

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

* fix(contrib): preserve .gh-token across install.sh reruns

`install_skill_to()` did `rm -rf $dest` before copying in the new skill,
which wiped any user-local state files. The most consequential one is
`.gh-token` — sandboxed agents (Codex.app, Cursor) write a GitHub token
there because they can't reach the macOS keychain (see check-prereqs.sh's
hint and config.sh's fallback path).

Effect: the documented upgrade path ("re-run the curl one-liner to pull
the latest skill") would silently lose the token on every refresh, and
the very next /od-contribute run would fail at the prereq gate with
"no GitHub credentials available", forcing the user back through manual
token setup. This affects exactly the audience the PR is aimed at.

Fix: stash any file in PRESERVE=(.gh-token) to a tempdir before rm -rf,
restore after the copy, re-chmod 600 on the way back. Test:

  1. Pre-seed .gh-token in all three target dirs
  2. Run installer
  3. Verify all three tokens still present, contents unchanged, perms 600

Centralized the preserved-state list as PRESERVE=() so future per-user
state (e.g. an OAuth-flow-saved refresh token) only has to be added in
one place.

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

* fix(contrib): i18n stale false-positive + tier markdown link check (round 4)

Two more blocking issues from looper, both real.

1) discover-i18n-gaps.sh: false-stale on same-commit translations

   `git log --since=@<epoch>` is INCLUSIVE of the boundary epoch, so when
   the English source and a translation get touched in the SAME commit
   (a very common pattern: bulk i18n refresh, structural edits applied
   across all locales), the shared commit was counted toward
   english_commits_since_translation. Result: an already-current
   translation was reported with `status="stale", english_commits_since_
   translation=1`, and Step 3b would suggest it for refresh — driving
   users into no-op PRs.

   Reproduced exactly per looper's case: README.md and README.uk.md both
   have last commit 338cb4d at epoch 1779948707; the OLD predicate
   returned 1, the NEW predicate returns 0.

   Switched commits_between() from `--since=@<epoch>` math to commit
   ancestry: `git rev-list <tr_sha>..HEAD -- <newer>`. tr_sha..HEAD reads
   "commits reachable from HEAD but not from tr_sha", which correctly
   excludes the shared tip when both files were last touched together.

2) validate-markdown.sh: brand-new files bypassed local link check

   The previous fix skipped relative-ref validation entirely when
   --reference was absent. That covered slug-style refs (good) but also
   covered explicit `./foo.md` and `../bar/baz.md` style refs (bad).
   Step 3c (new blog post) doesn't pass --reference, so a contribution
   could ship with `[broken](./missing.md)` and pass the validator.

   Tiered the relative-ref check:
     - Image refs (`![alt](path)`) — ALWAYS validated. Markdown image
       syntax is never a website route.
     - Refs starting with `./` or `../` — ALWAYS validated. Explicit
       relative paths are unambiguous file references.
     - Other link refs (`skills/blog-post/` style) — only validated
       when --reference is supplied; otherwise skipped (could be route).

   In all cases, refs already broken in --reference (when supplied) are
   excused as pre-existing rather than reported as regressions.

   Verified against looper's exact repro (`[new broken](./missing.md)`
   in a brand-new file with no --reference): now correctly fails. Also
   verified ambiguous-slug test (`skills/blog-post/`) still skips
   without --reference, image refs always check, and README.md regression
   tests both with and without --reference still pass.

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

* fix(contrib): catch bare-path refs in validators (review round 5)

Two narrow follow-ups to the round-4 tiered link checks:

- validate-skill-submission.sh: scan every non-URL, non-anchor markdown
  link target in SKILL.md (not just `./` / `../` prefixed paths). Plain
  intra-skill refs like `[ref](references/foo.md)` were previously
  ignored by the regex, letting broken bundles pass. Escape detection
  switches to lexical (segment count) instead of `cd … && pwd -P`, so a
  missing intermediate directory no longer masquerades as an escape.

- validate-markdown.sh: treat file-like targets (`*.md`, `*.png`,
  `*.svg`, image/asset/script extensions) as on-disk refs even without
  `--reference`. `[doc](missing.md)` is unambiguously a sibling file,
  not a website route, and Step 3c (new docs/blog) had no `--reference`
  to fall back on. Slug-style refs without an extension still get
  skipped without `--reference`.

Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)

* fix(contrib): scratch leak + dedupe gate + workdir reuse (review round 6)

Three blocking issues from looper round 6, all fixed.

1) create-pr.sh + setup-workspace.sh: .od-contrib/ scratch leaked into PR

   `git add -A` in create-pr.sh staged everything in the worktree, including
   the skill's internal scratch dir (.od-contrib/type.txt, .od-contrib/
   slug.txt, .od-contrib/PR-BODY.md created by setup-workspace.sh and the
   render step). OD's .gitignore doesn't exclude .od-contrib/, so every PR
   opened through this flow shipped those bookkeeping files in the user's
   contribution diff.

   Two layers of defense:
   - setup-workspace.sh now writes `.od-contrib/` to .git/info/exclude
     when preparing the workdir (repo-local exclude, not committed).
   - create-pr.sh now uses an explicit pathspec `:!:.od-contrib` on its
     git status / git add calls. So even if a workdir was prepared
     differently, this script alone refuses to stage the scratch dir.

   Verified with a temp repo containing both .od-contrib/PR-BODY.md and a
   user file: only the user file lands in the index after `git add -A
   -- . :!:.od-contrib`.

2) create-issue.sh: dedupe gate didn't actually gate

   The --dedupe-keywords flag printed search hits to stderr but then
   unconditionally fell through to `gh issue create`. The `|| true` after
   the gh search pipeline also swallowed network/jq failures, so a broken
   search looked identical to "no duplicates found" — and the issue got
   created either way. The user never got a real chance to choose
   "comment on existing / open anyway / cancel".

   Now:
   - Run gh search and jq as separate steps; either failure exits 2 with
     a structured REASON=search_failed/parse_failed.
   - If matches > 0 AND --allow-duplicates was NOT passed, exit 3 with
     REASON=duplicates_found and MATCH_COUNT=N. Caller must explicitly
     re-run with --allow-duplicates after surfacing matches to the user.
   - The script now requires `jq` (added od::require jq) since we
     actually parse JSON.
   - Updated the docstring at the top so the caller contract (ask the
     user, then re-invoke with --allow-duplicates) is explicit.

   Verified: searching keyword "preview" against nexu-io/open-design
   matches 5 open issues; the script exits 3 and never calls
   `gh issue create`.

3) setup-workspace.sh: same-day workdir reuse leaked stale state

   `SESSION_DIR=<TYPE>-<SLUG>-<YYYYMMDD>` reused the same directory for
   every same-day, same-(type,slug) invocation. The most acute case:
   SKILL.md 3b.1 calls `setup-workspace.sh i18n translate` BEFORE the
   user has picked a doc/language, so every i18n attempt on the same
   day landed in `i18n-translate-<date>/` — and untracked files from an
   abandoned earlier translation survived `git checkout`/`pull` and
   leaked into the next user's run.

   Two changes:
   - Bumped tag to second precision: `<YYYYMMDD>-<HHMMSS>`. Two human-
     paced sessions in the same second is vanishingly rare. Verified
     two rapid runs produce different tags (114208 vs 114209).
   - When a workdir IS reused (same SESSION_TAG passed in explicitly,
     or rare clock collision), now does `git reset --hard HEAD` and
     `git clean -fdx` first so the run starts from a known-good base
     instead of inheriting prior occupant state.

   The branch name now also tracks the timestamp tag, so two runs can't
   accidentally end up on the same feature branch either.

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

---------

Co-authored-by: leilei926524-tech <leilei926524-tech@users.noreply.github.com>
Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com>
2026-05-29 07:16:04 +00:00
..
od-contribute.md feat(contrib): add od-contribute skill for non-coder contributors (#3172) 2026-05-29 07:16:04 +00:00