Commit Graph

39 Commits

Author SHA1 Message Date
ethernet
7f1ddc70ce feat(python): move first-party runtime to 3.14; swap wake engine to pyopen-wakeword
requires-python >=3.14,<3.15 (was <3.14 ceiling): the old cap existed because
Rust-backed transitives lacked cp314 wheels; tflite-runtime (openwakeword's
hard linux dep) still caps at cp311, so the wake engine moves to
pyopen-wakeword 1.1.0 — py3 wheels + bundled tensorflowlite_c lib, whose
bundled melspectrogram/embedding models are byte-identical to the openWakeWord
v0.5.1 files (verified by sha256), and it loads the shipped hey_hermes.tflite.
Drops openwakeword/onnxruntime/ai-edge-litert/wake-tflite machinery entirely.
win32-arm64 gets a marker gate (no pyopen-wakeword wheel there; porcupine
covers wake). uv.lock regenerated for 3.14 (254 pkgs, all platforms).
Real-lib smoke verified: engine builds against the actual wheel, silence
scores 0, noise scores ~0.003 (threshold 0.6), scores flow 1:1 per frame.
2026-09-07 14:03:27 -04:00
ethernet
abfefbf1fa test(pm): exercise active profile propagation through real subprocess env 2026-09-06 22:26:02 -04:00
ethernet
7964d77059 feat(pm): assert plugin survival contracts through public admission
- 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)
2026-09-06 22:21:06 -04:00
ethernet
1a09c42414 refactor(bundle): share Python payload assembly across desktop and Termux 2026-09-06 22:21:06 -04:00
ethernet
0b30c2484d merge: integrate termux cli bundles into pm-clean
Merge ethie/cli-bundles at 0765ad689b.
Keep PM runtime publication, install identity, TLS policy, and module
boundaries from pm-clean.

Resolve the Node version-discovery method in its owning class. Preserve
staged tools if a repin download or publication fails. Carry extra-only
memory-provider setup through PM and retain restart-required reporting.
Keep target-specific TUI path assertions and discard obsolete self-lock
fixtures and the orphaned Windows service handler.

Verified locally with the canonical Python runner, root JS checks, TUI
checks/build, shell parsing, and workflow YAML parsing. Existing platform
pins and executable modes are unchanged. No full-suite CI, new bionic
bundle, or phone acceptance is claimed for this merge.
2026-09-06 20:13:45 -04:00
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
7f6d63bcbb fix(termux): classify bionic from its interpreter and run the Linux tests
The POSIX runner had a blank line after exec env, so it printed the environment and never ran pytest. Keep the command attached and prove failure propagation. The actual payload records ANDROID_API_LEVEL in sysconfig; use that instead of looking for text in a guessed libc file. Rebuild unproven runtime-library extracts from verified archives.
2026-09-06 14:12:12 -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
acba441012 fix(pm): align context-home publication and recovery scopes
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.
2026-09-06 13:31:24 -04:00
ethernet
3c08d16ba7 fix(pm): close runtime publication and updater audit gaps
Dependency publication now recovers interrupted config/facts changes before
activation and leases live generations during collection. Receipts retain
update correlation and failed steps across nested command boundaries.
Doctor and desktop surfaces report those failures through shared owners.

Move checkout updates out of the desktop facade. Stage a detached Windows
relaunch waiter before shutdown, with bounded handshake and process-birth
checks. Keep packaged lifecycle tests isolated from the installed app.

Native verification exposed two production races: cron maintenance imported
the interactive CLI and rewrote TERMINAL_CWD, and install-ID reads collided
with first publication. Use the existing owners and locks. Plugin checks
now run at startup and each due-gated housekeeping tick, not after 60 ticks.

Share updater-test mutation boundaries and remove collection-root fixtures.
Separate cold MCP startup from command latency and give the real HTTP drip
test enough time to reach body handling.

Root npm check passed, including packaging. The fixed-tree Windows Python
run reported 44557 passed, one failed, and 1404 skipped, plus one retry-only
HTTP test. Those final failures now pass in a 35-test bounded batch. A real
isolated gateway wrote startup and periodic plugin-check receipts.

Full final-tree CI, bundled Sandbox deployment, and actual App Installer
relaunch remain unverified. docs/pm-audit-status.md records these limits.
2026-09-06 11:45:41 -04:00
ethernet
92686159d1 fix(pm): integrate audited runtime and lifecycle repairs
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.
2026-09-05 22:36:48 -04:00
ethernet8023
0ae85925f3 feat(termux): pinned termux toolchain via pm (linux-arm64-bionic)
A seventh pm target (linux-arm64-bionic) stages the TUR python3.11
.deb, the termux nodejs/uv .debs, and their runtime-lib deps into the
payload -- same pm-consumer shape as the desktop legs: pm owns the pin,
the hardened download (redirect-safe, retry-wrapped), the ar+tar
DebPackage extraction, and file-evidence verify for binaries the
staging host cannot execute.
2026-09-05 20:00:00 -04:00
ethernet
5db2124515 fix(pm): drop x64 vcruntime140_1.dll pre-publish so the recorded digest matches shipped bytes
The win32-arm64 bundle stage deleted vcruntime140_1.dll (the x64 VC
runtime python-build-standalone ships beside ARM64 Python; it cannot
load there and would fail the arch guard) AFTER store.publish() — but
facts.record(digest=tree_digest(entry)) had already hashed the entry
WITH the dll. pm doctor's re-hash then flagged python on every
bundle: 'realized bytes do not match recorded digest' — red from the
commit that introduced both the drop and the digest check (3d12e86ef1),
masked until the smoke test's earlier dead-shim failure was fixed.

The drop belongs in Python.stage(), next to the macOS signing hook —
both are post-extract, pre-publish mutations, so the recorded digest
covers the shipped bytes. The cli-level _drop_unloadable_runtime_files
bundle hook is removed in full (its two tests now pin the stage()
contract). The pm-store CI cache key rotates to v2: failed arm64 runs
saved poisoned facts (dll-less entry + with-dll digest) under the old
key and would restore forever.
2026-09-04 19:01:27 -04:00
ethernet
ff685877f7 fix(pm): revert llamacpp pins 10679 → 10362 (b10679 darwin-arm64 links librdma.dylib)
PM Bundle darwin-arm64 lane failed verify: b10679's macos-arm64
llama binaries link librdma.dylib which doesn't exist on macOS — a
broken upstream release. 10362 was the known-good committed state that
passed every bundle lane. config_defaults' b10679 rolling tag is
aspirational; re-pin 10679 only when a darwin-working tag exists.
5890f3bbe3's re-pin was wrong — reverted.
2026-09-04 05:36:05 -04:00
ethernet
5890f3bbe3 fix(pm): re-pin llamacpp backends 10362 → 10679
Session 20260828_194913 bumped all four backends to b10679 with real
hashes; the pm-clean rebuild reverted the pins to 10362 while
config_defaults/binaries still reference b10679 rolling tags. Re-run
pm update (the designed resolution path) — lock now 19× 10679, 0× 10362.
2026-09-04 02:10:31 -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
1fc1d054af feat(receipts): update receipts embed the pm sync sections + plugin docs
- finalize_update_receipt folds the newest pm sync receipt's
  venv_rebuild / plugin_bisect / feature_list into the update receipt
  (pm_-prefixed) so ONE latest.json carries the whole update story —
  the embedding contract both receipt plans declared. Additive,
  exception-swallowing (an embed failure never breaks the update
  receipt).
- pm/receipt: _write_rotated mkdirs itself instead of depending on
  _receipt_dir()'s mkdir side effect (found via the embed tests — a
  patched dir lambda made writes FileNotFoundError under the OSError
  swallow).
- website plugins.md: the three new verbs (check-updates, adopt,
  trust-update-url) + the provenance/cadence/auto_apply/trust flow,
  user-facing.

tests: embed happy/absent/failure-isolated; existing receipt suites
green.
2026-09-03 14:01:37 -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
22e5a9d11a feat(plugins): update-check cadence — clock-gated, receipt-surfaced, apply-explicit
Task 3 of the plugin auto-update plan (settled 2026-09-03):

- hermes_cli/plugins_cadence.py: the read-only cadence. Clock gate
  (plugins.auto_update_check_hours, default 24, 0 disables) via a
  mtime marker; runs the checks, writes a plugin-check receipt (pm.
  receipt record_plugin_checks — the surface desktop/pm status
  already read), logs ONE actionable line for updates and one warning
  for needs-fixing (log, not system-prompt — cache safe). Opt-in
  auto_apply (plugins.auto_apply, default false): git rows ONLY,
  scan-gated by the update path; pinned/manual/drift/pip never
  auto-applied. A failed check still stamps the marker — a flapping
  network can't hammer PyPI/ls-remote every tick. Never raises.
- gateway/run.py: the boot kick — one daemon thread after the pm
  startup block; the clock gate makes it a no-op until due. Never
  blocks boot.
- pm/receipt.py: record_plugin_checks (additive section).
- config keys ride the existing plugins-settings shape (validated
  keys, no DEFAULT_CONFIG entry — written only when used, like
  index_url).

tests: 8 hermetic cadence tests (clock gate due/not-due/disabled,
config interval parsing, receipt write + marker once-only,
needs-fixing never applied, auto-apply opt-in + git-rows-only +
opt-out, check-failure non-fatal + marker stamp). Full sweep across
plugins + pm suites: 297 passed, 0 failed.
2026-09-03 11:55:41 -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
635881b830 feat(pm): lift GitHub API rate limits in pm update via GH_TOKEN
A full `pm update --check` hits every github-sourced package's releases
API at once and burns the unauthenticated 60 req/hr budget, degrading
to per-package `resolve failed`. Honor GH_TOKEN (falls back to
GITHUB_TOKEN) on all GitHub API calls: the releases-index helpers in
pm/update.py and the known_sha256 digest lookup in pm/packages.py.
Authenticated budget is 5000 req/hr.

Verified: header logic unit-checked (Bearer header only when the env var
is set), 12 pm update tests pass.
2026-09-01 23:01:23 -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