Opt-in shared metrics could not say whether anyone turns on
compression.prepare_ahead, or whether the passes it starts pay off.
- hermes.install.snapshot gains compression_prepare_ahead (on/off), read
from the profile config with the agent's own truthiness. Pre-upgrade
snapshot rows keep a legacy field set and the v4 schema field is
optional, so they still validate and package.
- New closed-enum counter hermes.prepared_compaction.count (mark name =
metric name, projected by the decision-mark table): event, origin,
reason, failure_class (compression vocabulary), wait_bucket.
record_prepared_compaction() takes raw values, normalises inside, is a
no-op unless collection is on and never raises.
- Vocabularies live in shared_metrics_prepared_compaction.py (the
contract sits at the 2,000-line cap); the contract imports it.
- v4 schema extended in place (additive), docs row + snapshot bullet,
relay smoke asserts the snapshot field.
Review finding "Exit can drop queued metrics" (Greptile P2 + independent review W2): the smoke's
docstring claimed stopping the auto-started gateway settles its asynchronous rows, but the gateway's
graceful exit ends in os._exit, which skips the shared-metrics flush. Poll the probe home's store
(read-only, so polling never creates it) with a 30 s deadline until the gateway turn rows validate,
then stop the gateway; on expiry the store validation reports what is missing.
Review finding "Cleanup repeats weaker process checks" (Greptile P2 + independent review W1): the
smoke's gateway stop trusted the probe home's gateway.pid record (kind + home) and signalled whatever
process held that PID, so a live non-gateway process or a recycled PID named by a stale record was
terminated. Resolve the PID with gateway.status.live_gateway_pid_for_home (start-time reuse guard,
gateway command line, home ownership) and do nothing on a miss; psutil binds the create time so
terminate() refuses a PID reused in between. The driver puts --hermes-repo first on sys.path so the
check comes from the tree under test.
The smoke's expected counters predate three merges, so on origin/main it stopped at the first
assertion:
- #106742 (unified gateway runtime): `hermes chat -q` is now served by a gateway it auto-starts
for the home, which adds hermes.gateway.reply_latency and hermes.platform.delivery (platform
local) and a second hermes.startup.latency row (surface gateway_boot).
- eb2edce53a: the turn's read_file call now also records hermes.tool_batch.count.
- e76fb951a1: a failed update counts the stage it died in, so the fetch-failed receipt adds an
(apply, failed) hermes.update.stage row.
Expectations now live in one place: a single EXPECTED_METRIC_NAMES set for the SQLite and the
package checks, exact dimension dicts for the new rows, the update stages derived from the receipt
the probe feeds (UPDATE_RECEIPT, also used to build the probe), and both startup surfaces.
The smoke also stops the gateway the chat auto-started (pid record of its own home, then wait),
so a run no longer leaves a detached `gateway run --idle-exit` behind for 10 minutes, and the
gateway's asynchronous rows land before the package export.
The smoke is not run by CI, so the test file now pins its expectations to the contract: every
expected name is a contract counter, every exact dimension dict has the metric's current field set
and passes counter_dimensions_are_valid, the startup surfaces are contract surfaces, and the
expected update rows equal update_receipt_fields(UPDATE_RECEIPT).
- tui_gateway _load_cfg_raw: only FileNotFoundError means "no config".
config_version_or_none folded every stat error into None, so after a
cached absent config a transient EACCES matched the cached {} and the
next config.set could save it over the now-existing file. Any other
stat error is a FailedConfigRead again, which _save_cfg refuses.
- ConfigBackend.read_user_doc replaces read_user_layer/UserLayer: no
caller used the version it stat'ed on every uncached read; cached
loaders already ask config_version first.
- Reader lint scopes follow Python: a self.<attr> bound in one method is
seen by every method of the class, and a comprehension's target no
longer leaks into the enclosing function. The class rule found
pm/publication.py's compare-and-swap re-read of self.path, now marked
like its twin in __init__.
Rebasing onto current main brought in direct config.yaml readers that the
seam's reader gate (scripts/check_config_yaml_readers.py) now rejects:
- gateway/session_config.py: the session `config.get mtime` getter uses
config_exists/config_version + the TUI's _version_token, like
tui_gateway/methods_config.py already does.
- hermes_cli/profiles.py: _strip_multiplex_flag checks config_exists.
- hermes_cli/config_backups.py: the byte comparison sits inside the
supports_file_tooling()-gated block; marked like its siblings there.
- hermes_cli/update_receipt.py: _bare_collection_enabled runs in the bare
bootstrap interpreter with no config stack; presence stat marked.
Main also turned on UP006/RUF100/B009 since the branch base: builtin
generics in the seam's new modules, stale noqa comments dropped, and the
newly unused typing imports removed.
- pm read_home_selection: only a missing config.yaml means "no config". A symlink loop or
permission error used to look like "no file" (config_exists is Path.exists), so PM published a
dependency generation without that home's plugins; it now refuses like any unreadable config.
The stat runs before the YAML stack (FileBackend.version_path stats before importing utils),
so PM's bare runtime tree still needs no YAML for a home without config.
- check_config_yaml_readers: alias bindings iterate to a fixed point instead of two passes, so
`first = home / "config.yaml"; second = first; third = second` binds `third` too.
- ConfigIssue/_issue move to hermes_cli/config_issues.py, imported by the facade and its check
siblings, so config_value_checks.py / config_home.py no longer import the facade at all.
- get_config_backend(): a shell HERMES_CONFIG_BACKEND that the launch home's .env (or managed
.env) overrides no longer refuses at import time. hermes_cli.config reads config before
load_hermes_dotenv() runs, so the selector applies those layers in the same order before refusing.
- _route() returns (ConfigBackend, home) for every path: explicit non-home files are served by
_ExplicitFile, a FileBackend bound to that path, so the helpers call one interface instead of
branching on an is_layer flag.
- config.get mtime: derived from the backend's opaque version (file backend unchanged: seconds
from st_mtime_ns; other versions: a stable non-zero crc32).
- The profile roster's config-model cache keys on config_version(), not a local stat; the
cache moves to hermes_cli/profiles_cache.py (keeps profiles.py under its cap).
- check_config_yaml_readers: catches io.open / builtins.open / os.open and path keywords
(open(file=p)); scripts/check runs it locally as config-readers.
update_channel now writes through hermes_cli.config_backend, so the CI
classifier lists it as an update dependency. The MoA transient-read test
patched hermes_cli.config.fast_safe_load, which the seam no longer calls;
it now patches utils.fast_safe_load, the parser the FileBackend uses.
The reader gate only bound plain `name = <config path>` assignments and never scanned pm/, so
direct local-file reads still passed it: `env_path, config_path = ...` then
`config_path.exists()` in tools/send_message_tool.py, and pm/plugins_state.read_home_selection
reading config.yaml with read_text().
The gate now binds tuple-unpacked elements, `self.<attr>` targets and for-loop targets; flags
unbound `Path.<method>(p)`, shutil copies and pm's byte readers (read_bytes_or_none,
file_digest); and scans pm/.
Routed through the backend: send_message_tool's missing-config check and
read_home_selection (same FileNotFoundError / OSError / YAMLError -> ValueError contract).
Marked as file tooling: pm's byte-level compare-and-swap publication/eviction rewrite and the
LKG backup copy, which operate on the local file itself.
Known limit: bindings flow into nested scopes, not across sibling methods, so a `self.x` bound in
`__init__` is not tracked in another method (pm/publication.PluginSelection.publish).
Add hermes_cli/config_backend.py (config-config design §4.1, D10/D11): a
ConfigBackend protocol, the FileBackend (today's behaviour), selection by
HERMES_CONFIG_BACKEND only (default file; 'remote' and unknown values fail
closed with ConfigBackendUnavailable), and path-level helpers
(read_config_doc, read_config_doc_readonly, config_version, config_exists,
write_config_key, write_config_document, supports_file_tooling).
Migrate every direct reader/stat/exists/bypass writer of a user config.yaml
to the helpers (config.py loaders + caches, config_effective, env_loader,
gateway, TUI, CLI watchers, cron, tools, profiles, logging, time). File
tooling (config edit, backups, xAI migration rewrite, raw dashboard editor)
checks supports_file_tooling() first; migrations skip write-back without it.
Add scripts/check_config_yaml_readers.py (CI lint next to the writers gate)
that rejects a direct read of a config path outside the backend.
Review round on #144647 (head d016f04694d):
- CI routing: the refresh (slot claim, bounded runner, stale check, detached spawn) moves to
hermes_cli/git_commit_graph.py, routed to e2e_upgrade with the update pipeline. The update path
imports it instead of all of worktree_ops; worktree_ops keeps its repack seams and imports the
shared slot claim and runner from it (no cycle).
- W1: the -w pass runs in whatever repo it was launched from, so its refresh sweeps only the
object-store locks (clear_stale_git_locks(names=OBJECT_STORE_LOCK_NAMES)); index.lock / HEAD.lock
there belong to the user's git. The install checkout's update sweep is unchanged.
- W2: timeout/exit reaping SIGTERMs the git tree first and SIGKILLs only after a 5 s grace
(kill_process_tree(grace=), default unchanged for every other caller), so git deletes
commit-graph-chain.lock and tmp_graph_*. The age-gated tmp sweep now also takes
objects/info/commit-graphs/tmp_graph_*.
- N1: a breakaway-forbidding Windows job gets the no-breakaway retry, and a spawn that fails gives
the 6 h slot back.
- N2: the detached child logs a failed write (git's fatal line) to the home's agent.log/errors.log.
- core.commitGraph=false in the target repo skips the write.
- N3: a successful no-op write stamps hermes-commit-graph.fresh, so an idle repo is not rewritten
every slot.
- N4: test_update_finish's completion child and every in-process hermes_cli test stub the
detached spawn; no writer outlives a test run.
- Greptile: the refresh test now runs on a treeless clone over a Bloom graph and asserts the
write adds no pack (red when --no-changed-paths is dropped).
hermes update rebuilt the TUI, the web UI and the desktop app but never apps/desktop/dist-webapp.
A gateway with webapp.enabled kept serving the bundle built before the update, which then talked
to the updated backend with an outdated contract.
The update now rebuilds that bundle when any home sharing this install has webapp.enabled, with
prepare_webapp_renderer, the function hermes webapp uses (a current bundle is reused). Like the
other products it is attempted on its own: a failure is reported with the rest at the end and
never skips another product.
Carried from BearHuddleston's #93508 onto main. The Webapp relay and the
profile-incarnation fence stay out: carried code follows main's
named-profile rule and refuses a missing or deleted profile home.
- Webapp routes: one-use launch tickets, window handoff, and gateway
WebSocket tickets minted for the signed-in browser
- Browser surface policy and the SPA document policy
- A host terminal for the browser that survives reconnects
- Staged browser uploads and file tickets; file.attach takes a staged upload
- hermes sessions adopt
- Webapp bundle build helpers and the renderer freshness check
- Regenerated gateway contracts
Co-authored-by: emozilla <emozilla@nousresearch.com>
A test file that raised at import (e.g. a decorator using pytest without
`import pytest`) exited 2 with "collected 0 items / 1 error" and no
failed-test count, so the Summary line read "2 tests passed, 0 failed"
while the run exited 1, and the file was filed under a generic "no tests
ran" bucket. A positional test path that did not exist was dropped by
discovery and the run exited 0 without testing it.
- mark a failing file whose output shows a collection error and give it
its own failure bucket, "FAILED AT IMPORT/COLLECTION", with pytest's
one-line cause (the last traceback E line)
- the Summary line names failing files: "; FAILED: 1 of 2 files
(1 at import/collection)"; error/crash/skip notes move into one helper
- the failure buckets become a cause -> (header, detail) table
- a positional path that does not exist exits 2 with
"error: no such test path: ..."; --paths defaults keep skipping missing
roots
- per-file pytest exit 5 (nothing collected, e.g. platform-gated) stays a
pass, as deliberately added with the run-level NO TESTS RAN guard
A file whose tests pass but whose fixtures error at setup ("41 passed,
7 errors") exits 1, yet the Summary read "41 tests passed, 0 failed" and
the file was filed under "all tests passed but pytest exited non-zero".
- accumulate summary["errors"] into tests_errored and print ", N errors"
on the Summary line when non-zero (same pattern as crashed_note)
- dedicated failure bucket "with pytest ERRORs" listing passed/error
counts per file; such files no longer land in the passed-but-nonzero
or no-tests-ran buckets
- tests: parser returns errors for "41 passed, 7 errors"; end-to-end
probe with a failing fixture asserts the Summary shows "3 errors";
clean run has no errors note
The shell pointed HERMES_PYTHON at the bare pm-pinned interpreter and reached
the uv2nix site-packages only through PYTHONPATH. Every consumer that replaces
PYTHONPATH (Ink's gateway bootstrap, image attachments, run_tests.sh's env -i)
got an interpreter with no dependencies, so 'hermes --tui' in the shell failed
with 'No module named ruamel'.
devVenv re-homes uv2nix's editable venv onto the pinned interpreter: a real
pyvenv.cfg whose home is the lock's python, devPython's site-packages, and
console scripts re-shebanged onto it. HERMES_PYTHON, VIRTUAL_ENV, python,
pytest and pm are now one interpreter and the shell sets no PYTHONPATH.
The shipped package and the shell share one tool list: runtimeDeps takes
ripgrep and ffmpeg from pm/lock.json too. Reverts the run_tests.sh PYTHONPATH
passthrough, which leaked a caller's PYTHONPATH into every HERMES_PYTHON run.
The dev-venv-self-contained check runs the venv under env -i.
post_update now imports hermes_cli/config_version_stamp.py (it converts legacy
AGENT_BROWSER_* settings before stamping), so a change to it must start the
e2e_upgrade lane like every other module the update path imports.
install.sh --skip-browser / --no-playwright and install.ps1 -SkipBrowser
now hand PM `--without agent-browser` again instead of `--without chromium`.
The downloaded installer is the newest script, but by default it installs
the stable tag, whose PM (v0.22.5) rejects `--without chromium` with
"--without accepts only optional default packages (agent-browser,
cua-driver)" and exits 1, failing the whole install. The new PM already
reads `agent-browser` as `chromium` (pm.defaults.RETIRED_DEFAULTS), so the
legacy spelling works against both the stable tag and main. Direct
`hermes pm install --without chromium` stays valid on the new PM.
The runtime reads only browser.* now and the AGENT_BROWSER_* -> browser.* move
lived only in the versioned v52 config step, so three kinds of home lost their
engine, headed mode and persistent profile without a word: a missing
config.yaml (reads as current), an unstamped one (boot skipped it as below the
support floor) and one first stamped by an ordinary save before any migration.
The move is now done once, at the home's first stamp:
- note_first_stamp (every save_config first stamp, and left-core's created
file) moves the settings along with the left-core rows it already records;
- stamp_missing_config creates config.yaml with that first stamp when a
missing config's .env still carries them, before the missing-config shortcut
in boot (post_update.step_migrate_config), migrate_config and the Docker
entrypoint;
- boot migrates an unstamped config whose .env still carries them through
migrate_config (legacy-key steps, v52 among them, then the stamp); other
unstamped configs are left alone as before.
The conversion reuses the v52 step's code (move_agent_browser_env) and only
ever runs while a home is unstamped or missing, so a retained .env never
overrides a later config edit.
Nothing reads the key since browser-extension control was removed; the existing v52 step (a
legacy-key step, so unstamped configs get it too) now strips it from user config.
- pm: AgentBrowser class + lock entry gone; Chromium default=True and its env
exports only PLAYWRIGHT_BROWSERS_PATH (Playwright launches without an
executable path still need it). A recorded or passed --without agent-browser
maps to chromium (pm.defaults.RETIRED_DEFAULTS), and pm gc forgets the stale
agent-browser fact so its store entry is reclaimed.
- install.sh --skip-browser/--no-playwright and install.ps1 -SkipBrowser pass
--without chromium; undo hint is hermes pm install chromium.
- Docker bakes /etc/hermes/chromium-executable-path; stage2-hook validates it
and exports nothing; the sandbox-desktop image drops the agent-browser npm
install and keeps its Playwright Chromium.
Review follow-up: short_alias now creates the junction's parent directory,
so preparation never depends on the caller having made the work dir first
(the existing prepare_tools test failed on both Windows lanes with WinError 3).
That test now checks the invariant the Windows path keeps: UV_CACHE_DIR
reaches the cache directory, through the short work-local junction on Windows.
With a cold desktop-inputs cache, Windows desktop preparation builds sdists
that have no cp314 Windows wheel (obstore on win32-x64, dependency-injector on
win32-arm64). uv builds each one in
<cache>\python\runtime\<toolchain id>\sdists-v10\pypi\<pkg>\<ver>\<id>\src,
about 200 characters below the deep .cache\desktop-inputs root, so MSVC
link.exe outputs passed MAX_PATH and the build failed with LNK1104.
On Windows, prepare_tools now hands uv a short job-local junction
(<work>\uv) that points at the same transported cache directory. uv keeps the
spelling it is given, so builds run under the short path while the bytes are
still restored and saved by the desktop-inputs cache. The native toolchain id
is read through the junction, and prepare_native keeps the absolute spelling
instead of resolving the junction back to the long path.
CARGO_TARGET_DIR is also owned by the preparation environment and points at
<work>\cargo, so Rust sdists link under the work root on every target even
if a tool resolves the junction.
The devShell's HERMES_PYTHON is the lock's interpreter, which finds pytest and
the uv2nix venv only through PYTHONPATH. env -i dropped it, so every file failed
with 'No module named pytest'. Only the unactivated HERMES_PYTHON branch keeps it;
the activated test environment stays hermetic.
From 40.10.3 on, every Electron line unpacks its binary with
@electron-internal/extract-zip, an MSVC-built binding that imports
VCRUNTIME140.dll. A Windows host that never installed the VC++
Redistributable has no copy, so install.js dies with ERR_DLOPEN_FAILED.
This is the failure that made bb8280b753 roll Electron back to 40.10.2.
Our Python ships vcruntime140.dll. source_build_env() passes its path to
the Node builders as HERMES_VC_RUNTIME, and electron-runtime.mjs copies it
beside the extractor's binding right before it runs install.js. Node
loads bindings with LOAD_WITH_ALTERED_SEARCH_PATH (libuv uv_dlopen), so
Windows finds that copy first. This is the same app-local copy
fdea3e92e0 ships beside ARM64 rg.exe.
Clean Windows Sandbox (ARM64, no redist), Node 26.7.0, electron 44.9.3:
- before: install.js exits 1, "Cannot find native binding", ERR_DLOPEN_FAILED
- after: install.js exits 0, dist/electron.exe --version prints v44.9.3
Electron 42 dropped the postinstall that unpacked its binary into
node_modules/electron/dist. The binary now appears on the first
require('electron'), so parallel vitest workers race to unpack it
("Text file busy" in #128267's CI), and `hermes desktop --source` finds
no path.txt to launch.
electron-runtime.mjs runs Electron's own install.js once: after npm ci
for the desktop workspace, and in CI before the parallel checks. It first
checks the installed package against the desktop pin and the lockfile,
and does nothing when the runtime is already in place. On the current
40.10.2 pin, whose postinstall still unpacks the binary, it is a no-op.
Salvaged from #137648 (the runtime-preparation subset only).
Use one escaped HTML fragment for the report and review comment instead
of maintaining separate Markdown and HTML table builders. GitHub's
renderer preserves the same table text and all 39 real job/step links.
Remove the obsolete phase-generic accounting helper, redundant attempt
list and save-evidence flag, and unused collected-job counter. Append
main's saved column in one place.
Use urllib's unredirected Authorization header instead of a custom
redirect handler. The real HTTP redirect test still proves that only
the API request receives the token. Exercise PM formatting directly,
while retaining the actual stdlib-only CLI invocation test.
Verification: 696 CI tests pass, one native skip, and all 12 checks pass.
Saved real-log parser records and rendered values remain unchanged.
put cache hits/attempts and restored/missed archive bytes above an expander.
show hit and miss job/step links in both Markdown and HTML. off main,
compare hits with main's latest measured archive and hide write figures;
on main, show saved bytes without self-comparison.
retain unknown outcomes and sizes instead of treating them as misses or
zero. miss-byte totals use main archive references off main, and newly
saved archive sizes on main. preserve the real run mode and step numbers
in the snapshot so artifact-only rendering keeps the same behavior.
Collect ordinary actions/cache job logs alongside the existing timing
snapshot and retain per-invocation restore/write receipts and archive bytes.
Compare each cache with main and show saved-minus-restored archive changes
in the review comment, Actions summary and complete HTML report.
Count successful writes only after their save receipt; transfer progress
supplies a size once, never a sum. Preserve unknown sizes, failed or missing
logs, lookup-only results and exact-hit save skips. Match content-keyed
writes to their restored paths, without inspecting cache contents or using
the GitHub inventory as a proxy for a runner provider's cache store.
Print a compact tools/uv/npm summary from setup-pm, including the selected
restore outcome, exact-hit output, cache path, ref scope and save policy.
Keep actual archive sizes and upload results in the native cache logs.
Do not scan cache trees or add logging wrappers to non-PM cache actions.
Requirements builds (Termux deb assembly and wheelhouse gate, PM runtime
wheelhouse install) install the lock's export with uv pip install, which
re-resolves and rejects the overridden graph. Pass the lock: its manifest
overrides become --overrides and the closing check accepts them.
FORCE_COLOR/CLICOLOR_FORCE made uv pip check emit ANSI the report parser
cannot read, failing every install with an expected override mismatch.
When a checkout's .git exists but git rejects it (empty HEAD, a gitdir:
file pointing nowhere), every `hermes update` failed at the fetch with a
generic "Failed to fetch updates" and, on Windows, left the paused
gateways stopped with "the next `hermes` command restarts them". No
later command could: the whole-tree gate held the set while HEAD was
unreadable, so bots stayed down until someone repaired the checkout by
hand.
- tree gate: a set whose every pause already found HEAD unreadable and
that never bound a tree move resumes. The gateways were serving that
exact tree and no update step wrote it. A recorded move, or a HEAD
that was readable at the pause, still holds the set.
- fetch failure: "not a git repository" now says the checkout's git data
is damaged and prints the installer one-liner that repairs it.
- install.sh: a dangling `.git` file is moved aside and re-cloned like a
commit-less one, matching install.ps1 (Test-Path already matched it);
both installers' warning names a damaged .git, not only an
interrupted clone.
save-python-cache and save-node-cache now default to false on
pull_request events, like save-tools-cache. Their restore-only steps
read the exact keys the saving steps write on main, so a PR gets main's
uv and npm caches and leaves nothing PR-scoped behind.
Restore-only used to mean an explicit-save flow: run- and job-scoped
keys meant for save-pm-cache or a caller's actions/cache/save to write
later. Nothing calls save-pm-cache, and the last caller restoring from
the npm explicit-save key moved to its own cache, so the flow and its
outputs are deleted.
stable-store commits the Store submission held (targetPublishMode Manual)
instead of leaving an uncommitted draft, so certification runs before the
release gate and a run whose stable-publish fails still shows customers
nothing. The publication pass then PUTs the held submission with
Immediate and commits it again, so it goes live without a Partner Center
click.
Microsoft documents no call that releases a held submission and does not
say whether update accepts a committed one (409 is documented only as
"the current state of the app"). A 409 from the switch leaves the
submission held and prints the Publish now instruction; the release
stays green. The first real release shows which answer the API gives.
Reconcile receipt-bound Store work independently of channel advancement, including
already-current published releases, so an interruption cannot strand a held
submission. Record each upload's claim in certification notes and replace a held
Manual predecessor only after its exact remote abandon marker matches the claim
commit. Unknown owners and publishing submissions remain protected.
candidate-manifest spent ~4 of its 5.2 min downloading 8.7 GB (stable run
38491090442: both MSIX bundles, including the 4.3 GB Store bundle) only to
re-hash them, and each stage-receipt job re-downloaded its group's ZIPs and
bundle (60-256 s). The same bytes are hashed by the build job that uploaded
them, bound by digest in its handoff receipt, and downloaded and digest-
checked again by release_artifacts.publish (materialize) before anything
goes live.
Candidate staging and receipts now download only the metadata JSONs; package
rows still carry the receipt digests. The one check that read package
contents, the universal bundle identity against its per-arch rows, moves to
publish, after materialize has verified the bytes. A Store-only claim has no
metadata to fetch, so candidate-manifest reads its receipts alone.
The Store build used to be committed at green with targetPublishMode
Manual, and the publication pass could only print "click Publish now":
the submission API has no call that releases a held submission (update
and commit return 409 once it is committed). Every stable release needed a
person in Partner Center.
stable-store now uploads the verified bundle as an uncommitted draft
(msstore publish --noCommit) and stops. The publication pass, which runs
only when the release really goes out, reads the receipt-bound candidate
manifest, checks the pending draft carries that release's Store bundle,
sets targetPublishMode Immediate and commits it. Certification starts
then, and the build goes live when it passes.
- A draft from a newer green run is left for its own release; a committed
or live submission is a no-op, so reruns are safe.
- submit refuses to replace a committed submission still on its way live
(an earlier release); msstore publish would delete it.
- complete's Reconcile step now receives the MS_STORE_* credentials; it had
none, so the success path silently skipped the Store.
- A failed or unreadable Store step stays a warning: the release publishes.
Windows CI on #141651:
- footguns lint: Microsoft.VCToolsVersion.default.txt is read with
'utf-8', which keeps a BOM in the version string. Use 'utf-8-sig'
(and write a BOM in the test fixture, as Visual Studio does).
- Windows-only test: locate_command joins the known dir with mixed
separators (".../Installer\vswhere.exe"), so the stub's string
comparison never matched, vswhere looked absent, and both calls fell
back to a random partition. Compare as paths.
Review (#141651): _msvc_identity hand-wrote the vswhere.exe install path,
which is the known-path table the hermes_platform rule keeps in one place.
Add visual_studio_installer_dirs() to resolver/known_dirs.py and locate
vswhere with locate_command (PATH first, then the installer directory).
Return None explicitly when vswhere reports no installations, before selecting
the newest instance.
Warn on stderr with the reason whenever MSVC identity detection returns None:
missing vswhere, no matching installations, or a failed probe/version read.