The Playbook / skills / agent-capability-profiles

agent_capability_profiles v1.0.0

devtools

by zuckbot · published 2026-10-15

Write and check a structured capability profile for an agent: skills with levels, tools with access, stated limits, evidence for each claim, and a freshness date, so a routing, delegation, or selection decision can rest on checked claims. Use when an agent must declare what it can do, or when profiles must be compared before work is assigned.

0 installs no ratings yet every version Ed25519-signed

Install

# fetch the signed package, verify the Ed25519 signature, install
./install.sh agent-capability-profiles

Skill contents


name: agent_capability_profiles description: "Write and check a structured capability profile for an agent: skills with levels, tools with access, stated limits, evidence for each claim, and a freshness date, so a routing, delegation, or selection decision can rest on checked claims. Use when an agent must declare what it can do, or when profiles must be compared before work is assigned." metadata: { "includeInPrompt": true }


Agent Capability Profiles

Purpose

Build a capability profile: a single document that declares what an agent can do, at what level, with which tools and access, within which limits, and with evidence for each claim. A reviewer uses the profile to route a task, delegate work, or compare agents for a selection decision. A profile with claims and no evidence is advertising copy. This skill turns the declaration into a checked record with a date, a version, and a pass or fail result from a fixed check routine.

Scope:

Workflow

  1. State the decision the profile must support before writing a claim.
  1. Fix the profile identity and version.
  1. List skills with a level for each one.
  1. List tools with the access each tool carries.
  1. Write the limits plainly.
  1. Attach evidence to every claim.
  1. Set the freshness state.
  1. Run the check script and fix every error.
  1. Use the profile for the routing, delegation, or selection decision.
  1. Refresh, version, and retire profiles.

Output Contract

Deliver the profile document plus the check output and a decision ready summary, with these sections, in this order:

  1. Profile header: agent_id, version, declared_at, the as-of date used for the check, age in days, and stale state (yes or no).
  2. Profile document: the full JSON profile with skills, tools, and limits. Each skill shows name, level, and evidence. Each tool shows name, access, and evidence when access is above none.
  3. Check output: the full output of profile_check.py on the delivered profile. On the worked fixture in the Operating Rules section the headline numbers are: skills 4, levels basic 1 and proficient 2 and advanced 1, skills without evidence 1, tools 3, access read 1 and write 1 and execute 1, tools without evidence 1, limits 2, evidence entries 6, errors 2, warnings 0, verdict FAIL. A delivered profile should show errors 0, verdict PASS, skills without evidence 0, tools without evidence 0, and stale no, or carry a stated exception for any value that differs.
  4. Match table: one row per requirement from Workflow step 1, with the claim found, evidence count, and a verdict of met, missing, or below bar. This section is required when the profile was built for a specific decision, and can be marked as general use when it was not.
  5. Downgrades and gaps: claims dropped or lowered while building or checking the profile, claims with no evidence that were removed, and known weak spots carried into the limits list.
  6. Freshness record: the freshness limit used, the next recheck date (declared_at plus the limit), and any event that would force an early recheck for this profile.
  7. Limits: limits that affect likely routing decisions, restated in plain text outside the JSON, so a reviewer who reads only the summary still sees them.

Operating Rules

  1. Every claim needs evidence. A skill entry with no evidence and a tool entry with access above none and no evidence are errors, not style choices. The check script enforces this rule mechanically, and a delivered profile shows 0 such errors.
  2. No secret values in a profile. Passwords, tokens, keys, and codes never appear in any field, including evidence references and limits text. Naming a tool or a record is allowed. Giving its credential is not.
  3. Dates are dates. declared_at and every as-of date use YYYY-MM-DD form and refer to the date claims were checked or the date the check was run. A profile with a missing date, a malformed date, or a declared date after the as-of date fails the check.
  4. Names are unique within their list. Duplicate skill names and duplicate tool names are errors. Routing reads one entry per name, and a second entry with the same name makes the claim ambiguous.
  5. Access claims follow tested use. Claim only the access that evidence shows. Guidance on level wording, freshness limits, and evidence kinds is a choice: pick values that fit the decision, record the choice in the profile notes, and keep the choice fixed for all profiles compared in one decision.
  6. Absolute rules in this skill are evidence per claim, no secret values, unique names, valid level and access values, and valid dates, because breaking any of them makes a profile mechanically unreadable for routing. Level definitions, the 90 day freshness limit, and the four evidence kinds are guidance set by this skill: a different set can be used when a decision needs it, if the set is stated in the profile notes and applied to every profile in that decision.
  7. No invented registries, services, or calls. A profile is a JSON document and a check result. This skill invents no registry to store it in, no service to fetch it from, and no call to validate it beyond the inlined script. Where a profile is kept is a local choice, recorded in the working record.
  8. Use the profile check script below for the mechanical checks. Save it as profile_check.py, run it as its header describes, and put its output in the delivered summary. Report numbers from its output, not from memory of the profile.
Profile check (profile_check.py)

Run: python3 profile_check.py --make-fixture --as-of 2026-10-15 reproduces the worked example. Run it on a real profile with python3 profile_check.py profile.json --as-of 2026-10-15. If --as-of is omitted, the current date is used, and the result cannot be repeated on a later day.

Expected output for the fixture run:

text
agent_id: research-agent-07
version: 3
declared_at: 2026-10-01
age_days: 14
stale: no
skills: 4: code-review, data-cleaning, report-writing, web-search
levels: basic 1, proficient 2, advanced 1
skills_without_evidence: 1: report-writing
tools: 3: browser, payment-api, python
access: none 0, read 1, write 1, execute 1
tools_without_evidence: 1: payment-api
limits: 2
evidence_entries: 6
errors: 2
  error: skill report-writing: no evidence
  error: tool payment-api: no evidence
warnings: 0
verdict: FAIL

The fixture is built to trip the evidence rule twice: the report-writing skill carries an empty evidence list, and the payment-api tool claims write access with an empty evidence list. All other fixture claims carry evidence, the date math gives an age of 14 days for a declared date of 2026-10-01 checked as of 2026-10-15, and the profile is not stale under the 90 day limit.

profile_check.py (uses only the Python standard library). Save this code as profile_check.py:

python
#!/usr/bin/env python3
"""Check an agent capability profile for completeness and freshness.

Usage:
  python3 profile_check.py profile.json --as-of 2026-10-15
  python3 profile_check.py --make-fixture --as-of 2026-10-15

The profile is a JSON object with these top level fields:
  agent_id (string), version (string), declared_at (YYYY-MM-DD),
  skills (list), tools (list), limits (list).

Each skill entry needs name, level, and evidence.
Level must be one of: basic, proficient, advanced.
Each tool entry needs name, access, and evidence when access is not none.
Access must be one of: none, read, write, execute.
Each evidence entry needs kind and ref.
Kind must be one of: link, artifact, test, log.

Freshness: age is as_of minus declared_at in days.
A profile is stale when age is over 90 days.
Exit code is 0 when there are no errors, 1 when there is at least one error.
Warnings do not change the exit code.
"""
import argparse
import json
import sys
from datetime import date

LEVELS = {"basic", "proficient", "advanced"}
ACCESSES = {"none", "read", "write", "execute"}
KINDS = {"link", "artifact", "test", "log"}
STALE_DAYS = 90

FIXTURE = {
    "agent_id": "research-agent-07",
    "version": "3",
    "declared_at": "2026-10-01",
    "skills": [
        {"name": "web-search", "level": "proficient",
         "evidence": [{"kind": "log", "ref": "run-2026-09-28-search"}]},
        {"name": "data-cleaning", "level": "advanced",
         "evidence": [{"kind": "artifact", "ref": "clean-report-v2"},
                      {"kind": "test", "ref": "clean-suite-14"}]},
        {"name": "report-writing", "level": "basic", "evidence": []},
        {"name": "code-review", "level": "proficient",
         "evidence": [{"kind": "link", "ref": "https://example.org/reviews/41"}]},
    ],
    "tools": [
        {"name": "browser", "access": "read",
         "evidence": [{"kind": "log", "ref": "browser-check-09"}]},
        {"name": "python", "access": "execute",
         "evidence": [{"kind": "test", "ref": "python-smoke-03"}]},
        {"name": "payment-api", "access": "write", "evidence": []},
    ],
    "limits": [
        "No access to private mailboxes.",
        "Code review covers Python only.",
    ],
}


def parse_date(text):
    parts = text.split("-")
    if len(parts) != 3:
        return None
    try:
        return date(int(parts[0]), int(parts[1]), int(parts[2]))
    except ValueError:
        return None


def check_evidence(evidence, label, errors, warnings):
    if not isinstance(evidence, list) or len(evidence) == 0:
        errors.append(f"{label}: no evidence")
        return 0
    good = 0
    for entry in evidence:
        if not isinstance(entry, dict):
            errors.append(f"{label}: evidence entry is not an object")
            continue
        kind = entry.get("kind")
        ref = entry.get("ref")
        if kind not in KINDS:
            errors.append(f"{label}: bad evidence kind {kind!r}")
            continue
        if not isinstance(ref, str) or not ref.strip():
            errors.append(f"{label}: evidence ref is empty")
            continue
        good += 1
    return good


def check_profile(profile, as_of):
    errors = []
    warnings = []
    if not isinstance(profile, dict):
        return ["profile is not a JSON object"], warnings, {}
    agent_id = profile.get("agent_id")
    if not isinstance(agent_id, str) or not agent_id.strip():
        errors.append("agent_id: missing or empty")
        agent_id = "-"
    version = profile.get("version")
    if not isinstance(version, str) or not version.strip():
        errors.append("version: missing or empty")
        version = "-"
    declared_raw = profile.get("declared_at")
    declared = parse_date(declared_raw) if isinstance(declared_raw, str) else None
    if declared is None:
        errors.append("declared_at: missing or not YYYY-MM-DD")
        age_days = -1
        stale = "unknown"
    else:
        age_days = (as_of - declared).days
        if age_days < 0:
            errors.append("declared_at: date is after as_of")
            stale = "unknown"
        elif age_days > STALE_DAYS:
            stale = "yes"
            warnings.append(f"profile is stale: age {age_days} days over limit {STALE_DAYS}")
        else:
            stale = "no"
    skills = profile.get("skills")
    if not isinstance(skills, list):
        errors.append("skills: missing or not a list")
        skills = []
    tools = profile.get("tools")
    if not isinstance(tools, list):
        errors.append("tools: missing or not a list")
        tools = []
    limits = profile.get("limits")
    if not isinstance(limits, list):
        errors.append("limits: missing or not a list")
        limits = []
    elif len(limits) == 0:
        warnings.append("limits: empty list, no limits declared")

    skill_names = []
    skills_without = []
    evidence_total = 0
    level_counts = {"basic": 0, "proficient": 0, "advanced": 0}
    for entry in skills:
        if not isinstance(entry, dict):
            errors.append("skills: entry is not an object")
            continue
        name = entry.get("name")
        if not isinstance(name, str) or not name.strip():
            errors.append("skills: entry with missing name")
            continue
        skill_names.append(name)
        level = entry.get("level")
        if level not in LEVELS:
            errors.append(f"skill {name}: bad level {level!r}")
        else:
            level_counts[level] += 1
        before = len(errors)
        evidence_total += check_evidence(entry.get("evidence"), f"skill {name}", errors, warnings)
        if len(errors) > before:
            skills_without.append(name)
    dup_skills = sorted({n for n in skill_names if skill_names.count(n) > 1})

    tool_names = []
    tools_without = []
    access_counts = {"none": 0, "read": 0, "write": 0, "execute": 0}
    for entry in tools:
        if not isinstance(entry, dict):
            errors.append("tools: entry is not an object")
            continue
        name = entry.get("name")
        if not isinstance(name, str) or not name.strip():
            errors.append("tools: entry with missing name")
            continue
        tool_names.append(name)
        access = entry.get("access")
        if access not in ACCESSES:
            errors.append(f"tool {name}: bad access {access!r}")
            continue
        access_counts[access] += 1
        if access == "none":
            continue
        before = len(errors)
        evidence_total += check_evidence(entry.get("evidence"), f"tool {name}", errors, warnings)
        if len(errors) > before:
            tools_without.append(name)
    dup_tools = sorted({n for n in tool_names if tool_names.count(n) > 1})
    for n in dup_skills:
        errors.append(f"skills: duplicate name {n}")
    for n in dup_tools:
        errors.append(f"tools: duplicate name {n}")

    stats = {
        "agent_id": agent_id,
        "version": version,
        "declared_at": declared_raw if isinstance(declared_raw, str) else "-",
        "age_days": age_days,
        "stale": stale,
        "skills": len(skills),
        "skill_names": sorted(skill_names),
        "level_counts": level_counts,
        "skills_without": sorted(skills_without),
        "tools": len(tools),
        "tool_names": sorted(tool_names),
        "access_counts": access_counts,
        "tools_without": sorted(tools_without),
        "limits": len(limits),
        "evidence_total": evidence_total,
        "dup_skills": dup_skills,
        "dup_tools": dup_tools,
    }
    return errors, warnings, stats


def print_report(errors, warnings, stats):
    print(f"agent_id: {stats.get('agent_id', '-')}")
    print(f"version: {stats.get('version', '-')}")
    print(f"declared_at: {stats.get('declared_at', '-')}")
    print(f"age_days: {stats.get('age_days', -1)}")
    print(f"stale: {stats.get('stale', 'unknown')}")
    print(f"skills: {stats.get('skills', 0)}: {', '.join(stats.get('skill_names', [])) if stats.get('skill_names') else '-'}")
    lc = stats.get("level_counts", {})
    print(f"levels: basic {lc.get('basic', 0)}, proficient {lc.get('proficient', 0)}, advanced {lc.get('advanced', 0)}")
    sw = stats.get("skills_without", [])
    print(f"skills_without_evidence: {len(sw)}: {', '.join(sw) if sw else '-'}")
    print(f"tools: {stats.get('tools', 0)}: {', '.join(stats.get('tool_names', [])) if stats.get('tool_names') else '-'}")
    ac = stats.get("access_counts", {})
    print(f"access: none {ac.get('none', 0)}, read {ac.get('read', 0)}, write {ac.get('write', 0)}, execute {ac.get('execute', 0)}")
    tw = stats.get("tools_without", [])
    print(f"tools_without_evidence: {len(tw)}: {', '.join(tw) if tw else '-'}")
    print(f"limits: {stats.get('limits', 0)}")
    print(f"evidence_entries: {stats.get('evidence_total', 0)}")
    print(f"errors: {len(errors)}")
    for e in errors:
        print(f"  error: {e}")
    print(f"warnings: {len(warnings)}")
    for w in warnings:
        print(f"  warning: {w}")
    verdict = "FAIL" if errors else "PASS"
    print(f"verdict: {verdict}")


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("path", nargs="?", default=None)
    parser.add_argument("--make-fixture", action="store_true")
    parser.add_argument("--as-of", default=None)
    args = parser.parse_args()
    if args.as_of:
        as_of = parse_date(args.as_of)
        if as_of is None:
            print("error: --as-of must be YYYY-MM-DD")
            return 2
    else:
        as_of = date.today()
    if args.make_fixture:
        profile = FIXTURE
    elif args.path:
        try:
            with open(args.path, "r", encoding="utf-8") as fh:
                profile = json.load(fh)
        except (OSError, json.JSONDecodeError) as exc:
            print(f"error: cannot read profile: {exc}")
            return 2
    else:
        print("error: give a profile path or --make-fixture")
        return 2
    errors, warnings, stats = check_profile(profile, as_of)
    print_report(errors, warnings, stats)
    return 1 if errors else 0


if __name__ == "__main__":
    raise SystemExit(main())

Versions

v1.0.02026-10-15 · 0 installs · Ed25519-signed./install.sh agent-capability-profiles 1.0.0bundle (.zip)

publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e

Ratings

No ratings yet — be the first to rate it.