Skip to content
296 changes: 296 additions & 0 deletions docs/plans/2026-08-10-001-feat-skill-update-deletion-handling-plan.md

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions docs/src/content/docs/docs/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ allagents plugin marketplace update [name]
allagents skill list [--scope <scope>]
allagents skill remove <skill> [--plugin <plugin>] [--scope <scope>]
allagents skill add <skill> [--from <source>] [--plugin <plugin>] [--scope <scope>]
allagents skill update [skill...] [--scope <scope>] [--yes]
```

### plugin list
Expand Down Expand Up @@ -257,6 +258,50 @@ allagents skill add brainstorming --plugin superpowers

After enabling, the skill is removed from `disabledSkills` and sync is run to restore it.

### skill update

Check installed remote skills for upstream changes, update surviving skills, and safely reconcile skills that were deleted upstream.

```bash
allagents skill update
allagents skill update code-review glow-api
allagents skill update --scope user
allagents skill update --scope all
allagents skill update --yes
allagents --json skill update --scope project
```

| Argument or flag | Description |
|------|-------------|
| `[skill...]` | Update only the physical sources containing the named enabled skills. Names, qualified paths, and `plugin:path` selectors are accepted. All enabled siblings sharing a selected source are still checked for deletion safety. |
| `-s, --scope <scope>` | Scope: `project` (default when a project config exists), `user`, or `all`. |
| `-y, --yes` | Run without prompts. This does **not** authorize deletion: a source with upstream deletions is retained and skipped. |

AllAgents performs a read-only preflight against disposable checkouts before changing a config or plugin cache. When an installed skill has disappeared upstream, interactive terminals list the affected skill copies and ask once for the shared physical source:

- **Yes** removes the deleted skill selectors or standalone skill entries, advances that source to the inspected revision, and updates its surviving skills.
- **No** keeps the local copies and skips every update from that physical source, including survivor updates.
- Cancelling any confirmation stops the whole operation before the first mutation.

Non-interactive runs—including `--yes`, redirected input/output, CI, and `--json`—behave like **No** for sources with deletions. This makes unattended updates safe by default. Sources without deletion candidates can still update normally.

Plugin subpaths, marketplace entries, and project/user installs can share one physical cache. A decision therefore applies to the complete connected cache unit rather than only the config spelling that selected it. If an update in one scope would affect a deleted skill in an unselected scope, AllAgents keeps the cache unchanged and asks you to rerun with `--scope all`. A failed or declined unit does not prevent independent physical sources from updating.

After accepted caches advance, AllAgents syncs affected clients from those exact cached revisions in offline mode. Declined caches are not refreshed indirectly by the final sync.

With `--json`, each physical source result has one of these statuses:

| Status | Meaning |
|------|-------------|
| `updated` | The source advanced and surviving skills were synced; no deletion was required. |
| `removed` | Confirmed deleted skills were removed and surviving skills were updated. |
| `retained` | Deleted local copies were kept, so the complete shared source was left unchanged. |
| `skipped` | A physical update unit was intentionally skipped without an operational failure. |
| `failed` | Preflight, transaction, or offline sync failed for this source. Independent sources may still succeed. |
| `cancelled` | Confirmation was cancelled before any changes were made. |

Local plugin sources are listed separately in `data.skippedLocalSources`. The JSON summary reports per-skill `updated`, `removed`, and `retained` counts plus physical-source `skipped`, `failed`, and `cancelled` counts. Usage errors exit with status 2, operational failures with status 1, and safe retained/skipped results with status 0.

:::tip
`allagents skill ...` is the canonical singular form. The plural alias
`allagents skills ...` is also accepted and produces identical output.
Expand Down
19 changes: 15 additions & 4 deletions src/cli/agent-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
skillsAddMeta,
skillsRemoveMeta,
skillsSearchMeta,
skillsUpdateMeta,
} from './metadata/plugin-skills.js';

const allCommands: AgentCommandMeta[] = [
Expand All @@ -38,6 +39,7 @@ const allCommands: AgentCommandMeta[] = [
skillsAddMeta,
skillsRemoveMeta,
skillsSearchMeta,
skillsUpdateMeta,
updateMeta,
];

Expand Down Expand Up @@ -82,14 +84,23 @@ function resolveAlias(commandPath: string): string {
}

/**
* Look up a meta by the runtime command path (e.g. "skills list").
* Look up metadata by a runtime command path (e.g. "skill update foo").
* Resolves deprecated aliases (e.g. "workspace status" -> "status").
* Used by index.ts to validate `--json=<fields>` against the per-command
* allowlist before dispatching.
* allowlist before dispatching. A longest-prefix match allows command metadata
* to resolve when positional arguments follow the command tokens.
*/
export function findMetaByCommand(commandPath: string): AgentCommandMeta | undefined {
export function findMetaByCommand(
commandPath: string,
): AgentCommandMeta | undefined {
if (!commandPath) return undefined;
return allCommands.find((c) => c.command === resolveAlias(commandPath));
const resolved = resolveAlias(commandPath);
const exact = allCommands.find((command) => command.command === resolved);
if (exact) return exact;

return allCommands
.filter((command) => resolved.startsWith(`${command.command} `))
.sort((a, b) => b.command.length - a.command.length)[0];
}

export function printAgentHelp(args: string[], version: string): void {
Expand Down
22 changes: 7 additions & 15 deletions src/cli/commands/plugin-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ import {
restPositionals,
string,
} from 'cmd-ts';
import {
CONFIG_DIR,
WORKSPACE_CONFIG_FILE,
getHomeDir,
} from '../../constants.js';
import { getHomeDir } from '../../constants.js';
import {
addMarketplace,
findMarketplace,
Expand Down Expand Up @@ -87,21 +83,16 @@ import {
skillsSearchMeta,
} from '../metadata/plugin-skills.js';
import { removeInstalledSkill } from '../skill-removal.js';

/**
* Check if a directory has a project-level .allagents config
*/
function hasProjectConfig(dir: string): boolean {
return existsSync(join(dir, CONFIG_DIR, WORKSPACE_CONFIG_FILE));
}
import { hasProjectSkillConfig } from '../skill-update.js';
import { skillUpdateCmd } from './skill-update.js';

/**
* Determine effective scope when no --scope flag is provided.
* Defaults to user scope unless cwd has a project config.
*/
function resolveScope(cwd: string): 'user' | 'project' {
if (isUserConfigPath(cwd)) return 'user';
if (hasProjectConfig(cwd)) return 'project';
if (hasProjectSkillConfig(cwd)) return 'project';
return 'user';
}

Expand Down Expand Up @@ -291,7 +282,7 @@ const listCmd = command({
handler: async ({ scope }) => {
try {
const cwd = process.cwd();
const inProjectDir = !isUserConfigPath(cwd) && hasProjectConfig(cwd);
const inProjectDir = !isUserConfigPath(cwd) && hasProjectSkillConfig(cwd);

// Resolve which scopes to display
const showUser = scope !== 'project';
Expand Down Expand Up @@ -2358,7 +2349,7 @@ async function installFromSearch(repos: string[]): Promise<boolean> {
const installableRepos: string[] = [];

for (const repo of repos) {
const isInstalledProject = hasProjectConfig(workspacePath)
const isInstalledProject = hasProjectSkillConfig(workspacePath)
? await hasPlugin(repo, workspacePath)
: false;
const isInstalledUser = await hasUserPlugin(repo);
Expand Down Expand Up @@ -2614,5 +2605,6 @@ export const skillsCmd = conciseSubcommands({
remove: removeCmd,
add: addCmd,
search: searchCmd,
update: skillUpdateCmd,
},
});
Loading