Skip to content

M4: marketplaces + plugins - #9

Merged
spxrogers merged 9 commits into
mainfrom
claude/agentsync-m4
May 7, 2026
Merged

M4: marketplaces + plugins#9
spxrogers merged 9 commits into
mainfrom
claude/agentsync-m4

Conversation

@spxrogers

Copy link
Copy Markdown
Owner

Summary

  • Marketplace + plugin manifest schemas (Claude marketplace spec).
  • 5 plugin sources: relative, github, url, git-subdir, npm.
  • Fetcher interface; go-git for git, registry HTTP for npm.
  • Plugin projection into canonical model (auto-fans to claude + opencode).
  • marketplace add/remove/list, plugin install/upgrade/enable/disable/remove/list, update [--apply].
  • Translation report on apply: per-plugin per-adapter ✓/◐/✗ coverage table.
  • Manifest sha pinning detects re-uploads as drift (manifest-sha-mismatch warning).
  • Stacked on M3: drift / status / diff / reconcile #8 (M3).

Test plan

  • go test -race ./...
  • go vet ./...
  • go build ./...
  • full plugin fanout integration test (local fixture marketplace → install → apply → both claude+opencode have demo-mcp)
  • SHA pinning integration test (install records manifest_sha; re-uploaded bytes trigger warning)

Plan: docs/superpowers/plans/2026-05-04-agentsync-m4-marketplaces.md

🤖 Generated with Claude Code

@spxrogers spxrogers mentioned this pull request May 5, 2026
7 tasks
@spxrogers
spxrogers changed the base branch from claude/agentsync-m3 to main May 7, 2026 02:52
spxrogers and others added 9 commits May 6, 2026 22:55
Add internal/marketplace package with Marketplace, PluginEntry, Source
(polymorphic string/object UnmarshalJSON), PluginManifest, and
ReservedMarketplaceNames. All 5 source types (relative, github, url,
git-subdir, npm) are modelled. Tests cover string/object round-trips,
full field parsing, and the GitSubdir + PluginManifest shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s (Task 2)

Add Fetcher interface with Dispatch(); RelativeFetcher uses os.CopyFS-
style manual walk; GitFetcher uses go-git/v5 PlainClone (shallow, depth=1)
with idempotent re-pull and git-subdir extraction via copyDir+extractSubdir;
NPMFetcher fetches tarball via registry HTTP API (no npm CLI). Tests cover
all three fetchers including file:// fake repos and httptest.Server NPM
registry. go-git/v5 added to module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s (Task 3)

Add projection.go: Project() decomposes PluginManifest (strict) or
PluginEntry (non-strict) into canonical MCPServers/Skills/Subagents/
Commands/Hooks/LSPServers; resolves \${CLAUDE_PLUGIN_ROOT} in all
command/arg/url/env fields. Add plugin.go: install/upgrade/enable/disable/
remove/list subcommands that cache plugin source and write plugins/<id>.toml
with manifest SHA. Add marketplace.go: add/remove/list subcommands that
fetch marketplace into .state/cache/ and write marketplaces/<name>.toml.
Wire both into root command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…model (Task 4)

After Load() reads canonical files, LoadWithCache also reads each
plugins/<id>.toml, resolves <cacheDir>/<id>/.claude-plugin/plugin.json
via the afero FS, and merges the projection (MCPServers, LSPServers, etc.)
into Canonical so downstream adapters see plugin components transparently.

Avoids the source↔marketplace import cycle by inlining the minimal JSON
parsing in the source package and introducing PluginProjection / PluginProjector
types. marketplace.ProjectWithReader is added as a testability hook for callers
that are not subject to the cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Task 5)

marketplace add/remove/list were already implemented (Task 3); this commit
makes them robust by also writing the marketplace's HeadSHA and FetchedAt
timestamp into .state/targets.json so the upcoming update command can track
fetch history without re-reading TOML files. remove clears the entry.

Two new tests (TestMarketplace_AddUpdatesState, TestMarketplace_RemoveUpdatesState)
verify the state.json round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…report pending bumps (Task 6)

Adds `agentsync update` (the only network-touching command):
- Re-fetches all registered marketplaces via marketplace.Dispatch
- Builds a fresh plugin index from each marketplace.json
- Calls marketplace.ComputePendingBumps to diff installed vs. available versions
- Prints pending bumps (track-mode only); does NOT touch agent configs by default
- Updates .state/targets.json with fresh HeadSHA + FetchedAt timestamps
- --apply flag: upgrades bumped plugins then runs a full apply pass
- --auto-safe flag placeholder for future non-lossy-only upgrades

marketplace/update.go adds ComputePendingBumps + Bump types (pinned/manual
modes are silently skipped; default "" → "track").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ry (Task 7)

- internal/render/report.go: TranslationReport + BuildReport + PrintText/PrintJSON
- apply emits per-plugin per-agent table after apply and --dry-run
- Tests: no-plugin base row, with-plugin rows, partial coverage, text/JSON output

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uploaded versions (Task 8)

- marketplace.DetectSHADrift: compare recorded manifest_sha to freshly-fetched sha256;
  return SHAWarning for same-version re-uploads.
- update command: compute fresh SHAs from plugin cache, call DetectSHADrift,
  emit manifest-sha-mismatch warning lines before pending bumps.
- computeFreshPluginSHAs: reads plugin.json from cache, hashes with sha256.
- Tests: no-drift, drift, no-recorded-sha, no-fresh-sha.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…k 9)

- apply uses LoadWithCache so plugin MCP servers project into both claude and opencode.
- TestIntegration_M4_PluginFanoutClaudeAndOpenCode: local marketplace → install → apply
  → verify demo-mcp in .claude.json and opencode.json + translation report in output.
- TestIntegration_M4_SHAPinning: install writes manifest_sha; re-uploaded bytes at
  same version trigger manifest-sha-mismatch warning on next update.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@spxrogers
spxrogers force-pushed the claude/agentsync-m4 branch from 0de0578 to ab09554 Compare May 7, 2026 02:55
@spxrogers
spxrogers merged commit 51ef3e3 into main May 7, 2026
3 of 5 checks passed
@spxrogers
spxrogers deleted the claude/agentsync-m4 branch May 7, 2026 02:55
spxrogers pushed a commit that referenced this pull request Sep 1, 2026
Closes round-3 review findings on PR #240.

1. The anti-vacuity check was still inferring. It matched substrings against
   cobra's error prose, which covers only the arg/flag layer — anything
   rejecting later scored as "it ran", including this repo's own
   enforceScopeStance, a PersistentPreRunE refusal that never reaches RunE.
   runBounded now WRAPS the resolved command's RunE, so "did the body start" is
   observed rather than deduced, and cannot drift with cobra's wording. Third
   version of this check; the first two were unable to fire at all.

   It is also falsifiable now. runBoundedE reports instead of failing, so
   TestRunBoundedDetectsACommandThatNeverRan can assert ran==false for a missing
   argument, an unknown command AND a PersistentPreRunE refusal — the case the
   substring list structurally could not catch — plus ran==true for a command
   that does run. Previously nothing failed if the check were reverted.

2. Subtest fixture bleed. The cleanup unlinked the FIFO but never restored the
   applied file, so the key-merge subtest ran against a home whose whole-file
   destination was missing, contradicting the test's own "a real applied home".
   Measured: with the skips deleted, `import claude` and `reconcile
   --auto-override` PASSED in a full run and HUNG in isolation — whoever closes
   #241/#242 would have inherited a row green for the wrong reason. The
   destination is now restored, and after the fix the row hangs both ways.

3. writeBackFileItem appended "remove or replace the non-regular file at that
   path" to EVERY read failure, including ENOENT. Deleting a managed file is
   itself drift and offers [w], so the common path produced "no such file or
   directory — remove or replace the non-regular file at that path": advice for
   a situation the user is not in. Gated on errors.Is(err, errDestNotRegular),
   with a test for the absent case. Third round running that this one message
   has been the site of a new defect.

4. Prose, instances #8 and #9. The CHANGELOG headline said a "directory" no
   longer hangs — a directory never hung (os.ReadFile fails it in ~18us with
   EISDIR); only the diagnosis changes. And destread.go justified leaving the
   symlink split by claiming AGENTSYNC_ALLOW_SYMLINK_DEST=1 would break, but
   that variable is read only in internal/iox, on the WRITE path, so a read gate
   cannot affect it. The real reason is that changing it changes what diff and
   reconcile have always reported (#229) — and the real consequence, now named,
   is that under that supported setup `status` reports drift no apply can clear.

5. The hash row computed its expectation with the function under test; salting
   hashContent left it green. Pinned to a literal digest, break-verified.

Also: docs/components.md's "Enforced, not asserted" overstated a two-spelling
text matcher whose own LIMITS exempt a read; softened, and the review-audit
parenthetical that had survived into a website-mirrored contract page is gone.
destread.go now documents that `diff` and readDestFile swallow the refusal and
render a refused destination as empty — a poorer diagnosis than it deserves,
left to #229 because fixing it changes what those commands print.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M4VNyoCuGXx7pYxNfLVbFG
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.

1 participant