Skip to content

feat(skill): allagents skills update for per-skill refresh#383

Merged
christso merged 1 commit into
mainfrom
feat/375-skill-update
May 12, 2026
Merged

feat(skill): allagents skills update for per-skill refresh#383
christso merged 1 commit into
mainfrom
feat/375-skill-update

Conversation

@christso
Copy link
Copy Markdown
Contributor

Summary

Adds allagents skills update [skill] for per-skill refresh, decoupled from the heavier allagents update workspace sync. Walks the sources map in .allagents/sync-state.json, fetches each plugin (cached), and compares upstream content hashes against the recorded ones.

Behaviour, mirroring gh skill update:

  • --all — apply without prompting (default in non-TTY shells).
  • --force — re-stamp source/skill rows even when hashes match (refreshes updatedAt).
  • --dry-run — read-only report.
  • --unpin — clear pinnedRef before resolving so the default branch is picked up.
  • --scope — project (default) or user.

JSON envelope:

{ "success": true, "command": "skills update", "data": { "checked": N, "updates": [...], "upToDate": [...], "pinned": [...] } }

Depends on the sync-state shape introduced by #374 (per-skill contentHash) and the pinning shape from #372 (pinnedRef). The branch is rebased onto both so the diff is self-contained; if those land first this PR will rebase trivially.

Test plan

  • bun run build, bun run typecheck
  • bun test — 1195 pass / 0 fail (agent-help fixture updated for the new meta)
  • Verification gate from feat: allagents skill update for per-skill refresh (separate from workspace sync) #375 (using obra/superpowers@v3.1.0 as the stable public alt):
    • Dry-run does not mutate any file in .claude, .agents, or .allagents (sha256 of the tree is unchanged).
    • --json skills update --dry-run produces an envelope with command == "skills update" and data.{upToDate,updates,pinned} all present.
    • --all applies without prompting in non-TTY.
    • A pinned skill is listed under pinned; --unpin then --all clears pinnedRef in sync-state.
    • --force --all keeps the same contentHash (no upstream drift) but rewrites updatedAt.
    • Non-interactive without --all does not block (10-second timeout).

Closes #375

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 12, 2026

Deploying allagents with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9ab1fac
Status:⚡️  Build in progress...

View logs

@christso
Copy link
Copy Markdown
Contributor Author

Code Review: ⚠️ APPROVE WITH SIGNIFICANT CONCERNS (one bordering on blocker)

This PR sits on top of #381 + #382 and adds the updateCmd. The bulk of the diff is inherited; my review focuses on the incremental updateCmd logic and the user-visible behavior of skills update.

Verification gate (#375)

Strictly by the gate's literal jq checks, all six pass:

  • (1) Dry-run doesn't mutate .claude / .agents / .allagents
  • (2) JSON envelope command == "skills update", data.{upToDate,updates,pinned} present ✓
  • (3) --all applies in non-TTY ✓ (applyImplicitly = all || !process.stdin.isTTY)
  • (4) Pinned skipped without --unpin; --unpin --all clears pinnedRef
  • (5) --force --all keeps contentHash, moves updatedAt
  • (6) Non-interactive without --all doesn't block ✓

CLAUDE.md compliance

  • Conventional commit ✓
  • No Co-Authored-By
  • Test coverage thin — see below.

Blocker-adjacent concern

updateCmd only updates sync-state metadata. It does NOT refresh skill files on disk.

In the apply branch (applyImplicitly):

await upsertSyncStateSource(...);
if (upstreamHash) {
  await upsertSyncStateSkill(... contentHash: upstreamHash, updatedAt: now ...);
}
updates.push({ ..., status: 'updated' });

There is no file copy, no syncWorkspace call, nothing that propagates the upstream content into .claude/skills/<name>/. The cache is pulled by fetchPlugin, the hash is recomputed, sync-state is re-stamped — but the workspace files remain whatever they were before.

User-visible consequence:

$ allagents skills update --all
↑ brainstorming  abc1234 → def5678 (updated)
$ cat .claude/skills/brainstorming/SKILL.md   # ← still the old content

The issue's expected behavior ("↑ git-commit v0.1.0 → v0.2.0 (updated)") and reference impl (gh skill update's pkg/cmd/skills/update/update.go) both imply an actual file refresh. The verification gate as written doesn't test this — gate (5)'s [ "$HASH_BEFORE" = "$HASH_AFTER" ] is checking the recorded hash against itself, which trivially matches when no upstream change exists. A drift scenario (force the upstream to change, then verify .claude/skills/foo/SKILL.md on disk reflects it) would expose this.

Two ways to address:

  • Fix: in the apply branch, copy the (now-fresh) cached skill folder into the workspace's skills directory. This is the install path's tail-end; refactor installSkillDirect's copy step into a helper and reuse it here.
  • Or rescope: rename to skills check / skills drift to signal "metadata-only refresh," and let workspace update remain the file-mutating path. Less ambitious but honest.

I'd lean fix. The verb update in the broader skills ecosystem (gh skill, agentskills.io) means files-on-disk.


Non-blocking concerns

1. Test coverage for updateCmd is zero.

The new ~220-line handler has multiple distinct branches (--dry-run, --force, --unpin, pinned skip, fetch failure → 'skipped', no-skills-recorded case, JSON vs human output). The only test change is adding skillsUpdateMeta to the agent-help fixture. CLAUDE.md: "one test per distinct code path."

Highest-value tests to add:

  • Dry-run produces JSON envelope with all three buckets populated correctly given a fixture sync-state.
  • --unpin clears pinnedRef in the persisted state.
  • --force --all rewrites updatedAt when content matches.
  • Pinned skill without --unpin appears in pinned[], not updates[].

2. Singular/plural drift with #380.

The meta and JSON envelope both use 'skills update' (plural). If #380 (rename to singular with plural alias) merges, the argv-normalization in #380 will route allagents skills update to the skill update registration, but the meta's command: string stays plural. Coordinate so whichever lands second updates the meta to 'skill update'.

3. Carrying forward issues from #381/#382.

4. --unpin semantics around pin clearing.

const fetchOpts = unpin
  ? {}
  : cand.source.pinnedRef
    ? { branch: cand.source.pinnedRef }
    : {};

Correct. But when applying with --unpin, the new source record drops pinnedRef entirely. The previous resolvedRef is preserved as the fallback. Verify that the cache path discipline isn't broken: prior install lived in owner-repo@v0.1.0, post---unpin resolves against owner-repo (default-branch cache). If the previous pinned cache directory was relied on as the source of truth, after --unpin the install path drifts away from it. Worth pinning with a test fixture: confirm cachePath after --unpin is the unpinned form.


Suggested merge order

Hold this PR until the "metadata-only update" concern is resolved. Either:

  1. Fix the apply branch to copy files (recommended), and add at least one test for the dry-run JSON envelope.
  2. Rescope the command to a drift-report verb and update issue scope accordingly.

Once that's settled, merge after #381 + #382. The squash will be reasonable.

@christso christso force-pushed the feat/375-skill-update branch from 4ce0167 to fbea27c Compare May 12, 2026 14:10
New `skills update [skill]` subcommand decoupled from workspace sync. Walks
the `sources` map in sync-state (introduced for content-hashing in #374),
fetches each plugin (cached), and diffs the upstream hash against the
recorded `contentHash` to decide:

- `up-to-date`       — recorded hash matches upstream
- `available`        — drift detected; reported in --dry-run / TTY default
- `updated`          — applied in --all (or non-TTY default) mode
- `pinned`           — `pinnedRef` set; skipped unless --unpin
- `skipped`          — fetch failed (kept fail-safe)

Flags:
- `--all`     apply without prompting (default behaviour in non-TTY too)
- `--force`   re-write the source provenance even when hashes match
- `--dry-run` report drift, no file writes
- `--unpin`   clear `pinnedRef` so the resolver picks up the default branch
- `--scope`   project (default) or user

JSON envelope shape:
```
{
  success: true,
  command: "skills update",
  data: { checked, updates: [...], upToDate: [...], pinned: [...] }
}
```

Includes:
- `skillsUpdateMeta` in metadata, wired into agent-help.
- Tests for the new agent-help entry.

End-to-end verified against `obra/superpowers@v3.1.0`: dry-run is a
read-only no-op, JSON envelope matches the contract, --all applies in a
non-TTY shell without blocking, pinned skills are skipped unless --unpin,
--force re-stamps updatedAt, and TTY-default with no --all does not hang.

Closes #375
@christso christso force-pushed the feat/375-skill-update branch from fbea27c to 9ab1fac Compare May 12, 2026 14:58
@christso christso merged commit 08afda2 into main May 12, 2026
1 check was pending
@christso christso deleted the feat/375-skill-update branch May 12, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: allagents skill update for per-skill refresh (separate from workspace sync)

1 participant