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.
- 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)
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.
Resolve deferred relative file links until no further alias can be materialized. Keep containment checks and leave unresolved cross-package copyright links alone.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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).
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.
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.
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`.
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
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.
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.