docs/agent-skills-research.md
Rama main · réplica de lectura del repositorio original.
# Repo-scoped agent skills: what to add, and how they stop drift
Question: which skills under `.claude/skills/` should this repository add so the
Go Catalog, the site and DCM stay in step, and what must each contain?
Requested by `.scratch/dcm-merge-spec.md:12` ("repo skills in `.claude/skills/`
built with skill-creator + writing-for-agents"). Other research notes live in
`docs/research/` and `docs/homarr-integration-coverage-research.md`; this one sits
at `docs/` root because the request named that path.
Date: 2026-10-30. Branch `dev-dcm-merge`.
## Result
Add three skills and one shared brief. Everything else already has a home that
works, or is too thin to earn an always-loaded description.
| Name | Invocation | One line |
| ---------------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `edit-catalog` | model | Add or change an App or Recipe, regenerate site and DCM data, fix the hand-kept counts. |
| `add-container` | model | Add or edit a DCM-only Container, with the Container-vs-App collision check nothing automates today. |
| `release` | user (`disable-model-invocation: true`) | Order the release gates from `docs/releasing.md` with completion criteria and approval stops. Optional, medium confidence. |
| `docs/agents/generated-data.md` (brief, not a skill) | pointer | The drift map below, kept once and pointed at by both skills and `AGENTS.md`. |
Fix first, or none of this ships: `.gitignore:8` ignores `/.claude/`, so a new
skill is silently untracked. See [Blocker](#blocker-gitignore-hides-new-skills).
## Method and limits
Read: `AGENTS.md`, `CONTRIBUTING.md`, `docs/agents/*`, `docs/releasing.md`,
`.claude/skills/release-visual/SKILL.md`, `.github/workflows/{ci,dcm-add-container,stars}.yml`,
`tools/sitegen/*`, `dcm/src/lib/{catalog,selfhost}.ts`, `internal/app/app.go`,
`internal/catalog/*_test.go`, `internal/skill/install.go`, and `git log -400`.
Authoring rules come from the local `writing-for-agents` (`SKILL.md`,
`SKILL-MECHANICS.md`) and `skill-creator` skills.
**Not verified: external documentation.** The fetches of the Anthropic Agent
Skills overview and best-practices pages, agentskills.io, and the Codex and
opencode docs were declined in this session, so none of them is cited. Claims that
depend on them are marked _unverified_ and listed in
[Discovery](#discovery-claudeskills-codex-opencode). Re-run them before relying on
those claims.
## Findings
### One skill exists, and the pointer pattern is already in use
- `.claude/skills/release-visual/SKILL.md` is the only repo skill: a
description, three steps (Discover, Render, Embed), each with a checkable end,
and a pointer to `docs/releasing.md` instead of a copy of it.
- `AGENTS.md` ("Agent skills") points at `docs/agents/*` briefs by trigger
("before adding or editing a container...", "Homarr integrations: before
adding, repairing or verifying..."). Those are context pointers in the
`writing-for-agents` sense, and they work in any agent that reads `AGENTS.md`
(`CLAUDE.md` is a symlink to it).
- `skills/self-host/SKILL.md` is the product skill shipped to users. It is not a
repo-maintenance skill and stays separate.
### What recurs in history
Counts from `git log --oneline -400`, grouped by conventional-commit scope:
| Workflow | Evidence | Has a brief? |
| --------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Add/change an App or Recipe | `feat(apps)` Cleanuparr (`ca78641b`, `00af75ef`), 5 `feat(catalog)`, `refactor(apps)` x4 | No |
| Homarr native wiring | 11 `fix(homarr)`, about 40 `chore: reconcile accepted Homarr integrations` | Yes, `docs/agents/homarr-integrations.md` (149 lines, 7 ordered steps, each with **Done**) |
| DCM Container | 8 `fix(dcm)`, 4 `feat(dcm)`, issue-driven `dcm-add-container.yml` | Yes, `docs/agents/dcm.md` |
| Release | 7 `feat(release)`, 5 `fix(release)` | Doc, not a brief: `docs/releasing.md`, 398 lines |
| Site work | 27 `feat(site)`, 7 `fix(site)` | Constraints live in `dcm.md` (CSP, Tailwind scope); no repeatable workflow |
| Issue/PR filing | `docs/agents/filing-issues.md`, 20 lines | Yes |
| e2e | 5 `test(e2e)`, 3 `fix(e2e)` | `e2e/README.md` (not read for this note) |
### What adding an App touches today
`ca78641b feat(apps): Cleanuparr (#554)` changed 13 files. That is the real
footprint, and no brief lists it:
- `internal/apps/cleanuparr/cleanuparr.go` and `cleanuparr_test.go`
- `internal/catalog/batch_misc2.go` (import plus `cleanuparr.App{}`)
- `docs/catalog/traps/cleanuparr.md` (the App's header comment points at it)
- `tools/sitegen/catalog.go` (`"cleanuparr": "png/cleanuparr.png"`; the icon tables now live in `icons.go`)
- `tools/sitegen/stars.go` (`"cleanuparr": "Cleanuparr/Cleanuparr"`)
- `site/src/data/catalog.json` (generated)
- `README.md` (count), `e2e/acceptance.md` (94 Apps), `e2e/acceptance_fixture_test.go`
(`len(got) != 101`), `e2e/testdata/inventory.json`, two TUI onboarding goldens
Eight of the thirteen (README, `e2e/acceptance.md`, the fixture test,
`inventory.json`, the two goldens, the icon entry, the stars entry) are hand-edited
counts and declarations that no generator writes. That is where drift enters.
### Drift mechanisms that already exist
- **Generated data.** `go run ./tools/sitegen` writes `site/src/data/catalog.json`,
`dcm/src/data/selfhost.json` and `site/src/styles/theme.css`; `-stars` writes
`site/src/data/stars.json`. CI's `test` job runs it and `git diff --exit-code`
on `site/src/data dcm/src/data site/src/styles` (`.github/workflows/ci.yml`,
step "Generated data is current").
- **Icons.** `tools/sitegen/icons.go`: an App must be in `icons`
(`""` means no icon); a missing, unknown or duplicated id fails sitegen.
- **Standalone guide.** `app.StandaloneGuide` is an optional App interface;
`checkGuide` in sitegen fails when the guide names a variable or folder the
converted compose does not read or mount.
- **Catalog tests.** `internal/catalog/`: `descriptions_test`, `parity_test`,
`ownership_test`, `generated_test`, `secondary_ports_test`, `unavailable_test`,
`homarr_recipe_test`.
- **Site schema and SEO.** `site/src/content.config.ts` validates `catalog.json`;
`site/src/lib/seo.mjs` fails the build on a page missing its SEO or share tags.
- **DCM tests.** `dcm/tests`: `validate-all-containers`, `templates`,
`validate-networks`, `compose-dedupe`, `compose-env`, and `validate-compose`
(runs `docker compose config`; CI as `test:compose`).
- **Stars.** `.github/workflows/stars.yml` opens a weekly PR from `bun run stars`.
### Unguarded drift (found, not fixed here)
1. **`.gitignore:8` ignores `/.claude/`.** `release-visual` is tracked only
because it was force-added (`git ls-files .claude` lists it; `git check-ignore`
exits 1 for it but 0 for a fresh `.claude/skills/add-app/SKILL.md`).
2. **Container-vs-App collisions have no test.** `dcm/src/lib/selfhost.ts:44`
holds `ALIASES` (7 hand-kept entries: `actualbudget`, `adguardhome`, `firefly`,
`homeassistant`, `jellyseerr`, `overseerr`, `postgres`). `coveredByApp` in
`dcm/src/lib/catalog.ts:82` only drops a Container whose id starts with
`<app>-` and whose services the App already provides. A Container named
`bitwarden` beside an App `vaultwarden`, or `nginxproxymanager` beside
`nginx-proxy-manager`, shows up twice. No file under `dcm/tests` mentions
`ALIASES` or `coveredByApp`.
3. **App counts are hand-kept and the README ones are unguarded.** `README.md`
lines 17, 68, 85 say 93; `e2e/acceptance.md:3` says 94. The numbers match
`site/src/data/catalog.json` (93 visible) and the full registry including the
hidden Passbolt (`README.md:266`); that reading is an inference. The e2e count
is pinned by `acceptance_fixture_test.go`; no test reads `README.md` for it.
4. **A missing `repos` entry in `tools/sitegen/stars.go` is not an error.** The App
sorts last on the site (`stars.go:15`, lookup near line 128).
5. **Two hand-kept lists of agent skill directories:** `internal/skill/install.go`
(`targets`) and `site/src/content/docs/docs/agents.md:18`.
## Discovery: `.claude/skills`, Codex, opencode
| Claim | Status | Source |
| -------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Claude Code reads project skills from `.claude/skills/<name>/SKILL.md` | **Observed** | `release-visual` exists only in this repo's `.claude/skills/` (absent from `~/.agents/skills`, `~/.claude/skills`, `~/.config/opencode/skills`) and appears in this session's skill list. |
| Per-agent _user-level_ skill dirs | **From the repo** | `internal/skill/install.go` `targets`: `~/.agents/skills` (always), `~/.claude/skills`, `~/.codex/skills`, `~/.pi/agent/skills`, `~/.gemini/config/skills`, `~/.hermes/skills`, `~/.config/opencode/skills`. |
| Codex discovers _project_ `.claude/skills` | **Unverified** | Needs the Codex docs. |
| opencode discovers project `.claude/skills` | **Unverified** | The local `opencode` skill (v2.2.1) names `.opencode/` and a `skills` config field and says nothing about `.claude/skills`; `opencode debug` has no skill subcommand. Docs: `opencode.ai/v2/docs/`. |
| `.agents/skills` is a cross-agent project path | **Unverified** | Only the user-level `~/.agents/skills` appears in this repo's code. |
| Frontmatter (`name`, `description` required), description as the trigger, three-level progressive disclosure, SKILL.md under 500 lines | **From local skills** | `skill-creator/SKILL.md` ("Progressive Disclosure": metadata always loaded, body under 500 lines), `writing-for-agents`. Anthropic's own docs not fetched. |
| `disable-model-invocation: true` makes a skill user-invoked only | **From local skills** | `writing-for-agents/SKILL-MECHANICS.md` ("Invocation"). |
Consequence: do not depend on skill discovery alone. Every new skill gets one
pointer line in `AGENTS.md` ("Agent skills"). That line is read by every agent
that reads `AGENTS.md`, costs one line, and keeps the skill body single-sourced.
To check later: Anthropic Agent Skills overview and best-practices, the
agentskills.io specification, the opencode V2 docs for skills and rules, and the
Codex docs for `AGENTS.md` and skills.
## Blocker: `.gitignore` hides new skills
`.gitignore:8` is `/.claude/`. A directory exclusion cannot be undone for a child
path, so a `!` line alone does nothing. Tested in a scratch repo: this pair
un-ignores skills and keeps the rest of `.claude/` ignored.
```gitignore
/.claude/*
!/.claude/skills/
```
Change line 8 accordingly in the same commit that adds the first skill.
## Authoring rules applied
From `writing-for-agents` and `skill-creator`:
- The description is a context pointer: lead with the one word the agent will see
in a task (`Catalog`, `Container`), one trigger per branch, no synonyms.
- Every step ends on a checkable **Done**.
- Single source of truth: the environment (`ci.yml`, `package.json`, `--help`,
the commit that did it last) already holds commands and file lists. Skills cache
only what the agent cannot find by looking: order, gotchas, reasons.
- Positive targets ("run sitegen, then the diff is empty"), not prohibitions.
- Branch-only reference goes behind a pointer, not in `SKILL.md`; keep `SKILL.md`
well under 500 lines (`release-visual` is 61).
- Model-invoked skills carry a description; user-invoked ones set
`disable-model-invocation: true`.
## Recommended skills
### 1. `edit-catalog` (model-invoked)
Description draft:
> Catalog edits: add an App, change an App's Compose, env, notes or StandaloneGuide, change a Recipe, or hide an App. Regenerates site and DCM data and updates the hand-kept App counts.
Branches: new App; change existing App; Recipe; hide/unhide (the `unavailable`
map in `internal/catalog/catalog.go`). Hand off, do not
duplicate: Homarr wiring to `docs/agents/homarr-integrations.md`; a DCM-only
Container to `add-container`.
Steps, each with its completion criterion:
1. **Resolve upstream facts** (image, tag, ports, volumes, env) from the project's
own docs or registry. _Done:_ each fact has a source URL in the PR body.
2. **Find an existing home.** Search `internal/apps/`, `dcm/src/tools/` and
`ALIASES` (`dcm/src/lib/selfhost.ts`) for the product under any spelling.
_Done:_ no hit, or the existing id is edited and a new one is not created.
3. **Write the App** in `internal/apps/<id>/<id>.go` with its test and
`docs/catalog/traps/<id>.md`; copy the shape of the last App commit
(`ca78641b`) rather than a described layout. _Done:_
`go test ./internal/apps/<id>/` passes.
4. **Register** in a `internal/catalog/batch_*.go` (Recipes through
`registerRecipes`). _Done:_ `go test ./internal/catalog/` passes.
5. **Declare site metadata** in `tools/sitegen`: icon in `icons`
(`""` for none), upstream repo in `stars.go`'s `repos`. _Done:_ both entries
exist; a missing `repos` entry would not fail anything, so check it by hand.
6. **Regenerate.** `go run ./tools/sitegen`. Add a `StandaloneGuide` only when the
standalone Compose needs guidance; `checkGuide` must pass. _Done:_ a second run
changes nothing and `git diff` shows only `site/src/data/catalog.json` and
`dcm/src/data/selfhost.json`.
7. **Fix the hand-kept counts**: `README.md` (badge, intro, table),
`e2e/acceptance.md`, `acceptance_fixture_test.go`, `e2e/testdata/inventory.json`,
TUI onboarding goldens (`-update`, then read each). _Done:_ every count equals
the registry length, with Apps hidden from new installs counted where the file
already counts them.
8. **Gate.** The `AGENTS.md` local gate plus the web gate, since generated data
changed. _Done:_ both exit 0.
Drift checks it owns: sitegen plus `git diff --exit-code`, icons, `repos`,
`StandaloneGuide`, counts, `ALIASES` (step 2).
Replaces: nothing; points to `docs/agents/dcm.md` ("How an App reaches DCM"),
`docs/agents/generated-data.md`, ADR 0001.
### 2. `add-container` (model-invoked)
Description draft:
> DCM Container: add or edit a Container in dcm/src/tools, including accepted `container` issues. Catalog Apps go to edit-catalog.
Steps:
1. **Collision check.** Search the App ids in `dcm/src/data/selfhost.json`, then
`ALIASES`, for the product under any spelling and for an `<app>-` id prefix.
_Done:_ none matches. A match means the App's Compose wins; stop and use
`edit-catalog`. This is the check the repo does not automate.
2. **Cite image and ports** from a current primary source. _Done:_ URL recorded.
3. **Add the `DockerTool`** to the right `dcm/src/tools/<category>.ts`. _Done:_ it
has a lowercase hyphenated `id`, `composeContent` starting `services:` with the
`${PUID}`... variables, a `githubUrl`, and no `stars`.
4. **Run what you touched**: `validate-all-containers`, `templates`,
`validate-networks` tests, then `bunx turbo run build lint`. _Done:_ all pass;
leave `validate-compose` to CI.
5. **Issue path.** For a PR opened by `dcm-add-container.yml`, review it against
steps 1 to 3. _Done:_ the diff touches only `dcm/src/tools/`.
Replaces: the "Add or edit a Container" section of `docs/agents/dcm.md`. Move its
five steps here and leave a one-line pointer, so the steps live once. The rest of
`dcm.md` (where things live, preloading, image, gate) stays as disclosed reference.
Drift checks it owns: collision and alias check, no hand-edited `stars`.
### 3. `release` (user-invoked, optional)
`disable-model-invocation: true`; the description is a one-line human summary.
Releases need the user's approval, so the agent should never fire this on its own.
Body: the gates of `docs/releasing.md` in order, each with a completion criterion
and a stop for approval before tag and publish: Prepare; Release from CI or
locally; Publish the channels; Verify; redeploy the site (the doc says to redeploy
after every release and that `publish-channels.sh` ends with `publish-site.sh`);
the new-Apps visual through `release-visual`. Link to doc sections, do not copy
them. Medium confidence: the doc is already the source of truth, so the skill adds
only ordering and stops. Write it last, and only after reading `docs/releasing.md`
end to end.
### Brief: `docs/agents/generated-data.md`
One home for the [drift map](#drift-map). `AGENTS.md` ("Ownership", "Generated
data") and both skills point at it. Skills with no description cannot share
reference; a plain file can (`SKILL-MECHANICS.md`, "Invocation").
## Considered and not recommended
- **`homarr-integration`.** The brief is already skill-shaped (ordered steps, a
**Done** each) and the `AGENTS.md` pointer carries the branch trigger. A skill
would be a second pointer to the same file.
- **`filing-issues` / `open-pr`.** 20 lines. A skill would duplicate it.
- **A standalone `drift-check` skill.** It would restate the `AGENTS.md` web gate
and `ci.yml`. Put the checks in each workflow's last step and the map in the
brief.
- **`site` work.** No repeatable workflow in 34 commits; constraints are in
`docs/agents/dcm.md`.
- **`e2e`.** Not enough read to recommend. `AGENTS.md` forbids running it for a
focused change; revisit after reading `e2e/README.md`.
## Drift map
Every shared fact, its source of truth, what is derived from it and what fails
when they disagree.
| Fact | Source of truth | Derived artifact | Check |
| ------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| App id, name, description, category, ports, image | `internal/apps/<id>/`, registered in `internal/catalog/batch_*.go` | `site/src/data/catalog.json`, `dcm/src/data/selfhost.json` | CI "Generated data is current"; `site/src/content.config.ts`; `descriptions_test`, `ownership_test`, `secondary_ports_test`, `generated_test` |
| Standalone Compose, env, notes | App plus optional `StandaloneGuide` through `tools/sitegen/standalone` | `dcm/src/data/selfhost.json` | `checkGuide` in sitegen; `dcm/tests/validate-compose.test.ts` (CI `test:compose`) |
| Icon | `tools/sitegen/icons.go` (`icons`) | icon path in both JSON files | `checkIcons` fails sitegen |
| Stars | `tools/sitegen/stars.go` `repos` | `site/src/data/stars.json`, `dcm/src/data/stars.json` | `stars.yml` weekly PR; **none** for a missing `repos` entry |
| Recipes | `registerRecipes` in `internal/catalog/batch_recipes.go` | `selfhost.json` recipes, DCM Templates | `parity_test`, `homarr_recipe_test` |
| Hidden App | `unavailable` map, `internal/catalog/catalog.go:42` | omitted from `Catalog.Apps()`, so from the site and new installs; `AllApps()` keeps it | `unavailable_test` |
| Dashboard theme | `tui.DarkColors` | `site/src/styles/theme.css` | sitegen diff in CI |
| DCM-only Container | `dcm/src/tools/<category>.ts` | merged by `dcm/src/lib/catalog.ts` | `validate-all-containers`, `templates`, `validate-networks`, `compose-dedupe`, `compose-env` |
| Container id to App id | `ALIASES`, `coveredByApp` | one merged DCM entry | **none** |
| App counts | registry length | `README.md` 17/68/85; `e2e/acceptance.md:3`; `e2e/acceptance_fixture_test.go`; `e2e/testdata/inventory.json`; TUI onboarding goldens | e2e fixture test and goldens fail; **none** for README |
| CLI command table | registered commands | `internal/skill/guide.md` | `TestSkillGuideCommandsTableListsEveryRegisteredCommand` |
| New-App release visual | App ids at two refs | `scripts/release-new-apps.sh` output | `release-visual` skill |
| Agent skill directories | `internal/skill/install.go` `targets` | `site/src/content/docs/docs/agents.md:18` | **none** |
## Decisions for the owner
1. Merge the `.gitignore` change first. Without it, step 1 of every skill fails
silently.
2. Close the Container-vs-App gap with a check, not a reminder. Options: a
`dcm/tests` case that fails when a Container id equals an App id or alias
target, or the skill's step 1 only. The first needs a test, which your rules say
to add only on request.
3. Count drift: derive the README and e2e counts from sitegen output, or keep
step 7 as the guard. Deriving removes five of the thirteen hand-edited files
per App (README, `e2e/acceptance.md`, the fixture test, two goldens); keeping step 7 costs nothing to build.
4. Re-run the declined external lookups before closing the discovery table.