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

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions