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
2 changes: 2 additions & 0 deletions .agents/skills/rebasing-adapted-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Two parts carry it:
- The front-matter `attributions` **pin** each upstream skill directory to a GitHub tree URL at a full commit SHA.
- The `## Deviations` body lists each intentional difference on an upstream-present path as one natural-language bullet, read as merge **policy**: keep what a bullet protects, and where a bullet is silent, match upstream.
It is a policy ledger, not a changelog, so it never chronicles the upstream changes a rebase absorbs.
A verbatim vendor with no intentional differences declares that with the single sentinel bullet `- no current deviations`.
Only that exact physical line, with no other nonblank section content, means the same as an empty section - zero deviations - so a byte-for-byte vendor audits clean while keeping that human-readable line.

Scaffold pins, validate a directory, or audit it with `scripts/skill-adaptation.py` (read its `--help` for exact commands and exit codes).

Expand Down
91 changes: 73 additions & 18 deletions .agents/skills/rebasing-adapted-skill/scripts/skill-adaptation.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,10 @@
import urllib.parse
import urllib.request
from pathlib import Path
from typing import TYPE_CHECKING, NamedTuple
from typing import NamedTuple

import yaml

if TYPE_CHECKING:
from collections.abc import Sequence

EXIT_OK = 0
EXIT_INVALID = 1
EXIT_USAGE = 2
Expand All @@ -34,6 +31,8 @@
_ADAPTATION_NAME = "ADAPTATION.md"
_SKILL_NAME = "SKILL.md"

_NO_DEVIATIONS_SENTINEL = "- no current deviations"

# Environment override: read pinned base files from a local cache tree instead
# of the network, so an audit runs deterministically and air-gapped. Layout:
# <dir>/<owner>/<repo>/<sha>/<path>/<relpath...>. Used by the offline tests.
Expand Down Expand Up @@ -93,6 +92,11 @@
difference is uncovered (the next rebase would silently revert it);
- stale deviations: deviations are declared but NO difference exists, so
every bullet maps to nothing.
A ## Deviations section whose sole nonblank content is the exact physical line
"- no current deviations" is the sentinel a verbatim vendor uses to declare
zero intentional differences. It reads as no deviations (never a stale
bullet), so a byte-for-byte vendor audits clean while keeping that
human-readable line instead of an empty section.
When both differences and deviations are present, the script cannot prove
which covers which, so it presents both sides and exits 0; confirming that
each difference has a covering bullet and each bullet a live difference is the
Expand Down Expand Up @@ -280,13 +284,9 @@ def load_attributions(skill_dir: Path) -> list[Attribution]:
return [parse_attribution(url) for url in urls] # type: ignore[union-attr]


def parse_deviation_bullets(body: str) -> list[str]:
"""Return the ``## Deviations`` bullet texts, minus angle-bracket stubs.

A stub bullet like ``- <describe the difference>`` is the unedited template
placeholder, not a real declaration, so it does not count as a deviation.
"""
bullets: list[str] = []
def _deviation_section_lines(body: str) -> list[str]:
"""Return physical lines from the exact ``## Deviations`` section."""
lines: list[str] = []
in_section = False
for raw in body.splitlines():
if raw == "## Deviations":
Expand All @@ -295,8 +295,19 @@ def parse_deviation_bullets(body: str) -> list[str]:
if re.match(r"^#{1,6}(?:\s|$)", raw) is not None:
in_section = False
continue
if not in_section:
continue
if in_section:
lines.append(raw)
return lines


def _collect_deviation_bullets(body: str) -> list[str]:
"""Return the raw ``## Deviations`` bullet texts, minus angle-bracket stubs.

A stub bullet like ``- <describe the difference>`` is the unedited template
placeholder, not a real declaration, so it does not count as a deviation.
"""
bullets: list[str] = []
for raw in _deviation_section_lines(body):
item = re.match(r"^[-*]\s+(.*)$", raw.strip())
if item is None:
continue
Expand All @@ -308,11 +319,49 @@ def parse_deviation_bullets(body: str) -> list[str]:
return bullets


def read_deviation_bullets(skill_dir: Path) -> list[str]:
"""Read ``## Deviations`` bullets from a skill directory's ADAPTATION.md."""
def is_no_deviations_sentinel(body: str) -> bool:
"""Whether the deviations section contains the exact sentinel line alone.

A ``## Deviations`` section whose sole nonblank content is the physical line
``- no current deviations`` declares that the skill is a verbatim vendor with
zero intentional differences. It is kept as a human-readable line rather than
an empty section, but it means the same thing: no deviations to protect.
"""
content = [line for line in _deviation_section_lines(body) if line.strip()]
return content == [_NO_DEVIATIONS_SENTINEL]


def parse_deviation_bullets(body: str) -> list[str]:
"""Return the declared ``## Deviations`` bullets, or ``[]`` for none.

Two forms declare zero deviations, both collapsing to an empty list: the
unedited angle-bracket placeholder, and the sole ``- no current deviations``
sentinel (see :func:`is_no_deviations_sentinel`).
"""
bullets = _collect_deviation_bullets(body)
if is_no_deviations_sentinel(body):
return []
return bullets


def read_deviation_ledger(skill_dir: Path) -> tuple[list[str], bool]:
"""Read a skill's ``## Deviations`` as ``(declared_bullets, is_sentinel)``.

``declared_bullets`` is empty when zero deviations are declared, and the
boolean records whether that emptiness came from the ``- no current
deviations`` sentinel so callers can echo the human-readable line.
"""
text = (skill_dir / _ADAPTATION_NAME).read_text(encoding="utf-8")
_front, body = split_front_matter(text)
return parse_deviation_bullets(body)
bullets = _collect_deviation_bullets(body)
sentinel = is_no_deviations_sentinel(body)
return ([] if sentinel else bullets), sentinel


def read_deviation_bullets(skill_dir: Path) -> list[str]:
"""Read ``## Deviations`` bullets from a skill directory's ADAPTATION.md."""
bullets, _sentinel = read_deviation_ledger(skill_dir)
return bullets


def _read_base_from_cache(
Expand Down Expand Up @@ -445,6 +494,7 @@ class AuditResult(NamedTuple):

drift: list[Drift]
bullets: list[str]
no_deviations_sentinel: bool = False

@property
def undeclared_drift(self) -> list[Drift]:
Expand All @@ -468,8 +518,10 @@ def audit_skill_dir(skill_dir: Path) -> AuditResult:
for attribution in load_attributions(skill_dir):
base_files = fetch_base_files(attribution)
drift.extend(compute_drift(attribution, base_files, skill_dir))
bullets = read_deviation_bullets(skill_dir)
return AuditResult(drift=sorted(drift), bullets=bullets)
bullets, sentinel = read_deviation_ledger(skill_dir)
return AuditResult(
drift=sorted(drift), bullets=bullets, no_deviations_sentinel=sentinel
)


def _render_audit_report(skill_dir: Path, result: AuditResult) -> list[str]:
Expand All @@ -488,6 +540,9 @@ def _render_audit_report(skill_dir: Path, result: AuditResult) -> list[str]:
lines.append("declared deviations (## Deviations):")
if result.bullets:
lines.extend(f" - {b}" for b in result.bullets)
elif result.no_deviations_sentinel:
lines.append(f" {_NO_DEVIATIONS_SENTINEL}")
lines.append(" (sentinel: declares zero deviations - treated as match-upstream)")
else:
lines.append(" none")
lines.append("")
Expand Down
8 changes: 8 additions & 0 deletions .agents/skills/writing-for-agents/ADAPTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
attributions:
- https://github.com/mattpocock/skills/tree/4aaccb58d40559d7e3c59a029b2290ae5ba538de/skills/productivity/writing-for-agents
---

## Deviations

- no current deviations
22 changes: 22 additions & 0 deletions .agents/skills/writing-for-agents/SKILL-MECHANICS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Skill mechanics

The skill-specific branch of [`writing-for-agents`](SKILL.md): what changes when the document is a skill — frontmatter, the invocation choice, and router skills. Everything else about writing it is the universal reference in `SKILL.md`.

## Invocation

Two choices, trading the two loads:

- A **model-invoked** skill keeps a `description`, so the agent can fire it autonomously — and other skills can reach it. You can still type its name: model-invocation always _includes_ user reach; a description only ever adds agent discovery, never removes the human's. The description is the skill's top-level context pointer, forced to stay loaded at all times — permanent context load in exchange for discoverability. A model-invoked skill whose content is all reference is also one home for shared reference: another skill can invoke it, so reference needed by several skills lives in one place. Mechanics: omit `disable-model-invocation`, and write a model-facing description carrying the trigger branches (the pointer-writing rules in `SKILL.md` apply in full).
- A **user-invoked** skill strips the description from the agent's reach: only the human typing its name can invoke it, and no other skill can. Zero context load, but it spends cognitive load — you are the index that must remember it exists. Mechanics: set `disable-model-invocation: true`; the `description` becomes human-facing — a one-line summary, trigger lists stripped.

Pick model-invocation only when the agent must reach the skill on its own, or another skill must. If it only ever fires by hand, make it user-invoked and pay no context load.

Shared reference that two user-invoked skills both need can live in neither — with no descriptions, neither can fire the other. Push it to a plain file outside the skill system: external reference any skill can point at.

## Splitting by invocation

The invocation cut of splitting (the sequence cut lives in `SKILL.md`): split off a model-invoked skill when you have a distinct leading word that should trigger it on its own — a trigger word you actually use in your prompts — or another skill must reach it. You pay context load for the new always-loaded description, so that independent reach has to be worth it.

## Router skills

When user-invoked skills multiply past what you can remember, that piled-up cognitive load is cured by a **router skill**: one user-invoked skill that names the others and when to reach for each, so the human has one skill to remember instead of many. It can only hint, never fire them: user-invoked skills have no description, so nothing but the human can reach them.
Loading
Loading