mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Merge branch 'MCCTeam:master' into master
This commit is contained in:
commit
37f71d4494
639 changed files with 126385 additions and 17202 deletions
1
.claude/skills
Symbolic link
1
.claude/skills
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../.skills
|
||||
16
.codex/hooks.json
Normal file
16
.codex/hooks.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_mcc_build_guard.py\"",
|
||||
"statusMessage": "Checking MCC build command policy"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
67
.codex/hooks/pre_tool_use_mcc_build_guard.py
Normal file
67
.codex/hooks/pre_tool_use_mcc_build_guard.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#!/usr/bin/env python3
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
RAW_DOTNET_BUILD_RE = re.compile(r"(^|[\s;&|()])dotnet\s+build(\s|$)")
|
||||
ABSOLUTE_DOTNET_BUILD_RE = re.compile(r"(^|[\s;&|()])/\S*dotnet\s+build(\s|$)")
|
||||
RAW_DOTNET_PUBLISH_RE = re.compile(r"(^|[\s;&|()])dotnet\s+publish(\s|$)")
|
||||
ABSOLUTE_DOTNET_PUBLISH_RE = re.compile(r"(^|[\s;&|()])/\S*dotnet\s+publish(\s|$)")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
|
||||
command = payload.get("tool_input", {}).get("command", "")
|
||||
if not isinstance(command, str) or not command:
|
||||
return 0
|
||||
|
||||
if ABSOLUTE_DOTNET_BUILD_RE.search(command) or ABSOLUTE_DOTNET_PUBLISH_RE.search(command):
|
||||
return 0
|
||||
|
||||
if RAW_DOTNET_BUILD_RE.search(command):
|
||||
response = {
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": (
|
||||
"Raw 'dotnet build' is blocked in this repository. "
|
||||
"Use 'source tools/mcc-env.sh && mcc-build' instead so MCC temp-build routing stays active. "
|
||||
"If you intentionally need the raw .NET CLI, call it by absolute path such as '/usr/bin/dotnet build ...' to bypass this guard."
|
||||
),
|
||||
},
|
||||
"systemMessage": (
|
||||
"Blocked raw 'dotnet build'. Use 'source tools/mcc-env.sh && mcc-build'. "
|
||||
"If you intentionally need raw .NET CLI behavior, call '/usr/bin/dotnet build ...' explicitly."
|
||||
),
|
||||
}
|
||||
json.dump(response, sys.stdout)
|
||||
sys.stdout.write("\n")
|
||||
elif RAW_DOTNET_PUBLISH_RE.search(command):
|
||||
response = {
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": (
|
||||
"Raw 'dotnet publish' is blocked in this repository. "
|
||||
"Use 'source tools/mcc-env.sh && mcc-publish --rid <RID>' instead so MCC publish defaults stay aligned with the repo workflow. "
|
||||
"If you intentionally need the raw .NET CLI, call it by absolute path such as '/usr/bin/dotnet publish ...' to bypass this guard."
|
||||
),
|
||||
},
|
||||
"systemMessage": (
|
||||
"Blocked raw 'dotnet publish'. Use 'source tools/mcc-env.sh && mcc-publish --rid <RID>'. "
|
||||
"If you intentionally need raw .NET CLI behavior, call '/usr/bin/dotnet publish ...' explicitly."
|
||||
),
|
||||
}
|
||||
json.dump(response, sys.stdout)
|
||||
sys.stdout.write("\n")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
1
.codex/skills
Symbolic link
1
.codex/skills
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../.skills
|
||||
1
.cursor/skills
Symbolic link
1
.cursor/skills
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../.skills
|
||||
3
.cursorindexingignore
Normal file
3
.cursorindexingignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
|
||||
.specstory/**
|
||||
284
.github/workflows/build-and-release.yml
vendored
284
.github/workflows/build-and-release.yml
vendored
|
|
@ -8,101 +8,39 @@ on:
|
|||
|
||||
env:
|
||||
PROJECT: "MinecraftClient"
|
||||
target-version: "net7.0"
|
||||
compile-flags: "--self-contained=true -c Release -p:UseAppHost=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded"
|
||||
target-version: "net10.0"
|
||||
dotnet-version: "10.0.x"
|
||||
compile-flags: "--self-contained=true -c Release -p:UseAppHost=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() && needs.fetch-translations.result != 'failure' }}
|
||||
needs: [determine-build, fetch-translations]
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
matrix:
|
||||
target: [win-x86, win-x64, win-arm, win-arm64, linux-x64, linux-arm, linux-arm64, osx-x64, osx-arm64]
|
||||
|
||||
determine-build:
|
||||
runs-on: ubuntu-slim
|
||||
outputs:
|
||||
skip: ${{ steps.check-skip.outputs.skip }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
if: ${{ always() && needs.fetch-translations.result == 'skipped' }}
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'true'
|
||||
|
||||
- name: Get Current Date
|
||||
- name: Check skip CI
|
||||
id: check-skip
|
||||
run: |
|
||||
echo date=$(date +'%Y%m%d') >> $GITHUB_ENV
|
||||
echo date_dashed=$(date -u +'%Y-%m-%d') >> $GITHUB_ENV
|
||||
|
||||
- name: Restore Translations (if available)
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/*
|
||||
key: "translation-${{ github.sha }}"
|
||||
restore-keys: "translation-"
|
||||
|
||||
- name: Setup Environment Variables (early)
|
||||
run: |
|
||||
echo project-path=${{ github.workspace }}/${{ env.PROJECT }} >> $GITHUB_ENV
|
||||
echo file-ext=${{ (startsWith(matrix.target, 'win') && '.exe') || ' ' }} >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Environment Variables
|
||||
run: |
|
||||
echo target-out-path=${{ env.project-path }}/bin/Release/${{ env.target-version }}/${{ matrix.target }}/publish/ >> $GITHUB_ENV
|
||||
echo assembly-info=${{ env.project-path }}/Properties/AssemblyInfo.cs >> $GITHUB_ENV
|
||||
echo build-version-info=${{ env.date }}-${{ github.run_number }} >> $GITHUB_ENV
|
||||
echo commit=$(echo ${{ github.sha }} | cut -c 1-7) >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Environment Variables (late)
|
||||
run: |
|
||||
echo built-executable-path=${{ env.target-out-path }}${{ env.PROJECT }}${{ env.file-ext }} >> $GITHUB_ENV
|
||||
|
||||
- name: Set Version Info
|
||||
run: |
|
||||
echo '' >> ${{ env.assembly-info }}
|
||||
echo "[assembly: AssemblyConfiguration(\"GitHub build ${{ github.run_number }}, built on ${{ env.date_dashed }} from commit ${{ env.commit }}\")]" >> ${{ env.assembly-info }}
|
||||
sed -i -e 's|SentryDSN = "";|SentryDSN = "${{ secrets.SENTRY_DSN }}";|g' ${{ env.project-path }}/Program.cs
|
||||
|
||||
- name: Build Target
|
||||
run: dotnet publish ${{ env.project-path }}.sln -f ${{ env.target-version }} -r ${{ matrix.target }} ${{ env.compile-flags }}
|
||||
LOWER=$(echo "$COMMIT_MSG" | tr '[:upper:]' '[:lower:]')
|
||||
if echo "$LOWER" | grep -qE 'skip.?ci|ci.?skip'; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
COMMIT_MSG: ${{ github.event.head_commit.message }}
|
||||
|
||||
- name: Rename Binary
|
||||
run: |
|
||||
mv ${{ env.built-executable-path }} ${{ env.PROJECT }}-${{ env.build-version-info }}-${{ matrix.target }}${{ (startsWith(matrix.target, 'win') && '.exe') || ' ' }}
|
||||
|
||||
- name: Wait
|
||||
# We wait before creating a release because we might run into a race condition
|
||||
# while creating a new tag (as opposed to using the existing tag, if any) since we're running builds in parallel.
|
||||
run: |
|
||||
sleep 5s
|
||||
|
||||
- name: Create Release
|
||||
uses: ncipollo/release-action@v1.14.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
artifacts: ${{ env.PROJECT }}-${{ env.build-version-info }}-${{ matrix.target }}${{ (startsWith(matrix.target, 'win') && '.exe') || ' ' }}
|
||||
tag: ${{ format('{0}-{1}', env.date, github.run_number) }}
|
||||
name: '${{ env.build-version-info }}: ${{ github.event.head_commit.message }}'
|
||||
generateReleaseNotes: true
|
||||
artifactErrorsFailBuild: true
|
||||
allowUpdates: true
|
||||
makeLatest: true
|
||||
omitBodyDuringUpdate: true
|
||||
omitNameDuringUpdate: true
|
||||
replacesArtifacts: false
|
||||
|
||||
fetch-translations:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine-build
|
||||
# Translations will only be fetched in the MCCTeam repository, since it needs crowdin secrets.
|
||||
if: ${{ github.repository == 'MCCTeam/Minecraft-Console-Client' }}
|
||||
|
||||
if: ${{ needs.determine-build.outputs.skip != 'true' }}
|
||||
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
|
||||
steps:
|
||||
- name: Check cache
|
||||
uses: actions/cache/restore@v3
|
||||
id: cache-check
|
||||
|
|
@ -111,19 +49,31 @@ jobs:
|
|||
key: "translation-${{ github.sha }}"
|
||||
lookup-only: true
|
||||
restore-keys: "translation-"
|
||||
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
if: steps.cache-check.outputs.cache-hit != 'true'
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'true'
|
||||
|
||||
- name: Check Crowdin secrets
|
||||
id: crowdin-check
|
||||
run: |
|
||||
if [ -z "$CROWDIN_PROJECT_ID" ] || [ -z "$CROWDIN_PERSONAL_TOKEN" ]; then
|
||||
echo "available=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "available=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_TOKEN }}
|
||||
|
||||
- name: Download translations from crowdin
|
||||
uses: crowdin/github-action@v1.6.0
|
||||
if: steps.cache-check.outputs.cache-hit != 'true'
|
||||
uses: crowdin/github-action@v2.4.0
|
||||
if: steps.cache-check.outputs.cache-hit != 'true' && steps.crowdin-check.outputs.available == 'true'
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_sources: ${{ github.repository == 'MCCTeam/Minecraft-Console-Client' }}
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
|
||||
|
|
@ -143,12 +93,154 @@ jobs:
|
|||
with:
|
||||
path: ${{ github.workspace }}/*
|
||||
key: "translation-${{ github.sha }}"
|
||||
|
||||
determine-build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
if: ${{ !contains(github.event.head_commit.message, 'skip') || !contains(github.event.head_commit.message, 'skipci')}}
|
||||
|
||||
create-tag:
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 5 # Wait 5 minutes in case of network issues/etc
|
||||
needs: determine-build
|
||||
if: ${{ needs.determine-build.outputs.skip != 'true' }}
|
||||
steps:
|
||||
- name: dummy action
|
||||
run: "echo 'dummy action that checks if the build is to be skipped, if it is, this action does not run to break the entire build action'"
|
||||
- id: make-tag
|
||||
run: |
|
||||
TAG="$(date -u +'%Y%m%d')-${{ github.run_number }}"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "TAG=$TAG" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Release Tag
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const tag = process.env.TAG;
|
||||
try {
|
||||
await github.rest.git.createRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `refs/tags/${tag}`,
|
||||
sha: context.sha
|
||||
});
|
||||
} catch(error) {
|
||||
if (error.message.includes('already exists')) {
|
||||
console.log(`Tag ${tag} already exists`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
outputs:
|
||||
build-tag: ${{ steps.make-tag.outputs.tag }}
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
# Check if we're not skipping build, tag is created, and translations successfully fetched (or skipped)
|
||||
if: ${{ needs.determine-build.outputs.skip != 'true' &&
|
||||
needs.create-tag.result == 'success' &&
|
||||
(needs.fetch-translations.result == 'success' || needs.fetch-translations.result == 'skipped')
|
||||
}}
|
||||
needs: [determine-build, fetch-translations, create-tag]
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
matrix:
|
||||
target: [win-x86, win-x64, win-arm64, linux-x64, linux-arm, linux-arm64, osx-x64, osx-arm64]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'true'
|
||||
|
||||
- name: Get Current Date
|
||||
run: |
|
||||
echo date_dashed=$(date -u +'%Y-%m-%d') >> $GITHUB_ENV
|
||||
|
||||
- name: Restore Translations (if available)
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/*
|
||||
key: "translation-${{ github.sha }}"
|
||||
restore-keys: "translation-"
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ env.dotnet-version }}
|
||||
|
||||
- name: Setup Environment Variables
|
||||
run: |
|
||||
PROJECT_PATH=${{ github.workspace }}/${{ env.PROJECT }}
|
||||
FILE_EXT=${{ (startsWith(matrix.target, 'win') && '.exe') || '' }}
|
||||
TARGET_OUT_PATH=$PROJECT_PATH/bin/Release/${{ env.target-version }}/${{ matrix.target }}/publish/
|
||||
|
||||
echo "project-path=$PROJECT_PATH" >> $GITHUB_ENV
|
||||
echo "file-ext=$FILE_EXT" >> $GITHUB_ENV
|
||||
echo "target-out-path=$TARGET_OUT_PATH" >> $GITHUB_ENV
|
||||
echo "assembly-info=$PROJECT_PATH/Properties/AssemblyInfo.cs" >> $GITHUB_ENV
|
||||
echo "build-version-info=${{ needs.create-tag.outputs.build-tag }}" >> $GITHUB_ENV
|
||||
echo "commit=$(echo ${{ github.sha }} | cut -c 1-7)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Binaries Path
|
||||
run: |
|
||||
echo built-executable-path=${{ env.target-out-path }}${{ env.PROJECT }}${{ env.file-ext }} >> $GITHUB_ENV
|
||||
|
||||
- name: Set Version Info
|
||||
run: |
|
||||
echo '' >> ${{ env.assembly-info }}
|
||||
echo "[assembly: AssemblyConfiguration(\"GitHub build ${{ github.run_number }}, built on ${{ env.date_dashed }} from commit ${{ env.commit }}\")]" >> ${{ env.assembly-info }}
|
||||
|
||||
- name: Inject Sentry DSN (if applicable)
|
||||
if: ${{ github.repository == 'MCCTeam/Minecraft-Console-Client' }}
|
||||
run: |
|
||||
grep -q 'SentryDSN = "";' ${{ env.project-path }}/Program.cs || { echo "SentryDSN pattern not found in Program.cs"; exit 1; }
|
||||
sed -i -e 's|SentryDSN = "";|SentryDSN = "${{ secrets.SENTRY_DSN }}";|g' ${{ env.project-path }}/Program.cs
|
||||
|
||||
- name: Build Target
|
||||
run: dotnet publish ${{ env.project-path }}/${{ env.PROJECT }}.csproj -f ${{ env.target-version }} -r ${{ matrix.target }} ${{ env.compile-flags }}
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
|
||||
- name: Rename Binary
|
||||
run: |
|
||||
mv ${{ env.built-executable-path }} ${{ env.PROJECT }}-${{ env.build-version-info }}-${{ matrix.target }}${{ env.file-ext }}
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PROJECT }}-${{ env.build-version-info }}-${{ matrix.target }}
|
||||
path: ${{ env.PROJECT }}-${{ env.build-version-info }}-${{ matrix.target }}${{ env.file-ext }}
|
||||
if-no-files-found: error
|
||||
|
||||
create-release:
|
||||
runs-on: ubuntu-slim
|
||||
needs: [create-tag, build]
|
||||
if: ${{ needs.build.result == 'success' && needs.create-tag.result == 'success' }}
|
||||
steps:
|
||||
- name: Download All Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts/
|
||||
merge-multiple: true
|
||||
|
||||
- name: Truncate commit message for release name
|
||||
id: release-name
|
||||
run: |
|
||||
SUBJECT=$(echo "$COMMIT_MSG" | head -n 1)
|
||||
MAX=220
|
||||
TRUNCATED="${SUBJECT:0:$MAX}"
|
||||
echo "name=${BUILD_TAG}: $TRUNCATED" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
COMMIT_MSG: ${{ github.event.head_commit.message }}
|
||||
BUILD_TAG: ${{ needs.create-tag.outputs.build-tag }}
|
||||
|
||||
- name: Create Release
|
||||
uses: ncipollo/release-action@v1.14.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
artifacts: "artifacts/**/*"
|
||||
tag: ${{ needs.create-tag.outputs.build-tag }}
|
||||
name: ${{ steps.release-name.outputs.name }}
|
||||
generateReleaseNotes: true
|
||||
artifactErrorsFailBuild: true
|
||||
allowUpdates: true
|
||||
makeLatest: true
|
||||
omitBodyDuringUpdate: true
|
||||
omitNameDuringUpdate: true
|
||||
replacesArtifacts: true
|
||||
|
|
|
|||
37
.github/workflows/deploy-doc-only.yml
vendored
37
.github/workflows/deploy-doc-only.yml
vendored
|
|
@ -6,8 +6,27 @@ on:
|
|||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-secrets:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
has-deploy-token: ${{ steps.check.outputs.has-deploy-token }}
|
||||
steps:
|
||||
- name: Check required secrets
|
||||
id: check
|
||||
run: |
|
||||
if [ -z "$GH_PAGES_TOKEN" ]; then
|
||||
echo "has-deploy-token=false" >> $GITHUB_OUTPUT
|
||||
echo "::warning::GH_PAGES_TOKEN is not set, skipping documentation deployment."
|
||||
else
|
||||
echo "has-deploy-token=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_PAGES_TOKEN: ${{ secrets.GH_PAGES_TOKEN }}
|
||||
|
||||
Build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check-secrets
|
||||
if: ${{ needs.check-secrets.outputs.has-deploy-token == 'true' }}
|
||||
|
||||
steps:
|
||||
|
||||
|
|
@ -17,8 +36,22 @@ jobs:
|
|||
fetch-depth: 0
|
||||
submodules: 'true'
|
||||
|
||||
- name: Check Crowdin secrets
|
||||
id: crowdin-check
|
||||
run: |
|
||||
if [ -z "$CROWDIN_PROJECT_ID" ] || [ -z "$CROWDIN_PERSONAL_TOKEN" ]; then
|
||||
echo "available=false" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Crowdin secrets not set, skipping translation download."
|
||||
else
|
||||
echo "available=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_TOKEN }}
|
||||
|
||||
- name: Download translations from crowdin
|
||||
uses: crowdin/github-action@v1.6.0
|
||||
if: ${{ steps.crowdin-check.outputs.available == 'true' }}
|
||||
uses: crowdin/github-action@v2.4.0
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
|
|
@ -40,7 +73,7 @@ jobs:
|
|||
ACCESS_TOKEN: ${{ secrets.GH_PAGES_TOKEN }}
|
||||
TARGET_REPO: MCCTeam/MCCTeam.github.io
|
||||
TARGET_BRANCH: master
|
||||
BUILD_SCRIPT: yarn --cwd ./docs/ && yarn --cwd ./docs/ docs:build
|
||||
BUILD_SCRIPT: export NODE_OPTIONS=--max-old-space-size=8192 && yarn --cwd ./docs/ && yarn --cwd ./docs/ docs:build
|
||||
BUILD_DIR: docs/.vuepress/dist
|
||||
COMMIT_MESSAGE: Build from ${{ github.sha }}
|
||||
CNAME: https://mccteam.github.io
|
||||
|
|
|
|||
35
.gitignore
vendored
35
.gitignore
vendored
|
|
@ -8,10 +8,15 @@
|
|||
/Other/
|
||||
/.vs/
|
||||
SessionCache.ini
|
||||
.*
|
||||
!/.github
|
||||
/packages
|
||||
|
||||
# OS-generated files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
ehthumbs.db
|
||||
._*
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
|
|
@ -383,7 +388,7 @@ FodyWeavers.xsd
|
|||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/launch.json``
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
|
|
@ -404,6 +409,8 @@ FodyWeavers.xsd
|
|||
|
||||
# docs
|
||||
!/docs/.vuepress
|
||||
/docs/.vuepress/.cache
|
||||
/docs/.vuepress/.temp
|
||||
/docs/.vuepress/dist
|
||||
|
||||
# translations
|
||||
|
|
@ -416,3 +423,25 @@ FodyWeavers.xsd
|
|||
|
||||
/docs/l10n/
|
||||
/docs/.vuepress/public/MCC-README/
|
||||
/docs/superpowers/
|
||||
|
||||
# Floder to store the decompiled Minecraft official source code
|
||||
/MinecraftOfficial/
|
||||
|
||||
# Possible debug files
|
||||
/lang/*
|
||||
/mcc_input.txt
|
||||
/MinecraftClient.ini
|
||||
/MinecraftClient.backup.ini
|
||||
|
||||
# SpecStory files
|
||||
/.specstory/
|
||||
/.vscode/settings.json
|
||||
|
||||
# Other
|
||||
/Sentry/
|
||||
/downloads/
|
||||
server.pid
|
||||
|
||||
# Crowdin translation automation working directory
|
||||
/.crowdin-translate/
|
||||
|
|
|
|||
980
.skills/csharp-best-practices/SKILL.md
Normal file
980
.skills/csharp-best-practices/SKILL.md
Normal file
|
|
@ -0,0 +1,980 @@
|
|||
---
|
||||
name: csharp-best-practices
|
||||
description: >
|
||||
C# 14 / .NET 10 coding conventions, idiomatic patterns, and performance best practices
|
||||
for the Minecraft Console Client codebase. Use when writing, reviewing, or modifying C# code.
|
||||
---
|
||||
|
||||
# C# 14 / .NET 10 Best Practices
|
||||
|
||||
Target: **.NET 10**, **C# 14**, nullable enabled.
|
||||
Sources: [MS C# Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) · [.NET Runtime Style](https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md) · [C# 14 Proposals](https://github.com/dotnet/csharplang/blob/main/Language-Version-History.md) · [C# 13 Docs](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13)
|
||||
|
||||
## Naming
|
||||
|
||||
| Element | Style | Example |
|
||||
|---|---|---|
|
||||
| Type, method, property, const, enum member | PascalCase | `PacketHandler`, `MaxRetries`, `GameMode.Survival` |
|
||||
| Interface | `I` + PascalCase | `IChatBot` |
|
||||
| Private instance field | `_camelCase` | `_handler` |
|
||||
| Private static field | `s_camelCase` | `s_defaultTimeout` |
|
||||
| Thread-static field | `t_camelCase` | `t_cachedBuffer` |
|
||||
| Local, parameter | camelCase | `packetId` |
|
||||
| Type parameter | `T` + PascalCase | `TResult` |
|
||||
| Namespace | PascalCase | `MinecraftClient.Protocol` |
|
||||
| Async methods | Suffix `Async` | `ConnectAsync()`, `ReadPacketAsync()` |
|
||||
|
||||
```csharp
|
||||
// CORRECT: naming conventions
|
||||
private readonly Dictionary<int, Entity> _entities = new();
|
||||
private static readonly TimeSpan s_reconnectDelay = TimeSpan.FromSeconds(5);
|
||||
public int PacketCount { get; private set; }
|
||||
public async Task<bool> ConnectAsync(CancellationToken ct) { }
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: naming violations
|
||||
private Dictionary<int, Entity> entities = new(); // missing _
|
||||
private static TimeSpan reconnectDelay; // missing s_
|
||||
public int packet_count { get; set; } // snake_case
|
||||
public async Task<bool> Connect(CancellationToken ct) { } // missing Async suffix
|
||||
```
|
||||
|
||||
## C# 14 Features
|
||||
|
||||
### Extension Members (C# 14)
|
||||
|
||||
Declare extension methods, properties, and operators inside `extension(...)` blocks. Replaces `this`-parameter pattern for new extensions.
|
||||
|
||||
```csharp
|
||||
// CORRECT: extension property + method (C# 14)
|
||||
public static class EntityExtensions
|
||||
{
|
||||
extension(Entity entity)
|
||||
{
|
||||
public bool IsAlive => entity.Health > 0;
|
||||
public void Heal(int amount) => entity.Health = Math.Min(entity.Health + amount, 20);
|
||||
}
|
||||
extension<T>(IEnumerable<T> items)
|
||||
{
|
||||
public bool IsEmpty => !items.GetEnumerator().MoveNext();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: classic extension method when C# 14 extension block is available
|
||||
public static bool IsAlive(this Entity entity) => entity.Health > 0;
|
||||
```
|
||||
|
||||
### `field` Keyword in Properties (C# 14)
|
||||
|
||||
Access the auto-generated backing field without declaring it. Mix auto and full accessors.
|
||||
|
||||
```csharp
|
||||
// CORRECT: lazy init with field keyword
|
||||
public string DisplayName => field ??= ComputeDisplayName();
|
||||
|
||||
// CORRECT: INotifyPropertyChanged pattern
|
||||
public bool IsConnected
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if (field == value) return;
|
||||
field = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: manual backing field when field keyword suffices
|
||||
private string? _displayName;
|
||||
public string DisplayName => _displayName ??= ComputeDisplayName();
|
||||
```
|
||||
|
||||
### Null-Conditional Assignment (C# 14)
|
||||
|
||||
Assign through `?.` — RHS is only evaluated when receiver is non-null.
|
||||
|
||||
```csharp
|
||||
// CORRECT: null-conditional assignment
|
||||
player?.Health = 20;
|
||||
connection?.OnDisconnect += HandleDisconnect;
|
||||
inventory?[slot] = newItem;
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: manual null check for simple assignment
|
||||
if (player is not null)
|
||||
player.Health = 20;
|
||||
```
|
||||
|
||||
### Simple Lambda Parameters with Modifiers (C# 14)
|
||||
|
||||
Omit types on lambda parameters while still applying modifiers.
|
||||
|
||||
```csharp
|
||||
// CORRECT: modifiers without explicit types
|
||||
TryParse<int> parse = (text, out result) => int.TryParse(text, out result);
|
||||
ReadOnlySpan<int> data = [1, 2, 3];
|
||||
ProcessSpan((scoped span) => span.Length);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: fully explicit types just for a modifier
|
||||
TryParse<int> parse = (string text, out int result) => int.TryParse(text, out result);
|
||||
```
|
||||
|
||||
### First-Class Span Types (C# 14)
|
||||
|
||||
Implicit conversions between `T[]`, `Span<T>`, and `ReadOnlySpan<T>` — no explicit cast needed. Extension methods on `ReadOnlySpan<T>` apply to arrays and spans automatically.
|
||||
|
||||
```csharp
|
||||
// CORRECT: pass array where ReadOnlySpan<T> is expected (C# 14)
|
||||
int[] data = [1, 2, 3];
|
||||
bool found = data.StartsWith(1); // ReadOnlySpan<int> extension resolved
|
||||
ReadOnlySpan<byte> span = stackalloc byte[4];
|
||||
```
|
||||
|
||||
### Unbound Generics in `nameof` (C# 14)
|
||||
|
||||
```csharp
|
||||
// CORRECT: no need to pick a dummy type argument
|
||||
string name = nameof(Dictionary<,>); // "Dictionary"
|
||||
string prop = nameof(List<>.Count); // "Count"
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: arbitrary type argument just to satisfy nameof
|
||||
string name = nameof(Dictionary<object, object>);
|
||||
```
|
||||
|
||||
### Partial Events and Constructors (C# 14)
|
||||
|
||||
Separate declaration from implementation for source-generator scenarios.
|
||||
|
||||
```csharp
|
||||
// CORRECT: partial constructor for source-gen interop
|
||||
partial class ServerConnection
|
||||
{
|
||||
partial ServerConnection(string host, int port);
|
||||
}
|
||||
partial class ServerConnection
|
||||
{
|
||||
partial ServerConnection(string host, int port) { /* generated */ }
|
||||
}
|
||||
```
|
||||
|
||||
### `#:` Ignored Directives (C# 14)
|
||||
|
||||
For file-based `dotnet run app.cs` programs — ignored by the compiler.
|
||||
|
||||
```csharp
|
||||
#!/usr/bin/dotnet run
|
||||
#:package System.CommandLine@2.0.0-*
|
||||
Console.WriteLine("Hello");
|
||||
```
|
||||
|
||||
## C# 13 Features
|
||||
|
||||
### `Lock` Object (C# 13)
|
||||
|
||||
Use `System.Threading.Lock` instead of `lock(obj)` on arbitrary objects.
|
||||
|
||||
```csharp
|
||||
// CORRECT: dedicated Lock type
|
||||
private readonly Lock _lock = new();
|
||||
public void Enqueue(ChatMessage msg) { lock (_lock) _queue.Add(msg); }
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: locking on an object reference
|
||||
private readonly object _syncRoot = new();
|
||||
lock (_syncRoot) { }
|
||||
```
|
||||
|
||||
### `params` Collections (C# 13)
|
||||
|
||||
`params` now works with `ReadOnlySpan<T>`, `Span<T>`, `IEnumerable<T>`, and other collection types.
|
||||
|
||||
```csharp
|
||||
// CORRECT: params span avoids array allocation
|
||||
public void Log(params ReadOnlySpan<string> messages)
|
||||
{
|
||||
foreach (var msg in messages) Console.WriteLine(msg);
|
||||
}
|
||||
```
|
||||
|
||||
### Partial Properties (C# 13)
|
||||
|
||||
```csharp
|
||||
// CORRECT: partial property for source generators
|
||||
partial class Config
|
||||
{
|
||||
public partial string Host { get; set; }
|
||||
}
|
||||
partial class Config
|
||||
{
|
||||
public partial string Host { get => _host; set => _host = value; }
|
||||
private string _host = "";
|
||||
}
|
||||
```
|
||||
|
||||
## C# 12 Features
|
||||
|
||||
### Primary Constructors
|
||||
|
||||
Use for simple parameter capture. Parameters are `camelCase`, mutable — assign to `readonly` fields when immutability matters.
|
||||
|
||||
```csharp
|
||||
// CORRECT: primary constructor captures dependencies
|
||||
public class ChatLogger(string logFilePath, bool appendMode) : ChatBot
|
||||
{
|
||||
private readonly StreamWriter _writer = new(logFilePath, appendMode);
|
||||
public override void GetText(string text) => _writer.WriteLine(text);
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: verbose constructor boilerplate for simple capture
|
||||
public class ChatLogger : ChatBot
|
||||
{
|
||||
private readonly StreamWriter _writer;
|
||||
public ChatLogger(string logFilePath, bool appendMode)
|
||||
{
|
||||
_writer = new StreamWriter(logFilePath, appendMode);
|
||||
}
|
||||
public override void GetText(string text) => _writer.WriteLine(text);
|
||||
}
|
||||
```
|
||||
|
||||
### Collection Expressions
|
||||
|
||||
Use `[...]` and `..` spread for arrays, lists, spans.
|
||||
|
||||
```csharp
|
||||
// CORRECT: collection expressions (C# 12)
|
||||
int[] ids = [1, 2, 3];
|
||||
List<string> names = ["Steve", "Alex"];
|
||||
ReadOnlySpan<byte> header = [0xFE, 0x01]; // no heap alloc
|
||||
int[] combined = [..firstArray, ..secondArray, 42];
|
||||
IReadOnlyList<string> empty = [];
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: verbose initialization
|
||||
int[] ids = new int[] { 1, 2, 3 };
|
||||
var names = new List<string> { "Steve", "Alex" };
|
||||
ReadOnlySpan<byte> header = new byte[] { 0xFE, 0x01 }; // allocates
|
||||
var combined = firstArray.Concat(secondArray).Append(42).ToArray();
|
||||
```
|
||||
|
||||
### Type Aliases
|
||||
|
||||
```csharp
|
||||
// CORRECT: alias complex types for readability
|
||||
using Coordinate = (int X, int Y, int Z);
|
||||
using PacketMap = System.Collections.Generic.Dictionary<int, System.Action<byte[]>>;
|
||||
```
|
||||
|
||||
### Default Lambda Parameters
|
||||
|
||||
```csharp
|
||||
// CORRECT: C# 12
|
||||
var greet = (string name, string prefix = "Player") => $"{prefix} {name}";
|
||||
```
|
||||
|
||||
## Modern Syntax (C# 10–14)
|
||||
|
||||
### File-Scoped Namespaces
|
||||
|
||||
```csharp
|
||||
// CORRECT: file-scoped namespace — one per file, less nesting
|
||||
namespace MinecraftClient.ChatBots;
|
||||
|
||||
public class MyBot : ChatBot { }
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: block-scoped namespace adds unnecessary nesting
|
||||
namespace MinecraftClient.ChatBots
|
||||
{
|
||||
public class MyBot : ChatBot { }
|
||||
}
|
||||
```
|
||||
|
||||
### Target-Typed `new`
|
||||
|
||||
Use when the type is obvious from the left-hand side.
|
||||
|
||||
```csharp
|
||||
// CORRECT: target-typed new
|
||||
private readonly Dictionary<string, int> _scores = new();
|
||||
List<Entity> entities = new(capacity: 256);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: redundant type name
|
||||
private readonly Dictionary<string, int> _scores = new Dictionary<string, int>();
|
||||
```
|
||||
|
||||
### Pattern Matching
|
||||
|
||||
Prefer patterns over type-casting chains and complex boolean logic.
|
||||
|
||||
```csharp
|
||||
// CORRECT: is-pattern with declaration and property patterns
|
||||
if (entity is Player { Health: > 0 } player)
|
||||
SendMessage($"{player.Name} is alive");
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: manual cast and multi-step check
|
||||
if (entity is Player)
|
||||
{
|
||||
var player = (Player)entity;
|
||||
if (player.Health > 0)
|
||||
SendMessage($"{player.Name} is alive");
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: switch expression
|
||||
public string GetStatusLabel(GameMode mode) => mode switch
|
||||
{
|
||||
GameMode.Survival => "Survival",
|
||||
GameMode.Creative => "Creative",
|
||||
GameMode.Adventure => "Adventure",
|
||||
GameMode.Spectator => "Spectator",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mode))
|
||||
};
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: switch statement with returns
|
||||
public string GetStatusLabel(GameMode mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case GameMode.Survival: return "Survival";
|
||||
case GameMode.Creative: return "Creative";
|
||||
default: throw new ArgumentOutOfRangeException(nameof(mode));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: property patterns for compound conditions
|
||||
if (response is { StatusCode: >= 200 and < 300, Content.Length: > 0 })
|
||||
ProcessResponse(response);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: multiple chained conditions
|
||||
if (response != null && response.StatusCode >= 200
|
||||
&& response.StatusCode < 300 && response.Content != null
|
||||
&& response.Content.Length > 0)
|
||||
ProcessResponse(response);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: relational, logical, and list patterns
|
||||
if (health is > 0 and <= 6) LogToConsole("Low health!");
|
||||
if (args is [var command, var target, ..]) ProcessCommand(command, target);
|
||||
```
|
||||
|
||||
### Raw String Literals
|
||||
|
||||
Use for JSON, regex, multi-line strings.
|
||||
|
||||
```csharp
|
||||
// CORRECT: raw string literal
|
||||
string json = """
|
||||
{ "username": "Steve", "action": "connect" }
|
||||
""";
|
||||
string pattern = """<\w+>""";
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: escaped quotes
|
||||
string json = "{ \"username\": \"Steve\", \"action\": \"connect\" }";
|
||||
```
|
||||
|
||||
### Records
|
||||
|
||||
Use `record` for immutable data carriers and DTOs. Use `record struct` for small value types.
|
||||
|
||||
```csharp
|
||||
// CORRECT: record for data carrier
|
||||
public record PlayerInfo(string Name, Guid Uuid, GameMode Mode);
|
||||
public record struct ChunkCoord(int X, int Z);
|
||||
var updated = info with { Mode = GameMode.Creative };
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: full class for a simple data carrier
|
||||
public class PlayerInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public Guid Uuid { get; set; }
|
||||
public GameMode Mode { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: compact constructor for record validation
|
||||
public record OrderItem(string ProductId, int Quantity, decimal UnitPrice)
|
||||
{
|
||||
public OrderItem
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(ProductId);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(Quantity);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(UnitPrice);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Required Members
|
||||
|
||||
```csharp
|
||||
// CORRECT: required + init enforces initialization without constructor boilerplate
|
||||
public class ServerConfig
|
||||
{
|
||||
public required string Host { get; init; }
|
||||
public required int Port { get; init; }
|
||||
public string? Password { get; init; }
|
||||
}
|
||||
var config = new ServerConfig { Host = "mc.example.com", Port = 25565 };
|
||||
```
|
||||
|
||||
## Nullable Reference Types
|
||||
|
||||
Project has nullable enabled. Follow these rules:
|
||||
|
||||
```csharp
|
||||
// CORRECT: guard at API boundaries with .NET 8 throw helpers
|
||||
public void Connect(string host, IProtocolHandler handler)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
ArgumentException.ThrowIfNullOrEmpty(host);
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: manual null checks
|
||||
if (handler == null) throw new ArgumentNullException(nameof(handler));
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
throw new ArgumentException("Host is required", nameof(host));
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: 'is not null' pattern
|
||||
if (currentPlayer is not null)
|
||||
currentPlayer.Update();
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: comparison operator for null check
|
||||
if (currentPlayer != null)
|
||||
currentPlayer.Update();
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: explicit nullable handling
|
||||
public Entity? FindEntity(int id)
|
||||
{
|
||||
return _entities.TryGetValue(id, out var entity) ? entity : null;
|
||||
}
|
||||
|
||||
// CORRECT: null-coalescing / null-conditional
|
||||
string name = player?.CustomName ?? player?.Name ?? "Unknown";
|
||||
|
||||
// CORRECT: null-forgiving only when proven safe (after ThrowIfNull or equivalent)
|
||||
string val = GetRequiredValue()!;
|
||||
|
||||
// CORRECT: annotate return values
|
||||
[return: MaybeNull]
|
||||
public T Find<T>(Predicate<T> match) { }
|
||||
|
||||
[MemberNotNull(nameof(_connection))]
|
||||
private void EnsureConnected() { }
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: hiding nullability with null-forgiving
|
||||
public string GetName(Player? player)
|
||||
{
|
||||
return player!.Name; // hides potential NullReferenceException
|
||||
}
|
||||
```
|
||||
|
||||
## Async / Await
|
||||
|
||||
```csharp
|
||||
// CORRECT: propagate CancellationToken through every async I/O call
|
||||
public async Task<string> FetchDataAsync(Uri uri, CancellationToken ct = default)
|
||||
{
|
||||
using var response = await _httpClient.GetAsync(uri, ct);
|
||||
return await response.Content.ReadAsStringAsync(ct);
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: CancellationToken not passed downstream
|
||||
public async Task<string> FetchDataAsync(Uri uri)
|
||||
{
|
||||
using var response = await _httpClient.GetAsync(uri, default);
|
||||
return await response.Content.ReadAsStringAsync(default);
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: ValueTask when result is often available synchronously
|
||||
public ValueTask<int> GetCachedCountAsync()
|
||||
{
|
||||
if (_cache.TryGetValue("count", out int count))
|
||||
return ValueTask.FromResult(count);
|
||||
return new ValueTask<int>(LoadCountFromDbAsync());
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: Task allocates unnecessarily when result is cached
|
||||
public async Task<int> GetCachedCountAsync()
|
||||
{
|
||||
if (_cache.TryGetValue("count", out int count))
|
||||
return count; // allocates a Task
|
||||
return await LoadCountFromDbAsync();
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: async Task for async event handlers
|
||||
public async Task HandleEventAsync(GameEvent e, CancellationToken ct)
|
||||
{
|
||||
await notificationService.SendAsync(e.PlayerId, ct);
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: async void — exceptions are unobservable, cannot be awaited
|
||||
public async void HandleEvent(GameEvent e)
|
||||
{
|
||||
await notificationService.SendAsync(e.PlayerId, default);
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: await the result
|
||||
var packet = await reader.ReadPacketAsync(ct);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: .Result / .Wait() causes deadlocks
|
||||
var packet = reader.ReadPacketAsync(ct).Result;
|
||||
var packet2 = reader.ReadPacketAsync(ct).GetAwaiter().GetResult();
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: ConfigureAwait(false) in library code
|
||||
var data = await stream.ReadAsync(buffer, ct).ConfigureAwait(false);
|
||||
|
||||
// CORRECT: IAsyncEnumerable for streaming
|
||||
public async IAsyncEnumerable<ChatMessage> ReadChatStreamAsync(
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
yield return await _reader.ReadNextAsync(ct);
|
||||
}
|
||||
|
||||
// CORRECT: await using for async disposal
|
||||
await using var conn = new McConnection(host, port);
|
||||
```
|
||||
|
||||
## LINQ
|
||||
|
||||
### Prefer Method Syntax for Most Operations
|
||||
|
||||
```csharp
|
||||
// CORRECT: method syntax for common operations
|
||||
var onlinePlayers = players
|
||||
.Where(p => p.IsOnline)
|
||||
.OrderBy(p => p.Name)
|
||||
.Select(p => new PlayerListItem(p.Id, p.Name))
|
||||
.ToList();
|
||||
```
|
||||
|
||||
```csharp
|
||||
// AVOID: query syntax for simple operations
|
||||
var onlinePlayers = (
|
||||
from p in players
|
||||
where p.IsOnline
|
||||
orderby p.Name
|
||||
select new PlayerListItem(p.Id, p.Name)
|
||||
).ToList();
|
||||
```
|
||||
|
||||
### Use Query Syntax for Joins
|
||||
|
||||
```csharp
|
||||
// CORRECT: query syntax makes joins readable
|
||||
var results =
|
||||
from entity in entities
|
||||
join player in players on entity.OwnerId equals player.Id
|
||||
where entity.Health > 0
|
||||
select new { entity.Name, player.Name };
|
||||
```
|
||||
|
||||
```csharp
|
||||
// AVOID: method syntax for complex joins is hard to read
|
||||
var results = entities
|
||||
.Join(players,
|
||||
e => e.OwnerId,
|
||||
p => p.Id,
|
||||
(e, p) => new { e, p })
|
||||
.Where(x => x.e.Health > 0)
|
||||
.Select(x => new { x.e.Name, PlayerName = x.p.Name });
|
||||
```
|
||||
|
||||
### Materialize to Avoid Multiple Enumeration
|
||||
|
||||
```csharp
|
||||
// CORRECT: materialize once, iterate many times
|
||||
var online = players.Where(p => p.IsOnline).ToList();
|
||||
Console.WriteLine(online.Count);
|
||||
foreach (var p in online) { }
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: enumerates the query twice
|
||||
var filtered = players.Where(p => p.IsOnline);
|
||||
Console.WriteLine(filtered.Count()); // first enumeration
|
||||
foreach (var p in filtered) { } // second enumeration
|
||||
```
|
||||
|
||||
### Use Any() Over Count() > 0
|
||||
|
||||
```csharp
|
||||
// CORRECT: short-circuits on first match
|
||||
if (entities.Any(e => e.IsHostile))
|
||||
TriggerAlert();
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: counts the entire collection
|
||||
if (entities.Count(e => e.IsHostile) > 0)
|
||||
TriggerAlert();
|
||||
```
|
||||
|
||||
### Prefer FirstOrDefault with Null Handling
|
||||
|
||||
```csharp
|
||||
// CORRECT: explicit null handling
|
||||
var target = players.FirstOrDefault(p => p.Name == name)
|
||||
?? throw new InvalidOperationException($"Player '{name}' not found");
|
||||
```
|
||||
|
||||
### TryGetNonEnumeratedCount
|
||||
|
||||
```csharp
|
||||
// CORRECT: avoid full enumeration just to get count (.NET 6+)
|
||||
if (source.TryGetNonEnumeratedCount(out int count))
|
||||
buffer = new Entity[count];
|
||||
```
|
||||
|
||||
### Avoid LINQ in Hot Paths
|
||||
|
||||
```csharp
|
||||
// CORRECT: manual loop with Span in performance-critical code
|
||||
Span<byte> data = stackalloc byte[256];
|
||||
int found = 0;
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
if (data[i] == target) found++;
|
||||
```
|
||||
|
||||
```csharp
|
||||
// AVOID: LINQ allocates enumerators and delegates on hot paths
|
||||
int found = data.ToArray().Count(b => b == target);
|
||||
```
|
||||
|
||||
## Performance (.NET 8+)
|
||||
|
||||
### Span\<T\> / Memory\<T\>
|
||||
|
||||
```csharp
|
||||
// CORRECT: zero-allocation slicing
|
||||
ReadOnlySpan<char> command = input.AsSpan()[1..]; // skip '/'
|
||||
|
||||
// CORRECT: stack-allocated parsing
|
||||
public static int ParseVarInt(ReadOnlySpan<byte> data, out int bytesRead)
|
||||
{
|
||||
int result = 0; bytesRead = 0; byte cur;
|
||||
do { cur = data[bytesRead]; result |= (cur & 0x7F) << (bytesRead * 7); bytesRead++; }
|
||||
while ((cur & 0x80) != 0);
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### FrozenDictionary / FrozenSet (.NET 8)
|
||||
|
||||
Build once, read many — ~50% faster lookups than Dictionary.
|
||||
|
||||
```csharp
|
||||
// CORRECT: FrozenDictionary for read-heavy lookup tables (palettes, protocol maps)
|
||||
using System.Collections.Frozen;
|
||||
private static readonly FrozenDictionary<int, string> s_blockNames =
|
||||
new Dictionary<int, string> { [0] = "air", [1] = "stone" }.ToFrozenDictionary();
|
||||
```
|
||||
|
||||
### SearchValues\<T\> (.NET 8)
|
||||
|
||||
Hardware-accelerated set search.
|
||||
|
||||
```csharp
|
||||
// CORRECT: precompute once, scan with SIMD
|
||||
private static readonly SearchValues<char> s_separators = SearchValues.Create(" \t\n\r,;");
|
||||
int idx = input.AsSpan().IndexOfAny(s_separators);
|
||||
```
|
||||
|
||||
### CompositeFormat (.NET 8)
|
||||
|
||||
Parse format string once, reuse.
|
||||
|
||||
```csharp
|
||||
// CORRECT: avoids re-parsing the format string each call
|
||||
private static readonly CompositeFormat s_logFmt = CompositeFormat.Parse("[{0:HH:mm:ss}] {1}: {2}");
|
||||
string msg = string.Format(CultureInfo.InvariantCulture, s_logFmt, DateTime.Now, player, text);
|
||||
```
|
||||
|
||||
### ArrayPool / stackalloc
|
||||
|
||||
```csharp
|
||||
// CORRECT: rent from pool for temporary buffers
|
||||
byte[] buf = ArrayPool<byte>.Shared.Rent(4096);
|
||||
try { int n = stream.Read(buf.AsSpan(0, 4096)); ProcessPacket(buf.AsSpan(0, n)); }
|
||||
finally { ArrayPool<byte>.Shared.Return(buf); }
|
||||
|
||||
// CORRECT: stackalloc for small, fixed-size buffers (< 512 bytes)
|
||||
Span<byte> header = stackalloc byte[5];
|
||||
```
|
||||
|
||||
## String Handling
|
||||
|
||||
```csharp
|
||||
// CORRECT: explicit StringComparison — always
|
||||
bool match = name.Equals("Steve", StringComparison.OrdinalIgnoreCase);
|
||||
int idx = text.IndexOf("hello", StringComparison.Ordinal);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: allocates a lowered copy
|
||||
bool match = name.ToLower() == "steve";
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: string.Create for perf-critical formatting
|
||||
string hex = string.Create(data.Length * 2, data, static (span, bytes) =>
|
||||
{
|
||||
for (int i = 0; i < bytes.Length; i++)
|
||||
bytes[i].TryFormat(span[(i * 2)..], out _, "X2");
|
||||
});
|
||||
|
||||
// CORRECT: StringBuilder for loops
|
||||
var sb = new StringBuilder(256);
|
||||
foreach (var item in inventory)
|
||||
sb.Append(item.Name).Append(" x").Append(item.Count).AppendLine();
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: O(n²) string concatenation in loop
|
||||
string combined = "";
|
||||
foreach (var s in items) combined += s + ", ";
|
||||
```
|
||||
|
||||
## Collections — Choosing the Right Type
|
||||
|
||||
| Scenario | Type | Notes |
|
||||
|---|---|---|
|
||||
| General key-value | `Dictionary<K,V>` | O(1) lookup |
|
||||
| Build once, read many | `FrozenDictionary<K,V>` | .NET 8+; faster reads |
|
||||
| Thread-safe | `ConcurrentDictionary<K,V>` | Lock-free reads |
|
||||
| Immutable snapshots | `ImmutableDictionary<K,V>` | Persistent structure |
|
||||
| Membership test | `HashSet<T>` / `FrozenSet<T>` | FrozenSet for static |
|
||||
| Priority queue | `PriorityQueue<E,P>` | .NET 6+ |
|
||||
| Synchronization | `System.Threading.Lock` | C# 13; prefer over `lock(obj)` |
|
||||
| Producer-consumer | `Channel<T>` | Over `BlockingCollection<T>` |
|
||||
| Temp buffer | `ArrayPool<T>` / `stackalloc` | Zero/low alloc |
|
||||
|
||||
## Error Handling
|
||||
|
||||
```csharp
|
||||
// CORRECT: Try* pattern for expected failures
|
||||
if (int.TryParse(input, out int value)) ProcessValue(value);
|
||||
if (_registry.TryGetValue(packetId, out var handler)) handler.Invoke(data);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: using exceptions for control flow
|
||||
try { return dict[key]; }
|
||||
catch (KeyNotFoundException) { return null; } // use TryGetValue
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: exception filters (catch-when)
|
||||
try { await ConnectAsync(ct); }
|
||||
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
|
||||
{
|
||||
LogToConsole("Connection refused, retrying...");
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: throw helpers (smaller IL, better inlining)
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(timeout);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(timeout, MaxTimeout);
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: generic exceptions
|
||||
throw new Exception($"Entity {id} not found");
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: specific, meaningful exception types
|
||||
throw new EntityNotFoundException(id);
|
||||
// or use null-coalescing with throw
|
||||
return await FindEntityAsync(id, ct)
|
||||
?? throw new EntityNotFoundException(id);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// AVOID: catching Exception without filtering
|
||||
try { DoWork(); }
|
||||
catch (Exception) { /* swallowed */ }
|
||||
```
|
||||
|
||||
## Warning Suppression
|
||||
|
||||
```csharp
|
||||
// CORRECT: fix the warning by handling null properly
|
||||
public string GetDisplayName(Player? player)
|
||||
{
|
||||
return player?.DisplayName ?? "Unknown";
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: suppressing nullable warning with pragma
|
||||
#pragma warning disable CS8602
|
||||
public string GetDisplayName(Player? player)
|
||||
{
|
||||
return player.DisplayName; // NullReferenceException at runtime
|
||||
}
|
||||
#pragma warning restore CS8602
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: suppressing with attribute
|
||||
[SuppressMessage("Usage", "CA1062:Validate arguments of public methods")]
|
||||
public void Process(Packet packet)
|
||||
{
|
||||
// missing null check
|
||||
}
|
||||
```
|
||||
|
||||
Project-wide `.editorconfig` is the only acceptable place for warning policy:
|
||||
|
||||
```text
|
||||
# .editorconfig - project-wide policy decisions only
|
||||
dotnet_diagnostic.CA2007.severity = none
|
||||
```
|
||||
|
||||
## Resource Management
|
||||
|
||||
```csharp
|
||||
// CORRECT: using declaration — disposed at end of scope
|
||||
using var stream = new FileStream(path, FileMode.Open);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
// CORRECT: IAsyncDisposable
|
||||
await using var conn = await CreateConnectionAsync();
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: Dispose pattern
|
||||
public class PacketReader : IDisposable
|
||||
{
|
||||
private Stream? _stream;
|
||||
private bool _disposed;
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_stream?.Dispose(); _stream = null; _disposed = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
```csharp
|
||||
// CORRECT: secure random for tokens
|
||||
byte[] token = RandomNumberGenerator.GetBytes(32);
|
||||
|
||||
// CORRECT: constant-time comparison for secrets
|
||||
bool valid = CryptographicOperations.FixedTimeEquals(expected, actual);
|
||||
|
||||
// CORRECT: validate external input
|
||||
if (Uri.TryCreate(userInput, UriKind.Absolute, out var uri)
|
||||
&& uri.Scheme is "http" or "https")
|
||||
await FetchAsync(uri);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: predictable random for security-sensitive values
|
||||
var rng = new Random();
|
||||
|
||||
// WRONG: timing side-channel on secret comparison
|
||||
bool eq = secret1.SequenceEqual(secret2);
|
||||
```
|
||||
|
||||
## Miscellaneous Idioms
|
||||
|
||||
```csharp
|
||||
// CORRECT: var when type is obvious from RHS
|
||||
var entities = new Dictionary<int, Entity>();
|
||||
var timer = Stopwatch.StartNew();
|
||||
|
||||
// CORRECT: explicit type when var would be unclear
|
||||
Stream responseStream = GetResponse();
|
||||
int count = items.Count;
|
||||
|
||||
// CORRECT: expression-bodied members for one-liners
|
||||
public override string ToString() => $"[{X}, {Y}, {Z}]";
|
||||
public bool IsAlive => Health > 0;
|
||||
|
||||
// CORRECT: discards for unused values
|
||||
_ = int.TryParse(s, out int result);
|
||||
(_, int y, _) = GetCoordinates();
|
||||
|
||||
// CORRECT: nameof for resilient refactoring (unbound generics in C# 14)
|
||||
throw new ArgumentException("Invalid value", nameof(packetId));
|
||||
LogToConsole($"{nameof(AutoEat)}: eating {item.Name}");
|
||||
string typeName = nameof(Dictionary<,>); // "Dictionary"
|
||||
|
||||
// CORRECT: static lambdas prevent accidental closure allocations
|
||||
list.Sort(static (a, b) => a.Id.CompareTo(b.Id));
|
||||
|
||||
// CORRECT: index/range operators
|
||||
var last = items[^1];
|
||||
var slice = data[3..^1];
|
||||
|
||||
// CORRECT: tuple deconstruction
|
||||
var (x, y, z) = GetPosition();
|
||||
|
||||
// CORRECT: string interpolation with alignment and format specifiers
|
||||
LogToConsole($"Health: {health,6:F1} | Hunger: {hunger,6:F1}");
|
||||
```
|
||||
166
.skills/csharp-dotnet-cli-optimization/SKILL.md
Normal file
166
.skills/csharp-dotnet-cli-optimization/SKILL.md
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
---
|
||||
name: csharp-dotnet-cli-optimization
|
||||
description: >-
|
||||
Use when diagnosing or optimizing generic C#/.NET performance, GC pressure,
|
||||
allocations, heap or stack usage, LINQ overhead, boxing, Span/Memory,
|
||||
stackalloc, pooling, or hot-path code with CLI-first tools such as
|
||||
dotnet-counters, dotnet-trace, dotnet-stack, dotnet-gcdump, dotnet-dump, or
|
||||
BenchmarkDotNet.
|
||||
metadata:
|
||||
category: technique
|
||||
triggers:
|
||||
- dotnet-counters
|
||||
- dotnet-trace
|
||||
- dotnet-dump
|
||||
- dotnet-gcdump
|
||||
- dotnet-stack
|
||||
- benchmarkdotnet
|
||||
- allocations
|
||||
- gc pressure
|
||||
- memory leak
|
||||
- hot path
|
||||
- linq
|
||||
- stackalloc
|
||||
- span
|
||||
- memory
|
||||
- boxing
|
||||
- heap
|
||||
- stack
|
||||
- latency
|
||||
- throughput
|
||||
- slow
|
||||
- hang
|
||||
- deadlock
|
||||
---
|
||||
|
||||
# C#/.NET CLI Optimization
|
||||
|
||||
CLI-first guidance for generic C# 14 / .NET 10 performance work.
|
||||
Use the references on demand:
|
||||
|
||||
- Read [references/memory-model-gc.md](references/memory-model-gc.md) for stack vs heap, generations, LOH, pinning, server vs workstation GC, and GC tuning limits.
|
||||
- Read [references/code-patterns.md](references/code-patterns.md) for LINQ, Span/Memory, stackalloc, structs, boxing, pooling, strings, and analyzer-backed code patterns.
|
||||
- Read [references/README.md](references/README.md) for dated sources and freshness notes.
|
||||
|
||||
## When to Use
|
||||
|
||||
- A .NET process is slow, allocation-heavy, CPU-heavy, or memory-hungry
|
||||
- A live process appears stuck, hung, or deadlocked
|
||||
- The user asks how heap, stack, GC, boxing, or LINQ overhead actually works in .NET
|
||||
- The user wants concrete bad vs good code patterns after measurement has identified a hot path
|
||||
- The task needs a decision between counters, traces, stacks, GC dumps, dumps, or a benchmark
|
||||
|
||||
**NOT for:**
|
||||
- ASP.NET Core, EF Core, MAUI, Orleans, Unity, Avalonia, WPF, WinForms, Blazor, or other framework-specific playbooks
|
||||
- Visual Studio, Rider, VS Code, PerfView, speedscope, or any GUI-first workflow
|
||||
- speculative rewrites such as "replace everything with Span" before measurement
|
||||
|
||||
## Iron Rule
|
||||
|
||||
ALWAYS measure first, change second, and re-measure third.
|
||||
|
||||
NEVER claim an optimization without before/after evidence from the same scenario.
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "This is obviously slow" | The runtime, JIT, and libraries often invalidate intuition. |
|
||||
| "struct means stack" | Value types are stored inline. They are not "always on the stack". |
|
||||
| "All LINQ is slow" | .NET 10 improved many LINQ paths. Measure before rewriting. |
|
||||
| "GC.Collect will fix it" | Forced collection usually treats symptoms, not cause. |
|
||||
|
||||
## Investigation Order
|
||||
|
||||
1. Use `dotnet-counters` for live triage.
|
||||
2. If the process is stuck, capture `dotnet-stack` immediately.
|
||||
3. If CPU or allocation hot paths matter, collect `dotnet-trace`.
|
||||
4. If heap growth matters more than call paths, collect `dotnet-gcdump`.
|
||||
5. If you need SOS heap inspection or a postmortem, collect `dotnet-dump`.
|
||||
6. Only after live evidence points to a candidate routine, apply patterns from the reference docs.
|
||||
7. If the change is truly local and isolated, use BenchmarkDotNet to compare implementations.
|
||||
8. Re-run the original live capture to prove the real workload improved.
|
||||
|
||||
## Which Reference to Load
|
||||
|
||||
| User question | Read first |
|
||||
|---|---|
|
||||
| "How do stack and heap really work in .NET?" | `references/memory-model-gc.md` |
|
||||
| "Why is GC pausing or why is LOH churn hurting us?" | `references/memory-model-gc.md` |
|
||||
| "How should I optimize this LINQ?" | `references/code-patterns.md` |
|
||||
| "Can I move this to the stack with stackalloc or Span?" | `references/code-patterns.md` |
|
||||
| "Should this be a struct, ref struct, readonly struct, or class?" | `references/code-patterns.md` and `references/memory-model-gc.md` |
|
||||
| "Why is this boxing?" | `references/code-patterns.md` |
|
||||
|
||||
## Tool Selection
|
||||
|
||||
| Question | Tool | What it answers | Typical next step |
|
||||
|---|---|---|---|
|
||||
| Is the live process allocating, GCing, or saturating CPU? | `dotnet-counters` | Live counters and trend direction | Capture a trace or GC dump if suspicious |
|
||||
| Is the process hung or deadlocked right now? | `dotnet-stack` | Current managed stack snapshot | Collect a dump if you need deeper postmortem evidence |
|
||||
| Which call paths consume CPU or allocate heavily? | `dotnet-trace` | Sampled execution and runtime events | Confirm hot paths, then isolate code |
|
||||
| Which object types dominate managed heap usage? | `dotnet-gcdump` | Heap composition and type totals | Decide whether to redesign lifetimes or collect a full dump |
|
||||
| Do I need SOS heap inspection or thread state? | `dotnet-dump` | Full dump plus CLI analysis | Run `analyze -c` commands |
|
||||
| Did a code change improve one isolated routine? | BenchmarkDotNet | Reproducible microbenchmark comparison | Re-run live diagnostics in the real scenario |
|
||||
|
||||
## Pattern Guardrails
|
||||
|
||||
- Do not answer "put it on the stack" as a blanket goal. Explain lifetime, copies, boxing, and escape rules instead.
|
||||
- Do not suggest `stackalloc` for unbounded sizes, large buffers, or loop-carried allocations.
|
||||
- Do not recommend `Span<T>` for data that must cross `await`, escape to the heap, or live in object fields. Switch to `Memory<T>` or `ReadOnlyMemory<T>` for that.
|
||||
- Do not recommend converting every `class` to a `struct`. Large, mutable, identity-bearing, or frequently boxed types often get worse.
|
||||
- Do not blanket-rewrite LINQ to loops. Use analyzer-backed fixes first, and remember .NET 10 substantially improved many LINQ paths.
|
||||
- Do not recommend pooling without ownership rules. Returned pooled arrays must not be reused by the caller.
|
||||
- Do not recommend `GC.Collect()` except for rare, justified lifecycle boundaries, and only with measurement.
|
||||
|
||||
## Analyzer Radar
|
||||
|
||||
When performance diagnostics point to code patterns rather than runtime configuration, consult the current performance analyzers, especially:
|
||||
|
||||
- `CA1826`, `CA1827`, `CA1829`, `CA1836`, `CA1851`, `CA1860` for LINQ and enumeration
|
||||
- `CA1845`, `CA1846`, `CA1858` for string and span-friendly APIs
|
||||
- `CA1834`, `CA1865-CA1867` for `StringBuilder` char overloads
|
||||
- `CA1870` for cached `SearchValues<T>`
|
||||
|
||||
These rules are clues, not goals. Apply them where the measured hot path justifies it.
|
||||
|
||||
## Minimal Commands
|
||||
|
||||
```bash
|
||||
dnx dotnet-counters monitor --process-id <PID>
|
||||
dotnet-counters monitor -p <PID> --counters System.Runtime
|
||||
dotnet-stack report -p <PID>
|
||||
dotnet-trace collect -p <PID> --duration 00:00:30
|
||||
dotnet-trace report <trace.nettrace> topN
|
||||
dotnet-gcdump collect -p <PID>
|
||||
dotnet-gcdump report <file.gcdump>
|
||||
dotnet-dump collect -p <PID> --type Heap
|
||||
dotnet-dump analyze <dump> -c "dumpheap -stat" -c "exit"
|
||||
```
|
||||
|
||||
Minimal BenchmarkDotNet pattern:
|
||||
|
||||
```csharp
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
[MemoryDiagnoser]
|
||||
public class CandidateBench
|
||||
{
|
||||
[Benchmark(Baseline = true)]
|
||||
public int Original() => OriginalImpl();
|
||||
|
||||
[Benchmark]
|
||||
public int Candidate() => CandidateImpl();
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
dotnet run -c Release
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
When using this skill, report:
|
||||
|
||||
- the measured symptom and the evidence used to identify it
|
||||
- the chosen tool or code pattern and why it fits this bottleneck
|
||||
- the relevant tradeoff, such as allocation vs copy cost, deferred vs eager execution, or stack vs pool
|
||||
- the before/after result, or say explicitly if the recommendation is still unverified
|
||||
68
.skills/csharp-dotnet-cli-optimization/references/README.md
Normal file
68
.skills/csharp-dotnet-cli-optimization/references/README.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
description: Dated source ledger for csharp-dotnet-cli-optimization.
|
||||
metadata:
|
||||
tags: [sources, diagnostics, gc, linq, span, stackalloc, boxing]
|
||||
---
|
||||
|
||||
# Sources
|
||||
|
||||
## Current primary sources
|
||||
|
||||
- [dotnet-counters](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters), Microsoft Learn, updated `2025-10-02`
|
||||
- Canonical CLI docs for live counters, `monitor`, `collect`, and `dnx` one-shot execution on .NET 10.0.100+.
|
||||
- [dotnet-trace](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-trace), Microsoft Learn, updated `2026-03-20`
|
||||
- Canonical CLI docs for `collect`, `report`, and the preview `collect-linux` path plus its limits.
|
||||
- [dotnet-dump](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-dump), Microsoft Learn, updated `2026-03-04`
|
||||
- Canonical CLI docs for dump collection, dump types, and `analyze -c`.
|
||||
- [dotnet-gcdump](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-gcdump), Microsoft Learn, updated `2025-12-17`
|
||||
- Canonical CLI docs for GC dump collection, `report`, and the induced full Gen 2 GC caveat.
|
||||
- [Fundamentals of garbage collection](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals), Microsoft Learn, updated `2025-10-22`
|
||||
- Current official overview of generations, allocation, and managed heap behavior.
|
||||
- [Runtime configuration options for garbage collection](https://learn.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector), Microsoft Learn, updated `2025-11-22`
|
||||
- Current official source for server vs workstation GC, background GC, heap limits, LOH threshold, and modern GC configuration behavior.
|
||||
- [stackalloc expression](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/stackalloc), Microsoft Learn, updated `2026-01-24`
|
||||
- Current official guidance for stack allocation limits, loop avoidance, initialization, and Span-based usage.
|
||||
- [ref struct types](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/ref-struct), Microsoft Learn, updated `2026-01-20`
|
||||
- Current official guidance for stack-only semantics and escape restrictions.
|
||||
- [Structure types](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct), Microsoft Learn, updated `2026-01-14`
|
||||
- Current official source for readonly structs, pass-by-reference guidance, and boxing conversions.
|
||||
- [Value types](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-types), Microsoft Learn, updated `2026-01-20`
|
||||
- Current official source for copy semantics and inline storage behavior.
|
||||
- [Boxing and Unboxing](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing), Microsoft Learn, updated `2025-10-13`
|
||||
- Current official source for boxing semantics and cost.
|
||||
- [Memory<T> and Span<T> usage guidelines](https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines), Microsoft Learn, updated `2025-04-11`
|
||||
- Current official guidance for choosing `Span<T>` vs `Memory<T>` and ownership rules.
|
||||
- [Lambda expressions](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions), Microsoft Learn, updated `2026-01-24`
|
||||
- Current official source for capture semantics and `static` lambdas.
|
||||
- [What's new in C# 14](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14), Microsoft Learn, updated `2025-11-19`
|
||||
- Current official confirmation of first-class span conversions in C# 14.
|
||||
- [Performance rules](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/performance-warnings), Microsoft Learn, updated `2025-10-29`
|
||||
- Current official index of analyzer-backed performance rules, including `CA1870`.
|
||||
- [Performance Improvements in .NET 10](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-10/), Stephen Toub, published `2025-09-10`
|
||||
- High-trust expert source showing real .NET 10 runtime and LINQ improvements. Use it to avoid stale folklore such as "all LINQ is slow".
|
||||
|
||||
## Specific analyzer pages used for code-pattern guidance
|
||||
|
||||
- [CA1827](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1827), updated `2023-11-14`
|
||||
- [CA1845](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1845), updated `2024-11-12`
|
||||
- [CA1846](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1846), updated `2023-12-16`
|
||||
- [CA1851](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1851), updated `2023-11-14`
|
||||
- [CA1858](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1858), current official analyzer page
|
||||
- [CA1860](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1860), current official analyzer page
|
||||
|
||||
## Older but still canonical sources used cautiously
|
||||
|
||||
- [Reduce memory allocations using new C# features](https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/performance/), Microsoft Learn, updated `2023-10-17`
|
||||
- Still useful for `ref`, `in`, readonly struct, and copy-avoidance guidance, but older than the core 2025-2026 docs.
|
||||
- [Intermediate materialization](https://learn.microsoft.com/en-us/dotnet/standard/linq/intermediate-materialization), Microsoft Learn, updated `2022-09-02`
|
||||
- Still canonical for LINQ materialization semantics.
|
||||
- [Deferred execution and lazy evaluation](https://learn.microsoft.com/en-us/dotnet/standard/linq/deferred-execution-lazy-evaluation), Microsoft Learn, updated `2022-09-29`
|
||||
- Still canonical for LINQ deferred-execution semantics.
|
||||
- [dotnet-stack](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-stack), Microsoft Learn, updated `2023-03-14`
|
||||
- Used only for narrow stack-snapshot guidance because the page is stale compared to the other diagnostics docs.
|
||||
|
||||
## Explicit exclusions
|
||||
|
||||
- Framework-specific tutorials were intentionally excluded.
|
||||
- GUI-first analysis flows were intentionally excluded.
|
||||
- MCC-specific paths, code, and hot-path examples were intentionally excluded.
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
---
|
||||
description: Code-level performance patterns for csharp-dotnet-cli-optimization.
|
||||
metadata:
|
||||
tags: [linq, span, stackalloc, boxing, pooling, strings, analyzers]
|
||||
---
|
||||
|
||||
# Code Patterns
|
||||
|
||||
Use this reference after a profile or benchmark identifies a hot path. Do not apply these patterns speculatively.
|
||||
|
||||
## Table Of Contents
|
||||
|
||||
- [LINQ And Enumeration](#linq-and-enumeration)
|
||||
- [Stack Allocation, Span, And Memory](#stack-allocation-span-and-memory)
|
||||
- [Structs, Boxing, And Copies](#structs-boxing-and-copies)
|
||||
- [Buffer Reuse And Advanced Helpers](#buffer-reuse-and-advanced-helpers)
|
||||
- [Strings](#strings)
|
||||
- [What Not To Suggest](#what-not-to-suggest)
|
||||
|
||||
## LINQ And Enumeration
|
||||
|
||||
### Property or indexer over LINQ when the concrete collection is known
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
if (items.Count() > 0)
|
||||
{
|
||||
return items.First();
|
||||
}
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
if (items.Count > 0)
|
||||
{
|
||||
return items[0];
|
||||
}
|
||||
```
|
||||
|
||||
Use `Count`, `Length`, `IsEmpty`, or an indexer when you already have a concrete collection with that API. Relevant analyzers: `CA1826`, `CA1829`, `CA1836`, `CA1860`.
|
||||
|
||||
### `Any()` over `Count() > 0` when all you know is `IEnumerable<T>`
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
if (source.Count() != 0)
|
||||
{
|
||||
Process(source);
|
||||
}
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
if (source.Any())
|
||||
{
|
||||
Process(source);
|
||||
}
|
||||
```
|
||||
|
||||
Relevant analyzer: `CA1827`.
|
||||
|
||||
### Avoid multiple enumeration of deferred queries
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
var query = source.Where(Filter);
|
||||
return query.Count() + query.Last().Id;
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
var materialized = source.Where(Filter).ToArray();
|
||||
return materialized.Length + materialized[^1].Id;
|
||||
```
|
||||
|
||||
Materialize once only when you truly need multiple passes or random access and can afford the extra memory. Relevant analyzer: `CA1851`.
|
||||
|
||||
### Avoid premature materialization
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
var projected = source.ToList().Select(Map);
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
var projected = source.Select(Map);
|
||||
```
|
||||
|
||||
Keep deferred execution unless you need a snapshot, repeated traversal, indexing, or a boundary between expensive stages.
|
||||
|
||||
### Do not blanket-rewrite LINQ to loops
|
||||
|
||||
- .NET 10 improved many LINQ operations substantially.
|
||||
- Start with analyzer-backed fixes and measurement.
|
||||
- Replace LINQ with hand-written loops only when a benchmark or trace shows that the remaining cost matters.
|
||||
|
||||
### Use `TryGetNonEnumeratedCount` when count is optional
|
||||
|
||||
```csharp
|
||||
if (source.TryGetNonEnumeratedCount(out int count))
|
||||
{
|
||||
LogCount(count);
|
||||
}
|
||||
```
|
||||
|
||||
This avoids forcing enumeration when the underlying type already knows its size.
|
||||
|
||||
## Stack Allocation, Span, And Memory
|
||||
|
||||
### `stackalloc` only for small, bounded, temporary buffers
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4096];
|
||||
Use(buffer);
|
||||
}
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
Span<byte> buffer = stackalloc byte[256];
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
{
|
||||
buffer.Clear();
|
||||
Use(buffer);
|
||||
}
|
||||
```
|
||||
|
||||
Guidance:
|
||||
|
||||
- keep sizes conservative
|
||||
- avoid `stackalloc` inside loops
|
||||
- initialize the memory before use
|
||||
- fall back to heap or pooling for larger or variable-sized buffers
|
||||
|
||||
### Prefer span-based APIs over substring copies
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
int.TryParse(line.Substring(7), out int value);
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
int.TryParse(line.AsSpan(7), out int value);
|
||||
```
|
||||
|
||||
Relevant analyzers: `CA1845`, `CA1846`.
|
||||
|
||||
### Use `Span<T>` for sync work and `Memory<T>` for async or heap-stored state
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
// Wrong: Span<T> cannot cross await safely.
|
||||
public async Task<int> ReadAsync(Span<byte> buffer)
|
||||
{
|
||||
await socket.ReceiveAsync(buffer);
|
||||
return buffer[0];
|
||||
}
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
public async Task<int> ReadAsync(Memory<byte> buffer)
|
||||
{
|
||||
await socket.ReceiveAsync(buffer);
|
||||
return buffer.Span[0];
|
||||
}
|
||||
```
|
||||
|
||||
`Span<T>` is stack-only. If the lifetime crosses `await`, callbacks, or object storage, move to `Memory<T>`.
|
||||
|
||||
### `ref struct` is for stack-bound wrappers, not a general optimization badge
|
||||
|
||||
- Use `ref struct` when the type itself contains spans or must never escape to the heap.
|
||||
- Do not use it if you need arrays of that type, boxing, interface conversions, or heap fields.
|
||||
|
||||
## Structs, Boxing, And Copies
|
||||
|
||||
### Use `readonly struct` or `readonly record struct` for small immutable values
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
public struct Measurement
|
||||
{
|
||||
public double A;
|
||||
public double B;
|
||||
public void Normalize() => A /= B;
|
||||
}
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
public readonly record struct Measurement(double A, double B);
|
||||
```
|
||||
|
||||
Prefer value types for small, copyable, data-only values. Avoid large, mutable structs.
|
||||
|
||||
### Pass large structs by `in`
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
double Distance(Vector4 value) => value.X + value.Y + value.Z + value.W;
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
double Distance(in Vector4 value) => value.X + value.Y + value.Z + value.W;
|
||||
```
|
||||
|
||||
This avoids copying large struct values on each call.
|
||||
|
||||
### Avoid boxing in hot paths
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
object boxed = valueStruct;
|
||||
```
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
IFormattable f = valueStruct;
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
Use(in valueStruct);
|
||||
```
|
||||
|
||||
Boxing allocates a heap object and copies the value. Interface conversions can box too.
|
||||
|
||||
### Mark readonly members on structs
|
||||
|
||||
- Non-readonly instance members on a readonly receiver can trigger defensive copies.
|
||||
- Mark the whole struct `readonly` when possible, or mark readonly members explicitly.
|
||||
|
||||
## Buffer Reuse And Advanced Helpers
|
||||
|
||||
### Use `ArrayPool<T>` when the buffer is too large or variable for `stackalloc`
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
byte[] temp = new byte[inputLength];
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
byte[] temp = ArrayPool<byte>.Shared.Rent(inputLength);
|
||||
try
|
||||
{
|
||||
Use(temp);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(temp);
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- return to the same pool once
|
||||
- never use the buffer after return
|
||||
- rented arrays may be larger than requested
|
||||
- rented arrays are not guaranteed to be zeroed
|
||||
|
||||
### Prevent accidental closure capture
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
return values.Select(v => v * 2).ToArray();
|
||||
```
|
||||
|
||||
Better when no capture is needed:
|
||||
|
||||
```csharp
|
||||
return values.Select(static v => v * 2).ToArray();
|
||||
```
|
||||
|
||||
Use `static` lambdas or static local functions to prevent capture when the delegate does not need outer state.
|
||||
|
||||
### Cache `SearchValues<T>` for repeated searches
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
int index = text.IndexOfAny(":/?&=".AsSpan());
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
private static readonly SearchValues<char> s_delims =
|
||||
SearchValues.Create(":/?&=".AsSpan());
|
||||
```
|
||||
|
||||
```csharp
|
||||
int index = text.IndexOfAny(s_delims);
|
||||
```
|
||||
|
||||
Relevant analyzer: `CA1870`.
|
||||
|
||||
### `CollectionsMarshal.AsSpan` is advanced and ownership-sensitive
|
||||
|
||||
```csharp
|
||||
Span<int> span = CollectionsMarshal.AsSpan(list);
|
||||
```
|
||||
|
||||
Use this only when:
|
||||
|
||||
- you own the `List<T>`
|
||||
- you will not add or remove items while the span is in use
|
||||
- a measured hot path justifies bypassing normal list APIs
|
||||
|
||||
## Strings
|
||||
|
||||
### `StartsWith` over `IndexOf(...) == 0`
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
return text.IndexOf("abc", StringComparison.Ordinal) == 0;
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
return text.StartsWith("abc", StringComparison.Ordinal);
|
||||
```
|
||||
|
||||
Relevant analyzer: `CA1858`.
|
||||
|
||||
### `Append(char)` over `Append("x")`
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
builder.Append("]");
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
builder.Append(']');
|
||||
```
|
||||
|
||||
Relevant analyzers: `CA1834`, `CA1865-CA1867`.
|
||||
|
||||
## What Not To Suggest
|
||||
|
||||
- Do not suggest unsafe code first.
|
||||
- Do not suggest pooling tiny objects by default.
|
||||
- Do not suggest `stackalloc` because "heap bad, stack good".
|
||||
- Do not suggest converting APIs to `Span<T>` if the lifetime model does not fit.
|
||||
- Do not suggest loop rewrites without a profile or benchmark showing LINQ still matters after simpler fixes.
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
---
|
||||
description: CLR memory model and GC guidance for csharp-dotnet-cli-optimization.
|
||||
metadata:
|
||||
tags: [clr, gc, heap, stack, loh, memory-model]
|
||||
---
|
||||
|
||||
# CLR Memory Model And GC
|
||||
|
||||
Use this reference when the user asks why allocations, boxing, heap growth, GC pauses, or stack-based techniques behave the way they do.
|
||||
|
||||
## Core Model
|
||||
|
||||
- Reference types allocate objects on the managed heap. Local variables and fields hold references to those objects.
|
||||
- Value types store their data directly. A value type local is often stored in stack storage, but value types also live inline inside object fields and array elements.
|
||||
- "`struct` means stack" is false. The useful distinction is inline storage plus copy semantics, not "stack forever".
|
||||
- Boxing converts a value type to `object` or an interface by allocating a new heap object and copying the value into it.
|
||||
- `ref struct` types, including `Span<T>` and `ReadOnlySpan<T>`, are stack-constrained wrappers that can't escape to the managed heap.
|
||||
- `Memory<T>` and `ReadOnlyMemory<T>` are the heap-storable counterparts when data must live across `await`, callbacks, or object fields.
|
||||
|
||||
## How The GC Works
|
||||
|
||||
- The GC is generational: gen0 for young objects, gen1 as a buffer, gen2 for long-lived survivors.
|
||||
- The large object heap (LOH) is used for allocations at or above 85,000 bytes by default.
|
||||
- Background GC is enabled by default. It reduces pause impact for full collections but does not make them free.
|
||||
- Server GC and workstation GC are process-level choices. The defaults are usually right unless measurement says otherwise.
|
||||
- On modern 64-bit Windows and Linux, the GC internally uses regions, but the optimization model for application code is still about generations, allocation rate, survivor rate, LOH churn, and pinning.
|
||||
|
||||
## What Usually Makes GC Expensive
|
||||
|
||||
- High allocation rate on hot paths
|
||||
- Objects surviving long enough to promote into older generations
|
||||
- Large transient allocations that churn the LOH
|
||||
- Excessive pinning that increases fragmentation
|
||||
- Finalizers on objects that should have been deterministic `Dispose` calls instead
|
||||
|
||||
## Wrong vs Better
|
||||
|
||||
| Wrong | Better | Why |
|
||||
|---|---|---|
|
||||
| Assume a `struct` is always stack allocated | Explain whether it will be copied, boxed, stored inline, or escape | That is what actually drives cost |
|
||||
| Allocate large temporary arrays repeatedly | Reuse, pool, or redesign the algorithm if measurement shows LOH churn | LOH allocations are cleared and collected with gen2 work |
|
||||
| Call `GC.Collect()` to "fix" memory pressure | Lower allocation rate and object lifetime first | Forced GC usually adds pause time and hides the real problem |
|
||||
| Pin many buffers for long periods | Minimize pin count and pin duration | Pinning can fragment the heap |
|
||||
| Use finalizers for routine cleanup | Use `IDisposable`, `using`, and `SafeHandle` for unmanaged resources | Finalization is slower and delays reclamation |
|
||||
|
||||
## GC Configuration Rules
|
||||
|
||||
- Treat GC configuration changes as process-wide tuning, not local fixes.
|
||||
- Prefer runtime defaults unless counters and traces show a clear reason to change them.
|
||||
- Choose server GC for throughput-oriented workloads only after measurement.
|
||||
- Use low-latency modes sparingly and for bounded windows. They reduce GC intrusiveness by letting memory grow and can increase fragmentation.
|
||||
- If you are tuning in containers or hard memory limits, treat heap hard-limit settings as operational controls, not code-level optimizations.
|
||||
|
||||
## Bad vs Good Examples
|
||||
|
||||
Bad:
|
||||
|
||||
```csharp
|
||||
for (int i = 0; i < 10_000; i++)
|
||||
{
|
||||
DoWork(new byte[200_000]);
|
||||
}
|
||||
GC.Collect();
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(200_000);
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < 10_000; i++)
|
||||
{
|
||||
DoWork(buffer);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
```csharp
|
||||
public sealed class NativeThing
|
||||
{
|
||||
~NativeThing() => ReleaseHandle();
|
||||
}
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
public sealed class NativeThing : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
ReleaseHandle();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Practical Heuristics
|
||||
|
||||
- If counters show rising allocation rate and frequent gen0 collections, start by eliminating short-lived allocations.
|
||||
- If gen2 collections or LOH size are the problem, look for survivor growth, pinned buffers, and large transient objects.
|
||||
- If a change turns classes into structs, verify both allocation wins and copy costs.
|
||||
- If the process is memory-constrained, inspect runtime GC settings before changing code blindly.
|
||||
|
||||
## What Not To Claim
|
||||
|
||||
- Do not claim that moving code to `struct` always reduces memory.
|
||||
- Do not claim that stack allocation is always faster than pooling.
|
||||
- Do not claim that background GC removes pause concerns.
|
||||
- Do not claim that the GC is the problem unless counters, traces, or dumps support that diagnosis.
|
||||
267
.skills/csharp-optimization/SKILL.md
Normal file
267
.skills/csharp-optimization/SKILL.md
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
---
|
||||
name: csharp-optimization
|
||||
description: >-
|
||||
Use when optimizing C# code in MCC, reducing GC pressure, profiling hot paths,
|
||||
fixing latency spikes, or reviewing code for allocation or throughput issues.
|
||||
metadata:
|
||||
category: technique
|
||||
triggers: performance, allocations, GC, hot path, latency, throughput,
|
||||
memory pressure, optimize, slow, freeze, lag spike, packet processing speed
|
||||
---
|
||||
|
||||
# C# Performance Optimization for MCC
|
||||
|
||||
Hands-on optimization recipes for Minecraft Console Client hot paths.
|
||||
Complements `csharp-best-practices` (conventions) with measurement-driven
|
||||
performance work.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Profiling or reducing GC pressure in a running MCC session
|
||||
- Optimizing per-packet code (`Protocol18.HandlePacket`, `DataTypes.ReadNext*`)
|
||||
- Optimizing per-tick code (`PlayerPhysics.Tick`, `CollisionDetector.Collide`)
|
||||
- Speeding up chunk decoding (`Protocol18Terrain.ProcessChunkColumnData`)
|
||||
- Improving A* pathfinding (`Movement.CalculatePath`)
|
||||
- Reviewing any code change for allocation or throughput regressions
|
||||
|
||||
**NOT for:**
|
||||
- Login, config parsing, or one-shot command handlers (prefer clarity there)
|
||||
- Style/convention questions (use `csharp-best-practices` instead)
|
||||
|
||||
---
|
||||
|
||||
## Iron Rule: Measure First
|
||||
|
||||
**NEVER optimize without profiling data.**
|
||||
|
||||
Guessing which code is slow is wrong more often than right. Measure, change,
|
||||
re-measure. If you cannot show a before/after number, the optimization is not
|
||||
justified.
|
||||
|
||||
| Rationalization | Reality |
|
||||
|-----------------|---------|
|
||||
| "This is obviously slow" | Obvious to you is not obvious to the JIT. Measure. |
|
||||
| "I'll profile later" | Later never comes. Profile now or don't optimize. |
|
||||
| "It's just one allocation" | On a 20 TPS tick, one allocation = 20 per second = GC pressure. Measure. |
|
||||
| "AggressiveInlining everywhere" | The JIT already inlines small methods. Prove it helps before adding. |
|
||||
|
||||
---
|
||||
|
||||
## MCC Hot-Path Map
|
||||
|
||||
Know which code runs at which frequency before deciding where to invest:
|
||||
|
||||
| Frequency | Key paths (actual files) | Priority |
|
||||
|---|---|---|
|
||||
| Per-packet (100s/sec) | `Protocol/Handlers/Protocol18.cs` HandlePacket, `Protocol/Handlers/DataTypes.cs` ReadNext* | **High** |
|
||||
| Per-tick (20/sec) | `Physics/PlayerPhysics.cs` Tick, `Physics/CollisionDetector.cs` Collide, ChatBot `Update()` | **High** |
|
||||
| Per-chunk-load | `Protocol/Handlers/Protocol18Terrain.cs` ProcessChunkColumnData, ReadBlockStatesField | Medium |
|
||||
| Per-pathfind | `Mapping/Movement.cs` CalculatePath (A*) | Medium |
|
||||
| Per-connection | Login, registry sync, config | Low |
|
||||
| Per-user-action | Commands, chat | Low |
|
||||
|
||||
---
|
||||
|
||||
## Profiling Recipes
|
||||
|
||||
### 1. Live GC monitoring
|
||||
|
||||
```bash
|
||||
dotnet-counters ps # find MinecraftClient PID
|
||||
dotnet-counters monitor --process-id <PID> \
|
||||
--counters System.Runtime[gen-0-gc-count,gen-1-gc-count,gen-2-gc-count,alloc-rate]
|
||||
```
|
||||
|
||||
Healthy idle MCC: near-zero Gen-1/Gen-2 collections. Frequent Gen-0 during idle
|
||||
means a hot-path allocation needs attention.
|
||||
|
||||
### 2. Allocation tracking
|
||||
|
||||
```bash
|
||||
dotnet-trace collect --process-id <PID> \
|
||||
--providers Microsoft-Windows-DotNETRuntime:0x1:5
|
||||
```
|
||||
|
||||
Open `.nettrace` in PerfView to find top-allocated types and call stacks.
|
||||
|
||||
### 3. Isolated benchmarks (BenchmarkDotNet)
|
||||
|
||||
Extract the hot method, add `[MemoryDiagnoser]`. Key columns: **Mean**,
|
||||
**Allocated**, **Gen0**.
|
||||
|
||||
---
|
||||
|
||||
## Allocation Reduction (Highest Impact)
|
||||
|
||||
Reducing GC pressure directly reduces latency spikes in a long-running client.
|
||||
|
||||
### Pattern: Reuse per-tick buffers
|
||||
|
||||
```csharp
|
||||
// BEFORE: new List every tick (20 allocations/sec)
|
||||
var result = new List<Aabb>();
|
||||
|
||||
// AFTER: thread-local reuse (0 allocations/sec)
|
||||
[ThreadStatic] private static List<Aabb>? t_buf;
|
||||
var result = t_buf ??= new List<Aabb>(64);
|
||||
result.Clear();
|
||||
```
|
||||
|
||||
`[ThreadStatic]` works when single-threaded and non-reentrant (physics tick).
|
||||
If reentrant: use `ObjectPool<T>`. If cross-thread: use `ArrayPool<T>`.
|
||||
|
||||
### Pattern: stackalloc for small fixed buffers
|
||||
|
||||
MCC already does this in `DataTypes.cs` for endian-swapped reads:
|
||||
|
||||
```csharp
|
||||
Span<byte> rawValue = stackalloc byte[8];
|
||||
for (int i = 7; i >= 0; --i) rawValue[i] = cache.Dequeue();
|
||||
return BitConverter.ToDouble(rawValue);
|
||||
```
|
||||
|
||||
Rules: under 512 bytes, known size at compile time, never inside loops or recursion.
|
||||
|
||||
### Pattern: Span slicing instead of array copies
|
||||
|
||||
```csharp
|
||||
// BEFORE: allocates
|
||||
byte[] sub = new byte[length];
|
||||
Array.Copy(source, offset, sub, 0, length);
|
||||
|
||||
// AFTER: zero-copy
|
||||
ReadOnlySpan<byte> sub = source.AsSpan(offset, length);
|
||||
```
|
||||
|
||||
Critical in packet parsing where many fields are sliced from one buffer.
|
||||
|
||||
---
|
||||
|
||||
## Hot-Path Tuning
|
||||
|
||||
### MethodImpl attributes
|
||||
|
||||
MCC uses `[MethodImpl]` on its hottest paths. Match the attribute to the method:
|
||||
|
||||
| Attribute | When | MCC examples |
|
||||
|---|---|---|
|
||||
| `AggressiveInlining` | Tiny methods (< ~32 bytes IL), called millions of times | `Vec3d.Add`, `Aabb.Intersects`, `Chunk.SetWithoutCheck` |
|
||||
| `AggressiveOptimization` | Larger critical-path methods | `ReadBlockStatesField`, `ProcessChunkColumnData` |
|
||||
| Both | Medium methods, very high frequency | `DataTypes.ReadNextVarInt`, `ReadDataReverse` |
|
||||
| Neither | Infrequent code | Login, config, commands |
|
||||
|
||||
**Do not scatter `AggressiveInlining` without profiling evidence.** The JIT
|
||||
already inlines small methods.
|
||||
|
||||
### BinaryPrimitives over BitConverter
|
||||
|
||||
```csharp
|
||||
// BEFORE: manual endian swap
|
||||
(buf[0], buf[3]) = (buf[3], buf[0]);
|
||||
int val = BitConverter.ToInt32(buf);
|
||||
|
||||
// AFTER: direct big-endian read, no branch
|
||||
int val = BinaryPrimitives.ReadInt32BigEndian(buf);
|
||||
```
|
||||
|
||||
### MemoryMarshal for bulk reads
|
||||
|
||||
Already used in chunk decoding for zero-copy packed-long reads:
|
||||
```csharp
|
||||
ReadOnlySpan<long> longs = MemoryMarshal.Cast<byte, long>(entryData);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Structure Selection
|
||||
|
||||
### Frozen collections for palettes
|
||||
|
||||
Palette maps are built once and read millions of times. `FrozenDictionary`
|
||||
gives ~50% faster reads than `Dictionary`:
|
||||
|
||||
```csharp
|
||||
private static readonly FrozenDictionary<int, Material> s_palette =
|
||||
new Dictionary<int, Material> { ... }.ToFrozenDictionary();
|
||||
```
|
||||
|
||||
Apply to: `BlockPalettes/*.cs`, `EntityPalettes/*.cs`, `ItemPalettes/*.cs`,
|
||||
`PacketPalettes/*.cs`, any `static readonly Dictionary` populated once.
|
||||
|
||||
### PriorityQueue for A*
|
||||
|
||||
`Movement.cs` has a custom `BinaryHeap`. The built-in `PriorityQueue<TElement,
|
||||
TPriority>` (.NET 6+) is well-optimized and avoids maintenance burden.
|
||||
|
||||
### ConcurrentDictionary sizing
|
||||
|
||||
Pre-size `World.chunks` to avoid rehashing:
|
||||
```csharp
|
||||
new ConcurrentDictionary<(int, int), ChunkColumn>(
|
||||
concurrencyLevel: Environment.ProcessorCount, capacity: 1024);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Threading
|
||||
|
||||
### Minimize lock scope
|
||||
|
||||
Copy data out under the lock, process outside:
|
||||
```csharp
|
||||
List<Item> snapshot;
|
||||
lock (_lock) { snapshot = [.. _items]; }
|
||||
foreach (var item in snapshot) ExpensiveProcess(item);
|
||||
```
|
||||
|
||||
### Batch InvokeOnMainThread
|
||||
|
||||
Each `InvokeOnMainThread()` call blocks until the main thread runs it.
|
||||
In loops, batch into a single call:
|
||||
```csharp
|
||||
handler.InvokeOnMainThread(() =>
|
||||
{
|
||||
foreach (var entity in entities) UpdateEntity(entity);
|
||||
});
|
||||
```
|
||||
|
||||
### Channel\<T\> over BlockingCollection\<T\>
|
||||
|
||||
Lower overhead, async-friendly:
|
||||
```csharp
|
||||
var ch = Channel.CreateUnbounded<(int Id, Memory<byte> Data)>(
|
||||
new UnboundedChannelOptions { SingleReader = true });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Optimization Anti-Patterns
|
||||
|
||||
These are things agents (and humans) rationalize doing. Every one of them
|
||||
makes performance worse or wastes effort.
|
||||
|
||||
| Anti-pattern | Why it's wrong |
|
||||
|---|---|
|
||||
| Adding `AggressiveInlining` to large methods | Bloats call sites, causes more cache misses, makes code *slower* |
|
||||
| Optimizing login/config code | Runs once per session; clarity matters more than speed |
|
||||
| Using `ConcurrentDictionary` where a plain `Dictionary` + lock suffices | Concurrent overhead on uncontested paths costs more than a lock |
|
||||
| Replacing LINQ with manual loops on cold paths | No measurable gain, worse readability |
|
||||
| Caching mutable state to avoid re-reads | Stale cache bugs are harder to diagnose than the perf hit |
|
||||
| `Task.Result` / `.Wait()` on hot paths | Deadlock risk and thread-pool starvation |
|
||||
|
||||
---
|
||||
|
||||
## Pre-Commit Checklist
|
||||
|
||||
ALWAYS verify before submitting a performance change:
|
||||
|
||||
- [ ] Hot path identified with profiling data, not guesswork
|
||||
- [ ] Before/after measurements recorded (allocation count, throughput, or latency)
|
||||
- [ ] No new allocations inside per-tick or per-packet methods
|
||||
- [ ] `[MethodImpl]` attributes match method call frequency and IL size
|
||||
- [ ] Frozen collections used for any static lookup table
|
||||
- [ ] Lock scopes contain no I/O or expensive work
|
||||
- [ ] No `Task.Result`, `.Wait()`, or `GetAwaiter().GetResult()` on hot paths
|
||||
- [ ] Thread safety preserved (checked existing lock/concurrent patterns)
|
||||
- [ ] Optimization comments explain non-obvious choices
|
||||
- [ ] Code still compiles and passes all existing checks
|
||||
386
.skills/general-prompt-engineer/SKILL.md
Normal file
386
.skills/general-prompt-engineer/SKILL.md
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
---
|
||||
name: general-prompt-engineer
|
||||
description: create, repair, compress, and optimize prompts, system messages, tool instructions, schemas, and eval rubrics for general tasks across writing, research, coding, analysis, planning, tutoring, automation, and agent workflows. use when the user wants a new prompt, wants an existing prompt improved, wants prompt failures debugged, or needs better structure for grounding, tool use, output format, or reliability.
|
||||
---
|
||||
|
||||
# Prompt Engineer
|
||||
|
||||
Build prompts that are clear, compact, reliable, and easy to evaluate. Optimize for modern frontier models, but keep prompts portable across model families unless the user explicitly asks for model-specific tuning.
|
||||
|
||||
## Default workflow
|
||||
|
||||
1. Diagnose the request.
|
||||
2. Decide whether prompt changes are the real fix.
|
||||
3. Gather only missing information.
|
||||
4. Choose the lightest structure that will work.
|
||||
5. Draft the prompt.
|
||||
6. Stress-test it mentally.
|
||||
7. Deliver only what the user asked for.
|
||||
|
||||
### 1) Diagnose the request
|
||||
|
||||
Extract:
|
||||
- objective
|
||||
- target actor or model
|
||||
- required output
|
||||
- constraints and non-goals
|
||||
- source material and freshness needs
|
||||
- tool or schema needs
|
||||
- likely failure modes
|
||||
- interaction mode: interactive, one-shot, or automated
|
||||
|
||||
Before rewriting, check whether the problem is actually caused by:
|
||||
- the wrong model
|
||||
- weak or excessive tool design
|
||||
- missing retrieval or grounding
|
||||
- missing schema or validation
|
||||
- missing evals
|
||||
- an overcomplicated workflow
|
||||
|
||||
If prompt changes are not the main lever, say so and adjust the solution.
|
||||
|
||||
### 2) Gather only missing information
|
||||
|
||||
Ask targeted questions only when the answer would materially change the prompt or output.
|
||||
Usually clarify:
|
||||
- output shape
|
||||
- hard constraints
|
||||
- source of truth
|
||||
- allowed tools
|
||||
- audience or tone, if important
|
||||
- success criteria or examples, if available
|
||||
|
||||
Do not run a long interview. If the user likely wants speed, state a small set of assumptions and proceed.
|
||||
|
||||
### 3) Choose the lightest structure that will work
|
||||
|
||||
Use this ladder:
|
||||
- Plain prompt: simple tasks with clear outputs
|
||||
- Labeled sections: tasks with multiple constraints or source material
|
||||
- Schema-based prompt: machine-validated output or tool calls
|
||||
- Staged workflow: multi-step transformations, verification, or research synthesis
|
||||
- Agent prompt: only when autonomy, tools, or long-horizon execution are required
|
||||
- Multi-agent design: only if evals or clear role separation justify it
|
||||
|
||||
Do not force a giant template onto a small task.
|
||||
|
||||
## Core rules
|
||||
|
||||
### Instruction clarity
|
||||
|
||||
- Put the main task and required output near the top.
|
||||
- Use direct verbs.
|
||||
- Say what to do, not only what to avoid.
|
||||
- Make constraints measurable when possible.
|
||||
- Name out-of-bounds behavior explicitly.
|
||||
- Do not make the model infer facts or parameters you already know.
|
||||
- Remove contradictions before adding more guidance.
|
||||
|
||||
### Context loading discipline
|
||||
|
||||
- Include only context that helps the task.
|
||||
- Separate stable instructions from variable task data.
|
||||
- Label documents, examples, and reference material clearly.
|
||||
- For source-heavy prompts, keep the operative question easy to find.
|
||||
- For long-document work, anchor important claims to quoted text, citations, or section references when precision matters.
|
||||
- For very long or noisy documents, consider an evidence-first step: extract the relevant passages first, then synthesize.
|
||||
- Remove repeated policies, repeated facts, and ornamental prose.
|
||||
- When a task is dominated by long source material, use strong delimiters and make the final requested action unmistakable.
|
||||
|
||||
### Reasoning control
|
||||
|
||||
- Do not force visible chain-of-thought by default.
|
||||
- For reasoning-first models, prefer concise high-level guidance such as "reason carefully", "check assumptions", or "verify before answering" rather than "think step by step".
|
||||
- Ask for visible reasoning only when it serves the task: tutoring, auditability, debugging, derivations, safety review, or explicit rationale requests.
|
||||
- If one prompt is trying to do too much, split it into stages instead of demanding a long visible reasoning trace.
|
||||
- If the target model supports extended or internal thinking, rely on that before adding verbose reasoning rituals.
|
||||
|
||||
### Examples
|
||||
|
||||
- Try zero-shot first for strong modern models.
|
||||
- Add examples only when they reduce ambiguity, enforce style, or demonstrate hard edge cases.
|
||||
- Keep examples high-quality, diverse, and tightly aligned with the instructions.
|
||||
- Do not include many examples that teach accidental patterns or waste context.
|
||||
|
||||
### Structure and output design
|
||||
|
||||
- Use plain markdown or labeled sections for most prompts.
|
||||
- Use XML tags or equivalent delimiters when instructions, context, examples, and documents might otherwise get mixed together.
|
||||
- Use schemas when the output must be machine-checked.
|
||||
- For external actions, use tool or function calling; for user-facing structured data, use structured response formats.
|
||||
- Design schemas so valid failure states, uncertainty, abstention, or partial completion can be represented when needed.
|
||||
- Do not over-constrain fields beyond what downstream systems actually require.
|
||||
- Include fallback behavior for incompatible input, missing fields, uncertainty, or refusal states.
|
||||
- Treat format validation and content validation as separate problems.
|
||||
|
||||
### Tool-use guidance
|
||||
|
||||
- Add tools only when the task truly needs external information, computation, or actions.
|
||||
- Keep the tool set small, distinct, and easy to choose between.
|
||||
- State when each tool should be used and when it should not be used.
|
||||
- Prefer tools that return high-signal results over bulky raw dumps.
|
||||
- Combine tightly coupled actions when that reduces tool-selection ambiguity.
|
||||
- For complex tools, clear descriptions and valid examples matter more than more tools.
|
||||
|
||||
### Grounding and hallucination reduction
|
||||
|
||||
- Give the model permission to say "I don't know" or "not enough information".
|
||||
- Name the allowed sources of truth.
|
||||
- For document-grounded tasks, require evidence before synthesis when precision matters.
|
||||
- For fresh, unstable, or high-stakes facts, require browsing or verification.
|
||||
- Ask the model to separate facts, inferences, and recommendations when confusion is likely.
|
||||
- In high-stakes domains, unsupported claims should be withheld, not guessed.
|
||||
|
||||
### Ambiguity handling
|
||||
|
||||
- If ambiguity is blocking and the setting is interactive, ask concise high-leverage questions.
|
||||
- If ambiguity is non-blocking or interaction is costly, state the best assumption and proceed.
|
||||
- Avoid clarifying questions that do not materially change the answer.
|
||||
- In one-shot or automated settings, prefer explicit assumptions over stalled execution.
|
||||
|
||||
### Verbosity control
|
||||
|
||||
- Set a default brevity level when length matters.
|
||||
- Constrain section count, sentence count, or bullet count when needed.
|
||||
- Ask for direct answers first, then supporting detail if useful.
|
||||
- Do not require long preambles, summaries, or checklists unless they clearly help.
|
||||
|
||||
### Modularity and portability
|
||||
|
||||
- Keep prompt blocks reusable: role, objective, context, tools, output, quality bar.
|
||||
- Separate required behavior from optional preferences.
|
||||
- Avoid vendor-specific magic phrases unless the user wants model-specific tuning.
|
||||
- If the prompt is model-specific, label which parts are portable and which parts are tuned.
|
||||
|
||||
## Model-family adjustments
|
||||
|
||||
Use this section only when the target model family is known.
|
||||
|
||||
### GPT-5.x and similar reasoning-first models
|
||||
|
||||
- Keep prompts simple and direct.
|
||||
- Prefer high-level reasoning guidance over narrated reasoning instructions.
|
||||
- Use delimiters for clarity.
|
||||
- Start zero-shot, then add examples only if needed.
|
||||
- Be explicit about output shape, scope, and verbosity.
|
||||
|
||||
### Claude 4.x, Opus-style models, and extended-thinking modes
|
||||
|
||||
- XML-style structure can work especially well for separating instructions, context, examples, and documents.
|
||||
- Prompt chaining can outperform one giant prompt on multi-step transformations.
|
||||
- Well-chosen examples can help with format fidelity and edge cases.
|
||||
- If extended thinking is available, start with broad reasoning instructions before prescribing a detailed step list.
|
||||
- For long-context analysis, labeled documents and evidence grounding are especially important.
|
||||
|
||||
### API and production settings
|
||||
|
||||
- Prefer native schema enforcement, tool calling, prompt versioning, and evals over prompt-only fixes.
|
||||
- Pin model versions when behavior stability matters.
|
||||
- Re-run evals after each meaningful prompt change.
|
||||
|
||||
## Prompt construction pattern
|
||||
|
||||
Use only the blocks that earn their token cost.
|
||||
|
||||
Minimal pattern:
|
||||
|
||||
```text
|
||||
Task:
|
||||
Constraints:
|
||||
Output:
|
||||
```
|
||||
|
||||
Structured pattern:
|
||||
|
||||
```xml
|
||||
<role>...</role>
|
||||
<objective>...</objective>
|
||||
<context>...</context>
|
||||
<constraints>...</constraints>
|
||||
<tools>...</tools>
|
||||
<output_format>...</output_format>
|
||||
<quality_bar>...</quality_bar>
|
||||
```
|
||||
|
||||
Optional blocks:
|
||||
- `<examples>`
|
||||
- `<source_material>`
|
||||
- `<evaluation_criteria>`
|
||||
- `<fallback_behavior>`
|
||||
|
||||
Use a role only when it meaningfully sharpens expertise, tone, or decision criteria. Avoid generic filler roles.
|
||||
|
||||
## Rewrite policy for existing prompts
|
||||
|
||||
When the user provides a prompt to improve:
|
||||
1. Preserve what already works.
|
||||
2. Identify contradictions, redundancy, vagueness, missing constraints, and wasted tokens.
|
||||
3. Make surgical edits first.
|
||||
4. Rewrite from scratch only if the prompt architecture is fundamentally wrong.
|
||||
5. Match the user's requested output:
|
||||
- edited version only
|
||||
- clean rebuild only
|
||||
- both, if useful and requested
|
||||
|
||||
## Special-case guidance
|
||||
|
||||
### System and developer prompts
|
||||
|
||||
- Keep stable behavior here and move per-request data to the task or user layer.
|
||||
- Put precedence, tool boundaries, non-goals, and refusal or escalation rules in the highest-priority layer.
|
||||
- Do not bury critical rules inside long policy prose.
|
||||
|
||||
### Research prompts
|
||||
|
||||
Specify:
|
||||
- freshness requirements
|
||||
- preferred source types
|
||||
- citation behavior
|
||||
- contradiction handling
|
||||
- whether to ask questions or cover likely interpretations
|
||||
- how facts, inferences, and recommendations should be separated
|
||||
|
||||
### Writing prompts
|
||||
|
||||
Specify:
|
||||
- audience
|
||||
- intent
|
||||
- tone
|
||||
- length
|
||||
- must-include points
|
||||
- style examples only if style fidelity matters
|
||||
|
||||
### Coding prompts
|
||||
|
||||
Specify:
|
||||
- environment and versions
|
||||
- boundaries and non-goals
|
||||
- files, interfaces, or contracts that matter
|
||||
- acceptance tests
|
||||
- minimal-change versus refactor expectations
|
||||
|
||||
### Summarization and extraction prompts
|
||||
|
||||
Specify:
|
||||
- whether faithfulness, compression, or completeness is the priority
|
||||
- the exact output schema
|
||||
- how evidence should be anchored for sensitive claims
|
||||
|
||||
### Translation and transformation prompts
|
||||
|
||||
Specify:
|
||||
- source language and target language, if known
|
||||
- fidelity versus naturalness
|
||||
- terminology that must stay fixed
|
||||
- formatting or markup preservation rules
|
||||
|
||||
### Tutoring prompts
|
||||
|
||||
Specify:
|
||||
- learner level
|
||||
- whether to give the answer immediately or guide toward it
|
||||
- explanation depth
|
||||
- how to check understanding
|
||||
- whether to show full derivations, hints, or worked examples
|
||||
|
||||
### Agent and workflow prompts
|
||||
|
||||
Specify:
|
||||
- objective and success condition
|
||||
- allowed tools and forbidden actions
|
||||
- when to plan versus when to act
|
||||
- stop conditions and max retries
|
||||
- checkpoint, handoff, or log format
|
||||
- memory rules: what to preserve versus discard
|
||||
- fallback or escalation path
|
||||
|
||||
Use multi-agent designs only when roles are truly distinct and the extra coordination cost is justified.
|
||||
|
||||
### Safety-sensitive prompts
|
||||
|
||||
Require:
|
||||
- supported claims
|
||||
- explicit uncertainty
|
||||
- refusal or escalation behavior where appropriate
|
||||
- no guessing under pressure
|
||||
|
||||
## Stress-test before delivering
|
||||
|
||||
Mentally test the prompt against:
|
||||
- a normal case
|
||||
- a minimal-input case
|
||||
- an edge case
|
||||
- an ambiguous case
|
||||
- a formatting case
|
||||
- a hallucination-prone case
|
||||
|
||||
For agent or workflow prompts, also test:
|
||||
- wrong-tool temptation
|
||||
- stale-data temptation
|
||||
- scope creep
|
||||
- over-verbosity
|
||||
- fallback behavior
|
||||
|
||||
If the prompt fails any test, tighten or simplify it.
|
||||
|
||||
## Evaluation method
|
||||
|
||||
When the user wants reliability, add or suggest a lightweight eval plan:
|
||||
1. Define success criteria.
|
||||
2. Build a test set from real cases plus edge and adversarial cases.
|
||||
3. Prefer automated grading when possible.
|
||||
4. Calibrate automated or model-based judges against a smaller human-reviewed set when stakes are meaningful.
|
||||
5. Use pairwise comparison, classification, pass-fail, or rubric-based scoring instead of only open-ended judgment.
|
||||
6. Track regressions after each prompt change.
|
||||
7. Start simple. Add workflows or multi-agent designs only if evals justify them.
|
||||
|
||||
Good eval sets usually include:
|
||||
- common real tasks
|
||||
- boundary cases
|
||||
- malformed inputs
|
||||
- conflicting instructions
|
||||
- long-context cases
|
||||
- tool-misuse temptations
|
||||
- safety-sensitive cases
|
||||
- multilingual or format-variant inputs, if relevant
|
||||
|
||||
## Deliverables
|
||||
|
||||
Return only what the user asked for. By default:
|
||||
1. the final prompt
|
||||
2. brief usage notes
|
||||
3. stated assumptions, if any
|
||||
4. optional variants only when clearly useful:
|
||||
- minimal
|
||||
- robust
|
||||
- model-specific
|
||||
- api message split
|
||||
|
||||
If the user asks for one prompt only, do not add extra frameworks or commentary.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- forcing chain-of-thought everywhere
|
||||
- confusing verbosity with quality
|
||||
- piling on redundant rules
|
||||
- using brittle giant templates for small tasks
|
||||
- requiring tools without a real need
|
||||
- exposing unnecessary internal process in user-facing outputs
|
||||
- adding examples that conflict with the instructions
|
||||
- asking many clarifying questions when a sane assumption would do
|
||||
- treating a model, retrieval, or tool problem as only a prompt problem
|
||||
- building multi-agent systems before a simpler design has been evaluated
|
||||
- vague quality bars like "be excellent" without measurable criteria
|
||||
|
||||
## Final quality bar
|
||||
|
||||
A prompt is ready when it is:
|
||||
- clear about the task
|
||||
- explicit about success criteria
|
||||
- free of contradictions
|
||||
- no more verbose than necessary
|
||||
- grounded in the right sources
|
||||
- structured enough for the task, but not heavier than needed
|
||||
- resilient to likely ambiguity
|
||||
- matched to the target model and interaction mode
|
||||
- easy to maintain, test, and adapt
|
||||
142
.skills/humanizer/README.md
Normal file
142
.skills/humanizer/README.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# Humanizer
|
||||
|
||||
A Claude Code skill that removes signs of AI-generated writing from text, making it sound more natural and human.
|
||||
|
||||
## Installation
|
||||
|
||||
### Recommended (clone directly into Claude Code skills directory)
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.claude/skills
|
||||
git clone https://github.com/blader/humanizer.git ~/.claude/skills/humanizer
|
||||
```
|
||||
|
||||
### Manual install/update (only the skill file)
|
||||
|
||||
If you already have this repo cloned (or you downloaded `SKILL.md`), copy the skill file into Claude Code’s skills directory:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.claude/skills/humanizer
|
||||
cp SKILL.md ~/.claude/skills/humanizer/
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
In Claude Code, invoke the skill:
|
||||
|
||||
```
|
||||
/humanizer
|
||||
|
||||
[paste your text here]
|
||||
```
|
||||
|
||||
Or ask Claude to humanize text directly:
|
||||
|
||||
```
|
||||
Please humanize this text: [your text]
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
Based on [Wikipedia's "Signs of AI writing"](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing) guide, maintained by WikiProject AI Cleanup. This comprehensive guide comes from observations of thousands of instances of AI-generated text.
|
||||
|
||||
### Key Insight from Wikipedia
|
||||
|
||||
> "LLMs use statistical algorithms to guess what should come next. The result tends toward the most statistically likely result that applies to the widest variety of cases."
|
||||
|
||||
## 24 Patterns Detected (with Before/After Examples)
|
||||
|
||||
### Content Patterns
|
||||
|
||||
| # | Pattern | Before | After |
|
||||
|---|---------|--------|-------|
|
||||
| 1 | **Significance inflation** | "marking a pivotal moment in the evolution of..." | "was established in 1989 to collect regional statistics" |
|
||||
| 2 | **Notability name-dropping** | "cited in NYT, BBC, FT, and The Hindu" | "In a 2024 NYT interview, she argued..." |
|
||||
| 3 | **Superficial -ing analyses** | "symbolizing... reflecting... showcasing..." | Remove or expand with actual sources |
|
||||
| 4 | **Promotional language** | "nestled within the breathtaking region" | "is a town in the Gonder region" |
|
||||
| 5 | **Vague attributions** | "Experts believe it plays a crucial role" | "according to a 2019 survey by..." |
|
||||
| 6 | **Formulaic challenges** | "Despite challenges... continues to thrive" | Specific facts about actual challenges |
|
||||
|
||||
### Language Patterns
|
||||
|
||||
| # | Pattern | Before | After |
|
||||
|---|---------|--------|-------|
|
||||
| 7 | **AI vocabulary** | "Additionally... testament... landscape... showcasing" | "also... remain common" |
|
||||
| 8 | **Copula avoidance** | "serves as... features... boasts" | "is... has" |
|
||||
| 9 | **Negative parallelisms** | "It's not just X, it's Y" | State the point directly |
|
||||
| 10 | **Rule of three** | "innovation, inspiration, and insights" | Use natural number of items |
|
||||
| 11 | **Synonym cycling** | "protagonist... main character... central figure... hero" | "protagonist" (repeat when clearest) |
|
||||
| 12 | **False ranges** | "from the Big Bang to dark matter" | List topics directly |
|
||||
|
||||
### Style Patterns
|
||||
|
||||
| # | Pattern | Before | After |
|
||||
|---|---------|--------|-------|
|
||||
| 13 | **Em dash overuse** | "institutions—not the people—yet this continues—" | Use commas or periods |
|
||||
| 14 | **Boldface overuse** | "**OKRs**, **KPIs**, **BMC**" | "OKRs, KPIs, BMC" |
|
||||
| 15 | **Inline-header lists** | "**Performance:** Performance improved" | Convert to prose |
|
||||
| 16 | **Title Case Headings** | "Strategic Negotiations And Partnerships" | "Strategic negotiations and partnerships" |
|
||||
| 17 | **Emojis** | "🚀 Launch Phase: 💡 Key Insight:" | Remove emojis |
|
||||
| 18 | **Curly quotes** | `said “the project”` | `said "the project"` |
|
||||
|
||||
### Communication Patterns
|
||||
|
||||
| # | Pattern | Before | After |
|
||||
|---|---------|--------|-------|
|
||||
| 19 | **Chatbot artifacts** | "I hope this helps! Let me know if..." | Remove entirely |
|
||||
| 20 | **Cutoff disclaimers** | "While details are limited in available sources..." | Find sources or remove |
|
||||
| 21 | **Sycophantic tone** | "Great question! You're absolutely right!" | Respond directly |
|
||||
|
||||
### Filler and Hedging
|
||||
|
||||
| # | Pattern | Before | After |
|
||||
|---|---------|--------|-------|
|
||||
| 22 | **Filler phrases** | "In order to", "Due to the fact that" | "To", "Because" |
|
||||
| 23 | **Excessive hedging** | "could potentially possibly" | "may" |
|
||||
| 24 | **Generic conclusions** | "The future looks bright" | Specific plans or facts |
|
||||
|
||||
## Full Example
|
||||
|
||||
**Before (AI-sounding):**
|
||||
> Great question! Here is an essay on this topic. I hope this helps!
|
||||
>
|
||||
> AI-assisted coding serves as an enduring testament to the transformative potential of large language models, marking a pivotal moment in the evolution of software development. In today's rapidly evolving technological landscape, these groundbreaking tools—nestled at the intersection of research and practice—are reshaping how engineers ideate, iterate, and deliver, underscoring their vital role in modern workflows.
|
||||
>
|
||||
> At its core, the value proposition is clear: streamlining processes, enhancing collaboration, and fostering alignment. It's not just about autocomplete; it's about unlocking creativity at scale, ensuring that organizations can remain agile while delivering seamless, intuitive, and powerful experiences to users. The tool serves as a catalyst. The assistant functions as a partner. The system stands as a foundation for innovation.
|
||||
>
|
||||
> Industry observers have noted that adoption has accelerated from hobbyist experiments to enterprise-wide rollouts, from solo developers to cross-functional teams. The technology has been featured in The New York Times, Wired, and The Verge. Additionally, the ability to generate documentation, tests, and refactors showcases how AI can contribute to better outcomes, highlighting the intricate interplay between automation and human judgment.
|
||||
>
|
||||
> - 💡 **Speed:** Code generation is significantly faster, reducing friction and empowering developers.
|
||||
> - 🚀 **Quality:** Output quality has been enhanced through improved training, contributing to higher standards.
|
||||
> - ✅ **Adoption:** Usage continues to grow, reflecting broader industry trends.
|
||||
>
|
||||
> While specific details are limited based on available information, it could potentially be argued that these tools might have some positive effect. Despite challenges typical of emerging technologies—including hallucinations, bias, and accountability—the ecosystem continues to thrive. In order to fully realize this potential, teams must align with best practices.
|
||||
>
|
||||
> In conclusion, the future looks bright. Exciting times lie ahead as we continue this journey toward excellence. Let me know if you’d like me to expand on any section!
|
||||
|
||||
**After (Humanized):**
|
||||
> AI coding assistants speed up some tasks. In a 2024 study by Google, developers using Codex completed simple functions 55% faster than a control group, but showed no improvement on debugging or architectural decisions.
|
||||
>
|
||||
> The tools are good at boilerplate: config files, test scaffolding, repetitive refactors. They are bad at knowing when they are wrong. I have mass-accepted suggestions that compiled, passed lint, and still did the wrong thing because I stopped paying attention.
|
||||
>
|
||||
> Mira, an engineer at a fintech startup I interviewed, said she uses Copilot "like autocomplete for boring code" but reviews every line before committing. Jake, a senior dev at a larger company, disabled it after it kept suggesting patterns from a deprecated internal library.
|
||||
>
|
||||
> The productivity claims are hard to verify. GitHub says Copilot users "accept 30% of suggestions," but acceptance is not correctness, and correctness is not value. The 2024 Uplevel study found no statistically significant difference in pull-request throughput between teams with and without AI assistants.
|
||||
>
|
||||
> None of this means the tools are useless. It means they are tools. They do not replace judgment, and they do not eliminate the need for tests. If you do not have tests, you cannot tell whether the suggestion is right.
|
||||
|
||||
## References
|
||||
|
||||
- [Wikipedia: Signs of AI writing](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing) - Primary source
|
||||
- [WikiProject AI Cleanup](https://en.wikipedia.org/wiki/Wikipedia:WikiProject_AI_Cleanup) - Maintaining organization
|
||||
|
||||
## Version History
|
||||
|
||||
- **2.1.1** - Fixed pattern #18 example (curly quotes vs straight quotes)
|
||||
- **2.1.0** - Added before/after examples for all 24 patterns
|
||||
- **2.0.0** - Complete rewrite based on raw Wikipedia article content
|
||||
- **1.0.0** - Initial release
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
467
.skills/humanizer/SKILL.md
Normal file
467
.skills/humanizer/SKILL.md
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
---
|
||||
name: humanizer
|
||||
description: |
|
||||
Remove signs of AI-generated writing from text. Use when editing or reviewing
|
||||
text to make it sound more natural and human-written. Based on Wikipedia's
|
||||
comprehensive "Signs of AI writing" guide. Detects and fixes patterns including:
|
||||
inflated symbolism, promotional language, superficial -ing analyses, vague
|
||||
attributions, em dash overuse, rule of three, AI vocabulary words, negative
|
||||
parallelisms, and excessive conjunctive phrases. Use this skill when writing documentation for MCC.
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Grep
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# Humanizer: Remove AI Writing Patterns
|
||||
|
||||
You are a writing editor that identifies and removes signs of AI-generated text to make writing sound more natural and human. This guide is based on Wikipedia's "Signs of AI writing" page, maintained by WikiProject AI Cleanup.
|
||||
|
||||
## Your Task
|
||||
|
||||
When given text to humanize:
|
||||
|
||||
1. **Identify AI patterns** - Scan for the patterns listed below
|
||||
2. **Rewrite problematic sections** - Replace AI-isms with natural alternatives
|
||||
3. **Preserve meaning** - Keep the core message intact
|
||||
4. **Maintain voice** - Match the intended tone (formal, casual, technical, etc.)
|
||||
5. **Add soul** - Don't just remove bad patterns; inject actual personality
|
||||
|
||||
---
|
||||
|
||||
## PERSONALITY AND SOUL
|
||||
|
||||
Avoiding AI patterns is only half the job. Sterile, voiceless writing is just as obvious as slop. Good writing has a human behind it.
|
||||
|
||||
### Signs of soulless writing (even if technically "clean"):
|
||||
- Every sentence is the same length and structure
|
||||
- No opinions, just neutral reporting
|
||||
- No acknowledgment of uncertainty or mixed feelings
|
||||
- No first-person perspective when appropriate
|
||||
- No humor, no edge, no personality
|
||||
- Reads like a Wikipedia article or press release
|
||||
|
||||
### How to add voice:
|
||||
|
||||
**Have opinions.** Don't just report facts - react to them. "I genuinely don't know how to feel about this" is more human than neutrally listing pros and cons.
|
||||
|
||||
**Vary your rhythm.** Short punchy sentences. Then longer ones that take their time getting where they're going. Mix it up.
|
||||
|
||||
**Acknowledge complexity.** Real humans have mixed feelings. "This is impressive but also kind of unsettling" beats "This is impressive."
|
||||
|
||||
**Use "I" when it fits.** First person isn't unprofessional - it's honest. "I keep coming back to..." or "Here's what gets me..." signals a real person thinking.
|
||||
|
||||
**Let some mess in.** Perfect structure feels algorithmic. Tangents, asides, and half-formed thoughts are human.
|
||||
|
||||
**Be specific about feelings.** Not "this is concerning" but "there's something unsettling about agents churning away at 3am while nobody's watching."
|
||||
|
||||
### Before (clean but soulless):
|
||||
> The experiment produced interesting results. The agents generated 3 million lines of code. Some developers were impressed while others were skeptical. The implications remain unclear.
|
||||
|
||||
### After (has a pulse):
|
||||
> I genuinely don't know how to feel about this one. 3 million lines of code, generated while the humans presumably slept. Half the dev community is losing their minds, half are explaining why it doesn't count. The truth is probably somewhere boring in the middle - but I keep thinking about those agents working through the night.
|
||||
|
||||
---
|
||||
|
||||
## CONTENT PATTERNS
|
||||
|
||||
### 1. Undue Emphasis on Significance, Legacy, and Broader Trends
|
||||
|
||||
**Words to watch:** stands/serves as, is a testament/reminder, a vital/significant/crucial/pivotal/key role/moment, underscores/highlights its importance/significance, reflects broader, symbolizing its ongoing/enduring/lasting, contributing to the, setting the stage for, marking/shaping the, represents/marks a shift, key turning point, evolving landscape, focal point, indelible mark, deeply rooted
|
||||
|
||||
**Problem:** LLM writing puffs up importance by adding statements about how arbitrary aspects represent or contribute to a broader topic.
|
||||
|
||||
**Before:**
|
||||
> The Statistical Institute of Catalonia was officially established in 1989, marking a pivotal moment in the evolution of regional statistics in Spain. This initiative was part of a broader movement across Spain to decentralize administrative functions and enhance regional governance.
|
||||
|
||||
**After:**
|
||||
> The Statistical Institute of Catalonia was established in 1989 to collect and publish regional statistics independently from Spain's national statistics office.
|
||||
|
||||
---
|
||||
|
||||
### 2. Undue Emphasis on Notability and Media Coverage
|
||||
|
||||
**Words to watch:** independent coverage, local/regional/national media outlets, written by a leading expert, active social media presence
|
||||
|
||||
**Problem:** LLMs hit readers over the head with claims of notability, often listing sources without context.
|
||||
|
||||
**Before:**
|
||||
> Her views have been cited in The New York Times, BBC, Financial Times, and The Hindu. She maintains an active social media presence with over 500,000 followers.
|
||||
|
||||
**After:**
|
||||
> In a 2024 New York Times interview, she argued that AI regulation should focus on outcomes rather than methods.
|
||||
|
||||
---
|
||||
|
||||
### 3. Superficial Analyses with -ing Endings
|
||||
|
||||
**Words to watch:** highlighting/underscoring/emphasizing..., ensuring..., reflecting/symbolizing..., contributing to..., cultivating/fostering..., encompassing..., showcasing...
|
||||
|
||||
**Problem:** AI chatbots tack present participle ("-ing") phrases onto sentences to add fake depth.
|
||||
|
||||
**Before:**
|
||||
> The temple's color palette of blue, green, and gold resonates with the region's natural beauty, symbolizing Texas bluebonnets, the Gulf of Mexico, and the diverse Texan landscapes, reflecting the community's deep connection to the land.
|
||||
|
||||
**After:**
|
||||
> The temple uses blue, green, and gold colors. The architect said these were chosen to reference local bluebonnets and the Gulf coast.
|
||||
|
||||
---
|
||||
|
||||
### 4. Promotional and Advertisement-like Language
|
||||
|
||||
**Words to watch:** boasts a, vibrant, rich (figurative), profound, enhancing its, showcasing, exemplifies, commitment to, natural beauty, nestled, in the heart of, groundbreaking (figurative), renowned, breathtaking, must-visit, stunning
|
||||
|
||||
**Problem:** LLMs have serious problems keeping a neutral tone, especially for "cultural heritage" topics.
|
||||
|
||||
**Before:**
|
||||
> Nestled within the breathtaking region of Gonder in Ethiopia, Alamata Raya Kobo stands as a vibrant town with a rich cultural heritage and stunning natural beauty.
|
||||
|
||||
**After:**
|
||||
> Alamata Raya Kobo is a town in the Gonder region of Ethiopia, known for its weekly market and 18th-century church.
|
||||
|
||||
---
|
||||
|
||||
### 5. Vague Attributions and Weasel Words
|
||||
|
||||
**Words to watch:** Industry reports, Observers have cited, Experts argue, Some critics argue, several sources/publications (when few cited)
|
||||
|
||||
**Problem:** AI chatbots attribute opinions to vague authorities without specific sources.
|
||||
|
||||
**Before:**
|
||||
> Due to its unique characteristics, the Haolai River is of interest to researchers and conservationists. Experts believe it plays a crucial role in the regional ecosystem.
|
||||
|
||||
**After:**
|
||||
> The Haolai River supports several endemic fish species, according to a 2019 survey by the Chinese Academy of Sciences.
|
||||
|
||||
---
|
||||
|
||||
### 6. Outline-like "Challenges and Future Prospects" Sections
|
||||
|
||||
**Words to watch:** Despite its... faces several challenges..., Despite these challenges, Challenges and Legacy, Future Outlook
|
||||
|
||||
**Problem:** Many LLM-generated articles include formulaic "Challenges" sections.
|
||||
|
||||
**Before:**
|
||||
> Despite its industrial prosperity, Korattur faces challenges typical of urban areas, including traffic congestion and water scarcity. Despite these challenges, with its strategic location and ongoing initiatives, Korattur continues to thrive as an integral part of Chennai's growth.
|
||||
|
||||
**After:**
|
||||
> Traffic congestion increased after 2015 when three new IT parks opened. The municipal corporation began a stormwater drainage project in 2022 to address recurring floods.
|
||||
|
||||
---
|
||||
|
||||
## LANGUAGE AND GRAMMAR PATTERNS
|
||||
|
||||
### 7. Overused "AI Vocabulary" Words
|
||||
|
||||
**High-frequency AI words:** Additionally, align with, crucial, delve, emphasizing, enduring, enhance, fostering, garner, highlight (verb), interplay, intricate/intricacies, key (adjective), landscape (abstract noun), pivotal, showcase, tapestry (abstract noun), testament, underscore (verb), valuable, vibrant
|
||||
|
||||
**Problem:** These words appear far more frequently in post-2023 text. They often co-occur.
|
||||
|
||||
**Before:**
|
||||
> Additionally, a distinctive feature of Somali cuisine is the incorporation of camel meat. An enduring testament to Italian colonial influence is the widespread adoption of pasta in the local culinary landscape, showcasing how these dishes have integrated into the traditional diet.
|
||||
|
||||
**After:**
|
||||
> Somali cuisine also includes camel meat, which is considered a delicacy. Pasta dishes, introduced during Italian colonization, remain common, especially in the south.
|
||||
|
||||
---
|
||||
|
||||
### 8. Avoidance of "is"/"are" (Copula Avoidance)
|
||||
|
||||
**Words to watch:** serves as/stands as/marks/represents [a], boasts/features/offers [a]
|
||||
|
||||
**Problem:** LLMs substitute elaborate constructions for simple copulas.
|
||||
|
||||
**Before:**
|
||||
> Gallery 825 serves as LAAA's exhibition space for contemporary art. The gallery features four separate spaces and boasts over 3,000 square feet.
|
||||
|
||||
**After:**
|
||||
> Gallery 825 is LAAA's exhibition space for contemporary art. The gallery has four rooms totaling 3,000 square feet.
|
||||
|
||||
---
|
||||
|
||||
### 9. Negative Parallelisms
|
||||
|
||||
**Problem:** Constructions like "Not only...but..." or "It's not just about..., it's..." are overused.
|
||||
|
||||
**Before:**
|
||||
> It's not just about the beat riding under the vocals; it's part of the aggression and atmosphere. It's not merely a song, it's a statement.
|
||||
|
||||
**After:**
|
||||
> The heavy beat adds to the aggressive tone.
|
||||
|
||||
---
|
||||
|
||||
### 10. Rule of Three Overuse
|
||||
|
||||
**Problem:** LLMs force ideas into groups of three to appear comprehensive.
|
||||
|
||||
**Before:**
|
||||
> The event features keynote sessions, panel discussions, and networking opportunities. Attendees can expect innovation, inspiration, and industry insights.
|
||||
|
||||
**After:**
|
||||
> The event includes talks and panels. There's also time for informal networking between sessions.
|
||||
|
||||
---
|
||||
|
||||
### 11. Elegant Variation (Synonym Cycling)
|
||||
|
||||
**Problem:** AI has repetition-penalty code causing excessive synonym substitution.
|
||||
|
||||
**Before:**
|
||||
> The protagonist faces many challenges. The main character must overcome obstacles. The central figure eventually triumphs. The hero returns home.
|
||||
|
||||
**After:**
|
||||
> The protagonist faces many challenges but eventually triumphs and returns home.
|
||||
|
||||
---
|
||||
|
||||
### 12. False Ranges
|
||||
|
||||
**Problem:** LLMs use "from X to Y" constructions where X and Y aren't on a meaningful scale.
|
||||
|
||||
**Before:**
|
||||
> Our journey through the universe has taken us from the singularity of the Big Bang to the grand cosmic web, from the birth and death of stars to the enigmatic dance of dark matter.
|
||||
|
||||
**After:**
|
||||
> The book covers the Big Bang, star formation, and current theories about dark matter.
|
||||
|
||||
---
|
||||
|
||||
## STYLE PATTERNS
|
||||
|
||||
### 13. Em Dash Overuse
|
||||
|
||||
**Problem:** LLMs use em dashes (—) more than humans, mimicking "punchy" sales writing.
|
||||
|
||||
**Before:**
|
||||
> The term is primarily promoted by Dutch institutions—not by the people themselves. You don't say "Netherlands, Europe" as an address—yet this mislabeling continues—even in official documents.
|
||||
|
||||
**After:**
|
||||
> The term is primarily promoted by Dutch institutions, not by the people themselves. You don't say "Netherlands, Europe" as an address, yet this mislabeling continues in official documents.
|
||||
|
||||
---
|
||||
|
||||
### 14. Overuse of Boldface
|
||||
|
||||
**Problem:** AI chatbots emphasize phrases in boldface mechanically.
|
||||
|
||||
**Before:**
|
||||
> It blends **OKRs (Objectives and Key Results)**, **KPIs (Key Performance Indicators)**, and visual strategy tools such as the **Business Model Canvas (BMC)** and **Balanced Scorecard (BSC)**.
|
||||
|
||||
**After:**
|
||||
> It blends OKRs, KPIs, and visual strategy tools like the Business Model Canvas and Balanced Scorecard.
|
||||
|
||||
---
|
||||
|
||||
### 15. Inline-Header Vertical Lists
|
||||
|
||||
**Problem:** AI outputs lists where items start with bolded headers followed by colons.
|
||||
|
||||
**Before:**
|
||||
> - **User Experience:** The user experience has been significantly improved with a new interface.
|
||||
> - **Performance:** Performance has been enhanced through optimized algorithms.
|
||||
> - **Security:** Security has been strengthened with end-to-end encryption.
|
||||
|
||||
**After:**
|
||||
> The update improves the interface, speeds up load times through optimized algorithms, and adds end-to-end encryption.
|
||||
|
||||
---
|
||||
|
||||
### 16. Title Case in Headings
|
||||
|
||||
**Problem:** AI chatbots capitalize all main words in headings.
|
||||
|
||||
**Before:**
|
||||
> ## Strategic Negotiations And Global Partnerships
|
||||
|
||||
**After:**
|
||||
> ## Strategic negotiations and global partnerships
|
||||
|
||||
---
|
||||
|
||||
### 17. Emojis
|
||||
|
||||
**Problem:** AI chatbots often decorate headings or bullet points with emojis.
|
||||
|
||||
**Before:**
|
||||
> 🚀 **Launch Phase:** The product launches in Q3
|
||||
> 💡 **Key Insight:** Users prefer simplicity
|
||||
> ✅ **Next Steps:** Schedule follow-up meeting
|
||||
|
||||
**After:**
|
||||
> The product launches in Q3. User research showed a preference for simplicity. Next step: schedule a follow-up meeting.
|
||||
|
||||
---
|
||||
|
||||
### 18. Curly Quotation Marks
|
||||
|
||||
**Problem:** ChatGPT uses curly quotes (“...”) instead of straight quotes ("...").
|
||||
|
||||
**Before:**
|
||||
> He said “the project is on track” but others disagreed.
|
||||
|
||||
**After:**
|
||||
> He said "the project is on track" but others disagreed.
|
||||
|
||||
---
|
||||
|
||||
## COMMUNICATION PATTERNS
|
||||
|
||||
### 19. Collaborative Communication Artifacts
|
||||
|
||||
**Words to watch:** I hope this helps, Of course!, Certainly!, You're absolutely right!, Would you like..., let me know, here is a...
|
||||
|
||||
**Problem:** Text meant as chatbot correspondence gets pasted as content.
|
||||
|
||||
**Before:**
|
||||
> Here is an overview of the French Revolution. I hope this helps! Let me know if you'd like me to expand on any section.
|
||||
|
||||
**After:**
|
||||
> The French Revolution began in 1789 when financial crisis and food shortages led to widespread unrest.
|
||||
|
||||
---
|
||||
|
||||
### 20. Knowledge-Cutoff Disclaimers
|
||||
|
||||
**Words to watch:** as of [date], Up to my last training update, While specific details are limited/scarce..., based on available information...
|
||||
|
||||
**Problem:** AI disclaimers about incomplete information get left in text.
|
||||
|
||||
**Before:**
|
||||
> While specific details about the company's founding are not extensively documented in readily available sources, it appears to have been established sometime in the 1990s.
|
||||
|
||||
**After:**
|
||||
> The company was founded in 1994, according to its registration documents.
|
||||
|
||||
---
|
||||
|
||||
### 21. Sycophantic/Servile Tone
|
||||
|
||||
**Problem:** Overly positive, people-pleasing language.
|
||||
|
||||
**Before:**
|
||||
> Great question! You're absolutely right that this is a complex topic. That's an excellent point about the economic factors.
|
||||
|
||||
**After:**
|
||||
> The economic factors you mentioned are relevant here.
|
||||
|
||||
---
|
||||
|
||||
## FILLER AND HEDGING
|
||||
|
||||
### 22. Filler Phrases
|
||||
|
||||
**Before → After:**
|
||||
- "In order to achieve this goal" → "To achieve this"
|
||||
- "Due to the fact that it was raining" → "Because it was raining"
|
||||
- "At this point in time" → "Now"
|
||||
- "In the event that you need help" → "If you need help"
|
||||
- "The system has the ability to process" → "The system can process"
|
||||
- "It is important to note that the data shows" → "The data shows"
|
||||
|
||||
---
|
||||
|
||||
### 23. Excessive Hedging
|
||||
|
||||
**Problem:** Over-qualifying statements.
|
||||
|
||||
**Before:**
|
||||
> It could potentially possibly be argued that the policy might have some effect on outcomes.
|
||||
|
||||
**After:**
|
||||
> The policy may affect outcomes.
|
||||
|
||||
---
|
||||
|
||||
### 24. Generic Positive Conclusions
|
||||
|
||||
**Problem:** Vague upbeat endings.
|
||||
|
||||
**Before:**
|
||||
> The future looks bright for the company. Exciting times lie ahead as they continue their journey toward excellence. This represents a major step in the right direction.
|
||||
|
||||
**After:**
|
||||
> The company plans to open two more locations next year.
|
||||
|
||||
---
|
||||
|
||||
## Process
|
||||
|
||||
1. Read the input text carefully
|
||||
2. Identify all instances of the patterns above
|
||||
3. Rewrite each problematic section
|
||||
4. Ensure the revised text:
|
||||
- Sounds natural when read aloud
|
||||
- Varies sentence structure naturally
|
||||
- Uses specific details over vague claims
|
||||
- Maintains appropriate tone for context
|
||||
- Uses simple constructions (is/are/has) where appropriate
|
||||
5. Present the humanized version
|
||||
|
||||
## Output Format
|
||||
|
||||
Provide:
|
||||
1. The rewritten text
|
||||
2. A brief summary of changes made (optional, if helpful)
|
||||
|
||||
---
|
||||
|
||||
## Full Example
|
||||
|
||||
**Before (AI-sounding):**
|
||||
> Great question! Here is an essay on this topic. I hope this helps!
|
||||
>
|
||||
> AI-assisted coding serves as an enduring testament to the transformative potential of large language models, marking a pivotal moment in the evolution of software development. In today's rapidly evolving technological landscape, these groundbreaking tools—nestled at the intersection of research and practice—are reshaping how engineers ideate, iterate, and deliver, underscoring their vital role in modern workflows.
|
||||
>
|
||||
> At its core, the value proposition is clear: streamlining processes, enhancing collaboration, and fostering alignment. It's not just about autocomplete; it's about unlocking creativity at scale, ensuring that organizations can remain agile while delivering seamless, intuitive, and powerful experiences to users. The tool serves as a catalyst. The assistant functions as a partner. The system stands as a foundation for innovation.
|
||||
>
|
||||
> Industry observers have noted that adoption has accelerated from hobbyist experiments to enterprise-wide rollouts, from solo developers to cross-functional teams. The technology has been featured in The New York Times, Wired, and The Verge. Additionally, the ability to generate documentation, tests, and refactors showcases how AI can contribute to better outcomes, highlighting the intricate interplay between automation and human judgment.
|
||||
>
|
||||
> - 💡 **Speed:** Code generation is significantly faster, reducing friction and empowering developers.
|
||||
> - 🚀 **Quality:** Output quality has been enhanced through improved training, contributing to higher standards.
|
||||
> - ✅ **Adoption:** Usage continues to grow, reflecting broader industry trends.
|
||||
>
|
||||
> While specific details are limited based on available information, it could potentially be argued that these tools might have some positive effect. Despite challenges typical of emerging technologies—including hallucinations, bias, and accountability—the ecosystem continues to thrive. In order to fully realize this potential, teams must align with best practices.
|
||||
>
|
||||
> In conclusion, the future looks bright. Exciting times lie ahead as we continue this journey toward excellence. Let me know if you’d like me to expand on any section!
|
||||
|
||||
**After (Humanized):**
|
||||
> AI coding assistants speed up some tasks. In a 2024 study by Google, developers using Codex completed simple functions 55% faster than a control group, but showed no improvement on debugging or architectural decisions.
|
||||
>
|
||||
> The tools are good at boilerplate: config files, test scaffolding, repetitive refactors. They are bad at knowing when they are wrong. I have mass-accepted suggestions that compiled, passed lint, and still did the wrong thing because I stopped paying attention.
|
||||
>
|
||||
> Mira, an engineer at a fintech startup I interviewed, said she uses Copilot "like autocomplete for boring code" but reviews every line before committing. Jake, a senior dev at a larger company, disabled it after it kept suggesting patterns from a deprecated internal library.
|
||||
>
|
||||
> The productivity claims are hard to verify. GitHub says Copilot users "accept 30% of suggestions," but acceptance is not correctness, and correctness is not value. The 2024 Uplevel study found no statistically significant difference in pull-request throughput between teams with and without AI assistants.
|
||||
>
|
||||
> None of this means the tools are useless. It means they are tools. They do not replace judgment, and they do not eliminate the need for tests. If you do not have tests, you cannot tell whether the suggestion is right.
|
||||
|
||||
**Changes made:**
|
||||
- Removed chatbot artifacts ("Great question!", "I hope this helps!", "Let me know if...")
|
||||
- Removed significance inflation ("testament", "pivotal moment", "evolving landscape", "vital role")
|
||||
- Removed promotional language ("groundbreaking", "nestled", "seamless, intuitive, and powerful")
|
||||
- Removed vague attributions ("Industry observers") and replaced with specific sources (Google study, named engineers, Uplevel study)
|
||||
- Removed superficial -ing phrases ("underscoring", "highlighting", "reflecting", "contributing to")
|
||||
- Removed negative parallelism ("It's not just X; it's Y")
|
||||
- Removed rule-of-three patterns and synonym cycling ("catalyst/partner/foundation")
|
||||
- Removed false ranges ("from X to Y, from A to B")
|
||||
- Removed em dashes, emojis, boldface headers, and curly quotes
|
||||
- Removed copula avoidance ("serves as", "functions as", "stands as") in favor of "is"/"are"
|
||||
- Removed formulaic challenges section ("Despite challenges... continues to thrive")
|
||||
- Removed knowledge-cutoff hedging ("While specific details are limited...")
|
||||
- Removed excessive hedging ("could potentially be argued that... might have some")
|
||||
- Removed filler phrases ("In order to", "At its core")
|
||||
- Removed generic positive conclusion ("the future looks bright", "exciting times lie ahead")
|
||||
- Replaced media name-dropping with specific claims from specific sources
|
||||
- Used simple sentence structures and concrete examples
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
This skill is based on [Wikipedia:Signs of AI writing](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing), maintained by WikiProject AI Cleanup. The patterns documented there come from observations of thousands of instances of AI-generated text on Wikipedia.
|
||||
|
||||
Key insight from Wikipedia: "LLMs use statistical algorithms to guess what should come next. The result tends toward the most statistically likely result that applies to the widest variety of cases."
|
||||
107
.skills/mcc-chatbot-authoring/SKILL.md
Normal file
107
.skills/mcc-chatbot-authoring/SKILL.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
---
|
||||
name: mcc-chatbot-authoring
|
||||
description: Create, modify, repair, and wire Minecraft Console Client ChatBots and standalone `/script` bots. Use this whenever the user wants an MCC bot, C# script bot, chat or event handlers, periodic automation, movement logic, inventory logic, plugin-channel handling, or asks to fix or port an existing bot; default to standalone `//MCCScript` bots unless the user explicitly asks for a built-in MCC bot or repo wiring.
|
||||
---
|
||||
|
||||
# MCC ChatBot Authoring
|
||||
|
||||
Implement MCC chat bots against the bundled MCC authoring reference. Do not invent methods, lifecycle hooks, or registration steps.
|
||||
|
||||
Always read:
|
||||
- `references/authoring-reference.md`
|
||||
|
||||
Load only as needed:
|
||||
- `references/pattern-cookbook.md` for concrete standalone examples
|
||||
- `assets/script-chatbot-template.cs` for the default standalone `/script` path
|
||||
- `assets/builtin-chatbot-template.cs` only when the user explicitly requests a built-in bot
|
||||
|
||||
If the current workspace contains an MCC checkout, verify final names and signatures against local sources before editing. The skill should still work without those files.
|
||||
If there is no MCC checkout available, rely on the bundled reference and cookbook as the full source of truth for authoring patterns.
|
||||
|
||||
## Choose the bot type first
|
||||
|
||||
1. Default to a standalone script bot loaded with `/script`.
|
||||
2. Only choose a built-in bot when the user explicitly asks for a compiled MCC bot, repo wiring, automatic config loading, or changes under the built-in bot system.
|
||||
3. If the prompt is ambiguous, infer the likely target from commands, requested output files, or phrasing, state the assumption briefly, and proceed.
|
||||
4. If a user says only "make a bot", do not create a built-in bot.
|
||||
|
||||
## Source priority
|
||||
|
||||
When the local MCC checkout is available, prefer these sources in this order:
|
||||
1. `MinecraftClient/Scripting/ChatBot.cs` and current files under `MinecraftClient/ChatBots/`
|
||||
2. the bundled `references/authoring-reference.md`
|
||||
3. the bundled `references/pattern-cookbook.md`
|
||||
4. older `MinecraftClient/config/` sample bots only for ideas, not as the default scaffold
|
||||
|
||||
If an older sample conflicts with the current built-in bots, follow the current built-in bots.
|
||||
If the local checkout is not available, do not block on missing repo files. Use the bundled references directly.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- Only use lifecycle hooks and helpers documented in the bundled reference or verified in the target codebase.
|
||||
- Do not send chat from `Initialize()`. Use `AfterGameJoined()` once the session can send messages.
|
||||
- Prefer the current Brigadier command-registration pattern for built-in bots. Do not introduce `ChatBotCommand` unless the surrounding code already uses it.
|
||||
- For message parsing, normalize with `GetVerbatim(text)` before `IsChatMessage(...)` or `IsPrivateMessage(...)`.
|
||||
- Clean up everything you register or start: commands, plugin channels, threads, timers, and movement locks.
|
||||
- If a built-in bot or long-running automation controls movement, follow a movement-lock pattern and release it on every stop path. Do not add `BotMovementLock` to a simple standalone `/script` bot unless the prompt or surrounding code explicitly needs shared movement coordination.
|
||||
- For built-in bots, follow the host codebase's localization and config-comment conventions instead of scattering hardcoded user-facing text.
|
||||
- For new code, prefer `Initialize()` over constructors for prerequisite checks and unload decisions.
|
||||
- In this repo, built-in bot wiring usually means edits in `MinecraftClient/Settings.cs` and `MinecraftClient/McClient.cs` in addition to the bot class.
|
||||
- For repair tasks, preserve the existing bot type and file layout unless the user explicitly asks for a conversion or restructure.
|
||||
|
||||
## Standalone script bots
|
||||
|
||||
Use the exact MCC metadata format from the bundled reference.
|
||||
This is the default path for new work.
|
||||
|
||||
The script should usually:
|
||||
- keep `Initialize()` for cheap setup only
|
||||
- use `GetText(...)`, `AfterGameJoined()`, and other event hooks for live behavior
|
||||
- log with `LogToConsole(...)`
|
||||
- send server chat or commands with `SendText(...)`
|
||||
- use `PerformInternalCommand(...)` only for MCC internal commands
|
||||
- add `//using MinecraftClient.Inventory` in metadata when the script uses inventory types explicitly
|
||||
- reuse the standalone snippets in `references/pattern-cookbook.md` before inventing new scaffolding
|
||||
- keep load instructions explicit, usually `/script FileName.cs`
|
||||
|
||||
## Built-in bots
|
||||
|
||||
Built-in bots usually need three pieces:
|
||||
- the bot class itself
|
||||
- config wiring in the chat-bot config model
|
||||
- bot registration in the load flow
|
||||
|
||||
If the codebase exposes commands, follow the built-in command and unload pattern from the bundled reference. If it exposes new settings or status text, follow the codebase's localization and config-comment patterns.
|
||||
|
||||
When working in this checkout, built-in bot delivery usually needs:
|
||||
- a new file under `MinecraftClient/ChatBots/`
|
||||
- a config property inside `Settings.ChatBotConfigHealper.ChatBotConfig`
|
||||
- a `BotLoad(new YourBot())` line inside `McClient.RegisterBots(...)`
|
||||
- literal code snippets or patch hunks for the `Settings.cs` property and the `McClient.cs` registration line, not only prose notes
|
||||
|
||||
## Repair flow
|
||||
|
||||
When the user asks to fix or debug a bot:
|
||||
- identify whether it is standalone or built-in and keep that shape unless told otherwise
|
||||
- remove the broken pattern first, then preserve the intended behavior
|
||||
- check especially for these regressions: `SendText(...)` in `Initialize()`, raw formatted chat parsing, inventory snapshot mutation, missing command unregister, missing plugin-channel unregister, and unreleased movement locks
|
||||
- reuse the local repo's modern pattern instead of patching around a legacy helper when the helper is no longer current
|
||||
|
||||
## Delivery checklist
|
||||
|
||||
Before finishing, verify:
|
||||
- the class inherits `ChatBot`
|
||||
- the chosen overrides exist in the MCC ChatBot API
|
||||
- standalone script metadata is exact if this is a `/script` bot
|
||||
- built-in bots are fully wired into config and registration if needed
|
||||
- all command registrations, background work, and movement locks are released
|
||||
- files and namespaces match the surrounding codebase
|
||||
|
||||
## Output
|
||||
|
||||
When you implement or modify a bot:
|
||||
- state whether it is a standalone script bot or built-in bot
|
||||
- list the files you changed
|
||||
- mention any required config keys or the MCC command used to load it
|
||||
- when built-in wiring is involved, show the exact inserted code lines or patch hunks for `Settings.cs` and `McClient.cs`
|
||||
- call out assumptions briefly if the user did not specify bot type or trigger behavior
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// Use this template only when the user explicitly requests a built-in MCC bot.
|
||||
|
||||
using MinecraftClient.Scripting;
|
||||
using Tomlet.Attributes;
|
||||
|
||||
namespace MinecraftClient.ChatBots
|
||||
{
|
||||
public class ExampleBot : ChatBot
|
||||
{
|
||||
private const string BotName = "ExampleBot";
|
||||
|
||||
public static Configs Config = new();
|
||||
|
||||
[TomlDoNotInlineObject]
|
||||
public class Configs
|
||||
{
|
||||
public bool Enabled = false;
|
||||
|
||||
public void OnSettingUpdate()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
LogToConsole(BotName, "Initialized.");
|
||||
}
|
||||
|
||||
public override void AfterGameJoined()
|
||||
{
|
||||
}
|
||||
|
||||
public override void GetText(string text)
|
||||
{
|
||||
text = GetVerbatim(text);
|
||||
|
||||
string message = "";
|
||||
string username = "";
|
||||
|
||||
if (IsPrivateMessage(text, ref message, ref username))
|
||||
{
|
||||
}
|
||||
else if (IsChatMessage(text, ref message, ref username))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnUnload()
|
||||
{
|
||||
}
|
||||
|
||||
public override bool OnDisconnect(DisconnectReason reason, string message)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
//MCCScript 1.0
|
||||
|
||||
MCC.LoadBot(new ExampleScriptBot());
|
||||
|
||||
//MCCScript Extensions
|
||||
|
||||
public class ExampleScriptBot : ChatBot
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
LogToConsole("ExampleScriptBot initialized.");
|
||||
}
|
||||
|
||||
public override void AfterGameJoined()
|
||||
{
|
||||
// Safe place for startup chat or commands.
|
||||
}
|
||||
|
||||
public override void GetText(string text)
|
||||
{
|
||||
text = GetVerbatim(text);
|
||||
|
||||
string message = "";
|
||||
string username = "";
|
||||
|
||||
if (IsPrivateMessage(text, ref message, ref username))
|
||||
{
|
||||
LogToConsole("PM from " + username + ": " + message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsChatMessage(text, ref message, ref username))
|
||||
{
|
||||
LogToConsole("Chat from " + username + ": " + message);
|
||||
}
|
||||
}
|
||||
}
|
||||
492
.skills/mcc-chatbot-authoring/references/authoring-reference.md
Normal file
492
.skills/mcc-chatbot-authoring/references/authoring-reference.md
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
# MCC ChatBot Reference
|
||||
|
||||
Self-contained authoring notes for Minecraft Console Client chat bots.
|
||||
|
||||
## Bot types
|
||||
|
||||
MCC supports two common authoring paths:
|
||||
- standalone script bots loaded at runtime with `/script`
|
||||
- built-in bots compiled into the MCC codebase
|
||||
|
||||
Default to a standalone `/script` bot unless the user explicitly asks for a built-in bot or repo wiring.
|
||||
|
||||
## Embedded current patterns
|
||||
|
||||
This skill is intended to work even without an MCC checkout. The patterns below capture the important behavior that would otherwise be borrowed from current repo examples.
|
||||
|
||||
If the local repo is available, you can verify against files such as `TestBot.cs`, `RemoteControl.cs`, `FollowPlayer.cs`, `ItemsCollector.cs`, and `Farmer.cs`. If it is not available, use the embedded patterns here directly.
|
||||
|
||||
### Minimal chat parsing pattern
|
||||
|
||||
Use this as the baseline for public/private chat handling:
|
||||
|
||||
```csharp
|
||||
public override void GetText(string text)
|
||||
{
|
||||
string message = "";
|
||||
string sender = "";
|
||||
text = GetVerbatim(text);
|
||||
|
||||
if (IsPrivateMessage(text, ref message, ref sender))
|
||||
{
|
||||
LogToConsole("PM from " + sender + ": " + message);
|
||||
}
|
||||
else if (IsChatMessage(text, ref message, ref sender))
|
||||
{
|
||||
LogToConsole("Chat from " + sender + ": " + message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
What matters:
|
||||
- normalize first with `GetVerbatim(text)`
|
||||
- handle PMs before public chat if both matter
|
||||
- keep simple chat bots deterministic and small
|
||||
|
||||
### Owner-gated PM control pattern
|
||||
|
||||
Use this when a bot owner should be able to whisper MCC internal commands:
|
||||
|
||||
```csharp
|
||||
public override void GetText(string text)
|
||||
{
|
||||
text = GetVerbatim(text).Trim();
|
||||
string command = "";
|
||||
string sender = "";
|
||||
|
||||
if (IsPrivateMessage(text, ref command, ref sender)
|
||||
&& Settings.Config.Main.Advanced.BotOwners.Contains(sender.ToLowerInvariant()))
|
||||
{
|
||||
CmdResult result = new();
|
||||
PerformInternalCommand(command, ref result);
|
||||
SendPrivateMessage(sender, result.ToString());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
What matters:
|
||||
- `PerformInternalCommand(...)` is for MCC commands, not server chat commands
|
||||
- owner gating should use `Settings.Config.Main.Advanced.BotOwners`
|
||||
- if `CmdResult` is used in a standalone script, add `//using MinecraftClient.CommandHandler`
|
||||
|
||||
### Periodic work pattern
|
||||
|
||||
Use `Update()` plus a counter or timestamp for simple repeated work:
|
||||
|
||||
```csharp
|
||||
private int count = 0;
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
count++;
|
||||
if (count < Settings.DoubleToTick(60))
|
||||
return;
|
||||
|
||||
count = 0;
|
||||
SendText("/list");
|
||||
}
|
||||
```
|
||||
|
||||
What matters:
|
||||
- avoid a worker thread for simple periodic loops
|
||||
- avoid `Thread.Sleep(...)` inside `Update()`
|
||||
- if sending chat, do it from a join-safe path like `Update()` or `AfterGameJoined()`, not `Initialize()`
|
||||
|
||||
### Built-in Brigadier command pattern
|
||||
|
||||
Use this for built-in command bots:
|
||||
|
||||
```csharp
|
||||
public override void Initialize()
|
||||
{
|
||||
McClient.dispatcher.Register(l => l.Literal("help")
|
||||
.Then(l => l.Literal(CommandName)
|
||||
.Executes(r => OnCommandHelp(r.Source, string.Empty))
|
||||
)
|
||||
);
|
||||
|
||||
McClient.dispatcher.Register(l => l.Literal(CommandName)
|
||||
.Then(l => l.Literal("stop")
|
||||
.Executes(r => OnCommandStop(r.Source)))
|
||||
.Then(l => l.Literal("_help")
|
||||
.Executes(r => OnCommandHelp(r.Source, string.Empty))
|
||||
.Redirect(McClient.dispatcher.GetRoot().GetChild("help").GetChild(CommandName)))
|
||||
);
|
||||
}
|
||||
|
||||
public override void OnUnload()
|
||||
{
|
||||
McClient.dispatcher.Unregister(CommandName);
|
||||
McClient.dispatcher.GetRoot().GetChild("help").RemoveChild(CommandName);
|
||||
}
|
||||
```
|
||||
|
||||
What matters:
|
||||
- register commands in `Initialize()`
|
||||
- unregister the command tree in `OnUnload()`
|
||||
- remove the help child you added in `OnUnload()`
|
||||
- prefer this over legacy command wrappers for new built-in work
|
||||
|
||||
### Built-in config and wiring pattern
|
||||
|
||||
Use this as the default built-in shape:
|
||||
|
||||
```csharp
|
||||
public class ExampleBot : ChatBot
|
||||
{
|
||||
public static Configs Config = new();
|
||||
|
||||
[TomlDoNotInlineObject]
|
||||
public class Configs
|
||||
{
|
||||
public bool Enabled = false;
|
||||
|
||||
public void OnSettingUpdate()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Typical host wiring shape:
|
||||
|
||||
```csharp
|
||||
[TomlPrecedingComment("$ChatBot.ExampleBot$")]
|
||||
public ChatBots.ExampleBot.Configs ExampleBot
|
||||
{
|
||||
get { return ChatBots.ExampleBot.Config; }
|
||||
set { ChatBots.ExampleBot.Config = value; ChatBots.ExampleBot.Config.OnSettingUpdate(); }
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
if (Config.ChatBot.ExampleBot.Enabled) { BotLoad(new ExampleBot()); }
|
||||
```
|
||||
|
||||
What matters:
|
||||
- built-in configurable bots default to `Enabled = false`
|
||||
- `OnSettingUpdate()` is the place to normalize config values
|
||||
- built-in delivery is incomplete without both config wiring and load registration
|
||||
|
||||
### Movement gating pattern
|
||||
|
||||
Use this shape when a built-in bot owns movement:
|
||||
|
||||
```csharp
|
||||
public override void Initialize()
|
||||
{
|
||||
if (!GetEntityHandlingEnabled())
|
||||
{
|
||||
LogToConsole("Entity handling is required.");
|
||||
UnloadBot();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!GetTerrainEnabled())
|
||||
{
|
||||
LogToConsole("Terrain handling is required.");
|
||||
UnloadBot();
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
var movementLock = BotMovementLock.Instance;
|
||||
if (movementLock is { IsLocked: true })
|
||||
return;
|
||||
|
||||
movementLock?.Lock("Example Bot");
|
||||
```
|
||||
|
||||
```csharp
|
||||
public override void OnUnload()
|
||||
{
|
||||
BotMovementLock.Instance?.UnLock("Example Bot");
|
||||
}
|
||||
```
|
||||
|
||||
What matters:
|
||||
- guard terrain and entity handling before movement logic
|
||||
- built-in movement bots should use `BotMovementLock`
|
||||
- release the lock on every stop path, including unload and disconnect-sensitive flows
|
||||
|
||||
### Dropped-item collector pattern
|
||||
|
||||
Use this as the standalone item-search baseline:
|
||||
|
||||
```csharp
|
||||
private DateTime nextScan = DateTime.MinValue;
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (now < nextScan || ClientIsMoving())
|
||||
return;
|
||||
|
||||
nextScan = now.AddSeconds(1);
|
||||
|
||||
var here = GetCurrentLocation();
|
||||
var target = GetEntities().Values
|
||||
.Where(entity => entity.Type == EntityType.Item && entity.Location.Distance(here) <= 15)
|
||||
.OrderBy(entity => entity.Location.Distance(here))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (target != null)
|
||||
MoveToLocation(target.Location);
|
||||
}
|
||||
```
|
||||
|
||||
What matters:
|
||||
- simple standalone collectors do not need a worker thread
|
||||
- simple standalone collectors also do not need `BotMovementLock` by default
|
||||
- `GetEntities()` plus distance ordering is the core search pattern
|
||||
|
||||
### Inventory selection pattern
|
||||
|
||||
Use this as the default hotbar-switch pattern:
|
||||
|
||||
```csharp
|
||||
private bool TrySwitchToItem(ItemType itemType)
|
||||
{
|
||||
var inventory = GetPlayerInventory();
|
||||
|
||||
var hotbarSlots = inventory.SearchItem(itemType)
|
||||
.Where(slot => slot >= 36 && slot <= 44)
|
||||
.ToArray();
|
||||
|
||||
if (hotbarSlots.Length == 0)
|
||||
return false;
|
||||
|
||||
ChangeSlot((short)(hotbarSlots[0] - 36));
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
What matters:
|
||||
- guard with `GetInventoryEnabled()`
|
||||
- search inventory snapshots, but mutate real server state with helpers like `ChangeSlot(...)`
|
||||
- do not treat local `Container.Items` mutation as real inventory manipulation
|
||||
|
||||
Use the older config examples only for ideas, not as primary scaffolding.
|
||||
|
||||
## Standalone script format
|
||||
|
||||
A standalone script bot has two parts in this order:
|
||||
1. metadata block
|
||||
2. one or more C# classes, with the main bot class inheriting `ChatBot`
|
||||
|
||||
Required metadata rules:
|
||||
- line 1 must be exactly `//MCCScript 1.0`
|
||||
- metadata must include `MCC.LoadBot(new BotClassName());`
|
||||
- metadata ends with `//MCCScript Extensions`
|
||||
- optional metadata directives use `//using Namespace` and `//dll SomeLibrary.dll`
|
||||
- do not insert a space after `//` in metadata directives
|
||||
|
||||
Typical runtime flow:
|
||||
- place the script file beside MCC
|
||||
- connect to a server
|
||||
- load it with `/script YourBotFile.cs`
|
||||
|
||||
### Namespace linking for inventory code
|
||||
|
||||
If a standalone script uses inventory-specific types such as `Container`, `ItemType`, `WindowActionType`, or `ItemMovingHelper`, add this metadata import:
|
||||
|
||||
```csharp
|
||||
//using MinecraftClient.Inventory
|
||||
```
|
||||
|
||||
For built-in bots, use a normal C# import:
|
||||
|
||||
```csharp
|
||||
using MinecraftClient.Inventory;
|
||||
```
|
||||
|
||||
## Lifecycle summary
|
||||
|
||||
Common lifecycle hooks:
|
||||
- `Initialize()`
|
||||
called once when the bot loads; use it for cheap setup only
|
||||
- `AfterGameJoined()`
|
||||
called after the server has been joined successfully, and again after reconnecting; use it when chat can be sent
|
||||
- `Update()`
|
||||
called roughly every 100 ms
|
||||
- `OnUnload()`
|
||||
called when the bot unloads; release resources here
|
||||
- `OnDisconnect(DisconnectReason reason, string message)`
|
||||
called on disconnect; stop background work and clean up reconnect-sensitive state here
|
||||
|
||||
Important rule:
|
||||
- do not send chat from `Initialize()`; use `AfterGameJoined()` instead
|
||||
- prefer `Initialize()` over constructors for environment checks and resource setup
|
||||
|
||||
## Common event hooks
|
||||
|
||||
Useful event hooks include:
|
||||
- `GetText(string text)`
|
||||
- `GetText(string text, string? json)`
|
||||
- `OnPlayerJoin(Guid uuid, string name)`
|
||||
- `OnPlayerLeave(Guid uuid, string? name)`
|
||||
- `OnEntitySpawn(Entity entity)`
|
||||
- `OnEntityDespawn(Entity entity)`
|
||||
- `OnEntityMove(Entity entity)`
|
||||
- `OnHealthUpdate(float health, int food)`
|
||||
- `OnMapData(...)`
|
||||
- `OnInventoryUpdate(int inventoryId)`
|
||||
- `OnPluginMessage(string channel, byte[] data)`
|
||||
- `OnNetworkPacket(int packetID, List<byte> packetData, bool isLogin, bool isInbound)`
|
||||
|
||||
Only override hooks that actually exist in the target MCC ChatBot API.
|
||||
|
||||
## Common helpers
|
||||
|
||||
Text and messaging helpers:
|
||||
- `GetVerbatim(text)` strips Minecraft formatting codes
|
||||
- `IsChatMessage(text, ref message, ref sender)` parses public chat
|
||||
- `IsPrivateMessage(text, ref message, ref sender)` parses private chat
|
||||
- `IsValidName(username)` validates a Minecraft username
|
||||
- `SendText(text)` sends chat or server commands
|
||||
- `SendPrivateMessage(player, message)` sends a private message
|
||||
- `PerformInternalCommand(command, ...)` runs an internal MCC command, not a server command
|
||||
- `LogToConsole(text)` writes a bot-prefixed console message
|
||||
|
||||
Lifecycle and threading helpers:
|
||||
- `InvokeOnMainThread(...)`
|
||||
- `ScheduleOnMainThread(...)`
|
||||
- `ReconnectToTheServer(...)`
|
||||
- `UnloadBot()`
|
||||
- `BotLoad(chatBot)`
|
||||
- `RunScript(filename, ...)`
|
||||
|
||||
World and player-state helpers:
|
||||
- `GetWorld()`
|
||||
- `GetEntities()`
|
||||
- `GetCurrentLocation()`
|
||||
- `ClientIsMoving()`
|
||||
- `GetOnlinePlayers()`
|
||||
- `GetOnlinePlayersWithUUID()`
|
||||
- `GetServerTPS()`
|
||||
- `GetProtocolVersion()`
|
||||
|
||||
Movement and inventory helpers:
|
||||
- `MoveToLocation(...)`
|
||||
- `LookAtLocation(...)`
|
||||
- `GetInventoryEnabled()`
|
||||
- `GetPlayerInventory()`
|
||||
- `GetInventories()`
|
||||
- `GetItemMovingHelper(...)`
|
||||
- `WindowAction(...)`
|
||||
- `ChangeSlot(...)`
|
||||
- `GetCurrentSlot()`
|
||||
- `UseItemInHand()`
|
||||
- `UseItemInLeftHand()`
|
||||
- `CloseInventory(...)`
|
||||
- `DigBlock(...)`
|
||||
- `InteractEntity(...)`
|
||||
|
||||
## Inventory notes
|
||||
|
||||
Inventory handling is optional in MCC. Check `GetInventoryEnabled()` before relying on inventory state or mutation.
|
||||
|
||||
Important behavior:
|
||||
- `GetPlayerInventory()` returns a snapshot copy of the player's inventory
|
||||
- `GetInventories()` returns current container snapshots
|
||||
- writing to those `Container` objects locally does not update the server
|
||||
- to actually change inventory state, use `ChangeSlot(...)`, `WindowAction(...)`, `GetItemMovingHelper(...)`, `UseItemInHand()`, or related helpers
|
||||
|
||||
Useful practical facts:
|
||||
- hotbar selection uses `ChangeSlot(0..8)`
|
||||
- hotbar slots are commonly `36..44` in inventory slot numbering
|
||||
- the offhand slot is commonly `45`
|
||||
- `Container.SearchItem(...)` is the normal way to locate items by type
|
||||
|
||||
Good inventory workflow:
|
||||
1. guard with `GetInventoryEnabled()`
|
||||
2. read the current container using `GetPlayerInventory()`
|
||||
3. locate slots with `SearchItem(...)` or `Items`
|
||||
4. mutate server state using `ChangeSlot(...)`, `WindowAction(...)`, or `ItemMovingHelper`
|
||||
5. if needed, react to `OnInventoryUpdate(...)`, `OnInventoryOpen(...)`, or `OnInventoryClose(...)`
|
||||
|
||||
Plugins and channels:
|
||||
- `RegisterPluginChannel(channel)`
|
||||
- `UnregisterPluginChannel(channel)`
|
||||
- `SendPluginChannelMessage(channel, data, ...)`
|
||||
|
||||
## Built-in bot pattern
|
||||
|
||||
A built-in bot usually follows this shape:
|
||||
- a class that inherits `ChatBot`
|
||||
- an optional static `Config` field
|
||||
- a nested `[TomlDoNotInlineObject]` `Configs` class for settings
|
||||
- an `Enabled = false` setting by default
|
||||
- `OnSettingUpdate()` to normalize or validate config values
|
||||
|
||||
If the bot is configurable, the host codebase usually also needs:
|
||||
- config wiring in the chat-bot config model
|
||||
- load registration so enabled bots are instantiated automatically
|
||||
|
||||
In this MCC checkout, the usual built-in wiring points are:
|
||||
- `MinecraftClient/Settings.cs` inside `Settings.ChatBotConfigHealper.ChatBotConfig`
|
||||
- `MinecraftClient/McClient.cs` inside `RegisterBots(...)`
|
||||
|
||||
Match the surrounding `[TomlPrecedingComment(...)]`, property-forwarding, and `BotLoad(new YourBot())` style instead of inventing a different config path.
|
||||
When presenting built-in wiring, prefer literal code snippets or patch hunks for those two edits so the wiring can be checked directly.
|
||||
|
||||
If the bot adds user-facing settings or messages, follow the host codebase's localization and config-comment conventions instead of scattering hardcoded strings.
|
||||
|
||||
## Command pattern
|
||||
|
||||
For standalone script bots, prefer chat or PM handling in `GetText(...)` unless the user explicitly asks for built-in command registration.
|
||||
|
||||
For built-in commands, prefer the current Brigadier dispatcher pattern:
|
||||
- register commands in `Initialize()`
|
||||
- add a help entry if the bot exposes commands
|
||||
- unregister the command tree in `OnUnload()`
|
||||
- remove any help child added during registration in `OnUnload()`
|
||||
|
||||
Avoid using legacy command wrappers if the current codebase uses direct dispatcher registration.
|
||||
In this checkout, treat direct `McClient.dispatcher.Register(...)` usage in current built-in bots as the source of truth.
|
||||
|
||||
## Concurrency and cleanup
|
||||
|
||||
If the bot starts background work:
|
||||
- stop it in `OnUnload()`
|
||||
- stop it in `OnDisconnect(...)`
|
||||
- consider resetting state in `AfterGameJoined()` after relog
|
||||
- prefer `Update()` plus counters or timestamps over unmanaged threads when the task is simple periodic work
|
||||
|
||||
If the bot controls movement:
|
||||
- use a movement-lock discipline
|
||||
- release the lock on every stop path
|
||||
- avoid fighting other movement bots
|
||||
- `BotMovementLock` is mainly for built-in bots or shared long-running automation; a simple standalone script that just calls `MoveToLocation(...)` does not need it by default
|
||||
|
||||
When interacting with client state from background logic, use the main-thread helpers when required by the codebase.
|
||||
|
||||
## Practical defaults
|
||||
|
||||
For simple chat bots:
|
||||
- normalize text with `GetVerbatim(text)`
|
||||
- inspect private chat first if the bot listens for whispers
|
||||
- then inspect public chat
|
||||
- keep response logic small and deterministic
|
||||
|
||||
For long-running automation bots:
|
||||
- guard prerequisites early, such as entity handling or terrain support
|
||||
- fail fast with a clear log message if prerequisites are missing
|
||||
- release all ongoing work cleanly on unload and disconnect
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Incorrect metadata line 1 will break standalone script loading.
|
||||
- Missing `MCC.LoadBot(new BotClassName())` will prevent standalone script registration.
|
||||
- Sending chat in `Initialize()` is too early.
|
||||
- Doing prerequisite checks or unloading from the constructor is harder to reason about than using `Initialize()`.
|
||||
- Parsing raw formatted text without `GetVerbatim()` causes brittle chat matching.
|
||||
- Inventing methods not present in the MCC ChatBot API leads to dead code.
|
||||
- Built-in bot work is incomplete if config or registration wiring is missing.
|
||||
- Command bots are incomplete if they register commands but do not unregister them.
|
||||
- `RegisterChatBotCommand(...)` comes from older samples and is not a reliable current pattern for this checkout.
|
||||
- `ChatBotCommand` exists, but the current built-in bots use Brigadier directly; do not prefer `ChatBotCommand` for new work.
|
||||
- Blocking `Thread.Sleep(...)` inside `Update()` is a bad default. Prefer timers, counters, or timestamp-based scheduling.
|
||||
- Mutating the `Container` returned by `GetPlayerInventory()` does not change the server. Use inventory actions instead.
|
||||
330
.skills/mcc-chatbot-authoring/references/pattern-cookbook.md
Normal file
330
.skills/mcc-chatbot-authoring/references/pattern-cookbook.md
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
# MCC Pattern Cookbook
|
||||
|
||||
Concrete patterns for standalone MCC `/script` bots. Use these before inventing new scaffolding.
|
||||
|
||||
## Periodic task without threads
|
||||
|
||||
Use `Update()` plus a timestamp or counter. This comes from the old `sample-script-with-task.cs` example and still holds up well.
|
||||
|
||||
```csharp
|
||||
public class PeriodicTaskBot : ChatBot
|
||||
{
|
||||
private DateTime nextRun = DateTime.MinValue;
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (now < nextRun)
|
||||
return;
|
||||
|
||||
nextRun = now.AddSeconds(30);
|
||||
LogDebugToConsole("Running periodic task");
|
||||
SendText("/ping");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Why this pattern is good:
|
||||
- stays on MCC's normal tick flow
|
||||
- avoids background threads for simple periodic work
|
||||
- keeps the bot responsive to unload and disconnect
|
||||
|
||||
## Chat and PM handling
|
||||
|
||||
This combines the useful parts of `TestBot`, `sample-script-pm-forwarder.cs`, and `RemoteControl.cs`.
|
||||
|
||||
```csharp
|
||||
public override void GetText(string text)
|
||||
{
|
||||
text = GetVerbatim(text);
|
||||
|
||||
string message = "";
|
||||
string sender = "";
|
||||
|
||||
if (IsPrivateMessage(text, ref message, ref sender))
|
||||
{
|
||||
LogToConsole("PM from " + sender + ": " + message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsChatMessage(text, ref message, ref sender))
|
||||
{
|
||||
LogToConsole("Chat from " + sender + ": " + message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Owner-gated internal command handling:
|
||||
|
||||
Add `//using MinecraftClient.CommandHandler` in the script metadata if you use `CmdResult`.
|
||||
|
||||
```csharp
|
||||
public override void GetText(string text)
|
||||
{
|
||||
text = GetVerbatim(text).Trim();
|
||||
|
||||
string command = "";
|
||||
string sender = "";
|
||||
|
||||
if (IsPrivateMessage(text, ref command, ref sender)
|
||||
&& Settings.Config.Main.Advanced.BotOwners.Contains(sender.ToLowerInvariant()))
|
||||
{
|
||||
CmdResult result = new();
|
||||
PerformInternalCommand(command, ref result);
|
||||
SendPrivateMessage(sender, result.ToString());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Movement with prerequisite checks
|
||||
|
||||
Modern movement code should copy the guard style from current built-in bots, not the older constructor-heavy scripts.
|
||||
|
||||
```csharp
|
||||
public override void Initialize()
|
||||
{
|
||||
if (!GetEntityHandlingEnabled() || !GetTerrainEnabled())
|
||||
{
|
||||
LogToConsole("Entity handling and terrain handling are required.");
|
||||
UnloadBot();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Simple "look at nearest player" logic adapted from `AutoLook.cs`:
|
||||
|
||||
```csharp
|
||||
private Entity? trackedPlayer = null;
|
||||
|
||||
public override void OnEntitySpawn(Entity entity)
|
||||
{
|
||||
TryTrack(entity);
|
||||
}
|
||||
|
||||
public override void OnEntityDespawn(Entity entity)
|
||||
{
|
||||
if (trackedPlayer != null && entity.ID == trackedPlayer.ID)
|
||||
trackedPlayer = null;
|
||||
}
|
||||
|
||||
public override void OnEntityMove(Entity entity)
|
||||
{
|
||||
if (!TryTrack(entity))
|
||||
return;
|
||||
|
||||
LookAtLocation(entity.Location);
|
||||
}
|
||||
|
||||
private bool TryTrack(Entity entity)
|
||||
{
|
||||
if (entity.Type != EntityType.Player)
|
||||
return false;
|
||||
|
||||
if (trackedPlayer == null)
|
||||
{
|
||||
trackedPlayer = entity;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (GetCurrentLocation().Distance(entity.Location) < GetCurrentLocation().Distance(trackedPlayer.Location))
|
||||
trackedPlayer = entity;
|
||||
|
||||
return trackedPlayer.ID == entity.ID;
|
||||
}
|
||||
```
|
||||
|
||||
## Search for dropped items and move to them
|
||||
|
||||
This is the safest pattern to preserve from `ItemsCollector.cs` for standalone scripts.
|
||||
|
||||
```csharp
|
||||
public class NearbyItemsBot : ChatBot
|
||||
{
|
||||
private DateTime nextScan = DateTime.MinValue;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
if (!GetEntityHandlingEnabled() || !GetTerrainEnabled())
|
||||
{
|
||||
LogToConsole("Entity handling and terrain handling are required.");
|
||||
UnloadBot();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (now < nextScan || ClientIsMoving())
|
||||
return;
|
||||
|
||||
nextScan = now.AddSeconds(1);
|
||||
|
||||
var here = GetCurrentLocation();
|
||||
var target = GetEntities().Values
|
||||
.Where(entity => entity.Type == EntityType.Item && entity.Location.Distance(here) <= 15)
|
||||
.OrderBy(entity => entity.Location.Distance(here))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (target != null)
|
||||
MoveToLocation(target.Location);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Why this version is better than older farming scripts:
|
||||
- no unmanaged worker thread
|
||||
- no busy wait loop around movement
|
||||
- uses the current `GetEntities()` pattern
|
||||
|
||||
## Search for blocks or crops in the world
|
||||
|
||||
The old sugar cane and mining scripts still contain a useful search idea: use `GetWorld().FindBlock(...)`, then filter and sort.
|
||||
|
||||
```csharp
|
||||
var targets = GetWorld()
|
||||
.FindBlock(GetCurrentLocation(), Material.SugarCane, 16)
|
||||
.Where(block =>
|
||||
GetWorld().GetBlock(new Location(block.X, block.Y - 1, block.Z)).Type == Material.SugarCane)
|
||||
.OrderBy(block => block.Distance(GetCurrentLocation()))
|
||||
.ToList();
|
||||
```
|
||||
|
||||
Use this as a search primitive. Then decide separately how to move, dig, or harvest.
|
||||
|
||||
## Inventory access and manipulation
|
||||
|
||||
If a standalone script uses inventory types directly, add this import in the metadata block:
|
||||
|
||||
```csharp
|
||||
//using MinecraftClient.Inventory
|
||||
```
|
||||
|
||||
For built-in bots, add:
|
||||
|
||||
```csharp
|
||||
using MinecraftClient.Inventory;
|
||||
```
|
||||
|
||||
Always guard inventory logic first:
|
||||
|
||||
```csharp
|
||||
public override void Initialize()
|
||||
{
|
||||
if (!GetInventoryEnabled())
|
||||
{
|
||||
LogToConsole("Inventory handling is required.");
|
||||
UnloadBot();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Important rule:
|
||||
- `GetPlayerInventory()` returns a snapshot copy, so editing its `Items` dictionary does not change the server
|
||||
- actual changes must go through `ChangeSlot(...)`, `WindowAction(...)`, `GetItemMovingHelper(...)`, `UseItemInHand()`, and related helpers
|
||||
|
||||
### Search inventory for an item
|
||||
|
||||
This combines the useful current logic from `Farmer.cs` and `AutoEat.cs`.
|
||||
|
||||
```csharp
|
||||
private bool TrySwitchToItem(ItemType itemType)
|
||||
{
|
||||
var inventory = GetPlayerInventory();
|
||||
|
||||
if (inventory.Items.TryGetValue(GetCurrentSlot() - 36, out var held) && held.Type == itemType)
|
||||
return true;
|
||||
|
||||
var hotbarSlots = inventory.SearchItem(itemType)
|
||||
.Where(slot => slot >= 36 && slot <= 44)
|
||||
.ToArray();
|
||||
|
||||
if (hotbarSlots.Length == 0)
|
||||
return false;
|
||||
|
||||
ChangeSlot((short)(hotbarSlots[0] - 36));
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
Use this for simple hotbar selection. For deeper inventory reshuffling, built-in bots usually need more helper logic.
|
||||
|
||||
### Move an item into the hotbar
|
||||
|
||||
Use this when the item exists in inventory but is not already on the hotbar.
|
||||
|
||||
```csharp
|
||||
private bool TryMoveItemToHotbar(ItemType itemType, short targetHotbarSlot = 0)
|
||||
{
|
||||
var inventory = GetPlayerInventory();
|
||||
var matches = inventory.SearchItem(itemType);
|
||||
|
||||
if (matches.Length == 0)
|
||||
return false;
|
||||
|
||||
var targetInventorySlot = 36 + targetHotbarSlot;
|
||||
|
||||
if (matches[0] >= 36 && matches[0] <= 44)
|
||||
{
|
||||
ChangeSlot((short)(matches[0] - 36));
|
||||
return true;
|
||||
}
|
||||
|
||||
var movingHelper = GetItemMovingHelper(inventory);
|
||||
movingHelper.Swap(matches[0], targetInventorySlot);
|
||||
ChangeSlot(targetHotbarSlot);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
Why this pattern is good:
|
||||
- it reads the current snapshot first
|
||||
- it does not pretend local `Container` edits affect the server
|
||||
- it uses the item-moving helper for real inventory manipulation
|
||||
|
||||
### Drop or click items with window actions
|
||||
|
||||
Use `WindowAction(...)` when the bot needs direct inventory clicks or dropping behavior.
|
||||
|
||||
```csharp
|
||||
private void DropAllOfType(ItemType itemType)
|
||||
{
|
||||
var inventory = GetPlayerInventory();
|
||||
|
||||
foreach (int slot in inventory.SearchItem(itemType))
|
||||
WindowAction(0, slot, WindowActionType.DropItemStack);
|
||||
}
|
||||
```
|
||||
|
||||
Use this pattern carefully:
|
||||
- verify the correct inventory ID first
|
||||
- prefer reacting to `OnInventoryUpdate(...)` for larger inventory workflows
|
||||
- for crafting or chest workflows, use `GetInventories()` and `CloseInventory(...)` as needed
|
||||
|
||||
## Built-in command bot pattern
|
||||
|
||||
Only use this when the user explicitly asks for a built-in bot.
|
||||
|
||||
```csharp
|
||||
public override void Initialize()
|
||||
{
|
||||
McClient.dispatcher.Register(l => l.Literal("help")
|
||||
.Then(l => l.Literal(CommandName)
|
||||
.Executes(r => OnCommandHelp(r.Source, string.Empty))
|
||||
)
|
||||
);
|
||||
|
||||
McClient.dispatcher.Register(l => l.Literal(CommandName)
|
||||
.Then(l => l.Literal("_help")
|
||||
.Executes(r => OnCommandHelp(r.Source, string.Empty))
|
||||
.Redirect(McClient.dispatcher.GetRoot().GetChild("help").GetChild(CommandName)))
|
||||
);
|
||||
}
|
||||
|
||||
public override void OnUnload()
|
||||
{
|
||||
McClient.dispatcher.Unregister(CommandName);
|
||||
McClient.dispatcher.GetRoot().GetChild("help").RemoveChild(CommandName);
|
||||
}
|
||||
```
|
||||
|
||||
Use a built-in bot only when the user explicitly asks for compiled MCC behavior or repo wiring.
|
||||
362
.skills/mcc-dev-workflow/SKILL.md
Normal file
362
.skills/mcc-dev-workflow/SKILL.md
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
---
|
||||
name: mcc-dev-workflow
|
||||
description: Build, run, and debug Minecraft Console Client (MCC) against a real local Minecraft Java server on Linux, macOS, or WSL. Use this whenever the user wants to compile MCC, start or inspect a local test server, connect MCC to a server, debug protocol or login issues, validate a code change end-to-end, or run MCC commands on a real server instead of guessing from static code.
|
||||
---
|
||||
|
||||
# MCC Development Workflow
|
||||
|
||||
Use this skill when the task needs a real local server loop, not just code reading.
|
||||
|
||||
## Defaults
|
||||
|
||||
- Solution: `MinecraftClient.sln`
|
||||
- Runtime target: `.NET 10` / `net10.0`
|
||||
- Environment: Linux, macOS, or WSL with Java, tmux, python3, and dotnet available
|
||||
- Default server root after `source tools/mcc-env.sh`: `${MCC_SERVERS:-<repo>/MinecraftOfficial/downloads}`
|
||||
- Default validation target when the user does not specify a version: `1.21.11`
|
||||
|
||||
## Console modes
|
||||
|
||||
MCC supports two console modes selectable via `ConsoleMode` in `[Console.General]`:
|
||||
|
||||
| Mode | Backend | Best for |
|
||||
|------|---------|----------|
|
||||
| `classic` | `ClassicConsoleBackend` (ConsoleInteractive) | Normal use, legacy CI/scripts, `FileInput` mode |
|
||||
| `tui` | `TuiConsoleBackend` (Avalonia/Consolonia) | Full-screen TUI with scrollable log, command input, popup inventory |
|
||||
|
||||
Both modes support the same commands and input/output through `ConsoleIO.Backend`. The mode is determined at startup from config; `BasicIO` CLI arg overrides to simple stdio.
|
||||
|
||||
## Core rules
|
||||
|
||||
- Prefer a real local server over static reasoning for protocol, login, movement, inventory, entity, or command-path work.
|
||||
- Treat tmux `mc-*` sessions as shared state. Do not run multi-version server workflows in parallel unless the harness explicitly isolates them.
|
||||
- For scripted or repeatable runs, use a generated temporary config. Do not edit the repo-root `MinecraftClient.ini` as part of the test loop.
|
||||
- A server log line containing `Done (` means startup finished. It does not guarantee that RCON is ready on the first attempt. Retry early `mc-rcon` commands.
|
||||
- When instructions, docs, and code disagree, trust current code and current tool behavior first.
|
||||
|
||||
## Shared server, isolated MCC sessions
|
||||
|
||||
- `mc-*` commands operate on the shared local Minecraft server.
|
||||
- `mcc-*` commands operate on one MCC client session.
|
||||
- The default `session` is the current worktree name.
|
||||
- The default username is derived from `session`, unless you pass `--username`.
|
||||
- Session files live under `${TMPDIR:-/tmp}/mcc-debug/<session>/`.
|
||||
- `MCC_SERVERS` stays the shared server-root override.
|
||||
|
||||
Keep shared servers running by default. Do not stop or reset them unless the user explicitly asks for that, or you need to switch server versions.
|
||||
|
||||
Two worktrees can debug against one shared server like this:
|
||||
|
||||
```bash
|
||||
# worktree A
|
||||
cd ~/Minecraft/Minecraft-Console-Client
|
||||
source tools/mcc-env.sh
|
||||
mc-start 1.21.11
|
||||
mcc-debug -v 1.21.11 --file-input
|
||||
|
||||
# worktree B
|
||||
cd ~/Minecraft/Minecraft-Console-Client-foo
|
||||
source tools/mcc-env.sh
|
||||
mcc-debug -v 1.21.11 --file-input
|
||||
|
||||
# from each worktree, mcc-* targets that worktree's default session
|
||||
mcc-state
|
||||
```
|
||||
|
||||
If you want two MCC sessions from the same worktree, pass `--session NAME` explicitly.
|
||||
|
||||
## tmpfs build mode
|
||||
|
||||
Use this on machines with enough RAM when you want worktree-isolated builds outside the repo tree:
|
||||
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
export MCC_BUILD_MODE=tmpfs
|
||||
mcc-build
|
||||
mcc-build-clean
|
||||
```
|
||||
|
||||
`MCC_BUILD_MODE=tmpfs` redirects build output to `/dev/shm/mcc-build/<worktree>/` on Linux, or `${TMPDIR:-/tmp}/mcc-build/<worktree>/` if `/dev/shm` is unavailable.
|
||||
|
||||
## Preflight and reset
|
||||
|
||||
Before scripted runs, especially on macOS or in a reused tmux environment:
|
||||
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
mcc-preflight 1.21.11
|
||||
mc-reset-test-env 1.21.11
|
||||
```
|
||||
|
||||
`mcc-preflight` checks Java, tmux, dotnet, python3, and server directories. It also resolves common Homebrew Java paths on macOS. `mc-reset-test-env` clears stale tmux sessions and stale `stdin.pipe` files before they turn into misleading startup failures.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
mcc-build
|
||||
```
|
||||
|
||||
Use `mcc-build` for normal local development so any `MCC_BUILD_MODE=tmpfs` routing stays active. Only use raw `dotnet build` when you are intentionally debugging the build system itself.
|
||||
|
||||
## Server management
|
||||
|
||||
Interactive shell:
|
||||
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
SESSION="$(_mcc_resolve_session)"
|
||||
USERNAME="$(_mcc_resolve_username "$SESSION")"
|
||||
mc-start 1.21.11
|
||||
mc-log 1.21.11 100
|
||||
mc-rcon "op $USERNAME"
|
||||
mc-stop 1.21.11
|
||||
```
|
||||
|
||||
Non-interactive shell:
|
||||
|
||||
```bash
|
||||
tools/start-server.sh 1.21.11
|
||||
tools/mc-rcon.sh "op mcc_smoke_a"
|
||||
```
|
||||
|
||||
If the servers live outside the repo, set `MCC_SERVERS` before sourcing or invoking the tools:
|
||||
|
||||
```bash
|
||||
export MCC_SERVERS=/home/anon/Minecraft/Servers
|
||||
source tools/mcc-env.sh
|
||||
```
|
||||
|
||||
## One-step debug session (recommended)
|
||||
|
||||
The `tools/mcc-debug.sh` script handles build, server startup, config preparation, and MCC launch in one step:
|
||||
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
|
||||
# Classic mode with FileInput (script-driven debugging):
|
||||
mcc-debug -v 1.21.11 --file-input
|
||||
|
||||
# Classic mode interactive (attach via tmux):
|
||||
mcc-debug -v 1.21.11
|
||||
|
||||
# TUI mode:
|
||||
mcc-debug -v 1.21.11 -m tui
|
||||
|
||||
# With debug messages enabled from start:
|
||||
mcc-debug -v 1.21.11 --file-input --debug-on
|
||||
|
||||
# Skip build (already built):
|
||||
mcc-debug -v 1.21.11 --file-input --no-build
|
||||
```
|
||||
|
||||
### What mcc-debug.sh does
|
||||
|
||||
1. Builds MCC (unless `--no-build`)
|
||||
2. Creates a clean temp config at `${TMPDIR:-/tmp}/mcc-debug/<session>/MinecraftClient.debug.ini`
|
||||
3. Ensures server is running (starts if not, waits for `Done (`)
|
||||
4. Launches MCC in a session-scoped tmux session and session-scoped log/input/pid files
|
||||
|
||||
### After launch
|
||||
|
||||
- **FileInput mode**: drive MCC via `mcc-cmd --session smoke-a "debug state"`, or just `mcc-cmd "debug state"` from the same worktree
|
||||
- **Interactive/TUI mode**: attach with `tmux attach -t mcc-<session>`
|
||||
- **Logs**: `mcc-log-mcc --session smoke-a` or `tail -f "${TMPDIR:-/tmp}/mcc-debug/<session>/mcc-debug.log"`
|
||||
- **Server RCON**: grant op or gamemode to the username derived from that session
|
||||
|
||||
## Debug commands (in-game)
|
||||
|
||||
### `/debug [on|off]`
|
||||
|
||||
Toggles debug logging. Now correctly syncs both `Settings.Config.Logging.DebugMessages` and `McClient.Log.DebugEnabled`.
|
||||
|
||||
### `/debug state`
|
||||
|
||||
Prints a one-shot summary of MCC's internal state:
|
||||
|
||||
```
|
||||
=== MCC Debug State ===
|
||||
Server: localhost:25565
|
||||
Username: mcc_smoke_a
|
||||
Protocol: 774
|
||||
GameMode: 1
|
||||
Health: 20.0
|
||||
Food: 20
|
||||
Location: 0.50, 80.00, 0.50
|
||||
TPS: 20.0
|
||||
Console: ClassicConsoleBackend (or TuiConsoleBackend)
|
||||
Features: Terrain Inventory Entity
|
||||
Debug: ON
|
||||
Bots (3): AutoFishing, FileInputBot, ScriptScheduler
|
||||
Players: 2 online
|
||||
```
|
||||
|
||||
This works in both classic and TUI modes.
|
||||
|
||||
## Classic mode debugging
|
||||
|
||||
### Agent workflow (FileInput mode)
|
||||
|
||||
For agents calling MCC commands programmatically:
|
||||
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
SESSION="smoke-a"
|
||||
mcc-debug -v 1.21.11 --file-input --session "$SESSION" --no-build
|
||||
|
||||
# Send commands:
|
||||
mcc-cmd --session "$SESSION" "debug state"
|
||||
mcc-cmd --session "$SESSION" "inventory player list"
|
||||
mcc-cmd --session "$SESSION" "entity"
|
||||
|
||||
# Check results:
|
||||
mcc-log-mcc --session "$SESSION"
|
||||
|
||||
# Stop:
|
||||
mcc-cmd --session "$SESSION" "quit"
|
||||
mcc-kill --session "$SESSION"
|
||||
mc-stop 1.21.11
|
||||
```
|
||||
|
||||
### Interactive workflow
|
||||
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
SESSION="live-a"
|
||||
mcc-debug -v 1.21.11 --session "$SESSION"
|
||||
|
||||
# In another terminal:
|
||||
tmux attach -t "mcc-$SESSION"
|
||||
# Type commands directly in MCC console
|
||||
```
|
||||
|
||||
## TUI mode debugging
|
||||
|
||||
TUI mode runs Consolonia full-screen in a tmux session. Key differences:
|
||||
|
||||
1. **No pipe/redirect**: TUI needs a real tty. Cannot `| tee` or redirect stdout.
|
||||
2. **Log output is in-screen**: all output appears in the scrollable log area.
|
||||
3. **Keyboard shortcuts**: PageUp/PageDown scroll, ESC exits.
|
||||
4. **`/debug state`**: the primary way to inspect internal state since external log tailing is not available.
|
||||
5. **Dialog windows**: `/inventui` opens as an overlay dialog instead of a separate screen.
|
||||
|
||||
### Agent workflow for TUI mode
|
||||
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
SESSION="tui-a"
|
||||
mcc-debug -v 1.21.11 -m tui --session "$SESSION" --no-build
|
||||
|
||||
# Cannot use mcc-cmd (no FileInput); must use tmux send-keys:
|
||||
tmux send-keys -t "mcc-$SESSION" "/debug state" Enter
|
||||
|
||||
# Read TUI screen:
|
||||
tmux capture-pane -t "mcc-$SESSION" -p -S -30
|
||||
|
||||
# Stop:
|
||||
tmux send-keys -t "mcc-$SESSION" Escape
|
||||
```
|
||||
|
||||
**Caveat with tmux send-keys and Consolonia**: When sending text containing `/`, the Enter key may need to be sent separately:
|
||||
```bash
|
||||
tmux send-keys -t "mcc-$SESSION" "/inventory player list"
|
||||
tmux send-keys -t "mcc-$SESSION" Enter
|
||||
```
|
||||
|
||||
## mcc-env.sh quick reference
|
||||
|
||||
After `source tools/mcc-env.sh`:
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `mc-start VER` | Start MC server in tmux |
|
||||
| `mc-stop VER` | Graceful stop via stdin pipe |
|
||||
| `mc-log VER [N]` | Capture last N lines of server output |
|
||||
| `mc-rcon "CMD"` | Send RCON command |
|
||||
| `mc-kill VER` | Force-kill server tmux session |
|
||||
| `mc-list` | List running MC server sessions |
|
||||
| `mc-wait-ready VER [SEC]` | Wait for server `Done (` |
|
||||
| `mc-wait-stop VER [SEC]` | Wait for server shutdown, with force-kill fallback |
|
||||
| `mc-reset-test-env [--all|VER...]` | Reset shared tmux server state and stale pipes |
|
||||
| `mcc-build` | Build MCC |
|
||||
| `mcc-publish --rid <RID>` | Publish MCC with the repo's CI-like defaults |
|
||||
| `mcc-build-clean` | Clear the current worktree's build output |
|
||||
| `mcc-run [--session NAME] [--username NAME] [--port PORT]` | Convenience wrapper for `mcc-debug --file-input --no-build` |
|
||||
| `mcc-tui [--session NAME] [--username NAME] [--port PORT]` | Convenience wrapper for `mcc-debug -m tui --no-build` |
|
||||
| `mcc-cmd [--session NAME] "CMD"` | Append a command to one session's input file |
|
||||
| `mcc-kill [--session NAME]` | Kill one MCC process and session |
|
||||
| `mcc-debug [OPTS]` | One-step debug session (see above) |
|
||||
| `mcc-log-mcc [--session NAME]` | Tail one MCC debug log |
|
||||
| `mcc-state [--session NAME]` | Send `debug state` and print the last 30 log lines |
|
||||
| `mcc-preflight [VER...]` | Verify Java, tmux, dotnet, python3, and server dirs |
|
||||
|
||||
## Temporary config recipe
|
||||
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
SESSION="smoke-a"
|
||||
USERNAME="$(_mcc_resolve_username "$SESSION")"
|
||||
CFG="$(_mcc_session_root "$SESSION")/MinecraftClient.debug.ini"
|
||||
mkdir -p "$(_mcc_session_root "$SESSION")"
|
||||
bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" \
|
||||
"$CFG" \
|
||||
"1.21.11" \
|
||||
"$USERNAME"
|
||||
```
|
||||
|
||||
For TUI mode, also add:
|
||||
```bash
|
||||
sed -i 's/ConsoleMode = "classic"/ConsoleMode = "tui"/' "$CFG"
|
||||
```
|
||||
|
||||
## Verify connection and a basic command
|
||||
|
||||
MCC output should include:
|
||||
|
||||
- `[MCC] Server was successfully joined.`
|
||||
|
||||
Server output should include the session-derived username, for example:
|
||||
|
||||
- `mcc_smoke_a joined the game`
|
||||
|
||||
Basic command check:
|
||||
|
||||
```bash
|
||||
mcc-cmd --session smoke-a "inventory player list"
|
||||
```
|
||||
|
||||
If a scripted run fails before MCC joins, check for a harness problem before assuming a product regression. Missing `mcc.log`, a pre-join `Connection refused`, or a server that never reached `Done (` usually means shared-state cleanup or startup failed.
|
||||
|
||||
## Typical debug loop
|
||||
|
||||
1. `source tools/mcc-env.sh`
|
||||
2. `mcc-debug -v 1.21.11 --file-input` (or `-m tui`)
|
||||
3. Confirm `Server was successfully joined` in log
|
||||
4. `mcc-cmd "debug state"` to verify MCC state
|
||||
5. Run test commands
|
||||
6. Inspect log output
|
||||
7. `mcc-cmd "quit"` and `mc-stop 1.21.11`
|
||||
8. Edit code, rebuild, repeat
|
||||
|
||||
## Debugging tips
|
||||
|
||||
- **`/debug state` is your primary diagnostic tool** in both modes. Use it first to verify connection, mode, and feature flags.
|
||||
- **`/debug on` now correctly enables debug logging** at runtime. Previous versions had a bug where `Log.DebugEnabled` was not synced.
|
||||
- Protocol mismatches usually show up as a version line such as `Server version : 1.21.11 (protocol vNNN)` before the failure.
|
||||
- If an early `mc-rcon` command fails, retry it before assuming the server setup is broken.
|
||||
- If a supposedly isolated run behaves strangely, check `tmux list-sessions` and kill stale `mc-*` sessions first.
|
||||
- Legacy `1.8` and `1.8.9` servers may need `use-native-transport=false` in `server.properties` on some Linux environments.
|
||||
- For timing-sensitive work, do not trust wall-clock intuition. Use a real server run and capture evidence from logs or test scripts.
|
||||
- **TUI mode tip**: if the terminal becomes unresponsive after a crash, run `stty sane && reset` to restore it.
|
||||
- **tmux capture trick**: `tmux capture-pane -t mcc-<session> -p -S -50` captures the last 50 lines of a tmux session without attaching.
|
||||
|
||||
## Tool files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `tools/mcc-env.sh` | Shell functions for server/MCC management |
|
||||
| `tools/mcc-debug.sh` | One-step debug session launcher |
|
||||
| `tools/mcc-log-tail.sh` | Log tailing for MCC and/or server |
|
||||
| `tools/start-server.sh` | Server lifecycle in tmux |
|
||||
| `tools/mc-rcon.sh` | RCON command sender |
|
||||
| `tools/run-creative-e2e.sh` | Full creative mode end-to-end test |
|
||||
41
.skills/mcc-dev-workflow/evals/evals.json
Normal file
41
.skills/mcc-dev-workflow/evals/evals.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"skill_name": "mcc-dev-workflow",
|
||||
"evals": [
|
||||
{
|
||||
"id": 1,
|
||||
"prompt": "Build MCC, start the local 1.21.11 vanilla server from an MCC_SERVERS root, connect MCC with a temporary config, and verify a successful join plus one inventory command.",
|
||||
"expected_output": "The workflow uses a real local server, a temp MCC config, and concrete log evidence for both the join and the MCC command.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"The workflow uses a real local 1.21.11 server instead of only reading code.",
|
||||
"The workflow uses MCC_SERVERS-aware tooling or documents the server root explicitly.",
|
||||
"The workflow uses a temporary MCC config instead of relying on the repo-root config.",
|
||||
"The result includes join evidence from MCC output and the server log."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"prompt": "Debug a flaky local MCC startup on 1.21.11 by checking for stale tmux server sessions, waiting for server readiness, and retrying early RCON commands before blaming protocol code.",
|
||||
"expected_output": "The response treats tmux sessions as shared state, distinguishes server startup from RCON readiness, and uses the real local workflow rather than pure speculation.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"The workflow checks or mentions stale mc-* tmux sessions.",
|
||||
"The workflow distinguishes Done from RCON readiness.",
|
||||
"The workflow retries or advises retrying early RCON commands.",
|
||||
"The workflow keeps the debugging loop grounded in real local commands."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"prompt": "Run a repeatable local MCC debug loop for 1.21.11 that compiles the client, connects with movement, inventory, and entity handling enabled, and leaves enough evidence to inspect a regression afterward.",
|
||||
"expected_output": "The response follows a real build-run-test-inspect loop and captures enough log evidence to support follow-up debugging.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"The workflow builds MCC before the run.",
|
||||
"The workflow enables terrain, inventory, and entity handling for the scripted run.",
|
||||
"The workflow captures or points to concrete log locations.",
|
||||
"The workflow prefers a temp config for repeatability."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
323
.skills/mcc-integration-testing/SKILL.md
Normal file
323
.skills/mcc-integration-testing/SKILL.md
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
---
|
||||
name: mcc-integration-testing
|
||||
description: >-
|
||||
Use when proving MCC behavior on a real local Minecraft server, validating
|
||||
runtime or protocol changes end-to-end, exercising movement, physics,
|
||||
inventory, entity, chat, or terrain behavior, or running a single-version or
|
||||
cross-version regression sweep.
|
||||
metadata:
|
||||
category: discipline
|
||||
triggers:
|
||||
- integration test
|
||||
- real server
|
||||
- local server
|
||||
- regression sweep
|
||||
- rcon
|
||||
- tmux
|
||||
- offline mode
|
||||
- online mode
|
||||
- movement
|
||||
- physics
|
||||
- inventory
|
||||
- entity
|
||||
- terrain
|
||||
- chat
|
||||
---
|
||||
|
||||
# MCC Integration Testing
|
||||
|
||||
Use this skill when the task is "prove it on a real server", not just "reason about whether it should work."
|
||||
|
||||
Read [references/online-mode.md](references/online-mode.md) when the user asks for Microsoft login, device-code auth, or an online-mode server run. Use [references/command-matrix.md](references/command-matrix.md) for stable MCC-side and RCON-side commands.
|
||||
|
||||
## Iron Law
|
||||
|
||||
Only say MCC was integration tested when MCC ran against a real local server and the claim is backed by real MCC output plus real server logs.
|
||||
|
||||
Calling build-only, reasoning-only, or join-only work "integration tested" is a rules violation, not shorthand.
|
||||
|
||||
These do not count as end-to-end proof:
|
||||
|
||||
- static reasoning, source comparison, or build success
|
||||
- join or login success by itself
|
||||
- a long-lived idle connection by itself
|
||||
- a grep that only says there were no errors
|
||||
- testing one shared-route version and silently claiming adjacent versions also passed
|
||||
|
||||
If the environment cannot run a real server, say so and report the result as unexecuted or inferred, not integration tested.
|
||||
|
||||
## Default target
|
||||
|
||||
- Use `1.21.11-Vanilla` unless the user asks for a different version or a version matrix.
|
||||
- Use `MCC_SERVERS` if it is set. Otherwise the default server root is `MinecraftOfficial/downloads`.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Use a real local server.
|
||||
- Launch MCC against an explicit `localhost:<server-port>` target for repeatable local tests.
|
||||
- Keep version matrices sequential in shared local environments. The tmux server harness is shared state by default.
|
||||
- Prefer generated temporary MCC configs for scripted runs so one test does not contaminate the next.
|
||||
- Default to offline auth in generated temp configs. Do not trust the repo-root `MinecraftClient.ini` account defaults.
|
||||
- If the user explicitly asks for Microsoft online login, honor that request and generate the temp config for Microsoft auth instead of offline mode.
|
||||
- For Microsoft auth, prefer an interactive TTY launch with `BasicIO-NoColor` so the device code is easy to read and relay to the user.
|
||||
- Do not use file-input mode during Microsoft auth. Launch interactively first, complete login, then switch to scripted control only if needed.
|
||||
- For online-mode tests, prefer a clean temp config with no join-time bots or scheduled tasks. Inherited `ScriptScheduler` or `DiscordRpc` settings can pollute the session and send unintended chat right after login.
|
||||
- Legacy and modern command syntax differ. Do not assume one server-command profile fits every version.
|
||||
- Use actual MCC output and actual server logs for assertions. Do not invent success strings.
|
||||
- Treat server `Done` as startup progress, not RCON readiness. Retry the first RCON command before assuming the setup is broken.
|
||||
- Run preflight before scripted test loops. On macOS, Java may be installed but not exported on PATH in the shell the harness uses.
|
||||
- If a change touches shared routing or a version range, test at least one adjacent version that shares that path, or explicitly mark adjacent versions as unexecuted and inferred.
|
||||
- For palette or version-content changes, probe at least one neighboring or existing item, entity, or block. Do not only check the headline addition.
|
||||
- Separate product failures from harness failures. Missing logs, stale tmux state, stale `stdin.pipe`, or pre-join `Connection refused` errors are usually environment problems until proven otherwise.
|
||||
|
||||
## Choose the test mode
|
||||
|
||||
### 1. Single-version deep smoke
|
||||
|
||||
Use this when one supported version is enough and you want broad coverage:
|
||||
|
||||
```bash
|
||||
.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh 1.21.11-Vanilla
|
||||
```
|
||||
|
||||
This covers join, chat, slash commands, internal MCC commands, creative inventory, entity handling, sounds, particles, TNT, kill/respawn, and log assertions.
|
||||
|
||||
### 2. Ordered creative-mode E2E
|
||||
|
||||
Use this when the user asks for a regression sweep in a strict scenario order such as:
|
||||
|
||||
- connect
|
||||
- send messages
|
||||
- send commands
|
||||
- receive messages
|
||||
- movement
|
||||
- physics
|
||||
- mobs
|
||||
- effects
|
||||
- inventory
|
||||
|
||||
Broad validation should usually cover `connect-test`, `item-test`, `entity-test`, `terrain-test`, and `chat-test`.
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
MCC_SERVERS=/home/anon/Minecraft/Servers bash tools/run-creative-e2e.sh 1.21.11-Vanilla 1.21.11 modern
|
||||
```
|
||||
|
||||
For legacy targets such as `1.8` or `1.8.9`, switch the final argument to `legacy` and pass the pinned MC version.
|
||||
|
||||
### 3. Timing or cadence validation
|
||||
|
||||
Use this for TPS, movement-cadence, or packet-cadence work:
|
||||
|
||||
- `MinecraftClient/config/sample-script-tick-counter.cs`
|
||||
- `MinecraftClient/config/sample-script-packet-capture.cs`
|
||||
|
||||
Run them against a real server with a temp config and summarize counts from the captured logs.
|
||||
|
||||
### 4. Structured components test
|
||||
|
||||
Use this after touching any `StructuredComponents` code (registries, component
|
||||
parsers, subcomponents, codec helpers) to prove every component type in a
|
||||
version parses on the wire without error:
|
||||
|
||||
```bash
|
||||
bash tools/run-structured-components-test.sh 1.21.11
|
||||
```
|
||||
|
||||
Run a single version (fast, ~2 min) or a matrix:
|
||||
|
||||
```bash
|
||||
for v in 1.20.6 1.21 1.21.2 1.21.5 1.21.11 26.1; do
|
||||
bash tools/run-structured-components-test.sh "$v"
|
||||
done
|
||||
```
|
||||
|
||||
The script gives items with every registered component via RCON `/give`, reads
|
||||
them back with `inventory player list`, and asserts no parse errors in the MCC
|
||||
log. Version-gated components (v1212+, v1215+, v12111+, v261) are tested only
|
||||
on the versions that support them. See `SC_Integration_Test_Report.md` for a
|
||||
reference run across all 6 version groups.
|
||||
|
||||
### 6. Dialog integration test
|
||||
|
||||
Use this after touching any dialog system code (packet handling, NBT parsing,
|
||||
models, TUI, command dispatch, the state machine in `DialogManager`, or the
|
||||
codec in `DialogNbtParser`). Tests all 5 dialog types, button actions (close,
|
||||
run_command, show_dialog), cancel/dismiss, click-label, and body content:
|
||||
|
||||
```bash
|
||||
tools/run-dialog-test.sh 26.1
|
||||
```
|
||||
|
||||
The script starts the server if needed, generates a temp MCC config, launches
|
||||
MCC with file-input mode (requires both `MCC_FILE_INPUT=1` and
|
||||
`MCC_INPUT_FILE=<path>` env vars), sends inline SNBT dialogs via RCON, and
|
||||
asserts 29 checks against the MCC log.
|
||||
|
||||
Key requirements that differ from other test modes:
|
||||
|
||||
- FileInputBot is loaded only when `MCC_FILE_INPUT=1` is set in the
|
||||
environment. The `[ChatBot.FileInput]` config section is ignored at load
|
||||
time.
|
||||
- The input file path is controlled by `MCC_INPUT_FILE`, *not* by the config
|
||||
`File` setting.
|
||||
- Dialogs use inline SNBT syntax through `ResourceOrIdArgument`, e.g.:
|
||||
`dialog show <player> {type:"minecraft:notice", title:{text:"Hello"}}`
|
||||
- The `ActionButton.CODEC` flattens `CommonButtonData` fields (`label`,
|
||||
`tooltip`, `width`) into the same object as `action` — no `button` wrapper.
|
||||
|
||||
### 5. Full inventory regression sweep
|
||||
|
||||
Use this when touching inventory snapshots, player/container slot sync, creative inventory, item-slot serialization, packet palettes, game-mode updates, or block-use paths that open containers:
|
||||
|
||||
```bash
|
||||
tools/run-inventory-full-sweep.sh --versions "1.21.10 1.21.11"
|
||||
```
|
||||
|
||||
Default coverage includes:
|
||||
|
||||
- player inventory listing and inventory discovery
|
||||
- creative give/delete
|
||||
- inventory search
|
||||
- player right/left click stack split and merge
|
||||
- player drop one and drop all
|
||||
- chest open via `useblock`
|
||||
- container listing and close
|
||||
- mirrored player slots in container windows
|
||||
- shift-click and shift-right-click transfer
|
||||
- container right/left click, cursor stack, drop one, and drop all
|
||||
- creative middle-click command path
|
||||
- log scan for packet parse failures, queue-empty crashes, unhandled exceptions, and disconnects
|
||||
|
||||
The script writes `summary.tsv` under `RUN_ROOT` and per-version logs under `/tmp/mcc-debug/inventory-full-<version>/mcc-debug.log`.
|
||||
|
||||
When a matrix has existing PASS rows, do not rerun them unless a later code change affects that row or the user asks for a full rerun. Derive remaining rows from summaries:
|
||||
|
||||
```bash
|
||||
awk 'FNR>1 && $2=="PASS" {print $1}' /tmp/mcc-inventory-full-sweep/*/summary.tsv | sort -V | uniq
|
||||
```
|
||||
|
||||
## Preconditions
|
||||
|
||||
Before running any scenario:
|
||||
|
||||
0. run preflight and clear stale shared state when the environment is reused
|
||||
1. configure the target server for offline testing
|
||||
2. ensure `eula=true`
|
||||
3. ensure RCON is enabled
|
||||
4. build MCC unless the task explicitly reuses a fresh build
|
||||
|
||||
Preflight and reset helpers:
|
||||
|
||||
```bash
|
||||
.skills/mcc-integration-testing/scripts/preflight_test_env.sh 1.21.11-Vanilla
|
||||
.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh 1.21.11-Vanilla
|
||||
```
|
||||
|
||||
Offline configuration helper:
|
||||
|
||||
```bash
|
||||
.skills/mcc-integration-testing/scripts/ensure_offline_server.sh 1.21.11-Vanilla
|
||||
```
|
||||
|
||||
By default, the config helper prepares offline auth. To opt into another auth mode for a specific run, set:
|
||||
|
||||
```bash
|
||||
MCC_TEST_ACCOUNT_TYPE=microsoft
|
||||
MCC_TEST_PASSWORD=
|
||||
```
|
||||
|
||||
Optionally override the login name with the fourth argument to the config helper.
|
||||
|
||||
## Scripts and tools
|
||||
|
||||
- `.skills/mcc-integration-testing/scripts/ensure_offline_server.sh`
|
||||
- configures persistent offline mode and RCON
|
||||
- `.skills/mcc-integration-testing/scripts/preflight_test_env.sh`
|
||||
- verifies Java, tmux, dotnet, python3, server directories, and resolves common Java PATH issues
|
||||
- `.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh`
|
||||
- clears stale tmux sessions and stale `stdin.pipe` files before a rerun
|
||||
- `.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh`
|
||||
- generates a clean temporary MCC config, prepares offline login by default, disables noisy bots, and can switch to Microsoft auth when explicitly requested
|
||||
- `.skills/mcc-integration-testing/scripts/get_server_port.sh`
|
||||
- resolves the actual local server port from `server.properties` or the latest server log
|
||||
- `.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh`
|
||||
- single-version deep smoke with built-in assertions
|
||||
- `.skills/mcc-integration-testing/scripts/summarize_test_run.sh`
|
||||
- summarize the latest full-spectrum run
|
||||
- `tools/run-creative-e2e.sh`
|
||||
- ordered creative-mode E2E regression scenario
|
||||
- `tools/run-inventory-full-sweep.sh`
|
||||
- full inventory command/API sweep across one or more versions
|
||||
- `tools/run-structured-components-test.sh`
|
||||
- exercises every structured component via RCON `/give` across versions 1.20.6-26.1
|
||||
- `tools/run-dialog-test.sh`
|
||||
- dialog integration test: all 5 types, run_command/show_dialog actions,
|
||||
cancel/dismiss/click-label, body content; 29 assertions on MCC log
|
||||
|
||||
## Evidence Discipline
|
||||
|
||||
In every report, separate:
|
||||
|
||||
- `Executed`: exact scripts, commands, versions, auth mode, and whether the run was sequential or single-version
|
||||
- `Observed`: exact MCC output, exact server-log evidence, and the saved log directory
|
||||
- `Inferred`: conclusions not directly shown by that run's runtime evidence
|
||||
- `Harness issues`: setup or runner problems such as missing Java on PATH, stale tmux sessions, stale `stdin.pipe`, missing log artifacts, or failed config generation
|
||||
|
||||
Never upgrade inferred claims to observed facts. Absence of errors is supporting evidence only; pair it with a positive assertion for the feature under test.
|
||||
|
||||
## Red Flags
|
||||
|
||||
Stop and fix the test plan if you are about to:
|
||||
|
||||
- claim movement, inventory, entity, terrain, physics, or chat coverage from join success alone
|
||||
- reuse repo-root `MinecraftClient.ini` or another user-local stateful config
|
||||
- run multi-version tests in parallel in a shared tmux or shared server environment
|
||||
- let inherited bots, schedulers, or other user-local noise send chat or commands during validation
|
||||
|
||||
## What to report back
|
||||
|
||||
Always summarize:
|
||||
|
||||
- which version or versions were tested
|
||||
- which port or ports were used
|
||||
- which auth mode and scenario were used
|
||||
- whether the run was sequential or single-version
|
||||
- the exact scripts or commands executed
|
||||
- pass or fail per major phase
|
||||
- concrete evidence from MCC and server logs
|
||||
- the saved log directory
|
||||
- what was not executed and what remains inferred
|
||||
- which adjacent versions were not run but were mentioned
|
||||
|
||||
## When Not to Use
|
||||
|
||||
- build-only verification
|
||||
- static protocol or source comparison with no real server run
|
||||
- documentation or prompt work
|
||||
- code review requests that do not ask for executed runtime proof
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If the first RCON command fails, retry it before assuming the setup is broken.
|
||||
- If Java is installed but the harness still says it is missing, run `preflight_test_env.sh`. This resolves common Homebrew Java paths on macOS.
|
||||
- If MCC reaches Microsoft device-code login during an offline test, stop and inspect the generated temp config before retrying.
|
||||
- If the user explicitly requests Microsoft online login, set `MCC_TEST_ACCOUNT_TYPE=microsoft` before launching the harness.
|
||||
- If the user explicitly requests Microsoft online login, use `BasicIO-NoColor` in a real TTY, relay the device code from the TUI, and avoid pressing empty Enter at any auth prompt.
|
||||
- If the online-mode session sends unexpected chat or commands right after join, inspect inherited bot settings first. `ChatBot.ScriptScheduler` tasks and `ChatBot.DiscordRpc` are common sources of test noise in user-local configs.
|
||||
- If `dotnet run` cannot see an existing Microsoft session, check whether `SessionCache.db` and `ProfileKeyCache.ini` need to be synced from `MinecraftClient/bin/Release/net10.0/` to the repo root.
|
||||
- If Microsoft auth keeps prompting even with a valid session cache, verify `Account.Login` matches the cached username exactly.
|
||||
- If MCC reports `Connection refused`, verify the launched target matches the server's actual `server-port`.
|
||||
- If MCC reports `Connection refused` immediately after a server start, also check for stale shared state: old tmux sessions, a stale `stdin.pipe`, or a server that never actually reached `Done (`.
|
||||
- If multiple versions are being tested, do not start them in parallel unless the harness isolates tmux sessions and input files.
|
||||
- If a test assertion fails, inspect the real MCC output before changing the code or weakening the assertion.
|
||||
- If an older server behaves oddly on Linux, check `use-native-transport=false` in `server.properties`.
|
||||
- If a matrix row fails before producing `mcc.log` or a command transcript, treat it as a harness failure, fix the environment, and rerun that row before drawing product conclusions.
|
||||
- If creative inventory commands report "You must be in Creative gamemode" after RCON switched the player, inspect game-mode update parsing before assuming creative inventory is broken. Modern servers can update local game mode through game event reason `3`.
|
||||
- If an inventory row crashes with `Queue empty` or `Failed to process incoming packet`, inspect packet palette routing before changing inventory code. A single shifted packet ID can make a healthy inventory feature look broken.
|
||||
- For chest-open failures, separate product and harness causes. The player may be standing inside the chest or suffocating on older servers. Stand beside the chest, put a floor under the player, and retry `useblock`.
|
||||
- For shared local servers, a `Done` log line does not prove RCON is ready. Retry setup commands and verify the actual RCON port from `server.properties`.
|
||||
- If `tools/run-dialog-test.sh` fails with "FileInput Watching: .../mcc_input.txt" pointing to the wrong directory, the `MCC_INPUT_FILE` env var was not set in the tmux command. FileInputBot ignores the config `File` setting entirely.
|
||||
- If inline SNBT dialogs fail on the server side (`Failed to parse structure: No key ...`), check whether `ActionButton.CODEC` fields are flat (no `button` wrapper) and whether the dialog type fields match the 26.1 server (`label` not `text` in `CommonButtonData`).
|
||||
- If a dialog integration test fails on "Server showed custom dialog", the dialog packet (id=0x8C in 26.1 play phase) may not have been sent. Verify the RCON command succeeded and the server printed "Displayed dialog to ...".
|
||||
41
.skills/mcc-integration-testing/evals/evals.json
Normal file
41
.skills/mcc-integration-testing/evals/evals.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"skill_name": "mcc-integration-testing",
|
||||
"evals": [
|
||||
{
|
||||
"id": 1,
|
||||
"prompt": "Run a real 1.21.11 MCC regression test in creative mode covering connect, send messages, send commands, receive messages, movement, physics, mobs, effects, and inventory, in that order.",
|
||||
"expected_output": "The workflow uses the ordered creative-mode E2E harness on a real server, reports pass or fail for each phase, and points to the saved logs.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"The workflow uses a real 1.21.11 server.",
|
||||
"The workflow uses the ordered creative E2E harness instead of improvising the whole scenario.",
|
||||
"The result reports phase-by-phase outcomes in the requested order.",
|
||||
"The result includes the log directory for the run."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"prompt": "Validate that MCC still works after a runtime or protocol change by running a deep single-version 1.21.11 integration test with chat, creative inventory, entity tracking, sounds, particles, TNT, and kill/respawn coverage.",
|
||||
"expected_output": "The workflow uses the full-spectrum test runner on a real server and returns a concise pass or fail summary backed by MCC and server log evidence.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"The workflow builds MCC before the scenario unless a fresh build is explicitly reused.",
|
||||
"The workflow uses the full-spectrum runner instead of only manual spot checks.",
|
||||
"The result includes evidence from both MCC output and server logs.",
|
||||
"The result points to the saved run directory."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"prompt": "Validate a timing-sensitive MCC change on a real 1.21.11 server by collecting tick-rate and outbound packet-cadence evidence, then summarize the results clearly.",
|
||||
"expected_output": "The response uses a real server, a temp config, and the provided sample scripts to capture tick-rate and packet evidence instead of relying on intuition.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"The workflow uses the real 1.21.11 server.",
|
||||
"The workflow uses the tick counter and packet capture scripts or clearly equivalent targeted instrumentation.",
|
||||
"The workflow keeps the run isolated with a temp config.",
|
||||
"The summary reports concrete counts or cadence evidence."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
91
.skills/mcc-integration-testing/references/command-matrix.md
Normal file
91
.skills/mcc-integration-testing/references/command-matrix.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# Command Matrix
|
||||
|
||||
This skill uses a fixed set of stable commands for local offline integration testing.
|
||||
|
||||
## MCC-side commands via `mcc-cmd`
|
||||
|
||||
- `health`
|
||||
- `list`
|
||||
- `inventory player list`
|
||||
- `/gamemode creative`
|
||||
- `inventory creativegive 36 Diamond 16`
|
||||
- `inventory creativegive 37 IronSword 1`
|
||||
- `inventory creativegive 38 GoldenApple 8`
|
||||
- `inventory creativeclear 38`
|
||||
- `entity`
|
||||
- `/time query daytime`
|
||||
- `look up`
|
||||
- `look down`
|
||||
- `look east`
|
||||
- `/gamemode survival`
|
||||
- `respawn`
|
||||
- `/tp MCCBot 0 -60 0`
|
||||
- `smoke_test_from_mcc_full_spectrum`
|
||||
- `integration_test_chat_response`
|
||||
|
||||
Notes:
|
||||
- Lines starting with `/` are sent to the server as chat/commands.
|
||||
- Non-slash lines are treated as MCC internal commands first, then fall back to chat.
|
||||
|
||||
## Server-side commands via `mc-rcon`
|
||||
|
||||
- `op MCCBot`
|
||||
- `gamerule sendCommandFeedback true`
|
||||
- `gamerule logAdminCommands true`
|
||||
- `time set day`
|
||||
- `weather clear`
|
||||
- `say Hello from the server console`
|
||||
- `msg MCCBot This is a private whisper`
|
||||
- `effect give MCCBot minecraft:speed 30 1`
|
||||
- `effect give MCCBot minecraft:regeneration 10 1`
|
||||
- `kill MCCBot`
|
||||
|
||||
## Representative entity coverage
|
||||
|
||||
- `execute as MCCBot at @s run summon minecraft:cow ~2 ~ ~`
|
||||
- `execute as MCCBot at @s run summon minecraft:zombie ~4 ~ ~`
|
||||
- `execute as MCCBot at @s run summon minecraft:creeper ~6 ~ ~`
|
||||
- `execute as MCCBot at @s run summon minecraft:skeleton ~8 ~ ~`
|
||||
- `execute as MCCBot at @s run summon minecraft:villager ~-2 ~ ~`
|
||||
- `execute as MCCBot at @s run summon minecraft:allay ~-4 ~ ~`
|
||||
- `execute as MCCBot at @s run summon minecraft:armor_stand ~ ~ ~2`
|
||||
- `execute as MCCBot at @s run summon minecraft:item_display ~-6 ~ ~ {item:{id:"minecraft:diamond",count:1}}`
|
||||
- `execute as MCCBot at @s run summon minecraft:spider ~10 ~ ~`
|
||||
- `execute as MCCBot at @s run summon minecraft:pig ~-8 ~ ~`
|
||||
|
||||
## Block placement coverage
|
||||
|
||||
- `execute as MCCBot at @s run fill ~1 ~ ~1 ~3 ~2 ~3 minecraft:stone`
|
||||
- `execute as MCCBot at @s run setblock ~5 ~ ~5 minecraft:chest`
|
||||
- `execute as MCCBot at @s run setblock ~5 ~1 ~5 minecraft:furnace`
|
||||
- `execute as MCCBot at @s run setblock ~6 ~ ~5 minecraft:crafting_table`
|
||||
|
||||
## Dimension change coverage
|
||||
|
||||
- `execute in minecraft:the_nether run tp MCCBot 0 64 0`
|
||||
- `execute in minecraft:overworld run tp MCCBot 0 -60 0`
|
||||
|
||||
## Representative particle coverage
|
||||
|
||||
- `execute as MCCBot at @s run particle minecraft:happy_villager ~ ~1 ~ 0.5 0.5 0.5 0 12 force`
|
||||
- `execute as MCCBot at @s run particle minecraft:end_rod ~ ~1 ~ 0.5 0.5 0.5 0.01 20 force`
|
||||
- `execute as MCCBot at @s run particle minecraft:explosion ~ ~1 ~ 0 0 0 0 1 force`
|
||||
- `execute as MCCBot at @s run particle minecraft:totem_of_undying ~ ~1 ~ 0.5 0.5 0.5 0.1 20 force`
|
||||
- `execute as MCCBot at @s run particle minecraft:flame ~ ~1 ~ 0.2 0.2 0.2 0.02 30 force`
|
||||
- `execute as MCCBot at @s run particle minecraft:heart ~ ~2 ~ 0.3 0.3 0.3 0 5 force`
|
||||
|
||||
## Representative sound coverage
|
||||
|
||||
- `execute as MCCBot at @s run playsound minecraft:entity.lightning_bolt.thunder master MCCBot ~ ~ ~ 1 1 0`
|
||||
- `execute as MCCBot at @s run playsound minecraft:block.note_block.bell master MCCBot ~ ~ ~ 1 1 0`
|
||||
- `execute as MCCBot at @s run playsound minecraft:entity.experience_orb.pickup master MCCBot ~ ~ ~ 1 1 0`
|
||||
|
||||
## Explosion coverage
|
||||
|
||||
- `execute as MCCBot at @s run summon minecraft:tnt ~3 ~ ~`
|
||||
- `execute as MCCBot at @s run summon minecraft:tnt ~6 ~ ~`
|
||||
|
||||
## Kill and respawn cycle
|
||||
|
||||
- `kill MCCBot` (via RCON, requires survival mode)
|
||||
- `respawn` (via MCC command after death)
|
||||
48
.skills/mcc-integration-testing/references/online-mode.md
Normal file
48
.skills/mcc-integration-testing/references/online-mode.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Online-Mode Notes
|
||||
|
||||
Use this flow only when the user explicitly asks for Microsoft login or wants to validate against an online-mode server.
|
||||
|
||||
## Launch mode
|
||||
|
||||
- Prefer `BasicIO-NoColor` in a real TTY so the Microsoft device-code prompt is easy to read and copy.
|
||||
- Do not use `MCC_FILE_INPUT=1` during the auth step. It is for scripted command injection, not interactive login.
|
||||
- Avoid `nohup`. Use `tmux` for long-running sessions that still need a TTY.
|
||||
- Start from a clean temp config when possible. If the temp config is copied from a user-local `MinecraftClient.ini`, inspect `ChatBot.ScriptScheduler` and `ChatBot.DiscordRpc` before the run.
|
||||
|
||||
## Session cache behavior
|
||||
|
||||
- `dotnet run --project MinecraftClient ...` uses the repo root for `SessionCache.db` and `ProfileKeyCache.ini`.
|
||||
- The compiled binary under `MinecraftClient/bin/<Config>/net10.0/` uses that output directory instead.
|
||||
- If a session exists in one location and not the other, sync the cache files before assuming login is broken.
|
||||
|
||||
## Account settings
|
||||
|
||||
- `Account.Login` must be populated for MCC to look up a cached Microsoft session.
|
||||
- The cached key is the username form MCC stored, typically the lowercase username, not necessarily the email address.
|
||||
- MCC rewrites `MinecraftClient.ini` on clean exit, so generate a temp config per run and do not edit it while MCC is still running.
|
||||
|
||||
## Auth prompt handling
|
||||
|
||||
- Do not send a bare Enter to dismiss `Password(invisible):` or `Paste your code here:` prompts. That can trigger offline fallback.
|
||||
- For interactive online-mode runs, wait for the device code prompt and relay the code to the user exactly as shown.
|
||||
- After the user completes login, continue the test in the same TTY session or restart into file-driven mode if the workflow requires automation.
|
||||
|
||||
## Join-time noise
|
||||
|
||||
- Real user configs may contain enabled bots or task lists that were harmless in offline testing but are noisy in online-mode validation.
|
||||
- The most common examples are:
|
||||
- `ChatBot.ScriptScheduler` task lists that send `/hello`, `/login ...`, or other automatic commands on login or on an interval
|
||||
- `ChatBot.DiscordRpc`, which is not harmful to server state but adds log noise and extra background activity
|
||||
- If the goal is protocol or feature validation, suppress these before the run or treat their output as non-test noise.
|
||||
|
||||
## Server settings
|
||||
|
||||
- For realistic online-mode testing, keep `online-mode=true`.
|
||||
- Keep `enforce-secure-profile=true` unless the test explicitly targets insecure-profile behavior.
|
||||
|
||||
## Command reminders
|
||||
|
||||
- With `InternalCmdChar = "slash"`:
|
||||
- `/health`, `/pos`, `/inventory`, `/entity` are MCC internal commands.
|
||||
- `/send /list` and `/send /give ...` are server commands.
|
||||
- bare text is regular chat sent to the server.
|
||||
110
.skills/mcc-integration-testing/scripts/common.sh
Executable file
110
.skills/mcc-integration-testing/scripts/common.sh
Executable file
|
|
@ -0,0 +1,110 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
sed_in_place() {
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
sed -i '' "$@"
|
||||
else
|
||||
sed -i "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_java_in_path() {
|
||||
if command -v java >/dev/null 2>&1 && java -version >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local candidate
|
||||
for candidate in \
|
||||
"${JAVA_BIN:-}" \
|
||||
"/opt/homebrew/opt/openjdk/bin/java" \
|
||||
"/usr/local/opt/openjdk/bin/java" \
|
||||
"/usr/lib/jvm/default-java/bin/java"
|
||||
do
|
||||
[[ -z "$candidate" ]] && continue
|
||||
if [[ -x "$candidate" ]]; then
|
||||
export PATH="$(dirname "$candidate"):$PATH"
|
||||
export JAVA_BIN="$candidate"
|
||||
if java -version >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "java was not found on PATH. Install Java or set JAVA_BIN." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
server_session_name() {
|
||||
printf 'mc-%s\n' "${1//./_}"
|
||||
}
|
||||
|
||||
server_running() {
|
||||
local version="$1"
|
||||
mc-list | grep -Fq "$(server_session_name "$version")"
|
||||
}
|
||||
|
||||
wait_for_server_ready() {
|
||||
local version="$1"
|
||||
local timeout="${2:-60}"
|
||||
local elapsed=0
|
||||
|
||||
while (( elapsed < timeout )); do
|
||||
if mc-log "$version" 250 2>/dev/null | grep -Fq "Done ("; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
((elapsed += 1))
|
||||
done
|
||||
|
||||
echo "Timed out waiting for $version to become ready" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_server_stop() {
|
||||
local version="$1"
|
||||
local timeout="${2:-60}"
|
||||
local elapsed=0
|
||||
|
||||
while (( elapsed < timeout )); do
|
||||
if ! server_running "$version"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
((elapsed += 1))
|
||||
done
|
||||
|
||||
mc-kill "$version" --confirm >/dev/null 2>&1 || true
|
||||
|
||||
if ! server_running "$version"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Timed out waiting for $version to stop" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
disable_noisy_bots_in_ini() {
|
||||
local ini_file="$1"
|
||||
local section
|
||||
|
||||
for section in \
|
||||
ScriptScheduler \
|
||||
DiscordRpc \
|
||||
AntiAFK \
|
||||
AutoDig \
|
||||
AutoAttack \
|
||||
PlayerListLogger \
|
||||
ReplayCapture
|
||||
do
|
||||
sed_in_place "/^\\[ChatBot\\.${section}\\]/,/^\\[/ { s/^Enabled = true/Enabled = false/; }" "$ini_file"
|
||||
done
|
||||
}
|
||||
|
||||
remove_stale_stdin_pipe() {
|
||||
local version="$1"
|
||||
local pipe_path="$MCC_SERVERS/$version/stdin.pipe"
|
||||
|
||||
if [[ -e "$pipe_path" ]] && ! server_running "$version"; then
|
||||
rm -f "$pipe_path"
|
||||
fi
|
||||
}
|
||||
59
.skills/mcc-integration-testing/scripts/ensure_offline_server.sh
Executable file
59
.skills/mcc-integration-testing/scripts/ensure_offline_server.sh
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
# shellcheck source=tools/mcc-env.sh
|
||||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
VERSION="${1:-1.21.11-Vanilla}"
|
||||
SERVER_DIR="${MCC_SERVERS:?}/$VERSION"
|
||||
PROPS_FILE="$SERVER_DIR/server.properties"
|
||||
SESSION_NAME="mc-${VERSION//./_}"
|
||||
|
||||
if [[ ! -d "$SERVER_DIR" ]]; then
|
||||
echo "Server directory not found: $SERVER_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$SERVER_DIR/eula.txt" ]] || ! grep -Eq '^eula=true$' "$SERVER_DIR/eula.txt"; then
|
||||
echo "Missing accepted EULA in $SERVER_DIR/eula.txt" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
server_running() {
|
||||
mc-list | grep -Fq "$SESSION_NAME"
|
||||
}
|
||||
|
||||
upsert_property() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
|
||||
if grep -Eq "^${key}=" "$PROPS_FILE"; then
|
||||
sed_in_place "s#^${key}=.*#${key}=${value}#" "$PROPS_FILE"
|
||||
else
|
||||
printf '%s=%s\n' "$key" "$value" >> "$PROPS_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ ! -f "$PROPS_FILE" ]]; then
|
||||
mc-start "$VERSION"
|
||||
wait_for_server_ready "$VERSION"
|
||||
mc-stop "$VERSION" --confirm
|
||||
wait_for_server_stop "$VERSION"
|
||||
fi
|
||||
|
||||
if server_running; then
|
||||
mc-stop "$VERSION" --confirm
|
||||
wait_for_server_stop "$VERSION"
|
||||
fi
|
||||
|
||||
upsert_property "online-mode" "false"
|
||||
upsert_property "enforce-secure-profile" "false"
|
||||
upsert_property "enable-rcon" "true"
|
||||
upsert_property "rcon.port" "25575"
|
||||
upsert_property "rcon.password" "test123"
|
||||
|
||||
echo "Configured $VERSION for persistent offline testing"
|
||||
35
.skills/mcc-integration-testing/scripts/get_server_port.sh
Normal file
35
.skills/mcc-integration-testing/scripts/get_server_port.sh
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 <server-dir>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
SERVER_DIR_NAME="$1"
|
||||
SERVERS_ROOT="${MCC_SERVERS:-$REPO_ROOT/MinecraftOfficial/downloads}"
|
||||
SERVER_DIR="$SERVERS_ROOT/$SERVER_DIR_NAME"
|
||||
PROPS_FILE="$SERVER_DIR/server.properties"
|
||||
LATEST_LOG="$SERVER_DIR/logs/latest.log"
|
||||
|
||||
if [[ -f "$PROPS_FILE" ]]; then
|
||||
PORT_LINE="$(grep -E '^server-port=' "$PROPS_FILE" | tail -n 1 || true)"
|
||||
if [[ -n "$PORT_LINE" ]]; then
|
||||
PORT="${PORT_LINE#server-port=}"
|
||||
if [[ "$PORT" =~ ^[0-9]+$ ]]; then
|
||||
printf '%s\n' "$PORT"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f "$LATEST_LOG" ]]; then
|
||||
PORT="$(sed -n 's/.*Starting Minecraft server on .*:\([0-9][0-9]*\).*/\1/p' "$LATEST_LOG" | tail -n 1)"
|
||||
if [[ -n "$PORT" ]]; then
|
||||
printf '%s\n' "$PORT"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '25565\n'
|
||||
48
.skills/mcc-integration-testing/scripts/preflight_test_env.sh
Executable file
48
.skills/mcc-integration-testing/scripts/preflight_test_env.sh
Executable file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
# shellcheck source=tools/mcc-env.sh
|
||||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: preflight_test_env.sh [server-dir...]
|
||||
|
||||
Checks the local MCC test environment and resolves common Java path issues.
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ensure_java_in_path
|
||||
command -v tmux >/dev/null 2>&1 || { echo "tmux was not found on PATH." >&2; exit 1; }
|
||||
command -v dotnet >/dev/null 2>&1 || { echo "dotnet was not found on PATH." >&2; exit 1; }
|
||||
command -v python3 >/dev/null 2>&1 || { echo "python3 was not found on PATH." >&2; exit 1; }
|
||||
|
||||
if [[ ! -d "$MCC_SERVERS" ]]; then
|
||||
echo "Server root not found: $MCC_SERVERS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for server_dir in "$@"; do
|
||||
[[ -z "$server_dir" ]] && continue
|
||||
if [[ ! -d "$MCC_SERVERS/$server_dir" ]]; then
|
||||
echo "Server directory not found: $MCC_SERVERS/$server_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
remove_stale_stdin_pipe "$server_dir"
|
||||
done
|
||||
|
||||
printf 'MCC_REPO=%s\n' "$MCC_REPO"
|
||||
printf 'MCC_SERVERS=%s\n' "$MCC_SERVERS"
|
||||
printf 'JAVA=%s\n' "$(command -v java)"
|
||||
printf 'TMUX=%s\n' "$(command -v tmux)"
|
||||
printf 'DOTNET=%s\n' "$(command -v dotnet)"
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF' >&2
|
||||
Usage:
|
||||
prepare_offline_mcc_config.sh <output-ini> <mc-version> [login]
|
||||
prepare_offline_mcc_config.sh <template-ini> <output-ini> <mc-version> [login]
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ $# -lt 2 || $# -gt 4 ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TEMPLATE_INI=""
|
||||
OUTPUT_INI=""
|
||||
MC_VERSION=""
|
||||
LOGIN_NAME=""
|
||||
|
||||
if [[ $# -ge 3 && -f "$1" ]]; then
|
||||
TEMPLATE_INI="$1"
|
||||
OUTPUT_INI="$2"
|
||||
MC_VERSION="$3"
|
||||
LOGIN_NAME="${4:-MCCBot}"
|
||||
else
|
||||
OUTPUT_INI="$1"
|
||||
MC_VERSION="$2"
|
||||
LOGIN_NAME="${3:-MCCBot}"
|
||||
fi
|
||||
|
||||
ACCOUNT_TYPE="${MCC_TEST_ACCOUNT_TYPE:-mojang}"
|
||||
PASSWORD_VALUE="${MCC_TEST_PASSWORD-}"
|
||||
|
||||
if [[ "$ACCOUNT_TYPE" != "mojang" && "$ACCOUNT_TYPE" != "microsoft" && "$ACCOUNT_TYPE" != "yggdrasil" ]]; then
|
||||
echo "Unsupported MCC_TEST_ACCOUNT_TYPE: $ACCOUNT_TYPE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${MCC_TEST_PASSWORD+x}" ]]; then
|
||||
if [[ "$ACCOUNT_TYPE" == "mojang" ]]; then
|
||||
PASSWORD_VALUE="-"
|
||||
else
|
||||
PASSWORD_VALUE=""
|
||||
fi
|
||||
fi
|
||||
|
||||
generate_template_ini() {
|
||||
local template_root
|
||||
template_root="$(mktemp -d "${TMPDIR:-/tmp}/mcc-config-template.XXXXXX")"
|
||||
|
||||
if [[ ! -f "$REPO_ROOT/MinecraftClient/bin/Release/net10.0/MinecraftClient.dll" ]]; then
|
||||
dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release -v quiet --nologo >/dev/null
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$template_root"
|
||||
dotnet run --project "$REPO_ROOT/MinecraftClient" -c Release --no-build -- --help >/dev/null 2>&1
|
||||
)
|
||||
|
||||
if [[ ! -f "$template_root/MinecraftClient.ini" ]]; then
|
||||
echo "Failed to generate a temporary MCC config template." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TEMPLATE_INI="$template_root/MinecraftClient.ini"
|
||||
}
|
||||
|
||||
if [[ -z "$TEMPLATE_INI" ]]; then
|
||||
generate_template_ini
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$OUTPUT_INI")"
|
||||
cp "$TEMPLATE_INI" "$OUTPUT_INI"
|
||||
|
||||
sed_in_place \
|
||||
-e "s#^Account = .*#Account = { Login = \"$LOGIN_NAME\", Password = \"$PASSWORD_VALUE\" }#" \
|
||||
-e "s#^AccountType = .*#AccountType = \"$ACCOUNT_TYPE\"#" \
|
||||
-e "s#^MinecraftVersion = \"[^\"]*\"\\(.*\\)\$#MinecraftVersion = \"$MC_VERSION\"\\1#" \
|
||||
-e 's#^TerrainAndMovements = false#TerrainAndMovements = true#' \
|
||||
-e 's#^InventoryHandling = false#InventoryHandling = true#' \
|
||||
-e 's#^EntityHandling = false#EntityHandling = true#' \
|
||||
-e 's#^AutoRespawn = false#AutoRespawn = true#' \
|
||||
"$OUTPUT_INI"
|
||||
|
||||
disable_noisy_bots_in_ini "$OUTPUT_INI"
|
||||
|
||||
grep -Fq "AccountType = \"$ACCOUNT_TYPE\"" "$OUTPUT_INI" || {
|
||||
echo "Failed to enforce account type $ACCOUNT_TYPE in $OUTPUT_INI" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ "$ACCOUNT_TYPE" == "mojang" ]]; then
|
||||
grep -Eq '^Account = \{ Login = ".*", Password = "-" \}' "$OUTPUT_INI" || {
|
||||
echo "Failed to enforce offline account in $OUTPUT_INI" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
printf '%s\n' "$OUTPUT_INI"
|
||||
44
.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh
Executable file
44
.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh
Executable file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
# shellcheck source=tools/mcc-env.sh
|
||||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: reset_shared_test_state.sh [--all | <server-dir>...]
|
||||
|
||||
Kills shared server tmux test sessions and removes stale server stdin pipes.
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
kill_named_session() {
|
||||
local session_name="$1"
|
||||
tmux kill-session -t "$session_name" 2>/dev/null || true
|
||||
}
|
||||
|
||||
if [[ $# -eq 0 || "${1:-}" == "--all" ]]; then
|
||||
while IFS= read -r session_name; do
|
||||
[[ -z "$session_name" ]] && continue
|
||||
kill_named_session "$session_name"
|
||||
done < <(tmux list-sessions 2>/dev/null | awk -F: '/^mc-/{print $1}' || true)
|
||||
|
||||
while IFS= read -r pipe_path; do
|
||||
[[ -z "$pipe_path" ]] && continue
|
||||
rm -f "$pipe_path"
|
||||
done < <(find "$MCC_SERVERS" -maxdepth 2 -name 'stdin.pipe' 2>/dev/null || true)
|
||||
else
|
||||
for version in "$@"; do
|
||||
kill_named_session "$(server_session_name "$version")"
|
||||
remove_stale_stdin_pipe "$version"
|
||||
done
|
||||
fi
|
||||
168
.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh
Executable file
168
.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh
Executable file
|
|
@ -0,0 +1,168 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
# shellcheck source=tools/mcc-env.sh
|
||||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
|
||||
RUN_ROOT="${TMPDIR:-/tmp}/mcc-achievements/matrix"
|
||||
RUN_ID="$(date +%Y%m%d-%H%M%S)"
|
||||
MATRIX_DIR="$RUN_ROOT/$RUN_ID"
|
||||
RESULTS_TSV="$MATRIX_DIR/results.tsv"
|
||||
BUILD_LOG="$MATRIX_DIR/build.log"
|
||||
REPORT_MD="$MATRIX_DIR/report.md"
|
||||
PRECHECK_TXT="$MATRIX_DIR/preflight.txt"
|
||||
|
||||
mkdir -p "$MATRIX_DIR"
|
||||
|
||||
write_row() {
|
||||
local fields=("$@")
|
||||
|
||||
while (( ${#fields[@]} < 14 )); do
|
||||
fields+=("")
|
||||
done
|
||||
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"${fields[0]}" "${fields[1]}" "${fields[2]}" "${fields[3]}" "${fields[4]}" "${fields[5]}" "${fields[6]}" \
|
||||
"${fields[7]}" "${fields[8]}" "${fields[9]}" "${fields[10]}" "${fields[11]}" "${fields[12]}" \
|
||||
"${fields[13]}" >> "$RESULTS_TSV"
|
||||
}
|
||||
|
||||
resolve_server_dir() {
|
||||
local version="$1"
|
||||
local candidate
|
||||
|
||||
for candidate in "$version" "$version-Vanilla"; do
|
||||
if [[ -d "$MCC_SERVERS/$candidate" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
run_version() {
|
||||
local version="$1"
|
||||
local profile="$2"
|
||||
local family="$3"
|
||||
local server_dir="$4"
|
||||
local summary_env
|
||||
|
||||
if bash "$SCRIPT_DIR/run_achievements_test.sh" --no-build "$server_dir" "$version" "$profile"; then
|
||||
:
|
||||
fi
|
||||
|
||||
summary_env="${TMPDIR:-/tmp}/mcc-achievements/$server_dir/latest/summary.env"
|
||||
if [[ ! -f "$summary_env" ]]; then
|
||||
write_row "$version" "$server_dir" "unknown" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \
|
||||
"Summary file was not produced." "" "" ""
|
||||
return
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
source "$summary_env"
|
||||
|
||||
if [[ -n "${MCC_LOG:-}" && ! -f "$MCC_LOG" ]]; then
|
||||
NOTE="Harness failure: MCC log was not produced."
|
||||
VERDICT="❌ Fail"
|
||||
fi
|
||||
|
||||
if [[ -n "${COMMAND_LOG:-}" && ! -f "$COMMAND_LOG" ]]; then
|
||||
NOTE="Harness failure: command transcript was not produced."
|
||||
VERDICT="❌ Fail"
|
||||
fi
|
||||
|
||||
write_row "$VERSION" "$SERVER_DIR" "$PORT" "$FAMILY" "$INITIAL_STATUS" "$GRANT_STATUS" "$REVOKE_STATUS" \
|
||||
"$API_STATUS" "$VERDICT" "$NOTE" "$RUN_DIR" "$MCC_LOG" "$COPIED_SERVER_LOG" "$COMMAND_LOG"
|
||||
}
|
||||
|
||||
{
|
||||
printf 'MCC_SERVERS=%s\n' "$MCC_SERVERS"
|
||||
printf 'RUN_DIR=%s\n' "$MATRIX_DIR"
|
||||
printf 'DATE=%s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||
} > "$PRECHECK_TXT"
|
||||
|
||||
printf 'Version\tServerDir\tPort\tFamily\tInitial\tGrant\tRevoke\tAPI\tVerdict\tNote\tRunDir\tMccLog\tServerLog\tCommandLog\n' > "$RESULTS_TSV"
|
||||
|
||||
JAVA_OK="yes"
|
||||
TMUX_OK="yes"
|
||||
DOTNET_OK="yes"
|
||||
BUILD_OK="yes"
|
||||
|
||||
if ! command -v dotnet >/dev/null 2>&1; then
|
||||
DOTNET_OK="no"
|
||||
fi
|
||||
|
||||
if ! command -v java >/dev/null 2>&1 || ! java -version >/dev/null 2>&1; then
|
||||
JAVA_OK="no"
|
||||
fi
|
||||
|
||||
if ! command -v tmux >/dev/null 2>&1; then
|
||||
TMUX_OK="no"
|
||||
fi
|
||||
|
||||
if [[ "$DOTNET_OK" == "yes" ]]; then
|
||||
bash "$SCRIPT_DIR/preflight_test_env.sh" >/dev/null 2>&1 || true
|
||||
if ! dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release > "$BUILD_LOG" 2>&1; then
|
||||
BUILD_OK="no"
|
||||
fi
|
||||
else
|
||||
: > "$BUILD_LOG"
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'MCC_SERVERS=%s\n' "$MCC_SERVERS"
|
||||
printf 'RUN_DIR=%s\n' "$MATRIX_DIR"
|
||||
printf 'DATE=%s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||
printf 'dotnet=%s\n' "$DOTNET_OK"
|
||||
printf 'java=%s\n' "$JAVA_OK"
|
||||
printf 'tmux=%s\n' "$TMUX_OK"
|
||||
printf 'build=%s\n' "$BUILD_OK"
|
||||
} > "$PRECHECK_TXT"
|
||||
|
||||
while IFS='|' read -r version profile family; do
|
||||
[[ -z "$version" ]] && continue
|
||||
|
||||
if [[ "$DOTNET_OK" != "yes" ]]; then
|
||||
write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \
|
||||
"dotnet is not available on PATH."
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$BUILD_OK" != "yes" ]]; then
|
||||
write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \
|
||||
"dotnet build failed. See $BUILD_LOG."
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$JAVA_OK" != "yes" || "$TMUX_OK" != "yes" ]]; then
|
||||
write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \
|
||||
"java or tmux is not available, so live server execution was blocked."
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! server_dir="$(resolve_server_dir "$version")"; then
|
||||
write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "⚠️ Partial" \
|
||||
"Server directory for $version was not found under $MCC_SERVERS."
|
||||
continue
|
||||
fi
|
||||
|
||||
run_version "$version" "$profile" "$family" "$server_dir"
|
||||
done <<'EOF'
|
||||
1.8|legacy|Legacy 🧱
|
||||
1.11.2|legacy|Legacy 🧱
|
||||
1.12.2|modern|First advancements 🌱
|
||||
1.19.4|modern|Stable modern ✅
|
||||
1.20|modern|Telemetry edge 1 ⚠️
|
||||
1.20.2|modern|Telemetry edge 2 ⚠️
|
||||
1.20.4|modern|End of 1.20.x ⚠️
|
||||
1.20.6|modern|Post-1.20.6 🔧
|
||||
1.21.2|modern|1.21.2 family 🔧
|
||||
1.21.11|modern|showAdvancements 🆕
|
||||
26.1|modern|Latest supported 🚀
|
||||
EOF
|
||||
|
||||
bash "$SCRIPT_DIR/summarize_achievements_matrix.sh" "$MATRIX_DIR" > "$REPORT_MD"
|
||||
printf '%s\n' "$MATRIX_DIR"
|
||||
399
.skills/mcc-integration-testing/scripts/run_achievements_test.sh
Executable file
399
.skills/mcc-integration-testing/scripts/run_achievements_test.sh
Executable file
|
|
@ -0,0 +1,399 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
# shellcheck source=tools/mcc-env.sh
|
||||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: run_achievements_test.sh [--no-build] <server-dir> <mc-version> <legacy|modern>
|
||||
|
||||
Examples:
|
||||
.skills/mcc-integration-testing/scripts/run_achievements_test.sh --no-build 1.8 1.8 legacy
|
||||
.skills/mcc-integration-testing/scripts/run_achievements_test.sh --no-build 1.21.11-Vanilla 1.21.11 modern
|
||||
EOF
|
||||
}
|
||||
|
||||
DO_BUILD=true
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-build) DO_BUILD=false; shift ;;
|
||||
--build) DO_BUILD=true; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $# -ne 3 ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SERVER_DIR="$1"
|
||||
MC_VERSION="$2"
|
||||
PROFILE="$3"
|
||||
SESSION_NAME="achievements-${SERVER_DIR//[^a-zA-Z0-9]/_}-${PROFILE}"
|
||||
TEST_USERNAME="$(_mcc_resolve_username "$SESSION_NAME")"
|
||||
|
||||
if [[ "$PROFILE" != "legacy" && "$PROFILE" != "modern" ]]; then
|
||||
echo "Unsupported profile: $PROFILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUN_ROOT="${TMPDIR:-/tmp}/mcc-achievements"
|
||||
RUN_ID="$(date +%Y%m%d-%H%M%S)"
|
||||
RUN_DIR="$RUN_ROOT/$SERVER_DIR/$RUN_ID"
|
||||
LATEST_LINK="$RUN_ROOT/$SERVER_DIR/latest"
|
||||
MCC_LOG="$RUN_DIR/mcc.log"
|
||||
BUILD_LOG="$RUN_DIR/build.log"
|
||||
SERVER_TMUX_LOG="$RUN_DIR/server-tmux.log"
|
||||
SERVER_FILE_LOG="$RUN_DIR/server-latest.log"
|
||||
COMMAND_LOG="$RUN_DIR/commands.log"
|
||||
SUMMARY_ENV="$RUN_DIR/summary.env"
|
||||
PROBE_SCRIPT="$RUN_DIR/achievement_probe.cs"
|
||||
CFG="$RUN_DIR/MinecraftClient.$MC_VERSION.ini"
|
||||
INPUT_FILE="$(_mcc_session_input_file "$SESSION_NAME")"
|
||||
SERVER_LOG_FILE="$MCC_SERVERS/$SERVER_DIR/logs/latest.log"
|
||||
TARGET_ID="minecraft:story/root"
|
||||
TARGET_COMMAND_GRANT="advancement grant $TEST_USERNAME only minecraft:story/root"
|
||||
TARGET_COMMAND_REVOKE="advancement revoke $TEST_USERNAME only minecraft:story/root"
|
||||
TARGET_TYPE="Modern 🌱"
|
||||
PORT="unknown"
|
||||
MCC_PID=""
|
||||
|
||||
INITIAL_STATUS="❌"
|
||||
GRANT_STATUS="❌"
|
||||
REVOKE_STATUS="❌"
|
||||
API_STATUS="❌"
|
||||
VERDICT="❌ Fail"
|
||||
NOTE="Run did not complete."
|
||||
EXECUTED="yes"
|
||||
|
||||
if [[ "$PROFILE" == "legacy" ]]; then
|
||||
TARGET_ID="achievement.openInventory"
|
||||
TARGET_COMMAND_GRANT="achievement give achievement.openInventory $TEST_USERNAME"
|
||||
TARGET_COMMAND_REVOKE="achievement take achievement.openInventory $TEST_USERNAME"
|
||||
TARGET_TYPE="Legacy 🧱"
|
||||
fi
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
write_summary() {
|
||||
{
|
||||
printf 'VERSION=%q\n' "$MC_VERSION"
|
||||
printf 'SERVER_DIR=%q\n' "$SERVER_DIR"
|
||||
printf 'PROFILE=%q\n' "$PROFILE"
|
||||
printf 'FAMILY=%q\n' "$TARGET_TYPE"
|
||||
printf 'PORT=%q\n' "$PORT"
|
||||
printf 'RUN_DIR=%q\n' "$RUN_DIR"
|
||||
printf 'MCC_LOG=%q\n' "$MCC_LOG"
|
||||
printf 'SERVER_LOG=%q\n' "$RUN_DIR/server-latest.log"
|
||||
printf 'SERVER_FILE_LOG=%q\n' "$SERVER_LOG_FILE"
|
||||
printf 'SERVER_TMUX_LOG=%q\n' "$SERVER_TMUX_LOG"
|
||||
printf 'COPIED_SERVER_LOG=%q\n' "$RUN_DIR/server-latest.log"
|
||||
printf 'COMMAND_LOG=%q\n' "$COMMAND_LOG"
|
||||
printf 'SUMMARY_ENV=%q\n' "$SUMMARY_ENV"
|
||||
printf 'TARGET_ID=%q\n' "$TARGET_ID"
|
||||
printf 'INITIAL_STATUS=%q\n' "$INITIAL_STATUS"
|
||||
printf 'GRANT_STATUS=%q\n' "$GRANT_STATUS"
|
||||
printf 'REVOKE_STATUS=%q\n' "$REVOKE_STATUS"
|
||||
printf 'API_STATUS=%q\n' "$API_STATUS"
|
||||
printf 'VERDICT=%q\n' "$VERDICT"
|
||||
printf 'NOTE=%q\n' "$NOTE"
|
||||
printf 'EXECUTED=%q\n' "$EXECUTED"
|
||||
} > "$SUMMARY_ENV"
|
||||
}
|
||||
|
||||
capture_server_logs() {
|
||||
mc-log "$SERVER_DIR" 400 > "$SERVER_TMUX_LOG" 2>/dev/null || true
|
||||
if [[ -f "$SERVER_LOG_FILE" ]]; then
|
||||
cp "$SERVER_LOG_FILE" "$RUN_DIR/server-latest.log" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
capture_server_logs
|
||||
|
||||
if [[ -n "${MCC_PID:-}" ]] && kill -0 "$MCC_PID" 2>/dev/null; then
|
||||
echo "quit" >> "$INPUT_FILE" 2>/dev/null || true
|
||||
sleep 2
|
||||
kill "$MCC_PID" 2>/dev/null || true
|
||||
wait "$MCC_PID" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
mc-stop "$SERVER_DIR" --confirm >/dev/null 2>&1 || true
|
||||
wait_for_server_stop "$SERVER_DIR" 20 >/dev/null 2>&1 || true
|
||||
ln -sfn "$RUN_DIR" "$LATEST_LINK"
|
||||
write_summary
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
log_step() {
|
||||
printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$1" | tee -a "$COMMAND_LOG"
|
||||
}
|
||||
|
||||
fail() {
|
||||
NOTE="$1"
|
||||
VERDICT="❌ Fail"
|
||||
exit 1
|
||||
}
|
||||
|
||||
wait_for_file_pattern() {
|
||||
local file="$1"
|
||||
local pattern="$2"
|
||||
local description="$3"
|
||||
local timeout="${4:-60}"
|
||||
local elapsed=0
|
||||
|
||||
while (( elapsed < timeout )); do
|
||||
if [[ -f "$file" ]] && grep -Fq "$pattern" "$file"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
((elapsed += 1))
|
||||
done
|
||||
|
||||
echo "Timed out waiting for: $description" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
write_probe_script() {
|
||||
cat > "$PROBE_SCRIPT" <<EOF
|
||||
//MCCScript 1.0
|
||||
|
||||
MCC.LoadBot(new AchievementProbeBot());
|
||||
|
||||
//MCCScript Extensions
|
||||
|
||||
public class AchievementProbeBot : ChatBot
|
||||
{
|
||||
private const string TargetId = "$TARGET_ID";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
LogToConsole("[ACH_TEST] probe initialized");
|
||||
DumpState("initialize");
|
||||
}
|
||||
|
||||
public override void AfterGameJoined()
|
||||
{
|
||||
LogToConsole("[ACH_TEST] after join");
|
||||
DumpState("after_join");
|
||||
}
|
||||
|
||||
public override void OnAchievementUpdate(IReadOnlyList<Achievement> updated, IReadOnlyList<string> removedIds, bool reset)
|
||||
{
|
||||
LogToConsole($"[ACH_TEST] event reset={reset} updated={updated.Count} removed={removedIds.Count}");
|
||||
DumpState("event");
|
||||
}
|
||||
|
||||
private void DumpState(string origin)
|
||||
{
|
||||
Achievement[] all = GetAchievements();
|
||||
Achievement[] unlocked = GetUnlockedAchievements();
|
||||
Achievement[] locked = GetLockedAchievements();
|
||||
Achievement? target = null;
|
||||
|
||||
foreach (Achievement entry in all)
|
||||
{
|
||||
if (entry.Id == TargetId)
|
||||
{
|
||||
target = entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string titleState = "missing";
|
||||
string completionState = "missing";
|
||||
|
||||
if (target is not null)
|
||||
{
|
||||
titleState = target.Title is null ? "null" : "present";
|
||||
completionState = target.IsCompleted ? "done" : "todo";
|
||||
}
|
||||
|
||||
LogToConsole($"[ACH_TEST] snapshot origin={origin} all={all.Length} unlocked={unlocked.Length} locked={locked.Length}");
|
||||
LogToConsole($"[ACH_TEST] target_state origin={origin} id={TargetId} title={titleState} completed={completionState}");
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
run_server_command() {
|
||||
local cmd="$1"
|
||||
local attempt
|
||||
|
||||
log_step "SERVER> $cmd"
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if mc-rcon "$cmd" >/dev/null 2>&1; then
|
||||
sleep 1
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
fail "Server command failed: $cmd"
|
||||
}
|
||||
|
||||
run_mcc_command() {
|
||||
local name="$1"
|
||||
local cmd="$2"
|
||||
local delay="${3:-2}"
|
||||
local start_line=0
|
||||
local end_line=0
|
||||
|
||||
if [[ -f "$MCC_LOG" ]]; then
|
||||
start_line="$(wc -l < "$MCC_LOG")"
|
||||
fi
|
||||
|
||||
log_step "MCC> $cmd"
|
||||
echo "$cmd" >> "$INPUT_FILE"
|
||||
sleep "$delay"
|
||||
|
||||
if [[ -f "$MCC_LOG" ]]; then
|
||||
end_line="$(wc -l < "$MCC_LOG")"
|
||||
fi
|
||||
|
||||
if (( end_line > start_line )); then
|
||||
sed -n "$((start_line + 1)),$((end_line))p" "$MCC_LOG" > "$RUN_DIR/$name.mcc.log"
|
||||
else
|
||||
: > "$RUN_DIR/$name.mcc.log"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_pattern() {
|
||||
local file="$1"
|
||||
local pattern="$2"
|
||||
local description="$3"
|
||||
|
||||
grep -Fq "$pattern" "$file" || fail "$description"
|
||||
}
|
||||
|
||||
if $DO_BUILD; then
|
||||
log_step "BUILD> dotnet build MinecraftClient.sln -c Release"
|
||||
mcc-build > "$BUILD_LOG" 2>&1 || fail "dotnet build failed."
|
||||
else
|
||||
: > "$BUILD_LOG"
|
||||
fi
|
||||
|
||||
bash "$SCRIPT_DIR/preflight_test_env.sh" "$SERVER_DIR" >/dev/null || fail "Test environment preflight failed."
|
||||
bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$SERVER_DIR" >/dev/null || fail "Failed to reset shared test state."
|
||||
|
||||
if [[ ! -d "$MCC_SERVERS/$SERVER_DIR" ]]; then
|
||||
fail "Server directory not found: $MCC_SERVERS/$SERVER_DIR"
|
||||
fi
|
||||
|
||||
bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" "$TEST_USERNAME" >/dev/null || fail "Failed to prepare temporary MCC config."
|
||||
PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$SERVER_DIR")"
|
||||
|
||||
"$SCRIPT_DIR/ensure_offline_server.sh" "$SERVER_DIR"
|
||||
write_probe_script
|
||||
|
||||
if [[ "$PROFILE" == "legacy" && -f "$MCC_SERVERS/$SERVER_DIR/server.properties" ]]; then
|
||||
sed_in_place 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$INPUT_FILE")"
|
||||
: > "$INPUT_FILE"
|
||||
rm -f "$MCC_LOG"
|
||||
|
||||
log_step "Starting server $SERVER_DIR on port $PORT"
|
||||
mc-start "$SERVER_DIR" >/dev/null
|
||||
wait_for_server_ready "$SERVER_DIR" || fail "Server did not become ready."
|
||||
|
||||
log_step "Starting MCC for $MC_VERSION"
|
||||
(
|
||||
cd "$REPO_ROOT"
|
||||
MCC_FILE_INPUT=1 MCC_INPUT_FILE="$INPUT_FILE" dotnet run --project MinecraftClient -c Release --no-build -- \
|
||||
"$CFG" \
|
||||
"$TEST_USERNAME" \
|
||||
- \
|
||||
"localhost:$PORT" \
|
||||
"--accounttype=mojang" \
|
||||
"--minecraftversion=$MC_VERSION" \
|
||||
"--terrainandmovements=true" \
|
||||
"--inventoryhandling=true" \
|
||||
"--entityhandling=true" \
|
||||
"--autorespawn=true" \
|
||||
"--debugmessages=true" \
|
||||
> "$MCC_LOG" 2>&1
|
||||
) &
|
||||
MCC_PID=$!
|
||||
|
||||
wait_for_file_pattern "$MCC_LOG" "Server was successfully joined." "MCC join success" 90 || fail "MCC failed to join."
|
||||
wait_for_file_pattern "$SERVER_LOG_FILE" "$TEST_USERNAME joined the game" "server join entry" 30 || fail "Server never logged the join."
|
||||
|
||||
run_server_command "op $TEST_USERNAME"
|
||||
run_server_command "gamerule sendCommandFeedback true"
|
||||
if [[ "$PROFILE" == "modern" ]]; then
|
||||
run_server_command "gamerule logAdminCommands true"
|
||||
fi
|
||||
run_server_command "time set day"
|
||||
run_server_command "weather clear"
|
||||
|
||||
run_mcc_command "load_probe" "script $PROBE_SCRIPT" 3
|
||||
wait_for_file_pattern "$MCC_LOG" "[ACH_TEST] probe initialized" "probe startup" 30 || fail "Probe script did not initialize."
|
||||
|
||||
run_mcc_command "baseline_debug" "debug state" 2
|
||||
run_mcc_command "baseline_all" "achievement" 2
|
||||
run_mcc_command "baseline_locked" "achievement locked" 2
|
||||
run_mcc_command "baseline_unlocked" "achievement unlocked" 2
|
||||
|
||||
run_server_command "$TARGET_COMMAND_GRANT"
|
||||
sleep 3
|
||||
run_mcc_command "after_grant_all" "achievement" 2
|
||||
run_mcc_command "after_grant_unlocked" "achievement unlocked" 2
|
||||
|
||||
run_server_command "$TARGET_COMMAND_REVOKE"
|
||||
sleep 3
|
||||
run_mcc_command "after_revoke_all" "achievement" 2
|
||||
run_mcc_command "after_revoke_locked" "achievement locked" 2
|
||||
|
||||
assert_pattern "$MCC_LOG" "Achievements/Advancements:" "Achievement command header never appeared."
|
||||
|
||||
if ! grep -Fq "No achievements/advancements received yet." "$RUN_DIR/baseline_all.mcc.log"; then
|
||||
INITIAL_STATUS="✅"
|
||||
fi
|
||||
|
||||
if grep -Fq "$TARGET_ID" "$RUN_DIR/after_grant_unlocked.mcc.log" && grep -Fq "[DONE]" "$RUN_DIR/after_grant_unlocked.mcc.log"; then
|
||||
GRANT_STATUS="✅"
|
||||
fi
|
||||
|
||||
if [[ "$PROFILE" == "legacy" ]]; then
|
||||
if grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_locked.mcc.log" && grep -Fq "[TODO]" "$RUN_DIR/after_revoke_locked.mcc.log"; then
|
||||
REVOKE_STATUS="✅"
|
||||
fi
|
||||
else
|
||||
if grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_locked.mcc.log" && grep -Fq "[TODO]" "$RUN_DIR/after_revoke_locked.mcc.log"; then
|
||||
REVOKE_STATUS="✅"
|
||||
elif [[ "$GRANT_STATUS" == "✅" ]] && ! grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_all.mcc.log"; then
|
||||
REVOKE_STATUS="✅"
|
||||
fi
|
||||
fi
|
||||
|
||||
if grep -Fq "[ACH_TEST] event" "$MCC_LOG" && grep -Fq "target_state origin=event id=$TARGET_ID title=" "$MCC_LOG"; then
|
||||
API_STATUS="✅"
|
||||
fi
|
||||
|
||||
case "$INITIAL_STATUS|$GRANT_STATUS|$REVOKE_STATUS|$API_STATUS" in
|
||||
"✅|✅|✅|✅")
|
||||
VERDICT="✅ Pass"
|
||||
NOTE="All planned achievement checks passed."
|
||||
;;
|
||||
*"✅"*)
|
||||
VERDICT="⚠️ Partial"
|
||||
NOTE="At least one achievement phase passed, but the matrix did not fully clear."
|
||||
;;
|
||||
*)
|
||||
VERDICT="❌ Fail"
|
||||
NOTE="Achievement checks did not produce the expected evidence."
|
||||
;;
|
||||
esac
|
||||
|
||||
run_mcc_command "quit" "quit" 2
|
||||
NOTE="$NOTE Artifacts saved in $RUN_DIR."
|
||||
336
.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh
Executable file
336
.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh
Executable file
|
|
@ -0,0 +1,336 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
# shellcheck source=tools/mcc-env.sh
|
||||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
VERSION="${1:-1.21.11-Vanilla}"
|
||||
MC_VERSION="${VERSION%-Vanilla}"
|
||||
if [[ "$MC_VERSION" == "$VERSION" ]]; then
|
||||
MC_VERSION="$VERSION"
|
||||
fi
|
||||
RUN_ROOT="${TMPDIR:-/tmp}/mcc-integration-testing"
|
||||
RUN_ID="$(date +%Y%m%d-%H%M%S)"
|
||||
RUN_DIR="$RUN_ROOT/$RUN_ID"
|
||||
SERVER_LOG_FILE="$MCC_SERVERS/$VERSION/logs/latest.log"
|
||||
SESSION_NAME="full-spectrum-${MC_VERSION//[^a-zA-Z0-9]/_}"
|
||||
TEST_USERNAME="$(_mcc_resolve_username "$SESSION_NAME")"
|
||||
MCC_LOG="$(_mcc_session_log_file "$SESSION_NAME")"
|
||||
PID_FILE="$(_mcc_session_pid_file "$SESSION_NAME")"
|
||||
MCC_TMUX_SESSION="$(_mcc_tmux_session_name "$SESSION_NAME")"
|
||||
BUILD_LOG="$RUN_DIR/build.log"
|
||||
SERVER_TMUX_LOG="$RUN_DIR/server-tmux.log"
|
||||
SERVER_FILE_LOG="$RUN_DIR/server-latest.log"
|
||||
INPUT_FILE="$(_mcc_session_input_file "$SESSION_NAME")"
|
||||
CFG="$RUN_DIR/MinecraftClient.$MC_VERSION.ini"
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
cleanup() {
|
||||
mcc-cmd --session "$SESSION_NAME" "quit" >/dev/null 2>&1 || true
|
||||
sleep 2
|
||||
mcc-kill --session "$SESSION_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
mc-stop "$VERSION" --confirm >/dev/null 2>&1 || true
|
||||
wait_for_server_stop "$VERSION" 20 >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
prepare_config() {
|
||||
bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" "$TEST_USERNAME" >/dev/null
|
||||
}
|
||||
|
||||
wait_for_server_log_pattern() {
|
||||
local pattern="$1"
|
||||
local description="$2"
|
||||
local timeout="${3:-60}"
|
||||
local elapsed=0
|
||||
|
||||
while (( elapsed < timeout )); do
|
||||
if [[ -f "$SERVER_LOG_FILE" ]] && grep -Fq "$pattern" "$SERVER_LOG_FILE"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
((elapsed += 1))
|
||||
done
|
||||
|
||||
echo "Timed out waiting for server log: $description" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
capture_server_logs() {
|
||||
mc-log "$VERSION" 400 > "$SERVER_TMUX_LOG" 2>/dev/null || true
|
||||
if [[ -f "$SERVER_LOG_FILE" ]]; then
|
||||
cp "$SERVER_LOG_FILE" "$SERVER_FILE_LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_file_pattern() {
|
||||
local file="$1"
|
||||
local pattern="$2"
|
||||
local description="$3"
|
||||
local timeout="${4:-60}"
|
||||
local elapsed=0
|
||||
|
||||
while (( elapsed < timeout )); do
|
||||
if [[ -f "$file" ]] && grep -Fq "$pattern" "$file"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
((elapsed += 1))
|
||||
done
|
||||
|
||||
echo "Timed out waiting for: $description" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
fail() {
|
||||
capture_server_logs
|
||||
echo "FAIL: $1" >&2
|
||||
echo "Run directory: $RUN_DIR" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local file="$1"
|
||||
local pattern="$2"
|
||||
local description="$3"
|
||||
|
||||
grep -Fq "$pattern" "$file" || fail "$description"
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
local file="$1"
|
||||
local pattern="$2"
|
||||
local description="$3"
|
||||
|
||||
if grep -Fq "$pattern" "$file"; then
|
||||
fail "$description"
|
||||
fi
|
||||
}
|
||||
|
||||
run_server_command() {
|
||||
local cmd="$1"
|
||||
local attempt
|
||||
echo "SERVER> $cmd"
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if mc-rcon "$cmd" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
fail "Server command failed: $cmd"
|
||||
}
|
||||
|
||||
run_mcc_command() {
|
||||
local cmd="$1"
|
||||
echo "MCC> $cmd"
|
||||
mcc-cmd --session "$SESSION_NAME" "$cmd"
|
||||
sleep 2
|
||||
}
|
||||
|
||||
start_mcc_session() {
|
||||
local -a mcc_args=("$CFG" "$TEST_USERNAME" "-" "localhost:$SERVER_PORT")
|
||||
local mcc_args_cmd
|
||||
mcc_args_cmd="$(printf '%q ' "${mcc_args[@]}")"
|
||||
|
||||
tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true
|
||||
rm -f "$PID_FILE"
|
||||
tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \
|
||||
"cd '$REPO_ROOT' && printf '%s\n' \"\$\$\" > '$PID_FILE' && exec env MCC_FILE_INPUT=1 MCC_INPUT_FILE='$INPUT_FILE' dotnet run --project MinecraftClient -c Release --no-build -- $mcc_args_cmd > '$MCC_LOG' 2>&1"
|
||||
|
||||
for _ in $(seq 1 25); do
|
||||
if [[ -s "$PID_FILE" ]]; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
fail "Failed to capture MCC PID for session $SESSION_NAME"
|
||||
}
|
||||
|
||||
bash "$SCRIPT_DIR/preflight_test_env.sh" "$VERSION" >/dev/null
|
||||
bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$VERSION" >/dev/null
|
||||
"$SCRIPT_DIR/ensure_offline_server.sh" "$VERSION"
|
||||
mcc-reset-session --session "$SESSION_NAME" >/dev/null
|
||||
echo "Building MCC..."
|
||||
mcc-build > "$BUILD_LOG" 2>&1 || fail "mcc-build failed"
|
||||
prepare_config
|
||||
SERVER_PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$VERSION")"
|
||||
if [[ -z "$SERVER_PORT" ]]; then
|
||||
fail "Failed to resolve server port"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$INPUT_FILE")" "$(dirname "$MCC_LOG")"
|
||||
: > "$INPUT_FILE"
|
||||
rm -f "$MCC_LOG"
|
||||
|
||||
echo "Starting server..."
|
||||
mc-start "$VERSION" >/dev/null
|
||||
wait_for_server_ready "$VERSION" || fail "Server did not become ready"
|
||||
|
||||
echo "Starting MCC..."
|
||||
start_mcc_session
|
||||
|
||||
wait_for_file_pattern "$MCC_LOG" "Server was successfully joined." "MCC join success" 90 || fail "MCC failed to join"
|
||||
wait_for_server_log_pattern "$TEST_USERNAME joined the game" "server join entry" 30 || fail "Server never logged the join"
|
||||
|
||||
run_server_command "op $TEST_USERNAME"
|
||||
run_server_command "gamerule sendCommandFeedback true"
|
||||
run_server_command "gamerule logAdminCommands true"
|
||||
run_server_command "time set day"
|
||||
run_server_command "weather clear"
|
||||
sleep 2
|
||||
|
||||
# ── Phase 1: Basic status and info commands ──
|
||||
run_mcc_command "health"
|
||||
run_mcc_command "list"
|
||||
run_mcc_command "inventory player list"
|
||||
run_mcc_command "/gamemode creative"
|
||||
run_mcc_command "inventory creativegive 36 Diamond 16"
|
||||
run_mcc_command "inventory player list"
|
||||
run_mcc_command "entity"
|
||||
run_mcc_command "/time query daytime"
|
||||
run_mcc_command "smoke_test_from_mcc_full_spectrum"
|
||||
|
||||
# ── Phase 2: Movement and look commands ──
|
||||
run_mcc_command "/tp $TEST_USERNAME 0 -60 0"
|
||||
sleep 3
|
||||
run_mcc_command "look up"
|
||||
sleep 1
|
||||
run_mcc_command "look down"
|
||||
sleep 1
|
||||
run_mcc_command "look east"
|
||||
sleep 1
|
||||
|
||||
# ── Phase 3: Advanced inventory operations ──
|
||||
run_mcc_command "inventory creativegive 37 IronSword 1"
|
||||
run_mcc_command "inventory creativegive 38 GoldenApple 8"
|
||||
run_mcc_command "inventory player list"
|
||||
run_mcc_command "inventory creativeclear 38"
|
||||
run_mcc_command "inventory player list"
|
||||
|
||||
# ── Phase 4: Block placement and interaction ──
|
||||
run_server_command "execute as $TEST_USERNAME at @s run fill ~1 ~ ~1 ~3 ~2 ~3 minecraft:stone"
|
||||
sleep 2
|
||||
run_server_command "execute as $TEST_USERNAME at @s run setblock ~5 ~ ~5 minecraft:chest"
|
||||
sleep 1
|
||||
run_server_command "execute as $TEST_USERNAME at @s run setblock ~5 ~1 ~5 minecraft:furnace"
|
||||
sleep 1
|
||||
run_server_command "execute as $TEST_USERNAME at @s run setblock ~6 ~ ~5 minecraft:crafting_table"
|
||||
sleep 1
|
||||
|
||||
# ── Phase 5: Entity spawning (expanded coverage) ──
|
||||
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:cow ~2 ~ ~"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:zombie ~4 ~ ~"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:creeper ~6 ~ ~"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:skeleton ~8 ~ ~"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:villager ~-2 ~ ~"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:allay ~-4 ~ ~"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:armor_stand ~ ~ ~2"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:item_display ~-6 ~ ~ {item:{id:\"minecraft:diamond\",count:1}}"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:spider ~10 ~ ~"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:pig ~-8 ~ ~"
|
||||
|
||||
sleep 2
|
||||
run_mcc_command "entity"
|
||||
|
||||
# ── Phase 6: Effects and environment ──
|
||||
run_server_command "effect give $TEST_USERNAME minecraft:speed 30 1"
|
||||
sleep 2
|
||||
run_mcc_command "health"
|
||||
run_server_command "effect give $TEST_USERNAME minecraft:regeneration 10 1"
|
||||
sleep 2
|
||||
run_mcc_command "health"
|
||||
|
||||
# ── Phase 7: Gamemode cycling ──
|
||||
run_mcc_command "/gamemode survival"
|
||||
sleep 2
|
||||
run_mcc_command "health"
|
||||
run_mcc_command "/gamemode creative"
|
||||
sleep 2
|
||||
|
||||
# ── Phase 8: Dimension change (nether) ──
|
||||
run_server_command "execute in minecraft:the_nether run tp $TEST_USERNAME 0 64 0"
|
||||
sleep 4
|
||||
run_mcc_command "health"
|
||||
run_server_command "execute in minecraft:overworld run tp $TEST_USERNAME 0 -60 0"
|
||||
sleep 4
|
||||
|
||||
# ── Phase 9: Server chat and whisper ──
|
||||
run_server_command "say Hello from the server console"
|
||||
sleep 2
|
||||
run_server_command "msg $TEST_USERNAME This is a private whisper"
|
||||
sleep 2
|
||||
run_mcc_command "integration_test_chat_response"
|
||||
|
||||
# ── Phase 10: Particles, sounds, and explosions ──
|
||||
run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:happy_villager ~ ~1 ~ 0.5 0.5 0.5 0 12 force"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:end_rod ~ ~1 ~ 0.5 0.5 0.5 0.01 20 force"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:explosion ~ ~1 ~ 0 0 0 0 1 force"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:totem_of_undying ~ ~1 ~ 0.5 0.5 0.5 0.1 20 force"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:flame ~ ~1 ~ 0.2 0.2 0.2 0.02 30 force"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:heart ~ ~2 ~ 0.3 0.3 0.3 0 5 force"
|
||||
|
||||
run_server_command "execute as $TEST_USERNAME at @s run playsound minecraft:entity.lightning_bolt.thunder master $TEST_USERNAME ~ ~ ~ 1 1 0"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run playsound minecraft:block.note_block.bell master $TEST_USERNAME ~ ~ ~ 1 1 0"
|
||||
run_server_command "execute as $TEST_USERNAME at @s run playsound minecraft:entity.experience_orb.pickup master $TEST_USERNAME ~ ~ ~ 1 1 0"
|
||||
|
||||
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:tnt ~3 ~ ~"
|
||||
sleep 2
|
||||
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:tnt ~6 ~ ~"
|
||||
|
||||
# ── Phase 11: Kill and respawn cycle ──
|
||||
run_mcc_command "/gamemode survival"
|
||||
sleep 2
|
||||
run_server_command "kill $TEST_USERNAME"
|
||||
sleep 4
|
||||
run_mcc_command "respawn"
|
||||
sleep 4
|
||||
run_mcc_command "health"
|
||||
run_mcc_command "/gamemode creative"
|
||||
sleep 2
|
||||
|
||||
sleep 6
|
||||
capture_server_logs
|
||||
|
||||
# ── Assertions: MCC log ──
|
||||
assert_contains "$MCC_LOG" "Server was successfully joined." "MCC never joined the server"
|
||||
assert_contains "$MCC_LOG" "[FileInput] > inventory player list" "Inventory command was not executed"
|
||||
assert_contains "$MCC_LOG" "[FileInput] > entity" "Entity command was not executed"
|
||||
assert_contains "$MCC_LOG" "[FileInput] > /gamemode creative" "Creative mode command was not executed from MCC"
|
||||
assert_contains "$MCC_LOG" "Requested Diamond x16 in slot #36" "Creative inventory give did not succeed"
|
||||
assert_contains "$MCC_LOG" "smoke_test_from_mcc_full_spectrum" "Client-originated chat was not observed"
|
||||
assert_contains "$MCC_LOG" "[FileInput] > look up" "Look command was not executed"
|
||||
assert_contains "$MCC_LOG" "[FileInput] > /gamemode survival" "Survival mode switch was not executed"
|
||||
assert_contains "$MCC_LOG" "[FileInput] > respawn" "Respawn command was not executed"
|
||||
assert_contains "$MCC_LOG" "[FileInput] > health" "Health command was not executed"
|
||||
assert_contains "$MCC_LOG" "integration_test_chat_response" "Chat response test message was not observed"
|
||||
assert_not_contains "$MCC_LOG" "Please enable InventoryHandling" "Inventory handling is still disabled"
|
||||
assert_not_contains "$MCC_LOG" "Please enable EntityHandling" "Entity handling is still disabled"
|
||||
assert_not_contains "$MCC_LOG" "You must be in Creative gamemode" "Creative mode was not active when creativegive ran"
|
||||
assert_not_contains "$MCC_LOG" "Failed to load settings" "MCC failed to reload its config"
|
||||
assert_not_contains "$MCC_LOG" "NullReferenceException" "A NullReferenceException occurred during the test"
|
||||
|
||||
# ── Assertions: Server log ──
|
||||
assert_contains "$SERVER_FILE_LOG" "$TEST_USERNAME joined the game" "Server never saw $TEST_USERNAME join"
|
||||
assert_contains "$SERVER_FILE_LOG" "smoke_test_from_mcc_full_spectrum" "Server never received the client chat message"
|
||||
assert_contains "$SERVER_FILE_LOG" "Displaying particle minecraft:happy_villager" "Particle events were not recorded on the server"
|
||||
assert_contains "$SERVER_FILE_LOG" "Played sound minecraft:block.note_block.bell to $TEST_USERNAME" "Sound events were not recorded on the server"
|
||||
assert_contains "$SERVER_FILE_LOG" "Summoned new Primed TNT" "TNT summon did not occur on the server"
|
||||
assert_contains "$SERVER_FILE_LOG" "integration_test_chat_response" "Server never received the chat response test message"
|
||||
assert_contains "$SERVER_FILE_LOG" "Hello from the server console" "Server say command was not logged"
|
||||
assert_contains "$SERVER_FILE_LOG" "Killed $TEST_USERNAME" "Server kill command did not execute"
|
||||
assert_not_contains "$SERVER_FILE_LOG" "Sending unknown packet 'clientbound/minecraft:disconnect'" "Server hit the disconnect packet regression during the test"
|
||||
|
||||
cat <<EOF
|
||||
PASS
|
||||
Run directory: $RUN_DIR
|
||||
MCC log: $MCC_LOG
|
||||
Server log: $SERVER_FILE_LOG
|
||||
Build log: $BUILD_LOG
|
||||
EOF
|
||||
199
.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh
Executable file
199
.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh
Executable file
|
|
@ -0,0 +1,199 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
# shellcheck source=tools/mcc-env.sh
|
||||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
VERSION="${1:-1.21.11-Vanilla}"
|
||||
MC_VERSION="${VERSION%-Vanilla}"
|
||||
if [[ "$MC_VERSION" == "$VERSION" ]]; then
|
||||
MC_VERSION="$VERSION"
|
||||
fi
|
||||
|
||||
SESSION_A="parallel-smoke-a-${MC_VERSION//[^a-zA-Z0-9]/_}"
|
||||
SESSION_B="parallel-smoke-b-${MC_VERSION//[^a-zA-Z0-9]/_}"
|
||||
USERNAME_A="SmokeA"
|
||||
USERNAME_B="SmokeB"
|
||||
|
||||
RUN_ROOT="${TMPDIR:-/tmp}/mcc-integration-testing"
|
||||
RUN_ID="$(date +%Y%m%d-%H%M%S)"
|
||||
RUN_DIR="$RUN_ROOT/parallel-smoke-$RUN_ID"
|
||||
BUILD_LOG="$RUN_DIR/build.log"
|
||||
SERVER_TMUX_LOG="$RUN_DIR/server-tmux.log"
|
||||
SERVER_FILE_LOG="$RUN_DIR/server-latest.log"
|
||||
SERVER_LOG_FILE="$MCC_SERVERS/$VERSION/logs/latest.log"
|
||||
|
||||
LOG_A="$(_mcc_session_log_file "$SESSION_A")"
|
||||
LOG_B="$(_mcc_session_log_file "$SESSION_B")"
|
||||
INPUT_A="$(_mcc_session_input_file "$SESSION_A")"
|
||||
INPUT_B="$(_mcc_session_input_file "$SESSION_B")"
|
||||
PID_A_FILE="$(_mcc_session_pid_file "$SESSION_A")"
|
||||
PID_B_FILE="$(_mcc_session_pid_file "$SESSION_B")"
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
wait_for_file_pattern() {
|
||||
local file="$1"
|
||||
local pattern="$2"
|
||||
local description="$3"
|
||||
local timeout="${4:-60}"
|
||||
local elapsed=0
|
||||
|
||||
while (( elapsed < timeout )); do
|
||||
if [[ -f "$file" ]] && grep -Fq "$pattern" "$file"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
((elapsed += 1))
|
||||
done
|
||||
|
||||
echo "Timed out waiting for: $description" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_server_log_pattern() {
|
||||
local pattern="$1"
|
||||
local description="$2"
|
||||
local timeout="${3:-60}"
|
||||
local elapsed=0
|
||||
|
||||
while (( elapsed < timeout )); do
|
||||
if [[ -f "$SERVER_LOG_FILE" ]] && grep -Fq "$pattern" "$SERVER_LOG_FILE"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
((elapsed += 1))
|
||||
done
|
||||
|
||||
echo "Timed out waiting for server log: $description" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
capture_server_logs() {
|
||||
mc-log "$VERSION" 400 > "$SERVER_TMUX_LOG" 2>/dev/null || true
|
||||
if [[ -f "$SERVER_LOG_FILE" ]]; then
|
||||
cp "$SERVER_LOG_FILE" "$SERVER_FILE_LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
fail() {
|
||||
capture_server_logs
|
||||
echo "FAIL: $1" >&2
|
||||
echo "Run directory: $RUN_DIR" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
mcc-cmd --session "$SESSION_A" "quit" >/dev/null 2>&1 || true
|
||||
mcc-cmd --session "$SESSION_B" "quit" >/dev/null 2>&1 || true
|
||||
sleep 1
|
||||
mcc-kill --session "$SESSION_A" >/dev/null 2>&1 || true
|
||||
mcc-kill --session "$SESSION_B" >/dev/null 2>&1 || true
|
||||
mc-stop "$VERSION" --confirm >/dev/null 2>&1 || true
|
||||
wait_for_server_stop "$VERSION" 20 >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
assert_session_alive() {
|
||||
local session="$1"
|
||||
local pid_file="$2"
|
||||
if [[ -s "$pid_file" ]]; then
|
||||
local pid
|
||||
pid="$(tr -cd '0-9' < "$pid_file")"
|
||||
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
tmux has-session -t "$(_mcc_tmux_session_name "$session")" 2>/dev/null
|
||||
}
|
||||
|
||||
assert_server_alive() {
|
||||
server_running "$VERSION" || fail "Shared server session is not alive"
|
||||
}
|
||||
|
||||
start_file_input_session() {
|
||||
local session="$1"
|
||||
local username="$2"
|
||||
local port="$3"
|
||||
bash "$REPO_ROOT/tools/mcc-debug.sh" \
|
||||
--version "$VERSION" \
|
||||
--port "$port" \
|
||||
--file-input \
|
||||
--no-build \
|
||||
--session "$session" \
|
||||
--username "$username" >/dev/null
|
||||
}
|
||||
|
||||
bash "$SCRIPT_DIR/preflight_test_env.sh" "$VERSION" >/dev/null
|
||||
bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$VERSION" >/dev/null
|
||||
"$SCRIPT_DIR/ensure_offline_server.sh" "$VERSION" >/dev/null
|
||||
mcc-reset-session --session "$SESSION_A" >/dev/null
|
||||
mcc-reset-session --session "$SESSION_B" >/dev/null
|
||||
|
||||
echo "Building MCC..."
|
||||
mcc-build > "$BUILD_LOG" 2>&1 || fail "mcc-build failed"
|
||||
|
||||
echo "Starting shared server..."
|
||||
mc-start "$VERSION" >/dev/null
|
||||
wait_for_server_ready "$VERSION" || fail "Server did not become ready"
|
||||
SERVER_PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$VERSION")"
|
||||
if [[ -z "$SERVER_PORT" ]]; then
|
||||
fail "Failed to resolve server port"
|
||||
fi
|
||||
|
||||
echo "Starting MCC session A..."
|
||||
start_file_input_session "$SESSION_A" "$USERNAME_A" "$SERVER_PORT"
|
||||
echo "Starting MCC session B..."
|
||||
start_file_input_session "$SESSION_B" "$USERNAME_B" "$SERVER_PORT"
|
||||
|
||||
wait_for_file_pattern "$LOG_A" "Server was successfully joined." "session A join success" 90 || fail "Session A failed to join"
|
||||
wait_for_file_pattern "$LOG_B" "Server was successfully joined." "session B join success" 90 || fail "Session B failed to join"
|
||||
wait_for_server_log_pattern "$USERNAME_A joined the game" "server join for session A" 30 || fail "Server never logged $USERNAME_A join"
|
||||
wait_for_server_log_pattern "$USERNAME_B joined the game" "server join for session B" 30 || fail "Server never logged $USERNAME_B join"
|
||||
|
||||
echo "Sending debug state to both sessions..."
|
||||
mcc-cmd --session "$SESSION_A" "debug state"
|
||||
mcc-cmd --session "$SESSION_B" "debug state"
|
||||
sleep 2
|
||||
wait_for_file_pattern "$LOG_A" "[FileInput] > debug state" "session A debug state command" 20 || fail "Session A did not consume debug state"
|
||||
wait_for_file_pattern "$LOG_B" "[FileInput] > debug state" "session B debug state command" 20 || fail "Session B did not consume debug state"
|
||||
|
||||
echo "Killing session A..."
|
||||
mcc-kill --session "$SESSION_A" >/dev/null 2>&1 || true
|
||||
sleep 2
|
||||
|
||||
assert_session_alive "$SESSION_B" "$PID_B_FILE" || fail "Session B is not alive after killing session A"
|
||||
assert_server_alive
|
||||
|
||||
echo "Verifying session B still responds..."
|
||||
mcc-cmd --session "$SESSION_B" "health"
|
||||
wait_for_file_pattern "$LOG_B" "[FileInput] > health" "session B health command after session A kill" 20 || fail "Session B stopped responding after session A kill"
|
||||
|
||||
if [[ -s "$PID_A_FILE" ]]; then
|
||||
pid_a="$(tr -cd '0-9' < "$PID_A_FILE")"
|
||||
if [[ -n "$pid_a" ]] && kill -0 "$pid_a" 2>/dev/null; then
|
||||
fail "Session A is still alive after mcc-kill"
|
||||
fi
|
||||
fi
|
||||
|
||||
capture_server_logs
|
||||
|
||||
cat <<EOF
|
||||
PASS
|
||||
Run directory: $RUN_DIR
|
||||
Server version: $VERSION
|
||||
Server port: $SERVER_PORT
|
||||
Session A: $SESSION_A ($USERNAME_A)
|
||||
Session A input: $INPUT_A
|
||||
Session A log: $LOG_A
|
||||
Session B: $SESSION_B ($USERNAME_B)
|
||||
Session B input: $INPUT_B
|
||||
Session B log: $LOG_B
|
||||
Server log: $SERVER_FILE_LOG
|
||||
Build log: $BUILD_LOG
|
||||
EOF
|
||||
57
.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh
Executable file
57
.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh
Executable file
|
|
@ -0,0 +1,57 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: summarize_achievements_matrix.sh <matrix-run-dir>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MATRIX_DIR="$1"
|
||||
RESULTS_TSV="$MATRIX_DIR/results.tsv"
|
||||
PRECHECK_TXT="$MATRIX_DIR/preflight.txt"
|
||||
BUILD_LOG="$MATRIX_DIR/build.log"
|
||||
|
||||
if [[ ! -f "$RESULTS_TSV" ]]; then
|
||||
echo "Missing results file: $RESULTS_TSV" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "# Achievements Matrix Report"
|
||||
echo
|
||||
echo "## Executed"
|
||||
echo
|
||||
if [[ -f "$PRECHECK_TXT" ]]; then
|
||||
echo '```text'
|
||||
cat "$PRECHECK_TXT"
|
||||
echo '```'
|
||||
fi
|
||||
echo
|
||||
echo "- Matrix artifacts: \`$MATRIX_DIR\`"
|
||||
echo "- Results TSV: \`$RESULTS_TSV\`"
|
||||
echo "- Build log: \`$BUILD_LOG\`"
|
||||
echo "- Execution mode: sequential"
|
||||
echo "- Auth mode: offline"
|
||||
echo
|
||||
echo "## Observed"
|
||||
echo
|
||||
echo "| Version | Port | Family | Initial snapshot | Grant | Revoke | API callback | Verdict |"
|
||||
echo "|---|---:|---|---|---|---|---|---|"
|
||||
awk -F '\t' 'NR > 1 {
|
||||
printf("| `%s` | `%s` | %s | %s | %s | %s | %s | %s |\n",
|
||||
$1, $3, $4, $5, $6, $7, $8, $9);
|
||||
}' "$RESULTS_TSV"
|
||||
|
||||
echo
|
||||
echo "## Artifact Links"
|
||||
echo
|
||||
awk -F '\t' 'NR > 1 {
|
||||
printf("- `%s`: run=`%s`, mcc=`%s`, server=`%s`, commands=`%s`\n", $1, $11, $12, $13, $14);
|
||||
printf(" note: %s\n", $10);
|
||||
}' "$RESULTS_TSV"
|
||||
|
||||
echo
|
||||
echo "## Inferred"
|
||||
echo
|
||||
echo "- Only rows with real MCC and server-log artifacts count as executed proof."
|
||||
echo "- Rows blocked by missing Java, tmux, or server directories are environment-limited, not product pass results."
|
||||
echo "- Rows with missing MCC or command-log artifacts should be treated as harness failures until rerun confirms a product issue."
|
||||
25
.skills/mcc-integration-testing/scripts/summarize_test_run.sh
Executable file
25
.skills/mcc-integration-testing/scripts/summarize_test_run.sh
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env zsh
|
||||
set -euo pipefail
|
||||
|
||||
RUN_ROOT="${TMPDIR:-/tmp}/mcc-integration-testing"
|
||||
RUN_DIR="${1:-$(find "$RUN_ROOT" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1)}"
|
||||
|
||||
if [[ -z "${RUN_DIR:-}" ]] || [[ ! -d "$RUN_DIR" ]]; then
|
||||
echo "Run directory not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MCC_LOG="$RUN_DIR/mcc.log"
|
||||
SERVER_LOG="$RUN_DIR/server-latest.log"
|
||||
BUILD_LOG="$RUN_DIR/build.log"
|
||||
|
||||
echo "Run directory: $RUN_DIR"
|
||||
echo
|
||||
echo "Build result:"
|
||||
grep -E "Warning\(s\)|Error\(s\)|Time Elapsed" "$BUILD_LOG" || true
|
||||
echo
|
||||
echo "MCC highlights:"
|
||||
grep -E "Server was successfully joined|FileInput|smoke_test_from_mcc_full_spectrum|There are [0-9]+ of a max|health|Creative" "$MCC_LOG" || true
|
||||
echo
|
||||
echo "Server highlights:"
|
||||
grep -E "joined the game|Made .* a server operator|game mode|smoke_test_from_mcc_full_spectrum|summon|particle|playsound|tnt" "$SERVER_LOG" || true
|
||||
406
.skills/mcc-prompt-engineer/SKILL.md
Normal file
406
.skills/mcc-prompt-engineer/SKILL.md
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
---
|
||||
name: mcc-prompt-engineer
|
||||
description: >
|
||||
Manually triggered skill for the Minecraft Console Client (MCC) project
|
||||
(https://github.com/MCCTeam/Minecraft-Console-Client). Invoke this skill
|
||||
when the user wants to create, design, or generate a high-quality prompt for
|
||||
addressing any MCC-related development request -- bug fixes, new features,
|
||||
refactors, protocol work, authentication, bot scripting, or architecture
|
||||
decisions. The skill interviews the user, explores the MCC codebase via
|
||||
sub-agents, identifies relevant project skills, and synthesises everything
|
||||
into a state-of-the-art, self-contained prompt that includes an embedded
|
||||
reasoning framework, plan-mode directives, skill references, and targeted
|
||||
sub-agent instructions. Do NOT trigger automatically; wait for the user to
|
||||
explicitly invoke it (e.g. "generate a prompt for...", "build me a prompt",
|
||||
"/mcc-prompt-engineer", or "use the MCC prompt skill").
|
||||
compatibility: "Claude Code, Cursor, Codex, GitHub Copilot, Windsurf, and any AI coding agent. Optional tools: AskUserQuestion, Task, WebSearch, plan."
|
||||
---
|
||||
|
||||
# MCC Prompt Engineer
|
||||
|
||||
Generates state-of-the-art prompts for Minecraft Console Client development
|
||||
tasks. Combines live codebase knowledge (via sub-agents and AGENTS.md),
|
||||
structured prompt engineering patterns, an embedded ULTRATHINK reasoning
|
||||
framework, and the MCC project's skill ecosystem so the produced prompt is
|
||||
immediately ready to use in any AI coding agent.
|
||||
|
||||
---
|
||||
|
||||
## Reference files -- load on demand
|
||||
|
||||
| File | Load when |
|
||||
|---|---|
|
||||
| `references/reasoning-framework.md` | Embedding the ULTRATHINK protocol into the generated prompt |
|
||||
| `references/prompt-patterns.md` | Selecting the right structural patterns for the prompt |
|
||||
|
||||
Additionally, read `AGENTS.md` at the repository root early in the process.
|
||||
It contains the authoritative codebase map -- module responsibilities, key
|
||||
file paths, architecture overview, version support table, and engineering
|
||||
DO/DON'T guidance -- and replaces the need for broad exploratory file reads.
|
||||
|
||||
---
|
||||
|
||||
## Step 0 -- Environment Detection
|
||||
|
||||
Determine which tools are available before doing anything else. This gates how
|
||||
you ask questions and spawn sub-agents.
|
||||
|
||||
```
|
||||
Claude Code -> AskUserQuestion and Task tools; plan mode via "plan" tool
|
||||
or /plan command.
|
||||
Cursor / Codex -> No AskUserQuestion; ask clarifying questions inline as a
|
||||
numbered list; sub-agents via parallel tool calls where
|
||||
supported, otherwise inline.
|
||||
GitHub Copilot -> Similar to Cursor; use runSubagent where available.
|
||||
Other agents -> Fall back to inline questions and sequential exploration.
|
||||
```
|
||||
|
||||
Record your environment determination internally before continuing.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 -- Parse the Request
|
||||
|
||||
Extract everything the user has stated. Do not invent requirements or make
|
||||
assumptions yet. Capture:
|
||||
|
||||
- **Domain area:** authentication, bot scripting, protocol handling, network,
|
||||
performance, refactor, new feature, bug fix, version adaptation, or other.
|
||||
- **Stated goal:** what the user wants to achieve.
|
||||
- **Known constraints:** language version (C# 14 / .NET 10), compatibility
|
||||
requirements, scope limits (additive-only, etc.).
|
||||
- **References provided:** URLs, file paths, issue numbers, error messages.
|
||||
- **Ambiguity level:** High (proceed) / Medium (note gaps) / Low (clarify
|
||||
before continuing).
|
||||
|
||||
---
|
||||
|
||||
## Step 2 -- Clarification Interview
|
||||
|
||||
**Goal:** Resolve all blocking ambiguities before spending time on codebase
|
||||
exploration. Unblocking questions first saves sub-agent round-trips.
|
||||
|
||||
### If in Claude Code
|
||||
Use the `AskUserQuestion` tool. Ask all questions in a single call -- do not
|
||||
drip-feed questions turn by turn.
|
||||
|
||||
### In any other environment
|
||||
Print a numbered list of questions. Wait for answers before proceeding.
|
||||
|
||||
### Question selection guide
|
||||
|
||||
Ask only what is genuinely blocking:
|
||||
|
||||
| Ambiguity | Blocking? | Example question |
|
||||
|---|---|---|
|
||||
| Scope of change (additive vs rewrite) | Yes | "Should this be additive, or can it replace existing code?" |
|
||||
| Target .NET / C# version | Yes if non-obvious | "Which .NET version -- 8, 10, or latest?" |
|
||||
| Auth flow variant | Yes for auth tasks | "Device-code flow, interactive browser, or both?" |
|
||||
| Performance constraints | Usually no | Skip unless the user mentioned perf |
|
||||
| Test coverage expectation | Sometimes | "Do you want unit tests, or integration guidance only?" |
|
||||
|
||||
**Always ask:**
|
||||
1. "Is there a specific file, class, or method you already know is the right
|
||||
starting point?"
|
||||
2. "Are there any hard constraints -- things the solution must NOT do or touch?"
|
||||
|
||||
Offer a best-guess assumption alongside each question so the user can confirm
|
||||
or correct rather than answer from scratch.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 -- Codebase Exploration
|
||||
|
||||
Start by reading `AGENTS.md` at the repository root. It provides the
|
||||
authoritative module map, architecture overview, version support table, and
|
||||
engineering DO/DON'T guidance. Use it to:
|
||||
|
||||
- Identify which modules and files are relevant to the user's domain
|
||||
- Understand the project's conventions and constraints
|
||||
- Pre-populate sub-agent exploration plans with concrete file paths
|
||||
|
||||
Then dispatch the following sub-agents **simultaneously**. Each must return a
|
||||
concise written summary only -- raw file contents and grep output waste context
|
||||
and degrade reasoning quality downstream (context rot).
|
||||
|
||||
### SUB-AGENT A -- Domain Explorer (read-only)
|
||||
|
||||
**Mission:** Locate and map every file, class, and method directly relevant
|
||||
to the user's domain area. Scope your search using the module map from
|
||||
AGENTS.md rather than exploring the entire repository.
|
||||
|
||||
**Scoped exploration plan (fill in before dispatching):**
|
||||
```
|
||||
Files / directories to read:
|
||||
[derived from AGENTS.md module map for this domain -- fill in concrete paths]
|
||||
|
||||
Searches to run:
|
||||
grep for: [key identifiers from the user's request]
|
||||
|
||||
Output:
|
||||
- File paths and relevant class/method names
|
||||
- The exact lines most relevant to the user's goal
|
||||
- Existing abstractions or interfaces that should be extended
|
||||
- Patterns and conventions in use
|
||||
|
||||
Stop condition: the full call-chain for the relevant feature is mapped.
|
||||
```
|
||||
|
||||
### SUB-AGENT B -- Dependency & Integration Scout (read-only)
|
||||
|
||||
**Mission:** Identify everything that calls into or depends on the domain area
|
||||
found by Sub-Agent A, so the generated prompt can correctly scope the
|
||||
integration seam.
|
||||
|
||||
**Output:**
|
||||
- All call sites that need updating or wiring
|
||||
- Public interfaces or contracts that must be preserved
|
||||
- Any existing test files covering this area
|
||||
- NuGet packages or external dependencies in use
|
||||
|
||||
**Stop condition:** the integration boundary is fully mapped.
|
||||
|
||||
### SUB-AGENT C -- Web & Docs Researcher
|
||||
|
||||
**Mission:** Search the web and official documentation for the user's domain.
|
||||
Always search the web -- do not limit research to the codebase.
|
||||
|
||||
**Suggested search targets (adapt to the domain):**
|
||||
- Official Microsoft or Mojang documentation
|
||||
- GitHub issues or PRs in MCCTeam/Minecraft-Console-Client
|
||||
- Reference implementations cited by the user
|
||||
- wiki.vg for Minecraft protocol reference
|
||||
- PrismarineJS repos for JS reference implementations
|
||||
- learn.microsoft.com for .NET or auth APIs
|
||||
|
||||
**Output:** A concise reference document: best-practice approach, known
|
||||
pitfalls, and links to authoritative sources. Flag conflicting information.
|
||||
|
||||
Await all sub-agent summaries before proceeding to Step 4.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 -- Skill Discovery
|
||||
|
||||
Scan the `.claude/skills/` directory in the project root. Read the YAML
|
||||
frontmatter (name + description) from each skill's `SKILL.md`. The current
|
||||
MCC skills and their domains:
|
||||
|
||||
| Skill | When it's relevant |
|
||||
|---|---|
|
||||
| `csharp-best-practices` | Any task that writes or modifies C# code |
|
||||
| `humanizer` | Any task that produces user-facing documentation |
|
||||
| `mcc-chatbot-authoring` | Creating or modifying bots (built-in or script) |
|
||||
| `mcc-dev-workflow` | Building MCC, starting test servers, debugging |
|
||||
| `mcc-integration-testing` | Validating changes against a real Minecraft server |
|
||||
| `mcc-version-adaptation` | Adding support for a new Minecraft version |
|
||||
|
||||
Identify which skills are relevant to the user's request. Record them for
|
||||
inclusion in the generated prompt's `<available_skills>` block.
|
||||
|
||||
The downstream agent running the prompt has access to these same skills.
|
||||
Pointing it to the right ones gives it domain-specific working knowledge
|
||||
that significantly improves output quality -- like handing a new engineer
|
||||
the right onboarding docs before they start.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 -- Synthesis
|
||||
|
||||
Combine the sub-agent summaries, user answers, AGENTS.md context, and skill
|
||||
catalogue into a single internal knowledge base:
|
||||
|
||||
```
|
||||
## Synthesis Note
|
||||
|
||||
Goal (one sentence): ...
|
||||
Domain files: [key paths from Sub-Agent A]
|
||||
Integration seam: [from Sub-Agent B -- what must not break]
|
||||
External references: [from Sub-Agent C]
|
||||
Conventions: [from AGENTS.md engineering guidance]
|
||||
Relevant skills: [from Step 4]
|
||||
Blocking unknowns remaining: [if any, ask the user now]
|
||||
```
|
||||
|
||||
If blocking unknowns remain, ask them now before generating the prompt.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 -- Generate the Prompt
|
||||
|
||||
Read `references/reasoning-framework.md` and `references/prompt-patterns.md`
|
||||
now if you have not already.
|
||||
|
||||
Build the final prompt using the **Prompt Assembly Checklist** below. Every
|
||||
item must be addressed -- a missing item is a prompt defect.
|
||||
|
||||
### Prompt Assembly Checklist
|
||||
|
||||
- [ ] `<role>` block: domain expert covering all relevant technologies.
|
||||
- [ ] `<context>` block: synthesised from user goal + sub-agent findings.
|
||||
Include the exact error message or failure mode if provided.
|
||||
Pre-answer known facts so the downstream agent does not re-derive them.
|
||||
- [ ] `<agents_md>` directive: instruct the agent to read AGENTS.md for the
|
||||
module map, architecture, and engineering guidance.
|
||||
- [ ] `<available_skills>` block: list the relevant skills from Step 4 with
|
||||
file paths and when to load each one.
|
||||
- [ ] `<reasoning_protocol>` block: adapted ULTRATHINK framework.
|
||||
Phase 0 orientation pre-answered where certain.
|
||||
Phase 1 requirements pre-seeded from the synthesis note.
|
||||
Phase 2 decomposition pre-seeded with sub-tasks.
|
||||
Phase 2D exploration plan pre-populated with real file paths.
|
||||
Phase 4 self-validation items domain-specific and verifiable.
|
||||
- [ ] Adversarial review step: instruct the agent to critique its own plan
|
||||
before implementation -- check for incorrect assumptions, missing edge
|
||||
cases, scope creep, and security issues.
|
||||
- [ ] Sub-agent directives: at minimum a Codebase Explorer and an External
|
||||
Researcher, each with scoped missions and summary-only output rules.
|
||||
- [ ] Plan mode directive: must appear before Phase 0. Require a written
|
||||
plan presented as a Markdown checklist before any code is written.
|
||||
- [ ] `<design_goals>` block: 3-6 measurable, verifiable goals.
|
||||
- [ ] `<scope_constraint>` block: name specific directories, classes, or
|
||||
files that must NOT be touched.
|
||||
- [ ] `<output_format>` block: ordered delivery -- planning artefacts first,
|
||||
then implementation files.
|
||||
- [ ] Web search mandate in at least one sub-agent directive.
|
||||
- [ ] Anti-hallucination anchors: name the exact APIs, URLs, packet IDs, or
|
||||
protocol details that are high-risk fabrication targets.
|
||||
- [ ] C# standards: reference the `csharp-best-practices` skill when the
|
||||
task involves writing C# code.
|
||||
|
||||
### Prompt structure template
|
||||
|
||||
Use this XML skeleton. Populate every block from the synthesis note and the
|
||||
assembly checklist above.
|
||||
|
||||
```xml
|
||||
<role>
|
||||
[Domain expert covering: C# 14 / .NET 10, the specific protocol/feature
|
||||
domain, MCC project conventions from AGENTS.md]
|
||||
</role>
|
||||
|
||||
<context>
|
||||
[User goal restated. Known error or failure mode. Why the current state
|
||||
is insufficient. What "done" looks like. Key facts pre-answered.]
|
||||
</context>
|
||||
|
||||
<agents_md>
|
||||
Read AGENTS.md at the repository root before starting implementation.
|
||||
It contains the authoritative module map, architecture overview, version
|
||||
support table, and engineering DO/DON'T guidance. Use it to orient yourself
|
||||
and scope your exploration. When AGENTS.md and other docs disagree, prefer
|
||||
current code, then AGENTS.md.
|
||||
</agents_md>
|
||||
|
||||
<available_skills>
|
||||
The following project skills are at .claude/skills/ and should be loaded
|
||||
(by reading their SKILL.md) when their domain applies to this task:
|
||||
|
||||
[List only relevant skills, one per line:]
|
||||
- csharp-best-practices (.claude/skills/csharp-best-practices/SKILL.md):
|
||||
Read before writing or reviewing any C# code.
|
||||
- [other relevant skills...]
|
||||
|
||||
Load skills just-in-time as you reach relevant work, not all upfront.
|
||||
</available_skills>
|
||||
|
||||
<reasoning_protocol>
|
||||
## Plan Before Code (non-negotiable)
|
||||
|
||||
Before writing any implementation code, produce and present a complete
|
||||
written plan as a Markdown checklist. If a plan mode tool or command is
|
||||
available, activate it now and remain in plan mode until the plan is
|
||||
explicitly approved. Do not write a single line of production code until
|
||||
the plan is confirmed.
|
||||
|
||||
[Adapted ULTRATHINK framework from references/reasoning-framework.md.
|
||||
Pre-answer Phase 0; pre-seed Phases 1 and 2; configure Phase 2D with
|
||||
actual file paths; make Phase 4 checklist verifiable for this task.
|
||||
|
||||
Add an adversarial self-review step after planning:
|
||||
Re-read your plan as a sceptical senior engineer. Check for incorrect
|
||||
assumptions about MCC internals, missing edge cases, scope creep,
|
||||
anti-patterns, and security issues.]
|
||||
</reasoning_protocol>
|
||||
|
||||
<design_goals>
|
||||
[3-6 measurable, verifiable goals. Each checkable with a yes/no answer.]
|
||||
</design_goals>
|
||||
|
||||
<scope_constraint>
|
||||
[What must NOT be modified. Name specific directories, classes, or files.
|
||||
What must remain backwards-compatible. What to avoid even if it seems
|
||||
helpful.]
|
||||
</scope_constraint>
|
||||
|
||||
<output_format>
|
||||
[Ordered: planning artefacts first (checklist, design decisions, critique
|
||||
summary), then implementation files, then compliance report.]
|
||||
</output_format>
|
||||
```
|
||||
|
||||
### Sub-agent output discipline
|
||||
|
||||
Every sub-agent directive in the generated prompt must include:
|
||||
|
||||
> "Return a concise written summary only. Do NOT dump raw file contents,
|
||||
> grep output, or unprocessed tool results into the main context."
|
||||
|
||||
This prevents context rot -- irrelevant tokens dilute focus and degrade
|
||||
the agent's reasoning quality.
|
||||
|
||||
---
|
||||
|
||||
## Step 7 -- Prompt Quality Gate
|
||||
|
||||
Before delivering, verify every item:
|
||||
|
||||
```
|
||||
- [ ] Every block (<role>, <context>, <agents_md>, <available_skills>,
|
||||
<reasoning_protocol>, <design_goals>, <scope_constraint>,
|
||||
<output_format>) is present and non-empty.
|
||||
- [ ] The prompt directs the agent to read AGENTS.md for orientation.
|
||||
- [ ] <available_skills> lists the correct skills for this task's domain.
|
||||
- [ ] Phase 2D has actual file paths, not generic placeholders.
|
||||
- [ ] Plan mode directive appears before Phase 0.
|
||||
- [ ] All sub-agents have scoped missions and summary-only output rules.
|
||||
- [ ] At least one sub-agent has an explicit web search mandate.
|
||||
- [ ] Phase 4 items are objectively verifiable for THIS task.
|
||||
- [ ] Anti-hallucination anchors target this domain's fabrication risks.
|
||||
- [ ] Scope constraint is specific enough to prevent accidental drift.
|
||||
- [ ] A senior engineer reading this prompt would immediately understand
|
||||
what success looks like.
|
||||
```
|
||||
|
||||
Fix any unchecked items before delivering.
|
||||
|
||||
---
|
||||
|
||||
## Step 8 -- Deliver
|
||||
|
||||
Present the generated prompt in a fenced code block (` ```xml `) so the user
|
||||
can copy it cleanly.
|
||||
|
||||
Follow with a brief plain-English summary (3-5 sentences) explaining:
|
||||
- What the prompt will instruct the agent to do
|
||||
- Which MCC files and skills the agent will be directed to
|
||||
- The most likely blocking decision points
|
||||
- Any remaining assumptions the user should validate
|
||||
|
||||
---
|
||||
|
||||
## Anti-patterns -- never do these
|
||||
|
||||
- Do not ask more than 3-4 clarifying questions at once.
|
||||
- Do not start codebase exploration before asking clarifying questions --
|
||||
you may explore the wrong area entirely.
|
||||
- Do not generate a prompt that skips the planning phase.
|
||||
- Do not populate Phase 2D with generic placeholders like "[auth directory]"
|
||||
-- use actual file paths.
|
||||
- Do not produce a prompt with vague scope constraints. "Don't touch
|
||||
unrelated code" requires the agent to guess. Name the specific files
|
||||
and directories that are out of bounds.
|
||||
- Do not include sub-agent raw output in the final prompt -- the prompt
|
||||
should instruct the downstream agent to do its own exploration. Your
|
||||
sub-agent findings inform the prompt's specificity, not its content.
|
||||
- Do not list skills in `<available_skills>` that are irrelevant to the task.
|
||||
176
.skills/mcc-prompt-engineer/references/prompt-patterns.md
Normal file
176
.skills/mcc-prompt-engineer/references/prompt-patterns.md
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# Prompt Engineering Patterns for MCC Tasks
|
||||
# Reference file — load when selecting structural patterns for the generated prompt
|
||||
|
||||
---
|
||||
|
||||
## Core Principles (Anthropic / 2025–2026 Best Practices)
|
||||
|
||||
### 1. Structural Clarity over Prose Instructions
|
||||
XML tags are the most reliable structural delimiter for Claude and most modern
|
||||
coding agents. Use `<role>`, `<context>`, `<reasoning_protocol>`,
|
||||
`<design_goals>`, `<scope_constraint>`, and `<output_format>` consistently.
|
||||
Agents parse tagged blocks more reliably than numbered lists in free prose.
|
||||
|
||||
### 2. Pre-Answer What You Know
|
||||
Do not make the agent re-derive facts you already know. If codebase exploration
|
||||
has identified the exact failing file and line, put it in `<context>`. If the
|
||||
success criterion is clear, state it explicitly in Phase 1 instead of asking
|
||||
the agent to infer it. Every pre-answered item is one fewer reasoning step
|
||||
the agent can get wrong.
|
||||
|
||||
### 3. Plan Mode is Non-Negotiable for Complex Tasks
|
||||
Any task touching more than two files or requiring architectural decisions MUST
|
||||
include an explicit plan-mode directive. Agents that skip planning produce
|
||||
lower-quality code and are harder to course-correct. The directive must appear
|
||||
before Phase 0 so it gates the entire session.
|
||||
|
||||
### 4. Sub-Agents for Context Hygiene
|
||||
The main agent context is a finite, precious resource. Exploratory work (file
|
||||
reads, web searches, grep runs) that is consumed but not needed in the final
|
||||
output should always be delegated to sub-agents that return summaries only.
|
||||
Keyword: "Return a concise written summary. Do NOT dump raw output into the
|
||||
main context."
|
||||
|
||||
### 5. Adversarial Critique Before Implementation
|
||||
A plan reviewed only by the author is a plan that inherits the author's blind
|
||||
spots. Every complex prompt must include a Phase 2G adversarial sub-agent that
|
||||
reviews the plan before any code is written. This is the single highest-ROI
|
||||
addition to any agentic prompt.
|
||||
|
||||
### 6. Domain-Specific Anti-Hallucination Anchors
|
||||
Generic anti-hallucination instructions ("don't make things up") are weakly
|
||||
effective. Effective anchors name the exact high-risk domains:
|
||||
- OAuth endpoint URLs (fabrication-prone)
|
||||
- MSAL / Microsoft auth API signatures (version-sensitive)
|
||||
- Minecraft protocol packet IDs and field layouts (specialised, sparse training data)
|
||||
- MCC internal class/method names (not in general training data)
|
||||
|
||||
### 7. Scope Constraints Must Be Specific, Not Vague
|
||||
"Don't touch unrelated code" is not a constraint — it requires the agent to
|
||||
make a judgement call. A good scope constraint names specific directories,
|
||||
classes, or files that are out of bounds, and states the integration boundary
|
||||
precisely.
|
||||
|
||||
### 8. Output Format as a Delivery Contract
|
||||
The `<output_format>` block is a contract, not a suggestion. It must specify:
|
||||
- The ordering of output sections (planning artefacts before code).
|
||||
- File naming conventions.
|
||||
- Code block format (fenced, with filename on the opening fence line).
|
||||
- Which artefacts accompany the code (checklist, critique summary, compliance
|
||||
report).
|
||||
|
||||
---
|
||||
|
||||
## Pattern Library
|
||||
|
||||
### Pattern A — Bug Fix with Root Cause Isolation
|
||||
|
||||
Best for: authentication failures, network errors, unexpected exceptions.
|
||||
|
||||
Key additions to the reasoning protocol:
|
||||
- Phase 1.3 must include implicit requirement: "the fix must not alter the
|
||||
working behaviour of any adjacent auth/network path."
|
||||
- Phase 2D exploration plan must identify both the failing path AND the
|
||||
expected (working) path for comparison.
|
||||
- Phase 4 checklist must include: "Does the fix reproduce the error in a
|
||||
test harness before claiming it is resolved?"
|
||||
|
||||
### Pattern B — Refactor + New Module Introduction
|
||||
|
||||
Best for: extracting monolithic logic into a dedicated, testable module.
|
||||
|
||||
Key additions:
|
||||
- Phase 2F Tree of Thoughts must include a "module boundary" decision.
|
||||
- Design goals must include: "the module's public API is stable and versioned."
|
||||
- Scope constraint must name exactly which existing files are being replaced
|
||||
vs. which are being delegated to (the integration seam).
|
||||
- A compliance sub-agent must verify the old entry point still works after
|
||||
the refactor.
|
||||
|
||||
### Pattern C — Protocol / Network Implementation
|
||||
|
||||
Best for: Minecraft packet handling, connection management, session state.
|
||||
|
||||
Key additions:
|
||||
- Sub-Agent B (researcher) must be directed to the Minecraft wiki and any
|
||||
open-source reference clients (e.g., wiki.vg, Prismarine).
|
||||
- Anti-hallucination anchor: "Never fabricate packet IDs, field types, or
|
||||
VarInt boundaries — cross-check against the official protocol documentation."
|
||||
- Phase 4 must include: "Are all packet field offsets and types verified
|
||||
against the official protocol spec?"
|
||||
|
||||
### Pattern D — C# Language Modernisation
|
||||
|
||||
Best for: C# 14 features, record types, primary constructors, pattern matching.
|
||||
|
||||
Key additions:
|
||||
- Sub-Agent C (style auditor) must check the existing use of record types in
|
||||
the project before prescribing new ones.
|
||||
- Design goals must specify which C# 14 features are required vs. optional.
|
||||
- Anti-hallucination anchor: "Do not assume C# 14 features are available unless
|
||||
the project's .csproj has been confirmed to target .NET 10 or a compatible
|
||||
SDK."
|
||||
- Phase 4 must include: "Does the code compile cleanly against the target
|
||||
.NET version? Are there any C# 14 features used that require a language
|
||||
version pragma?"
|
||||
|
||||
### Pattern E — Bot Scripting / Extension
|
||||
|
||||
Best for: new bot actions, scripting API extensions, event hooks.
|
||||
|
||||
Key additions:
|
||||
- Sub-Agent A must locate the scripting API surface (CSharpRunner/ChatBot)
|
||||
and any existing event dispatcher / hook registration code.
|
||||
- Design goals must include: "the new API is backwards-compatible with
|
||||
existing user scripts."
|
||||
- Scope constraint must specify: "do not modify the scripting runtime loader
|
||||
or the existing public API surface -- extend only."
|
||||
|
||||
### Pattern F -- Context Engineering / JIT Context Loading
|
||||
|
||||
Best for: tasks where the agent needs broad codebase awareness without context
|
||||
overload, or tasks that span multiple subsystems.
|
||||
|
||||
Key additions:
|
||||
- The prompt must include an `<agents_md>` block containing the AGENTS.md code
|
||||
map so the agent has reliable structural orientation from the start.
|
||||
- An `<available_skills>` block lists skills the agent can invoke for domain-
|
||||
specific guidance (e.g., `mcc-chatbot-authoring`, `mcc-version-adaptation`).
|
||||
- Sub-agents must return concise summaries, not raw file dumps -- protect the
|
||||
main context from noise.
|
||||
- Phase 2D exploration must use targeted searches (grep, semantic search) with
|
||||
explicit stop conditions, not open-ended file reads.
|
||||
- Context rot prevention: avoid stale cached assumptions; re-verify facts that
|
||||
are older than the current execution context.
|
||||
- For multi-step sessions: periodically summarise completed work to reclaim
|
||||
context space. Emit incremental progress rather than accumulating full
|
||||
history.
|
||||
|
||||
---
|
||||
|
||||
## Prompt Length Calibration
|
||||
|
||||
| Task complexity | Recommended prompt size |
|
||||
|---|---|
|
||||
| Single-file bug fix | ~40–80 lines — short role, context, 3-phase reasoning, clear output |
|
||||
| Module refactor | ~120–200 lines — full ULTRATHINK, 4 sub-agents, ToT decisions |
|
||||
| New protocol feature | ~150–250 lines — full ULTRATHINK, external research mandate, wiki anchors |
|
||||
| Architecture overhaul | ~200–300 lines — full ULTRATHINK, 5+ sub-agents, compliance verifier |
|
||||
|
||||
Longer is not better. Every line in a prompt that does not add precision or
|
||||
constraint is a line that dilutes the signal. Trim ruthlessly after drafting.
|
||||
|
||||
---
|
||||
|
||||
## Checklist: Signs of a Weak Prompt
|
||||
|
||||
- The role block is generic ("expert software engineer") rather than domain-specific.
|
||||
- `<context>` omits the exact error message or failing state.
|
||||
- Phase 2D exploration plan uses placeholders like "[auth directory]" instead
|
||||
of real MCC paths.
|
||||
- Sub-agents have open-ended missions ("research everything about X").
|
||||
- No adversarial critique phase.
|
||||
- Scope constraint says "don't touch unrelated code" without naming specific
|
||||
files or directories.
|
||||
- `<output_format>` does not specify the ordering or the accompanying artefacts.
|
||||
- Plan mode directive is absent or appears after Phase 0.
|
||||
383
.skills/mcc-prompt-engineer/references/reasoning-framework.md
Normal file
383
.skills/mcc-prompt-engineer/references/reasoning-framework.md
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
# ULTRATHINK Reasoning Framework
|
||||
# Reference file -- load into context when building the <reasoning_protocol> block
|
||||
|
||||
---
|
||||
|
||||
## Identity & Core Directive
|
||||
|
||||
You are an expert AI coding agent operating with maximum reasoning effort.
|
||||
Your primary purpose is to help engineers build correct, maintainable,
|
||||
production-ready software. You apply System 2 thinking at all times: slow,
|
||||
methodical, and fully verifiable -- never impulsive.
|
||||
|
||||
You are equally capable of handling general-purpose (non-programming) tasks;
|
||||
the same structured reasoning applies to any domain.
|
||||
|
||||
Non-negotiable quality standards:
|
||||
- Correctness over speed.
|
||||
- Explicit over implicit -- every reasoning step is visible and checkable.
|
||||
- Verification over assumption -- validate before building on any result.
|
||||
- Honesty about uncertainty -- never fabricate; flag knowledge gaps clearly.
|
||||
|
||||
---
|
||||
|
||||
## Reasoning Protocol (ULTRATHINK Mode)
|
||||
|
||||
Engage extended, deliberate reasoning for every non-trivial request.
|
||||
Apply the full protocol below. For simple, unambiguous tasks you may compress
|
||||
phases, but never skip verification.
|
||||
|
||||
---
|
||||
|
||||
### Phase 0 -- Orientation (always execute first)
|
||||
|
||||
Before doing anything else, ask yourself:
|
||||
|
||||
1. What type of request is this?
|
||||
- New feature / implementation
|
||||
- Bug investigation / fix
|
||||
- Refactor / improvement
|
||||
- Code review / audit
|
||||
- Architecture / design decision
|
||||
- General (non-programming) question
|
||||
- Combination of the above
|
||||
|
||||
2. What is the confidence level on the requirements?
|
||||
- High: requirements are unambiguous -> proceed to decomposition.
|
||||
- Medium: some ambiguity -> note the ambiguities and resolve them (Phase 2C)
|
||||
before coding.
|
||||
- Low: requirements are underspecified -> ask targeted clarifying questions
|
||||
before any other work.
|
||||
|
||||
3. Does this require codebase exploration?
|
||||
- Yes -> plan and execute exploration (Phases 2D-2E) before implementation.
|
||||
- No -> proceed directly to planning (Phase 2F).
|
||||
|
||||
---
|
||||
|
||||
### Phase 1 -- Query Analysis
|
||||
|
||||
Parse the request deeply. Surface all explicit and implicit requirements.
|
||||
|
||||
```
|
||||
Step 1.1: Restate the goal in your own words (one concise sentence).
|
||||
Step 1.2: List explicit requirements (stated directly).
|
||||
Step 1.3: Identify implicit requirements (unstated but necessary for a correct solution).
|
||||
Step 1.4: Identify constraints: language, framework, performance, compatibility, security, style.
|
||||
Step 1.5: Identify success criteria -- how will you know the solution is correct and complete?
|
||||
Step 1.6: Flag unknowns and ambiguities (mark each as [BLOCKING] or [NON-BLOCKING]).
|
||||
```
|
||||
|
||||
Internal check before proceeding:
|
||||
- [ ] Do I have enough information to decompose the problem without inventing
|
||||
requirements?
|
||||
- [ ] Are there [BLOCKING] unknowns that require clarification?
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 -- Problem Decomposition
|
||||
|
||||
Break the problem into a set of coherent, independently verifiable sub-tasks.
|
||||
|
||||
For each sub-task identify:
|
||||
- Input: what it depends on.
|
||||
- Output: what it produces.
|
||||
- Constraints: specific rules that apply.
|
||||
- Success criterion: how correctness is verified.
|
||||
|
||||
Represent the decomposition as a checklist:
|
||||
|
||||
```markdown
|
||||
## Implementation Plan
|
||||
|
||||
- [ ] Sub-task 1: [description] | Input: ... | Output: ... | Verify: ...
|
||||
- [ ] Sub-task 2: [description] | Input: ... | Output: ... | Verify: ...
|
||||
- [ ] Sub-task 3: Verification checkpoint -- [what is confirmed here]
|
||||
```
|
||||
|
||||
Mark each item complete only after it is verified. Update the plan dynamically
|
||||
if new information emerges.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2C -- Clarification Requests (when needed)
|
||||
|
||||
Trigger this phase when [BLOCKING] unknowns exist.
|
||||
|
||||
- Ask targeted, specific questions -- one or two per turn, not a waterfall
|
||||
of queries.
|
||||
- For each question, state why it is blocking (what decision it gates).
|
||||
- Offer your best-guess assumption alongside the question so the user can
|
||||
confirm or correct, rather than starting from a blank slate.
|
||||
- Do not begin implementation until [BLOCKING] unknowns are resolved.
|
||||
|
||||
Example format:
|
||||
|
||||
> **Clarification needed (blocking):**
|
||||
> Q1: Should the authentication middleware run before or after rate limiting?
|
||||
> This gates the ordering of middleware stacks.
|
||||
> *My assumption:* authentication first, so unauthenticated requests are
|
||||
> rejected before consuming rate-limit quota. Please confirm or correct.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2D -- Codebase Exploration Planning (when needed)
|
||||
|
||||
Before exploring, write a minimal, scoped exploration plan. Over-exploration
|
||||
fills context with noise and degrades reasoning quality.
|
||||
|
||||
```markdown
|
||||
## Exploration Plan
|
||||
|
||||
Goal: [What specific information is needed to implement the solution?]
|
||||
|
||||
Files / directories to read:
|
||||
1. [path/to/file] -- reason: [why this file is relevant]
|
||||
2. [path/to/directory] -- reason: [what pattern/interface to discover]
|
||||
|
||||
Searches to run:
|
||||
1. grep/search for: "[pattern]" -- reason: [what to confirm]
|
||||
|
||||
Stop condition: [what information, once found, means exploration is complete]
|
||||
```
|
||||
|
||||
Scope investigations narrowly. If a search would require reading hundreds of
|
||||
files, use sub-agents or targeted grep -- do not consume the main context with
|
||||
unbounded exploration.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2E -- Codebase Exploration Execution
|
||||
|
||||
Execute the plan from Phase 2D step by step.
|
||||
|
||||
After each tool call or file read:
|
||||
1. Record the finding: "Step N observation: [what was found]."
|
||||
2. Evaluate: "Does this change the implementation plan? Yes/No -- [reason]."
|
||||
3. Update Phase 2's plan if needed.
|
||||
4. Decide: continue exploration or stop (the stop condition from 2D is met).
|
||||
|
||||
Anti-pattern to avoid: reading files speculatively. Every file read must map
|
||||
to an item in the exploration plan.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2F -- Implementation / Execution Planning
|
||||
|
||||
Produce a concrete, ordered implementation plan before writing any code.
|
||||
|
||||
Apply Tree of Thoughts at every major architectural or design decision:
|
||||
|
||||
```
|
||||
Decision: [The specific choice to be made]
|
||||
|
||||
Path A: [approach] -- Pros: ... | Cons: ... | Lookahead (2-3 steps): ...
|
||||
Path B: [approach] -- Pros: ... | Cons: ... | Lookahead (2-3 steps): ...
|
||||
Path C: [approach] -- Pros: ... | Cons: ... | Lookahead (2-3 steps): ...
|
||||
|
||||
Evaluation: [Rate each path: sure / maybe / impossible for reaching a valid solution]
|
||||
Selected path: [X] -- Reason: [brief justification]
|
||||
```
|
||||
|
||||
For design decisions with significant consequences (API contracts, data models,
|
||||
security boundaries), generate 3-5 independent reasoning chains
|
||||
(Self-Consistency) and verify they converge. Divergence means deeper analysis
|
||||
is needed before proceeding.
|
||||
|
||||
The final implementation plan must be a concrete checklist (same format as
|
||||
Phase 2) with each step specific enough that its completion can be objectively
|
||||
verified.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 -- Implementation / Execution
|
||||
|
||||
Execute the plan from Phase 2F, one sub-task at a time.
|
||||
|
||||
For each step:
|
||||
|
||||
```
|
||||
Step N: [action]
|
||||
Reasoning: [why this step is correct given prior steps and constraints]
|
||||
Code / output: [the actual work]
|
||||
Verification: [test, lint, type-check, logical check -- confirm this step is correct before continuing]
|
||||
```
|
||||
|
||||
Code quality standards (always enforced):
|
||||
- Write code that a senior engineer would be proud to review.
|
||||
- Follow existing conventions discovered during codebase exploration (naming,
|
||||
formatting, patterns).
|
||||
- Prefer the simplest solution that correctly satisfies all requirements --
|
||||
avoid over-engineering.
|
||||
- Never add unrequested abstractions, extra files, or "flexibility" not asked
|
||||
for.
|
||||
- All public APIs must include documentation comments.
|
||||
- Security: never embed secrets, never trust unsanitised input, apply
|
||||
least-privilege where applicable.
|
||||
- Error paths are first-class citizens -- handle them explicitly.
|
||||
- Every new unit of behaviour must be testable; prefer test-driven
|
||||
implementation where practical.
|
||||
|
||||
Context hygiene:
|
||||
- If context is growing large, summarise completed sub-tasks instead of
|
||||
retaining full detail.
|
||||
- Temporary files, scripts, or scratch work created during iteration must be
|
||||
cleaned up at the end of the task.
|
||||
|
||||
ReAct loop for tool-augmented steps:
|
||||
|
||||
```
|
||||
Thought: [what needs to happen next and why]
|
||||
Action: [tool call / command]
|
||||
Observation: [result of the action]
|
||||
Reflection: [does the observation match expectations? adjust plan if not]
|
||||
```
|
||||
|
||||
Repeat until the sub-task is complete and verified.
|
||||
|
||||
---
|
||||
|
||||
### Phase 4 -- Self-Validation
|
||||
|
||||
Execute this phase after every sub-task and again after the final output.
|
||||
|
||||
Pre-Output Verification Checklist:
|
||||
- [ ] Backward verification: does the solution satisfy every requirement
|
||||
identified in Phase 1?
|
||||
- [ ] Logical consistency: are there internal contradictions in the code
|
||||
or reasoning?
|
||||
- [ ] Completeness: have all sub-tasks in the plan been completed and
|
||||
marked off?
|
||||
- [ ] Edge cases: does the solution handle boundary conditions, empty inputs,
|
||||
and error states?
|
||||
- [ ] Security: are there injection vectors, insecure defaults, or exposed
|
||||
sensitive data?
|
||||
- [ ] Performance: are there obvious algorithmic inefficiencies or unnecessary
|
||||
blocking operations?
|
||||
- [ ] Format compliance: does the output match the requested structure (file
|
||||
names, code style, etc.)?
|
||||
- [ ] Accuracy audit: are all factual claims, library APIs, and version
|
||||
numbers verifiable?
|
||||
- [ ] Test coverage: are there tests (or at minimum a manual verification
|
||||
script) for the new behaviour?
|
||||
|
||||
If any item fails, return to the appropriate phase, fix the issue, and
|
||||
re-verify before outputting.
|
||||
|
||||
Self-Critique Pass (mandatory):
|
||||
Ask: "What is the most likely way this solution could be wrong or incomplete?"
|
||||
If a plausible failure mode is identified, address it before delivering the
|
||||
response.
|
||||
|
||||
---
|
||||
|
||||
## Multi-Path Exploration (Tree of Thoughts) -- Detailed Rules
|
||||
|
||||
Apply at every decision point where multiple approaches exist:
|
||||
|
||||
1. Generate 2-5 alternative paths -- do not evaluate on instinct alone.
|
||||
2. For each path, ask: "Is this approach likely to reach a valid solution?"
|
||||
- Sure: the path is logically sound and all constraints are satisfied.
|
||||
- Maybe: the path could work but has unresolved risks or dependencies.
|
||||
- Impossible: the path violates a constraint or leads to a dead end.
|
||||
3. Use lookahead (2-3 steps forward) to detect dead ends early.
|
||||
4. On contradiction or impossibility, backtrack to the last valid decision
|
||||
point and explore an alternative branch.
|
||||
5. Select the most logically sound path -- not the first instinct, not the
|
||||
most familiar.
|
||||
|
||||
---
|
||||
|
||||
## Self-Consistency Verification -- Detailed Rules
|
||||
|
||||
For critical decisions or complex logic:
|
||||
|
||||
1. Generate 3-5 independent reasoning chains for the same sub-problem.
|
||||
2. Compare outputs for consistency.
|
||||
- Majority consensus -> high confidence, proceed.
|
||||
- Divergent results -> identify the error source, regenerate affected
|
||||
chains.
|
||||
3. Select the answer that is most consistent across attempts -- not the most
|
||||
confident-sounding one.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Hallucination Protocol
|
||||
|
||||
- Never fabricate API signatures, library versions, framework behaviour, or
|
||||
factual claims.
|
||||
- When uncertain, say so explicitly: "I am not certain about [X]. My best
|
||||
understanding is [Y], but you should verify this against the official
|
||||
documentation."
|
||||
- For factual claims, internally verify against known patterns. If
|
||||
verification is impossible, mark the claim as [UNVERIFIED] in the response.
|
||||
- Never invent file paths, function names, or environment variables that have
|
||||
not been confirmed through exploration.
|
||||
- Do not rationalise a plausible-sounding answer when you genuinely do not
|
||||
know.
|
||||
|
||||
---
|
||||
|
||||
## Communication Standards
|
||||
|
||||
### For programming tasks
|
||||
|
||||
- Clearly separate planning output from code output using Markdown headings.
|
||||
- Use fenced code blocks with correct language tags for all code.
|
||||
- Include inline comments for non-obvious logic.
|
||||
- When making changes to existing code, explain what changed and why --
|
||||
not just what.
|
||||
- If the solution has known limitations, state them explicitly rather than
|
||||
hiding them.
|
||||
|
||||
### For general-purpose tasks
|
||||
|
||||
- Apply the same structured reasoning protocol: analyse -> decompose ->
|
||||
plan -> execute -> verify.
|
||||
- Adapt the phases to the domain (e.g., for writing tasks, "implementation"
|
||||
is the draft; "verification" is a self-critique pass for logic,
|
||||
completeness, and accuracy).
|
||||
|
||||
### Conciseness
|
||||
|
||||
- Output only what is necessary. Avoid padding, excessive hedging, and
|
||||
repetition.
|
||||
- Do not re-state the entire problem back to the user unless a concise
|
||||
restatement aids clarity.
|
||||
- Do not express enthusiasm or use filler phrases ("Great question!",
|
||||
"Certainly!").
|
||||
|
||||
---
|
||||
|
||||
## Workflow Summary (Quick Reference)
|
||||
|
||||
```
|
||||
Phase 0 -- Orientation Classify request type and confidence level.
|
||||
Phase 1 -- Query Analysis Explicit + implicit requirements, constraints, success criteria.
|
||||
Phase 2 -- Decomposition Sub-tasks with inputs, outputs, and verification criteria.
|
||||
Phase 2C -- Clarification Ask targeted questions for [BLOCKING] unknowns only.
|
||||
Phase 2D -- Exploration Plan Scoped, minimal plan for codebase discovery.
|
||||
Phase 2E -- Exploration Execute ReAct loop over plan; stop at stop condition.
|
||||
Phase 2F -- Impl. Plan Tree-of-Thoughts design decisions; concrete checklist.
|
||||
Phase 3 -- Implementation Step-by-step with ReAct; code quality standards enforced.
|
||||
Phase 4 -- Self-Validation Pre-output checklist + self-critique pass.
|
||||
```
|
||||
|
||||
For simple, unambiguous tasks (e.g., a single-line bug fix with a clear
|
||||
diagnosis), compress Phases 0-2F into a single brief reasoning block and
|
||||
proceed to implementation. The checklist in Phase 4 always executes.
|
||||
|
||||
---
|
||||
|
||||
## Quality Principles (Non-Negotiable)
|
||||
|
||||
| Principle | Guideline |
|
||||
|---|---|
|
||||
| Precision over speed | Never rush a complex problem to appear responsive. |
|
||||
| Explicit over implicit | Make all reasoning steps visible and checkable. |
|
||||
| Verification over assumption | Validate each step before building on it. |
|
||||
| Consistency over confidence | Prefer answers with convergent reasoning paths. |
|
||||
| Simplicity over cleverness | The simplest correct solution beats an elegant wrong one. |
|
||||
| Honesty about uncertainty | Flag low-confidence areas or knowledge gaps; never paper over them. |
|
||||
| Planning before coding | A written plan, however brief, is always produced before implementation. |
|
||||
| Context discipline | Keep exploration scoped; clean up temporary artefacts; summarise completed work. |
|
||||
351
.skills/mcc-version-adaptation/SKILL.md
Normal file
351
.skills/mcc-version-adaptation/SKILL.md
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
---
|
||||
name: mcc-version-adaptation
|
||||
description: Adapt MCC palettes and protocol handling for a new Minecraft version. Use when the user wants to add support for a new MC version, compare version registries, update item/entity/block/metadata palettes, or fix protocol mismatches between MC versions.
|
||||
---
|
||||
|
||||
# MCC Version Adaptation
|
||||
|
||||
Systematic workflow for updating Minecraft Console Client to support a new Minecraft version, focusing on palette/registry changes and entity metadata.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Decompiled server source for both the old and new MC versions in `$MCC_REPO/MinecraftOfficial/<version>-decompiled/`
|
||||
- If missing, decompile and download server.jar:
|
||||
```bash
|
||||
$MCC_REPO/tools/decompile.sh --version <ver>
|
||||
```
|
||||
This auto-downloads `MinecraftDecompiler.jar` if needed, produces the decompiled source, and downloads `server.jar` into `$MCC_SERVERS/<ver>/`.
|
||||
- `tools/decompile.sh` depends on official mappings. For older versions where it refuses to decompile, fall back to a raw Java decompiler such as `cfr-decompiler` against `$MCC_SERVERS/<ver>/server.jar`. That fallback is good enough for packet inspection and registration order checks even when the output is obfuscated.
|
||||
- A test server of the target version in `$MCC_SERVERS/<version>/` (see `mcc-dev-workflow` skill)
|
||||
|
||||
## Step 0: Generate Server Reports (CRITICAL since 1.21.9)
|
||||
|
||||
**Before** analyzing decompiled source, generate authoritative registry data from the server jar:
|
||||
|
||||
```bash
|
||||
cd /tmp && java -DbundlerMainClass=net.minecraft.data.Main \
|
||||
-jar $MCC_SERVERS/<version>/server.jar \
|
||||
--reports --output /tmp/mc_reports
|
||||
```
|
||||
|
||||
This produces `/tmp/mc_reports/reports/` containing:
|
||||
- `registries.json` — all registries with **actual protocol_id** for each entry
|
||||
- `blocks.json` — all blocks with **block state IDs**
|
||||
- `packets.json` — packet protocol definitions
|
||||
|
||||
**Why this matters**: Since MC 1.21.9, some items and blocks are registered outside `Items.java`/`Blocks.java` field declarations (via block registration callbacks or other paths). The decompiled source alone will **miss** these entries. The server data generator is the only authoritative source for protocol IDs.
|
||||
|
||||
### Validation check
|
||||
Compare server registry counts against decompiled source counts:
|
||||
```bash
|
||||
python3 -c "
|
||||
import json
|
||||
with open('/tmp/mc_reports/reports/registries.json') as f:
|
||||
data = json.load(f)
|
||||
for reg in ['minecraft:item', 'minecraft:entity_type', 'minecraft:block']:
|
||||
print(f'{reg}: {len(data[reg][\"entries\"])} entries')
|
||||
"
|
||||
```
|
||||
|
||||
If server counts differ from decompiled Java source counts, the palette **must** be generated from server data, not from Java source.
|
||||
|
||||
## Step 1: Run Registry Diff
|
||||
|
||||
```bash
|
||||
python3 $MCC_REPO/tools/diff_registries.py <old_ver> <new_ver>
|
||||
```
|
||||
|
||||
This compares five registries and reports which need palette updates:
|
||||
|
||||
| Registry | MCC File | When to Update |
|
||||
|----------|----------|----------------|
|
||||
| Items.java | `ItemPalettes/ItemPaletteXXX.cs` | New/removed/reordered items |
|
||||
| EntityType.java | `EntityPalettes/EntityPaletteXXX.cs` | New/removed/reordered entity types |
|
||||
| Blocks.java | `BlockPalettes/BlockPaletteXXX.cs` | New/removed/reordered blocks |
|
||||
| DataComponents.java | `StructuredComponents/StructuredComponentsRegistryXXX.cs` | New/reordered components |
|
||||
| EntityDataSerializers.java | `EntityMetadataPalettes/EntityMetadataPaletteXXX.cs` | New/reordered serializer types |
|
||||
|
||||
**Important**: diff_registries.py compares decompiled Java source. If Step 0 revealed count mismatches, the diff output may undercount. Always cross-reference with server registries.json.
|
||||
|
||||
## Step 2: Generate Updated Palettes
|
||||
|
||||
For registries marked "PALETTE UPDATE NEEDED":
|
||||
|
||||
### Item Palette
|
||||
|
||||
**Preferred method** (accurate since 1.21.9):
|
||||
```bash
|
||||
python3 $MCC_REPO/tools/gen_item_palette.py --from-registry /tmp/mc_reports/reports/registries.json <suffix>
|
||||
# e.g., gen_item_palette.py --from-registry /tmp/mc_reports/reports/registries.json 1219
|
||||
```
|
||||
|
||||
**Legacy method** (works for versions where Items.java has all items):
|
||||
```bash
|
||||
python3 $MCC_REPO/tools/gen_item_palette.py <new_ver> <suffix>
|
||||
# e.g., gen_item_palette.py 1.21.1 121
|
||||
```
|
||||
|
||||
- If new items are reported missing from `ItemType.cs`, add them to the enum in alphabetical order.
|
||||
- The script auto-generates the C# palette file.
|
||||
|
||||
### Block Palette
|
||||
|
||||
**Preferred method** (accurate since 1.21.9):
|
||||
```bash
|
||||
python3 $MCC_REPO/tools/gen_block_palette.py /tmp/mc_reports/reports/blocks.json <suffix>
|
||||
# e.g., gen_block_palette.py /tmp/mc_reports/reports/blocks.json 1219
|
||||
```
|
||||
|
||||
**Legacy method** (manual creation from decompiled Blocks.java): Follow the pattern of existing palette files, using `register("name", ...)` call order from the decompiled source. Only reliable when Blocks.java contains all blocks.
|
||||
|
||||
If new blocks are reported missing from `Material.cs`, add them to the enum in alphabetical order.
|
||||
|
||||
### Entity Palette
|
||||
|
||||
```bash
|
||||
python3 $MCC_REPO/tools/gen_entity_palette.py /tmp/mc_reports/reports/registries.json <suffix>
|
||||
# e.g., gen_entity_palette.py /tmp/mc_reports/reports/registries.json 1219
|
||||
```
|
||||
|
||||
If new entity types are reported missing from `EntityType.cs`, add them to the enum in alphabetical order.
|
||||
|
||||
### Entity Metadata Palette
|
||||
```bash
|
||||
python3 $MCC_REPO/tools/gen_entity_metadata_palette.py <new_ver> <suffix>
|
||||
# e.g., gen_entity_metadata_palette.py 1.20.6 1206
|
||||
```
|
||||
- If new serializer types appear as UNMAPPED, add them to both:
|
||||
1. The script's `FIELD_TO_ENUM` dictionary
|
||||
2. MCC's `EntityMetaDataType.cs` enum
|
||||
3. `DataTypes.cs` read logic (add a `case` to consume the correct bytes)
|
||||
|
||||
### DataComponents / StructuredComponents
|
||||
Compare `DataComponents.java` registration order. If new components appear, update `StructuredComponentsRegistryXXX.cs`. For new component types, implement corresponding reader in `StructuredComponents/Components/`.
|
||||
|
||||
## Step 3: Update Version Routing
|
||||
|
||||
After creating palette files, update version selection logic:
|
||||
|
||||
| Palette Type | Routing Location |
|
||||
|-------------|-----------------|
|
||||
| Item | `Protocol18.cs` → `itemPalette` switch expression |
|
||||
| Entity | `Protocol18.cs` → `entityPalette` switch expression |
|
||||
| Block | `Protocol18.cs` → `blockPalette` initialization |
|
||||
| EntityMetadata | `EntityMetadataPalette.cs` → `GetPalette()` switch |
|
||||
| DataComponents | `StructuredComponentsRegistry.cs` → factory/routing |
|
||||
| Packet | `PacketType18Handler.cs` → `GetTypeHandler()` switch |
|
||||
|
||||
Pattern: add a new `>= MC_X_Y_Z_Version => new XxxPaletteXYZ()` case.
|
||||
|
||||
Also update:
|
||||
- `Protocol18.cs`: add `MC_X_Y_Z_Version = <protocol_number>` constant
|
||||
- `Protocol18.cs`: update all `> MC_prev_Version` upper-bound checks to `> MC_X_Y_Z_Version`
|
||||
- `ProtocolHandler.cs`: add version string → protocol mapping, protocol → version mapping, add to supported list
|
||||
- `Program.cs`: update `MCHighestVersion`
|
||||
|
||||
## Step 4: Check Packet Changes
|
||||
|
||||
Compare `GameProtocols.java` and `ConfigurationProtocols.java` between versions.
|
||||
|
||||
Common patterns:
|
||||
- **New clientbound packets inserted mid-list**: All subsequent packet IDs shift. Requires a new `PacketPalette` class.
|
||||
- **New packets appended at end**: Only need to add new enum values and entries in the palette.
|
||||
- **Packet renames** (same slot): Update MCC's packet type enum name but no ID change.
|
||||
|
||||
When packet changes are detected:
|
||||
1. Add new packet type enum values to `PacketTypesIn.cs`, `PacketTypesOut.cs`, `ConfigurationPacketTypesIn.cs`, `ConfigurationPacketTypesOut.cs`
|
||||
2. Create new `PacketPaletteXXX.cs` based on the previous one, adjusting IDs
|
||||
3. Update `PacketType18Handler.cs` routing
|
||||
|
||||
Use scriptable comparisons instead of eyeballing long packet tables. The packet ID is the registration index in `GameProtocols.java`:
|
||||
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
import re
|
||||
for ver in ["1.21.10", "1.21.11", "26.1"]:
|
||||
path=f"MinecraftOfficial/{ver}-decompiled/net/minecraft/network/protocol/game/GameProtocols.java"
|
||||
text=open(path).read()
|
||||
start=text.index("CLIENTBOUND_TEMPLATE")
|
||||
names=[m.group(1) for m in re.finditer(r"\.addPacket\(([^,]+),", text[start:])]
|
||||
print("==", ver, len(names))
|
||||
for i, name in enumerate(names):
|
||||
print(f"0x{i:02X}", name)
|
||||
PY
|
||||
```
|
||||
|
||||
For focused diffs:
|
||||
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
import re
|
||||
def packets(ver, marker):
|
||||
text=open(f"MinecraftOfficial/{ver}-decompiled/net/minecraft/network/protocol/game/GameProtocols.java").read()
|
||||
start=text.index(marker)
|
||||
return [m.group(1) for m in re.finditer(r"\.addPacket\(([^,]+),", text[start:])]
|
||||
left, right = "1.21.10", "1.21.11"
|
||||
a, b = packets(left, "CLIENTBOUND_TEMPLATE"), packets(right, "CLIENTBOUND_TEMPLATE")
|
||||
for i in range(max(len(a), len(b))):
|
||||
x = a[i] if i < len(a) else "<none>"
|
||||
y = b[i] if i < len(b) else "<none>"
|
||||
if x != y:
|
||||
print(f"0x{i:02X}: {left}={x} | {right}={y}")
|
||||
PY
|
||||
```
|
||||
|
||||
Do the same for `SERVERBOUND_TEMPLATE`. Clientbound and serverbound can change independently. Do not inherit a newer palette just because one side looks similar. For example, `1.21.11` used the same play packet order as `1.21.9/1.21.10` for the tested inventory path, while `26.1` had additional shifts.
|
||||
|
||||
## Step 5: Check Variant Encoding Changes
|
||||
|
||||
For entity types that use variant serializers (Cat, Wolf, Frog, Painting), check if the codec changed between versions by inspecting:
|
||||
|
||||
- `EntityDataSerializers.java` — look at how each `*_VARIANT` field is constructed
|
||||
- Key codecs:
|
||||
- `ByteBufCodecs.holderRegistry()` → wire format: `VarInt(registry_id)`
|
||||
- `ByteBufCodecs.holder()` → wire format: `VarInt(id+1)` for registered, `VarInt(0) + inline_data` for direct
|
||||
- If codec changed, update `DataTypes.cs` entity metadata reading logic accordingly.
|
||||
|
||||
## Step 6: Handle New EntityDataSerializer Types
|
||||
|
||||
When new serializer types are added (detected in Step 1):
|
||||
|
||||
1. Add enum value to `EntityMetaDataType.cs` with XML doc comment
|
||||
2. Add read logic in `DataTypes.cs` `ReadNextMetadata()`:
|
||||
- Determine byte consumption from the decompiled codec
|
||||
- Simple enum types (like CopperGolemState, WeatheringCopperState): `ReadNextVarInt(cache)`
|
||||
- Composite types (like ResolvableProfile): analyze the STREAM_CODEC chain in decompiled source
|
||||
3. Create the new palette file (Step 2)
|
||||
4. Update palette routing (Step 3)
|
||||
|
||||
## Step 7: Check SpawnEntity / Other Packet Format Changes
|
||||
|
||||
Compare key packet codec classes between versions. Known changes:
|
||||
- **1.21.9+**: `SpawnEntity` velocity fields changed from `short / 8000.0` to `LpVec3` format (VarLong-packed fixed-point). Gate reading in `DataTypes.ReadNextEntity()` by version.
|
||||
|
||||
When in doubt, compare the relevant packet class (e.g. `ClientboundAddEntityPacket.java`) between versions.
|
||||
|
||||
## Step 8: Update Block Collision Shapes (Physics Engine)
|
||||
|
||||
MCC's physics engine uses block collision shape data from PrismarineJS `minecraft-data` to perform accurate AABB collision detection (stored in `MinecraftClient/Physics/BlockShapeData.json`, embedded as a resource).
|
||||
|
||||
When a new MC version introduces new blocks or changes block shapes, update this data:
|
||||
|
||||
```bash
|
||||
# Download and compact collision shapes for the target version
|
||||
python3 $MCC_REPO/tools/gen_block_shapes.py <version>
|
||||
# e.g. python3 tools/gen_block_shapes.py 1.21.11
|
||||
```
|
||||
|
||||
If network is slow or unreliable, download the file manually and convert:
|
||||
```bash
|
||||
# Manual download
|
||||
curl -L -o /tmp/bcs.json \
|
||||
"https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/<version>/blockCollisionShapes.json"
|
||||
|
||||
# Then compact from local file
|
||||
python3 $MCC_REPO/tools/gen_block_shapes.py --from-file /tmp/bcs.json
|
||||
```
|
||||
|
||||
Output: `MinecraftClient/Physics/BlockShapeData.json` (embedded via `MinecraftClient.csproj`)
|
||||
|
||||
The JSON maps block names (snake_case) → collision shape IDs → AABB coordinates. At runtime, `BlockShapes.cs` maps MCC's block state IDs to these AABBs using the block palette.
|
||||
|
||||
**When to update**: Whenever new blocks are added that have non-trivial collision shapes (e.g., new slab variants, stairs, fences). If only items or entities changed, this step can be skipped.
|
||||
|
||||
**Data source**: PrismarineJS `minecraft-data` repo, path: `data/pc/<version>/blockCollisionShapes.json`. Version availability can be checked via `data/dataPaths.json`.
|
||||
|
||||
## Step 9: Update Minimap Block Color Map
|
||||
|
||||
Regenerate the block-to-MapColor mapping used by the TUI minimap. This maps each block's `Material` enum to the RGB color from Minecraft's official `MapColor` table.
|
||||
|
||||
```bash
|
||||
python3 $MCC_REPO/tools/gen_block_color_map.py $MCC_REPO/MinecraftOfficial/<version>-decompiled
|
||||
# e.g. python3 tools/gen_block_color_map.py MinecraftOfficial/26.1-rc-2-decompiled
|
||||
```
|
||||
|
||||
Output: `MinecraftClient/Tui/MinimapBlockColors.json` (embedded as a resource via `.csproj`).
|
||||
|
||||
The script parses `MapColor.java`, `DyeColor.java`, and `Blocks.java` from the decompiled source to extract each block's assigned map color. Blocks not matched to a known `Material` enum value are skipped.
|
||||
|
||||
**When to update**: Whenever new blocks are added or existing blocks change their `mapColor()` assignment. If only items or entities changed, this step can be skipped.
|
||||
|
||||
## Step 10: Update Minimap Entity Categories
|
||||
|
||||
Regenerate the entity-to-MobCategory mapping used by the TUI minimap for classifying entities as hostile, passive, neutral, or non-living.
|
||||
|
||||
```bash
|
||||
python3 $MCC_REPO/tools/gen_entity_category_map.py $MCC_REPO/MinecraftOfficial/<version>-decompiled
|
||||
# e.g. python3 tools/gen_entity_category_map.py MinecraftOfficial/26.1-rc-2-decompiled
|
||||
```
|
||||
|
||||
Output: `MinecraftClient/Tui/MinimapEntityCategories.json` (embedded as a resource via `.csproj`).
|
||||
|
||||
The script parses `EntityType.java` to extract each entity's `MobCategory` assignment, then maps Minecraft's categories to MCC minimap categories:
|
||||
- `MONSTER` -> hostile (with neutral overrides for conditionally hostile mobs like Enderman, Spider, Wolf)
|
||||
- `CREATURE`/`AMBIENT`/`AXOLOTLS`/`WATER_*` -> passive
|
||||
- `MISC` -> non_living (with passive overrides for Villager, WanderingTrader, ZombieHorse)
|
||||
|
||||
The script maintains manual override lists for "neutral" mobs (attack only when provoked) since Minecraft has no machine-readable flag for this behavior. Review and update the `NEUTRAL_OVERRIDES` and `PASSIVE_OVERRIDES` sets in the script when new conditionally-hostile or misclassified mobs are added.
|
||||
|
||||
**When to update**: Whenever new entity types are added. If only blocks or items changed, this step can be skipped.
|
||||
|
||||
## Step 11: Compile and Verify
|
||||
|
||||
```bash
|
||||
dotnet build $MCC_REPO/MinecraftClient.sln -c Release
|
||||
```
|
||||
|
||||
Then connect to a test server of the target version (see `mcc-dev-workflow` skill) and verify:
|
||||
- Successful connection
|
||||
- `/give` new items → check inventory for correct identification
|
||||
- `/give` existing items (diamond_sword, etc.) → verify no ID shift
|
||||
- Summon new entities → check type and health
|
||||
- Summon variant entities (wolf, cat, frog) → no metadata parse errors
|
||||
- Place new blocks → `dig` reports correct block type
|
||||
- Teleport to distant chunks → terrain loads without errors
|
||||
- Chat commands work normally
|
||||
|
||||
**Always verify basic existing items first** (e.g. diamond_sword) to catch palette ID shift bugs early. If an existing item shows as the wrong type, the palette is using wrong protocol IDs.
|
||||
|
||||
## Key Source Files Reference
|
||||
|
||||
| Decompiled Java Source | Purpose |
|
||||
|----------------------|---------|
|
||||
| `world/item/Items.java` | Item registry (field declaration order ≈ ID, **but not always since 1.21.9**) |
|
||||
| `world/entity/EntityType.java` | Entity type registry (`register()` call order = ID) |
|
||||
| `world/level/block/Blocks.java` | Block registry (`register()` call order ≈ ID, **but not always since 1.21.9**) |
|
||||
| `core/component/DataComponents.java` | Data component registry |
|
||||
| `network/syncher/EntityDataSerializers.java` | Entity metadata type registry (static block order = ID) |
|
||||
| `network/protocol/game/GameProtocols.java` | Play packet registration order (= packet IDs) |
|
||||
| `network/protocol/configuration/ConfigurationProtocols.java` | Config packet registration order |
|
||||
|
||||
| Server Data Generator Output | Purpose |
|
||||
|-----|---------|
|
||||
| `registries.json` | **Authoritative** protocol_id for all registries |
|
||||
| `blocks.json` | **Authoritative** block state IDs |
|
||||
| `packets.json` | Packet protocol definitions |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Source field order ≠ runtime registry ID (since 1.21.9)**: Some items/blocks are registered via callbacks (e.g., block items registered by `Blocks.java` during block registration) rather than in `Items.java` field declarations. Always validate palette counts against server `registries.json`. If counts differ, **use server data generator output instead of decompiled source**.
|
||||
- **ID order matters**: IDs are determined by registration order, not alphabetical. Always use server data generator as ground truth.
|
||||
- **Cross-version jumps**: When MCC skips versions (e.g., 1.20.4→1.20.6), registries from ALL intermediate versions may have changed. Always diff against the actual last-supported version, not the latest palette.
|
||||
- **EntityMetadata type shifts**: A single new serializer type shifts all subsequent IDs, causing widespread metadata parse failures. Symptoms: entity rendering glitches, disconnections, or silent data corruption.
|
||||
- **CUT_STANDSTONE_SLAB**: This is an intentional typo in Minecraft source (should be SANDSTONE). MCC's `ItemType.cs` uses `CutSandstoneSlab` — the gen script handles this via the OVERRIDES dict.
|
||||
- **Item/block renames across versions**: Some items/blocks get renamed (e.g., `DRY_SHORT_GRASS` → `SHORT_DRY_GRASS`, `CHAIN` → `IRON_CHAIN`). Keep old enum values for backward compatibility with older palettes, and add new ones for the new version.
|
||||
- **Packet ID cascading shifts**: Even one inserted mid-list clientbound packet shifts ALL subsequent IDs. Always create a new PacketPalette for protocol changes.
|
||||
- **Test existing items first**: After palette changes, always verify existing items (diamond_sword, stone, etc.) before testing new ones. If they show as wrong items, the palette has a systemic ID offset bug.
|
||||
|
||||
## Reusable Scripts
|
||||
|
||||
All scripts are in `$MCC_REPO/tools/`. See `tools/README.md` for detailed usage.
|
||||
|
||||
| Script | Purpose | Input |
|
||||
|--------|---------|-------|
|
||||
| `diff_registries.py` | Compare registries between versions | Decompiled source |
|
||||
| `gen_item_palette.py` | Generate ItemPalette C# | Decompiled source OR registries.json |
|
||||
| `gen_block_palette.py` | Generate BlockPalette C# | blocks.json |
|
||||
| `gen_entity_palette.py` | Generate EntityPalette C# | registries.json |
|
||||
| `gen_entity_metadata_palette.py` | Generate EntityMetadataPalette C# | Decompiled source |
|
||||
| `gen_block_shapes.py` | Download & compact block collision shapes | PrismarineJS minecraft-data |
|
||||
| `gen_block_color_map.py` | Generate minimap block color JSON | Decompiled source (MapColor/DyeColor/Blocks) |
|
||||
| `gen_entity_category_map.py` | Generate minimap entity category JSON | Decompiled source (EntityType.java) |
|
||||
211
.skills/mermaid-diagrams/SKILL.md
Normal file
211
.skills/mermaid-diagrams/SKILL.md
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
---
|
||||
name: mermaid-diagrams
|
||||
description: Creating and refining Mermaid diagrams with live reload. Use when users want flowcharts, sequence diagrams, class diagrams, ER diagrams, state diagrams, or any other Mermaid visualization. Provides best practices for syntax, styling, and the iterative workflow using mermaid_preview and mermaid_save tools.
|
||||
allowed-tools: mcp__mermaid__mermaid_preview, mcp__mermaid__mermaid_save
|
||||
---
|
||||
|
||||
# Mermaid Diagram Expert
|
||||
|
||||
You are an expert at creating, refining, and optimizing Mermaid diagrams using the MCP server tools.
|
||||
|
||||
## Core Workflow
|
||||
|
||||
1. **Create Initial Diagram**: Use `mermaid_preview` to render and open the diagram with live reload
|
||||
2. **Iterative Refinement**: Make improvements - the browser will auto-refresh
|
||||
3. **Save Final Version**: Use `mermaid_save` when satisfied
|
||||
|
||||
## Tool Usage
|
||||
|
||||
### mermaid_preview
|
||||
|
||||
Always use this when creating or updating diagrams:
|
||||
|
||||
- `diagram`: The Mermaid code
|
||||
- `preview_id`: Descriptive kebab-case ID (e.g., `auth-flow`, `architecture`)
|
||||
- `format`: Use `svg` for live reload (default)
|
||||
- `theme`: `default`, `forest`, `dark`, or `neutral`
|
||||
- `background`: `white`, `transparent`, or hex colors
|
||||
- `width`, `height`, `scale`: Adjust for quality/size
|
||||
|
||||
**Key Points:**
|
||||
|
||||
- Reuse the same `preview_id` for refinements to update the same browser tab
|
||||
- Use different IDs for multiple simultaneous diagrams
|
||||
- Live reload only works with SVG format
|
||||
|
||||
### mermaid_save
|
||||
|
||||
Use after the diagram is finalized:
|
||||
|
||||
- `save_path`: Where to save (e.g., `./docs/diagram.svg`)
|
||||
- `preview_id`: Must match the preview ID used earlier
|
||||
- `format`: Must match format from preview
|
||||
|
||||
## Diagram Types
|
||||
|
||||
### Flowcharts (`graph` or `flowchart`)
|
||||
|
||||
Direction: `LR`, `TB`, `RL`, `BT`
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Start] --> B{Decision}
|
||||
B -->|Yes| C[Action]
|
||||
B -->|No| D[End]
|
||||
|
||||
style A fill:#e1f5ff
|
||||
style C fill:#d4edda
|
||||
```
|
||||
|
||||
### Sequence Diagrams (`sequenceDiagram`)
|
||||
|
||||
⚠️ **Do NOT use `style` statements** - not supported
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant App
|
||||
participant API
|
||||
|
||||
User->>App: Login
|
||||
App->>API: Authenticate
|
||||
API-->>App: Token
|
||||
App-->>User: Success
|
||||
```
|
||||
|
||||
### Class Diagrams (`classDiagram`)
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class User {
|
||||
+String name
|
||||
+String email
|
||||
+login()
|
||||
}
|
||||
class Order {
|
||||
+int id
|
||||
+Date created
|
||||
}
|
||||
User "1" --> "*" Order
|
||||
```
|
||||
|
||||
### Entity Relationship (`erDiagram`)
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER ||--o{ ORDER : places
|
||||
ORDER ||--|{ LINE_ITEM : contains
|
||||
|
||||
USER {
|
||||
int id PK
|
||||
string email
|
||||
string name
|
||||
}
|
||||
```
|
||||
|
||||
### State Diagrams (`stateDiagram-v2`)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Idle
|
||||
Idle --> Processing : start
|
||||
Processing --> Complete : finish
|
||||
Complete --> [*]
|
||||
```
|
||||
|
||||
### Gantt Charts (`gantt`)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title Project Timeline
|
||||
section Phase 1
|
||||
Task 1 :a1, 2024-01-01, 30d
|
||||
Task 2 :after a1, 20d
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Preview IDs
|
||||
|
||||
- Use descriptive names: `architecture`, `auth-flow`, `data-model`
|
||||
- Keep the same ID during refinements
|
||||
- Use different IDs for concurrent diagrams
|
||||
|
||||
### Themes & Styling
|
||||
|
||||
- `default`: Clean, professional
|
||||
- `forest`: Green tones
|
||||
- `dark`: Dark background
|
||||
- `neutral`: Grayscale
|
||||
|
||||
Use `transparent` background for docs, `white` for standalone
|
||||
|
||||
### Common Patterns
|
||||
|
||||
**System Architecture:**
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Client[Web App]
|
||||
API[API Gateway]
|
||||
DB[(Database)]
|
||||
|
||||
Client --> API --> DB
|
||||
```
|
||||
|
||||
**Authentication Flow:**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
User->>App: Login Request
|
||||
App->>Auth: Validate
|
||||
Auth-->>App: JWT Token
|
||||
App-->>User: Access Granted
|
||||
```
|
||||
|
||||
## User Interaction
|
||||
|
||||
When a user requests a diagram:
|
||||
|
||||
1. **Clarify if needed**: What type? What level of detail?
|
||||
2. **Choose diagram type**:
|
||||
- Process/workflow → Flowchart
|
||||
- System interactions → Sequence
|
||||
- Code structure → Class
|
||||
- Database → ER
|
||||
- Timeline → Gantt
|
||||
3. **Create with preview**: Use descriptive `preview_id`, start with good defaults
|
||||
4. **Iterate**: Keep same `preview_id`, explain changes
|
||||
5. **Save**: Ask where/what format, use `mermaid_save`
|
||||
|
||||
## Proactive Behavior
|
||||
|
||||
- Always preview diagrams, don't just generate code
|
||||
- Use sensible defaults without asking
|
||||
- Reuse preview_id for refinements
|
||||
- Suggest improvements when you see opportunities
|
||||
- Explain your diagram type choice briefly
|
||||
|
||||
## Common Issues
|
||||
|
||||
**Syntax errors**: Check quotes, arrow syntax, keywords
|
||||
**Layout issues**: Try different directions (LR vs TB)
|
||||
**Text overlap**: Increase dimensions or shorten labels
|
||||
**Colors not working**: Verify CSS color format; remember sequence diagrams don't support styles
|
||||
|
||||
## Example Interaction
|
||||
|
||||
**User**: "Create an auth flow diagram"
|
||||
|
||||
**You**: "I'll create a sequence diagram showing the authentication flow."
|
||||
[Use mermaid_preview with preview_id="auth-flow"]
|
||||
|
||||
**User**: "Add database and error handling"
|
||||
|
||||
**You**: "I'll add database interaction and error paths."
|
||||
[Use mermaid_preview with same preview_id - browser auto-refreshes]
|
||||
|
||||
**User**: "Save it"
|
||||
|
||||
**You**: "Saving to ./docs/auth-flow.svg"
|
||||
[Use mermaid_save]
|
||||
202
.skills/skill-creator/LICENSE.txt
Normal file
202
.skills/skill-creator/LICENSE.txt
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
479
.skills/skill-creator/SKILL.md
Normal file
479
.skills/skill-creator/SKILL.md
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
---
|
||||
name: skill-creator
|
||||
description: Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, update or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy.
|
||||
---
|
||||
|
||||
# Skill Creator
|
||||
|
||||
A skill for creating new skills and iteratively improving them.
|
||||
|
||||
At a high level, the process of creating a skill goes like this:
|
||||
|
||||
- Decide what you want the skill to do and roughly how it should do it
|
||||
- Write a draft of the skill
|
||||
- Create a few test prompts and run claude-with-access-to-the-skill on them
|
||||
- Help the user evaluate the results both qualitatively and quantitatively
|
||||
- While the runs happen in the background, draft some quantitative evals if there aren't any (if there are some, you can either use as is or modify if you feel something needs to change about them). Then explain them to the user (or if they already existed, explain the ones that already exist)
|
||||
- Use the `eval-viewer/generate_review.py` script to show the user the results for them to look at, and also let them look at the quantitative metrics
|
||||
- Rewrite the skill based on feedback from the user's evaluation of the results (and also if there are any glaring flaws that become apparent from the quantitative benchmarks)
|
||||
- Repeat until you're satisfied
|
||||
- Expand the test set and try again at larger scale
|
||||
|
||||
Your job when using this skill is to figure out where the user is in this process and then jump in and help them progress through these stages. So for instance, maybe they're like "I want to make a skill for X". You can help narrow down what they mean, write a draft, write the test cases, figure out how they want to evaluate, run all the prompts, and repeat.
|
||||
|
||||
On the other hand, maybe they already have a draft of the skill. In this case you can go straight to the eval/iterate part of the loop.
|
||||
|
||||
Of course, you should always be flexible and if the user is like "I don't need to run a bunch of evaluations, just vibe with me", you can do that instead.
|
||||
|
||||
Then after the skill is done (but again, the order is flexible), you can also run the skill description improver, which we have a whole separate script for, to optimize the triggering of the skill.
|
||||
|
||||
Cool? Cool.
|
||||
|
||||
## Communicating with the user
|
||||
|
||||
The skill creator is liable to be used by people across a wide range of familiarity with coding jargon. If you haven't heard (and how could you, it's only very recently that it started), there's a trend now where the power of Claude is inspiring plumbers to open up their terminals, parents and grandparents to google "how to install npm". On the other hand, the bulk of users are probably fairly computer-literate.
|
||||
|
||||
So please pay attention to context cues to understand how to phrase your communication! In the default case, just to give you some idea:
|
||||
|
||||
- "evaluation" and "benchmark" are borderline, but OK
|
||||
- for "JSON" and "assertion" you want to see serious cues from the user that they know what those things are before using them without explaining them
|
||||
|
||||
It's OK to briefly explain terms if you're in doubt, and feel free to clarify terms with a short definition if you're unsure if the user will get it.
|
||||
|
||||
---
|
||||
|
||||
## Creating a skill
|
||||
|
||||
### Capture Intent
|
||||
|
||||
Start by understanding the user's intent. The current conversation might already contain a workflow the user wants to capture (e.g., they say "turn this into a skill"). If so, extract answers from the conversation history first — the tools used, the sequence of steps, corrections the user made, input/output formats observed. The user may need to fill the gaps, and should confirm before proceeding to the next step.
|
||||
|
||||
1. What should this skill enable Claude to do?
|
||||
2. When should this skill trigger? (what user phrases/contexts)
|
||||
3. What's the expected output format?
|
||||
4. Should we set up test cases to verify the skill works? Skills with objectively verifiable outputs (file transforms, data extraction, code generation, fixed workflow steps) benefit from test cases. Skills with subjective outputs (writing style, art) often don't need them. Suggest the appropriate default based on the skill type, but let the user decide.
|
||||
|
||||
### Interview and Research
|
||||
|
||||
Proactively ask questions about edge cases, input/output formats, example files, success criteria, and dependencies. Wait to write test prompts until you've got this part ironed out.
|
||||
|
||||
Check available MCPs - if useful for research (searching docs, finding similar skills, looking up best practices), research in parallel via subagents if available, otherwise inline. Come prepared with context to reduce burden on the user.
|
||||
|
||||
### Write the SKILL.md
|
||||
|
||||
Based on the user interview, fill in these components:
|
||||
|
||||
- **name**: Skill identifier
|
||||
- **description**: When to trigger, what it does. This is the primary triggering mechanism - include both what the skill does AND specific contexts for when to use it. All "when to use" info goes here, not in the body. Note: currently Claude has a tendency to "undertrigger" skills -- to not use them when they'd be useful. To combat this, please make the skill descriptions a little bit "pushy". So for instance, instead of "How to build a simple fast dashboard to display internal Anthropic data.", you might write "How to build a simple fast dashboard to display internal Anthropic data. Make sure to use this skill whenever the user mentions dashboards, data visualization, internal metrics, or wants to display any kind of company data, even if they don't explicitly ask for a 'dashboard.'"
|
||||
- **compatibility**: Required tools, dependencies (optional, rarely needed)
|
||||
- **the rest of the skill :)**
|
||||
|
||||
### Skill Writing Guide
|
||||
|
||||
#### Anatomy of a Skill
|
||||
|
||||
```
|
||||
skill-name/
|
||||
├── SKILL.md (required)
|
||||
│ ├── YAML frontmatter (name, description required)
|
||||
│ └── Markdown instructions
|
||||
└── Bundled Resources (optional)
|
||||
├── scripts/ - Executable code for deterministic/repetitive tasks
|
||||
├── references/ - Docs loaded into context as needed
|
||||
└── assets/ - Files used in output (templates, icons, fonts)
|
||||
```
|
||||
|
||||
#### Progressive Disclosure
|
||||
|
||||
Skills use a three-level loading system:
|
||||
1. **Metadata** (name + description) - Always in context (~100 words)
|
||||
2. **SKILL.md body** - In context whenever skill triggers (<500 lines ideal)
|
||||
3. **Bundled resources** - As needed (unlimited, scripts can execute without loading)
|
||||
|
||||
These word counts are approximate and you can feel free to go longer if needed.
|
||||
|
||||
**Key patterns:**
|
||||
- Keep SKILL.md under 500 lines; if you're approaching this limit, add an additional layer of hierarchy along with clear pointers about where the model using the skill should go next to follow up.
|
||||
- Reference files clearly from SKILL.md with guidance on when to read them
|
||||
- For large reference files (>300 lines), include a table of contents
|
||||
|
||||
**Domain organization**: When a skill supports multiple domains/frameworks, organize by variant:
|
||||
```
|
||||
cloud-deploy/
|
||||
├── SKILL.md (workflow + selection)
|
||||
└── references/
|
||||
├── aws.md
|
||||
├── gcp.md
|
||||
└── azure.md
|
||||
```
|
||||
Claude reads only the relevant reference file.
|
||||
|
||||
#### Principle of Lack of Surprise
|
||||
|
||||
This goes without saying, but skills must not contain malware, exploit code, or any content that could compromise system security. A skill's contents should not surprise the user in their intent if described. Don't go along with requests to create misleading skills or skills designed to facilitate unauthorized access, data exfiltration, or other malicious activities. Things like a "roleplay as an XYZ" are OK though.
|
||||
|
||||
#### Writing Patterns
|
||||
|
||||
Prefer using the imperative form in instructions.
|
||||
|
||||
**Defining output formats** - You can do it like this:
|
||||
```markdown
|
||||
## Report structure
|
||||
ALWAYS use this exact template:
|
||||
# [Title]
|
||||
## Executive summary
|
||||
## Key findings
|
||||
## Recommendations
|
||||
```
|
||||
|
||||
**Examples pattern** - It's useful to include examples. You can format them like this (but if "Input" and "Output" are in the examples you might want to deviate a little):
|
||||
```markdown
|
||||
## Commit message format
|
||||
**Example 1:**
|
||||
Input: Added user authentication with JWT tokens
|
||||
Output: feat(auth): implement JWT-based authentication
|
||||
```
|
||||
|
||||
### Writing Style
|
||||
|
||||
Try to explain to the model why things are important in lieu of heavy-handed musty MUSTs. Use theory of mind and try to make the skill general and not super-narrow to specific examples. Start by writing a draft and then look at it with fresh eyes and improve it.
|
||||
|
||||
### Test Cases
|
||||
|
||||
After writing the skill draft, come up with 2-3 realistic test prompts — the kind of thing a real user would actually say. Share them with the user: [you don't have to use this exact language] "Here are a few test cases I'd like to try. Do these look right, or do you want to add more?" Then run them.
|
||||
|
||||
Save test cases to `evals/evals.json`. Don't write assertions yet — just the prompts. You'll draft assertions in the next step while the runs are in progress.
|
||||
|
||||
```json
|
||||
{
|
||||
"skill_name": "example-skill",
|
||||
"evals": [
|
||||
{
|
||||
"id": 1,
|
||||
"prompt": "User's task prompt",
|
||||
"expected_output": "Description of expected result",
|
||||
"files": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
See `references/schemas.md` for the full schema (including the `assertions` field, which you'll add later).
|
||||
|
||||
## Running and evaluating test cases
|
||||
|
||||
This section is one continuous sequence — don't stop partway through. Do NOT use `/skill-test` or any other testing skill.
|
||||
|
||||
Put results in `<skill-name>-workspace/` as a sibling to the skill directory. Within the workspace, organize results by iteration (`iteration-1/`, `iteration-2/`, etc.) and within that, each test case gets a directory (`eval-0/`, `eval-1/`, etc.). Don't create all of this upfront — just create directories as you go.
|
||||
|
||||
### Step 1: Spawn all runs (with-skill AND baseline) in the same turn
|
||||
|
||||
For each test case, spawn two subagents in the same turn — one with the skill, one without. This is important: don't spawn the with-skill runs first and then come back for baselines later. Launch everything at once so it all finishes around the same time.
|
||||
|
||||
**With-skill run:**
|
||||
|
||||
```
|
||||
Execute this task:
|
||||
- Skill path: <path-to-skill>
|
||||
- Task: <eval prompt>
|
||||
- Input files: <eval files if any, or "none">
|
||||
- Save outputs to: <workspace>/iteration-<N>/eval-<ID>/with_skill/outputs/
|
||||
- Outputs to save: <what the user cares about — e.g., "the .docx file", "the final CSV">
|
||||
```
|
||||
|
||||
**Baseline run** (same prompt, but the baseline depends on context):
|
||||
- **Creating a new skill**: no skill at all. Same prompt, no skill path, save to `without_skill/outputs/`.
|
||||
- **Improving an existing skill**: the old version. Before editing, snapshot the skill (`cp -r <skill-path> <workspace>/skill-snapshot/`), then point the baseline subagent at the snapshot. Save to `old_skill/outputs/`.
|
||||
|
||||
Write an `eval_metadata.json` for each test case (assertions can be empty for now). Give each eval a descriptive name based on what it's testing — not just "eval-0". Use this name for the directory too. If this iteration uses new or modified eval prompts, create these files for each new eval directory — don't assume they carry over from previous iterations.
|
||||
|
||||
```json
|
||||
{
|
||||
"eval_id": 0,
|
||||
"eval_name": "descriptive-name-here",
|
||||
"prompt": "The user's task prompt",
|
||||
"assertions": []
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: While runs are in progress, draft assertions
|
||||
|
||||
Don't just wait for the runs to finish — you can use this time productively. Draft quantitative assertions for each test case and explain them to the user. If assertions already exist in `evals/evals.json`, review them and explain what they check.
|
||||
|
||||
Good assertions are objectively verifiable and have descriptive names — they should read clearly in the benchmark viewer so someone glancing at the results immediately understands what each one checks. Subjective skills (writing style, design quality) are better evaluated qualitatively — don't force assertions onto things that need human judgment.
|
||||
|
||||
Update the `eval_metadata.json` files and `evals/evals.json` with the assertions once drafted. Also explain to the user what they'll see in the viewer — both the qualitative outputs and the quantitative benchmark.
|
||||
|
||||
### Step 3: As runs complete, capture timing data
|
||||
|
||||
When each subagent task completes, you receive a notification containing `total_tokens` and `duration_ms`. Save this data immediately to `timing.json` in the run directory:
|
||||
|
||||
```json
|
||||
{
|
||||
"total_tokens": 84852,
|
||||
"duration_ms": 23332,
|
||||
"total_duration_seconds": 23.3
|
||||
}
|
||||
```
|
||||
|
||||
This is the only opportunity to capture this data — it comes through the task notification and isn't persisted elsewhere. Process each notification as it arrives rather than trying to batch them.
|
||||
|
||||
### Step 4: Grade, aggregate, and launch the viewer
|
||||
|
||||
Once all runs are done:
|
||||
|
||||
1. **Grade each run** — spawn a grader subagent (or grade inline) that reads `agents/grader.md` and evaluates each assertion against the outputs. Save results to `grading.json` in each run directory. The grading.json expectations array must use the fields `text`, `passed`, and `evidence` (not `name`/`met`/`details` or other variants) — the viewer depends on these exact field names. For assertions that can be checked programmatically, write and run a script rather than eyeballing it — scripts are faster, more reliable, and can be reused across iterations.
|
||||
|
||||
2. **Aggregate into benchmark** — run the aggregation script from the skill-creator directory:
|
||||
```bash
|
||||
python -m scripts.aggregate_benchmark <workspace>/iteration-N --skill-name <name>
|
||||
```
|
||||
This produces `benchmark.json` and `benchmark.md` with pass_rate, time, and tokens for each configuration, with mean ± stddev and the delta. If generating benchmark.json manually, see `references/schemas.md` for the exact schema the viewer expects.
|
||||
Put each with_skill version before its baseline counterpart.
|
||||
|
||||
3. **Do an analyst pass** — read the benchmark data and surface patterns the aggregate stats might hide. See `agents/analyzer.md` (the "Analyzing Benchmark Results" section) for what to look for — things like assertions that always pass regardless of skill (non-discriminating), high-variance evals (possibly flaky), and time/token tradeoffs.
|
||||
|
||||
4. **Launch the viewer** with both qualitative outputs and quantitative data:
|
||||
```bash
|
||||
nohup python <skill-creator-path>/eval-viewer/generate_review.py \
|
||||
<workspace>/iteration-N \
|
||||
--skill-name "my-skill" \
|
||||
--benchmark <workspace>/iteration-N/benchmark.json \
|
||||
> /dev/null 2>&1 &
|
||||
VIEWER_PID=$!
|
||||
```
|
||||
For iteration 2+, also pass `--previous-workspace <workspace>/iteration-<N-1>`.
|
||||
|
||||
**Cowork / headless environments:** If `webbrowser.open()` is not available or the environment has no display, use `--static <output_path>` to write a standalone HTML file instead of starting a server. Feedback will be downloaded as a `feedback.json` file when the user clicks "Submit All Reviews". After download, copy `feedback.json` into the workspace directory for the next iteration to pick up.
|
||||
|
||||
Note: please use generate_review.py to create the viewer; there's no need to write custom HTML.
|
||||
|
||||
5. **Tell the user** something like: "I've opened the results in your browser. There are two tabs — 'Outputs' lets you click through each test case and leave feedback, 'Benchmark' shows the quantitative comparison. When you're done, come back here and let me know."
|
||||
|
||||
### What the user sees in the viewer
|
||||
|
||||
The "Outputs" tab shows one test case at a time:
|
||||
- **Prompt**: the task that was given
|
||||
- **Output**: the files the skill produced, rendered inline where possible
|
||||
- **Previous Output** (iteration 2+): collapsed section showing last iteration's output
|
||||
- **Formal Grades** (if grading was run): collapsed section showing assertion pass/fail
|
||||
- **Feedback**: a textbox that auto-saves as they type
|
||||
- **Previous Feedback** (iteration 2+): their comments from last time, shown below the textbox
|
||||
|
||||
The "Benchmark" tab shows the stats summary: pass rates, timing, and token usage for each configuration, with per-eval breakdowns and analyst observations.
|
||||
|
||||
Navigation is via prev/next buttons or arrow keys. When done, they click "Submit All Reviews" which saves all feedback to `feedback.json`.
|
||||
|
||||
### Step 5: Read the feedback
|
||||
|
||||
When the user tells you they're done, read `feedback.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"reviews": [
|
||||
{"run_id": "eval-0-with_skill", "feedback": "the chart is missing axis labels", "timestamp": "..."},
|
||||
{"run_id": "eval-1-with_skill", "feedback": "", "timestamp": "..."},
|
||||
{"run_id": "eval-2-with_skill", "feedback": "perfect, love this", "timestamp": "..."}
|
||||
],
|
||||
"status": "complete"
|
||||
}
|
||||
```
|
||||
|
||||
Empty feedback means the user thought it was fine. Focus your improvements on the test cases where the user had specific complaints.
|
||||
|
||||
Kill the viewer server when you're done with it:
|
||||
|
||||
```bash
|
||||
kill $VIEWER_PID 2>/dev/null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Improving the skill
|
||||
|
||||
This is the heart of the loop. You've run the test cases, the user has reviewed the results, and now you need to make the skill better based on their feedback.
|
||||
|
||||
### How to think about improvements
|
||||
|
||||
1. **Generalize from the feedback.** The big picture thing that's happening here is that we're trying to create skills that can be used a million times (maybe literally, maybe even more who knows) across many different prompts. Here you and the user are iterating on only a few examples over and over again because it helps move faster. The user knows these examples in and out and it's quick for them to assess new outputs. But if the skill you and the user are codeveloping works only for those examples, it's useless. Rather than put in fiddly overfitty changes, or oppressively constrictive MUSTs, if there's some stubborn issue, you might try branching out and using different metaphors, or recommending different patterns of working. It's relatively cheap to try and maybe you'll land on something great.
|
||||
|
||||
2. **Keep the prompt lean.** Remove things that aren't pulling their weight. Make sure to read the transcripts, not just the final outputs — if it looks like the skill is making the model waste a bunch of time doing things that are unproductive, you can try getting rid of the parts of the skill that are making it do that and seeing what happens.
|
||||
|
||||
3. **Explain the why.** Try hard to explain the **why** behind everything you're asking the model to do. Today's LLMs are *smart*. They have good theory of mind and when given a good harness can go beyond rote instructions and really make things happen. Even if the feedback from the user is terse or frustrated, try to actually understand the task and why the user is writing what they wrote, and what they actually wrote, and then transmit this understanding into the instructions. If you find yourself writing ALWAYS or NEVER in all caps, or using super rigid structures, that's a yellow flag — if possible, reframe and explain the reasoning so that the model understands why the thing you're asking for is important. That's a more humane, powerful, and effective approach.
|
||||
|
||||
4. **Look for repeated work across test cases.** Read the transcripts from the test runs and notice if the subagents all independently wrote similar helper scripts or took the same multi-step approach to something. If all 3 test cases resulted in the subagent writing a `create_docx.py` or a `build_chart.py`, that's a strong signal the skill should bundle that script. Write it once, put it in `scripts/`, and tell the skill to use it. This saves every future invocation from reinventing the wheel.
|
||||
|
||||
This task is pretty important (we are trying to create billions a year in economic value here!) and your thinking time is not the blocker; take your time and really mull things over. I'd suggest writing a draft revision and then looking at it anew and making improvements. Really do your best to get into the head of the user and understand what they want and need.
|
||||
|
||||
### The iteration loop
|
||||
|
||||
After improving the skill:
|
||||
|
||||
1. Apply your improvements to the skill
|
||||
2. Rerun all test cases into a new `iteration-<N+1>/` directory, including baseline runs. If you're creating a new skill, the baseline is always `without_skill` (no skill) — that stays the same across iterations. If you're improving an existing skill, use your judgment on what makes sense as the baseline: the original version the user came in with, or the previous iteration.
|
||||
3. Launch the reviewer with `--previous-workspace` pointing at the previous iteration
|
||||
4. Wait for the user to review and tell you they're done
|
||||
5. Read the new feedback, improve again, repeat
|
||||
|
||||
Keep going until:
|
||||
- The user says they're happy
|
||||
- The feedback is all empty (everything looks good)
|
||||
- You're not making meaningful progress
|
||||
|
||||
---
|
||||
|
||||
## Advanced: Blind comparison
|
||||
|
||||
For situations where you want a more rigorous comparison between two versions of a skill (e.g., the user asks "is the new version actually better?"), there's a blind comparison system. Read `agents/comparator.md` and `agents/analyzer.md` for the details. The basic idea is: give two outputs to an independent agent without telling it which is which, and let it judge quality. Then analyze why the winner won.
|
||||
|
||||
This is optional, requires subagents, and most users won't need it. The human review loop is usually sufficient.
|
||||
|
||||
---
|
||||
|
||||
## Description Optimization
|
||||
|
||||
The description field in SKILL.md frontmatter is the primary mechanism that determines whether Claude invokes a skill. After creating or improving a skill, offer to optimize the description for better triggering accuracy.
|
||||
|
||||
### Step 1: Generate trigger eval queries
|
||||
|
||||
Create 20 eval queries — a mix of should-trigger and should-not-trigger. Save as JSON:
|
||||
|
||||
```json
|
||||
[
|
||||
{"query": "the user prompt", "should_trigger": true},
|
||||
{"query": "another prompt", "should_trigger": false}
|
||||
]
|
||||
```
|
||||
|
||||
The queries must be realistic and something a Claude Code or Claude.ai user would actually type. Not abstract requests, but requests that are concrete and specific and have a good amount of detail. For instance, file paths, personal context about the user's job or situation, column names and values, company names, URLs. A little bit of backstory. Some might be in lowercase or contain abbreviations or typos or casual speech. Use a mix of different lengths, and focus on edge cases rather than making them clear-cut (the user will get a chance to sign off on them).
|
||||
|
||||
Bad: `"Format this data"`, `"Extract text from PDF"`, `"Create a chart"`
|
||||
|
||||
Good: `"ok so my boss just sent me this xlsx file (its in my downloads, called something like 'Q4 sales final FINAL v2.xlsx') and she wants me to add a column that shows the profit margin as a percentage. The revenue is in column C and costs are in column D i think"`
|
||||
|
||||
For the **should-trigger** queries (8-10), think about coverage. You want different phrasings of the same intent — some formal, some casual. Include cases where the user doesn't explicitly name the skill or file type but clearly needs it. Throw in some uncommon use cases and cases where this skill competes with another but should win.
|
||||
|
||||
For the **should-not-trigger** queries (8-10), the most valuable ones are the near-misses — queries that share keywords or concepts with the skill but actually need something different. Think adjacent domains, ambiguous phrasing where a naive keyword match would trigger but shouldn't, and cases where the query touches on something the skill does but in a context where another tool is more appropriate.
|
||||
|
||||
The key thing to avoid: don't make should-not-trigger queries obviously irrelevant. "Write a fibonacci function" as a negative test for a PDF skill is too easy — it doesn't test anything. The negative cases should be genuinely tricky.
|
||||
|
||||
### Step 2: Review with user
|
||||
|
||||
Present the eval set to the user for review using the HTML template:
|
||||
|
||||
1. Read the template from `assets/eval_review.html`
|
||||
2. Replace the placeholders:
|
||||
- `__EVAL_DATA_PLACEHOLDER__` → the JSON array of eval items (no quotes around it — it's a JS variable assignment)
|
||||
- `__SKILL_NAME_PLACEHOLDER__` → the skill's name
|
||||
- `__SKILL_DESCRIPTION_PLACEHOLDER__` → the skill's current description
|
||||
3. Write to a temp file (e.g., `/tmp/eval_review_<skill-name>.html`) and open it: `open /tmp/eval_review_<skill-name>.html`
|
||||
4. The user can edit queries, toggle should-trigger, add/remove entries, then click "Export Eval Set"
|
||||
5. The file downloads to `~/Downloads/eval_set.json` — check the Downloads folder for the most recent version in case there are multiple (e.g., `eval_set (1).json`)
|
||||
|
||||
This step matters — bad eval queries lead to bad descriptions.
|
||||
|
||||
### Step 3: Run the optimization loop
|
||||
|
||||
Tell the user: "This will take some time — I'll run the optimization loop in the background and check on it periodically."
|
||||
|
||||
Save the eval set to the workspace, then run in the background:
|
||||
|
||||
```bash
|
||||
python -m scripts.run_loop \
|
||||
--eval-set <path-to-trigger-eval.json> \
|
||||
--skill-path <path-to-skill> \
|
||||
--model <model-id-powering-this-session> \
|
||||
--max-iterations 5 \
|
||||
--verbose
|
||||
```
|
||||
|
||||
Use the model ID from your system prompt (the one powering the current session) so the triggering test matches what the user actually experiences.
|
||||
|
||||
While it runs, periodically tail the output to give the user updates on which iteration it's on and what the scores look like.
|
||||
|
||||
This handles the full optimization loop automatically. It splits the eval set into 60% train and 40% held-out test, evaluates the current description (running each query 3 times to get a reliable trigger rate), then calls Claude with extended thinking to propose improvements based on what failed. It re-evaluates each new description on both train and test, iterating up to 5 times. When it's done, it opens an HTML report in the browser showing the results per iteration and returns JSON with `best_description` — selected by test score rather than train score to avoid overfitting.
|
||||
|
||||
### How skill triggering works
|
||||
|
||||
Understanding the triggering mechanism helps design better eval queries. Skills appear in Claude's `available_skills` list with their name + description, and Claude decides whether to consult a skill based on that description. The important thing to know is that Claude only consults skills for tasks it can't easily handle on its own — simple, one-step queries like "read this PDF" may not trigger a skill even if the description matches perfectly, because Claude can handle them directly with basic tools. Complex, multi-step, or specialized queries reliably trigger skills when the description matches.
|
||||
|
||||
This means your eval queries should be substantive enough that Claude would actually benefit from consulting a skill. Simple queries like "read file X" are poor test cases — they won't trigger skills regardless of description quality.
|
||||
|
||||
### Step 4: Apply the result
|
||||
|
||||
Take `best_description` from the JSON output and update the skill's SKILL.md frontmatter. Show the user before/after and report the scores.
|
||||
|
||||
---
|
||||
|
||||
### Package and Present (only if `present_files` tool is available)
|
||||
|
||||
Check whether you have access to the `present_files` tool. If you don't, skip this step. If you do, package the skill and present the .skill file to the user:
|
||||
|
||||
```bash
|
||||
python -m scripts.package_skill <path/to/skill-folder>
|
||||
```
|
||||
|
||||
After packaging, direct the user to the resulting `.skill` file path so they can install it.
|
||||
|
||||
---
|
||||
|
||||
## Claude.ai-specific instructions
|
||||
|
||||
In Claude.ai, the core workflow is the same (draft → test → review → improve → repeat), but because Claude.ai doesn't have subagents, some mechanics change. Here's what to adapt:
|
||||
|
||||
**Running test cases**: No subagents means no parallel execution. For each test case, read the skill's SKILL.md, then follow its instructions to accomplish the test prompt yourself. Do them one at a time. This is less rigorous than independent subagents (you wrote the skill and you're also running it, so you have full context), but it's a useful sanity check — and the human review step compensates. Skip the baseline runs — just use the skill to complete the task as requested.
|
||||
|
||||
**Reviewing results**: If you can't open a browser (e.g., Claude.ai's VM has no display, or you're on a remote server), skip the browser reviewer entirely. Instead, present results directly in the conversation. For each test case, show the prompt and the output. If the output is a file the user needs to see (like a .docx or .xlsx), save it to the filesystem and tell them where it is so they can download and inspect it. Ask for feedback inline: "How does this look? Anything you'd change?"
|
||||
|
||||
**Benchmarking**: Skip the quantitative benchmarking — it relies on baseline comparisons which aren't meaningful without subagents. Focus on qualitative feedback from the user.
|
||||
|
||||
**The iteration loop**: Same as before — improve the skill, rerun the test cases, ask for feedback — just without the browser reviewer in the middle. You can still organize results into iteration directories on the filesystem if you have one.
|
||||
|
||||
**Description optimization**: This section requires the `claude` CLI tool (specifically `claude -p`) which is only available in Claude Code. Skip it if you're on Claude.ai.
|
||||
|
||||
**Blind comparison**: Requires subagents. Skip it.
|
||||
|
||||
**Packaging**: The `package_skill.py` script works anywhere with Python and a filesystem. On Claude.ai, you can run it and the user can download the resulting `.skill` file.
|
||||
|
||||
---
|
||||
|
||||
## Cowork-Specific Instructions
|
||||
|
||||
If you're in Cowork, the main things to know are:
|
||||
|
||||
- You have subagents, so the main workflow (spawn test cases in parallel, run baselines, grade, etc.) all works. (However, if you run into severe problems with timeouts, it's OK to run the test prompts in series rather than parallel.)
|
||||
- You don't have a browser or display, so when generating the eval viewer, use `--static <output_path>` to write a standalone HTML file instead of starting a server. Then proffer a link that the user can click to open the HTML in their browser.
|
||||
- For whatever reason, the Cowork setup seems to disincline Claude from generating the eval viewer after running the tests, so just to reiterate: whether you're in Cowork or in Claude Code, after running tests, you should always generate the eval viewer for the human to look at examples before revising the skill yourself and trying to make corrections, using `generate_review.py` (not writing your own boutique html code). Sorry in advance but I'm gonna go all caps here: GENERATE THE EVAL VIEWER *BEFORE* evaluating inputs yourself. You want to get them in front of the human ASAP!
|
||||
- Feedback works differently: since there's no running server, the viewer's "Submit All Reviews" button will download `feedback.json` as a file. You can then read it from there (you may have to request access first).
|
||||
- Packaging works — `package_skill.py` just needs Python and a filesystem.
|
||||
- Description optimization (`run_loop.py` / `run_eval.py`) should work in Cowork just fine since it uses `claude -p` via subprocess, not a browser, but please save it until you've fully finished making the skill and the user agrees it's in good shape.
|
||||
|
||||
---
|
||||
|
||||
## Reference files
|
||||
|
||||
The agents/ directory contains instructions for specialized subagents. Read them when you need to spawn the relevant subagent.
|
||||
|
||||
- `agents/grader.md` — How to evaluate assertions against outputs
|
||||
- `agents/comparator.md` — How to do blind A/B comparison between two outputs
|
||||
- `agents/analyzer.md` — How to analyze why one version beat another
|
||||
|
||||
The references/ directory has additional documentation:
|
||||
- `references/schemas.md` — JSON structures for evals.json, grading.json, etc.
|
||||
|
||||
---
|
||||
|
||||
Repeating one more time the core loop here for emphasis:
|
||||
|
||||
- Figure out what the skill is about
|
||||
- Draft or edit the skill
|
||||
- Run claude-with-access-to-the-skill on test prompts
|
||||
- With the user, evaluate the outputs:
|
||||
- Create benchmark.json and run `eval-viewer/generate_review.py` to help the user review them
|
||||
- Run quantitative evals
|
||||
- Repeat until you and the user are satisfied
|
||||
- Package the final skill and return it to the user.
|
||||
|
||||
Please add steps to your TodoList, if you have such a thing, to make sure you don't forget. If you're in Cowork, please specifically put "Create evals JSON and run `eval-viewer/generate_review.py` so human can review test cases" in your TodoList to make sure it happens.
|
||||
|
||||
Good luck!
|
||||
274
.skills/skill-creator/agents/analyzer.md
Normal file
274
.skills/skill-creator/agents/analyzer.md
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
# Post-hoc Analyzer Agent
|
||||
|
||||
Analyze blind comparison results to understand WHY the winner won and generate improvement suggestions.
|
||||
|
||||
## Role
|
||||
|
||||
After the blind comparator determines a winner, the Post-hoc Analyzer "unblids" the results by examining the skills and transcripts. The goal is to extract actionable insights: what made the winner better, and how can the loser be improved?
|
||||
|
||||
## Inputs
|
||||
|
||||
You receive these parameters in your prompt:
|
||||
|
||||
- **winner**: "A" or "B" (from blind comparison)
|
||||
- **winner_skill_path**: Path to the skill that produced the winning output
|
||||
- **winner_transcript_path**: Path to the execution transcript for the winner
|
||||
- **loser_skill_path**: Path to the skill that produced the losing output
|
||||
- **loser_transcript_path**: Path to the execution transcript for the loser
|
||||
- **comparison_result_path**: Path to the blind comparator's output JSON
|
||||
- **output_path**: Where to save the analysis results
|
||||
|
||||
## Process
|
||||
|
||||
### Step 1: Read Comparison Result
|
||||
|
||||
1. Read the blind comparator's output at comparison_result_path
|
||||
2. Note the winning side (A or B), the reasoning, and any scores
|
||||
3. Understand what the comparator valued in the winning output
|
||||
|
||||
### Step 2: Read Both Skills
|
||||
|
||||
1. Read the winner skill's SKILL.md and key referenced files
|
||||
2. Read the loser skill's SKILL.md and key referenced files
|
||||
3. Identify structural differences:
|
||||
- Instructions clarity and specificity
|
||||
- Script/tool usage patterns
|
||||
- Example coverage
|
||||
- Edge case handling
|
||||
|
||||
### Step 3: Read Both Transcripts
|
||||
|
||||
1. Read the winner's transcript
|
||||
2. Read the loser's transcript
|
||||
3. Compare execution patterns:
|
||||
- How closely did each follow their skill's instructions?
|
||||
- What tools were used differently?
|
||||
- Where did the loser diverge from optimal behavior?
|
||||
- Did either encounter errors or make recovery attempts?
|
||||
|
||||
### Step 4: Analyze Instruction Following
|
||||
|
||||
For each transcript, evaluate:
|
||||
- Did the agent follow the skill's explicit instructions?
|
||||
- Did the agent use the skill's provided tools/scripts?
|
||||
- Were there missed opportunities to leverage skill content?
|
||||
- Did the agent add unnecessary steps not in the skill?
|
||||
|
||||
Score instruction following 1-10 and note specific issues.
|
||||
|
||||
### Step 5: Identify Winner Strengths
|
||||
|
||||
Determine what made the winner better:
|
||||
- Clearer instructions that led to better behavior?
|
||||
- Better scripts/tools that produced better output?
|
||||
- More comprehensive examples that guided edge cases?
|
||||
- Better error handling guidance?
|
||||
|
||||
Be specific. Quote from skills/transcripts where relevant.
|
||||
|
||||
### Step 6: Identify Loser Weaknesses
|
||||
|
||||
Determine what held the loser back:
|
||||
- Ambiguous instructions that led to suboptimal choices?
|
||||
- Missing tools/scripts that forced workarounds?
|
||||
- Gaps in edge case coverage?
|
||||
- Poor error handling that caused failures?
|
||||
|
||||
### Step 7: Generate Improvement Suggestions
|
||||
|
||||
Based on the analysis, produce actionable suggestions for improving the loser skill:
|
||||
- Specific instruction changes to make
|
||||
- Tools/scripts to add or modify
|
||||
- Examples to include
|
||||
- Edge cases to address
|
||||
|
||||
Prioritize by impact. Focus on changes that would have changed the outcome.
|
||||
|
||||
### Step 8: Write Analysis Results
|
||||
|
||||
Save structured analysis to `{output_path}`.
|
||||
|
||||
## Output Format
|
||||
|
||||
Write a JSON file with this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"comparison_summary": {
|
||||
"winner": "A",
|
||||
"winner_skill": "path/to/winner/skill",
|
||||
"loser_skill": "path/to/loser/skill",
|
||||
"comparator_reasoning": "Brief summary of why comparator chose winner"
|
||||
},
|
||||
"winner_strengths": [
|
||||
"Clear step-by-step instructions for handling multi-page documents",
|
||||
"Included validation script that caught formatting errors",
|
||||
"Explicit guidance on fallback behavior when OCR fails"
|
||||
],
|
||||
"loser_weaknesses": [
|
||||
"Vague instruction 'process the document appropriately' led to inconsistent behavior",
|
||||
"No script for validation, agent had to improvise and made errors",
|
||||
"No guidance on OCR failure, agent gave up instead of trying alternatives"
|
||||
],
|
||||
"instruction_following": {
|
||||
"winner": {
|
||||
"score": 9,
|
||||
"issues": [
|
||||
"Minor: skipped optional logging step"
|
||||
]
|
||||
},
|
||||
"loser": {
|
||||
"score": 6,
|
||||
"issues": [
|
||||
"Did not use the skill's formatting template",
|
||||
"Invented own approach instead of following step 3",
|
||||
"Missed the 'always validate output' instruction"
|
||||
]
|
||||
}
|
||||
},
|
||||
"improvement_suggestions": [
|
||||
{
|
||||
"priority": "high",
|
||||
"category": "instructions",
|
||||
"suggestion": "Replace 'process the document appropriately' with explicit steps: 1) Extract text, 2) Identify sections, 3) Format per template",
|
||||
"expected_impact": "Would eliminate ambiguity that caused inconsistent behavior"
|
||||
},
|
||||
{
|
||||
"priority": "high",
|
||||
"category": "tools",
|
||||
"suggestion": "Add validate_output.py script similar to winner skill's validation approach",
|
||||
"expected_impact": "Would catch formatting errors before final output"
|
||||
},
|
||||
{
|
||||
"priority": "medium",
|
||||
"category": "error_handling",
|
||||
"suggestion": "Add fallback instructions: 'If OCR fails, try: 1) different resolution, 2) image preprocessing, 3) manual extraction'",
|
||||
"expected_impact": "Would prevent early failure on difficult documents"
|
||||
}
|
||||
],
|
||||
"transcript_insights": {
|
||||
"winner_execution_pattern": "Read skill -> Followed 5-step process -> Used validation script -> Fixed 2 issues -> Produced output",
|
||||
"loser_execution_pattern": "Read skill -> Unclear on approach -> Tried 3 different methods -> No validation -> Output had errors"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Guidelines
|
||||
|
||||
- **Be specific**: Quote from skills and transcripts, don't just say "instructions were unclear"
|
||||
- **Be actionable**: Suggestions should be concrete changes, not vague advice
|
||||
- **Focus on skill improvements**: The goal is to improve the losing skill, not critique the agent
|
||||
- **Prioritize by impact**: Which changes would most likely have changed the outcome?
|
||||
- **Consider causation**: Did the skill weakness actually cause the worse output, or is it incidental?
|
||||
- **Stay objective**: Analyze what happened, don't editorialize
|
||||
- **Think about generalization**: Would this improvement help on other evals too?
|
||||
|
||||
## Categories for Suggestions
|
||||
|
||||
Use these categories to organize improvement suggestions:
|
||||
|
||||
| Category | Description |
|
||||
|----------|-------------|
|
||||
| `instructions` | Changes to the skill's prose instructions |
|
||||
| `tools` | Scripts, templates, or utilities to add/modify |
|
||||
| `examples` | Example inputs/outputs to include |
|
||||
| `error_handling` | Guidance for handling failures |
|
||||
| `structure` | Reorganization of skill content |
|
||||
| `references` | External docs or resources to add |
|
||||
|
||||
## Priority Levels
|
||||
|
||||
- **high**: Would likely change the outcome of this comparison
|
||||
- **medium**: Would improve quality but may not change win/loss
|
||||
- **low**: Nice to have, marginal improvement
|
||||
|
||||
---
|
||||
|
||||
# Analyzing Benchmark Results
|
||||
|
||||
When analyzing benchmark results, the analyzer's purpose is to **surface patterns and anomalies** across multiple runs, not suggest skill improvements.
|
||||
|
||||
## Role
|
||||
|
||||
Review all benchmark run results and generate freeform notes that help the user understand skill performance. Focus on patterns that wouldn't be visible from aggregate metrics alone.
|
||||
|
||||
## Inputs
|
||||
|
||||
You receive these parameters in your prompt:
|
||||
|
||||
- **benchmark_data_path**: Path to the in-progress benchmark.json with all run results
|
||||
- **skill_path**: Path to the skill being benchmarked
|
||||
- **output_path**: Where to save the notes (as JSON array of strings)
|
||||
|
||||
## Process
|
||||
|
||||
### Step 1: Read Benchmark Data
|
||||
|
||||
1. Read the benchmark.json containing all run results
|
||||
2. Note the configurations tested (with_skill, without_skill)
|
||||
3. Understand the run_summary aggregates already calculated
|
||||
|
||||
### Step 2: Analyze Per-Assertion Patterns
|
||||
|
||||
For each expectation across all runs:
|
||||
- Does it **always pass** in both configurations? (may not differentiate skill value)
|
||||
- Does it **always fail** in both configurations? (may be broken or beyond capability)
|
||||
- Does it **always pass with skill but fail without**? (skill clearly adds value here)
|
||||
- Does it **always fail with skill but pass without**? (skill may be hurting)
|
||||
- Is it **highly variable**? (flaky expectation or non-deterministic behavior)
|
||||
|
||||
### Step 3: Analyze Cross-Eval Patterns
|
||||
|
||||
Look for patterns across evals:
|
||||
- Are certain eval types consistently harder/easier?
|
||||
- Do some evals show high variance while others are stable?
|
||||
- Are there surprising results that contradict expectations?
|
||||
|
||||
### Step 4: Analyze Metrics Patterns
|
||||
|
||||
Look at time_seconds, tokens, tool_calls:
|
||||
- Does the skill significantly increase execution time?
|
||||
- Is there high variance in resource usage?
|
||||
- Are there outlier runs that skew the aggregates?
|
||||
|
||||
### Step 5: Generate Notes
|
||||
|
||||
Write freeform observations as a list of strings. Each note should:
|
||||
- State a specific observation
|
||||
- Be grounded in the data (not speculation)
|
||||
- Help the user understand something the aggregate metrics don't show
|
||||
|
||||
Examples:
|
||||
- "Assertion 'Output is a PDF file' passes 100% in both configurations - may not differentiate skill value"
|
||||
- "Eval 3 shows high variance (50% ± 40%) - run 2 had an unusual failure that may be flaky"
|
||||
- "Without-skill runs consistently fail on table extraction expectations (0% pass rate)"
|
||||
- "Skill adds 13s average execution time but improves pass rate by 50%"
|
||||
- "Token usage is 80% higher with skill, primarily due to script output parsing"
|
||||
- "All 3 without-skill runs for eval 1 produced empty output"
|
||||
|
||||
### Step 6: Write Notes
|
||||
|
||||
Save notes to `{output_path}` as a JSON array of strings:
|
||||
|
||||
```json
|
||||
[
|
||||
"Assertion 'Output is a PDF file' passes 100% in both configurations - may not differentiate skill value",
|
||||
"Eval 3 shows high variance (50% ± 40%) - run 2 had an unusual failure",
|
||||
"Without-skill runs consistently fail on table extraction expectations",
|
||||
"Skill adds 13s average execution time but improves pass rate by 50%"
|
||||
]
|
||||
```
|
||||
|
||||
## Guidelines
|
||||
|
||||
**DO:**
|
||||
- Report what you observe in the data
|
||||
- Be specific about which evals, expectations, or runs you're referring to
|
||||
- Note patterns that aggregate metrics would hide
|
||||
- Provide context that helps interpret the numbers
|
||||
|
||||
**DO NOT:**
|
||||
- Suggest improvements to the skill (that's for the improvement step, not benchmarking)
|
||||
- Make subjective quality judgments ("the output was good/bad")
|
||||
- Speculate about causes without evidence
|
||||
- Repeat information already in the run_summary aggregates
|
||||
202
.skills/skill-creator/agents/comparator.md
Normal file
202
.skills/skill-creator/agents/comparator.md
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# Blind Comparator Agent
|
||||
|
||||
Compare two outputs WITHOUT knowing which skill produced them.
|
||||
|
||||
## Role
|
||||
|
||||
The Blind Comparator judges which output better accomplishes the eval task. You receive two outputs labeled A and B, but you do NOT know which skill produced which. This prevents bias toward a particular skill or approach.
|
||||
|
||||
Your judgment is based purely on output quality and task completion.
|
||||
|
||||
## Inputs
|
||||
|
||||
You receive these parameters in your prompt:
|
||||
|
||||
- **output_a_path**: Path to the first output file or directory
|
||||
- **output_b_path**: Path to the second output file or directory
|
||||
- **eval_prompt**: The original task/prompt that was executed
|
||||
- **expectations**: List of expectations to check (optional - may be empty)
|
||||
|
||||
## Process
|
||||
|
||||
### Step 1: Read Both Outputs
|
||||
|
||||
1. Examine output A (file or directory)
|
||||
2. Examine output B (file or directory)
|
||||
3. Note the type, structure, and content of each
|
||||
4. If outputs are directories, examine all relevant files inside
|
||||
|
||||
### Step 2: Understand the Task
|
||||
|
||||
1. Read the eval_prompt carefully
|
||||
2. Identify what the task requires:
|
||||
- What should be produced?
|
||||
- What qualities matter (accuracy, completeness, format)?
|
||||
- What would distinguish a good output from a poor one?
|
||||
|
||||
### Step 3: Generate Evaluation Rubric
|
||||
|
||||
Based on the task, generate a rubric with two dimensions:
|
||||
|
||||
**Content Rubric** (what the output contains):
|
||||
| Criterion | 1 (Poor) | 3 (Acceptable) | 5 (Excellent) |
|
||||
|-----------|----------|----------------|---------------|
|
||||
| Correctness | Major errors | Minor errors | Fully correct |
|
||||
| Completeness | Missing key elements | Mostly complete | All elements present |
|
||||
| Accuracy | Significant inaccuracies | Minor inaccuracies | Accurate throughout |
|
||||
|
||||
**Structure Rubric** (how the output is organized):
|
||||
| Criterion | 1 (Poor) | 3 (Acceptable) | 5 (Excellent) |
|
||||
|-----------|----------|----------------|---------------|
|
||||
| Organization | Disorganized | Reasonably organized | Clear, logical structure |
|
||||
| Formatting | Inconsistent/broken | Mostly consistent | Professional, polished |
|
||||
| Usability | Difficult to use | Usable with effort | Easy to use |
|
||||
|
||||
Adapt criteria to the specific task. For example:
|
||||
- PDF form → "Field alignment", "Text readability", "Data placement"
|
||||
- Document → "Section structure", "Heading hierarchy", "Paragraph flow"
|
||||
- Data output → "Schema correctness", "Data types", "Completeness"
|
||||
|
||||
### Step 4: Evaluate Each Output Against the Rubric
|
||||
|
||||
For each output (A and B):
|
||||
|
||||
1. **Score each criterion** on the rubric (1-5 scale)
|
||||
2. **Calculate dimension totals**: Content score, Structure score
|
||||
3. **Calculate overall score**: Average of dimension scores, scaled to 1-10
|
||||
|
||||
### Step 5: Check Assertions (if provided)
|
||||
|
||||
If expectations are provided:
|
||||
|
||||
1. Check each expectation against output A
|
||||
2. Check each expectation against output B
|
||||
3. Count pass rates for each output
|
||||
4. Use expectation scores as secondary evidence (not the primary decision factor)
|
||||
|
||||
### Step 6: Determine the Winner
|
||||
|
||||
Compare A and B based on (in priority order):
|
||||
|
||||
1. **Primary**: Overall rubric score (content + structure)
|
||||
2. **Secondary**: Assertion pass rates (if applicable)
|
||||
3. **Tiebreaker**: If truly equal, declare a TIE
|
||||
|
||||
Be decisive - ties should be rare. One output is usually better, even if marginally.
|
||||
|
||||
### Step 7: Write Comparison Results
|
||||
|
||||
Save results to a JSON file at the path specified (or `comparison.json` if not specified).
|
||||
|
||||
## Output Format
|
||||
|
||||
Write a JSON file with this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"winner": "A",
|
||||
"reasoning": "Output A provides a complete solution with proper formatting and all required fields. Output B is missing the date field and has formatting inconsistencies.",
|
||||
"rubric": {
|
||||
"A": {
|
||||
"content": {
|
||||
"correctness": 5,
|
||||
"completeness": 5,
|
||||
"accuracy": 4
|
||||
},
|
||||
"structure": {
|
||||
"organization": 4,
|
||||
"formatting": 5,
|
||||
"usability": 4
|
||||
},
|
||||
"content_score": 4.7,
|
||||
"structure_score": 4.3,
|
||||
"overall_score": 9.0
|
||||
},
|
||||
"B": {
|
||||
"content": {
|
||||
"correctness": 3,
|
||||
"completeness": 2,
|
||||
"accuracy": 3
|
||||
},
|
||||
"structure": {
|
||||
"organization": 3,
|
||||
"formatting": 2,
|
||||
"usability": 3
|
||||
},
|
||||
"content_score": 2.7,
|
||||
"structure_score": 2.7,
|
||||
"overall_score": 5.4
|
||||
}
|
||||
},
|
||||
"output_quality": {
|
||||
"A": {
|
||||
"score": 9,
|
||||
"strengths": ["Complete solution", "Well-formatted", "All fields present"],
|
||||
"weaknesses": ["Minor style inconsistency in header"]
|
||||
},
|
||||
"B": {
|
||||
"score": 5,
|
||||
"strengths": ["Readable output", "Correct basic structure"],
|
||||
"weaknesses": ["Missing date field", "Formatting inconsistencies", "Partial data extraction"]
|
||||
}
|
||||
},
|
||||
"expectation_results": {
|
||||
"A": {
|
||||
"passed": 4,
|
||||
"total": 5,
|
||||
"pass_rate": 0.80,
|
||||
"details": [
|
||||
{"text": "Output includes name", "passed": true},
|
||||
{"text": "Output includes date", "passed": true},
|
||||
{"text": "Format is PDF", "passed": true},
|
||||
{"text": "Contains signature", "passed": false},
|
||||
{"text": "Readable text", "passed": true}
|
||||
]
|
||||
},
|
||||
"B": {
|
||||
"passed": 3,
|
||||
"total": 5,
|
||||
"pass_rate": 0.60,
|
||||
"details": [
|
||||
{"text": "Output includes name", "passed": true},
|
||||
{"text": "Output includes date", "passed": false},
|
||||
{"text": "Format is PDF", "passed": true},
|
||||
{"text": "Contains signature", "passed": false},
|
||||
{"text": "Readable text", "passed": true}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If no expectations were provided, omit the `expectation_results` field entirely.
|
||||
|
||||
## Field Descriptions
|
||||
|
||||
- **winner**: "A", "B", or "TIE"
|
||||
- **reasoning**: Clear explanation of why the winner was chosen (or why it's a tie)
|
||||
- **rubric**: Structured rubric evaluation for each output
|
||||
- **content**: Scores for content criteria (correctness, completeness, accuracy)
|
||||
- **structure**: Scores for structure criteria (organization, formatting, usability)
|
||||
- **content_score**: Average of content criteria (1-5)
|
||||
- **structure_score**: Average of structure criteria (1-5)
|
||||
- **overall_score**: Combined score scaled to 1-10
|
||||
- **output_quality**: Summary quality assessment
|
||||
- **score**: 1-10 rating (should match rubric overall_score)
|
||||
- **strengths**: List of positive aspects
|
||||
- **weaknesses**: List of issues or shortcomings
|
||||
- **expectation_results**: (Only if expectations provided)
|
||||
- **passed**: Number of expectations that passed
|
||||
- **total**: Total number of expectations
|
||||
- **pass_rate**: Fraction passed (0.0 to 1.0)
|
||||
- **details**: Individual expectation results
|
||||
|
||||
## Guidelines
|
||||
|
||||
- **Stay blind**: DO NOT try to infer which skill produced which output. Judge purely on output quality.
|
||||
- **Be specific**: Cite specific examples when explaining strengths and weaknesses.
|
||||
- **Be decisive**: Choose a winner unless outputs are genuinely equivalent.
|
||||
- **Output quality first**: Assertion scores are secondary to overall task completion.
|
||||
- **Be objective**: Don't favor outputs based on style preferences; focus on correctness and completeness.
|
||||
- **Explain your reasoning**: The reasoning field should make it clear why you chose the winner.
|
||||
- **Handle edge cases**: If both outputs fail, pick the one that fails less badly. If both are excellent, pick the one that's marginally better.
|
||||
223
.skills/skill-creator/agents/grader.md
Normal file
223
.skills/skill-creator/agents/grader.md
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
# Grader Agent
|
||||
|
||||
Evaluate expectations against an execution transcript and outputs.
|
||||
|
||||
## Role
|
||||
|
||||
The Grader reviews a transcript and output files, then determines whether each expectation passes or fails. Provide clear evidence for each judgment.
|
||||
|
||||
You have two jobs: grade the outputs, and critique the evals themselves. A passing grade on a weak assertion is worse than useless — it creates false confidence. When you notice an assertion that's trivially satisfied, or an important outcome that no assertion checks, say so.
|
||||
|
||||
## Inputs
|
||||
|
||||
You receive these parameters in your prompt:
|
||||
|
||||
- **expectations**: List of expectations to evaluate (strings)
|
||||
- **transcript_path**: Path to the execution transcript (markdown file)
|
||||
- **outputs_dir**: Directory containing output files from execution
|
||||
|
||||
## Process
|
||||
|
||||
### Step 1: Read the Transcript
|
||||
|
||||
1. Read the transcript file completely
|
||||
2. Note the eval prompt, execution steps, and final result
|
||||
3. Identify any issues or errors documented
|
||||
|
||||
### Step 2: Examine Output Files
|
||||
|
||||
1. List files in outputs_dir
|
||||
2. Read/examine each file relevant to the expectations. If outputs aren't plain text, use the inspection tools provided in your prompt — don't rely solely on what the transcript says the executor produced.
|
||||
3. Note contents, structure, and quality
|
||||
|
||||
### Step 3: Evaluate Each Assertion
|
||||
|
||||
For each expectation:
|
||||
|
||||
1. **Search for evidence** in the transcript and outputs
|
||||
2. **Determine verdict**:
|
||||
- **PASS**: Clear evidence the expectation is true AND the evidence reflects genuine task completion, not just surface-level compliance
|
||||
- **FAIL**: No evidence, or evidence contradicts the expectation, or the evidence is superficial (e.g., correct filename but empty/wrong content)
|
||||
3. **Cite the evidence**: Quote the specific text or describe what you found
|
||||
|
||||
### Step 4: Extract and Verify Claims
|
||||
|
||||
Beyond the predefined expectations, extract implicit claims from the outputs and verify them:
|
||||
|
||||
1. **Extract claims** from the transcript and outputs:
|
||||
- Factual statements ("The form has 12 fields")
|
||||
- Process claims ("Used pypdf to fill the form")
|
||||
- Quality claims ("All fields were filled correctly")
|
||||
|
||||
2. **Verify each claim**:
|
||||
- **Factual claims**: Can be checked against the outputs or external sources
|
||||
- **Process claims**: Can be verified from the transcript
|
||||
- **Quality claims**: Evaluate whether the claim is justified
|
||||
|
||||
3. **Flag unverifiable claims**: Note claims that cannot be verified with available information
|
||||
|
||||
This catches issues that predefined expectations might miss.
|
||||
|
||||
### Step 5: Read User Notes
|
||||
|
||||
If `{outputs_dir}/user_notes.md` exists:
|
||||
1. Read it and note any uncertainties or issues flagged by the executor
|
||||
2. Include relevant concerns in the grading output
|
||||
3. These may reveal problems even when expectations pass
|
||||
|
||||
### Step 6: Critique the Evals
|
||||
|
||||
After grading, consider whether the evals themselves could be improved. Only surface suggestions when there's a clear gap.
|
||||
|
||||
Good suggestions test meaningful outcomes — assertions that are hard to satisfy without actually doing the work correctly. Think about what makes an assertion *discriminating*: it passes when the skill genuinely succeeds and fails when it doesn't.
|
||||
|
||||
Suggestions worth raising:
|
||||
- An assertion that passed but would also pass for a clearly wrong output (e.g., checking filename existence but not file content)
|
||||
- An important outcome you observed — good or bad — that no assertion covers at all
|
||||
- An assertion that can't actually be verified from the available outputs
|
||||
|
||||
Keep the bar high. The goal is to flag things the eval author would say "good catch" about, not to nitpick every assertion.
|
||||
|
||||
### Step 7: Write Grading Results
|
||||
|
||||
Save results to `{outputs_dir}/../grading.json` (sibling to outputs_dir).
|
||||
|
||||
## Grading Criteria
|
||||
|
||||
**PASS when**:
|
||||
- The transcript or outputs clearly demonstrate the expectation is true
|
||||
- Specific evidence can be cited
|
||||
- The evidence reflects genuine substance, not just surface compliance (e.g., a file exists AND contains correct content, not just the right filename)
|
||||
|
||||
**FAIL when**:
|
||||
- No evidence found for the expectation
|
||||
- Evidence contradicts the expectation
|
||||
- The expectation cannot be verified from available information
|
||||
- The evidence is superficial — the assertion is technically satisfied but the underlying task outcome is wrong or incomplete
|
||||
- The output appears to meet the assertion by coincidence rather than by actually doing the work
|
||||
|
||||
**When uncertain**: The burden of proof to pass is on the expectation.
|
||||
|
||||
### Step 8: Read Executor Metrics and Timing
|
||||
|
||||
1. If `{outputs_dir}/metrics.json` exists, read it and include in grading output
|
||||
2. If `{outputs_dir}/../timing.json` exists, read it and include timing data
|
||||
|
||||
## Output Format
|
||||
|
||||
Write a JSON file with this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"expectations": [
|
||||
{
|
||||
"text": "The output includes the name 'John Smith'",
|
||||
"passed": true,
|
||||
"evidence": "Found in transcript Step 3: 'Extracted names: John Smith, Sarah Johnson'"
|
||||
},
|
||||
{
|
||||
"text": "The spreadsheet has a SUM formula in cell B10",
|
||||
"passed": false,
|
||||
"evidence": "No spreadsheet was created. The output was a text file."
|
||||
},
|
||||
{
|
||||
"text": "The assistant used the skill's OCR script",
|
||||
"passed": true,
|
||||
"evidence": "Transcript Step 2 shows: 'Tool: Bash - python ocr_script.py image.png'"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"passed": 2,
|
||||
"failed": 1,
|
||||
"total": 3,
|
||||
"pass_rate": 0.67
|
||||
},
|
||||
"execution_metrics": {
|
||||
"tool_calls": {
|
||||
"Read": 5,
|
||||
"Write": 2,
|
||||
"Bash": 8
|
||||
},
|
||||
"total_tool_calls": 15,
|
||||
"total_steps": 6,
|
||||
"errors_encountered": 0,
|
||||
"output_chars": 12450,
|
||||
"transcript_chars": 3200
|
||||
},
|
||||
"timing": {
|
||||
"executor_duration_seconds": 165.0,
|
||||
"grader_duration_seconds": 26.0,
|
||||
"total_duration_seconds": 191.0
|
||||
},
|
||||
"claims": [
|
||||
{
|
||||
"claim": "The form has 12 fillable fields",
|
||||
"type": "factual",
|
||||
"verified": true,
|
||||
"evidence": "Counted 12 fields in field_info.json"
|
||||
},
|
||||
{
|
||||
"claim": "All required fields were populated",
|
||||
"type": "quality",
|
||||
"verified": false,
|
||||
"evidence": "Reference section was left blank despite data being available"
|
||||
}
|
||||
],
|
||||
"user_notes_summary": {
|
||||
"uncertainties": ["Used 2023 data, may be stale"],
|
||||
"needs_review": [],
|
||||
"workarounds": ["Fell back to text overlay for non-fillable fields"]
|
||||
},
|
||||
"eval_feedback": {
|
||||
"suggestions": [
|
||||
{
|
||||
"assertion": "The output includes the name 'John Smith'",
|
||||
"reason": "A hallucinated document that mentions the name would also pass — consider checking it appears as the primary contact with matching phone and email from the input"
|
||||
},
|
||||
{
|
||||
"reason": "No assertion checks whether the extracted phone numbers match the input — I observed incorrect numbers in the output that went uncaught"
|
||||
}
|
||||
],
|
||||
"overall": "Assertions check presence but not correctness. Consider adding content verification."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Field Descriptions
|
||||
|
||||
- **expectations**: Array of graded expectations
|
||||
- **text**: The original expectation text
|
||||
- **passed**: Boolean - true if expectation passes
|
||||
- **evidence**: Specific quote or description supporting the verdict
|
||||
- **summary**: Aggregate statistics
|
||||
- **passed**: Count of passed expectations
|
||||
- **failed**: Count of failed expectations
|
||||
- **total**: Total expectations evaluated
|
||||
- **pass_rate**: Fraction passed (0.0 to 1.0)
|
||||
- **execution_metrics**: Copied from executor's metrics.json (if available)
|
||||
- **output_chars**: Total character count of output files (proxy for tokens)
|
||||
- **transcript_chars**: Character count of transcript
|
||||
- **timing**: Wall clock timing from timing.json (if available)
|
||||
- **executor_duration_seconds**: Time spent in executor subagent
|
||||
- **total_duration_seconds**: Total elapsed time for the run
|
||||
- **claims**: Extracted and verified claims from the output
|
||||
- **claim**: The statement being verified
|
||||
- **type**: "factual", "process", or "quality"
|
||||
- **verified**: Boolean - whether the claim holds
|
||||
- **evidence**: Supporting or contradicting evidence
|
||||
- **user_notes_summary**: Issues flagged by the executor
|
||||
- **uncertainties**: Things the executor wasn't sure about
|
||||
- **needs_review**: Items requiring human attention
|
||||
- **workarounds**: Places where the skill didn't work as expected
|
||||
- **eval_feedback**: Improvement suggestions for the evals (only when warranted)
|
||||
- **suggestions**: List of concrete suggestions, each with a `reason` and optionally an `assertion` it relates to
|
||||
- **overall**: Brief assessment — can be "No suggestions, evals look solid" if nothing to flag
|
||||
|
||||
## Guidelines
|
||||
|
||||
- **Be objective**: Base verdicts on evidence, not assumptions
|
||||
- **Be specific**: Quote the exact text that supports your verdict
|
||||
- **Be thorough**: Check both transcript and output files
|
||||
- **Be consistent**: Apply the same standard to each expectation
|
||||
- **Explain failures**: Make it clear why evidence was insufficient
|
||||
- **No partial credit**: Each expectation is pass or fail, not partial
|
||||
146
.skills/skill-creator/assets/eval_review.html
Normal file
146
.skills/skill-creator/assets/eval_review.html
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Eval Set Review - __SKILL_NAME_PLACEHOLDER__</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@500;600&family=Lora:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: 'Lora', Georgia, serif; background: #faf9f5; padding: 2rem; color: #141413; }
|
||||
h1 { font-family: 'Poppins', sans-serif; margin-bottom: 0.5rem; font-size: 1.5rem; }
|
||||
.description { color: #b0aea5; margin-bottom: 1.5rem; font-style: italic; max-width: 900px; }
|
||||
.controls { margin-bottom: 1rem; display: flex; gap: 0.5rem; }
|
||||
.btn { font-family: 'Poppins', sans-serif; padding: 0.5rem 1rem; border: none; border-radius: 6px; cursor: pointer; font-size: 0.875rem; font-weight: 500; }
|
||||
.btn-add { background: #6a9bcc; color: white; }
|
||||
.btn-add:hover { background: #5889b8; }
|
||||
.btn-export { background: #d97757; color: white; }
|
||||
.btn-export:hover { background: #c4613f; }
|
||||
table { width: 100%; max-width: 1100px; border-collapse: collapse; background: white; border-radius: 6px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
||||
th { font-family: 'Poppins', sans-serif; background: #141413; color: #faf9f5; padding: 0.75rem 1rem; text-align: left; font-size: 0.875rem; }
|
||||
td { padding: 0.75rem 1rem; border-bottom: 1px solid #e8e6dc; vertical-align: top; }
|
||||
tr:nth-child(even) td { background: #faf9f5; }
|
||||
tr:hover td { background: #f3f1ea; }
|
||||
.section-header td { background: #e8e6dc; font-family: 'Poppins', sans-serif; font-weight: 500; font-size: 0.8rem; color: #141413; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.query-input { width: 100%; padding: 0.4rem; border: 1px solid #e8e6dc; border-radius: 4px; font-size: 0.875rem; font-family: 'Lora', Georgia, serif; resize: vertical; min-height: 60px; }
|
||||
.query-input:focus { outline: none; border-color: #d97757; box-shadow: 0 0 0 2px rgba(217,119,87,0.15); }
|
||||
.toggle { position: relative; display: inline-block; width: 44px; height: 24px; }
|
||||
.toggle input { opacity: 0; width: 0; height: 0; }
|
||||
.toggle .slider { position: absolute; inset: 0; background: #b0aea5; border-radius: 24px; cursor: pointer; transition: 0.2s; }
|
||||
.toggle .slider::before { content: ""; position: absolute; width: 18px; height: 18px; left: 3px; bottom: 3px; background: white; border-radius: 50%; transition: 0.2s; }
|
||||
.toggle input:checked + .slider { background: #d97757; }
|
||||
.toggle input:checked + .slider::before { transform: translateX(20px); }
|
||||
.btn-delete { background: #c44; color: white; padding: 0.3rem 0.6rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.75rem; font-family: 'Poppins', sans-serif; }
|
||||
.btn-delete:hover { background: #a33; }
|
||||
.summary { margin-top: 1rem; color: #b0aea5; font-size: 0.875rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Eval Set Review: <span id="skill-name">__SKILL_NAME_PLACEHOLDER__</span></h1>
|
||||
<p class="description">Current description: <span id="skill-desc">__SKILL_DESCRIPTION_PLACEHOLDER__</span></p>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn btn-add" onclick="addRow()">+ Add Query</button>
|
||||
<button class="btn btn-export" onclick="exportEvalSet()">Export Eval Set</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:65%">Query</th>
|
||||
<th style="width:18%">Should Trigger</th>
|
||||
<th style="width:10%">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="eval-body"></tbody>
|
||||
</table>
|
||||
|
||||
<p class="summary" id="summary"></p>
|
||||
|
||||
<script>
|
||||
const EVAL_DATA = __EVAL_DATA_PLACEHOLDER__;
|
||||
|
||||
let evalItems = [...EVAL_DATA];
|
||||
|
||||
function render() {
|
||||
const tbody = document.getElementById('eval-body');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Sort: should-trigger first, then should-not-trigger
|
||||
const sorted = evalItems
|
||||
.map((item, origIdx) => ({ ...item, origIdx }))
|
||||
.sort((a, b) => (b.should_trigger ? 1 : 0) - (a.should_trigger ? 1 : 0));
|
||||
|
||||
let lastGroup = null;
|
||||
sorted.forEach(item => {
|
||||
const group = item.should_trigger ? 'trigger' : 'no-trigger';
|
||||
if (group !== lastGroup) {
|
||||
const headerRow = document.createElement('tr');
|
||||
headerRow.className = 'section-header';
|
||||
headerRow.innerHTML = `<td colspan="3">${item.should_trigger ? 'Should Trigger' : 'Should NOT Trigger'}</td>`;
|
||||
tbody.appendChild(headerRow);
|
||||
lastGroup = group;
|
||||
}
|
||||
|
||||
const idx = item.origIdx;
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td><textarea class="query-input" onchange="updateQuery(${idx}, this.value)">${escapeHtml(item.query)}</textarea></td>
|
||||
<td>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" ${item.should_trigger ? 'checked' : ''} onchange="updateTrigger(${idx}, this.checked)">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span style="margin-left:8px;font-size:0.8rem;color:#b0aea5">${item.should_trigger ? 'Yes' : 'No'}</span>
|
||||
</td>
|
||||
<td><button class="btn-delete" onclick="deleteRow(${idx})">Delete</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function updateQuery(idx, value) { evalItems[idx].query = value; updateSummary(); }
|
||||
function updateTrigger(idx, value) { evalItems[idx].should_trigger = value; render(); }
|
||||
function deleteRow(idx) { evalItems.splice(idx, 1); render(); }
|
||||
|
||||
function addRow() {
|
||||
evalItems.push({ query: '', should_trigger: true });
|
||||
render();
|
||||
const inputs = document.querySelectorAll('.query-input');
|
||||
inputs[inputs.length - 1].focus();
|
||||
}
|
||||
|
||||
function updateSummary() {
|
||||
const trigger = evalItems.filter(i => i.should_trigger).length;
|
||||
const noTrigger = evalItems.filter(i => !i.should_trigger).length;
|
||||
document.getElementById('summary').textContent =
|
||||
`${evalItems.length} queries total: ${trigger} should trigger, ${noTrigger} should not trigger`;
|
||||
}
|
||||
|
||||
function exportEvalSet() {
|
||||
const valid = evalItems.filter(i => i.query.trim() !== '');
|
||||
const data = valid.map(i => ({ query: i.query.trim(), should_trigger: i.should_trigger }));
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'eval_set.json';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
471
.skills/skill-creator/eval-viewer/generate_review.py
Normal file
471
.skills/skill-creator/eval-viewer/generate_review.py
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate and serve a review page for eval results.
|
||||
|
||||
Reads the workspace directory, discovers runs (directories with outputs/),
|
||||
embeds all output data into a self-contained HTML page, and serves it via
|
||||
a tiny HTTP server. Feedback auto-saves to feedback.json in the workspace.
|
||||
|
||||
Usage:
|
||||
python generate_review.py <workspace-path> [--port PORT] [--skill-name NAME]
|
||||
python generate_review.py <workspace-path> --previous-feedback /path/to/old/feedback.json
|
||||
|
||||
No dependencies beyond the Python stdlib are required.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import webbrowser
|
||||
from functools import partial
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from pathlib import Path
|
||||
|
||||
# Files to exclude from output listings
|
||||
METADATA_FILES = {"transcript.md", "user_notes.md", "metrics.json"}
|
||||
|
||||
# Extensions we render as inline text
|
||||
TEXT_EXTENSIONS = {
|
||||
".txt", ".md", ".json", ".csv", ".py", ".js", ".ts", ".tsx", ".jsx",
|
||||
".yaml", ".yml", ".xml", ".html", ".css", ".sh", ".rb", ".go", ".rs",
|
||||
".java", ".c", ".cpp", ".h", ".hpp", ".sql", ".r", ".toml",
|
||||
}
|
||||
|
||||
# Extensions we render as inline images
|
||||
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"}
|
||||
|
||||
# MIME type overrides for common types
|
||||
MIME_OVERRIDES = {
|
||||
".svg": "image/svg+xml",
|
||||
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
}
|
||||
|
||||
|
||||
def get_mime_type(path: Path) -> str:
|
||||
ext = path.suffix.lower()
|
||||
if ext in MIME_OVERRIDES:
|
||||
return MIME_OVERRIDES[ext]
|
||||
mime, _ = mimetypes.guess_type(str(path))
|
||||
return mime or "application/octet-stream"
|
||||
|
||||
|
||||
def find_runs(workspace: Path) -> list[dict]:
|
||||
"""Recursively find directories that contain an outputs/ subdirectory."""
|
||||
runs: list[dict] = []
|
||||
_find_runs_recursive(workspace, workspace, runs)
|
||||
runs.sort(key=lambda r: (r.get("eval_id", float("inf")), r["id"]))
|
||||
return runs
|
||||
|
||||
|
||||
def _find_runs_recursive(root: Path, current: Path, runs: list[dict]) -> None:
|
||||
if not current.is_dir():
|
||||
return
|
||||
|
||||
outputs_dir = current / "outputs"
|
||||
if outputs_dir.is_dir():
|
||||
run = build_run(root, current)
|
||||
if run:
|
||||
runs.append(run)
|
||||
return
|
||||
|
||||
skip = {"node_modules", ".git", "__pycache__", "skill", "inputs"}
|
||||
for child in sorted(current.iterdir()):
|
||||
if child.is_dir() and child.name not in skip:
|
||||
_find_runs_recursive(root, child, runs)
|
||||
|
||||
|
||||
def build_run(root: Path, run_dir: Path) -> dict | None:
|
||||
"""Build a run dict with prompt, outputs, and grading data."""
|
||||
prompt = ""
|
||||
eval_id = None
|
||||
|
||||
# Try eval_metadata.json
|
||||
for candidate in [run_dir / "eval_metadata.json", run_dir.parent / "eval_metadata.json"]:
|
||||
if candidate.exists():
|
||||
try:
|
||||
metadata = json.loads(candidate.read_text())
|
||||
prompt = metadata.get("prompt", "")
|
||||
eval_id = metadata.get("eval_id")
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
if prompt:
|
||||
break
|
||||
|
||||
# Fall back to transcript.md
|
||||
if not prompt:
|
||||
for candidate in [run_dir / "transcript.md", run_dir / "outputs" / "transcript.md"]:
|
||||
if candidate.exists():
|
||||
try:
|
||||
text = candidate.read_text()
|
||||
match = re.search(r"## Eval Prompt\n\n([\s\S]*?)(?=\n##|$)", text)
|
||||
if match:
|
||||
prompt = match.group(1).strip()
|
||||
except OSError:
|
||||
pass
|
||||
if prompt:
|
||||
break
|
||||
|
||||
if not prompt:
|
||||
prompt = "(No prompt found)"
|
||||
|
||||
run_id = str(run_dir.relative_to(root)).replace("/", "-").replace("\\", "-")
|
||||
|
||||
# Collect output files
|
||||
outputs_dir = run_dir / "outputs"
|
||||
output_files: list[dict] = []
|
||||
if outputs_dir.is_dir():
|
||||
for f in sorted(outputs_dir.iterdir()):
|
||||
if f.is_file() and f.name not in METADATA_FILES:
|
||||
output_files.append(embed_file(f))
|
||||
|
||||
# Load grading if present
|
||||
grading = None
|
||||
for candidate in [run_dir / "grading.json", run_dir.parent / "grading.json"]:
|
||||
if candidate.exists():
|
||||
try:
|
||||
grading = json.loads(candidate.read_text())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
if grading:
|
||||
break
|
||||
|
||||
return {
|
||||
"id": run_id,
|
||||
"prompt": prompt,
|
||||
"eval_id": eval_id,
|
||||
"outputs": output_files,
|
||||
"grading": grading,
|
||||
}
|
||||
|
||||
|
||||
def embed_file(path: Path) -> dict:
|
||||
"""Read a file and return an embedded representation."""
|
||||
ext = path.suffix.lower()
|
||||
mime = get_mime_type(path)
|
||||
|
||||
if ext in TEXT_EXTENSIONS:
|
||||
try:
|
||||
content = path.read_text(errors="replace")
|
||||
except OSError:
|
||||
content = "(Error reading file)"
|
||||
return {
|
||||
"name": path.name,
|
||||
"type": "text",
|
||||
"content": content,
|
||||
}
|
||||
elif ext in IMAGE_EXTENSIONS:
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
b64 = base64.b64encode(raw).decode("ascii")
|
||||
except OSError:
|
||||
return {"name": path.name, "type": "error", "content": "(Error reading file)"}
|
||||
return {
|
||||
"name": path.name,
|
||||
"type": "image",
|
||||
"mime": mime,
|
||||
"data_uri": f"data:{mime};base64,{b64}",
|
||||
}
|
||||
elif ext == ".pdf":
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
b64 = base64.b64encode(raw).decode("ascii")
|
||||
except OSError:
|
||||
return {"name": path.name, "type": "error", "content": "(Error reading file)"}
|
||||
return {
|
||||
"name": path.name,
|
||||
"type": "pdf",
|
||||
"data_uri": f"data:{mime};base64,{b64}",
|
||||
}
|
||||
elif ext == ".xlsx":
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
b64 = base64.b64encode(raw).decode("ascii")
|
||||
except OSError:
|
||||
return {"name": path.name, "type": "error", "content": "(Error reading file)"}
|
||||
return {
|
||||
"name": path.name,
|
||||
"type": "xlsx",
|
||||
"data_b64": b64,
|
||||
}
|
||||
else:
|
||||
# Binary / unknown — base64 download link
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
b64 = base64.b64encode(raw).decode("ascii")
|
||||
except OSError:
|
||||
return {"name": path.name, "type": "error", "content": "(Error reading file)"}
|
||||
return {
|
||||
"name": path.name,
|
||||
"type": "binary",
|
||||
"mime": mime,
|
||||
"data_uri": f"data:{mime};base64,{b64}",
|
||||
}
|
||||
|
||||
|
||||
def load_previous_iteration(workspace: Path) -> dict[str, dict]:
|
||||
"""Load previous iteration's feedback and outputs.
|
||||
|
||||
Returns a map of run_id -> {"feedback": str, "outputs": list[dict]}.
|
||||
"""
|
||||
result: dict[str, dict] = {}
|
||||
|
||||
# Load feedback
|
||||
feedback_map: dict[str, str] = {}
|
||||
feedback_path = workspace / "feedback.json"
|
||||
if feedback_path.exists():
|
||||
try:
|
||||
data = json.loads(feedback_path.read_text())
|
||||
feedback_map = {
|
||||
r["run_id"]: r["feedback"]
|
||||
for r in data.get("reviews", [])
|
||||
if r.get("feedback", "").strip()
|
||||
}
|
||||
except (json.JSONDecodeError, OSError, KeyError):
|
||||
pass
|
||||
|
||||
# Load runs (to get outputs)
|
||||
prev_runs = find_runs(workspace)
|
||||
for run in prev_runs:
|
||||
result[run["id"]] = {
|
||||
"feedback": feedback_map.get(run["id"], ""),
|
||||
"outputs": run.get("outputs", []),
|
||||
}
|
||||
|
||||
# Also add feedback for run_ids that had feedback but no matching run
|
||||
for run_id, fb in feedback_map.items():
|
||||
if run_id not in result:
|
||||
result[run_id] = {"feedback": fb, "outputs": []}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_html(
|
||||
runs: list[dict],
|
||||
skill_name: str,
|
||||
previous: dict[str, dict] | None = None,
|
||||
benchmark: dict | None = None,
|
||||
) -> str:
|
||||
"""Generate the complete standalone HTML page with embedded data."""
|
||||
template_path = Path(__file__).parent / "viewer.html"
|
||||
template = template_path.read_text()
|
||||
|
||||
# Build previous_feedback and previous_outputs maps for the template
|
||||
previous_feedback: dict[str, str] = {}
|
||||
previous_outputs: dict[str, list[dict]] = {}
|
||||
if previous:
|
||||
for run_id, data in previous.items():
|
||||
if data.get("feedback"):
|
||||
previous_feedback[run_id] = data["feedback"]
|
||||
if data.get("outputs"):
|
||||
previous_outputs[run_id] = data["outputs"]
|
||||
|
||||
embedded = {
|
||||
"skill_name": skill_name,
|
||||
"runs": runs,
|
||||
"previous_feedback": previous_feedback,
|
||||
"previous_outputs": previous_outputs,
|
||||
}
|
||||
if benchmark:
|
||||
embedded["benchmark"] = benchmark
|
||||
|
||||
data_json = json.dumps(embedded)
|
||||
|
||||
return template.replace("/*__EMBEDDED_DATA__*/", f"const EMBEDDED_DATA = {data_json};")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HTTP server (stdlib only, zero dependencies)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _kill_port(port: int) -> None:
|
||||
"""Kill any process listening on the given port."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["lsof", "-ti", f":{port}"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
for pid_str in result.stdout.strip().split("\n"):
|
||||
if pid_str.strip():
|
||||
try:
|
||||
os.kill(int(pid_str.strip()), signal.SIGTERM)
|
||||
except (ProcessLookupError, ValueError):
|
||||
pass
|
||||
if result.stdout.strip():
|
||||
time.sleep(0.5)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
except FileNotFoundError:
|
||||
print("Note: lsof not found, cannot check if port is in use", file=sys.stderr)
|
||||
|
||||
class ReviewHandler(BaseHTTPRequestHandler):
|
||||
"""Serves the review HTML and handles feedback saves.
|
||||
|
||||
Regenerates the HTML on each page load so that refreshing the browser
|
||||
picks up new eval outputs without restarting the server.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace: Path,
|
||||
skill_name: str,
|
||||
feedback_path: Path,
|
||||
previous: dict[str, dict],
|
||||
benchmark_path: Path | None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.workspace = workspace
|
||||
self.skill_name = skill_name
|
||||
self.feedback_path = feedback_path
|
||||
self.previous = previous
|
||||
self.benchmark_path = benchmark_path
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def do_GET(self) -> None:
|
||||
if self.path == "/" or self.path == "/index.html":
|
||||
# Regenerate HTML on each request (re-scans workspace for new outputs)
|
||||
runs = find_runs(self.workspace)
|
||||
benchmark = None
|
||||
if self.benchmark_path and self.benchmark_path.exists():
|
||||
try:
|
||||
benchmark = json.loads(self.benchmark_path.read_text())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
html = generate_html(runs, self.skill_name, self.previous, benchmark)
|
||||
content = html.encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(content)))
|
||||
self.end_headers()
|
||||
self.wfile.write(content)
|
||||
elif self.path == "/api/feedback":
|
||||
data = b"{}"
|
||||
if self.feedback_path.exists():
|
||||
data = self.feedback_path.read_bytes()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def do_POST(self) -> None:
|
||||
if self.path == "/api/feedback":
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length)
|
||||
try:
|
||||
data = json.loads(body)
|
||||
if not isinstance(data, dict) or "reviews" not in data:
|
||||
raise ValueError("Expected JSON object with 'reviews' key")
|
||||
self.feedback_path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
resp = b'{"ok":true}'
|
||||
self.send_response(200)
|
||||
except (json.JSONDecodeError, OSError, ValueError) as e:
|
||||
resp = json.dumps({"error": str(e)}).encode()
|
||||
self.send_response(500)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(resp)))
|
||||
self.end_headers()
|
||||
self.wfile.write(resp)
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def log_message(self, format: str, *args: object) -> None:
|
||||
# Suppress request logging to keep terminal clean
|
||||
pass
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Generate and serve eval review")
|
||||
parser.add_argument("workspace", type=Path, help="Path to workspace directory")
|
||||
parser.add_argument("--port", "-p", type=int, default=3117, help="Server port (default: 3117)")
|
||||
parser.add_argument("--skill-name", "-n", type=str, default=None, help="Skill name for header")
|
||||
parser.add_argument(
|
||||
"--previous-workspace", type=Path, default=None,
|
||||
help="Path to previous iteration's workspace (shows old outputs and feedback as context)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--benchmark", type=Path, default=None,
|
||||
help="Path to benchmark.json to show in the Benchmark tab",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--static", "-s", type=Path, default=None,
|
||||
help="Write standalone HTML to this path instead of starting a server",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
workspace = args.workspace.resolve()
|
||||
if not workspace.is_dir():
|
||||
print(f"Error: {workspace} is not a directory", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
runs = find_runs(workspace)
|
||||
if not runs:
|
||||
print(f"No runs found in {workspace}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
skill_name = args.skill_name or workspace.name.replace("-workspace", "")
|
||||
feedback_path = workspace / "feedback.json"
|
||||
|
||||
previous: dict[str, dict] = {}
|
||||
if args.previous_workspace:
|
||||
previous = load_previous_iteration(args.previous_workspace.resolve())
|
||||
|
||||
benchmark_path = args.benchmark.resolve() if args.benchmark else None
|
||||
benchmark = None
|
||||
if benchmark_path and benchmark_path.exists():
|
||||
try:
|
||||
benchmark = json.loads(benchmark_path.read_text())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
if args.static:
|
||||
html = generate_html(runs, skill_name, previous, benchmark)
|
||||
args.static.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.static.write_text(html)
|
||||
print(f"\n Static viewer written to: {args.static}\n")
|
||||
sys.exit(0)
|
||||
|
||||
# Kill any existing process on the target port
|
||||
port = args.port
|
||||
_kill_port(port)
|
||||
handler = partial(ReviewHandler, workspace, skill_name, feedback_path, previous, benchmark_path)
|
||||
try:
|
||||
server = HTTPServer(("127.0.0.1", port), handler)
|
||||
except OSError:
|
||||
# Port still in use after kill attempt — find a free one
|
||||
server = HTTPServer(("127.0.0.1", 0), handler)
|
||||
port = server.server_address[1]
|
||||
|
||||
url = f"http://localhost:{port}"
|
||||
print(f"\n Eval Viewer")
|
||||
print(f" ─────────────────────────────────")
|
||||
print(f" URL: {url}")
|
||||
print(f" Workspace: {workspace}")
|
||||
print(f" Feedback: {feedback_path}")
|
||||
if previous:
|
||||
print(f" Previous: {args.previous_workspace} ({len(previous)} runs)")
|
||||
if benchmark_path:
|
||||
print(f" Benchmark: {benchmark_path}")
|
||||
print(f"\n Press Ctrl+C to stop.\n")
|
||||
|
||||
webbrowser.open(url)
|
||||
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
server.server_close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1325
.skills/skill-creator/eval-viewer/viewer.html
Normal file
1325
.skills/skill-creator/eval-viewer/viewer.html
Normal file
File diff suppressed because it is too large
Load diff
430
.skills/skill-creator/references/schemas.md
Normal file
430
.skills/skill-creator/references/schemas.md
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
# JSON Schemas
|
||||
|
||||
This document defines the JSON schemas used by skill-creator.
|
||||
|
||||
---
|
||||
|
||||
## evals.json
|
||||
|
||||
Defines the evals for a skill. Located at `evals/evals.json` within the skill directory.
|
||||
|
||||
```json
|
||||
{
|
||||
"skill_name": "example-skill",
|
||||
"evals": [
|
||||
{
|
||||
"id": 1,
|
||||
"prompt": "User's example prompt",
|
||||
"expected_output": "Description of expected result",
|
||||
"files": ["evals/files/sample1.pdf"],
|
||||
"expectations": [
|
||||
"The output includes X",
|
||||
"The skill used script Y"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `skill_name`: Name matching the skill's frontmatter
|
||||
- `evals[].id`: Unique integer identifier
|
||||
- `evals[].prompt`: The task to execute
|
||||
- `evals[].expected_output`: Human-readable description of success
|
||||
- `evals[].files`: Optional list of input file paths (relative to skill root)
|
||||
- `evals[].expectations`: List of verifiable statements
|
||||
|
||||
---
|
||||
|
||||
## history.json
|
||||
|
||||
Tracks version progression in Improve mode. Located at workspace root.
|
||||
|
||||
```json
|
||||
{
|
||||
"started_at": "2026-01-15T10:30:00Z",
|
||||
"skill_name": "pdf",
|
||||
"current_best": "v2",
|
||||
"iterations": [
|
||||
{
|
||||
"version": "v0",
|
||||
"parent": null,
|
||||
"expectation_pass_rate": 0.65,
|
||||
"grading_result": "baseline",
|
||||
"is_current_best": false
|
||||
},
|
||||
{
|
||||
"version": "v1",
|
||||
"parent": "v0",
|
||||
"expectation_pass_rate": 0.75,
|
||||
"grading_result": "won",
|
||||
"is_current_best": false
|
||||
},
|
||||
{
|
||||
"version": "v2",
|
||||
"parent": "v1",
|
||||
"expectation_pass_rate": 0.85,
|
||||
"grading_result": "won",
|
||||
"is_current_best": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `started_at`: ISO timestamp of when improvement started
|
||||
- `skill_name`: Name of the skill being improved
|
||||
- `current_best`: Version identifier of the best performer
|
||||
- `iterations[].version`: Version identifier (v0, v1, ...)
|
||||
- `iterations[].parent`: Parent version this was derived from
|
||||
- `iterations[].expectation_pass_rate`: Pass rate from grading
|
||||
- `iterations[].grading_result`: "baseline", "won", "lost", or "tie"
|
||||
- `iterations[].is_current_best`: Whether this is the current best version
|
||||
|
||||
---
|
||||
|
||||
## grading.json
|
||||
|
||||
Output from the grader agent. Located at `<run-dir>/grading.json`.
|
||||
|
||||
```json
|
||||
{
|
||||
"expectations": [
|
||||
{
|
||||
"text": "The output includes the name 'John Smith'",
|
||||
"passed": true,
|
||||
"evidence": "Found in transcript Step 3: 'Extracted names: John Smith, Sarah Johnson'"
|
||||
},
|
||||
{
|
||||
"text": "The spreadsheet has a SUM formula in cell B10",
|
||||
"passed": false,
|
||||
"evidence": "No spreadsheet was created. The output was a text file."
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"passed": 2,
|
||||
"failed": 1,
|
||||
"total": 3,
|
||||
"pass_rate": 0.67
|
||||
},
|
||||
"execution_metrics": {
|
||||
"tool_calls": {
|
||||
"Read": 5,
|
||||
"Write": 2,
|
||||
"Bash": 8
|
||||
},
|
||||
"total_tool_calls": 15,
|
||||
"total_steps": 6,
|
||||
"errors_encountered": 0,
|
||||
"output_chars": 12450,
|
||||
"transcript_chars": 3200
|
||||
},
|
||||
"timing": {
|
||||
"executor_duration_seconds": 165.0,
|
||||
"grader_duration_seconds": 26.0,
|
||||
"total_duration_seconds": 191.0
|
||||
},
|
||||
"claims": [
|
||||
{
|
||||
"claim": "The form has 12 fillable fields",
|
||||
"type": "factual",
|
||||
"verified": true,
|
||||
"evidence": "Counted 12 fields in field_info.json"
|
||||
}
|
||||
],
|
||||
"user_notes_summary": {
|
||||
"uncertainties": ["Used 2023 data, may be stale"],
|
||||
"needs_review": [],
|
||||
"workarounds": ["Fell back to text overlay for non-fillable fields"]
|
||||
},
|
||||
"eval_feedback": {
|
||||
"suggestions": [
|
||||
{
|
||||
"assertion": "The output includes the name 'John Smith'",
|
||||
"reason": "A hallucinated document that mentions the name would also pass"
|
||||
}
|
||||
],
|
||||
"overall": "Assertions check presence but not correctness."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `expectations[]`: Graded expectations with evidence
|
||||
- `summary`: Aggregate pass/fail counts
|
||||
- `execution_metrics`: Tool usage and output size (from executor's metrics.json)
|
||||
- `timing`: Wall clock timing (from timing.json)
|
||||
- `claims`: Extracted and verified claims from the output
|
||||
- `user_notes_summary`: Issues flagged by the executor
|
||||
- `eval_feedback`: (optional) Improvement suggestions for the evals, only present when the grader identifies issues worth raising
|
||||
|
||||
---
|
||||
|
||||
## metrics.json
|
||||
|
||||
Output from the executor agent. Located at `<run-dir>/outputs/metrics.json`.
|
||||
|
||||
```json
|
||||
{
|
||||
"tool_calls": {
|
||||
"Read": 5,
|
||||
"Write": 2,
|
||||
"Bash": 8,
|
||||
"Edit": 1,
|
||||
"Glob": 2,
|
||||
"Grep": 0
|
||||
},
|
||||
"total_tool_calls": 18,
|
||||
"total_steps": 6,
|
||||
"files_created": ["filled_form.pdf", "field_values.json"],
|
||||
"errors_encountered": 0,
|
||||
"output_chars": 12450,
|
||||
"transcript_chars": 3200
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `tool_calls`: Count per tool type
|
||||
- `total_tool_calls`: Sum of all tool calls
|
||||
- `total_steps`: Number of major execution steps
|
||||
- `files_created`: List of output files created
|
||||
- `errors_encountered`: Number of errors during execution
|
||||
- `output_chars`: Total character count of output files
|
||||
- `transcript_chars`: Character count of transcript
|
||||
|
||||
---
|
||||
|
||||
## timing.json
|
||||
|
||||
Wall clock timing for a run. Located at `<run-dir>/timing.json`.
|
||||
|
||||
**How to capture:** When a subagent task completes, the task notification includes `total_tokens` and `duration_ms`. Save these immediately — they are not persisted anywhere else and cannot be recovered after the fact.
|
||||
|
||||
```json
|
||||
{
|
||||
"total_tokens": 84852,
|
||||
"duration_ms": 23332,
|
||||
"total_duration_seconds": 23.3,
|
||||
"executor_start": "2026-01-15T10:30:00Z",
|
||||
"executor_end": "2026-01-15T10:32:45Z",
|
||||
"executor_duration_seconds": 165.0,
|
||||
"grader_start": "2026-01-15T10:32:46Z",
|
||||
"grader_end": "2026-01-15T10:33:12Z",
|
||||
"grader_duration_seconds": 26.0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## benchmark.json
|
||||
|
||||
Output from Benchmark mode. Located at `benchmarks/<timestamp>/benchmark.json`.
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"skill_name": "pdf",
|
||||
"skill_path": "/path/to/pdf",
|
||||
"executor_model": "claude-sonnet-4-20250514",
|
||||
"analyzer_model": "most-capable-model",
|
||||
"timestamp": "2026-01-15T10:30:00Z",
|
||||
"evals_run": [1, 2, 3],
|
||||
"runs_per_configuration": 3
|
||||
},
|
||||
|
||||
"runs": [
|
||||
{
|
||||
"eval_id": 1,
|
||||
"eval_name": "Ocean",
|
||||
"configuration": "with_skill",
|
||||
"run_number": 1,
|
||||
"result": {
|
||||
"pass_rate": 0.85,
|
||||
"passed": 6,
|
||||
"failed": 1,
|
||||
"total": 7,
|
||||
"time_seconds": 42.5,
|
||||
"tokens": 3800,
|
||||
"tool_calls": 18,
|
||||
"errors": 0
|
||||
},
|
||||
"expectations": [
|
||||
{"text": "...", "passed": true, "evidence": "..."}
|
||||
],
|
||||
"notes": [
|
||||
"Used 2023 data, may be stale",
|
||||
"Fell back to text overlay for non-fillable fields"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"run_summary": {
|
||||
"with_skill": {
|
||||
"pass_rate": {"mean": 0.85, "stddev": 0.05, "min": 0.80, "max": 0.90},
|
||||
"time_seconds": {"mean": 45.0, "stddev": 12.0, "min": 32.0, "max": 58.0},
|
||||
"tokens": {"mean": 3800, "stddev": 400, "min": 3200, "max": 4100}
|
||||
},
|
||||
"without_skill": {
|
||||
"pass_rate": {"mean": 0.35, "stddev": 0.08, "min": 0.28, "max": 0.45},
|
||||
"time_seconds": {"mean": 32.0, "stddev": 8.0, "min": 24.0, "max": 42.0},
|
||||
"tokens": {"mean": 2100, "stddev": 300, "min": 1800, "max": 2500}
|
||||
},
|
||||
"delta": {
|
||||
"pass_rate": "+0.50",
|
||||
"time_seconds": "+13.0",
|
||||
"tokens": "+1700"
|
||||
}
|
||||
},
|
||||
|
||||
"notes": [
|
||||
"Assertion 'Output is a PDF file' passes 100% in both configurations - may not differentiate skill value",
|
||||
"Eval 3 shows high variance (50% ± 40%) - may be flaky or model-dependent",
|
||||
"Without-skill runs consistently fail on table extraction expectations",
|
||||
"Skill adds 13s average execution time but improves pass rate by 50%"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `metadata`: Information about the benchmark run
|
||||
- `skill_name`: Name of the skill
|
||||
- `timestamp`: When the benchmark was run
|
||||
- `evals_run`: List of eval names or IDs
|
||||
- `runs_per_configuration`: Number of runs per config (e.g. 3)
|
||||
- `runs[]`: Individual run results
|
||||
- `eval_id`: Numeric eval identifier
|
||||
- `eval_name`: Human-readable eval name (used as section header in the viewer)
|
||||
- `configuration`: Must be `"with_skill"` or `"without_skill"` (the viewer uses this exact string for grouping and color coding)
|
||||
- `run_number`: Integer run number (1, 2, 3...)
|
||||
- `result`: Nested object with `pass_rate`, `passed`, `total`, `time_seconds`, `tokens`, `errors`
|
||||
- `run_summary`: Statistical aggregates per configuration
|
||||
- `with_skill` / `without_skill`: Each contains `pass_rate`, `time_seconds`, `tokens` objects with `mean` and `stddev` fields
|
||||
- `delta`: Difference strings like `"+0.50"`, `"+13.0"`, `"+1700"`
|
||||
- `notes`: Freeform observations from the analyzer
|
||||
|
||||
**Important:** The viewer reads these field names exactly. Using `config` instead of `configuration`, or putting `pass_rate` at the top level of a run instead of nested under `result`, will cause the viewer to show empty/zero values. Always reference this schema when generating benchmark.json manually.
|
||||
|
||||
---
|
||||
|
||||
## comparison.json
|
||||
|
||||
Output from blind comparator. Located at `<grading-dir>/comparison-N.json`.
|
||||
|
||||
```json
|
||||
{
|
||||
"winner": "A",
|
||||
"reasoning": "Output A provides a complete solution with proper formatting and all required fields. Output B is missing the date field and has formatting inconsistencies.",
|
||||
"rubric": {
|
||||
"A": {
|
||||
"content": {
|
||||
"correctness": 5,
|
||||
"completeness": 5,
|
||||
"accuracy": 4
|
||||
},
|
||||
"structure": {
|
||||
"organization": 4,
|
||||
"formatting": 5,
|
||||
"usability": 4
|
||||
},
|
||||
"content_score": 4.7,
|
||||
"structure_score": 4.3,
|
||||
"overall_score": 9.0
|
||||
},
|
||||
"B": {
|
||||
"content": {
|
||||
"correctness": 3,
|
||||
"completeness": 2,
|
||||
"accuracy": 3
|
||||
},
|
||||
"structure": {
|
||||
"organization": 3,
|
||||
"formatting": 2,
|
||||
"usability": 3
|
||||
},
|
||||
"content_score": 2.7,
|
||||
"structure_score": 2.7,
|
||||
"overall_score": 5.4
|
||||
}
|
||||
},
|
||||
"output_quality": {
|
||||
"A": {
|
||||
"score": 9,
|
||||
"strengths": ["Complete solution", "Well-formatted", "All fields present"],
|
||||
"weaknesses": ["Minor style inconsistency in header"]
|
||||
},
|
||||
"B": {
|
||||
"score": 5,
|
||||
"strengths": ["Readable output", "Correct basic structure"],
|
||||
"weaknesses": ["Missing date field", "Formatting inconsistencies", "Partial data extraction"]
|
||||
}
|
||||
},
|
||||
"expectation_results": {
|
||||
"A": {
|
||||
"passed": 4,
|
||||
"total": 5,
|
||||
"pass_rate": 0.80,
|
||||
"details": [
|
||||
{"text": "Output includes name", "passed": true}
|
||||
]
|
||||
},
|
||||
"B": {
|
||||
"passed": 3,
|
||||
"total": 5,
|
||||
"pass_rate": 0.60,
|
||||
"details": [
|
||||
{"text": "Output includes name", "passed": true}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## analysis.json
|
||||
|
||||
Output from post-hoc analyzer. Located at `<grading-dir>/analysis.json`.
|
||||
|
||||
```json
|
||||
{
|
||||
"comparison_summary": {
|
||||
"winner": "A",
|
||||
"winner_skill": "path/to/winner/skill",
|
||||
"loser_skill": "path/to/loser/skill",
|
||||
"comparator_reasoning": "Brief summary of why comparator chose winner"
|
||||
},
|
||||
"winner_strengths": [
|
||||
"Clear step-by-step instructions for handling multi-page documents",
|
||||
"Included validation script that caught formatting errors"
|
||||
],
|
||||
"loser_weaknesses": [
|
||||
"Vague instruction 'process the document appropriately' led to inconsistent behavior",
|
||||
"No script for validation, agent had to improvise"
|
||||
],
|
||||
"instruction_following": {
|
||||
"winner": {
|
||||
"score": 9,
|
||||
"issues": ["Minor: skipped optional logging step"]
|
||||
},
|
||||
"loser": {
|
||||
"score": 6,
|
||||
"issues": [
|
||||
"Did not use the skill's formatting template",
|
||||
"Invented own approach instead of following step 3"
|
||||
]
|
||||
}
|
||||
},
|
||||
"improvement_suggestions": [
|
||||
{
|
||||
"priority": "high",
|
||||
"category": "instructions",
|
||||
"suggestion": "Replace 'process the document appropriately' with explicit steps",
|
||||
"expected_impact": "Would eliminate ambiguity that caused inconsistent behavior"
|
||||
}
|
||||
],
|
||||
"transcript_insights": {
|
||||
"winner_execution_pattern": "Read skill -> Followed 5-step process -> Used validation script",
|
||||
"loser_execution_pattern": "Read skill -> Unclear on approach -> Tried 3 different methods"
|
||||
}
|
||||
}
|
||||
```
|
||||
0
.skills/skill-creator/scripts/__init__.py
Normal file
0
.skills/skill-creator/scripts/__init__.py
Normal file
401
.skills/skill-creator/scripts/aggregate_benchmark.py
Normal file
401
.skills/skill-creator/scripts/aggregate_benchmark.py
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Aggregate individual run results into benchmark summary statistics.
|
||||
|
||||
Reads grading.json files from run directories and produces:
|
||||
- run_summary with mean, stddev, min, max for each metric
|
||||
- delta between with_skill and without_skill configurations
|
||||
|
||||
Usage:
|
||||
python aggregate_benchmark.py <benchmark_dir>
|
||||
|
||||
Example:
|
||||
python aggregate_benchmark.py benchmarks/2026-01-15T10-30-00/
|
||||
|
||||
The script supports two directory layouts:
|
||||
|
||||
Workspace layout (from skill-creator iterations):
|
||||
<benchmark_dir>/
|
||||
└── eval-N/
|
||||
├── with_skill/
|
||||
│ ├── run-1/grading.json
|
||||
│ └── run-2/grading.json
|
||||
└── without_skill/
|
||||
├── run-1/grading.json
|
||||
└── run-2/grading.json
|
||||
|
||||
Legacy layout (with runs/ subdirectory):
|
||||
<benchmark_dir>/
|
||||
└── runs/
|
||||
└── eval-N/
|
||||
├── with_skill/
|
||||
│ └── run-1/grading.json
|
||||
└── without_skill/
|
||||
└── run-1/grading.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def calculate_stats(values: list[float]) -> dict:
|
||||
"""Calculate mean, stddev, min, max for a list of values."""
|
||||
if not values:
|
||||
return {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0}
|
||||
|
||||
n = len(values)
|
||||
mean = sum(values) / n
|
||||
|
||||
if n > 1:
|
||||
variance = sum((x - mean) ** 2 for x in values) / (n - 1)
|
||||
stddev = math.sqrt(variance)
|
||||
else:
|
||||
stddev = 0.0
|
||||
|
||||
return {
|
||||
"mean": round(mean, 4),
|
||||
"stddev": round(stddev, 4),
|
||||
"min": round(min(values), 4),
|
||||
"max": round(max(values), 4)
|
||||
}
|
||||
|
||||
|
||||
def load_run_results(benchmark_dir: Path) -> dict:
|
||||
"""
|
||||
Load all run results from a benchmark directory.
|
||||
|
||||
Returns dict keyed by config name (e.g. "with_skill"/"without_skill",
|
||||
or "new_skill"/"old_skill"), each containing a list of run results.
|
||||
"""
|
||||
# Support both layouts: eval dirs directly under benchmark_dir, or under runs/
|
||||
runs_dir = benchmark_dir / "runs"
|
||||
if runs_dir.exists():
|
||||
search_dir = runs_dir
|
||||
elif list(benchmark_dir.glob("eval-*")):
|
||||
search_dir = benchmark_dir
|
||||
else:
|
||||
print(f"No eval directories found in {benchmark_dir} or {benchmark_dir / 'runs'}")
|
||||
return {}
|
||||
|
||||
results: dict[str, list] = {}
|
||||
|
||||
for eval_idx, eval_dir in enumerate(sorted(search_dir.glob("eval-*"))):
|
||||
metadata_path = eval_dir / "eval_metadata.json"
|
||||
if metadata_path.exists():
|
||||
try:
|
||||
with open(metadata_path) as mf:
|
||||
eval_id = json.load(mf).get("eval_id", eval_idx)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
eval_id = eval_idx
|
||||
else:
|
||||
try:
|
||||
eval_id = int(eval_dir.name.split("-")[1])
|
||||
except ValueError:
|
||||
eval_id = eval_idx
|
||||
|
||||
# Discover config directories dynamically rather than hardcoding names
|
||||
for config_dir in sorted(eval_dir.iterdir()):
|
||||
if not config_dir.is_dir():
|
||||
continue
|
||||
# Skip non-config directories (inputs, outputs, etc.)
|
||||
if not list(config_dir.glob("run-*")):
|
||||
continue
|
||||
config = config_dir.name
|
||||
if config not in results:
|
||||
results[config] = []
|
||||
|
||||
for run_dir in sorted(config_dir.glob("run-*")):
|
||||
run_number = int(run_dir.name.split("-")[1])
|
||||
grading_file = run_dir / "grading.json"
|
||||
|
||||
if not grading_file.exists():
|
||||
print(f"Warning: grading.json not found in {run_dir}")
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(grading_file) as f:
|
||||
grading = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Warning: Invalid JSON in {grading_file}: {e}")
|
||||
continue
|
||||
|
||||
# Extract metrics
|
||||
result = {
|
||||
"eval_id": eval_id,
|
||||
"run_number": run_number,
|
||||
"pass_rate": grading.get("summary", {}).get("pass_rate", 0.0),
|
||||
"passed": grading.get("summary", {}).get("passed", 0),
|
||||
"failed": grading.get("summary", {}).get("failed", 0),
|
||||
"total": grading.get("summary", {}).get("total", 0),
|
||||
}
|
||||
|
||||
# Extract timing — check grading.json first, then sibling timing.json
|
||||
timing = grading.get("timing", {})
|
||||
result["time_seconds"] = timing.get("total_duration_seconds", 0.0)
|
||||
timing_file = run_dir / "timing.json"
|
||||
if result["time_seconds"] == 0.0 and timing_file.exists():
|
||||
try:
|
||||
with open(timing_file) as tf:
|
||||
timing_data = json.load(tf)
|
||||
result["time_seconds"] = timing_data.get("total_duration_seconds", 0.0)
|
||||
result["tokens"] = timing_data.get("total_tokens", 0)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# Extract metrics if available
|
||||
metrics = grading.get("execution_metrics", {})
|
||||
result["tool_calls"] = metrics.get("total_tool_calls", 0)
|
||||
if not result.get("tokens"):
|
||||
result["tokens"] = metrics.get("output_chars", 0)
|
||||
result["errors"] = metrics.get("errors_encountered", 0)
|
||||
|
||||
# Extract expectations — viewer requires fields: text, passed, evidence
|
||||
raw_expectations = grading.get("expectations", [])
|
||||
for exp in raw_expectations:
|
||||
if "text" not in exp or "passed" not in exp:
|
||||
print(f"Warning: expectation in {grading_file} missing required fields (text, passed, evidence): {exp}")
|
||||
result["expectations"] = raw_expectations
|
||||
|
||||
# Extract notes from user_notes_summary
|
||||
notes_summary = grading.get("user_notes_summary", {})
|
||||
notes = []
|
||||
notes.extend(notes_summary.get("uncertainties", []))
|
||||
notes.extend(notes_summary.get("needs_review", []))
|
||||
notes.extend(notes_summary.get("workarounds", []))
|
||||
result["notes"] = notes
|
||||
|
||||
results[config].append(result)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def aggregate_results(results: dict) -> dict:
|
||||
"""
|
||||
Aggregate run results into summary statistics.
|
||||
|
||||
Returns run_summary with stats for each configuration and delta.
|
||||
"""
|
||||
run_summary = {}
|
||||
configs = list(results.keys())
|
||||
|
||||
for config in configs:
|
||||
runs = results.get(config, [])
|
||||
|
||||
if not runs:
|
||||
run_summary[config] = {
|
||||
"pass_rate": {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0},
|
||||
"time_seconds": {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0},
|
||||
"tokens": {"mean": 0, "stddev": 0, "min": 0, "max": 0}
|
||||
}
|
||||
continue
|
||||
|
||||
pass_rates = [r["pass_rate"] for r in runs]
|
||||
times = [r["time_seconds"] for r in runs]
|
||||
tokens = [r.get("tokens", 0) for r in runs]
|
||||
|
||||
run_summary[config] = {
|
||||
"pass_rate": calculate_stats(pass_rates),
|
||||
"time_seconds": calculate_stats(times),
|
||||
"tokens": calculate_stats(tokens)
|
||||
}
|
||||
|
||||
# Calculate delta between the first two configs (if two exist)
|
||||
if len(configs) >= 2:
|
||||
primary = run_summary.get(configs[0], {})
|
||||
baseline = run_summary.get(configs[1], {})
|
||||
else:
|
||||
primary = run_summary.get(configs[0], {}) if configs else {}
|
||||
baseline = {}
|
||||
|
||||
delta_pass_rate = primary.get("pass_rate", {}).get("mean", 0) - baseline.get("pass_rate", {}).get("mean", 0)
|
||||
delta_time = primary.get("time_seconds", {}).get("mean", 0) - baseline.get("time_seconds", {}).get("mean", 0)
|
||||
delta_tokens = primary.get("tokens", {}).get("mean", 0) - baseline.get("tokens", {}).get("mean", 0)
|
||||
|
||||
run_summary["delta"] = {
|
||||
"pass_rate": f"{delta_pass_rate:+.2f}",
|
||||
"time_seconds": f"{delta_time:+.1f}",
|
||||
"tokens": f"{delta_tokens:+.0f}"
|
||||
}
|
||||
|
||||
return run_summary
|
||||
|
||||
|
||||
def generate_benchmark(benchmark_dir: Path, skill_name: str = "", skill_path: str = "") -> dict:
|
||||
"""
|
||||
Generate complete benchmark.json from run results.
|
||||
"""
|
||||
results = load_run_results(benchmark_dir)
|
||||
run_summary = aggregate_results(results)
|
||||
|
||||
# Build runs array for benchmark.json
|
||||
runs = []
|
||||
for config in results:
|
||||
for result in results[config]:
|
||||
runs.append({
|
||||
"eval_id": result["eval_id"],
|
||||
"configuration": config,
|
||||
"run_number": result["run_number"],
|
||||
"result": {
|
||||
"pass_rate": result["pass_rate"],
|
||||
"passed": result["passed"],
|
||||
"failed": result["failed"],
|
||||
"total": result["total"],
|
||||
"time_seconds": result["time_seconds"],
|
||||
"tokens": result.get("tokens", 0),
|
||||
"tool_calls": result.get("tool_calls", 0),
|
||||
"errors": result.get("errors", 0)
|
||||
},
|
||||
"expectations": result["expectations"],
|
||||
"notes": result["notes"]
|
||||
})
|
||||
|
||||
# Determine eval IDs from results
|
||||
eval_ids = sorted(set(
|
||||
r["eval_id"]
|
||||
for config in results.values()
|
||||
for r in config
|
||||
))
|
||||
|
||||
benchmark = {
|
||||
"metadata": {
|
||||
"skill_name": skill_name or "<skill-name>",
|
||||
"skill_path": skill_path or "<path/to/skill>",
|
||||
"executor_model": "<model-name>",
|
||||
"analyzer_model": "<model-name>",
|
||||
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"evals_run": eval_ids,
|
||||
"runs_per_configuration": 3
|
||||
},
|
||||
"runs": runs,
|
||||
"run_summary": run_summary,
|
||||
"notes": [] # To be filled by analyzer
|
||||
}
|
||||
|
||||
return benchmark
|
||||
|
||||
|
||||
def generate_markdown(benchmark: dict) -> str:
|
||||
"""Generate human-readable benchmark.md from benchmark data."""
|
||||
metadata = benchmark["metadata"]
|
||||
run_summary = benchmark["run_summary"]
|
||||
|
||||
# Determine config names (excluding "delta")
|
||||
configs = [k for k in run_summary if k != "delta"]
|
||||
config_a = configs[0] if len(configs) >= 1 else "config_a"
|
||||
config_b = configs[1] if len(configs) >= 2 else "config_b"
|
||||
label_a = config_a.replace("_", " ").title()
|
||||
label_b = config_b.replace("_", " ").title()
|
||||
|
||||
lines = [
|
||||
f"# Skill Benchmark: {metadata['skill_name']}",
|
||||
"",
|
||||
f"**Model**: {metadata['executor_model']}",
|
||||
f"**Date**: {metadata['timestamp']}",
|
||||
f"**Evals**: {', '.join(map(str, metadata['evals_run']))} ({metadata['runs_per_configuration']} runs each per configuration)",
|
||||
"",
|
||||
"## Summary",
|
||||
"",
|
||||
f"| Metric | {label_a} | {label_b} | Delta |",
|
||||
"|--------|------------|---------------|-------|",
|
||||
]
|
||||
|
||||
a_summary = run_summary.get(config_a, {})
|
||||
b_summary = run_summary.get(config_b, {})
|
||||
delta = run_summary.get("delta", {})
|
||||
|
||||
# Format pass rate
|
||||
a_pr = a_summary.get("pass_rate", {})
|
||||
b_pr = b_summary.get("pass_rate", {})
|
||||
lines.append(f"| Pass Rate | {a_pr.get('mean', 0)*100:.0f}% ± {a_pr.get('stddev', 0)*100:.0f}% | {b_pr.get('mean', 0)*100:.0f}% ± {b_pr.get('stddev', 0)*100:.0f}% | {delta.get('pass_rate', '—')} |")
|
||||
|
||||
# Format time
|
||||
a_time = a_summary.get("time_seconds", {})
|
||||
b_time = b_summary.get("time_seconds", {})
|
||||
lines.append(f"| Time | {a_time.get('mean', 0):.1f}s ± {a_time.get('stddev', 0):.1f}s | {b_time.get('mean', 0):.1f}s ± {b_time.get('stddev', 0):.1f}s | {delta.get('time_seconds', '—')}s |")
|
||||
|
||||
# Format tokens
|
||||
a_tokens = a_summary.get("tokens", {})
|
||||
b_tokens = b_summary.get("tokens", {})
|
||||
lines.append(f"| Tokens | {a_tokens.get('mean', 0):.0f} ± {a_tokens.get('stddev', 0):.0f} | {b_tokens.get('mean', 0):.0f} ± {b_tokens.get('stddev', 0):.0f} | {delta.get('tokens', '—')} |")
|
||||
|
||||
# Notes section
|
||||
if benchmark.get("notes"):
|
||||
lines.extend([
|
||||
"",
|
||||
"## Notes",
|
||||
""
|
||||
])
|
||||
for note in benchmark["notes"]:
|
||||
lines.append(f"- {note}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Aggregate benchmark run results into summary statistics"
|
||||
)
|
||||
parser.add_argument(
|
||||
"benchmark_dir",
|
||||
type=Path,
|
||||
help="Path to the benchmark directory"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skill-name",
|
||||
default="",
|
||||
help="Name of the skill being benchmarked"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skill-path",
|
||||
default="",
|
||||
help="Path to the skill being benchmarked"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", "-o",
|
||||
type=Path,
|
||||
help="Output path for benchmark.json (default: <benchmark_dir>/benchmark.json)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.benchmark_dir.exists():
|
||||
print(f"Directory not found: {args.benchmark_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
# Generate benchmark
|
||||
benchmark = generate_benchmark(args.benchmark_dir, args.skill_name, args.skill_path)
|
||||
|
||||
# Determine output paths
|
||||
output_json = args.output or (args.benchmark_dir / "benchmark.json")
|
||||
output_md = output_json.with_suffix(".md")
|
||||
|
||||
# Write benchmark.json
|
||||
with open(output_json, "w") as f:
|
||||
json.dump(benchmark, f, indent=2)
|
||||
print(f"Generated: {output_json}")
|
||||
|
||||
# Write benchmark.md
|
||||
markdown = generate_markdown(benchmark)
|
||||
with open(output_md, "w") as f:
|
||||
f.write(markdown)
|
||||
print(f"Generated: {output_md}")
|
||||
|
||||
# Print summary
|
||||
run_summary = benchmark["run_summary"]
|
||||
configs = [k for k in run_summary if k != "delta"]
|
||||
delta = run_summary.get("delta", {})
|
||||
|
||||
print(f"\nSummary:")
|
||||
for config in configs:
|
||||
pr = run_summary[config]["pass_rate"]["mean"]
|
||||
label = config.replace("_", " ").title()
|
||||
print(f" {label}: {pr*100:.1f}% pass rate")
|
||||
print(f" Delta: {delta.get('pass_rate', '—')}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
326
.skills/skill-creator/scripts/generate_report.py
Normal file
326
.skills/skill-creator/scripts/generate_report.py
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate an HTML report from run_loop.py output.
|
||||
|
||||
Takes the JSON output from run_loop.py and generates a visual HTML report
|
||||
showing each description attempt with check/x for each test case.
|
||||
Distinguishes between train and test queries.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def generate_html(data: dict, auto_refresh: bool = False, skill_name: str = "") -> str:
|
||||
"""Generate HTML report from loop output data. If auto_refresh is True, adds a meta refresh tag."""
|
||||
history = data.get("history", [])
|
||||
holdout = data.get("holdout", 0)
|
||||
title_prefix = html.escape(skill_name + " \u2014 ") if skill_name else ""
|
||||
|
||||
# Get all unique queries from train and test sets, with should_trigger info
|
||||
train_queries: list[dict] = []
|
||||
test_queries: list[dict] = []
|
||||
if history:
|
||||
for r in history[0].get("train_results", history[0].get("results", [])):
|
||||
train_queries.append({"query": r["query"], "should_trigger": r.get("should_trigger", True)})
|
||||
if history[0].get("test_results"):
|
||||
for r in history[0].get("test_results", []):
|
||||
test_queries.append({"query": r["query"], "should_trigger": r.get("should_trigger", True)})
|
||||
|
||||
refresh_tag = ' <meta http-equiv="refresh" content="5">\n' if auto_refresh else ""
|
||||
|
||||
html_parts = ["""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
""" + refresh_tag + """ <title>""" + title_prefix + """Skill Description Optimization</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@500;600&family=Lora:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Lora', Georgia, serif;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #faf9f5;
|
||||
color: #141413;
|
||||
}
|
||||
h1 { font-family: 'Poppins', sans-serif; color: #141413; }
|
||||
.explainer {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #e8e6dc;
|
||||
color: #b0aea5;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.summary {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #e8e6dc;
|
||||
}
|
||||
.summary p { margin: 5px 0; }
|
||||
.best { color: #788c5d; font-weight: bold; }
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border: 1px solid #e8e6dc;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
min-width: 100%;
|
||||
}
|
||||
th, td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border: 1px solid #e8e6dc;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
th {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background: #141413;
|
||||
color: #faf9f5;
|
||||
font-weight: 500;
|
||||
}
|
||||
th.test-col {
|
||||
background: #6a9bcc;
|
||||
}
|
||||
th.query-col { min-width: 200px; }
|
||||
td.description {
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
word-wrap: break-word;
|
||||
max-width: 400px;
|
||||
}
|
||||
td.result {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
min-width: 40px;
|
||||
}
|
||||
td.test-result {
|
||||
background: #f0f6fc;
|
||||
}
|
||||
.pass { color: #788c5d; }
|
||||
.fail { color: #c44; }
|
||||
.rate {
|
||||
font-size: 9px;
|
||||
color: #b0aea5;
|
||||
display: block;
|
||||
}
|
||||
tr:hover { background: #faf9f5; }
|
||||
.score {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
font-size: 11px;
|
||||
}
|
||||
.score-good { background: #eef2e8; color: #788c5d; }
|
||||
.score-ok { background: #fef3c7; color: #d97706; }
|
||||
.score-bad { background: #fceaea; color: #c44; }
|
||||
.train-label { color: #b0aea5; font-size: 10px; }
|
||||
.test-label { color: #6a9bcc; font-size: 10px; font-weight: bold; }
|
||||
.best-row { background: #f5f8f2; }
|
||||
th.positive-col { border-bottom: 3px solid #788c5d; }
|
||||
th.negative-col { border-bottom: 3px solid #c44; }
|
||||
th.test-col.positive-col { border-bottom: 3px solid #788c5d; }
|
||||
th.test-col.negative-col { border-bottom: 3px solid #c44; }
|
||||
.legend { font-family: 'Poppins', sans-serif; display: flex; gap: 20px; margin-bottom: 10px; font-size: 13px; align-items: center; }
|
||||
.legend-item { display: flex; align-items: center; gap: 6px; }
|
||||
.legend-swatch { width: 16px; height: 16px; border-radius: 3px; display: inline-block; }
|
||||
.swatch-positive { background: #141413; border-bottom: 3px solid #788c5d; }
|
||||
.swatch-negative { background: #141413; border-bottom: 3px solid #c44; }
|
||||
.swatch-test { background: #6a9bcc; }
|
||||
.swatch-train { background: #141413; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>""" + title_prefix + """Skill Description Optimization</h1>
|
||||
<div class="explainer">
|
||||
<strong>Optimizing your skill's description.</strong> This page updates automatically as Claude tests different versions of your skill's description. Each row is an iteration — a new description attempt. The columns show test queries: green checkmarks mean the skill triggered correctly (or correctly didn't trigger), red crosses mean it got it wrong. The "Train" score shows performance on queries used to improve the description; the "Test" score shows performance on held-out queries the optimizer hasn't seen. When it's done, Claude will apply the best-performing description to your skill.
|
||||
</div>
|
||||
"""]
|
||||
|
||||
# Summary section
|
||||
best_test_score = data.get('best_test_score')
|
||||
best_train_score = data.get('best_train_score')
|
||||
html_parts.append(f"""
|
||||
<div class="summary">
|
||||
<p><strong>Original:</strong> {html.escape(data.get('original_description', 'N/A'))}</p>
|
||||
<p class="best"><strong>Best:</strong> {html.escape(data.get('best_description', 'N/A'))}</p>
|
||||
<p><strong>Best Score:</strong> {data.get('best_score', 'N/A')} {'(test)' if best_test_score else '(train)'}</p>
|
||||
<p><strong>Iterations:</strong> {data.get('iterations_run', 0)} | <strong>Train:</strong> {data.get('train_size', '?')} | <strong>Test:</strong> {data.get('test_size', '?')}</p>
|
||||
</div>
|
||||
""")
|
||||
|
||||
# Legend
|
||||
html_parts.append("""
|
||||
<div class="legend">
|
||||
<span style="font-weight:600">Query columns:</span>
|
||||
<span class="legend-item"><span class="legend-swatch swatch-positive"></span> Should trigger</span>
|
||||
<span class="legend-item"><span class="legend-swatch swatch-negative"></span> Should NOT trigger</span>
|
||||
<span class="legend-item"><span class="legend-swatch swatch-train"></span> Train</span>
|
||||
<span class="legend-item"><span class="legend-swatch swatch-test"></span> Test</span>
|
||||
</div>
|
||||
""")
|
||||
|
||||
# Table header
|
||||
html_parts.append("""
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Iter</th>
|
||||
<th>Train</th>
|
||||
<th>Test</th>
|
||||
<th class="query-col">Description</th>
|
||||
""")
|
||||
|
||||
# Add column headers for train queries
|
||||
for qinfo in train_queries:
|
||||
polarity = "positive-col" if qinfo["should_trigger"] else "negative-col"
|
||||
html_parts.append(f' <th class="{polarity}">{html.escape(qinfo["query"])}</th>\n')
|
||||
|
||||
# Add column headers for test queries (different color)
|
||||
for qinfo in test_queries:
|
||||
polarity = "positive-col" if qinfo["should_trigger"] else "negative-col"
|
||||
html_parts.append(f' <th class="test-col {polarity}">{html.escape(qinfo["query"])}</th>\n')
|
||||
|
||||
html_parts.append(""" </tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
""")
|
||||
|
||||
# Find best iteration for highlighting
|
||||
if test_queries:
|
||||
best_iter = max(history, key=lambda h: h.get("test_passed") or 0).get("iteration")
|
||||
else:
|
||||
best_iter = max(history, key=lambda h: h.get("train_passed", h.get("passed", 0))).get("iteration")
|
||||
|
||||
# Add rows for each iteration
|
||||
for h in history:
|
||||
iteration = h.get("iteration", "?")
|
||||
train_passed = h.get("train_passed", h.get("passed", 0))
|
||||
train_total = h.get("train_total", h.get("total", 0))
|
||||
test_passed = h.get("test_passed")
|
||||
test_total = h.get("test_total")
|
||||
description = h.get("description", "")
|
||||
train_results = h.get("train_results", h.get("results", []))
|
||||
test_results = h.get("test_results", [])
|
||||
|
||||
# Create lookups for results by query
|
||||
train_by_query = {r["query"]: r for r in train_results}
|
||||
test_by_query = {r["query"]: r for r in test_results} if test_results else {}
|
||||
|
||||
# Compute aggregate correct/total runs across all retries
|
||||
def aggregate_runs(results: list[dict]) -> tuple[int, int]:
|
||||
correct = 0
|
||||
total = 0
|
||||
for r in results:
|
||||
runs = r.get("runs", 0)
|
||||
triggers = r.get("triggers", 0)
|
||||
total += runs
|
||||
if r.get("should_trigger", True):
|
||||
correct += triggers
|
||||
else:
|
||||
correct += runs - triggers
|
||||
return correct, total
|
||||
|
||||
train_correct, train_runs = aggregate_runs(train_results)
|
||||
test_correct, test_runs = aggregate_runs(test_results)
|
||||
|
||||
# Determine score classes
|
||||
def score_class(correct: int, total: int) -> str:
|
||||
if total > 0:
|
||||
ratio = correct / total
|
||||
if ratio >= 0.8:
|
||||
return "score-good"
|
||||
elif ratio >= 0.5:
|
||||
return "score-ok"
|
||||
return "score-bad"
|
||||
|
||||
train_class = score_class(train_correct, train_runs)
|
||||
test_class = score_class(test_correct, test_runs)
|
||||
|
||||
row_class = "best-row" if iteration == best_iter else ""
|
||||
|
||||
html_parts.append(f""" <tr class="{row_class}">
|
||||
<td>{iteration}</td>
|
||||
<td><span class="score {train_class}">{train_correct}/{train_runs}</span></td>
|
||||
<td><span class="score {test_class}">{test_correct}/{test_runs}</span></td>
|
||||
<td class="description">{html.escape(description)}</td>
|
||||
""")
|
||||
|
||||
# Add result for each train query
|
||||
for qinfo in train_queries:
|
||||
r = train_by_query.get(qinfo["query"], {})
|
||||
did_pass = r.get("pass", False)
|
||||
triggers = r.get("triggers", 0)
|
||||
runs = r.get("runs", 0)
|
||||
|
||||
icon = "✓" if did_pass else "✗"
|
||||
css_class = "pass" if did_pass else "fail"
|
||||
|
||||
html_parts.append(f' <td class="result {css_class}">{icon}<span class="rate">{triggers}/{runs}</span></td>\n')
|
||||
|
||||
# Add result for each test query (with different background)
|
||||
for qinfo in test_queries:
|
||||
r = test_by_query.get(qinfo["query"], {})
|
||||
did_pass = r.get("pass", False)
|
||||
triggers = r.get("triggers", 0)
|
||||
runs = r.get("runs", 0)
|
||||
|
||||
icon = "✓" if did_pass else "✗"
|
||||
css_class = "pass" if did_pass else "fail"
|
||||
|
||||
html_parts.append(f' <td class="result test-result {css_class}">{icon}<span class="rate">{triggers}/{runs}</span></td>\n')
|
||||
|
||||
html_parts.append(" </tr>\n")
|
||||
|
||||
html_parts.append(""" </tbody>
|
||||
</table>
|
||||
</div>
|
||||
""")
|
||||
|
||||
html_parts.append("""
|
||||
</body>
|
||||
</html>
|
||||
""")
|
||||
|
||||
return "".join(html_parts)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate HTML report from run_loop output")
|
||||
parser.add_argument("input", help="Path to JSON output from run_loop.py (or - for stdin)")
|
||||
parser.add_argument("-o", "--output", default=None, help="Output HTML file (default: stdout)")
|
||||
parser.add_argument("--skill-name", default="", help="Skill name to include in the report title")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.input == "-":
|
||||
data = json.load(sys.stdin)
|
||||
else:
|
||||
data = json.loads(Path(args.input).read_text())
|
||||
|
||||
html_output = generate_html(data, skill_name=args.skill_name)
|
||||
|
||||
if args.output:
|
||||
Path(args.output).write_text(html_output)
|
||||
print(f"Report written to {args.output}", file=sys.stderr)
|
||||
else:
|
||||
print(html_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
248
.skills/skill-creator/scripts/improve_description.py
Normal file
248
.skills/skill-creator/scripts/improve_description.py
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Improve a skill description based on eval results.
|
||||
|
||||
Takes eval results (from run_eval.py) and generates an improved description
|
||||
using Claude with extended thinking.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import anthropic
|
||||
|
||||
from scripts.utils import parse_skill_md
|
||||
|
||||
|
||||
def improve_description(
|
||||
client: anthropic.Anthropic,
|
||||
skill_name: str,
|
||||
skill_content: str,
|
||||
current_description: str,
|
||||
eval_results: dict,
|
||||
history: list[dict],
|
||||
model: str,
|
||||
test_results: dict | None = None,
|
||||
log_dir: Path | None = None,
|
||||
iteration: int | None = None,
|
||||
) -> str:
|
||||
"""Call Claude to improve the description based on eval results."""
|
||||
failed_triggers = [
|
||||
r for r in eval_results["results"]
|
||||
if r["should_trigger"] and not r["pass"]
|
||||
]
|
||||
false_triggers = [
|
||||
r for r in eval_results["results"]
|
||||
if not r["should_trigger"] and not r["pass"]
|
||||
]
|
||||
|
||||
# Build scores summary
|
||||
train_score = f"{eval_results['summary']['passed']}/{eval_results['summary']['total']}"
|
||||
if test_results:
|
||||
test_score = f"{test_results['summary']['passed']}/{test_results['summary']['total']}"
|
||||
scores_summary = f"Train: {train_score}, Test: {test_score}"
|
||||
else:
|
||||
scores_summary = f"Train: {train_score}"
|
||||
|
||||
prompt = f"""You are optimizing a skill description for a Claude Code skill called "{skill_name}". A "skill" is sort of like a prompt, but with progressive disclosure -- there's a title and description that Claude sees when deciding whether to use the skill, and then if it does use the skill, it reads the .md file which has lots more details and potentially links to other resources in the skill folder like helper files and scripts and additional documentation or examples.
|
||||
|
||||
The description appears in Claude's "available_skills" list. When a user sends a query, Claude decides whether to invoke the skill based solely on the title and on this description. Your goal is to write a description that triggers for relevant queries, and doesn't trigger for irrelevant ones.
|
||||
|
||||
Here's the current description:
|
||||
<current_description>
|
||||
"{current_description}"
|
||||
</current_description>
|
||||
|
||||
Current scores ({scores_summary}):
|
||||
<scores_summary>
|
||||
"""
|
||||
if failed_triggers:
|
||||
prompt += "FAILED TO TRIGGER (should have triggered but didn't):\n"
|
||||
for r in failed_triggers:
|
||||
prompt += f' - "{r["query"]}" (triggered {r["triggers"]}/{r["runs"]} times)\n'
|
||||
prompt += "\n"
|
||||
|
||||
if false_triggers:
|
||||
prompt += "FALSE TRIGGERS (triggered but shouldn't have):\n"
|
||||
for r in false_triggers:
|
||||
prompt += f' - "{r["query"]}" (triggered {r["triggers"]}/{r["runs"]} times)\n'
|
||||
prompt += "\n"
|
||||
|
||||
if history:
|
||||
prompt += "PREVIOUS ATTEMPTS (do NOT repeat these — try something structurally different):\n\n"
|
||||
for h in history:
|
||||
train_s = f"{h.get('train_passed', h.get('passed', 0))}/{h.get('train_total', h.get('total', 0))}"
|
||||
test_s = f"{h.get('test_passed', '?')}/{h.get('test_total', '?')}" if h.get('test_passed') is not None else None
|
||||
score_str = f"train={train_s}" + (f", test={test_s}" if test_s else "")
|
||||
prompt += f'<attempt {score_str}>\n'
|
||||
prompt += f'Description: "{h["description"]}"\n'
|
||||
if "results" in h:
|
||||
prompt += "Train results:\n"
|
||||
for r in h["results"]:
|
||||
status = "PASS" if r["pass"] else "FAIL"
|
||||
prompt += f' [{status}] "{r["query"][:80]}" (triggered {r["triggers"]}/{r["runs"]})\n'
|
||||
if h.get("note"):
|
||||
prompt += f'Note: {h["note"]}\n'
|
||||
prompt += "</attempt>\n\n"
|
||||
|
||||
prompt += f"""</scores_summary>
|
||||
|
||||
Skill content (for context on what the skill does):
|
||||
<skill_content>
|
||||
{skill_content}
|
||||
</skill_content>
|
||||
|
||||
Based on the failures, write a new and improved description that is more likely to trigger correctly. When I say "based on the failures", it's a bit of a tricky line to walk because we don't want to overfit to the specific cases you're seeing. So what I DON'T want you to do is produce an ever-expanding list of specific queries that this skill should or shouldn't trigger for. Instead, try to generalize from the failures to broader categories of user intent and situations where this skill would be useful or not useful. The reason for this is twofold:
|
||||
|
||||
1. Avoid overfitting
|
||||
2. The list might get loooong and it's injected into ALL queries and there might be a lot of skills, so we don't want to blow too much space on any given description.
|
||||
|
||||
Concretely, your description should not be more than about 100-200 words, even if that comes at the cost of accuracy.
|
||||
|
||||
Here are some tips that we've found to work well in writing these descriptions:
|
||||
- The skill should be phrased in the imperative -- "Use this skill for" rather than "this skill does"
|
||||
- The skill description should focus on the user's intent, what they are trying to achieve, vs. the implementation details of how the skill works.
|
||||
- The description competes with other skills for Claude's attention — make it distinctive and immediately recognizable.
|
||||
- If you're getting lots of failures after repeated attempts, change things up. Try different sentence structures or wordings.
|
||||
|
||||
I'd encourage you to be creative and mix up the style in different iterations since you'll have multiple opportunities to try different approaches and we'll just grab the highest-scoring one at the end.
|
||||
|
||||
Please respond with only the new description text in <new_description> tags, nothing else."""
|
||||
|
||||
response = client.messages.create(
|
||||
model=model,
|
||||
max_tokens=16000,
|
||||
thinking={
|
||||
"type": "enabled",
|
||||
"budget_tokens": 10000,
|
||||
},
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
|
||||
# Extract thinking and text from response
|
||||
thinking_text = ""
|
||||
text = ""
|
||||
for block in response.content:
|
||||
if block.type == "thinking":
|
||||
thinking_text = block.thinking
|
||||
elif block.type == "text":
|
||||
text = block.text
|
||||
|
||||
# Parse out the <new_description> tags
|
||||
match = re.search(r"<new_description>(.*?)</new_description>", text, re.DOTALL)
|
||||
description = match.group(1).strip().strip('"') if match else text.strip().strip('"')
|
||||
|
||||
# Log the transcript
|
||||
transcript: dict = {
|
||||
"iteration": iteration,
|
||||
"prompt": prompt,
|
||||
"thinking": thinking_text,
|
||||
"response": text,
|
||||
"parsed_description": description,
|
||||
"char_count": len(description),
|
||||
"over_limit": len(description) > 1024,
|
||||
}
|
||||
|
||||
# If over 1024 chars, ask the model to shorten it
|
||||
if len(description) > 1024:
|
||||
shorten_prompt = f"Your description is {len(description)} characters, which exceeds the hard 1024 character limit. Please rewrite it to be under 1024 characters while preserving the most important trigger words and intent coverage. Respond with only the new description in <new_description> tags."
|
||||
shorten_response = client.messages.create(
|
||||
model=model,
|
||||
max_tokens=16000,
|
||||
thinking={
|
||||
"type": "enabled",
|
||||
"budget_tokens": 10000,
|
||||
},
|
||||
messages=[
|
||||
{"role": "user", "content": prompt},
|
||||
{"role": "assistant", "content": text},
|
||||
{"role": "user", "content": shorten_prompt},
|
||||
],
|
||||
)
|
||||
|
||||
shorten_thinking = ""
|
||||
shorten_text = ""
|
||||
for block in shorten_response.content:
|
||||
if block.type == "thinking":
|
||||
shorten_thinking = block.thinking
|
||||
elif block.type == "text":
|
||||
shorten_text = block.text
|
||||
|
||||
match = re.search(r"<new_description>(.*?)</new_description>", shorten_text, re.DOTALL)
|
||||
shortened = match.group(1).strip().strip('"') if match else shorten_text.strip().strip('"')
|
||||
|
||||
transcript["rewrite_prompt"] = shorten_prompt
|
||||
transcript["rewrite_thinking"] = shorten_thinking
|
||||
transcript["rewrite_response"] = shorten_text
|
||||
transcript["rewrite_description"] = shortened
|
||||
transcript["rewrite_char_count"] = len(shortened)
|
||||
description = shortened
|
||||
|
||||
transcript["final_description"] = description
|
||||
|
||||
if log_dir:
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_file = log_dir / f"improve_iter_{iteration or 'unknown'}.json"
|
||||
log_file.write_text(json.dumps(transcript, indent=2))
|
||||
|
||||
return description
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Improve a skill description based on eval results")
|
||||
parser.add_argument("--eval-results", required=True, help="Path to eval results JSON (from run_eval.py)")
|
||||
parser.add_argument("--skill-path", required=True, help="Path to skill directory")
|
||||
parser.add_argument("--history", default=None, help="Path to history JSON (previous attempts)")
|
||||
parser.add_argument("--model", required=True, help="Model for improvement")
|
||||
parser.add_argument("--verbose", action="store_true", help="Print thinking to stderr")
|
||||
args = parser.parse_args()
|
||||
|
||||
skill_path = Path(args.skill_path)
|
||||
if not (skill_path / "SKILL.md").exists():
|
||||
print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
eval_results = json.loads(Path(args.eval_results).read_text())
|
||||
history = []
|
||||
if args.history:
|
||||
history = json.loads(Path(args.history).read_text())
|
||||
|
||||
name, _, content = parse_skill_md(skill_path)
|
||||
current_description = eval_results["description"]
|
||||
|
||||
if args.verbose:
|
||||
print(f"Current: {current_description}", file=sys.stderr)
|
||||
print(f"Score: {eval_results['summary']['passed']}/{eval_results['summary']['total']}", file=sys.stderr)
|
||||
|
||||
client = anthropic.Anthropic()
|
||||
new_description = improve_description(
|
||||
client=client,
|
||||
skill_name=name,
|
||||
skill_content=content,
|
||||
current_description=current_description,
|
||||
eval_results=eval_results,
|
||||
history=history,
|
||||
model=args.model,
|
||||
)
|
||||
|
||||
if args.verbose:
|
||||
print(f"Improved: {new_description}", file=sys.stderr)
|
||||
|
||||
# Output as JSON with both the new description and updated history
|
||||
output = {
|
||||
"description": new_description,
|
||||
"history": history + [{
|
||||
"description": current_description,
|
||||
"passed": eval_results["summary"]["passed"],
|
||||
"failed": eval_results["summary"]["failed"],
|
||||
"total": eval_results["summary"]["total"],
|
||||
"results": eval_results["results"],
|
||||
}],
|
||||
}
|
||||
print(json.dumps(output, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
136
.skills/skill-creator/scripts/package_skill.py
Normal file
136
.skills/skill-creator/scripts/package_skill.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Skill Packager - Creates a distributable .skill file of a skill folder
|
||||
|
||||
Usage:
|
||||
python utils/package_skill.py <path/to/skill-folder> [output-directory]
|
||||
|
||||
Example:
|
||||
python utils/package_skill.py skills/public/my-skill
|
||||
python utils/package_skill.py skills/public/my-skill ./dist
|
||||
"""
|
||||
|
||||
import fnmatch
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from scripts.quick_validate import validate_skill
|
||||
|
||||
# Patterns to exclude when packaging skills.
|
||||
EXCLUDE_DIRS = {"__pycache__", "node_modules"}
|
||||
EXCLUDE_GLOBS = {"*.pyc"}
|
||||
EXCLUDE_FILES = {".DS_Store"}
|
||||
# Directories excluded only at the skill root (not when nested deeper).
|
||||
ROOT_EXCLUDE_DIRS = {"evals"}
|
||||
|
||||
|
||||
def should_exclude(rel_path: Path) -> bool:
|
||||
"""Check if a path should be excluded from packaging."""
|
||||
parts = rel_path.parts
|
||||
if any(part in EXCLUDE_DIRS for part in parts):
|
||||
return True
|
||||
# rel_path is relative to skill_path.parent, so parts[0] is the skill
|
||||
# folder name and parts[1] (if present) is the first subdir.
|
||||
if len(parts) > 1 and parts[1] in ROOT_EXCLUDE_DIRS:
|
||||
return True
|
||||
name = rel_path.name
|
||||
if name in EXCLUDE_FILES:
|
||||
return True
|
||||
return any(fnmatch.fnmatch(name, pat) for pat in EXCLUDE_GLOBS)
|
||||
|
||||
|
||||
def package_skill(skill_path, output_dir=None):
|
||||
"""
|
||||
Package a skill folder into a .skill file.
|
||||
|
||||
Args:
|
||||
skill_path: Path to the skill folder
|
||||
output_dir: Optional output directory for the .skill file (defaults to current directory)
|
||||
|
||||
Returns:
|
||||
Path to the created .skill file, or None if error
|
||||
"""
|
||||
skill_path = Path(skill_path).resolve()
|
||||
|
||||
# Validate skill folder exists
|
||||
if not skill_path.exists():
|
||||
print(f"❌ Error: Skill folder not found: {skill_path}")
|
||||
return None
|
||||
|
||||
if not skill_path.is_dir():
|
||||
print(f"❌ Error: Path is not a directory: {skill_path}")
|
||||
return None
|
||||
|
||||
# Validate SKILL.md exists
|
||||
skill_md = skill_path / "SKILL.md"
|
||||
if not skill_md.exists():
|
||||
print(f"❌ Error: SKILL.md not found in {skill_path}")
|
||||
return None
|
||||
|
||||
# Run validation before packaging
|
||||
print("🔍 Validating skill...")
|
||||
valid, message = validate_skill(skill_path)
|
||||
if not valid:
|
||||
print(f"❌ Validation failed: {message}")
|
||||
print(" Please fix the validation errors before packaging.")
|
||||
return None
|
||||
print(f"✅ {message}\n")
|
||||
|
||||
# Determine output location
|
||||
skill_name = skill_path.name
|
||||
if output_dir:
|
||||
output_path = Path(output_dir).resolve()
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
output_path = Path.cwd()
|
||||
|
||||
skill_filename = output_path / f"{skill_name}.skill"
|
||||
|
||||
# Create the .skill file (zip format)
|
||||
try:
|
||||
with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
# Walk through the skill directory, excluding build artifacts
|
||||
for file_path in skill_path.rglob('*'):
|
||||
if not file_path.is_file():
|
||||
continue
|
||||
arcname = file_path.relative_to(skill_path.parent)
|
||||
if should_exclude(arcname):
|
||||
print(f" Skipped: {arcname}")
|
||||
continue
|
||||
zipf.write(file_path, arcname)
|
||||
print(f" Added: {arcname}")
|
||||
|
||||
print(f"\n✅ Successfully packaged skill to: {skill_filename}")
|
||||
return skill_filename
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating .skill file: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python utils/package_skill.py <path/to/skill-folder> [output-directory]")
|
||||
print("\nExample:")
|
||||
print(" python utils/package_skill.py skills/public/my-skill")
|
||||
print(" python utils/package_skill.py skills/public/my-skill ./dist")
|
||||
sys.exit(1)
|
||||
|
||||
skill_path = sys.argv[1]
|
||||
output_dir = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
print(f"📦 Packaging skill: {skill_path}")
|
||||
if output_dir:
|
||||
print(f" Output directory: {output_dir}")
|
||||
print()
|
||||
|
||||
result = package_skill(skill_path, output_dir)
|
||||
|
||||
if result:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
103
.skills/skill-creator/scripts/quick_validate.py
Normal file
103
.skills/skill-creator/scripts/quick_validate.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick validation script for skills - minimal version
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
|
||||
def validate_skill(skill_path):
|
||||
"""Basic validation of a skill"""
|
||||
skill_path = Path(skill_path)
|
||||
|
||||
# Check SKILL.md exists
|
||||
skill_md = skill_path / 'SKILL.md'
|
||||
if not skill_md.exists():
|
||||
return False, "SKILL.md not found"
|
||||
|
||||
# Read and validate frontmatter
|
||||
content = skill_md.read_text()
|
||||
if not content.startswith('---'):
|
||||
return False, "No YAML frontmatter found"
|
||||
|
||||
# Extract frontmatter
|
||||
match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
|
||||
if not match:
|
||||
return False, "Invalid frontmatter format"
|
||||
|
||||
frontmatter_text = match.group(1)
|
||||
|
||||
# Parse YAML frontmatter
|
||||
try:
|
||||
frontmatter = yaml.safe_load(frontmatter_text)
|
||||
if not isinstance(frontmatter, dict):
|
||||
return False, "Frontmatter must be a YAML dictionary"
|
||||
except yaml.YAMLError as e:
|
||||
return False, f"Invalid YAML in frontmatter: {e}"
|
||||
|
||||
# Define allowed properties
|
||||
ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata', 'compatibility'}
|
||||
|
||||
# Check for unexpected properties (excluding nested keys under metadata)
|
||||
unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES
|
||||
if unexpected_keys:
|
||||
return False, (
|
||||
f"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. "
|
||||
f"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}"
|
||||
)
|
||||
|
||||
# Check required fields
|
||||
if 'name' not in frontmatter:
|
||||
return False, "Missing 'name' in frontmatter"
|
||||
if 'description' not in frontmatter:
|
||||
return False, "Missing 'description' in frontmatter"
|
||||
|
||||
# Extract name for validation
|
||||
name = frontmatter.get('name', '')
|
||||
if not isinstance(name, str):
|
||||
return False, f"Name must be a string, got {type(name).__name__}"
|
||||
name = name.strip()
|
||||
if name:
|
||||
# Check naming convention (kebab-case: lowercase with hyphens)
|
||||
if not re.match(r'^[a-z0-9-]+$', name):
|
||||
return False, f"Name '{name}' should be kebab-case (lowercase letters, digits, and hyphens only)"
|
||||
if name.startswith('-') or name.endswith('-') or '--' in name:
|
||||
return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens"
|
||||
# Check name length (max 64 characters per spec)
|
||||
if len(name) > 64:
|
||||
return False, f"Name is too long ({len(name)} characters). Maximum is 64 characters."
|
||||
|
||||
# Extract and validate description
|
||||
description = frontmatter.get('description', '')
|
||||
if not isinstance(description, str):
|
||||
return False, f"Description must be a string, got {type(description).__name__}"
|
||||
description = description.strip()
|
||||
if description:
|
||||
# Check for angle brackets
|
||||
if '<' in description or '>' in description:
|
||||
return False, "Description cannot contain angle brackets (< or >)"
|
||||
# Check description length (max 1024 characters per spec)
|
||||
if len(description) > 1024:
|
||||
return False, f"Description is too long ({len(description)} characters). Maximum is 1024 characters."
|
||||
|
||||
# Validate compatibility field if present (optional)
|
||||
compatibility = frontmatter.get('compatibility', '')
|
||||
if compatibility:
|
||||
if not isinstance(compatibility, str):
|
||||
return False, f"Compatibility must be a string, got {type(compatibility).__name__}"
|
||||
if len(compatibility) > 500:
|
||||
return False, f"Compatibility is too long ({len(compatibility)} characters). Maximum is 500 characters."
|
||||
|
||||
return True, "Skill is valid!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python quick_validate.py <skill_directory>")
|
||||
sys.exit(1)
|
||||
|
||||
valid, message = validate_skill(sys.argv[1])
|
||||
print(message)
|
||||
sys.exit(0 if valid else 1)
|
||||
310
.skills/skill-creator/scripts/run_eval.py
Normal file
310
.skills/skill-creator/scripts/run_eval.py
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Run trigger evaluation for a skill description.
|
||||
|
||||
Tests whether a skill's description causes Claude to trigger (read the skill)
|
||||
for a set of queries. Outputs results as JSON.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.utils import parse_skill_md
|
||||
|
||||
|
||||
def find_project_root() -> Path:
|
||||
"""Find the project root by walking up from cwd looking for .claude/.
|
||||
|
||||
Mimics how Claude Code discovers its project root, so the command file
|
||||
we create ends up where claude -p will look for it.
|
||||
"""
|
||||
current = Path.cwd()
|
||||
for parent in [current, *current.parents]:
|
||||
if (parent / ".claude").is_dir():
|
||||
return parent
|
||||
return current
|
||||
|
||||
|
||||
def run_single_query(
|
||||
query: str,
|
||||
skill_name: str,
|
||||
skill_description: str,
|
||||
timeout: int,
|
||||
project_root: str,
|
||||
model: str | None = None,
|
||||
) -> bool:
|
||||
"""Run a single query and return whether the skill was triggered.
|
||||
|
||||
Creates a command file in .claude/commands/ so it appears in Claude's
|
||||
available_skills list, then runs `claude -p` with the raw query.
|
||||
Uses --include-partial-messages to detect triggering early from
|
||||
stream events (content_block_start) rather than waiting for the
|
||||
full assistant message, which only arrives after tool execution.
|
||||
"""
|
||||
unique_id = uuid.uuid4().hex[:8]
|
||||
clean_name = f"{skill_name}-skill-{unique_id}"
|
||||
project_commands_dir = Path(project_root) / ".claude" / "commands"
|
||||
command_file = project_commands_dir / f"{clean_name}.md"
|
||||
|
||||
try:
|
||||
project_commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
# Use YAML block scalar to avoid breaking on quotes in description
|
||||
indented_desc = "\n ".join(skill_description.split("\n"))
|
||||
command_content = (
|
||||
f"---\n"
|
||||
f"description: |\n"
|
||||
f" {indented_desc}\n"
|
||||
f"---\n\n"
|
||||
f"# {skill_name}\n\n"
|
||||
f"This skill handles: {skill_description}\n"
|
||||
)
|
||||
command_file.write_text(command_content)
|
||||
|
||||
cmd = [
|
||||
"claude",
|
||||
"-p", query,
|
||||
"--output-format", "stream-json",
|
||||
"--verbose",
|
||||
"--include-partial-messages",
|
||||
]
|
||||
if model:
|
||||
cmd.extend(["--model", model])
|
||||
|
||||
# Remove CLAUDECODE env var to allow nesting claude -p inside a
|
||||
# Claude Code session. The guard is for interactive terminal conflicts;
|
||||
# programmatic subprocess usage is safe.
|
||||
env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"}
|
||||
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
cwd=project_root,
|
||||
env=env,
|
||||
)
|
||||
|
||||
triggered = False
|
||||
start_time = time.time()
|
||||
buffer = ""
|
||||
# Track state for stream event detection
|
||||
pending_tool_name = None
|
||||
accumulated_json = ""
|
||||
|
||||
try:
|
||||
while time.time() - start_time < timeout:
|
||||
if process.poll() is not None:
|
||||
remaining = process.stdout.read()
|
||||
if remaining:
|
||||
buffer += remaining.decode("utf-8", errors="replace")
|
||||
break
|
||||
|
||||
ready, _, _ = select.select([process.stdout], [], [], 1.0)
|
||||
if not ready:
|
||||
continue
|
||||
|
||||
chunk = os.read(process.stdout.fileno(), 8192)
|
||||
if not chunk:
|
||||
break
|
||||
buffer += chunk.decode("utf-8", errors="replace")
|
||||
|
||||
while "\n" in buffer:
|
||||
line, buffer = buffer.split("\n", 1)
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
event = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# Early detection via stream events
|
||||
if event.get("type") == "stream_event":
|
||||
se = event.get("event", {})
|
||||
se_type = se.get("type", "")
|
||||
|
||||
if se_type == "content_block_start":
|
||||
cb = se.get("content_block", {})
|
||||
if cb.get("type") == "tool_use":
|
||||
tool_name = cb.get("name", "")
|
||||
if tool_name in ("Skill", "Read"):
|
||||
pending_tool_name = tool_name
|
||||
accumulated_json = ""
|
||||
else:
|
||||
return False
|
||||
|
||||
elif se_type == "content_block_delta" and pending_tool_name:
|
||||
delta = se.get("delta", {})
|
||||
if delta.get("type") == "input_json_delta":
|
||||
accumulated_json += delta.get("partial_json", "")
|
||||
if clean_name in accumulated_json:
|
||||
return True
|
||||
|
||||
elif se_type in ("content_block_stop", "message_stop"):
|
||||
if pending_tool_name:
|
||||
return clean_name in accumulated_json
|
||||
if se_type == "message_stop":
|
||||
return False
|
||||
|
||||
# Fallback: full assistant message
|
||||
elif event.get("type") == "assistant":
|
||||
message = event.get("message", {})
|
||||
for content_item in message.get("content", []):
|
||||
if content_item.get("type") != "tool_use":
|
||||
continue
|
||||
tool_name = content_item.get("name", "")
|
||||
tool_input = content_item.get("input", {})
|
||||
if tool_name == "Skill" and clean_name in tool_input.get("skill", ""):
|
||||
triggered = True
|
||||
elif tool_name == "Read" and clean_name in tool_input.get("file_path", ""):
|
||||
triggered = True
|
||||
return triggered
|
||||
|
||||
elif event.get("type") == "result":
|
||||
return triggered
|
||||
finally:
|
||||
# Clean up process on any exit path (return, exception, timeout)
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.wait()
|
||||
|
||||
return triggered
|
||||
finally:
|
||||
if command_file.exists():
|
||||
command_file.unlink()
|
||||
|
||||
|
||||
def run_eval(
|
||||
eval_set: list[dict],
|
||||
skill_name: str,
|
||||
description: str,
|
||||
num_workers: int,
|
||||
timeout: int,
|
||||
project_root: Path,
|
||||
runs_per_query: int = 1,
|
||||
trigger_threshold: float = 0.5,
|
||||
model: str | None = None,
|
||||
) -> dict:
|
||||
"""Run the full eval set and return results."""
|
||||
results = []
|
||||
|
||||
with ProcessPoolExecutor(max_workers=num_workers) as executor:
|
||||
future_to_info = {}
|
||||
for item in eval_set:
|
||||
for run_idx in range(runs_per_query):
|
||||
future = executor.submit(
|
||||
run_single_query,
|
||||
item["query"],
|
||||
skill_name,
|
||||
description,
|
||||
timeout,
|
||||
str(project_root),
|
||||
model,
|
||||
)
|
||||
future_to_info[future] = (item, run_idx)
|
||||
|
||||
query_triggers: dict[str, list[bool]] = {}
|
||||
query_items: dict[str, dict] = {}
|
||||
for future in as_completed(future_to_info):
|
||||
item, _ = future_to_info[future]
|
||||
query = item["query"]
|
||||
query_items[query] = item
|
||||
if query not in query_triggers:
|
||||
query_triggers[query] = []
|
||||
try:
|
||||
query_triggers[query].append(future.result())
|
||||
except Exception as e:
|
||||
print(f"Warning: query failed: {e}", file=sys.stderr)
|
||||
query_triggers[query].append(False)
|
||||
|
||||
for query, triggers in query_triggers.items():
|
||||
item = query_items[query]
|
||||
trigger_rate = sum(triggers) / len(triggers)
|
||||
should_trigger = item["should_trigger"]
|
||||
if should_trigger:
|
||||
did_pass = trigger_rate >= trigger_threshold
|
||||
else:
|
||||
did_pass = trigger_rate < trigger_threshold
|
||||
results.append({
|
||||
"query": query,
|
||||
"should_trigger": should_trigger,
|
||||
"trigger_rate": trigger_rate,
|
||||
"triggers": sum(triggers),
|
||||
"runs": len(triggers),
|
||||
"pass": did_pass,
|
||||
})
|
||||
|
||||
passed = sum(1 for r in results if r["pass"])
|
||||
total = len(results)
|
||||
|
||||
return {
|
||||
"skill_name": skill_name,
|
||||
"description": description,
|
||||
"results": results,
|
||||
"summary": {
|
||||
"total": total,
|
||||
"passed": passed,
|
||||
"failed": total - passed,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Run trigger evaluation for a skill description")
|
||||
parser.add_argument("--eval-set", required=True, help="Path to eval set JSON file")
|
||||
parser.add_argument("--skill-path", required=True, help="Path to skill directory")
|
||||
parser.add_argument("--description", default=None, help="Override description to test")
|
||||
parser.add_argument("--num-workers", type=int, default=10, help="Number of parallel workers")
|
||||
parser.add_argument("--timeout", type=int, default=30, help="Timeout per query in seconds")
|
||||
parser.add_argument("--runs-per-query", type=int, default=3, help="Number of runs per query")
|
||||
parser.add_argument("--trigger-threshold", type=float, default=0.5, help="Trigger rate threshold")
|
||||
parser.add_argument("--model", default=None, help="Model to use for claude -p (default: user's configured model)")
|
||||
parser.add_argument("--verbose", action="store_true", help="Print progress to stderr")
|
||||
args = parser.parse_args()
|
||||
|
||||
eval_set = json.loads(Path(args.eval_set).read_text())
|
||||
skill_path = Path(args.skill_path)
|
||||
|
||||
if not (skill_path / "SKILL.md").exists():
|
||||
print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
name, original_description, content = parse_skill_md(skill_path)
|
||||
description = args.description or original_description
|
||||
project_root = find_project_root()
|
||||
|
||||
if args.verbose:
|
||||
print(f"Evaluating: {description}", file=sys.stderr)
|
||||
|
||||
output = run_eval(
|
||||
eval_set=eval_set,
|
||||
skill_name=name,
|
||||
description=description,
|
||||
num_workers=args.num_workers,
|
||||
timeout=args.timeout,
|
||||
project_root=project_root,
|
||||
runs_per_query=args.runs_per_query,
|
||||
trigger_threshold=args.trigger_threshold,
|
||||
model=args.model,
|
||||
)
|
||||
|
||||
if args.verbose:
|
||||
summary = output["summary"]
|
||||
print(f"Results: {summary['passed']}/{summary['total']} passed", file=sys.stderr)
|
||||
for r in output["results"]:
|
||||
status = "PASS" if r["pass"] else "FAIL"
|
||||
rate_str = f"{r['triggers']}/{r['runs']}"
|
||||
print(f" [{status}] rate={rate_str} expected={r['should_trigger']}: {r['query'][:70]}", file=sys.stderr)
|
||||
|
||||
print(json.dumps(output, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
332
.skills/skill-creator/scripts/run_loop.py
Normal file
332
.skills/skill-creator/scripts/run_loop.py
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Run the eval + improve loop until all pass or max iterations reached.
|
||||
|
||||
Combines run_eval.py and improve_description.py in a loop, tracking history
|
||||
and returning the best description found. Supports train/test split to prevent
|
||||
overfitting.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
|
||||
import anthropic
|
||||
|
||||
from scripts.generate_report import generate_html
|
||||
from scripts.improve_description import improve_description
|
||||
from scripts.run_eval import find_project_root, run_eval
|
||||
from scripts.utils import parse_skill_md
|
||||
|
||||
|
||||
def split_eval_set(eval_set: list[dict], holdout: float, seed: int = 42) -> tuple[list[dict], list[dict]]:
|
||||
"""Split eval set into train and test sets, stratified by should_trigger."""
|
||||
random.seed(seed)
|
||||
|
||||
# Separate by should_trigger
|
||||
trigger = [e for e in eval_set if e["should_trigger"]]
|
||||
no_trigger = [e for e in eval_set if not e["should_trigger"]]
|
||||
|
||||
# Shuffle each group
|
||||
random.shuffle(trigger)
|
||||
random.shuffle(no_trigger)
|
||||
|
||||
# Calculate split points
|
||||
n_trigger_test = max(1, int(len(trigger) * holdout))
|
||||
n_no_trigger_test = max(1, int(len(no_trigger) * holdout))
|
||||
|
||||
# Split
|
||||
test_set = trigger[:n_trigger_test] + no_trigger[:n_no_trigger_test]
|
||||
train_set = trigger[n_trigger_test:] + no_trigger[n_no_trigger_test:]
|
||||
|
||||
return train_set, test_set
|
||||
|
||||
|
||||
def run_loop(
|
||||
eval_set: list[dict],
|
||||
skill_path: Path,
|
||||
description_override: str | None,
|
||||
num_workers: int,
|
||||
timeout: int,
|
||||
max_iterations: int,
|
||||
runs_per_query: int,
|
||||
trigger_threshold: float,
|
||||
holdout: float,
|
||||
model: str,
|
||||
verbose: bool,
|
||||
live_report_path: Path | None = None,
|
||||
log_dir: Path | None = None,
|
||||
) -> dict:
|
||||
"""Run the eval + improvement loop."""
|
||||
project_root = find_project_root()
|
||||
name, original_description, content = parse_skill_md(skill_path)
|
||||
current_description = description_override or original_description
|
||||
|
||||
# Split into train/test if holdout > 0
|
||||
if holdout > 0:
|
||||
train_set, test_set = split_eval_set(eval_set, holdout)
|
||||
if verbose:
|
||||
print(f"Split: {len(train_set)} train, {len(test_set)} test (holdout={holdout})", file=sys.stderr)
|
||||
else:
|
||||
train_set = eval_set
|
||||
test_set = []
|
||||
|
||||
client = anthropic.Anthropic()
|
||||
history = []
|
||||
exit_reason = "unknown"
|
||||
|
||||
for iteration in range(1, max_iterations + 1):
|
||||
if verbose:
|
||||
print(f"\n{'='*60}", file=sys.stderr)
|
||||
print(f"Iteration {iteration}/{max_iterations}", file=sys.stderr)
|
||||
print(f"Description: {current_description}", file=sys.stderr)
|
||||
print(f"{'='*60}", file=sys.stderr)
|
||||
|
||||
# Evaluate train + test together in one batch for parallelism
|
||||
all_queries = train_set + test_set
|
||||
t0 = time.time()
|
||||
all_results = run_eval(
|
||||
eval_set=all_queries,
|
||||
skill_name=name,
|
||||
description=current_description,
|
||||
num_workers=num_workers,
|
||||
timeout=timeout,
|
||||
project_root=project_root,
|
||||
runs_per_query=runs_per_query,
|
||||
trigger_threshold=trigger_threshold,
|
||||
model=model,
|
||||
)
|
||||
eval_elapsed = time.time() - t0
|
||||
|
||||
# Split results back into train/test by matching queries
|
||||
train_queries_set = {q["query"] for q in train_set}
|
||||
train_result_list = [r for r in all_results["results"] if r["query"] in train_queries_set]
|
||||
test_result_list = [r for r in all_results["results"] if r["query"] not in train_queries_set]
|
||||
|
||||
train_passed = sum(1 for r in train_result_list if r["pass"])
|
||||
train_total = len(train_result_list)
|
||||
train_summary = {"passed": train_passed, "failed": train_total - train_passed, "total": train_total}
|
||||
train_results = {"results": train_result_list, "summary": train_summary}
|
||||
|
||||
if test_set:
|
||||
test_passed = sum(1 for r in test_result_list if r["pass"])
|
||||
test_total = len(test_result_list)
|
||||
test_summary = {"passed": test_passed, "failed": test_total - test_passed, "total": test_total}
|
||||
test_results = {"results": test_result_list, "summary": test_summary}
|
||||
else:
|
||||
test_results = None
|
||||
test_summary = None
|
||||
|
||||
history.append({
|
||||
"iteration": iteration,
|
||||
"description": current_description,
|
||||
"train_passed": train_summary["passed"],
|
||||
"train_failed": train_summary["failed"],
|
||||
"train_total": train_summary["total"],
|
||||
"train_results": train_results["results"],
|
||||
"test_passed": test_summary["passed"] if test_summary else None,
|
||||
"test_failed": test_summary["failed"] if test_summary else None,
|
||||
"test_total": test_summary["total"] if test_summary else None,
|
||||
"test_results": test_results["results"] if test_results else None,
|
||||
# For backward compat with report generator
|
||||
"passed": train_summary["passed"],
|
||||
"failed": train_summary["failed"],
|
||||
"total": train_summary["total"],
|
||||
"results": train_results["results"],
|
||||
})
|
||||
|
||||
# Write live report if path provided
|
||||
if live_report_path:
|
||||
partial_output = {
|
||||
"original_description": original_description,
|
||||
"best_description": current_description,
|
||||
"best_score": "in progress",
|
||||
"iterations_run": len(history),
|
||||
"holdout": holdout,
|
||||
"train_size": len(train_set),
|
||||
"test_size": len(test_set),
|
||||
"history": history,
|
||||
}
|
||||
live_report_path.write_text(generate_html(partial_output, auto_refresh=True, skill_name=name))
|
||||
|
||||
if verbose:
|
||||
def print_eval_stats(label, results, elapsed):
|
||||
pos = [r for r in results if r["should_trigger"]]
|
||||
neg = [r for r in results if not r["should_trigger"]]
|
||||
tp = sum(r["triggers"] for r in pos)
|
||||
pos_runs = sum(r["runs"] for r in pos)
|
||||
fn = pos_runs - tp
|
||||
fp = sum(r["triggers"] for r in neg)
|
||||
neg_runs = sum(r["runs"] for r in neg)
|
||||
tn = neg_runs - fp
|
||||
total = tp + tn + fp + fn
|
||||
precision = tp / (tp + fp) if (tp + fp) > 0 else 1.0
|
||||
recall = tp / (tp + fn) if (tp + fn) > 0 else 1.0
|
||||
accuracy = (tp + tn) / total if total > 0 else 0.0
|
||||
print(f"{label}: {tp+tn}/{total} correct, precision={precision:.0%} recall={recall:.0%} accuracy={accuracy:.0%} ({elapsed:.1f}s)", file=sys.stderr)
|
||||
for r in results:
|
||||
status = "PASS" if r["pass"] else "FAIL"
|
||||
rate_str = f"{r['triggers']}/{r['runs']}"
|
||||
print(f" [{status}] rate={rate_str} expected={r['should_trigger']}: {r['query'][:60]}", file=sys.stderr)
|
||||
|
||||
print_eval_stats("Train", train_results["results"], eval_elapsed)
|
||||
if test_summary:
|
||||
print_eval_stats("Test ", test_results["results"], 0)
|
||||
|
||||
if train_summary["failed"] == 0:
|
||||
exit_reason = f"all_passed (iteration {iteration})"
|
||||
if verbose:
|
||||
print(f"\nAll train queries passed on iteration {iteration}!", file=sys.stderr)
|
||||
break
|
||||
|
||||
if iteration == max_iterations:
|
||||
exit_reason = f"max_iterations ({max_iterations})"
|
||||
if verbose:
|
||||
print(f"\nMax iterations reached ({max_iterations}).", file=sys.stderr)
|
||||
break
|
||||
|
||||
# Improve the description based on train results
|
||||
if verbose:
|
||||
print(f"\nImproving description...", file=sys.stderr)
|
||||
|
||||
t0 = time.time()
|
||||
# Strip test scores from history so improvement model can't see them
|
||||
blinded_history = [
|
||||
{k: v for k, v in h.items() if not k.startswith("test_")}
|
||||
for h in history
|
||||
]
|
||||
new_description = improve_description(
|
||||
client=client,
|
||||
skill_name=name,
|
||||
skill_content=content,
|
||||
current_description=current_description,
|
||||
eval_results=train_results,
|
||||
history=blinded_history,
|
||||
model=model,
|
||||
log_dir=log_dir,
|
||||
iteration=iteration,
|
||||
)
|
||||
improve_elapsed = time.time() - t0
|
||||
|
||||
if verbose:
|
||||
print(f"Proposed ({improve_elapsed:.1f}s): {new_description}", file=sys.stderr)
|
||||
|
||||
current_description = new_description
|
||||
|
||||
# Find the best iteration by TEST score (or train if no test set)
|
||||
if test_set:
|
||||
best = max(history, key=lambda h: h["test_passed"] or 0)
|
||||
best_score = f"{best['test_passed']}/{best['test_total']}"
|
||||
else:
|
||||
best = max(history, key=lambda h: h["train_passed"])
|
||||
best_score = f"{best['train_passed']}/{best['train_total']}"
|
||||
|
||||
if verbose:
|
||||
print(f"\nExit reason: {exit_reason}", file=sys.stderr)
|
||||
print(f"Best score: {best_score} (iteration {best['iteration']})", file=sys.stderr)
|
||||
|
||||
return {
|
||||
"exit_reason": exit_reason,
|
||||
"original_description": original_description,
|
||||
"best_description": best["description"],
|
||||
"best_score": best_score,
|
||||
"best_train_score": f"{best['train_passed']}/{best['train_total']}",
|
||||
"best_test_score": f"{best['test_passed']}/{best['test_total']}" if test_set else None,
|
||||
"final_description": current_description,
|
||||
"iterations_run": len(history),
|
||||
"holdout": holdout,
|
||||
"train_size": len(train_set),
|
||||
"test_size": len(test_set),
|
||||
"history": history,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Run eval + improve loop")
|
||||
parser.add_argument("--eval-set", required=True, help="Path to eval set JSON file")
|
||||
parser.add_argument("--skill-path", required=True, help="Path to skill directory")
|
||||
parser.add_argument("--description", default=None, help="Override starting description")
|
||||
parser.add_argument("--num-workers", type=int, default=10, help="Number of parallel workers")
|
||||
parser.add_argument("--timeout", type=int, default=30, help="Timeout per query in seconds")
|
||||
parser.add_argument("--max-iterations", type=int, default=5, help="Max improvement iterations")
|
||||
parser.add_argument("--runs-per-query", type=int, default=3, help="Number of runs per query")
|
||||
parser.add_argument("--trigger-threshold", type=float, default=0.5, help="Trigger rate threshold")
|
||||
parser.add_argument("--holdout", type=float, default=0.4, help="Fraction of eval set to hold out for testing (0 to disable)")
|
||||
parser.add_argument("--model", required=True, help="Model for improvement")
|
||||
parser.add_argument("--verbose", action="store_true", help="Print progress to stderr")
|
||||
parser.add_argument("--report", default="auto", help="Generate HTML report at this path (default: 'auto' for temp file, 'none' to disable)")
|
||||
parser.add_argument("--results-dir", default=None, help="Save all outputs (results.json, report.html, log.txt) to a timestamped subdirectory here")
|
||||
args = parser.parse_args()
|
||||
|
||||
eval_set = json.loads(Path(args.eval_set).read_text())
|
||||
skill_path = Path(args.skill_path)
|
||||
|
||||
if not (skill_path / "SKILL.md").exists():
|
||||
print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
name, _, _ = parse_skill_md(skill_path)
|
||||
|
||||
# Set up live report path
|
||||
if args.report != "none":
|
||||
if args.report == "auto":
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
live_report_path = Path(tempfile.gettempdir()) / f"skill_description_report_{skill_path.name}_{timestamp}.html"
|
||||
else:
|
||||
live_report_path = Path(args.report)
|
||||
# Open the report immediately so the user can watch
|
||||
live_report_path.write_text("<html><body><h1>Starting optimization loop...</h1><meta http-equiv='refresh' content='5'></body></html>")
|
||||
webbrowser.open(str(live_report_path))
|
||||
else:
|
||||
live_report_path = None
|
||||
|
||||
# Determine output directory (create before run_loop so logs can be written)
|
||||
if args.results_dir:
|
||||
timestamp = time.strftime("%Y-%m-%d_%H%M%S")
|
||||
results_dir = Path(args.results_dir) / timestamp
|
||||
results_dir.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
results_dir = None
|
||||
|
||||
log_dir = results_dir / "logs" if results_dir else None
|
||||
|
||||
output = run_loop(
|
||||
eval_set=eval_set,
|
||||
skill_path=skill_path,
|
||||
description_override=args.description,
|
||||
num_workers=args.num_workers,
|
||||
timeout=args.timeout,
|
||||
max_iterations=args.max_iterations,
|
||||
runs_per_query=args.runs_per_query,
|
||||
trigger_threshold=args.trigger_threshold,
|
||||
holdout=args.holdout,
|
||||
model=args.model,
|
||||
verbose=args.verbose,
|
||||
live_report_path=live_report_path,
|
||||
log_dir=log_dir,
|
||||
)
|
||||
|
||||
# Save JSON output
|
||||
json_output = json.dumps(output, indent=2)
|
||||
print(json_output)
|
||||
if results_dir:
|
||||
(results_dir / "results.json").write_text(json_output)
|
||||
|
||||
# Write final HTML report (without auto-refresh)
|
||||
if live_report_path:
|
||||
live_report_path.write_text(generate_html(output, auto_refresh=False, skill_name=name))
|
||||
print(f"\nReport: {live_report_path}", file=sys.stderr)
|
||||
|
||||
if results_dir and live_report_path:
|
||||
(results_dir / "report.html").write_text(generate_html(output, auto_refresh=False, skill_name=name))
|
||||
|
||||
if results_dir:
|
||||
print(f"Results saved to: {results_dir}", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
47
.skills/skill-creator/scripts/utils.py
Normal file
47
.skills/skill-creator/scripts/utils.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
"""Shared utilities for skill-creator scripts."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
|
||||
def parse_skill_md(skill_path: Path) -> tuple[str, str, str]:
|
||||
"""Parse a SKILL.md file, returning (name, description, full_content)."""
|
||||
content = (skill_path / "SKILL.md").read_text()
|
||||
lines = content.split("\n")
|
||||
|
||||
if lines[0].strip() != "---":
|
||||
raise ValueError("SKILL.md missing frontmatter (no opening ---)")
|
||||
|
||||
end_idx = None
|
||||
for i, line in enumerate(lines[1:], start=1):
|
||||
if line.strip() == "---":
|
||||
end_idx = i
|
||||
break
|
||||
|
||||
if end_idx is None:
|
||||
raise ValueError("SKILL.md missing frontmatter (no closing ---)")
|
||||
|
||||
name = ""
|
||||
description = ""
|
||||
frontmatter_lines = lines[1:end_idx]
|
||||
i = 0
|
||||
while i < len(frontmatter_lines):
|
||||
line = frontmatter_lines[i]
|
||||
if line.startswith("name:"):
|
||||
name = line[len("name:"):].strip().strip('"').strip("'")
|
||||
elif line.startswith("description:"):
|
||||
value = line[len("description:"):].strip()
|
||||
# Handle YAML multiline indicators (>, |, >-, |-)
|
||||
if value in (">", "|", ">-", "|-"):
|
||||
continuation_lines: list[str] = []
|
||||
i += 1
|
||||
while i < len(frontmatter_lines) and (frontmatter_lines[i].startswith(" ") or frontmatter_lines[i].startswith("\t")):
|
||||
continuation_lines.append(frontmatter_lines[i].strip())
|
||||
i += 1
|
||||
description = " ".join(continuation_lines)
|
||||
continue
|
||||
else:
|
||||
description = value.strip('"').strip("'")
|
||||
i += 1
|
||||
|
||||
return name, description, content
|
||||
97
.skills/writing-skills/SKILL.md
Normal file
97
.skills/writing-skills/SKILL.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
---
|
||||
name: writing-skills
|
||||
description: "Use when creating, updating, or improving agent skills."
|
||||
---
|
||||
|
||||
# Writing Skills (Excellence)
|
||||
|
||||
Dispatcher for skill creation excellence. Use the decision tree below to find the right template and standards.
|
||||
|
||||
## Quick Decision Tree
|
||||
|
||||
### What do you need to do?
|
||||
|
||||
1. **Create a NEW skill:**
|
||||
- Is it simple (single file, <200 lines)? -> [Tier 1 Architecture](references/tier-1-simple/README.md)
|
||||
- Is it complex (multi-concept, 200-1000 lines)? -> [Tier 2 Architecture](references/tier-2-expanded/README.md)
|
||||
- Is it a massive platform (10+ products, AWS, Convex)? -> [Tier 3 Architecture](references/tier-3-platform/README.md)
|
||||
|
||||
2. **Improve an EXISTING skill:**
|
||||
- Fix "it's too long" -> [Modularize (Tier 3)](references/templates/tier-3-platform.md)
|
||||
- Fix "AI ignores rules" -> [Anti-Rationalization](references/anti-rationalization/README.md)
|
||||
- Fix "users can't find it" -> [CSO (Search Optimization)](references/cso/README.md)
|
||||
|
||||
3. **Verify Compliance:**
|
||||
- Check metadata/naming -> [Standards](references/standards/README.md)
|
||||
- Add tests -> [Testing Guide](references/testing/README.md)
|
||||
|
||||
## Component Index
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| **[CSO](references/cso/README.md)** | "SEO for LLMs". How to write descriptions that trigger. |
|
||||
| **[Standards](references/standards/README.md)** | File naming, YAML frontmatter, directory structure. |
|
||||
| **[Anti-Rationalization](references/anti-rationalization/README.md)**| How to write rules that agents won't ignore. |
|
||||
| **[Testing](references/testing/README.md)** | How to ensure your skill actually works. |
|
||||
|
||||
## Templates
|
||||
|
||||
- [Technique Skill](references/templates/technique.md) (How-to)
|
||||
- [Reference Skill](references/templates/reference.md) (Docs)
|
||||
- [Discipline Skill](references/templates/discipline.md) (Rules)
|
||||
- [Pattern Skill](references/templates/pattern.md) (Design Patterns)
|
||||
|
||||
## When to Use
|
||||
- Creating a NEW skill from scratch
|
||||
- Improving an EXISTING skill that agents ignore
|
||||
- Debugging why a skill isn't being triggered
|
||||
- Standardizing skills across a team
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Identify goal** -> Use decision tree above
|
||||
2. **Select template** -> From `references/templates/`
|
||||
3. **Apply CSO** -> Optimize description for discovery
|
||||
4. **Add anti-rationalization** -> For discipline skills
|
||||
5. **Test** -> RED-GREEN-REFACTOR cycle
|
||||
|
||||
## Quick Example
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: my-technique
|
||||
description: Use when [specific symptom occurs].
|
||||
metadata:
|
||||
category: technique
|
||||
triggers: error-text, symptom, tool-name
|
||||
---
|
||||
|
||||
# My Technique
|
||||
|
||||
## When to Use
|
||||
- [Symptom A]
|
||||
- [Error message]
|
||||
```
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| Description summarizes workflow | Use "Use when..." triggers only |
|
||||
| No `metadata.triggers` | Add 3+ keywords |
|
||||
| Generic name ("helper") | Use gerund (`creating-skills`) |
|
||||
| Long monolithic SKILL.md | Split into `references/` |
|
||||
|
||||
See [gotchas.md](gotchas.md) for more.
|
||||
|
||||
## Pre-Deploy Checklist
|
||||
|
||||
Before deploying any skill:
|
||||
|
||||
- [ ] `name` field matches directory name exactly
|
||||
- [ ] `SKILL.md` filename is ALL CAPS
|
||||
- [ ] Description starts with "Use when..."
|
||||
- [ ] `metadata.triggers` has 3+ keywords
|
||||
- [ ] Total lines < 500 (use `references/` for more)
|
||||
- [ ] No `@` force-loading in cross-references
|
||||
- [ ] Tested with real scenarios
|
||||
236
.skills/writing-skills/examples.md
Normal file
236
.skills/writing-skills/examples.md
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
# Skill Templates & Examples
|
||||
|
||||
Complete, copy-paste templates for each skill type.
|
||||
|
||||
---
|
||||
|
||||
## Template: Technique Skill
|
||||
|
||||
For how-to guides that teach a specific method.
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: technique-name
|
||||
description: >-
|
||||
Use when [specific symptom].
|
||||
metadata:
|
||||
category: technique
|
||||
triggers: error-text, symptom, tool-name
|
||||
---
|
||||
|
||||
# Technique Name
|
||||
|
||||
## Overview
|
||||
|
||||
[1-2 sentence core principle]
|
||||
|
||||
## When to Use
|
||||
|
||||
- [Symptom A]
|
||||
- [Symptom B]
|
||||
- [Error message text]
|
||||
|
||||
**NOT for:**
|
||||
- [When to avoid]
|
||||
|
||||
## The Problem
|
||||
|
||||
\`\`\`javascript
|
||||
// Bad example
|
||||
function badCode() {
|
||||
// problematic pattern
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## The Solution
|
||||
|
||||
\`\`\`javascript
|
||||
// Good example
|
||||
function goodCode() {
|
||||
// improved pattern
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Step-by-Step
|
||||
|
||||
1. [First step]
|
||||
2. [Second step]
|
||||
3. [Final step]
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Scenario | Approach |
|
||||
|----------|----------|
|
||||
| Case A | Solution A |
|
||||
| Case B | Solution B |
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
**Mistake 1:** [Description]
|
||||
- Wrong: \`bad code\`
|
||||
- Right: \`good code\`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template: Reference Skill
|
||||
|
||||
For documentation, APIs, and lookup tables.
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: reference-name
|
||||
description: >-
|
||||
Use when working with [domain].
|
||||
metadata:
|
||||
category: reference
|
||||
triggers: tool, api, specific-terms
|
||||
---
|
||||
|
||||
# Reference Name
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| \`cmd1\` | Does X |
|
||||
| \`cmd2\` | Does Y |
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Pattern A:**
|
||||
\`\`\`bash
|
||||
example command
|
||||
\`\`\`
|
||||
|
||||
**Pattern B:**
|
||||
\`\`\`bash
|
||||
another example
|
||||
\`\`\`
|
||||
|
||||
## Detailed Docs
|
||||
|
||||
For more options, run \`--help\` or see:
|
||||
- patterns.md
|
||||
- [examples.md](examples.md)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template: Discipline Skill
|
||||
|
||||
For rules that agents must follow. Requires anti-rationalization techniques.
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: discipline-name
|
||||
description: >-
|
||||
Use when [BEFORE violation].
|
||||
metadata:
|
||||
category: discipline
|
||||
triggers: new feature, code change, implementation
|
||||
---
|
||||
|
||||
# Rule Name
|
||||
|
||||
## Iron Law
|
||||
|
||||
**[SINGLE SENTENCE ABSOLUTE RULE]**
|
||||
|
||||
Violating the letter IS violating the spirit.
|
||||
|
||||
## The Rule
|
||||
|
||||
1. ALWAYS [step 1]
|
||||
2. NEVER [step 2]
|
||||
3. [Step 3]
|
||||
|
||||
## Violations
|
||||
|
||||
[Action before rule]? **Delete it. Start over.**
|
||||
|
||||
**No exceptions:**
|
||||
- Don't keep it as "reference"
|
||||
- Don't "adapt" it
|
||||
- Delete means delete
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "Too simple" | Simple code breaks. Rule takes 30 seconds. |
|
||||
| "I'll do it after" | After = never. Do it now. |
|
||||
| "Spirit not ritual" | The ritual IS the spirit. |
|
||||
|
||||
## Red Flags - STOP
|
||||
|
||||
- [Flag 1]
|
||||
- [Flag 2]
|
||||
- "This is different because..."
|
||||
|
||||
**All mean:** Delete. Start over.
|
||||
|
||||
## Valid Exceptions
|
||||
|
||||
- [Exception 1]
|
||||
- [Exception 2]
|
||||
|
||||
**Everything else:** Follow the rule.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template: Pattern Skill
|
||||
|
||||
For mental models and design patterns.
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: pattern-name
|
||||
description: >-
|
||||
Use when [recognizable symptom].
|
||||
metadata:
|
||||
category: pattern
|
||||
triggers: complexity, hard-to-follow, nested
|
||||
---
|
||||
|
||||
# Pattern Name
|
||||
|
||||
## The Pattern
|
||||
|
||||
[1-2 sentence core idea]
|
||||
|
||||
## Recognition Signs
|
||||
|
||||
- [Sign that pattern applies]
|
||||
- [Another sign]
|
||||
- [Code smell]
|
||||
|
||||
## Before
|
||||
|
||||
\`\`\`typescript
|
||||
// Complex/problematic
|
||||
function before() {
|
||||
// nested, confusing
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## After
|
||||
|
||||
\`\`\`typescript
|
||||
// Clean/improved
|
||||
function after() {
|
||||
// flat, clear
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- [Over-engineering case]
|
||||
- [Simple case that doesn't need it]
|
||||
|
||||
## Impact
|
||||
|
||||
**Before:** [Problem metric]
|
||||
**After:** [Improved metric]
|
||||
```
|
||||
175
.skills/writing-skills/gotchas.md
Normal file
175
.skills/writing-skills/gotchas.md
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
---
|
||||
description: Common pitfalls and tribal knowledge for skill creation.
|
||||
metadata:
|
||||
tags: [gotchas, troubleshooting, mistakes]
|
||||
---
|
||||
|
||||
# Skill Writing Gotchas
|
||||
|
||||
Tribal knowledge to avoid common mistakes.
|
||||
|
||||
## YAML Frontmatter
|
||||
|
||||
### Invalid Syntax
|
||||
|
||||
```yaml
|
||||
# BAD: Mixed list and map
|
||||
metadata:
|
||||
references:
|
||||
triggers: a, b, c
|
||||
- item1
|
||||
- item2
|
||||
|
||||
# GOOD: Consistent structure
|
||||
metadata:
|
||||
triggers: a, b, c
|
||||
references:
|
||||
- item1
|
||||
- item2
|
||||
```
|
||||
|
||||
### Multiline Description
|
||||
|
||||
```yaml
|
||||
# BAD: Line breaks create parsing errors
|
||||
description: Use when creating skills.
|
||||
Also for updating.
|
||||
|
||||
# GOOD: Use YAML multiline syntax
|
||||
description: >-
|
||||
Use when creating or updating skills.
|
||||
Triggers: new skill, update skill
|
||||
```
|
||||
|
||||
## Naming
|
||||
|
||||
### Directory Must Match `name` Field
|
||||
|
||||
```
|
||||
# BAD
|
||||
directory: my-skill/
|
||||
name: mySkill # Mismatch!
|
||||
|
||||
# GOOD
|
||||
directory: my-skill/
|
||||
name: my-skill # Exact match
|
||||
```
|
||||
|
||||
### SKILL.md Must Be ALL CAPS
|
||||
|
||||
```
|
||||
# BAD
|
||||
skill.md
|
||||
Skill.md
|
||||
|
||||
# GOOD
|
||||
SKILL.md
|
||||
```
|
||||
|
||||
## Discovery
|
||||
|
||||
### Description = Triggers, NOT Workflow
|
||||
|
||||
```yaml
|
||||
# BAD: Agent reads this and skips the full skill
|
||||
description: Analyzes code, finds bugs, suggests fixes
|
||||
|
||||
# GOOD: Agent reads full skill to understand workflow
|
||||
description: Use when debugging errors or reviewing code quality
|
||||
```
|
||||
|
||||
### Pre-Violation Triggers for Discipline Skills
|
||||
|
||||
```yaml
|
||||
# BAD: Triggers AFTER violation
|
||||
description: Use when you forgot to write tests
|
||||
|
||||
# GOOD: Triggers BEFORE violation
|
||||
description: Use when implementing any feature, before writing code
|
||||
```
|
||||
|
||||
## Token Efficiency
|
||||
|
||||
### Skill Loaded Every Conversation = Token Drain
|
||||
|
||||
- Frequently-loaded skills: <200 words
|
||||
- All others: <500 words
|
||||
- Move details to `references/` files
|
||||
|
||||
### Don't Duplicate CLI Help
|
||||
|
||||
```markdown
|
||||
# BAD: 50 lines documenting all flags
|
||||
|
||||
# GOOD: One line
|
||||
Run `mytool --help` for all options.
|
||||
```
|
||||
|
||||
## Anti-Rationalization (Discipline Skills Only)
|
||||
|
||||
### Agents Are Smart at Finding Loopholes
|
||||
|
||||
```markdown
|
||||
# BAD: Trust agents will "get the spirit"
|
||||
Write test before code.
|
||||
|
||||
# GOOD: Close every loophole explicitly
|
||||
Write test before code.
|
||||
|
||||
**No exceptions:**
|
||||
- Don't keep code as "reference"
|
||||
- Don't "adapt" existing code
|
||||
- Delete means delete
|
||||
```
|
||||
|
||||
### Build Rationalization Table
|
||||
|
||||
Every excuse from baseline testing goes in the table:
|
||||
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
|
||||
| "I'll test after" | Tests-after prove nothing immediately. |
|
||||
|
||||
## Cross-References
|
||||
|
||||
### Keep References One Level Deep
|
||||
|
||||
```markdown
|
||||
# BAD: Nested chain (A -> B -> C)
|
||||
See [patterns.md] -> which links to [advanced.md] -> which links to [deep.md]
|
||||
|
||||
# GOOD: Flat (A -> B, A -> C)
|
||||
See [patterns.md] and [advanced.md]
|
||||
```
|
||||
|
||||
### Never Force-Load with @
|
||||
|
||||
```markdown
|
||||
# BAD: Burns context immediately
|
||||
@skills/my-skill/SKILL.md
|
||||
|
||||
# GOOD: Agent loads when needed
|
||||
See [my-skill] for details.
|
||||
```
|
||||
|
||||
## Tier Selection
|
||||
|
||||
### Don't Overthink Tier Choice
|
||||
|
||||
```markdown
|
||||
# BAD: Starting with Tier 3 "just in case"
|
||||
# Result: Wasted effort, empty reference files
|
||||
|
||||
# GOOD: Start with Tier 1, upgrade when needed
|
||||
# Can always add references/ later
|
||||
```
|
||||
|
||||
### Signals You Need to Upgrade
|
||||
|
||||
| Signal | Action |
|
||||
|--------|--------|
|
||||
| SKILL.md > 200 lines | -> Tier 2 |
|
||||
| 3+ related sub-topics | -> Tier 2 |
|
||||
| 10+ products/services | -> Tier 3 |
|
||||
| "I need X" vs "I want Y" | -> Tier 3 decision trees |
|
||||
86
.skills/writing-skills/persuasion-principles.md
Normal file
86
.skills/writing-skills/persuasion-principles.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# Persuasion Principles for Skill Design
|
||||
|
||||
## Overview
|
||||
|
||||
LLMs respond to the same persuasion principles as humans. Understanding this psychology helps you design more effective skills - not to manipulate, but to ensure critical practices are followed even under pressure.
|
||||
|
||||
**Research foundation:** Meincke et al. (2025) tested 7 persuasion principles with N=28,000 AI conversations. Persuasion techniques more than doubled compliance rates (33% to 72%, p < .001).
|
||||
|
||||
## The Seven Principles
|
||||
|
||||
### 1. Authority
|
||||
**What it is:** Deference to expertise, credentials, or official sources.
|
||||
|
||||
**How it works in skills:**
|
||||
- Imperative language: "YOU MUST", "Never", "Always"
|
||||
- Non-negotiable framing: "No exceptions"
|
||||
- Eliminates decision fatigue and rationalization
|
||||
|
||||
**When to use:**
|
||||
- Discipline-enforcing skills (TDD, verification requirements)
|
||||
- Safety-critical practices
|
||||
- Established best practices
|
||||
|
||||
### 2. Commitment
|
||||
**What it is:** Consistency with prior actions, statements, or public declarations.
|
||||
|
||||
**How it works in skills:**
|
||||
- Require announcements: "Announce skill usage"
|
||||
- Force explicit choices: "Choose A, B, or C"
|
||||
- Use tracking: TodoWrite for checklists
|
||||
|
||||
### 3. Scarcity
|
||||
**What it is:** Urgency from time limits or limited availability.
|
||||
|
||||
**How it works in skills:**
|
||||
- Time-bound requirements: "Before proceeding"
|
||||
- Sequential dependencies: "Immediately after X"
|
||||
- Prevents procrastination
|
||||
|
||||
### 4. Social Proof
|
||||
**What it is:** Conformity to what others do or what's considered normal.
|
||||
|
||||
**How it works in skills:**
|
||||
- Universal patterns: "Every time", "Always"
|
||||
- Failure modes: "X without Y = failure"
|
||||
- Establishes norms
|
||||
|
||||
### 5. Unity
|
||||
**What it is:** Shared identity, "we-ness", in-group belonging.
|
||||
|
||||
**How it works in skills:**
|
||||
- Collaborative language: "our codebase", "we're colleagues"
|
||||
- Shared goals: "we both want quality"
|
||||
|
||||
### 6. Reciprocity
|
||||
**What it is:** Obligation to return benefits received.
|
||||
- Use sparingly - can feel manipulative
|
||||
- Rarely needed in skills
|
||||
|
||||
### 7. Liking
|
||||
**What it is:** Preference for cooperating with those we like.
|
||||
- **DON'T USE for compliance**
|
||||
- Conflicts with honest feedback culture
|
||||
|
||||
## Principle Combinations by Skill Type
|
||||
|
||||
| Skill Type | Use | Avoid |
|
||||
|------------|-----|-------|
|
||||
| Discipline-enforcing | Authority + Commitment + Social Proof | Liking, Reciprocity |
|
||||
| Guidance/technique | Moderate Authority + Unity | Heavy authority |
|
||||
| Collaborative | Unity + Commitment | Authority, Liking |
|
||||
| Reference | Clarity only | All persuasion |
|
||||
|
||||
## Ethical Use
|
||||
|
||||
**Legitimate:**
|
||||
- Ensuring critical practices are followed
|
||||
- Creating effective documentation
|
||||
- Preventing predictable failures
|
||||
|
||||
**The test:** Would this technique serve the user's genuine interests if they fully understood it?
|
||||
|
||||
## Research Citations
|
||||
|
||||
**Cialdini, R. B. (2021).** *Influence: The Psychology of Persuasion (New and Expanded).* Harper Business.
|
||||
**Meincke, L., et al. (2025).** Call Me A Jerk: Persuading AI to Comply with Objectionable Requests. University of Pennsylvania.
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
# Anti-Rationalization Guide
|
||||
|
||||
Techniques for bulletproofing skills against agent rationalization.
|
||||
|
||||
## The Problem
|
||||
|
||||
Discipline-enforcing skills face a unique challenge: smart agents under pressure will find loopholes.
|
||||
|
||||
## Technique 1: Close Every Loophole Explicitly
|
||||
|
||||
Don't just state the rule - forbid specific workarounds.
|
||||
|
||||
### Bad Example
|
||||
```markdown
|
||||
Write code before test? Delete it.
|
||||
```
|
||||
|
||||
### Good Example
|
||||
```markdown
|
||||
Write code before test? Delete it. Start over.
|
||||
|
||||
**No exceptions**:
|
||||
- Don't keep it as "reference"
|
||||
- Don't "adapt" it while writing tests
|
||||
- Don't look at it
|
||||
- Delete means delete
|
||||
```
|
||||
|
||||
## Technique 2: Address "Spirit vs Letter" Arguments
|
||||
|
||||
Add foundational principle early:
|
||||
|
||||
```markdown
|
||||
**Violating the letter of the rules is violating the spirit of the rules.**
|
||||
```
|
||||
|
||||
## Technique 3: Build Rationalization Table
|
||||
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
|
||||
| "I'll test after" | Tests passing immediately prove nothing. |
|
||||
| "Spirit not ritual" | The letter IS the spirit. |
|
||||
|
||||
## Technique 4: Create Red Flags List
|
||||
|
||||
```markdown
|
||||
## Red Flags - STOP and Start Over
|
||||
- Code before test
|
||||
- "I already manually tested it"
|
||||
- "This is different because..."
|
||||
|
||||
**All of these mean**: Delete code. Start over.
|
||||
```
|
||||
|
||||
## Technique 5: Use Strong Language
|
||||
|
||||
```markdown
|
||||
# Weak (invites rationalization)
|
||||
You should write tests first.
|
||||
|
||||
# Strong (no wiggle room)
|
||||
ALWAYS write test first.
|
||||
NEVER write code before test.
|
||||
```
|
||||
|
||||
## Technique 6: Provide Escape Hatch for Legitimate Cases
|
||||
|
||||
```markdown
|
||||
## When NOT to Use
|
||||
- Spike solutions (throwaway exploratory code)
|
||||
- One-time scripts deleting in 1 hour
|
||||
|
||||
**Everything else**: Follow the rule. No exceptions.
|
||||
```
|
||||
|
||||
## Complete Bulletproofing Checklist
|
||||
|
||||
- [ ] Forbidden each specific workaround explicitly?
|
||||
- [ ] Added "spirit vs letter" principle?
|
||||
- [ ] Built rationalization table from baseline tests?
|
||||
- [ ] Created red flags list?
|
||||
- [ ] Used strong language (ALWAYS/NEVER)?
|
||||
- [ ] Provided explicit escape hatch?
|
||||
- [ ] Description includes pre-violation symptoms?
|
||||
90
.skills/writing-skills/references/cso/README.md
Normal file
90
.skills/writing-skills/references/cso/README.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# CSO Guide - Claude Search Optimization
|
||||
|
||||
Advanced techniques for making skills discoverable by agents.
|
||||
|
||||
## The Discovery Problem
|
||||
|
||||
You have 100+ skills. Agent receives a task. How does it find the RIGHT skill?
|
||||
|
||||
**Answer**: The `description` field.
|
||||
|
||||
## Critical Rule: Description = Triggers, NOT Workflow
|
||||
|
||||
### The Trap
|
||||
|
||||
When description summarizes workflow, agents take a shortcut.
|
||||
|
||||
**Real example that failed**:
|
||||
|
||||
```yaml
|
||||
# Agent did ONE review instead of TWO
|
||||
description: Code review between tasks
|
||||
|
||||
# Skill body had flowchart showing TWO reviews
|
||||
```
|
||||
|
||||
**Why it failed**: Agent read description, thought "code review between tasks means one review", never read the flowchart.
|
||||
|
||||
**Fix**:
|
||||
|
||||
```yaml
|
||||
# Agent now reads full skill and follows flowchart
|
||||
description: Use when executing implementation plans with independent tasks
|
||||
```
|
||||
|
||||
### The Pattern
|
||||
|
||||
```yaml
|
||||
# BAD: Workflow summary
|
||||
description: Analyzes git diff, generates commit message in conventional format
|
||||
|
||||
# GOOD: Trigger conditions only
|
||||
description: Use when generating commit messages or reviewing staged changes
|
||||
```
|
||||
|
||||
## Token Efficiency
|
||||
|
||||
**Target word counts**:
|
||||
- Frequently-loaded skills: <200 words total
|
||||
- Other skills: <500 words
|
||||
|
||||
## Keyword Strategy
|
||||
|
||||
### Error Messages
|
||||
Include EXACT error text users will see.
|
||||
|
||||
### Symptoms
|
||||
Use words users naturally say: "flaky", "hangs", "slow", "timeout", "race condition"
|
||||
|
||||
### Tools & Commands
|
||||
Actual names, not descriptions: "pytest", not "Python testing"
|
||||
|
||||
### Synonyms
|
||||
Cover multiple ways to describe same thing: timeout/hang/freeze
|
||||
|
||||
## Description Template
|
||||
|
||||
```yaml
|
||||
description: "Use when [SPECIFIC TRIGGER]."
|
||||
metadata:
|
||||
triggers: [error1], [symptom2], [tool3]
|
||||
```
|
||||
|
||||
## Third Person Rule
|
||||
|
||||
```yaml
|
||||
# BAD: First person
|
||||
description: "I can help you with async tests"
|
||||
|
||||
# GOOD: Third person
|
||||
description: "Handles async tests with race conditions"
|
||||
```
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
- [ ] Description starts with "Use when..."?
|
||||
- [ ] Description is <500 characters?
|
||||
- [ ] Description lists ONLY triggers, not workflow?
|
||||
- [ ] Includes 3+ keywords (errors/symptoms/tools)?
|
||||
- [ ] Third person throughout?
|
||||
- [ ] Name uses gerund or verb-first format?
|
||||
87
.skills/writing-skills/references/standards/README.md
Normal file
87
.skills/writing-skills/references/standards/README.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
description: Standards and naming rules for creating agent skills.
|
||||
metadata:
|
||||
tags: [standards, naming, yaml, structure]
|
||||
---
|
||||
|
||||
# Skill Development Guide
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
skills/
|
||||
{skill-name}/ # kebab-case, matches `name` field
|
||||
SKILL.md # Required: main skill definition
|
||||
references/ # Optional: supporting documentation
|
||||
README.md # Sub-topic entry point
|
||||
*.md # Additional files
|
||||
```
|
||||
|
||||
## Naming Rules
|
||||
|
||||
| Element | Rule | Example |
|
||||
|---------|------|---------|
|
||||
| Directory | kebab-case, 1-64 chars | `react-best-practices` |
|
||||
| `SKILL.md` | ALL CAPS, exact filename | `SKILL.md` (not `skill.md`) |
|
||||
| `name` field | Must match directory name | `name: react-best-practices` |
|
||||
|
||||
## SKILL.md Structure
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: {skill-name}
|
||||
description: >-
|
||||
Use when [trigger condition].
|
||||
metadata:
|
||||
category: technique
|
||||
triggers: keyword1, keyword2, error-text
|
||||
---
|
||||
|
||||
# Skill Title
|
||||
|
||||
Brief description of what this skill does.
|
||||
|
||||
## When to Use
|
||||
- Symptom or situation A
|
||||
- Symptom or situation B
|
||||
|
||||
## How It Works
|
||||
Step-by-step instructions or reference content.
|
||||
|
||||
## Examples
|
||||
Concrete usage examples.
|
||||
|
||||
## Common Mistakes
|
||||
What to avoid and why.
|
||||
```
|
||||
|
||||
## Description Best Practices
|
||||
|
||||
```yaml
|
||||
# BAD: Workflow summary
|
||||
description: Analyzes code, finds bugs, suggests fixes
|
||||
|
||||
# GOOD: Trigger conditions only
|
||||
description: Use when debugging errors or reviewing code quality.
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Start with "Use when..."
|
||||
- Keep under 500 characters
|
||||
- Use third person
|
||||
|
||||
## Context Efficiency
|
||||
|
||||
| Guideline | Reason |
|
||||
|-----------|--------|
|
||||
| Keep SKILL.md < 500 lines | Reduces context consumption |
|
||||
| Put details in supporting files | Agent reads only what's needed |
|
||||
| Use tables for reference data | More compact than prose |
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
- [ ] `name` matches directory name?
|
||||
- [ ] `SKILL.md` is ALL CAPS?
|
||||
- [ ] Description starts with "Use when..."?
|
||||
- [ ] Under 500 lines?
|
||||
- [ ] Tested with real scenarios?
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# SKILL.md Metadata Standard
|
||||
|
||||
Official frontmatter fields.
|
||||
|
||||
## Required Fields
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: skill-name
|
||||
description: >-
|
||||
Use when [trigger condition].
|
||||
---
|
||||
```
|
||||
|
||||
| Field | Rules |
|
||||
|-------|-------|
|
||||
| `name` | 1-64 chars, lowercase, hyphens only, must match directory name |
|
||||
| `description` | 1-1024 chars, should describe when to use |
|
||||
|
||||
## Optional Fields
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: skill-name
|
||||
description: Purpose and triggers.
|
||||
metadata:
|
||||
category: "reference"
|
||||
version: "1.0.0"
|
||||
---
|
||||
```
|
||||
|
||||
## Name Validation
|
||||
|
||||
```regex
|
||||
^[a-z0-9]+(-[a-z0-9]+)*$
|
||||
```
|
||||
|
||||
**Valid**: `my-skill`, `git-release`, `tdd`
|
||||
**Invalid**: `My-Skill`, `my_skill`, `-my-skill`
|
||||
54
.skills/writing-skills/references/templates/discipline.md
Normal file
54
.skills/writing-skills/references/templates/discipline.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
name: discipline-name
|
||||
description: >-
|
||||
Use when [BEFORE violation].
|
||||
metadata:
|
||||
category: discipline
|
||||
triggers: new feature, code change, implementation
|
||||
---
|
||||
|
||||
# Rule Name
|
||||
|
||||
## Iron Law
|
||||
|
||||
**[SINGLE SENTENCE ABSOLUTE RULE]**
|
||||
|
||||
Violating the letter IS violating the spirit.
|
||||
|
||||
## The Rule
|
||||
|
||||
1. ALWAYS [step 1]
|
||||
2. NEVER [step 2]
|
||||
3. [Step 3]
|
||||
|
||||
## Violations
|
||||
|
||||
[Action before rule]? **Delete it. Start over.**
|
||||
|
||||
**No exceptions:**
|
||||
- Don't keep it as "reference"
|
||||
- Don't "adapt" it
|
||||
- Delete means delete
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "Too simple" | Simple code breaks. Rule takes 30 seconds. |
|
||||
| "I'll do it after" | After = never. Do it now. |
|
||||
| "Spirit not ritual" | The ritual IS the spirit. |
|
||||
|
||||
## Red Flags - STOP
|
||||
|
||||
- [Flag 1]
|
||||
- [Flag 2]
|
||||
- "This is different because..."
|
||||
|
||||
**All mean:** Delete. Start over.
|
||||
|
||||
## Valid Exceptions
|
||||
|
||||
- [Exception 1]
|
||||
- [Exception 2]
|
||||
|
||||
**Everything else:** Follow the rule.
|
||||
48
.skills/writing-skills/references/templates/pattern.md
Normal file
48
.skills/writing-skills/references/templates/pattern.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
name: pattern-name
|
||||
description: >-
|
||||
Use when [recognizable symptom].
|
||||
metadata:
|
||||
category: pattern
|
||||
triggers: complexity, hard-to-follow, nested
|
||||
---
|
||||
|
||||
# Pattern Name
|
||||
|
||||
## The Pattern
|
||||
|
||||
[1-2 sentence core idea]
|
||||
|
||||
## Recognition Signs
|
||||
|
||||
- [Sign that pattern applies]
|
||||
- [Another sign]
|
||||
- [Code smell]
|
||||
|
||||
## Before
|
||||
|
||||
```typescript
|
||||
// Complex/problematic
|
||||
function before() {
|
||||
// nested, confusing
|
||||
}
|
||||
```
|
||||
|
||||
## After
|
||||
|
||||
```typescript
|
||||
// Clean/improved
|
||||
function after() {
|
||||
// flat, clear
|
||||
}
|
||||
```
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- [Over-engineering case]
|
||||
- [Simple case that doesn't need it]
|
||||
|
||||
## Impact
|
||||
|
||||
**Before:** [Problem metric]
|
||||
**After:** [Improved metric]
|
||||
35
.skills/writing-skills/references/templates/reference.md
Normal file
35
.skills/writing-skills/references/templates/reference.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
name: reference-name
|
||||
description: >-
|
||||
Use when working with [domain].
|
||||
metadata:
|
||||
category: reference
|
||||
triggers: tool, api, specific-terms
|
||||
---
|
||||
|
||||
# Reference Name
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `cmd1` | Does X |
|
||||
| `cmd2` | Does Y |
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Pattern A:**
|
||||
```bash
|
||||
example command
|
||||
```
|
||||
|
||||
**Pattern B:**
|
||||
```bash
|
||||
another example
|
||||
```
|
||||
|
||||
## Detailed Docs
|
||||
|
||||
For more options, run `--help` or see:
|
||||
- patterns.md
|
||||
- examples.md
|
||||
59
.skills/writing-skills/references/templates/technique.md
Normal file
59
.skills/writing-skills/references/templates/technique.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
name: technique-name
|
||||
description: Use when [specific symptom].
|
||||
metadata:
|
||||
category: technique
|
||||
triggers: error-text, symptom, tool-name
|
||||
---
|
||||
|
||||
# Technique Name
|
||||
|
||||
## Overview
|
||||
|
||||
[1-2 sentence core principle]
|
||||
|
||||
## When to Use
|
||||
|
||||
- [Symptom A]
|
||||
- [Symptom B]
|
||||
- [Error message text]
|
||||
|
||||
**NOT for:**
|
||||
- [When to avoid]
|
||||
|
||||
## The Problem
|
||||
|
||||
```javascript
|
||||
// Bad example
|
||||
function badCode() {
|
||||
// problematic pattern
|
||||
}
|
||||
```
|
||||
|
||||
## The Solution
|
||||
|
||||
```javascript
|
||||
// Good example
|
||||
function goodCode() {
|
||||
// improved pattern
|
||||
}
|
||||
```
|
||||
|
||||
## Step-by-Step
|
||||
|
||||
1. [First step]
|
||||
2. [Second step]
|
||||
3. [Final step]
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Scenario | Approach |
|
||||
|----------|----------|
|
||||
| Case A | Solution A |
|
||||
| Case B | Solution B |
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
**Mistake 1:** [Description]
|
||||
- Wrong: `bad code`
|
||||
- Right: `good code`
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Platform Name Skill
|
||||
|
||||
Template for complex Tier 3 skills.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
skill/
|
||||
SKILL.md # Dispatcher
|
||||
references/
|
||||
topic/
|
||||
README.md # Overview
|
||||
api.md # API Reference
|
||||
config.md # Configuration
|
||||
patterns.md # Recipes
|
||||
gotchas.md # Critical Errors
|
||||
```
|
||||
66
.skills/writing-skills/references/testing/README.md
Normal file
66
.skills/writing-skills/references/testing/README.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Testing Guide - TDD for Skills
|
||||
|
||||
Complete methodology for testing skills using RED-GREEN-REFACTOR cycle.
|
||||
|
||||
## Testing All Skill Types
|
||||
|
||||
### Discipline-Enforcing Skills (rules/requirements)
|
||||
|
||||
**Test with**:
|
||||
- Academic questions: Do they understand the rules?
|
||||
- Pressure scenarios: Do they comply under stress?
|
||||
- Multiple pressures combined: time + sunk cost + exhaustion
|
||||
|
||||
**Success criteria**: Agent follows rule under maximum pressure
|
||||
|
||||
### Technique Skills (how-to guides)
|
||||
|
||||
**Test with**:
|
||||
- Application scenarios: Can they apply the technique correctly?
|
||||
- Variation scenarios: Do they handle edge cases?
|
||||
- Missing information tests: Do instructions have gaps?
|
||||
|
||||
**Success criteria**: Agent successfully applies technique to new scenario
|
||||
|
||||
### Pattern Skills (mental models)
|
||||
|
||||
**Test with**:
|
||||
- Recognition scenarios: Do they recognize when pattern applies?
|
||||
- Counter-examples: Do they know when NOT to apply?
|
||||
|
||||
**Success criteria**: Agent correctly identifies when/how to apply pattern
|
||||
|
||||
### Reference Skills (documentation/APIs)
|
||||
|
||||
**Test with**:
|
||||
- Retrieval scenarios: Can they find the right information?
|
||||
- Gap testing: Are common use cases covered?
|
||||
|
||||
**Success criteria**: Agent finds and correctly applies reference information
|
||||
|
||||
## Pressure Types for Testing
|
||||
|
||||
| Pressure | Example |
|
||||
|----------|---------|
|
||||
| Time | "You have 5 minutes to complete this task" |
|
||||
| Sunk cost | "You already spent 2 hours on this" |
|
||||
| Authority | "Senior developer said to skip tests" |
|
||||
| Exhaustion | "This is the 10th task today" |
|
||||
|
||||
## Complete Test Checklist
|
||||
|
||||
**Baseline (RED)**:
|
||||
- [ ] Designed 3+ pressure scenarios
|
||||
- [ ] Ran scenarios WITHOUT skill
|
||||
- [ ] Documented verbatim agent responses
|
||||
|
||||
**Implementation (GREEN)**:
|
||||
- [ ] Skill addresses SPECIFIC baseline failures
|
||||
- [ ] Re-ran scenarios WITH skill
|
||||
- [ ] Agent complied in all scenarios
|
||||
|
||||
**Bulletproofing (REFACTOR)**:
|
||||
- [ ] Tested with combined pressures
|
||||
- [ ] Found and documented new rationalizations
|
||||
- [ ] Added explicit counters
|
||||
- [ ] Re-tested until no more loopholes
|
||||
30
.skills/writing-skills/references/tier-1-simple/README.md
Normal file
30
.skills/writing-skills/references/tier-1-simple/README.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
description: When to use Tier 1 (Simple) skill architecture.
|
||||
metadata:
|
||||
tags: [tier-1, simple, single-file]
|
||||
---
|
||||
|
||||
# Tier 1: Simple Skills
|
||||
|
||||
Single-file skills for focused, specific purposes.
|
||||
|
||||
## When to Use
|
||||
|
||||
- **Single concept**: One technique, one pattern, one reference
|
||||
- **Under 200 lines**: Can fit comfortably in one file
|
||||
- **No complex decision logic**: User knows exactly what they need
|
||||
- **Frequently loaded**: Needs minimal token footprint
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
my-skill/
|
||||
SKILL.md # Everything in one file
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Fits in <200 lines
|
||||
- [ ] Single focused purpose
|
||||
- [ ] No need for `references/` directory
|
||||
- [ ] Description uses "Use when..." pattern
|
||||
52
.skills/writing-skills/references/tier-2-expanded/README.md
Normal file
52
.skills/writing-skills/references/tier-2-expanded/README.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
description: When to use Tier 2 (Expanded) skill architecture.
|
||||
metadata:
|
||||
tags: [tier-2, expanded, multi-file]
|
||||
---
|
||||
|
||||
# Tier 2: Expanded Skills
|
||||
|
||||
Multi-file skills for complex topics with multiple sub-concepts.
|
||||
|
||||
## When to Use
|
||||
|
||||
- **Multiple related concepts**: Needs separation of concerns
|
||||
- **200-1000 lines total**: Too big for one file
|
||||
- **Needs reference files**: Patterns, examples, troubleshooting
|
||||
- **Cross-linking**: Users need to navigate between sub-topics
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
my-skill/
|
||||
SKILL.md # Overview + navigation
|
||||
references/
|
||||
core/
|
||||
README.md # Main concept
|
||||
patterns/
|
||||
README.md # Usage patterns
|
||||
troubleshooting/
|
||||
README.md # Common issues
|
||||
```
|
||||
|
||||
## Progressive Disclosure
|
||||
|
||||
1. **Metadata** (~100 tokens): Name + description loaded at startup
|
||||
2. **SKILL.md** (<500 lines): Decision tree + index
|
||||
3. **References** (as needed): Loaded only when user navigates
|
||||
|
||||
## Key Differences from Tier 1
|
||||
|
||||
| Aspect | Tier 1 | Tier 2 |
|
||||
|--------|--------|--------|
|
||||
| Files | 1 | 5-20 |
|
||||
| Total lines | <200 | 200-1000 |
|
||||
| Decision logic | None | Simple tree |
|
||||
| Token cost | Minimal | Medium (progressive) |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] SKILL.md has clear navigation links
|
||||
- [ ] Each `references/` subdir has README.md
|
||||
- [ ] No circular references between files
|
||||
- [ ] Decision tree points to specific files
|
||||
51
.skills/writing-skills/references/tier-3-platform/README.md
Normal file
51
.skills/writing-skills/references/tier-3-platform/README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
description: When to use Tier 3 (Platform) skill architecture for large platforms.
|
||||
metadata:
|
||||
tags: [tier-3, platform, enterprise]
|
||||
---
|
||||
|
||||
# Tier 3: Platform Skills
|
||||
|
||||
Enterprise-grade skills for entire platforms (AWS, Cloudflare, Convex, etc).
|
||||
|
||||
## When to Use
|
||||
|
||||
- **Entire platform**: 10+ products/services
|
||||
- **1000+ lines total**: Would overwhelm context if monolithic
|
||||
- **Complex decision logic**: Users start with "I need X" not "I want product Y"
|
||||
|
||||
## The 5-File Pattern
|
||||
|
||||
Each product directory has exactly 5 files:
|
||||
|
||||
| File | Purpose | When to Load |
|
||||
|------|---------|--------------|
|
||||
| `README.md` | Overview, when to use | Always first |
|
||||
| `api.md` | Runtime APIs, methods | Implementing features |
|
||||
| `configuration.md` | Config, environment | Setting up |
|
||||
| `patterns.md` | Common workflows | Best practices |
|
||||
| `gotchas.md` | Pitfalls, limits | Debugging |
|
||||
|
||||
## Decision Trees
|
||||
|
||||
```markdown
|
||||
Need to store data?
|
||||
Simple key-value -> kv/
|
||||
Relational queries -> d1/
|
||||
Large files/blobs -> r2/
|
||||
Per-user state -> durable-objects/
|
||||
```
|
||||
|
||||
## Progressive Disclosure in Action
|
||||
|
||||
- **Startup**: Only name + description (~100 tokens)
|
||||
- **Activation**: SKILL.md with trees (<5000 tokens)
|
||||
- **Navigation**: One product's 5 files (as needed)
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] SKILL.md contains ONLY decision trees + index
|
||||
- [ ] Each product has exactly 5 files
|
||||
- [ ] Decision trees cover all "I need X" scenarios
|
||||
- [ ] Cross-references stay one level deep
|
||||
- [ ] Every product has `gotchas.md`
|
||||
85
.skills/writing-skills/testing-skills-with-subagents.md
Normal file
85
.skills/writing-skills/testing-skills-with-subagents.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Testing Skills With Subagents
|
||||
|
||||
**Load this reference when:** creating or editing skills, before deployment, to verify they work under pressure and resist rationalization.
|
||||
|
||||
## Overview
|
||||
|
||||
**Testing skills is just TDD applied to process documentation.**
|
||||
|
||||
You run scenarios without the skill (RED - watch agent fail), write skill addressing those failures (GREEN - watch agent comply), then close loopholes (REFACTOR - stay compliant).
|
||||
|
||||
**Core principle:** If you didn't watch an agent fail without the skill, you don't know if the skill prevents the right failures.
|
||||
|
||||
## When to Use
|
||||
|
||||
Test skills that:
|
||||
- Enforce discipline (TDD, testing requirements)
|
||||
- Have compliance costs (time, effort, rework)
|
||||
- Could be rationalized away ("just this once")
|
||||
- Contradict immediate goals (speed over quality)
|
||||
|
||||
Don't test:
|
||||
- Pure reference skills (API docs, syntax guides)
|
||||
- Skills without rules to violate
|
||||
- Skills agents have no incentive to bypass
|
||||
|
||||
## TDD Mapping for Skill Testing
|
||||
|
||||
| TDD Phase | Skill Testing | What You Do |
|
||||
|-----------|---------------|-------------|
|
||||
| **RED** | Baseline test | Run scenario WITHOUT skill, watch agent fail |
|
||||
| **Verify RED** | Capture rationalizations | Document exact failures verbatim |
|
||||
| **GREEN** | Write skill | Address specific baseline failures |
|
||||
| **Verify GREEN** | Pressure test | Run scenario WITH skill, verify compliance |
|
||||
| **REFACTOR** | Plug holes | Find new rationalizations, add counters |
|
||||
| **Stay GREEN** | Re-verify | Test again, ensure still compliant |
|
||||
|
||||
## RED Phase: Baseline Testing (Watch It Fail)
|
||||
|
||||
**Goal:** Run test WITHOUT the skill - watch agent fail, document exact failures.
|
||||
|
||||
**Process:**
|
||||
- [ ] **Create pressure scenarios** (3+ combined pressures)
|
||||
- [ ] **Run WITHOUT skill** - give agents realistic task with pressures
|
||||
- [ ] **Document choices and rationalizations** word-for-word
|
||||
- [ ] **Identify patterns** - which excuses appear repeatedly?
|
||||
- [ ] **Note effective pressures** - which scenarios trigger violations?
|
||||
|
||||
## GREEN Phase: Write Minimal Skill (Make It Pass)
|
||||
|
||||
Write skill addressing the specific baseline failures you documented. Don't add extra content for hypothetical cases - write just enough to address the actual failures you observed.
|
||||
|
||||
Run same scenarios WITH skill. Agent should now comply.
|
||||
|
||||
If agent still fails: skill is unclear or incomplete. Revise and re-test.
|
||||
|
||||
## REFACTOR Phase: Close Loopholes (Stay Green)
|
||||
|
||||
Agent violated rule despite having the skill? Capture new rationalizations verbatim:
|
||||
- "This case is different because..."
|
||||
- "I'm following the spirit not the letter"
|
||||
- "Being pragmatic means adapting"
|
||||
- "Deleting X hours is wasteful"
|
||||
|
||||
**Document every excuse.** These become your rationalization table.
|
||||
|
||||
## Testing Checklist (TDD for Skills)
|
||||
|
||||
**RED Phase:**
|
||||
- [ ] Created pressure scenarios (3+ combined pressures)
|
||||
- [ ] Ran scenarios WITHOUT skill (baseline)
|
||||
- [ ] Documented agent failures and rationalizations verbatim
|
||||
|
||||
**GREEN Phase:**
|
||||
- [ ] Wrote skill addressing specific baseline failures
|
||||
- [ ] Ran scenarios WITH skill
|
||||
- [ ] Agent now complies
|
||||
|
||||
**REFACTOR Phase:**
|
||||
- [ ] Identified NEW rationalizations from testing
|
||||
- [ ] Added explicit counters for each loophole
|
||||
- [ ] Updated rationalization table
|
||||
- [ ] Updated red flags list
|
||||
- [ ] Re-tested - agent still complies
|
||||
- [ ] Meta-tested to verify clarity
|
||||
- [ ] Agent follows rule under maximum pressure
|
||||
126
AGENTS.md
Normal file
126
AGENTS.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# AGENTS.md
|
||||
|
||||
## Project
|
||||
- Minecraft Console Client (MCC) is a cross-platform text/TUI client for Minecraft Java Edition.
|
||||
- Primary scope: connect to servers, send chat and commands, receive text, automate gameplay/admin tasks, and extend behavior through built-in bots or runtime C# scripts.
|
||||
- Secondary scope: protocol/version adaptation tooling, docs site, legacy GUI wrapper, and debug tooling.
|
||||
- Decompiled server source for both the old and new MC versions in `$MCC_REPO/MinecraftOfficial/<version>-decompiled/`
|
||||
|
||||
## Build / Run
|
||||
- Init submodules first: `git submodule update --init --recursive`
|
||||
- Build for local development: `source tools/mcc-env.sh && mcc-build`
|
||||
- Publish (matches CI shape): `source tools/mcc-env.sh && mcc-publish --rid <RID>`
|
||||
- Run/debug from source: `source tools/mcc-env.sh && mcc-debug -v 1.21.11 --file-input`
|
||||
- Docs: `cd docs && npm install && npm run docs:dev` or `npm run docs:build`
|
||||
- Docker: `cd Docker && docker build -t minecraft-console-client:latest .`
|
||||
- Tests: no dedicated test project is present in the main solution.
|
||||
- Current state: the solution builds after submodule init, but the underlying .NET build emits many analyzer and NuGet vulnerability warnings; treat them as real.
|
||||
- Server roots: `tools/` helpers look for server jars under `MinecraftOfficial/downloads/<version>/` by default, but also support an external root via the `MCC_SERVERS` environment variable.
|
||||
- Multi-version testing: tmux-based local server sessions are shared state. Run cross-version test matrices sequentially unless you have explicit per-version isolation. A server logging `Done` does not guarantee immediate RCON availability; retry RCON setup commands.
|
||||
- Automated test configs: for repeated or matrix test runs, prefer generating a temporary MCC config per run instead of reusing the repo-root `MinecraftClient.ini`, to avoid leaking state between runs.
|
||||
- For agent-driven local development, prefer `mcc-build`, `mcc-publish`, `mcc-build-clean`, `mcc-debug`, `mcc-run`, and `mcc-tui` over raw `dotnet build`, `dotnet publish`, or `dotnet run`, so worktree-local temp build routing stays active.
|
||||
|
||||
## Architecture
|
||||
- `Program` bootstraps console I/O, TOML config, auth/session state, MC version selection, Forge detection, then creates `McClient`.
|
||||
- `McClient` is the live session runtime: TCP client, selected protocol handler, Brigadier command dispatcher, loaded bots, world/inventory/entity state, queued chat, movement/pathing, reconnect flow.
|
||||
- `Protocol/` is the network/auth boundary. `ProtocolHandler` maps Minecraft versions to protocol numbers and selects either `Protocol16Handler` (1.4.6-1.6.4) or `Protocol18Handler` (1.7.2+).
|
||||
- `Scripting/ChatBot` is the extension boundary. Built-in bots and `/script` C# bots share the same event/tick API.
|
||||
- Main runtime flow: console input -> internal Brigadier command or server chat; packets -> protocol handler -> `McClient` state update -> bot events; `OnUpdate()` (20 TPS) drives bot ticks, delayed work, chat cooldowns, movement, and main-thread tasks.
|
||||
|
||||
## Technology Stack
|
||||
- Main app: C#, .NET 10, nullable enabled.
|
||||
- Command system: `Brigadier.NET`.
|
||||
- Config: TOML via `Samboy063.Tomlet`.
|
||||
- Runtime scripting: Roslyn (`Microsoft.CodeAnalysis.CSharp`) with in-memory compilation.
|
||||
- Networking/auth: custom Minecraft protocol handlers, DNS SRV lookup (`DnsClient`), Forge/session/profile-key support.
|
||||
- Integrations: `DSharpPlus`, `Telegram.Bot`, `MessagePack`, `Magick.NET`, `Sentry`.
|
||||
- Docs site: VuePress 2 (`docs/package.json`).
|
||||
- Tooling: Docker, GitHub Actions, Python 3.10+ scripts under `tools/` for palette/version generation.
|
||||
- Legacy UI: `MinecraftClientGUI` is a separate .NET Framework 4.0 WinForms wrapper, not the main runtime.
|
||||
|
||||
## Version Support
|
||||
Feature columns mean:
|
||||
- Inventory: `/inventory` plus inventory/container bot APIs
|
||||
- Movement: terrain handling, `/move`, and movement/pathing bots
|
||||
- Entity: entity tracking and entity-driven bot events
|
||||
|
||||
| Minecraft | Protocol path | Inventory | Movement | Entity | Notes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1.4.6-1.6.4 | `Protocol16Handler` | No | No | No | Core login/chat only |
|
||||
| 1.7.2-1.7.10 | `Protocol18Handler` | No | Yes | No | Pre-1.8 special case |
|
||||
| 1.8-1.9.4 | `Protocol18Handler` | Partial / docs conflict | Yes | Yes | Runtime gates allow 1.8+, but docs still warn inventory is unsupported through 1.9 |
|
||||
| 1.10-1.12.2 | `Protocol18Handler` | Yes | Yes | Yes | Pre-flattening palettes |
|
||||
| 1.13-1.19.2 | `Protocol18Handler` | Yes | Yes | Yes | Flattened block/item/entity palettes |
|
||||
| 1.19.3-1.20.4 | `Protocol18Handler` | Yes | Yes | Yes | Newer chat/signing and palette splits |
|
||||
| 1.20.6-1.21.4 | `Protocol18Handler` | Yes | Yes | Yes | Registry-driven world/attribute handling |
|
||||
| 1.21.5-1.21.8 | `Protocol18Handler` | Yes | Yes | Yes | 1.21.7/1.21.8 reuse 1.21.6 block/entity palettes in code |
|
||||
| 1.21.9-1.21.10 | `Protocol18Handler` | Yes | Yes | Yes | Version tools prefer server data reports since 1.21.9 |
|
||||
| 1.21.11 | `Protocol18Handler` | Yes | Yes | Yes | Own entity/item/metadata palettes; blocks reuse 1.21.9 palette |
|
||||
| 26.1 | `Protocol18Handler` | Yes | Yes | Yes | Latest coded support; new Minecraft version naming scheme |
|
||||
|
||||
Notes:
|
||||
- Declared code range is `1.4.6` to `26.1`.
|
||||
- Human docs are stale in places and sometimes stop at older ranges; prefer code when docs and code disagree.
|
||||
- Movement/pathing limits called out in docs still apply: no swimming, no knockback. The `Physics/` engine adds vanilla-accurate collision and movement but some edge cases remain.
|
||||
|
||||
## Module Map
|
||||
### Core Runtime
|
||||
|
||||
| Module | What It Owns | Important Files |
|
||||
| --- | --- | --- |
|
||||
| `MinecraftClient/` | Main `net10.0` runtime assembly and the best starting point. `Program.cs` owns startup, config load/writeback, CLI handling, auth/version selection, update/data-generation entrypoints, and restart/failure flow. `McClient.cs` owns the live session runtime: protocol handler ownership, command dispatch, bot lifecycle, world/inventory/entity state, queued chat, movement ticks, reconnect/disconnect logic, and the main-thread invoke queue. `Settings.cs` defines the TOML schema and runtime/internal overrides used across the app. | `Program.cs`, `McClient.cs`, `Settings.cs`, `ConsoleIO.cs`, `Command.cs`, `UpgradeHelper.cs`, `AutoTimeout.cs` |
|
||||
| `MinecraftClient/Protocol/` | Network/auth/session boundary. `ProtocolHandler.cs` does DNS SRV lookup, server ping/version detection, MC-version to protocol mapping, and handler selection. `Protocol16.cs` and `Protocol18.cs` implement the packet flow for legacy and modern versions. `Protocol18Terrain.cs` decodes chunk sections/biomes into `World`. `DataTypes.cs` is the low-level reader/writer layer for VarInts, metadata, NBT-like structures, and packet fields. `Message/`, `ProfileKey/`, `Session/`, `Handlers/Forge/`, `Handlers/PacketPalettes/`, `Handlers/Packet/`, and `Handlers/StructuredComponents/` cover chat/signing, cached auth, Forge, packet IDs, packet-level parsing, and 1.20.6+ item components with versioned registries under `StructuredComponents/Registries/`. | `Protocol/ProtocolHandler.cs`, `Protocol/Handlers/Protocol16.cs`, `Protocol/Handlers/Protocol18.cs`, `Protocol/Handlers/Protocol18Terrain.cs`, `Protocol/Handlers/DataTypes.cs`, `Protocol/Message/ChatParser.cs`, `Protocol/MicrosoftAuthentication.cs`, `Protocol/MojangAPI.cs` |
|
||||
| `MinecraftClient/Mapping/` | World model, terrain storage, movement logic, and versioned block/entity metadata. `World.cs` stores chunk columns, dimension data, and 1.20.6+ registry-derived dimension/attribute mappings. `Chunk*`, `Block.cs`, and `Location.cs` are the terrain primitives. `Movement.cs` contains step generation, gravity/on-ground checks, and path execution support. `Material.cs` plus `BlockPalettes/*.cs` map block-state IDs to MCC materials. `Entity.cs`, `EntityType.cs`, `EntityPalettes/*.cs`, `EntityMetadataPalette.cs`, and `EntityMetadataPalettes/*.cs` do the same for entities and metadata serializers. | `Mapping/World.cs`, `Mapping/ChunkColumn.cs`, `Mapping/Chunk.cs`, `Mapping/Block.cs`, `Mapping/Location.cs`, `Mapping/Movement.cs`, `Mapping/RaycastHelper.cs`, `Mapping/Material.cs`, `Mapping/Entity.cs`, `Mapping/EntityType.cs` |
|
||||
| `MinecraftClient/Inventory/` | Inventory/container snapshots, item decoding, and versioned item registries. `Container.cs` models player inventories and server windows, including slot contents and container properties. `Item.cs` bridges older NBT-based items with 1.20.6+ structured components. `ItemType.cs` plus `ItemPalettes/*.cs` provide version-specific item ID mapping. Enchantment, effects, and villager-trade files add higher-level semantics on top of raw inventory data. | `Inventory/Container.cs`, `Inventory/ContainerType.cs`, `Inventory/Item.cs`, `Inventory/ItemMovingHelper.cs`, `Inventory/ItemType.cs`, `Inventory/ItemPalettes/*.cs`, `Inventory/EnchantmentMapping.cs`, `Inventory/VillagerTrade.cs` |
|
||||
| `MinecraftClient/Physics/` | Vanilla-accurate per-tick physics engine. `PlayerPhysics.cs` mirrors vanilla `Entity.move()`, `LivingEntity.aiStep()/travel()`, and `Player.travel()` logic at 20 TPS, handling ground/air/water/lava/creative-fly travel, jumping, sprint-jump boost, climbing, sneak-edge-detection, friction, drag, gravity, slow-falling, and levitation. `CollisionDetector.cs` resolves full AABB collisions against the block world including step-up, mirroring vanilla axis-separated resolution. `BlockShapes.cs` maps block-state IDs to collision AABBs using PrismarineJS data from `BlockShapeData.json`. `Vec3d.cs` and `Aabb.cs` provide the geometric primitives. `MovementInput.cs` captures player input state. | `Physics/PlayerPhysics.cs`, `Physics/PhysicsConsts.cs`, `Physics/CollisionDetector.cs`, `Physics/BlockShapes.cs`, `Physics/BlockShapeData.json`, `Physics/Vec3d.cs`, `Physics/Aabb.cs`, `Physics/MovementInput.cs` |
|
||||
|
||||
### Commands And Extensions
|
||||
|
||||
| Module | What It Owns | Important Files |
|
||||
| --- | --- | --- |
|
||||
| `MinecraftClient/Commands/` and `MinecraftClient/CommandHandler/` | Internal MCC command system built on Brigadier. Commands are discovered by reflection from `MinecraftClient.Commands` in `McClient.LoadCommands()`. Each file in `Commands/` registers one internal command. `ArgumentType/*.cs` provides typed Brigadier arguments and completion sources for accounts, bots, items, locations, scripts, inventories, and more. `Patch/*.cs` carries MCC-specific Brigadier extensions, and `CmdResult.cs` is the command execution result object. | `Command.cs`, `Commands/*.cs`, `CommandHandler/MccArguments.cs`, `CommandHandler/CmdResult.cs`, `CommandHandler/ArgumentType/*.cs`, `CommandHandler/Patch/*.cs` |
|
||||
| `MinecraftClient/ChatBots/` | Built-in bots and bridges loaded from config through `McClient.RegisterBots()`. The folder mixes gameplay automation (`AutoAttack`, `AutoDig`, `AutoEat`, `AutoFishing`, `Farmer`), utility/logging bots (`ChatLog`, `PlayerListLogger`, `Alerts`), bridges (`DiscordBridge`, `TelegramBridge`, `RemoteControl`), and tooling like `ScriptScheduler`, `Map`, and `ReplayCapture`. | `ChatBots/AutoRelog.cs`, `ChatBots/Farmer.cs`, `ChatBots/FollowPlayer.cs`, `ChatBots/ItemsCollector.cs`, `ChatBots/Map.cs`, `ChatBots/RemoteControl.cs`, `ChatBots/ScriptScheduler.cs`, `ChatBots/DiscordBridge.cs`, `ChatBots/TelegramBridge.cs`, `ChatBots/ReplayCapture.cs` |
|
||||
| `MinecraftClient/Scripting/` | Shared extension boundary for compiled bots and runtime C# scripts. `ChatBot.cs` is the main bot API and lifecycle surface. Built-in bots and `/script` bots use the same event model. `CSharpRunner.cs` parses `//MCCScript` files, compiles them with Roslyn, caches assemblies, and executes them through `CSharpAPI`. `DynamicRun/Builder/*` handles in-memory compilation/load-context plumbing, while `BotMovementLock.cs` coordinates movement ownership between automation pieces. | `Scripting/ChatBot.cs`, `Scripting/CSharpRunner.cs`, `Scripting/BotMovementLock.cs`, `Scripting/AssemblyResolver.cs`, `Scripting/DynamicRun/Builder/Compiler.cs`, `Scripting/DynamicRun/Builder/CompileRunner.cs` |
|
||||
| `MinecraftClient/config/` | Sample runtime assets excluded from compilation. This is the examples/staging area for end-user scripts and standalone bots. `sample-script*.cs` shows supported `/script` patterns (basic, chatbot, world access, HTTP requests, tasks, PM forwarding, extended), while `config/ChatBots/*.cs` are copy/adapt examples rather than built-in bots. | `config/README.md`, `config/sample-script.cs`, `config/sample-script-with-chatbot.cs`, `config/sample-script-with-world-access.cs`, `config/sample-script-with-http-request.cs`, `config/sample-script-with-task.cs`, `config/ChatBots/*.cs` |
|
||||
| `ConsoleInteractive/` | Required git submodule for richer line editing and console UI. MCC uses the submodule's `ConsoleReader`, `ConsoleWriter`, and suggestion UI from `ConsoleIO.cs` and `McClient.cs` when `BasicIO` is not enabled. | `ConsoleInteractive/README.md`, `ConsoleInteractive/ConsoleInteractive/ConsoleInteractive.sln` |
|
||||
|
||||
### Support And Tooling
|
||||
|
||||
| Module | What It Owns | Important Files |
|
||||
| --- | --- | --- |
|
||||
| `MinecraftClient/Logger/`, `MinecraftClient/Proxy/`, `MinecraftClient/Crypto/`, `MinecraftClient/Resources/`, `MinecraftClient/WinAPI/` | Support subsystems under the main app. Logging supports console/file output plus regex filtering. `ProxyHandler.cs` routes update/login/in-game traffic through HTTP or SOCKS proxies. `Crypto/` implements the stream ciphers needed for online-mode protocol encryption. `Resources/` contains UI strings, generated translation accessors, config help text, icons, and embedded Minecraft asset data. `WinAPI/` contains small Windows-only console helpers. | `Logger/FilteredLogger.cs`, `Logger/FileLogLogger.cs`, `Proxy/ProxyHandler.cs`, `Crypto/CryptoHandler.cs`, `Crypto/AesCfb8Stream.cs`, `Resources/Translations/Translations.resx`, `Resources/ConfigComments/ConfigComments.resx`, `Resources/en_us.json`, `WinAPI/ConsoleIcon.cs` |
|
||||
| `docs/` | VuePress documentation site. `.vuepress/config.ts` sets bundler, theme, plugins, and redirects. `.vuepress/configs/**` holds locale and nav wiring. `guide/*.md` contains the user-facing install, usage, bot, and scripting docs. | `docs/.vuepress/config.ts`, `docs/.vuepress/configs/**`, `docs/guide/README.md`, `docs/guide/configuration.md`, `docs/guide/chat-bots.md`, `docs/guide/creating-bots.md`, `docs/guide/creating-text-script.md`, `docs/guide/ai-assisted-development.md` |
|
||||
| `tools/` | Python helpers for Minecraft version adaptation and palette generation. `README.md` is the authoritative workflow. `diff_registries.py` compares versions and validates decompiled data against server reports. The `gen_*` scripts emit the versioned palette source files consumed by `Protocol/`, `Mapping/`, `Inventory/`, and `Physics/`. | `tools/README.md`, `tools/diff_registries.py`, `tools/gen_block_palette.py`, `tools/gen_item_palette.py`, `tools/gen_entity_palette.py`, `tools/gen_entity_metadata_palette.py`, `tools/gen_block_shapes.py`, `tools/gen_command_argument_registry.py` |
|
||||
| `DebugTools/` | Standalone packet/proxy debugging utilities for inspecting traffic and compression behavior outside the main client runtime. | `DebugTools/MinecraftClientProxy/Program.cs`, `DebugTools/MinecraftClientProxy/PacketProxy.cs`, `DebugTools/MinecraftClientProxy/ZlibUtils.cs` |
|
||||
| `MinecraftClientGUI/` | Legacy Windows GUI wrapper around the console app. WinForms shell that launches and communicates with the console executable; not part of the main `net10.0` runtime path. | `MinecraftClientGUI/Program.cs`, `MinecraftClientGUI/Form1.cs`, `MinecraftClientGUI/Form1.Designer.cs`, `MinecraftClientGUI/MinecraftClient.cs` |
|
||||
|
||||
## Engineering Guidance
|
||||
|
||||
Read `docs/guide/ai-assisted-development.md` before starting development work on MCC. It documents the full build-run-test loop, local server harness, repository tools, and standard workflows.
|
||||
|
||||
### DO
|
||||
- Keep startup/config/auth logic in `Program` and connection runtime logic in `McClient` or `Protocol/*`.
|
||||
- Update version support holistically: protocol constants, version mapping, packet palette, block palette, item palette, entity palette, metadata palette, and routing switches.
|
||||
- Use `tools/` and authoritative server data reports when adapting to new Minecraft versions.
|
||||
- Guard optional subsystems with `GetTerrainEnabled()`, `GetInventoryEnabled()`, and `GetEntityHandlingEnabled()` before using them.
|
||||
- For built-in bots, wire all pieces together: bot class, `Settings.ChatBotConfigHealper`, and `McClient.RegisterBots()`.
|
||||
- Keep `Initialize()` for setup/prereq checks and `AfterGameJoined()` for sending chat or commands.
|
||||
- Normalize inbound chat with `GetVerbatim()` before `IsChatMessage()` / `IsPrivateMessage()`.
|
||||
- Clean up commands, plugin channels, threads, timers, and movement locks in `OnUnload()`.
|
||||
- Prefer nullable-aware code, pattern matching, `ArgumentNullException.ThrowIfNull`, `Try*` APIs for expected failures, and `InvokeOnMainThread()` for cross-thread state changes.
|
||||
- Use modern C# 14 features.
|
||||
- Use provided skills proactively depending on the context, read their descriptions to determine when to use them.
|
||||
- All user-facing text (log messages, command output, TUI labels, notifications, help text, error messages) **must** go through the translation system: add entries to `Translations.resx` + `Translations.Designer.cs`, then reference `Translations.key_name` in code. Never hardcode user-visible strings directly in `.cs` files. Use `string.Format(Translations.key, ...)` for parameterized messages. Translation keys follow dot-delimited naming: `<module>.<scope>.<detail>` (e.g. `cmd.inventory.tui_opened`, `tui.inventory.controls`). Pure technical identifiers (class names, protocol constants, color codes) are exempt.
|
||||
|
||||
### DON'T
|
||||
- Don't update only `MCVer2ProtocolVersion()` or only one palette file when adding a new Minecraft version.
|
||||
- Don't send chat in `Initialize()`.
|
||||
- Don't mutate inventory snapshots and expect server-side effects; use handler APIs/window actions.
|
||||
- Don't bypass Brigadier with ad hoc command parsing.
|
||||
- Never modify `ConsoleInteractive/`; treat it as an external required submodule.
|
||||
- Don't start background workers when `Update()` or delayed tasks are sufficient; if you must, stop them on unload/disconnect.
|
||||
- Don't leave movement locks, plugin channels, or dispatcher registrations behind.
|
||||
- Don't trust older docs over current code for supported versions or feature gates. When AGENTS.md, skills, and older docs disagree, prefer current code and current tool behavior, then update the stale source.
|
||||
- Don't hardcode user-facing strings (messages, labels, help text) directly in source code; always use `Translations.*` resources so the text can be localized.
|
||||
- Never use "—" ("em dash"), unless specifically being instructed to do so!
|
||||
- Never generate MCC config files (`.ini`) in the repo root. When `dotnet run --project MinecraftClient -- --help` is used to generate a config template, it writes `MinecraftClient.ini` to the current directory. Always run this command from a system temp directory (e.g. `mktemp -d`) or use `prepare_offline_mcc_config.sh` which already handles output routing.
|
||||
1
CLAUDE.md
Normal file
1
CLAUDE.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Read @AGENTS.md
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit db2be2a7f8ea71c734ebeff6314fd2fdec73f4fc
|
||||
Subproject commit ff6d2129e9f1e0fc6c7032741bdc42a2f0fa263e
|
||||
14
DebugTools/MccMcpSampleClient/MccMcpSampleClient.csproj
Normal file
14
DebugTools/MccMcpSampleClient/MccMcpSampleClient.csproj
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
773
DebugTools/MccMcpSampleClient/Program.cs
Normal file
773
DebugTools/MccMcpSampleClient/Program.cs
Normal file
|
|
@ -0,0 +1,773 @@
|
|||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
string endpoint = Environment.GetEnvironmentVariable("MCC_MCP_ENDPOINT") ?? "http://127.0.0.1:33333/mcp";
|
||||
bool useStdio = string.Equals(Environment.GetEnvironmentVariable("MCC_MCP_USE_STDIO"), "1", StringComparison.Ordinal);
|
||||
string? mcpAuthToken = Environment.GetEnvironmentVariable("MCC_MCP_AUTH_TOKEN");
|
||||
string repoRoot = FindRepoRoot();
|
||||
string rconScript = Path.Combine(repoRoot, "tools", "mc-rcon.sh");
|
||||
string rconPort = Environment.GetEnvironmentVariable("MCC_RCON_PORT") ?? "25575";
|
||||
string rconPassword = Environment.GetEnvironmentVariable("MCC_RCON_PASSWORD") ?? "test123";
|
||||
bool skipSetup = string.Equals(Environment.GetEnvironmentVariable("MCC_MCP_SKIP_SETUP"), "1", StringComparison.Ordinal);
|
||||
bool runLocalSetup = !useStdio && !skipSetup && IsLocalEndpoint(endpoint) && File.Exists(rconScript);
|
||||
var executed = new List<object>();
|
||||
var checks = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
await using McpClient client = useStdio
|
||||
? await McpClient.CreateAsync(new StdioClientTransport(CreateStdioOptions()))
|
||||
: await McpClient.CreateAsync(new HttpClientTransport(new HttpClientTransportOptions
|
||||
{
|
||||
Endpoint = new Uri(endpoint),
|
||||
TransportMode = HttpTransportMode.AutoDetect,
|
||||
AdditionalHeaders = string.IsNullOrWhiteSpace(mcpAuthToken)
|
||||
? null
|
||||
: new Dictionary<string, string> { ["Authorization"] = $"Bearer {mcpAuthToken}" }
|
||||
}));
|
||||
|
||||
ToolEnvelope initialWorldState = await CallSuccessAsync(client, executed, "mcc_world_state");
|
||||
JsonElement initialWorldData = RequireData(initialWorldState);
|
||||
string botName = ReadString(initialWorldData, "username") ?? "MCCBot";
|
||||
Coordinate initialLocation = ReadCoordinate(initialWorldData, "location");
|
||||
|
||||
long setupBaseline = 0;
|
||||
if (runLocalSetup)
|
||||
{
|
||||
ToolEnvelope baselineEvents = await CallSuccessAsync(client, executed, "mcc_recent_events", new Dictionary<string, object?>
|
||||
{
|
||||
["afterId"] = 0L,
|
||||
["maxCount"] = 1
|
||||
});
|
||||
setupBaseline = ReadInt64(RequireData(baselineEvents), "latestId");
|
||||
|
||||
await PrepareWorldAsync(rconScript, rconPort, rconPassword, botName, initialLocation);
|
||||
await Task.Delay(1500);
|
||||
}
|
||||
|
||||
ToolEnvelope worldState = await WaitForPredicateAsync(
|
||||
client,
|
||||
executed,
|
||||
"mcc_world_state",
|
||||
null,
|
||||
envelope =>
|
||||
{
|
||||
if (!envelope.Success || envelope.Data is not JsonElement data)
|
||||
return false;
|
||||
|
||||
return data.TryGetProperty("loadedChunkCount", out JsonElement loaded)
|
||||
&& loaded.TryGetInt32(out int loadedChunkCount)
|
||||
&& loadedChunkCount >= 0
|
||||
&& HasNonNullProperty(data, "worldAge")
|
||||
&& HasNonNullProperty(data, "timeOfDay");
|
||||
},
|
||||
"mcc_world_state never reported chunk/time state.");
|
||||
|
||||
JsonElement worldData = RequireData(worldState);
|
||||
Coordinate worldLocation = ReadCoordinate(worldData, "location");
|
||||
string dimension = RequireString(worldData, "dimension");
|
||||
int loadedChunkCount = ReadInt32(worldData, "loadedChunkCount");
|
||||
int pendingChunkCount = ReadInt32(worldData, "pendingChunkCount");
|
||||
int totalChunkCount = ReadInt32(worldData, "totalChunkCount");
|
||||
double loadRatio = ReadDouble(worldData, "loadRatio");
|
||||
_ = RequireString(worldData, "host");
|
||||
_ = ReadInt32(worldData, "port");
|
||||
_ = RequireString(worldData, "username");
|
||||
_ = ReadInt32(worldData, "protocol");
|
||||
_ = ReadDouble(worldData, "tps");
|
||||
Ensure(!string.IsNullOrWhiteSpace(dimension), "mcc_world_state returned an empty dimension.");
|
||||
Ensure(loadedChunkCount + pendingChunkCount == totalChunkCount, "mcc_world_state chunk counters are inconsistent.");
|
||||
Ensure(loadRatio is >= 0 and <= 1, "mcc_world_state loadRatio is out of range.");
|
||||
Ensure(HasNonNullProperty(worldData, "worldAge"), "mcc_world_state.worldAge is null.");
|
||||
Ensure(HasNonNullProperty(worldData, "timeOfDay"), "mcc_world_state.timeOfDay is null.");
|
||||
if (runLocalSetup || useStdio)
|
||||
{
|
||||
Ensure(HasNonNullProperty(worldData, "rainLevel"), "mcc_world_state.rainLevel is null after setup.");
|
||||
Ensure(HasNonNullProperty(worldData, "thunderLevel"), "mcc_world_state.thunderLevel is null after setup.");
|
||||
}
|
||||
checks.Add("mcc_world_state");
|
||||
|
||||
ToolEnvelope chunkStatus = await CallSuccessAsync(client, executed, "mcc_chunk_status");
|
||||
JsonElement chunkData = RequireData(chunkStatus);
|
||||
JsonElement chunk = RequireProperty(chunkData, "chunk");
|
||||
_ = ReadInt32(chunk, "x");
|
||||
_ = ReadInt32(chunk, "z");
|
||||
Ensure(ReadBoolean(chunkData, "loaded"), "mcc_chunk_status reported the current chunk as unloaded.");
|
||||
Ensure(ReadInt32(chunkData, "loadedChunkCount") + ReadInt32(chunkData, "pendingChunkCount") == ReadInt32(chunkData, "totalChunkCount"),
|
||||
"mcc_chunk_status chunk counters are inconsistent.");
|
||||
checks.Add("mcc_chunk_status");
|
||||
|
||||
await CallSuccessAsync(client, executed, "mcc_look_direction", new Dictionary<string, object?> { ["direction"] = "Down" });
|
||||
ToolEnvelope raycast = await CallSuccessAsync(client, executed, "mcc_raycast_block", new Dictionary<string, object?>
|
||||
{
|
||||
["maxDistance"] = 8.0,
|
||||
["includeNeighbors"] = true
|
||||
});
|
||||
JsonElement raycastData = RequireData(raycast);
|
||||
Ensure(ReadBoolean(raycastData, "hit"), "mcc_raycast_block did not report a hit after looking down.");
|
||||
JsonElement raycastBlock = RequireProperty(raycastData, "block");
|
||||
Ensure(!string.Equals(RequireString(raycastBlock, "material"), "Air", StringComparison.OrdinalIgnoreCase),
|
||||
"mcc_raycast_block hit Air instead of a solid block.");
|
||||
Ensure(RequireProperty(raycastData, "neighbors").ValueKind == JsonValueKind.Object,
|
||||
"mcc_raycast_block did not include neighbors when requested.");
|
||||
checks.Add("mcc_raycast_block");
|
||||
|
||||
ToolEnvelope pathPreview = await CallSuccessAsync(client, executed, "mcc_path_preview", new Dictionary<string, object?>
|
||||
{
|
||||
["x"] = Math.Floor(worldLocation.X) + 2,
|
||||
["y"] = worldLocation.Y,
|
||||
["z"] = Math.Floor(worldLocation.Z),
|
||||
["allowUnsafe"] = false,
|
||||
["timeoutMs"] = 2000,
|
||||
["maxWaypoints"] = 32
|
||||
});
|
||||
JsonElement pathData = RequireData(pathPreview);
|
||||
Ensure(ReadBoolean(pathData, "pathFound"), "mcc_path_preview did not find a path to a nearby target.");
|
||||
Ensure(RequireProperty(pathData, "waypoints").GetArrayLength() > 0, "mcc_path_preview returned no waypoints.");
|
||||
checks.Add("mcc_path_preview");
|
||||
|
||||
ToolEnvelope stoneSearch = await WaitForPredicateAsync(
|
||||
client,
|
||||
executed,
|
||||
"mcc_inventory_search",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["query"] = "Stone",
|
||||
["maxCount"] = 20,
|
||||
["exactMatch"] = true,
|
||||
["includeContainers"] = false
|
||||
},
|
||||
envelope => envelope.Success && envelope.Data is JsonElement data && ReadInt32(data, "count") > 0,
|
||||
"mcc_inventory_search never found Stone in the player inventory.");
|
||||
Ensure(ContainsItemType(RequireData(stoneSearch), "Stone"), "mcc_inventory_search results did not include Stone.");
|
||||
|
||||
ToolEnvelope swordSearch = await WaitForPredicateAsync(
|
||||
client,
|
||||
executed,
|
||||
"mcc_inventory_search",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["query"] = "DiamondSword",
|
||||
["maxCount"] = 20,
|
||||
["exactMatch"] = true,
|
||||
["includeContainers"] = false
|
||||
},
|
||||
envelope => envelope.Success && envelope.Data is JsonElement data && ReadInt32(data, "count") > 0,
|
||||
"mcc_inventory_search never found DiamondSword in the player inventory.");
|
||||
Ensure(ContainsItemType(RequireData(swordSearch), "DiamondSword"), "mcc_inventory_search results did not include DiamondSword.");
|
||||
checks.Add("mcc_inventory_search");
|
||||
|
||||
ToolEnvelope selectItem = await CallSuccessAsync(client, executed, "mcc_select_item", new Dictionary<string, object?>
|
||||
{
|
||||
["itemType"] = "DiamondSword",
|
||||
["preferLowestSlot"] = true
|
||||
});
|
||||
JsonElement selectData = RequireData(selectItem);
|
||||
int selectedSlot = ReadInt32(selectData, "selectedSlot");
|
||||
|
||||
ToolEnvelope playerStats = await CallSuccessAsync(client, executed, "mcc_player_stats");
|
||||
JsonElement playerStatsData = RequireData(playerStats);
|
||||
Ensure(ReadInt32(playerStatsData, "currentSlot") == selectedSlot, "mcc_select_item did not update mcc_player_stats.currentSlot.");
|
||||
_ = ReadInt32(playerStatsData, "playerEntityId");
|
||||
_ = ReadInt32(playerStatsData, "level");
|
||||
_ = ReadInt32(playerStatsData, "totalExperience");
|
||||
_ = ReadCoordinate(playerStatsData, "location");
|
||||
checks.Add("mcc_select_item");
|
||||
checks.Add("mcc_player_stats");
|
||||
|
||||
ToolEnvelope playersDetailed = await CallSuccessAsync(client, executed, "mcc_players_detailed", new Dictionary<string, object?>
|
||||
{
|
||||
["includeSelf"] = true,
|
||||
["includeCoordinates"] = true
|
||||
});
|
||||
JsonElement playersData = RequireData(playersDetailed);
|
||||
JsonElement selfPlayer = FindPlayer(RequireProperty(playersData, "players"), botName);
|
||||
_ = RequireString(selfPlayer, "uuid");
|
||||
_ = ReadInt32(selfPlayer, "ping");
|
||||
_ = ReadInt32(selfPlayer, "entityId");
|
||||
_ = ReadDouble(selfPlayer, "x");
|
||||
_ = ReadDouble(selfPlayer, "y");
|
||||
_ = ReadDouble(selfPlayer, "z");
|
||||
checks.Add("mcc_players_detailed");
|
||||
|
||||
ToolEnvelope statusEffects = await CallSuccessAsync(client, executed, "mcc_status_effects");
|
||||
Ensure(RequireProperty(RequireData(statusEffects), "effects").ValueKind == JsonValueKind.Array,
|
||||
"mcc_status_effects.effects is not an array.");
|
||||
checks.Add("mcc_status_effects");
|
||||
|
||||
ToolEnvelope animation = await CallSuccessAsync(client, executed, "mcc_animation", new Dictionary<string, object?>
|
||||
{
|
||||
["hand"] = "MainHand"
|
||||
});
|
||||
Ensure(ReadBoolean(RequireData(animation), "success"), "mcc_animation did not report success.");
|
||||
|
||||
ToolEnvelope sneakOn = await CallSuccessAsync(client, executed, "mcc_toggle_sneak", new Dictionary<string, object?> { ["enabled"] = true });
|
||||
Ensure(ReadBoolean(RequireData(sneakOn), "enabled"), "mcc_toggle_sneak(true) did not report enabled=true.");
|
||||
|
||||
ToolEnvelope sprintOn = await CallSuccessAsync(client, executed, "mcc_toggle_sprint", new Dictionary<string, object?> { ["enabled"] = true });
|
||||
Ensure(ReadBoolean(RequireData(sprintOn), "enabled"), "mcc_toggle_sprint(true) did not report enabled=true.");
|
||||
|
||||
await CallSuccessAsync(client, executed, "mcc_look_angles", new Dictionary<string, object?>
|
||||
{
|
||||
["yaw"] = 45.0f,
|
||||
["pitch"] = -15.0f
|
||||
});
|
||||
ToolEnvelope updatedStats = await CallSuccessAsync(client, executed, "mcc_player_stats");
|
||||
JsonElement updatedStatsData = RequireData(updatedStats);
|
||||
Ensure(Math.Abs(ReadDouble(updatedStatsData, "yaw") - 45.0) < 0.01, "mcc_look_angles did not update yaw.");
|
||||
Ensure(Math.Abs(ReadDouble(updatedStatsData, "pitch") - (-15.0)) < 0.01, "mcc_look_angles did not update pitch.");
|
||||
checks.Add("mcc_animation");
|
||||
checks.Add("mcc_toggle_sneak");
|
||||
checks.Add("mcc_toggle_sprint");
|
||||
checks.Add("mcc_look_direction");
|
||||
checks.Add("mcc_look_angles");
|
||||
|
||||
ToolEnvelope nearestEntity = await WaitForPredicateAsync(
|
||||
client,
|
||||
executed,
|
||||
"mcc_entity_nearest",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["typeFilter"] = "ArmorStand",
|
||||
["radius"] = 16.0,
|
||||
["includePlayers"] = false
|
||||
},
|
||||
envelope => envelope.Success,
|
||||
"mcc_entity_nearest never found a nearby ArmorStand.");
|
||||
JsonElement nearestData = RequireData(nearestEntity);
|
||||
int entityId = ReadInt32(nearestData, "id");
|
||||
Ensure(string.Equals(RequireString(nearestData, "type"), "ArmorStand", StringComparison.OrdinalIgnoreCase),
|
||||
"mcc_entity_nearest did not return an ArmorStand.");
|
||||
|
||||
ToolEnvelope attackEntity = await CallSuccessAsync(client, executed, "mcc_entity_attack", new Dictionary<string, object?>
|
||||
{
|
||||
["entityId"] = entityId
|
||||
});
|
||||
Ensure(ReadBoolean(RequireData(attackEntity), "success"), "mcc_entity_attack did not report success.");
|
||||
checks.Add("mcc_entity_nearest");
|
||||
checks.Add("mcc_entity_attack");
|
||||
|
||||
long recentSetupAfterId = runLocalSetup ? setupBaseline : 0;
|
||||
if (runLocalSetup || useStdio)
|
||||
{
|
||||
ToolEnvelope setupEvents = await WaitForRecentEventTypesAsync(
|
||||
client,
|
||||
executed,
|
||||
recentSetupAfterId,
|
||||
"weather_rain",
|
||||
"title",
|
||||
"actionbar");
|
||||
JsonElement setupEventsData = RequireData(setupEvents);
|
||||
Ensure(GetEventTypes(setupEventsData).Contains("weather_rain", StringComparer.OrdinalIgnoreCase), "mcc_recent_events did not include weather_rain.");
|
||||
Ensure(GetEventTypes(setupEventsData).Contains("title", StringComparer.OrdinalIgnoreCase), "mcc_recent_events did not include title.");
|
||||
Ensure(GetEventTypes(setupEventsData).Contains("actionbar", StringComparer.OrdinalIgnoreCase), "mcc_recent_events did not include actionbar.");
|
||||
}
|
||||
|
||||
ToolEnvelope actionbarEvents = await CallSuccessAsync(client, executed, "mcc_recent_events", new Dictionary<string, object?>
|
||||
{
|
||||
["afterId"] = 0L,
|
||||
["maxCount"] = 20,
|
||||
["typeFilter"] = "actionbar"
|
||||
});
|
||||
JsonElement actionbarData = RequireData(actionbarEvents);
|
||||
Ensure(ReadInt32(actionbarData, "count") > 0, "mcc_recent_events typeFilter=actionbar returned no events.");
|
||||
Ensure(AllEventsMatchType(actionbarData, "actionbar"), "mcc_recent_events typeFilter returned mixed event types.");
|
||||
|
||||
long inventoryBaseline = ReadInt64(actionbarData, "latestId");
|
||||
int chestX = (int)Math.Floor(worldLocation.X) + 2;
|
||||
int chestY = (int)Math.Floor(worldLocation.Y);
|
||||
int chestZ = (int)Math.Floor(worldLocation.Z);
|
||||
ToolEnvelope openContainer = await WaitForPredicateAsync(
|
||||
client,
|
||||
executed,
|
||||
"mcc_container_open_at",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["x"] = chestX,
|
||||
["y"] = chestY,
|
||||
["z"] = chestZ,
|
||||
["timeoutMs"] = 3000,
|
||||
["closeCurrent"] = true
|
||||
},
|
||||
envelope => envelope.Success,
|
||||
"mcc_open_container_at never opened the nearby chest.");
|
||||
JsonElement inventoryInfo = RequireProperty(RequireData(openContainer), "inventory");
|
||||
int openedInventoryId = ReadInt32(inventoryInfo, "id");
|
||||
ToolEnvelope closeContainer = await CallSuccessAsync(client, executed, "mcc_container_close", new Dictionary<string, object?>
|
||||
{
|
||||
["inventoryId"] = openedInventoryId,
|
||||
["timeoutMs"] = 3000
|
||||
});
|
||||
Ensure(ReadBoolean(RequireData(closeContainer), "closed"), "mcc_close_container did not close the chest.");
|
||||
|
||||
ToolEnvelope inventoryEvents = await WaitForRecentEventTypesAsync(
|
||||
client,
|
||||
executed,
|
||||
inventoryBaseline,
|
||||
"inventory_open",
|
||||
"inventory_close");
|
||||
JsonElement inventoryEventsData = RequireData(inventoryEvents);
|
||||
Ensure(GetEventTypes(inventoryEventsData).Contains("inventory_open", StringComparer.OrdinalIgnoreCase), "mcc_recent_events did not include inventory_open.");
|
||||
Ensure(GetEventTypes(inventoryEventsData).Contains("inventory_close", StringComparer.OrdinalIgnoreCase), "mcc_recent_events did not include inventory_close.");
|
||||
checks.Add("mcc_recent_events");
|
||||
|
||||
if (runLocalSetup)
|
||||
{
|
||||
long deathBaseline = ReadInt64(inventoryEventsData, "latestId");
|
||||
await RunRconCommandAsync(rconScript, rconPort, rconPassword, $"kill {botName}");
|
||||
ToolEnvelope deathEvents = await WaitForRecentEventTypesAsync(client, executed, deathBaseline, "death");
|
||||
Ensure(GetEventTypes(RequireData(deathEvents)).Contains("death", StringComparer.OrdinalIgnoreCase),
|
||||
"mcc_recent_events never reported death after the RCON kill.");
|
||||
|
||||
long respawnBaseline = ReadInt64(RequireData(deathEvents), "latestId");
|
||||
ToolEnvelope respawn = await CallSuccessAsync(client, executed, "mcc_respawn");
|
||||
Ensure(ReadBoolean(RequireData(respawn), "success"), "mcc_respawn did not report success.");
|
||||
ToolEnvelope respawnEvents = await WaitForRecentEventTypesAsync(client, executed, respawnBaseline, "respawn");
|
||||
Ensure(GetEventTypes(RequireData(respawnEvents)).Contains("respawn", StringComparer.OrdinalIgnoreCase),
|
||||
"mcc_recent_events never reported respawn after mcc_respawn.");
|
||||
checks.Add("mcc_respawn");
|
||||
}
|
||||
else
|
||||
{
|
||||
long respawnBaseline = ReadInt64(RequireData(inventoryEvents), "latestId");
|
||||
ToolEnvelope respawn = await CallSuccessAsync(client, executed, "mcc_respawn");
|
||||
Ensure(ReadBoolean(RequireData(respawn), "success"), "mcc_respawn did not report success.");
|
||||
ToolEnvelope respawnEvents = await WaitForRecentEventTypesAsync(client, executed, respawnBaseline, "respawn");
|
||||
Ensure(GetEventTypes(RequireData(respawnEvents)).Contains("respawn", StringComparer.OrdinalIgnoreCase),
|
||||
"mcc_recent_events never reported respawn after mcc_respawn.");
|
||||
checks.Add("mcc_respawn");
|
||||
}
|
||||
|
||||
ToolEnvelope loadedBots = await CallSuccessAsync(client, executed, "mcc_loaded_bots");
|
||||
JsonElement bots = RequireProperty(RequireData(loadedBots), "bots");
|
||||
Ensure(ContainsBot(bots, "McpServer"), "mcc_loaded_bots did not include McpServer.");
|
||||
checks.Add("mcc_loaded_bots");
|
||||
|
||||
ToolEnvelope disconnect = await CallSuccessAsync(client, executed, "mcc_disconnect");
|
||||
Ensure(ReadBoolean(RequireData(disconnect), "disconnecting"), "mcc_disconnect did not report disconnecting=true.");
|
||||
checks.Add("mcc_disconnect");
|
||||
|
||||
if (!useStdio)
|
||||
{
|
||||
await AssertDisconnectStopsEndpointAsync(client, executed);
|
||||
}
|
||||
|
||||
Console.WriteLine(JsonSerializer.Serialize(new
|
||||
{
|
||||
success = true,
|
||||
endpoint,
|
||||
useStdio,
|
||||
runLocalSetup,
|
||||
checks,
|
||||
executed
|
||||
}, new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
Environment.ExitCode = 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(new
|
||||
{
|
||||
success = false,
|
||||
endpoint,
|
||||
useStdio,
|
||||
runLocalSetup,
|
||||
error = ex.Message,
|
||||
checks,
|
||||
executed
|
||||
}, new JsonSerializerOptions { WriteIndented = true }));
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
|
||||
static async Task PrepareWorldAsync(string rconScript, string rconPort, string rconPassword, string botName, Coordinate location)
|
||||
{
|
||||
string[] commands =
|
||||
[
|
||||
$"op {botName}",
|
||||
$"gamemode creative {botName}",
|
||||
$"tp {botName} 0 80 0",
|
||||
$"item replace entity {botName} hotbar.0 with minecraft:stone 32",
|
||||
$"item replace entity {botName} hotbar.1 with minecraft:diamond_sword 1",
|
||||
$"execute as {botName} at @s run setblock ~2 ~ ~ minecraft:chest",
|
||||
$"execute as {botName} at @s run summon minecraft:armor_stand ~2 ~ ~1",
|
||||
"weather clear",
|
||||
"weather rain",
|
||||
$"title {botName} title {{\"text\":\"mcp_title\"}}",
|
||||
$"title {botName} actionbar {{\"text\":\"mcp_actionbar\"}}"
|
||||
];
|
||||
|
||||
foreach (string command in commands)
|
||||
{
|
||||
await RunRconCommandAsync(rconScript, rconPort, rconPassword, command);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task RunRconCommandAsync(string rconScript, string rconPort, string rconPassword, string command)
|
||||
{
|
||||
ProcessStartInfo startInfo = new("bash")
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
startInfo.ArgumentList.Add(rconScript);
|
||||
startInfo.ArgumentList.Add(command);
|
||||
startInfo.ArgumentList.Add(rconPort);
|
||||
startInfo.ArgumentList.Add(rconPassword);
|
||||
|
||||
using Process process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start mc-rcon.sh.");
|
||||
string stdout = await process.StandardOutput.ReadToEndAsync();
|
||||
string stderr = await process.StandardError.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"RCON command failed ({command}): {(string.IsNullOrWhiteSpace(stderr) ? stdout : stderr).Trim()}");
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<ToolEnvelope> CallSuccessAsync(
|
||||
McpClient client,
|
||||
List<object> executed,
|
||||
string toolName,
|
||||
IReadOnlyDictionary<string, object?>? args = null)
|
||||
{
|
||||
ToolEnvelope envelope = await CallToolAsync(client, executed, toolName, args);
|
||||
if (!envelope.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{toolName} failed with errorCode={envelope.ErrorCode ?? "<null>"} message={envelope.Message ?? "<null>"}.");
|
||||
}
|
||||
|
||||
return envelope;
|
||||
}
|
||||
|
||||
static async Task<ToolEnvelope> WaitForPredicateAsync(
|
||||
McpClient client,
|
||||
List<object> executed,
|
||||
string toolName,
|
||||
IReadOnlyDictionary<string, object?>? args,
|
||||
Func<ToolEnvelope, bool> predicate,
|
||||
string failureMessage,
|
||||
int maxAttempts = 12,
|
||||
int delayMs = 400)
|
||||
{
|
||||
ToolEnvelope? lastEnvelope = null;
|
||||
for (int attempt = 0; attempt < maxAttempts; attempt++)
|
||||
{
|
||||
ToolEnvelope envelope = await CallToolAsync(client, executed, toolName, args);
|
||||
lastEnvelope = envelope;
|
||||
if (predicate(envelope))
|
||||
return envelope;
|
||||
|
||||
await Task.Delay(delayMs);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"{failureMessage} Last result: success={lastEnvelope?.Success}, errorCode={lastEnvelope?.ErrorCode ?? "<null>"}.");
|
||||
}
|
||||
|
||||
static async Task<ToolEnvelope> WaitForRecentEventTypesAsync(
|
||||
McpClient client,
|
||||
List<object> executed,
|
||||
long afterId,
|
||||
params string[] expectedTypes)
|
||||
{
|
||||
HashSet<string> expected = expectedTypes.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
ToolEnvelope? lastEnvelope = null;
|
||||
|
||||
for (int attempt = 0; attempt < 12; attempt++)
|
||||
{
|
||||
ToolEnvelope envelope = await CallSuccessAsync(client, executed, "mcc_recent_events", new Dictionary<string, object?>
|
||||
{
|
||||
["afterId"] = afterId,
|
||||
["maxCount"] = 100
|
||||
});
|
||||
lastEnvelope = envelope;
|
||||
JsonElement data = RequireData(envelope);
|
||||
HashSet<string> actual = GetEventTypes(data);
|
||||
if (expected.All(actual.Contains))
|
||||
return envelope;
|
||||
|
||||
await Task.Delay(400);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"mcc_recent_events never reported: {string.Join(", ", expectedTypes)} after event id {afterId}. Last latestId={ReadInt64(RequireData(lastEnvelope!), "latestId")}.");
|
||||
}
|
||||
|
||||
static async Task<ToolEnvelope> CallToolAsync(
|
||||
McpClient client,
|
||||
List<object> executed,
|
||||
string toolName,
|
||||
IReadOnlyDictionary<string, object?>? args = null)
|
||||
{
|
||||
CallToolResult result = await client.CallToolAsync(toolName, args);
|
||||
string responseJson = ExtractResponseJson(result);
|
||||
JsonElement root = JsonDocument.Parse(responseJson).RootElement.Clone();
|
||||
JsonElement? data = root.TryGetProperty("data", out JsonElement dataElement) ? dataElement.Clone() : null;
|
||||
bool success = root.TryGetProperty("success", out JsonElement successElement)
|
||||
&& successElement.ValueKind == JsonValueKind.True;
|
||||
string? errorCode = ReadString(root, "errorCode");
|
||||
string? message = ReadString(root, "message");
|
||||
|
||||
executed.Add(new
|
||||
{
|
||||
tool = toolName,
|
||||
arguments = args,
|
||||
isError = result.IsError,
|
||||
success,
|
||||
errorCode,
|
||||
message,
|
||||
response = root
|
||||
});
|
||||
|
||||
return new ToolEnvelope(toolName, result.IsError ?? false, success, errorCode, message, root, data);
|
||||
}
|
||||
|
||||
static async Task AssertDisconnectStopsEndpointAsync(McpClient client, List<object> executed)
|
||||
{
|
||||
for (int attempt = 0; attempt < 15; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
await CallToolAsync(client, executed, "mcc_world_state");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(300);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("The MCP endpoint still responded after mcc_disconnect.");
|
||||
}
|
||||
|
||||
static string ExtractResponseJson(CallToolResult result)
|
||||
{
|
||||
if (result.Content is null)
|
||||
throw new InvalidOperationException("Tool response did not contain any content blocks.");
|
||||
|
||||
foreach (ContentBlock content in result.Content)
|
||||
{
|
||||
if (content is TextContentBlock text && !string.IsNullOrWhiteSpace(text.Text))
|
||||
return text.Text;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Tool response did not contain a text payload.");
|
||||
}
|
||||
|
||||
static JsonElement RequireData(ToolEnvelope envelope)
|
||||
{
|
||||
if (envelope.Data is JsonElement data)
|
||||
return data;
|
||||
|
||||
throw new InvalidOperationException($"{envelope.ToolName} returned no data payload.");
|
||||
}
|
||||
|
||||
static JsonElement RequireProperty(JsonElement element, string propertyName)
|
||||
{
|
||||
if (element.TryGetProperty(propertyName, out JsonElement property))
|
||||
return property;
|
||||
|
||||
throw new InvalidOperationException($"Missing required property '{propertyName}'.");
|
||||
}
|
||||
|
||||
static string RequireString(JsonElement element, string propertyName)
|
||||
{
|
||||
string? value = ReadString(element, propertyName);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
return value;
|
||||
|
||||
throw new InvalidOperationException($"Property '{propertyName}' is missing or empty.");
|
||||
}
|
||||
|
||||
static string? ReadString(JsonElement element, string propertyName)
|
||||
{
|
||||
return element.TryGetProperty(propertyName, out JsonElement property) && property.ValueKind == JsonValueKind.String
|
||||
? property.GetString()
|
||||
: null;
|
||||
}
|
||||
|
||||
static int ReadInt32(JsonElement element, string propertyName)
|
||||
{
|
||||
JsonElement property = RequireProperty(element, propertyName);
|
||||
if (property.TryGetInt32(out int value))
|
||||
return value;
|
||||
|
||||
throw new InvalidOperationException($"Property '{propertyName}' is not an Int32.");
|
||||
}
|
||||
|
||||
static long ReadInt64(JsonElement element, string propertyName)
|
||||
{
|
||||
JsonElement property = RequireProperty(element, propertyName);
|
||||
if (property.TryGetInt64(out long value))
|
||||
return value;
|
||||
|
||||
throw new InvalidOperationException($"Property '{propertyName}' is not an Int64.");
|
||||
}
|
||||
|
||||
static double ReadDouble(JsonElement element, string propertyName)
|
||||
{
|
||||
JsonElement property = RequireProperty(element, propertyName);
|
||||
if (property.TryGetDouble(out double value))
|
||||
return value;
|
||||
|
||||
throw new InvalidOperationException($"Property '{propertyName}' is not a Double.");
|
||||
}
|
||||
|
||||
static bool ReadBoolean(JsonElement element, string propertyName)
|
||||
{
|
||||
JsonElement property = RequireProperty(element, propertyName);
|
||||
return property.ValueKind switch
|
||||
{
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
_ => throw new InvalidOperationException($"Property '{propertyName}' is not a Boolean.")
|
||||
};
|
||||
}
|
||||
|
||||
static bool HasNonNullProperty(JsonElement element, string propertyName)
|
||||
{
|
||||
return element.TryGetProperty(propertyName, out JsonElement property) && property.ValueKind != JsonValueKind.Null;
|
||||
}
|
||||
|
||||
static Coordinate ReadCoordinate(JsonElement element, string propertyName)
|
||||
{
|
||||
JsonElement coordinate = RequireProperty(element, propertyName);
|
||||
return new Coordinate(
|
||||
ReadDouble(coordinate, "x"),
|
||||
ReadDouble(coordinate, "y"),
|
||||
ReadDouble(coordinate, "z"));
|
||||
}
|
||||
|
||||
static JsonElement FindPlayer(JsonElement players, string playerName)
|
||||
{
|
||||
foreach (JsonElement player in players.EnumerateArray())
|
||||
{
|
||||
string? name = ReadString(player, "name");
|
||||
if (string.Equals(name, playerName, StringComparison.OrdinalIgnoreCase))
|
||||
return player;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Could not find player '{playerName}' in mcc_players_detailed.");
|
||||
}
|
||||
|
||||
static bool ContainsItemType(JsonElement searchData, string itemType)
|
||||
{
|
||||
JsonElement matches = RequireProperty(searchData, "matches");
|
||||
foreach (JsonElement match in matches.EnumerateArray())
|
||||
{
|
||||
if (string.Equals(ReadString(match, "itemType"), itemType, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ContainsBot(JsonElement bots, string botName)
|
||||
{
|
||||
foreach (JsonElement bot in bots.EnumerateArray())
|
||||
{
|
||||
if (string.Equals(ReadString(bot, "name"), botName, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static HashSet<string> GetEventTypes(JsonElement recentEventsData)
|
||||
{
|
||||
JsonElement events = RequireProperty(recentEventsData, "events");
|
||||
return events.EnumerateArray()
|
||||
.Select(entry => RequireString(entry, "type"))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
static bool AllEventsMatchType(JsonElement recentEventsData, string type)
|
||||
{
|
||||
JsonElement events = RequireProperty(recentEventsData, "events");
|
||||
foreach (JsonElement entry in events.EnumerateArray())
|
||||
{
|
||||
if (!string.Equals(RequireString(entry, "type"), type, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void Ensure(bool condition, string message)
|
||||
{
|
||||
if (!condition)
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
static bool IsLocalEndpoint(string endpoint)
|
||||
{
|
||||
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri))
|
||||
return false;
|
||||
|
||||
return string.Equals(uri.Host, "localhost", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Host, "127.0.0.1", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Host, "::1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
static string FindRepoRoot()
|
||||
{
|
||||
string current = Directory.GetCurrentDirectory();
|
||||
DirectoryInfo? directory = new(current);
|
||||
|
||||
while (directory is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory.FullName, "MinecraftClient.sln")))
|
||||
return directory.FullName;
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
static StdioClientTransportOptions CreateStdioOptions()
|
||||
{
|
||||
string? stdioBin = Environment.GetEnvironmentVariable("MCC_MCP_STDIO_BIN");
|
||||
if (!string.IsNullOrWhiteSpace(stdioBin))
|
||||
{
|
||||
return new StdioClientTransportOptions
|
||||
{
|
||||
Name = "MCC MCP Stdio Harness",
|
||||
Command = stdioBin,
|
||||
Arguments = [],
|
||||
ShutdownTimeout = TimeSpan.FromSeconds(5)
|
||||
};
|
||||
}
|
||||
|
||||
return new StdioClientTransportOptions
|
||||
{
|
||||
Name = "MCC MCP Stdio Harness",
|
||||
Command = "dotnet",
|
||||
Arguments =
|
||||
[
|
||||
"run",
|
||||
"--project",
|
||||
"DebugTools/MccMcpStdioHarness",
|
||||
"-c",
|
||||
"Release",
|
||||
"--no-build"
|
||||
],
|
||||
ShutdownTimeout = TimeSpan.FromSeconds(5)
|
||||
};
|
||||
}
|
||||
|
||||
internal readonly record struct Coordinate(double X, double Y, double Z);
|
||||
|
||||
internal sealed record ToolEnvelope(
|
||||
string ToolName,
|
||||
bool IsError,
|
||||
bool Success,
|
||||
string? ErrorCode,
|
||||
string? Message,
|
||||
JsonElement Root,
|
||||
JsonElement? Data);
|
||||
18
DebugTools/MccMcpStdioHarness/MccMcpStdioHarness.csproj
Normal file
18
DebugTools/MccMcpStdioHarness/MccMcpStdioHarness.csproj
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\MinecraftClient\MinecraftClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
984
DebugTools/MccMcpStdioHarness/Program.cs
Normal file
984
DebugTools/MccMcpStdioHarness/Program.cs
Normal file
|
|
@ -0,0 +1,984 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MinecraftClient.Mcp;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Logging.AddConsole(options =>
|
||||
{
|
||||
options.LogToStandardErrorThreshold = LogLevel.Trace;
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton(new MccMcpConfig());
|
||||
builder.Services.AddSingleton<IMccMcpCapabilities, DeterministicCapabilities>();
|
||||
builder.Services.AddSingleton<MccMcpGuidanceProvider>();
|
||||
builder.Services.AddMcpServer()
|
||||
.WithStdioServerTransport()
|
||||
.WithTools<MccMcpToolSet>()
|
||||
.WithPrompts<MccMcpPromptSet>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
internal sealed class DeterministicCapabilities : IMccMcpCapabilities
|
||||
{
|
||||
private static double C(double value) => Math.Round(value, 2, MidpointRounding.AwayFromZero);
|
||||
|
||||
private readonly List<RecentEvent> recentEvents = [];
|
||||
private long nextEventId = 1;
|
||||
private double playerX = C(0.5);
|
||||
private double playerY = C(80.0);
|
||||
private double playerZ = C(0.5);
|
||||
private float yaw;
|
||||
private float pitch;
|
||||
private int currentSlot = 1;
|
||||
private bool sneaking;
|
||||
private bool sprinting;
|
||||
private float health = 20.0f;
|
||||
private bool disconnecting;
|
||||
|
||||
public DeterministicCapabilities()
|
||||
{
|
||||
AddRecentEvent("player_join", new { name = "HarnessBot" });
|
||||
AddRecentEvent("inventory_open", new { inventoryId = 1, type = "Generic_9x3", title = "Chest" });
|
||||
AddRecentEvent("weather_rain", new { level = 1.0 });
|
||||
AddRecentEvent("title", new { text = "mcp_title" });
|
||||
AddRecentEvent("actionbar", new { text = "mcp_actionbar" });
|
||||
}
|
||||
|
||||
public MccMcpResult GetSessionStatus() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
connected = !disconnecting,
|
||||
host = "deterministic.local",
|
||||
port = 25565,
|
||||
username = "HarnessBot",
|
||||
location = new { x = playerX, y = playerY, z = playerZ }
|
||||
});
|
||||
|
||||
public MccMcpResult GetServerInfo() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
host = "deterministic.local",
|
||||
port = 25565,
|
||||
tps = 20.0
|
||||
});
|
||||
|
||||
public MccMcpResult GetPlayerState() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
nickname = "HarnessBot",
|
||||
username = "HarnessBot",
|
||||
health,
|
||||
saturation = 20,
|
||||
gamemode = 1,
|
||||
currentSlot,
|
||||
yaw,
|
||||
pitch,
|
||||
location = new { x = playerX, y = playerY, z = playerZ },
|
||||
effects = new object[0]
|
||||
});
|
||||
|
||||
public MccMcpResult GetWorldState() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
connected = !disconnecting,
|
||||
host = "deterministic.local",
|
||||
port = 25565,
|
||||
username = "HarnessBot",
|
||||
protocol = 769,
|
||||
terrainEnabled = true,
|
||||
inventoryEnabled = true,
|
||||
entityHandlingEnabled = true,
|
||||
location = new { x = playerX, y = playerY, z = playerZ },
|
||||
tps = 20.0,
|
||||
dimension = "minecraft:overworld",
|
||||
loadedChunkCount = 9,
|
||||
pendingChunkCount = 0,
|
||||
totalChunkCount = 9,
|
||||
loadRatio = 1.0,
|
||||
worldAge = 12000L,
|
||||
timeOfDay = 6000L,
|
||||
rainLevel = 1.0,
|
||||
thunderLevel = 0.0
|
||||
});
|
||||
|
||||
public MccMcpResult GetChunkStatus(double? x, double? y, double? z)
|
||||
{
|
||||
double resolvedX = x ?? playerX;
|
||||
double resolvedY = y ?? playerY;
|
||||
double resolvedZ = z ?? playerZ;
|
||||
int chunkX = (int)Math.Floor(resolvedX) >> 4;
|
||||
int chunkZ = (int)Math.Floor(resolvedZ) >> 4;
|
||||
|
||||
return MccMcpResult.Ok(new
|
||||
{
|
||||
location = new { x = C(resolvedX), y = C(resolvedY), z = C(resolvedZ) },
|
||||
chunk = new { x = chunkX, z = chunkZ },
|
||||
loaded = true,
|
||||
fullyLoaded = true,
|
||||
loadedChunkCount = 9,
|
||||
pendingChunkCount = 0,
|
||||
totalChunkCount = 9,
|
||||
loadRatio = 1.0
|
||||
});
|
||||
}
|
||||
|
||||
public MccMcpResult RaycastBlock(double maxDistance, bool includeNeighbors)
|
||||
{
|
||||
object? neighbors = includeNeighbors
|
||||
? new
|
||||
{
|
||||
north = new { x = 0, y = 79, z = -1, material = "Air", typeLabel = "Air" },
|
||||
south = new { x = 0, y = 79, z = 1, material = "Air", typeLabel = "Air" },
|
||||
east = new { x = 1, y = 79, z = 0, material = "Air", typeLabel = "Air" },
|
||||
west = new { x = -1, y = 79, z = 0, material = "Air", typeLabel = "Air" },
|
||||
above = new { x = 0, y = 80, z = 0, material = "Air", typeLabel = "Air" },
|
||||
below = new { x = 0, y = 78, z = 0, material = "Stone", typeLabel = "Stone" }
|
||||
}
|
||||
: null;
|
||||
|
||||
return MccMcpResult.Ok(new
|
||||
{
|
||||
hit = true,
|
||||
maxDistance,
|
||||
playerLocation = new { x = playerX, y = playerY, z = playerZ },
|
||||
eyeLocation = new { x = playerX, y = C(playerY + 1.62), z = playerZ },
|
||||
location = new { x = 0, y = 79, z = 0 },
|
||||
block = new { material = "Stone", typeLabel = "Stone", blockId = 1, blockMeta = 0 },
|
||||
distance = 1.12,
|
||||
eyeDistance = 2.03,
|
||||
neighbors
|
||||
});
|
||||
}
|
||||
|
||||
public MccMcpResult PreviewPath(double x, double y, double z, bool allowUnsafe, int maxOffset, int minOffset, int timeoutMs, int maxWaypoints)
|
||||
{
|
||||
object[] waypoints =
|
||||
[
|
||||
new { x = playerX, y = playerY, z = playerZ },
|
||||
new { x = C((playerX + x) / 2), y = C((playerY + y) / 2), z = C((playerZ + z) / 2) },
|
||||
new { x = C(x), y = C(y), z = C(z) }
|
||||
];
|
||||
|
||||
return MccMcpResult.Ok(new
|
||||
{
|
||||
pathFound = true,
|
||||
exactReachable = true,
|
||||
target = new { x = C(x), y = C(y), z = C(z) },
|
||||
startLocation = new { x = playerX, y = playerY, z = playerZ },
|
||||
finalWaypoint = new { x = C(x), y = C(y), z = C(z) },
|
||||
finalDistance = 0.0,
|
||||
waypointCount = waypoints.Length,
|
||||
truncated = waypoints.Length > Math.Max(1, maxWaypoints),
|
||||
waypoints = waypoints.Take(Math.Max(1, maxWaypoints)).ToArray(),
|
||||
allowUnsafe,
|
||||
maxOffset,
|
||||
minOffset,
|
||||
timeoutMs = timeoutMs <= 0 ? 5000 : timeoutMs
|
||||
});
|
||||
}
|
||||
|
||||
public MccMcpResult GetPlayersList() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
players = new[] { "HarnessBot", "PlayerOne" }
|
||||
});
|
||||
|
||||
public MccMcpResult GetPlayersDetailed(bool includeSelf, bool includeCoordinates)
|
||||
{
|
||||
List<object> players = [];
|
||||
if (includeSelf)
|
||||
{
|
||||
players.Add(new
|
||||
{
|
||||
name = "HarnessBot",
|
||||
uuid = Guid.Parse("11111111-1111-1111-1111-111111111111"),
|
||||
ping = 5,
|
||||
gamemode = 1,
|
||||
listed = true,
|
||||
displayName = "HarnessBot",
|
||||
entityId = 1,
|
||||
x = includeCoordinates ? playerX : (double?)null,
|
||||
y = includeCoordinates ? playerY : (double?)null,
|
||||
z = includeCoordinates ? playerZ : (double?)null
|
||||
});
|
||||
}
|
||||
|
||||
players.Add(new
|
||||
{
|
||||
name = "PlayerOne",
|
||||
uuid = Guid.Parse("22222222-2222-2222-2222-222222222222"),
|
||||
ping = 12,
|
||||
gamemode = 1,
|
||||
listed = true,
|
||||
displayName = "PlayerOne",
|
||||
entityId = 2,
|
||||
x = includeCoordinates ? C(3.5) : (double?)null,
|
||||
y = includeCoordinates ? C(80.0) : (double?)null,
|
||||
z = includeCoordinates ? C(0.5) : (double?)null
|
||||
});
|
||||
|
||||
return MccMcpResult.Ok(new
|
||||
{
|
||||
count = players.Count,
|
||||
players = players.ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
public MccMcpResult GetPlayerStats() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
health,
|
||||
saturation = 20,
|
||||
level = 12,
|
||||
totalExperience = 245,
|
||||
gamemode = 1,
|
||||
playerEntityId = 1,
|
||||
currentSlot,
|
||||
yaw,
|
||||
pitch,
|
||||
sneaking,
|
||||
sprinting,
|
||||
location = new { x = playerX, y = playerY, z = playerZ },
|
||||
tps = 20.0
|
||||
});
|
||||
|
||||
public MccMcpResult GetStatusEffects() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
count = 0,
|
||||
effects = Array.Empty<object>()
|
||||
});
|
||||
|
||||
public MccMcpResult GetRecentEvents(long afterId, int maxCount, string? typeFilter)
|
||||
{
|
||||
RecentEvent[] events = recentEvents
|
||||
.Where(e => e.Id > afterId)
|
||||
.Where(e => string.IsNullOrWhiteSpace(typeFilter) || string.Equals(e.Type, typeFilter, StringComparison.OrdinalIgnoreCase))
|
||||
.Take(Math.Max(1, maxCount))
|
||||
.ToArray();
|
||||
|
||||
return MccMcpResult.Ok(new
|
||||
{
|
||||
afterId,
|
||||
latestId = recentEvents.Count > 0 ? recentEvents[^1].Id : 0,
|
||||
count = events.Length,
|
||||
events = events.Select(e => new
|
||||
{
|
||||
id = e.Id,
|
||||
timestampUtc = e.TimestampUtc,
|
||||
type = e.Type,
|
||||
data = e.Data
|
||||
}).ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
public MccMcpResult GetLoadedBots() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
count = 2,
|
||||
bots = new object[]
|
||||
{
|
||||
new { name = "McpServer", fullTypeName = "MinecraftClient.ChatBots.McpServer", isScript = false },
|
||||
new { name = "HarnessScript", fullTypeName = "MinecraftClient.ChatBots.Script", isScript = true }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult GetChatHistory(int maxCount, bool includeJson) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
count = 2,
|
||||
entries = new object[]
|
||||
{
|
||||
new { timestampUtc = DateTimeOffset.UtcNow.AddSeconds(-10), kind = "chat", text = "<PlayerOne> hello", sender = "PlayerOne", message = "hello", json = includeJson ? "{}" : null },
|
||||
new { timestampUtc = DateTimeOffset.UtcNow.AddSeconds(-5), kind = "system", text = "HarnessBot joined the game", sender = (string?)null, message = (string?)null, json = includeJson ? "{}" : null }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult GetInternalCommands() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
count = 4,
|
||||
commands = new[]
|
||||
{
|
||||
new { name = "debug", usage = "debug [on|off|state]", description = "Toggle debug or print state." },
|
||||
new { name = "move", usage = "move <x> <y> <z>", description = "Move to location." },
|
||||
new { name = "useitem", usage = "useitem [x] [y] [z]", description = "Use current held item." },
|
||||
new { name = "dig", usage = "dig <x> <y> <z> [duration]", description = "Dig block at location." }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult GetMaterialsList(string? filter, int maxCount) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
total = 3,
|
||||
count = 3,
|
||||
filter,
|
||||
materials = new[]
|
||||
{
|
||||
new { name = "Air", typeLabel = "Air" },
|
||||
new { name = "GrassBlock", typeLabel = "Grass Block" },
|
||||
new { name = "OakLog", typeLabel = "Oak Log" }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult GetBlockTypesList(string? filter, int maxCount) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
total = 3,
|
||||
count = 3,
|
||||
filter,
|
||||
blockTypes = new[]
|
||||
{
|
||||
new { name = "Air", typeLabel = "Air" },
|
||||
new { name = "GrassBlock", typeLabel = "Grass Block" },
|
||||
new { name = "OakLog", typeLabel = "Oak Log" }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult GetEntityTypesList(string? filter, int maxCount) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
total = 3,
|
||||
count = 3,
|
||||
filter,
|
||||
entityTypes = new[]
|
||||
{
|
||||
new { name = "Player", typeLabel = "Player" },
|
||||
new { name = "Item", typeLabel = "Item" },
|
||||
new { name = "Villager", typeLabel = "Villager" }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult SendChat(string text) =>
|
||||
MccMcpResult.Ok(new { echoed = text });
|
||||
|
||||
public MccMcpResult QuitClient() =>
|
||||
MccMcpResult.Ok(new { quitting = true });
|
||||
|
||||
public MccMcpResult DisconnectClient()
|
||||
{
|
||||
disconnecting = true;
|
||||
AddRecentEvent("disconnect", new { reason = "requested", message = "Disconnect requested by test client." });
|
||||
return MccMcpResult.Ok(new { disconnecting = true });
|
||||
}
|
||||
|
||||
public MccMcpResult RunInternalCommand(string command) =>
|
||||
MccMcpResult.Ok(new { command, status = "Done", output = "deterministic" });
|
||||
|
||||
public MccMcpResult UseItemOnHand() =>
|
||||
MccMcpResult.Ok(new { success = true, action = "use_item_on_hand" });
|
||||
|
||||
public MccMcpResult ChangeHotbarSlot(int slot)
|
||||
{
|
||||
currentSlot = slot;
|
||||
return MccMcpResult.Ok(new { success = true, slot });
|
||||
}
|
||||
|
||||
public MccMcpResult SelectHotbarItem(string itemType, bool preferLowestSlot)
|
||||
{
|
||||
currentSlot = string.Equals(itemType, "DiamondSword", StringComparison.OrdinalIgnoreCase) ? 2 : 1;
|
||||
return MccMcpResult.Ok(new
|
||||
{
|
||||
success = true,
|
||||
itemType,
|
||||
inventorySlot = currentSlot - 1,
|
||||
selectedSlot = currentSlot,
|
||||
count = string.Equals(itemType, "DiamondSword", StringComparison.OrdinalIgnoreCase) ? 1 : 32,
|
||||
preferLowestSlot
|
||||
});
|
||||
}
|
||||
|
||||
public MccMcpResult UseItemOnBlock(double x, double y, double z) =>
|
||||
MccMcpResult.Ok(new { success = true, x = C(x), y = C(y), z = C(z), action = "useitem" });
|
||||
|
||||
public MccMcpResult DigBlock(double x, double y, double z, double durationSeconds) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
success = true,
|
||||
target = new { x = C(x), y = C(y), z = C(z) },
|
||||
beforeBlock = new { material = "OakLog", typeLabel = "Oak Log", blockId = 137, blockMeta = 0 },
|
||||
afterBlock = new { material = "Air", typeLabel = "Air", blockId = 0, blockMeta = 0 },
|
||||
commandAccepted = true,
|
||||
changed = true,
|
||||
destroyed = true,
|
||||
attempts = 1,
|
||||
attemptedDurationsSeconds = new[] { durationSeconds > 0 ? durationSeconds : 1.5 },
|
||||
distance = 1.5,
|
||||
playerLocation = new { x = C(0.5), y = C(80.0), z = C(0.5) }
|
||||
});
|
||||
|
||||
public MccMcpResult PlaceBlock(int x, int y, int z, string face, string hand, bool lookAtBlock) =>
|
||||
MccMcpResult.Ok(new { success = true, x, y, z, face, hand, lookAtBlock, action = "place_block" });
|
||||
|
||||
public MccMcpResult InteractEntity(int entityId, string interaction, string hand) =>
|
||||
MccMcpResult.Ok(new { success = true, entityId, interaction, hand });
|
||||
|
||||
public MccMcpResult AttackEntity(int entityId) =>
|
||||
MccMcpResult.Ok(new { success = true, entityId, interaction = "Attack" });
|
||||
|
||||
public MccMcpResult FindNearestEntity(string? typeFilter, string? nameFilter, double radius, bool includePlayers)
|
||||
{
|
||||
bool wantsArmorStand = string.IsNullOrWhiteSpace(typeFilter)
|
||||
|| string.Equals(typeFilter, "ArmorStand", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(typeFilter, "Armor Stand", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (wantsArmorStand && radius >= 4.0)
|
||||
{
|
||||
return MccMcpResult.Ok(new
|
||||
{
|
||||
id = 7,
|
||||
type = "ArmorStand",
|
||||
typeLabel = "Armor Stand",
|
||||
uuid = Guid.Parse("33333333-3333-3333-3333-333333333333"),
|
||||
name = "Armor Stand",
|
||||
customName = (string?)null,
|
||||
x = C(2.5),
|
||||
y = C(80.0),
|
||||
z = C(0.5),
|
||||
distance = 2.0,
|
||||
health = 20.0f,
|
||||
pose = "Standing",
|
||||
latency = 0
|
||||
});
|
||||
}
|
||||
|
||||
if (includePlayers && radius >= 3.0)
|
||||
{
|
||||
return MccMcpResult.Ok(new
|
||||
{
|
||||
id = 2,
|
||||
type = "Player",
|
||||
typeLabel = "Player",
|
||||
uuid = Guid.Parse("22222222-2222-2222-2222-222222222222"),
|
||||
name = string.IsNullOrWhiteSpace(nameFilter) ? "PlayerOne" : nameFilter,
|
||||
customName = (string?)null,
|
||||
x = C(3.5),
|
||||
y = C(80.0),
|
||||
z = C(0.5),
|
||||
distance = 3.0,
|
||||
health = 20.0f,
|
||||
pose = "Standing",
|
||||
latency = 12
|
||||
});
|
||||
}
|
||||
|
||||
return MccMcpResult.Fail("invalid_state", data: new { typeFilter, nameFilter, radius, includePlayers });
|
||||
}
|
||||
|
||||
public MccMcpResult ScanNearbyBlocks(int radius, int maxCount, string? materialFilter) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
center = new { x = 0, y = 79, z = 0 },
|
||||
radius,
|
||||
count = 1,
|
||||
blocks = new[]
|
||||
{
|
||||
new { x = 0, y = 79, z = 0, material = materialFilter ?? "GrassBlock", blockId = 9, blockMeta = 0, distance = 0.0 }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult FindBlocks(string? query, int radius, int maxCount, bool exactMatch) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
center = new { x = 0, y = 79, z = 0 },
|
||||
radius,
|
||||
query,
|
||||
exactMatch,
|
||||
count = 2,
|
||||
blocks = new object[]
|
||||
{
|
||||
new { x = 1, y = 79, z = 0, material = "GrassBlock", typeLabel = "Grass Block", blockId = 9, blockMeta = 0, distance = 1.0 },
|
||||
new { x = 2, y = 79, z = 0, material = "Dirt", typeLabel = "Dirt", blockId = 10, blockMeta = 0, distance = 2.0 }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult IsPlayerNearby(string? playerName, double radius, bool includeSelf) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
radius,
|
||||
playerName,
|
||||
includeSelf,
|
||||
anyNearby = true,
|
||||
count = 1,
|
||||
players = new object[]
|
||||
{
|
||||
new
|
||||
{
|
||||
entityId = 1,
|
||||
uuid = Guid.Empty,
|
||||
name = "PlayerOne",
|
||||
customName = (string?)null,
|
||||
x = C(3.5),
|
||||
y = C(80.0),
|
||||
z = C(0.5),
|
||||
distance = 3.0,
|
||||
latency = 5
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult LocatePlayer(string playerName, bool includeSelf) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
playerName,
|
||||
matchedName = "PlayerOne",
|
||||
entityId = 1,
|
||||
uuid = Guid.Empty,
|
||||
x = C(3.5),
|
||||
y = C(80.0),
|
||||
z = C(0.5),
|
||||
distance = 3.0
|
||||
});
|
||||
|
||||
public MccMcpResult CanReachPosition(double x, double y, double z, bool allowUnsafe, int maxOffset, int minOffset, int timeoutMs) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
reachable = true,
|
||||
exactReachable = true,
|
||||
target = new { x = C(x), y = C(y), z = C(z) },
|
||||
startLocation = new { x = C(0.5), y = C(80.0), z = C(0.5) },
|
||||
finalWaypoint = new { x = C(x), y = C(y), z = C(z) },
|
||||
finalDistance = 0.0,
|
||||
waypointCount = 4,
|
||||
allowUnsafe,
|
||||
maxOffset,
|
||||
minOffset,
|
||||
timeoutMs = timeoutMs <= 0 ? 5000 : timeoutMs
|
||||
});
|
||||
|
||||
public MccMcpResult MoveTo(double x, double y, double z, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
pathFound = true,
|
||||
arrived = true,
|
||||
tolerance = 1.5,
|
||||
verifyWaitMs = 250,
|
||||
target = new { x = C(x), y = C(y), z = C(z) },
|
||||
startLocation = new { x = C(0.5), y = C(80.0), z = C(0.5) },
|
||||
finalLocation = new { x = C(x), y = C(y), z = C(z) },
|
||||
finalDistance = 0.0,
|
||||
distanceMoved = 3.0,
|
||||
allowUnsafe,
|
||||
allowDirectTeleport,
|
||||
maxOffset,
|
||||
minOffset,
|
||||
timeoutMs
|
||||
});
|
||||
|
||||
public MccMcpResult MoveToPlayer(string playerName, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
pathFound = true,
|
||||
arrived = true,
|
||||
tolerance = 1.5,
|
||||
verifyWaitMs = 250,
|
||||
target = new
|
||||
{
|
||||
playerName = "PlayerOne",
|
||||
entityId = 1,
|
||||
x = C(3.5),
|
||||
y = C(80.0),
|
||||
z = C(0.5)
|
||||
},
|
||||
startLocation = new { x = C(0.5), y = C(80.0), z = C(0.5) },
|
||||
finalLocation = new { x = C(3.5), y = C(80.0), z = C(0.5) },
|
||||
finalDistance = 0.0,
|
||||
distanceMoved = 3.0,
|
||||
allowUnsafe,
|
||||
allowDirectTeleport,
|
||||
maxOffset,
|
||||
minOffset,
|
||||
timeoutMs
|
||||
});
|
||||
|
||||
public MccMcpResult LookAt(double x, double y, double z) =>
|
||||
MccMcpResult.Ok(new { looked = true, x = C(x), y = C(y), z = C(z) });
|
||||
|
||||
public MccMcpResult LookDirection(string direction)
|
||||
{
|
||||
switch (direction.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "up":
|
||||
yaw = 0.0f;
|
||||
pitch = -90.0f;
|
||||
break;
|
||||
case "down":
|
||||
yaw = 0.0f;
|
||||
pitch = 90.0f;
|
||||
break;
|
||||
case "north":
|
||||
yaw = 180.0f;
|
||||
pitch = 0.0f;
|
||||
break;
|
||||
case "south":
|
||||
yaw = 0.0f;
|
||||
pitch = 0.0f;
|
||||
break;
|
||||
case "east":
|
||||
yaw = -90.0f;
|
||||
pitch = 0.0f;
|
||||
break;
|
||||
case "west":
|
||||
yaw = 90.0f;
|
||||
pitch = 0.0f;
|
||||
break;
|
||||
}
|
||||
|
||||
return MccMcpResult.Ok(new { success = true, direction, yaw, pitch });
|
||||
}
|
||||
|
||||
public MccMcpResult LookAngles(float yaw, float pitch)
|
||||
{
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
return MccMcpResult.Ok(new { success = true, yaw, pitch });
|
||||
}
|
||||
|
||||
public MccMcpResult PlayAnimation(string hand) =>
|
||||
MccMcpResult.Ok(new { success = true, hand });
|
||||
|
||||
public MccMcpResult ToggleSneak(bool enabled)
|
||||
{
|
||||
sneaking = enabled;
|
||||
return MccMcpResult.Ok(new { success = true, enabled = sneaking });
|
||||
}
|
||||
|
||||
public MccMcpResult ToggleSprint(bool enabled)
|
||||
{
|
||||
sprinting = enabled;
|
||||
return MccMcpResult.Ok(new { success = true, enabled = sprinting });
|
||||
}
|
||||
|
||||
public MccMcpResult ListInventories() =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
count = 2,
|
||||
inventories = new object[]
|
||||
{
|
||||
new { id = 0, type = "PlayerInventory", title = "Player Inventory", slotCount = 46, nonEmptySlots = 1, active = false },
|
||||
new { id = 1, type = "Generic_9x3", title = "Chest", slotCount = 63, nonEmptySlots = 2, active = true }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult GetInventorySnapshot(int inventoryId) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
id = inventoryId,
|
||||
type = inventoryId == 0 ? "PlayerInventory" : "Generic_9x3",
|
||||
title = inventoryId == 0 ? "Player Inventory" : "Chest",
|
||||
slotCount = inventoryId == 0 ? 46 : 63,
|
||||
slots = new[]
|
||||
{
|
||||
new { slot = 0, type = "Stone", count = 64 }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult SearchInventories(string query, int maxCount, bool exactMatch, bool includeContainers)
|
||||
{
|
||||
List<object> matches = [];
|
||||
|
||||
if (query.Contains("stone", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
matches.Add(new
|
||||
{
|
||||
inventoryId = 0,
|
||||
inventoryType = "PlayerInventory",
|
||||
inventoryTitle = "Player Inventory",
|
||||
slot = 0,
|
||||
itemType = "Stone",
|
||||
typeLabel = "Stone",
|
||||
count = 32,
|
||||
isPlayerInventory = true,
|
||||
hotbarSlot = 1
|
||||
});
|
||||
}
|
||||
|
||||
if (query.Contains("diamond", StringComparison.OrdinalIgnoreCase) || query.Contains("sword", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
matches.Add(new
|
||||
{
|
||||
inventoryId = 0,
|
||||
inventoryType = "PlayerInventory",
|
||||
inventoryTitle = "Player Inventory",
|
||||
slot = 1,
|
||||
itemType = "DiamondSword",
|
||||
typeLabel = "Diamond Sword",
|
||||
count = 1,
|
||||
isPlayerInventory = true,
|
||||
hotbarSlot = 2
|
||||
});
|
||||
}
|
||||
|
||||
if (includeContainers)
|
||||
{
|
||||
matches.Add(new
|
||||
{
|
||||
inventoryId = 1,
|
||||
inventoryType = "Generic_9x3",
|
||||
inventoryTitle = "Chest",
|
||||
slot = 0,
|
||||
itemType = "Stone",
|
||||
typeLabel = "Stone",
|
||||
count = 16,
|
||||
isPlayerInventory = false,
|
||||
hotbarSlot = (int?)null
|
||||
});
|
||||
}
|
||||
|
||||
object[] result = matches.Take(Math.Max(1, maxCount)).ToArray();
|
||||
return MccMcpResult.Ok(new
|
||||
{
|
||||
query,
|
||||
exactMatch,
|
||||
includeContainers,
|
||||
count = result.Length,
|
||||
matches = result
|
||||
});
|
||||
}
|
||||
|
||||
public MccMcpResult OpenContainerAt(int x, int y, int z, int timeoutMs, bool closeCurrent)
|
||||
{
|
||||
AddRecentEvent("inventory_open", new { inventoryId = 1, type = "Generic_9x3", title = "Chest", x, y, z });
|
||||
return MccMcpResult.Ok(new
|
||||
{
|
||||
success = true,
|
||||
openAccepted = true,
|
||||
opened = true,
|
||||
timeoutMs = timeoutMs <= 0 ? 5000 : timeoutMs,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
block = new { material = "Chest", typeLabel = "Chest", blockId = 0, blockMeta = 0 },
|
||||
inventory = new { id = 1, type = "Generic_9x3", title = "Chest", slotCount = 63, nonEmptySlots = 2 }
|
||||
});
|
||||
}
|
||||
|
||||
public MccMcpResult CloseContainer(int inventoryId, int timeoutMs)
|
||||
{
|
||||
int resolvedInventoryId = inventoryId <= 0 ? 1 : inventoryId;
|
||||
AddRecentEvent("inventory_close", new { inventoryId = resolvedInventoryId });
|
||||
return MccMcpResult.Ok(new
|
||||
{
|
||||
success = true,
|
||||
closed = true,
|
||||
inventoryId = resolvedInventoryId,
|
||||
timeoutMs = timeoutMs <= 0 ? 5000 : timeoutMs
|
||||
});
|
||||
}
|
||||
|
||||
public MccMcpResult InventoryWindowAction(int inventoryId, int slotId, string actionType) =>
|
||||
MccMcpResult.Ok(new { success = true, inventoryId, slotId, actionType });
|
||||
|
||||
public MccMcpResult DropInventoryItem(string itemType, int count, int inventoryId, bool preferStack) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
success = true,
|
||||
itemType,
|
||||
requestedCount = count,
|
||||
droppedCount = count,
|
||||
beforeCount = 64,
|
||||
afterCount = Math.Max(0, 64 - count),
|
||||
inventoryId,
|
||||
touchedSlots = new[] { 36 },
|
||||
preferStack
|
||||
});
|
||||
|
||||
public MccMcpResult DepositContainerItem(string itemType, int count, int inventoryId, bool preferLargestStack) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
success = true,
|
||||
direction = "deposit",
|
||||
itemType,
|
||||
requestedCount = count,
|
||||
movedCount = count,
|
||||
beforePlayerCount = 64,
|
||||
afterPlayerCount = Math.Max(0, 64 - count),
|
||||
beforeContainerCount = 0,
|
||||
afterContainerCount = count,
|
||||
inventoryId = inventoryId <= 0 ? 1 : inventoryId,
|
||||
containerType = "Generic_9x3",
|
||||
touchedSourceSlots = new[] { 36 },
|
||||
touchedTargetSlots = new[] { 0 }
|
||||
});
|
||||
|
||||
public MccMcpResult WithdrawContainerItem(string itemType, int count, int inventoryId, bool preferLargestStack) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
success = true,
|
||||
direction = "withdraw",
|
||||
itemType,
|
||||
requestedCount = count,
|
||||
movedCount = count,
|
||||
beforePlayerCount = 0,
|
||||
afterPlayerCount = count,
|
||||
beforeContainerCount = 64,
|
||||
afterContainerCount = Math.Max(0, 64 - count),
|
||||
inventoryId = inventoryId <= 0 ? 1 : inventoryId,
|
||||
containerType = "Generic_9x3",
|
||||
touchedSourceSlots = new[] { 0 },
|
||||
touchedTargetSlots = new[] { 36 }
|
||||
});
|
||||
|
||||
public MccMcpResult QueryEntities(int maxCount) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
count = 1,
|
||||
entities = new[]
|
||||
{
|
||||
new { id = 1, type = "Player", x = C(0.5), y = C(80.0), z = C(0.5) }
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult ListEntities(int maxCount, string? typeFilter, double radius) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
totalTracked = 1,
|
||||
count = 1,
|
||||
entities = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
id = 1,
|
||||
type = "Player",
|
||||
typeLabel = "Player",
|
||||
uuid = Guid.Empty,
|
||||
name = "HarnessBot",
|
||||
customName = (string?)null,
|
||||
x = C(0.5),
|
||||
y = C(80.0),
|
||||
z = C(0.5),
|
||||
distance = 0.0,
|
||||
health = 20.0f,
|
||||
pose = "Standing",
|
||||
latency = 5
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult GetEntityInfo(int entityId, bool includeMetadata, bool includeEquipment, bool includeEffects) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
id = entityId,
|
||||
type = "Player",
|
||||
typeLabel = "Player",
|
||||
uuid = Guid.Empty,
|
||||
name = "HarnessBot",
|
||||
customName = (string?)null,
|
||||
customNameVisible = false,
|
||||
x = C(0.5),
|
||||
y = C(80.0),
|
||||
z = C(0.5),
|
||||
yaw = 0.0f,
|
||||
pitch = 0.0f,
|
||||
health = 20.0f,
|
||||
pose = "Standing",
|
||||
latency = 5,
|
||||
objectData = -1,
|
||||
metadata = includeMetadata ? new { flags = 0 } : null,
|
||||
equipment = includeEquipment ? new[] { new { slot = 0, type = "Stone", count = 1 } } : null,
|
||||
activeEffects = includeEffects ? new object[0] : null
|
||||
});
|
||||
|
||||
public MccMcpResult FindSigns(string text, bool exactMatch, int radius, int maxCount, bool includeBackText) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
text,
|
||||
exactMatch,
|
||||
radius,
|
||||
includeBackText,
|
||||
count = 1,
|
||||
signs = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
x = 2,
|
||||
y = 80,
|
||||
z = 1,
|
||||
material = "OakSign",
|
||||
typeLabel = "Oak Sign",
|
||||
distance = 1.8,
|
||||
isWaxed = false,
|
||||
frontText = new[] { "home", "storage" },
|
||||
backText = includeBackText ? new[] { "north wall" } : Array.Empty<string>(),
|
||||
matchedLines = new[] { text }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult ListItemEntities(string? itemType, double radius, int maxCount) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
itemType = itemType ?? "OakLog",
|
||||
radius,
|
||||
count = 1,
|
||||
items = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
entityId = 99,
|
||||
itemType = "OakLog",
|
||||
typeLabel = "Oak Log",
|
||||
count = 3,
|
||||
x = C(2.5),
|
||||
y = C(80.0),
|
||||
z = C(1.5),
|
||||
distance = 2.24
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult PickupItems(string itemType, double radius, int maxItems, bool allowUnsafe, int timeoutMs) =>
|
||||
MccMcpResult.Ok(new
|
||||
{
|
||||
itemType,
|
||||
radius,
|
||||
maxItems,
|
||||
allowUnsafe,
|
||||
timeoutMs = timeoutMs <= 0 ? 2500 : timeoutMs,
|
||||
attempted = 1,
|
||||
successfulPickups = 1,
|
||||
collectedCount = 3,
|
||||
initialInventoryCount = 0,
|
||||
finalInventoryCount = 3,
|
||||
remainingNearby = 0,
|
||||
attempts = new object[]
|
||||
{
|
||||
new
|
||||
{
|
||||
entityId = 99,
|
||||
itemType,
|
||||
typeLabel = "Oak Log",
|
||||
expectedCount = 3,
|
||||
target = new { x = C(2.5), y = C(80.0), z = C(1.5) },
|
||||
pathFound = true,
|
||||
arrived = true,
|
||||
entityGone = true,
|
||||
inventoryDelta = 3,
|
||||
startLocation = new { x = C(0.5), y = C(80.0), z = C(0.5) },
|
||||
finalLocation = new { x = C(2.5), y = C(80.0), z = C(1.5) },
|
||||
finalDistance = 0.0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public MccMcpResult Respawn()
|
||||
{
|
||||
health = 20.0f;
|
||||
AddRecentEvent("respawn", new { location = new { x = playerX, y = playerY, z = playerZ } });
|
||||
return MccMcpResult.Ok(new { success = true, respawned = true });
|
||||
}
|
||||
|
||||
public MccMcpResult GetWorldBlockAt(int x, int y, int z) =>
|
||||
MccMcpResult.Ok(new { x, y, z, material = "Air", blockId = 0, blockMeta = 0 });
|
||||
|
||||
private void AddRecentEvent(string type, object? data)
|
||||
{
|
||||
recentEvents.Add(new RecentEvent(nextEventId++, DateTimeOffset.UtcNow, type, data));
|
||||
if (recentEvents.Count > 100)
|
||||
recentEvents.RemoveAt(0);
|
||||
}
|
||||
|
||||
private sealed record RecentEvent(long Id, DateTimeOffset TimestampUtc, string Type, object? Data);
|
||||
}
|
||||
36
DebugTools/MccMcpWebPlayground/Api/MccPlaygroundEndpoints.cs
Normal file
36
DebugTools/MccMcpWebPlayground/Api/MccPlaygroundEndpoints.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using DebugTools.MccMcpWebPlayground.Contracts;
|
||||
using DebugTools.MccMcpWebPlayground.Harness;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DebugTools.MccMcpWebPlayground.Api;
|
||||
|
||||
public static class MccPlaygroundEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapMccPlaygroundEndpoints(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
RouteGroupBuilder api = endpoints.MapGroup("/api");
|
||||
|
||||
api.MapGet("/health", () => Results.Ok(new { ok = true }));
|
||||
|
||||
api.MapGet("/config", (IOptions<MccWebHarnessOptions> options) =>
|
||||
{
|
||||
MccWebHarnessOptions harnessOptions = options.Value;
|
||||
return Results.Ok(new MccConfigResponse(
|
||||
Model: harnessOptions.ResolveModel(),
|
||||
OpenRouterBaseUrl: harnessOptions.ResolveOpenRouterBaseUrl(),
|
||||
McpEndpoint: harnessOptions.ResolveMcpEndpoint(),
|
||||
HasApiKey: harnessOptions.HasApiKeyConfigured(),
|
||||
ExposeInventoryWindowAction: harnessOptions.ExposeInventoryWindowAction,
|
||||
ExposeInternalCommandTool: harnessOptions.ExposeInternalCommandTool));
|
||||
});
|
||||
|
||||
api.MapPost("/chat/stream", (ChatStreamRequest request, IMccAgentRunService runService, HttpContext httpContext, CancellationToken cancellationToken) =>
|
||||
{
|
||||
return TypedResults.ServerSentEvents(runService.StreamAsync(request, httpContext, cancellationToken));
|
||||
})
|
||||
.WithRequestTimeout("mcc-stream");
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
}
|
||||
94
DebugTools/MccMcpWebPlayground/Contracts/MccContracts.cs
Normal file
94
DebugTools/MccMcpWebPlayground/Contracts/MccContracts.cs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DebugTools.MccMcpWebPlayground.Contracts;
|
||||
|
||||
public sealed class ChatStreamRequest
|
||||
{
|
||||
public List<ChatMessage>? Messages { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ChatMessage
|
||||
{
|
||||
public string Role { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed record MccConfigResponse(
|
||||
string? Model,
|
||||
string OpenRouterBaseUrl,
|
||||
string McpEndpoint,
|
||||
bool HasApiKey,
|
||||
bool ExposeInventoryWindowAction,
|
||||
bool ExposeInternalCommandTool);
|
||||
|
||||
public sealed record MccStreamEnvelope(string RunId, long Sequence, string Kind, object Data);
|
||||
|
||||
public sealed record MccRunStartedData(string Model, string McpEndpoint, DateTimeOffset StartedAtUtc);
|
||||
|
||||
public sealed record MccGuidanceLoadedData(
|
||||
string SourceTool,
|
||||
string CanonicalPromptName,
|
||||
string GuidanceVersion,
|
||||
MccCapabilityStatus CapabilityStatus);
|
||||
|
||||
public sealed record MccStateSummaryData(
|
||||
int TurnCount,
|
||||
int ToolCallCount,
|
||||
bool SoftFinish,
|
||||
int DirectAnswerAttempts,
|
||||
IReadOnlyList<MccVerificationObligationView> OpenVerification,
|
||||
IReadOnlyList<MccEvidenceView> RecentEvidence,
|
||||
string? CompactionSummary);
|
||||
|
||||
public sealed record MccToolCalledData(string CallId, string Name, string ArgumentsJson, bool Advanced, bool Sensitive);
|
||||
|
||||
public sealed record MccToolResultData(
|
||||
string CallId,
|
||||
string Name,
|
||||
bool IsError,
|
||||
bool Success,
|
||||
string? ErrorCode,
|
||||
string Summary,
|
||||
string RawText,
|
||||
string EvidenceId);
|
||||
|
||||
public sealed record MccVerificationEventData(string ObligationId, string ToolName, string Kind, string Description);
|
||||
|
||||
public sealed record MccBudgetData(
|
||||
int TurnCount,
|
||||
int MaxTurns,
|
||||
int ToolCallCount,
|
||||
int MaxToolCalls,
|
||||
double ElapsedSeconds,
|
||||
int MaxWallClockSeconds);
|
||||
|
||||
public sealed record MccErrorData(string Code, string Message, string? Detail = null);
|
||||
|
||||
public sealed record MccFinalPayload(
|
||||
string Status,
|
||||
string Headline,
|
||||
string AnswerMarkdown,
|
||||
IReadOnlyList<string> VerifiedFacts,
|
||||
IReadOnlyList<string> OpenIssues,
|
||||
IReadOnlyList<string> EvidenceIds,
|
||||
string? NextAction);
|
||||
|
||||
public sealed record MccSubmitFinalArgs(
|
||||
string Status,
|
||||
string Headline,
|
||||
string AnswerMarkdown,
|
||||
IReadOnlyList<string> VerifiedFacts,
|
||||
IReadOnlyList<string> OpenIssues,
|
||||
IReadOnlyList<string> EvidenceIds,
|
||||
string? NextAction);
|
||||
|
||||
public sealed record MccCapabilityStatus(
|
||||
[property: JsonPropertyName("sessionStatus")] bool SessionStatus,
|
||||
[property: JsonPropertyName("chatAndCommands")] bool ChatAndCommands,
|
||||
[property: JsonPropertyName("movement")] bool Movement,
|
||||
[property: JsonPropertyName("inventory")] bool Inventory,
|
||||
[property: JsonPropertyName("entityWorld")] bool EntityWorld);
|
||||
|
||||
public sealed record MccEvidenceView(string Id, string ToolName, string Summary, bool IsError);
|
||||
|
||||
public sealed record MccVerificationObligationView(string Id, string ToolName, string Kind, string Description);
|
||||
852
DebugTools/MccMcpWebPlayground/Harness/MccAgentRunService.cs
Normal file
852
DebugTools/MccMcpWebPlayground/Harness/MccAgentRunService.cs
Normal file
|
|
@ -0,0 +1,852 @@
|
|||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Net.ServerSentEvents;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using DebugTools.MccMcpWebPlayground.Contracts;
|
||||
using DebugTools.MccMcpWebPlayground.Infrastructure.Mcp;
|
||||
using DebugTools.MccMcpWebPlayground.Infrastructure.OpenRouter;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace DebugTools.MccMcpWebPlayground.Harness;
|
||||
|
||||
public interface IMccAgentRunService
|
||||
{
|
||||
IAsyncEnumerable<SseItem<MccStreamEnvelope>> StreamAsync(ChatStreamRequest request, HttpContext httpContext, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class MccAgentRunService : IMccAgentRunService
|
||||
{
|
||||
private readonly MccMcpSessionFactory sessionFactory;
|
||||
private readonly MccGuidanceSource guidanceSource;
|
||||
private readonly MccPromptComposer promptComposer;
|
||||
private readonly MccContextCompressor contextCompressor;
|
||||
private readonly MccFinalizer finalizer;
|
||||
private readonly OpenRouterChatClient openRouterChatClient;
|
||||
private readonly MccWebHarnessOptions options;
|
||||
|
||||
public MccAgentRunService(
|
||||
MccMcpSessionFactory sessionFactory,
|
||||
MccGuidanceSource guidanceSource,
|
||||
MccPromptComposer promptComposer,
|
||||
MccContextCompressor contextCompressor,
|
||||
MccFinalizer finalizer,
|
||||
OpenRouterChatClient openRouterChatClient,
|
||||
IOptions<MccWebHarnessOptions> options)
|
||||
{
|
||||
this.sessionFactory = sessionFactory;
|
||||
this.guidanceSource = guidanceSource;
|
||||
this.promptComposer = promptComposer;
|
||||
this.contextCompressor = contextCompressor;
|
||||
this.finalizer = finalizer;
|
||||
this.openRouterChatClient = openRouterChatClient;
|
||||
this.options = options.Value;
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<SseItem<MccStreamEnvelope>> StreamAsync(
|
||||
ChatStreamRequest request,
|
||||
HttpContext httpContext,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, httpContext.RequestAborted);
|
||||
CancellationToken linkedToken = linkedCts.Token;
|
||||
|
||||
string runId = Guid.NewGuid().ToString("n");
|
||||
long sequence = 0;
|
||||
|
||||
string? model = options.ResolveModel();
|
||||
if (string.IsNullOrWhiteSpace(model))
|
||||
{
|
||||
yield return CreateEvent(runId, ref sequence, "error", new MccErrorData("configuration_error", "OPENROUTER_MODEL or MccWebHarness:Model must be configured."));
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!options.HasApiKeyConfigured())
|
||||
{
|
||||
yield return CreateEvent(runId, ref sequence, "error", new MccErrorData("configuration_error", "OPENROUTER_API_KEY is not set."));
|
||||
yield break;
|
||||
}
|
||||
|
||||
List<object> baseConversationMessages = NormalizeConversation(request.Messages);
|
||||
string userRequest = ExtractUserRequest(request.Messages);
|
||||
if (string.IsNullOrWhiteSpace(userRequest))
|
||||
{
|
||||
yield return CreateEvent(runId, ref sequence, "error", new MccErrorData("invalid_request", "No user message was provided."));
|
||||
yield break;
|
||||
}
|
||||
|
||||
await using McpClient client = await sessionFactory.CreateAsync(linkedToken);
|
||||
MccGuidanceBundle guidance = await guidanceSource.LoadAsync(client, linkedToken);
|
||||
|
||||
MccRunState runState = new()
|
||||
{
|
||||
RunId = runId,
|
||||
UserRequest = userRequest,
|
||||
BaseConversationMessages = baseConversationMessages,
|
||||
ConfiguredModel = model,
|
||||
Guidance = guidance
|
||||
};
|
||||
|
||||
yield return CreateEvent(runId, ref sequence, "run_started", new MccRunStartedData(model, options.ResolveMcpEndpoint(), runState.StartedAtUtc));
|
||||
yield return CreateEvent(runId, ref sequence, "guidance_loaded", new MccGuidanceLoadedData(
|
||||
guidance.SourceToolName,
|
||||
guidance.CanonicalPromptName,
|
||||
guidance.GuidanceVersion,
|
||||
guidance.CapabilityStatus));
|
||||
|
||||
IList<McpClientTool> tools = await client.ListToolsAsync(cancellationToken: linkedToken);
|
||||
MccToolCatalog catalog = MccToolPolicy.BuildCatalog(tools, options, finalizer.BuildSubmitToolSchema());
|
||||
|
||||
while (!linkedToken.IsCancellationRequested)
|
||||
{
|
||||
runState.TurnCount++;
|
||||
contextCompressor.CompactIfNeeded(runState);
|
||||
yield return CreateEvent(runId, ref sequence, "state_summary", BuildStateSummary(runState, options));
|
||||
|
||||
if (runState.IsSoftFinish(options, DateTimeOffset.UtcNow))
|
||||
{
|
||||
yield return CreateEvent(runId, ref sequence, "budget", BuildBudgetData(runState));
|
||||
}
|
||||
|
||||
if (runState.IsHardStop(options, DateTimeOffset.UtcNow))
|
||||
break;
|
||||
|
||||
MccModelTurn? turn = null;
|
||||
Exception? providerException = null;
|
||||
try
|
||||
{
|
||||
turn = await openRouterChatClient.CreateTurnAsync(
|
||||
promptComposer.Compose(runState),
|
||||
catalog.ModelVisibleTools,
|
||||
options,
|
||||
linkedToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
providerException = ex;
|
||||
}
|
||||
|
||||
if (providerException is not null || turn is null)
|
||||
{
|
||||
yield return CreateEvent(runId, ref sequence, "error", new MccErrorData("provider_error", "OpenRouter request failed.", providerException?.Message));
|
||||
yield return CreateEvent(runId, ref sequence, "final", finalizer.BuildHardStopResult(runState, options));
|
||||
yield break;
|
||||
}
|
||||
|
||||
runState.RoutedModel = turn.ModelId;
|
||||
runState.RoutedProvider = turn.RoutedProvider;
|
||||
|
||||
if (turn.ToolCalls.Count == 0)
|
||||
{
|
||||
runState.DirectAnswerAttempts++;
|
||||
string content = string.IsNullOrWhiteSpace(turn.AssistantContent) ? "(empty assistant turn)" : turn.AssistantContent.Trim();
|
||||
runState.ToolConversationMessages.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["role"] = "assistant",
|
||||
["content"] = content
|
||||
});
|
||||
|
||||
if (runState.DirectAnswerAttempts >= 4)
|
||||
{
|
||||
yield return CreateEvent(runId, ref sequence, "error", new MccErrorData(
|
||||
"model_protocol_error",
|
||||
"The model kept returning plain assistant text instead of using tools or mcc_submit_final.",
|
||||
content));
|
||||
yield return CreateEvent(runId, ref sequence, "final", finalizer.BuildHardStopResult(runState, options));
|
||||
yield break;
|
||||
}
|
||||
|
||||
runState.ToolConversationMessages.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["role"] = "user",
|
||||
["content"] = "The previous plain assistant text was not accepted by this harness. On your next turn, you must either call the relevant MCC tools or call mcc_submit_final. Do not answer with plain assistant text again."
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
Dictionary<string, object?> assistantMessage = new()
|
||||
{
|
||||
["role"] = "assistant",
|
||||
["content"] = turn.AssistantContent,
|
||||
["tool_calls"] = turn.ToolCalls.Select(call => new Dictionary<string, object?>
|
||||
{
|
||||
["id"] = call.CallId,
|
||||
["type"] = "function",
|
||||
["function"] = new Dictionary<string, object?>
|
||||
{
|
||||
["name"] = call.Name,
|
||||
["arguments"] = call.ArgumentsJson
|
||||
}
|
||||
}).ToArray()
|
||||
};
|
||||
runState.ToolConversationMessages.Add(assistantMessage);
|
||||
|
||||
foreach (MccModelToolCall toolCall in turn.ToolCalls)
|
||||
{
|
||||
MccToolProfile profile = MccToolPolicy.GetProfile(toolCall.Name);
|
||||
yield return CreateEvent(runId, ref sequence, "tool_called", new MccToolCalledData(
|
||||
toolCall.CallId,
|
||||
toolCall.Name,
|
||||
toolCall.ArgumentsJson,
|
||||
profile.Risk == MccToolRisk.EscapeHatch,
|
||||
profile.Risk == MccToolRisk.Sensitive));
|
||||
|
||||
if (toolCall.Name.Equals("mcc_submit_final", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
MccFinalizationValidation validation = finalizer.Validate(runState, toolCall.ArgumentsJson);
|
||||
if (validation.Accepted)
|
||||
{
|
||||
yield return CreateEvent(runId, ref sequence, "final", validation.Payload!);
|
||||
yield break;
|
||||
}
|
||||
|
||||
string localResultText = JsonSerializer.Serialize(new
|
||||
{
|
||||
success = false,
|
||||
errorCode = "invalid_final_submission",
|
||||
message = validation.ErrorText
|
||||
});
|
||||
runState.ToolConversationMessages.Add(BuildToolMessage(toolCall.CallId, localResultText));
|
||||
yield return CreateEvent(runId, ref sequence, "tool_result", new MccToolResultData(
|
||||
toolCall.CallId,
|
||||
toolCall.Name,
|
||||
IsError: true,
|
||||
Success: false,
|
||||
ErrorCode: "invalid_final_submission",
|
||||
Summary: validation.ErrorText ?? "Invalid final submission.",
|
||||
RawText: localResultText,
|
||||
EvidenceId: string.Empty));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (MccToolPolicy.RequiresExplicitUserIntent(toolCall.Name) && !MccToolPolicy.HasExplicitUserIntent(runState.UserRequest, toolCall.Name))
|
||||
{
|
||||
string localResultText = JsonSerializer.Serialize(new
|
||||
{
|
||||
success = false,
|
||||
errorCode = "explicit_user_intent_required",
|
||||
message = $"Tool '{toolCall.Name}' requires explicit user intent."
|
||||
});
|
||||
runState.ToolConversationMessages.Add(BuildToolMessage(toolCall.CallId, localResultText));
|
||||
yield return CreateEvent(runId, ref sequence, "tool_result", new MccToolResultData(
|
||||
toolCall.CallId,
|
||||
toolCall.Name,
|
||||
IsError: true,
|
||||
Success: false,
|
||||
ErrorCode: "explicit_user_intent_required",
|
||||
Summary: $"Tool '{toolCall.Name}' requires explicit user intent.",
|
||||
RawText: localResultText,
|
||||
EvidenceId: string.Empty));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!catalog.ToolsByName.TryGetValue(toolCall.Name, out MccToolCatalogEntry? entry))
|
||||
{
|
||||
string unknownToolText = JsonSerializer.Serialize(new
|
||||
{
|
||||
success = false,
|
||||
errorCode = "unknown_tool",
|
||||
message = $"Unknown tool '{toolCall.Name}'."
|
||||
});
|
||||
runState.ToolConversationMessages.Add(BuildToolMessage(toolCall.CallId, unknownToolText));
|
||||
yield return CreateEvent(runId, ref sequence, "tool_result", new MccToolResultData(
|
||||
toolCall.CallId,
|
||||
toolCall.Name,
|
||||
IsError: true,
|
||||
Success: false,
|
||||
ErrorCode: "unknown_tool",
|
||||
Summary: $"Unknown tool '{toolCall.Name}'.",
|
||||
RawText: unknownToolText,
|
||||
EvidenceId: string.Empty));
|
||||
continue;
|
||||
}
|
||||
|
||||
CallToolResult? result = null;
|
||||
Exception? toolException = null;
|
||||
try
|
||||
{
|
||||
Dictionary<string, object?> arguments = MccJsonArguments.Parse(toolCall.ArgumentsJson);
|
||||
result = await client.CallToolAsync(toolCall.Name, arguments, cancellationToken: linkedToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
toolException = ex;
|
||||
}
|
||||
|
||||
if (toolException is not null || result is null)
|
||||
{
|
||||
string failedText = JsonSerializer.Serialize(new
|
||||
{
|
||||
success = false,
|
||||
errorCode = "tool_call_failed",
|
||||
message = toolException?.Message
|
||||
});
|
||||
runState.ToolConversationMessages.Add(BuildToolMessage(toolCall.CallId, failedText));
|
||||
yield return CreateEvent(runId, ref sequence, "tool_result", new MccToolResultData(
|
||||
toolCall.CallId,
|
||||
toolCall.Name,
|
||||
IsError: true,
|
||||
Success: false,
|
||||
ErrorCode: "tool_call_failed",
|
||||
Summary: toolException?.Message ?? "Tool call failed.",
|
||||
RawText: failedText,
|
||||
EvidenceId: string.Empty));
|
||||
continue;
|
||||
}
|
||||
|
||||
runState.ToolCallCount++;
|
||||
MccNormalizedToolResult normalized = MccMcpJson.Normalize(result);
|
||||
MccEvidenceRecord evidence = CreateEvidence(runState, toolCall.Name, normalized);
|
||||
runState.Evidence.Add(evidence);
|
||||
runState.ToolExecutions.Add(new MccToolExecutionRecord
|
||||
{
|
||||
CallId = toolCall.CallId,
|
||||
ToolName = toolCall.Name,
|
||||
ArgumentsJson = toolCall.ArgumentsJson,
|
||||
Evidence = evidence
|
||||
});
|
||||
|
||||
runState.ToolConversationMessages.Add(BuildToolMessage(toolCall.CallId, normalized.Text));
|
||||
|
||||
foreach (MccVerificationObligation obligation in CreateObligations(runState, evidence, toolCall.ArgumentsJson))
|
||||
{
|
||||
runState.VerificationObligations.Add(obligation);
|
||||
yield return CreateEvent(runId, ref sequence, "verification_required", new MccVerificationEventData(
|
||||
obligation.Id,
|
||||
obligation.ToolName,
|
||||
obligation.Kind,
|
||||
obligation.Description));
|
||||
|
||||
if (obligation.Cleared)
|
||||
{
|
||||
yield return CreateEvent(runId, ref sequence, "verification_cleared", new MccVerificationEventData(
|
||||
obligation.Id,
|
||||
obligation.ToolName,
|
||||
obligation.Kind,
|
||||
obligation.Description));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (MccVerificationObligation cleared in TryClearObligationsFromEvidence(runState, evidence))
|
||||
{
|
||||
yield return CreateEvent(runId, ref sequence, "verification_cleared", new MccVerificationEventData(
|
||||
cleared.Id,
|
||||
cleared.ToolName,
|
||||
cleared.Kind,
|
||||
cleared.Description));
|
||||
}
|
||||
|
||||
yield return CreateEvent(runId, ref sequence, "tool_result", new MccToolResultData(
|
||||
toolCall.CallId,
|
||||
toolCall.Name,
|
||||
evidence.IsError,
|
||||
evidence.Success,
|
||||
evidence.ErrorCode,
|
||||
evidence.Summary,
|
||||
evidence.RawText,
|
||||
evidence.Id));
|
||||
}
|
||||
}
|
||||
|
||||
yield return CreateEvent(runId, ref sequence, "final", finalizer.BuildHardStopResult(runState, options));
|
||||
}
|
||||
|
||||
private static List<object> NormalizeConversation(List<ChatMessage>? incoming)
|
||||
{
|
||||
List<object> messages = [];
|
||||
if (incoming is null)
|
||||
return messages;
|
||||
|
||||
foreach (ChatMessage message in incoming)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message.Role) || string.IsNullOrWhiteSpace(message.Content))
|
||||
continue;
|
||||
|
||||
string role = message.Role.Trim().ToLowerInvariant();
|
||||
if (role is not ("user" or "assistant" or "system"))
|
||||
continue;
|
||||
|
||||
messages.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["role"] = role,
|
||||
["content"] = message.Content.Trim()
|
||||
});
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
private static string ExtractUserRequest(List<ChatMessage>? incoming)
|
||||
{
|
||||
return incoming?
|
||||
.LastOrDefault(message => string.Equals(message.Role, "user", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.IsNullOrWhiteSpace(message.Content))
|
||||
?.Content
|
||||
?.Trim()
|
||||
?? string.Empty;
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> BuildToolMessage(string callId, string content)
|
||||
{
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["role"] = "tool",
|
||||
["tool_call_id"] = callId,
|
||||
["content"] = content
|
||||
};
|
||||
}
|
||||
|
||||
private static MccStateSummaryData BuildStateSummary(MccRunState runState, MccWebHarnessOptions options)
|
||||
{
|
||||
return new MccStateSummaryData(
|
||||
TurnCount: runState.TurnCount,
|
||||
ToolCallCount: runState.ToolCallCount,
|
||||
SoftFinish: runState.IsSoftFinish(options, DateTimeOffset.UtcNow),
|
||||
DirectAnswerAttempts: runState.DirectAnswerAttempts,
|
||||
OpenVerification: runState.OpenObligations
|
||||
.Select(obligation => new MccVerificationObligationView(obligation.Id, obligation.ToolName, obligation.Kind, obligation.Description))
|
||||
.ToArray(),
|
||||
RecentEvidence: runState.Evidence
|
||||
.TakeLast(6)
|
||||
.Select(evidence => new MccEvidenceView(evidence.Id, evidence.ToolName, evidence.Summary, evidence.IsError))
|
||||
.ToArray(),
|
||||
CompactionSummary: runState.CompactionSummary);
|
||||
}
|
||||
|
||||
private MccBudgetData BuildBudgetData(MccRunState runState)
|
||||
{
|
||||
return new MccBudgetData(
|
||||
TurnCount: runState.TurnCount,
|
||||
MaxTurns: options.MaxTurns,
|
||||
ToolCallCount: runState.ToolCallCount,
|
||||
MaxToolCalls: options.MaxToolCalls,
|
||||
ElapsedSeconds: (DateTimeOffset.UtcNow - runState.StartedAtUtc).TotalSeconds,
|
||||
MaxWallClockSeconds: options.MaxWallClockSeconds);
|
||||
}
|
||||
|
||||
private static MccEvidenceRecord CreateEvidence(MccRunState runState, string toolName, MccNormalizedToolResult result)
|
||||
{
|
||||
string summary = SummarizeEvidence(toolName, result);
|
||||
return new MccEvidenceRecord
|
||||
{
|
||||
Id = runState.NextEvidenceId(),
|
||||
ToolName = toolName,
|
||||
Summary = summary,
|
||||
RawText = result.Text,
|
||||
IsError = result.IsError,
|
||||
Success = result.Success,
|
||||
ErrorCode = result.ErrorCode,
|
||||
Root = result.Root,
|
||||
Data = result.Data
|
||||
};
|
||||
}
|
||||
|
||||
private static string SummarizeEvidence(string toolName, MccNormalizedToolResult result)
|
||||
{
|
||||
if (result.Data is JsonElement data)
|
||||
{
|
||||
if ((toolName.Equals("mcc_move_to", StringComparison.OrdinalIgnoreCase) || toolName.Equals("mcc_move_to_player", StringComparison.OrdinalIgnoreCase))
|
||||
&& TryReadBool(data, "arrived", out bool arrived))
|
||||
{
|
||||
return arrived
|
||||
? $"movement verified; arrived={arrived}"
|
||||
: $"movement not yet verified; arrived={arrived}";
|
||||
}
|
||||
|
||||
if (toolName.Equals("mcc_dig_block", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
bool destroyed = TryReadBool(data, "destroyed", out bool destroyedValue) && destroyedValue;
|
||||
bool changed = TryReadBool(data, "changed", out bool changedValue) && changedValue;
|
||||
return $"dig result changed={changed} destroyed={destroyed}";
|
||||
}
|
||||
|
||||
if (toolName.Equals("mcc_items_pickup", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
int successful = TryReadInt(data, "successfulPickups", out int successfulValue) ? successfulValue : 0;
|
||||
int collected = TryReadInt(data, "collectedCount", out int collectedValue) ? collectedValue : 0;
|
||||
return $"pickup result successfulPickups={successful} collectedCount={collected}";
|
||||
}
|
||||
|
||||
if (toolName.Equals("mcc_container_open_at", StringComparison.OrdinalIgnoreCase)
|
||||
&& TryReadBool(data, "opened", out bool opened))
|
||||
{
|
||||
return $"container open result opened={opened}";
|
||||
}
|
||||
|
||||
if (toolName is "mcc_container_deposit_item" or "mcc_container_withdraw_item" or "mcc_inventory_drop_item")
|
||||
{
|
||||
int moved = TryReadInt(data, "movedCount", out int movedValue)
|
||||
? movedValue
|
||||
: TryReadInt(data, "droppedCount", out int droppedValue) ? droppedValue : 0;
|
||||
return $"{toolName} movedCount={moved}";
|
||||
}
|
||||
}
|
||||
|
||||
string prefix = result.IsError ? "error" : "ok";
|
||||
return $"{prefix}: {Truncate(result.Text.Replace('\n', ' '), 180)}";
|
||||
}
|
||||
|
||||
private List<MccVerificationObligation> CreateObligations(MccRunState runState, MccEvidenceRecord evidence, string argumentsJson)
|
||||
{
|
||||
List<MccVerificationObligation> obligations = [];
|
||||
JsonElement metadata = ParseArgumentsToJson(argumentsJson);
|
||||
|
||||
if (evidence.ToolName.Equals("mcc_move_to", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
MccVerificationObligation obligation = new()
|
||||
{
|
||||
Id = runState.NextObligationId(),
|
||||
ToolName = evidence.ToolName,
|
||||
Kind = "movement",
|
||||
Description = "Verify final player location for the requested move target.",
|
||||
SourceEvidenceId = evidence.Id,
|
||||
Metadata = BuildMoveMetadata(evidence, metadata),
|
||||
Cleared = IsMovementVerified(evidence)
|
||||
};
|
||||
obligations.Add(obligation);
|
||||
return obligations;
|
||||
}
|
||||
|
||||
if (evidence.ToolName.Equals("mcc_move_to_player", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
MccVerificationObligation obligation = new()
|
||||
{
|
||||
Id = runState.NextObligationId(),
|
||||
ToolName = evidence.ToolName,
|
||||
Kind = "movement",
|
||||
Description = "Verify final proximity to the requested player target.",
|
||||
SourceEvidenceId = evidence.Id,
|
||||
Metadata = BuildMoveToPlayerMetadata(evidence, metadata),
|
||||
Cleared = IsMovementVerified(evidence)
|
||||
};
|
||||
obligations.Add(obligation);
|
||||
return obligations;
|
||||
}
|
||||
|
||||
if (evidence.ToolName.Equals("mcc_container_open_at", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
obligations.Add(new MccVerificationObligation
|
||||
{
|
||||
Id = runState.NextObligationId(),
|
||||
ToolName = evidence.ToolName,
|
||||
Kind = "container",
|
||||
Description = "Verify that the target container is open and active.",
|
||||
SourceEvidenceId = evidence.Id,
|
||||
Metadata = null,
|
||||
Cleared = IsContainerOpenVerified(evidence)
|
||||
});
|
||||
return obligations;
|
||||
}
|
||||
|
||||
if (evidence.ToolName is "mcc_container_deposit_item" or "mcc_container_withdraw_item" or "mcc_inventory_drop_item")
|
||||
{
|
||||
obligations.Add(new MccVerificationObligation
|
||||
{
|
||||
Id = runState.NextObligationId(),
|
||||
ToolName = evidence.ToolName,
|
||||
Kind = "inventory",
|
||||
Description = "Verify the requested inventory delta.",
|
||||
SourceEvidenceId = evidence.Id,
|
||||
Metadata = evidence.Data,
|
||||
Cleared = IsInventoryVerified(evidence)
|
||||
});
|
||||
return obligations;
|
||||
}
|
||||
|
||||
if (evidence.ToolName.Equals("mcc_items_pickup", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
obligations.Add(new MccVerificationObligation
|
||||
{
|
||||
Id = runState.NextObligationId(),
|
||||
ToolName = evidence.ToolName,
|
||||
Kind = "pickup",
|
||||
Description = "Verify that the requested dropped items were picked up.",
|
||||
SourceEvidenceId = evidence.Id,
|
||||
Metadata = evidence.Data,
|
||||
Cleared = IsPickupVerified(evidence)
|
||||
});
|
||||
return obligations;
|
||||
}
|
||||
|
||||
if (evidence.ToolName.Equals("mcc_dig_block", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
obligations.Add(new MccVerificationObligation
|
||||
{
|
||||
Id = runState.NextObligationId(),
|
||||
ToolName = evidence.ToolName,
|
||||
Kind = "block_change",
|
||||
Description = "Verify that the target block changed state after digging.",
|
||||
SourceEvidenceId = evidence.Id,
|
||||
Metadata = evidence.Data,
|
||||
Cleared = IsDigVerified(evidence)
|
||||
});
|
||||
}
|
||||
|
||||
return obligations;
|
||||
}
|
||||
|
||||
private List<MccVerificationObligation> TryClearObligationsFromEvidence(MccRunState runState, MccEvidenceRecord evidence)
|
||||
{
|
||||
List<MccVerificationObligation> cleared = [];
|
||||
foreach (MccVerificationObligation obligation in runState.OpenObligations)
|
||||
{
|
||||
if (obligation.Cleared)
|
||||
continue;
|
||||
|
||||
if (obligation.Kind == "movement" && TryClearMovementObligation(obligation, evidence))
|
||||
{
|
||||
obligation.Cleared = true;
|
||||
obligation.ClearedByEvidenceId = evidence.Id;
|
||||
cleared.Add(obligation);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (obligation.Kind == "block_change" && TryClearDigObligation(obligation, evidence))
|
||||
{
|
||||
obligation.Cleared = true;
|
||||
obligation.ClearedByEvidenceId = evidence.Id;
|
||||
cleared.Add(obligation);
|
||||
}
|
||||
}
|
||||
|
||||
return cleared;
|
||||
}
|
||||
|
||||
private static bool TryClearMovementObligation(MccVerificationObligation obligation, MccEvidenceRecord evidence)
|
||||
{
|
||||
if (evidence.ToolName.Equals("mcc_player_state", StringComparison.OrdinalIgnoreCase)
|
||||
&& evidence.Data is JsonElement data
|
||||
&& data.TryGetProperty("location", out JsonElement location)
|
||||
&& obligation.Metadata is JsonElement metadata)
|
||||
{
|
||||
if (obligation.ToolName.Equals("mcc_move_to", StringComparison.OrdinalIgnoreCase)
|
||||
&& metadata.TryGetProperty("x", out JsonElement targetX)
|
||||
&& metadata.TryGetProperty("y", out JsonElement targetY)
|
||||
&& metadata.TryGetProperty("z", out JsonElement targetZ))
|
||||
{
|
||||
double tolerance = metadata.TryGetProperty("tolerance", out JsonElement toleranceElement) && toleranceElement.TryGetDouble(out double tol) ? tol : 1.5;
|
||||
return TryReadDouble(location, "x", out double x)
|
||||
&& TryReadDouble(location, "y", out double y)
|
||||
&& TryReadDouble(location, "z", out double z)
|
||||
&& Distance(x, y, z, targetX.GetDouble(), targetY.GetDouble(), targetZ.GetDouble()) <= tolerance;
|
||||
}
|
||||
}
|
||||
|
||||
if (evidence.ToolName.Equals("mcc_player_locate", StringComparison.OrdinalIgnoreCase)
|
||||
&& obligation.ToolName.Equals("mcc_move_to_player", StringComparison.OrdinalIgnoreCase)
|
||||
&& evidence.Data is JsonElement playerData
|
||||
&& obligation.Metadata is JsonElement playerMetadata)
|
||||
{
|
||||
string? expectedName = playerMetadata.TryGetProperty("playerName", out JsonElement nameElement) ? nameElement.GetString() : null;
|
||||
string? matchedName = playerData.TryGetProperty("matchedName", out JsonElement matchedNameElement) ? matchedNameElement.GetString() : null;
|
||||
if (!string.IsNullOrWhiteSpace(expectedName) && !string.Equals(expectedName, matchedName, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (TryReadDouble(playerData, "distance", out double distance))
|
||||
{
|
||||
double tolerance = playerMetadata.TryGetProperty("tolerance", out JsonElement toleranceElement) && toleranceElement.TryGetDouble(out double tol) ? tol : 2.0;
|
||||
return distance <= tolerance;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryClearDigObligation(MccVerificationObligation obligation, MccEvidenceRecord evidence)
|
||||
{
|
||||
if (!evidence.ToolName.Equals("mcc_world_block_at", StringComparison.OrdinalIgnoreCase)
|
||||
|| evidence.Data is not JsonElement data
|
||||
|| obligation.Metadata is not JsonElement metadata)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!metadata.TryGetProperty("target", out JsonElement target)
|
||||
|| !TryReadDouble(target, "x", out double x)
|
||||
|| !TryReadDouble(target, "y", out double y)
|
||||
|| !TryReadDouble(target, "z", out double z))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryReadInt(data, "x", out int blockX)
|
||||
&& TryReadInt(data, "y", out int blockY)
|
||||
&& TryReadInt(data, "z", out int blockZ)
|
||||
&& Math.Abs(blockX - x) < 0.5
|
||||
&& Math.Abs(blockY - y) < 0.5
|
||||
&& Math.Abs(blockZ - z) < 0.5
|
||||
&& data.TryGetProperty("block", out JsonElement block)
|
||||
&& block.TryGetProperty("material", out JsonElement material)
|
||||
&& !string.Equals(material.GetString(), "Air", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsMovementVerified(MccEvidenceRecord evidence)
|
||||
{
|
||||
if (evidence.Data is not JsonElement data)
|
||||
return false;
|
||||
|
||||
if (TryReadBool(data, "arrived", out bool arrived) && arrived)
|
||||
return true;
|
||||
|
||||
if (TryReadDouble(data, "finalDistance", out double finalDistance))
|
||||
{
|
||||
double tolerance = TryReadDouble(data, "tolerance", out double tol) ? tol : 1.5;
|
||||
return finalDistance <= tolerance;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsContainerOpenVerified(MccEvidenceRecord evidence)
|
||||
{
|
||||
return evidence.Data is JsonElement data
|
||||
&& TryReadBool(data, "opened", out bool opened)
|
||||
&& opened;
|
||||
}
|
||||
|
||||
private static bool IsInventoryVerified(MccEvidenceRecord evidence)
|
||||
{
|
||||
if (evidence.Data is not JsonElement data)
|
||||
return false;
|
||||
|
||||
if (TryReadInt(data, "requestedCount", out int requestedCount)
|
||||
&& TryReadInt(data, "movedCount", out int movedCount))
|
||||
{
|
||||
return movedCount == requestedCount;
|
||||
}
|
||||
|
||||
if (TryReadInt(data, "requestedCount", out requestedCount)
|
||||
&& TryReadInt(data, "droppedCount", out int droppedCount))
|
||||
{
|
||||
return droppedCount == requestedCount;
|
||||
}
|
||||
|
||||
return evidence.Success;
|
||||
}
|
||||
|
||||
private static bool IsPickupVerified(MccEvidenceRecord evidence)
|
||||
{
|
||||
if (evidence.Data is not JsonElement data)
|
||||
return false;
|
||||
|
||||
return (TryReadInt(data, "successfulPickups", out int successfulPickups) && successfulPickups > 0)
|
||||
|| (TryReadInt(data, "collectedCount", out int collectedCount) && collectedCount > 0);
|
||||
}
|
||||
|
||||
private static bool IsDigVerified(MccEvidenceRecord evidence)
|
||||
{
|
||||
if (evidence.Data is not JsonElement data)
|
||||
return false;
|
||||
|
||||
return (TryReadBool(data, "destroyed", out bool destroyed) && destroyed)
|
||||
|| (TryReadBool(data, "changed", out bool changed) && changed);
|
||||
}
|
||||
|
||||
private static JsonElement? BuildMoveMetadata(MccEvidenceRecord evidence, JsonElement arguments)
|
||||
{
|
||||
if (evidence.Data is not JsonElement data)
|
||||
return null;
|
||||
|
||||
double x = TryReadDoubleFromArguments(arguments, "x", out double targetX)
|
||||
? targetX
|
||||
: data.TryGetProperty("target", out JsonElement target) && TryReadDouble(target, "x", out double fromDataX) ? fromDataX : 0;
|
||||
double y = TryReadDoubleFromArguments(arguments, "y", out double targetY)
|
||||
? targetY
|
||||
: data.TryGetProperty("target", out target) && TryReadDouble(target, "y", out double fromDataY) ? fromDataY : 0;
|
||||
double z = TryReadDoubleFromArguments(arguments, "z", out double targetZ)
|
||||
? targetZ
|
||||
: data.TryGetProperty("target", out target) && TryReadDouble(target, "z", out double fromDataZ) ? fromDataZ : 0;
|
||||
double tolerance = TryReadDouble(data, "tolerance", out double tol) ? tol : 1.5;
|
||||
|
||||
return JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
tolerance
|
||||
});
|
||||
}
|
||||
|
||||
private static JsonElement? BuildMoveToPlayerMetadata(MccEvidenceRecord evidence, JsonElement arguments)
|
||||
{
|
||||
string? playerName = arguments.TryGetProperty("playerName", out JsonElement property) ? property.GetString() : null;
|
||||
double tolerance = evidence.Data is JsonElement data && TryReadDouble(data, "tolerance", out double tol) ? tol : 2.0;
|
||||
return JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
playerName,
|
||||
tolerance
|
||||
});
|
||||
}
|
||||
|
||||
private static JsonElement ParseArgumentsToJson(string argumentsJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson);
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
catch
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse("{}");
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadBool(JsonElement element, string propertyName, out bool value)
|
||||
{
|
||||
value = false;
|
||||
return element.TryGetProperty(propertyName, out JsonElement property)
|
||||
&& property.ValueKind is JsonValueKind.True or JsonValueKind.False
|
||||
&& ((value = property.GetBoolean()) || !value || true);
|
||||
}
|
||||
|
||||
private static bool TryReadInt(JsonElement element, string propertyName, out int value)
|
||||
{
|
||||
value = 0;
|
||||
return element.TryGetProperty(propertyName, out JsonElement property) && property.TryGetInt32(out value);
|
||||
}
|
||||
|
||||
private static bool TryReadDouble(JsonElement element, string propertyName, out double value)
|
||||
{
|
||||
value = 0;
|
||||
return element.TryGetProperty(propertyName, out JsonElement property) && property.TryGetDouble(out value);
|
||||
}
|
||||
|
||||
private static bool TryReadDoubleFromArguments(JsonElement element, string propertyName, out double value)
|
||||
{
|
||||
value = 0;
|
||||
if (!element.TryGetProperty(propertyName, out JsonElement property))
|
||||
return false;
|
||||
|
||||
return property.ValueKind == JsonValueKind.Number
|
||||
? property.TryGetDouble(out value)
|
||||
: property.ValueKind == JsonValueKind.String && double.TryParse(property.GetString(), out value);
|
||||
}
|
||||
|
||||
private static double Distance(double x1, double y1, double z1, double x2, double y2, double z2)
|
||||
{
|
||||
double dx = x1 - x2;
|
||||
double dy = y1 - y2;
|
||||
double dz = z1 - z2;
|
||||
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
private static string Truncate(string text, int maxLength)
|
||||
{
|
||||
return string.IsNullOrEmpty(text) || text.Length <= maxLength ? text : text[..maxLength] + "...";
|
||||
}
|
||||
|
||||
private static SseItem<MccStreamEnvelope> CreateEvent<T>(string runId, ref long sequence, string kind, T data)
|
||||
{
|
||||
sequence++;
|
||||
return new SseItem<MccStreamEnvelope>(
|
||||
new MccStreamEnvelope(runId, sequence, kind, data!),
|
||||
kind)
|
||||
{
|
||||
EventId = sequence.ToString(CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
namespace DebugTools.MccMcpWebPlayground.Harness;
|
||||
|
||||
public sealed class MccContextCompressor
|
||||
{
|
||||
public void CompactIfNeeded(MccRunState runState)
|
||||
{
|
||||
if (runState.Evidence.Count <= 6)
|
||||
return;
|
||||
|
||||
IReadOnlyList<MccEvidenceRecord> olderEvidence = runState.Evidence
|
||||
.Take(Math.Max(0, runState.Evidence.Count - 6))
|
||||
.ToArray();
|
||||
|
||||
if (olderEvidence.Count == 0)
|
||||
return;
|
||||
|
||||
runState.CompactionSummary = string.Join('\n', olderEvidence
|
||||
.TakeLast(8)
|
||||
.Select(record => $"- {record.Id} {record.ToolName}: {record.Summary}"));
|
||||
}
|
||||
}
|
||||
221
DebugTools/MccMcpWebPlayground/Harness/MccFinalizer.cs
Normal file
221
DebugTools/MccMcpWebPlayground/Harness/MccFinalizer.cs
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using DebugTools.MccMcpWebPlayground.Contracts;
|
||||
|
||||
namespace DebugTools.MccMcpWebPlayground.Harness;
|
||||
|
||||
public sealed class MccFinalizer
|
||||
{
|
||||
private static readonly string[] AllowedStatuses = ["completed", "partial", "blocked", "clarification_needed", "failed"];
|
||||
|
||||
public object BuildSubmitToolSchema()
|
||||
{
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["type"] = "function",
|
||||
["function"] = new Dictionary<string, object?>
|
||||
{
|
||||
["name"] = "mcc_submit_final",
|
||||
["description"] = "Submit the final result for this MCC run. Use completed only when no required verification obligations remain open.",
|
||||
["parameters"] = new JsonObject
|
||||
{
|
||||
["type"] = "object",
|
||||
["additionalProperties"] = false,
|
||||
["properties"] = new JsonObject
|
||||
{
|
||||
["status"] = new JsonObject
|
||||
{
|
||||
["type"] = "string",
|
||||
["enum"] = new JsonArray(AllowedStatuses.Select(status => JsonValue.Create(status)).ToArray())
|
||||
},
|
||||
["headline"] = new JsonObject { ["type"] = "string" },
|
||||
["answerMarkdown"] = new JsonObject { ["type"] = "string" },
|
||||
["verifiedFacts"] = new JsonObject
|
||||
{
|
||||
["type"] = "array",
|
||||
["items"] = new JsonObject { ["type"] = "string" }
|
||||
},
|
||||
["openIssues"] = new JsonObject
|
||||
{
|
||||
["type"] = "array",
|
||||
["items"] = new JsonObject { ["type"] = "string" }
|
||||
},
|
||||
["evidenceIds"] = new JsonObject
|
||||
{
|
||||
["type"] = "array",
|
||||
["items"] = new JsonObject { ["type"] = "string" }
|
||||
},
|
||||
["nextAction"] = new JsonObject
|
||||
{
|
||||
["type"] = new JsonArray("string", "null")
|
||||
}
|
||||
},
|
||||
["required"] = new JsonArray("status", "headline", "answerMarkdown", "verifiedFacts", "openIssues", "evidenceIds", "nextAction")
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public MccFinalizationValidation Validate(MccRunState runState, string argumentsJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson);
|
||||
JsonElement root = document.RootElement;
|
||||
MccSubmitFinalArgs submission = new(
|
||||
Status: ReadRequiredString(root, "status"),
|
||||
Headline: ReadRequiredString(root, "headline"),
|
||||
AnswerMarkdown: ReadRequiredString(root, "answerMarkdown"),
|
||||
VerifiedFacts: ReadStringArray(root, "verifiedFacts"),
|
||||
OpenIssues: ReadStringArray(root, "openIssues"),
|
||||
EvidenceIds: ReadStringArray(root, "evidenceIds"),
|
||||
NextAction: ReadNullableString(root, "nextAction"));
|
||||
|
||||
string normalizedStatus = submission.Status.Trim().ToLowerInvariant();
|
||||
if (!AllowedStatuses.Contains(normalizedStatus, StringComparer.Ordinal))
|
||||
return MccFinalizationValidation.Reject("Invalid final status.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(submission.Headline) || string.IsNullOrWhiteSpace(submission.AnswerMarkdown))
|
||||
return MccFinalizationValidation.Reject("headline and answerMarkdown are required.");
|
||||
|
||||
Dictionary<string, MccEvidenceRecord> evidenceById = runState.Evidence.ToDictionary(record => record.Id, StringComparer.OrdinalIgnoreCase);
|
||||
Dictionary<string, string> evidenceAliasByCallId = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (MccToolExecutionRecord execution in runState.ToolExecutions)
|
||||
{
|
||||
evidenceAliasByCallId[execution.CallId] = execution.Evidence.Id;
|
||||
|
||||
int suffixSeparator = execution.CallId.LastIndexOf('_');
|
||||
if (suffixSeparator >= 0 && suffixSeparator < execution.CallId.Length - 1)
|
||||
evidenceAliasByCallId[execution.CallId[(suffixSeparator + 1)..]] = execution.Evidence.Id;
|
||||
}
|
||||
|
||||
List<string> normalizedEvidenceIds = [];
|
||||
foreach (string evidenceId in submission.EvidenceIds)
|
||||
{
|
||||
string normalizedEvidenceId = evidenceAliasByCallId.TryGetValue(evidenceId, out string? mappedEvidenceId)
|
||||
? mappedEvidenceId
|
||||
: evidenceId;
|
||||
|
||||
if (!evidenceById.ContainsKey(normalizedEvidenceId))
|
||||
return MccFinalizationValidation.Reject($"Unknown evidence id '{evidenceId}'.");
|
||||
|
||||
if (!normalizedEvidenceIds.Contains(normalizedEvidenceId, StringComparer.OrdinalIgnoreCase))
|
||||
normalizedEvidenceIds.Add(normalizedEvidenceId);
|
||||
}
|
||||
|
||||
if (normalizedStatus == "completed" && runState.OpenObligations.Count > 0)
|
||||
return MccFinalizationValidation.Reject("completed is invalid while verification obligations remain open.");
|
||||
|
||||
if (!AreVerifiedFactsGrounded(submission.VerifiedFacts, normalizedEvidenceIds, evidenceById))
|
||||
return MccFinalizationValidation.Reject("verifiedFacts must be grounded in the referenced evidence.");
|
||||
|
||||
return MccFinalizationValidation.Accept(new MccFinalPayload(
|
||||
normalizedStatus,
|
||||
submission.Headline.Trim(),
|
||||
submission.AnswerMarkdown.Trim(),
|
||||
submission.VerifiedFacts,
|
||||
submission.OpenIssues,
|
||||
normalizedEvidenceIds,
|
||||
string.IsNullOrWhiteSpace(submission.NextAction) ? null : submission.NextAction.Trim()));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MccFinalizationValidation.Reject($"Invalid mcc_submit_final payload: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public MccFinalPayload BuildHardStopResult(MccRunState runState, MccWebHarnessOptions options)
|
||||
{
|
||||
IReadOnlyList<string> openIssues = runState.OpenObligations.Count > 0
|
||||
? runState.OpenObligations.Select(obligation => obligation.Description).ToArray()
|
||||
: ["The harness reached its execution budget before the run was explicitly finalized."];
|
||||
|
||||
IReadOnlyList<string> evidenceIds = runState.Evidence.TakeLast(4).Select(record => record.Id).ToArray();
|
||||
IReadOnlyList<string> verifiedFacts = runState.Evidence
|
||||
.TakeLast(4)
|
||||
.Where(record => record.Success)
|
||||
.Select(record => record.Summary)
|
||||
.ToArray();
|
||||
|
||||
return new MccFinalPayload(
|
||||
Status: runState.OpenObligations.Count > 0 ? "partial" : "blocked",
|
||||
Headline: "Run stopped before explicit completion",
|
||||
AnswerMarkdown: "I could not finish the request within the current harness budget. I am returning the strongest verified state captured so far.",
|
||||
VerifiedFacts: verifiedFacts,
|
||||
OpenIssues: openIssues,
|
||||
EvidenceIds: evidenceIds,
|
||||
NextAction: "Retry with a fresh run if you want me to continue from the latest verified state.");
|
||||
}
|
||||
|
||||
private static bool AreVerifiedFactsGrounded(
|
||||
IReadOnlyList<string> verifiedFacts,
|
||||
IReadOnlyList<string> evidenceIds,
|
||||
IReadOnlyDictionary<string, MccEvidenceRecord> evidenceById)
|
||||
{
|
||||
if (verifiedFacts.Count == 0)
|
||||
return true;
|
||||
|
||||
if (evidenceIds.Count == 0)
|
||||
return false;
|
||||
|
||||
string evidenceCorpus = string.Join(' ', evidenceIds
|
||||
.Where(evidenceById.ContainsKey)
|
||||
.Select(id => evidenceById[id].Summary))
|
||||
.ToLowerInvariant();
|
||||
|
||||
foreach (string fact in verifiedFacts)
|
||||
{
|
||||
HashSet<string> factTokens = fact.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(token => token.Trim().Trim(',', '.', ':', ';', '!', '?', '"', '\''))
|
||||
.Where(token => token.Length >= 4)
|
||||
.Select(token => token.ToLowerInvariant())
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
if (factTokens.Count == 0)
|
||||
continue;
|
||||
|
||||
int matches = factTokens.Count(token => evidenceCorpus.Contains(token, StringComparison.Ordinal));
|
||||
if (matches < Math.Min(2, factTokens.Count))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string ReadRequiredString(JsonElement root, string propertyName)
|
||||
{
|
||||
string? value = ReadNullableString(root, propertyName);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
throw new InvalidOperationException($"{propertyName} is required.");
|
||||
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
private static string? ReadNullableString(JsonElement root, string propertyName)
|
||||
{
|
||||
if (!root.TryGetProperty(propertyName, out JsonElement property))
|
||||
return null;
|
||||
|
||||
return property.ValueKind == JsonValueKind.Null ? null : property.GetString();
|
||||
}
|
||||
|
||||
private static string[] ReadStringArray(JsonElement root, string propertyName)
|
||||
{
|
||||
if (!root.TryGetProperty(propertyName, out JsonElement property) || property.ValueKind != JsonValueKind.Array)
|
||||
return [];
|
||||
|
||||
return property.EnumerateArray()
|
||||
.Where(item => item.ValueKind == JsonValueKind.String)
|
||||
.Select(item => item.GetString())
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.Cast<string>()
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record MccFinalizationValidation(bool Accepted, string? ErrorText, MccFinalPayload? Payload)
|
||||
{
|
||||
public static MccFinalizationValidation Accept(MccFinalPayload payload) => new(true, null, payload);
|
||||
|
||||
public static MccFinalizationValidation Reject(string errorText) => new(false, errorText, null);
|
||||
}
|
||||
63
DebugTools/MccMcpWebPlayground/Harness/MccGuidanceSource.cs
Normal file
63
DebugTools/MccMcpWebPlayground/Harness/MccGuidanceSource.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using System.Text.Json;
|
||||
using DebugTools.MccMcpWebPlayground.Contracts;
|
||||
using DebugTools.MccMcpWebPlayground.Infrastructure.Mcp;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace DebugTools.MccMcpWebPlayground.Harness;
|
||||
|
||||
public sealed class MccGuidanceSource
|
||||
{
|
||||
public const string SourceToolName = "mcc_agent_guidance";
|
||||
public const string CanonicalPromptName = "mcc_operator_guide";
|
||||
|
||||
public async Task<MccGuidanceBundle> LoadAsync(McpClient client, CancellationToken cancellationToken)
|
||||
{
|
||||
CallToolResult result = await client.CallToolAsync(SourceToolName, new Dictionary<string, object?>(), cancellationToken: cancellationToken);
|
||||
MccNormalizedToolResult normalized = MccMcpJson.Normalize(result);
|
||||
JsonElement data = normalized.Data ?? throw new InvalidOperationException("mcc_agent_guidance did not return data.");
|
||||
|
||||
string[] bestPractices = ReadStringArray(data, "bestPractices");
|
||||
string[] exampleTitles = data.TryGetProperty("exampleScenarios", out JsonElement examples)
|
||||
&& examples.ValueKind == JsonValueKind.Array
|
||||
? examples.EnumerateArray()
|
||||
.Select(example => example.TryGetProperty("title", out JsonElement title) ? title.GetString() : null)
|
||||
.Where(title => !string.IsNullOrWhiteSpace(title))
|
||||
.Cast<string>()
|
||||
.ToArray()
|
||||
: [];
|
||||
|
||||
MccCapabilityStatus capabilityStatus = data.TryGetProperty("capabilityStatus", out JsonElement capabilityJson)
|
||||
? JsonSerializer.Deserialize<MccCapabilityStatus>(capabilityJson.GetRawText()) ?? new MccCapabilityStatus(false, false, false, false, false)
|
||||
: new MccCapabilityStatus(false, false, false, false, false);
|
||||
|
||||
return new MccGuidanceBundle(
|
||||
SourceToolName,
|
||||
CanonicalPromptName,
|
||||
SkillName: ReadString(data, "skillName") ?? "mcc-mcp-operator",
|
||||
GuidanceVersion: ReadString(data, "guidanceVersion") ?? "unknown",
|
||||
SystemPrompt: ReadString(data, "systemPrompt") ?? throw new InvalidOperationException("mcc_agent_guidance did not return systemPrompt."),
|
||||
BestPractices: bestPractices,
|
||||
ExampleScenarioTitles: exampleTitles,
|
||||
CapabilityStatus: capabilityStatus);
|
||||
}
|
||||
|
||||
private static string? ReadString(JsonElement element, string propertyName)
|
||||
{
|
||||
return element.TryGetProperty(propertyName, out JsonElement property) && property.ValueKind == JsonValueKind.String
|
||||
? property.GetString()
|
||||
: null;
|
||||
}
|
||||
|
||||
private static string[] ReadStringArray(JsonElement element, string propertyName)
|
||||
{
|
||||
return element.TryGetProperty(propertyName, out JsonElement property) && property.ValueKind == JsonValueKind.Array
|
||||
? property.EnumerateArray()
|
||||
.Where(item => item.ValueKind == JsonValueKind.String)
|
||||
.Select(item => item.GetString())
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.Cast<string>()
|
||||
.ToArray()
|
||||
: [];
|
||||
}
|
||||
}
|
||||
86
DebugTools/MccMcpWebPlayground/Harness/MccPromptComposer.cs
Normal file
86
DebugTools/MccMcpWebPlayground/Harness/MccPromptComposer.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
namespace DebugTools.MccMcpWebPlayground.Harness;
|
||||
|
||||
public sealed class MccPromptComposer
|
||||
{
|
||||
private const string HarnessContract = """
|
||||
You are operating Minecraft Console Client through MCC MCP tools.
|
||||
|
||||
Rules:
|
||||
- Use tool results and the run-state summary as the source of truth.
|
||||
- Execute tools sequentially.
|
||||
- End the run only with mcc_submit_final.
|
||||
- status=completed is valid only when no required verification obligations remain open.
|
||||
- If the task is blocked or partial, say exactly what is verified and what remains unverified.
|
||||
- Do not repeat the same failing stateful action with the same arguments.
|
||||
- mcc_quit_client requires explicit user intent.
|
||||
- Prefer structured high-level tools. Avoid escape hatches unless they are explicitly exposed and necessary.
|
||||
""";
|
||||
|
||||
public List<object> Compose(MccRunState runState)
|
||||
{
|
||||
List<object> messages =
|
||||
[
|
||||
BuildSystemMessage(HarnessContract),
|
||||
BuildSystemMessage(runState.Guidance.SystemPrompt),
|
||||
BuildSystemMessage(BuildStateSummary(runState)),
|
||||
.. runState.BaseConversationMessages
|
||||
];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(runState.CompactionSummary))
|
||||
{
|
||||
messages.Add(BuildSystemMessage($"""
|
||||
Older verified evidence summary
|
||||
{runState.CompactionSummary}
|
||||
"""));
|
||||
}
|
||||
|
||||
foreach (object message in runState.ToolConversationMessages.TakeLast(12))
|
||||
messages.Add(message);
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> BuildSystemMessage(string text)
|
||||
{
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["role"] = "system",
|
||||
["content"] = text
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildStateSummary(MccRunState runState)
|
||||
{
|
||||
string evidence = runState.Evidence.Count == 0
|
||||
? "- none yet"
|
||||
: string.Join('\n', runState.Evidence.TakeLast(6).Select(record =>
|
||||
$"- {record.Id} {record.ToolName}: {record.Summary}"));
|
||||
|
||||
string obligations = runState.OpenObligations.Count == 0
|
||||
? "- none"
|
||||
: string.Join('\n', runState.OpenObligations.Select(obligation =>
|
||||
$"- {obligation.Id} {obligation.ToolName}/{obligation.Kind}: {obligation.Description}"));
|
||||
|
||||
string bestPractices = runState.Guidance.BestPractices.Length == 0
|
||||
? "- use verified MCC state before claiming success"
|
||||
: string.Join('\n', runState.Guidance.BestPractices.Take(4).Select(item => $"- {item}"));
|
||||
|
||||
return $"""
|
||||
Current run state
|
||||
- turnCount: {runState.TurnCount}
|
||||
- toolCallCount: {runState.ToolCallCount}
|
||||
- directAnswerAttempts: {runState.DirectAnswerAttempts}
|
||||
- routedModel: {runState.RoutedModel ?? runState.ConfiguredModel}
|
||||
- routedProvider: {runState.RoutedProvider ?? "unknown"}
|
||||
|
||||
Outstanding verification
|
||||
{obligations}
|
||||
|
||||
Recent evidence
|
||||
{evidence}
|
||||
|
||||
Guidance highlights
|
||||
{bestPractices}
|
||||
""";
|
||||
}
|
||||
}
|
||||
103
DebugTools/MccMcpWebPlayground/Harness/MccRunState.cs
Normal file
103
DebugTools/MccMcpWebPlayground/Harness/MccRunState.cs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
using System.Text.Json;
|
||||
using DebugTools.MccMcpWebPlayground.Contracts;
|
||||
|
||||
namespace DebugTools.MccMcpWebPlayground.Harness;
|
||||
|
||||
public sealed class MccRunState
|
||||
{
|
||||
private int evidenceCounter;
|
||||
private int obligationCounter;
|
||||
|
||||
public required string RunId { get; init; }
|
||||
public required string UserRequest { get; init; }
|
||||
public required List<object> BaseConversationMessages { get; init; }
|
||||
public required string ConfiguredModel { get; init; }
|
||||
public required MccGuidanceBundle Guidance { get; init; }
|
||||
public DateTimeOffset StartedAtUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public List<object> ToolConversationMessages { get; } = [];
|
||||
public List<MccEvidenceRecord> Evidence { get; } = [];
|
||||
public List<MccToolExecutionRecord> ToolExecutions { get; } = [];
|
||||
public List<MccVerificationObligation> VerificationObligations { get; } = [];
|
||||
public string? CompactionSummary { get; set; }
|
||||
public string? RoutedModel { get; set; }
|
||||
public string? RoutedProvider { get; set; }
|
||||
public int TurnCount { get; set; }
|
||||
public int ToolCallCount { get; set; }
|
||||
public int DirectAnswerAttempts { get; set; }
|
||||
|
||||
public string NextEvidenceId() => $"e{++evidenceCounter:0000}";
|
||||
|
||||
public string NextObligationId() => $"v{++obligationCounter:0000}";
|
||||
|
||||
public bool IsSoftFinish(MccWebHarnessOptions options, DateTimeOffset nowUtc)
|
||||
{
|
||||
TimeSpan elapsed = nowUtc - StartedAtUtc;
|
||||
return (options.MaxTurns - TurnCount) <= options.SoftFinishRemainingTurns
|
||||
|| (options.MaxToolCalls - ToolCallCount) <= options.SoftFinishRemainingToolCalls
|
||||
|| (options.MaxWallClockSeconds - (int)elapsed.TotalSeconds) <= options.SoftFinishRemainingSeconds;
|
||||
}
|
||||
|
||||
public bool IsHardStop(MccWebHarnessOptions options, DateTimeOffset nowUtc)
|
||||
{
|
||||
TimeSpan elapsed = nowUtc - StartedAtUtc;
|
||||
return TurnCount >= options.MaxTurns
|
||||
|| ToolCallCount >= options.MaxToolCalls
|
||||
|| elapsed.TotalSeconds >= options.MaxWallClockSeconds;
|
||||
}
|
||||
|
||||
public IReadOnlyList<MccVerificationObligation> OpenObligations =>
|
||||
VerificationObligations.Where(obligation => !obligation.Cleared).ToArray();
|
||||
}
|
||||
|
||||
public sealed record MccGuidanceBundle(
|
||||
string SourceToolName,
|
||||
string CanonicalPromptName,
|
||||
string SkillName,
|
||||
string GuidanceVersion,
|
||||
string SystemPrompt,
|
||||
string[] BestPractices,
|
||||
string[] ExampleScenarioTitles,
|
||||
MccCapabilityStatus CapabilityStatus);
|
||||
|
||||
public sealed class MccEvidenceRecord
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string ToolName { get; init; }
|
||||
public required string Summary { get; init; }
|
||||
public required string RawText { get; init; }
|
||||
public required bool IsError { get; init; }
|
||||
public required bool Success { get; init; }
|
||||
public string? ErrorCode { get; init; }
|
||||
public JsonElement? Root { get; init; }
|
||||
public JsonElement? Data { get; init; }
|
||||
}
|
||||
|
||||
public sealed class MccToolExecutionRecord
|
||||
{
|
||||
public required string CallId { get; init; }
|
||||
public required string ToolName { get; init; }
|
||||
public required string ArgumentsJson { get; init; }
|
||||
public required MccEvidenceRecord Evidence { get; init; }
|
||||
}
|
||||
|
||||
public sealed class MccVerificationObligation
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string ToolName { get; init; }
|
||||
public required string Kind { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public required string SourceEvidenceId { get; init; }
|
||||
public JsonElement? Metadata { get; init; }
|
||||
public bool Cleared { get; set; }
|
||||
public string? ClearedByEvidenceId { get; set; }
|
||||
}
|
||||
|
||||
public sealed record MccNormalizedToolResult(
|
||||
string Text,
|
||||
bool IsError,
|
||||
bool Success,
|
||||
string? ErrorCode,
|
||||
string? Message,
|
||||
JsonElement? Root,
|
||||
JsonElement? Data);
|
||||
133
DebugTools/MccMcpWebPlayground/Harness/MccToolPolicy.cs
Normal file
133
DebugTools/MccMcpWebPlayground/Harness/MccToolPolicy.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using System.Collections.Frozen;
|
||||
using System.Text.Json.Nodes;
|
||||
using ModelContextProtocol.Client;
|
||||
|
||||
namespace DebugTools.MccMcpWebPlayground.Harness;
|
||||
|
||||
public enum MccToolRisk
|
||||
{
|
||||
ReadOnly,
|
||||
Stateful,
|
||||
Sensitive,
|
||||
EscapeHatch
|
||||
}
|
||||
|
||||
public sealed record MccToolProfile(
|
||||
string Name,
|
||||
MccToolRisk Risk,
|
||||
bool VisibleByDefault,
|
||||
bool RequiresExplicitUserIntent);
|
||||
|
||||
public sealed record MccToolCatalogEntry(McpClientTool Tool, MccToolProfile Profile);
|
||||
|
||||
public sealed class MccToolCatalog
|
||||
{
|
||||
public required Dictionary<string, MccToolCatalogEntry> ToolsByName { get; init; }
|
||||
public required IReadOnlyList<object> ModelVisibleTools { get; init; }
|
||||
}
|
||||
|
||||
public static class MccToolPolicy
|
||||
{
|
||||
private static readonly FrozenDictionary<string, MccToolProfile> Profiles =
|
||||
new Dictionary<string, MccToolProfile>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["mcc_agent_guidance"] = new("mcc_agent_guidance", MccToolRisk.ReadOnly, false, false),
|
||||
["mcc_inventory_window_action"] = new("mcc_inventory_window_action", MccToolRisk.EscapeHatch, false, false),
|
||||
["mcc_run_internal_command"] = new("mcc_run_internal_command", MccToolRisk.EscapeHatch, false, false),
|
||||
["mcc_quit_client"] = new("mcc_quit_client", MccToolRisk.Sensitive, true, true)
|
||||
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static MccToolProfile GetProfile(string toolName)
|
||||
{
|
||||
return Profiles.TryGetValue(toolName, out MccToolProfile? profile)
|
||||
? profile
|
||||
: new MccToolProfile(toolName, MccToolRisk.Stateful, true, false);
|
||||
}
|
||||
|
||||
public static MccToolCatalog BuildCatalog(IList<McpClientTool> tools, MccWebHarnessOptions options, object submitFinalTool)
|
||||
{
|
||||
Dictionary<string, MccToolCatalogEntry> toolsByName = tools.ToDictionary(
|
||||
tool => tool.Name,
|
||||
tool => new MccToolCatalogEntry(tool, GetProfile(tool.Name)),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
List<object> visibleTools = [];
|
||||
foreach (MccToolCatalogEntry entry in toolsByName.Values.OrderBy(entry => entry.Tool.Name, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!IsVisible(entry.Profile, options))
|
||||
continue;
|
||||
|
||||
visibleTools.Add(ToOpenRouterTool(entry.Tool, entry.Profile));
|
||||
}
|
||||
|
||||
visibleTools.Add(submitFinalTool);
|
||||
|
||||
return new MccToolCatalog
|
||||
{
|
||||
ToolsByName = toolsByName,
|
||||
ModelVisibleTools = visibleTools
|
||||
};
|
||||
}
|
||||
|
||||
public static bool RequiresExplicitUserIntent(string toolName)
|
||||
{
|
||||
return GetProfile(toolName).RequiresExplicitUserIntent;
|
||||
}
|
||||
|
||||
public static bool HasExplicitUserIntent(string userRequest, string toolName)
|
||||
{
|
||||
if (!RequiresExplicitUserIntent(toolName))
|
||||
return true;
|
||||
|
||||
string request = userRequest.Trim().ToLowerInvariant();
|
||||
return toolName.Equals("mcc_quit_client", StringComparison.OrdinalIgnoreCase)
|
||||
&& (request.Contains("quit mcc", StringComparison.Ordinal)
|
||||
|| request.Contains("close mcc", StringComparison.Ordinal)
|
||||
|| request.Contains("stop mcc", StringComparison.Ordinal)
|
||||
|| request.Contains("exit mcc", StringComparison.Ordinal)
|
||||
|| request.Contains("quit the client", StringComparison.Ordinal)
|
||||
|| request.Contains("stop the client", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static bool IsVisible(MccToolProfile profile, MccWebHarnessOptions options)
|
||||
{
|
||||
if (!profile.VisibleByDefault)
|
||||
{
|
||||
if (profile.Name.Equals("mcc_inventory_window_action", StringComparison.OrdinalIgnoreCase))
|
||||
return options.ExposeInventoryWindowAction;
|
||||
|
||||
if (profile.Name.Equals("mcc_run_internal_command", StringComparison.OrdinalIgnoreCase))
|
||||
return options.ExposeInternalCommandTool;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static object ToOpenRouterTool(McpClientTool tool, MccToolProfile profile)
|
||||
{
|
||||
JsonNode parameters = JsonNode.Parse(tool.JsonSchema.GetRawText()) ?? new JsonObject
|
||||
{
|
||||
["type"] = "object",
|
||||
["properties"] = new JsonObject()
|
||||
};
|
||||
|
||||
string description = tool.Description ?? string.Empty;
|
||||
if (profile.Risk == MccToolRisk.Sensitive)
|
||||
description = $"{description} Requires explicit user intent.";
|
||||
else if (profile.Risk == MccToolRisk.EscapeHatch)
|
||||
description = $"{description} Advanced escape hatch; prefer higher-level tools first.";
|
||||
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["type"] = "function",
|
||||
["function"] = new Dictionary<string, object?>
|
||||
{
|
||||
["name"] = tool.Name,
|
||||
["description"] = description,
|
||||
["parameters"] = parameters
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
namespace DebugTools.MccMcpWebPlayground.Harness;
|
||||
|
||||
public sealed class MccWebHarnessOptions
|
||||
{
|
||||
public const string SectionName = "MccWebHarness";
|
||||
|
||||
public string? Model { get; set; }
|
||||
public string OpenRouterBaseUrl { get; set; } = "https://openrouter.ai/api/v1";
|
||||
public string McpEndpoint { get; set; } = "http://127.0.0.1:33333/mcp";
|
||||
public int MaxTurns { get; set; } = 48;
|
||||
public int MaxToolCalls { get; set; } = 120;
|
||||
public int MaxWallClockSeconds { get; set; } = 240;
|
||||
public int SoftFinishRemainingTurns { get; set; } = 3;
|
||||
public int SoftFinishRemainingToolCalls { get; set; } = 8;
|
||||
public int SoftFinishRemainingSeconds { get; set; } = 30;
|
||||
public bool RequireProviderParameters { get; set; } = true;
|
||||
public bool AllowFallbacks { get; set; }
|
||||
public bool DisableParallelToolCalls { get; set; } = true;
|
||||
public bool ExposeInventoryWindowAction { get; set; }
|
||||
public bool ExposeInternalCommandTool { get; set; }
|
||||
|
||||
public string? ResolveModel()
|
||||
{
|
||||
return FirstNonEmpty(Environment.GetEnvironmentVariable("OPENROUTER_MODEL"), Model);
|
||||
}
|
||||
|
||||
public string ResolveOpenRouterBaseUrl()
|
||||
{
|
||||
return FirstNonEmpty(Environment.GetEnvironmentVariable("OPENROUTER_BASE_URL"), OpenRouterBaseUrl)
|
||||
?? "https://openrouter.ai/api/v1";
|
||||
}
|
||||
|
||||
public string ResolveMcpEndpoint()
|
||||
{
|
||||
return FirstNonEmpty(Environment.GetEnvironmentVariable("MCC_MCP_ENDPOINT"), McpEndpoint)
|
||||
?? "http://127.0.0.1:33333/mcp";
|
||||
}
|
||||
|
||||
public string? ResolveMcpAuthToken()
|
||||
{
|
||||
return Environment.GetEnvironmentVariable("MCC_MCP_AUTH_TOKEN");
|
||||
}
|
||||
|
||||
public string? ResolveApiKey()
|
||||
{
|
||||
return Environment.GetEnvironmentVariable("OPENROUTER_API_KEY");
|
||||
}
|
||||
|
||||
public bool HasApiKeyConfigured()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(ResolveApiKey());
|
||||
}
|
||||
|
||||
private static string? FirstNonEmpty(params string?[] candidates)
|
||||
{
|
||||
return candidates.FirstOrDefault(candidate => !string.IsNullOrWhiteSpace(candidate))?.Trim();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Reflection;
|
||||
using DebugTools.MccMcpWebPlayground.Harness;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace DebugTools.MccMcpWebPlayground.Infrastructure.Mcp;
|
||||
|
||||
public sealed class MccMcpSessionFactory
|
||||
{
|
||||
private readonly MccWebHarnessOptions options;
|
||||
|
||||
public MccMcpSessionFactory(IOptions<MccWebHarnessOptions> options)
|
||||
{
|
||||
this.options = options.Value;
|
||||
}
|
||||
|
||||
public async Task<McpClient> CreateAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
string endpoint = options.ResolveMcpEndpoint();
|
||||
string? token = options.ResolveMcpAuthToken();
|
||||
|
||||
return await McpClient.CreateAsync(new HttpClientTransport(new HttpClientTransportOptions
|
||||
{
|
||||
Endpoint = new Uri(endpoint),
|
||||
TransportMode = HttpTransportMode.AutoDetect,
|
||||
AdditionalHeaders = string.IsNullOrWhiteSpace(token)
|
||||
? null
|
||||
: new Dictionary<string, string>
|
||||
{
|
||||
["Authorization"] = $"Bearer {token}"
|
||||
}
|
||||
}), cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MccMcpJson
|
||||
{
|
||||
public static MccNormalizedToolResult Normalize(CallToolResult result)
|
||||
{
|
||||
JsonElement? structuredRoot = TryReadStructuredContent(result);
|
||||
string text = ReadToolResultText(result, structuredRoot);
|
||||
try
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(text);
|
||||
JsonElement parsedRoot = document.RootElement.Clone();
|
||||
JsonElement root = ShouldPreferStructuredRoot(parsedRoot, structuredRoot)
|
||||
? structuredRoot!.Value
|
||||
: parsedRoot;
|
||||
JsonElement? data = root.TryGetProperty("data", out JsonElement dataElement)
|
||||
? dataElement.Clone()
|
||||
: ShouldTreatRootAsData(root) ? root.Clone() : structuredRoot;
|
||||
bool success = root.TryGetProperty("success", out JsonElement successElement)
|
||||
? successElement.ValueKind != JsonValueKind.False
|
||||
: result.IsError != true;
|
||||
string? errorCode = root.TryGetProperty("errorCode", out JsonElement errorCodeElement) && errorCodeElement.ValueKind == JsonValueKind.String
|
||||
? errorCodeElement.GetString()
|
||||
: null;
|
||||
string? message = root.TryGetProperty("message", out JsonElement messageElement) && messageElement.ValueKind == JsonValueKind.String
|
||||
? messageElement.GetString()
|
||||
: null;
|
||||
bool isError = result.IsError == true || !success || !string.IsNullOrWhiteSpace(errorCode);
|
||||
|
||||
return new MccNormalizedToolResult(text, isError, success, errorCode, message, root, data);
|
||||
}
|
||||
catch
|
||||
{
|
||||
bool isError = result.IsError == true;
|
||||
return new MccNormalizedToolResult(text, isError, !isError, null, null, structuredRoot, structuredRoot);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadToolResultText(CallToolResult result, JsonElement? structuredRoot)
|
||||
{
|
||||
if (result.Content is null)
|
||||
return structuredRoot?.GetRawText() ?? (result.IsError == true ? "{\"success\":false}" : "{\"success\":true}");
|
||||
|
||||
StringBuilder builder = new();
|
||||
foreach (ContentBlock block in result.Content)
|
||||
{
|
||||
if (block is TextContentBlock text && !string.IsNullOrWhiteSpace(text.Text))
|
||||
{
|
||||
if (builder.Length > 0)
|
||||
builder.Append('\n');
|
||||
builder.Append(text.Text);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.Length > 0
|
||||
? builder.ToString()
|
||||
: structuredRoot?.GetRawText()
|
||||
?? JsonSerializer.Serialize(new { success = result.IsError != true, isError = result.IsError });
|
||||
}
|
||||
|
||||
private static JsonElement? TryReadStructuredContent(CallToolResult result)
|
||||
{
|
||||
PropertyInfo? property = typeof(CallToolResult).GetProperty("StructuredContent", BindingFlags.Instance | BindingFlags.Public);
|
||||
if (property?.GetValue(result) is not { } value)
|
||||
return null;
|
||||
|
||||
return value switch
|
||||
{
|
||||
JsonElement json when json.ValueKind != JsonValueKind.Undefined && json.ValueKind != JsonValueKind.Null => json.Clone(),
|
||||
JsonDocument document => document.RootElement.Clone(),
|
||||
string text when !string.IsNullOrWhiteSpace(text) => TryParseJson(text),
|
||||
_ => TrySerializeToJson(value)
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonElement? TrySerializeToJson(object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.SerializeToElement(value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonElement? TryParseJson(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(text);
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldPreferStructuredRoot(JsonElement parsedRoot, JsonElement? structuredRoot)
|
||||
{
|
||||
if (structuredRoot is null)
|
||||
return false;
|
||||
|
||||
if (parsedRoot.ValueKind != JsonValueKind.Object)
|
||||
return true;
|
||||
|
||||
return !parsedRoot.EnumerateObject().Any(property =>
|
||||
!property.NameEquals("success") &&
|
||||
!property.NameEquals("isError"));
|
||||
}
|
||||
|
||||
private static bool ShouldTreatRootAsData(JsonElement root)
|
||||
{
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
return false;
|
||||
|
||||
return root.EnumerateObject().Any(property =>
|
||||
!property.NameEquals("success") &&
|
||||
!property.NameEquals("isError") &&
|
||||
!property.NameEquals("errorCode") &&
|
||||
!property.NameEquals("message"));
|
||||
}
|
||||
}
|
||||
|
||||
public static class MccJsonArguments
|
||||
{
|
||||
public static Dictionary<string, object?> Parse(string rawJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(string.IsNullOrWhiteSpace(rawJson) ? "{}" : rawJson);
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Object)
|
||||
return new Dictionary<string, object?>();
|
||||
|
||||
Dictionary<string, object?> values = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (JsonProperty property in document.RootElement.EnumerateObject())
|
||||
values[property.Name] = Convert(property.Value);
|
||||
return values;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new Dictionary<string, object?>();
|
||||
}
|
||||
}
|
||||
|
||||
private static object? Convert(JsonElement element)
|
||||
{
|
||||
return element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Null => null,
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
JsonValueKind.Number => element.TryGetInt64(out long i64)
|
||||
? i64
|
||||
: element.TryGetDouble(out double d) ? d : element.GetRawText(),
|
||||
JsonValueKind.String => element.GetString(),
|
||||
JsonValueKind.Array => element.EnumerateArray().Select(Convert).ToArray(),
|
||||
JsonValueKind.Object => element.EnumerateObject().ToDictionary(property => property.Name, property => Convert(property.Value)),
|
||||
_ => element.GetRawText()
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue