Skip to content

feat: version pinning for skills and plugins (@version suffix and --pin flag) #372

@christso

Description

@christso

Priority: high · Unlocks reproducible installs across team members and across agent sessions.

Problem

`allagents` has no way to pin a skill or plugin to a specific Git ref. `src/utils/plugin-path.ts::parseGitHubUrl` recognizes `branch` (via `/tree/` in a URL) but doesn't accept an `owner/repo@ref` shorthand, and `@` in install args is already taken by the marketplace shorthand (`my-plugin@official`). Consequences:

  • Two team members running `allagents update` on different days can get different skill bodies.
  • An agent running in CI cannot guarantee reproducible session-startup state.
  • There is no audit trail for which version of a skill a deployed agent was running.

By contrast `gh skill install` supports both inline `@v1.2.0` and `--pin ` (mutually exclusive). Implementation reference: `pkg/cmd/skills/install/install.go::parseSkillFromOpts` and `resolveVersion` in `cli/cli`.

Current behavior

```bash
$ allagents plugin install --help | grep -E 'pin|version|@'
$ allagents plugin install my-plugin@official # @ means marketplace, NOT version
$ allagents plugin install my-plugin@official --scope user

No --pin flag exists. Passing a tag-style @ref does not work:

$ allagents plugin install owner/repo@v1.2.0

Treats v1.2.0 as a marketplace name and fails to resolve

workspace.yaml has no pin field; sync-state records no resolved ref:

$ cat .allagents/sync-state.json
{
"version": 1,
"lastSync": "2026-05-12T10:54:56.311Z",
"files": { "claude": [] },
"mcpServers": { "claude": [] }
}
```

Expected behavior

```bash

Inline pin

$ allagents skills add foo --from owner/repo@v1.2.0
✓ Added foo from owner/repo (pinned to v1.2.0)

Explicit flag (equivalent)

$ allagents skills add foo --from owner/repo --pin v1.2.0
✓ Added foo from owner/repo (pinned to v1.2.0)

Inline + --pin → conflict

$ allagents skills add foo --from owner/repo@v1.2.0 --pin abc1234
Error: cannot combine inline @Version with --pin

workspace.yaml form

plugins:

  • source: owner/repo
    pin: v1.2.0 # new optional field

sync-state.json records the resolved ref + SHA so subsequent updates can detect drift

$ cat .allagents/sync-state.json | jq .sources
{
"owner/repo": {
"pluginSpec": "owner/repo",
"resolvedRef": "v1.2.0",
"resolvedSha": "abc1234...",
"pinnedRef": "v1.2.0"
}
}
```

Verification gate (must pass before closing)

```bash
set -euo pipefail

bun run build
WS=$(mktemp -d)
cd "$WS"
allagents workspace init --client claude

(1) Inline @Version is accepted (use any small public skills repo with a tagged release)

allagents skills add brainstorming --from anthropics/superpowers@v0.1.0
test -d .agents/skills/brainstorming/

(2) --pin flag is accepted and equivalent

rm -rf .agents
allagents skills add brainstorming --from anthropics/superpowers --pin v0.1.0
test -d .agents/skills/brainstorming/

(3) Inline + --pin together is rejected

! allagents skills add brainstorming --from anthropics/superpowers@v0.1.0 --pin v0.1.0

(4) Resolved ref is recorded in sync-state

jq -e '.sources["anthropics/superpowers"].pinnedRef == "v0.1.0"' .allagents/sync-state.json
jq -e '.sources["anthropics/superpowers"].resolvedSha | length >= 7' .allagents/sync-state.json

(5) workspace.yaml round-trips a pin field

cat > .allagents/workspace.yaml <<YAML
clients: [claude]
plugins:

  • source: anthropics/superpowers
    pin: v0.1.0
    YAML
    allagents update
    jq -e '.sources["anthropics/superpowers"].pinnedRef == "v0.1.0"' .allagents/sync-state.json

cd / && rm -rf "$WS"
```

All five checks must pass. (Substitute the example repo/tag with a stable public alternative if needed; the integration test should use a known stable source.)

Implementation notes

  • `src/utils/plugin-path.ts::parseGitHubUrl`: extend to recognize `owner/repo@` shorthand. Distinguish from `name@marketplace` by checking whether the left side looks like `owner/repo` (contains a slash) — `@` after a slash is a Git ref; `@` without a slash is a marketplace.
  • `src/cli/commands/plugin-skills.ts::addCmd`: add `--pin ` option. Validate mutual exclusivity with inline `@version` in the positional.
  • `src/cli/commands/plugin.ts::installCmd` (`plugin install`): same `--pin` flag.
  • `src/core/plugin.ts::fetchPlugin`: extend signature to accept `{ ref?: string }` and return the resolved ref + SHA. The shallow-clone path needs to clone the specific ref (or fetch + checkout).
  • `src/models/workspace-config.ts::PluginEntrySchema`: add an optional `pin: z.string().optional()` field to the object form.
  • `src/models/sync-state.ts::SyncStateSchema`: add an optional `sources` record keyed by plugin spec with `{ pluginSpec, resolvedRef, resolvedSha, pinnedRef? }`. Don't break the existing `files` field — additive only.
  • This issue partially overlaps with content-hash tracking; the `sources` block above is designed to be a strict subset of the schema proposed in the companion lockfile-hashes issue, so the two can land together cleanly.

Refs

  • Reference impl: `cli/cli` `pkg/cmd/skills/install/install.go::parseSkillFromOpts`, `resolveVersion`.
  • Companion wiki page: `concepts/allagents-vs-gh-skill.md` § "Pinning vs declarative re-sync".
  • Related issue: content-hash tracking in sync-state (filed separately).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions