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
15 changes: 10 additions & 5 deletions .github/workflows/_lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ permissions:
contents: read

env:
HATCH_VERSION: "1.14.0"
HATCH_VERSION: "1.16.5"
RUFF_OUTPUT_FORMAT: github

jobs:
build:
name: "make lint #${{ matrix.python-version }}"
name: "lint+typecheck #${{ matrix.python-version }}"
runs-on: ubuntu-latest
strategy:
matrix:
# Only lint on the min and max supported Python versions.
# Lint on the min and max supported Python versions. mypy can
# surface version-specific type errors (e.g. ``typing.Self`` in
# 3.11 vs 3.12+) so we cover both ends of the matrix.
python-version:
- "3.11"
- "3.14"
Expand All @@ -34,5 +36,8 @@ jobs:
- name: Install Hatch
run: pip install hatch==${{ env.HATCH_VERSION }}

- name: Run linting
run: hatch run lint
# `hatch run check` runs format-check + ruff lint + mypy in one
# command (defined in pyproject.toml). All three must pass for
# the job to succeed.
- name: Run format-check + ruff + mypy
run: hatch run check
52 changes: 52 additions & 0 deletions .github/workflows/_precommit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: pre-commit

on:
workflow_call:

permissions:
contents: read

jobs:
build:
name: pre-commit hooks
runs-on: ubuntu-latest
steps:
# actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
# gitleaks needs full history to scan every commit on the PR.
fetch-depth: 0

# actions/setup-python@v5
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"

- name: Install pre-commit
run: pip install pre-commit

# Cache the pre-commit hook environments keyed off the config file
# contents — re-uses across PRs so each run only pays for the new
# work.
# actions/cache@v4
- name: Cache pre-commit env
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}

# Run every hook in `.pre-commit-config.yaml` against the full
# working tree. Catches whitespace, EOL, large files, secrets,
# markdown lint, YAML format, doc8, codespell, ruff, and ruff
# format. mypy stays in `_lint.yml` so its dependency graph is
# consistent across the matrix.
- name: Run pre-commit
run: |
pre-commit run --all-files \
--show-diff-on-failure \
--hook-stage pre-commit
env:
# Skip mypy (already covered by _lint.yml across the Python
# matrix) and the manual-only poetry-check hook.
SKIP: "mypy,poetry-check"
2 changes: 1 addition & 1 deletion .github/workflows/_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ permissions:

env:
PYTHON_VERSION: "3.11"
HATCH_VERSION: "1.14.0"
HATCH_VERSION: "1.16.5"

jobs:
test:
Expand Down
28 changes: 18 additions & 10 deletions .github/workflows/_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@ on:
permissions:
contents: read

env:
HATCH_VERSION: "1.14.0"

jobs:
build:
name: "make test #${{ matrix.python-version }}"
name: "test #${{ matrix.python-version }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.11"
Expand All @@ -30,14 +28,24 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: pip

# Install locus with every optional extra so tests that import
# ``oci``, ``openai``, ``fastmcp``, ``mcp``, ``redis``, etc. don't
# fail with ModuleNotFoundError. We bypass hatch here because
# ``hatch run test`` uses the default env which only carries
# ``[dev]`` deps; spinning up ``hatch -e test`` per matrix slot
# would re-resolve every dep into a per-Python virtualenv and
# dominate the CI wall-clock.
- name: Install locus + all extras
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,all]"

- name: Install Hatch
run: pip install hatch==${{ env.HATCH_VERSION }}

- name: Run core tests
run: hatch run test
- name: Run unit tests
run: pytest tests/unit/

- name: Ensure the tests did not create any additional files
- name: Ensure tests did not create stray files
run: |
set -eu
STATUS="$(git status)"
Expand Down
81 changes: 46 additions & 35 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,9 @@ on:
pull_request:
workflow_dispatch:

# Minimal default permissions for the CI pipeline. Individual jobs can raise
# to the narrowest scope they actually need; reusable sub-workflows declare
# their own permissions blocks.
permissions:
contents: read

# If another push to the same PR or branch happens while this workflow is still running,
# cancel the earlier run in favor of the next run.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Expand All @@ -22,56 +17,72 @@ jobs:
lint:
name: Lint
uses: ./.github/workflows/_lint.yml
permissions:
contents: read

test:
name: Test
uses: ./.github/workflows/_test.yml
permissions:
contents: read

codespell:
name: Codespell
uses: ./.github/workflows/_codespell.yml
permissions:
contents: read
precommit:
name: Pre-commit
uses: ./.github/workflows/_precommit.yml

# Verify commits are signed off (OCA compliance)
# Verify commits are signed off (OCA / DCO compliance). Inline bash
# check — replaces ``tisonkun/actions-dco`` which was deprecated by
# GitHub for using Node 16 (caused startup_failure on every CI run).
dco:
name: DCO Check
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: read
steps:
# actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Verify every commit is signed off
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -eu
echo "Checking commits between $BASE_SHA and $HEAD_SHA"
missing=$(git log --no-merges --pretty='%H %s' "$BASE_SHA..$HEAD_SHA" \
| while read sha _subject; do
if ! git log -1 --pretty='%B' "$sha" \
| grep -qE '^Signed-off-by: .+ <.+@.+>$'; then
printf '%s\n' "$sha"
fi
done)
if [ -n "$missing" ]; then
echo "::error::The following commits are missing a Signed-off-by line:"
echo "$missing" | while read sha; do
echo " - $(git log -1 --pretty='%h %s' "$sha")"
done
echo
echo "Add a sign-off with: git commit -s --amend --no-edit"
echo "and then force-push the branch."
exit 1
fi
echo "All commits are signed off."

- name: Check DCO
# tisonkun/actions-dco@v1.1 — v1.2 was deleted upstream; pin to SHA
# to avoid silently using a tag that disappears or is rewritten.
uses: tisonkun/actions-dco@f1024cd563550b5632e754df11b7d30b73be54a5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

# Aggregate gate. Branch-protection should require this single status
# check rather than each individual one — that lets us add / remove
# jobs without retouching protection rules.
ci-success:
name: CI Success
needs: [lint, test, codespell]
needs: [lint, test, precommit, dco]
if: always()
runs-on: ubuntu-latest
permissions: {}
env:
JOBS_JSON: ${{ toJSON(needs) }}
RESULTS_JSON: ${{ toJSON(needs.*.result) }}
EXIT_CODE: ${{!contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && '0' || '1'}}
steps:
- name: CI Success
- name: Verify required jobs succeeded
env:
NEEDS_JSON: ${{ toJSON(needs) }}
run: |
echo "$JOBS_JSON"
echo "$RESULTS_JSON"
echo "Exiting with $EXIT_CODE"
exit "$EXIT_CODE"
set -eu
echo "$NEEDS_JSON"
if echo "$NEEDS_JSON" | grep -qE '"result":[[:space:]]*"(failure|cancelled)"'; then
echo "::error::At least one required job failed or was cancelled."
exit 1
fi
echo "All required jobs passed."
19 changes: 19 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,25 @@ repos:
# Skip if not using poetry
stages: [manual]

# ==========================================================================
# Spell-checking
# ==========================================================================
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
hooks:
- id: codespell
args:
# Project-specific terms + identifiers + intentional typo-squat
# test fixtures that codespell otherwise mis-flags:
# te — loop variable for ``tool_execution``
# whats — test data string ("whats" stripped from query)
# pres — variable shorthand for ``presence_penalty``
# reqeusts — intentional typo-squat fixture for OSV malware test
# unparseable — accepted English variant of ``unparsable``
# re-use — accepted hyphenation
- --ignore-words-list=oci,orcl,te,whats,re-use,pres,reqeusts,unparseable
- --skip=*.lock,*.json,*.yaml,*.yml,*.svg,*.gif,*.png,*.jpg,*.pdf

# ==========================================================================
# Documentation
# ==========================================================================
Expand Down
10 changes: 5 additions & 5 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
# Copy this to config.local.yaml and fill in your values

oci:
profile_name: "YOUR_PROFILE"
auth_type: "api_key" # or "security_token"
region: "us-phoenix-1"
profile_name: YOUR_PROFILE
auth_type: api_key # or "security_token"
region: us-phoenix-1
models:
gpt: "openai.gpt-4o"
cohere: "cohere.command-r-plus"
gpt: openai.gpt-4o
cohere: cohere.command-r-plus
5 changes: 5 additions & 0 deletions examples/skills/api-design/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,29 @@ metadata:
# REST API Design Best Practices

## URL Structure

- Use nouns, not verbs: `/users` not `/getUsers`
- Use plural: `/orders` not `/order`
- Nest for relationships: `/users/{id}/orders`
- Max 3 levels of nesting

## HTTP Methods

- GET: Read (idempotent, no body)
- POST: Create (returns 201 + Location header)
- PUT: Full update (idempotent)
- PATCH: Partial update
- DELETE: Remove (returns 204)

## Response Format

- Always return JSON with consistent structure
- Include `data`, `error`, `meta` top-level keys
- Paginate collections: `?page=1&limit=20`
- Return total count in meta for pagination

## Error Handling

- Use standard HTTP status codes
- 400: Bad request (validation failed)
- 401: Unauthorized (no/invalid auth)
Expand All @@ -38,6 +42,7 @@ metadata:
- 500: Server error (never expose internals)

## Versioning

- Use URL path versioning: `/v1/users`
- Never break existing clients
- Deprecate with headers before removing
7 changes: 7 additions & 0 deletions examples/skills/code-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,41 @@ metadata:
# Code Review Checklist

## 1. Security

- Check for hardcoded secrets (API keys, passwords, tokens)
- Validate all user inputs before use
- Check for SQL injection, XSS, command injection
- Ensure sensitive data is not logged

## 2. Error Handling

- All external calls wrapped in try/except
- Errors logged with context (not just swallowed)
- User-facing errors are safe (no stack traces leaked)

## 3. Code Quality

- Functions are under 50 lines
- No duplicated logic (DRY principle)
- Clear variable and function names
- Type hints on public functions

## 4. Testing

- New code has corresponding tests
- Edge cases covered (empty input, None, large data)
- Tests are independent (no shared mutable state)

## 5. Performance

- No N+1 queries
- Large collections use generators, not lists
- Expensive operations are cached where appropriate

## Summary Format

After reviewing, provide:

1. **Critical issues** (must fix before merge)
2. **Suggestions** (improve quality but not blocking)
3. **Positives** (what was done well)
2 changes: 1 addition & 1 deletion examples/tutorial_16_agent_handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ async def main():
print("Chain handoff completed:")
for i, result in enumerate(chain_results):
status = "OK" if result.success else f"FAILED: {result.error}"
print(f" Step {i+1}: {result.source_agent_id} -> {result.target_agent_id}: {status}")
print(f" Step {i + 1}: {result.source_agent_id} -> {result.target_agent_id}: {status}")

# =========================================================================
# Part 8: Handoff History
Expand Down
2 changes: 1 addition & 1 deletion examples/tutorial_17_orchestrator_pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ async def check_cache() -> str:
print(f" Decisions made: {len(orch_result.decisions)}")

for i, decision in enumerate(orch_result.decisions):
print(f"\n Decision {i+1}: {decision.decision_type}")
print(f"\n Decision {i + 1}: {decision.decision_type}")
if decision.specialists:
print(f" Specialists: {decision.specialists}")

Expand Down
Loading
Loading