Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,74 @@ Results are saved to `evaluations/results/` (gitignored). See `evaluations/icp-c

**Eval prompt guidelines:** Keep prompts focused to avoid the 120s timeout. Scope the response ("just the function, no deploy steps"), ask for one thing, and match expected behaviors to what the prompt actually asks. See CONTRIBUTING.md for details and examples.

## Upstream Sync Strategy

Several skills track content from external upstream repositories. **Do not use git submodules** — we only need a small number of files per repo and submodules complicate every clone.

### Upstream comment format

Every skill that tracks upstream content must have this comment block at the top of the SKILL.md body (after frontmatter):

```html
<!-- Upstream: https://github.com/<org>/<repo>
Tag: <tag> Commit: <full-sha>
File: <path-to-upstream-skill-file>
Last synced: YYYY-MM-DD
Sections owned by icskills (do not overwrite from upstream):
<comma-separated list of sections that icskills extends or differs> -->
```

Always use the **full commit SHA** of the tag, not just the tag name. Annotated tags require a two-step dereference (tag object → commit SHA).

### Getting a commit SHA for a tag

```bash
# Step 1: get tag object SHA and type
TAG_SHA=$(curl -s "https://api.github.com/repos/<org>/<repo>/git/ref/tags/<tag>" | \
python3 -c "import sys,json; d=json.load(sys.stdin); print(d['object']['sha'], d['object']['type'])")

# Step 2: if type is "tag" (annotated), dereference to the commit
curl -s "https://api.github.com/repos/<org>/<repo>/git/tags/<tag-sha>" | \
python3 -c "import sys,json; print(json.load(sys.stdin)['object']['sha'])"
```

### Checking for upstream changes

```bash
# Fetch upstream skill file at a new tag/commit
curl -s "https://raw.githubusercontent.com/<org>/<repo>/<commit>/<path>" > /tmp/upstream.md

# Compare to find what changed
diff /tmp/upstream.md skills/<skill-name>/SKILL.md
```

### Current upstream sources

| Skill | Upstream repo | Tag | Commit |
|-------|--------------|-----|--------|
| `motoko` | [caffeinelabs/motoko](https://github.com/caffeinelabs/motoko) | 1.7.0 | `1e65e26346b35927869dda044bb76763627c2c57` |
| `migrating-motoko` | [caffeinelabs/motoko](https://github.com/caffeinelabs/motoko) | 1.7.0 | `1e65e26346b35927869dda044bb76763627c2c57` |
| `migrating-motoko-enhanced` | [caffeinelabs/motoko](https://github.com/caffeinelabs/motoko) | 1.7.0 | `1e65e26346b35927869dda044bb76763627c2c57` |
| `mops-cli` | [caffeinelabs/mops](https://github.com/caffeinelabs/mops) | cli-v2.13.1 | `c947a79fc68d2d4d5b0d3bad10e23370b8134364` |

When a new version of an upstream repo is released: (1) get the new commit SHA, (2) diff the upstream skill file against what we have, (3) apply non-conflicting improvements **except for sections listed as "owned by icskills"**, (4) update the upstream comment with the new tag and SHA, (5) update the table above.

### What icskills changes vs upstream

| Change type | Examples | Rule |
|-------------|---------|------|
| **Cross-reference links** | `Load motoko skill`, `Load mops-cli skill` | Always rewrite to icskills skill names; never overwrite from upstream |
| **Bug fixes to upstream** | Removed `args = ["--enhanced-migration=..."]` from migrating-motoko-enhanced | Keep our fix; consider contributing upstream |
| **icp-cli / IC-specific additions** | Runtime.envVar, icp-cli deployment notes, M0141/M0145 pitfalls | icskills-owned; do not overwrite |
| **Style additions** | transient var section, variant tag examples | icskills-owned if not in upstream; otherwise align |
| **Content shared with upstream** | All patterns, syntax, error tables | Sync from upstream when unchanged |

### Automated upstream release detection

`.github/workflows/sync-upstream.yml` runs weekly to detect new releases of tracked upstream repos. When a new release is found it opens a PR with the raw upstream diff, so maintainers can review and cherry-pick improvements manually. The workflow does NOT auto-apply changes — it only surfaces the diff.

This is adapted from [dfinity/developer-docs sync-motoko.yml](https://github.com/dfinity/developer-docs/blob/main/.github/workflows/sync-motoko.yml), simplified for a curl-based approach (no submodules).

## Writing Guidelines

- **Write for agents, not humans.** Be explicit with canister IDs, function signatures, and error messages.
Expand All @@ -78,7 +146,7 @@ Results are saved to `evaluations/results/` (gitignored). See `evaluations/icp-c

## Categories

Known categories: Auth, Core, DeFi, Frontend, Governance, Infrastructure, Integration, Security. New categories are allowed — the validator warns but does not block.
Known categories: Auth, Core, DeFi, Frontend, Governance, Infrastructure, Integration, Motoko, Security. New categories are allowed — the validator warns but does not block.

## Tech Stack

Expand Down
7 changes: 7 additions & 0 deletions .github/repo_policies/BOT_APPROVED_FILES
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Files the pr-automation-bot-public bot is allowed to change via automated PRs.

# sync-upstream: opens sync-check PRs when upstream repos release new versions
skills/motoko/*
skills/migrating-motoko/*
skills/migrating-motoko-enhanced/*
skills/mops-cli/*
4 changes: 2 additions & 2 deletions .github/workflows/_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ jobs:
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
dirs=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- 'skills/' \
| grep -oP '^skills/\K[^/]+' \
| grep -oP '^skills/\K[^/]+(?=/)' \
| sort -u \
| grep -v '^_' \
| while read -r d; do [ -f "skills/$d/SKILL.md" ] && echo "$d"; done)
| while read -r d; do [ -f "skills/$d/SKILL.md" ] && echo "$d" || true; done)
if [ -n "$dirs" ]; then
# Build space-separated paths for skill-validator
paths=$(echo "$dirs" | sed 's|^|skills/|' | tr '\n' ' ')
Expand Down
257 changes: 257 additions & 0 deletions .github/workflows/sync-upstream.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
name: Upstream sync check

on:
schedule:
- cron: '0 8 * * 1' # Weekly on Monday
workflow_dispatch:

jobs:
check-motoko:
name: Check caffeinelabs/motoko
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Create GitHub App Token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
id: app-token
with:
client-id: ${{ vars.PR_AUTOMATION_BOT_PUBLIC_CLIENT_ID }}
private-key: ${{ secrets.PR_AUTOMATION_BOT_PUBLIC_PRIVATE_KEY }}

- name: Get latest motoko release tag
id: latest
run: |
TAG=$(gh release view --repo caffeinelabs/motoko --json tagName -q .tagName)
echo "tag=$TAG" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}

- name: Get current pinned tag
id: current
run: |
TAG=$(grep 'Tag:' skills/motoko/SKILL.md | head -1 | sed 's/.*Tag: \([^ ]*\).*/\1/')
echo "tag=$TAG" >> $GITHUB_OUTPUT

- name: Check if update needed
id: check
run: |
LATEST="${{ steps.latest.outputs.tag }}"
CURRENT="${{ steps.current.outputs.tag }}"
BRANCH="infra/sync-motoko-${LATEST}"
if [ "$LATEST" = "$CURRENT" ]; then
echo "needed=false" >> $GITHUB_OUTPUT
echo "Already at latest: $CURRENT"
elif git ls-remote --exit-code origin "refs/heads/${BRANCH}" > /dev/null 2>&1; then
echo "needed=false" >> $GITHUB_OUTPUT
echo "Branch $BRANCH already exists — PR likely open, skipping"
else
echo "needed=true" >> $GITHUB_OUTPUT
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
echo "New release: $LATEST (current: $CURRENT)"
fi

- name: Resolve commit SHA for new release
if: steps.check.outputs.needed == 'true'
id: sha
run: |
TAG="${{ steps.latest.outputs.tag }}"
RESULT=$(curl -sf "https://api.github.com/repos/caffeinelabs/motoko/git/ref/tags/${TAG}" \
-H "Authorization: Bearer $GH_TOKEN" | \
python3 -c "import sys,json; d=json.load(sys.stdin); print(d['object']['sha'], d['object']['type'])")
OBJ_SHA=$(echo "$RESULT" | awk '{print $1}')
OBJ_TYPE=$(echo "$RESULT" | awk '{print $2}')
if [ "$OBJ_TYPE" = "tag" ]; then
COMMIT=$(curl -sf "https://api.github.com/repos/caffeinelabs/motoko/git/tags/${OBJ_SHA}" \
-H "Authorization: Bearer $GH_TOKEN" | \
python3 -c "import sys,json; print(json.load(sys.stdin)['object']['sha'])")
else
COMMIT="$OBJ_SHA"
fi
echo "commit=$COMMIT" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}

- name: Fetch upstream files and build diff
if: steps.check.outputs.needed == 'true'
run: |
SHA="${{ steps.sha.outputs.commit }}"
CURRENT="${{ steps.current.outputs.tag }}"
LATEST="${{ steps.latest.outputs.tag }}"

declare -A UPSTREAM_TO_LOCAL=(
["writing-motoko"]="motoko"
["migrating-motoko"]="migrating-motoko"
["migrating-motoko-enhanced"]="migrating-motoko-enhanced"
)

{
echo "## Upstream diff: caffeinelabs/motoko \`${CURRENT}\` → \`${LATEST}\`"
echo ""
echo "Commit: [\`${SHA:0:12}\`](https://github.com/caffeinelabs/motoko/commit/${SHA})"
echo ""
echo "**Review instructions:** check which sections are listed as \`owned by icskills\` in each"
echo "skill's upstream comment block before applying changes. Do NOT overwrite those sections."
echo ""
} > /tmp/pr-body.md

for upstream_name in "writing-motoko" "migrating-motoko" "migrating-motoko-enhanced"; do
local_name="${UPSTREAM_TO_LOCAL[$upstream_name]}"
curl -sf "https://raw.githubusercontent.com/caffeinelabs/motoko/${SHA}/.agents/skills/${upstream_name}/SKILL.md" \
> /tmp/upstream-${upstream_name}.md 2>/dev/null || {
echo "(skill not found at this path)" > /tmp/upstream-${upstream_name}.md
}
DIFF=$(diff skills/${local_name}/SKILL.md /tmp/upstream-${upstream_name}.md || true)
if [ -n "$DIFF" ]; then
{
echo "### \`${local_name}\` ← upstream \`${upstream_name}\`"
echo ""
echo '<details><summary>Show diff</summary>'
echo ""
echo '```diff'
echo "$DIFF"
echo '```'
echo ""
echo '</details>'
echo ""
} >> /tmp/pr-body.md
else
echo "### \`${local_name}\` — no changes" >> /tmp/pr-body.md
echo "" >> /tmp/pr-body.md
fi
done

- name: Create sync PR
if: steps.check.outputs.needed == 'true'
run: |
BRANCH="${{ steps.check.outputs.branch }}"
git config user.name "pr-automation-bot-public[bot]"
git config user.email "pr-automation-bot-public[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
git commit --allow-empty -m "chore: upstream sync check — caffeinelabs/motoko ${{ steps.latest.outputs.tag }}"
git push -u origin "$BRANCH"
gh pr create \
--title "chore: sync check — caffeinelabs/motoko ${{ steps.latest.outputs.tag }}" \
--body-file /tmp/pr-body.md
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}

check-mops:
name: Check caffeinelabs/mops
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Create GitHub App Token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
id: app-token
with:
client-id: ${{ vars.PR_AUTOMATION_BOT_PUBLIC_CLIENT_ID }}
private-key: ${{ secrets.PR_AUTOMATION_BOT_PUBLIC_PRIVATE_KEY }}

- name: Get latest mops release tag
id: latest
run: |
TAG=$(gh release list --repo caffeinelabs/mops --limit 100 --json tagName --jq '[.[] | select(.tagName | startswith("cli-"))] | first | .tagName')
echo "tag=$TAG" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}

- name: Get current pinned tag
id: current
run: |
TAG=$(grep 'Tag:' skills/mops-cli/SKILL.md | head -1 | sed 's/.*Tag: \([^ ]*\).*/\1/')
echo "tag=$TAG" >> $GITHUB_OUTPUT

- name: Check if update needed
id: check
run: |
LATEST="${{ steps.latest.outputs.tag }}"
CURRENT="${{ steps.current.outputs.tag }}"
BRANCH="infra/sync-mops-${LATEST}"
if [ "$LATEST" = "$CURRENT" ]; then
echo "needed=false" >> $GITHUB_OUTPUT
echo "Already at latest: $CURRENT"
elif git ls-remote --exit-code origin "refs/heads/${BRANCH}" > /dev/null 2>&1; then
echo "needed=false" >> $GITHUB_OUTPUT
echo "Branch $BRANCH already exists — PR likely open, skipping"
else
echo "needed=true" >> $GITHUB_OUTPUT
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
echo "New release: $LATEST (current: $CURRENT)"
fi

- name: Resolve commit SHA for new release
if: steps.check.outputs.needed == 'true'
id: sha
run: |
TAG="${{ steps.latest.outputs.tag }}"
RESULT=$(curl -sf "https://api.github.com/repos/caffeinelabs/mops/git/ref/tags/${TAG}" \
-H "Authorization: Bearer $GH_TOKEN" | \
python3 -c "import sys,json; d=json.load(sys.stdin); print(d['object']['sha'], d['object']['type'])")
OBJ_SHA=$(echo "$RESULT" | awk '{print $1}')
OBJ_TYPE=$(echo "$RESULT" | awk '{print $2}')
if [ "$OBJ_TYPE" = "tag" ]; then
COMMIT=$(curl -sf "https://api.github.com/repos/caffeinelabs/mops/git/tags/${OBJ_SHA}" \
-H "Authorization: Bearer $GH_TOKEN" | \
python3 -c "import sys,json; print(json.load(sys.stdin)['object']['sha'])")
else
COMMIT="$OBJ_SHA"
fi
echo "commit=$COMMIT" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}

- name: Fetch upstream file and build diff
if: steps.check.outputs.needed == 'true'
run: |
SHA="${{ steps.sha.outputs.commit }}"
CURRENT="${{ steps.current.outputs.tag }}"
LATEST="${{ steps.latest.outputs.tag }}"

curl -sf "https://raw.githubusercontent.com/caffeinelabs/mops/${SHA}/.agents/skills/mops-cli/SKILL.md" \
> /tmp/upstream-mops-cli.md 2>/dev/null || {
echo "(skill not found at this path)" > /tmp/upstream-mops-cli.md
}

DIFF=$(diff skills/mops-cli/SKILL.md /tmp/upstream-mops-cli.md || true)

{
echo "## Upstream diff: caffeinelabs/mops \`${CURRENT}\` → \`${LATEST}\`"
echo ""
echo "Commit: [\`${SHA:0:12}\`](https://github.com/caffeinelabs/mops/commit/${SHA})"
echo ""
echo "**Review instructions:** check which sections are listed as \`owned by icskills\` in the"
echo "skill's upstream comment block before applying changes. Do NOT overwrite those sections."
echo ""
echo "### \`mops-cli\`"
echo ""
if [ -n "$DIFF" ]; then
echo '<details><summary>Show diff</summary>'
echo ""
echo '```diff'
echo "$DIFF"
echo '```'
echo ""
echo '</details>'
else
echo "No changes."
fi
} > /tmp/pr-body.md

- name: Create sync PR
if: steps.check.outputs.needed == 'true'
run: |
BRANCH="${{ steps.check.outputs.branch }}"
git config user.name "pr-automation-bot-public[bot]"
git config user.email "pr-automation-bot-public[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
git commit --allow-empty -m "chore: upstream sync check — caffeinelabs/mops ${{ steps.latest.outputs.tag }}"
git push -u origin "$BRANCH"
gh pr create \
--title "chore: sync check — caffeinelabs/mops ${{ steps.latest.outputs.tag }}" \
--body-file /tmp/pr-body.md
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,6 @@ The website auto-generates from SKILL.md frontmatter — no need to edit any sou

Use an existing category when possible. The validator warns on unknown categories to catch typos, but new categories are not blocked.

Current categories: **Auth**, **Core**, **DeFi**, **Frontend**, **Governance**, **Infrastructure**, **Integration**, **Security**
Current categories: **Auth**, **Core**, **DeFi**, **Frontend**, **Governance**, **Infrastructure**, **Integration**, **Motoko**, **Security**

To add a new category: update the enum in `skills/skill.schema.json` and the icon in `src/components/Icons.tsx`.
To add a new category: update the description string in `skills/skill.schema.json`, the `KNOWN_CATEGORIES` array in `scripts/check-project.js`, and the `CATEGORY_ORDER` array in `src/lib/skills.ts`.
Loading
Loading