feat(extensions): namespace pulled extensions by scoped name (per-extension layout)#1186
feat(extensions): namespace pulled extensions by scoped name (per-extension layout)#1186
Conversation
…ension layout)
Fixes swamp-club#120. Pre-change, all pulled extensions extracted into a
shared flat layout at .swamp/pulled-extensions/<type>/. Files with the
same basename across extensions (e.g. README.md, LICENSE.txt, _lib/aws.ts
in sibling AWS extensions) silently overwrote each other and
`extension rm` of one extension could destroy files another still needed.
## On-disk layout
Each installed extension now owns a dedicated subtree:
.swamp/pulled-extensions/<ext-name>/{models,workflows,vaults,drivers,
datastores,reports,files}/
.swamp/pulled-extensions/<ext-name>/manifest.yaml (read-only, new)
Bundle cache isolation falls out for free: bundleNamespace() hashes the
per-extension models dir, which is unique per extension, so each
extension gets its own bundle namespace under .swamp/bundles/<hash>/.
Skills remain at the tool-specific dir (.claude/skills/ etc.) — they're
already per-skill and not collision-prone.
## Integrity anchor
upstream_extensions.json already records a SHA-256 checksum per install
but never used it for verification. installExtension now accepts an
optional expectedChecksum, and extensionInstall (the lockfile-restore
path) passes the stored value. On mismatch, the install fails loudly
with an actionable message offering `swamp extension pull <name>` as
the recovery path. Explicit `swamp extension pull` stays opt-in (no
expectedChecksum) so users can still accept drifted registry content
by intent. Pre-f4dfc083 entries without a checksum skip verification
gracefully.
## Migration
RepoService.migrateExtensionLayout runs during `swamp repo upgrade` and
handles two legacy generations:
- gen-1 (pre-.swamp/): rename extensions/<type>/... files to their
.swamp/pulled-extensions/<type>/... equivalents. Unchanged behavior.
- gen-2 (flat-under-.swamp/): because sibling extensions may have
silently overwritten each other's files, rename cannot restore
authentic content. Phase 2 selectively deletes each gen-2 entry's
tracked files (NotFound tolerated — expected on retry after an
interrupted prior pass; all other IO errors abort the migration
with the lockfile intact). `swamp extension install` then re-pulls
each affected extension with the integrity anchor enforcing
byte-identical restores.
Migration output is surfaced through the repo-upgrade renderer with
per-extension deletion counts and a "run `swamp extension install`"
next-step hint so users understand what happened.
## Warn-not-block guard
Replaces requireCurrentExtensionLayout (throw on legacy) with
warnLegacyExtensionLayout (log + continue). The lockfile tolerates
mixed-generation state, and individual extensions can migrate
independently, so blocking all commands until a full migration was
all-or-nothing is unnecessary. Users with partial migration state
keep using their migrated extensions while being gently reminded to
complete via `swamp repo upgrade`.
## Auto-resolve adapter
Fixed a live regression: the auto-resolve hot-load path in
auto_resolver_adapters.ts and the datastore auto-update path in
resolve_datastore.ts were walking the pre-120 flat layout. Both now
route through enumeratePulledExtensionDirs to discover per-extension
dirs from the lockfile. Locked in by a new test file.
## Dead API cleanup
InstallContext and ExtensionPullDeps lose six fields
(modelsDir/workflowsDir/vaultsDir/driversDir/datastoresDir/reportsDir)
that installExtension no longer consults — destinations are derived
from ref.name. createExtensionPullDeps drops from 10 positional params
to 4. Simplifies the libswamp extension surface and prevents silent
no-op bugs where callers think they're configuring a destination path.
## Testing
- pull.ts: per-extension layout smoke-tested against real registry
(@stack72/ubiquity, @swamp/aws/ec2, @swamp/aws/eks)
- layout.ts: 14 tests covering generation classification and guard
- enumerate_pulled.ts: 6 tests for the loader-additionalDirs helper
- repo_service.ts: 5 migration scenarios (happy path, partial fail,
NotFound tolerance, fatal IO abort, mixed-state)
- install.ts: checksum mismatch end-to-end with drift message
- rm_test.ts: sibling-prune regression + findDependents via tracked
manifest (fixes a latent bug — manifest.yaml was never persisted
for findDependents to resolve)
- auto_resolver_adapters_test.ts: hot-load plumbing regression
- 4392 total tests pass, deno check/lint/fmt clean
End-to-end verification against the triage reproduction snapshot:
repo upgrade → extension install (integrity-verified) → extension
rm leaves siblings intact.
## Follow-up UAT
- swamp-uat#142: auto-resolution end-to-end coverage
- swamp-uat#143: co-install sibling extensions user story
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
JSON field names expose internal phase jargon (
repo_service.ts,ExtensionLayoutMigrationSummary):phase1MovedCountandphase2DeletedPerExtensionare now stable public JSON API. Users scripting againstswamp repo upgrade --jsonwill see these names with no context for what "phase1" or "phase2" mean. ConsiderrenamedFileCount/deletedPerExtensionfor clearer, more stable naming. -
"flat-layout file(s)" is internal jargon (
repo_init.ts:132): The log lineRemoved Y flat-layout file(s) across Z extension(s)uses the term "flat-layout" which has no meaning to users who haven't read the design doc. "Removed Y outdated file(s) across Z extension(s)" communicates the same intent without leaking the internal layout taxonomy. -
Next-step hint appends implementation detail (
repo_init.ts:139):"The lockfile's stored checksum is verified on each re-pull."is useful for security-conscious users but is noise for the typical migration case. The actionable part ends atRun 'swamp extension install' to restore these extensions into the per-extension layout.
Verdict
PASS — the warn-not-block migration, per-extension output in repo upgrade, and JSON extensionMigration field all land cleanly from a UX perspective. No blocking issues.
There was a problem hiding this comment.
Code Review
Blocking Issues
-
libswamp import boundary violation (
src/cli/mod.ts:25): ImportsenumeratePulledExtensionDirsfrom the internal path"../libswamp/extensions/enumerate_pulled.ts"instead of"../libswamp/mod.ts". Per CLAUDE.md: "CLI commands and presentation renderers must import libswamp types and functions fromsrc/libswamp/mod.ts— never from internal module paths." The function is already exported frommod.ts(line 646), so this is a one-line fix. -
libswamp import boundary violation (
src/cli/repo_context.ts:49): Same issue — importsenumeratePulledExtensionDirsfrom"../libswamp/extensions/enumerate_pulled.ts"instead of"../libswamp/mod.ts".
Suggestions
-
DDD alignment looks good:
ExtensionLayoutGenerationis a clean union-type value object,LegacyFileEntry/LegacyLayoutSummary/ExtensionLayoutMigrationSummaryare properly immutable summary types, migration logic correctly lives inRepoService, andenumeratePulledExtensionDirsis well-placed as a libswamp query helper. -
The migration phase separation (phase 1: rename gen-1→gen-2, phase 2: delete gen-2 for re-install) is well-designed. The lockfile-as-coordination-primitive pattern — preserving it on fatal errors, tolerating NotFound on retry — makes partial migrations safely resumable.
-
Test coverage is solid: 5 migration tests covering happy path, partial retry tolerance, idempotency, fatal IO error abort, and mixed-generation state; 6 enumerate-helper tests; checksum-mismatch test; auto-resolver adapter regression tests. Good edge case coverage.
-
The integrity anchor design (lockfile-anchored SHA-256 verification on restore, opt-in on explicit pull, graceful skip for pre-checksum entries) is a nice security improvement.
There was a problem hiding this comment.
Adversarial Review
Critical / High
No critical or high severity issues found. The code is well-structured for a large migration change. Key safety mechanisms are solid: the archive path traversal checks (.. and / prefix rejection at pull.ts:743-747, symlink escape validation at pull.ts:770-776), the checksum integrity anchor (pull.ts:700-721), the advisory lock around lockfile writes (pull.ts:217-297), and the atomicWriteTextFile for crash-safe lockfile updates all look correct.
Medium
-
Phase 1 lockfile write bypasses advisory lock —
repo_service.ts:1751-1754:migrateExtensionLayoutPhase1rewritesupstream_extensions.jsonviaatomicWriteTextFiledirectly, without acquiring the.lockfile thatupdateUpstreamExtensions(pull.ts:260-297) uses for concurrency safety. If a user concurrently runsswamp extension pullduringswamp repo upgrade, the two writers can clobber each other. Low probability sincerepo upgradeis typically run standalone, but the advisory lock exists for exactly this reason. -
Stale advisory lock after crash —
pull.ts:217-240, 291-297: The advisory lock file contains no PID or timestamp. If the process is killed betweenacquireLockand thefinallyblock'sDeno.remove, the.lockfile remains permanently. All subsequent operations fail after 10 retries (1 second) with "Another pull may be in progress." The only recovery is manual deletion. Consider writing the PID and checking liveness on conflict, or at minimum adding a stale-lock age check. -
Broad
catch {}infileExists—pull.ts:341-348: Returnsfalsefor permission errors, not justNotFound. If a file exists but is unreadable,detectConflictsskips it and the install silently overwrites it. Same pattern appears invalidateSourceCompleteness(pull.ts:603) andcollectTsFiles(pull.ts:635). -
hasAnyMissingFilesreturns false for emptyfilesarray —install.ts:137-153: An extension whose lockfile entry hasfiles: [](e.g., from a crash after lockfile write but before file extraction, or a corrupt lockfile edit) is treated as "up to date" and never re-installed. ThecheckForMissingPulledExtensionsguard inmod.ts:676-681also skips the extension because it filters to source files and finds none. This is a narrow edge case but could cause confusion. -
PULLED_TYPE_DIRSduplicated between layout.ts and repo_service.ts —layout.ts:43-52definesPULLED_TYPE_DIRS,repo_service.ts:1783-1792independently definespulledTypeDirswith the same entries. If one is updated without the other,classifyExtensionFileandmigrateExtensionLayoutPhase2would disagree on what constitutes a gen-2 file vs. a current-layout file. Should be a shared constant.
Low
-
Redundant lockfile reads in dependency resolution —
pull.ts:1096: Each dependency iteration re-reads and re-parses the lockfile to check if a dependency is already installed. WithMAX_DEPENDENCY_DEPTH = 10and multiple deps, this is wasteful. The lockfile was just written at line 1075. A cached read would be cleaner. -
resolveSwampBinaryPathuses synchronousoutputSync()—repo_service.ts:1349: Blocks the event loop forwhich swamp. Minor since this runs once during upgrade, but inconsistent with the project's async conventions. -
Lockfile path resolution copy-pasted three times —
repo_context.ts:252-260, 386-395, 493-501: Identical lockfile path construction logic (with double-call toresolveModelsDir) is repeated verbatim. Maintenance hazard but not a correctness issue. -
checkForMissingPulledExtensionsignores.jsfiles —mod.ts:678-681: Missing bundle.jsfiles won't trigger the warning. Intentional per the comment, but could mask real breakage if an extension's only artifact is a bundled JS file.
Verdict
PASS — The migration design is sound: phase 1 renames (reversible), phase 2 deletes with lockfile-anchored re-install, and the integrity checksum prevents silent registry drift. The archive extraction has proper path traversal guards. No data loss or security issues in production paths. The medium items are worth addressing in follow-ups but none block this PR.
Claude Code Review blockers: - src/cli/mod.ts and src/cli/repo_context.ts imported `enumeratePulledExtensionDirs` from the internal module path; CLAUDE.md requires CLI callers to import libswamp exports from `src/libswamp/mod.ts` only. Both now go through the barrel. CLI UX Review suggestions: - Renamed `ExtensionLayoutMigrationSummary.phase1MovedCount` → `renamedFileCount` and `phase2DeletedPerExtension` → `deletedPerExtension`. The old names leaked internal "phase 1 / phase 2" jargon into the public JSON surface of `swamp repo upgrade --json`. - Replaced the `"flat-layout file(s)"` log line with `"outdated file(s)"` — the "flat-layout" term only makes sense to readers of the design doc. - Dropped the trailing "The lockfile's stored checksum is verified on each re-pull" note from the migration hint. The actionable part is `swamp extension install`; the integrity anchor is an implementation detail that doesn't belong in the normal upgrade output. Adversarial Review medium — duplicated constant: - `PULLED_TYPE_DIRS` was defined inline in both `libswamp/extensions/layout.ts` (for `classifyExtensionFile`) and `domain/repo/repo_service.ts` (for phase-two migration). If the two drifted, the detector and the migrator would disagree on what counts as a gen-2 file. Exported from `layout.ts`, re-exported through `libswamp/mod.ts`, consumed by `repo_service.ts`. Verified: deno check/lint/fmt clean, 4392 tests pass, compiled binary end-to-end against the triage reproduction snapshot shows the cleaner migration output. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
`.claude/scheduled_tasks.lock` was inadvertently staged in the previous commit — it's a runtime lock managed by the scheduler subsystem and shouldn't live in version control. Remove from the index and gitignore both the lock file and the sibling state file so neither leaks into future commits. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
Warn message omits extension names —
warnLegacyExtensionLayoutemits"3 extension(s) pending migration. Run 'swamp repo upgrade' to complete."The count is there but the names aren't.summariseLegacyLayoutalready returnsextensionNames; surfacing them (e.g.@swamp/aws/ec2, @swamp/aws/eks pending migration...) would let users confirm which ones need upgrading without a separate lookup. -
Awkward pluralization in
repo upgradeoutput —"1 file(s)","1 extension(s)"is a minor roughness. Simple ternary (file(s)→file/files) would polish the log output.
Verdict
PASS — The migration summary added to repo upgrade is clear and includes an explicit CTA (swamp extension install). The checksum-mismatch and migration-failure error messages are actionable and tell users exactly what to run. Flipping warn-instead-of-block is a net UX improvement. JSON mode inherits extensionMigration automatically via JSON.stringify(e.data). No blocking issues.
There was a problem hiding this comment.
Code Review
Well-structured PR that solves a real data-integrity problem (silent cross-extension file overwrites) with a clean per-extension layout, integrity anchoring, and a resumable migration path. Comprehensive test coverage with 23+ new tests covering happy paths, edge cases (NotFound tolerance, IO abort), and regressions. Design doc updated, API surface cleaned up.
Blocking Issues
None.
Suggestions
-
Domain→libswamp internal import —
src/domain/repo/repo_service.ts:27importsPULLED_TYPE_DIRSdirectly from../../libswamp/extensions/layout.ts(internal path) rather than from../../libswamp/mod.ts. While the CLAUDE.md import boundary rule literally targets CLI commands and presentation renderers, this creates a domain→application-layer dependency that inverts the DDD dependency direction. Consider importing throughmod.ts, or better, promotingPULLED_TYPE_DIRSinto a shared domain/infrastructure location since it's fundamentally a filesystem-convention constant used by both layers. -
Repeated lockfile-path computation in
repo_context.ts— the patternisAbsolute(resolveModelsDir(marker)) ? resolveModelsDir(marker) : resolve(repoPath.value, resolveModelsDir(marker))appears three times (lines ~252, ~385, ~493), callingresolveModelsDir(marker)twice per occurrence. A small helper (e.g.absoluteModelsDir(marker, repoPath)) would reduce duplication and the redundant calls. -
Deprecated
requireCurrentExtensionLayoutuses dynamic import — the backwards-compat shim atlayout.ts:183lazily importsUserErrorviaawait import(...)to avoid circular deps. If the function is truly deprecated and all call sites have migrated towarnLegacyExtensionLayout, consider removing it entirely rather than maintaining it with the dynamic-import workaround. If it must stay for one more release cycle, that's fine, but note the removal in a follow-up issue.
There was a problem hiding this comment.
Adversarial Review
Critical / High
No critical or high severity findings.
Medium
-
src/cli/repo_context.ts:252-259, 383-392, 475-484 — Repeated inline lockfile-path computation is fragile and callsresolveModelsDir(marker)twice per site.Three nearly identical 8-line blocks compute the lockfile path inline. Each calls
resolveModelsDir(marker)twice (once for theisAbsolutecheck, once for theresolve). WhileresolveModelsDiris pure (no side effects) and this is functionally correct, the triplication makes it easy for a future editor to fix one site and miss the other two. A locallockfilePathhelper or a shared utility would be more robust.Breaking example: If
resolveModelsDirwere ever made stateful or expensive (unlikely but possible), this would double the cost. More practically, a future change to the path convention only updated in one of three places would produce incorrect workflow enumeration in the other two.Suggested fix: Extract a
buildLockfilePath(repoDir, marker)function used by all three call sites, or at minimum bindconst modelsDir = resolveModelsDir(marker)once before theisAbsolutecheck. -
src/libswamp/extensions/layout.ts:74-86 —classifyExtensionFileassumes extension names always start with@to distinguish from type dirs, butPULLED_TYPE_DIRScould collide with a hypothetical future non-scoped name.Currently safe because
validateExtensionNameenforces@scope/nameformat, butclassifyExtensionFilehas no direct dependency on that invariant. If an extension named e.g.models/evilwere ever allowed (it wouldn't pass the current regex, but the layout classifier doesn't know that), it would be misclassified as gen-2.Suggested fix: No code change needed today — the scoped-name constraint is enforced. This is a documentation-level concern: a comment noting the invariant dependency would prevent future drift.
-
src/libswamp/extensions/pull.ts:700-701 — Lockfile-anchored checksum comparison uses raw string equality between two hex checksums. If the lockfile were ever hand-edited or a future serialisation prependedsha256-(as the design doc's example"sha256-…"suggests), the comparison would always fail.The design doc at line 32 of the diff (
"checksum": "sha256-…") uses asha256-prefix in its illustrative example, whilecomputeChecksumreturns raw hex. If someone reads the design doc and manually constructs or patches a lockfile entry with asha256-prefix, every restore would trigger a false-positive mismatch and tell the user their registry has drifted.Suggested fix: Either normalise the comparison (strip any
sha256-prefix before comparing), or update the design doc example to use raw hex to match the actual stored format.
Low
-
src/libswamp/extensions/enumerate_pulled.ts:54 —readUpstreamExtensionsis called on every type enumeration (models, workflows, vaults, drivers, datastores, reports). Insrc/cli/mod.ts, this means the lockfile is read and parsed 5 times during startup.Each of
loadUserModels,loadUserVaults,loadUserDrivers,loadUserDatastores, andloadUserReportsindependently callsenumeratePulledExtensionDirs, which reads and parses the lockfile. The lockfile is small and this is I/O-cached by the OS, so the practical cost is negligible, but a single parse shared across all loaders would be cleaner. -
src/domain/repo/repo_service.ts:1749 — Phase 1 migration assignsnewPatheven when the rename was skipped (NotFound). The lockfile entry is still rewritten tonewPathfor a file that doesn't exist on disk.When
Deno.renamethrowsNotFound(source file missing), the code catches it silently but still pushesnewPathintoupdatedFiles. This means the lockfile path is rewritten to the new location even though the file was never actually moved there. Subsequent tooling will look for the file at the new path and not find it — which is the same outcome as if it were at the old path. Not harmful becauseextension installhandles missing files by re-pulling, but it's semantically sloppy. -
src/libswamp/extensions/pull.ts:975 —Deno.mkdir(absoluteFilesDir, { recursive: true })is called unconditionally beforecopyDir, even when the archive has nofiles/directory. This creates an emptyfiles/dir in every extension's subtree.Harmless but produces filesystem noise. The
models,workflows, etc. dirs are only created bycopyDirwhen content exists;filesis the exception.
Verdict
PASS — The code is well-structured, thoroughly tested, and handles the hard parts (migration idempotency, selective delete with abort semantics, integrity verification, shared-scope-root preservation) correctly. The gen-1 → gen-2 → current classification logic is sound, the lockfile coordination invariants hold, and the error paths are well-tested. Medium findings are about future robustness, not current correctness.
…uto-resolver (systeminit#1187) Fixes [swamp-club#121](https://swamp.club/lab/issues/121). ## Summary `workflow validate` and other commands that trigger auto-resolution could silently overwrite local edits to pulled extensions. The auto-resolver adapter hardcoded `force: true` when installing a type it failed to find locally, and `installExtension`'s conflict check was gated on `!force` — so any user edits in the pulled directory were `copyDir`'d over with no prompt, no warning, and no diff. The fix adds an `isInstalled` capability to `ExtensionInstallerPort` and has the domain service consult it before calling `install`. If the extension is already on disk (lockfile entry **and** per-extension directory both present), the resolver surfaces a new `alreadyInstalledButFailed` event naming the path and the explicit opt-in command (`swamp extension pull <name> --force`) and returns failure rather than clobbering. Belt-and-braces: the adapter now passes `force: false` and catches `ConflictError`, so a race past the per-type re-entrancy guard (concurrent auto-resolves for sibling types) still fails safely. The datastore auto-update path in `resolve_datastore.ts:132` has a related but distinct `force: true` bug with a different trigger and fix shape; tracked separately as [swamp-club#126](https://swamp.club/lab/issues/126). ## What changed - `src/domain/extensions/extension_auto_resolver.ts` — port gains `isInstalled` and `installedPath`; service gains `alreadyInstalledButFailed` output event and the pre-install check in `installAndLoad`. - `src/cli/auto_resolver_adapters.ts` — installer adapter implements `isInstalled` (dual lockfile + filesystem check against the per-extension layout from systeminit#1186), `installedPath` returns the per-extension root, `install()` flips to `force: false` and catches `ConflictError` with a defence-in-depth comment. - `src/presentation/renderers/extension_auto_resolve.ts` — new `renderAutoResolveAlreadyInstalled` for both `log` and `json` modes. - `design/extension.md` — new \"Safety: never overwrite on-disk extensions\" subsection under Automatic Resolution. ## Test plan - [x] `deno fmt` clean - [x] `deno check` clean - [x] `deno lint` clean - [x] `deno run test` — 4400 pass / 0 fail (2 new service tests, 5 new adapter tests, 1 new integration regression test) - [x] Manual reproduction against `/tmp/swamp-repro-issue-121`: - **Pre-fix:** md5 of `system_usage.ts` changed from user-edited to pristine registry version; WIP marker comment lost - **Post-fix:** md5 identical before/after trigger; marker preserved; user sees three ERR lines naming the path and the `--force` recovery command ## Follow-up - [swamp-club#126](https://swamp.club/lab/issues/126) — datastore auto-update `force: true` in `resolve_datastore.ts:132` (related data-loss vector, different fix shape). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Fixes swamp-club#120.
Summary
Pulled extensions now land in per-extension on-disk subtrees
(
.swamp/pulled-extensions/<ext-name>/{models,workflows,…}/) insteadof a shared flat layout. Fixes the silent overwrite of shared
filenames (
_lib/aws.ts,README.md,LICENSE.txt) across siblingextensions from the same collective — a bug observed in the wild with
@swamp/aws/ec2+@swamp/aws/eks, where prior pulls corrupted eachother's helpers.
Additional changes riding along:
upstream_extensions.jsonstores a SHA-256 perinstall and
extension install/ migration re-pull now verifyfreshly-downloaded archives against it. Explicit
extension pullstays opt-in to new registry content.
swamp repo upgradedetects legacy layouts (gen-1extensions/<type>/, gen-2 flat.swamp/pulled-extensions/<type>/)and migrates. Gen-2 uses selective delete + re-install via the
integrity anchor because rename cannot restore content that was
silently overwritten pre-fix. Partial migrations are resumable
(NotFound on retry is tolerated; any other IO error aborts with the
lockfile intact).
repo upgradesurfaces per-extension deletioncounts and a next-step hint so users understand what happened.
already-migrated extensions stay usable during partial state.
read-only
manifest.yamlat its root. Fixes a latentfindDependentsbug (never worked pre-120 because the manifest wasnever persisted).
auto_resolver_adapters.tsand
resolve_datastore.tshot-load paths that were still walkingthe flat layout.
InstallContext/ExtensionPullDepslose sixdead path fields;
createExtensionPullDepsshrinks from 10 to 4params.
Test Plan
deno checkcleandeno lintcleandeno fmtcleandeno run test— 4392 pass / 0 fail (up from 4379 baseline):5 new migration tests, 6 enumerate-helper tests, 4 layout-tests,
1 checksum-mismatch test, 2 rm regression tests, 5 auto-resolve
adapter tests
repo upgrade→ migration summary →extension install(integrity-verified) →
extension list→extension rm @stack72/ubiquity→ siblings@swamp/aws/ec2+@swamp/aws/eksintact
SWAMP_CLUB_URL=http://localhost:8080duringextension install→ all entries recorded as failed, lockfilepreserved, online retry recovers cleanly
design/extension.mdupdated with new layout, integrity anchor,and migration generations
Follow-up UAT
— auto-resolution end-to-end coverage
— co-install sibling extensions user story
🤖 Generated with Claude Code