Commit Graph

24 Commits

Author SHA1 Message Date
ethernet
e9dfe6f7d8 fix(pm): retain chained library aliases on no-symlink hosts
Resolve deferred relative file links until no further alias can be materialized. Keep containment checks and leave unresolved cross-package copyright links alone.
2026-09-06 15:49:41 -04:00
ethernet
1ce0998ac9 refactor(termux): reuse package requirements and harden launchers
Use one requirements writer for the wheelhouse and installed venv. Do not retry failed hashes or paused downloads. Skip pure-wheel decompression, preserve runtime library notices, and keep the current working directory off the launcher import path. Document the prerelease canary package without migration or downgrade guidance.
2026-09-06 14:34:52 -04:00
ethernet
af8212b1f6 fix(runtime): route remaining feature consumers through pm
Migrate callers to declared extra names instead of adding legacy alias maps. Preserve lazy-install refusal behavior, isolate reimported test homes, and exercise the real callback boundaries. The relevant integrated batch passes 607 tests.
2026-09-06 14:34:51 -04:00
ethernet
7606b014f5 fix(termux): complete the sealed CLI payload and verify installed startup
Stage npm, ffmpeg and its bionic runtime libraries, and static ARM ripgrep. Bind caches to actual build inputs. Generate entrypoints from the project manifest and verify real CLI/TUI startup and media conversion offline. Keep versions unchanged. Windows service work remains out of scope.
2026-09-06 14:00:42 -04:00
ethernet
2d7d43c9be feat(pm): npm ci executor for plugin package.json sidecars — wired
The declared-but-unwired surface from the plugin-deps plan §B item 2:
scan_plugin classified package.json sidecars but nothing executed.

- pm/workspace.install_node_sidecar(): npm ci (with package-lock.json)
  or npm install (without) into the plugin's OWN node_modules — never a
  global prefix; pm's pinned npm store-first (PATH second, the same
  precedence as _uv_binary); lazy-install-gated; returns a reason
  string on failure, never raises. Injectable runner for hermetic
  tests.
- plugins install flow: a package.json plugin gets its own y/n consent
  question; a node-dep failure warns but never blocks the python-dep
  path or the install.

tests: 6 hermetic tests (no-package no-op, ci-vs-install selection by
lockfile presence, lazy-off refusal, failure reason surfacing, runner
explosion isolation). Full sweep: 210 passed, 0 failed.
2026-09-03 14:03:58 -04:00
ethernet
7a7abd3ebe fix(pm): active memory provider joins the union — the mnemosyne path
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.
2026-09-03 13:46:53 -04:00
ethernet
9ba553870f fix(pm): member stamp includes pyproject CONTENT — dep bumps re-sync
Task 4 of the plugin auto-update plan: members_stamp hashed resolved
paths only, so a plugin update that changed its pins left the venv
stamp unchanged — the union never re-synced and the new deps never
installed (path-only hashing made pulled-plugin dep bumps invisible).

members_stamp now folds each member's pyproject.toml bytes into the
hash: same member set + changed pins = changed stamp = re-sync. A
missing/unreadable pyproject degrades to path-only (a vanished
pyproject moves the stamp by dropping its content term — still
correct).

tests: content-change moves stamp, bare-dir hashes on identity,
vanished-pyproject moves stamp without crashing. tests/pm: 196
passed, 0 failed.
2026-09-03 11:56:34 -04:00
ethernet
d5922fb698 fix(pm): derive profile roots from get_default_hermes_root — custom HERMES_HOME joins the union
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.
2026-09-03 10:55:26 -04:00
ethernet
ef5a98ed57 fix(pm): union sync must install member deps — add --all-packages
Probed live 2026-09-03: plain `uv sync --frozen` installs only the
ROOT project's dependencies — workspace-member deps are locked by
`uv lock` but silently never reach site-packages (a member's
pyfiglet stayed absent under plain --frozen, present under
--all-packages). The union's whole contract is that plugin deps ride
the venv; without the flag every member install is a green-looking
lock over an empty site-packages.

- lock_and_sync now passes --all-packages, with the probe rationale
  in place so the flag can never be 'simplified' away.
- _uv_binary() falls back to shutil.which('uv') when the pm store has
  no provisioned uv — store-first, PATH-second (the activate()
  precedence), fixing 'uv is not installed' lies on dev machines and
  test envs that have uv on PATH but no store.
- tests/pm/test_union_installs_members.py: two REAL end-to-end tests
  (mini workspace, real uv): member deps land in site-packages after
  lock_and_sync, and SURVIVE a second sync (the update-rebuild prune
  contract). These fail under the old flagless command by
  construction — the probe scenario can never silently return.

Found while porting mnemosyne to the union (the manifest-less pip
route prunes on resync; the union route needs member deps to
actually install). tests/pm: 190 passed, 0 failed.
2026-09-03 10:28:23 -04:00
ethernet
ad433f8029 fix(pm): review fixes — enabled-state union, recency tiebreak, gated bridge, sync authority
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.
2026-09-02 20:21:00 -04:00
ethernet
bf242f3fe7 feat(pm): receipts as the universal machine-readable venv-op surface
Every pm venv sync — startup, plugin install, update rebuild — now
writes a receipt with the SAME schema the updater's receipts use,
into the same <HERMES_HOME>/logs/update_receipts/ dir, separated by a
'kind' field (settled 2026-09-02 plan, task 8):

- pm/receipt.py: begin/record_step/record_venv_rebuild/
  record_bisect/record_feature_list/finalize + rotation (keep 20) +
  latest.json pointer. snapshot() lets the updater EMBED the sync
  sections into its own receipt (one schema, one dir, one reader).
  Exception-swallowing throughout — receipt machinery can never break
  a sync.
- Venv.apply union path: records feature_list, venv_rebuild, and
  bisect decisions; outcome 'ok' | 'bisected' (failures raise before
  the receipt block and surface via the update receipt's error path).
- `hermes pm status` (new verb): prints the latest receipt as JSON —
  the CLI/TUI reader. Desktop IPC reads the same latest.json (the
  hermes:version/syncStatus wiring lands with the desktop branch's
  About/update surface, which already consumes update receipts).

tests/pm/test_receipt.py: 5 tests (roundtrip, latest-pointer,
no-begin no-op, snapshot lifecycle, empty home). tests/pm: 180
passed, 0 failed.
2026-09-02 20:02:15 -04:00
ethernet
ba39525c39 feat(pm): ship the uv cache + mutable-venv bootstrap seed for sealed installs
The blow-the-venv-on-update contract needs rebuilds to be cheap, and
sealed installs need a writable venv at all (settled 2026-09-02 plan,
task 7):

- uv_cache_dir(): hermes-owned machine cache at
  <default hermes root>/cache/uv — content-addressed, shared across
  profiles. uv_env() ALWAYS pins UV_CACHE_DIR there (ambient UV_*
  stripped), so the cache that ships is the cache that gets used.
  First call on a sealed install seeds it from the payload's shipped
  uv-cache/ (read-only payload can't serve uv's working cache); the
  .seeded marker makes it once-only and non-clobbering.
- pm bundle stages the warmed cache into the payload after the venv
  sync (uv-cache/ beside manifest.json) — warm 'uv sync --offline'
  rebuilds probed at 0.4s vs 1.2s cold.
- Venv.venv_dir(): sealed installs resolve the MUTABLE venv to the
  machine hermes root (<root>/venv), not the read-only payload;
  dev/source installs keep the repo-local venv unchanged.
- Venv.seed_mutable_venv(): the bootstrap seed — lazy-off installs
  copy the payload's shipped venv out as the starting point; lazy-on
  installs skip the copy (first sync builds fresh from the shipped
  cache). adopt() triggers it (KeyError-guarded, failure reported
  never fatal — a cold sync still converges).

tests/pm/test_uv_cache.py: 6 tests (env pinning + ambient strip,
payload seed + marker once-only, cold machine, sealed venv_dir, seed
copy idempotence, lazy-on skip). tests/pm: 175 passed, 0 failed.
2026-09-02 20:00:34 -04:00
ethernet
64a6564084 feat(pm): per-extra platform gates — [tool.hermes.extras-platforms]
Platform gating moves from scattered per-package markers to a
readable per-extra authority (settled 2026-09-02 plan, task 9):

- pyproject [tool.hermes.extras-platforms]: extra -> PEP 508 marker.
  matrix = linux-only (python-olm has no win/darwin build);
  google-chat / mem0 = all but win32-arm64 (grpcio has no win_arm64
  wheel). The per-package markers stay — uv's resolver needs them for
  --all-extras (probe: a fully marker-gated extra syncs clean on
  Windows); this table is the readable single authority for WHICH
  extra is supported WHERE.
- pm.extras.extra_supported(): gate consult with an installed-override
  rule (an extra whose anchors import on this machine is supported
  regardless — dev machines, hand-synced venvs) and a fail-open-on-
  malformed-marker posture. Cached table read.
- available() reads gated-off extras as unavailable; ensure_import()
  raises InstallError naming the gate instead of a resolver no-op —
  the adapter degrades with a readable reason, never a mystery.

Live-verified on this win32 host: matrix -> not supported (gate:
sys_platform == 'linux'), web -> supported, table parses.
tests/pm: 169 passed, 0 failed (4 new gate tests).
2026-09-02 19:57:36 -04:00
ethernet
1e97613206 feat(pm): venv-union auto-bisect — fail-alone disabled, incumbent wins
When the plugin union fails to resolve (update rebuild, plugin
install, any sync), resolve_union() bisects instead of leaving the
venv broken (settled 2026-09-02 plan, task 6):

- try the full union first — clean resolve passes through untouched;
- phase 1: each member alone against core — a plugin that conflicts
  with core pins is disabled with the resolver's message;
- phase 2: reduced union retry; mutual conflicts resolve incumbent-
  wins — the newest-enabled member (last in the discovery list) is
  disabled, the longer-standing setup survives; retries until the
  union resolves or every member is dropped.

Venv.apply routes the union through resolve_union and logs each
disable decision with its resolver reason (receipt surfacing lands
with the universal-receipts task). Pure and unit-tested with stubbed
lock_and_sync: pass-through, fail-alone, incumbent-wins tiebreak.

tests/pm: 165 passed, 0 failed.
2026-09-02 19:55:44 -04:00
ethernet
02eb115f60 feat(pm): frozen bundle feature set — enabled-features.json
Lazy installs OFF is now a real contract, not just 'refuse
everything': the bundle's EXACT extras list is the frozen feature set
(settled 2026-09-02 plan, task 5).

- pm/features.py: write_features()/read_features() over
  enabled-features.json at the payload root (bundle-written, beside
  manifest.json; at runtime store_root().parent — the same relative
  location on both install kinds). installed_extras() records what
  `uv sync --all-extras` ACTUALLY installed on the target: every
  declared extra whose pm anchor resolves in the staged venv —
  marker-gated extras show up as missing anchors, the honest
  per-platform record.
- cmd_bundle: after the staged venv sync, write the features file into
  the payload ('✓ enabled-features.json (N extras recorded)').
- sync_venv: when security.allow_lazy_installs is false and the file
  exists, requested extras OUTSIDE the frozen set are refused with a
  message naming the policy.
- Venv.apply: plugin members + lazy-off + frozen file = loud InstallError
  (the bundle IS the install; never union plugin deps into it).

tests/pm/test_features.py: 7 tests (roundtrip, absent/garbage reads,
payload-root path, anchor-truth installed_extras, frozen refusal,
in-set pass). tests/pm: 162 passed 0 failed.
2026-09-02 19:54:34 -04:00
ethernet
86dee7e074 feat(pm): auto-pickup scan + legacy pip_dependencies bridge
Third-party directory plugins declare python deps two ways; both now
install through the workspace union (settled design:
.hermes/plans/2026-09-02_164500-plugin-deps-workspace-union.md):

- materialize_legacy_pyproject(): a plugin.yaml with pip_dependencies/
  python_dependencies and no user-owned pyproject gets one GENERATED in
  its dir (specs carried verbatim, 'GENERATED by pm' header marks pm's
  own output — a user pyproject is never touched, a generated one is
  rewritten on spec change, byte-identical regen is a no-op).
- scan_plugin(): auto-pickup classification of one plugin dir —
  pyproject (python), package.json (node sidecar), packages.py (pm
  store binaries), legacy manifest deps (bridge).
- plugins.py manifest parser: pip_dependencies was whitelisted-but-
  silently-dropped (the memory-plugin legacy key real external plugins
  like basic-memory use); now parsed into python_dependencies with a
  deprecation warning naming the migration path.
- _warn_python_dependencies(): the 'declaration seam ONLY' posture is
  replaced by the bridge — materialize + presence-check, pointing at
  the pm sync as the installer (conflict = loud refusal there).
- memory_setup._provider_extras(): external legacy manifests bridged
  too, so hermes memory setup heals an external provider's deps through
  the union instead of print-only advice.

tests: 3 new (materialize happy/idempotent/rewrite, skip-modern/
depless, scan classification); tests/pm 155 passed 0 failed.
2026-09-02 19:48:21 -04:00
ethernet
878d33c3ad feat(pm): generated uv-workspace root unions plugin deps into the venv
pm/workspace.py owns the generated workspace root (settled design:
.hermes/plans/2026-09-02_164500-plugin-deps-workspace-union.md):

- build_root() writes <store_root>/.pm-workspace/pyproject.toml =
  core's pyproject verbatim + [tool.uv.workspace] members pointing at
  each enabled plugin dir via relative ../-escaping paths. The
  committed pyproject is never touched (sealed installs are read-only;
  member lists are machine-specific).
- enabled_member_dirs() scans all profiles' plugin dirs for python-dep
  declarations: pyproject.toml (modern) or legacy pip_dependencies/
  python_dependencies in plugin.yaml (bridge candidates). Per-install
  union (settled): profiles share the venv, so their plugins share the
  resolution graph. OSError-safe walks (dangling junctions).
- lock_and_sync(): build root -> uv lock -> uv sync --frozen --extras
  into the venv (UV_PROJECT_ENVIRONMENT pinned). One lock, one graph:
  core pins preserved (proven live: union of the real repo pyproject +
  a plugin kept rich==14.3.3 / httpx==0.28.1 while adding the plugin's
  own dep), conflict = loud resolver refusal naming both sides
  (proven live: a plugin pinning rich==13.9.4 against core's 14.3.3).
- Venv.expected_stamp folds in members_stamp() so a changed plugin set
  re-syncs; Venv.apply routes through the workspace root when members
  exist, plain uv sync otherwise (no behavior change for
  zero-plugin installs).

tests/pm/test_workspace.py: 8 tests (root location, verbatim core
carry-over, relative escaping members, idempotent build, zero-member
build, stamp hashing, member discovery incl. legacy + broken-root
resilience). tests/pm suite: 152 passed, 0 failed.
2026-09-02 19:43:35 -04:00
ethernet
52510d26fd fix(pm): migrate the last tools.lazy_deps call sites to pm extras
tools/lazy_deps.py was deleted by the pm migration but 8 call sites
still imported it — slack and feishu lazy-install were dead (ImportError
swallowed by platform_registry's ensure_deps_fn guard, platform stayed
disabled with a warning), and cua/anthropic/read_extract/vision/
dashboard self-heal paths silently skipped their installs.

- slack: pm.extras.ensure_and_bind('slack', ...) (slack extra)
- feishu: pm.extras.available / pm.ensure_import('feishu')
- cua_backend: pm.ensure_import('computer-use')
- anthropic_adapter: pm.ensure_import('anthropic')
- read_extract: pm.ensure_import('doc-extract')
- vision_tools: pm.ensure_import('vision') + new 'vision' -> PIL anchor
  (extra is a no-op alias, Pillow is core; anchor keeps availability
  checks working)
- web_server dashboard import: pm.ensure_import('web')
- web_server memory-provider pip install surface: the deleted
  install_specs pipeline is replaced with an actionable manual remedy
  status until the plugin-deps workspace bridge wires this surface
- wecom callback docstring: drop the stale LAZY_DEPS vocabulary

test_pin_lockstep now asserts the core pin and doc-extract extra pin
of firecrawl-anydoc agree (pyproject is the single pin authority);
new tests/pm/test_no_lazy_deps.py keeps the migration complete with a
tree-wide contract (scripts/release.py credit comment + the dashboard
legacy surface documented as allowed).

tests/pm 144 passed; touched lazy-install suites green; remaining
tests/plugins failures proven pre-existing on the unmodified tree
(Windows host artifacts).
2026-09-02 19:38:30 -04:00
ethernet
3cbd410d4b feat(pm): resolve llama.cpp updates from the llama.app installer bucket
The llama-install.sh installer keeps its own version index on Hugging
Face (ggml-org/install.sh): a `latest` pointer (resolve/latest →
"b10679") plus a per-build tree of prebuilt llama-app binaries. That
pointer IS the installer's updater — the "what should we be on now"
signal, unauthenticated and unrate-limited.

Wire the llama.cpp package family to it:
- llama_app_latest() reads the `latest` pointer (bare build number).
- llama_app_bucket_versions() enumerates the tree API (deduped, sorted
  desc). Verified: the HF bucket tree API IGNORES the offset param —
  every offset returns the same first page (the oldest ~1000 paths) — so
  the tree can only see the OLDEST builds; `latest` is the authoritative
  source and callers put it first.
- LlamaCpp.latest_versions(): [latest, *tree] when the bucket answers,
  falling back to the GitHub releases tags when it doesn't. Artifacts
  still fetch from the llama.cpp GitHub releases (1:1 tag correspondence
  — every bucket tag is a GitHub release tag, so a bump always has our
  per-target assets); the bucket's CONFIG-coded llama-app binaries are
  hardware-probe-derived and not precomputable, so they stay out.

Live: `pm update --check llamacpp-cpu` → 10362 → 10679 via the bucket.
7 new pure tests (monkeypatched fetchers) covering latest parsing,
HF_TOKEN auth, tree dedupe/sort, and the GitHub fallback.

Verified: 143 pm tests pass.
2026-09-01 23:31:15 -04:00
ethernet
c52974ce8a feat(pm): add pm update — resolve latest versions, re-pin the lockfile
Each Package subclass now declares how to find its own latest via a
latest_versions(target) hook (empty = no auto source). `hermes pm update`
intersects those candidate lists across every target the package serves,
compares against the lockfile, and re-pins + reinstalls the changed ones.

Version styles (Package.version_style):
- semver (default): one shared version across targets; update = highest
  version every relevant target serves (node, uv, gh, ripgrep, git,
  cua-driver, llama.cpp, agent-browser, npm).
- minor (ffmpeg): posix martin-riedl and win32 BtbN autobuilds have no
  shared release cadence, so the lockfile version label is major.minor
  and each target's exact patch lives in ITS artifact urls. An update
  moves to the highest major.minor every target serves; within it each
  target pins its own newest patch. A patch-only drift inside the shared
  minor does not move the label.

Resolvers hit the real upstream indexes (GitHub releases with prefix
strip + version-shape filter that drops sandbox/experimental tags,
nodejs.org index.json, npm dist-tags, martin-riedl root page parse for
per-platform epochs, BtbN autobuild assets, python-build-standalone
filtered to the locked 3.11 line — a major bump is never automatic).

CLI:
- `pm update [names...]` — re-pin + install changed packages, sync venv
- `pm update --check` — dry-run report (exit 1 if updates exist), never
  writes the lockfile, store, or venv
- `pm update --target T` — check-only cross-resolution for another target
- `pm update --uv` / `--npm` — also refresh uv.lock + venv / package-lock

chromium + chromium-headless-shell have no resolver (they follow
agent-browser, which pins its own browser pairing); venv is a state
package. Tests are pure (monkeypatched candidate lists) — the index
helpers are network I/O by design.

Verified: 136 pm tests pass; live `pm update --check` resolves node
26.7.0 → 26.8.1, uv 0.12.3 → 0.12.9, ffmpeg up to date, and reports
`no source` for chromium. GitHub unauthenticated rate limits (60/hr)
degrade to an honest per-package `resolve failed`.
2026-09-01 22:59:19 -04:00
ethernet
578a15a190 fix(pm): anchor downloader partials to a writable shared root
store_root() resolves into the read-only sealed payload on MSIX installs
(WindowsApps/agent-payload/tools), so the default partials area there
failed with WinError 5 on local-model downloads. Partials are mutable
machine-scoped state keyed by sha256(url); anchor them to the default
hermes root (cache/partials) instead of the immutable byte store.

- pm/paths.py: add partials_root(); drop the _default_partials indirection
- pm/downloader.py: Download resolves paths.partials_root() directly
- pm/cli.py: pm gc sweeps the new partials area (store root no longer
  contains them); guard bails only when both roots are absent
- tests: point resume + gc tests at the new root
2026-09-01 14:44:19 -04:00
ethernet
3de5990bd0 fix(pm): prune fetch-* cache and stale entries during bundle, test gc
The store's fetch-<sha> download-cache archives are install-time only —
dead weight once a package is published, in a staged payload AND in the
CI cache that restores apps/desktop/build/agent-payload/tools. They were
never in facts.entries_in_use(), so gc already dropped them, but nothing
proved it and nothing ran the sweep during the bundle, so the cached
store accumulated orphaned versions from older locks.

- extract the sweep core into _gc_store() (shared by pm gc and the
  bundle command).
- run it at the end of pm bundle, before venv sync and packaging, so a
  staged payload (and the CI cache) ships only live entries.
- test that gc removes fetch-<sha> dirs while keeping the live package
  entry.
2026-09-01 11:24:45 -04:00
ethernet
a9be133aea merge: local-models (upstream/feat/local-models) onto the pm-clean stack 2026-08-31 18:00:49 -04:00
ethernet
3d12e86ef1 feat(pm): unified package manager — pm store foundation
Introduce the pm store: a unified, hash-verified package store that
replaces lazy_deps and the old installer's ad-hoc tool downloads.
Store tools are provisioned on PATH (ffmpeg, node/npm via pinned uv),
with a resumable 8-way downloader, verify() returning failure reasons,
and adopt() made EPERM-safe. chromium ships in the payload for every
target. The 3600-line install.sh is replaced by a staged bootstrapper
(heavy deps are pm's job after this); setup-hermes.sh, Dockerfile and
nix pin tables are rewired onto the store. Old install-script tests,
lazy_deps/managed_uv/build_info, and the ps1/bash installer test
batteries are removed with the machinery they tested.

Rebuilt from ethie/pm onto upstream/main (ac6c8028e0) after the
utf-8-sig sweep. 16 hot files (main also churned them) hand-merged:
platform adapters, main.py, electron/main.ts, tui_gateway/server.py,
cua_backend, installer-tests workflow, install.sh (full rewrite),
setup-hermes.sh, plugins doc.
2026-08-31 18:00:48 -04:00