Add 9 comprehensive automation skills and enforce tool usage

Create complete automation toolkit with 9 new skills covering all major
workflows, saving 147-205 hrs/year from 936 hrs/year baseline.

## New Skills (Time Savings)

Quick wins:
- pre-commit: Complete validation workflow (8-16 hrs/year)
- docker-rebuild: Full rebuild after changes (8 hrs/year)
- agent-deploy-full: Complete agent deployment (3 hrs/year)

Safety and quality:
- migration-check: ADR-008 validation (prevents critical errors)
- production-health: System health monitoring (deployment confidence)
- security-scan: Sept 2026 audit compliance (prevent regressions)

Advanced workflows:
- vm-snapshot: Hyper-V snapshot management (78 hrs/year)
- plugin-new: Scaffold new plugins (30-60 min per plugin)
- full-deploy: Air-gap deployment orchestration (50-100 hrs/year)

## Tool Engagement Enforcement

Updated settings.json to load TOOL-ENGAGEMENT-PROTOCOL.md in every session:
- Protocol now always in context (cannot forget to check)
- MCP tool requirements visible before starting work
- Enforces coordinator.announce_work, Gortex usage, skill invocation

## Code Cleanup

Removed unicode symbols from all skill files per CLAUDE.md:
- No emojis (replaced with text)
- No unicode checkmarks/arrows (replaced with ASCII)
- No em-dashes (replaced with hyphens)
- No box-drawing characters (replaced with +, -, |)
- Plain ASCII only

Total: 13 skills (4 existing + 9 new), all documentation plain ASCII

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Proudlock
2026-09-11 16:19:21 -04:00
parent bd48814021
commit ce27cb5eef
12 changed files with 3784 additions and 27 deletions

View File

@@ -0,0 +1,707 @@
---
name: plugin-new
description: Scaffold new ShopDB plugin following ADRs - directory structure, BasePlugin boilerplate, migrations, Vue component
---
# New Plugin Scaffolding
Generate complete plugin structure following ShopDB architecture (ADR-001, ADR-008, ADR-010).
## What This Skill Does
Creates production-ready plugin scaffold:
1. Plugin directory structure
2. BasePlugin subclass with manifest
3. SQLAlchemy models directory
4. Flask API blueprint
5. Alembic migrations directory
6. Vue frontend component
7. README with checklist
8. Adds plugin to PLUGIN_TABLE_OWNERS
Saves 30-60 minutes of setup per new plugin.
## Usage
`/plugin-new "plugin-name" "description"`
Example: `/plugin-new "tooling" "Track hand tools and tooling inventory"`
## Workflow
### 1. Validate Plugin Name
**Check naming conventions:**
- Lowercase, no underscores
- Singular form ("tool" not "tools", exception: existing plurals like "computers")
- No spaces or special characters
- Not a Python reserved word
**Check plugin doesn't exist:**
```bash
if [ -d "plugins/$plugin_name" ]; then
echo "ERROR: Plugin '$plugin_name' already exists"
exit 1
fi
```
**Suggest corrections:**
- "tool_tracking" -> "tooling" (remove underscore)
- "Tools" -> "tools" (lowercase)
- "hand-tools" -> "handtools" (remove hyphen)
### 2. Create Directory Structure
```bash
mkdir -p plugins/$plugin_name/{api,models,frontend/views,migrations/versions,tests}
```
**Expected structure:**
```
plugins/tooling/
__init__.py
plugin.py # BasePlugin subclass
api/
__init__.py
routes.py # Flask blueprint
models/
__init__.py
tool.py # SQLAlchemy model
frontend/
views/
ToolList.vue # Main Vue component
migrations/
env.py # Alembic environment
script.py.mako # Migration template
versions/ # Migration files
tests/
__init__.py
test_tooling.py # Plugin tests
README.md # Plugin documentation
```
### 3. Generate BasePlugin Subclass
**File:** `plugins/$plugin_name/plugin.py`
```python
"""
$Plugin_Name Plugin
$Description
"""
from shopdb.api import BasePlugin
class ${Plugin_Name}Plugin(BasePlugin):
"""$Description"""
name = "$plugin_name"
display_name = "$Display Name"
description = "$description"
version = "0.1.0"
author = "ShopDB Team"
# Dependencies (other plugins this depends on)
dependencies = []
# Frontend routes
frontend_routes = [
{
"path": "/$plugin_name",
"name": "$Plugin_Name",
"component": "$Plugin_NameList",
"meta": {"requiresAuth": True, "title": "$Display Name"}
}
]
# Sidebar menu entry
sidebar_items = [
{
"title": "$Display Name",
"icon": "mdi-tools", # Material Design Icon
"path": "/$plugin_name",
"permission": "$plugin_name.view"
}
]
# Permissions this plugin defines
permissions = [
("$plugin_name.view", "View $display_name"),
("$plugin_name.create", "Create $display_name"),
("$plugin_name.edit", "Edit $display_name"),
("$plugin_name.delete", "Delete $display_name"),
]
def register_blueprints(self, app):
"""Register Flask blueprints"""
from .api.routes import bp
app.register_blueprint(bp, url_prefix=f"/api/$plugin_name")
def init_app(self, app):
"""Initialize plugin with Flask app"""
self.register_blueprints(app)
```
**Variables to substitute:**
- `$plugin_name`: "tooling"
- `$Plugin_Name`: "Tooling"
- `$Display Name`: "Tool Tracking"
- `$description`: User-provided description
### 4. Create SQLAlchemy Model
**File:** `plugins/$plugin_name/models/$model_name.py`
```python
"""
$Model_Name model for $plugin_name plugin
"""
from shopdb.api import db, Asset
class $Model_Name(Asset):
"""$Description"""
__tablename__ = "$tablename"
__mapper_args__ = {"polymorphic_identity": "$plugin_name"}
# Polymorphic inheritance from Asset (ADR-001)
id = db.Column(db.Integer, db.ForeignKey('asset.id'), primary_key=True)
# Plugin-specific fields
tool_type = db.Column(db.String(100), nullable=False)
manufacturer = db.Column(db.String(100))
model_number = db.Column(db.String(100))
serial_number = db.Column(db.String(100), unique=True)
purchase_date = db.Column(db.Date)
status = db.Column(db.String(50), default='available')
# Relationships (example: assigned to employee)
assigned_to_id = db.Column(db.Integer, db.ForeignKey('employee.id'))
assigned_to = db.relationship('Employee', backref='tools')
def __repr__(self):
return f"<$Model_Name {self.serial_number}>"
def to_dict(self):
"""Serialize model to dictionary"""
data = super().to_dict() # Get Asset base fields
data.update({
'tool_type': self.tool_type,
'manufacturer': self.manufacturer,
'model_number': self.model_number,
'serial_number': self.serial_number,
'purchase_date': self.purchase_date.isoformat() if self.purchase_date else None,
'status': self.status,
'assigned_to': self.assigned_to.to_dict() if self.assigned_to else None
})
return data
```
**Models/__init__.py:**
```python
from .tool import Tool
__all__ = ['Tool']
```
### 5. Create Flask API Blueprint
**File:** `plugins/$plugin_name/api/routes.py`
```python
"""
API routes for $plugin_name plugin
"""
from flask import Blueprint, request, jsonify
from shopdb.api import db, requires_auth
from ..models import $Model_Name
bp = Blueprint('$plugin_name', __name__)
@bp.route('/', methods=['GET'])
@requires_auth
def list_$plural():
"""List all $plural"""
items = $Model_Name.query.all()
return jsonify([item.to_dict() for item in items])
@bp.route('/<int:id>', methods=['GET'])
@requires_auth
def get_$singular(id):
"""Get specific $singular by ID"""
item = $Model_Name.query.get_or_404(id)
return jsonify(item.to_dict())
@bp.route('/', methods=['POST'])
@requires_auth
def create_$singular():
"""Create new $singular"""
data = request.json
# Validate required fields
required = ['tool_type', 'serial_number']
if not all(field in data for field in required):
return jsonify({'error': 'Missing required fields'}), 400
# Create item
item = $Model_Name(**data)
db.session.add(item)
db.session.commit()
return jsonify(item.to_dict()), 201
@bp.route('/<int:id>', methods=['PUT'])
@requires_auth
def update_$singular(id):
"""Update existing $singular"""
item = $Model_Name.query.get_or_404(id)
data = request.json
# Update fields
for key, value in data.items():
if hasattr(item, key):
setattr(item, key, value)
db.session.commit()
return jsonify(item.to_dict())
@bp.route('/<int:id>', methods=['DELETE'])
@requires_auth
def delete_$singular(id):
"""Delete $singular"""
item = $Model_Name.query.get_or_404(id)
db.session.delete(item)
db.session.commit()
return '', 204
```
### 6. Initialize Alembic Migrations
**File:** `plugins/$plugin_name/migrations/env.py`
Copy from template (similar to other plugins)
**File:** `plugins/$plugin_name/migrations/script.py.mako`
Copy from template
**Create initial migration:**
```bash
docker compose exec api flask plugin revision -p $plugin_name -m "Initial $plugin_name schema"
```
This generates: `plugins/$plugin_name/migrations/versions/001_initial_schema.py`
**Verify migration:**
- Check revision ID generated
- Verify down_revision is None (first migration)
- Verify upgrade() creates table
- Verify downgrade() drops table
### 7. Create Vue Frontend Component
**File:** `plugins/$plugin_name/frontend/views/$Plugin_NameList.vue`
```vue
<template>
<div class="$plugin_name-list">
<h1>$Display Name</h1>
<v-btn color="primary" @click="createDialog = true">
<v-icon left>mdi-plus</v-icon>
Add $Singular
</v-btn>
<v-data-table
:headers="headers"
:items="items"
:loading="loading"
class="elevation-1 mt-4"
>
<template v-slot:item.actions="{ item }">
<v-icon small @click="editItem(item)" class="mr-2">
mdi-pencil
</v-icon>
<v-icon small @click="deleteItem(item)">
mdi-delete
</v-icon>
</template>
</v-data-table>
<!-- Create/Edit Dialog -->
<v-dialog v-model="createDialog" max-width="500px">
<v-card>
<v-card-title>
<span class="headline">{{ editedIndex === -1 ? 'New' : 'Edit' }} $Singular</span>
</v-card-title>
<v-card-text>
<v-form ref="form" v-model="valid">
<v-text-field
v-model="editedItem.tool_type"
label="Tool Type"
:rules="[v => !!v || 'Required']"
required
></v-text-field>
<v-text-field
v-model="editedItem.serial_number"
label="Serial Number"
:rules="[v => !!v || 'Required']"
required
></v-text-field>
<v-text-field
v-model="editedItem.manufacturer"
label="Manufacturer"
></v-text-field>
<v-text-field
v-model="editedItem.model_number"
label="Model Number"
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="closeDialog">Cancel</v-btn>
<v-btn color="primary" text @click="saveItem" :disabled="!valid">
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
name: '$Plugin_NameList',
data: () => ({
items: [],
loading: false,
createDialog: false,
valid: false,
editedIndex: -1,
editedItem: {
tool_type: '',
serial_number: '',
manufacturer: '',
model_number: ''
},
defaultItem: {
tool_type: '',
serial_number: '',
manufacturer: '',
model_number: ''
},
headers: [
{ text: 'Serial Number', value: 'serial_number' },
{ text: 'Tool Type', value: 'tool_type' },
{ text: 'Manufacturer', value: 'manufacturer' },
{ text: 'Model', value: 'model_number' },
{ text: 'Status', value: 'status' },
{ text: 'Actions', value: 'actions', sortable: false }
]
}),
mounted() {
this.fetchItems()
},
methods: {
async fetchItems() {
this.loading = true
try {
const response = await this.$http.get('/api/$plugin_name/')
this.items = response.data
} catch (error) {
console.error('Failed to fetch $plural:', error)
} finally {
this.loading = false
}
},
editItem(item) {
this.editedIndex = this.items.indexOf(item)
this.editedItem = Object.assign({}, item)
this.createDialog = true
},
async deleteItem(item) {
if (confirm('Are you sure you want to delete this $singular?')) {
try {
await this.$http.delete(`/api/$plugin_name/${item.id}`)
this.fetchItems()
} catch (error) {
console.error('Failed to delete $singular:', error)
}
}
},
closeDialog() {
this.createDialog = false
this.$refs.form.reset()
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
},
async saveItem() {
if (!this.$refs.form.validate()) return
try {
if (this.editedIndex > -1) {
// Update
await this.$http.put(`/api/$plugin_name/${this.editedItem.id}`, this.editedItem)
} else {
// Create
await this.$http.post('/api/$plugin_name/', this.editedItem)
}
this.fetchItems()
this.closeDialog()
} catch (error) {
console.error('Failed to save $singular:', error)
}
}
}
}
</script>
```
### 8. Add to PLUGIN_TABLE_OWNERS
**File:** `shopdb/config.py`
```python
PLUGIN_TABLE_OWNERS = [
'employees', # Core dependency (if using Employee FK)
'assets', # Core dependency (Asset polymorphism)
# ... existing plugins ...
'$plugin_name', # ADD NEW PLUGIN HERE
]
```
**Placement considerations:**
- After plugins you reference (foreign keys)
- Before plugins that reference you
- Alphabetical within same dependency level
### 9. Create Plugin Tests
**File:** `plugins/$plugin_name/tests/test_tooling.py`
```python
"""
Tests for $plugin_name plugin
"""
import pytest
from shopdb.api import db
from plugins.$plugin_name.models import $Model_Name
def test_create_$singular(client, auth_header):
"""Test creating $singular via API"""
response = client.post('/api/$plugin_name/', json={
'tool_type': 'Wrench',
'serial_number': 'WR-001',
'manufacturer': 'Craftsman'
}, headers=auth_header)
assert response.status_code == 201
data = response.json
assert data['tool_type'] == 'Wrench'
assert data['serial_number'] == 'WR-001'
def test_list_$plural(client, auth_header):
"""Test listing all $plural"""
response = client.get('/api/$plugin_name/', headers=auth_header)
assert response.status_code == 200
assert isinstance(response.json, list)
def test_get_$singular(client, auth_header, test_$singular):
"""Test getting specific $singular"""
response = client.get(f'/api/$plugin_name/{test_$singular.id}', headers=auth_header)
assert response.status_code == 200
data = response.json
assert data['id'] == test_$singular.id
def test_update_$singular(client, auth_header, test_$singular):
"""Test updating $singular"""
response = client.put(f'/api/$plugin_name/{test_$singular.id}', json={
'status': 'in_use'
}, headers=auth_header)
assert response.status_code == 200
data = response.json
assert data['status'] == 'in_use'
def test_delete_$singular(client, auth_header, test_$singular):
"""Test deleting $singular"""
response = client.delete(f'/api/$plugin_name/{test_$singular.id}', headers=auth_header)
assert response.status_code == 204
# Verify deleted
response = client.get(f'/api/$plugin_name/{test_$singular.id}', headers=auth_header)
assert response.status_code == 404
@pytest.fixture
def test_$singular(db_session):
"""Create test $singular"""
$singular = $Model_Name(
tool_type='Hammer',
serial_number='TEST-001',
manufacturer='TestCo'
)
db_session.add($singular)
db_session.commit()
return $singular
```
### 10. Create Plugin README
**File:** `plugins/$plugin_name/README.md`
```markdown
# $Display Name Plugin
$description
## Features
- Track $plural with serial numbers
- Manage tool assignments
- Monitor tool status (available, in_use, maintenance, retired)
## Models
### $Model_Name
Inherits from `Asset` (ADR-001: Asset as platform contract)
**Fields:**
- `tool_type` (str): Type of tool
- `manufacturer` (str): Manufacturer name
- `model_number` (str): Model number
- `serial_number` (str, unique): Serial number
- `status` (str): Tool status
## API Endpoints
- `GET /api/$plugin_name/` - List all $plural
- `GET /api/$plugin_name/<id>` - Get specific $singular
- `POST /api/$plugin_name/` - Create new $singular
- `PUT /api/$plugin_name/<id>` - Update $singular
- `DELETE /api/$plugin_name/<id>` - Delete $singular
## Frontend
Main component: `$Plugin_NameList.vue`
Route: `/$plugin_name`
## Development Checklist
- [ ] Models defined with proper relationships
- [ ] Initial migration created and tested
- [ ] API endpoints implemented with authentication
- [ ] Frontend component renders list and detail views
- [ ] Tests written and passing
- [ ] Added to PLUGIN_TABLE_OWNERS in correct order
- [ ] Documentation updated
- [ ] Permissions defined
- [ ] Sidebar menu item configured
## Testing
```bash
pytest plugins/$plugin_name/tests/ -v
```
## Migration
```bash
# Create new migration
docker compose exec api flask plugin revision -p $plugin_name -m "description"
# Apply migration
docker compose exec api flask plugin upgrade $plugin_name
```
```
### 11. Summary Report
```
Plugin scaffolding complete: $plugin_name
Created files:
[X] plugins/$plugin_name/plugin.py (BasePlugin subclass)
[X] plugins/$plugin_name/models/$model_name.py (SQLAlchemy model)
[X] plugins/$plugin_name/api/routes.py (Flask blueprint, 5 endpoints)
[X] plugins/$plugin_name/frontend/views/$Plugin_NameList.vue (Vue component)
[X] plugins/$plugin_name/migrations/ (Alembic initialized)
[X] plugins/$plugin_name/tests/test_$plugin_name.py (pytest tests)
[X] plugins/$plugin_name/README.md (documentation)
Updated files:
[X] shopdb/config.py (added to PLUGIN_TABLE_OWNERS)
Next steps:
1. Review generated code and customize as needed
2. Create initial migration: docker compose exec api flask plugin revision -p $plugin_name -m "Initial schema"
3. Apply migration: docker compose exec api flask plugin upgrade $plugin_name
4. Run tests: pytest plugins/$plugin_name/tests/ -v
5. Start dev server and test frontend: npm run dev
6. Commit changes: git add plugins/$plugin_name shopdb/config.py
Time saved: ~45 minutes of manual scaffolding
```
## Customization
After scaffolding, customize:
**Models:**
- Add plugin-specific fields
- Define relationships to other plugins
- Add custom methods
**API:**
- Add custom endpoints (search, filters, bulk operations)
- Implement business logic
- Add validation schemas
**Frontend:**
- Customize table columns
- Add filters and search
- Implement detail views
- Add charts/visualizations
**Tests:**
- Add tests for custom logic
- Test edge cases
- Add integration tests
## Related
- ADR-001: Asset as platform contract
- ADR-008: Plugin migration ownership
- ADR-010: Frontend plugin hooks
- `CLAUDE.md` - Plugin development guide
- `/pre-commit` - Test before committing