The installed desktop app polls `desktop-latest/latest.json`. Previously
every push to `main` (autotag -> v*.devN -> desktop.yml dispatch) rebuilt
and republished `desktop-latest` as a DEV prerelease, so stable users were
auto-updated onto unvetted dev builds, and any manual stable mirror was
clobbered on the next merge.
Split the streams so the app's channel only ever serves vetted stable:
- Dev/rolling builds (v* autotag + manual workflow_dispatch) now publish to
a new `desktop-edge` pre-release. The shipped app does not poll edge, so
dev builds never auto-install onto stable users.
- Stable `desktop-v*` builds publish the user-facing release as before, then
a new `refresh-stable-channel` job copies that release's signed
`latest.json` into `desktop-latest` (mirror; URLs already point at the
desktop-v* assets). Cut a `desktop-v*` tag to ship an update.
- `clean-release` now targets `desktop-edge`; `desktop-latest` is never
wiped by CI.
No app/tauri.conf.json change — the updater endpoint stays `desktop-latest`.
Doc updated to describe the now-implemented stable/edge split.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Repoints all desktop download links (README, docs/index.md, downloads.md, getting-started/installation.md) from the removed desktop-latest prerelease (with wrong 0.1.0 filenames) to the stable desktop-v1.0.2 release. All 5 URLs verified live (200). macOS .dmg now included (addresses #356).
Every desktop download link in the docs was broken on two counts:
1. They pointed at the rolling `desktop-latest` prerelease, which had
been removed (404 for all of README, docs/index.md,
docs/downloads.md, docs/getting-started/installation.md).
2. They used the wrong version in the filenames (`OpenJarvis_0.1.0_*`)
— the actual published assets were never `0.1.0`.
Repointed all four files at the new stable `Desktop desktop-v1.0.2`
release with the exact asset filenames it ships
(`OpenJarvis_1.0.1_*` — the Tauri bundle version is 1.0.1, distinct
from the 1.0.2 Python/CLI release). This release also includes a
macOS universal `.dmg`, which the prior desktop releases lacked
(addresses #356 "No Mac Download") — so the macOS rows now say
"Universal" (Apple Silicon + Intel) instead of "Apple Silicon".
All five download URLs verified live (HTTP 200) against the
desktop-v1.0.2 release before committing.
Note: `docs/desktop-auto-update.md` still references the
`desktop-latest` rolling channel — that's the auto-updater's endpoint,
a separate concern from the manual download links, and is left as-is.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps 1.0.1 → 1.0.2 so the merged fixes (#372 wheel packaging, #389 pynvml, #373 Windows RAM, #331 desktop diagnostics, #337/#352 install URL) can ship to PyPI. See PR #411.
Patch release bundling the fixes merged since v1.0.1. The headline is
#372 — the v1.0.1 wheel on PyPI is missing `openjarvis/traces/`, so
every `pip install openjarvis==1.0.1` breaks at import. PyPI filenames
are immutable, so the fix has to ship under a new version number.
Bumps version 1.0.1 → 1.0.2 and adds the CHANGELOG entry covering
#372 (wheel packaging), #389 (pynvml warning), #373 (Windows RAM),
#331 (desktop uv-sync diagnostics), and #337/#352 (install URL → GitHub
Pages).
After this merges, cut the release:
uv build
unzip -l dist/openjarvis-1.0.2-*.whl | grep traces # verify
twine upload dist/openjarvis-1.0.2-*
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts the #331 uv-sync error-formatting logic into pure functions with 7 unit tests, now run via cargo test in desktop.yml's validate job. Verified: all 7 pass on the real Tauri crate in CI.
The uv-sync failure handling added in #398 was inline in the async
`boot_backend` GUI path, so its logic — exit-code rendering and the
stderr "tail" extraction — could only be verified by running the
desktop app. This extracts the pure logic into three free functions
and adds unit tests, so the message formatting is now covered by
`cargo test` with no GUI / webview runtime needed.
Extracted:
- `uv_sync_stderr_tail(stderr, max_chars)` — last N chars of stderr,
trimmed, on char boundaries. The original inline version used
`.rev().take(N).collect().chars().rev().collect()`; the new version
is `skip(count - N)` which is clearer and equally UTF-8-safe (matters
because Windows consoles emit non-ASCII cp9xx bytes — a byte slice
could split a codepoint).
- `format_uv_sync_failure(root, exit_code, stderr)` — the non-zero-exit
message. Now renders a missing exit code (signal-terminated process)
as "unknown" instead of a misleading "-1".
- `format_uv_sync_spawn_error(root, uv_bin, err)` — the can't-spawn
message.
`boot_backend` now calls these instead of formatting inline.
Tests (7, in `#[cfg(test)] mod tests`):
- tail returns whole string when shorter than the limit
- tail keeps the END (the actionable line), not the spinner-noise start
- tail trims surrounding whitespace
- tail never splits a multi-byte codepoint (500×"é", limit 100 → exactly
100 chars, all "é")
- failure message includes exit code + stderr tail + the actionable
"run uv sync manually" hint
- missing exit code renders as "exit unknown", never "exit -1"
- spawn-error names the uv binary path and repo root
Verified the logic standalone via `rustc --test` (7/7 pass). In CI they
run via `cargo test` in desktop.yml's `validate` job, which already
builds the Tauri crate with the webkit deps and runs on every PR that
touches `frontend/**` — so this changes the existing `cargo check` step
to `cargo test` (a superset: same build coverage, plus the tests).
This doesn't verify the GUI *behavior* (that still needs a human running
the app, or the windows-latest empirical path) — but the error-message
logic that was previously untestable now has automated coverage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a windows-latest CI job that executes the real GlobalMemoryStatusEx RAM path (#373), the #293 stdout reconfigure test, and builds+imports the openjarvis_rust PyO3 extension on Windows. Verified green on a real Windows runner (4m1s, all steps success).
The `test` job runs only on ubuntu-latest, so the Windows-specific
branches added for #373 (RAM detection via GlobalMemoryStatusEx) and
#293 (cp9xx → UTF-8 stdout reconfigure) were never *executed* in CI —
only unit-tested with mocks on Linux. `tests/hardware/test_hardware_profiles.py::test_total_ram_gb_windows`
has existed all along but is `skipif(sys.platform != "win32")`, so it
silently skipped on every run.
This adds a `test-windows` job that runs on a real Windows runner
(free for public repos) and:
1. Verifies `_total_ram_gb()` returns > 0 on actual Windows — executes
the real `ctypes.windll.kernel32.GlobalMemoryStatusEx` path (#373).
Runs as a pure-Python step BEFORE any Rust build, so a flaky
toolchain install can't mask the result.
2. Runs `tests/hardware/test_hardware_profiles.py` (the now-unskipped
Windows RAM test) and `tests/cli/test_cli.py` (the
`test_windows_reconfigures_stdout_to_utf8` test from #293).
3. Builds + imports the `openjarvis_rust` PyO3 extension on Windows —
the only CI job that does so. The extension is mandatory at runtime
(`_rust_bridge.py` hard-errors without it), yet nothing else
verified it compiles/imports on Windows. desktop.yml builds the
Tauri app's Rust, not this extension.
4. Smoke-tests `jarvis --version`.
Scoped to the platform-relevant test files (not the full 6700-test
suite) so the job stays fast; the slow part is the Rust build, which
doubles as Windows-extension-build coverage.
All `run:` steps are static commands with no `github.event.*`
interpolation — no workflow-injection surface.
Note: #331 (desktop "did not become healthy" — uv sync error
surfacing) is GUI-triggered Tauri boot logic and isn't covered here;
its only automatable surface is string formatting, and testing it
needs the heavy webview build. Left as a possible follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves the openjarvis.ai SSL failure (#337, #352) by serving the installer from the project-controlled GitHub Pages site. Adds uv prerequisite docs for the Windows desktop path. See PR #402 for the full breakdown and the openjarvis.ai CNAME migration note.
The documented install command pointed at `https://openjarvis.ai/install.sh`,
but that domain is community-operated (not controlled by this project) and
its TLS config broke — every new user hit `sslv3 alert handshake failure`
(#337, #352). Since we can't fix a domain we don't control, this moves the
installer onto infrastructure we DO control: the project's GitHub Pages
docs site.
Changes:
- **`docs/gen_install_script.py`** (new) + **`mkdocs.yml`**: a `gen-files`
hook copies `scripts/install/install.sh` verbatim into the built site at
`install.sh` on every `mkdocs build`. Single source of truth — the script
stays at `scripts/install/install.sh` (still bundled into the wheel as
`_install_scripts/`); the published copy can't drift. Verified locally:
`mkdocs build` emits `site/install.sh` byte-identical to the source.
New canonical URL: `https://open-jarvis.github.io/OpenJarvis/install.sh`
— HTTPS always valid (GitHub's cert), fully under project control.
- **`README.md`**: canonical command switched to the github.io URL for
both the Installation and Quick Start blocks. Also addresses the uv
discoverability gap — explicitly states the curl installer downloads uv
for you (no prerequisite), and that the Windows **desktop .exe** expects
uv to be installed first, with the exact PowerShell command.
- **`docs/getting-started/{install,wsl2,macos,linux}.md`**: canonical URL
switched to github.io. `install.md` gains an "Install URL" info note
explaining the github.io URL is canonical and that the older
`openjarvis.ai` URL is community-operated with intermittent TLS issues.
- **`scripts/install/install.sh`** + **`jarvis-wrapper.sh`**: usage comment,
the WSL re-run hint (added in #399), and the wrapper's re-install message
all updated to the github.io URL.
Migration note for maintainers: ask whoever operates `openjarvis.ai` to
CNAME it to `open-jarvis.github.io`. Once they do, `openjarvis.ai/install.sh`
will serve this same GitHub Pages content with a valid GitHub-managed cert,
and the nicer brand URL can become canonical again with zero further code
changes. Until then, the github.io URL works and is under our control.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to PR #398. Three surface fixes for the Windows install confusion documented in two Discord support threads. See PR #399 for the full breakdown.
Addresses the second half of the Discord support thread on the
"Jarvis server did not become healthy in time" issue (PR #398 fixed
the diagnostic gap; this fixes discoverability of the right install
path so users don't end up there in the first place).
Three changes, all surface improvements:
1. **`README.md`** — explicit Windows section in the Installation
block. Previously, the only Windows mention was a footnote
("Platforms: ... WSL2 on Windows") that came AFTER the `curl … |
bash` install command. Users on PowerShell would copy/paste the
command, get a syntax error, then try to debug bash on Windows.
Now the README clearly says: bash installer is macOS/Linux only;
Windows users have two paths (WSL2 with one-time `wsl --install`
setup, or the desktop .exe from Releases). Both link to the
relevant docs.
2. **`scripts/install/install.sh`** — early bail when running under
Git Bash / MSYS2 / Cygwin (MINGW*, MSYS*, CYGWIN* per `uname -s`).
These environments aren't WSL — `uv` and `git` will install to
Windows-side paths that OpenJarvis can't reach, Ollama integration
silently breaks, and the user gets to debug it 3 minutes into a
doomed install. The bail message points at both the WSL2 setup
command (`wsl --install -d Ubuntu-24.04`) and the desktop .exe
download as alternatives.
Verified the case-match doesn't fire on Linux (`uname -s` →
`Linux`, matches the wildcard fall-through, not the MINGW patterns).
3. **`frontend/src-tauri/src/lib.rs`** — when `resolve_bin("uv")`
can't find uv, the per-OS error message now contains the exact
install command for the user's OS, ready to copy/paste. On Windows
that's the `irm https://astral.sh/uv/install.ps1 | iex` command
Marc kept reposting on the Discord support thread (5/12-5/14).
On macOS/Linux it's the standard `curl | sh` installer.
The previous generic "Install it from https://astral.sh/uv" left
users guessing whether to use winget, scoop, pip, or the official
installer — which is exactly the confusion the Discord thread
captured.
None of these are root-cause code fixes (Discord users' uv installs
fail for environment-specific reasons we can't diagnose remotely),
but together they remove the three biggest friction sources we saw:
copy-paste install command that can't possibly work, doomed git-bash
installs that fail mysteriously, and missing exact install commands
when uv isn't found.
The Tauri change ships in the desktop binary; the README change is
visible immediately on the repo page; the install.sh change reaches
users via openjarvis.ai (when it's restored) and via the GitHub-raw
fallback from PR #398.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses #331 (multiple Windows users hit "Jarvis server did not
become healthy in time" with no actionable detail).
The boot sequence ran `uv sync` with **both** stdout AND stderr piped
to `/dev/null` and discarded the exit code (`let _ = …`). When
`uv sync` failed for any reason — Windows PATH/permission issues,
network problems, lockfile conflicts, stale `.venv/` — the user saw
nothing useful. The boot proceeded to `uv run jarvis serve` in an
under-provisioned venv, then waited the full 600-second health-check
window before showing a generic "did not start" message with no
hint of what actually went wrong.
Fix: capture stderr, check the exit status, and surface a useful
error to the user **before** the long server-start wait, including:
- The exit code
- The last ~800 chars of `uv sync` stderr (where the diagnostic
message usually lives)
- A concrete next step (open a terminal and run `uv sync --extra server`
manually for full output)
Also updated the status detail message to "Installing dependencies
(uv sync — may take 1-2 min on first boot)..." so users on slow
connections don't restart the app thinking it's stuck.
Discord support thread (5/12-5/14) shows the same pattern across
multiple Windows users (@ItsVoyage, @Mystic_irl, @doevud, @Sainthood):
all stuck on "Starting api server" / "did not become healthy in time"
for 4+ minutes, with the actual root cause turning out to be a uv
installation issue that the discarded stderr would have surfaced
immediately. The community workaround (uninstall, install Feb
pre-release, run a magic PowerShell `irm` command for uv) is the
right diagnosis applied without diagnostic output — this commit
makes that diagnostic output visible.
Note: this fix lands in the desktop binary's source (`frontend/src-tauri/src/lib.rs`).
Users running the v1.0.1 desktop binary won't see the improved
error message until the desktop release is re-cut from this branch.
Until then, the workaround documented in this PR's `openjarvis.ai`
fallback section (curl the install.sh from GitHub raw) gets new
users past the install step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses #337 (also reported as #352).
`curl -fsSL https://openjarvis.ai/install.sh | bash` — the documented
one-liner — currently fails with:
curl: (35) ... sslv3 alert handshake failure
Reproduced from this machine just now; @kumanday and @filactre both
report the same against different OpenSSL and LibreSSL versions. The
underlying SSL issue is on the openjarvis.ai server (cert / TLS
config) and needs an operational fix at the infra layer — not
something a code change in this repo can resolve.
What this commit *can* do is unblock users immediately:
- `README.md` (under "Installation"): callout pointing at issue #337
and the GitHub-mirror fallback.
- `docs/getting-started/install.md`: same callout as a
Material-for-MkDocs `!!! warning` admonition.
Both link to the canonical script at
`https://raw.githubusercontent.com/open-jarvis/OpenJarvis/main/scripts/install/install.sh`.
The script is identical content — it's the same `scripts/install/install.sh`
served from GitHub's CDN instead of openjarvis.ai. Once the
installer runs, it pulls everything else (`uv` from astral.sh, the
project source from `github.com/open-jarvis/OpenJarvis.git`, Ollama
from `ollama.com`) — none of which depend on `openjarvis.ai`. So
the rest of install proceeds normally.
When the openjarvis.ai SSL issue is fixed at the server / DNS layer,
both callouts can be removed and the canonical URL becomes the only
documented path again.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes#389.
The legacy `pynvml` PyPI package (since version 13.x) registers a
meta-path-finder shim — `_pynvml_redirector.py` — that prints a
`FutureWarning("The pynvml package is deprecated. Please install
nvidia-ml-py instead.")` on every `import pynvml`, even when the
caller's project doesn't depend on pynvml directly. The warning was
firing on every `jarvis --version` / `jarvis ask` / any command that
touches the telemetry path.
Two-layer fix:
1. **pyproject.toml**: switch `pynvml>=13.0.1` → `nvidia-ml-py>=12.560.30`
in the core deps, `gpu-metrics` extra, and `energy-all` extra.
`nvidia-ml-py` is NVIDIA's official package and ships the same
`pynvml` module name without the redirector shim, so the warning
doesn't fire.
2. **Defensive filters** at all four `import pynvml` sites
(`telemetry/gpu_monitor.py`, `telemetry/energy_nvidia.py`,
`server/research_router.py`, `evals/backends/external/_subprocess_runner.py`):
wrap the import in a narrowly-scoped `warnings.filterwarnings("ignore",
message=r"The pynvml package is deprecated.*", category=FutureWarning)`.
Belt-and-suspenders for the case where `pynvml` gets pulled in
transitively by torch / vllm / etc. — the user's environment may
still have it installed even if our deps don't pull it in.
Verified locally:
- `uv sync` swaps pynvml → nvidia-ml-py.
- `python -c "import warnings; warnings.simplefilter('error', FutureWarning); from openjarvis.telemetry import gpu_monitor"` → no warning fires (would raise if it did).
- `jarvis --version` → clean output, no FutureWarning preceding the version string.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes#372.
The `.gitignore` had `traces/` (unanchored), which matches any directory
named `traces/` anywhere in the tree — including the runtime module at
`src/openjarvis/traces/`. hatchling honors `.gitignore` when building
the wheel, so it silently dropped the entire `openjarvis/traces/`
package.
Effect: every fresh `pip install openjarvis==1.0.1` failed at import
time with `ModuleNotFoundError: No module named 'openjarvis.traces'`
the moment the user touched `jarvis ask`, learning, or the server.
Confirmed by @gilbert-barajas with a clean repro on macOS Apple Silicon.
Reproduced locally by running `uv build --wheel` on a clean checkout
of `main`:
- Before this change: `openjarvis/traces/` absent from the wheel.
- After this change: all 4 expected files present
(`__init__.py`, `analyzer.py`, `collector.py`, `store.py`).
Anchored the pattern to `/traces/` so it only matches a top-level
`traces/` directory (where ad-hoc trace dumps may live during
development), not any nested `traces/` subdir. Added a comment in the
gitignore explaining the gotcha so it doesn't recur.
The other unanchored directory patterns in the file (`results/`,
`logs/`, `htmlcov/`, `site/`, `build/`, `dist/`, `venv/`, `env/`)
were audited — none collide with any directory currently under
`src/openjarvis/`. Left them unanchored to keep the diff minimal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two clean contributor PRs landed with original authorship preserved. Two related PRs (#116, #119) excluded — both need rebase against significant main refactors (see PR #368 for details). Credits: @bootcrowns (#203), @samy19980109 (#260).
OperatorManifest has had a `metrics: List[str]` field since the initial
commit but OperatorManager never read it. This change wires that field
through the manager in three ways:
1. activate(): passes `metrics` into the scheduler task metadata so
workers can introspect which metrics an operator cares about.
2. status(): includes `metrics` in the per-operator status dict returned
to callers, making it visible alongside tools and schedule info.
3. collect_metrics(operator_id, *, since, until) [new method]: queries
the system's TelemetryAggregator (system.telemetry) and returns only
the summary fields explicitly declared in manifest.metrics. Unknown
metric names are skipped with a DEBUG log so old manifests remain
forward-compatible. Returns an empty dict gracefully when telemetry
is not configured.
Consolidates five contributor PRs into one bundle to avoid overlapping issues (notably #340/#341 both touching mcp/transport.py + agent_cmd.py + executor.py). Original authorship preserved on every commit. See PR #367 for full per-author breakdown.
Credits: @Dilligaf371 (#340, #341), @eddierichter-amd (#301), @kriptoburak (#347), @bootcrowns (#202).
Follow-up on the cherry-picks from @Dilligaf371's #340 and
@bootcrowns's #202. Both fail ruff's E501 (88-char line limit) on
main:
- ``src/openjarvis/agents/executor.py:325`` (from #340) — the
``self._system is not None and getattr(..., "tool_executor", None)
is not None`` guard was 101 chars. Wrapped the condition.
- ``src/openjarvis/telemetry/gpu_monitor.py:55-60`` (from #202) —
five new GPU_SPECS entries (Jetson Orin NX 16GB/8GB, AGX Orin,
Snapdragon X Elite/Plus) were 90-93 chars. Wrapped each
GpuHardwareSpec constructor call onto its own block.
- ``tests/telemetry/test_gpu_monitor.py`` (from #202) — removed a
spurious blank line between imports and the first ``#`` comment
block, picked up by ``ruff check --fix`` (rule I001).
No behavior change — pure formatting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MCPClient.list_tools constructs ToolSpec without a timeout_seconds
override, so MCP tools inherit the dataclass default of 30s. Most MCP
servers in practice wrap long-running tools (pentest scanners, build
runners, search agents, …) that comfortably take longer than 30s.
The symptom is misleading — the ToolExecutor reports the timeout, and
the LLM relays it as "command execution timed out", with no hint that
the cause is the OpenJarvis client side and not the MCP server's
actual policy. (CyberStrikeAI, for example, allows 30 *minutes* on its
side via tool_timeout_minutes.)
Bump the default to 600s (10 min) — still bounded, but enough for the
typical long-running tool. Individual MCP servers can shorten via
their own timeout policy if they care.
`jarvis agents ask` is a non-interactive CLI path, so the AgentExecutor's
ToolExecutor never had a confirm_callback wired. Tools whose ToolSpec
sets requires_confirmation=True (shell_exec, git_*, ...) returned
"requires confirmation but no confirmation callback is available" and
the agent relayed that back as natural language, never executing.
This adds a --yes/--no-yes flag (default --yes):
- --yes: auto-approve (lambda _prompt: True). Suited for CLI runs where
the operator already authorized the engagement scope.
- --no-yes: prompt on TTY via click.confirm.
The callback is set on the AgentExecutor itself; executor.py:_invoke_agent
reads it and forwards it to the constructed agent through agent_kwargs
(both `interactive=True` and `confirm_callback=...`).
The AgentExecutor builds agents from a template's `tools` whitelist by
resolving each name against the static `ToolRegistry`. External MCP
tools (discovered at SystemBuilder.build() time via _discover_external_mcp)
were never picked up — they exist on system.tool_executor._tools but
the per-agent build path never read from there.
Result: any agent whose template declared MCP tool names by name got
0 of them at runtime, falling back to natives only. The agent's system
prompt could still mention the tools, but the model could not call them
(only shell_exec or other native fallbacks were available).
This adds a fallback after the ToolRegistry loop: for any tool name not
resolved from the registry, look it up in system.tool_executor._tools
and append the already-instantiated MCP adapter. Native tools still
take precedence (same name, the registry hit wins).
Verified by configuring a CyberStrikeAI stdio MCP server (78 tools).
Before this patch: agent built with 4 tools. After: 4 native + 78 MCP.
Lands 5 high-priority contributor PRs as one bundle, with original authorship preserved on every commit, plus follow-up commits from me to make each PR CI-green and non-regressive. See PR #362 for the full per-author commit breakdown and credits.
Credits: @tomaioo (#235), @Dilligaf371 (#339), @TX-Huang (#293, #294, #295).
Follow-up on @TX-Huang's PR #295. The new
test_soul_md_content_reaches_engine_in_simple_agent test has one
line at 92 chars that trips ruff's E501 (88 char limit). Wrap the
ternary onto multiple lines so the file passes ruff check on CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`SystemPromptBuilder` is fully implemented and tested in
`openjarvis.prompt.builder`, and `BaseAgent.__init__` accepts a
`prompt_builder` kwarg. But no production code path ever instantiates
the builder, so the persona-files feature documented in
`MemoryFilesConfig` (SOUL.md / MEMORY.md / USER.md) had no effect.
Users could write a fully-customized `~/.openjarvis/SOUL.md` and the
file was never read.
This PR wires it up in `_run_agent` (called by `jarvis ask --agent
<name>` and the new fallback from #294):
```python
if "prompt_builder" in inspect.signature(agent_cls.__init__).parameters:
agent_kwargs["prompt_builder"] = SystemPromptBuilder(
agent_template=config.agent.default_system_prompt or "",
memory_files_config=config.memory_files,
system_prompt_config=config.system_prompt,
)
```
The `inspect` guard means agents that override `__init__` without
forwarding `prompt_builder` (e.g. OrchestratorAgent, which has its
own tool-aware system prompt) opt out automatically and keep
working unchanged. SimpleAgent and any future agent that inherits
`BaseAgent.__init__` directly picks up the persona files.
Also fixes a latent bug in `SystemPromptBuilder._load_file`: it
called `path.read_text()` with no encoding, which on Windows falls
back to the system code page (cp950 / cp932 / cp949) and raises
`UnicodeDecodeError` on any non-ASCII persona content. Pin to UTF-8.
## Tests
- Add `test_soul_md_content_reaches_engine_in_simple_agent` — writes
sentinel SOUL.md / MEMORY.md / USER.md to tmp_path, runs the
command, and asserts each sentinel appears in the SYSTEM message
passed to engine.generate.
- Add `test_orchestrator_keeps_its_own_system_prompt` — exercises
the `inspect`-based opt-out so OrchestratorAgent doesn't crash
on the unexpected kwarg.
Run: `pytest tests/cli/test_ask_agent.py tests/cli/test_ask_router.py
tests/cli/test_ask_e2e.py tests/agents/ tests/prompt/`
The 6 remaining failures (test_base_agent, test_loop_guard,
test_manager, test_native_openhands) are pre-existing on
origin/main and unrelated to this change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>