chore: import upstream snapshot with attribution
Build Backend Images / generate-matrix (push) Waiting to run
Build Backend Images / build-and-push (push) Blocked by required conditions
Build Web Image / build (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:01:53 +08:00
commit e6402c600b
3410 changed files with 563062 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "aitoearn-web",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["run", "dev"],
"port": 6060,
"cwd": "project/aitoearn-web"
},
{
"name": "aitoearn-server",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["nx", "serve", "aitoearn-server"],
"port": 3002,
"cwd": "project/aitoearn-backend"
},
{
"name": "aitoearn-ai",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["nx", "serve", "aitoearn-ai"],
"port": 3010,
"cwd": "project/aitoearn-backend"
}
]
}
+62
View File
@@ -0,0 +1,62 @@
name: "🕷️ Bug report"
description: Report errors or unexpected behavior
labels:
- bug
body:
- type: checkboxes
attributes:
label: Self Checks
description: "Please review the following before submitting. We recommend completing as many items as possible."
options:
- label: I have read the [Contributing Guide](https://github.com/yikart/AiToEarn/blob/main/CONTRIBUTING.md).
required: false
- label: I have searched for existing issues [search for existing issues](https://github.com/yikart/AiToEarn/issues), including closed ones.
required: false
- label: I can provide the report in English if needed to help more contributors participate in the discussion.
required: false
- label: "Please do not modify this template :) and fill in all the required fields."
required: true
- type: input
attributes:
label: Aitoearn version
description:
validations:
required: true
- type: dropdown
attributes:
label: Please select your platform
description:
multiple: true
options:
- Android
- Windows
- Mac
- Web
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: We highly suggest including screenshots and a bug report log. Please use the right markdown syntax for code blocks.
placeholder: Having detailed steps helps us reproduce the bug. If you have logs, please use fenced code blocks (triple backticks ```) to format them.
validations:
required: true
- type: textarea
attributes:
label: ✔️ Expected Behavior
description: Describe what you expected to happen.
placeholder: What were you expecting? Please do not copy and paste the steps to reproduce here.
validations:
required: true
- type: textarea
attributes:
label: ❌ Actual Behavior
description: Describe what actually happened.
placeholder: What happened instead? Please do not copy and paste the steps to reproduce here.
validations:
required: false
+40
View File
@@ -0,0 +1,40 @@
name: General Issue
description: Report a bug, request a feature, or ask a question
title: "[Issue] "
labels: []
body:
- type: markdown
attributes:
value: |
### What happened?
(One short sentence)
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Describe the issue in one short sentence.
placeholder: Something happened...
validations:
required: true
- type: checkboxes
id: type
attributes:
label: Type
options:
- label: Bug
- label: Idea
- label: Improvement
- label: Question
validations:
required: true
- type: textarea
id: extra
attributes:
label: Extra (optional)
description: Screenshot / steps to reproduce / environment
placeholder: Attach screenshot / describe steps...
validations:
required: false
@@ -0,0 +1,40 @@
name: "⭐ Feature or enhancement request"
description: Propose something new.
labels:
- enhancement
body:
- type: checkboxes
attributes:
label: Self Checks
description: "Please review the following before submitting. We recommend completing as many items as possible."
options:
- label: I have read the [Contributing Guide](https://github.com/yikart/AiToEarn/blob/main/CONTRIBUTING.md).
required: false
- label: I have searched for existing issues [search for existing issues](https://github.com/yikart/AiToEarn/issues), including closed ones.
required: false
- label: I can provide the request in English if needed to help more contributors participate in the discussion.
required: false
- label: "Please do not modify this template :) and fill in all the required fields."
required: true
- type: textarea
attributes:
label: 1. Is this request related to a challenge you're experiencing? Tell me about your story.
placeholder: Please describe the specific scenario or problem you're facing as clearly as possible. For instance "I was trying to use [feature] for [specific task], and [what happened]... It was frustrating because...."
validations:
required: true
- type: textarea
attributes:
label: 2. Additional context or comments
placeholder: (Any other information, comments, documentations, links, or screenshots that would provide more clarity. This is the place to add anything else not covered above.)
validations:
required: false
- type: checkboxes
attributes:
label: 3. Can you help us with this feature?
description: Let us know! This is not a commitment, but a starting point for collaboration.
options:
- label: I am interested in contributing to this feature.
required: false
- type: markdown
attributes:
value: You may include related requests in one issue when they belong to the same use case or problem context.
+39
View File
@@ -0,0 +1,39 @@
name: "✨ Refactor"
description: Refactor existing code for improved readability and maintainability.
labels:
- refactor
body:
- type: checkboxes
attributes:
label: Self Checks
description: "Please review the following before submitting. We recommend completing as many items as possible."
options:
- label: I have read the [Contributing Guide](https://github.com/yikart/AiToEarn/blob/main/CONTRIBUTING.md).
required: false
- label: I have searched for existing issues [search for existing issues](https://github.com/yikart/AiToEarn/issues), including closed ones.
required: false
- label: I can provide the request in English if needed to help more contributors participate in the discussion.
required: false
- label: "Please do not modify this template :) and fill in all the required fields."
required: true
- type: textarea
id: description
attributes:
label: Description
placeholder: "Describe the refactor you are proposing."
validations:
required: true
- type: textarea
id: motivation
attributes:
label: Motivation
placeholder: "Explain why this refactor is necessary."
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional Context
placeholder: "Add any other context or screenshots about the request here."
validations:
required: false
+2
View File
@@ -0,0 +1,2 @@
l10n:
default_locale: zh-CN
+19
View File
@@ -0,0 +1,19 @@
# Pull Request
## 🧩 Issue Link
<!--
Required: Please link at least one Issue.
Closing keywords: Closes #123 / Fixes #123 / Resolves #123
Reference only: Related to #123 / Refs #123
Multiple links supported: Closes #12, Related to #34
Cross-repo: Closes owner/repo#123
-->
Closes #
## 📝 Description
<!-- Briefly describe the purpose of this PR and the main changes introduced. -->
+73
View File
@@ -0,0 +1,73 @@
name: AitoEarn Backend Check
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: read
env:
NX_NO_CLOUD: true
MONOREPO_DIR: project/aitoearn-backend
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint-and-build:
name: Lint & Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if backend files changed
id: check-changes
run: |
if git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -q "^project/aitoearn-backend/"; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi
- name: Skip if no changes
if: steps.check-changes.outputs.changed == 'false'
run: |
echo "No changes in project/aitoearn-backend/, skipping checks"
exit 0
- name: Install pnpm
if: steps.check-changes.outputs.changed == 'true'
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
if: steps.check-changes.outputs.changed == 'true'
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: ${{ env.MONOREPO_DIR }}/pnpm-lock.yaml
- name: Install dependencies
if: steps.check-changes.outputs.changed == 'true'
working-directory: ${{ env.MONOREPO_DIR }}
run: pnpm install --frozen-lockfile
- name: Run ESLint
if: steps.check-changes.outputs.changed == 'true'
working-directory: ${{ env.MONOREPO_DIR }}
run: pnpm nx run-many --target=lint --no-fix --nxBail --outputStyle=static
- name: Run build
if: steps.check-changes.outputs.changed == 'true'
working-directory: ${{ env.MONOREPO_DIR }}
run: pnpm nx run-many --target=build --nxBail --outputStyle=static
+352
View File
@@ -0,0 +1,352 @@
name: Build Backend Images
on:
push:
branches:
- main
paths:
- 'project/aitoearn-backend/**'
- '.github/workflows/backend-build.yml'
workflow_dispatch:
inputs:
build_aitoearn_ai:
description: 'aitoearn-ai'
required: false
type: boolean
default: false
build_aitoearn_server:
description: 'aitoearn-server'
required: false
type: boolean
default: false
push_latest:
description: 'Push as latest tag'
required: false
type: boolean
default: false
workflow_call:
inputs:
base_ref:
description: 'Base branch for affected detection (for PR preview)'
required: false
type: string
default: ''
manual_apps:
description: 'Comma-separated list of apps to build (for manual deployment)'
required: false
type: string
default: ''
outputs:
has_changes:
description: 'Whether any apps were affected'
value: ${{ jobs.generate-matrix.outputs.has_changes }}
permissions:
contents: read
actions: write
env:
DOCKER_HUB_USERNAME: aitoearn
NX_NO_CLOUD: true
MONOREPO_DIR: project/aitoearn-backend
jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
has_changes: ${{ steps.set-matrix.outputs.has_changes }}
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: ${{ env.MONOREPO_DIR }}/pnpm-lock.yaml
- name: Install dependencies
working-directory: ${{ env.MONOREPO_DIR }}
run: pnpm install --frozen-lockfile
- name: Parse manual app selection
id: parse-manual
run: |
MANUAL_APPS=""
# If manual_apps passed via workflow_call, use directly
if [ -n "${{ inputs.manual_apps }}" ]; then
MANUAL_APPS="${{ inputs.manual_apps }}"
echo "✅ Using workflow_call manual apps: $MANUAL_APPS"
else
# Parse workflow_dispatch boolean inputs
if [ "${{ inputs.build_aitoearn_ai }}" = "true" ]; then
MANUAL_APPS="aitoearn-ai"
fi
if [ "${{ inputs.build_aitoearn_server }}" = "true" ]; then
if [ -n "$MANUAL_APPS" ]; then
MANUAL_APPS="$MANUAL_APPS,aitoearn-server"
else
MANUAL_APPS="aitoearn-server"
fi
fi
if [ -n "$MANUAL_APPS" ]; then
echo "✅ Manually selected apps: $MANUAL_APPS"
else
echo "⚠️ No apps manually selected"
fi
fi
echo "manual_apps=$MANUAL_APPS" >> $GITHUB_OUTPUT
- name: Detect affected apps
id: detect-changes
working-directory: ${{ env.MONOREPO_DIR }}
run: |
MANUAL_APPS="${{ steps.parse-manual.outputs.manual_apps }}"
# Check if manual mode
if [ -n "$MANUAL_APPS" ]; then
echo "🎯 Manual build mode: Using specified app list"
AFFECTED_APPS="$MANUAL_APPS"
echo "✅ Manually selected apps: $AFFECTED_APPS"
echo "affected_apps=$AFFECTED_APPS" >> $GITHUB_OUTPUT
echo ""
echo "Details:"
for APP in $(echo "$AFFECTED_APPS" | tr ',' ' '); do
echo " - $APP"
done
else
# Set base branch based on trigger type
if [ -n "${{ inputs.base_ref }}" ]; then
BASE_REF="origin/${{ inputs.base_ref }}"
echo "📋 PR mode: Detecting changes from $BASE_REF to HEAD"
else
BASE_REF="HEAD~1"
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "📋 Manual trigger mode: Detecting changes from last commit"
else
echo "📋 Push mode: Detecting changes from last commit"
fi
fi
echo "Base: $BASE_REF"
echo "Head: HEAD"
echo ""
echo "📊 Diagnostic:"
git log --oneline -3
echo ""
echo "Changed files:"
git diff --name-only $BASE_REF HEAD 2>/dev/null || true
echo ""
NX_OUTPUT=$(pnpm nx show projects --affected --type=app --sep "," --exclude="@yikart/source" --base=$BASE_REF --head=HEAD 2>&1)
NX_EXIT_CODE=$?
echo "Nx exit code: $NX_EXIT_CODE"
echo "Nx output: $NX_OUTPUT"
if [ $NX_EXIT_CODE -ne 0 ]; then
echo "⚠️ Nx affected detection failed (exit code: $NX_EXIT_CODE)"
echo "Falling back to building ALL apps"
AFFECTED_APPS="aitoearn-ai,aitoearn-server"
else
AFFECTED_APPS="$NX_OUTPUT"
fi
if [ -z "$AFFECTED_APPS" ]; then
echo "️ No affected apps detected"
echo "affected_apps=" >> $GITHUB_OUTPUT
else
echo "✅ Affected apps: $AFFECTED_APPS"
echo "affected_apps=$AFFECTED_APPS" >> $GITHUB_OUTPUT
echo ""
echo "Details:"
for APP in $(echo "$AFFECTED_APPS" | tr ',' ' '); do
echo " - $APP"
done
fi
fi
- name: Generate version
id: version
run: |
VERSION="$(date +%Y%m%d)-$(git rev-parse --short HEAD)"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "📦 Version: $VERSION"
- name: Generate matrix
id: set-matrix
working-directory: ${{ env.MONOREPO_DIR }}
run: |
AFFECTED_APPS="${{ steps.detect-changes.outputs.affected_apps }}"
VERSION="${{ steps.version.outputs.version }}"
if [ -z "$AFFECTED_APPS" ]; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "matrix={\"include\":[]}" >> $GITHUB_OUTPUT
echo "⚠️ No changes, skipping build"
exit 0
fi
echo "has_changes=true" >> $GITHUB_OUTPUT
# Determine if should push latest tag
PUSH_LATEST=false
if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then
PUSH_LATEST=true
echo "📌 Main branch push: Will push latest tag"
elif [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.push_latest }}" = "true" ]; then
PUSH_LATEST=true
echo "📌 Manual trigger with push_latest: Will push latest tag"
fi
echo "push_latest=$PUSH_LATEST" >> $GITHUB_OUTPUT
# Build matrix JSON array
MATRIX_JSON="{"
MATRIX_JSON="$MATRIX_JSON\"include\":["
FIRST=true
IFS=',' read -ra APPS <<< "$AFFECTED_APPS"
for APP in "${APPS[@]}"; do
IMAGE_TAG="${{ env.DOCKER_HUB_USERNAME }}/$APP:$VERSION"
# Add to matrix
if [ "$FIRST" = true ]; then
FIRST=false
else
MATRIX_JSON="$MATRIX_JSON,"
fi
MATRIX_JSON="$MATRIX_JSON{\"app\":\"$APP\",\"image\":\"$IMAGE_TAG\",\"push_latest\":$PUSH_LATEST}"
done
MATRIX_JSON="$MATRIX_JSON]}"
echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT
echo ""
echo "🎯 Matrix configuration:"
echo "$MATRIX_JSON" | jq .
build-and-push:
needs: generate-matrix
if: needs.generate-matrix.outputs.has_changes == 'true'
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: ${{ env.MONOREPO_DIR }}/pnpm-lock.yaml
- name: Install dependencies
working-directory: ${{ env.MONOREPO_DIR }}
run: pnpm install --frozen-lockfile
- name: Restore Nx cache
uses: actions/cache@v4
with:
path: ${{ env.MONOREPO_DIR }}/.nx/cache
key: nx-${{ runner.os }}-${{ hashFiles('project/aitoearn-backend/pnpm-lock.yaml') }}-${{ matrix.app }}-${{ github.sha }}
restore-keys: |
nx-${{ runner.os }}-${{ hashFiles('project/aitoearn-backend/pnpm-lock.yaml') }}-${{ matrix.app }}-
nx-${{ runner.os }}-${{ hashFiles('project/aitoearn-backend/pnpm-lock.yaml') }}-
nx-${{ runner.os }}-
- name: Build application
working-directory: ${{ env.MONOREPO_DIR }}
run: |
echo "🔨 Building app: ${{ matrix.app }}"
pnpm nx run ${{ matrix.app }}:build
- name: Prepare Docker context
working-directory: ${{ env.MONOREPO_DIR }}
run: pnpm nx run ${{ matrix.app }}:docker-context
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
network=host
image=moby/buildkit:latest
buildkitd-flags: --debug
- name: Prepare tags
id: tags
run: |
TAGS="${{ matrix.image }}"
if [ "${{ matrix.push_latest }}" = "true" ]; then
TAGS="$TAGS"$'\n'"${{ env.DOCKER_HUB_USERNAME }}/${{ matrix.app }}:latest"
fi
echo "tags<<EOF" >> $GITHUB_OUTPUT
echo "$TAGS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: ${{ env.MONOREPO_DIR }}/tmp/docker-context
file: ${{ env.MONOREPO_DIR }}/tmp/docker-context/Dockerfile
platforms: linux/amd64,linux/arm64
build-args: |
APP_NAME=${{ matrix.app }}
tags: ${{ steps.tags.outputs.tags }}
push: true
cache-from: |
type=gha,scope=${{ matrix.app }}
type=registry,ref=${{ env.DOCKER_HUB_USERNAME }}/${{ matrix.app }}:buildcache
cache-to: |
type=gha,mode=max,scope=${{ matrix.app }}
type=inline
provenance: false
sbom: false
- name: Build summary
run: |
echo "### ✅ Build successful: ${{ matrix.app }}" >> $GITHUB_STEP_SUMMARY
echo "- **Image**: \`${{ matrix.image }}\`" >> $GITHUB_STEP_SUMMARY
if [ "${{ matrix.push_latest }}" = "true" ]; then
echo "- **Latest**: \`${{ env.DOCKER_HUB_USERNAME }}/${{ matrix.app }}:latest\`" >> $GITHUB_STEP_SUMMARY
fi
+54
View File
@@ -0,0 +1,54 @@
name: PR Issue Check
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
pull-requests: read
issues: read
jobs:
check-issue-linked:
name: Check Issue Linked
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check if PR is linked to an issue
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = context.payload.pull_request.number;
let prBody = context.payload.pull_request.body || '';
const prTitle = context.payload.pull_request.title || '';
const conventionalCommitsPattern = /^(feat|fix|docs|style|refactor|perf|test|chore|ci|build)(\(.+\))?: .+$/;
const validTypes = ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'ci', 'build'];
if (!conventionalCommitsPattern.test(prTitle)) {
const errorMessage =
'❌ PR title does not follow Conventional Commits format.\n\n' +
'Required format: <type>(<scope>): <description>\n\n' +
'Valid types:\n' +
validTypes.map(t => ` - ${t}`).join('\n') + '\n\n' +
'Examples:\n' +
' ✅ feat: add user authentication\n' +
' ✅ fix(web): resolve login button issue\n' +
' ✅ docs: update API documentation\n' +
' ✅ refactor(backend): improve error handling\n' +
' ✅ chore: update dependencies\n\n' +
'Common mistakes:\n' +
' ❌ "Add new feature" (missing type and colon)\n' +
' ❌ "feat add feature" (missing colon)\n' +
' ❌ "feat: " (missing description)\n' +
' ❌ "feature: add something" (invalid type)\n\n' +
`Current title: "${prTitle}"\n\n` +
'Please update your PR title to follow the Conventional Commits specification.';
core.setFailed(errorMessage);
return;
}
+17
View File
@@ -0,0 +1,17 @@
name: Feishu Notify (Org-wide, No Relay)
on:
pull_request:
types: [ready_for_review, closed]
issues:
types: [opened, closed, reopened]
jobs:
notify:
uses: yikart/.github/.github/workflows/feishu-notify-template.yml@main
secrets: inherit
with:
important_labels: "urgent,security,release-blocker"
important_paths_regex: "^(apps/web/|infra/|payment/|security/)"
pr_min_changed_files: "30"
title_keywords_regex: "(hotfix|rollback|security|incident)"
notify_issue_on_open_close_without_labels: "false"
+108
View File
@@ -0,0 +1,108 @@
name: Build Web Image
on:
push:
branches:
- main
paths:
- 'project/aitoearn-web/**'
- '.github/workflows/web-build.yml'
workflow_dispatch:
inputs:
version:
description: Image version (leave empty to use default generation rule)
required: false
type: string
push_latest:
description: 'Push as latest tag'
required: false
type: boolean
default: false
env:
APP_NAME: aitoearn-web
MONOREPO_DIR: project/aitoearn-web
DOCKER_HUB_USERNAME: aitoearn
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Generate version
id: version
run: |
if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
else
VERSION=$(date +%Y%m%d)-$(git rev-parse --short HEAD)
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Determine push latest
id: push-latest
run: |
PUSH_LATEST=false
if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then
PUSH_LATEST=true
echo "📌 Main branch push: Will push latest tag"
elif [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ github.event.inputs.push_latest }}" = "true" ]; then
PUSH_LATEST=true
echo "📌 Manual trigger with push_latest: Will push latest tag"
fi
echo "push_latest=$PUSH_LATEST" >> $GITHUB_OUTPUT
- name: Prepare tags
id: tags
run: |
TAGS="${{ env.DOCKER_HUB_USERNAME }}/${{ env.APP_NAME }}:${{ steps.version.outputs.version }}"
if [ "${{ steps.push-latest.outputs.push_latest }}" = "true" ]; then
TAGS="$TAGS"$'\n'"${{ env.DOCKER_HUB_USERNAME }}/${{ env.APP_NAME }}:latest"
fi
echo "tags<<EOF" >> $GITHUB_OUTPUT
echo "$TAGS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Build and push application
uses: docker/build-push-action@v5
with:
context: ${{ env.MONOREPO_DIR }}/
file: ${{ env.MONOREPO_DIR }}/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.tags.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Summary
if: always()
run: |
echo "## 📦 Build Summary" >> $GITHUB_STEP_SUMMARY
echo "- **App**: ${{ env.APP_NAME }}" >> $GITHUB_STEP_SUMMARY
echo "- **Version**: ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Platform**: linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🐋 Docker Hub Images" >> $GITHUB_STEP_SUMMARY
echo "- **Version Image**: \`${{ env.DOCKER_HUB_USERNAME }}/${{ env.APP_NAME }}:${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.push-latest.outputs.push_latest }}" = "true" ]; then
echo "- **Latest**: \`${{ env.DOCKER_HUB_USERNAME }}/${{ env.APP_NAME }}:latest\`" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🔗 Quick Links" >> $GITHUB_STEP_SUMMARY
echo "- [Source Commit](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})" >> $GITHUB_STEP_SUMMARY
echo "- [Build Log](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
+73
View File
@@ -0,0 +1,73 @@
name: AitoEarn Web Check
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: read
env:
MONOREPO_DIR: project/aitoearn-web
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint-and-build:
name: Lint & Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if web files changed
id: check-changes
run: |
if git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -q "^project/aitoearn-web/"; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi
- name: Skip if no changes
if: steps.check-changes.outputs.changed == 'false'
run: |
echo "No changes in project/aitoearn-web/, skipping checks"
exit 0
- name: Install pnpm
if: steps.check-changes.outputs.changed == 'true'
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
if: steps.check-changes.outputs.changed == 'true'
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: ${{ env.MONOREPO_DIR }}/pnpm-lock.yaml
- name: Install dependencies
if: steps.check-changes.outputs.changed == 'true'
working-directory: ${{ env.MONOREPO_DIR }}
run: pnpm install --frozen-lockfile
# - name: Run ESLint
# if: steps.check-changes.outputs.changed == 'true'
# working-directory: ${{ env.MONOREPO_DIR }}
# run: pnpm lint
- name: Run build
if: steps.check-changes.outputs.changed == 'true'
working-directory: ${{ env.MONOREPO_DIR }}
run: pnpm build
+47
View File
@@ -0,0 +1,47 @@
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
dist-electron
release
*.local
.vscode
# Editor directories and files
.vscode/.debug.env
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# .env
#lockfile
# package-lock.json
# pnpm-lock.yaml
# yarn.lock
/test-results/
/playwright-report/
/playwright/.cache/
# 忽略所有.db文件
*.db
# 忽略所有.exe文件
*.exe
# 忽略/public/bin目录下的非.md文件
public/bin/*
!public/bin/webkit
!public/bin/*.md
sh/*
project/web/.cursor/
+35
View File
@@ -0,0 +1,35 @@
# AGENTS.md
本文件定义 Codex 在 `AiToEarn` 仓库内的默认工作规则。
## Communication
- 默认使用简体中文回复。
## Project Layout
- `project/aitoearn-backend` 是 Nx + pnpm 后端工作区。
- `project/aitoearn-web` 是 Next.js + pnpm 前端项目。
- 根目录主要维护 README、Docker 部署文档、`docker-compose.yml` 和展示资源。
## Package & Command Rules
- backend/web 使用 `pnpm`
- 根目录没有统一 package,不要在根目录随手执行 install/build。
- backend 改动优先在 `project/aitoearn-backend``pnpm nx ...` 验证,并遵循 `project/aitoearn-backend/CLAUDE.md`
- web 改动在 `project/aitoearn-web` 验证,优先使用 `pnpm run type-check``pnpm build`
- 纯文档改动至少运行 `git diff --check`
## Documentation Rules
- 根 README 对外文档包含 `README.md``README_EN.md``README_JA.md`;涉及用户可见能力、安装、OpenClaw、MCP、Relay、API Key 或环境地址时默认三语同步。
- Docker 部署说明涉及生产部署、环境变量或 `docker compose` 时,同步检查 `DOCKER_DEPLOYMENT_CN.md``DOCKER_DEPLOYMENT_EN.md`
- README 类改动保持最小可用改写,不要把参考文档整段复制进来。
- 用户可见 README、skill、capability reference 只写当前能力与环境规则,不写 `dev`、测试环境、验证日期等来源说明。
## Environment Rules
- OpenClaw、MCP、Relay 都必须明确区分中国版和国际版环境:`*.aitoearn.cn` 属于中国版,`*.aitoearn.ai` 属于国际版。
- 中国版 API Key 只能搭配 `aitoearn.cn` 相关 URL;国际版 API Key 只能搭配 `aitoearn.ai` 相关 URL。环境和 Key 不匹配会导致 401。
- MCP 示例需要按环境区分 `https://aitoearn.cn/api/unified/mcp` / `https://aitoearn.ai/api/unified/mcp`SSE 示例同理区分 `/api/unified/sse`
- Relay 示例需要按 `RELAY_API_KEY` 来源选择 `RELAY_SERVER_URL`:中国版使用 `https://aitoearn.cn/api`,国际版使用 `https://aitoearn.ai/api`
+102
View File
@@ -0,0 +1,102 @@
# CONTRIBUTING
So you're looking to contribute to AiToEarn - that's awesome! We can't wait to see what you do. We have grand ambitions to build the best platform for AI-driven earning. Any help from the community counts, truly.
We need to be nimble and ship fast, but we also want to make sure that contributors like you get as smooth an experience as possible. We've assembled this contribution guide for that purpose, aiming at getting you familiarized with the codebase & how we work with contributors, so you could quickly jump to the fun part.
This guide is a constant work in progress. We highly appreciate your understanding if at times it lags behind the actual project, and welcome any feedback for us to improve.
## Before you jump in
Looking for something to tackle? Browse our [issues](https://github.com/AiToEarn/AiToEarn/issues) and pick one to get started!
### Good first issue
Issues with titles containing **【good frist】** are issues we provide for new contributors. If you want to join our team, please submit a PR linked to such an issue.
Join us, contribute code, and let's build something awesome together! 💡✨
Don't forget to link an existing issue in the PR's description.
### Bug reports
> [!IMPORTANT]
> Please make sure to include the following information when submitting a bug report:
- A clear and descriptive title
- A detailed description of the bug, including any error messages
- Steps to reproduce the bug
- Expected behavior
- **Logs**, if available (really important for backend issues)
- Screenshots or videos, if applicable
How we prioritize:
| Issue Type | Priority |
| ------------------------------------------------------------ | --------------- |
| Bugs in core functions (cannot login, applications not working, security loopholes) | Critical |
| Non-critical bugs, performance boosts | Medium Priority |
| Minor fixes (typos, confusing but working UI) | Low Priority |
### Feature requests
> [!NOTE]
> Please make sure to include the following information when submitting a feature request:
- A clear and descriptive title
- A detailed description of the feature
- A use case for the feature
- Any other context or screenshots about the feature request
How we prioritize:
| Feature Type | Priority |
| ------------------------------------------------------------ | --------------- |
| High-Priority Features as being labeled by a team member | High Priority |
| Popular feature requests from our community | Medium Priority |
| Non-core features and minor enhancements | Low Priority |
| Valuable but not immediate | Future-Feature |
## Development Environment Setup
1. Fork the project to your own GitHub account
2. Create a feature branch:
```bash
git checkout -b feature/your-feature-name
```
3. Commit your changes:
```bash
git add .
git commit -m "feat: add new feature"
```
4. Push to your forked repository:
```bash
git push origin feature/your-feature-name
```
5. Create a Pull Request on GitHub
## Submitting your PR
### Pull Request Process
1. Fork the repository
2. Before you draft a PR, please create an issue to discuss the changes you want to make
3. Create a new branch for your changes
4. Please add tests for your changes accordingly
5. Ensure your code passes the existing tests
6. Please link the issue in the PR description, `fixes #<issue_number>`
7. Get merged!
### Setup the project
#### Backend
For setting up the backend service, kindly refer to our detailed instructions in [project/backend/DEVELOPER_GUIDE.md](./project/backend/DEVELOPER_GUIDE.md). This document contains step-by-step guidance to help you get the backend up and running smoothly.
#### Other things to note
We recommend reviewing these documents carefully before proceeding with the setup.
## Getting Help
If you ever get stuck or get a burning question while contributing, simply shoot your queries our way via the related GitHub issue.
+102
View File
@@ -0,0 +1,102 @@
# 贡献指南
很高兴你有兴趣为 AiToEarn 做出贡献——这太棒了!我们迫不及待想看到你的成果。我们致力于构建最佳的 AI 赚钱平台。来自社区的任何帮助都非常宝贵。
我们需要快速迭代,但也希望确保像你一样的贡献者能获得尽可能顺畅的体验。我们编写这份贡献指南正是为了这个目的,旨在让你熟悉代码库以及我们与贡献者的合作方式,以便你可以快速进入正题。
本指南是一个持续完善的过程。如果有时它滞后于实际项目,我们非常感谢你的理解,并欢迎提出任何改进反馈。
## 在开始之前
正在寻找可以解决的问题?浏览我们的 [issues](https://github.com/AiToEarn/AiToEarn/issues) 并挑选一个开始吧!
### 友好的第一次开始
标题包含 **【good frist】** 的 issues,是我们为新的贡献者提供的issues。如果想加入我们的团队,请提交关联该 issues 的 PR。
加入我们,通过贡献代码,让我们一起构建一些很棒的东西!💡✨
别忘了在 PR 描述中链接现有的 issue
### 错误报告 (Bug Reports)
> [!IMPORTANT]
> 提交错误报告时,请务必包含以下信息:
- 清晰且描述性的标题
- 错误的详细描述,包括任何错误消息
- 复现错误的步骤
- 预期行为
- **日志**(如果有),对于后端问题,这非常重要
- 截图或视频(如果适用)
我们的优先级排序方式:
| 问题类型 | 优先级 |
| ------------------------------------------------------------ | --------------- |
| 核心功能错误(无法登录、应用无法工作、安全漏洞) | 严重 (Critical) |
| 非严重错误、性能提升 | 中等优先级 (Medium Priority) |
| 小修补(拼写错误、UI 令人困惑但可工作) | 低优先级 (Low Priority) |
### 功能请求 (Feature Requests)
> [!NOTE]
> 提交功能请求时,请务必包含以下信息:
- 清晰且描述性的标题
- 功能的详细描述
- 功能的使用场景
- 关于功能请求的任何其他背景信息或截图
我们的优先级排序方式:
| 功能类型 | 优先级 |
| ------------------------------------------------------------ | --------------- |
| 团队成员标记为高优先级的功能 | 高优先级 (High Priority) |
| 社区反馈中受欢迎的功能请求 | 中等优先级 (Medium Priority) |
| 非核心功能和小幅增强 | 低优先级 (Low Priority) |
| 有价值但不紧急 | 未来特性 (Future-Feature) |
## 开发环境设置
1. Fork 项目到你的 GitHub 账号
2. 创建特性分支:
```bash
git checkout -b feature/your-feature-name
```
3. 提交你的更改:
```bash
git add .
git commit -m "feat: add new feature"
```
4. 推送到你的 Fork 仓库:
```bash
git push origin feature/your-feature-name
```
5. 在 GitHub 上创建 Pull Request
## 提交你的 PR
### Pull Request 流程
1. Fork 本仓库
2. 在起草 PR 之前,请创建一个 issue 来讨论你想要进行的更改
3. 为你的更改创建一个新分支
4. 请相应地为你的更改添加测试
5. 确保你的代码通过现有的测试
6. 请在 PR 描述中链接该 issue,例如 `fixes #<issue_number>`
7. 等待合并!
### 项目设置
#### 后端
关于设置后端服务,请参考 [project/backend/README.md](./project/backend/README.md) 中的详细说明。该文档包含逐步指导,帮助你顺利运行后端。
#### 其他注意事项
我们建议在继续设置之前仔细阅读这些文档。
## 获取帮助
如果你在贡献过程中遇到困难或有紧急问题,只需通过相关的 GitHub issue 向我们提问即可。
+137
View File
@@ -0,0 +1,137 @@
# AiToEarn Docker 部署指南
本指南帮助你使用 Docker Compose 快速部署完整的 AiToEarn 应用。
## 服务架构
```
┌──────────┐
│ Nginx │
│ :8080 │
└────┬─────┘
┌───────────────┼───────────────┐
│ │ │
┌─────┴─────┐ ┌─────┴──────┐ ┌─────┴─────┐
│ Web (FE) │ │ Server │ │ AI │
│ :3000 │ │ :3002 │ │ :3010 │
└────────────┘ └──────┬─────┘ └─────┬─────┘
│ │
┌────────────┼──────────────┤
│ │ │
┌────┴─────┐ ┌───┴────┐ ┌──────┴───┐
│ MongoDB │ │ Redis │ │ RustFS │
│ :27017 │ │ :6379 │ │ :9000/01 │
└──────────┘ └────────┘ └──────────┘
```
| 服务 | 说明 | 端口 |
|------|------|------|
| **Nginx** | 反向代理,统一入口 | 8080 (对外) |
| **aitoearn-web** | Next.js 前端 | 3000 (内部) |
| **aitoearn-server** | NestJS 主后端 API | 3002 (内部) |
| **aitoearn-ai** | NestJS AI 服务 | 3010 (内部) |
| **MongoDB** | 数据库 | 27017 |
| **Redis** | 缓存/队列 | 6379 |
| **RustFS** | S3 兼容对象存储 | 9000 (API) / 9001 (控制台) |
## 前置要求
- **Docker**: 20.10+
- **Docker Compose**: 2.0+
- **系统内存**: 建议 4GB+
- **磁盘空间**: 建议 20GB+
验证安装:
```bash
docker --version
docker compose version
```
---
## 🚀 3 分钟快速启动
只需 3 步,即可在本地跑起完整的 AiToEarn。
### 第 1 步:克隆并启动
```bash
git clone https://github.com/yikart/AiToEarn.git
cd AiToEarn
docker compose up -d
```
首次启动会拉取镜像,可能需要几分钟。运行 `docker compose ps` 确认所有服务为 `healthy``running`
### 第 2 步:打开界面
启动成功后,打开浏览器访问:**[http://localhost:8080](http://localhost:8080)**
> 首次启动会自动创建管理员账号并自动登录,无需手动注册。
### 第 3 步:配置 Relay 中继(强烈推荐)
> **为什么要配 Relay**
>
> AiToEarn 需要登录你的社交媒体账号(抖音、小红书、TikTok 等)才能发布内容。这些平台要求 OAuth 开发者凭据才能授权登录。
>
> - **不配 Relay**:你需要自己去十几个平台逐一申请开发者账号,获取 client_id/secret,非常麻烦。
> - **配了 Relay**:借用官方 aitoearn.ai 的凭据完成授权,**一个 API Key 搞定所有平台**。
**配置方法**
1. 在 [aitoearn.ai](https://aitoearn.ai)(国际)或 [aitoearn.cn](https://aitoearn.cn)(中国)注册登录,进入 **设置 → API Key**,创建一个 API Key
2. 在浏览器打开部署后的界面,进入 **配置管理**
3. 选择 **Server → Relay 中转**,配置发布平台授权使用的 Relay
4. 如需使用平台提供的 AI 模型,选择 **AI → Relay 中转** 并配置 AI Relay
5. OpenAI、Gemini、Anthropic 等模型服务商也可以在 **AI → 模型服务商** 中填写平台提供的 API Key 和 API 地址
6. 保存后点击 **保存并重启**,让对应服务重新加载配置
中国版 Key 搭配 `https://aitoearn.cn/api`,国际版 Key 搭配 `https://aitoearn.ai/api`;环境和 Key 不匹配会导致 401。
**到这里,你已经可以正常使用 AiToEarn 了!** 🎉
## 运维参考
### 自动登录
自动登录默认已启用。首次启动时,`aitoearn-init` 服务会生成管理员 token 并保存到共享卷中,`aitoearn-web` 服务自动读取该 token 完成登录。
### 镜像拉取策略
所有应用服务镜像使用 `pull_policy: always`,确保每次 `docker compose up` 都拉取最新镜像。
### 内部服务通信
以下配置用于服务间通信,使用 Docker 内部网络,通常无需修改:
| 配置项 | 所属服务 | 默认值 |
|------|----------|--------|
| `serverClient.baseUrl` | aitoearn-ai | `http://aitoearn-server:3002` |
| `aiClient.baseUrl` | aitoearn-server | `http://aitoearn-ai:3010` |
### 配置文件
配置文件以可写卷挂载到容器中,可通过配置管理界面修改,保存后需重启对应服务:
| 文件 | 挂载到 | 说明 |
|------|--------|------|
| `project/aitoearn-backend/apps/aitoearn-ai/config/config.yaml` | aitoearn-ai:/app/config.yaml | AI 服务配置 |
| `project/aitoearn-backend/apps/aitoearn-server/config/config.yaml` | aitoearn-server:/app/config.yaml | 后端配置 |
---
## 配置速查表
`aitoearn-ai``aitoearn-server` 不再通过 `docker-compose.yml``environment` 配置运行参数,统一读取挂载的 `config.yaml`,并通过配置管理界面修改。
### Compose 中仍保留的环境变量
| 变量 | 所属服务 | 说明 | 默认值 |
|------|----------|------|--------|
| `MONGO_INITDB_ROOT_PASSWORD` | mongodb | MongoDB root 密码 | `password` |
| `RUSTFS_ACCESS_KEY` / `RUSTFS_SECRET_KEY` | rustfs | RustFS 访问凭证 | `rustfsadmin` |
| `MONGO_URI` / `JWT_SECRET` / `DB_NAME` / `AUTO_LOGIN_TOKEN_PATH` | aitoearn-init | 首次启动初始化管理员与自动登录 token | 内置默认值 |
| `NODE_ENV` / `NEXT_TELEMETRY_DISABLED` | aitoearn-web | Web 运行环境与 Next.js telemetry 设置 | `production` / `1` |
+137
View File
@@ -0,0 +1,137 @@
# AiToEarn Docker Deployment Guide
This guide helps you quickly deploy the complete AiToEarn application using Docker Compose.
## Architecture
```
┌──────────┐
│ Nginx │
│ :8080 │
└────┬─────┘
┌───────────────┼───────────────┐
│ │ │
┌─────┴─────┐ ┌─────┴──────┐ ┌─────┴─────┐
│ Web (FE) │ │ Server │ │ AI │
│ :3000 │ │ :3002 │ │ :3010 │
└────────────┘ └──────┬─────┘ └─────┬─────┘
│ │
┌────────────┼──────────────┤
│ │ │
┌────┴─────┐ ┌───┴────┐ ┌──────┴───┐
│ MongoDB │ │ Redis │ │ RustFS │
│ :27017 │ │ :6379 │ │ :9000/01 │
└──────────┘ └────────┘ └──────────┘
```
| Service | Description | Port |
|---------|-------------|------|
| **Nginx** | Reverse proxy, unified entry | 8080 (public) |
| **aitoearn-web** | Next.js frontend | 3000 (internal) |
| **aitoearn-server** | NestJS main backend API | 3002 (internal) |
| **aitoearn-ai** | NestJS AI service | 3010 (internal) |
| **MongoDB** | Database | 27017 |
| **Redis** | Cache / Queue | 6379 |
| **RustFS** | S3-compatible object storage | 9000 (API) / 9001 (Console) |
## Prerequisites
- **Docker**: 20.10+
- **Docker Compose**: 2.0+
- **System RAM**: 4GB+ recommended
- **Disk Space**: 20GB+ recommended
Verify installation:
```bash
docker --version
docker compose version
```
---
## 🚀 Get Running in 3 Minutes
Just 3 steps to run the complete AiToEarn on your machine.
### Step 1: Clone and Start
```bash
git clone https://github.com/yikart/AiToEarn.git
cd AiToEarn
docker compose up -d
```
First startup pulls images — may take a few minutes. Run `docker compose ps` to confirm all services are `healthy` or `running`.
### Step 2: Open the UI
Visit: **[http://localhost:8080](http://localhost:8080)**
> First startup auto-creates an admin account and logs you in automatically.
### Step 3: Configure Relay (Strongly Recommended)
> **Why configure Relay?**
>
> AiToEarn needs to log into your social media accounts (TikTok, Instagram, YouTube, etc.) to publish content. These platforms require OAuth developer credentials for authorization.
>
> - **Without Relay**: You'd need to register as a developer on each platform and obtain client_id/secret — extremely tedious.
> - **With Relay**: Use the official aitoearn.ai credentials to authorize all platforms with **just one API Key**.
**How to configure**:
1. Sign up at [aitoearn.ai](https://aitoearn.ai) (international) or [aitoearn.cn](https://aitoearn.cn) (China), go to **Settings → API Key**, and create an API Key
2. Open the deployed UI in your browser and go to **Configuration**
3. Select **Server → Relay** and configure Relay for publishing platform authorization
4. To use AI models provided by the platform, select **AI → Relay** and configure AI Relay
5. For OpenAI, Gemini, Anthropic, and other model providers, you can also fill in the platform-provided API key and API URL under **AI → Model providers**
6. After saving, click **Save and restart** so the corresponding service reloads the configuration
China keys must use `https://aitoearn.cn/api`, and international keys must use `https://aitoearn.ai/api`; mismatched environments return 401.
**You're all set!** 🎉
## Operations Reference
### Auto-Login
Enabled by default. On first startup, `aitoearn-init` generates an admin token saved to a shared volume. `aitoearn-web` reads it automatically.
### Image Pull Policy
All app images use `pull_policy: always` to pull the latest on every `docker compose up`.
### Internal Service Communication
These settings handle inter-service communication via Docker networking. Usually no changes needed:
| Setting | Service | Default |
|----------|---------|---------|
| `serverClient.baseUrl` | aitoearn-ai | `http://aitoearn-server:3002` |
| `aiClient.baseUrl` | aitoearn-server | `http://aitoearn-ai:3010` |
### Config Files
Mounted as writable volumes and editable from the Configuration UI. Restart the corresponding service after changes:
| File | Mounted to | Description |
|------|------------|-------------|
| `project/aitoearn-backend/apps/aitoearn-ai/config/config.yaml` | aitoearn-ai:/app/config.yaml | AI service config |
| `project/aitoearn-backend/apps/aitoearn-server/config/config.yaml` | aitoearn-server:/app/config.yaml | Backend config |
---
## Configuration Quick Reference
`aitoearn-ai` and `aitoearn-server` no longer use `docker-compose.yml` `environment` entries for runtime settings. They read the mounted `config.yaml`, which you can edit from the Configuration UI.
### Environment Variables Still Kept in Compose
| Variable | Service(s) | Description | Default |
|----------|------------|-------------|---------|
| `MONGO_INITDB_ROOT_PASSWORD` | mongodb | MongoDB root password | `password` |
| `RUSTFS_ACCESS_KEY` / `RUSTFS_SECRET_KEY` | rustfs | RustFS credentials | `rustfsadmin` |
| `MONGO_URI` / `JWT_SECRET` / `DB_NAME` / `AUTO_LOGIN_TOKEN_PATH` | aitoearn-init | First-start admin initialization and auto-login token | Built-in defaults |
| `NODE_ENV` / `NEXT_TELEMETRY_DISABLED` | aitoearn-web | Web runtime mode and Next.js telemetry setting | `production` / `1` |
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 AiToEarn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+353
View File
@@ -0,0 +1,353 @@
# [Aitoearn#1 AI内容营销智能体](https://aitoearn.ai)
<a href="https://trendshift.io/repositories/20785" target="_blank"><img src="https://trendshift.io/api/badge/repositories/20785" alt="yikart%2FAiToEarn | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![GitHub stars](https://img.shields.io/github/stars/yikart/AiToEarn?color=fa6470)](https://github.com/yikart/AiToEarn/stargazers)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Required Node.JS 20.18.x](https://img.shields.io/static/v1?label=node&message=20.18.x&logo=node.js&color=3f893e)](https://nodejs.org/about/releases)
简体中文 | [English](README_EN.md) | [日本語](README_JA.md)
**Monetize · Publish · Engage · Create —— 一站式平台。**
AiToEarn 通过 **AI Agent自动化**,帮助 OPC(一人公司)、创作者、品牌与企业在全球主流平台上构建、分发并变现内容。
支持渠道:
抖音、小红书(Rednote)、快手、哔哩哔哩、视频号、TikTok、YouTube、Facebook、Instagram、Threads、TwitterX)、Pinterest、LinkedIn
## 🚀 快速使用 AiToEarn5 种方式)
| 方式 | 适合谁 | 需要部署吗 |
|------|--------|-----------|
| [① 打开网站直接用](#use-web) | 所有用户 | ❌ 不需要 |
| [② 在龙虾 OpenClaw 中用](#use-in-openclaw) | 龙虾用户 | ❌ 不需要 |
| [③ 在 Claude / Cursor 等 AI 助手中用](#use-in-claude) | AI 工具用户 | ❌ 不需要 |
| [④ Docker 一键部署](#use-docker) | 想私有化部署的团队 | ✅ 需要服务器 |
| [⑤ 源码开发](#use-source) | 开发者 | ✅ 需要开发环境 |
> 💡 **方式 ②③④ 都需要先获取 API Key**,请先看 [如何获取 API Key](#get-api-key)。
## 最新动态
- **2026-06-23**: [2.5 version](https://github.com/yikart/AiToEarn/releases/tag/v2.5.0) — Relay 配置改为在配置管理界面中操作,并拆分为 Server Relay 与 AI RelayServer Relay 用于发布平台授权,AI Relay 用于使用平台提供的 AI 模型,新增[开放平台](https://docs.aitoearn.cn/)。
- **2026-05-21**: [2.4 version](https://github.com/yikart/AiToEarn/releases/tag/v2.4.0) — 草稿生成新增支持 HappyHorse 1.0 和 Seedance 2.0,增强视频/图文草稿批量生成、多模型选择、参考图片/视频、目标平台限制与文案提示词;带来全新界面风格,并增强 Twitter/X 探索与互动能力。
- **2026-04-20**: OpenClaw(龙虾)新增 AiToEarn 赚钱支持,可在龙虾中直接接收并执行内容变现任务。
- **2026-03-26**: [2.1 version](https://www.aitoearn.ai/) — 内容交易市场上线;新增 OpenClaw(龙虾)支持,可在龙虾中直接使用 AiToEarn;新增 MCP 协议支持,可在 Claude、Cursor 等任何支持 MCP 的 Agent 或大模型中使用 AiToEarn。
- **2026-02-07**: [1.8.0 version](https://www.aitoearn.ai/),新增线下商户推广解决方案,支持餐厅、零售店、民宿、美容美发、健身房等多种线下业态,将线下推广活动转化为可执行的线上传播任务,通过内容发布与用户参与机制,帮助门店获取更多线上曝光和到店流量。
- **2025-12-15**: "All In Agent" 的开始!我们加入了能够自动内容生成和发布以及一些帮助你操作 Aitoearn 的超级 AI 智能 Agent。[v1.4.3](https://github.com/yikart/AiToEarn/releases/tag/v1.4.3)
- **2025-11-28**: 支持应用内自动更新。在创作界面新增大量 AI 功能,例如:缩写、扩写、图片生成、视频生成、标签生成等,并支持 Nano Banana Pro。[v1.4.0](https://github.com/yikart/AiToEarn/releases/tag/v1.4.0)
- **2025-11-12**: 首个开源且可完全使用的版本。[v1.3.2](https://github.com/yikart/AiToEarn/releases/tag/v1.3.2)
- **2025-09-16**: 首个出海版本,新增支持 Facebook、Instagram、Threads、Twitter、YouTube、TikTok、Pinterest。[v1.0.18](https://github.com/yikart/AiToEarn/releases/tag/v1.0.18)
- **2025-02-26**: 首个开源版本,初步实现小红书、抖音、快手、视频号视频一键发布。[v0.1.1](https://github.com/yikart/AiToEarn/releases/tag/v0.1.1)
<details>
<summary><h2 style="display:inline;margin:0">目录</h2></summary>
<br/>
1. [快速使用 AiToEarn(5 种方式)](#-快速使用-aitoearn5-种方式)
2. [最新动态](#最新动态)
3. [核心功能](#核心功能)
4. [如何获取 API Key](#get-api-key)
5. [贡献指南](#贡献指南)
6. [联系](#联系)
7. [推荐](#推荐)
</details>
## 核心功能
AiToEarn 围绕内容创作者的完整变现链路,提供四大 Agent 能力:
> **Monetize · Publish · Engage · Create**
---
### 💰 Monetize —— 内容赚钱
AiToEarn 最核心的目标:**帮助每一位创作者赚钱**。
创作者可以在平台出售内容以完成商家的推广任务。所有结算以结果为导向,我们提供三种结算模式:
| 结算模式 | 全称 | 含义 |
|---------|------|------|
| **CPS** | Cost Per Sale | 按成交额结算 |
| **CPE** | Cost Per Engagement | 按互动量结算 |
| **CPM** | Cost Per Mille | 按播放量结算 |
<img src="presentation/monetize-cn.png" width="30%">
---
### 📢 Publish —— 内容发布 Agent
一键将内容分发到全球 10+ 主流平台,告别逐个平台手动发布。
- **全网分发**:覆盖抖音、快手、B站、小红书、视频号、微信公众号、TikTok、YouTube、Facebook、Instagram、Threads、XTwitter)、Pinterest、LinkedIn
- **日历排期**:像排日程一样统一规划所有平台的内容发布时间
<img src="presentation/publish-cn.png" width="30%"> <img src="presentation/channel-cn.png" width="30%">
> ▶ 观看演示视频
<a href="https://www.youtube.com/watch?v=5041jEKaiU8">
<img src="https://img.youtube.com/vi/5041jEKaiU8/0.jpg" alt="Publish 演示视频" width="480">
</a>
---
### 💬 Engage —— 内容互动 Agent
通过 AiToEarn 浏览器插件,在上述所有平台上实现自动化互动运营。
- **自动化操作**:自动点赞、收藏、关注,批量高效运营
- **AI 智能回复**:调用大模型为每条评论生成针对性回复,精准互动
- **评论挖掘**:识别"求链接""怎么购买"等高转化信号,快速响应
- **品牌监测**:实时追踪关于你品牌的讨论,主动参与热点话题
> ▶ 观看演示视频
<a href="https://youtu.be/-QoHNrZBmp0">
<img src="./presentation/engage-thumbnail-cn.png" alt="Engage 演示视频" width="480">
</a>
---
### 🎨 Create —— 内容创作 Agent
我们用 Agent 的方式重构了内容制作流程。只需告诉 Agent 你的内容需求,它会自动完成从创意到成品的全部工作。
**视频内容**:Agent 自动调用视频生成模型(Grok、Veo、Seedance 等)、视频翻译模块、视频剪辑模块,一站式完成视频制作。
**图文内容**:支持调用 Nano Banana 等顶级图片模型,自动生成高质量图文内容。
**批量生成**:支持批量下发创作任务,Agent 可并行生成多条内容,快速铺量,适合矩阵账号运营和大规模内容分发场景。
> ▶ 观看演示视频
<a href="https://youtu.be/y900LxIrZT4">
<img src="./presentation/display-1.5.2png.png" alt="Create 演示视频" width="480">
</a>
---
<h2 id="use-web">① 打开网站直接用</h2>
最简单的方式,打开浏览器即可使用,无需任何配置:
- 🇨🇳 中国用户访问:**[aitoearn.cn](https://aitoearn.cn/)**
- 🌍 国际用户访问:**[aitoearn.ai](https://aitoearn.ai/)**
---
<h2 id="get-api-key">🔑 如何获取 API Key(后续步骤的前置条件)</h2>
> 以下的方式 ②③④ 都需要 API Key。只需获取一次,所有方式通用。
**3 步获取**
1. 打开 [aitoearn.cn](https://aitoearn.cn/)(中国用户)或 [aitoearn.ai](https://aitoearn.ai/)(国际用户),注册并登录
2. 点击左侧菜单 **设置**
3.**API Key** 中点击创建,复制生成的 Key
<img src="presentation/app-screenshot/0.%20api-key/api-key-settings.png" alt="获取 API Key" width="600">
---
<h2 id="use-in-openclaw">② 在龙虾 OpenClaw 中使用</h2>
> 前置条件:已 [获取 API Key](#get-api-key)
**请在服务器终端输入以下命令!请在服务器终端输入以下命令!请在服务器终端输入以下命令!**
**安装插件**
```bash
npx -y @aitoearn/openclaw-plugin-cli
```
首次运行后会先让你选择环境并输入 API Key。请确保环境与 Key 匹配:
- 中国版:使用 `aitoearn.cn` 获取的 API Key
- 国际版:使用 `aitoearn.ai` 获取的 API Key
环境和 Key 不匹配会导致 401。
安装完成后,你就可以在 OpenClaw 中直接接收并执行 AiToEarn 的赚钱任务:
<img src="presentation/openclaw-earn-demo.png" alt="在 OpenClaw 中执行 AiToEarn 赚钱任务" width="360">
---
<h2 id="use-in-claude">③ 在 Claude / Cursor / 其他 AI 助手中使用</h2>
> 前置条件:已 [获取 API Key](#get-api-key)
AiToEarn 支持所有兼容 MCP 协议的 AI 助手。以下是常见工具的配置方式:
请根据 API Key 来源选择地址,环境和 Key 不匹配会导致 401:
| 环境 | MCP 地址 | SSE 地址 |
|------|---------|---------|
| 中国版 | `https://aitoearn.cn/api/unified/mcp` | `https://aitoearn.cn/api/unified/sse` |
| 国际版 | `https://aitoearn.ai/api/unified/mcp` | `https://aitoearn.ai/api/unified/sse` |
<details open>
<summary><b>Claude Desktop</b></summary>
找到并编辑 `claude_desktop_config.json`,添加:
```json
{
"mcpServers": {
"aitoearn": {
"type": "http",
"url": "https://aitoearn.ai/api/unified/mcp",
"headers": {
"x-api-key": "你的API-Key"
}
}
}
}
```
</details>
<details>
<summary><b>Cursor</b></summary>
在 Cursor 的 MCP 设置中添加:
```
MCP 地址:https://aitoearn.ai/api/unified/mcp
认证 Headerx-api-key: 你的API-Key
```
</details>
<details>
<summary><b>其他 AI 助手(通用配置)</b></summary>
任何支持 MCP 协议的工具,只需要两个信息:
| 配置项 | 值 |
|--------|------|
| **MCP 地址** | `https://aitoearn.ai/api/unified/mcp` |
| **认证 Header** | `x-api-key: 你的API-Key` |
也支持 SSE 长连接方式:`https://aitoearn.ai/api/unified/sse`
</details>
> 💡 如果你是自部署用户,将 `aitoearn.ai` 替换为你自己的地址(如 `localhost:8080`)。
---
<h2 id="use-docker">④ Docker 一键部署</h2>
> 前置条件:已安装 [Docker](https://docs.docker.com/get-docker/)
适合想把 AiToEarn 部署在自己服务器上的团队。3 条命令搞定,无需手动安装数据库:
```bash
git clone https://github.com/yikart/AiToEarn.git
cd AiToEarn
docker compose up -d
```
启动后打开 **[http://localhost:8080](http://localhost:8080)** 即可使用。
#### 配置 Relay(强烈推荐)
> **为什么要配 Relay?** 发布内容需要登录社交媒体账号(抖音、小红书、TikTok 等),而这些平台的 OAuth 登录需要开发者凭据。配置 Relay 后,你可以直接借用官方 aitoearn.ai 的凭据完成授权,**不需要自己去各平台申请开发者账号**。
在浏览器打开部署后的界面,进入 **配置管理**,按需分别配置:
- **Server → Relay 中转**:用于内容发布和社交平台 OAuth 授权。
- **AI → Relay 中转**:用于使用平台提供的 AI 模型。
OpenAI、Gemini、Anthropic 等模型服务商也可以在 **AI → 模型服务商** 中填写平台提供的 API Key 和 API 地址。
API Key 获取方式见 [上方说明](#get-api-key)。中国版 Key 搭配 `https://aitoearn.cn/api`,国际版 Key 搭配 `https://aitoearn.ai/api`;环境和 Key 不匹配会导致 401。
保存后点击 **保存并重启**,让对应服务重新加载配置。
> 📖 完整部署指南(生产环境配置、AI 服务、OAuth、存储等)请参阅 [DOCKER_DEPLOYMENT_CN.md](DOCKER_DEPLOYMENT_CN.md)。
---
<h2 id="use-source">⑤ 源码开发</h2>
<details>
<summary>🧪 手动运行后端和前端(开发模式)</summary>
此模式主要用于本地开发和调试。
您仍然可以使用 Docker 运行 MongoDB/Redis,或在配置文件中指向您自己的服务。
#### 1. 启动后端服务
```bash
cd project/aitoearn-backend
pnpm install
# 复制配置文件用于本地开发
cp apps/aitoearn-ai/config/config.yaml apps/aitoearn-ai/config/local.config.yaml
cp apps/aitoearn-server/config/config.yaml apps/aitoearn-server/config/local.config.yaml
pnpm nx serve aitoearn-ai
# 在另一个终端
pnpm nx serve aitoearn-server
```
#### 2. 启动前端 `aitoearn-web`
```bash
pnpm install
pnpm run dev
```
</details>
<details>
<summary>🖥️ 启动 Electron 桌面项目</summary>
```bash
# 克隆仓库
git clone https://github.com/yikart/AttAiToEarn.git
# 进入目录
cd AttAiToEarn
# 安装依赖
npm install
# 编译 sqlitebetter-sqlite3 需要 node-gyp 和本地 Python
npm run rebuild
# 启动开发
npm run dev
```
Electron 项目为 AiToEarn 提供桌面客户端。
</details>
## 贡献指南
请查看 [贡献指南](./CONTRIBUTING.md) 开始参与。
## 联系
如果你在使用过程中遇到困难、使用问题或异常情况,优先通过 [GitHub Issues](https://github.com/yikart/AiToEarn/issues) 提交反馈,方便我们统一跟进和处理。
- Telegram: [https://t.me/harryyyy2025](https://t.me/harryyyy2025)
- 微信:扫码添加
<img src="presentation/wechat-0706.png" alt="微信二维码" width="200">
## 推荐
- [AtomGit托管](https://atomgit.com/yikart/AitoEarn)
- [MuseTalk](https://github.com/TMElyralab/MuseTalk)
- [video_spider](https://github.com/5ime/video_spider)
- [CosyVoice](https://github.com/FunAudioLLM/CosyVoice?tab=readme-ov-file)
- [facefusion](https://github.com/facefusion/facefusion)
- [NarratoAI](https://github.com/linyqh/NarratoAI)
- [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo)
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`yikart/AiToEarn`
- 原始仓库:https://github.com/yikart/AiToEarn
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+346
View File
@@ -0,0 +1,346 @@
# [Aitoearn: The Best Open-Source AI Agent for Content Marketing](https://aitoearn.ai)
<a href="https://trendshift.io/repositories/20785" target="_blank"><img src="https://trendshift.io/api/badge/repositories/20785" alt="yikart%2FAiToEarn | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![GitHub stars](https://img.shields.io/github/stars/yikart/AiToEarn?color=fa6470)](https://github.com/yikart/AiToEarn/stargazers)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Required Node.JS 20.18.x](https://img.shields.io/static/v1?label=node&message=20.18.x&logo=node.js&color=3f893e)](https://nodejs.org/about/releases)
English | [简体中文](README.md) | [日本語](README_JA.md)
**Monetize · Publish · Engage · Create — all in one platform.**
AiToEarn helps OPCs (One-Person Companies), creators, brands, and businesses build, distribute, and monetize content with **AI-powered automation** across the world's most popular platforms.
Supported Channels:
Douyin, Xiaohongshu (Rednote), Kuaishou, Bilibili, WeChat Channels, WeChat Official Accounts, TikTok, YouTube, Facebook, Instagram, Threads, Twitter (X), Pinterest, LinkedIn
## 🚀 Quick Start with AiToEarn (5 Ways)
| Option | Best for | Deployment needed? |
|--------|----------|-------------------|
| [① Use the Website](#use-web) | Everyone | ❌ No |
| [② Use in OpenClaw](#use-in-openclaw) | OpenClaw users | ❌ No |
| [③ Use in Claude / Cursor / Other AI Assistants](#use-in-claude) | AI tool users | ❌ No |
| [④ Docker One-Click Deploy](#use-docker) | Teams wanting self-hosted | ✅ Server needed |
| [⑤ Build from Source](#use-source) | Developers | ✅ Dev environment needed |
> 💡 **Options ②③④ require an API Key first.** See [How to Get an API Key](#get-api-key).
## What's New
- **2026-06-23**: [2.5 version](https://github.com/yikart/AiToEarn/releases/tag/v2.5.0) — Relay configuration now happens in the Configuration UI and is split into Server Relay and AI Relay: Server Relay handles publishing platform authorization, while AI Relay lets users use AI models provided by the platform.
- **2026-05-21**: [2.4 version](https://github.com/yikart/AiToEarn/releases/tag/v2.4.0) — Draft generation now supports HappyHorse 1.0 and Seedance 2.0, with improved batch video/image-text draft generation, multi-model selection, reference images/videos, target-platform limits, and caption prompts; refreshed interface style and enhanced Twitter/X exploration and engagement.
- **2026-04-20**: OpenClaw now supports AiToEarn earning workflows, so you can receive and execute monetization tasks directly inside OpenClaw.
- **2026-03-26**: [2.1 version](https://www.aitoearn.ai/) — Content marketplace launched; added OpenClaw support for using AiToEarn directly within OpenClaw; added MCP protocol support for using AiToEarn in Claude, Cursor, and any MCP-compatible Agent or LLM.
- **2026-02-07**: [1.8.0 version](https://www.aitoearn.ai/) — Added offline business promotion solutions for restaurants, retail stores, hotels, beauty salons, gyms, and more.
- **2025-12-15**: "All In Agent" arrives! We've introduced a super AI agent that can automatically generate and publish content. [v1.4.3](https://github.com/yikart/AiToEarn/releases/tag/v1.4.3)
- **2025-11-28**: Support automatic updates within the application. Added AI functions: abbreviation, expansion, image creation, video creation, tag generation, etc. [v1.4.0](https://github.com/yikart/AiToEarn/releases/tag/v1.4.0)
- **2025-11-12**: The first open-source, fully usable version. [v1.3.2](https://github.com/yikart/AiToEarn/releases/tag/v1.3.2)
- **2025-09-16**: First international version, added support for Facebook, Instagram, Threads, Twitter, YouTube, TikTok, Pinterest. [v1.0.18](https://github.com/yikart/AiToEarn/releases/tag/v1.0.18)
- **2025-02-26**: First open-source release, initial support for one-click publishing to Xiaohongshu, Douyin, Kuaishou, and WeChat Channels. [v0.1.1](https://github.com/yikart/AiToEarn/releases/tag/v0.1.1)
<details>
<summary><h2 style="display:inline;margin:0">Table of Contents</h2></summary>
<br/>
1. [Quick Start with AiToEarn (5 Ways)](#-quick-start-with-aitoearn-5-ways)
2. [What's New](#whats-new)
3. [Key Features](#key-features)
4. [How to Get an API Key](#get-api-key)
5. [Contributing](#contributing)
6. [Contact](#contact)
7. [Recommended](#recommended)
</details>
## Key Features
AiToEarn provides four core Agent capabilities around the creator's full monetization pipeline:
> **Monetize · Publish · Engage · Create**
---
### 💰 Monetize — Earn from Your Content
The core mission of AiToEarn: **help every creator earn money**.
Creators can sell content on the platform to complete brand promotion tasks. All settlements are results-driven, with three models:
| Model | Full Name | Meaning |
|-------|-----------|---------|
| **CPS** | Cost Per Sale | Settle by transaction amount |
| **CPE** | Cost Per Engagement | Settle by engagement count |
| **CPM** | Cost Per Mille | Settle by view count |
<img src="presentation/monetize-cn.png" width="30%">
---
### 📢 Publish — Content Publishing Agent
Distribute content to 10+ major platforms worldwide with one click — no more manual posting on each platform.
- **Multi-Platform Distribution**: Douyin, Kwai, Bilibili, Rednote, WeChat Channels, WeChat Official Accounts, TikTok, YouTube, Facebook, Instagram, Threads, X (Twitter), Pinterest, LinkedIn
- **Calendar Scheduler**: Plan and coordinate content publishing across all platforms like a calendar
<img src="presentation/publish-cn.png" width="30%"> <img src="presentation/channel-cn.png" width="30%">
> ▶ Watch Demo Video
<a href="https://www.youtube.com/watch?v=5041jEKaiU8">
<img src="https://img.youtube.com/vi/5041jEKaiU8/0.jpg" alt="Publish Demo Video" width="480">
</a>
---
### 💬 Engage — Content Engagement Agent
Automate engagement operations across all supported platforms via the AiToEarn browser extension.
- **Automated Actions**: Auto-like, bookmark, and follow — batch operations at scale
- **AI Smart Replies**: Use LLMs to generate targeted replies for each comment
- **Comment Mining**: Detect high-conversion signals like "link please" or "how to buy" and respond instantly
- **Brand Monitoring**: Track brand mentions in real-time and proactively join trending conversations
> ▶ Watch Demo Video
<a href="https://youtu.be/-QoHNrZBmp0">
<img src="./presentation/engage-thumbnail-cn.png" alt="Engage Demo Video" width="480">
</a>
---
### 🎨 Create — Content Creation Agent
We've rebuilt the content creation workflow with Agents. Just tell the Agent what you need — it handles everything from idea to finished product.
**Video Content**: The Agent automatically invokes video generation models (Grok, Veo, Seedance, etc.), video translation modules, and video editing modules to produce a complete video.
**Image & Text Content**: Supports top-tier image models like Nano Banana to create high-quality visual content automatically.
**Batch Generation**: Submit creation tasks in bulk — the Agent generates multiple pieces of content in parallel, perfect for matrix account operations and large-scale content distribution.
> ▶ Watch Demo Video
<a href="https://youtu.be/y900LxIrZT4">
<img src="./presentation/display-1.5.2png.png" alt="Create Demo Video" width="480">
</a>
---
<h2 id="use-web">① Use the Website</h2>
The simplest way — just open your browser:
- 🇨🇳 China users: **[aitoearn.cn](https://aitoearn.cn/)**
- 🌍 International users: **[aitoearn.ai](https://aitoearn.ai/)**
---
<h2 id="get-api-key">🔑 How to Get an API Key (Required for Steps Below)</h2>
> Options ②③④ all need an API Key. You only need to get it once.
**3 steps**:
1. Open [aitoearn.cn](https://aitoearn.cn/) (China) or [aitoearn.ai](https://aitoearn.ai/) (international), sign up and log in
2. Click **Settings** in the left menu
3. Go to **API Key**, click Create, and copy the generated key
<img src="presentation/app-screenshot/0.%20api-key/api-key-settings.png" alt="Get API Key" width="600">
> ⚠️ Keep your API Key safe. Do not share it with others.
---
<h2 id="use-in-openclaw">② Use in OpenClaw</h2>
> Prerequisite: [Get an API Key](#get-api-key) first
**Install the plugin**
```bash
npx -y @aitoearn/openclaw-plugin-cli
```
On first run, select the environment and enter your API Key. Make sure they match: China uses an API Key from `aitoearn.cn`, and international uses one from `aitoearn.ai`. A mismatch returns 401.
After setup, you can receive and execute AiToEarn earning tasks directly inside OpenClaw:
<img src="presentation/openclaw-earn-demo.png" alt="Run AiToEarn earning tasks in OpenClaw" width="360">
---
<h2 id="use-in-claude">③ Use in Claude / Cursor / Other AI Assistants</h2>
> Prerequisite: [Get an API Key](#get-api-key) first
AiToEarn works with any MCP-compatible AI assistant. Here's how to configure the most popular ones:
Choose the URL that matches your API Key. A mismatched environment and key returns 401:
| Environment | MCP URL | SSE URL |
|-------------|---------|---------|
| China | `https://aitoearn.cn/api/unified/mcp` | `https://aitoearn.cn/api/unified/sse` |
| International | `https://aitoearn.ai/api/unified/mcp` | `https://aitoearn.ai/api/unified/sse` |
<details open>
<summary><b>Claude Desktop</b></summary>
Find and edit `claude_desktop_config.json`, add:
```json
{
"mcpServers": {
"aitoearn": {
"type": "http",
"url": "https://aitoearn.ai/api/unified/mcp",
"headers": {
"x-api-key": "your-api-key"
}
}
}
}
```
</details>
<details>
<summary><b>Cursor</b></summary>
In Cursor's MCP settings, add:
```
MCP URL: https://aitoearn.ai/api/unified/mcp
Auth Header: x-api-key: your-api-key
```
</details>
<details>
<summary><b>Other AI Assistants (Generic Config)</b></summary>
Any MCP-compatible tool just needs two pieces of info:
| Setting | Value |
|---------|-------|
| **MCP URL** | `https://aitoearn.ai/api/unified/mcp` |
| **Auth Header** | `x-api-key: your-api-key` |
SSE transport is also available: `https://aitoearn.ai/api/unified/sse`
</details>
> 💡 For self-hosted instances, replace `aitoearn.ai` with your own address (e.g., `localhost:8080`).
---
<h2 id="use-docker">④ Docker One-Click Deploy</h2>
> Prerequisite: [Docker](https://docs.docker.com/get-docker/) installed
For teams wanting to run AiToEarn on their own server. 3 commands, no manual database setup:
```bash
git clone https://github.com/yikart/AiToEarn.git
cd AiToEarn
docker compose up -d
```
Open **[http://localhost:8080](http://localhost:8080)** and you're ready to go.
#### Configure Relay (Strongly Recommended)
> **Why Relay?** Publishing content requires logging into social media accounts (TikTok, Instagram, YouTube, etc.), which need OAuth developer credentials. With Relay, you can use the official aitoearn.ai credentials — **no need to register as a developer on each platform**.
Open the deployed UI in your browser, go to **Configuration**, and configure these sections as needed:
- **Server → Relay**: for content publishing and social platform OAuth authorization.
- **AI → Relay**: for using AI models provided by the platform.
For OpenAI, Gemini, Anthropic, and other model providers, you can also fill in the platform-provided API key and API URL under **AI → Model providers**.
See [How to Get an API Key](#get-api-key). China keys must use `https://aitoearn.cn/api`, and international keys must use `https://aitoearn.ai/api`; mismatched environments return 401.
After saving, click **Save and restart** so the corresponding service reloads the configuration.
> 📖 Full deployment guide (production config, AI services, OAuth, storage, etc.): [DOCKER_DEPLOYMENT_EN.md](DOCKER_DEPLOYMENT_EN.md).
---
<h2 id="use-source">⑤ Build from Source</h2>
<details>
<summary>🧪 Run backend & frontend manually (dev mode)</summary>
For local development and debugging. You can use Docker for MongoDB/Redis, or point to your own services.
#### 1. Start the backend services
```bash
cd project/aitoearn-backend
pnpm install
# Copy config files for local development
cp apps/aitoearn-ai/config/config.yaml apps/aitoearn-ai/config/local.config.yaml
cp apps/aitoearn-server/config/config.yaml apps/aitoearn-server/config/local.config.yaml
pnpm nx serve aitoearn-ai
# in another terminal
pnpm nx serve aitoearn-server
```
#### 2. Start the frontend `aitoearn-web`
```bash
pnpm install
pnpm run dev
```
</details>
<details>
<summary>🖥️ Start Electron desktop project</summary>
```bash
# Clone the repo
git clone https://github.com/yikart/AttAiToEarn.git
# Enter directory
cd AttAiToEarn
# Install dependencies
npm install
# Compile sqlite (better-sqlite3 requires node-gyp and local Python)
npm run rebuild
# Start development
npm run dev
```
The Electron project provides a desktop client for AiToEarn.
</details>
## Contributing
Please see [Contributing Guide](./CONTRIBUTING.md) to get started.
## Contact
If you run into usage difficulties, questions, or unexpected behavior, please open a [GitHub Issue](https://github.com/yikart/AiToEarn/issues) first so we can track and respond in one place.
- Telegram: [https://t.me/harryyyy2025](https://t.me/harryyyy2025)
- WeChat: Scan to add
<img src="presentation/wechat.jpg" alt="WeChat QR Code" width="200">
## Recommended
- [MuseTalk](https://github.com/TMElyralab/MuseTalk)
- [video_spider](https://github.com/5ime/video_spider)
- [CosyVoice](https://github.com/FunAudioLLM/CosyVoice?tab=readme-ov-file)
- [facefusion](https://github.com/facefusion/facefusion)
- [NarratoAI](https://github.com/linyqh/NarratoAI)
- [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo)
+352
View File
@@ -0,0 +1,352 @@
# [Aitoearn:個人ビジネス向けAIコンテンツマーケティングエージェント](https://aitoearn.ai)
<a href="https://trendshift.io/repositories/20785" target="_blank"><img src="https://trendshift.io/api/badge/repositories/20785" alt="yikart%2FAiToEarn | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![GitHub stars](https://img.shields.io/github/stars/yikart/AiToEarn?color=fa6470)](https://github.com/yikart/AiToEarn/stargazers)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Required Node.JS 20.18.x](https://img.shields.io/static/v1?label=node&message=20.18.x&logo=node.js&color=3f893e)](https://nodejs.org/about/releases)
日本語 | [简体中文](README.md) | [English](README_EN.md)
**収益化 · 公開 · エンゲージメント · クリエイト —— オールインワンプラットフォーム。**
AiToEarnは**AI自動化**を通じて、クリエイター、ブランド、企業が世界中の主要プラットフォームでコンテンツを構築、配信、収益化するのを支援します。
対応チャンネル:
抖音(Douyin)、小紅書(Rednote)、快手(Kuaishou)、bilibili、視頻号(WeChat Channels)、微信公式アカウント(WeChat Official Accounts)、TikTok、YouTube、Facebook、Instagram、Threads、TwitterX)、Pinterest、LinkedIn
## 🚀 AiToEarnをすぐに使う(5つの方法)
| 方法 | 対象 | デプロイ必要? |
|------|------|--------------|
| [① ウェブサイトで直接使う](#use-web) | すべてのユーザー | ❌ 不要 |
| [② OpenClaw(ロブスター)で使う](#use-in-openclaw) | OpenClawユーザー | ❌ 不要 |
| [③ Claude / Cursor などのAIアシスタントで使う](#use-in-claude) | AIツールユーザー | ❌ 不要 |
| [④ Dockerワンクリックデプロイ](#use-docker) | 自己デプロイしたいチーム | ✅ サーバー必要 |
| [⑤ ソースコードから開発](#use-source) | 開発者 | ✅ 開発環境必要 |
> 💡 **方法②③④は事前にAPI Keyの取得が必要です**。[API Keyの取得方法](#get-api-key)を先にご覧ください。
## 最新情報
- **2026-06-23**: [2.5バージョン](https://github.com/yikart/AiToEarn/releases/tag/v2.5.0) — Relay 設定は設定管理画面で操作する方式になり、Server Relay と AI Relay に分かれました。Server Relay は公開プラットフォームの認証に、AI Relay はプラットフォーム提供の AI モデル利用に使用します。
- **2026-05-21**: [2.4バージョン](https://github.com/yikart/AiToEarn/releases/tag/v2.4.0) — 草稿生成が HappyHorse 1.0 と Seedance 2.0 に新対応。動画・画像/テキスト草稿の一括生成、複数モデル選択、参照画像/動画、対象プラットフォーム制限、文案プロンプトを強化し、新しいUIスタイルと Twitter/X の探索・エンゲージメント機能も強化しました。
- **2026-04-20**: OpenClaw(ロブスター)で AiToEarn の収益化タスクに新対応し、OpenClaw 内で直接受け取り実行できるようになりました。
- **2026-03-26**: [2.1バージョン](https://www.aitoearn.ai/) — コンテンツ取引マーケットプレイスをリリース。OpenClaw(ロブスター)対応を追加し、OpenClaw内で直接AiToEarnを使用可能に。MCPプロトコル対応を追加し、Claude、CursorなどMCP対応のエージェントやLLMでAiToEarnを使用可能に。
- **2026-02-07**: [1.8.0バージョン](https://www.aitoearn.ai/) — オフライン店舗プロモーションソリューションを追加。レストラン、小売店、民宿、美容室、ジムなど多様なオフラインビジネスに対応。オフラインのプロモーション活動を実行可能なオンライン拡散タスクに変換し、コンテンツ公開とユーザー参加メカニズムを通じて店舗のオンライン露出と来店トラフィックの増加を支援。
- **2025-12-15**: 「All In Agent」の始まり!コンテンツの自動生成・公開、そしてAitoearnの操作を支援するスーパーAIエージェントを追加。[v1.4.3](https://github.com/yikart/AiToEarn/releases/tag/v1.4.3)
- **2025-11-28**: アプリ内自動更新に対応。作成画面に多くのAI機能を追加:要約、拡張、画像生成、動画生成、タグ生成など。Nano Banana Proにも対応。[v1.4.0](https://github.com/yikart/AiToEarn/releases/tag/v1.4.0)
- **2025-11-12**: 初のオープンソースで完全に使用可能なバージョン。[v1.3.2](https://github.com/yikart/AiToEarn/releases/tag/v1.3.2)
- **2025-09-16**: 初の海外展開バージョン、Facebook、Instagram、Threads、Twitter、YouTube、TikTok、Pinterestに対応。[v1.0.18](https://github.com/yikart/AiToEarn/releases/tag/v1.0.18)
- **2025-02-26**: 初のオープンソースバージョン、小紅書・抖音・快手・ビデオアカウントへのワンクリック動画投稿を実現。[v0.1.1](https://github.com/yikart/AiToEarn/releases/tag/v0.1.1)
<details>
<summary><h2 style="display:inline;margin:0">目次</h2></summary>
<br/>
1. [AiToEarnをすぐに使う(5つの方法)](#-aitoearnをすぐに使う5つの方法)
2. [最新情報](#最新情報)
3. [主な機能](#主な機能)
4. [API Keyの取得方法](#get-api-key)
5. [貢献ガイド](#貢献ガイド)
6. [お問い合わせ](#お問い合わせ)
7. [推奨](#推奨)
</details>
## 主な機能
AiToEarnはコンテンツクリエイターの完全な収益化パイプラインを中心に、4つのエージェント機能を提供します:
> **収益化 · 公開 · エンゲージメント · クリエイト**
---
### 💰 収益化 —— コンテンツで稼ぐ
AiToEarnの最も重要な目標:**すべてのクリエイターが稼げるようにする**。
クリエイターはプラットフォーム上でコンテンツを販売し、ブランドのプロモーションタスクを完了できます。すべての決済は成果報酬型で、3つの決済モードを提供:
| 決済モード | 正式名称 | 意味 |
|---------|------|------|
| **CPS** | Cost Per Sale | 売上額に基づいて決済 |
| **CPE** | Cost Per Engagement | エンゲージメント数に基づいて決済 |
| **CPM** | Cost Per Mille | 再生数に基づいて決済 |
<div style="display: flex; justify-content: space-around;">
<img src="presentation/monetize-cn.png" width="50%">
</div>
---
### 📢 公開 —— コンテンツ公開エージェント
ワンクリックで世界中の10以上の主要プラットフォームにコンテンツを配信。各プラットフォームで手動投稿する手間から解放されます。
- **マルチプラットフォーム配信**:抖音、快手、B站、小紅書、視頻号(WeChat Channels)、微信公式アカウント(WeChat Official Accounts)、TikTok、YouTube、Facebook、Instagram、Threads、XTwitter)、Pinterest、LinkedInに対応
- **カレンダースケジュール**:カレンダーのように全プラットフォームのコンテンツ公開時間を統一的に計画
<div style="display: flex; justify-content: space-around;">
<img src="presentation/publish-cn.png" width="30%">
<img src="presentation/app-screenshot/1.%20content%20publish/support_channels.jpeg" width="30%">
</div>
> ▶ デモ動画を見る
<a href="https://www.youtube.com/watch?v=5041jEKaiU8">
<img src="https://img.youtube.com/vi/5041jEKaiU8/0.jpg" alt="公開 デモ動画" width="480">
</a>
---
### 💬 エンゲージメント —— コンテンツ交流エージェント
AiToEarnブラウザ拡張機能を通じて、上記のすべてのプラットフォームで自動化された交流運用を実現。
- **自動化アクション**:自動いいね、保存、フォロー — 大規模な一括操作
- **AIスマート返信**:LLMを使用して各コメントに的確な返信を生成
- **コメントマイニング**:「リンクください」「購入方法は」などの高コンバージョンシグナルを検出し、即座に対応
- **ブランドモニタリング**:ブランドに関する言及をリアルタイムで追跡し、トレンドの会話に積極的に参加
> ▶ デモ動画を見る
<a href="https://youtu.be/-QoHNrZBmp0">
<img src="./presentation/engage-thumbnail-cn.png" alt="エンゲージメント デモ動画" width="480">
</a>
---
### 🎨 クリエイト —— コンテンツ作成エージェント
エージェント方式でコンテンツ制作ワークフローを再構築しました。エージェントにコンテンツのニーズを伝えるだけで、アイデアから完成品まで全てを自動的に処理します。
**動画コンテンツ**:エージェントが自動的に動画生成モデル(Grok、Veo、Seedanceなど)、動画翻訳モジュール、動画編集モジュールを呼び出し、一貫して動画を制作。
**画像・テキストコンテンツ**Nano Bananaなどのトップクラスの画像モデルをサポートし、高品質なビジュアルコンテンツを自動生成。
**一括生成**:作成タスクを一括で投入 — エージェントが複数のコンテンツを並列生成。マトリックスアカウント運用や大規模コンテンツ配信に最適。
> ▶ デモ動画を見る
<a href="https://youtu.be/y900LxIrZT4">
<img src="./presentation/display-1.5.2png.png" alt="クリエイト デモ動画" width="480">
</a>
---
<h2 id="use-web">① ウェブサイトで直接使う</h2>
最も簡単な方法 — ブラウザを開くだけで使用可能:
- 🇨🇳 中国のユーザー:**[aitoearn.cn](https://aitoearn.cn/)**
- 🌍 その他のユーザー:**[aitoearn.ai](https://aitoearn.ai/)**
---
<h2 id="get-api-key">🔑 API Keyの取得方法(以下の手順に必要)</h2>
> 以下の方法②③④にはAPI Keyが必要です。一度取得すれば、すべての方法で使用可能です。
**3ステップで取得**
1. [aitoearn.cn](https://aitoearn.cn/)(中国)または[aitoearn.ai](https://aitoearn.ai/)(その他)を開き、登録・ログイン
2. 左メニューの**設定**をクリック
3. **API Key**に移動し、作成をクリックして、生成されたキーをコピー
<img src="presentation/app-screenshot/0.%20api-key/api-key-settings.png" alt="API Key取得" width="600">
> ⚠️ API Keyは安全に保管し、他人に共有しないでください。
---
<h2 id="use-in-openclaw">② OpenClaw(ロブスター)で使う</h2>
> 前提条件:[API Keyを取得済み](#get-api-key)
**プラグインをインストール**
```bash
npx -y @aitoearn/openclaw-plugin-cli
```
初回実行時は環境を選択し、API Key を入力してください。中国版は `aitoearn.cn` の API Key、国際版は `aitoearn.ai` の API Key を使用します。環境と Key が一致しない場合は 401 になります。
設定後は、OpenClaw 内で AiToEarn の収益化タスクを直接受け取り実行できます。
<img src="presentation/openclaw-earn-demo.png" alt="OpenClaw で AiToEarn の収益化タスクを実行" width="360">
---
<h2 id="use-in-claude">③ Claude / Cursor / その他のAIアシスタントで使う</h2>
> 前提条件:[API Keyを取得済み](#get-api-key)
AiToEarnはMCPプロトコルに対応するすべてのAIアシスタントで動作します。一般的なツールの設定方法:
API Key の取得元に合わせて URL を選択してください。環境と Key が一致しない場合は 401 になります。
| 環境 | MCP URL | SSE URL |
|------|---------|---------|
| 中国版 | `https://aitoearn.cn/api/unified/mcp` | `https://aitoearn.cn/api/unified/sse` |
| 国際版 | `https://aitoearn.ai/api/unified/mcp` | `https://aitoearn.ai/api/unified/sse` |
<details open>
<summary><b>Claude Desktop</b></summary>
`claude_desktop_config.json`を見つけて編集し、以下を追加:
```json
{
"mcpServers": {
"aitoearn": {
"type": "http",
"url": "https://aitoearn.ai/api/unified/mcp",
"headers": {
"x-api-key": "あなたのAPI-Key"
}
}
}
}
```
</details>
<details>
<summary><b>Cursor</b></summary>
CursorのMCP設定で以下を追加:
```
MCP URL: https://aitoearn.ai/api/unified/mcp
認証ヘッダー: x-api-key: あなたのAPI-Key
```
</details>
<details>
<summary><b>その他のAIアシスタント(汎用設定)</b></summary>
MCPプロトコル対応のツールなら、2つの情報だけでOK:
| 設定項目 | 値 |
|--------|------|
| **MCP URL** | `https://aitoearn.ai/api/unified/mcp` |
| **認証ヘッダー** | `x-api-key: あなたのAPI-Key` |
SSE接続もサポート:`https://aitoearn.ai/api/unified/sse`
</details>
> 💡 自己デプロイの場合は、`aitoearn.ai`をご自身のアドレス(例:`localhost:8080`)に置き換えてください。
---
<h2 id="use-docker">④ Dockerワンクリックデプロイ</h2>
> 前提条件:[Docker](https://docs.docker.com/get-docker/)がインストール済み
自分のサーバーでAiToEarnを運用したいチーム向け。3つのコマンドで完了、データベースの手動インストール不要:
```bash
git clone https://github.com/yikart/AiToEarn.git
cd AiToEarn
docker compose up -d
```
起動後、**[http://localhost:8080](http://localhost:8080)** を開けば使用可能。
#### Relayの設定(強く推奨)
> **なぜRelayが必要?** コンテンツを公開するにはソーシャルメディアアカウント(TikTok、Instagram、YouTubeなど)へのログインが必要で、これらのプラットフォームのOAuthログインには開発者認証情報が必要です。Relayを設定すれば、公式aitoearn.aiの認証情報を直接借用して認証を完了できるため、**各プラットフォームで開発者アカウントを申請する必要がありません**。
ブラウザでデプロイ済みの画面を開き、**設定管理** に入り、必要に応じて次の項目を設定します:
- **Server → Relay**:コンテンツ公開とソーシャルプラットフォームの OAuth 認証に使用します。
- **AI → Relay**:プラットフォームが提供する AI モデルを利用するために使用します。
OpenAI、Gemini、Anthropic などのモデルプロバイダーも、**AI → モデルプロバイダー** でプラットフォーム提供の API Key と API URL を入力できます。
API Key の取得方法は [上記](#get-api-key) を参照してください。中国版 Key は `https://aitoearn.cn/api`、国際版 Key は `https://aitoearn.ai/api` と組み合わせて使用します。環境と Key が一致しない場合は 401 になります。
保存後、**保存して再起動** をクリックして、対象サービスに設定を再読み込みさせます。
> 📖 完全なデプロイガイド(本番環境設定、AIサービス、OAuth、ストレージなど):[DOCKER_DEPLOYMENT_EN.md](DOCKER_DEPLOYMENT_EN.md)を参照。
---
<h2 id="use-source">⑤ ソースコードから開発</h2>
<details>
<summary>🧪 バックエンドとフロントエンドを手動で実行(開発モード)</summary>
このモードは主にローカル開発とデバッグ用です。
Dockerを使用してMongoDB/Redisを実行するか、設定ファイルで独自のサービスを指定できます。
#### 1. バックエンドサービスを起動
```bash
cd project/aitoearn-backend
pnpm install
# ローカル開発用設定ファイルをコピー
cp apps/aitoearn-ai/config/config.yaml apps/aitoearn-ai/config/local.config.yaml
cp apps/aitoearn-server/config/config.yaml apps/aitoearn-server/config/local.config.yaml
pnpm nx serve aitoearn-ai
# 別のターミナルで
pnpm nx serve aitoearn-server
```
#### 2. フロントエンド `aitoearn-web` を起動
```bash
pnpm install
pnpm run dev
```
</details>
<details>
<summary>🖥️ Electronデスクトッププロジェクトを起動</summary>
```bash
# リポジトリをクローン
git clone https://github.com/yikart/AttAiToEarn.git
# ディレクトリに移動
cd AttAiToEarn
# 依存関係をインストール
npm install
# sqliteをコンパイル(better-sqlite3にはnode-gypとローカルPythonが必要)
npm run rebuild
# 開発を起動
npm run dev
```
ElectronプロジェクトはAiToEarnのデスクトップクライアントを提供します。
</details>
## 貢献ガイド
参加するには[貢献ガイド](./CONTRIBUTING.md)をご覧ください。
## お問い合わせ
利用中に困ったこと、使い方の質問、不具合がある場合は、まず [GitHub Issues](https://github.com/yikart/AiToEarn/issues) でご連絡ください。内容を一元管理し、順番に対応できます。
- Telegram: [https://t.me/harryyyy2025](https://t.me/harryyyy2025)
- WeChat:QRコードをスキャンして追加
<img src="presentation/wechat.jpg" alt="WeChat QRコード" width="200">
## 推奨
- [MuseTalk](https://github.com/TMElyralab/MuseTalk)
- [video_spider](https://github.com/5ime/video_spider)
- [CosyVoice](https://github.com/FunAudioLLM/CosyVoice?tab=readme-ov-file)
- [facefusion](https://github.com/facefusion/facefusion)
- [NarratoAI](https://github.com/linyqh/NarratoAI)
- [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo)
+18
View File
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button onclick="loginClick()">登录</button>
<script>
const appId = "ks715790869885446758";
const appSecret = "cqSvJvBSPJjd-4pBH_4N0Q";
function loginClick() {
window.href = `https://open.kuaishou.com/oauth2/authorize?app_id=${appId}&scope=user_info,user_video_publish&response_type=code&ua=pc&redirect_uri=your_callback_url&state=your_state`
}
</script>
</body>
</html>
+39
View File
@@ -0,0 +1,39 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>小红书api demo</title>
</head>
<body>
<button onclick="start()">开始</button>
<script src="https://fe-static.xhscdn.com/biz-static/goten/xhs-1.0.1.js"></script>
<script>
function start() {
xhs.share({
shareInfo: {
type: 'video', // 必填,笔记类型 'video' | 'normal'
title: '', // 笔记标题
content: '', // 笔记正文
images: [], //图文类型必填,笔记图片,必须是服务器地址,暂时不支持本地文件
video: `https://v2.kwaicdn.com/ksc2/SMdhuIRTGiY9m3ArzOK-oHez6UYVeUxXanHgmB1xBBnsbp74iJ_obAfIGVj_d7RHk6egi72Fhhavh4Qt3kjvcX800rPTC5C4syQmMn0JNmCLXkLc0x0eZWPixd9v8ojThsZlGa5-R-cGkJ6Yz_5w3o6LTNvznfEwWK5IuTbsTyo6JyqnSjXHJXTJt60OfHLe.mp4?pkey=AAUeTe5wQYga8lqgFEiu9QADrMJ-cmeeNpa4a56yDq9mUM4dwIxc3BPJngfj0w8jM6NTOFDeMt89qwdGNeinaf0mnVdWuk3OXt32rpKDlVwqQgN2grlkN-TtNH9tSRnd_hE&tag=1-1741227422-unknown-0-6brouypjgg-0d40512211807271&clientCacheKey=3xm49hn8ttwywgm_b.mp4&di=72fe0292&bp=14944&tt=b&ss=vp`,
cover: '' // 视频封面图,必须是服务器地址,暂时不支持本地文件
},
verifyConfig: {
appKey: 'red.gLvsVoksierVz0uF', //必填,应用的唯一标识,
nonce: 'xln7snms4mh', // 必填,服务端生成签名的随机字符串
timestamp: 1741227154604, // 必填,服务端生成签名的时间戳
signature: '646b7c0c796fdc8f7b15eb1aaeac0b198ce875716742a3d3b8af0b583c66bf71', // 必填,服务端生成的签名
},
fail: (e) => {
console.log(e)
// 调用失败时执行的回调函数
},
})
}
</script>
</body>
</html>
+70
View File
@@ -0,0 +1,70 @@
import axios from "axios";
import crypto from "crypto-js"
const appKey = "red.gLvsVoksierVz0uF";
const appSecret = "f13a2266d1e2c32a553cb7a42ea63c48";
let cachedAccessToken = null;
let accessTokenExpiresAt = 0; // 记录 access_token 过期时间
// 生成小红书签名
function generateSignature(appKey, nonce, timeStamp, appSecret) {
const params = {
appKey,
nonce,
timeStamp,
};
const sortedParams = Object.keys(params)
.sort()
.map((key) => `${key}=${params[key]}`)
.join("&");
const stringToSign = sortedParams + appSecret;
console.log(stringToSign);
return crypto.SHA256(stringToSign).toString();
}
// 获取小红书access_token
const getAccessToken = async (nonce, timestamp) => {
if (cachedAccessToken && Date.now() < accessTokenExpiresAt) {
// 如果 access_token 未过期,则直接返回缓存的 token
return cachedAccessToken;
}
const signature = generateSignature(appKey, nonce, timestamp, appSecret);
console.log({
app_key: appKey,
nonce: nonce,
timestamp: timestamp,
signature: signature,
});
try {
const response = await axios.post("https://edith.xiaohongshu.com/api/sns/v1/ext/access/token", {
app_key: appKey,
nonce: nonce,
timestamp: timestamp,
signature: signature,
}, {
headers: {
"Content-Type": "application/json",
},
});
console.log(response.data);
const { access_token, expires_in } = response.data.data;
// 缓存 access_token 和计算过期时间
cachedAccessToken = access_token;
accessTokenExpiresAt = expires_in;
return cachedAccessToken;
} catch (error) {
console.error('请求失败:', error);
throw error; // 处理错误
}
};
const nonce = Math.random().toString(36).substring(2);
const timestamp = Date.now();
const accessToken = await getAccessToken(nonce, timestamp);
const signature = generateSignature(appKey, nonce, timestamp, accessToken);
console.log("appKey", appKey);
console.log("nonce", nonce);
console.log("timestamp", timestamp);
console.log("signature", signature);
+222
View File
@@ -0,0 +1,222 @@
services:
mongodb:
image: mongo:latest
container_name: aitoearn-mongodb
restart: unless-stopped
entrypoint: ["bash", "-c", "openssl rand -base64 756 > /data/mongodb-keyfile && chmod 400 /data/mongodb-keyfile && chown mongodb:mongodb /data/mongodb-keyfile && exec docker-entrypoint.sh mongod --replSet rs0 --keyFile /data/mongodb-keyfile"]
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password
ports:
- "27017:27017"
volumes:
- mongodb-data:/data/db
- mongodb-config:/data/configdb
networks:
- aitoearn-network
healthcheck:
test: ["CMD-SHELL", "mongosh mongodb://admin:password@localhost:27017/?authSource=admin --quiet --eval \"try { quit(rs.status().ok === 1 ? 0 : 1) } catch(e) { quit(1) }\""]
interval: 10s
timeout: 5s
retries: 10
start_period: 40s
mongodb-rs-init:
image: mongo:latest
container_name: aitoearn-mongodb-rs-init
depends_on:
mongodb:
condition: service_started
entrypoint: ["/bin/sh", "-c", "until mongosh mongodb://admin:password@mongodb:27017/?authSource=admin --quiet --eval \"db.adminCommand('ping').ok\" >/dev/null 2>&1; do sleep 2; done; mongosh mongodb://admin:password@mongodb:27017/?authSource=admin --eval \"try { rs.initiate({_id:'rs0', members:[{_id:0, host:'mongodb:27017'}]}) } catch(e) { if (e.codeName!=='AlreadyInitialized') throw e }\""]
restart: "no"
networks:
- aitoearn-network
redis:
image: redis:latest
container_name: aitoearn-redis
restart: unless-stopped
command: redis-server --requirepass password
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- aitoearn-network
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 3s
retries: 5
start_period: 30s
rustfs:
image: rustfs/rustfs:latest
container_name: aitoearn-rustfs
hostname: rustfs.local
restart: unless-stopped
environment:
RUSTFS_ACCESS_KEY: rustfsadmin
RUSTFS_SECRET_KEY: rustfsadmin
ports:
- "9001:9001"
volumes:
- rustfs-data:/data
networks:
aitoearn-network:
aliases:
- rustfs.local
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
aitoearn-init:
image: node:lts-alpine
container_name: aitoearn-init
restart: "no"
depends_on:
mongodb:
condition: service_healthy
volumes:
- ./scripts/init.mjs:/app/init.mjs:ro
- ./scripts/init-package.json:/app/package.json:ro
- init-data:/data/init
working_dir: /app
environment:
MONGO_URI: mongodb://admin:password@mongodb:27017
JWT_SECRET: change-this-jwt-secret
DB_NAME: aitoearn
AUTO_LOGIN_TOKEN_PATH: /data/init/token.txt
entrypoint: /bin/sh -c "npm install --omit=dev 2>/dev/null && node init.mjs"
networks:
- aitoearn-network
rustfs-init:
image: minio/mc:latest
container_name: aitoearn-rustfs-init
depends_on:
rustfs:
condition: service_healthy
networks:
- aitoearn-network
entrypoint: >
/bin/sh -c "
mc alias set rustfs http://rustfs.local:9000 rustfsadmin rustfsadmin;
mc mb rustfs/aitoearn --ignore-existing;
mc anonymous set download rustfs/aitoearn;
exit 0;
"
aitoearn-ai:
image: aitoearn/aitoearn-ai:latest
pull_policy: always
container_name: aitoearn-ai
restart: unless-stopped
depends_on:
mongodb:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- ./project/aitoearn-backend/apps/aitoearn-ai/config/config.yaml:/app/config.yaml:rw
networks:
- aitoearn-network
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3010/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
aitoearn-server:
image: aitoearn/aitoearn-server:latest
pull_policy: always
container_name: aitoearn-server
restart: unless-stopped
depends_on:
mongodb:
condition: service_healthy
redis:
condition: service_healthy
aitoearn-ai:
condition: service_healthy
volumes:
- ./project/aitoearn-backend/apps/aitoearn-server/config/config.yaml:/app/config.yaml:rw
networks:
- aitoearn-network
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3002/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
aitoearn-web:
image: aitoearn/aitoearn-web:latest
pull_policy: always
container_name: aitoearn-web
restart: unless-stopped
depends_on:
aitoearn-server:
condition: service_healthy
aitoearn-init:
condition: service_completed_successfully
volumes:
- init-data:/data/init:ro
networks:
- aitoearn-network
command: ["/bin/sh", "-c", "AUTO_LOGIN_TOKEN=$$(cat /data/init/token.txt 2>/dev/null || true); if [ -z \"$$AUTO_LOGIN_TOKEN\" ]; then echo 'AUTO_LOGIN_TOKEN is missing. Check the aitoearn-init container logs.' >&2; exit 1; fi; export AUTO_LOGIN_TOKEN; exec node server.js"]
environment:
NODE_ENV: production
NEXT_TELEMETRY_DISABLED: 1
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000', (r) => { process.exit(r.statusCode < 500 ? 0 : 1) }).on('error', () => process.exit(1))"]
interval: 10s
timeout: 5s
retries: 6
start_period: 30s
nginx:
image: nginx:alpine
container_name: aitoearn-nginx
restart: unless-stopped
depends_on:
aitoearn-web:
condition: service_healthy
aitoearn-server:
condition: service_healthy
aitoearn-ai:
condition: service_healthy
ports:
- "8080:80"
- "9000:9000"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
networks:
- aitoearn-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/_nhealth"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
networks:
aitoearn-network:
driver: bridge
name: aitoearn-network
volumes:
mongodb-data:
driver: local
mongodb-config:
driver: local
redis-data:
driver: local
rustfs-data:
driver: local
init-data:
driver: local
+132
View File
@@ -0,0 +1,132 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
client_max_body_size 50m;
map $http_origin $cors_origin {
default "";
"~^https?://(.*\.)?(dev\.)?aitoearn\.ai$" $http_origin;
"~^https?://localhost(:[0-9]+)?$" $http_origin;
"~^https?://127\.0\.0\.1(:[0-9]+)?$" $http_origin;
"~^https?://0\.0\.0\.0(:[0-9]+)?$" $http_origin;
}
proxy_set_header Host $proxy_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_http_version 1.1;
# Hide backend CORS headers, managed by nginx
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Max-Age;
proxy_connect_timeout 120s;
proxy_send_timeout 60s;
proxy_read_timeout 86400s;
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Accept-Encoding, Accept-Language, Authorization, Cache-Control, Content-Type, Cookie, DNT, Origin, Pragma, Referer, User-Agent, X-Requested-With, X-Request-ID, X-CSRF-Token, X-Auth-Token, X-HTTP-Method-Override, Next-Router-Prefetch, Next-Router-State-Tree, Next-Url, RSC, Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform, Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site, Priority, If-None-Match, If-Modified-Since" always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Max-Age 86400 always;
server {
listen 80;
listen [::]:80;
server_name _;
location = /_nhealth {
access_log off;
default_type text/plain;
return 200 "healthy\n";
}
if ($request_method = 'OPTIONS') {
return 204;
}
location /api/ai/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://aitoearn-ai:3010;
}
location /api/agent/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://aitoearn-ai:3010;
}
location /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://aitoearn-server:3002;
}
location /oss/ {
proxy_pass http://aitoearn-rustfs:9000/aitoearn/;
}
location / {
proxy_pass http://aitoearn-web:3000;
}
}
# S3 代理:浏览器通过此端口上传文件到 rustfs
server {
listen 9000;
listen [::]:9000;
server_name _;
client_max_body_size 500m;
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, HEAD, PUT, POST, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers * always;
add_header Access-Control-Expose-Headers "ETag" always;
add_header Access-Control-Max-Age 86400 always;
if ($request_method = 'OPTIONS') {
return 204;
}
location / {
proxy_pass http://aitoearn-rustfs:9000;
proxy_set_header Host $http_host;
proxy_http_version 1.1;
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 655 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

@@ -0,0 +1,49 @@
---
name: caveman
description: >
Ultra-compressed communication mode. Cuts token usage ~75% by dropping
filler, articles, and pleasantries while keeping full technical accuracy.
Use when user says "caveman mode", "talk like caveman", "use caveman",
"less tokens", "be brief", or invokes /caveman.
---
Respond terse like smart caveman. All technical substance stay. Only fluff die.
## Persistence
ACTIVE EVERY RESPONSE once triggered. No revert after many turns. No filler drift. Still active if unsure. Off only when user says "stop caveman" or "normal mode".
## Rules
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Abbreviate common terms (DB/auth/config/req/res/fn/impl). Strip conjunctions. Use arrows for causality (X -> Y). One word when one word enough.
Technical terms stay exact. Code blocks unchanged. Errors quoted exact.
Pattern: `[thing] [action] [reason]. [next step].`
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
### Examples
**"Why React component re-render?"**
> Inline obj prop -> new ref -> re-render. `useMemo`.
**"Explain database connection pooling."**
> Pool = reuse DB conn. Skip handshake -> fast under load.
## Auto-Clarity Exception
Drop caveman temporarily for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done.
Example -- destructive op:
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
>
> ```sql
> DROP TABLE users;
> ```
>
> Caveman resume. Verify backup exist first.
@@ -0,0 +1,117 @@
---
name: diagnose
description: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says "diagnose this" / "debug this", reports a bug, says something is broken/throwing/failing, or describes a performance regression.
---
# Diagnose
A discipline for hard bugs. Skip phases only when explicitly justified.
When exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.
## Phase 1 — Build a feedback loop
**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you.
Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**
### Ways to construct one — try them in roughly this order
1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e.
2. **Curl / HTTP script** against a running dev server.
3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot.
4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network.
5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation.
6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call.
7. **Property / fuzz loop.** If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode.
8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can `git bisect run` it.
9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs.
10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you.
Build the right feedback loop, and the bug is 90% fixed.
### Iterate on the loop itself
Treat the loop as a product. Once you have _a_ loop, ask:
- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.)
- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".)
- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.)
A 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower.
### Non-deterministic bugs
The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable.
### When you genuinely cannot build a loop
Stop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop.
Do not proceed to Phase 2 until you have a loop you believe in.
## Phase 2 — Reproduce
Run the loop. Watch the bug appear.
Confirm:
- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix.
- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against).
- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it.
Do not proceed until you reproduce the bug.
## Phase 3 — Hypothesise
Generate **35 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea.
Each hypothesis must be **falsifiable**: state the prediction it makes.
> Format: "If <X> is the cause, then <changing Y> will make the bug disappear / <changing Z> will make it worse."
If you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it.
**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly ("we just deployed a change to #3"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK.
## Phase 4 — Instrument
Each probe must map to a specific prediction from Phase 3. **Change one variable at a time.**
Tool preference:
1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs.
2. **Targeted logs** at the boundaries that distinguish hypotheses.
3. Never "log everything and grep".
**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die.
**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second.
## Phase 5 — Fix + regression test
Write the regression test **before the fix** — but only if there is a **correct seam** for it.
A correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence.
**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase.
If a correct seam exists:
1. Turn the minimised repro into a failing test at that seam.
2. Watch it fail.
3. Apply the fix.
4. Watch it pass.
5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario.
## Phase 6 — Cleanup + post-mortem
Required before declaring done:
- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)
- [ ] Regression test passes (or absence of seam is documented)
- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)
- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)
- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns
**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started.
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Human-in-the-loop reproduction loop.
# Copy this file, edit the steps below, and run it.
# The agent runs the script; the user follows prompts in their terminal.
#
# Usage:
# bash hitl-loop.template.sh
#
# Two helpers:
# step "<instruction>" → show instruction, wait for Enter
# capture VAR "<question>" → show question, read response into VAR
#
# At the end, captured values are printed as KEY=VALUE for the agent to parse.
set -euo pipefail
step() {
printf '\n>>> %s\n' "$1"
read -r -p " [Enter when done] " _
}
capture() {
local var="$1" question="$2" answer
printf '\n>>> %s\n' "$question"
read -r -p " > " answer
printf -v "$var" '%s' "$answer"
}
# --- edit below ---------------------------------------------------------
step "Open the app at http://localhost:3000 and sign in."
capture ERRORED "Click the 'Export' button. Did it throw an error? (y/n)"
capture ERROR_MSG "Paste the error message (or 'none'):"
# --- edit above ---------------------------------------------------------
printf '\n--- Captured ---\n'
printf 'ERRORED=%s\n' "$ERRORED"
printf 'ERROR_MSG=%s\n' "$ERROR_MSG"
@@ -0,0 +1,10 @@
---
name: grill-me
description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
---
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
Ask the questions one at a time.
If a question can be answered by exploring the codebase, explore the codebase instead.
@@ -0,0 +1,47 @@
# ADR Format
ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc.
Create the `docs/adr/` directory lazily — only when the first ADR is needed.
## Template
```md
# {Short title of the decision}
{1-3 sentences: what's the context, what did we decide, and why.}
```
That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections.
## Optional sections
Only include these when they add genuine value. Most ADRs won't need them.
- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited
- **Considered Options** — only when the rejected alternatives are worth remembering
- **Consequences** — only when non-obvious downstream effects need to be called out
## Numbering
Scan `docs/adr/` for the highest existing number and increment by one.
## When to offer an ADR
All three of these must be true:
1. **Hard to reverse** — the cost of changing your mind later is meaningful
2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?"
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing."
### What qualifies
- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres."
- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP."
- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out.
- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s.
- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate.
- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract."
- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months.
@@ -0,0 +1,77 @@
# CONTEXT.md Format
## Structure
```md
# {Context Name}
{One or two sentence description of what this context is and why it exists.}
## Language
**Order**:
{A concise description of the term}
_Avoid_: Purchase, transaction
**Invoice**:
A request for payment sent to a customer after delivery.
_Avoid_: Bill, payment request
**Customer**:
A person or organization that places orders.
_Avoid_: Client, buyer, account
## Relationships
- An **Order** produces one or more **Invoices**
- An **Invoice** belongs to exactly one **Customer**
## Example dialogue
> **Dev:** "When a **Customer** places an **Order**, do we create the **Invoice** immediately?"
> **Domain expert:** "No — an **Invoice** is only generated once a **Fulfillment** is confirmed."
## Flagged ambiguities
- "account" was used to mean both **Customer** and **User** — resolved: these are distinct concepts.
```
## Rules
- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others as aliases to avoid.
- **Flag conflicts explicitly.** If a term is used ambiguously, call it out in "Flagged ambiguities" with a clear resolution.
- **Keep definitions tight.** One sentence max. Define what it IS, not what it does.
- **Show relationships.** Use bold term names and express cardinality where obvious.
- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs.
- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine.
- **Write an example dialogue.** A conversation between a dev and a domain expert that demonstrates how the terms interact naturally and clarifies boundaries between related concepts.
## Single vs multi-context repos
**Single context (most repos):** One `CONTEXT.md` at the repo root.
**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other:
```md
# Context Map
## Contexts
- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders
- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments
- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping
## Relationships
- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking
- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices
- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money`
```
The skill infers which structure applies:
- If `CONTEXT-MAP.md` exists, read it to find contexts
- If only a root `CONTEXT.md` exists, single context
- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved
When multiple contexts exist, infer which one the current topic relates to. If unclear, ask.
@@ -0,0 +1,88 @@
---
name: grill-with-docs
description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.
---
<what-to-do>
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
Ask the questions one at a time, waiting for feedback on each question before continuing.
If a question can be answered by exploring the codebase, explore the codebase instead.
</what-to-do>
<supporting-info>
## Domain awareness
During codebase exploration, also look for existing documentation:
### File structure
Most repos have a single context:
```
/
├── CONTEXT.md
├── docs/
│ └── adr/
│ ├── 0001-event-sourced-orders.md
│ └── 0002-postgres-for-write-model.md
└── src/
```
If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:
```
/
├── CONTEXT-MAP.md
├── docs/
│ └── adr/ ← system-wide decisions
├── src/
│ ├── ordering/
│ │ ├── CONTEXT.md
│ │ └── docs/adr/ ← context-specific decisions
│ └── billing/
│ ├── CONTEXT.md
│ └── docs/adr/
```
Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed.
## During the session
### Challenge against the glossary
When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?"
### Sharpen fuzzy language
When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things."
### Discuss concrete scenarios
When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.
### Cross-reference with code
When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?"
### Update CONTEXT.md inline
When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md).
Don't couple `CONTEXT.md` to implementation details. Only include terms that are meaningful to domain experts.
### Offer ADRs sparingly
Only offer to create an ADR when all three are true:
1. **Hard to reverse** — the cost of changing your mind later is meaningful
2. **Surprising without context** — a future reader will wonder "why did they do it this way?"
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md).
</supporting-info>
@@ -0,0 +1,13 @@
---
name: handoff
description: Compact the current conversation into a handoff document for another agent to pick up.
argument-hint: "What will the next session be used for?"
---
Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save it to a path produced by `mktemp -t handoff-XXXXXX.md` (read the file before you write to it).
Suggest the skills to be used, if any, by the next session.
Do not duplicate content already captured in other artifacts (PRDs, plans, ADRs, issues, commits, diffs). Reference them by path or URL instead.
If the user passed arguments, treat them as a description of what the next session will focus on and tailor the doc accordingly.
@@ -0,0 +1,37 @@
# Deepening
How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**.
## Dependency categories
When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam.
### 1. In-process
Pure computation, in-memory state, no I/O. Always deepenable — merge the modules and test through the new interface directly. No adapter needed.
### 2. Local-substitutable
Dependencies that have local test stand-ins (PGLite for Postgres, in-memory filesystem). Deepenable if the stand-in exists. The deepened module is tested with the stand-in running in the test suite. The seam is internal; no port at the module's external interface.
### 3. Remote but owned (Ports & Adapters)
Your own services across a network boundary (microservices, internal APIs). Define a **port** (interface) at the seam. The deep module owns the logic; the transport is injected as an **adapter**. Tests use an in-memory adapter. Production uses an HTTP/gRPC/queue adapter.
Recommendation shape: *"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."*
### 4. True external (Mock)
Third-party services (Stripe, Twilio, etc.) you don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter.
## Seam discipline
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). A single-adapter seam is just indirection.
- **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them.
## Testing strategy: replace, don't layer
- Old unit tests on shallow modules become waste once tests at the deepened module's interface exist — delete them.
- Write new tests at the deepened module's interface. The **interface is the test surface**.
- Tests assert on observable outcomes through the interface, not internal state.
- Tests should survive internal refactors — they describe behaviour, not implementation. If a test has to change when the implementation changes, it's testing past the interface.
@@ -0,0 +1,44 @@
# Interface Design
When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout) — your first idea is unlikely to be the best.
Uses the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**, **leverage**.
## Process
### 1. Frame the problem space
Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate:
- The constraints any new interface would need to satisfy
- The dependencies it would rely on, and which category they fall into (see [DEEPENING.md](DEEPENING.md))
- A rough illustrative code sketch to ground the constraints — not a proposal, just a way to make the constraints concrete
Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel.
### 2. Spawn sub-agents
Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module.
Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint:
- Agent 1: "Minimize the interface — aim for 13 entry points max. Maximise leverage per entry point."
- Agent 2: "Maximise flexibility — support many use cases and extension."
- Agent 3: "Optimise for the most common caller — make the default case trivial."
- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies."
Include both [LANGUAGE.md](LANGUAGE.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language.
Each sub-agent outputs:
1. Interface (types, methods, params — plus invariants, ordering, error modes)
2. Usage example showing how callers use it
3. What the implementation hides behind the seam
4. Dependency strategy and adapters (see [DEEPENING.md](DEEPENING.md))
5. Trade-offs — where leverage is high, where it's thin
### 3. Present and compare
Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by **depth** (leverage at the interface), **locality** (where change concentrates), and **seam placement**.
After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated — the user wants a strong read, not a menu.
@@ -0,0 +1,53 @@
# Language
Shared vocabulary for every suggestion this skill makes. Use these terms exactly — don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point.
## Terms
**Module**
Anything with an interface and an implementation. Deliberately scale-agnostic — applies equally to a function, class, package, or tier-spanning slice.
_Avoid_: unit, component, service.
**Interface**
Everything a caller must know to use the module correctly. Includes the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics.
_Avoid_: API, signature (too narrow — those refer only to the type-level surface).
**Implementation**
What's inside a module — its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise.
**Depth**
Leverage at the interface — the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface. A module is **shallow** when the interface is nearly as complex as the implementation.
**Seam** _(from Michael Feathers)_
A place where you can alter behaviour without editing in that place. The *location* at which a module's interface lives. Choosing where to put the seam is its own design decision, distinct from what goes behind it.
_Avoid_: boundary (overloaded with DDD's bounded context).
**Adapter**
A concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside).
**Leverage**
What callers get from depth. More capability per unit of interface they have to learn. One implementation pays back across N call sites and M tests.
**Locality**
What maintainers get from depth. Change, bugs, knowledge, and verification concentrate at one place rather than spreading across callers. Fix once, fixed everywhere.
## Principles
- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface.
- **The deletion test.** Imagine deleting the module. If complexity vanishes, the module wasn't hiding anything (it was a pass-through). If complexity reappears across N callers, the module was earning its keep.
- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test *past* the interface, the module is probably the wrong shape.
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it.
## Relationships
- A **Module** has exactly one **Interface** (the surface it presents to callers and tests).
- **Depth** is a property of a **Module**, measured against its **Interface**.
- A **Seam** is where a **Module**'s **Interface** lives.
- An **Adapter** sits at a **Seam** and satisfies the **Interface**.
- **Depth** produces **Leverage** for callers and **Locality** for maintainers.
## Rejected framings
- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.
- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow — interface here includes every fact a caller must know.
- **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**.
@@ -0,0 +1,71 @@
---
name: improve-codebase-architecture
description: Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable.
---
# Improve Codebase Architecture
Surface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability.
## Glossary
Use these terms exactly in every suggestion. Consistent language is the point — don't drift into "component," "service," "API," or "boundary." Full definitions in [LANGUAGE.md](LANGUAGE.md).
- **Module** — anything with an interface and an implementation (function, class, package, slice).
- **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature.
- **Implementation** — the code inside.
- **Depth** — leverage at the interface: a lot of behaviour behind a small interface. **Deep** = high leverage. **Shallow** = interface nearly as complex as the implementation.
- **Seam** — where an interface lives; a place behaviour can be altered without editing in place. (Use this, not "boundary.")
- **Adapter** — a concrete thing satisfying an interface at a seam.
- **Leverage** — what callers get from depth.
- **Locality** — what maintainers get from depth: change, bugs, knowledge concentrated in one place.
Key principles (see [LANGUAGE.md](LANGUAGE.md) for the full list):
- **Deletion test**: imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.
- **The interface is the test surface.**
- **One adapter = hypothetical seam. Two adapters = real seam.**
This skill is _informed_ by the project's domain model. The domain language gives names to good seams; ADRs record decisions the skill should not re-litigate.
## Process
### 1. Explore
Read the project's domain glossary and any ADRs in the area you're touching first.
Then use the Agent tool with `subagent_type=Explore` to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction:
- Where does understanding one concept require bouncing between many small modules?
- Where are modules **shallow** — interface nearly as complex as the implementation?
- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)?
- Where do tightly-coupled modules leak across their seams?
- Which parts of the codebase are untested, or hard to test through their current interface?
Apply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A "yes, concentrates" is the signal you want.
### 2. Present candidates
Present a numbered list of deepening opportunities. For each candidate:
- **Files** — which files/modules are involved
- **Problem** — why the current architecture is causing friction
- **Solution** — plain English description of what would change
- **Benefits** — explained in terms of locality and leverage, and also in how tests would improve
**Use CONTEXT.md vocabulary for the domain, and [LANGUAGE.md](LANGUAGE.md) vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service."
**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly (e.g. _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids.
Do NOT propose interfaces yet. Ask the user: "Which of these would you like to explore?"
### 3. Grilling loop
Once the user picks a candidate, drop into a grilling conversation. Walk the design tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive.
Side effects happen inline as decisions crystallize:
- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md` — same discipline as `/grill-with-docs` (see [CONTEXT-FORMAT.md](../grill-with-docs/CONTEXT-FORMAT.md)). Create the file lazily if it doesn't exist.
- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there.
- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones. See [ADR-FORMAT.md](../grill-with-docs/ADR-FORMAT.md).
- **Want to explore alternative interfaces for the deepened module?** See [INTERFACE-DESIGN.md](INTERFACE-DESIGN.md).
@@ -0,0 +1,79 @@
# Logic Prototype
A tiny interactive terminal app that lets the user drive a state model by hand. Use this when the question is about **business logic, state transitions, or data shape** — the kind of thing that looks reasonable on paper but only feels wrong once you push it through real cases.
## When this is the right shape
- "I'm not sure if this state machine handles the edge case where X then Y."
- "Does this data model actually let me represent the case where..."
- "I want to feel out what the API should look like before writing it."
- Anything where the user wants to **press buttons and watch state change**.
If the question is "what should this look like" — wrong branch. Use [UI.md](UI.md).
## Process
### 1. State the question
Before writing code, write down what state model and what question you're prototyping. One paragraph, in the prototype's README or a comment at the top of the file. A logic prototype that answers the wrong question is pure waste — make the question explicit so it can be checked later, whether the user is watching now or returning to it AFK.
### 2. Pick the language
Use whatever the host project uses. If the project has no obvious runtime (e.g. a docs repo), ask.
Match the project's existing conventions for tooling — don't add a new package manager or runtime just for the prototype.
### 3. Isolate the logic in a portable module
Put the actual logic — the bit that's answering the question — behind a small, pure interface that could be lifted out and dropped into the real codebase later. The TUI around it is throwaway; the logic module shouldn't be.
The right shape depends on the question:
- **A pure reducer** — `(state, action) => state`. Good when actions are discrete events and state is a single value.
- **A state machine** — explicit states and transitions. Good when "which actions are even legal right now" is part of the question.
- **A small set of pure functions** over a plain data type. Good when there's no implicit current state — just transformations.
- **A class or module with a clear method surface** when the logic genuinely owns ongoing internal state.
Pick whichever shape best fits the question being asked, *not* whichever is easiest to wire to a TUI. Keep it pure: no I/O, no terminal code, no `console.log` for control flow. The TUI imports it and calls into it; nothing flows the other direction.
This is what makes the prototype useful past its own lifetime. When the question's been answered, the validated reducer / machine / function set can be lifted into the real module — the TUI shell gets deleted.
### 4. Build the smallest TUI that exposes the state
Build it as a **lightweight TUI** — on every tick, clear the screen (`console.clear()` / `print("\033[2J\033[H")` / equivalent) and re-render the whole frame. The user should always see one stable view, not an ever-growing scrollback.
Each frame has two parts, in this order:
1. **Current state**, pretty-printed and diff-friendly (one field per line, or formatted JSON). Use **bold** for field names or section headers and **dim** for less important context (timestamps, IDs, derived values). Native ANSI escape codes are fine — `\x1b[1m` bold, `\x1b[2m` dim, `\x1b[0m` reset. No need to pull in a styling library unless one is already in the project.
2. **Keyboard shortcuts**, listed at the bottom: `[a] add user [d] delete user [t] tick clock [q] quit`. Bold the key, dim the description, or vice-versa — whatever reads cleanly.
Behaviour:
1. **Initialise state** — a single in-memory object/struct. Render the first frame on start.
2. **Read one keystroke (or one line)** at a time, dispatch to a handler that mutates state.
3. **Re-render** the full frame after every action — don't append, replace.
4. **Loop until quit.**
The whole frame should fit on one screen.
### 5. Make it runnable in one command
Add a script to the project's existing task runner (`package.json` scripts, `Makefile`, `justfile`, `pyproject.toml`). The user should run `pnpm run <prototype-name>` or equivalent — never need to remember a path.
If the host project has no task runner, just put the command at the top of the prototype's README.
### 6. Hand it over
Give the user the run command. They'll drive it themselves; the interesting moments are when they say "wait, that shouldn't be possible" or "huh, I assumed X would be different" — those are the bugs in the _idea_, which is the whole point. If they want new actions added, add them. Prototypes evolve.
### 7. Capture the answer
When the prototype has done its job, the answer to the question is the only thing worth keeping. If the user is around, ask what it taught them. If not, leave a `NOTES.md` next to the prototype so the answer can be filled in (or filled in by you, if you've watched the session) before the prototype gets deleted.
## Anti-patterns
- **Don't add tests.** A prototype that needs tests is no longer a prototype.
- **Don't wire it to the real database.** Use an in-memory store unless the question is specifically about persistence.
- **Don't generalise.** No "what if we wanted to support X later." The prototype answers one question.
- **Don't blur the logic and the TUI together.** If the reducer / state machine references `console.log`, prompts, or terminal escape codes, it's no longer portable. Keep the TUI as a thin shell over a pure module.
- **Don't ship the TUI shell into production.** The shell is optimised for being driven by hand from a terminal. The logic module behind it is the bit worth keeping.
@@ -0,0 +1,30 @@
---
name: prototype
description: Build a throwaway prototype to flesh out a design before committing to it. Routes between two branches — a runnable terminal app for state/business-logic questions, or several radically different UI variations toggleable from one route. Use when the user wants to prototype, sanity-check a data model or state machine, mock up a UI, explore design options, or says "prototype this", "let me play with it", "try a few designs".
---
# Prototype
A prototype is **throwaway code that answers a question**. The question decides the shape.
## Pick a branch
Identify which question is being answered — from the user's prompt, the surrounding code, or by asking if the user is around:
- **"Does this logic / state model feel right?"** → [LOGIC.md](LOGIC.md). Build a tiny interactive terminal app that pushes the state machine through cases that are hard to reason about on paper.
- **"What should this look like?"** → [UI.md](UI.md). Generate several radically different UI variations on a single route, switchable via a URL search param and a floating bottom bar.
The two branches produce very different artifacts — getting this wrong wastes the whole prototype. If the question is genuinely ambiguous and the user isn't reachable, default to whichever branch better matches the surrounding code (a backend module → logic; a page or component → UI) and state the assumption at the top of the prototype.
## Rules that apply to both
1. **Throwaway from day one, and clearly marked as such.** Locate the prototype code close to where it will actually be used (next to the module or page it's prototyping for) so context is obvious — but name it so a casual reader can see it's a prototype, not production. For throwaway UI routes, obey whatever routing convention the project already uses; don't invent a new top-level structure.
2. **One command to run.** Whatever the project's existing task runner supports — `pnpm <name>`, `python <path>`, `bun <path>`, etc. The user must be able to start it without thinking.
3. **No persistence by default.** State lives in memory. Persistence is the thing the prototype is _checking_, not something it should depend on. If the question explicitly involves a database, hit a scratch DB or a local file with a clear "PROTOTYPE — wipe me" name.
4. **Skip the polish.** No tests, no error handling beyond what makes the prototype _runnable_, no abstractions. The point is to learn something fast and then delete it.
5. **Surface the state.** After every action (logic) or on every variant switch (UI), print or render the full relevant state so the user can see what changed.
6. **Delete or absorb when done.** When the prototype has answered its question, either delete it or fold the validated decision into the real code — don't leave it rotting in the repo.
## When done
The _answer_ is the only thing worth keeping from a prototype. Capture it somewhere durable (commit message, ADR, issue, or a `NOTES.md` next to the prototype) along with the question it was answering. If the user is around, that capture is a quick conversation; if not, leave the placeholder so they (or you, on the next pass) can fill in the verdict before deleting the prototype.
@@ -0,0 +1,112 @@
# UI Prototype
Generate **several radically different UI variations** on a single route, switchable from a floating bottom bar. The user flips between variants in the browser, picks one (or steals bits from each), then throws the rest away.
If the question is about logic/state rather than what something looks like — wrong branch. Use [LOGIC.md](LOGIC.md).
## When this is the right shape
- "What should this page look like?"
- "I want to see a few options for this dashboard before committing."
- "Try a different layout for the settings screen."
- Any time the user would otherwise spend a day picking between three vague mockups in their head.
## Two sub-shapes — strongly prefer sub-shape A
A UI prototype is much easier to judge when it's **butting up against the rest of the app** — real header, real sidebar, real data, real density. A throwaway route on its own is a vacuum: every variant looks fine in isolation. Default to sub-shape A whenever there's a plausible existing page to host the variants. Only reach for sub-shape B if the prototype genuinely has no nearby home.
### Sub-shape A — adjustment to an existing page (preferred)
The route already exists. Variants are rendered **on the same route**, gated by a `?variant=` URL search param. The existing data fetching, params, and auth all stay — only the rendering swaps. This is the default; pick it unless there's a specific reason not to.
If the prototype is for something that doesn't yet have a page but *would naturally live inside one* (a new section of the dashboard, a new card on the settings screen, a new step in an existing flow) — that's still sub-shape A. Mount the variants inside the host page.
### Sub-shape B — a new page (last resort)
Only use this when the thing being prototyped genuinely has no existing page to live inside — e.g. an entirely new top-level surface, or a flow that can't be embedded anywhere sensible.
Create a **throwaway route** following whatever routing convention the project already uses — don't invent a new top-level structure. Name it so it's obviously a prototype (e.g. include the word `prototype` in the path or filename). Same `?variant=` pattern.
Before committing to sub-shape B, sanity-check: is there really no existing page this could be embedded in? An empty route hides design problems that a populated one would expose.
In both sub-shapes the floating bottom bar is identical.
## Process
### 1. State the question and pick N
Default to **3 variants**. More than 5 stops being radically different and starts being noise — cap there.
Write down the plan in one line, in the prototype's location or a top-of-file comment:
> "Three variants of the settings page, switchable via `?variant=`, on the existing `/settings` route."
This works whether the user is here to push back or not.
### 2. Generate radically different variants
Draft each variant. Hold each one to:
- The page's purpose and the data it has access to.
- The project's component library / styling system (TailwindCSS, shadcn, MUI, plain CSS, whatever).
- A clear exported component name, e.g. `VariantA`, `VariantB`, `VariantC`.
Variants must be **structurally different** — different layout, different information hierarchy, different primary affordance, not just different colours. Three slightly-tweaked card grids isn't a UI prototype, it's wallpaper. If two drafts come out too similar, redo one with explicit "do not use a card grid" guidance.
### 3. Wire them together
Create a single switcher component on the route:
```tsx
// pseudo-code — adapt to the project's framework
const variant = searchParams.get('variant') ?? 'A';
return (
<>
{variant === 'A' && <VariantA {...data} />}
{variant === 'B' && <VariantB {...data} />}
{variant === 'C' && <VariantC {...data} />}
<PrototypeSwitcher variants={['A','B','C']} current={variant} />
</>
);
```
For sub-shape A (existing page): keep all the existing data fetching above the switcher; only the rendered subtree changes per variant.
For sub-shape B (new page): the throwaway route under `/prototype/<name>` mounts the same switcher.
### 4. Build the floating switcher
A small fixed-position bar at the bottom-centre of the screen with three pieces:
- **Left arrow** — cycles to the previous variant (wraps around).
- **Variant label** — shows the current variant key and, if the variant exports a name, that name too. e.g. `B — Sidebar layout`.
- **Right arrow** — cycles forward (wraps around).
Behaviour:
- Clicking an arrow updates the URL search param (use the framework's router — `router.replace` on Next, `navigate` on React Router, etc) so the variant is shareable and reload-stable.
- Keyboard: `←` and `→` arrow keys also cycle. Don't intercept arrow keys when an `<input>`, `<textarea>`, or `[contenteditable]` is focused.
- Visually distinct from the page (e.g. high-contrast pill, subtle shadow) so it's obviously not part of the design being evaluated.
- Hidden in production builds — gate on `process.env.NODE_ENV !== 'production'` or an equivalent check, so a stray prototype merge can't ship the bar to users.
Put the switcher in a single shared component so both sub-shapes can reuse it. Locate it wherever shared UI lives in the project.
### 5. Hand it over
Surface the URL (and the `?variant=` keys). The user will flip through whenever they get to it. The interesting feedback is usually **"I want the header from B with the sidebar from C"** — that's the actual design they want.
### 6. Capture the answer and clean up
Once a variant has won, write down which one and why (commit message, ADR, issue, or a `NOTES.md` next to the prototype if running AFK and the user hasn't responded yet). Then:
- **Sub-shape A** — delete the losing variants and the switcher; fold the winner into the existing page.
- **Sub-shape B** — promote the winning variant to a real route, delete the throwaway route and the switcher.
Don't leave variant components or the switcher lying around. They rot fast and confuse the next reader.
## Anti-patterns
- **Variants that differ only in colour or copy.** That's a tweak, not a prototype. Real variants disagree about structure.
- **Sharing too much code between variants.** A shared `<Header>` is fine; a shared `<Layout>` defeats the point. Each variant should be free to throw out the layout.
- **Wiring variants to real mutations.** Read-only prototypes are fine. If a variant needs to mutate, point it at a stub — the question is "what should this look like", not "does the backend work".
- **Promoting the prototype directly to production.** The variant code was written under prototype constraints (no tests, minimal error handling). Rewrite it properly when you fold it in.
@@ -0,0 +1,121 @@
---
name: setup-matt-pocock-skills
description: Sets up an `## Agent skills` block in AGENTS.md/CLAUDE.md and `docs/agents/` so the engineering skills know this repo's issue tracker (GitHub or local markdown), triage label vocabulary, and domain doc layout. Run before first use of `to-issues`, `to-prd`, `triage`, `diagnose`, `tdd`, `improve-codebase-architecture`, or `zoom-out` — or if those skills appear to be missing context about the issue tracker, triage labels, or domain docs.
disable-model-invocation: true
---
# Setup Matt Pocock's Skills
Scaffold the per-repo configuration that the engineering skills assume:
- **Issue tracker** — where issues live (GitHub by default; local markdown is also supported out of the box)
- **Triage labels** — the strings used for the five canonical triage roles
- **Domain docs** — where `CONTEXT.md` and ADRs live, and the consumer rules for reading them
This is a prompt-driven skill, not a deterministic script. Explore, present what you found, confirm with the user, then write.
## Process
### 1. Explore
Look at the current repo to understand its starting state. Read whatever exists; don't assume:
- `git remote -v` and `.git/config` — is this a GitHub repo? Which one?
- `AGENTS.md` and `CLAUDE.md` at the repo root — does either exist? Is there already an `## Agent skills` section in either?
- `CONTEXT.md` and `CONTEXT-MAP.md` at the repo root
- `docs/adr/` and any `src/*/docs/adr/` directories
- `docs/agents/` — does this skill's prior output already exist?
- `.scratch/` — sign that a local-markdown issue tracker convention is already in use
### 2. Present findings and ask
Summarise what's present and what's missing. Then walk the user through the three decisions **one at a time** — present a section, get the user's answer, then move to the next. Don't dump all three at once.
Assume the user does not know what these terms mean. Each section starts with a short explainer (what it is, why these skills need it, what changes if they pick differently). Then show the choices and the default.
**Section A — Issue tracker.**
> Explainer: The "issue tracker" is where issues live for this repo. Skills like `to-issues`, `triage`, `to-prd`, and `qa` read from and write to it — they need to know whether to call `gh issue create`, write a markdown file under `.scratch/`, or follow some other workflow you describe. Pick the place you actually track work for this repo.
Default posture: these skills were designed for GitHub. If a `git remote` points at GitHub, propose that. If a `git remote` points at GitLab (`gitlab.com` or a self-hosted host), propose GitLab. Otherwise (or if the user prefers), offer:
- **GitHub** — issues live in the repo's GitHub Issues (uses the `gh` CLI)
- **GitLab** — issues live in the repo's GitLab Issues (uses the [`glab`](https://gitlab.com/gitlab-org/cli) CLI)
- **Local markdown** — issues live as files under `.scratch/<feature>/` in this repo (good for solo projects or repos without a remote)
- **Other** (Jira, Linear, etc.) — ask the user to describe the workflow in one paragraph; the skill will record it as freeform prose
**Section B — Triage label vocabulary.**
> Explainer: When the `triage` skill processes an incoming issue, it moves it through a state machine — needs evaluation, waiting on reporter, ready for an AFK agent to pick up, ready for a human, or won't fix. To do that, it needs to apply labels (or the equivalent in your issue tracker) that match strings *you've actually configured*. If your repo already uses different label names (e.g. `bug:triage` instead of `needs-triage`), map them here so the skill applies the right ones instead of creating duplicates.
The five canonical roles:
- `needs-triage` — maintainer needs to evaluate
- `needs-info` — waiting on reporter
- `ready-for-agent` — fully specified, AFK-ready (an agent can pick it up with no human context)
- `ready-for-human` — needs human implementation
- `wontfix` — will not be actioned
Default: each role's string equals its name. Ask the user if they want to override any. If their issue tracker has no existing labels, the defaults are fine.
**Section C — Domain docs.**
> Explainer: Some skills (`improve-codebase-architecture`, `diagnose`, `tdd`) read a `CONTEXT.md` file to learn the project's domain language, and `docs/adr/` for past architectural decisions. They need to know whether the repo has one global context or multiple (e.g. a monorepo with separate frontend/backend contexts) so they look in the right place.
Confirm the layout:
- **Single-context** — one `CONTEXT.md` + `docs/adr/` at the repo root. Most repos are this.
- **Multi-context** — `CONTEXT-MAP.md` at the root pointing to per-context `CONTEXT.md` files (typically a monorepo).
### 3. Confirm and edit
Show the user a draft of:
- The `## Agent skills` block to add to whichever of `CLAUDE.md` / `AGENTS.md` is being edited (see step 4 for selection rules)
- The contents of `docs/agents/issue-tracker.md`, `docs/agents/triage-labels.md`, `docs/agents/domain.md`
Let them edit before writing.
### 4. Write
**Pick the file to edit:**
- If `CLAUDE.md` exists, edit it.
- Else if `AGENTS.md` exists, edit it.
- If neither exists, ask the user which one to create — don't pick for them.
Never create `AGENTS.md` when `CLAUDE.md` already exists (or vice versa) — always edit the one that's already there.
If an `## Agent skills` block already exists in the chosen file, update its contents in-place rather than appending a duplicate. Don't overwrite user edits to the surrounding sections.
The block:
```markdown
## Agent skills
### Issue tracker
[one-line summary of where issues are tracked]. See `docs/agents/issue-tracker.md`.
### Triage labels
[one-line summary of the label vocabulary]. See `docs/agents/triage-labels.md`.
### Domain docs
[one-line summary of layout — "single-context" or "multi-context"]. See `docs/agents/domain.md`.
```
Then write the three docs files using the seed templates in this skill folder as a starting point:
- [issue-tracker-github.md](./issue-tracker-github.md) — GitHub issue tracker
- [issue-tracker-gitlab.md](./issue-tracker-gitlab.md) — GitLab issue tracker
- [issue-tracker-local.md](./issue-tracker-local.md) — local-markdown issue tracker
- [triage-labels.md](./triage-labels.md) — label mapping
- [domain.md](./domain.md) — domain doc consumer rules + layout
For "other" issue trackers, write `docs/agents/issue-tracker.md` from scratch using the user's description.
### 5. Done
Tell the user the setup is complete and which engineering skills will now read from these files. Mention they can edit `docs/agents/*.md` directly later — re-running this skill is only necessary if they want to switch issue trackers or restart from scratch.
@@ -0,0 +1,51 @@
# Domain Docs
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
## Before exploring, read these
- **`CONTEXT.md`** at the repo root, or
- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The producer skill (`/grill-with-docs`) creates them lazily when terms or decisions actually get resolved.
## File structure
Single-context repo (most repos):
```
/
├── CONTEXT.md
├── docs/adr/
│ ├── 0001-event-sourced-orders.md
│ └── 0002-postgres-for-write-model.md
└── src/
```
Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
```
/
├── CONTEXT-MAP.md
├── docs/adr/ ← system-wide decisions
└── src/
├── ordering/
│ ├── CONTEXT.md
│ └── docs/adr/ ← context-specific decisions
└── billing/
├── CONTEXT.md
└── docs/adr/
```
## Use the glossary's vocabulary
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/grill-with-docs`).
## Flag ADR conflicts
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
@@ -0,0 +1,22 @@
# Issue tracker: GitHub
Issues and PRDs for this repo live as GitHub issues. Use the `gh` CLI for all operations.
## Conventions
- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
- **Read an issue**: `gh issue view <number> --comments`, filtering comments by `jq` and also fetching labels.
- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
- **Comment on an issue**: `gh issue comment <number> --body "..."`
- **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
- **Close**: `gh issue close <number> --comment "..."`
Infer the repo from `git remote -v``gh` does this automatically when run inside a clone.
## When a skill says "publish to the issue tracker"
Create a GitHub issue.
## When a skill says "fetch the relevant ticket"
Run `gh issue view <number> --comments`.
@@ -0,0 +1,23 @@
# Issue tracker: GitLab
Issues and PRDs for this repo live as GitLab issues. Use the [`glab`](https://gitlab.com/gitlab-org/cli) CLI for all operations.
## Conventions
- **Create an issue**: `glab issue create --title "..." --description "..."`. Use a heredoc for multi-line descriptions. Pass `--description -` to open an editor.
- **Read an issue**: `glab issue view <number> --comments`. Use `-F json` for machine-readable output.
- **List issues**: `glab issue list -F json` with appropriate `--label` filters.
- **Comment on an issue**: `glab issue note <number> --message "..."`. GitLab calls comments "notes".
- **Apply / remove labels**: `glab issue update <number> --label "..."` / `--unlabel "..."`. Multiple labels can be comma-separated or by repeating the flag.
- **Close**: `glab issue close <number>`. `glab issue close` does not accept a closing comment, so post the explanation first with `glab issue note <number> --message "..."`, then close.
- **Merge requests**: GitLab calls PRs "merge requests". Use `glab mr create`, `glab mr view`, `glab mr note`, etc. — the same shape as `gh pr ...` with `mr` in place of `pr` and `note`/`--message` in place of `comment`/`--body`.
Infer the repo from `git remote -v``glab` does this automatically when run inside a clone.
## When a skill says "publish to the issue tracker"
Create a GitLab issue.
## When a skill says "fetch the relevant ticket"
Run `glab issue view <number> --comments`.
@@ -0,0 +1,19 @@
# Issue tracker: Local Markdown
Issues and PRDs for this repo live as markdown files in `.scratch/`.
## Conventions
- One feature per directory: `.scratch/<feature-slug>/`
- The PRD is `.scratch/<feature-slug>/PRD.md`
- Implementation issues are `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01`
- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings)
- Comments and conversation history append to the bottom of the file under a `## Comments` heading
## When a skill says "publish to the issue tracker"
Create a new file under `.scratch/<feature-slug>/` (creating the directory if needed).
## When a skill says "fetch the relevant ticket"
Read the file at the referenced path. The user will normally pass the path or the issue number directly.
@@ -0,0 +1,15 @@
# Triage Labels
The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
| Label in mattpocock/skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `wontfix` | `wontfix` | Will not be actioned |
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
Edit the right-hand column to match whatever vocabulary you actually use.
@@ -0,0 +1,109 @@
---
name: tdd
description: Test-driven development with red-green-refactor loop. Use when user wants to build features or fix bugs using TDD, mentions "red-green-refactor", wants integration tests, or asks for test-first development.
---
# Test-Driven Development
## Philosophy
**Core principle**: Tests should verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't.
**Good tests** are integration-style: they exercise real code paths through public APIs. They describe _what_ the system does, not _how_ it does it. A good test reads like a specification - "user can checkout with valid cart" tells you exactly what capability exists. These tests survive refactors because they don't care about internal structure.
**Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior.
See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines.
## Anti-Pattern: Horizontal Slices
**DO NOT write all tests first, then all implementation.** This is "horizontal slicing" - treating RED as "write all tests" and GREEN as "write all code."
This produces **crap tests**:
- Tests written in bulk test _imagined_ behavior, not _actual_ behavior
- You end up testing the _shape_ of things (data structures, function signatures) rather than user-facing behavior
- Tests become insensitive to real changes - they pass when behavior breaks, fail when behavior is fine
- You outrun your headlights, committing to test structure before understanding the implementation
**Correct approach**: Vertical slices via tracer bullets. One test → one implementation → repeat. Each test responds to what you learned from the previous cycle. Because you just wrote the code, you know exactly what behavior matters and how to verify it.
```
WRONG (horizontal):
RED: test1, test2, test3, test4, test5
GREEN: impl1, impl2, impl3, impl4, impl5
RIGHT (vertical):
RED→GREEN: test1→impl1
RED→GREEN: test2→impl2
RED→GREEN: test3→impl3
...
```
## Workflow
### 1. Planning
When exploring the codebase, use the project's domain glossary so that test names and interface vocabulary match the project's language, and respect ADRs in the area you're touching.
Before writing any code:
- [ ] Confirm with user what interface changes are needed
- [ ] Confirm with user which behaviors to test (prioritize)
- [ ] Identify opportunities for [deep modules](deep-modules.md) (small interface, deep implementation)
- [ ] Design interfaces for [testability](interface-design.md)
- [ ] List the behaviors to test (not implementation steps)
- [ ] Get user approval on the plan
Ask: "What should the public interface look like? Which behaviors are most important to test?"
**You can't test everything.** Confirm with the user exactly which behaviors matter most. Focus testing effort on critical paths and complex logic, not every possible edge case.
### 2. Tracer Bullet
Write ONE test that confirms ONE thing about the system:
```
RED: Write test for first behavior → test fails
GREEN: Write minimal code to pass → test passes
```
This is your tracer bullet - proves the path works end-to-end.
### 3. Incremental Loop
For each remaining behavior:
```
RED: Write next test → fails
GREEN: Minimal code to pass → passes
```
Rules:
- One test at a time
- Only enough code to pass current test
- Don't anticipate future tests
- Keep tests focused on observable behavior
### 4. Refactor
After all tests pass, look for [refactor candidates](refactoring.md):
- [ ] Extract duplication
- [ ] Deepen modules (move complexity behind simple interfaces)
- [ ] Apply SOLID principles where natural
- [ ] Consider what new code reveals about existing code
- [ ] Run tests after each refactor step
**Never refactor while RED.** Get to GREEN first.
## Checklist Per Cycle
```
[ ] Test describes behavior, not implementation
[ ] Test uses public interface only
[ ] Test would survive internal refactor
[ ] Code is minimal for this test
[ ] No speculative features added
```
@@ -0,0 +1,33 @@
# Deep Modules
From "A Philosophy of Software Design":
**Deep module** = small interface + lots of implementation
```
┌─────────────────────┐
│ Small Interface │ ← Few methods, simple params
├─────────────────────┤
│ │
│ │
│ Deep Implementation│ ← Complex logic hidden
│ │
│ │
└─────────────────────┘
```
**Shallow module** = large interface + little implementation (avoid)
```
┌─────────────────────────────────┐
│ Large Interface │ ← Many methods, complex params
├─────────────────────────────────┤
│ Thin Implementation │ ← Just passes through
└─────────────────────────────────┘
```
When designing interfaces, ask:
- Can I reduce the number of methods?
- Can I simplify the parameters?
- Can I hide more complexity inside?
@@ -0,0 +1,31 @@
# Interface Design for Testability
Good interfaces make testing natural:
1. **Accept dependencies, don't create them**
```typescript
// Testable
function processOrder(order, paymentGateway) {}
// Hard to test
function processOrder(order) {
const gateway = new StripeGateway();
}
```
2. **Return results, don't produce side effects**
```typescript
// Testable
function calculateDiscount(cart): Discount {}
// Hard to test
function applyDiscount(cart): void {
cart.total -= discount;
}
```
3. **Small surface area**
- Fewer methods = fewer tests needed
- Fewer params = simpler test setup
@@ -0,0 +1,59 @@
# When to Mock
Mock at **system boundaries** only:
- External APIs (payment, email, etc.)
- Databases (sometimes - prefer test DB)
- Time/randomness
- File system (sometimes)
Don't mock:
- Your own classes/modules
- Internal collaborators
- Anything you control
## Designing for Mockability
At system boundaries, design interfaces that are easy to mock:
**1. Use dependency injection**
Pass external dependencies in rather than creating them internally:
```typescript
// Easy to mock
function processPayment(order, paymentClient) {
return paymentClient.charge(order.total);
}
// Hard to mock
function processPayment(order) {
const client = new StripeClient(process.env.STRIPE_KEY);
return client.charge(order.total);
}
```
**2. Prefer SDK-style interfaces over generic fetchers**
Create specific functions for each external operation instead of one generic function with conditional logic:
```typescript
// GOOD: Each function is independently mockable
const api = {
getUser: (id) => fetch(`/users/${id}`),
getOrders: (userId) => fetch(`/users/${userId}/orders`),
createOrder: (data) => fetch('/orders', { method: 'POST', body: data }),
};
// BAD: Mocking requires conditional logic inside the mock
const api = {
fetch: (endpoint, options) => fetch(endpoint, options),
};
```
The SDK approach means:
- Each mock returns one specific shape
- No conditional logic in test setup
- Easier to see which endpoints a test exercises
- Type safety per endpoint
@@ -0,0 +1,10 @@
# Refactor Candidates
After TDD cycle, look for:
- **Duplication** → Extract function/class
- **Long methods** → Break into private helpers (keep tests on public interface)
- **Shallow modules** → Combine or deepen
- **Feature envy** → Move logic to where data lives
- **Primitive obsession** → Introduce value objects
- **Existing code** the new code reveals as problematic
@@ -0,0 +1,61 @@
# Good and Bad Tests
## Good Tests
**Integration-style**: Test through real interfaces, not mocks of internal parts.
```typescript
// GOOD: Tests observable behavior
test("user can checkout with valid cart", async () => {
const cart = createCart();
cart.add(product);
const result = await checkout(cart, paymentMethod);
expect(result.status).toBe("confirmed");
});
```
Characteristics:
- Tests behavior users/callers care about
- Uses public API only
- Survives internal refactors
- Describes WHAT, not HOW
- One logical assertion per test
## Bad Tests
**Implementation-detail tests**: Coupled to internal structure.
```typescript
// BAD: Tests implementation details
test("checkout calls paymentService.process", async () => {
const mockPayment = jest.mock(paymentService);
await checkout(cart, payment);
expect(mockPayment.process).toHaveBeenCalledWith(cart.total);
});
```
Red flags:
- Mocking internal collaborators
- Testing private methods
- Asserting on call counts/order
- Test breaks when refactoring without behavior change
- Test name describes HOW not WHAT
- Verifying through external means instead of interface
```typescript
// BAD: Bypasses interface to verify
test("createUser saves to database", async () => {
await createUser({ name: "Alice" });
const row = await db.query("SELECT * FROM users WHERE name = ?", ["Alice"]);
expect(row).toBeDefined();
});
// GOOD: Verifies through interface
test("createUser makes user retrievable", async () => {
const user = await createUser({ name: "Alice" });
const retrieved = await getUser(user.id);
expect(retrieved.name).toBe("Alice");
});
```
@@ -0,0 +1,83 @@
---
name: to-issues
description: Break a plan, spec, or PRD into independently-grabbable issues on the project issue tracker using tracer-bullet vertical slices. Use when user wants to convert a plan into issues, create implementation tickets, or break down work into issues.
---
# To Issues
Break a plan into independently-grabbable issues using vertical slices (tracer bullets).
The issue tracker and triage label vocabulary should have been provided to you — run `/setup-matt-pocock-skills` if not.
## Process
### 1. Gather context
Work from whatever is already in the conversation context. If the user passes an issue reference (issue number, URL, or path) as an argument, fetch it from the issue tracker and read its full body and comments.
### 2. Explore the codebase (optional)
If you have not already explored the codebase, do so to understand the current state of the code. Issue titles and descriptions should use the project's domain glossary vocabulary, and respect ADRs in the area you're touching.
### 3. Draft vertical slices
Break the plan into **tracer bullet** issues. Each issue is a thin vertical slice that cuts through ALL integration layers end-to-end, NOT a horizontal slice of one layer.
Slices may be 'HITL' or 'AFK'. HITL slices require human interaction, such as an architectural decision or a design review. AFK slices can be implemented and merged without human interaction. Prefer AFK over HITL where possible.
<vertical-slice-rules>
- Each slice delivers a narrow but COMPLETE path through every layer (schema, API, UI, tests)
- A completed slice is demoable or verifiable on its own
- Prefer many thin slices over few thick ones
</vertical-slice-rules>
### 4. Quiz the user
Present the proposed breakdown as a numbered list. For each slice, show:
- **Title**: short descriptive name
- **Type**: HITL / AFK
- **Blocked by**: which other slices (if any) must complete first
- **User stories covered**: which user stories this addresses (if the source material has them)
Ask the user:
- Does the granularity feel right? (too coarse / too fine)
- Are the dependency relationships correct?
- Should any slices be merged or split further?
- Are the correct slices marked as HITL and AFK?
Iterate until the user approves the breakdown.
### 5. Publish the issues to the issue tracker
For each approved slice, publish a new issue to the issue tracker. Use the issue body template below. These issues are considered ready for AFK agents, so publish them with the correct triage label unless instructed otherwise.
Publish issues in dependency order (blockers first) so you can reference real issue identifiers in the "Blocked by" field.
<issue-template>
## Parent
A reference to the parent issue on the issue tracker (if the source was an existing issue, otherwise omit this section).
## What to build
A concise description of this vertical slice. Describe the end-to-end behavior, not layer-by-layer implementation.
Avoid specific file paths or code snippets — they go stale fast. Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it here and note briefly that it came from a prototype. Trim to the decision-rich parts — not a working demo, just the important bits.
## Acceptance criteria
- [ ] Criterion 1
- [ ] Criterion 2
- [ ] Criterion 3
## Blocked by
- A reference to the blocking ticket (if any)
Or "None - can start immediately" if no blockers.
</issue-template>
Do NOT close or modify any parent issue.
@@ -0,0 +1,76 @@
---
name: to-prd
description: Turn the current conversation context into a PRD and publish it to the project issue tracker. Use when user wants to create a PRD from the current context.
---
This skill takes the current conversation context and codebase understanding and produces a PRD. Do NOT interview the user — just synthesize what you already know.
The issue tracker and triage label vocabulary should have been provided to you — run `/setup-matt-pocock-skills` if not.
## Process
1. Explore the repo to understand the current state of the codebase, if you haven't already. Use the project's domain glossary vocabulary throughout the PRD, and respect any ADRs in the area you're touching.
2. Sketch out the major modules you will need to build or modify to complete the implementation. Actively look for opportunities to extract deep modules that can be tested in isolation.
A deep module (as opposed to a shallow module) is one which encapsulates a lot of functionality in a simple, testable interface which rarely changes.
Check with the user that these modules match their expectations. Check with the user which modules they want tests written for.
3. Write the PRD using the template below, then publish it to the project issue tracker. Apply the `ready-for-agent` triage label - no need for additional triage.
<prd-template>
## Problem Statement
The problem that the user is facing, from the user's perspective.
## Solution
The solution to the problem, from the user's perspective.
## User Stories
A LONG, numbered list of user stories. Each user story should be in the format of:
1. As an <actor>, I want a <feature>, so that <benefit>
<user-story-example>
1. As a mobile bank customer, I want to see balance on my accounts, so that I can make better informed decisions about my spending
</user-story-example>
This list of user stories should be extremely extensive and cover all aspects of the feature.
## Implementation Decisions
A list of implementation decisions that were made. This can include:
- The modules that will be built/modified
- The interfaces of those modules that will be modified
- Technical clarifications from the developer
- Architectural decisions
- Schema changes
- API contracts
- Specific interactions
Do NOT include specific file paths or code snippets. They may end up being outdated very quickly.
Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it within the relevant decision and note briefly that it came from a prototype. Trim to the decision-rich parts — not a working demo, just the important bits.
## Testing Decisions
A list of testing decisions that were made. Include:
- A description of what makes a good test (only test external behavior, not implementation details)
- Which modules will be tested
- Prior art for the tests (i.e. similar types of tests in the codebase)
## Out of Scope
A description of the things that are out of scope for this PRD.
## Further Notes
Any further notes about the feature.
</prd-template>
@@ -0,0 +1,168 @@
# Writing Agent Briefs
An agent brief is a structured comment posted on a GitHub issue when it moves to `ready-for-agent`. It is the authoritative specification that an AFK agent will work from. The original issue body and discussion are context — the agent brief is the contract.
## Principles
### Durability over precision
The issue may sit in `ready-for-agent` for days or weeks. The codebase will change in the meantime. Write the brief so it stays useful even as files are renamed, moved, or refactored.
- **Do** describe interfaces, types, and behavioral contracts
- **Do** name specific types, function signatures, or config shapes that the agent should look for or modify
- **Don't** reference file paths — they go stale
- **Don't** reference line numbers
- **Don't** assume the current implementation structure will remain the same
### Behavioral, not procedural
Describe **what** the system should do, not **how** to implement it. The agent will explore the codebase fresh and make its own implementation decisions.
- **Good:** "The `SkillConfig` type should accept an optional `schedule` field of type `CronExpression`"
- **Bad:** "Open src/types/skill.ts and add a schedule field on line 42"
- **Good:** "When a user runs `/triage` with no arguments, they should see a summary of issues needing attention"
- **Bad:** "Add a switch statement in the main handler function"
### Complete acceptance criteria
The agent needs to know when it's done. Every agent brief must have concrete, testable acceptance criteria. Each criterion should be independently verifiable.
- **Good:** "Running `gh issue list --label needs-triage` returns issues that have been through initial classification"
- **Bad:** "Triage should work correctly"
### Explicit scope boundaries
State what is out of scope. This prevents the agent from gold-plating or making assumptions about adjacent features.
## Template
```markdown
## Agent Brief
**Category:** bug / enhancement
**Summary:** one-line description of what needs to happen
**Current behavior:**
Describe what happens now. For bugs, this is the broken behavior.
For enhancements, this is the status quo the feature builds on.
**Desired behavior:**
Describe what should happen after the agent's work is complete.
Be specific about edge cases and error conditions.
**Key interfaces:**
- `TypeName` — what needs to change and why
- `functionName()` return type — what it currently returns vs what it should return
- Config shape — any new configuration options needed
**Acceptance criteria:**
- [ ] Specific, testable criterion 1
- [ ] Specific, testable criterion 2
- [ ] Specific, testable criterion 3
**Out of scope:**
- Thing that should NOT be changed or addressed in this issue
- Adjacent feature that might seem related but is separate
```
## Examples
### Good agent brief (bug)
```markdown
## Agent Brief
**Category:** bug
**Summary:** Skill description truncation drops mid-word, producing broken output
**Current behavior:**
When a skill description exceeds 1024 characters, it is truncated at exactly
1024 characters regardless of word boundaries. This produces descriptions
that end mid-word (e.g. "Use when the user wants to confi").
**Desired behavior:**
Truncation should break at the last word boundary before 1024 characters
and append "..." to indicate truncation.
**Key interfaces:**
- The `SkillMetadata` type's `description` field — no type change needed,
but the validation/processing logic that populates it needs to respect
word boundaries
- Any function that reads SKILL.md frontmatter and extracts the description
**Acceptance criteria:**
- [ ] Descriptions under 1024 chars are unchanged
- [ ] Descriptions over 1024 chars are truncated at the last word boundary
before 1024 chars
- [ ] Truncated descriptions end with "..."
- [ ] The total length including "..." does not exceed 1024 chars
**Out of scope:**
- Changing the 1024 char limit itself
- Multi-line description support
```
### Good agent brief (enhancement)
```markdown
## Agent Brief
**Category:** enhancement
**Summary:** Add `.out-of-scope/` directory support for tracking rejected feature requests
**Current behavior:**
When a feature request is rejected, the issue is closed with a `wontfix` label
and a comment. There is no persistent record of the decision or reasoning.
Future similar requests require the maintainer to recall or search for the
prior discussion.
**Desired behavior:**
Rejected feature requests should be documented in `.out-of-scope/<concept>.md`
files that capture the decision, reasoning, and links to all issues that
requested the feature. When triaging new issues, these files should be
checked for matches.
**Key interfaces:**
- Markdown file format in `.out-of-scope/` — each file should have a
`# Concept Name` heading, a `**Decision:**` line, a `**Reason:**` line,
and a `**Prior requests:**` list with issue links
- The triage workflow should read all `.out-of-scope/*.md` files early
and match incoming issues against them by concept similarity
**Acceptance criteria:**
- [ ] Closing a feature as wontfix creates/updates a file in `.out-of-scope/`
- [ ] The file includes the decision, reasoning, and link to the closed issue
- [ ] If a matching `.out-of-scope/` file already exists, the new issue is
appended to its "Prior requests" list rather than creating a duplicate
- [ ] During triage, existing `.out-of-scope/` files are checked and surfaced
when a new issue matches a prior rejection
**Out of scope:**
- Automated matching (human confirms the match)
- Reopening previously rejected features
- Bug reports (only enhancement rejections go to `.out-of-scope/`)
```
### Bad agent brief
```markdown
## Agent Brief
**Summary:** Fix the triage bug
**What to do:**
The triage thing is broken. Look at the main file and fix it.
The function around line 150 has the issue.
**Files to change:**
- src/triage/handler.ts (line 150)
- src/types.ts (line 42)
```
This is bad because:
- No category
- Vague description ("the triage thing is broken")
- References file paths and line numbers that will go stale
- No acceptance criteria
- No scope boundaries
- No description of current vs desired behavior
@@ -0,0 +1,101 @@
# Out-of-Scope Knowledge Base
The `.out-of-scope/` directory in a repo stores persistent records of rejected feature requests. It serves two purposes:
1. **Institutional memory** — why a feature was rejected, so the reasoning isn't lost when the issue is closed
2. **Deduplication** — when a new issue comes in that matches a prior rejection, the skill can surface the previous decision instead of re-litigating it
## Directory structure
```
.out-of-scope/
├── dark-mode.md
├── plugin-system.md
└── graphql-api.md
```
One file per **concept**, not per issue. Multiple issues requesting the same thing are grouped under one file.
## File format
The file should be written in a relaxed, readable style — more like a short design document than a database entry. Use paragraphs, code samples, and examples to make the reasoning clear and useful to someone encountering it for the first time.
```markdown
# Dark Mode
This project does not support dark mode or user-facing theming.
## Why this is out of scope
The rendering pipeline assumes a single color palette defined in
`ThemeConfig`. Supporting multiple themes would require:
- A theme context provider wrapping the entire component tree
- Per-component theme-aware style resolution
- A persistence layer for user theme preferences
This is a significant architectural change that doesn't align with the
project's focus on content authoring. Theming is a concern for downstream
consumers who embed or redistribute the output.
```ts
// The current ThemeConfig interface is not designed for runtime switching:
interface ThemeConfig {
colors: ColorPalette; // single palette, resolved at build time
fonts: FontStack;
}
```
## Prior requests
- #42 — "Add dark mode support"
- #87 — "Night theme for accessibility"
- #134 — "Dark theme option"
```
### Naming the file
Use a short, descriptive kebab-case name for the concept: `dark-mode.md`, `plugin-system.md`, `graphql-api.md`. The name should be recognizable enough that someone browsing the directory understands what was rejected without opening the file.
### Writing the reason
The reason should be substantive — not "we don't want this" but why. Good reasons reference:
- Project scope or philosophy ("This project focuses on X; theming is a downstream concern")
- Technical constraints ("Supporting this would require Y, which conflicts with our Z architecture")
- Strategic decisions ("We chose to use A instead of B because...")
The reason should be durable. Avoid referencing temporary circumstances ("we're too busy right now") — those aren't real rejections, they're deferrals.
## When to check `.out-of-scope/`
During triage (Step 1: Gather context), read all files in `.out-of-scope/`. When evaluating a new issue:
- Check if the request matches an existing out-of-scope concept
- Matching is by concept similarity, not keyword — "night theme" matches `dark-mode.md`
- If there's a match, surface it to the maintainer: "This is similar to `.out-of-scope/dark-mode.md` — we rejected this before because [reason]. Do you still feel the same way?"
The maintainer may:
- **Confirm** — the new issue gets added to the existing file's "Prior requests" list, then closed
- **Reconsider** — the out-of-scope file gets deleted or updated, and the issue proceeds through normal triage
- **Disagree** — the issues are related but distinct, proceed with normal triage
## When to write to `.out-of-scope/`
Only when an **enhancement** (not a bug) is rejected as `wontfix`. The flow:
1. Maintainer decides a feature request is out of scope
2. Check if a matching `.out-of-scope/` file already exists
3. If yes: append the new issue to the "Prior requests" list
4. If no: create a new file with the concept name, decision, reason, and first prior request
5. Post a comment on the issue explaining the decision and mentioning the `.out-of-scope/` file
6. Close the issue with the `wontfix` label
## Updating or removing out-of-scope files
If the maintainer changes their mind about a previously rejected concept:
- Delete the `.out-of-scope/` file
- The skill does not need to reopen old issues — they're historical records
- The new issue that triggered the reconsideration proceeds through normal triage
@@ -0,0 +1,103 @@
---
name: triage
description: Triage issues through a state machine driven by triage roles. Use when user wants to create an issue, triage issues, review incoming bugs or feature requests, prepare issues for an AFK agent, or manage issue workflow.
---
# Triage
Move issues on the project issue tracker through a small state machine of triage roles.
Every comment or issue posted to the issue tracker during triage **must** start with this disclaimer:
```
> *This was generated by AI during triage.*
```
## Reference docs
- [AGENT-BRIEF.md](AGENT-BRIEF.md) — how to write durable agent briefs
- [OUT-OF-SCOPE.md](OUT-OF-SCOPE.md) — how the `.out-of-scope/` knowledge base works
## Roles
Two **category** roles:
- `bug` — something is broken
- `enhancement` — new feature or improvement
Five **state** roles:
- `needs-triage` — maintainer needs to evaluate
- `needs-info` — waiting on reporter for more information
- `ready-for-agent` — fully specified, ready for an AFK agent
- `ready-for-human` — needs human implementation
- `wontfix` — will not be actioned
Every triaged issue should carry exactly one category role and one state role. If state roles conflict, flag it and ask the maintainer before doing anything else.
These are canonical role names — the actual label strings used in the issue tracker may differ. The mapping should have been provided to you - run `/setup-matt-pocock-skills` if not.
State transitions: an unlabeled issue normally goes to `needs-triage` first; from there it moves to `needs-info`, `ready-for-agent`, `ready-for-human`, or `wontfix`. `needs-info` returns to `needs-triage` once the reporter replies. The maintainer can override at any time — flag transitions that look unusual and ask before proceeding.
## Invocation
The maintainer invokes `/triage` and describes what they want in natural language. Interpret the request and act. Examples:
- "Show me anything that needs my attention"
- "Let's look at #42"
- "Move #42 to ready-for-agent"
- "What's ready for agents to pick up?"
## Show what needs attention
Query the issue tracker and present three buckets, oldest first:
1. **Unlabeled** — never triaged.
2. **`needs-triage`** — evaluation in progress.
3. **`needs-info` with reporter activity since the last triage notes** — needs re-evaluation.
Show counts and a one-line summary per issue. Let the maintainer pick.
## Triage a specific issue
1. **Gather context.** Read the full issue (body, comments, labels, reporter, dates). Parse any prior triage notes so you don't re-ask resolved questions. Explore the codebase using the project's domain glossary, respecting ADRs in the area. Read `.out-of-scope/*.md` and surface any prior rejection that resembles this issue.
2. **Recommend.** Tell the maintainer your category and state recommendation with reasoning, plus a brief codebase summary relevant to the issue. Wait for direction.
3. **Reproduce (bugs only).** Before any grilling, attempt reproduction: read the reporter's steps, trace the relevant code, run tests or commands. Report what happened — successful repro with code path, failed repro, or insufficient detail (a strong `needs-info` signal). A confirmed repro makes a much stronger agent brief.
4. **Grill (if needed).** If the issue needs fleshing out, run a `/grill-with-docs` session.
5. **Apply the outcome:**
- `ready-for-agent` — post an agent brief comment ([AGENT-BRIEF.md](AGENT-BRIEF.md)).
- `ready-for-human` — same structure as an agent brief, but note why it can't be delegated (judgment calls, external access, design decisions, manual testing).
- `needs-info` — post triage notes (template below).
- `wontfix` (bug) — polite explanation, then close.
- `wontfix` (enhancement) — write to `.out-of-scope/`, link to it from a comment, then close ([OUT-OF-SCOPE.md](OUT-OF-SCOPE.md)).
- `needs-triage` — apply the role. Optional comment if there's partial progress.
## Quick state override
If the maintainer says "move #42 to ready-for-agent", trust them and apply the role directly. Confirm what you're about to do (role changes, comment, close), then act. Skip grilling. If moving to `ready-for-agent` without a grilling session, ask whether they want to write an agent brief.
## Needs-info template
```markdown
## Triage Notes
**What we've established so far:**
- point 1
- point 2
**What we still need from you (@reporter):**
- question 1
- question 2
```
Capture everything resolved during grilling under "established so far" so the work isn't lost. Questions must be specific and actionable, not "please provide more info".
## Resuming a previous session
If prior triage notes exist on the issue, read them, check whether the reporter has answered any outstanding questions, and present an updated picture before continuing. Don't re-ask resolved questions.
@@ -0,0 +1,117 @@
---
name: write-a-skill
description: Create new agent skills with proper structure, progressive disclosure, and bundled resources. Use when user wants to create, write, or build a new skill.
---
# Writing Skills
## Process
1. **Gather requirements** - ask user about:
- What task/domain does the skill cover?
- What specific use cases should it handle?
- Does it need executable scripts or just instructions?
- Any reference materials to include?
2. **Draft the skill** - create:
- SKILL.md with concise instructions
- Additional reference files if content exceeds 500 lines
- Utility scripts if deterministic operations needed
3. **Review with user** - present draft and ask:
- Does this cover your use cases?
- Anything missing or unclear?
- Should any section be more/less detailed?
## Skill Structure
```
skill-name/
├── SKILL.md # Main instructions (required)
├── REFERENCE.md # Detailed docs (if needed)
├── EXAMPLES.md # Usage examples (if needed)
└── scripts/ # Utility scripts (if needed)
└── helper.js
```
## SKILL.md Template
```md
---
name: skill-name
description: Brief description of capability. Use when [specific triggers].
---
# Skill Name
## Quick start
[Minimal working example]
## Workflows
[Step-by-step processes with checklists for complex tasks]
## Advanced features
[Link to separate files: See [REFERENCE.md](REFERENCE.md)]
```
## Description Requirements
The description is **the only thing your agent sees** when deciding which skill to load. It's surfaced in the system prompt alongside all other installed skills. Your agent reads these descriptions and picks the relevant skill based on the user's request.
**Goal**: Give your agent just enough info to know:
1. What capability this skill provides
2. When/why to trigger it (specific keywords, contexts, file types)
**Format**:
- Max 1024 chars
- Write in third person
- First sentence: what it does
- Second sentence: "Use when [specific triggers]"
**Good example**:
```
Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when user mentions PDFs, forms, or document extraction.
```
**Bad example**:
```
Helps with documents.
```
The bad example gives your agent no way to distinguish this from other document skills.
## When to Add Scripts
Add utility scripts when:
- Operation is deterministic (validation, formatting)
- Same code would be generated repeatedly
- Errors need explicit handling
Scripts save tokens and improve reliability vs generated code.
## When to Split Files
Split into separate files when:
- SKILL.md exceeds 100 lines
- Content has distinct domains (finance vs sales schemas)
- Advanced features are rarely needed
## Review Checklist
After drafting, verify:
- [ ] Description includes triggers ("Use when...")
- [ ] SKILL.md under 100 lines
- [ ] No time-sensitive info
- [ ] Consistent terminology
- [ ] Concrete examples included
- [ ] References one level deep
@@ -0,0 +1,7 @@
---
name: zoom-out
description: Tell the agent to zoom out and give broader context or a higher-level perspective. Use when you're unfamiliar with a section of code or need to understand how it fits into the bigger picture.
disable-model-invocation: true
---
I don't know this area of code well. Go up a layer of abstraction. Give me a map of all the relevant modules and callers, using the project's domain glossary vocabulary.
@@ -0,0 +1,32 @@
---
id: aitoearn-commit-convention
trigger: "when writing a commit message"
confidence: 0.86
domain: git
source: local-repo-analysis
---
# Use Conventional Commits
## Action
Prefix commits with: `feat:`, `fix:`, `chore:`, `docs:`, `test:`, `refactor:`
Use scope in parentheses for module-specific changes: `feat(content):`, `fix(tiktok):`
## Format
```
<type>(<scope>): <description>
[optional body]
```
## Examples
- `feat(login): 更新登录控制器中的注释和文档为中文`
- `refactor(mongodb): 移除materialGroup.repository中多余的type参数`
- `fix: 草稿箱创建缺失平台字段`
- `chore: add vitest setup and MCP unit tests`
## Evidence
- Analyzed 200 commits
- 86% follow conventional commit format
- Common scopes: content, mongodb, tiktok, douyin, channel-db, publish
@@ -0,0 +1,58 @@
---
id: aitoearn-dto-vo-pattern
trigger: "when creating DTOs or VOs"
confidence: 0.95
domain: typescript
source: local-repo-analysis
---
# Zod Schema + DTO/VO Pattern
## Action
Always define Zod schema first, then generate DTO/VO class using `createZodDto`.
## DTO (Input)
```typescript
import { createZodDto, PaginationDtoSchema } from '@yikart/common'
import { z } from 'zod'
export const CreateOrderDtoSchema = z.object({
productId: z.string(),
quantity: z.number().int().positive().default(1),
returnTo: z.url().optional(),
})
export class CreateOrderDto extends createZodDto(CreateOrderDtoSchema, 'CreateOrderDto') {}
```
## VO (Output)
```typescript
import { createPaginationVo, createZodDto } from '@yikart/common'
import { z } from 'zod'
export const OrderDetailVoSchema = z.object({
id: z.string(),
amount: z.number(),
createdAt: z.coerce.date()
})
export class OrderDetailVo extends createZodDto(OrderDetailVoSchema, 'OrderDetailVo') {}
export class OrderListVo extends createPaginationVo(OrderDetailVoSchema, 'OrderListVo') {}
```
## Controller Usage
```typescript
// Regular VO - use static create()
return OrderDetailVo.create(data)
// Pagination VO - use new
return new OrderListVo({ page, pageSize, total, totalPages, list })
```
## Rules
- Never use entity as input (use DTO)
- Never return entity directly (use VO)
- Pagination input: use `PaginationDtoSchema`
- Pagination output fields: page, pageSize, totalPages, total, list
## Evidence
- Pattern defined in CLAUDE.md
- Used consistently across all services
@@ -0,0 +1,51 @@
---
id: aitoearn-exception-handling
trigger: "when throwing exceptions or handling errors"
confidence: 0.95
domain: typescript
source: local-repo-analysis
---
# AppException Pattern
## Action
Only use `AppException` with `ResponseCode` for business errors.
## Usage
```typescript
import { AppException, ResponseCode } from '@yikart/common'
// Only code
throw new AppException(ResponseCode.MaterialGroupNotFound)
// Code + data
throw new AppException(ResponseCode.MaterialGroupNotFound, { groupId: 'group_xxx' })
```
## ResponseCode Rules
- Success: `Success = 0`
- Business errors: Start from `10000`, allocated by module
- Naming: PascalCase, specific resource name
- Location: Only in `libs/common/src/enums/response-code.enum.ts`
## Adding New Error Code
1. Add constant in `ResponseCode` enum
2. Add message mapping in `libs/common/src/i18n/messages.ts`
3. Use in business code
## Forbidden
- ❌ Custom message override
- ❌ Generic names: `Unauthorized`, `PermissionDenied`, `AccessDenied`
- ❌ Custom exception classes
- ❌ Custom HTTP status codes in business code
## Correct Naming
- ✅ `ContractNotFound`
- ✅ `CommentNotFound`
- ✅ `MaterialGroupNotFound`
- ❌ `Unauthorized`
- ❌ `AccessDenied`
## Evidence
- Enforced in CLAUDE.md development standards
- Consistent pattern across all services
@@ -0,0 +1,41 @@
---
id: aitoearn-nestjs-layering
trigger: "when creating or modifying NestJS code"
confidence: 0.95
domain: architecture
source: local-repo-analysis
---
# NestJS Layered Architecture
## Action
Follow strict separation of concerns:
1. **Controller** - Only routing, parameter binding, VO transformation
2. **Service** - Business logic, permission filtering, entity mapping
3. **Repository** - Data access only, no business logic
## Rules
- Controller MUST NOT contain business logic
- Controller MUST NOT access database directly
- Controller MUST return VO (not entity)
- Service handles permission filtering via query conditions
- Repository MUST NOT contain permission checks
- Repository MUST NOT perform cross-model operations
## File Structure
```
src/
├── module-name/
│ ├── module-name.controller.ts
│ ├── module-name.service.ts
│ ├── module-name.module.ts
│ ├── dto/
│ │ ├── module-name.dto.ts
│ │ └── module-name.vo.ts
│ └── (repository in libs/)
```
## Evidence
- Consistent pattern across all apps
- Enforced in CLAUDE.md development standards
@@ -0,0 +1,52 @@
---
id: aitoearn-nx-workflow
trigger: "when building, serving, or testing the project"
confidence: 0.95
domain: build
source: local-repo-analysis
---
# Nx Monorepo Workflow
## Action
Use Nx commands for all build and development tasks.
## Commands
### Serve Application
```bash
pnpm nx serve <project>
```
### Build Application
```bash
pnpm nx build <project>
```
### Run Linting
```bash
pnpm lint -w
```
### Run Tests
```bash
pnpm nx test <project>
```
## Available Projects
- `aitoearn-ai`
- `aitoearn-server`
## Build Verification Rules
- Always use `pnpm nx build <project>` to verify
- Never use `tsc` directly
- After renaming methods, run build to ensure all references updated
## Pre-commit
- Lint runs automatically via `lint-staged`
- Must pass `pnpm lint -w` before commit
## Evidence
- Nx configuration in `nx.json`
- Build scripts in `package.json`
- Enforced in CLAUDE.md development standards
@@ -0,0 +1,42 @@
---
id: aitoearn-repository-naming
trigger: "when creating or naming repository methods"
confidence: 0.95
domain: typescript
source: local-repo-analysis
---
# Repository Method Naming Convention
## Action
Use strict prefix conventions for repository methods.
## Required Prefixes
| Prefix | Returns | Format |
|--------|---------|--------|
| `get` | Single value | `getById`, `getByUserId`, `getByIdAndStatus` |
| `list` | Array | `listByUserId`, `listByStatus`, `listWithPagination` |
| `create` | Created entity | `create`, `createMany`, `createByUser` |
| `update` | Updated entity | `updateById`, `updateByStatus`, `updateManyByIds` |
| `delete` | Deleted entity | `deleteById`, `deleteByUserId`, `deleteManyByIds` |
| `count` | Number | `countByUserId`, `countByStatus` |
| `aggregate` | Aggregation | `aggregateByDate`, `sum`, `avg` |
## Pagination Methods
Must end with `WithPagination`: `listWithPagination`
## Forbidden Prefixes
| ❌ Wrong | ✅ Correct |
|----------|-----------|
| `findById` | `getById` |
| `findList` | `listWithPagination` |
| `delOne` | `deleteById` |
| `addUseCount` | `updateUseCountById` |
| `verify` | `updateVerifyById` |
| `markAsRead` | `updateAsReadByIds` |
| `check` | `get` + business logic in Service |
## Evidence
- Enforced in CLAUDE.md development standards
- Consistent pattern in libs/*-db/repositories/
@@ -0,0 +1,49 @@
# Agent Orchestration
## Available Agents
Located in `~/.claude/agents/`:
| Agent | Purpose | When to Use |
|-------|---------|-------------|
| planner | Implementation planning | Complex features, refactoring |
| architect | System design | Architectural decisions |
| tdd-guide | Test-driven development | New features, bug fixes |
| code-reviewer | Code review | After writing code |
| security-reviewer | Security analysis | Before commits |
| build-error-resolver | Fix build errors | When build fails |
| e2e-runner | E2E testing | Critical user flows |
| refactor-cleaner | Dead code cleanup | Code maintenance |
| doc-updater | Documentation | Updating docs |
## Immediate Agent Usage
No user prompt needed:
1. Complex feature requests - Use **planner** agent
2. Code just written/modified - Use **code-reviewer** agent
3. Bug fix or new feature - Use **tdd-guide** agent
4. Architectural decision - Use **architect** agent
## Parallel Task Execution
ALWAYS use parallel Task execution for independent operations:
```markdown
# GOOD: Parallel execution
Launch 3 agents in parallel:
1. Agent 1: Security analysis of auth.ts
2. Agent 2: Performance review of cache system
3. Agent 3: Type checking of utils.ts
# BAD: Sequential when unnecessary
First agent 1, then agent 2, then agent 3
```
## Multi-Perspective Analysis
For complex problems, use split role sub-agents:
- Factual reviewer
- Senior engineer
- Security expert
- Consistency reviewer
- Redundancy checker
@@ -0,0 +1,47 @@
# AI Workflow and Model Selection
## Model Selection Strategy
**Haiku 4.5** (90% of Sonnet capability, 3x cost savings):
- Lightweight agents with frequent invocation
- Pair programming and code generation
- Worker agents in multi-agent systems
**Sonnet 4.5** (Best coding model):
- Main development work
- Orchestrating multi-agent workflows
- Complex coding tasks
**Opus 4.5** (Deepest reasoning):
- Complex architectural decisions
- Maximum reasoning requirements
- Research and analysis tasks
## Context Window Management
Avoid last 20% of context window for:
- Large-scale refactoring
- Feature implementation spanning multiple files
- Debugging complex interactions
Lower context sensitivity tasks:
- Single-file edits
- Independent utility creation
- Documentation updates
- Simple bug fixes
## Ultrathink + Plan Mode
For complex tasks requiring deep reasoning:
1. Use `ultrathink` for enhanced thinking
2. Enable **Plan Mode** for structured approach
3. "Rev the engine" with multiple critique rounds
4. Use split role sub-agents for diverse analysis
## Build Troubleshooting
If build fails:
1. Use **build-error-resolver** agent
2. Analyze error messages
3. Fix incrementally
4. Verify after each fix
@@ -0,0 +1,106 @@
# Coding Style
## Immutability (CRITICAL)
ALWAYS create new objects, NEVER mutate:
```javascript
// WRONG: Mutation
function updateUser(user, name) {
user.name = name // MUTATION!
return user
}
// CORRECT: Immutability
function updateUser(user, name) {
return {
...user,
name
}
}
```
## File Organization
MANY SMALL FILES > FEW LARGE FILES:
- High cohesion, low coupling
- 200-400 lines typical, 800 max
- Extract utilities from large components
- Organize by feature/domain, not by type
## Error Handling
Business errors use `AppException + ResponseCode`, while protocol-layer errors may use standard `HttpException`. Do NOT wrap business logic with try-catch.
```typescript
// WRONG: try-catch + console + throw new Error
try {
const order = await this.orderService.getById(id)
} catch (error) {
console.error('Failed:', error) // ❌ console prohibited
throw new Error('Order not found') // ❌ use AppException
}
// CORRECT: Let the framework handle exceptions
const order = await this.orderRepository.getById(id)
if (!order) {
throw new AppException(ResponseCode.OrderNotFound)
}
```
Only use try-catch for infrastructure-level operations (external API calls, file I/O), and log with `this.logger.error()` instead of `console`.
## Input Validation
ALWAYS validate user input:
```typescript
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
age: z.number().int().min(0).max(150)
})
const validated = schema.parse(input)
```
## Logger Error Format
Error must be the first argument, message the second as a plain string:
```typescript
// CORRECT
this.logger.error(error, `Failed to process order ${orderId}`)
this.logger.warn(error, `Inventory check failed for product ${productId}`)
// WRONG — error nested in object
this.logger.error({ path: 'xxx', message: 'yyy', error: err })
// WRONG — error interpolated into string template
this.logger.error(`Failed to process: ${error}`)
// WRONG — message as object
this.logger.error(error, { context: 'xxx' })
```
Promise catch follows the same rule:
```typescript
// CORRECT
promise.catch((err: Error) => this.logger.error(err, 'Failed to send notification'))
// WRONG
promise.catch((err: unknown) => this.logger.error({ message: 'xxx', error: err }))
```
## Code Quality Checklist
Before marking work complete:
- [ ] Code is readable and well-named
- [ ] Functions are small (<50 lines)
- [ ] Files are focused (<800 lines)
- [ ] No deep nesting (>4 levels)
- [ ] Proper error handling
- [ ] No console usage (use Logger instance)
- [ ] No hardcoded values
- [ ] No mutation (immutable patterns used)

Some files were not shown because too many files have changed in this diff Show More