Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions agent-docs/mcp-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ Always define input schemas with validation — never accept raw unvalidated inp
from mcp.server import Server
from pydantic import BaseModel, Field


class QueryInput(BaseModel):
dataset: str = Field(description="Dataset identifier")
filters: dict[str, str] = Field(default_factory=dict)
limit: int = Field(default=100, le=10000, description="Max rows to return")


@server.tool("query_data", "Query a dataset with filters")
async def query_data(input: QueryInput):
# Input is already validated by Pydantic
Expand Down Expand Up @@ -79,12 +81,14 @@ headers = {"Authorization": f"Bearer {user_context.token}"}
# Validate and sanitize all inputs before use
import re


def validate_jira_key(key: str) -> str:
"""Only allow valid Jira key format."""
if not re.match(r'^[A-Z][A-Z0-9]+-\d+$', key):
if not re.match(r"^[A-Z][A-Z0-9]+-\d+$", key):
raise ValueError(f"Invalid Jira key format: {key}")
return key


# Never pass raw user input to shell commands
# BAD:
os.system(f"grep {user_input} data.json")
Expand All @@ -98,6 +102,7 @@ subprocess.run(["grep", user_input, "data.json"], capture_output=True)
# Require confirmation for destructive or sensitive actions
RISKY_OPERATIONS = {"delete", "bulk_update", "export_pii", "drop_table"}


@server.tool("delete_record")
async def delete_record(record_id: str):
# MCP framework handles approval — tool description should state:
Expand All @@ -110,6 +115,7 @@ async def delete_record(record_id: str):
```python
import logging


@server.tool("query_data")
async def query_data(input: QueryInput, context: RequestContext):
logging.info(
Expand All @@ -119,7 +125,7 @@ async def query_data(input: QueryInput, context: RequestContext):
"user": context.user_id,
"params": {"dataset": input.dataset, "limit": input.limit},
"timestamp": datetime.now().isoformat(),
}
},
)
# ... execute query
```
Expand Down
118 changes: 118 additions & 0 deletions skills/ci-guard/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
name: ci-guard
version: "1.0"
description: Use when starting work in a repository under repositories/ that may lack CI configuration. Detects missing CI workflows (GitHub Actions, GitLab CI, CircleCI) and alerts the user to add one. Skips repos marked as research-only.
---

# CI Guard

Every repository using this basecamp should have CI unless explicitly marked as research. This skill checks for CI configuration and alerts when it's missing.

## When to Activate

- At session start, after repository status is reported
- When a user clones or creates a new repo under `repositories/`
- When running `/verify` or `/quality-gate` in a repo without CI

## How to Check

Run the check script against any repo:

```bash
uv run skills/ci-guard/scripts/check-ci.py <repo-path>
```

The script checks for common CI indicators and the research exemption.

## CI Indicators (any one is sufficient)

| Provider | Path |
|----------|------|
| GitHub Actions | `.github/workflows/` (with at least one `.yml`/`.yaml`) |
| GitLab CI | `.gitlab-ci.yml` |
| CircleCI | `.circleci/config.yml` |
| Jenkins | `Jenkinsfile` |
| Travis | `.travis.yml` |
| Azure | `azure-pipelines.yml` |

## Research Exemption

A repo is exempt from the CI requirement if:

1. A `.research` file exists in the repo root, OR
2. `pyproject.toml` contains `purpose = "research"` in `[project.optional]` or as a comment marker

To mark a repo as research: `touch <repo>/.research`

## Alert Behavior

When CI is missing and the repo is not research:

1. **Warn clearly** at session start: "This repo has no CI configuration."
2. **Suggest a starter workflow** based on the detected stack (Python/Node/Rust/Go)
3. **Reference the basecamp's own CI** (`.github/workflows/ci.yml`) as a working template
4. **Do not block work** - this is an alert, not a gate

## Starter Templates

### Python (uv-based, matching basecamp conventions)

```yaml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: uv python install 3.12
- run: uv sync --group dev
- run: uv run ruff check .
- run: uv run ruff format --check .

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: uv python install 3.12
- run: uv sync --group dev
- run: uv run pytest -v
```

### Node.js

```yaml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run lint
- run: npm test
```

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Forgetting CI on quick prototype repos | Use `.research` file for true prototypes, add CI for everything else |
| Adding CI but not running tests | CI without tests is a false safety net - at minimum lint |
| Blocking the user from working | This is an alert, not a blocker - warn and continue |
75 changes: 75 additions & 0 deletions skills/ci-guard/scripts/check-ci.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# ///
"""Check if a repository has CI configuration.

Exit codes:
0 - CI found or repo is research-exempt
1 - No CI configuration found
"""

from __future__ import annotations

import sys
from pathlib import Path

CI_INDICATORS = [
(".github/workflows", True), # (path, is_directory)
(".gitlab-ci.yml", False),
(".circleci/config.yml", False),
("Jenkinsfile", False),
(".travis.yml", False),
("azure-pipelines.yml", False),
("bitbucket-pipelines.yml", False),
]


def has_ci(repo_root: Path) -> str | None:
"""Return the CI provider name if found, None otherwise."""
for indicator, is_dir in CI_INDICATORS:
path = repo_root / indicator
if is_dir:
if path.is_dir() and any(path.glob("*.y*ml")):
return indicator
elif path.is_file():
return indicator
return None


def is_research(repo_root: Path) -> bool:
"""Check if the repo is marked as research-only."""
if (repo_root / ".research").exists():
return True
toml = repo_root / "pyproject.toml"
if toml.is_file():
content = toml.read_text()
if 'purpose = "research"' in content or "purpose = 'research'" in content:
return True
return False


def main() -> None:
repo_path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
repo_path = repo_path.resolve()

if not repo_path.is_dir():
print(f"ERROR: {repo_path} is not a directory", file=sys.stderr)
sys.exit(2)

if is_research(repo_path):
print(f"SKIP: {repo_path.name} is marked as research — CI not required")
sys.exit(0)

ci = has_ci(repo_path)
if ci:
print(f"OK: CI found ({ci}) in {repo_path.name}")
sys.exit(0)

print(f"WARNING: No CI configuration found in {repo_path.name}")
print(" Add a CI workflow or mark as research: touch .research")
sys.exit(1)


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion skills/data-pipeline-patterns/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def main(argv=None):
"generated_at": datetime.now().isoformat(),
"items_processed": len(result),
},
"data": result
"data": result,
}
save_json_file(output, output_path)
```
Expand Down
5 changes: 4 additions & 1 deletion skills/python-conventions/skills/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,16 @@ LLM outputs often include markdown fences that break JSON parsing. Always clean
```python
import json


def clean_llm_response(text):
for prefix in ("```markdown", "```json", "```"):
if text.startswith(prefix):
text = text[len(prefix):].strip()
text = text[len(prefix) :].strip()
if text.endswith("```"):
text = text[:-3].strip()
return text


def parse_llm_json(text):
text = clean_llm_response(text)
try:
Expand Down Expand Up @@ -86,6 +88,7 @@ def parse_llm_json(text):
# DataFrame testing
import pandas.testing as tm


def test_feature_engineering():
input_df = pd.DataFrame({"price": [100, 200], "quantity": [2, 3]})
result = add_total_column(input_df)
Expand Down
93 changes: 93 additions & 0 deletions skills/semantic-versioning/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
name: semantic-versioning
version: "1.0"
description: Use when committing or pushing changes and the repository needs a version bump. Analyzes commits using conventional commit prefixes to determine whether the next release is a major, minor, or patch increment. Also use when the user asks about versioning, release planning, or changelog generation.
---

# Semantic Versioning

Determine the next version from commit history. Every pushed change gets classified; the highest-impact commit drives the bump.

## When to Activate

- Before or after pushing changes (suggest the version bump)
- When the user asks "what version should this be?"
- When preparing a release or tag
- When reviewing a set of commits for release notes

## Version Rules

Follows [Semantic Versioning 2.0.0](https://semver.org):

| Bump | Trigger | Examples |
|------|---------|----------|
| **Major** (X.0.0) | Breaking change | `feat!:`, `fix!:`, `BREAKING CHANGE:` in body/footer |
| **Minor** (x.Y.0) | New feature | `feat:`, `feature:` |
| **Patch** (x.y.Z) | Everything else | `fix:`, `chore:`, `docs:`, `style:`, `refactor:`, `test:`, `ci:`, `perf:` |

The highest bump wins: if any commit is `major`, the release is major — regardless of how many patches are in the batch.

## How to Check

Run the analysis script:

```bash
uv run skills/semantic-versioning/scripts/version-bump.py [repo-path]
```

Options:
- `--apply` — update the version file after confirmation
- `--tag` — also create a git tag

## Version File Detection

The script auto-detects where the version lives:

| File | Field |
|------|-------|
| `pyproject.toml` | `[project] version = "x.y.z"` |
| `package.json` | `"version": "x.y.z"` |
| `Cargo.toml` | `[package] version = "x.y.z"` |
| `VERSION` | Plain text file |

## Workflow

1. **Analyze** — scan commits since last tag, classify each
2. **Report** — show the bump type, commit breakdown, and suggested next version
3. **Confirm** — wait for user approval before making changes
4. **Apply** (if `--apply`) — update version file and optionally tag

Never auto-apply version changes without user confirmation.

## Commit Format Guide

For best results, follow conventional commits:

```
<type>[optional scope][!]: <description>

[optional body]

[optional footer(s)]
```

If commits don't follow conventional format, fall back to keyword analysis:
- Words like "add", "new", "feature" → minor
- Words like "fix", "bug", "patch", "correct" → patch
- Words like "breaking", "remove", "drop", "rename API" → major

## Pre-1.0 Semantics

For versions `0.y.z` (pre-stable):
- API is considered unstable
- Minor bumps may include breaking changes
- Use `0.y.z` until the project declares stability

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Bumping major for every `feat!` in pre-1.0 | Pre-1.0: breaking changes go in minor |
| Forgetting to tag after version bump | Always tag: `git tag v<version>` |
| Version in multiple files getting out of sync | Use the script to update all detected version files |
| Non-conventional commit messages | Fall back to keyword analysis, but encourage conventional commits |
Loading
Loading