Initial commit
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: clawhub
|
||||
description: Search and install agent skills from ClawHub, the public skill registry. Use when needing new capabilities, searching for skills, installing skills, or updating installed skills.
|
||||
always: true
|
||||
homepage: https://clawhub.ai
|
||||
---
|
||||
|
||||
# ClawHub
|
||||
|
||||
Public skill registry for AI agents. Search by natural language (vector search).
|
||||
|
||||
## When to use
|
||||
|
||||
- You encounter an unfamiliar domain or technology and need guidance
|
||||
- The user asks to find, search, install, or update skills
|
||||
- You need specialized knowledge that existing skills don't cover
|
||||
- A task requires capabilities beyond your current skill set
|
||||
|
||||
## Search
|
||||
|
||||
```bash
|
||||
npx --yes clawhub@latest search "web scraping" --limit 5
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
Install skills into the current project's skill directory:
|
||||
|
||||
```bash
|
||||
npx --yes clawhub@latest install <slug> --workdir .opc/projects/<project_id>
|
||||
```
|
||||
|
||||
Replace `<slug>` with the skill name from search results and `<project_id>` with the current project ID. This places the skill into `.opc/projects/<project_id>/skills/<slug>/SKILL.md`.
|
||||
|
||||
After install, read the installed SKILL.md with `file_read` to apply its guidance immediately.
|
||||
|
||||
## Update
|
||||
|
||||
```bash
|
||||
npx --yes clawhub@latest update --all --workdir .opc/projects/<project_id>
|
||||
```
|
||||
|
||||
## List installed
|
||||
|
||||
```bash
|
||||
npx --yes clawhub@latest list --workdir .opc/projects/<project_id>
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Search for relevant skills with a descriptive query
|
||||
2. Review search results and pick the best match
|
||||
3. Install the skill into the project directory
|
||||
4. Read the installed SKILL.md with `file_read`
|
||||
5. Follow the skill's instructions for the current task
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires Node.js (`npx` comes with it).
|
||||
- No API key needed for search and install.
|
||||
- Login (`npx --yes clawhub@latest login`) is only required for publishing.
|
||||
- `--workdir` must point to the project directory so skills install correctly.
|
||||
@@ -0,0 +1,153 @@
|
||||
---
|
||||
name: collaboration-playbook
|
||||
description: Standing rules for how any role in an OpenOPC company coordinates with its peers — work-item discipline, messaging, meetings, and blocking collaboration. Loaded only in company_mode.
|
||||
always: true
|
||||
modes:
|
||||
- company_mode
|
||||
---
|
||||
|
||||
# Collaboration Playbook
|
||||
|
||||
You are one role inside an OpenOPC company. Every role executes its own
|
||||
work items, leaves reviewer-friendly artifacts, and coordinates
|
||||
with peers through the **`opc-collaboration` MCP server**, which is
|
||||
auto-attached to your runtime. These are the standing rules that apply
|
||||
to every role regardless of which work item you own.
|
||||
|
||||
You are never the whole project. Stay inside your work-item boundary.
|
||||
|
||||
## Work Item Discipline
|
||||
|
||||
- Own your work item. Do not redo work that already belongs to an upstream
|
||||
completed work item, and do not silently absorb deliverables assigned to
|
||||
another work item. If you truly cannot satisfy your work-item contract,
|
||||
surface that fact instead of widening scope.
|
||||
- Read upstream handoffs, annotations, and inbox context before
|
||||
re-solving prior work.
|
||||
- Your completion bar is higher than "it works on my turn." Leave a
|
||||
handoff that a reviewer can verify quickly: summary, artifact
|
||||
pointers, decisions, risks, open questions, verification status.
|
||||
- Prefer direct execution for the assigned slice once the approach is
|
||||
clear. Investigate first, then act.
|
||||
|
||||
## Messaging (the default path)
|
||||
|
||||
You communicate with peers via MCP tools provided by the
|
||||
`opc-collaboration` server. Your identity (`OPC_COMMS_FROM`) is
|
||||
already injected by OpenOPC for each turn — you never pass
|
||||
`from_agent` as a tool argument; the server reads it from the
|
||||
environment so you cannot accidentally (or deliberately) impersonate
|
||||
another role.
|
||||
|
||||
### The collaboration tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `send_dm(to_agent, subject, body, blocking=False)` | Send a direct message to another role. |
|
||||
| `read_inbox(limit=10, mark_read=True)` | Read your own unread messages. |
|
||||
| `reply_message(message_id, body, subject="")` | Reply to a specific message by id. |
|
||||
| `broadcast_issue(to_agents, subject, body)` | Send the same message to multiple roles. |
|
||||
| `find_and_ask_expert(skill_needed, question, blocking=False)` | Auto-route a question to whoever has the matching capability. |
|
||||
| `list_colleagues()` | Discover which roles are in the company. |
|
||||
| `start_meeting(topic, participants)` | Open a multi-party meeting room. |
|
||||
| `respond_meeting(meeting_id, content)` | Speak in an open meeting. |
|
||||
| `read_meeting(meeting_id)` | Read a meeting transcript. |
|
||||
|
||||
### When to send a message
|
||||
|
||||
Send ONLY when one of these is true:
|
||||
|
||||
- You need the recipient to confirm or change something specific
|
||||
before your deliverable can be finalized.
|
||||
- You discovered a conflict with a completed upstream work item that you
|
||||
cannot resolve from context.
|
||||
- You hold information another role provably needs and will not see
|
||||
otherwise (i.e. it is not already in a handoff or shared artifact).
|
||||
|
||||
### When NOT to send a message
|
||||
|
||||
- Do NOT send messages to acknowledge, summarize what you just did, or
|
||||
loop people in for visibility. Your handoff file IS the visibility
|
||||
mechanism.
|
||||
- Do NOT broadcast status updates. If a peer needs the latest status,
|
||||
they will read the artifacts you left.
|
||||
- Do NOT send a message that would duplicate information already in a
|
||||
handoff, annotation, or shared memory entry.
|
||||
|
||||
### When to reply to a message you received
|
||||
|
||||
Reply ONLY when the sender explicitly asked for your confirmation or
|
||||
a change AND the answer is non-trivial. If your reply would be "ack,
|
||||
no changes needed", stay silent — silence is the ack. This rule keeps
|
||||
the team from oscillating on trivial back-and-forth.
|
||||
|
||||
### Checking your inbox
|
||||
|
||||
The per-turn prompt's "Comms" section tells you whether you have
|
||||
unread messages. When it does, call `read_inbox` first thing to see
|
||||
what arrived. Otherwise you do not need to poll the inbox.
|
||||
|
||||
## Meetings (rare, for genuine cross-role decisions)
|
||||
|
||||
Meetings are for decisions or conflicts that truly need more than one
|
||||
role in the room at once. A normal work-item handoff is NOT a meeting.
|
||||
|
||||
To start a meeting, call `start_meeting(topic, participants)`. The
|
||||
tool returns a `meeting_id`. To speak, call
|
||||
`respond_meeting(meeting_id, content)`. To read what others have
|
||||
posted, call `read_meeting(meeting_id)`.
|
||||
|
||||
If you are already inside an open meeting room, the per-turn prompt
|
||||
will list it under the runtime state block. Wrap up the meeting with
|
||||
a concrete decision summary — an open meeting room with no decision
|
||||
is worse than no meeting at all.
|
||||
|
||||
## Blocking Collaboration (rare — the 10% case)
|
||||
|
||||
By default, collaboration is non-blocking: you call `send_dm` and
|
||||
continue with your own work. But there are situations where your
|
||||
work item genuinely cannot continue without a peer's reply — an urgent
|
||||
decision, an unresolvable conflict with an upstream work item, or a
|
||||
meeting you must wait on.
|
||||
|
||||
In those cases, pass `blocking=True` to `send_dm`:
|
||||
|
||||
```
|
||||
send_dm(to_agent="qa_engineer", subject="...", body="...", blocking=True)
|
||||
```
|
||||
|
||||
OpenOPC will detect the blocking marker, park your work item in
|
||||
AWAITING_PEER, run the recipient with your message available, then
|
||||
resume your work item once a reply has been written. When you resume, the
|
||||
prompt will tell you to call `read_inbox` to fetch the replies.
|
||||
|
||||
Do NOT use `blocking=True` for:
|
||||
|
||||
- routine acknowledgements,
|
||||
- visibility pings,
|
||||
- anything that could be resolved by reading existing handoffs.
|
||||
|
||||
Abusing blocking semantics defeats the convergence rule and stalls
|
||||
the whole company. If you are tempted to use it, first ask: "could I
|
||||
finish this turn without the reply, leave the question as an open
|
||||
issue in my handoff, and let the peer respond asynchronously?" If
|
||||
yes, do that instead.
|
||||
|
||||
## Shared Team Memory
|
||||
|
||||
The company has a shared team memory file. Its path is provided in
|
||||
the per-turn prompt under the runtime state block. Write durable
|
||||
shared state there — current conclusions, active risks, decisions,
|
||||
open questions, important constraints. Do NOT use it as a chat log
|
||||
or an activity feed.
|
||||
|
||||
## What This Playbook Is NOT
|
||||
|
||||
- It is not a per-work-item checklist. Your work-item-specific deliverables,
|
||||
inputs, acceptance criteria, and out-of-scope items come from the
|
||||
per-turn task brief, not from here.
|
||||
- It is not a tool reference for non-collaboration tools. The set of
|
||||
tools available to you this turn is declared in the tool surface
|
||||
artifact.
|
||||
- It is not an org chart. Who you may directly contact is listed in
|
||||
the per-turn topology section.
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: coding
|
||||
description: "Best practices for code development tasks"
|
||||
domain:
|
||||
- coding
|
||||
- frontend
|
||||
- backend
|
||||
- devops
|
||||
trigger: "When executing code-related tasks"
|
||||
always_on: false
|
||||
---
|
||||
|
||||
# Coding Skill
|
||||
|
||||
## Workflow
|
||||
1. **Understand** — Read existing code and understand the codebase structure before making changes
|
||||
2. **Plan** — Break down the implementation into clear steps
|
||||
3. **Implement** — Write clean, well-structured code following project conventions
|
||||
4. **Verify** — Run tests, check for errors, and validate the implementation
|
||||
5. **Document** — Add necessary comments for non-obvious logic only
|
||||
|
||||
## Best Practices
|
||||
- Always read files before editing them
|
||||
- Use `list_dir` to understand project structure
|
||||
- Use `file_search` to find relevant code
|
||||
- Prefer editing existing files over creating new ones
|
||||
- Follow existing code style and conventions
|
||||
- Include error handling in all code
|
||||
- Write tests when appropriate
|
||||
- Use version control (git) for all changes
|
||||
- Keep functions small and focused
|
||||
- Use meaningful variable and function names
|
||||
|
||||
## Code Quality
|
||||
- No unnecessary comments that just restate the code
|
||||
- Proper error handling and edge cases
|
||||
- Consistent formatting with the rest of the codebase
|
||||
- No hardcoded secrets or credentials
|
||||
- Follow language-specific best practices
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: deployment
|
||||
description: "Application deployment and DevOps practices"
|
||||
domain:
|
||||
- devops
|
||||
- deployment
|
||||
trigger: "When deploying applications or managing infrastructure"
|
||||
always_on: false
|
||||
---
|
||||
|
||||
# Deployment Skill
|
||||
|
||||
## Pre-deployment Checklist
|
||||
1. All tests passing
|
||||
2. No security vulnerabilities (check dependencies)
|
||||
3. Environment variables configured
|
||||
4. Database migrations ready
|
||||
5. Backup plan in place
|
||||
|
||||
## Common Deployment Targets
|
||||
- **GitHub Pages** — Static sites
|
||||
- **Vercel / Netlify** — Frontend apps
|
||||
- **Docker** — Containerized applications
|
||||
- **Cloud VMs** — Custom deployments
|
||||
|
||||
## Best Practices
|
||||
- Never deploy directly to production without testing
|
||||
- Use environment variables for all secrets
|
||||
- Set up CI/CD pipelines when possible
|
||||
- Keep deployment scripts version-controlled
|
||||
- Monitor after deployment
|
||||
- Have a rollback plan
|
||||
|
||||
## Security
|
||||
- No secrets in code or version control
|
||||
- Use HTTPS everywhere
|
||||
- Keep dependencies updated
|
||||
- Set appropriate file permissions
|
||||
- Use least-privilege access
|
||||
@@ -0,0 +1,335 @@
|
||||
---
|
||||
name: env_provisioning
|
||||
description: "Cross-platform environment probing, dependency installation, toolchain configuration, and manifest generation for any domain"
|
||||
domain:
|
||||
- environment
|
||||
- setup
|
||||
- provisioning
|
||||
- devops
|
||||
- toolchain
|
||||
trigger: "When a task requires installing tools, configuring environments, or setting up dependencies before execution"
|
||||
always_on: false
|
||||
---
|
||||
|
||||
# Environment Provisioning Skill
|
||||
|
||||
You are the Environment Engineer. Your job is to ensure the host environment has everything
|
||||
downstream stages need — regardless of domain: coding, video production, 3D game development,
|
||||
audio engineering, ML training, document generation, or anything else.
|
||||
|
||||
**You MUST support Linux, macOS, and Windows.** Detect the platform first, then use
|
||||
platform-appropriate commands throughout.
|
||||
|
||||
## Core Workflow
|
||||
|
||||
1. **Detect platform** — Determine OS, architecture, and available package managers
|
||||
2. **Probe** — Discover what is already installed. Never install blindly.
|
||||
3. **Plan** — Determine what is missing and how to install it.
|
||||
4. **Install** — Use the appropriate package manager or installer.
|
||||
5. **Configure** — Set environment variables, paths, configs.
|
||||
6. **Verify** — Run verification commands to confirm everything works.
|
||||
7. **Manifest** — Output a structured `environment_manifest` JSON with both Unix and Windows variants.
|
||||
|
||||
---
|
||||
|
||||
## Platform Detection
|
||||
|
||||
**ALWAYS start here.** The output determines all subsequent commands.
|
||||
|
||||
### Linux
|
||||
```bash
|
||||
uname -a
|
||||
cat /etc/os-release
|
||||
# Determine distro family: debian/ubuntu, rhel/fedora, arch, suse
|
||||
dpkg --version 2>/dev/null && echo "PACKAGE_MANAGER=apt"
|
||||
rpm --version 2>/dev/null && echo "PACKAGE_MANAGER=dnf"
|
||||
pacman --version 2>/dev/null && echo "PACKAGE_MANAGER=pacman"
|
||||
arch=$(uname -m) # x86_64, aarch64
|
||||
```
|
||||
|
||||
### macOS
|
||||
```bash
|
||||
sw_vers
|
||||
uname -m # x86_64 or arm64 (Apple Silicon)
|
||||
which brew && brew --version
|
||||
xcode-select -p 2>/dev/null # Xcode CLI tools installed?
|
||||
```
|
||||
|
||||
### Windows (PowerShell)
|
||||
```powershell
|
||||
[System.Environment]::OSVersion | Format-List
|
||||
(Get-CimInstance Win32_OperatingSystem).Caption
|
||||
$env:PROCESSOR_ARCHITECTURE # AMD64, ARM64
|
||||
# Check package managers
|
||||
Get-Command winget -ErrorAction SilentlyContinue | Select-Object Source
|
||||
Get-Command choco -ErrorAction SilentlyContinue | Select-Object Source
|
||||
Get-Command scoop -ErrorAction SilentlyContinue | Select-Object Source
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Probing Strategy (Cross-Platform)
|
||||
|
||||
### Linux / macOS (Bash)
|
||||
```bash
|
||||
# Package managers
|
||||
which apt-get brew dnf yum pacman conda pip pip3 uv npm cargo go rustup 2>/dev/null
|
||||
|
||||
# GPU
|
||||
nvidia-smi 2>/dev/null || rocm-smi 2>/dev/null
|
||||
python3 -c "import torch; print('CUDA:', torch.cuda.is_available())" 2>/dev/null
|
||||
|
||||
# Python
|
||||
which python3 python && python3 --version
|
||||
pip3 --version 2>/dev/null
|
||||
conda --version 2>/dev/null
|
||||
uv --version 2>/dev/null
|
||||
|
||||
# Common tools
|
||||
which ffmpeg blender docker node npm java gcc g++ cmake make git curl wget 2>/dev/null
|
||||
```
|
||||
|
||||
### Windows (PowerShell)
|
||||
```powershell
|
||||
# Package managers
|
||||
@('winget','choco','scoop','conda','pip','uv','npm','cargo','go') | ForEach-Object {
|
||||
$cmd = Get-Command $_ -ErrorAction SilentlyContinue
|
||||
if ($cmd) { "$_ : $($cmd.Source)" }
|
||||
}
|
||||
|
||||
# GPU
|
||||
try { nvidia-smi } catch {}
|
||||
python -c "import torch; print('CUDA:', torch.cuda.is_available())" 2>$null
|
||||
|
||||
# Python
|
||||
python --version 2>$null
|
||||
python3 --version 2>$null
|
||||
pip --version 2>$null
|
||||
conda --version 2>$null
|
||||
|
||||
# Common tools
|
||||
@('ffmpeg','blender','docker','node','npm','java','gcc','cmake','git','curl') | ForEach-Object {
|
||||
$cmd = Get-Command $_ -ErrorAction SilentlyContinue
|
||||
if ($cmd) { "$_ : $($cmd.Source)" }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installation By Platform
|
||||
|
||||
### System Packages
|
||||
|
||||
| Platform | Package Manager | Install Command | Update Command |
|
||||
|----------|----------------|-----------------|----------------|
|
||||
| Ubuntu/Debian | apt-get | `sudo apt-get install -y <pkg>` | `sudo apt-get update` |
|
||||
| Fedora/RHEL | dnf | `sudo dnf install -y <pkg>` | `sudo dnf check-update` |
|
||||
| Arch | pacman | `sudo pacman -S --noconfirm <pkg>` | `sudo pacman -Sy` |
|
||||
| macOS | brew | `brew install <pkg>` | `brew update` |
|
||||
| macOS (GUI apps) | brew cask | `brew install --cask <app>` | — |
|
||||
| Windows | winget | `winget install --accept-package-agreements -e --id <id>` | `winget upgrade` |
|
||||
| Windows | choco | `choco install <pkg> -y` | `choco upgrade all -y` |
|
||||
| Windows | scoop | `scoop install <pkg>` | `scoop update *` |
|
||||
|
||||
### Python Packages (All Platforms)
|
||||
```bash
|
||||
# Prefer uv if available (fastest)
|
||||
uv pip install <package>
|
||||
|
||||
# Standard pip
|
||||
pip install <package>
|
||||
pip3 install <package> # Linux/macOS
|
||||
|
||||
# Conda (for complex ML envs)
|
||||
conda create -n <name> python=3.x -y
|
||||
conda activate <name>
|
||||
conda install <package> -y
|
||||
```
|
||||
|
||||
### Node.js / JavaScript (All Platforms)
|
||||
```bash
|
||||
# Install Node.js
|
||||
# Linux: sudo apt-get install -y nodejs npm OR use nvm
|
||||
# macOS: brew install node
|
||||
# Windows: winget install OpenJS.NodeJS.LTS OR choco install nodejs-lts -y
|
||||
|
||||
npm install -g <package>
|
||||
npx <tool>
|
||||
```
|
||||
|
||||
### Rust / Go (All Platforms)
|
||||
```bash
|
||||
# Rust: install via rustup (cross-platform)
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y # Linux/macOS
|
||||
# Windows: winget install Rustlang.Rustup
|
||||
|
||||
cargo install <crate>
|
||||
go install <module>@latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Domain-Specific Guidance (Cross-Platform)
|
||||
|
||||
### Video Production
|
||||
|
||||
| Tool | Linux | macOS | Windows |
|
||||
|------|-------|-------|---------|
|
||||
| FFmpeg | `sudo apt-get install -y ffmpeg` | `brew install ffmpeg` | `winget install Gyan.FFmpeg` or `choco install ffmpeg -y` |
|
||||
| yt-dlp | `pip install yt-dlp` | `brew install yt-dlp` or `pip install yt-dlp` | `winget install yt-dlp.yt-dlp` or `pip install yt-dlp` |
|
||||
| Whisper | `pip install openai-whisper` | `pip install openai-whisper` | `pip install openai-whisper` |
|
||||
| ImageMagick | `sudo apt-get install -y imagemagick` | `brew install imagemagick` | `choco install imagemagick -y` |
|
||||
| HandBrake CLI | `sudo apt-get install -y handbrake-cli` | `brew install handbrake` | `choco install handbrake.install -y` |
|
||||
|
||||
**Verify (bash):** `ffmpeg -version && yt-dlp --version`
|
||||
**Verify (PowerShell):** `ffmpeg -version ; yt-dlp --version`
|
||||
|
||||
### 3D / Game Development
|
||||
|
||||
| Tool | Linux | macOS | Windows |
|
||||
|------|-------|-------|---------|
|
||||
| Blender | `sudo apt-get install -y blender` or snap | `brew install --cask blender` | `winget install BlenderFoundation.Blender` |
|
||||
| Unity Hub | Download AppImage or deb | `brew install --cask unity-hub` | `winget install Unity.UnityHub` |
|
||||
| Godot | `sudo apt-get install -y godot3` or flatpak | `brew install --cask godot` | `winget install GodotEngine.GodotEngine` or `choco install godot -y` |
|
||||
| Assimp | `sudo apt-get install -y libassimp-dev` | `brew install assimp` | `vcpkg install assimp` |
|
||||
| FBX SDK | Download from Autodesk | Download from Autodesk | Download from Autodesk |
|
||||
|
||||
**Blender scripting (all platforms):** `blender --background --python <script.py>`
|
||||
**Unity CLI (all platforms):** Check Unity Hub install path, then use `unity-editor` or `Unity.exe`
|
||||
|
||||
### Audio
|
||||
|
||||
| Tool | Linux | macOS | Windows |
|
||||
|------|-------|-------|---------|
|
||||
| SoX | `sudo apt-get install -y sox` | `brew install sox` | `choco install sox -y` |
|
||||
| PortAudio | `sudo apt-get install -y portaudio19-dev` | `brew install portaudio` | `vcpkg install portaudio` |
|
||||
| Audacity (CLI) | `sudo apt-get install -y audacity` | `brew install --cask audacity` | `winget install Audacity.Audacity` |
|
||||
|
||||
### ML / AI
|
||||
|
||||
| Tool | Linux | macOS | Windows |
|
||||
|------|-------|-------|---------|
|
||||
| PyTorch (CPU) | `pip install torch` | `pip install torch` | `pip install torch` |
|
||||
| PyTorch (CUDA) | `pip install torch --index-url https://download.pytorch.org/whl/cu121` | N/A (no CUDA on Mac) | Same as Linux |
|
||||
| PyTorch (MPS) | N/A | `pip install torch` (MPS auto) | N/A |
|
||||
| TensorFlow | `pip install tensorflow` | `pip install tensorflow` | `pip install tensorflow` |
|
||||
| CUDA Toolkit | `sudo apt-get install -y nvidia-cuda-toolkit` | N/A | Install from NVIDIA site or `choco install cuda -y` |
|
||||
| cuDNN | `conda install cudnn -y` | N/A | `conda install cudnn -y` |
|
||||
|
||||
**GPU verification:**
|
||||
- Linux: `nvidia-smi && python3 -c "import torch; print(torch.cuda.is_available())"`
|
||||
- macOS (Apple Silicon): `python3 -c "import torch; print(torch.backends.mps.is_available())"`
|
||||
- Windows: `nvidia-smi ; python -c "import torch; print(torch.cuda.is_available())"`
|
||||
|
||||
### Design / Documents
|
||||
|
||||
| Tool | Linux | macOS | Windows |
|
||||
|------|-------|-------|---------|
|
||||
| Inkscape | `sudo apt-get install -y inkscape` | `brew install --cask inkscape` | `winget install Inkscape.Inkscape` |
|
||||
| GIMP | `sudo apt-get install -y gimp` | `brew install --cask gimp` | `winget install GIMP.GIMP` |
|
||||
| LaTeX | `sudo apt-get install -y texlive-full` | `brew install --cask mactex` | `choco install miktex -y` |
|
||||
| Pandoc | `sudo apt-get install -y pandoc` | `brew install pandoc` | `choco install pandoc -y` |
|
||||
|
||||
### Web / Frontend
|
||||
|
||||
| Tool | Linux | macOS | Windows |
|
||||
|------|-------|-------|---------|
|
||||
| Chrome | `sudo apt-get install -y chromium-browser` | `brew install --cask google-chrome` | Pre-installed or `winget install Google.Chrome` |
|
||||
| Playwright | `pip install playwright && python -m playwright install chromium` | Same | Same |
|
||||
| Node.js | `sudo apt-get install -y nodejs npm` | `brew install node` | `winget install OpenJS.NodeJS.LTS` |
|
||||
|
||||
### DevOps / Infrastructure
|
||||
|
||||
| Tool | Linux | macOS | Windows |
|
||||
|------|-------|-------|---------|
|
||||
| Docker | `sudo apt-get install -y docker.io` | `brew install --cask docker` | `winget install Docker.DockerDesktop` |
|
||||
| kubectl | `sudo apt-get install -y kubectl` | `brew install kubectl` | `choco install kubernetes-cli -y` |
|
||||
| Terraform | `sudo apt-get install -y terraform` | `brew install terraform` | `choco install terraform -y` |
|
||||
|
||||
---
|
||||
|
||||
## Environment Manifest Format (Cross-Platform)
|
||||
|
||||
After all installation and verification, output this JSON structure as your final artifact.
|
||||
**Both `shell_prefix` and `shell_prefix_win` must be populated** when the environment needs activation:
|
||||
|
||||
```json
|
||||
{
|
||||
"environment_manifest": {
|
||||
"platform": "linux",
|
||||
"tools_installed": [
|
||||
{
|
||||
"name": "ffmpeg",
|
||||
"version": "6.1",
|
||||
"path": "/usr/bin/ffmpeg",
|
||||
"path_win": "C:\\ProgramData\\chocolatey\\bin\\ffmpeg.exe",
|
||||
"installed_by": "apt-get",
|
||||
"installed_by_win": "choco",
|
||||
"verified": true
|
||||
}
|
||||
],
|
||||
"env_vars": {
|
||||
"CUDA_HOME": "/usr/local/cuda"
|
||||
},
|
||||
"runtime_type": "conda",
|
||||
"runtime_path": "/data2/conda_envs/video_env",
|
||||
"activate_command": "conda activate video_env",
|
||||
"shell_prefix": "source /data2/conda_envs/video_env/bin/activate",
|
||||
"shell_prefix_win": "conda activate video_env",
|
||||
"gpu_available": true,
|
||||
"gpu_info": "NVIDIA RTX 4090, CUDA 12.1",
|
||||
"verification_checks": [
|
||||
{"command": "ffmpeg -version", "description": "FFmpeg available"},
|
||||
{"command": "python3 -c 'import torch; assert torch.cuda.is_available()'", "description": "PyTorch GPU"}
|
||||
],
|
||||
"verification_checks_win": [
|
||||
{"command": "ffmpeg -version", "description": "FFmpeg available"},
|
||||
{"command": "python -c \"import torch; assert torch.cuda.is_available()\"", "description": "PyTorch GPU"}
|
||||
],
|
||||
"notes": "Conda env 'video_env' created. FFmpeg installed via apt. PyTorch 2.3 with CUDA 12.1."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Field Reference
|
||||
|
||||
| Field | Purpose |
|
||||
|-------|---------|
|
||||
| `platform` | Detected OS: `linux`, `macos`, or `windows` |
|
||||
| `tools_installed` | Tools with version, path, and install method per platform |
|
||||
| `env_vars` | Environment variables for downstream (use forward slashes or platform-native) |
|
||||
| `runtime_type` | `native` / `conda` / `venv` / `docker` / `remote` |
|
||||
| `shell_prefix` | **Bash/sh** prefix auto-prepended to downstream shell commands |
|
||||
| `shell_prefix_win` | **PowerShell** prefix auto-prepended on Windows |
|
||||
| `verification_checks` | Bash commands to verify readiness |
|
||||
| `verification_checks_win` | PowerShell commands to verify readiness on Windows |
|
||||
| `gpu_available` | Whether GPU acceleration is available |
|
||||
| `gpu_info` | GPU model, CUDA/MPS/ROCm version |
|
||||
|
||||
---
|
||||
|
||||
## Platform-Specific Activation Commands
|
||||
|
||||
| Runtime | Linux/macOS (shell_prefix) | Windows (shell_prefix_win) |
|
||||
|---------|---------------------------|---------------------------|
|
||||
| conda | `source activate <env>` or `conda activate <env>` | `conda activate <env>` |
|
||||
| venv | `source /path/to/venv/bin/activate` | `/path/to/venv/Scripts/Activate.ps1` |
|
||||
| uv venv | `source .venv/bin/activate` | `.venv\Scripts\Activate.ps1` |
|
||||
| Docker | `docker run --rm -v $(pwd):/workspace <img>` | `docker run --rm -v ${PWD}:/workspace <img>` |
|
||||
| nvm | `source ~/.nvm/nvm.sh && nvm use <ver>` | `nvm use <ver>` (nvm-windows) |
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Platform-first**: Always detect OS before running any install command
|
||||
- **Idempotent**: Running the setup twice should not break anything
|
||||
- **Non-interactive**: All commands must use `-y` / `--yes` / `--noconfirm` / `--accept-package-agreements`
|
||||
- **Cross-platform manifest**: Always populate both `shell_prefix` + `shell_prefix_win` and both `verification_checks` + `verification_checks_win`
|
||||
- **Minimal**: Only install what the task actually needs
|
||||
- **Isolated**: Prefer virtual environments over global installs when possible
|
||||
- **Documented**: Every installed tool should appear in the manifest
|
||||
- **Verified**: Every critical tool should have a verification command
|
||||
- **Recoverable**: If an install fails, report clearly what failed and why
|
||||
- **Apple Silicon aware**: On macOS arm64, check if tools have native ARM builds
|
||||
- **Windows paths**: Use forward slashes in JSON, or escape backslashes (`\\`)
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: external_agents
|
||||
description: "External agent capability profiles and delegation guidance"
|
||||
domain:
|
||||
- general
|
||||
- coding
|
||||
- frontend
|
||||
- backend
|
||||
- devops
|
||||
- writing
|
||||
- documentation
|
||||
- automation
|
||||
always_on: true
|
||||
trigger: "When deciding whether to use the native agent or an external CLI agent"
|
||||
---
|
||||
|
||||
# External Agent Selection Skill
|
||||
|
||||
## Goal
|
||||
Choose between the OPC native agent and available external agents based on the actual task,
|
||||
the configured agent profile, and expected execution style. Do not rely on fixed domain
|
||||
rules alone.
|
||||
|
||||
## Native Agent
|
||||
- Strong default choice for lightweight reasoning, conversation, clarification, and tasks
|
||||
that benefit from tight integration with OPC memory, organization, and tool orchestration.
|
||||
- Prefer native when direct continuity inside OPC matters more than delegating to an
|
||||
external CLI agent.
|
||||
|
||||
## External CLI Agents
|
||||
- External agents such as Cursor, Claude Code, and Codex are usually strongest for complex,
|
||||
tool-heavy, multi-step execution where a dedicated CLI agent can work in an isolated
|
||||
workspace.
|
||||
- Their strengths are not limited to writing code. They can also handle bash-driven tasks
|
||||
such as generating or transforming Markdown, documents, PDFs, slide content, reports,
|
||||
scripts, and repository-wide edits.
|
||||
- When choosing among external agents, consider the configured model, whether the run should
|
||||
start a new session or continue an existing one, and any extra CLI arguments already set.
|
||||
|
||||
## Decision Heuristics
|
||||
- Prefer an external agent when the task requires sustained autonomous execution over files,
|
||||
shell commands, or project artifacts.
|
||||
- Prefer an external agent when the request is complex enough that a specialized coding or
|
||||
CLI workflow is likely to outperform the native agent.
|
||||
- Prefer native when the task is simple, mostly conversational, or better served by keeping
|
||||
reasoning and tool use inside OPC itself.
|
||||
- If multiple external agents are available, pick the one whose configured profile best
|
||||
matches the task instead of following a fixed ranking.
|
||||
|
||||
## Approval And Autonomy
|
||||
- Treat external agents as part of the same bounded-autonomy system as native tools.
|
||||
- Routine, low-risk actions can be auto-approved when they match learned user preferences.
|
||||
- Ambiguous, sensitive, or destructive operations should trigger escalation to the user.
|
||||
- Learn from explicit user approvals or rejections so future runs behave more like the
|
||||
user's trusted second self rather than a static automation pipeline.
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: web_search
|
||||
description: "Effective web searching and information gathering"
|
||||
domain:
|
||||
- research
|
||||
- general
|
||||
trigger: "When needing to find information from the web"
|
||||
always_on: false
|
||||
---
|
||||
|
||||
# Web Search Skill
|
||||
|
||||
## When to Search
|
||||
- Task requires up-to-date information (current events, latest versions, etc.)
|
||||
- Technical documentation or API references needed
|
||||
- Evaluating solutions, libraries, or tools
|
||||
- Fact-checking or verification
|
||||
|
||||
## Search Strategy
|
||||
1. Start with specific, targeted queries
|
||||
2. If results are poor, broaden the query or try different keywords
|
||||
3. Use `web_fetch` to read promising URLs for detailed information
|
||||
4. Cross-reference multiple sources for accuracy
|
||||
5. Summarize findings with source attribution
|
||||
|
||||
## Best Practices
|
||||
- Use specific technical terms in queries
|
||||
- Include version numbers when looking for docs
|
||||
- Prefer official documentation over blog posts
|
||||
- When evaluating tools, compare at least 2-3 options
|
||||
- Always note the source of information
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: writing
|
||||
description: "Document and content creation best practices"
|
||||
domain:
|
||||
- writing
|
||||
- documentation
|
||||
trigger: "When creating documents, reports, or written content"
|
||||
always_on: false
|
||||
---
|
||||
|
||||
# Writing Skill
|
||||
|
||||
## Document Types
|
||||
- Technical documentation
|
||||
- Reports and analyses
|
||||
- Emails and communications
|
||||
- Product requirements documents (PRD)
|
||||
- README files
|
||||
- Blog posts and articles
|
||||
|
||||
## Workflow
|
||||
1. Understand the audience and purpose
|
||||
2. Create an outline with clear structure
|
||||
3. Write the first draft
|
||||
4. Review for clarity, accuracy, and completeness
|
||||
5. Format appropriately for the medium
|
||||
|
||||
## Best Practices
|
||||
- Lead with the most important information
|
||||
- Use clear, concise language
|
||||
- Structure with headings and bullet points
|
||||
- Include examples where helpful
|
||||
- Maintain consistent tone and style
|
||||
- Proofread before delivery
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: cron
|
||||
description: Schedule reminders, recurring tasks, and one-time jobs using system crontab.
|
||||
---
|
||||
|
||||
# Cron
|
||||
|
||||
Schedule tasks using the system crontab via `shell_exec`.
|
||||
|
||||
## List current cron jobs
|
||||
|
||||
```bash
|
||||
crontab -l
|
||||
```
|
||||
|
||||
## Add a recurring job
|
||||
|
||||
```bash
|
||||
(crontab -l 2>/dev/null; echo "*/20 * * * * echo 'Time to take a break!' >> /tmp/opc-reminders.log") | crontab -
|
||||
```
|
||||
|
||||
## Common schedules
|
||||
|
||||
| Schedule | Cron expression |
|
||||
|----------|----------------|
|
||||
| Every 20 minutes | `*/20 * * * *` |
|
||||
| Every hour | `0 * * * *` |
|
||||
| Every day at 8am | `0 8 * * *` |
|
||||
| Weekdays at 5pm | `0 17 * * 1-5` |
|
||||
| Every Monday at 9am | `0 9 * * 1` |
|
||||
|
||||
## One-time scheduled task
|
||||
|
||||
Use `at` for one-time jobs:
|
||||
```bash
|
||||
echo "echo 'Meeting reminder' >> /tmp/opc-reminders.log" | at 14:30
|
||||
```
|
||||
|
||||
Or schedule with a specific date:
|
||||
```bash
|
||||
echo "echo 'Deadline reminder' >> /tmp/opc-reminders.log" | at 10:00 2026-03-20
|
||||
```
|
||||
|
||||
## Remove a job
|
||||
|
||||
Edit the crontab directly:
|
||||
```bash
|
||||
crontab -e
|
||||
```
|
||||
|
||||
Or filter out a specific job:
|
||||
```bash
|
||||
crontab -l | grep -v 'pattern-to-remove' | crontab -
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Use full paths in cron commands (cron has a minimal `$PATH`).
|
||||
- Redirect output to a log file or `/dev/null` to avoid mail noise.
|
||||
- Check `at` availability: `which at` (install with `apt install at` if missing).
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: github
|
||||
description: "Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries."
|
||||
metadata: {"requires":{"bins":["gh"]}}
|
||||
---
|
||||
|
||||
# GitHub Skill
|
||||
|
||||
Use the `gh` CLI to interact with GitHub. Always specify `--repo owner/repo` when not in a git directory, or use URLs directly.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
Check CI status on a PR:
|
||||
```bash
|
||||
gh pr checks 55 --repo owner/repo
|
||||
```
|
||||
|
||||
List recent workflow runs:
|
||||
```bash
|
||||
gh run list --repo owner/repo --limit 10
|
||||
```
|
||||
|
||||
View a run and see which steps failed:
|
||||
```bash
|
||||
gh run view <run-id> --repo owner/repo
|
||||
```
|
||||
|
||||
View logs for failed steps only:
|
||||
```bash
|
||||
gh run view <run-id> --repo owner/repo --log-failed
|
||||
```
|
||||
|
||||
## API for Advanced Queries
|
||||
|
||||
The `gh api` command is useful for accessing data not available through other subcommands.
|
||||
|
||||
Get PR with specific fields:
|
||||
```bash
|
||||
gh api repos/owner/repo/pulls/55 --jq '.title, .state, .user.login'
|
||||
```
|
||||
|
||||
## JSON Output
|
||||
|
||||
Most commands support `--json` for structured output. You can use `--jq` to filter:
|
||||
|
||||
```bash
|
||||
gh issue list --repo owner/repo --json number,title --jq '.[] | "\(.number): \(.title)"'
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: memory
|
||||
description: Manage durable global/project memory in `.opc/memory/global.md` and `.opc/memory/projects/<current_project_id>.md`.
|
||||
always: true
|
||||
---
|
||||
|
||||
# Memory
|
||||
|
||||
Use this skill only for durable Markdown memory:
|
||||
- Global memory: `.opc/memory/global.md`
|
||||
- Project memory: `.opc/memory/projects/<current_project_id>.md`
|
||||
- Prefer the absolute `OPC_GLOBAL_MEMORY_PATH` and `OPC_PROJECT_MEMORY_PATH` shown in runtime context or environment; do not create a separate `.opc/memory` under the workplace.
|
||||
|
||||
## What To Save
|
||||
|
||||
- Global: stable cross-project user preferences, communication defaults, standing constraints.
|
||||
- Project: current-project preferences, workspace overrides, repo paths, architecture constraints, coding conventions, delivery requirements.
|
||||
- Never save secrets, copied transcripts, temporary progress, speculative notes, or one-off task results.
|
||||
|
||||
## When to Update
|
||||
|
||||
- Update only during interaction with the user, and only when the user states or confirms something that should matter in later sessions.
|
||||
|
||||
## How to Update
|
||||
|
||||
Read before editing. Merge, deduplicate, replace stale items, and keep entries compact. Do not mix project IDs: write project memory only to the current project's file.
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
name: skill-creator
|
||||
description: Create or update skills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
|
||||
---
|
||||
|
||||
# Skill Creator
|
||||
|
||||
Guidance for creating effective skills.
|
||||
|
||||
## About Skills
|
||||
|
||||
Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools. Each skill is a directory containing a `SKILL.md` file and optional bundled resources.
|
||||
|
||||
### What Skills Provide
|
||||
|
||||
1. Specialized workflows - Multi-step procedures for specific domains
|
||||
2. Tool integrations - Instructions for working with specific file formats or APIs
|
||||
3. Domain expertise - Project-specific knowledge, schemas, business logic
|
||||
4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks
|
||||
|
||||
### Skill Locations in OPC
|
||||
|
||||
OPC uses a two-level skill system:
|
||||
|
||||
| Level | Path | Scope |
|
||||
|-------|------|-------|
|
||||
| System | `.opc/skills/<skill-name>/` | Shared across all projects |
|
||||
| Project | `.opc/projects/<project_id>/skills/<skill-name>/` | Specific to one project |
|
||||
|
||||
Project skills with the same name override system skills. When creating new skills from project experience, always use the **project** level.
|
||||
|
||||
### How Skills Are Created
|
||||
|
||||
Skills enter the system through two paths:
|
||||
|
||||
1. **Agent-driven** (this skill): You identify a pattern, read this guide, and create the skill manually using `init_skill.py` or `file_write`.
|
||||
2. **Auto-promoted**: The system automatically distills playbook skills from repeated project reflections (threshold: 2 reflections with recurring patterns). These are saved as `<employee>-<role>-<domain>-playbook` under the project skills directory.
|
||||
|
||||
Both paths produce the same `<skill-name>/SKILL.md` format. This guide covers the agent-driven path; auto-promoted skills follow the same naming and format conventions.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### Concise is Key
|
||||
|
||||
The context window is shared. Only add context the agent doesn't already have. Prefer concise examples over verbose explanations.
|
||||
|
||||
### Anatomy of a Skill
|
||||
|
||||
```
|
||||
skill-name/
|
||||
├── SKILL.md (required)
|
||||
│ ├── YAML frontmatter (name, description required)
|
||||
│ └── Markdown instructions
|
||||
└── Bundled Resources (optional)
|
||||
├── scripts/ - Executable code
|
||||
├── references/ - Documentation loaded as needed
|
||||
└── assets/ - Files used in output
|
||||
```
|
||||
|
||||
### Progressive Disclosure
|
||||
|
||||
1. **Metadata (name + description)** - Always in context (~100 words)
|
||||
2. **SKILL.md body** - When agent reads the skill (<5k words)
|
||||
3. **Bundled resources** - As needed (scripts can be executed without reading into context)
|
||||
|
||||
## Naming
|
||||
|
||||
All skills — whether agent-created or auto-promoted — must follow these conventions:
|
||||
|
||||
- Lowercase letters, digits, and hyphens only (e.g., `backend-api-playbook`)
|
||||
- Under 64 characters
|
||||
- Directory name must match the `name` field in frontmatter
|
||||
|
||||
## Skill Creation Process
|
||||
|
||||
### Step 1: Understand the skill with concrete examples
|
||||
|
||||
Clarify use cases: what triggers this skill, what does it produce?
|
||||
|
||||
### Step 2: Plan reusable contents
|
||||
|
||||
Identify what scripts, references, and assets would help.
|
||||
|
||||
### Step 3: Initialize the skill
|
||||
|
||||
For project-specific skills, create under `.opc/projects/<project_id>/skills/`:
|
||||
|
||||
```bash
|
||||
python3 {baseDir}/scripts/init_skill.py <skill-name> --path .opc/projects/<project_id>/skills
|
||||
```
|
||||
|
||||
Options: `--resources scripts,references,assets` and `--examples`.
|
||||
|
||||
### Step 4: Edit the skill
|
||||
|
||||
Write SKILL.md with:
|
||||
- **Frontmatter**: `name` (hyphen-case, matches directory) and `description` (what it does + when to use it — this is the primary trigger for skill discovery)
|
||||
- **Body**: Instructions, examples, references to bundled resources
|
||||
|
||||
### Step 5: Package (optional)
|
||||
|
||||
```bash
|
||||
python3 {baseDir}/scripts/package_skill.py <path/to/skill-folder>
|
||||
```
|
||||
|
||||
Validates the skill then creates a distributable `.skill` zip file.
|
||||
|
||||
### Step 6: Iterate
|
||||
|
||||
Test, notice struggles, improve SKILL.md and resources.
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Skill Initializer - Creates a new skill from template
|
||||
|
||||
Usage:
|
||||
init_skill.py <skill-name> --path <path> [--resources scripts,references,assets] [--examples]
|
||||
|
||||
Examples:
|
||||
init_skill.py my-new-skill --path skills/public
|
||||
init_skill.py my-new-skill --path skills/public --resources scripts,references
|
||||
init_skill.py my-api-helper --path skills/private --resources scripts --examples
|
||||
init_skill.py custom-skill --path /custom/location
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MAX_SKILL_NAME_LENGTH = 64
|
||||
ALLOWED_RESOURCES = {"scripts", "references", "assets"}
|
||||
|
||||
SKILL_TEMPLATE = """---
|
||||
name: {skill_name}
|
||||
description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]
|
||||
---
|
||||
|
||||
# {skill_title}
|
||||
|
||||
## Overview
|
||||
|
||||
[TODO: 1-2 sentences explaining what this skill enables]
|
||||
|
||||
## Structuring This Skill
|
||||
|
||||
[TODO: Choose the structure that best fits this skill's purpose. Common patterns:
|
||||
|
||||
**1. Workflow-Based** (best for sequential processes)
|
||||
- Works well when there are clear step-by-step procedures
|
||||
- Example: DOCX skill with "Workflow Decision Tree" -> "Reading" -> "Creating" -> "Editing"
|
||||
- Structure: ## Overview -> ## Workflow Decision Tree -> ## Step 1 -> ## Step 2...
|
||||
|
||||
**2. Task-Based** (best for tool collections)
|
||||
- Works well when the skill offers different operations/capabilities
|
||||
- Example: PDF skill with "Quick Start" -> "Merge PDFs" -> "Split PDFs" -> "Extract Text"
|
||||
- Structure: ## Overview -> ## Quick Start -> ## Task Category 1 -> ## Task Category 2...
|
||||
|
||||
**3. Reference/Guidelines** (best for standards or specifications)
|
||||
- Works well for brand guidelines, coding standards, or requirements
|
||||
- Example: Brand styling with "Brand Guidelines" -> "Colors" -> "Typography" -> "Features"
|
||||
- Structure: ## Overview -> ## Guidelines -> ## Specifications -> ## Usage...
|
||||
|
||||
**4. Capabilities-Based** (best for integrated systems)
|
||||
- Works well when the skill provides multiple interrelated features
|
||||
- Example: Product Management with "Core Capabilities" -> numbered capability list
|
||||
- Structure: ## Overview -> ## Core Capabilities -> ### 1. Feature -> ### 2. Feature...
|
||||
|
||||
Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).
|
||||
|
||||
Delete this entire "Structuring This Skill" section when done - it's just guidance.]
|
||||
|
||||
## [TODO: Replace with the first main section based on chosen structure]
|
||||
|
||||
[TODO: Add content here. See examples in existing skills:
|
||||
- Code samples for technical skills
|
||||
- Decision trees for complex workflows
|
||||
- Concrete examples with realistic user requests
|
||||
- References to scripts/templates/references as needed]
|
||||
|
||||
## Resources (optional)
|
||||
|
||||
Create only the resource directories this skill actually needs. Delete this section if no resources are required.
|
||||
|
||||
### scripts/
|
||||
Executable code (Python/Bash/etc.) that can be run directly to perform specific operations.
|
||||
|
||||
**Examples from other skills:**
|
||||
- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation
|
||||
- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing
|
||||
|
||||
**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations.
|
||||
|
||||
**Note:** Scripts may be executed without loading into context, but can still be read by Codex for patching or environment adjustments.
|
||||
|
||||
### references/
|
||||
Documentation and reference material intended to be loaded into context to inform Codex's process and thinking.
|
||||
|
||||
**Examples from other skills:**
|
||||
- Product management: `communication.md`, `context_building.md` - detailed workflow guides
|
||||
- BigQuery: API reference documentation and query examples
|
||||
- Finance: Schema documentation, company policies
|
||||
|
||||
**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Codex should reference while working.
|
||||
|
||||
### assets/
|
||||
Files not intended to be loaded into context, but rather used within the output Codex produces.
|
||||
|
||||
**Examples from other skills:**
|
||||
- Brand styling: PowerPoint template files (.pptx), logo files
|
||||
- Frontend builder: HTML/React boilerplate project directories
|
||||
- Typography: Font files (.ttf, .woff2)
|
||||
|
||||
**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.
|
||||
|
||||
---
|
||||
|
||||
**Not every skill requires all three types of resources.**
|
||||
"""
|
||||
|
||||
EXAMPLE_SCRIPT = '''#!/usr/bin/env python3
|
||||
"""
|
||||
Example helper script for {skill_name}
|
||||
|
||||
This is a placeholder script that can be executed directly.
|
||||
Replace with actual implementation or delete if not needed.
|
||||
|
||||
Example real scripts from other skills:
|
||||
- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields
|
||||
- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images
|
||||
"""
|
||||
|
||||
def main():
|
||||
print("This is an example script for {skill_name}")
|
||||
# TODO: Add actual script logic here
|
||||
# This could be data processing, file conversion, API calls, etc.
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
'''
|
||||
|
||||
EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title}
|
||||
|
||||
This is a placeholder for detailed reference documentation.
|
||||
Replace with actual reference content or delete if not needed.
|
||||
|
||||
Example real reference docs from other skills:
|
||||
- product-management/references/communication.md - Comprehensive guide for status updates
|
||||
- product-management/references/context_building.md - Deep-dive on gathering context
|
||||
- bigquery/references/ - API references and query examples
|
||||
|
||||
## When Reference Docs Are Useful
|
||||
|
||||
Reference docs are ideal for:
|
||||
- Comprehensive API documentation
|
||||
- Detailed workflow guides
|
||||
- Complex multi-step processes
|
||||
- Information too lengthy for main SKILL.md
|
||||
- Content that's only needed for specific use cases
|
||||
|
||||
## Structure Suggestions
|
||||
|
||||
### API Reference Example
|
||||
- Overview
|
||||
- Authentication
|
||||
- Endpoints with examples
|
||||
- Error codes
|
||||
- Rate limits
|
||||
|
||||
### Workflow Guide Example
|
||||
- Prerequisites
|
||||
- Step-by-step instructions
|
||||
- Common patterns
|
||||
- Troubleshooting
|
||||
- Best practices
|
||||
"""
|
||||
|
||||
EXAMPLE_ASSET = """# Example Asset File
|
||||
|
||||
This placeholder represents where asset files would be stored.
|
||||
Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.
|
||||
|
||||
Asset files are NOT intended to be loaded into context, but rather used within
|
||||
the output Codex produces.
|
||||
|
||||
Example asset files from other skills:
|
||||
- Brand guidelines: logo.png, slides_template.pptx
|
||||
- Frontend builder: hello-world/ directory with HTML/React boilerplate
|
||||
- Typography: custom-font.ttf, font-family.woff2
|
||||
- Data: sample_data.csv, test_dataset.json
|
||||
|
||||
## Common Asset Types
|
||||
|
||||
- Templates: .pptx, .docx, boilerplate directories
|
||||
- Images: .png, .jpg, .svg, .gif
|
||||
- Fonts: .ttf, .otf, .woff, .woff2
|
||||
- Boilerplate code: Project directories, starter files
|
||||
- Icons: .ico, .svg
|
||||
- Data files: .csv, .json, .xml, .yaml
|
||||
|
||||
Note: This is a text placeholder. Actual assets can be any file type.
|
||||
"""
|
||||
|
||||
|
||||
def normalize_skill_name(skill_name):
|
||||
"""Normalize a skill name to lowercase hyphen-case."""
|
||||
normalized = skill_name.strip().lower()
|
||||
normalized = re.sub(r"[^a-z0-9]+", "-", normalized)
|
||||
normalized = normalized.strip("-")
|
||||
normalized = re.sub(r"-{2,}", "-", normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def title_case_skill_name(skill_name):
|
||||
"""Convert hyphenated skill name to Title Case for display."""
|
||||
return " ".join(word.capitalize() for word in skill_name.split("-"))
|
||||
|
||||
|
||||
def parse_resources(raw_resources):
|
||||
if not raw_resources:
|
||||
return []
|
||||
resources = [item.strip() for item in raw_resources.split(",") if item.strip()]
|
||||
invalid = sorted({item for item in resources if item not in ALLOWED_RESOURCES})
|
||||
if invalid:
|
||||
allowed = ", ".join(sorted(ALLOWED_RESOURCES))
|
||||
print(f"[ERROR] Unknown resource type(s): {', '.join(invalid)}")
|
||||
print(f" Allowed: {allowed}")
|
||||
sys.exit(1)
|
||||
deduped = []
|
||||
seen = set()
|
||||
for resource in resources:
|
||||
if resource not in seen:
|
||||
deduped.append(resource)
|
||||
seen.add(resource)
|
||||
return deduped
|
||||
|
||||
|
||||
def create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples):
|
||||
for resource in resources:
|
||||
resource_dir = skill_dir / resource
|
||||
resource_dir.mkdir(exist_ok=True)
|
||||
if resource == "scripts":
|
||||
if include_examples:
|
||||
example_script = resource_dir / "example.py"
|
||||
example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))
|
||||
example_script.chmod(0o755)
|
||||
print("[OK] Created scripts/example.py")
|
||||
else:
|
||||
print("[OK] Created scripts/")
|
||||
elif resource == "references":
|
||||
if include_examples:
|
||||
example_reference = resource_dir / "api_reference.md"
|
||||
example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))
|
||||
print("[OK] Created references/api_reference.md")
|
||||
else:
|
||||
print("[OK] Created references/")
|
||||
elif resource == "assets":
|
||||
if include_examples:
|
||||
example_asset = resource_dir / "example_asset.txt"
|
||||
example_asset.write_text(EXAMPLE_ASSET)
|
||||
print("[OK] Created assets/example_asset.txt")
|
||||
else:
|
||||
print("[OK] Created assets/")
|
||||
|
||||
|
||||
def init_skill(skill_name, path, resources, include_examples):
|
||||
"""
|
||||
Initialize a new skill directory with template SKILL.md.
|
||||
|
||||
Args:
|
||||
skill_name: Name of the skill
|
||||
path: Path where the skill directory should be created
|
||||
resources: Resource directories to create
|
||||
include_examples: Whether to create example files in resource directories
|
||||
|
||||
Returns:
|
||||
Path to created skill directory, or None if error
|
||||
"""
|
||||
# Determine skill directory path
|
||||
skill_dir = Path(path).resolve() / skill_name
|
||||
|
||||
# Check if directory already exists
|
||||
if skill_dir.exists():
|
||||
print(f"[ERROR] Skill directory already exists: {skill_dir}")
|
||||
return None
|
||||
|
||||
# Create skill directory
|
||||
try:
|
||||
skill_dir.mkdir(parents=True, exist_ok=False)
|
||||
print(f"[OK] Created skill directory: {skill_dir}")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error creating directory: {e}")
|
||||
return None
|
||||
|
||||
# Create SKILL.md from template
|
||||
skill_title = title_case_skill_name(skill_name)
|
||||
skill_content = SKILL_TEMPLATE.format(skill_name=skill_name, skill_title=skill_title)
|
||||
|
||||
skill_md_path = skill_dir / "SKILL.md"
|
||||
try:
|
||||
skill_md_path.write_text(skill_content)
|
||||
print("[OK] Created SKILL.md")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error creating SKILL.md: {e}")
|
||||
return None
|
||||
|
||||
# Create resource directories if requested
|
||||
if resources:
|
||||
try:
|
||||
create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error creating resource directories: {e}")
|
||||
return None
|
||||
|
||||
# Print next steps
|
||||
print(f"\n[OK] Skill '{skill_name}' initialized successfully at {skill_dir}")
|
||||
print("\nNext steps:")
|
||||
print("1. Edit SKILL.md to complete the TODO items and update the description")
|
||||
if resources:
|
||||
if include_examples:
|
||||
print("2. Customize or delete the example files in scripts/, references/, and assets/")
|
||||
else:
|
||||
print("2. Add resources to scripts/, references/, and assets/ as needed")
|
||||
else:
|
||||
print("2. Create resource directories only if needed (scripts/, references/, assets/)")
|
||||
print("3. Run the validator when ready to check the skill structure")
|
||||
|
||||
return skill_dir
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create a new skill directory with a SKILL.md template.",
|
||||
)
|
||||
parser.add_argument("skill_name", help="Skill name (normalized to hyphen-case)")
|
||||
parser.add_argument("--path", required=True, help="Output directory for the skill")
|
||||
parser.add_argument(
|
||||
"--resources",
|
||||
default="",
|
||||
help="Comma-separated list: scripts,references,assets",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--examples",
|
||||
action="store_true",
|
||||
help="Create example files inside the selected resource directories",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
raw_skill_name = args.skill_name
|
||||
skill_name = normalize_skill_name(raw_skill_name)
|
||||
if not skill_name:
|
||||
print("[ERROR] Skill name must include at least one letter or digit.")
|
||||
sys.exit(1)
|
||||
if len(skill_name) > MAX_SKILL_NAME_LENGTH:
|
||||
print(
|
||||
f"[ERROR] Skill name '{skill_name}' is too long ({len(skill_name)} characters). "
|
||||
f"Maximum is {MAX_SKILL_NAME_LENGTH} characters."
|
||||
)
|
||||
sys.exit(1)
|
||||
if skill_name != raw_skill_name:
|
||||
print(f"Note: Normalized skill name from '{raw_skill_name}' to '{skill_name}'.")
|
||||
|
||||
resources = parse_resources(args.resources)
|
||||
if args.examples and not resources:
|
||||
print("[ERROR] --examples requires --resources to be set.")
|
||||
sys.exit(1)
|
||||
|
||||
path = args.path
|
||||
|
||||
print(f"Initializing skill: {skill_name}")
|
||||
print(f" Location: {path}")
|
||||
if resources:
|
||||
print(f" Resources: {', '.join(resources)}")
|
||||
if args.examples:
|
||||
print(" Examples: enabled")
|
||||
else:
|
||||
print(" Resources: none (create as needed)")
|
||||
print()
|
||||
|
||||
result = init_skill(skill_name, path, resources, args.examples)
|
||||
|
||||
if result:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Skill Packager - Creates a distributable .skill file of a skill folder
|
||||
|
||||
Usage:
|
||||
python package_skill.py <path/to/skill-folder> [output-directory]
|
||||
|
||||
Example:
|
||||
python package_skill.py skills/public/my-skill
|
||||
python package_skill.py skills/public/my-skill ./dist
|
||||
"""
|
||||
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from quick_validate import validate_skill
|
||||
|
||||
|
||||
def _is_within(path: Path, root: Path) -> bool:
|
||||
try:
|
||||
path.relative_to(root)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _cleanup_partial_archive(skill_filename: Path) -> None:
|
||||
try:
|
||||
if skill_filename.exists():
|
||||
skill_filename.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def package_skill(skill_path, output_dir=None):
|
||||
"""
|
||||
Package a skill folder into a .skill file.
|
||||
|
||||
Args:
|
||||
skill_path: Path to the skill folder
|
||||
output_dir: Optional output directory for the .skill file (defaults to current directory)
|
||||
|
||||
Returns:
|
||||
Path to the created .skill file, or None if error
|
||||
"""
|
||||
skill_path = Path(skill_path).resolve()
|
||||
|
||||
# Validate skill folder exists
|
||||
if not skill_path.exists():
|
||||
print(f"[ERROR] Skill folder not found: {skill_path}")
|
||||
return None
|
||||
|
||||
if not skill_path.is_dir():
|
||||
print(f"[ERROR] Path is not a directory: {skill_path}")
|
||||
return None
|
||||
|
||||
# Validate SKILL.md exists
|
||||
skill_md = skill_path / "SKILL.md"
|
||||
if not skill_md.exists():
|
||||
print(f"[ERROR] SKILL.md not found in {skill_path}")
|
||||
return None
|
||||
|
||||
# Run validation before packaging
|
||||
print("Validating skill...")
|
||||
valid, message = validate_skill(skill_path)
|
||||
if not valid:
|
||||
print(f"[ERROR] Validation failed: {message}")
|
||||
print(" Please fix the validation errors before packaging.")
|
||||
return None
|
||||
print(f"[OK] {message}\n")
|
||||
|
||||
# Determine output location
|
||||
skill_name = skill_path.name
|
||||
if output_dir:
|
||||
output_path = Path(output_dir).resolve()
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
output_path = Path.cwd()
|
||||
|
||||
skill_filename = output_path / f"{skill_name}.skill"
|
||||
|
||||
EXCLUDED_DIRS = {".git", ".svn", ".hg", "__pycache__", "node_modules"}
|
||||
|
||||
files_to_package = []
|
||||
resolved_archive = skill_filename.resolve()
|
||||
|
||||
for file_path in skill_path.rglob("*"):
|
||||
# Fail closed on symlinks so the packaged contents are explicit and predictable.
|
||||
if file_path.is_symlink():
|
||||
print(f"[ERROR] Symlink not allowed in packaged skill: {file_path}")
|
||||
_cleanup_partial_archive(skill_filename)
|
||||
return None
|
||||
|
||||
rel_parts = file_path.relative_to(skill_path).parts
|
||||
if any(part in EXCLUDED_DIRS for part in rel_parts):
|
||||
continue
|
||||
|
||||
if file_path.is_file():
|
||||
resolved_file = file_path.resolve()
|
||||
if not _is_within(resolved_file, skill_path):
|
||||
print(f"[ERROR] File escapes skill root: {file_path}")
|
||||
_cleanup_partial_archive(skill_filename)
|
||||
return None
|
||||
# If output lives under skill_path, avoid writing archive into itself.
|
||||
if resolved_file == resolved_archive:
|
||||
print(f"[WARN] Skipping output archive: {file_path}")
|
||||
continue
|
||||
files_to_package.append(file_path)
|
||||
|
||||
# Create the .skill file (zip format)
|
||||
try:
|
||||
with zipfile.ZipFile(skill_filename, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||
for file_path in files_to_package:
|
||||
# Calculate the relative path within the zip.
|
||||
arcname = Path(skill_name) / file_path.relative_to(skill_path)
|
||||
zipf.write(file_path, arcname)
|
||||
print(f" Added: {arcname}")
|
||||
|
||||
print(f"\n[OK] Successfully packaged skill to: {skill_filename}")
|
||||
return skill_filename
|
||||
|
||||
except Exception as e:
|
||||
_cleanup_partial_archive(skill_filename)
|
||||
print(f"[ERROR] Error creating .skill file: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python package_skill.py <path/to/skill-folder> [output-directory]")
|
||||
print("\nExample:")
|
||||
print(" python package_skill.py skills/public/my-skill")
|
||||
print(" python package_skill.py skills/public/my-skill ./dist")
|
||||
sys.exit(1)
|
||||
|
||||
skill_path = sys.argv[1]
|
||||
output_dir = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
print(f"Packaging skill: {skill_path}")
|
||||
if output_dir:
|
||||
print(f" Output directory: {output_dir}")
|
||||
print()
|
||||
|
||||
result = package_skill(skill_path, output_dir)
|
||||
|
||||
if result:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Minimal validator for nanobot skill folders.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ModuleNotFoundError:
|
||||
yaml = None
|
||||
|
||||
MAX_SKILL_NAME_LENGTH = 64
|
||||
ALLOWED_FRONTMATTER_KEYS = {
|
||||
"name",
|
||||
"description",
|
||||
"metadata",
|
||||
"always",
|
||||
"license",
|
||||
"allowed-tools",
|
||||
"homepage",
|
||||
}
|
||||
ALLOWED_RESOURCE_DIRS = {"scripts", "references", "assets"}
|
||||
PLACEHOLDER_MARKERS = ("[todo", "todo:")
|
||||
|
||||
|
||||
def _extract_frontmatter(content: str) -> Optional[str]:
|
||||
lines = content.splitlines()
|
||||
if not lines or lines[0].strip() != "---":
|
||||
return None
|
||||
for i in range(1, len(lines)):
|
||||
if lines[i].strip() == "---":
|
||||
return "\n".join(lines[1:i])
|
||||
return None
|
||||
|
||||
|
||||
def _parse_simple_frontmatter(frontmatter_text: str) -> Optional[dict[str, str]]:
|
||||
"""Fallback parser for simple frontmatter when PyYAML is unavailable."""
|
||||
parsed: dict[str, str] = {}
|
||||
current_key: Optional[str] = None
|
||||
multiline_key: Optional[str] = None
|
||||
|
||||
for raw_line in frontmatter_text.splitlines():
|
||||
stripped = raw_line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
|
||||
is_indented = raw_line[:1].isspace()
|
||||
if is_indented:
|
||||
if current_key is None:
|
||||
return None
|
||||
current_value = parsed[current_key]
|
||||
parsed[current_key] = f"{current_value}\n{stripped}" if current_value else stripped
|
||||
continue
|
||||
|
||||
if ":" not in stripped:
|
||||
return None
|
||||
|
||||
key, value = stripped.split(":", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if not key:
|
||||
return None
|
||||
|
||||
if value in {"|", ">"}:
|
||||
parsed[key] = ""
|
||||
current_key = key
|
||||
multiline_key = key
|
||||
continue
|
||||
|
||||
if (value.startswith('"') and value.endswith('"')) or (
|
||||
value.startswith("'") and value.endswith("'")
|
||||
):
|
||||
value = value[1:-1]
|
||||
parsed[key] = value
|
||||
current_key = key
|
||||
multiline_key = None
|
||||
|
||||
if multiline_key is not None and multiline_key not in parsed:
|
||||
return None
|
||||
return parsed
|
||||
|
||||
|
||||
def _load_frontmatter(frontmatter_text: str) -> tuple[Optional[dict], Optional[str]]:
|
||||
if yaml is not None:
|
||||
try:
|
||||
frontmatter = yaml.safe_load(frontmatter_text)
|
||||
except yaml.YAMLError as exc:
|
||||
return None, f"Invalid YAML in frontmatter: {exc}"
|
||||
if not isinstance(frontmatter, dict):
|
||||
return None, "Frontmatter must be a YAML dictionary"
|
||||
return frontmatter, None
|
||||
|
||||
frontmatter = _parse_simple_frontmatter(frontmatter_text)
|
||||
if frontmatter is None:
|
||||
return None, "Invalid YAML in frontmatter: unsupported syntax without PyYAML installed"
|
||||
return frontmatter, None
|
||||
|
||||
|
||||
def _validate_skill_name(name: str, folder_name: str) -> Optional[str]:
|
||||
if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name):
|
||||
return (
|
||||
f"Name '{name}' should be hyphen-case "
|
||||
"(lowercase letters, digits, and single hyphens only)"
|
||||
)
|
||||
if len(name) > MAX_SKILL_NAME_LENGTH:
|
||||
return (
|
||||
f"Name is too long ({len(name)} characters). "
|
||||
f"Maximum is {MAX_SKILL_NAME_LENGTH} characters."
|
||||
)
|
||||
if name != folder_name:
|
||||
return f"Skill name '{name}' must match directory name '{folder_name}'"
|
||||
return None
|
||||
|
||||
|
||||
def _validate_description(description: str) -> Optional[str]:
|
||||
trimmed = description.strip()
|
||||
if not trimmed:
|
||||
return "Description cannot be empty"
|
||||
lowered = trimmed.lower()
|
||||
if any(marker in lowered for marker in PLACEHOLDER_MARKERS):
|
||||
return "Description still contains TODO placeholder text"
|
||||
if "<" in trimmed or ">" in trimmed:
|
||||
return "Description cannot contain angle brackets (< or >)"
|
||||
if len(trimmed) > 1024:
|
||||
return f"Description is too long ({len(trimmed)} characters). Maximum is 1024 characters."
|
||||
return None
|
||||
|
||||
|
||||
def validate_skill(skill_path):
|
||||
"""Validate a skill folder structure and required frontmatter."""
|
||||
skill_path = Path(skill_path).resolve()
|
||||
|
||||
if not skill_path.exists():
|
||||
return False, f"Skill folder not found: {skill_path}"
|
||||
if not skill_path.is_dir():
|
||||
return False, f"Path is not a directory: {skill_path}"
|
||||
|
||||
skill_md = skill_path / "SKILL.md"
|
||||
if not skill_md.exists():
|
||||
return False, "SKILL.md not found"
|
||||
|
||||
try:
|
||||
content = skill_md.read_text(encoding="utf-8")
|
||||
except OSError as exc:
|
||||
return False, f"Could not read SKILL.md: {exc}"
|
||||
|
||||
frontmatter_text = _extract_frontmatter(content)
|
||||
if frontmatter_text is None:
|
||||
return False, "Invalid frontmatter format"
|
||||
|
||||
frontmatter, error = _load_frontmatter(frontmatter_text)
|
||||
if error:
|
||||
return False, error
|
||||
|
||||
unexpected_keys = sorted(set(frontmatter.keys()) - ALLOWED_FRONTMATTER_KEYS)
|
||||
if unexpected_keys:
|
||||
allowed = ", ".join(sorted(ALLOWED_FRONTMATTER_KEYS))
|
||||
unexpected = ", ".join(unexpected_keys)
|
||||
return (
|
||||
False,
|
||||
f"Unexpected key(s) in SKILL.md frontmatter: {unexpected}. Allowed properties are: {allowed}",
|
||||
)
|
||||
|
||||
if "name" not in frontmatter:
|
||||
return False, "Missing 'name' in frontmatter"
|
||||
if "description" not in frontmatter:
|
||||
return False, "Missing 'description' in frontmatter"
|
||||
|
||||
name = frontmatter["name"]
|
||||
if not isinstance(name, str):
|
||||
return False, f"Name must be a string, got {type(name).__name__}"
|
||||
name_error = _validate_skill_name(name.strip(), skill_path.name)
|
||||
if name_error:
|
||||
return False, name_error
|
||||
|
||||
description = frontmatter["description"]
|
||||
if not isinstance(description, str):
|
||||
return False, f"Description must be a string, got {type(description).__name__}"
|
||||
description_error = _validate_description(description)
|
||||
if description_error:
|
||||
return False, description_error
|
||||
|
||||
always = frontmatter.get("always")
|
||||
if always is not None and not isinstance(always, bool):
|
||||
return False, f"'always' must be a boolean, got {type(always).__name__}"
|
||||
|
||||
for child in skill_path.iterdir():
|
||||
if child.name == "SKILL.md":
|
||||
continue
|
||||
if child.is_dir() and child.name in ALLOWED_RESOURCE_DIRS:
|
||||
continue
|
||||
if child.is_symlink():
|
||||
continue
|
||||
return (
|
||||
False,
|
||||
f"Unexpected file or directory in skill root: {child.name}. "
|
||||
"Only SKILL.md, scripts/, references/, and assets/ are allowed.",
|
||||
)
|
||||
|
||||
return True, "Skill is valid!"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python quick_validate.py <skill_directory>")
|
||||
sys.exit(1)
|
||||
|
||||
valid, message = validate_skill(sys.argv[1])
|
||||
print(message)
|
||||
sys.exit(0 if valid else 1)
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: skill-evolution
|
||||
description: Evolve new skills from project experience. Use when a repeating pattern, useful workflow, or hard-won knowledge should be captured as a reusable skill for the current project.
|
||||
---
|
||||
|
||||
# Skill Evolution
|
||||
|
||||
Distill project experience into reusable skills that live under the current project.
|
||||
|
||||
## Two evolution paths
|
||||
|
||||
1. **Automatic**: The system promotes playbook skills when an employee's pattern reaches 2 project reflections with recurring behaviors, checklists, or preferences. These are saved as `<employee>-<role>-<domain>-playbook` and require no manual action.
|
||||
2. **Agent-driven** (this skill): You proactively identify valuable patterns and create skills manually. Use this when domain knowledge, workflows, or tool sequences should be preserved before the auto-promotion threshold is reached, or when the knowledge is not tied to a specific employee pattern.
|
||||
|
||||
## When to evolve a skill (agent-driven)
|
||||
|
||||
- A task required non-obvious steps that will likely recur
|
||||
- You discovered a workflow that worked well and should be preserved
|
||||
- Domain-specific knowledge was gathered that future tasks will need
|
||||
- A sequence of tool calls forms a reliable pattern worth codifying
|
||||
|
||||
## Process
|
||||
|
||||
1. **Identify the pattern**: What knowledge or workflow is worth preserving?
|
||||
2. **Read the skill-creator skill**: `file_read` the `skill-creator/SKILL.md` for format and naming guidelines
|
||||
3. **Name the skill**: Lowercase letters, digits, and hyphens only, under 64 characters (e.g., `api-validation-workflow`)
|
||||
4. **Create the skill directory**: Under `.opc/projects/<project_id>/skills/<skill-name>/`
|
||||
5. **Write SKILL.md** with proper frontmatter (`name`, `description`) and concise instructions
|
||||
6. **Add scripts/references/assets** if the skill benefits from bundled resources
|
||||
|
||||
## Skill location
|
||||
|
||||
Evolved skills belong to the project that produced them:
|
||||
|
||||
```
|
||||
.opc/projects/<project_id>/skills/<skill-name>/SKILL.md
|
||||
```
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Keep it concise: only include what the agent doesn't already know
|
||||
- Use concrete examples over abstract explanations
|
||||
- Include the minimal set of steps needed to reproduce the workflow
|
||||
- Add `scripts/` for deterministic operations that get rewritten repeatedly
|
||||
- Add `references/` for domain docs the agent should consult
|
||||
- Follow the same naming conventions as auto-promoted skills (hyphen-case)
|
||||
|
||||
## Cross-project reuse
|
||||
|
||||
Other projects can reference this project's skills via `file_read` when the secretary recommends them. Do not duplicate skills across projects; read from the source project instead.
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
name: tmux
|
||||
description: Remote-control tmux sessions for interactive CLIs by sending keystrokes and scraping pane output.
|
||||
metadata: {"requires":{"bins":["tmux"]}}
|
||||
---
|
||||
|
||||
# tmux Skill
|
||||
|
||||
Use tmux only when you need an interactive TTY. Prefer `shell_exec` for long-running, non-interactive tasks.
|
||||
|
||||
## Quickstart (isolated socket)
|
||||
|
||||
```bash
|
||||
SOCKET_DIR="${OPC_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/opc-tmux-sockets}"
|
||||
mkdir -p "$SOCKET_DIR"
|
||||
SOCKET="$SOCKET_DIR/opc.sock"
|
||||
SESSION=opc-python
|
||||
|
||||
tmux -S "$SOCKET" new -d -s "$SESSION" -n shell
|
||||
tmux -S "$SOCKET" send-keys -t "$SESSION":0.0 -- 'PYTHON_BASIC_REPL=1 python3 -q' Enter
|
||||
tmux -S "$SOCKET" capture-pane -p -J -t "$SESSION":0.0 -S -200
|
||||
```
|
||||
|
||||
After starting a session, always print monitor commands:
|
||||
|
||||
```
|
||||
To monitor:
|
||||
tmux -S "$SOCKET" attach -t "$SESSION"
|
||||
tmux -S "$SOCKET" capture-pane -p -J -t "$SESSION":0.0 -S -200
|
||||
```
|
||||
|
||||
## Socket convention
|
||||
|
||||
- Use `OPC_TMUX_SOCKET_DIR` environment variable.
|
||||
- Default socket path: `"$OPC_TMUX_SOCKET_DIR/opc.sock"`.
|
||||
|
||||
## Targeting panes and naming
|
||||
|
||||
- Target format: `session:window.pane` (defaults to `:0.0`).
|
||||
- Keep names short; avoid spaces.
|
||||
- Inspect: `tmux -S "$SOCKET" list-sessions`, `tmux -S "$SOCKET" list-panes -a`.
|
||||
|
||||
## Finding sessions
|
||||
|
||||
- List sessions on your socket: `{baseDir}/scripts/find-sessions.sh -S "$SOCKET"`.
|
||||
- Scan all sockets: `{baseDir}/scripts/find-sessions.sh --all` (uses `OPC_TMUX_SOCKET_DIR`).
|
||||
|
||||
## Sending input safely
|
||||
|
||||
- Prefer literal sends: `tmux -S "$SOCKET" send-keys -t target -l -- "$cmd"`.
|
||||
- Control keys: `tmux -S "$SOCKET" send-keys -t target C-c`.
|
||||
|
||||
## Watching output
|
||||
|
||||
- Capture recent history: `tmux -S "$SOCKET" capture-pane -p -J -t target -S -200`.
|
||||
- Wait for prompts: `{baseDir}/scripts/wait-for-text.sh -t session:0.0 -p 'pattern'`.
|
||||
- Attaching is OK; detach with `Ctrl+b d`.
|
||||
|
||||
## Spawning processes
|
||||
|
||||
- For python REPLs, set `PYTHON_BASIC_REPL=1` (non-basic REPL breaks send-keys flows).
|
||||
|
||||
## Orchestrating Coding Agents
|
||||
|
||||
tmux excels at running multiple coding agents in parallel:
|
||||
|
||||
```bash
|
||||
SOCKET="${TMPDIR:-/tmp}/opc-army.sock"
|
||||
|
||||
for i in 1 2 3 4 5; do
|
||||
tmux -S "$SOCKET" new-session -d -s "agent-$i"
|
||||
done
|
||||
|
||||
tmux -S "$SOCKET" send-keys -t agent-1 "cd /tmp/project1 && codex --yolo 'Fix bug X'" Enter
|
||||
tmux -S "$SOCKET" send-keys -t agent-2 "cd /tmp/project2 && codex --yolo 'Fix bug Y'" Enter
|
||||
|
||||
for sess in agent-1 agent-2; do
|
||||
if tmux -S "$SOCKET" capture-pane -p -t "$sess" -S -3 | grep -q "❯"; then
|
||||
echo "$sess: DONE"
|
||||
else
|
||||
echo "$sess: Running..."
|
||||
fi
|
||||
done
|
||||
|
||||
tmux -S "$SOCKET" capture-pane -p -t agent-1 -S -500
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
- Kill a session: `tmux -S "$SOCKET" kill-session -t "$SESSION"`.
|
||||
- Kill all sessions on a socket: `tmux -S "$SOCKET" list-sessions -F '#{session_name}' | xargs -r -n1 tmux -S "$SOCKET" kill-session -t`.
|
||||
- Remove everything on the private socket: `tmux -S "$SOCKET" kill-server`.
|
||||
|
||||
## Helper: wait-for-text.sh
|
||||
|
||||
`{baseDir}/scripts/wait-for-text.sh` polls a pane for a regex (or fixed string) with a timeout.
|
||||
|
||||
```bash
|
||||
{baseDir}/scripts/wait-for-text.sh -t session:0.0 -p 'pattern' [-F] [-T 20] [-i 0.5] [-l 2000]
|
||||
```
|
||||
|
||||
- `-t`/`--target` pane target (required)
|
||||
- `-p`/`--pattern` regex to match (required); add `-F` for fixed string
|
||||
- `-T` timeout seconds (integer, default 15)
|
||||
- `-i` poll interval seconds (default 0.5)
|
||||
- `-l` history lines to search (integer, default 1000)
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: find-sessions.sh [-L socket-name|-S socket-path|-A] [-q pattern]
|
||||
|
||||
List tmux sessions on a socket (default tmux socket if none provided).
|
||||
|
||||
Options:
|
||||
-L, --socket tmux socket name (passed to tmux -L)
|
||||
-S, --socket-path tmux socket path (passed to tmux -S)
|
||||
-A, --all scan all sockets under NANOBOT_TMUX_SOCKET_DIR
|
||||
-q, --query case-insensitive substring to filter session names
|
||||
-h, --help show this help
|
||||
USAGE
|
||||
}
|
||||
|
||||
socket_name=""
|
||||
socket_path=""
|
||||
query=""
|
||||
scan_all=false
|
||||
socket_dir="${OPC_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/opc-tmux-sockets}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-L|--socket) socket_name="${2-}"; shift 2 ;;
|
||||
-S|--socket-path) socket_path="${2-}"; shift 2 ;;
|
||||
-A|--all) scan_all=true; shift ;;
|
||||
-q|--query) query="${2-}"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$scan_all" == true && ( -n "$socket_name" || -n "$socket_path" ) ]]; then
|
||||
echo "Cannot combine --all with -L or -S" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$socket_name" && -n "$socket_path" ]]; then
|
||||
echo "Use either -L or -S, not both" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v tmux >/dev/null 2>&1; then
|
||||
echo "tmux not found in PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
list_sessions() {
|
||||
local label="$1"; shift
|
||||
local tmux_cmd=(tmux "$@")
|
||||
|
||||
if ! sessions="$("${tmux_cmd[@]}" list-sessions -F '#{session_name}\t#{session_attached}\t#{session_created_string}' 2>/dev/null)"; then
|
||||
echo "No tmux server found on $label" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -n "$query" ]]; then
|
||||
sessions="$(printf '%s\n' "$sessions" | grep -i -- "$query" || true)"
|
||||
fi
|
||||
|
||||
if [[ -z "$sessions" ]]; then
|
||||
echo "No sessions found on $label"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Sessions on $label:"
|
||||
printf '%s\n' "$sessions" | while IFS=$'\t' read -r name attached created; do
|
||||
attached_label=$([[ "$attached" == "1" ]] && echo "attached" || echo "detached")
|
||||
printf ' - %s (%s, started %s)\n' "$name" "$attached_label" "$created"
|
||||
done
|
||||
}
|
||||
|
||||
if [[ "$scan_all" == true ]]; then
|
||||
if [[ ! -d "$socket_dir" ]]; then
|
||||
echo "Socket directory not found: $socket_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
shopt -s nullglob
|
||||
sockets=("$socket_dir"/*)
|
||||
shopt -u nullglob
|
||||
|
||||
if [[ "${#sockets[@]}" -eq 0 ]]; then
|
||||
echo "No sockets found under $socket_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit_code=0
|
||||
for sock in "${sockets[@]}"; do
|
||||
if [[ ! -S "$sock" ]]; then
|
||||
continue
|
||||
fi
|
||||
list_sessions "socket path '$sock'" -S "$sock" || exit_code=$?
|
||||
done
|
||||
exit "$exit_code"
|
||||
fi
|
||||
|
||||
tmux_cmd=(tmux)
|
||||
socket_label="default socket"
|
||||
|
||||
if [[ -n "$socket_name" ]]; then
|
||||
tmux_cmd+=(-L "$socket_name")
|
||||
socket_label="socket name '$socket_name'"
|
||||
elif [[ -n "$socket_path" ]]; then
|
||||
tmux_cmd+=(-S "$socket_path")
|
||||
socket_label="socket path '$socket_path'"
|
||||
fi
|
||||
|
||||
list_sessions "$socket_label" "${tmux_cmd[@]:1}"
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: wait-for-text.sh -t target -p pattern [options]
|
||||
|
||||
Poll a tmux pane for text and exit when found.
|
||||
|
||||
Options:
|
||||
-t, --target tmux target (session:window.pane), required
|
||||
-p, --pattern regex pattern to look for, required
|
||||
-F, --fixed treat pattern as a fixed string (grep -F)
|
||||
-T, --timeout seconds to wait (integer, default: 15)
|
||||
-i, --interval poll interval in seconds (default: 0.5)
|
||||
-l, --lines number of history lines to inspect (integer, default: 1000)
|
||||
-h, --help show this help
|
||||
USAGE
|
||||
}
|
||||
|
||||
target=""
|
||||
pattern=""
|
||||
grep_flag="-E"
|
||||
timeout=15
|
||||
interval=0.5
|
||||
lines=1000
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-t|--target) target="${2-}"; shift 2 ;;
|
||||
-p|--pattern) pattern="${2-}"; shift 2 ;;
|
||||
-F|--fixed) grep_flag="-F"; shift ;;
|
||||
-T|--timeout) timeout="${2-}"; shift 2 ;;
|
||||
-i|--interval) interval="${2-}"; shift 2 ;;
|
||||
-l|--lines) lines="${2-}"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$target" || -z "$pattern" ]]; then
|
||||
echo "target and pattern are required" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
|
||||
echo "timeout must be an integer number of seconds" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ "$lines" =~ ^[0-9]+$ ]]; then
|
||||
echo "lines must be an integer" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v tmux >/dev/null 2>&1; then
|
||||
echo "tmux not found in PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# End time in epoch seconds (integer, good enough for polling)
|
||||
start_epoch=$(date +%s)
|
||||
deadline=$((start_epoch + timeout))
|
||||
|
||||
while true; do
|
||||
# -J joins wrapped lines, -S uses negative index to read last N lines
|
||||
pane_text="$(tmux capture-pane -p -J -t "$target" -S "-${lines}" 2>/dev/null || true)"
|
||||
|
||||
if printf '%s\n' "$pane_text" | grep $grep_flag -- "$pattern" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
now=$(date +%s)
|
||||
if (( now >= deadline )); then
|
||||
echo "Timed out after ${timeout}s waiting for pattern: $pattern" >&2
|
||||
echo "Last ${lines} lines from $target:" >&2
|
||||
printf '%s\n' "$pane_text" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep "$interval"
|
||||
done
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: weather
|
||||
description: Get current weather and forecasts (no API key required).
|
||||
homepage: https://wttr.in/:help
|
||||
metadata: {"requires":{"bins":["curl"]}}
|
||||
---
|
||||
|
||||
# Weather
|
||||
|
||||
Two free services, no API keys needed.
|
||||
|
||||
## wttr.in (primary)
|
||||
|
||||
Quick one-liner:
|
||||
```bash
|
||||
curl -s "wttr.in/London?format=3"
|
||||
# Output: London: ⛅️ +8°C
|
||||
```
|
||||
|
||||
Compact format:
|
||||
```bash
|
||||
curl -s "wttr.in/London?format=%l:+%c+%t+%h+%w"
|
||||
# Output: London: ⛅️ +8°C 71% ↙5km/h
|
||||
```
|
||||
|
||||
Full forecast:
|
||||
```bash
|
||||
curl -s "wttr.in/London?T"
|
||||
```
|
||||
|
||||
Format codes: `%c` condition · `%t` temp · `%h` humidity · `%w` wind · `%l` location · `%m` moon
|
||||
|
||||
Tips:
|
||||
- URL-encode spaces: `wttr.in/New+York`
|
||||
- Airport codes: `wttr.in/JFK`
|
||||
- Units: `?m` (metric) `?u` (USCS)
|
||||
- Today only: `?1` · Current only: `?0`
|
||||
- PNG: `curl -s "wttr.in/Berlin.png" -o /tmp/weather.png`
|
||||
|
||||
## Open-Meteo (fallback, JSON)
|
||||
|
||||
Free, no key, good for programmatic use:
|
||||
```bash
|
||||
curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12¤t_weather=true"
|
||||
```
|
||||
|
||||
Find coordinates for a city, then query. Returns JSON with temp, windspeed, weathercode.
|
||||
|
||||
Docs: https://open-meteo.com/en/docs
|
||||
Reference in New Issue
Block a user