diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..57f4aaf --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-marketplace-manifest.json", + "name": "dumbify", + "owner": { + "name": "smartwatermelon", + "url": "https://github.com/smartwatermelon" + }, + "description": "The dumbify skill, installable as a Claude Code plugin.", + "plugins": [ + { + "name": "dumbify", + "source": "./", + "description": "Rewrite competent workplace writing into terse, lowercase, fragment-heavy engineering communication, preserving meaning and technical precision while removing ceremony.", + "license": "MIT", + "keywords": [ + "writing", + "editing", + "prose", + "style", + "compression", + "skill" + ] + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..bf42d88 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", + "name": "dumbify", + "description": "Rewrite competent workplace writing into terse, lowercase, fragment-heavy engineering communication, preserving meaning and technical precision while removing ceremony.", + "version": "0.1.0", + "author": { + "name": "smartwatermelon", + "url": "https://github.com/smartwatermelon" + }, + "homepage": "https://github.com/smartwatermelon/dumbify", + "repository": "https://github.com/smartwatermelon/dumbify", + "license": "MIT", + "keywords": ["writing", "editing", "prose", "style", "compression", "skill"], + "skills": ["./"] +} diff --git a/.claude/README.md b/.claude/README.md new file mode 100644 index 0000000..cdc3aef --- /dev/null +++ b/.claude/README.md @@ -0,0 +1,190 @@ +# Claude Code Infrastructure - Project-Specific + +This directory was automatically created by Git template when you initialized or cloned this repository. + +## What is this? + +The `.claude/` directory provides project-specific configuration and extensions for Claude Code CLI (CCCLI) collaboration. It integrates with global infrastructure at `~/.claude/` to provide: + +- Project-specific configuration (Node version, required tools, deployment secrets) +- Custom git hook extensions for project-specific validation +- Project-local documentation and patterns + +## Directory Structure + +``` +.claude/ +├── README.md # This file +├── config.sh.template # Template for project configuration +└── hooks/ + └── extensions/ # Project-specific git hook extensions + └── example.sh.disabled # Example extension (disabled by default) +``` + +## Quick Start + +### Option 1: No Additional Configuration Needed + +If your project doesn't need special validation, required secrets, or version constraints, **you're done**! Your project already benefits from global infrastructure: + +- Global git hooks (pre-commit, pre-push) +- Code review automation +- Branch protection +- Standard workflows + +### Option 2: Add Project Configuration + +If you need project-specific settings: + +1. Copy the template: + + ```bash + cp .claude/config.sh.template .claude/config.sh + ``` + +2. Edit `.claude/config.sh` to define: + - Required Node version + - Required tools (EAS, Maestro, jq, etc.) + - Deployment secrets + - Custom pre/post build hooks + +3. Update your build/deploy scripts to source the config: + + ```bash + # In build scripts + source "${HOME}/.claude/lib/build-commons.sh" + [[ -f ".claude/config.sh" ]] && source ".claude/config.sh" + run_preflight_checks + + # In deploy scripts + source "${HOME}/.claude/lib/deploy-commons.sh" + source ".claude/config.sh" + verify_cloudflare_secrets "${DEPLOYMENT_REQUIRED_SECRETS[@]}" + ``` + +### Option 3: Add Custom Hook Extensions + +If you need project-specific validation: + +1. Create a new file in `.claude/hooks/extensions/`: + + ```bash + touch .claude/hooks/extensions/my-validation.sh + chmod +x .claude/hooks/extensions/my-validation.sh + ``` + +2. Write your validation logic: + + ```bash + #!/usr/bin/env bash + # Extension contract: + # - Exit 0: Check passed (allow git operation) + # - Exit 1: Check failed (block git operation) + # - Can use functions from ~/.claude/hooks/lib/hook-common.sh + + # Your validation logic here + if [[ condition_fails ]]; then + echo "ERROR: Validation failed" + exit 1 + fi + + exit 0 + ``` + +3. Extensions run automatically on relevant git operations (commit, push, etc.) + +## Common Patterns + +### Node.js Project with Version Requirement + +```bash +# .claude/config.sh +export REQUIRED_NODE_VERSION="20" +``` + +### Project with Deployment Secrets + +```bash +# .claude/config.sh +export DEPLOYMENT_REQUIRED_SECRETS=( + "API_KEY" + "DATABASE_URL" + "JWT_SECRET" +) +``` + +### Custom Security Check + +```bash +# .claude/hooks/extensions/security.sh +#!/usr/bin/env bash + +# Block commits with hardcoded API keys +if git diff --cached | grep -iE 'API_KEY.*=.*"[A-Za-z0-9]{32}"'; then + echo "ERROR: Hardcoded API key detected" + exit 1 +fi + +exit 0 +``` + +## Integration with Global Infrastructure + +Global hooks at `~/.config/git/hooks/` automatically discover and run extensions in this directory. No configuration needed - just add your `.sh` files and make them executable. + +**Global Infrastructure Documentation**: `~/.claude/docs/INFRASTRUCTURE.md` + +## Files Included + +### config.sh.template + +Template for project configuration. Copy to `config.sh` and customize with your project's requirements. + +### hooks/extensions/example.sh.disabled + +Example extension showing the basic structure. Disabled by default (`.disabled` suffix prevents execution). + +To enable: + +1. Remove `.disabled` suffix: `mv example.sh.disabled my-check.sh` +2. Customize validation logic +3. Ensure executable: `chmod +x .claude/hooks/extensions/my-check.sh` + +## Next Steps + +1. **Review your needs**: Do you need project-specific configuration or validation? +2. **If yes**: Follow Quick Start Option 2 or 3 above +3. **If no**: You're done! Just start working + +## Documentation + +- **Global Infrastructure**: `~/.claude/docs/INFRASTRUCTURE.md` +- **Build Patterns**: `~/.claude/docs/BUILD_PATTERNS.md` (if exists) +- **Deployment Patterns**: `~/.claude/docs/DEPLOYMENT_PATTERNS.md` (if exists) +- **Hook System**: `~/.claude/docs/HOOKS.md` (if exists) + +## Troubleshooting + +### Extensions not running? + +```bash +# Check extensions are executable +ls -la .claude/hooks/extensions/ + +# Make executable if needed +chmod +x .claude/hooks/extensions/*.sh +``` + +### Config not being used? + +```bash +# Verify config exists and is sourced +ls -la .claude/config.sh + +# Check your build/deploy scripts source it +grep -r "source.*config.sh" scripts/ +``` + +### Need help? + +See global infrastructure documentation at `~/.claude/docs/INFRASTRUCTURE.md` for complete reference. diff --git a/.claude/config.sh.template b/.claude/config.sh.template new file mode 100644 index 0000000..9a65a0d --- /dev/null +++ b/.claude/config.sh.template @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# Project Configuration Template +# Copy to .claude/config.sh and customize for your project +# +# This file is sourced by: +# - Build scripts (when using build-commons.sh) +# - Deploy scripts (when using deploy-commons.sh) +# - Any other scripts that need project-specific configuration + +# ============================================ +# NODE.JS PROJECTS +# ============================================ + +# Required Node version (used by build-commons.sh) +# Uncomment and set to your required major version +# export REQUIRED_NODE_VERSION="20" + +# Additional required tools beyond standard development tools +# build-commons.sh will verify these are installed +export PROJECT_REQUIRED_TOOLS=( + # "eas" # Expo Application Services (React Native) + # "maestro" # Mobile E2E testing framework + # "jq" # JSON parsing (for CI/CD scripts) + # "wrangler" # Cloudflare Workers CLI +) + +# ============================================ +# DEPLOYMENT CONFIGURATION +# ============================================ + +# Required secrets for deployment (hard-blocks deployment if missing) +# Example: Cloudflare Workers secrets +export DEPLOYMENT_REQUIRED_SECRETS=( + # "API_KEY" + # "DATABASE_URL" + # "JWT_SECRET" +) + +# Optional secrets (warns if missing, doesn't block) +export DEPLOYMENT_OPTIONAL_SECRETS=( + # "SENTRY_DSN" + # "ANALYTICS_KEY" + # "FEATURE_FLAG_KEY" +) + +# Deployment smoke test endpoints (relative to base URL) +# Used by deploy-commons.sh run_endpoint_smoke_tests() +export DEPLOYMENT_SMOKE_TEST_ENDPOINTS=( + # "/health" + # "/api/v1/status" + # "/.well-known/health" +) + +# ============================================ +# BUILD CONFIGURATION +# ============================================ + +# Skip certain checks if needed (use sparingly) +# export SKIP_NODE_VERSION_CHECK=1 +# export SKIP_DEPENDENCY_CHECK=1 + +# Custom npm/yarn/pnpm commands +# export NPM_CLIENT="pnpm" # Default: npm +# export TEST_COMMAND="npm test" +# export BUILD_COMMAND="npm run build" + +# ============================================ +# CUSTOM HOOKS +# ============================================ + +# Pre-build validation (called by build-commons.sh if this function exists) +# Return 0 for success, 1 for failure +pre_build_validation() { + # Add your project-specific validation here + + # Example: Check for uncommitted config changes + # if ! git diff-index --quiet HEAD -- config.json; then + # echo "WARNING: Uncommitted changes in config.json" + # read -p "Continue anyway? (y/N): " -n 1 -r + # echo + # [[ ! ${REPLY} =~ ^[Yy]$ ]] && return 1 + # fi + + # Example: Verify environment variables + # if [[ -z "${REQUIRED_ENV_VAR}" ]]; then + # echo "ERROR: REQUIRED_ENV_VAR not set" + # return 1 + # fi + + return 0 +} + +# Post-build validation (called by build-commons.sh if this function exists) +# Return 0 for success, 1 for failure +post_build_validation() { + # Add your project-specific validation here + + # Example: Check build output + # if [[ ! -f "dist/index.js" ]]; then + # echo "ERROR: Build did not produce expected output" + # return 1 + # fi + + # Example: Run bundle size check + # local bundle_size=$(stat -f%z dist/bundle.js) + # local max_size=$((500 * 1024)) # 500KB + # if [[ ${bundle_size} -gt ${max_size} ]]; then + # echo "WARNING: Bundle size ${bundle_size} bytes exceeds ${max_size} bytes" + # return 1 + # fi + + return 0 +} + +# Pre-deploy validation (called by deploy-commons.sh if this function exists) +# Return 0 for success, 1 for failure +pre_deploy_validation() { + # Add your project-specific validation here + + # Example: Check git status + # if ! git diff-index --quiet HEAD --; then + # echo "ERROR: Uncommitted changes detected" + # echo "Deploy from clean working directory only" + # return 1 + # fi + + # Example: Verify on correct branch + # local current_branch=$(git branch --show-current) + # if [[ "${current_branch}" != "main" ]]; then + # echo "WARNING: Deploying from branch '${current_branch}', not 'main'" + # read -p "Continue? (y/N): " -n 1 -r + # echo + # [[ ! ${REPLY} =~ ^[Yy]$ ]] && return 1 + # fi + + return 0 +} + +# Post-deploy validation (called by deploy-commons.sh if this function exists) +# Return 0 for success, 1 for failure +post_deploy_validation() { + # Add your project-specific validation here + + # Example: Tag deployment + # local version=$(jq -r .version package.json) + # git tag -a "deploy-${version}-$(date +%Y%m%d-%H%M%S)" -m "Deployed version ${version}" + + return 0 +} + +# ============================================ +# PROJECT-SPECIFIC VARIABLES +# ============================================ + +# Add any other project-specific configuration here +# export PROJECT_NAME="my-app" +# export PROJECT_ENV="production" +# export API_BASE_URL="https://api.example.com" + +# ============================================ +# USAGE EXAMPLES +# ============================================ + +# In build scripts: +# source "${HOME}/.claude/lib/build-commons.sh" +# [[ -f ".claude/config.sh" ]] && source ".claude/config.sh" +# run_preflight_checks +# # Your build commands here + +# In deploy scripts: +# source "${HOME}/.claude/lib/deploy-commons.sh" +# source ".claude/config.sh" +# verify_cloudflare_secrets "${DEPLOYMENT_REQUIRED_SECRETS[@]}" +# # Your deploy commands here +# run_endpoint_smoke_tests "${BASE_URL}" "${DEPLOYMENT_SMOKE_TEST_ENDPOINTS[@]}" diff --git a/.claude/hooks/extensions/example.sh.disabled b/.claude/hooks/extensions/example.sh.disabled new file mode 100644 index 0000000..e9d7c76 --- /dev/null +++ b/.claude/hooks/extensions/example.sh.disabled @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# Example Git Hook Extension (DISABLED by default) +# +# This file demonstrates how to create project-specific git hook extensions. +# Extensions are discovered and executed by global hooks automatically. +# +# TO ENABLE THIS EXTENSION: +# 1. Rename to remove .disabled suffix: +# mv example.sh.disabled my-validation.sh +# 2. Customize the validation logic below +# 3. Ensure it's executable: +# chmod +x .claude/hooks/extensions/my-validation.sh +# +# EXTENSION CONTRACT: +# - Exit 0: Check passed (allow git operation to proceed) +# - Exit 1: Check failed (block git operation) +# - Can use functions from ~/.claude/hooks/lib/hook-common.sh +# - Receives same arguments as parent hook (e.g., commit message file for commit-msg hook) +# +# AVAILABLE FUNCTIONS (from hook-common.sh): +# log_info "message" - Blue informational message +# log_success "message" - Green success message +# log_warn "message" - Yellow warning message +# log_error "message" - Red error message +# get_staged_files - Get list of staged files +# get_repo_root - Get repository root directory +# is_protected_branch - Check if on main/master branch + +# ============================================ +# EXAMPLE 1: Block commits during business hours +# ============================================ + +check_business_hours() { + local current_hour=$(date +%H) + + # Check if current time is during business hours (9 AM - 5 PM) + if [[ ${current_hour} -ge 9 && ${current_hour} -lt 17 ]]; then + log_warn "⏰ Commit during business hours detected" + + # Check if commit message includes ticket reference + local commit_msg_file="$1" + if [[ -n "${commit_msg_file}" ]] && [[ -f "${commit_msg_file}" ]]; then + if ! grep -qE '(JIRA|TICKET|#)[- ]?[0-9]+' "${commit_msg_file}"; then + log_error "❌ Commits during business hours must reference a ticket" + echo " Format: JIRA-123, TICKET-456, or #789" + return 1 + fi + fi + fi + + return 0 +} + +# ============================================ +# EXAMPLE 2: Check for TODO comments without issue references +# ============================================ + +check_todo_comments() { + # Get staged changes + local staged_changes=$(git diff --cached) + + # Look for TODO comments without issue references + # Pattern: TODO without a # followed by digits + if echo "${staged_changes}" | grep -iE '^\+.*TODO(?! #[0-9])'; then + log_warn "⚠️ TODO comment without issue reference detected" + echo "" + echo "Found TODO comments that don't reference an issue:" + echo "${staged_changes}" | grep -iE '^\+.*TODO(?! #[0-9])' | sed 's/^/ /' + echo "" + echo "Please use format: TODO #123 (with GitHub issue number)" + return 1 + fi + + return 0 +} + +# ============================================ +# EXAMPLE 3: Prevent hardcoded secrets +# ============================================ + +check_hardcoded_secrets() { + # Get staged changes + local staged_changes=$(git diff --cached) + + # Check for common secret patterns + local secret_patterns=( + 'api[_-]?key.*=.*["\x27][a-zA-Z0-9]{32,}' + 'secret[_-]?key.*=.*["\x27][a-zA-Z0-9]{32,}' + 'password.*=.*["\x27][^"\x27]{8,}' + 'token.*=.*["\x27][a-zA-Z0-9]{32,}' + ) + + for pattern in "${secret_patterns[@]}"; do + if echo "${staged_changes}" | grep -iE "^\+.*${pattern}"; then + log_error "❌ Potential hardcoded secret detected" + echo "" + echo "Pattern matched: ${pattern}" + echo "" + echo "Please use environment variables or secret management instead." + return 1 + fi + done + + return 0 +} + +# ============================================ +# EXAMPLE 4: Enforce code formatting +# ============================================ + +check_formatting() { + # Get list of staged files + local staged_files=$(git diff --cached --name-only --diff-filter=ACM) + + # Check if prettier is available + if ! command -v prettier &>/dev/null; then + log_warn "⚠️ Prettier not found, skipping format check" + return 0 + fi + + # Check JavaScript/TypeScript files + local js_files=$(echo "${staged_files}" | grep -E '\.(js|jsx|ts|tsx)$' || true) + + if [[ -n "${js_files}" ]]; then + local unformatted_files=$(echo "${js_files}" | xargs prettier --check 2>&1 | grep -E '^/' || true) + + if [[ -n "${unformatted_files}" ]]; then + log_error "❌ Unformatted files detected" + echo "" + echo "The following files are not formatted:" + echo "${unformatted_files}" | sed 's/^/ /' + echo "" + echo "Run: prettier --write " + return 1 + fi + fi + + return 0 +} + +# ============================================ +# EXAMPLE 5: Validate commit message format +# ============================================ + +check_commit_message_format() { + local commit_msg_file="$1" + + if [[ -z "${commit_msg_file}" ]] || [[ ! -f "${commit_msg_file}" ]]; then + # Not a commit message hook call + return 0 + fi + + local commit_msg=$(cat "${commit_msg_file}") + + # Skip merge commits + if [[ "${commit_msg}" =~ ^Merge ]]; then + return 0 + fi + + # Check conventional commit format: type(scope): subject + if ! echo "${commit_msg}" | grep -qE '^(feat|fix|docs|style|refactor|test|chore)(\([a-z0-9-]+\))?: .+'; then + log_error "❌ Commit message does not follow conventional format" + echo "" + echo "Current message:" + echo " ${commit_msg}" + echo "" + echo "Expected format:" + echo " type(scope): subject" + echo "" + echo "Types: feat, fix, docs, style, refactor, test, chore" + echo "Example: feat(auth): add JWT token refresh" + return 1 + fi + + return 0 +} + +# ============================================ +# MAIN EXECUTION +# ============================================ + +main() { + # Uncomment the checks you want to enable: + + # check_business_hours "$@" || exit 1 + # check_todo_comments || exit 1 + # check_hardcoded_secrets || exit 1 + # check_formatting || exit 1 + # check_commit_message_format "$@" || exit 1 + + # If all checks pass (or none are enabled) + log_success "✅ Example validation passed" + exit 0 +} + +# Run main function +main "$@" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..db83b19 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# Dependabot: auto-PR bumps for GitHub Actions pins in .github/workflows/. +# Primary target: the smartwatermelon/github-workflows reusable workflow +# (see https://github.com/smartwatermelon/github-workflows/releases). +# Secondary: any other actions/* or third-party actions used here. +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + # cooldown is a valid dependabot.yml key (GitHub docs: + # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#cooldown) + cooldown: + default-days: 7 + commit-message: + prefix: "chore" diff --git a/.github/workflows/claude-blocking-review.yml b/.github/workflows/claude-blocking-review.yml new file mode 100644 index 0000000..6a82219 --- /dev/null +++ b/.github/workflows/claude-blocking-review.yml @@ -0,0 +1,52 @@ +name: Claude Blocking Review + +# Two jobs run the SAME reusable review workflow, same PR, same BLOCK/PASS +# policy — the only difference is `model`. `claude-review` uses the reusable +# workflow's default (Sonnet); `claude-review-haiku` overrides to Haiku. +# Both are CONFIGURED identically and intended to gate equally, but as of +# this writing only `claude-review / run-review` is wired into main's +# branch protection as a required status check (verify with `gh api +# repos///branches/main/protection --jq +# '.required_status_checks.contexts'`). `claude-review-haiku` runs the +# identical policy and can still fail its own job/exit 1 on VERDICT: BLOCK, +# but that failure does not currently block the merge button — it's a +# live A/B comparison of Haiku's verdicts against Sonnet's, run in +# production but not (yet) enforced. Add it to required_status_checks once +# the comparison shows its verdicts are trustworthy enough to gate on. +# +# `claude-review-haiku` declares `needs: claude-review` with `if: always()` +# so the two jobs run sequentially instead of racing to post/update the +# same PR comment marker (see issue #162). `if: always()` is required +# alongside `needs:` because GitHub Actions otherwise skips a dependent job +# when its `needs:` job fails — and `claude-review` "fails" (exits 1) on +# VERDICT: BLOCK, which is a normal, expected outcome, not an error that +# should cancel the Haiku A/B run. Serializing changes only *when* +# claude-review-haiku starts, not *whether* it runs. + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + +permissions: + contents: read + pull-requests: write + issues: write + id-token: write + +jobs: + claude-review: + uses: smartwatermelon/github-workflows/.github/workflows/claude-blocking-review.yml@v3.1.0 + with: + pr_number: ${{ github.event.pull_request.number }} + secrets: + claude_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + claude-review-haiku: + needs: claude-review + if: always() + uses: smartwatermelon/github-workflows/.github/workflows/claude-blocking-review.yml@v3.1.0 + with: + pr_number: ${{ github.event.pull_request.number }} + model: "claude-haiku-4-5-20251001" + secrets: + claude_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..2ebc05c --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,24 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) + permissions: + contents: read + issues: read + pull-requests: read + id-token: write # required by claude-code-action for internal authentication + uses: smartwatermelon/github-workflows/.github/workflows/claude-assistant.yml@v3.0.0 + secrets: + claude_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..3fedb8e --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,15 @@ +name: Dependabot Auto-Merge + +on: # zizmor: ignore[dangerous-triggers] required to run from base branch; no PR code executed here + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot-auto-merge: + uses: smartwatermelon/github-workflows/.github/workflows/dependabot-auto-merge.yml@dependabot-auto-merge-v2 + with: + trusted_namespaces: 'smartwatermelon' diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..442ff8a --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,20 @@ +name: Validate skill + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: "3.12" + - name: Validate SKILL.md and plugin manifests + run: python3 scripts/validate_skill.py diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8189ee3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,64 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this repo is + +Dumbify is a single-artifact agent skill: a prompt, not a program. There is no +build, no test suite, no lint step, no dependencies, and no executable code. +The entire runtime artifact is `SKILL.md`; everything else is packaging. + +- `SKILL.md` — the skill. YAML frontmatter (`name`, `version`, `description`, + `license`) followed by the instruction body. Consumed verbatim by Claude Code + and any other Markdown-skill harness. +- `README.md` — human-facing install/usage docs. +- `.claude/` — this repo's own Claude Code config, unrelated to the skill's content. + +Verification is by reading and by trying the skill on sample text, not by +running anything. + +## Editing SKILL.md + +The frontmatter `description` is what the harness matches against to decide +whether to load the skill, so it is load-bearing prose, not a summary — changing +it changes when Dumbify fires. + +The body is organized as layered constraints, and the ordering matters to how +the model resolves conflicts: + +1. **Core principle** — "makes the writing look dumber, not the thinking." +2. **Non-negotiable constraints** — preserve meaning, preserve real uncertainty, + genuinely rewrite rather than word-substitute. These override everything below. +3. **Process** (10 steps) and the **stupidity audit** (a self-check list run + before output). +4. **Register rules** (10 numbered rules: lowercase, fragments, articles, + subjects, auxiliaries, connectives, social cushioning, disagreement, + punctuation, markdown). +5. **Mode sections** — GitHub PR, PR review comments, Asana/task, Slack, incident. + These specialize the register rules per surface. +6. **Intensity levels 1–4**, default 2; 3–4 only on explicit request. +7. **Failure modes** — the negative space, defining what Dumbify must not become. + +When adding a rule, put it at the layer that matches its precedence and follow +the existing shape: a short imperative, then before/after examples in blockquotes +(`> normal` / `→` / `> dumbified`). Examples carry more weight than explanation +here — prefer adding one over adding a paragraph. + +Two invariants that most proposed edits threaten: + +- **Compression is linguistic, never semantic.** Any rule that could drop a fact, + number, identifier, caveat, or genuine hedge is wrong regardless of how much + shorter it makes the output. +- **The target register is a busy engineer, not a teenager.** Slang, emoji, + deliberate misspellings, and acronym soup are explicitly enumerated failure + modes. Do not soften those prohibitions. + +Keep `README.md`'s examples and the intensity-level description consistent with +`SKILL.md` when either changes. + +## Watermarks + +The repo takes a deliberate, non-negotiable position: Dumbify is not a +watermark-removal tool, and no claim may be made that it defeats or evades any +detector. Both `SKILL.md` and `README.md` state this. Do not weaken, hedge, or +remove that framing. diff --git a/README.md b/README.md index 6ba3b31..b0de9f2 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,13 @@ The target is not fake Gen-Z slang. It is compressed engineering communication: ### Claude Code plugin -If this repository is added as a Claude Code marketplace, install it with: - ```text /plugin marketplace add smartwatermelon/dumbify +/plugin install dumbify@dumbify ``` +Once installed, invoke it as `/dumbify:dumbify`. + The runtime artifact is `SKILL.md`. ### Claude Code, project-local @@ -82,6 +83,36 @@ Default is level 2. The skill supports levels 1–4, with higher levels producin Level 3 or 4 should be explicitly requested. +## Works with personify and pr-review + +Dumbify is the last of three sibling skills that compose into one path from +"review this PR" to a posted comment that reads like a person wrote it: + +```text +pr-review → personify → dumbify +(find it) (de-AI it) (compress it) +``` + +- [pr-review](https://github.com/smartwatermelon/pr-review) does the review + itself: traces claims against the repo, triages findings down to the one + that matters, and stages a pending GitHub review rather than posting it. +- [personify](https://github.com/smartwatermelon/personify) strips AI-writing + tells from the draft and, given a `VOICE.md`, makes it sound like a specific + person instead of a generically clean one. +- Dumbify compresses the register the rest of the way. + +Personify and dumbify overlap, and it's worth being clear about where. +Personify's work register already does lowercase starts, fragments, contractions, +and hedge-cutting, so for most work writing personify alone is enough and +dumbify adds nothing. Reach for dumbify when you want the register pushed +past what personify does: level 2 is roughly where personify's work register +lands, and levels 3–4 go further than personify ever will. Run it after +personify, not before — personify's de-abstraction pass wants the actor and +the full sentence present to work on, and dumbify deletes exactly those. + +Each skill stands alone. pr-review runs without either; personify runs without +dumbify; dumbify runs on any text at all. + ## Watermarks Dumbify is not advertised as a watermark-removal mechanism. Any change to watermark detectability is an empirical property of the watermarking system and must be tested rather than assumed. diff --git a/scripts/validate_skill.py b/scripts/validate_skill.py new file mode 100644 index 0000000..aeb8550 --- /dev/null +++ b/scripts/validate_skill.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Sanity-check SKILL.md structure and the plugin/marketplace manifests.""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SKILL_PATH = ROOT / "SKILL.md" + + +def fail(message: str) -> None: + print(f"FAIL: {message}", file=sys.stderr) + sys.exit(1) + + +def main() -> None: + if not SKILL_PATH.exists(): + fail("SKILL.md not found") + + text = SKILL_PATH.read_text(encoding="utf-8") + + frontmatter_match = re.match(r"\A---\n(.*?)\n---\n", text, re.DOTALL) + if frontmatter_match is None: + fail("SKILL.md must start with YAML frontmatter") + frontmatter = frontmatter_match.group(1) + + for required_key in ("name:", "description:"): + if not re.search(rf"(?m)^{re.escape(required_key)}", frontmatter): + key = required_key[:-1] + fail(f"SKILL.md frontmatter missing required key: {key}") + + for nonportable_key in ("compatibility:", "allowed-tools:"): + if re.search(rf"(?m)^{re.escape(nonportable_key)}", frontmatter): + key = nonportable_key[:-1] + fail(f"Remove nonportable frontmatter key: {key}") + + manifests = {} + for manifest_name in ("plugin.json", "marketplace.json"): + manifest_path = ROOT / ".claude-plugin" / manifest_name + try: + manifests[manifest_name] = json.loads( + manifest_path.read_text(encoding="utf-8") + ) + except FileNotFoundError: + fail(f"{manifest_name} not found") + except json.JSONDecodeError as exc: + fail(f"{manifest_name} is not valid JSON: {exc}") + + skill_version_match = re.search( + r"(?m)^\s*version:\s*[\"']?([^\"'\n]+)", frontmatter + ) + if skill_version_match is None: + fail("SKILL.md frontmatter missing metadata.version") + skill_version = skill_version_match.group(1).strip() + plugin_version = manifests["plugin.json"].get("version") + if skill_version != plugin_version: + fail( + "Version mismatch: SKILL.md metadata.version=" + f"{skill_version!r} vs. plugin.json version={plugin_version!r}" + ) + + print("SKILL.md and plugin manifests are valid") + + +if __name__ == "__main__": + main()