# ADR-003: Skill Versioning Strategy

## Status

Draft (Revised)

## Context

Skills are deployed via Nix/direnv, which means:
- The "installed" version is a build artifact, not just source code
- Git SHA may not exist or match deployed content
- Skills can reference external scripts/binaries
- Protos and molecules need stable references

A single version identifier is insufficient. We need to answer:
1. How do we identify what version of a skill ran?
2. How do protos reference skills (pin vs float)?
3. How do we handle breaking changes?

## Decision

### Version Tuple

Every skill execution records a version tuple:

```yaml
skill_version:
  # Primary identity - Nix store path (immutable, content-addressed)
  nix_store_path: "/nix/store/abc123-worklog-1.0.0"

  # Source identity (where it came from)
  source_ref: "git+file:///home/dan/proj/skills#worklog"
  source_rev: "abc123def"  # git SHA, null if not in git

  # Content identity (what was actually deployed)
  content_hash: "sha256:789xyz..."  # hash of skill content per algorithm below

  # Semantic version from manifest (optional)
  version: "1.0.0"

  # Deployment metadata
  deployed_at: "2025-12-23T10:00:00Z"
```

#### Identity Selection by Context

| Context | Primary Identity | Rationale |
|---------|------------------|-----------|
| Nix-deployed skills | `nix_store_path` | Immutable, content-addressed by Nix |
| Development/local | `content_hash` | No Nix path available |
| Trace replay | `nix_store_path` or `content_hash` | Exact reproducibility |
| Proto pinning | `content_hash` or `version` | Portable across machines |

### Computing `content_hash`

Hash computation must be deterministic and portable:

```bash
#!/usr/bin/env bash
# skill-content-hash.sh <skill-dir>
set -euo pipefail

SKILL_DIR="${1:-.}"
SKILL_DIR="$(cd "$SKILL_DIR" && pwd)"

# Use .skillignore if present, otherwise default exclusions
if [[ -f "$SKILL_DIR/.skillignore" ]]; then
    EXCLUDE_FILE="$SKILL_DIR/.skillignore"
else
    EXCLUDE_FILE=""
fi

# Find files, convert to relative paths, sort, hash
(
    cd "$SKILL_DIR"
    find . -type f \
        ! -path './.git/*' \
        ! -path './.skillignore' \
        ! -name '*.pyc' \
        ! -name '.DS_Store' \
        ! -name '__pycache__' \
        ${EXCLUDE_FILE:+-not -path "$(cat "$EXCLUDE_FILE" | grep -v '^#' | tr '\n' '|' | sed 's/|$//')"} \
        -print0 | \
        sort -z | \
        xargs -0 -I {} sh -c 'echo "{}"; cat "{}"' | \
        sha256sum | \
        cut -d' ' -f1
)
```

**Critical requirements:**
- Use relative paths (not absolute) for portability
- Include filename in hash stream (not just content)
- Sort files deterministically before hashing
- Exclude non-functional files via `.skillignore`

#### `.skillignore` Format

Skills can exclude files from content hash (like `.gitignore`):

```
# .skillignore - files excluded from content_hash
README.md
CHANGELOG.md
docs/
tests/
*.test.js
```

This allows documentation changes without invalidating version pins.

### Proto Reference Modes

#### 1. Float (default, development)
```yaml
skill: worklog
```
Uses whatever version is currently deployed. Simple but unstable.

#### 2. Pin to content hash (CI/automation)
```yaml
skill:
  id: worklog
  content_hash: "sha256:789xyz..."
```
Fails if deployed skill doesn't match. Most stable for automation.

#### 3. Pin to minimum version (published templates)
```yaml
skill:
  id: worklog
  min_version: "1.0.0"
```
Requires skill manifest to declare `version` field with semantic versioning.

### Lockfile Workflow

For reproducible proto execution, use `proto.lock`:

```yaml
# my-proto.lock
# Auto-generated - do not edit manually
# Regenerate with: bd proto lock my-proto

generated_at: "2025-12-23T10:00:00Z"
beads_version: "0.35.0"

skills:
  worklog:
    content_hash: "sha256:789xyz..."
    nix_store_path: "/nix/store/abc123-worklog-1.0.0"
    version: "1.0.0"
    source_rev: "abc123def"

  deploy:
    content_hash: "sha256:456abc..."
    nix_store_path: "/nix/store/def456-deploy-2.1.0"
    version: "2.1.0"
    source_rev: "def456ghi"
```

**Workflow:**
```bash
# Development: float freely
bd mol spawn my-proto

# CI/production: lock versions
bd proto lock my-proto        # Generate/update lockfile
bd mol spawn my-proto --locked  # Fail if versions don't match lock
```

Lockfile should be committed to version control for reproducible builds.

### Breaking Change Handling

#### Interface Contracts

For semantic versioning to be meaningful, skills should declare their interface contract:

```yaml
# In SKILL.md manifest
interface:
  inputs:
    - session_date    # Required inputs are part of contract
    - topic           # Optional inputs with defaults
  outputs:
    - pattern: "docs/worklogs/*.org"
  env:
    - PROJECT         # Required env vars
```

**Breaking changes** (bump major version):
- Renamed/removed required inputs
- Changed required input types
- Changed output patterns
- Added new required inputs without defaults
- Removed required env vars

**Non-breaking changes** (bump minor/patch):
- Added optional inputs with defaults
- Documentation changes
- Bug fixes
- Performance improvements

#### Version Validation

```bash
# When spawning a proto with pinned skill
bd mol spawn my-proto --var x=1
# → Validates skill content_hash or version matches pin
# → Fails early if mismatch

# Check for breaking changes
bd skill check-compat worklog@1.0.0 worklog@2.0.0
# → Reports interface differences
```

### Path Sanitization

Traces should sanitize paths to avoid leaking local structure:

```yaml
# Before sanitization
skill_version:
  source_ref: "git+file:///home/dan/proj/skills#worklog"
  nix_store_path: "/nix/store/abc123-worklog-1.0.0"

# After sanitization (for sharing/elevation)
skill_version:
  source_ref: "git+file://LOCAL/skills#worklog"
  nix_store_path: "/nix/store/abc123-worklog-1.0.0"  # Already safe
```

Sanitization patterns:
- `/home/<username>/` → `LOCAL/`
- `/Users/<username>/` → `LOCAL/`
- Nix store paths are already content-addressed and safe

### Recording in Traces

Wisp traces always record the full version tuple:

```yaml
execution:
  skill_version:
    nix_store_path: "/nix/store/abc123-worklog-1.0.0"
    source_ref: "git+file://LOCAL/skills#worklog"  # Sanitized
    source_rev: "abc123def"
    content_hash: "sha256:789xyz..."
    version: "1.0.0"
```

This enables:
- Replay with exact version
- Diff between executions
- Debugging "it worked before" issues
- Portable sharing (sanitized paths)

### Recommendations

| Use Case | Mode | Identity | Why |
|----------|------|----------|-----|
| Active development | Float | N/A | Iterate quickly |
| Local testing | Float or pin | `content_hash` | Reproducible locally |
| Shared proto | Pin + lock | `content_hash` | Portable across machines |
| Published template | Pin to version | `min_version` | Semantic compatibility |
| CI/automation | Locked | `content_hash` | Exact reproducibility |

## Consequences

### Positive

- Full traceability of what ran
- Reproducible executions via lockfile
- Clear failure when version mismatch
- Supports gradual adoption (float first, pin later)
- Portable hashing (relative paths)
- Interface contracts enable meaningful SemVer

### Negative

- Content hash computation adds overhead
- Pinned protos need updates when skills change
- More fields to manage
- Lockfile adds another file to maintain

### Neutral

- Float mode preserves current behavior
- Version tuple is metadata, not enforcement
- Nix store path available only in Nix-deployed environments

## Implementation Checklist

- [ ] Implement deterministic content hash script
- [ ] Add `.skillignore` support to hash computation
- [ ] Add `nix_store_path` capture for Nix-deployed skills
- [ ] Implement `bd proto lock` command
- [ ] Implement `bd mol spawn --locked` validation
- [ ] Add path sanitization to trace writer
- [ ] Add interface contract validation
- [ ] Implement `bd skill check-compat` command

## Open Questions

1. Should lockfile include transitive dependencies (skills that call other skills)?
2. How to handle skills that shell out to system binaries (git, curl)? Version those too?
3. Cache content_hash or compute on every invocation?
4. Should we support nix flake references directly? (e.g., `github:user/skills#worklog`)
