Activation reaches plugin discovery before the application dependencies
exist. Give PM its own locked Python project and runtime so it can install
or repair the application without importing that dependency tree.
Keep PM outside the application workspace. A shared uv workspace resolves
the application graph and cannot provide this isolation. Route mutations
through an isolated worker and preserve transaction callbacks, cancellation,
custom package registrations, and correlated receipts.
Use the same runtime builder for source installs and packaged payloads.
Keep offline wheelhouse support in that builder. Nix builds the independent
PM lock as a separate derivation. Refuse lazy-disabled bootstrap before
installing tools or dependencies.
Move first-party YAML readers and writers to ruamel. Keep the application
lock's transitive PyYAML requirements for third-party packages.
Verification:
- Focused canonical Python suite: 177 passed, 1 host-gated skip.
- Electron backend probes: 12 passed. Electron typecheck passed.
- Both uv locks, scoped lint, Bash syntax, and whitespace checks passed.
- Cold activation, corrupt-app repair, offline staging, and relocation ran.
- Built and exercised the Nix PM runtime and standalone YAML merge script.
Six broader caller test files retain the same 24 failing test IDs as an
archive of HEAD. The existing real-home guard blocks those tests before
they can exercise the affected paths. No full-suite pass is claimed.
Native Windows signing and full Bionic package execution remain unverified.
Keep downloads bound to their remote representation and publish through
atomic destination-local staging. Serialize shared partial ownership.
Keep explicit CA trust scoped to provider probes. Preserve checkpoint
history and edited files, validate all profile inputs before dependency
publication, and separate data removal from installed runtime ownership.
Exclude machine-specific PM state from portable transfers. Keep plugin
files and nested skill tools intact. Preserve native test isolation.
Focused native Windows receipts cover the individual repairs and their
integration. This commit does not claim a full-suite or release build.
Pin uv and uvx to the PM interpreter instead of ambient Python discovery.
A matching dependency stamp cannot prove that installed files still exist.
Repair now rebuilds the recorded workspace and lock in a fresh generation,
checks startup imports, and publishes the selection only after success.
Run startup recovery before dependency activation. Keep manual PM repair
reachable when the selected environment is damaged. Preserve plugin
selection, retry ownership, and the previous generation on failure.
Remove the separate pip, ensurepip, per-extra, and install-time quarantine
ladders. Keep orphan launcher restoration.
Verification: 717 targeted tests passed on native Windows ARM64, with
56 skipped. Ruff, diff checks, and the source-scoped compat check passed.
A disposable real Hermes install recovered deleted YAML and dotenv files,
then printed CLI help with exit 0. Its lock and stamp stayed unchanged.
The full suite and a release build were not run for this change.
- fix conflict classification: uv's real 'Requirements contain
conflicting URLs' refusal now matches _RESOLVER_MARKERS (was
misclassified as a generic install error)
- correct stale bisect docstrings: pm has no automatic
bisect/disable decision; memory-provider preference is a stated
requirement for any future decision, not implemented behavior
- new tests/pm/test_plugin_survival_contract.py: sidecar without a
root dependency surface excludes nested/external pyprojects from
the union; public admit_plugin_set_change refuses an unsatisfiable
offline local-source union with identity+reason, leaves the
candidate unenabled/unimported, preserves plugin trees and config,
records the failure receipt, and the retry commits the resolvable
candidate; active context home exports to wrapper subprocess env
- document the HERMES_HOME survival contract for memory-provider
wrappers (mnemosyne-oss/mnemosyne#859)
Resolve dependency state and the plugin union from the active home without changing process environment. Use the existing home-root derivation for custom and named profiles. The journal validator checks the same root as the writer.
Verified 71 tests passed, 8 skipped across root resolution, union, recovery, and selection. The new context-only-home regression failed before the fix.
Prepare dependency generations before selecting them. Keep shipped tool
bytes separate from writable additions, and store facts beside their entries.
Validate proposed plugin sets before config publication. Restore the previous
config if the facts write fails.
Consolidate duplicate updater, backup, setup, and voice helpers. Repair
launcher selection, dependency consumers, download ownership, update feeds,
and native Windows process and file handling.
Verification: 206 changed/prior-failing Python files reported 4630 passed,
one failed, and 330 skipped. Fix the remaining Hindsight fixture boundary.
The final targeted rerun reported 234 passed and two skipped. The store
review regression batch reported 83 passed and one skipped. Desktop
TypeScript checks, 56 selected Electron tests, 24 release tests, and the
removed-import/compatibility guards passed.
This is an integration checkpoint, not full audit acceptance. The complete
Python suite has not run on this fixed tree. Crash-atomic plugin publication,
generation cleanup, receipt correlation, and packaged lifecycle acceptance
remain open in docs/pm-audit-status.md.
Memory providers install via memory.provider (mnemosyne's documented
path), not plugins.enabled — enabled_member_dirs never saw them, so a
provider's dep plugin never joined the workspace union. The ordered
enabled read now appends the home's active memory.provider (when its
plugin dir exists on disk; no-dup; rides LAST so the incumbent-wins
tiebreak prefers older plugins over the provider).
Closes the our-side half of the mnemosyne port; the upstream half is
their member manifest. tests: provider joins, ghost-provider skipped,
dual-listed not duplicated. tests/pm: 199 passed, 0 failed.
Profile-discovery gap (found in deployment review): with a custom
HERMES_HOME root (Docker /opt/data, non-default local roots), profiles
live under <HERMES_HOME>/profiles/<name>, but the new pm code scanned
Path.home()/.hermes/profiles in two places (plugins_state._profiles_root,
workspace._plugin_dir_roots). Result: an enabled dep plugin in a
custom-root profile was silently omitted from the union, and bisect
disable decisions never wrote back to that profile's config.
Fix: both sites derive from get_default_hermes_root() — the ONE
authority (hermes_constants), which already handles every layout:
custom HERMES_HOME → that root directly, profile-mode
<root>/profiles/<name> → <root>, standard ~/.hermes unchanged. The
hardcoded restatement (with a comment even citing the HOME-anchor rule
as justification — the authority IS home-anchored, it just also
handles custom roots) is gone.
tests/pm/test_custom_root_union.py: 4 e2e tests through the REAL
authority (no path mocks on the derivation itself): Docker-shape root
(union discovery + disable write-back to the profile's config),
standard layout (unchanged), profile-mode HERMES_HOME (sibling
profiles visible). tests/pm: 194 passed, 0 failed.
Spec + standards review (2-subagent /code-review) found five real
gaps against the settled design; all fixed:
- enabled_member_dirs() now FILTERS by enabled state: only plugins in
some profile's plugins.enabled join the union (a disabled plugin
never syncs). Result is ENABLE-RECENCY-ORDERED (newest last) via
new pm/plugins_state.py — order-preserving reads of every profile's
enabled list — so the bisect's incumbent-wins tiebreak (pop last)
now disables the most-recently-enabled, not the alphabetical last.
- materialize_legacy_pyproject() is gated on lazy_installs_allowed():
lazy-off installs keep the plugin dir untouched (materializing
would create a member candidate and then hard-fail every sealed
sync). Settled: 'never runs when lazy installs are disabled'.
- plugins_cmd dep install routes through resolve_union + the lazy
gate (it previously drove lock_and_sync directly, bypassing both):
the would-be union resolves as a check before enable, with the
plugin's own bisect decision surfaced as the refusal reason. The
real sync after enable still runs through sync_venv (the one
authority, with receipt + write-back).
- resolve_union disable decisions are written back to the plugins
enabled config (record_disabled_plugins → pm.plugins_state.
disable_plugins) so hermes plugins list reflects reality and
re-enable retries; best-effort, never breaks the sync.
tests: member discovery now asserts enabled-filter + recency order +
orphan exclusion; plugins_state suite (5: cross-home reads, order
preservation, disable write-back across homes, noop, garbage-config);
materialize lazy-off test; deps_flow updated to the resolve_union
shape. tests/pm + deps_flow: 193 passed, 0 failed.