Contracts

drift-policy.md

Contract: Surface Drift Policy

Mission: agent-profile-projection-plugin-production-01KV3NGS Contract ID: drift-policy-01 Status: Proposed Enforced by: SurfaceRepairService, test_drift_policy.py


Definitions

TermDefinition
MissingA surface the manifest expects to exist, but the file is absent from disk
StaleA manifest-owned file present on disk whose content hash matches the installed_at hash but does not match the current canonical rendering
DriftedA manifest-owned file present on disk whose content hash matches neither the installed_at hash nor the current canonical rendering — the user or another tool changed it
PresentA manifest-owned file present on disk whose content hash matches the current canonical rendering
Not applicableA surface kind that has been explicitly ruled as having no valid representation for a given harness

Policy Rules

Rule 1: Missing → Auto-create (no prompt)

IF surface_status == "missing":
    create_file(canonical_content)
    update_manifest(content_hash=hash(canonical_content))
    report: created

Rule 2: Stale → Auto-repair (no prompt)

IF surface_status == "stale":
    overwrite_file(canonical_content)
    update_manifest(content_hash=hash(canonical_content))
    report: repaired

Rule 3: Drifted + Interactive → Prompt before overwrite

IF surface_status == "drifted" AND is_interactive:
    prompt: "⚠ {path} has been locally modified. Overwrite? [y/N]"
    IF user_confirms:
        overwrite_file(canonical_content)
        update_manifest(content_hash=hash(canonical_content))
        report: overwritten
    ELSE:
        report: kept (drift preserved)

Rule 4: Drifted + Non-interactive → Report only

IF surface_status == "drifted" AND NOT is_interactive:
    IF repair_drift_flag == "overwrite":
        overwrite_file(canonical_content)
        update_manifest(content_hash=hash(canonical_content))
        report: overwritten
    ELSE:
        report: drift_detected (path, no modification made)
        exit_code: non-zero

Rule 5: --yes does NOT imply drift overwrite

IF --yes is passed AND surface_status == "drifted":
    APPLY Rule 4 (non-interactive path)
    # --yes sets is_interactive=False but does NOT set repair_drift_flag="overwrite"

Rule 6: Not applicable → Skip silently

IF surface_kind == "not_applicable":
    NO file operation
    report: skipped (included in summary count only)

Machine-Readable Output (doctor tool-surfaces --json)

The JSON output per surface uses SurfaceStatus.to_json() fields. The core fields are:

{
  "surfaces": [
    {
      "tool": "claude",
      "kind": "agent_profile",
      "state": "present | missing | drifted | stale | not_applicable | research_gap",
      "provider": "AgentProfilesProvider",
      "path": ".claude/agents/analyst.md",
      "source_kind": "built_in",
      "manifest": "analyst",
      "repair_command": null
    }
  ],
  "summary": {
    "created": 2,
    "repaired": 1,
    "drifted": 1,
    "overwritten": 0,
    "skipped_not_applicable": 8,
    "errors": 0
  }
}

Required fields per surface: tool, kind, state, path. The repair_command field is non-null when a specific CLI command can fix the finding. Additional additive fields (e.g., provider, source_kind, manifest) may be present — callers MUST tolerate unknown fields.

Note: file_hash in ProfileManifest stores the SHA-256 of content at install time (the "installed-at hash"). Stale detection: disk_hash == file_hash && disk_hash != canonical_hash. Drift detection: disk_hash != file_hash && disk_hash != canonical_hash.

Regression Protection

The test_migration_compat.py integration test freezes the doctor tool-surfaces --json schema. Any change to the top-level keys or the status enum values is a breaking change requiring a schema version bump and migration documentation.

plugin-manifest-claude.md

Contract: Claude Code Plugin Manifest

Mission: agent-profile-projection-plugin-production-01KV3NGS Contract ID: plugin-manifest-claude-01 Status: Proposed


Output Path

dist/spec-kitty-plugins/claude-code/.claude-plugin/plugin.json

Required Fields

FieldValueSource
name"spec-kitty"Constant
displayName"Spec Kitty"Constant
versionCurrent spec-kitty-cli releaseimportlib.metadata.version("spec-kitty-cli")
descriptionHuman-facing descriptionConstant string
author.name"Priivacy AI"Constant

Component Pointers (present only when component exists in bundle)

FieldPathCondition
skills"./skills/"Always — canonical skills always present
agents"./agents/"Always — built-in profiles always present
hooks"./hooks/hooks.json"Only if hooks.json is non-empty

Bundle Directory Layout

dist/spec-kitty-plugins/claude-code/
├── .claude-plugin/
│   └── plugin.json         ← this contract
├── skills/
│   └── spec-kitty.<cmd>/   ← one per canonical command (≥15)
│       └── SKILL.md
├── agents/
│   └── <profile_id>.md     ← one per built-in profile
├── bin/
│   └── spec-kitty-wrapper  ← CLI check + uvx fallback script
│       └── spec-kitty-wrapper.cmd  ← Windows equivalent
└── marketplace.json        ← git-based distribution catalog

Validation Gate

claude plugin validate --strict dist/spec-kitty-plugins/claude-code/ must exit 0. Runs in CI in the plugin-validate job before release.

Version Invariant

plugin.json:version MUST equal importlib.metadata.version("spec-kitty-cli") at build time. A mismatch is a build error, not a warning.

plugin-manifest-codex.md

Contract: Codex Plugin Manifest

Mission: agent-profile-projection-plugin-production-01KV3NGS Contract ID: plugin-manifest-codex-01 Status: Proposed


Output Path

dist/spec-kitty-plugins/codex/.codex-plugin/plugin.json

Required Fields

FieldValueSource
name"spec-kitty"Constant
versionCurrent spec-kitty-cli release (strict semver)importlib.metadata.version("spec-kitty-cli")
descriptionHuman-facing descriptionConstant string
author.name"Priivacy AI"Constant
interface.displayName"Spec Kitty"Constant
interface.shortDescription≤120 charsConstant string

Component Pointers (only when companion files exist)

FieldPathCondition
skills"./skills/"Always — canonical skills always present
mcpServers"./.mcp.json"Only if .mcp.json exists in bundle
apps"./.app.json"Only if .app.json exists in bundle

Explicitly Forbidden

  • "hooks" as a top-level key in plugin.json — Codex rejects this field; hooks are discovered by filesystem presence of the hooks/ directory
  • "agents" as a top-level key — Codex plugin-level agent packaging is NOT confirmed as supported; omit entirely

Bundle Directory Layout

dist/spec-kitty-plugins/codex/
├── .codex-plugin/
│   └── plugin.json         ← this contract
├── skills/
│   └── spec-kitty.<cmd>/   ← one per canonical command
│       └── SKILL.md
├── hooks/                  ← discovered by presence, NOT referenced in plugin.json
│   └── (hook scripts if any)
└── marketplace.json        ← repo-local marketplace catalog

marketplace.json Format

{
  "name": "spec-kitty-plugins",
  "interface": { "displayName": "Spec Kitty Plugins" },
  "plugins": [{
    "name": "spec-kitty",
    "source": { "source": "local", "path": "." },
    "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" },
    "category": "Productivity"
  }]
}

Install Command (documented in README)

codex plugin marketplace add <path-to-dist/spec-kitty-plugins/codex>
codex plugin add spec-kitty@spec-kitty-plugins

Version Invariant

plugin.json:version MUST equal importlib.metadata.version("spec-kitty-cli") and be valid semver. A mismatch or non-semver value is a build error.