Skip to content
Open
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,14 @@

**Migration**: Re-run `allagents mcp update` or `allagents update` after upgrading so synced client configs are regenerated with `mcp proxy`.

- **Plugin Git ref terminology**: Renamed workspace plugin `pin` to `ref`, CLI
`--pin` to `--ref`, and sync-state `pinnedRef` to `requestedRef`. Inline
`owner/repo@ref` sources are unchanged.

**Migration**: Replace `pin:` with `ref:` in plugin objects and `--pin` with
`--ref` in scripts. This is a clean cutover; the old names are not accepted.


## [1.0.0] - 2026-03-13

### Breaking Changes
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,7 +40,7 @@ Skill installations are source-driven: one plugin or repository can provide seve

**Deletion safety**

- R4. Before persistent refresh, AllAgents compares its enabled pre-update skill inventory with full-depth discovery from a temporary checkout of each physical refresh unit. Inventory is derived from raw plugin entries so object-form `pin`, inline refs, marketplace plugin selection, configured selector, scope, and qualified skill subpath are retained. A unit is the connected dependency graph of canonical remote/ref/cache nodes required to resolve its entries: one node for a direct source, a marketplace node for embedded entries, or marketplace plus external-repository nodes for external entries. Shared nodes join aliases, marketplace siblings, and cross-scope installs into one decision boundary.
- R4. Before persistent refresh, AllAgents compares its enabled pre-update skill inventory with full-depth discovery from a temporary checkout of each physical refresh unit. Inventory is derived from raw plugin entries so object-form `ref`, inline refs, marketplace plugin selection, configured selector, scope, and qualified skill subpath are retained. A unit is the connected dependency graph of canonical remote/ref/cache nodes required to resolve its entries: one node for a direct source, a marketplace node for embedded entries, or marketplace plus external-repository nodes for external entries. Shared nodes join aliases, marketplace siblings, and cross-scope installs into one decision boundary.
- R5. Fetch, authentication, malformed manifest, missing declared root, and incomplete discovery are reported as source failures and never interpreted as upstream deletions. An existing resolved root with a valid empty inventory may classify installed skills as deleted. A valid marketplace that no longer declares an installed plugin is a distinct authoritative removal: confirmation explicitly removes that entire config entry and every AllAgents-managed artifact from it, not only its skills; declining retains the old marketplace checkout and local artifacts.
- R6. Interactive deletion candidates are grouped by physical refresh unit, list every affected selected-scope installation, and require one affirmative confirmation per unit. The prompt states that Yes removes the named skills and updates survivors, while No keeps them and skips every update backed by that unit. Marketplace-entry removal uses separate copy naming the whole plugin entry and managed-artifact consequence.
- R7. Confirmed deletions use an atomic per-scope/per-refresh-unit configuration transform, preserving unrelated plugin-entry fields and user-owned client files. All new config bytes are prepared before any rename; on any transform, write, or exact-revision checkout-node update failure, every config and cache node belonging to that unit is restored to its original bytes/revision and the unit is not synced.
Expand DownExpand Up@@ -93,7 +93,7 @@ Skill installations are source-driven: one plugin or repository can provide seve

- KTD1. **Preflight in temporary checkouts.** Resolve each unique remote source into a disposable checkout and discover skill paths there before touching persistent caches. This preserves the meaning of a No answer under symlink installs and follows the safety shape of `vercel-labs/skills` without copying its lock-file model.
- KTD2. **Skip an affected source on retention.** AllAgents cannot update surviving skills from a repository-level cache while truthfully retaining a deleted symlinked skill without adding a new archival subsystem. Skipping only that source is the smallest deterministic ownership-safe behavior.
- KTD3. **Treat the connected checkout graph as the transaction boundary.** Build nodes from canonical remote, effective ref/pin, and cache path; connect marketplace manifests to embedded or external plugin checkouts; then attach every project/user/plugin entry consuming any shared node. A retained or out-of-scope impacted entry blocks the entire connected unit.
- KTD3. **Treat the connected checkout graph as the transaction boundary.** Build nodes from canonical remote, effective ref, and cache path; connect marketplace manifests to embedded or external plugin checkouts; then attach every project/user/plugin entry consuming any shared node. A retained or out-of-scope impacted entry blocks the entire connected unit.
- KTD4. **Separate orchestration from presentation.** A core/service result describes per-scope, per-refresh-unit, and per-skill outcomes. The cmd-ts handler owns Clack prompts and human/JSON rendering, enabling focused unit tests without terminal coupling.
- KTD5. **Batch through configuration ownership.** Prepare every selector mutation for a refresh unit in memory, validate the complete result, then replace affected config files atomically. Do not recursively invoke `skill remove`, which would repeat scans, prompts, syncs, and outros.
- KTD6. **Safe automation retains.** Match upstream's important safety semantic: `--yes` means no questions, not permission to delete. JSON and non-TTY execution follow the same retention rule.
Expand DownExpand Up@@ -165,7 +165,7 @@ flowchart TB
- A marketplace plugin may be embedded or point to an external repository. Resolve the temporary discovery root with the same manifest rules as persistent update and test both shapes.
- External marketplace entries form a compound unit. Record marketplace and external checkout SHAs, advance external nodes before the manifest node, and roll back changed nodes in reverse order on failure.
- Direct GitHub subpaths, marketplace siblings, and project/user entries can share one cache checkout. Canonicalize the physical unit and inventory both scopes before making any decision.
- Object `pin` and inline `@ref` can select a different revision than the raw source. Share one effective-source resolver with normal sync and make the ref part of the unit key.
- Object-form and inline `@ref` selectors can choose a different revision than the raw source. Share one effective-source resolver with normal sync and make the ref part of the unit key.
- Snapshot discovery can seed the process-level fetch cache with offline results. Keep preflight cache-independent or reset the fetch cache before persistent refresh.
- A successful fresh clone can currently collapse to a skipped plugin-update result. Decide sync eligibility from successful accepted refresh work, not only `action === updated`.
- Multiple removed siblings can leave an empty allowlist if each helper sees the original snapshot. Compute the whole config transform in memory, write once, and test last-skill removal.
Expand DownExpand Up@@ -211,7 +211,7 @@ flowchart TB
- **Test scenarios:**
- Multiple allowlisted deletions remove selectors sequentially; the last deletion preserves a source with commands/hooks/MCP through an empty allowlist and removes only a standalone skill source.
- A qualified nested selector is removed without affecting another skill with the same leaf name.
- Object fields for clients, install mode, artifact exclusions, and pin survive partial pruning.
- Object fields for clients, install mode, artifact exclusions, and ref survive partial pruning.
- A blocklist/implicit source does not gain an exclusion for an upstream-absent skill.
- A confirmed marketplace-entry removal deletes the complete plugin config entry, preserves unrelated entries, and lets an empty-plan offline sync purge its previously tracked artifacts.
- A validation/write failure in the second deletion leaves all config files in that refresh unit unchanged and prevents cache refresh, while unrelated units continue.
Expand Down
20 changes: 20 additions & 0 deletions docs/src/content/docs/docs/reference/configuration.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -92,6 +92,26 @@ allagents plugin install superpowers@marketplace --skill brainstorming
The top-level `disabledSkills` and `enabledSkills` arrays are deprecated (v1 format). They still work as a fallback for configs that haven't migrated, but new configs should use the inline `skills` field on plugin entries instead.
:::

### Plugin Git Refs

GitHub plugin entries can select a release tag or branch with `ref`:

```yaml
plugins:
- source: https://github.com/EveryInc/compound-engineering-plugin
ref: compound-engineering-v3.23.3
```

This is equivalent to the inline `owner/repo@<ref>` source form. An inline ref
takes precedence, so do not configure both forms on the same entry. Local paths,
marketplace plugin selectors, and GitHub `/tree/` or `/blob/` URLs already carry
their own location and ignore object-form `ref`.

`ref` selects a symbolic Git ref; it is not a content-integrity lock. Sync state
records the resolved commit SHA for provenance, but AllAgents does not currently
reject a tag or branch that later resolves to a different commit.


### Workspace File Sync

The `workspace` section enables syncing files from external sources to your workspace root.
Expand Down
74 changes: 37 additions & 37 deletions src/cli/commands/plugin-skills.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -97,24 +97,24 @@ function resolveScope(cwd: string): 'user' | 'project' {
}

/**
* Record per-source provenance (resolvedRef + resolvedSha + optional pin)
* Record per-source provenance (resolvedRef + resolvedSha + optional requested ref)
* into sync-state for the given install. Identity for git-based plugins is
* `url + ref`; `resolvedSha` (from `git rev-parse HEAD` after fetch) gives
* content identity, so per-skill content hashing is unnecessary.
*
* The source key is the spec with any `@<ref>` suffix stripped so all installs
* of `owner/repo` map to one entry regardless of pin.
* of `owner/repo` map to one entry regardless of requested ref.
*
* No-op for non-GitHub sources (local paths, marketplace shorthand) since we
* can't resolve a SHA from them.
*/
async function recordSourceProvenance(opts: {
from: string;
pinnedRef?: string | undefined;
requestedRef?: string | undefined;
workspacePath: string;
isUser: boolean;
}): Promise<void> {
const { from, pinnedRef, workspacePath, isUser } = opts;
const { from, requestedRef, workspacePath, isUser } = opts;
if (!isGitHubUrl(from)) return;
const parsed = parseGitHubUrl(from);
if (!parsed) return;
Expand All@@ -131,7 +131,7 @@ async function recordSourceProvenance(opts: {
pluginSpec: key,
resolvedRef: fetchResult.resolvedRef ?? parsed.branch ?? 'HEAD',
resolvedSha: fetchResult.resolvedSha,
...(pinnedRef && { pinnedRef }),
...(requestedRef && { requestedRef }),
});
}

Expand DownExpand Up@@ -1699,11 +1699,11 @@ const addCmd = command({
description:
'Comma-separated skill names to install when the positional argument is a plugin source (e.g., owner/repo --skill foo,bar)',
}),
pin: option({
ref: option({
type: optional(string),
long: 'pin',
long: 'ref',
description:
'Pin the plugin to a specific Git ref (tag, branch, or SHA). Mutually exclusive with inline @ref in --from.',
'Git ref to use for the plugin (tagor branch). Mutually exclusive with inline @ref in --from.',
}),
list: flag({
long: 'list',
Expand All@@ -1721,7 +1721,7 @@ const addCmd = command({
plugin,
from: fromArg,
skillFlag,
pin,
ref,
list,
all,
}) => {
Expand DownExpand Up@@ -1766,33 +1766,33 @@ const addCmd = command({
process.exit(1);
}

// Resolve --pin together with inline @ref. Three legal states:
// • --pin only → splice into fromArg
// • inline @ref → leave fromArg alone, remember pinnedRef
// • neither → no pin
// Mutex: --pin combined with inline @ref is rejected.
let pinnedRef: string | undefined;
if (pin || fromArg) {
// Resolve --ref together with inline @ref. Three legal states:
// • --ref only → splice into fromArg
// • inline @ref → leave fromArg alone, remember requestedRef
// • neither → use the source's default branch
// Mutex: --ref combined with inline @ref is rejected.
let requestedRef: string | undefined;
if (ref || fromArg) {
const inlineRef = fromArg ? extractInlineRef(fromArg) : undefined;
if (pin && inlineRef) {
if (ref && inlineRef) {
const error =
'Cannot combine inline @version in --from with --pin. Use one or the other.';
'Cannot combine inline @ref in --from with --ref. Use one or the other.';
if (isJsonMode()) {
jsonOutput({ success: false, command: 'skill add', error });
process.exit(1);
}
console.error(`Error: ${error}`);
process.exit(1);
}
if (pin && fromArg) {
// Splice the pin into the source string so downstream parseGitHubUrl
if (ref && fromArg) {
// Splice the ref into the source string so downstream parseGitHubUrl
// picks it up as the branch/tag.
fromArg = `${fromArg}@${pin}`;
pinnedRef = pin;
fromArg = `${fromArg}@${ref}`;
requestedRef = ref;
} else if (inlineRef) {
pinnedRef = inlineRef;
} else if (pin && !fromArg) {
const error = '--pin requires --from to specify a plugin source.';
requestedRef = inlineRef;
} else if (ref && !fromArg) {
const error = '--ref requires --from to specify a plugin source.';
if (isJsonMode()) {
jsonOutput({ success: false, command: 'skill add', error });
process.exit(1);
Expand DownExpand Up@@ -1938,10 +1938,10 @@ const addCmd = command({
process.exit(1);
}

// Record per-source ref/SHA + optional pin for the --all install path.
// Record per-source ref/SHA provenance for the --all install path.
await recordSourceProvenance({
from: fromArg,
pinnedRef,
requestedRef,
workspacePath: workspacePathAll,
isUser: isUserAll,
});
Expand All@@ -1957,7 +1957,7 @@ const addCmd = command({
copied: installResult.syncResult.totalCopied,
failed: installResult.syncResult.totalFailed,
},
...(pinnedRef && { pinnedRef }),
...(requestedRef && { requestedRef }),
},
});
return;
Expand DownExpand Up@@ -2023,7 +2023,7 @@ const addCmd = command({
if (succeeded.length > 0) {
await recordSourceProvenance({
from: fromArg,
pinnedRef,
requestedRef,
workspacePath: workspacePathSel,
isUser: isUserSel,
});
Expand All@@ -2038,7 +2038,7 @@ const addCmd = command({
source: fromArg,
installed: succeeded,
failed: failures,
...(pinnedRef && { pinnedRef }),
...(requestedRef && { requestedRef }),
},
...(allFailed && {
error: failures.map((f) => `${f.skill}: ${f.error}`).join('; '),
Expand All@@ -2051,8 +2051,8 @@ const addCmd = command({
for (const f of failures) {
console.error(`Error installing '${f.skill}': ${f.error}`);
}
if (pinnedRef && succeeded.length > 0) {
console.log(`Pinned to ${pinnedRef}.`);
if (requestedRef && succeeded.length > 0) {
console.log(`Using ref ${requestedRef}.`);
}
if (allFailed) process.exit(1);
return;
Expand DownExpand Up@@ -2133,10 +2133,10 @@ const addCmd = command({
process.exit(1);
}

// Record per-source ref/SHA + optional pin in sync-state.
// Record per-source ref/SHA provenance in sync-state.
await recordSourceProvenance({
from,
pinnedRef,
requestedRef,
workspacePath,
isUser,
});
Expand All@@ -2149,14 +2149,14 @@ const addCmd = command({
skill,
plugin: installFromResult.pluginName,
syncResult: installFromResult.syncResult,
...(pinnedRef && { pinnedRef }),
...(requestedRef && { requestedRef }),
},
});
return;
}

if (pinnedRef) {
console.log(`Pinned to ${pinnedRef}.`);
if (requestedRef) {
console.log(`Using ref ${requestedRef}.`);
}
return;
}
Expand Down
8 changes: 8 additions & 0 deletions src/cli/metadata/plugin-skills.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -200,6 +200,7 @@ export const skillsAddMeta: AgentCommandMeta = {
examples: [
'allagents skill add ReScienceLab/opc-skills',
'allagents skill add reddit --from ReScienceLab/opc-skills',
'allagents skill add reddit --from ReScienceLab/opc-skills --ref v2',
'allagents skill add NousResearch/hermes-agent --skill llm-wiki',
'allagents skill add NousResearch/hermes-agent --skill llm-wiki,dogfood',
'allagents skill add NousResearch/hermes-agent --list',
Expand DownExpand Up@@ -249,6 +250,12 @@ export const skillsAddMeta: AgentCommandMeta = {
description:
'Comma-separated skill names to install when the positional is a plugin source (e.g., `skill add owner/repo --skill foo,bar`).',
},
{
flag: '--ref',
type: 'string',
description:
'Git tag or branch to use for the source. Mutually exclusive with an inline @ref.',
},
{
flag: '--list',
short: '-l',
Expand All@@ -265,5 +272,6 @@ export const skillsAddMeta: AgentCommandMeta = {
skill: 'string',
plugin: 'string',
syncResult: { copied: 'number', failed: 'number' },
requestedRef: 'string?',
},
};
9 changes: 5 additions & 4 deletions src/core/marketplace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -537,7 +537,7 @@ export async function addMarketplace(

// Check if already registered by source location (idempotent)
// Use the full location including branch — each branch is a separate marketplace.
// For branch-pinned registrations, effectiveBranch overrides parsed.location.
// For branch-specific registrations, effectiveBranch overrides parsed.location.
const sourceLocation = (() => {
if (parsed.type === 'github') {
const { owner, repo } = parseLocation(parsed.location);
Expand DownExpand Up@@ -995,7 +995,7 @@ export async function updateMarketplace(

let targetBranch: string;
if (storedBranch) {
// Branch-pinned marketplace: use stored branch directly
// Branch-specific marketplace: use stored branch directly
targetBranch = storedBranch;
} else {
// Default branch marketplace: detect default branch
Expand DownExpand Up@@ -1814,10 +1814,11 @@ async function autoRegisterMarketplace(
*
* `plugin@marketplace` → true (plain marketplace shorthand)
* `plugin@owner/repo[/sub]` → true (marketplace by GitHub repo)
* `owner/repo@ref[/sub]` → false (GitHub plugin with `@ref` version pin)
* `owner/repo@ref[/sub]` → false (GitHub plugin with an inline ref)
*
* The disambiguation: when the segment before the last `@` itself contains a
* `/`, the spec is an `owner/repo` GitHub URL with a `@ref` pin, not a
* `/`, the spec is an `owner/repo` GitHub URL with an inline ref, not a
*
* plugin@marketplace pair. Plugin names in marketplaces are bare identifiers
* without slashes.
*/
Expand Down
Loading