feat(multi-agent): install a skill once and share it across agents#11
Merged
Conversation
Introduce a three-layer install model (content store → .agents/skills active layer → per-agent targets) so a skill is resolved and stored once and shared by every agent that targets it. Adding an agent creates only the missing link — no re-download, no duplicate content. - new internal/active layer; the installer routes activation through it - declarative `gskill sync` reconciles disk to the manifest: idempotent, drift-aware, desired-aware --prune, and legacy migration - `gskill status` and `gskill unlink` (retain-unless-prune on the last agent) - chain-aware `check`/`repair` over a shared health model - auto install mode (--symlink/--copy/--auto), default auto - lockfile records active_path (schema stays v1, additive) Security & integrity hardening: - confine prune/unlink/remove deletions to adapter-derived, in-bounds paths; out-of-bounds lockfile paths are skipped, never deleted - EnsureActive fails closed on foreign content instead of overwriting it - check/repair hash-verify the store and copy targets and fail closed (exit 6) - adding an agent activates only the new agents and resolves nothing
Contributor
There was a problem hiding this comment.
Pull request overview
Implements a multi-agent “install once, share everywhere” model by introducing a project-level active layer that sits between the content-addressed store and per-agent skill targets. This enables gskill sync to reconcile desired agent targeting without re-downloading content, and adds new operational commands (status, unlink) plus chain-aware health/check/repair behavior.
Changes:
- Add the active layer (
.agents/skills/<name>) and extend installer/lockfile to recordactive_pathand per-agentmodes. - Rework
gskill syncto be manifest-driven reconciliation (with--prune/ orphan reporting) and addgskill status+gskill unlink. - Add chain health evaluation and broaden integration/unit coverage, including adversarial tests.
Reviewed changes
Copilot reviewed 35 out of 35 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/integration/sync_reconcile_test.go | Integration coverage for manifest-driven sync reconcile, idempotency, prune, and legacy migration. |
| test/integration/status_unlink_test.go | Integration coverage for status JSON + unlink behaviors (retain vs prune). |
| test/integration/multi_agent_share_test.go | Integration coverage for install-once/share behavior, default agent, collision, and mode flags. |
| test/integration/check_repair_chain_test.go | Integration coverage for chain-aware check/repair including drift and store corruption. |
| test/integration/adversarial_test.go | Integration adversarial scenarios: lockfile path escape, foreign active entry, corrupt store/copy, offline agent-add. |
| test/integration/adapter_extensibility_test.go | Integration proof that adding a new agent adapter works end-to-end with the new active-layer model. |
| README.md | Document multi-agent sharing, new commands, and updated sync semantics. |
| internal/lockfile/testdata/lock.golden | Update golden lockfile fixture to include active_path and per-agent modes. |
| internal/lockfile/lockfile.go | Extend Installation with ActivePath and clarify semantics in comments. |
| internal/lockfile/lockfile_golden_test.go | Update lockfile golden test sample to include ActivePath/Modes. |
| internal/lockfile/consistency.go | Add consistency check to ensure manifest-declared agents are present in the lock. |
| internal/lockfile/consistency_test.go | Add tests for agent-set drift and superset acceptance. |
| internal/installer/types.go | Introduce install mode preferences: auto (default), symlink, copy. |
| internal/installer/installer.go | Route activation through the active layer and record ActivePath; add agent activation behavior for link/copy modes. |
| internal/installer/installer_test.go | Assert the new “store → active → agent” symlink chain. |
| internal/cli/unlink.go | New CLI command wiring for gskill unlink. |
| internal/cli/sync.go | Change sync to manifest-desired reconciliation, with richer JSON output and up-to-date reporting. |
| internal/cli/status.go | New CLI command wiring for gskill status. |
| internal/cli/root.go | Register new commands and update help strings for sync. |
| internal/cli/add.go | Add --auto mode and adjust install-mode flag parsing/help. |
| internal/app/unlink.go | Implement unlinking a single agent from a skill, with optional prune/GC behavior. |
| internal/app/sync.go | Rewrite sync as manifest-driven reconcile, including desired agent sets, orphans, pruning, and lock updates. |
| internal/app/status.go | Implement read-only per-skill/per-agent status report using the shared health model. |
| internal/app/repair.go | Repair now uses the shared chain-health evaluation to decide what to re-materialize. |
| internal/app/paths.go | Add confined deletion helpers to prevent lockfile-driven out-of-bounds deletions. |
| internal/app/lifecycle.go | Update remove flow to use confined target deletion and remove active-layer entries. |
| internal/app/install.go | Add local “agent-add” fast path, plan/merge semantics, and manifest agent unioning. |
| internal/app/inspect.go | Make check chain-aware and fail-closed on integrity faults. |
| internal/app/health.go | New chain health model (store/active/agent targets), with hash verification for integrity faults. |
| internal/app/health_test.go | Unit tests for the chain health evaluator (missing/broken/corrupt/legacy). |
| internal/app/gitignore_test.go | Verify init gitignores both .gskill/ and .agents/. |
| internal/active/doc.go | Package docs for the active-layer model. |
| internal/active/active.go | Active-layer management: ensure, health, remove, list; fail-closed on foreign occupants. |
| internal/active/active_test.go | Unit tests for active-layer behaviors (idempotent, repoint, fail-closed, list/remove). |
| docs/reference/commands.md | Update CLI reference for new flags/commands and revised sync semantics. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+55
to
+59
| locked, inLock := lf.Skills[skill] | ||
| ms, inManifest := m.Skills[skill] | ||
| if !inLock && !inManifest { | ||
| return fmt.Errorf("%w: skill %q is not declared", errs.ErrInvalidManifest, skill) | ||
| } |
Comment on lines
+321
to
+325
| if linkErr := os.Symlink(abs, dest); linkErr == nil { | ||
| return ModeSymlink, nil | ||
| } | ||
| if err := fsutil.CopyDir(copySource, dest); err != nil { | ||
| return "", fmt.Errorf("copy fallback: %w", err) |
- unlink: a skill present in only the manifest or only the lockfile is a lock mismatch (exit 4), not a half-edit that writes a zero-value entry - installer: --symlink is now strict — it fails instead of silently copying when a symlink can't be created, so policy/permission failures surface; only --auto (and symlink-incapable agents) fall back to a copy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces a three-layer install model so the same skill is resolved and stored once and shared by every AI agent that targets it:
Adding an agent to a skill creates only the missing link — no re-download, no duplicate content. Store + active layer are gitignored and regenerated by
gskill sync.What's new
internal/activelayer; the installer routes activation through it (store → active → agent).gskill sync— reconciles disk to the manifest's desired state: idempotent, drift-aware, desired-aware--prune, legacy migration.gskill status(per-skill, per-agent mode + health) andgskill unlink(retain-unless-prune on the last agent).check/repairover a shared health model.--symlink/--copy/--auto(defaultauto).active_path(schema stays v1, additive).Security & integrity hardening
Addresses an adversarial review of the branch:
EnsureActivefails closed on foreign content instead of overwriting it.check/repairhash-verify the store and copy targets and fail closed (exit 6) on tampering.Testing
./scripts/verify.shpasses (fmt, vet, golangci-lint,-racetests + coverage, govulncheck, gitleaks). New unit + integration coverage includes anadversarial_test.gosuite (malicious lock path, corrupt store, corrupt copy, foreign active entry, offline agent-add) — each exercises behavior that was broken before this change.