diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index c02fbd2f..bf2f30cc 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -343,7 +343,7 @@ "icon": "./plugins/ejentum/ejentum-mcp/assets/ejentum-icon.svg" }, { - "name": "epic-harness", + "name": "epic", "displayName": "Epic Harness", "source": { "source": "local", @@ -550,6 +550,21 @@ "description": "High-compression communication mode for Codex agents that removes filler while preserving search, validation, and implementation effort.", "icon": "./plugins/Maksim-Burtsev/simple-man/assets/icon.png" }, + { + "name": "skill-routing-kit", + "displayName": "Skill Routing Kit", + "source": { + "source": "local", + "path": "./plugins/juew/Skill-Routing-Kit" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Development & Workflow", + "description": "Local-first Codex routing guard that improves skill and plugin selection with a capability registry, negative examples, and explainable diagnostics.", + "icon": "./plugins/juew/Skill-Routing-Kit/assets/composer-icon.png" + }, { "name": "spec-driven", "displayName": "Spec-Driven Development", diff --git a/README.md b/README.md index 7ce8c43f..427fe807 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,7 @@ Third-party plugins built by the community. [PRs welcome](#contributing)! - [Secret Guard](./plugins/mturac/secret-guard) - Pre-commit secret scanner using pattern and entropy detection. - [Session Orchestrator](https://github.com/Kanevry/session-orchestrator) - Session orchestration for Claude Code, Codex, and Cursor IDE — structured planning, wave-based execution, VCS integration (GitLab + GitHub), quality gates, and clean session close-out with issue tracking. - [Simple Man](https://github.com/Maksim-Burtsev/simple-man) - High-compression communication mode for Codex agents that removes filler while preserving search, validation, and implementation effort. +- [Skill Routing Kit](https://github.com/juew/Skill-Routing-Kit) - Local-first Codex routing guard that improves skill and plugin selection with a capability registry, negative examples, and explainable diagnostics. - [Spec-Driven Development](https://github.com/Habib0x0/spec-driven-plugin) - Three-phase Requirements → Design → Tasks workflow for Claude Code and Codex — EARS notation acceptance criteria, autonomous execution loop, cross-spec dependencies, and post-implementation acceptance testing. - [Staff Engineer Mode](https://github.com/sirmarkz/staff-engineer-mode) - Routes engineering design, delivery, reliability, security, operations, and maintenance prompts to focused staff-level specialist guidance for AI coding agents. - [Standup Generator](./plugins/mturac/standup-gen) - Daily standup notes from git activity across repos. diff --git a/plugins.json b/plugins.json index dfeebe7b..66a268b9 100644 --- a/plugins.json +++ b/plugins.json @@ -3,7 +3,7 @@ "name": "awesome-codex-plugins", "version": "1.0.0", "last_updated": "2026-06-04", - "total": 98, + "total": 99, "categories": [ "Development & Workflow", "Tools & Integrations" @@ -379,6 +379,16 @@ "source": "awesome-codex-plugins", "install_url": "https://raw.githubusercontent.com/Maksim-Burtsev/simple-man/HEAD/plugins/simple-man/.codex-plugin/plugin.json" }, + { + "name": "Skill Routing Kit", + "url": "https://github.com/juew/Skill-Routing-Kit", + "owner": "juew", + "repo": "Skill-Routing-Kit", + "description": "Local-first Codex routing guard that improves skill and plugin selection with a capability registry, negative examples, and explainable diagnostics.", + "category": "Development & Workflow", + "source": "awesome-codex-plugins", + "install_url": "https://raw.githubusercontent.com/juew/Skill-Routing-Kit/HEAD/.codex-plugin/plugin.json" + }, { "name": "Spec-Driven Development", "url": "https://github.com/Habib0x0/spec-driven-plugin", diff --git a/plugins/juew/Skill-Routing-Kit/.codex-plugin/plugin.json b/plugins/juew/Skill-Routing-Kit/.codex-plugin/plugin.json new file mode 100644 index 00000000..ede8229b --- /dev/null +++ b/plugins/juew/Skill-Routing-Kit/.codex-plugin/plugin.json @@ -0,0 +1,36 @@ +{ + "name": "skill-routing-kit", + "version": "0.1.0+codex.20260604032338", + "description": "Local-first skill and plugin routing guard, registry, and diagnostics for Codex.", + "author": { + "name": "Zhonghao" + }, + "repository": "https://github.com/juew/Skill-Routing-Kit", + "license": "MIT", + "keywords": [ + "codex", + "codex-plugin", + "skill-routing", + "ai-agents", + "local-first", + "developer-tools" + ], + "skills": "./skills/", + "interface": { + "displayName": "Skill Routing Kit", + "shortDescription": "Local-first skill routing guard and diagnostics.", + "longDescription": "Improve Codex skill and plugin hit rate with an always-on AGENTS routing guard, a local capability registry, and explicit diagnostic scripts.", + "defaultPrompt": "Use Skill Routing Kit to diagnose which Codex skill or plugin should handle a request, refresh the local capability registry, or improve skill routing hit rate.", + "developerName": "Zhonghao", + "category": "Productivity", + "brandColor": "#22D3EE", + "composerIcon": "./assets/icon.png", + "logo": "./assets/logo.png", + "capabilities": [ + "Skill routing guard", + "Local capability registry", + "Routing diagnostics", + "Registry maintenance" + ] + } +} diff --git a/plugins/juew/Skill-Routing-Kit/.codexignore b/plugins/juew/Skill-Routing-Kit/.codexignore new file mode 100644 index 00000000..26c647d4 --- /dev/null +++ b/plugins/juew/Skill-Routing-Kit/.codexignore @@ -0,0 +1,14 @@ +__pycache__/ +*.py[cod] +.pytest_cache/ +.DS_Store +.git/ +.github/ +tests/ +docs/launch/ +launch/ +launch-pack/ +promotion-drafts/ +*.draft.md +posts.csv +interactions.md diff --git a/plugins/juew/Skill-Routing-Kit/LICENSE b/plugins/juew/Skill-Routing-Kit/LICENSE new file mode 100644 index 00000000..999ce413 --- /dev/null +++ b/plugins/juew/Skill-Routing-Kit/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Zhonghao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/juew/Skill-Routing-Kit/README.md b/plugins/juew/Skill-Routing-Kit/README.md new file mode 100644 index 00000000..bdec75e7 --- /dev/null +++ b/plugins/juew/Skill-Routing-Kit/README.md @@ -0,0 +1,197 @@ +

+ Skill Routing Kit logo +

+ +

Skill Routing Kit

+ +

+ Make Codex pick the right skill or plugin more often. +

+ +

+ 中文说明 + · + Full English docs + · + Demo + · + Release notes +

+ +

+ Codex plugin + Local first + CI + License +

+ +## Why This Exists + +Codex can have dozens of skills and plugins installed, but the correct one does not always trigger. + +That creates a quiet productivity tax: + +- a PDF input gets mistaken for the final artifact; +- a routing/debugging question gets handled by the wrong domain skill; +- connector plugins are considered even when the user's work is local; +- new skills are installed, but nobody remembers when to use them. + +Skill Routing Kit adds a small local-first routing layer for Codex. It helps Codex answer: + +```text +What kind of task is this? +Where is the source of truth? +What is the final artifact? +Which skill should be primary? +Which plugin is only a helper? +When should a tempting skill not be used? +``` + +## What You Get + +- `skill-router`: a Codex skill for diagnosing skill/plugin routing decisions. +- Local registry: a JSON capability index with categories, use cases, negative examples, and provenance. +- Routing guard: an `AGENTS.md` snippet that makes routing a quiet default behavior. +- Diagnostic scripts: route a request, refresh the registry, and check stale or broken entries. +- Local-first safety: no background scan, no telemetry, no connector content reads, no network by default. + +## 30-Second Install + +If you use Codex, ask it to install the plugin for you: + +```text +Please install the Skill Routing Kit plugin from https://github.com/juew/Skill-Routing-Kit. Install the plugin source globally at ~/plugins/skill-routing-kit, register it in ~/.agents/plugins/marketplace.json, run codex plugin add skill-routing-kit@personal, and enable the routing guard globally in ~/.codex/AGENTS.md. Do not install it into the current project. Do not ask me to create directories manually; use the repository installer and verify the plugin after installation. +``` + +Or run one command: + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/juew/Skill-Routing-Kit/main/scripts/install.sh)" -- --install-agents --codex-add +``` + +The installer creates directories, updates your personal Codex marketplace, enables the plugin, and backs up older local copies. + +## Before And After + +### Routing Diagnostic + +```bash +python3 scripts/route_request.py "为什么 pdf skill 没有命中这个请求" +``` + +Expected result: + +```text +Recommended skill/plugin: +- Skill Router (skill-router) + +Helper skills/plugins: +- PDF (pdf) + +Why: +- routing diagnostic request prefers the skill-router capability +- local-first preference +``` + +### Final Artifact Routing + +```bash +python3 scripts/route_request.py "把这个 PDF 整理成一份 PPT" +``` + +Expected result: + +```text +Recommended skill/plugin: +- Presentations (presentations) + +Helper skills/plugins: +- PDF (pdf) +``` + +The final artifact is the presentation. The PDF skill is useful context, not the primary route. + +More examples: [docs/demo.md](docs/demo.md) + +## How It Works + +Skill Routing Kit turns skill/plugin selection into a lightweight recall-and-rerank loop. + +```mermaid +flowchart LR + A["User request"] --> B["Routing guard"] + B --> C["Classify task, source, artifact, process"] + C --> D["Recall candidates from registry"] + D --> E["Rerank by final artifact, source, action, risk"] + E --> F["Use primary skill/plugin"] + E --> G["Keep helper skills contextual"] +``` + +The methodology is simple: + +- describe **when to use** each capability; +- classify capabilities into layered routing categories such as `process`, `source`, `artifact`, `domain`, and `risk`; +- describe **when not to use** each capability with negative examples; +- recall broad candidates first, then rerank by final artifact, source, task action, and permission risk. + +## Safety Model + +This project is intentionally conservative. + +- It reads local `SKILL.md`, `plugin.json`, and registry metadata. +- It does not read Gmail, Slack, Notion, Drive, or other connector content. +- It does not check connector authorization. +- It does not install hooks, telemetry, daemons, or background scanners. +- It can be removed by deleting the `AGENTS.md` block and uninstalling the plugin. + +## Common Commands + +```bash +# Validate the plugin +python3 /Users/zhonghao/.codex/skills/.system/plugin-creator/scripts/validate_plugin.py . + +# Run tests +python3 -B -m unittest discover -s tests + +# Route one request +python3 scripts/route_request.py "帮我把这个 PDF 做成 PPT 并保留版式" + +# Check registry health +python3 scripts/route_request.py --check-registry + +# Refresh generated registry +python3 scripts/build_registry.py --yes +``` + +## Who Should Star This + +Star this repo if you are: + +- building Codex skills or plugins; +- using many AI agent capabilities and seeing missed triggers; +- designing local-first agent workflows; +- maintaining a team skill library; +- interested in explainable routing for AI tools. + +## Roadmap + +- Registry diff after skill/plugin install or removal. +- Better negative-example authoring tools. +- Optional embedding-based reranker. +- Route quality evaluation suite. +- UI screenshots and short demo GIF. +- More connector-aware but still local-first policy cards. + +## Documentation + +- [Chinese README](README.zh-CN.md) +- [Full English documentation](README.en.md) +- [Demo scenarios](docs/demo.md) +- [Release notes](docs/release-v0.1.0.md) +- [Promotion kit](docs/promotion-kit.md) +- [Contributing guide](CONTRIBUTING.md) +- [Changelog](CHANGELOG.md) + +## License + +MIT diff --git a/plugins/juew/Skill-Routing-Kit/assets/composer-icon.png b/plugins/juew/Skill-Routing-Kit/assets/composer-icon.png new file mode 100644 index 00000000..42e6ca70 Binary files /dev/null and b/plugins/juew/Skill-Routing-Kit/assets/composer-icon.png differ diff --git a/plugins/juew/Skill-Routing-Kit/assets/icon.png b/plugins/juew/Skill-Routing-Kit/assets/icon.png new file mode 100644 index 00000000..b2a10f6a Binary files /dev/null and b/plugins/juew/Skill-Routing-Kit/assets/icon.png differ diff --git a/plugins/juew/Skill-Routing-Kit/assets/logo.png b/plugins/juew/Skill-Routing-Kit/assets/logo.png new file mode 100644 index 00000000..48133077 Binary files /dev/null and b/plugins/juew/Skill-Routing-Kit/assets/logo.png differ diff --git a/plugins/juew/Skill-Routing-Kit/registry/core-capabilities.json b/plugins/juew/Skill-Routing-Kit/registry/core-capabilities.json new file mode 100644 index 00000000..79b49f74 --- /dev/null +++ b/plugins/juew/Skill-Routing-Kit/registry/core-capabilities.json @@ -0,0 +1,273 @@ +{ + "schema_version": "1.0", + "generated_at": "2026-06-04T00:00:00+08:00", + "description": "Static core routing capabilities for Skill Routing Kit v1.", + "capabilities": [ + { + "id": "skill-router", + "name": "Skill Router", + "kind": "skill", + "categories": ["process", "routing", "diagnostic", "local", "skill_discovery"], + "use_when": "Use when diagnosing skill or plugin routing, hit rate, registry freshness, why a skill did not trigger, or which capability should handle a request.", + "avoid_when": ["Do not use for ordinary task execution when a concrete domain skill clearly applies."], + "inputs": ["user_request", "registry"], + "outputs": ["recommended_skill", "helper_skills", "routing_reasons"], + "requires": ["registry_optional"], + "examples": ["Why did this skill not trigger?", "Which skill should handle this request?"], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "superpowers-using-superpowers", + "name": "Superpowers: Using Superpowers", + "kind": "skill", + "categories": ["process", "skill_discovery", "routing", "local"], + "use_when": "Use at conversation start or when deciding whether a process skill applies.", + "avoid_when": ["Do not use inside a delegated subagent task."], + "inputs": ["user_request"], + "outputs": ["skill_selection"], + "requires": ["skill_metadata"], + "examples": ["Which skill should I use before starting this task?"], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "superpowers-brainstorming", + "name": "Superpowers: Brainstorming", + "kind": "skill", + "categories": ["process", "planning", "creative"], + "use_when": "Use before creative feature work, product ideas, new components, or behavior changes.", + "avoid_when": ["Do not use for direct factual answers or routine command execution."], + "inputs": ["idea", "requirements"], + "outputs": ["clarified_requirements", "options"], + "requires": ["conversation"], + "examples": ["Let's design a new workflow.", "Help me shape this product idea."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "superpowers-systematic-debugging", + "name": "Superpowers: Systematic Debugging", + "kind": "skill", + "categories": ["process", "debugging", "verification"], + "use_when": "Use for bugs, failing tests, unexpected behavior, regressions, or unclear failures.", + "avoid_when": ["Do not use for purely conceptual explanations with no failure to investigate."], + "inputs": ["error", "test_failure", "logs", "reproduction"], + "outputs": ["root_cause", "fix_plan"], + "requires": ["evidence"], + "examples": ["Fix this failing test.", "Investigate this UI bug."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "superpowers-writing-plans", + "name": "Superpowers: Writing Plans", + "kind": "skill", + "categories": ["process", "planning", "implementation"], + "use_when": "Use when a multi-step implementation plan is needed before touching code.", + "avoid_when": ["Do not use for tiny one-line changes or direct answers."], + "inputs": ["requirements", "spec"], + "outputs": ["implementation_plan"], + "requires": ["repo_context"], + "examples": ["Plan this refactor before implementing."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "superpowers-test-driven-development", + "name": "Superpowers: Test Driven Development", + "kind": "skill", + "categories": ["process", "testing", "implementation"], + "use_when": "Use when implementing a feature or bug fix where tests can express behavior first.", + "avoid_when": ["Do not use when tests are impossible and user asks for a quick exploratory answer."], + "inputs": ["feature", "bugfix"], + "outputs": ["tests", "implementation"], + "requires": ["test_runner"], + "examples": ["Add this feature with tests first."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "superpowers-verification-before-completion", + "name": "Superpowers: Verification Before Completion", + "kind": "skill", + "categories": ["process", "verification", "completion"], + "use_when": "Use before claiming work is complete, fixed, or passing.", + "avoid_when": ["Do not use for pure discussion with no completed work."], + "inputs": ["changes", "tests"], + "outputs": ["verification_evidence"], + "requires": ["commands_or_checks"], + "examples": ["Verify before saying this is done."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "subagent-orchestration", + "name": "Subagent Orchestration", + "kind": "skill", + "categories": ["process", "orchestration", "multi_agent", "long_running"], + "use_when": "Use for long-running, multi-agent, handoff-sensitive, tool-policy-sensitive, or artifact-consistency-heavy work.", + "avoid_when": ["Do not use for ordinary single-agent tasks or simple local edits."], + "inputs": ["objective", "task_slices"], + "outputs": ["delegation_contracts", "acceptance_gates", "handoffs"], + "requires": ["subagents"], + "examples": ["Arrange multiple agents to review this plan."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "frontend-app-builder", + "name": "Frontend App Builder", + "kind": "skill", + "categories": ["artifact", "frontend", "web_app", "local", "writes_files"], + "use_when": "Use for building new frontend apps, dashboards, games, creative websites, or redesigns.", + "avoid_when": ["Do not use for plain documents, slide decks, or simple explanations."], + "inputs": ["brief", "design", "local_repo"], + "outputs": ["web_app", "code"], + "requires": ["filesystem", "browser_verification"], + "examples": ["Build a dashboard app.", "Create a polished frontend prototype."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "frontend-testing-debugging", + "name": "Frontend Testing and Debugging", + "kind": "skill", + "categories": ["process", "frontend", "testing", "browser"], + "use_when": "Use for testing or debugging rendered frontend apps, console errors, responsive issues, and UI regressions.", + "avoid_when": ["Do not use before any rendered frontend target exists."], + "inputs": ["localhost", "screenshot", "console_error"], + "outputs": ["test_evidence", "bug_report", "fix"], + "requires": ["browser"], + "examples": ["Check this page in the browser.", "Fix this responsive layout bug."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "documents", + "name": "Documents", + "kind": "skill", + "categories": ["artifact", "document", "docx", "local", "writes_files"], + "use_when": "Use for creating, editing, redlining, rendering, or verifying Word/DOCX documents.", + "avoid_when": ["Do not use when final output is a slide deck, spreadsheet, or web app."], + "inputs": ["docx", "outline", "markdown"], + "outputs": ["docx", "pdf_preview"], + "requires": ["filesystem"], + "examples": ["Create a Word report.", "Edit this DOCX and render it."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "spreadsheets", + "name": "Spreadsheets", + "kind": "skill", + "categories": ["artifact", "spreadsheet", "xlsx", "csv", "local", "writes_files"], + "use_when": "Use for creating, editing, analyzing, visualizing, or validating spreadsheet files.", + "avoid_when": ["Do not use for prose-only summaries unless a spreadsheet is the final artifact."], + "inputs": ["xlsx", "csv", "tsv", "data"], + "outputs": ["xlsx", "csv", "charts"], + "requires": ["filesystem"], + "examples": ["Analyze this CSV.", "Create an Excel workbook with formulas."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "presentations", + "name": "Presentations", + "kind": "skill", + "categories": ["artifact", "presentation", "pptx", "local", "writes_files"], + "use_when": "Use for creating, editing, rendering, verifying, or exporting slide decks and PPTX files.", + "avoid_when": ["Do not use for simple prose outlines unless the final deliverable is slides."], + "inputs": ["outline", "doc", "spreadsheet", "images"], + "outputs": ["pptx", "rendered_slides"], + "requires": ["filesystem"], + "examples": ["Turn this PDF into a PPT.", "Create an investor deck."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "pdf", + "name": "PDF", + "kind": "skill", + "categories": ["artifact", "pdf", "local", "read_only"], + "use_when": "Use for reading, creating, rendering, extracting, or visually reviewing PDF files.", + "avoid_when": ["Do not use as primary when PDF is only an input to a slide deck or spreadsheet deliverable."], + "inputs": ["pdf"], + "outputs": ["text", "pdf", "rendered_pages"], + "requires": ["filesystem"], + "examples": ["Read this PDF.", "Render this PDF and inspect pages."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "imagegen", + "name": "Image Generation", + "kind": "skill", + "categories": ["artifact", "image", "visual", "generation"], + "use_when": "Use for generating or editing bitmap images, illustrations, sprites, mockups, or visual variants.", + "avoid_when": ["Do not use for code-native SVG or existing icon-system edits unless bitmap output is needed."], + "inputs": ["prompt", "reference_image"], + "outputs": ["image"], + "requires": ["image_generation"], + "examples": ["Generate a mascot image.", "Edit this image."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "github", + "name": "GitHub", + "kind": "plugin", + "categories": ["source", "github", "connector", "requires_auth"], + "use_when": "Use when the user references GitHub repositories, issues, pull requests, reviews, or CI.", + "avoid_when": ["Do not use for local-only git work unless the user asks for remote GitHub context."], + "inputs": ["repo", "issue", "pull_request", "ci"], + "outputs": ["repo_context", "pr", "issue_update"], + "requires": ["connector_auth"], + "examples": ["Summarize this PR.", "Fix failing GitHub Actions."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "gmail", + "name": "Gmail", + "kind": "plugin", + "categories": ["source", "gmail", "connector", "requires_auth"], + "use_when": "Use when the user asks to search, summarize, draft, forward, label, archive, or triage Gmail messages.", + "avoid_when": ["Do not use when the user provides local email exports or only wants generic writing help."], + "inputs": ["gmail_query", "message_id"], + "outputs": ["email_summary", "draft", "labels"], + "requires": ["connector_auth"], + "examples": ["Find recent emails about this client.", "Draft a reply."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "google-drive", + "name": "Google Drive", + "kind": "plugin", + "categories": ["source", "drive", "docs", "sheets", "slides", "connector", "requires_auth"], + "use_when": "Use when the user asks to find, fetch, edit, organize, export, or summarize Drive/Docs/Sheets/Slides files.", + "avoid_when": ["Do not use when files are local unless the user explicitly names Google Drive."], + "inputs": ["drive_file", "google_doc", "google_sheet", "google_slide"], + "outputs": ["file_summary", "updated_file"], + "requires": ["connector_auth"], + "examples": ["Find this Google Doc.", "Summarize files in Drive."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + }, + { + "id": "canva", + "name": "Canva", + "kind": "plugin", + "categories": ["artifact", "visual", "design", "connector", "requires_auth"], + "use_when": "Use when the user asks to create, edit, translate, resize, or inspect Canva designs.", + "avoid_when": ["Do not use for local bitmap generation unless Canva output is requested."], + "inputs": ["design_brief", "canva_design"], + "outputs": ["canva_design"], + "requires": ["connector_auth"], + "examples": ["Create social posts in Canva.", "Translate this Canva design."], + "provenance": {"source_type": "core_static", "path": "registry/core-capabilities.json"}, + "updated_at": "2026-06-04T00:00:00+08:00" + } + ] +} diff --git a/plugins/juew/Skill-Routing-Kit/scripts/build_registry.py b/plugins/juew/Skill-Routing-Kit/scripts/build_registry.py new file mode 100755 index 00000000..dff52243 --- /dev/null +++ b/plugins/juew/Skill-Routing-Kit/scripts/build_registry.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Build a local capability registry from SKILL.md and plugin.json files. + +The builder is intentionally local and conservative: +- read-only scan of metadata files +- no network +- no connector content access +- no authorization checks +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_OUTPUT = ROOT / "registry" / "capabilities.generated.json" +FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*", re.DOTALL) + + +def iso_now() -> str: + return datetime.now(timezone.utc).isoformat() + + +def parse_frontmatter(text: str) -> dict[str, str]: + match = FRONTMATTER_RE.match(text) + if not match: + return {} + data: dict[str, str] = {} + current_key: str | None = None + current_value: list[str] = [] + for raw_line in match.group(1).splitlines(): + line = raw_line.rstrip() + if not line.strip(): + continue + if re.match(r"^[A-Za-z0-9_-]+:", line): + if current_key: + data[current_key] = " ".join(current_value).strip() + key, value = line.split(":", 1) + current_key = key.strip() + current_value = [value.strip().strip('"').strip("'")] + elif current_key: + current_value.append(line.strip()) + if current_key: + data[current_key] = " ".join(current_value).strip() + return data + + +def categories_for(name: str, description: str, kind: str) -> list[str]: + haystack = f"{name} {description}".lower() + categories: set[str] = set() + if kind == "plugin": + categories.add("plugin") + if kind == "skill": + categories.add("skill") + + mapping = { + "debug": ["process", "debugging"], + "bug": ["process", "debugging"], + "test": ["process", "testing"], + "verify": ["process", "verification"], + "review": ["process", "review"], + "plan": ["process", "planning"], + "routing": ["process", "routing"], + "router": ["process", "routing"], + "registry": ["process", "routing", "maintenance"], + "orchestration": ["process", "orchestration", "multi_agent"], + "subagent": ["process", "orchestration", "multi_agent"], + "github": ["source", "github", "external_connector", "requires_auth"], + "gmail": ["source", "gmail", "external_connector", "requires_auth"], + "drive": ["source", "drive", "external_connector", "requires_auth"], + "notion": ["source", "notion", "external_connector", "requires_auth"], + "slack": ["source", "slack", "external_connector", "requires_auth"], + "figma": ["source", "figma", "external_connector", "requires_auth", "design"], + "linear": ["source", "linear", "external_connector", "requires_auth"], + "canva": ["artifact", "visual", "external_connector", "requires_auth"], + "pdf": ["artifact", "pdf", "local"], + "docx": ["artifact", "document", "local"], + "document": ["artifact", "document"], + "spreadsheet": ["artifact", "spreadsheet"], + "xlsx": ["artifact", "spreadsheet", "local"], + "csv": ["artifact", "spreadsheet", "local"], + "presentation": ["artifact", "presentation"], + "ppt": ["artifact", "presentation", "local"], + "frontend": ["artifact", "frontend", "web_app", "local"], + "react": ["artifact", "frontend", "web_app", "local"], + "image": ["artifact", "image", "visual"], + } + for token, values in mapping.items(): + if token in haystack: + categories.update(values) + if "external_connector" not in categories: + categories.add("local") + return sorted(categories) + + +def file_mtime(path: Path) -> str: + return datetime.fromtimestamp(path.stat().st_mtime, timezone.utc).isoformat() + + +def skill_card(path: Path) -> dict[str, Any] | None: + try: + text = path.read_text(encoding="utf-8") + except UnicodeDecodeError: + return None + meta = parse_frontmatter(text) + name = meta.get("name") or path.parent.name + description = meta.get("description", "") + return { + "id": name.lower().replace(":", "-").replace("_", "-"), + "name": name, + "kind": "skill", + "categories": categories_for(name, description, "skill"), + "use_when": description, + "avoid_when": [], + "inputs": [], + "outputs": [], + "requires": [], + "examples": [], + "provenance": { + "source_type": "skill", + "path": str(path), + }, + "updated_at": file_mtime(path), + } + + +def plugin_card(path: Path) -> dict[str, Any] | None: + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return None + name = data.get("name") or path.parent.parent.name + description = data.get("description", "") + return { + "id": str(name).lower().replace("_", "-"), + "name": name, + "kind": "plugin", + "categories": categories_for(str(name), str(description), "plugin"), + "use_when": str(description), + "avoid_when": [], + "inputs": [], + "outputs": [], + "requires": ["connector_auth"] if "external_connector" in categories_for(str(name), str(description), "plugin") else [], + "examples": [], + "provenance": { + "source_type": "plugin", + "path": str(path), + }, + "updated_at": file_mtime(path), + } + + +def iter_files(root: Path, filename: str) -> list[Path]: + if not root.exists(): + return [] + return [path for path in root.rglob(filename) if path.is_file()] + + +def default_roots() -> list[Path]: + roots = [ + Path(os.path.expanduser("~/.codex/skills")), + Path(os.path.expanduser("~/.codex/plugins/cache")), + ROOT / "skills", + ] + return roots + + +def build_registry(roots: list[Path]) -> dict[str, Any]: + cards: list[dict[str, Any]] = [] + seen: set[tuple[str, str]] = set() + for root in roots: + for path in iter_files(root, "SKILL.md"): + card = skill_card(path) + if card: + key = (card["kind"], card["id"]) + if key not in seen: + seen.add(key) + cards.append(card) + for path in iter_files(root, "plugin.json"): + if path.parent.name != ".codex-plugin": + continue + card = plugin_card(path) + if card: + key = (card["kind"], card["id"]) + if key not in seen: + seen.add(key) + cards.append(card) + + return { + "schema_version": "1.0", + "generated_at": iso_now(), + "source_roots": [str(root) for root in roots], + "capabilities": sorted(cards, key=lambda item: (item["kind"], item["id"])), + } + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Build a local Skill Routing Kit registry.") + parser.add_argument("--root", action="append", type=Path, help="Additional root to scan. Can be repeated.") + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT, help="Output registry JSON path.") + parser.add_argument("--yes", action="store_true", help="Write without interactive confirmation.") + parser.add_argument("--dry-run", action="store_true", help="Print summary without writing.") + args = parser.parse_args(argv) + + roots = default_roots() + if args.root: + roots.extend(args.root) + + registry = build_registry(roots) + print("Skill Routing Kit registry summary:") + print(f"- source roots: {len(roots)}") + print(f"- capabilities: {len(registry['capabilities'])}") + print(f"- output: {args.output}") + + if args.dry_run: + return 0 + + should_write = args.yes + if not should_write and sys.stdin.isatty(): + answer = input("Write registry? [y/N] ").strip().lower() + should_write = answer in {"y", "yes"} + + if not should_write: + print("Registry not written. Re-run with --yes to write non-interactively.") + return 1 + + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(registry, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + print(f"Wrote {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins/juew/Skill-Routing-Kit/scripts/hooks/pre-push b/plugins/juew/Skill-Routing-Kit/scripts/hooks/pre-push new file mode 100755 index 00000000..7f26bd74 --- /dev/null +++ b/plugins/juew/Skill-Routing-Kit/scripts/hooks/pre-push @@ -0,0 +1,66 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd) +case "$script_dir" in + */.git/hooks) + repo_root=$(CDPATH= cd "$script_dir/../.." && pwd) + ;; + */scripts/hooks) + repo_root=$(CDPATH= cd "$script_dir/../.." && pwd) + ;; + *) + repo_root=$(git rev-parse --show-toplevel) + ;; +esac +cd "$repo_root" + +blocked_paths='(^|/)(docs/launch|launch|launch-pack|promotion-drafts)(/|$)|(^|/)(posts\.csv|interactions\.md)$|\.draft\.md$' +blocked_terms='V2EX|Reddit|Product Hunt|Hacker News|outreach|launch pack|promotion draft' + +staged_files=$(git diff --cached --name-only) +uncommitted_files=$(git diff --name-only) +committed_range_files='' +committed_range_text='' + +while read -r local_ref local_sha remote_ref remote_sha +do + case "$local_sha" in + 0000000000000000000000000000000000000000) + continue + ;; + esac + + if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then + range="$local_sha" + else + range="$remote_sha..$local_sha" + fi + + committed_range_files="$committed_range_files +$(git diff --name-only "$range")" + committed_range_text="$committed_range_text +$(git diff --cached --name-only) +$(git log --format=%B "$range")" +done + +files_to_check="$staged_files +$uncommitted_files +$committed_range_files" + +if printf '%s\n' "$files_to_check" | grep -Eiq "$blocked_paths"; then + echo "pre-push blocked: launch/promotion artifacts appear in files being pushed." >&2 + printf '%s\n' "$files_to_check" | grep -Ei "$blocked_paths" >&2 || true + echo "Move private launch materials outside this repository before pushing." >&2 + exit 1 +fi + +if printf '%s\n' "$committed_range_text" | grep -Eiq "$blocked_terms"; then + echo "pre-push warning: promotion-related terms appear in commit messages for this push." >&2 + echo "If this is intentional product documentation, set ALLOW_PROMOTION_PUSH=1 and push again." >&2 + if [ "${ALLOW_PROMOTION_PUSH:-}" != "1" ]; then + exit 1 + fi +fi + +exit 0 diff --git a/plugins/juew/Skill-Routing-Kit/scripts/install.py b/plugins/juew/Skill-Routing-Kit/scripts/install.py new file mode 100755 index 00000000..00f3a47c --- /dev/null +++ b/plugins/juew/Skill-Routing-Kit/scripts/install.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +"""Install Skill Routing Kit into a local Codex home.""" + +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import shutil +import sys +from datetime import datetime +from pathlib import Path + + +BEGIN_MARKER = "" +END_MARKER = "" +PLUGIN_DIR_NAME = "skill-routing-kit" +DEFAULT_MARKETPLACE_NAME = "personal" +SKIP_DIRS = {".git", "__pycache__", ".pytest_cache"} +SKIP_SUFFIXES = {".pyc", ".pyo"} + + +def default_codex_home() -> Path: + return Path(os.environ.get("CODEX_HOME", "~/.codex")).expanduser() + + +def default_target() -> Path: + return Path.home() / "plugins" / PLUGIN_DIR_NAME + + +def default_marketplace_path() -> Path: + return Path.home() / ".agents" / "plugins" / "marketplace.json" + + +def default_agents_path() -> Path: + return default_codex_home() / "AGENTS.md" + + +def repo_root() -> Path: + root = Path(__file__).resolve().parents[1] + manifest = root / ".codex-plugin" / "plugin.json" + if not manifest.exists(): + raise SystemExit(f"Cannot find plugin manifest at {manifest}") + return root + + +def should_skip(path: Path) -> bool: + if any(part in SKIP_DIRS for part in path.parts): + return True + return path.suffix in SKIP_SUFFIXES + + +def copy_plugin(source: Path, target: Path) -> None: + if target.exists(): + backup_root = target.parent / ".backups" + backup_root.mkdir(parents=True, exist_ok=True) + stamp = datetime.now().strftime("%Y%m%d-%H%M%S") + backup_path = backup_root / f"{target.name}-{stamp}" + shutil.move(str(target), str(backup_path)) + print(f"Backed up existing plugin to: {backup_path}") + + target.mkdir(parents=True, exist_ok=True) + + for item in source.rglob("*"): + relative = item.relative_to(source) + if should_skip(relative): + continue + destination = target / relative + if item.is_dir(): + destination.mkdir(parents=True, exist_ok=True) + else: + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(item, destination) + + +def install_agents_snippet(snippet_path: Path, agents_path: Path) -> str: + snippet = snippet_path.read_text(encoding="utf-8") + agents_path.parent.mkdir(parents=True, exist_ok=True) + + if agents_path.exists(): + current = agents_path.read_text(encoding="utf-8") + else: + current = "" + + if BEGIN_MARKER in current and END_MARKER in current: + before = current.split(BEGIN_MARKER, 1)[0].rstrip() + after = current.split(END_MARKER, 1)[1].lstrip() + updated = f"{before}\n\n{snippet.rstrip()}\n\n{after}".strip() + "\n" + action = "Updated" + else: + separator = "\n\n" if current.strip() else "" + updated = f"{current.rstrip()}{separator}{snippet.rstrip()}\n" + action = "Added" + + agents_path.write_text(updated, encoding="utf-8") + return action + + +def marketplace_display_name(name: str) -> str: + return " ".join(part.capitalize() for part in name.replace("_", "-").split("-") if part) + + +def build_marketplace_entry() -> dict[str, object]: + return { + "name": PLUGIN_DIR_NAME, + "source": { + "source": "local", + "path": f"./plugins/{PLUGIN_DIR_NAME}", + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL", + }, + "category": "Productivity", + } + + +def load_or_create_marketplace(marketplace_path: Path, marketplace_name: str) -> dict[str, object]: + if marketplace_path.exists(): + payload = json.loads(marketplace_path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise SystemExit(f"{marketplace_path} must contain a JSON object.") + return payload + + return { + "name": marketplace_name, + "interface": { + "displayName": marketplace_display_name(marketplace_name), + }, + "plugins": [], + } + + +def update_marketplace_json(marketplace_path: Path, marketplace_name: str) -> tuple[str, str]: + marketplace_path.parent.mkdir(parents=True, exist_ok=True) + payload = load_or_create_marketplace(marketplace_path, marketplace_name) + + existing_name = payload.get("name") + if not isinstance(existing_name, str) or not existing_name.strip(): + raise SystemExit(f"{marketplace_path} must contain a non-empty string 'name'.") + marketplace_name = existing_name.strip() + + interface = payload.setdefault("interface", {}) + if not isinstance(interface, dict): + raise SystemExit(f"{marketplace_path} field 'interface' must be an object.") + interface.setdefault("displayName", marketplace_display_name(marketplace_name)) + + plugins = payload.setdefault("plugins", []) + if not isinstance(plugins, list): + raise SystemExit(f"{marketplace_path} field 'plugins' must be an array.") + + entry = build_marketplace_entry() + action = "Added" + for index, current in enumerate(plugins): + if isinstance(current, dict) and current.get("name") == PLUGIN_DIR_NAME: + plugins[index] = entry + action = "Updated" + break + else: + plugins.append(entry) + + marketplace_path.write_text( + json.dumps(payload, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + return action, marketplace_name + + +def run_codex_plugin_add(marketplace_name: str) -> None: + if shutil.which("codex") is None: + raise SystemExit( + "codex CLI not found. Marketplace entry was written, but automatic plugin " + f"activation was skipped. Run later: codex plugin add {PLUGIN_DIR_NAME}@{marketplace_name}" + ) + + command = ["codex", "plugin", "add", f"{PLUGIN_DIR_NAME}@{marketplace_name}"] + result = subprocess.run(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if result.stdout.strip(): + print(result.stdout.strip()) + if result.stderr.strip(): + print(result.stderr.strip(), file=sys.stderr) + if result.returncode != 0: + raise SystemExit( + f"codex plugin add failed with exit code {result.returncode}. " + f"Run manually after fixing the issue: {' '.join(command)}" + ) + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Install Skill Routing Kit without manually creating directories." + ) + parser.add_argument( + "--target", + type=Path, + default=default_target(), + help="Plugin source directory. Default: ~/plugins/skill-routing-kit.", + ) + parser.add_argument( + "--marketplace", + type=Path, + default=default_marketplace_path(), + help="Codex marketplace.json path. Default: ~/.agents/plugins/marketplace.json.", + ) + parser.add_argument( + "--marketplace-name", + default=DEFAULT_MARKETPLACE_NAME, + help="Marketplace name to use when creating a new marketplace.json. Default: personal.", + ) + parser.add_argument( + "--skip-marketplace", + action="store_true", + help="Install files only; do not create or update marketplace.json.", + ) + parser.add_argument( + "--codex-add", + action="store_true", + help="After updating marketplace.json, run `codex plugin add skill-routing-kit@`.", + ) + parser.add_argument( + "--install-agents", + action="store_true", + help="Also install the routing guard snippet into an AGENTS.md file.", + ) + parser.add_argument( + "--agents", + type=Path, + default=default_agents_path(), + help="AGENTS.md path used with --install-agents. Default: $CODEX_HOME/AGENTS.md or ~/.codex/AGENTS.md.", + ) + return parser.parse_args(argv) + + +def main(argv: list[str]) -> int: + args = parse_args(argv) + source = repo_root() + target = args.target.expanduser().resolve() + + if source.resolve() == target: + print(f"Skill Routing Kit is already at: {target}") + else: + try: + target.relative_to(source) + except ValueError: + pass + else: + raise SystemExit("Install target cannot be inside the repository being installed.") + copy_plugin(source, target) + print(f"Installed Skill Routing Kit to: {target}") + + if args.install_agents: + snippet_path = target / "assets" / "agents-routing-snippet.md" + action = install_agents_snippet(snippet_path, args.agents.expanduser().resolve()) + print(f"{action} routing guard in: {args.agents.expanduser().resolve()}") + else: + print("Routing guard not installed. Add --install-agents to enable it automatically.") + + marketplace_name = args.marketplace_name + if args.skip_marketplace: + print("Marketplace entry not updated. Omit --skip-marketplace to make the plugin discoverable.") + else: + marketplace_path = args.marketplace.expanduser().resolve() + action, marketplace_name = update_marketplace_json(marketplace_path, marketplace_name) + print( + f"{action} marketplace entry in: {marketplace_path} " + f"({PLUGIN_DIR_NAME}@{marketplace_name})" + ) + + if args.codex_add: + if args.skip_marketplace: + raise SystemExit("--codex-add requires marketplace registration. Remove --skip-marketplace.") + run_codex_plugin_add(marketplace_name) + elif not args.skip_marketplace: + print(f"To activate through Codex CLI, run: codex plugin add {PLUGIN_DIR_NAME}@{marketplace_name}") + + print("Restart Codex or reload plugins if the new plugin does not appear immediately.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/plugins/juew/Skill-Routing-Kit/scripts/install.sh b/plugins/juew/Skill-Routing-Kit/scripts/install.sh new file mode 100755 index 00000000..9194e964 --- /dev/null +++ b/plugins/juew/Skill-Routing-Kit/scripts/install.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_URL="${SKILL_ROUTING_KIT_REPO:-https://github.com/juew/Skill-Routing-Kit.git}" +SCRIPT_SOURCE="${BASH_SOURCE[0]:-}" +if [[ -z "$SCRIPT_SOURCE" || "$SCRIPT_SOURCE" == -* ]]; then + SCRIPT_SOURCE="${0:-}" +fi + +SCRIPT_DIR="" +if [[ -n "$SCRIPT_SOURCE" && "$SCRIPT_SOURCE" != -* ]]; then + SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_SOURCE")" >/dev/null 2>&1 && pwd || true)" +fi + +cleanup_dir="" +if [[ -n "$SCRIPT_DIR" && -f "$SCRIPT_DIR/../.codex-plugin/plugin.json" ]]; then + ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +else + if ! command -v git >/dev/null 2>&1; then + echo "git is required for remote installation. Install git or clone the repository manually." >&2 + exit 1 + fi + cleanup_dir="$(mktemp -d)" + git clone --depth 1 "$REPO_URL" "$cleanup_dir/Skill-Routing-Kit" >/dev/null + ROOT_DIR="$cleanup_dir/Skill-Routing-Kit" +fi + +if ! command -v python3 >/dev/null 2>&1; then + echo "python3 is required to run the installer." >&2 + exit 1 +fi + +python3 "$ROOT_DIR/scripts/install.py" "$@" + +if [[ -n "$cleanup_dir" ]]; then + rm -rf "$cleanup_dir" +fi diff --git a/plugins/juew/Skill-Routing-Kit/scripts/route_request.py b/plugins/juew/Skill-Routing-Kit/scripts/route_request.py new file mode 100755 index 00000000..0c804e41 --- /dev/null +++ b/plugins/juew/Skill-Routing-Kit/scripts/route_request.py @@ -0,0 +1,461 @@ +#!/usr/bin/env python3 +"""Route a user request against a local Skill Routing Kit registry. + +This script is intentionally local, read-only, and conservative by default. +It does not call external connectors or inspect connector content. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_GENERATED = ROOT / "registry" / "capabilities.generated.json" +DEFAULT_CORE = ROOT / "registry" / "core-capabilities.json" +STALE_DAYS = 7 + + +TOKEN_RE = re.compile(r"[\w.+#/-]+", re.UNICODE) + + +def now_utc() -> datetime: + return datetime.now(timezone.utc) + + +def parse_time(value: str | None) -> datetime | None: + if not value: + return None + try: + if value.endswith("Z"): + value = value[:-1] + "+00:00" + return datetime.fromisoformat(value) + except ValueError: + return None + + +def tokenize(text: str) -> set[str]: + text = text.lower() + raw = set(TOKEN_RE.findall(text)) + expanded = set(raw) + aliases = { + "ppt": "presentation", + "pptx": "presentation", + "slides": "presentation", + "slide": "presentation", + "word": "document", + "docx": "document", + "excel": "spreadsheet", + "xlsx": "spreadsheet", + "csv": "spreadsheet", + "pdf": "pdf", + "bug": "debug", + "error": "debug", + "failing": "debug", + "test": "testing", + "tests": "testing", + "frontend": "frontend", + "web": "web_app", + "github": "github", + "gmail": "gmail", + "drive": "drive", + "canva": "canva", + "figma": "figma", + "slack": "slack", + "linear": "linear", + "agent": "orchestration", + "agents": "orchestration", + } + for token in list(raw): + if token in aliases: + expanded.add(aliases[token]) + return expanded + + +def infer_intents(request: str) -> dict[str, set[str]]: + text = request.lower() + artifacts: set[str] = set() + processes: set[str] = set() + sources: set[str] = set() + + if any(token in text for token in ["ppt", "pptx", "slide", "slides", "deck", "幻灯", "路演"]): + artifacts.add("presentation") + if any(token in text for token in ["pdf"]): + artifacts.add("pdf") + if any(token in text for token in ["docx", "word", "文档", "报告"]): + artifacts.add("document") + if any(token in text for token in ["xlsx", "excel", "csv", "spreadsheet", "表格"]): + artifacts.add("spreadsheet") + if any(token in text for token in ["web", "frontend", "react", "网页", "前端", "看板"]): + artifacts.add("web_app") + if any(token in text for token in ["image", "poster", "海报", "图片"]): + artifacts.add("image") + + if any(token in text for token in ["bug", "error", "failing", "报错", "失败", "修复"]): + processes.add("debugging") + if any(token in text for token in ["test", "测试"]): + processes.add("testing") + if any(token in text for token in ["review", "审核", "评审"]): + processes.add("review") + if any(token in text for token in ["plan", "计划", "方案"]): + processes.add("planning") + if any(token in text for token in ["agent", "agents", "subagent", "多 agent", "多agent"]): + processes.add("orchestration") + if any( + token in text + for token in [ + "没命中", + "未命中", + "没有命中", + "命中", + "触发", + "routing", + "router", + "route", + "registry", + "did not trigger", + "didn't trigger", + "not trigger", + "路由", + ] + ): + processes.add("routing") + + for source in ["github", "gmail", "drive", "notion", "slack", "figma", "linear", "canva"]: + if source in text: + sources.add(source) + + return {"artifacts": artifacts, "processes": processes, "sources": sources} + + +def load_registry(path: Path | None) -> tuple[dict[str, Any] | None, Path]: + if path: + registry_path = path + elif DEFAULT_GENERATED.exists(): + registry_path = DEFAULT_GENERATED + else: + registry_path = DEFAULT_CORE + + if not registry_path.exists(): + return None, registry_path + + with registry_path.open("r", encoding="utf-8") as f: + return json.load(f), registry_path + + +def registry_warnings(registry: dict[str, Any] | None, path: Path) -> list[str]: + warnings: list[str] = [] + if registry is None or not path.exists(): + return [f"Registry not found: {path}"] + + generated_at = parse_time(registry.get("generated_at")) + if generated_at: + age_days = (now_utc() - generated_at.astimezone(timezone.utc)).days + if age_days > STALE_DAYS: + warnings.append( + f"Registry is stale ({age_days} days old). Refresh with scripts/build_registry.py." + ) + else: + warnings.append("Registry has no parseable generated_at timestamp.") + + if registry.get("schema_version") != "1.0": + warnings.append( + f"Registry schema_version is {registry.get('schema_version')!r}; expected '1.0'." + ) + + missing_sources = [] + for card in registry.get("capabilities", []): + provenance = card.get("provenance", {}) + source_path = provenance.get("path") + source_type = provenance.get("source_type") + if source_path and source_type != "core_static": + expanded = Path(os.path.expanduser(source_path)) + if not expanded.exists(): + missing_sources.append(source_path) + if missing_sources: + warnings.append( + f"{len(missing_sources)} capability source path(s) are missing; run --check-registry --debug." + ) + + return warnings + + +def text_blob(card: dict[str, Any]) -> str: + values: list[str] = [] + for key in ("id", "name", "use_when"): + value = card.get(key) + if isinstance(value, str): + values.append(value) + for key in ("categories", "inputs", "outputs", "requires", "examples"): + value = card.get(key, []) + if isinstance(value, list): + values.extend(str(item) for item in value) + return " ".join(values).lower() + + +def score_card( + card: dict[str, Any], + request: str, + request_tokens: set[str], + intents: dict[str, set[str]], +) -> tuple[int, list[str], list[str]]: + score = 0 + reasons: list[str] = [] + do_not_use: list[str] = [] + blob = text_blob(card) + blob_tokens = tokenize(blob) + overlap = request_tokens & blob_tokens + if overlap: + score += min(len(overlap) * 4, 24) + reasons.append("matched terms: " + ", ".join(sorted(list(overlap))[:8])) + + card_id = str(card.get("id", "")).lower() + name = str(card.get("name", "")).lower() + if card_id and card_id in request.lower(): + score += 25 + reasons.append("explicit capability id mentioned") + if name and name in request.lower(): + score += 20 + reasons.append("explicit capability name mentioned") + + categories = set(str(c).lower() for c in card.get("categories", [])) + outputs = set(str(o).lower() for o in card.get("outputs", [])) + + for artifact in intents["artifacts"]: + if artifact in categories or artifact in outputs: + boost = 35 if artifact != "pdf" else 8 + score += boost + reasons.append(f"matches inferred artifact target: {artifact}") + + for process in intents["processes"]: + if process == "routing": + continue + if process in categories: + score += 45 + reasons.append(f"matches inferred process need: {process}") + + is_skill_router = card_id == "skill-router" or name == "skill router" or name == "skill-router" + if "routing" in intents["processes"] and is_skill_router: + score += 120 + reasons.append("routing diagnostic request prefers the skill-router capability") + elif "routing" in intents["processes"] and "routing" in categories: + score += 30 if card_id == "skill-routing-kit" else 10 + reasons.append("matches inferred process need: routing") + elif "routing" in intents["processes"] and "routing" not in categories: + score -= 30 + reasons.append("routing diagnostic request prefers the skill-router capability") + + if "debugging" in intents["processes"] and "debugging" not in categories and "process" not in categories: + score -= 18 + reasons.append("debugging request prefers a process/debugging primary skill") + + for source in intents["sources"]: + if source == card_id or source in categories: + score += 35 + reasons.append(f"matches explicit source/connector: {source}") + + if "presentation" in intents["artifacts"] and "pdf" in categories and "pdf" in request.lower(): + score -= 35 + do_not_use.append("PDF appears to be an input; presentation is the final artifact.") + + if "spreadsheet" in intents["artifacts"] and "pdf" in categories: + score -= 25 + do_not_use.append("PDF is not the final spreadsheet artifact.") + + if "external_connector" in categories or "connector" in categories or "requires_auth" in categories: + explicit = any(source in request.lower() for source in [card_id, name, "github", "gmail", "drive", "canva", "figma", "slack", "linear", "notion"]) + if explicit: + reasons.append("external connector explicitly referenced") + else: + score -= 12 + reasons.append("external connector not explicitly referenced") + + for avoid in card.get("avoid_when", []): + avoid_tokens = tokenize(str(avoid)) + if avoid_tokens and len(avoid_tokens & request_tokens) >= 2: + score -= 20 + do_not_use.append(str(avoid)) + + if "local" in categories and not any(x in request.lower() for x in ["github", "gmail", "drive", "canva", "figma", "slack", "linear", "notion"]): + score += 5 + reasons.append("local-first preference") + + return score, reasons, do_not_use + + +def route_request(request: str, registry: dict[str, Any], debug: bool) -> dict[str, Any]: + request_tokens = tokenize(request) + intents = infer_intents(request) + scored = [] + for card in registry.get("capabilities", []): + score, reasons, do_not_use = score_card(card, request, request_tokens, intents) + if score > 0 or debug: + scored.append( + { + "id": card.get("id"), + "name": card.get("name"), + "kind": card.get("kind"), + "categories": card.get("categories", []), + "score": score, + "reasons": reasons, + "do_not_use": do_not_use, + "requires": card.get("requires", []), + } + ) + + scored.sort(key=lambda item: item["score"], reverse=True) + positive = [item for item in scored if item["score"] > 0] + primary = positive[0] if positive else None + helpers = [ + item + for item in positive[1:] + if item["score"] >= 20 or item.get("do_not_use") + ][:3] if primary else [] + + needs_confirmation = [] + for item in ([primary] if primary else []) + helpers: + if not item: + continue + requires = set(str(r).lower() for r in item.get("requires", [])) + categories = set(str(c).lower() for c in item.get("categories", [])) + if "connector_auth" in requires or "requires_auth" in categories: + needs_confirmation.append(f"{item['name']} access/authentication") + + result: dict[str, Any] = { + "recommended": primary, + "helpers": helpers, + "needs_confirmation": needs_confirmation, + } + if debug: + result["candidates"] = scored[:12] + return result + + +def print_route(result: dict[str, Any], warnings: list[str], debug: bool) -> None: + if warnings: + print("Registry warnings:") + for warning in warnings: + print(f"- {warning}") + print() + + recommended = result.get("recommended") + print("Recommended skill/plugin:") + if recommended: + print(f"- {recommended['name']} ({recommended['id']})") + else: + print("- None with confidence. Use native judgment or ask for clarification.") + print() + + print("Helper skills/plugins:") + helpers = result.get("helpers") or [] + if helpers: + for helper in helpers: + print(f"- {helper['name']} ({helper['id']})") + else: + print("- None") + print() + + print("Why:") + if recommended and recommended.get("reasons"): + for reason in recommended["reasons"]: + print(f"- {reason}") + else: + print("- No strong registry match.") + print() + + print("Do not use:") + do_not = recommended.get("do_not_use", []) if recommended else [] + if do_not: + for item in do_not: + print(f"- {item}") + else: + print("- None from the selected card.") + print() + + print("Needs confirmation:") + confirmations = result.get("needs_confirmation", []) + if confirmations: + for item in confirmations: + print(f"- {item}") + else: + print("- None") + + if debug: + print() + print("Debug candidates:") + for candidate in result.get("candidates", []): + print(f"- {candidate['score']:>3} {candidate['id']}: {', '.join(candidate.get('reasons', []))}") + + +def check_registry(registry: dict[str, Any] | None, path: Path, debug: bool) -> int: + print(f"Registry: {path}") + print(f"Schema: {registry.get('schema_version') if registry else None}") + print(f"Generated at: {registry.get('generated_at') if registry else None}") + print(f"Capabilities: {len(registry.get('capabilities', [])) if registry else 0}") + warnings = registry_warnings(registry, path) + if warnings: + print("Warnings:") + for warning in warnings: + print(f"- {warning}") + else: + print("Warnings: none") + if debug and registry: + for card in registry.get("capabilities", []): + provenance = card.get("provenance", {}) + print(f"- {card.get('id')}: {provenance.get('source_type')} {provenance.get('path')}") + return 1 if warnings else 0 + + +def refresh_registry(output: Path | None = None) -> int: + script = ROOT / "scripts" / "build_registry.py" + if not script.exists(): + print(f"Cannot refresh; build script not found: {script}", file=sys.stderr) + return 2 + cmd = [sys.executable, str(script), "--yes"] + if output is not None: + cmd.extend(["--output", str(output)]) + return subprocess.call(cmd) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Route a request using a local Skill Routing Kit registry.") + parser.add_argument("request", nargs="?", help="User request to route.") + parser.add_argument("--registry", type=Path, help="Registry JSON path.") + parser.add_argument("--debug", action="store_true", help="Show candidates and detailed reasons.") + parser.add_argument("--check-registry", action="store_true", help="Check registry freshness and provenance.") + parser.add_argument("--refresh", action="store_true", help="Explicitly rebuild the registry before routing/checking.") + args = parser.parse_args(argv) + + if args.refresh: + refresh_output = args.registry if args.registry else DEFAULT_GENERATED + status = refresh_registry(refresh_output) + if status != 0: + return status + + registry, registry_path = load_registry(args.registry) + + if args.check_registry: + return check_registry(registry, registry_path, args.debug) + + if not args.request: + parser.error("request is required unless --check-registry is used") + + warnings = registry_warnings(registry, registry_path) + if registry is None: + print_route({"recommended": None, "helpers": [], "needs_confirmation": []}, warnings, args.debug) + return 1 + result = route_request(args.request, registry, args.debug) + print_route(result, warnings, args.debug) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins/juew/Skill-Routing-Kit/skills/skill-router/SKILL.md b/plugins/juew/Skill-Routing-Kit/skills/skill-router/SKILL.md new file mode 100644 index 00000000..e7997ec6 --- /dev/null +++ b/plugins/juew/Skill-Routing-Kit/skills/skill-router/SKILL.md @@ -0,0 +1,83 @@ +--- +name: skill-router +description: Use when the user explicitly asks to diagnose or improve Codex skill/plugin routing, determine which skill or plugin should handle a request, refresh or check a skill registry, investigate why a skill/plugin did not trigger, or review skill descriptions for trigger quality. Do not use for ordinary task execution when a concrete domain skill clearly applies, unless routing is ambiguous or the user asks for routing. +--- + +# Skill Router + +Use this skill as a diagnostic and routing-assistance layer. It does not replace Codex native skill discovery, Superpowers process skills, or subagent orchestration. It helps decide which capabilities are relevant, why they are relevant, and when not to use them. + +## Boundaries + +- Do not execute the user's domain task unless the user asks you to continue after routing. +- Do not assume external connectors are authenticated or can see data. +- Do not treat registry cards as the source of truth. Use them as hints with provenance. +- Do not perform dynamic scanning unless the user asks to refresh or check the registry. +- Prefer local files, local repositories, browser verification, and build/test commands unless the user explicitly names an external app or the source of truth clearly lives there. + +## When To Use + +Use this skill when the user asks things like: + +- "Which skill should handle this?" +- "Why did this skill not trigger?" +- "Refresh the skill registry." +- "Review this skill description for trigger quality." +- "List likely skills/plugins for this task." +- "Help me improve skill/plugin hit rate." + +## When Not To Use + +Do not use this skill for simple tasks with an obvious primary skill: + +- Editing a local PDF: use the PDF skill. +- Creating a slide deck: use the presentation skill. +- Debugging a failing test: use systematic debugging first. +- Implementing a frontend app: use frontend app building/testing skills. + +If the task is complex or ambiguous, perform a brief routing check and then continue with the selected primary skill. + +## Diagnostic Workflow + +1. Classify the user request: + - task type: explain, create, edit, debug, analyze, test, review, deploy + - source: local, web, GitHub, Gmail, Drive, Notion, Slack, Figma, Linear + - artifact: code, document, spreadsheet, presentation, PDF, image, webpage, report + - process need: planning, debugging, TDD, verification, review, orchestration +2. Read the registry if available. +3. Identify recommended skill/plugin candidates. +4. Apply local-first and connector-explicit rules. +5. Return concise output: + - Recommended skill + - Helper skills + - Why + - Do not use + - Needs confirmation +6. If debug mode is requested, include skipped candidates and scoring details. + +## Output Format + +```text +Recommended skill: +- ... + +Helper skills: +- ... + +Why: +- ... + +Do not use: +- ... + +Needs confirmation: +- ... +``` + +## Registry Maintenance + +Use `scripts/build_registry.py` only when the user explicitly asks to refresh the registry or when the registry is stale, missing, or inconsistent. + +Use `scripts/route_request.py --check-registry` to inspect registry health. + +See `references/capability-card-schema.md` for card fields and provenance requirements. diff --git a/plugins/juew/Skill-Routing-Kit/skills/skill-router/references/capability-card-schema.md b/plugins/juew/Skill-Routing-Kit/skills/skill-router/references/capability-card-schema.md new file mode 100644 index 00000000..23a869f9 --- /dev/null +++ b/plugins/juew/Skill-Routing-Kit/skills/skill-router/references/capability-card-schema.md @@ -0,0 +1,48 @@ +# Capability Card Schema + +Capability cards are routing hints. They are not the source of truth and must preserve provenance. + +Required fields: + +- `id`: stable identifier, lower-case kebab-case where possible. +- `name`: human-readable capability name. +- `kind`: `skill`, `plugin`, or `connector`. +- `categories`: list of routing categories. +- `use_when`: short text describing positive triggers. +- `avoid_when`: short list of negative triggers. +- `inputs`: likely input sources or file types. +- `outputs`: likely output artifacts. +- `requires`: required access or execution capability. +- `examples`: short positive example prompts. +- `provenance`: source metadata. +- `updated_at`: source file modified time if known. + +Recommended categories: + +- `process`: planning, debugging, TDD, verification, review, orchestration +- `source`: local, web, github, gmail, drive, notion, slack, figma, linear +- `artifact`: code, web_app, document, spreadsheet, presentation, pdf, image, dashboard +- `domain`: frontend, ios, data, finance, sales, design, devops, research +- `risk`: read_only, writes_files, external_connector, requires_auth, network, destructive_possible + +Example: + +```json +{ + "id": "pdf", + "name": "PDF", + "kind": "skill", + "categories": ["artifact", "local", "pdf", "read_only"], + "use_when": "Use for reading, creating, rendering, or reviewing PDF files.", + "avoid_when": ["Do not use when final output is a slide deck and PDF is only an input."], + "inputs": ["pdf"], + "outputs": ["text", "pdf", "rendered_pages"], + "requires": ["filesystem"], + "examples": ["Summarize this PDF.", "Render this PDF and check layout."], + "provenance": { + "source_type": "skill", + "path": "/absolute/path/to/SKILL.md" + }, + "updated_at": "2026-06-04T10:00:00+08:00" +} +```