# Item links (single-item source instances)
How `mind` consumes a deep link to one item inside a repo - the URL a user
copies from their forge's file browser - as a self-contained, managed install.
A repo may publish a `.claude-plugin/marketplace.json` that lists only a subset
of its skills (marketplace.md); an item link reaches an item the manifest does
not list, because the consumer names the exact path.
The linked item is a skill directory (the original form) or, when the link names
a single markdown file, an agent, rule, or command (LNK-20..23).
Each accepted link becomes its own *source instance*: a registry entry with an
extended identity, its own clone, pin, and lifecycle. Several links into the
same repo, and a plain meld of that repo, coexist as separate sources.
## Link form
- `LNK-1` An item link is a URL naming a directory or a file inside a repo:
`https://<host>/<owner>/<repo>/tree/<ref>/<path>` or
`https://<host>/<owner>/<repo>/blob/<ref>/<path>`, plus the GitLab
`/-/tree/<ref>/<path>` and `/-/blob/<ref>/<path>` variants, and the
`file:///<repo-path>/tree|blob/...` form for a local repo (the marker is
only recognized in the explicit `file://` spelling, so a bare local path
containing a `tree` directory stays a plain repo spec). A query string or
fragment (`?plain=1`, `#L10`) is stripped before parsing. The `<path>`
decides what is linked:
- ending in `/SKILL.md`: the skill directory that is its parent (the anchor
file names the skill),
- any other path ending in `.md`: that single file, an agent, rule, or
command (LNK-20),
- any other path: the skill directory itself.
A `blob` link must end in `.md`: a forge blob URL names a file, and a blob
path with any other extension (or none) is a `BadItemLink`. A `tree` link
accepts either shape. A local `file://` link instance is always a cloned
snapshot at its pin (the CLI-27 pinned-local flow), never a live-read working
tree. Tools are not linkable: a tool directory has no anchor file to
classify it by, and a tool is store-only (tooling.md), meaningful only
alongside the item that references it.
- `LNK-2` An item link is accepted anywhere a repo spec (CLI-11) is: `meld`,
`learn` (LNK-6), and a `[discover].sources` entry (DSC-38), so a curator can
curate individual items. A URL with a `tree`/`blob` segment whose tail fails
to complete as a valid item link is `BadItemLink` (LNK-14), not the generic
`InvalidRepoSpec`. For a remote URL (the `scheme://host/owner/repo/...`
branch), this holds only once the `owner/repo` portion ahead of the marker
itself parses: a malformed `owner/repo` there still reports
`InvalidRepoSpec` regardless of the marker or the tail, since owner/repo
splitting runs before the tail is parsed. The local `file://` branch parses
in the opposite order -- the `<ref>/<path>` tail is parsed before the
`owner/repo` portion is sliced off the remaining path -- so with no usable
repo path at all ahead of the marker (e.g. `file:///tree/main`), a broken
tail still reports `BadItemLink` rather than falling through to
`InvalidRepoSpec`. This is a real behavioral difference between the two
branches, not just wording; pinned by
`file_link_bad_tail_with_no_repo_ahead_of_marker_is_still_bad_item_link` and
`github_malformed_owner_repo_wins_over_bad_tail` in `src/source.rs`.
- `LNK-3` The `<ref>` segment supplies the instance's pin: a 40-hex ref is a
commit pin (as `--pin <sha>`), anything else follows that branch (as
`--pin branch=<name>`). An explicit consumer pin flag (CLI-17) overrides the
URL's ref; the URL's ref overrides the repo's `[source]` pin directive
(DSC-41). `learn <url> --pin` (a bare flag on `learn`, CLI-200) freezes the link's
branch ref to its current commit, so a branch-ref link can be snapshotted in one
step. The ref is a single path segment: a branch name containing `/` is not
representable in a link (the first segment after `tree`/`blob` is taken as the
ref); meld the repo with `--pin branch=<name>` instead.
- `LNK-14` A spec whose path carries a `tree`/`blob` link marker (LNK-1) is
unambiguously an attempted item link, so once the `owner/repo` portion ahead
of the marker parses, every failure in its `<ref>/<path>` tail reports
`BadItemLink`, never the generic `InvalidRepoSpec`: a missing ref, a missing
item path, a `blob` link not ending in `.md`, and an unsafe path or
ref value (LNK-10) all take this branch. The message names the offending URL
and the three expected shapes, `<repo-url>/tree/<ref>/<skill-dir>`,
`<repo-url>/blob/<ref>/<skill-dir>/SKILL.md`, and
`<repo-url>/blob/<ref>/<file>.md` (plus the GitLab `/-/tree/` and
`/-/blob/` spellings), instead of the generic repo-spec message, so a user
who pasted a real forge URL is told what shape a link needs rather than that
the URL is invalid. A spec with no `tree`/`blob` marker at all is not an
attempted link and keeps reporting `InvalidRepoSpec` unchanged.
## Single-file items
- `LNK-20` A link whose `<path>` names a `.md` file other than `SKILL.md` is a
FILE link: the linked item is that one file, installed as an agent, rule, or
command (kind by LNK-21), bare name the file stem. Everything else about the
instance is the skill-link behavior unchanged: identity (LNK-4), clone and pin
(LNK-3), lifecycle (LNK-5), no nested sources (LNK-8), reference reconciliation
(LNK-18). This is what lets a `[discover].sources` entry (LNK-2) curate an
individual agent or rule the way it already curates a skill.
- `LNK-21` A file link's kind is resolved in this order, first hit wins:
1. the consumer's explicit kind: `meld`/`learn --kind <kind>` (CLI-239), or a
`[discover].sources` entry's `kind = "<kind>"` (DSC-100);
2. the containing directory, by the DSC-11..14 convention names:
`agents/` -> agent, `rules/` -> rule, `commands/` -> command, so a link
into a conventional layout needs no annotation;
3. the file's own frontmatter `kind:` scalar, for a file that sits outside a
conventional directory.
Directory outranks frontmatter so that a link installs the SAME item a plain
meld of the repo would (a whole-repo scan classifies by directory, DSC-11..14):
a file under `agents/` is an agent to both. Only agent, rule, and command
resolve here: `skill` and `tool` are directory kinds, so an explicit or
frontmatter `kind` naming either on a file link is `LinkKindMismatch`, as is an
explicit kind other than `skill` on a link naming a directory. `meld`/`learn
--kind` is typed to accept only `agent`, `rule`, or `command` in the first
place (a clap value enum, distinct from `recall`/`probe --kind`'s five-kind
filter), so `skill`/`tool` there is refused before any clone rather than
reaching this mismatch; a curator's `kind =` is a bare TOML string with no
such upfront check, so it still reaches LNK-21 directly. A file link none of
the three steps resolves is `LinkKindUnresolved`, which names the three ways
to resolve it; when step 3's frontmatter `kind:` is present but not an item
kind word, the error additionally names `--kind` as the override. Every error
here is raised before anything is installed, and before anything registers.
- `LNK-22` An explicit kind (LNK-21 step 1) is recorded on the source instance
(`item_kind` in sources.json, STO-81) and applies to every later scan of it,
so `recall`, `upgrade`, and `sync` classify the item exactly as the meld did,
and it is fixed at meld/registration time: `--kind` on a re-meld, or on
`learn <url>` of an already-melded link, changes nothing and is noted rather
than silently dropped (CLI-206's ignored-flags note covers it on the re-meld
path). A kind resolved by directory or frontmatter records nothing, and is
re-read from the clone on every scan: for a commit-pinned link those inputs
cannot change, but a branch-following link's clone moves on `sync`, so an
upstream edit to the file's directory or frontmatter can change the resolved
kind (and so the item's `(source, kind, bare_name)` identity) between scans.
`--kind` is the way to freeze it against that. The field is absent from every
source an older binary registered, and absent means the pre-existing reading
(a skill link), so an existing registry is unaffected.
## Identity and lifecycle
- `LNK-4` An item-link source's identity is `host/owner/repo#<path>`, where
`<path>` is the linked item's repo-root-relative path: the skill directory,
or the file itself for a file link (LNK-20). An identity alias
composes as a trailing `@<alias>` segment (STO-58): `host/owner/repo#<path>@<alias>`.
Instances from the same repo, and a plain meld of `host/owner/repo`, are
distinct registry entries with independent pins, commits, and lifecycles.
Re-melding an identical link follows the CLI-12 re-meld flow. The `#<path>` IS
part of the clone leaf (STO-70: an item-link instance's leaf encodes its
`item_path`, not just its `@<alias>`), so every link instance -- aliased or
not -- gets its own independent checkout, distinct from a plain meld of the
repo and from every other link into it. Only a genuinely bare instance (no
`item_path`, no `@<alias>`) can ever share the plain `sources/<host>/<owner>/<repo>`
clone, and by construction there is at most one such instance per identity
(a second bare meld of the same repo is a re-meld, CLI-12, not a second
instance), so in practice nothing shares a checkout post-STO-70. (Before
STO-70, the `#<path>` was NOT part of the leaf, so every non-aliased link
into a repo, and a plain meld of it, all shared one clone; two non-aliased
links pinned to different refs would contend over that one working tree,
each re-clone silently discarding the others' checkout. STO-70's migration
note applies to any instance registered under that older behavior.)
- `LNK-5` `sync`, `upgrade`, `introspect`, `recall`, and `unmeld` treat the
instance as an ordinary source: `sync` fetches per its pin (CLI-55),
`upgrade` compares source content hash and commit, `unmeld` uninstalls its
item (CLI-21). `forget` of the instance's skill leaves the instance
registered; when the instance has no installed items left, `forget` prints a
hint to `mind unmeld <identity>`.
- `LNK-15` The identity-alias suffix (STO-58) distinguishes link instances of
the SAME `<path>` exactly as it distinguishes any other source instance: a
bare link `host/owner/repo#<path>` and an aliased link
`host/owner/repo#<path>@<alias>` are distinct registry entries that coexist,
each with its own clone (STO-70: the `item_path` alone is now enough to earn
an independent checkout, so both the bare and the aliased instance here get
one), pin, and installed items, untouched by the other's lifecycle.
Consequently `learn <url>`
(LNK-6), with or without `--pin`, checks membership under the SPECIFIC
alias it is melding as (here, none): if the repo has only been melded under
a DIFFERENT alias (or no alias at all when the new call supplies one), the
bare identity is not yet registered, so `learn` registers it as a new,
coexisting instance rather than reusing or being blocked by the
differently-aliased one. This is not a collision: it is the normal STO-58
per-instance model applied to links.
- `LNK-16` A link's `<path>` may not contain `@` or `#`; a link URL whose skill
path carries either is a `BadItemLink`. This is STO-64's rule one segment
over. A link instance's identity is `host/owner/repo#<path>`, and an identity
alias appends `@<alias>` to the whole identity (STO-58), so an unaliased link
to `skills/foo@bar` and an `@bar`-aliased link to `skills/foo` would compute
the same identity and, per STO-59, the same clone directory; a `#` would put a
second marker into an identity that is split on the first one. Refusing both
at parse time keeps a link identity decomposable back into the parts it was
built from. Note that `#` reaches the path only through the `file://` form: a
remote URL is truncated at its first `#` as a pasted browser fragment (LNK-1)
before the path is taken, so `.../skills/foo#bar` links to `skills/foo`.
- `LNK-17` The `<path>` is lexically normalized before it becomes identity:
`.` segments are dropped, so `.../tree/main/./skills/foo` and
`.../tree/main/skills/./foo` parse to the same `skills/foo` instance
(identity, clone directory, and re-meld detection) as the plain form,
rather than registering a duplicate instance of the same on-disk skill.
## Install
- `LNK-6` `learn <url>` is the one-shot form: it registers the link instance
exactly as `meld <url>` would (clone, hook consent, prompts) and installs its
skill in one step. `meld <url>` follows the standard meld flow (the CLI-23
install offer, `--register-only`, `--yes`). The meld flags apply to a link
meld unchanged (`--namespace`, pin flags, `--install-hook`).
- `LNK-7` A link instance's catalog is exactly one item: for a skill link the
directory at `<path>` containing `SKILL.md`, bare name the directory's
basename; for a file link (LNK-20) the file at `<path>`, bare name its stem,
kind resolved by LNK-21. Either way the
description comes from frontmatter (DSC-30). Discovery bypasses the repo's declared
inventory: an authoritative `mind.toml` (DSC-3) and a `.claude-plugin/`
manifest (MKT-2/MKT-14) do not gate the link, so it can install an item the
repo does not export - the consumer named the exact path. The
`min-mind-version` gate (DSC-40) and `[source]` metadata (description,
declared prefix) still apply. A skill link whose `<path>` has no `SKILL.md`
in the clone is an error (`LinkNotASkill`), and a file link whose `<path>` is
not a file there is an error (`LinkNotAFile`); either way nothing is
registered.
- `LNK-8` A link instance registers no nested sources: `[discover].sources`
entries and marketplace external plugins in the linked repo are not walked.
The link is a single-item grab, not a super-source adoption.
- `LNK-18` A link instance's catalog is exactly the linked item (LNK-7), so an
intra-source reference (dependencies.md, tooling.md) to any other name can
never resolve, however the repo is laid out. The two reference FORMS are
handled differently, following DEP-4's own distinction between them:
- A `requires:` entry (DEP-4) that names something other than the linked item
is NOT the DEP-6 `BadReference` error here. It is dropped, recorded
(LNK-19), and warned about, and the item installs. `requires` is pure
metadata (DEP-4): it is never rewritten into the item body, so a dangling
entry degrades the item rather than corrupting it, and refusing would make an
item that declares one unreachable by link at all. Other `requires`
failures keep their DEP-7 causes as hard errors: a malformed ref
(`InvalidRef`) and a source-qualified ref (`CrossSource`) are wrong
regardless of the catalog. `AmbiguousKind` cannot arise, since a single-item
catalog offers at most one match. An entry naming the linked item's own
name under the WRONG kind (`agent:<skill-name>`) is a `NoMatch` like any
other and is dropped with the rest.
- A TOKEN naming a sibling stays a hard error and nothing is installed: a
token IS rewritten into the item's text at install, so it cannot be left
dangling. This covers every token family that resolves against siblings, not
just `{{ns:name}}` (NS-10/DEP-1): `{{tools:name}}` (TOOL-15) and
`{{path:[kind:]name}}` (TOOL-18) are equally unsatisfiable in a one-item
catalog, and a skill shipping alongside a sibling tool is a common shape.
`{{self}}` is not a sibling reference and always resolves. The scan covers
exactly the files install expands, markdown (NS-53) plus any non-markdown
file the item lists in `expand:` (NS-57); scanning a narrower set would let a
reference slip through to the blunt error this rule exists to replace. The
error is `LinkRefUnsatisfiable`, not the generic `BadReference`/TOOL-17
cause: it names the token as written, says the source is a single-item
link with no siblings, and carries the remedy, so a user who pasted a deep
URL is told what to run instead of being told the item references something
missing.
*The remedy.* Both paths name one of two forms of the same two-step command,
decided by scanning the clone already on disk as an ordinary whole-repo
source (the same `Source`, with `item_path` cleared). When that scan finds an
item of the linked kind at the exact path the link points at (a same-named
item declared at a DIFFERENT path in the repo does not count), a plain meld
would discover it on its own:
```
mind unmeld '<identity>' --yes && mind meld '<repo-url>' --learn '<kind>:<name>' --yes
```
Otherwise -- an authoritative `mind.toml` or `.claude-plugin` manifest that
does not declare the item, the case item links exist for -- a plain meld
would not reach it, so the remedy adds a scan root:
```
mind unmeld '<identity>' --yes && mind meld '<repo-url>' --add-root '<root>' --learn '<kind>:<name>' --yes
```
(CLI-236, DSC-84). Either form drops the link instance and then melds the
whole repo, installing just that item together with its dependency closure
(DEP-30). Any error from the reachability scan -- an unreadable clone, a
malformed `mind.toml`, a version gate -- answers "not reachable" rather than
propagating, so the remedy falls back to the added-root form; the scan
runs only when a remedy is about to be printed, not on every link install.
The `<root>` is DERIVED from the link's own path, not fixed at `.`. An added
root is convention-scanned one level deep (flat child directories, plus the
kind containers `skills/`, `agents/`, `rules/`, `commands/`, `tools/`
directly under it), so the root that reaches the linked item is its parent
directory, or its grandparent when that parent is the kind's container:
`skills/foo` and a flat `foo` both give `.`, `agents/dev.md` gives `.`, while
`vendor/pkg/skills/foo` gives `vendor/pkg` and a flat `vendor/foo` gives
`vendor`. A fixed `.` would reach only an item at the repo root, so for
anything deeper the meld half would fail with `LearnPatternNoMatch` AFTER the
unmeld half had already succeeded -- the destroy-then-fail sequence this
two-branch remedy exists to prevent.
A file link (LNK-20) whose parent directory is NOT its kind's container is
the one shape with no working remedy: convention discovery finds a file item
only at `<root>/<kind>s/<name>.md` (there is no flat pass for file kinds, the
way there is for skills), so no `--add-root` value reaches it. Such a link is
reachable only as a link, so no command is printed: the error says plainly
that the item cannot be installed any other way, names why (the file sits
outside a conventional `<kind>s/` directory), and names the consumer's own
options -- drop the reference, ask upstream to move the file and re-link it,
or fork the repo, move the file there, and link the fork -- rather than only
the upstream-only remedy a repo author could act on. Printing a command that
unmelds first and then fails to install is the outcome this replaces.
The `unmeld` step is required. The link instance is registered before the
install runs, so it is registered on both paths when this is printed -- the
warning path goes on to install the skill, the error path installs nothing
-- and a bare `meld ... --learn` would therefore leave a second source for
the same repo, and collide with the name the link already installed on the
warning path (NS-43/CLI-33). Removing the instance first is what makes the
printed command work when pasted.
The `<identity>` is the instance's registered identity (LNK-4) and the
`<repo-url>` its recorded clone URL, never the deep link URL. The pattern is
kind-qualified (`<kind>:<name>`) with the linked item's own kind (LNK-21):
a bare name would additionally match a same-named item of another kind in the
repo, installing content the user never asked for. Each of the three values
is sanitized (DSC-95) and shell-quoted (CLI-225) before composition, and the
identity and the name are also glob-escaped: `--learn`'s pattern and
`unmeld`'s selector (CLI-28) both read `*`, `?`, and `[` as glob syntax, and
`is_safe_item_name` (DSC-71/DSC-96) permits those characters. A skill named
`pdf[x]` would otherwise make the pattern match some OTHER item, and --
since a link identity embeds the skill's repo path (LNK-4) -- would make the
raw identity compile as a pattern matching no source, stopping the remedy at
its first command. Escaping is a no-op for an identity or name with no
metacharacters, so both commands above are unchanged for the common case.
*Where it applies.* Wherever a link instance's item is installed: `learn
<url>` (LNK-6), the install pass of `meld <url>`, and a later `upgrade` of the
instance, so an upstream edit that adds a `requires` entry does not turn every
subsequent upgrade of that link into a hard failure.
*What it cannot fix.* One shape defeats the added-root form: a declared
inventory that offers a DIFFERENT item of the same bare name (say it declares
`vendor/review` while the link points at `skills/review`). The reachability
scan correctly answers "not reachable" there, since it matches by path, but
the added root then makes both items visible at once and the meld half fails
with the DSC-85 `DuplicateItem` guard ("appears under more than one scan
root"). The unmeld half has already run at that point, so this is the one
remaining case where the remedy leaves the user worse off than before pasting
it. Reaching that skill needs `--root` (replacing the inventory's roots)
rather than `--add-root` (composing with them), which is a choice about what
the whole source offers, not something a per-item remedy should make.
The added-root form otherwise reaches a skill an authoritative
`mind.toml` or `.claude-plugin` manifest does not declare (DSC-84) at any
depth, since the root is derived from the link's path. What it does not carry
over is the rest of the source's consumer configuration: the remedy melds the
repo fresh, so a `--namespace`, `--pin`, or `--flat-skills` the original link
was melded with is not reproduced, and neither are any `--add-root` values
beyond the one derived here. A link melded with extra scan roots therefore
needs them restated by hand on the meld half.
- `LNK-19` A `requires:` entry dropped under LNK-18 is recorded on the installed
item (`dropped_requires` in the manifest) rather than existing only as the
install-time warning. The warning scrolls away, is reduced to a summary line by
the TUI's output capture, and goes to stderr under `--json`, so without a
durable record an item can carry a silently absent dependency with no way to
discover it later. The record is what `recall <item>` shows as a `dropped`
line, what `recall <item> --json` carries as a `dropped_requires` array (absent
when empty), and what `introspect` reports as a `dropped-requires` issue naming
the item and the entry. `introspect --fix` does not repair it: the only fix is
to replace the link with the whole repo, which changes which sources are
registered and so is the user's call. The field is re-derived on every install
and upgrade of the item, so it always describes the version on disk; it is
empty (and omitted from `manifest.json`) for every ordinary install, so an item
installed by an older binary, or from a non-link source, reads back unchanged.
- `LNK-9` Namespacing and collisions are unchanged: the effective prefix is
the alias or `[source].namespace` (NS rules), and collisions with installed
items surface through the existing checks (NS-41, NS-43, CLI-33). Source
install hooks are disclosed and consent-gated as at any meld (HOOK-20,
HOOK-50..60).
## Safety and policy
- `LNK-10` The `<path>` must be a safe relative path (the DSC-71..73 rule: not
absolute, not `~`-rooted, no `..` component, no NUL); the `<ref>` is
validated as a git ref value (DSC-66). A violation is `InvalidRepoSpec` at
parse, before any clone.
- `LNK-11` Managed-policy allowlist matching (POL-11, POL-36) uses the base
repo identity `host/owner/repo`, not the extended instance identity: a
policy that allows the repo allows links into it. `require-pinned` (POL-20)
evaluates the instance's effective pin as usual, so a branch-ref link is
refused under a pinned policy.
## Display
- `LNK-12` `recall --sources` and the probe source view show a link instance
under its full `#`-suffixed identity. Compare and browse URLs (CLI-176,
HOOK-24) derive from the base repo and keep their host-gated shapes.
## Reproduction
- `LNK-13` `dump` emits a link instance as a `[discover].sources` entry: its
`source` is a `tree/<ref>/<path>` deep URL reconstructed from the recorded
`host`/`owner`/`repo` (a `file://`-prefixed path for a local instance,
matching LNK-1's local form) and `item_path`, with the recorded commit as
the `<ref>` segment. The entry also carries `pin-ref = <commit>` (dump.md
DUMP-1/DUMP-4), the SAME commit as the URL's own ref: `pin-ref` is what
actually pins the reproduction (a curator pin outranks an item link's own
URL-ref pin, DSC-65 over LNK-3), and the URL's ref exists to keep the URL
syntactically complete (a `tree`/`blob` link always needs one) and in
agreement with `pin-ref` rather than a possibly-since-moved branch tip. The
entry's `namespace` carries the recorded identity alias (`as_alias`,
STO-58), not a display-only prefix, so re-melding reproduces the exact
`host/owner/repo#path@alias` instance (LNK-4) an aliased link was
registered under; an unaliased link emits no `namespace` and, on re-meld,
naturally re-reads the repo's own `[source].prefix` (if any) exactly as the
original meld did. `roots`/`add-roots`/`flat-skills` are never emitted for
a link entry: a link instance's catalog is exactly its one skill (LNK-7),
so convention scan roots do not apply to it. A link instance always has a
recorded commit once registered (LNK-3/LNK-4: its checkout point is never
the default branch, so `meld`/`learn`/`sync` always resolve and record one
before it is added to the registry); the only way `dump` cannot reconstruct
the entry is a hand-edited registry with a missing commit, in which case
`dump` skips the instance with a note rather than emit an unpinned entry.
The reconstructed URL is always the `https://` form built from the recorded
`host`/`owner`/`repo`, never the source's own `url` field: `prefer_ssh`
(DSC-66) may have rewritten `url` to the `git@host:owner/repo` SSH form
after the meld, and that form cannot express a `tree/<ref>/<path>` link
suffix, so `dump` always derives the link URL from the identity parts
rather than echoing `url` verbatim.
- `LNK-23` A file link instance (LNK-20) is emitted with the `blob/<ref>/<path>`
shape instead of `tree/<ref>/<path>`: a forge blob URL is what names a file,
and it is the shape the link was pasted in. The entry also carries
`kind = "<kind>"` when, and only when, the instance recorded an explicit kind
(LNK-22); a kind that came from the containing directory or the file's
frontmatter re-resolves identically from the pinned clone, so emitting it
would add nothing. Everything else about the entry is LNK-13 unchanged.