Conversation
…uto-resolver (#121) Fixes swamp-club#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. ## 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 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 — datastore auto-update `force: true` in `resolve_datastore.ts:132` Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
Log-mode message preamble inconsistency —
renderAutoResolveNotFoundandrenderAutoResolveNetworkErrorboth open withAuto-resolution failed for type ${type}: .... The newrenderAutoResolveAlreadyInstalledopens differently:Extension ${extension} is already installed at ${path} but failed to load.The deviation is arguably clearer for this case, but a user scanning a log with multiple auto-resolve failures will notice the formatting change. Consider:Auto-resolution failed for ${extension}: already installed at ${path} but failed to load. -
JSON output missing
typefield — Thenot_foundandnetwork_errorJSON objects both include the originaltype(e.g.@swamp/aws/ec2/instance). The newalready_installedobject omits it, including onlyextensionandpath. A script correlating a type lookup against the failure event would have to infer the type from the extension name. Low impact since the extension name is usually enough, but addingtypewould keep the JSON shapes uniform.
Verdict
PASS — no blocking issues. Recovery command (swamp extension pull <name> --force) is correct, all three error lines are actionable, JSON mode is present and well-structured, and the behavioral change is safe and backwards-compatible.
There was a problem hiding this comment.
Code Review
Clean, well-structured fix for the silent-overwrite data-loss bug. The DDD layering is correct: the domain service defines the isInstalled/installedPath ports on ExtensionInstallerPort, the CLI adapter implements them with proper infrastructure concerns (dual lockfile + filesystem check), and the presentation layer handles both log and json output modes for the new alreadyInstalledButFailed event.
Blocking Issues
None.
Suggestions
-
Renderer test coverage:
src/presentation/renderers/extension_auto_resolve.tshas no corresponding_test.tsfile. The newrenderAutoResolveAlreadyInstalledfunction is covered indirectly by the adapter and integration tests, and follows the same pattern as the existing untested functions in that file, so this is pre-existing rather than a regression — but worth noting for future cleanup. -
"pulled-extensions"string literal: TheisInstalledandinstalledPathmethods use the raw string"pulled-extensions"inswampPath(repoDir, "pulled-extensions", extensionName). There's no existingSWAMP_SUBDIRSconstant for just the parent directory (only type-specific ones likepulledModels), so this is reasonable as-is. A shared constant could prevent drift if the path ever changes, but that's marginal.
What looks good
- DDD port design:
isInstalled(async, needs I/O) andinstalledPath(sync, deterministic computation) are well-separated on the port interface. The domain service never reaches into infrastructure. - Defence-in-depth: The
ConflictErrorcatch in the adapter'sinstall()method covers the race window betweenisInstalledandinstallthat the per-type re-entrancy guard can't fully close (concurrent resolves for sibling types in the same extension). - Dual check logic: Requiring both lockfile entry AND filesystem directory prevents confusing error messages when the user has manually deleted the extension directory — a clean reinstall proceeds instead.
- Test coverage: Three levels — domain unit tests (mock ports), adapter unit tests (real filesystem), and an integration test that wires the real adapter to the real domain service and asserts file contents + mtime are unchanged.
- Import boundary: All libswamp imports in the adapter go through
src/libswamp/mod.ts(ConflictError,installExtension,enumeratePulledExtensionDirs). ThereadUpstreamExtensionsimport from infrastructure is consistent with the established pattern in other CLI files. - Output modes: Both
log(three actionable error lines with the recovery command) andjson(structured event withreason: "already_installed") are implemented. - Design doc updated with the safety invariant and the two-check rationale.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
TOCTOU race between
isInstalledandinstallfor concurrent sibling types —src/domain/extensions/extension_auto_resolver.ts:328/src/cli/auto_resolver_adapters.ts:113-138The re-entrancy guard (
this.resolving) is keyed on normalized type strings, not extension names. Two types from the same extension (e.g.@swamp/aws/ec2/instanceand@swamp/aws/s3/bucket) can resolve concurrently. Both passisInstalled→ false, both enterinstall. The first succeeds; the second hitsConflictError→ returns null →installAndLoadreturns false. The caller for type B (resolveModelType) receivesfalseand returnsundefined— a false-negative even though the extension was loaded by type A's hot-load.The PR already acknowledges this race in code comments and provides ConflictError catch as defense-in-depth. This is a pre-existing architectural limitation that the PR makes safer (ConflictError vs. silent overwrite). Noting for awareness — not blocking.
Low
-
Bare
catchinisInstalled'sDeno.stat—src/cli/auto_resolver_adapters.ts:96Catches all errors, not just
Deno.errors.NotFound. A permission error would silently returnfalse, causing a fresh install attempt that would then likely also fail with a different (potentially confusing) error. In practice, if the directory is unreadable,installExtensionwould also fail at the same path, so the end result is the same. Purely theoretical. -
readUpstreamExtensionsre-reads lockfile on everyisInstalledcall —src/cli/auto_resolver_adapters.ts:89Parses the full lockfile JSON each time. Negligible for auto-resolution (one call per type per resolve), but worth noting if the call site ever changes to a batch pattern.
Verdict
PASS — The fix correctly addresses the data-loss bug (issue #121). The domain-level isInstalled check prevents silent overwrites; the adapter's force: false + ConflictError catch provides belt-and-braces safety for the TOCTOU race. Port interface additions are backwards-compatible (only two implementations, both updated). Test coverage is strong: 2 domain unit tests, 5 adapter unit tests, and 1 integration regression test that verifies file-survival invariant via mtime + content comparison. Design documentation is accurate and actionable.
…lub#126) (systeminit#1190) ## Summary Fixes swamp-club#126 — datastore auto-update uses `force: true` in the `pullExtension` callback and silently overwrites any local edits a user has made under `.swamp/pulled-extensions/<name>/`. Same data-loss family as swamp-club#121/systeminit#1187 (auto-resolver force-pull), different trigger (version-behind rather than type-not-found), different fix shape (per-file anchor rather than `isInstalled` skip — because auto-update, by definition, wants to replace the prior install). **Approach:** - Record a rolled-up SHA-256 digest of the per-extension on-disk subtree in `upstream_extensions.json` at install time (`filesChecksum`), distinct from the existing archive `checksum`. - Before auto-update fires `installExtension` with `force: true`, the service calls a new `detectLocalEdits` dep that compares the stored anchor with a fresh disk digest. Tri-state outcome: `match` proceeds; `mismatch` returns a structured `{ skipped: "local_edits", previousVersion, newVersion }` result and does NOT call `pullExtension`; `no-anchor` grandfathers pre-existing installs and proceeds. - The CLI caller (`resolve_datastore.ts`) inspects the skipped result and emits `logger.warn` directing the user to `swamp extension pull <name> --force` as the explicit opt-in. The refusal is a STRUCTURED RESULT rather than a thrown error because the outer try/catch intentionally swallows exceptions to honor "never block command execution for auto-update failures"; a structured result separates \"error — swallow\" from \"refusal — surface\" cleanly. **DDD split:** - Pure combiner in `src/domain/extensions/installed_extension_digest.ts` takes pre-hashed entries and produces the rollup digest. No FS access. - FS-walking reader in `src/infrastructure/persistence/installed_extension_digest_reader.ts` walks the per-extension tree, hashes each file, and calls the pure combiner. - Mirrors the pattern already in use for `src/domain/models/checksum.ts` (pure compute) vs callers that read bytes from disk. **Grandfather / migration:** Pre-anchor lockfile entries (installs from before this change) lack `filesChecksum`. `detectLocalEdits` returns `no-anchor` for them and auto-update proceeds as it does today. On the next install, the anchor is written and all subsequent auto-updates are protected. A one-time silent-overwrite window remains for users with pending edits AND a stale lockfile entry AND an auto-update firing before they next pull — acceptable given the scope of the fix; mentioned here for release notes. ## Follow-ups filed separately - **swamp-club#129** — `src/cli/commands/open.ts:185` has the same `force: true` pattern in the web UI's non-interactive install callback. Same data-loss family, different trigger; tracked separately. - **swamp-club#130** — `logger.warn` is silent in `--json` mode because the logger's catch-all is `lowestLevel: \"fatal\"` in JSON output mode. The auto-update refusal is silent in JSON mode (data is still protected — refusal short-circuits before `installExtension` — but the user gets no signal why auto-update isn't happening). Pre-existing issue affecting ~50 `logger.warn` call sites; changing it is too much blast radius for a bug-fix PR. **Explicitly out of scope / confirmed safe:** - `src/cli/commands/extension_install.ts:114` uses `force: true` but the caller in `src/libswamp/extensions/install.ts:88-97` short-circuits via `hasAnyMissingFiles` before calling `installExtension` if any tracked file exists. Local edits and force:true cannot co-exist on that path. - `swamp extension pull` / `extension update` — explicit user opt-in, not a silent path; unaffected. ## Test plan - [x] `deno check` clean - [x] `deno lint` clean - [x] `deno fmt` applied - [x] `deno run test` — 4447 passed, 0 failed (includes 7 new tests covering: digest determinism, FS-reader invariants, auto-update tri-state decision tree, caller-layer warning message) - [x] `deno run compile` succeeded - [ ] Manual JSON-mode verification deliberately skipped — this PR does not change JSON-mode behavior (the WARN is silent in JSON mode by pre-existing design; tracked as swamp-club#130) - [ ] UAT follow-up: a new `tests/cli/extension/auto_update_preserves_local_edits_test.ts` parallel to `pull_wip_preservation_test.ts` (which locks in the systeminit#121 fix) should be filed as a swamp-uat issue after merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
systeminit#1191) ## Summary Third in the data-loss family (sibling of systeminit#121/systeminit#1187 and systeminit#126/systeminit#1190). `swamp open`'s web UI install callback calls `pullExtension` with `force: true` and, pre-fix, had no local-edits check — so hitting `POST /api/extensions/install` for an already-installed extension would silently overwrite anything the user had edited under `.swamp/pulled-extensions/<name>/`. Port systeminit#126's `filesChecksum` anchor + detectLocalEdits pattern into a shared libswamp helper and consult it from the web UI before the force-pull. On mismatch, throw a typed `LocalEditsError`; the HTTP handler maps it to **409 Conflict** with a body that names the extension and the opt-in remediation (`swamp extension pull <name> --force`). The ui.ts Install button only renders for **uninstalled** extensions today, so there's no UI click path that reaches this bug — the fix is defense-in-depth for the HTTP endpoint itself, which is reachable by curl, scripts, or any future UI change (e.g. an "Update / Reinstall" button). Scope and fix-shape intentionally mirror systeminit#126. ### What changes - New `src/libswamp/extensions/local_edits.ts` — shared helper (`detectLocalEditsForExtension`, `LocalEditsStatus`, `LocalEditsError`), re-exported through `libswamp/mod.ts` - `src/cli/resolve_datastore.ts` — systeminit#126's inline `detectLocalEdits` closure collapses to a one-line call into the new helper (pure DRY) - `src/cli/commands/open.ts` — `installExtension` callback runs the local-edits check before force-pull; throws `LocalEditsError` on mismatch - `src/serve/open/http.ts` — `handleExtensionInstall` maps `LocalEditsError` → 409, unchanged 500 fallthrough for other errors - Unit tests at the new helper and at the HTTP handler layer ### What's out of scope - `extension_update.ts:118` and `extension_install.ts:114` still use `force:true`; the former is an explicit user command, the latter is lockfile-restore. Refusing them would break their contracts. - The dependency-with-corrupt-lockfile edge case (parent reinstall recurses into a dep missing from `upstream_extensions.json` but present on disk with edits) — matches systeminit#126's scope. Fixing it would require pushing the check into `installExtension` itself, which would wrongly refuse the opt-in paths above. - End-to-end UAT for the web UI refuse flow: filed as [systeminit/swamp-uat#150](systeminit/swamp-uat#150). ## Test Plan - [x] `deno check`, `deno lint`, `deno fmt`, full `deno run test` (4455 passed), `deno run compile` - [x] New unit coverage: 6 tests in `src/libswamp/extensions/local_edits_test.ts` (match, mismatch, no-anchor × 3 conditions, LocalEditsError message shape) - [x] New handler coverage: 2 tests in `src/serve/open/http_test.ts` (409 on `LocalEditsError`, 500 on plain `Error` as a regression fence) - [x] Manual E2E with locally compiled binary: scratch repo → `swamp extension pull @stack72/system-extensions` → edit a file → `curl POST /api/extensions/install` → got 409 with the expected remediation body. `swamp extension pull --force` → curl again → 200. - [x] systeminit#126's `datastore_auto_update_test.ts` and `resolve_datastore_test.ts` both still green after the helper refactor 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
…lve (swamp-club#133) (systeminit#1197) Fixes [swamp-club#133](https://swamp.club/lab/issues/133). ## Summary Auto-resolve treated any `pulled-extensions/<name>/` directory that existed on disk as "installed" — regardless of whether its contents were complete. A tree with `manifest.yaml` missing or a declared kind subdirectory missing still passed the `isInstalled` check, so the resolver emitted `reason: already_installed` and the downstream loader produced a confusing `Unknown datastore type "@swamp/s3-datastore"` / `Unknown model type: @swamp/issue-lifecycle` error with no pointer at what was actually wrong. This is a regression from [PR systeminit#1187](systeminit#1187) (swamp-club#121). Before that change, the adapter called `installExtension` with `force: true` on any load failure — a truncated tree self-healed on the next command. After systeminit#1187 the strict "never overwrite" guard caught the truncated case alongside the user-edit case it was meant for, and the truncation recovery path was lost. ## Approach The fix extends the "never overwrite" guarantee to truncation while giving users an actionable error. It is **read-only** — no filesystem mutation in the auto-resolve path. `--force` remains the only way auto-install state can overwrite a pulled extension. - Introduces a tri-state `InstallationInspection` result (`missing` | `intact` | `truncated`) on `ExtensionInstallerPort`, replacing `isInstalled` + `installedPath`. Both the path and the missing-files list ride back on the result. - The adapter drives the check from the lockfile's file list (authoritative file-level granularity, so partial truncation inside a kind subdir is caught too — not just whole-subdir removal). - The domain service branches on the three states: `missing` → install, `intact` → `alreadyInstalledButFailed` (unchanged systeminit#121 path for user WIP), `truncated` → new `alreadyInstalledTruncated` event naming every missing file plus the `swamp extension pull <name> --force` recovery command. Auto-repair was considered and rejected. The user-journey analysis surfaced that mutating the filesystem in response to a read-shaped command has too many quiet side effects for shared-repo / multi-worktree setups: silent version bumps (install is always `version: null` → latest), concurrent-worktree-rm reversal, masking of deliberate user state changes, and latency surprise. A loud, actionable error with an explicit recovery command is safer and keeps the systeminit#121 invariant intact. ## What changed - `src/domain/extensions/extension_auto_resolver.ts` — new `InstallationInspection` type, port method `inspectInstallation` (replaces `isInstalled` + `installedPath`), new `alreadyInstalledTruncated` event on `AutoResolveOutputPort`, three-way branch in `installAndLoad`. - `src/cli/auto_resolver_adapters.ts` — implements `inspectInstallation` against the lockfile + per-extension directory + per-file stats; wires the new renderer through `createAutoResolveOutputAdapter`. - `src/presentation/renderers/extension_auto_resolve.ts` — new `renderAutoResolveTruncated` for both `log` (three-line actionable error) and `json` (`{"event":"auto_resolve","status":"failed","reason":"truncated","missing":[...]}`). - `src/cli/auto_resolver_adapters_test.ts` — 7 new `inspectInstallation` tests replacing the 5 prior `isInstalled`/`installedPath` cases; covers missing / intact / truncated / partial-truncation / all-files-missing / empty-files edge cases. - `src/domain/extensions/extension_auto_resolver_test.ts` — updated mock port; new regression guards for the three-way branch (`intact → alreadyInstalledButFailed` preserved from systeminit#121, `truncated → alreadyInstalledTruncated` is new). - `integration/auto_resolver_truncated_test.ts` — new hermetic integration test: hand-written scratch fixture, deletes one declared file, asserts the truncated event fires with the missing file AND the on-disk tree is byte-identical after the resolve attempt. - `integration/auto_resolver_no_clobber_test.ts` — ported to the new port method (no behavior change; same systeminit#121 invariant). - `design/extension.md` — "Safety: never overwrite on-disk extensions" section extended with the tri-state taxonomy, truncation predicate, and JSON event shape for scripting consumers. ## Coverage Works across all kinds that go through auto-resolve (models, vaults, datastores) — any file missing anywhere in the extension tree is detected when auto-resolve fires for a type from that extension, because `inspectInstallation` walks the whole lockfile file list. Driver-only or report-only extensions (no auto-resolve trigger) are not covered by this PR; extending to those would require new `resolveDriverType` / `resolveReportType` plumbing and is scope creep for systeminit#133. Worth a follow-up if belt-and-braces coverage is desired. ## Test plan - [x] `deno fmt` clean - [x] `deno check` clean - [x] `deno lint` clean - [x] `deno run test` — 4445 pass / 0 fail (6 new adapter tests, 2 new domain-service tests, 1 new integration test; rewrote 5 systeminit#121 regression cases against the new port shape) - [x] `deno run compile` — binary built - [x] Manual reproduction against `/tmp/swamp-repro-issue-133` with the compiled binary: - **Before truncation:** `swamp extension pull @swamp/digitalocean` then `swamp model create @swamp/digitalocean/droplet test-droplet` succeeds. - **Truncate:** `rm -rf .swamp/pulled-extensions/@swamp/digitalocean/models`. - **Re-run `model create`:** json mode emits `{"event":"auto_resolve","status":"failed","reason":"truncated","missing":[68 files]}`; log mode emits three ERR lines naming the missing files and the `swamp extension pull @swamp/digitalocean --force` recovery. - **Follow the recovery command:** `swamp extension pull @swamp/digitalocean --force` restores the tree. - **Re-run `model create`:** succeeds. - [x] Tree on disk is byte-identical before and after the resolve attempt — no auto-repair, no silent overwrite. ## Follow-up - Driver-only / report-only extensions remain uncovered by auto-resolve (see "Coverage" above). Worth a separate issue if exhaustive cross-kind coverage is desired. - Root cause of truncation itself is still unknown (partial extract, interrupted install, concurrent worktree op). This PR makes the symptom actionable so the cause is easier to investigate when it next occurs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
…tion check (systeminit#1199) ## Summary The tri-state `inspectInstallation` introduced in systeminit#1197 stats every path in the lockfile's file list to detect a truncated install. That list includes compiled bundles under `.swamp/bundles/**`, `.swamp/vault-bundles/**`, `.swamp/driver-bundles/**`, `.swamp/datastore-bundles/**`, and `.swamp/report-bundles/**` — regenerable build artifacts, not source. When a user clears any of those caches (normal hygiene, stale rebuild, fresh worktree), auto-resolve flipped from `intact` → `truncated` and emitted: ``` incomplete — missing 1 file(s): ".swamp/bundles/91139983/system_usage.js" ``` …instead of the `alreadyInstalledButFailed` ("already installed at … run `--force` to recover") path that protects user WIP per systeminit#121. The motivating swamp-club#133 symptoms are all source-tree artifacts (manifest, kind subdirs, declared source), so the fix is to scope the truncation predicate to source files only. This filters bundle prefixes out of the stat loop in `inspectInstallation`. Prefixes are derived from `SWAMP_SUBDIRS.{bundles, vaultBundles, driverBundles, datastoreBundles, reportBundles}` so a future bundle-dir addition stays in sync. No filesystem mutation, no API/event/wire-format changes. ## Test Plan - [x] `deno fmt --check`, `deno lint`, `deno check` clean - [x] `src/cli/auto_resolver_adapters_test.ts` — 18/18 pass, including 2 new regression cases: - `inspectInstallation ignores absent bundle artifacts and stays intact` (covers all 5 bundle subdirs) - `inspectInstallation reports truncated only for missing source, not missing bundles` - [x] `integration/auto_resolver_truncated_test.ts` — passes (systeminit#1197/systeminit#133 source-truncation behaviour preserved) - [x] `integration/auto_resolver_no_clobber_test.ts` — passes (systeminit#121 no-clobber invariant preserved) - [x] `deno run compile` succeeded - [x] **End-to-end repro against the rebuilt binary:** `swamp extension pull @stack72/[email protected]` → edit pulled `.ts` with WIP marker + syntax error → `rm -rf .swamp/bundles` → `swamp model create @stack72/system-usage instance1` now produces: ``` Extension "@stack72/system-extensions" is already installed at … but failed to load. Local edits may be preventing it from registering — inspect the source and fix errors. To reset to the registry version and discard local changes, run: swamp extension pull "@stack72/system-extensions" --force ``` …instead of the buggy `incomplete — missing 1 file(s): ".swamp/bundles/91139983/system_usage.js"`. - [ ] UAT `tests/cli/extension/pull_wip_preservation_test.ts:156` in swamp-uat (CI gate — local run blocked by membership auto-trust collision unrelated to this fix) ## Cross-references - Introducing PR: systeminit#1197 - Fixes regression to swamp-club#121 / systeminit#1187 user-WIP preservation - swamp-uat coverage: swamp-uat#149 Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Fixes swamp-club#121.
Summary
workflow validateand other commands that trigger auto-resolution could silently overwrite local edits to pulled extensions. The auto-resolver adapter hardcodedforce: truewhen installing a type it failed to find locally, andinstallExtension's conflict check was gated on!force— so any user edits in the pulled directory werecopyDir'd over with no prompt, no warning, and no diff.The fix adds an
isInstalledcapability toExtensionInstallerPortand has the domain service consult it before callinginstall. If the extension is already on disk (lockfile entry and per-extension directory both present), the resolver surfaces a newalreadyInstalledButFailedevent 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 passesforce: falseand catchesConflictError, 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:132has a related but distinctforce: truebug with a different trigger and fix shape; tracked separately as swamp-club#126.What changed
src/domain/extensions/extension_auto_resolver.ts— port gainsisInstalledandinstalledPath; service gainsalreadyInstalledButFailedoutput event and the pre-install check ininstallAndLoad.src/cli/auto_resolver_adapters.ts— installer adapter implementsisInstalled(dual lockfile + filesystem check against the per-extension layout from feat(extensions): namespace pulled extensions by scoped name (per-extension layout) #1186),installedPathreturns the per-extension root,install()flips toforce: falseand catchesConflictErrorwith a defence-in-depth comment.src/presentation/renderers/extension_auto_resolve.ts— newrenderAutoResolveAlreadyInstalledfor bothlogandjsonmodes.design/extension.md— new "Safety: never overwrite on-disk extensions" subsection under Automatic Resolution.Test plan
deno fmtcleandeno checkcleandeno lintcleandeno run test— 4400 pass / 0 fail (2 new service tests, 5 new adapter tests, 1 new integration regression test)/tmp/swamp-repro-issue-121:system_usage.tschanged from user-edited to pristine registry version; WIP marker comment lost--forcerecovery commandFollow-up
force: trueinresolve_datastore.ts:132(related data-loss vector, different fix shape).🤖 Generated with Claude Code