feat: put ncp on PATH on both platforms; rename WSL reqs to Windows

Ports the ncp PATH work from upstream. Registering ncp as a shell-profile
function failed three ways on Windows: the default Restricted execution policy
blocks the profile itself, profiles don't exist outside PowerShell (cmd, Win+R,
Task Scheduler), and the self-elevating installer writes the admin's profile.

Windows now ships management/ncp.cmd and the installer appends management\ to
the Machine PATH via [Environment]::SetEnvironmentVariable -- never setx, which
truncates PATH at 1024 chars. A .cmd is exempt from the execution policy.

Linux symlinks /usr/local/bin/ncp -> management/nexus-cli.sh, falling back to
the old .bashrc function when sudo is unavailable.

Also renames requirements-wsl.txt to requirements-windows.txt and purges stale
WSL references, including vite.config.js's dev-server comment and
controlpanel.py's "Check WSLg." error string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jon
2026-07-22 14:11:18 -05:00
co-authored by Claude Opus 4.8
parent 9befa1d561
commit 6137710c25
14 changed files with 124 additions and 91 deletions
+3 -3
View File
@@ -16,7 +16,7 @@ start --ai`), not on backend startup.
**Windows (recommended):**
```powershell
powershell -ExecutionPolicy Bypass -File .\install-windows.ps1 # one-time native install
.\launch_nexus.ps1 # memory :8001 + backend :8000
ncp web # memory :8001 + backend :8000 + UI
```
**Linux full stack (dev):**
@@ -80,7 +80,7 @@ cd interface/web && npm run build
## Architecture
### Python venv
All Python code runs inside `Promethean/` (a local venv). Always activate it before running backend commands: `source Promethean/bin/activate`. Dependencies are layered: `requirements-base.txt` holds the GPU-agnostic core, and a thin overlay pins the right PyTorch build for the target — `requirements-amd.txt` (ROCm), `requirements-nvidia.txt` (CUDA, generated by `bin/gen-nvidia-reqs.py`), or `requirements-wsl.txt` (CPU-only). `bin/install.sh` selects NVIDIA, AMD, or CPU/WSL requirements from the host.
All Python code runs inside `Promethean/` (a local venv). Always activate it before running backend commands: `source Promethean/bin/activate`. Dependencies are layered: `requirements-base.txt` holds the GPU-agnostic core, and a thin overlay pins the right PyTorch build for the target — `requirements-amd.txt` (ROCm), `requirements-nvidia.txt` (CUDA, generated by `bin/gen-nvidia-reqs.py`), or `requirements-windows.txt` (CPU-only). `bin/sync.py` (`requirements()`) selects NVIDIA, AMD, or CPU/Windows requirements from the host.
### Synapse Backend (`synapse/`)
FastAPI app at `synapse/main.py`. Key responsibilities:
@@ -125,4 +125,4 @@ Most data lands in `synapse/memory/memory.db` (SQLite, WAL mode). Tables: memory
| Python dependencies (base) | `requirements-base.txt` |
| Python dependencies (AMD/ROCm) | `requirements-amd.txt` |
| Python dependencies (NVIDIA/CUDA) | `requirements-nvidia.txt` (generated by `bin/gen-nvidia-reqs.py`) |
| Python dependencies (WSL/CPU) | `requirements-wsl.txt` |
| Python dependencies (Windows/CPU) | `requirements-windows.txt` |
+19 -20
View File
@@ -17,8 +17,8 @@ memory service, and a React frontend. No external AI provider is called.
NexusOS ("Nexus") is a self-hosted assistant you actually own. All inference
runs through a **locally bundled Ollama** on `localhost`; conversations, facts,
and settings live in local SQLite. It ships with desktop branding (XFCE theme,
icons, boot splash) so it can be run as a full assistant environment, not just
a web app.
icons, boot splash) so it can be run as a full assistant environment on Linux,
not just a web app.
- **Chat** — streaming responses from local Ollama models (SSE).
- **Persistent memory** — a dedicated service auto-extracts durable facts from
@@ -47,26 +47,21 @@ cd nexus-core
powershell -ExecutionPolicy Bypass -File .\install-windows.ps1
```
> **Execution policy:** Windows blocks unsigned `.ps1` scripts by default, so
> run them with `-ExecutionPolicy Bypass` as shown (a one-run override — nothing
> permanent). Double-clicking `install-windows.ps1` or running `.\install-windows.ps1`
> bare will fail with *"running scripts is disabled on this system"*.
> The installer self-elevates (a UAC prompt will appear).
>
> To allow scripts persistently instead (then you can run `.\...ps1` directly):
> ```powershell
> Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
> ```
Then double-click the **NexusOS** desktop icon (the shortcut already passes the
bypass), or launch from a shell with:
Then double-click the **NexusOS** desktop icon, or launch from a **new** shell
(PATH is read at process start, so already-open windows won't have `ncp` yet):
```powershell
powershell -ExecutionPolicy Bypass -File .\launch_nexus.ps1
ncp web
```
> The installer needs `-ExecutionPolicy Bypass` because Windows blocks unsigned
> `.ps1` by default; it's a one-run override, nothing permanent. It self-elevates,
> so a UAC prompt appears. `ncp` itself is `management\ncp.cmd` on the machine
> PATH — a `.cmd`, so no execution-policy change is ever needed, and it works
> from cmd and Task Scheduler as well as PowerShell.
The app opens at `:8000`; click **Start AI** to launch Ollama. The installer
uses `requirements-wsl.txt` (CPU-only, pure-Python — no ML stack, since Ollama
uses `requirements-windows.txt` (CPU-only, pure-Python — no ML stack, since Ollama
does all inference over HTTP).
### Linux
@@ -77,12 +72,13 @@ does all inference over HTTP).
./install.sh
# 2. Launch (memory :8001, backend :8000 — backend also serves the built UI)
./launch_nexus.sh
# The install symlinks ncp into /usr/local/bin (sudo); open a new shell first.
ncp web
```
Python deps are layered: `requirements-base.txt` (GPU-agnostic core) plus one
GPU overlay — `requirements-amd.txt` (ROCm) or `requirements-nvidia.txt` (CUDA).
`requirements-wsl.txt` is the standalone CPU-only runtime (no base overlay).
`requirements-windows.txt` is the standalone CPU-only runtime (no base overlay).
`bin/sync.py` picks the right one for the host.
`./install.sh` is also the update path — re-run it any time to pull the latest
@@ -115,12 +111,14 @@ ncp chat "<message>" # stream a reply
ncp memory list | add <text> | rm <id>
ncp playbook list | show <id> # first playbook (*) = active system prompt
ncp history [query] # recent conversations
ncp doctor # diagnostics: venv, Node, imports, Ollama, status
```
## Architecture
| Component | Location | Role |
|---|---|---|
| **Promethean** (venv) | `Promethean/` | The Python venv all backend code runs in — `source Promethean/bin/activate` (Linux) / `Promethean\Scripts\python.exe` (Windows). Keeps deps out of the system Python. |
| **Synapse** (backend) | `synapse/` | FastAPI app. `/chat/stream`, `/playbooks`, `/memory`, `/models`, `/conversations`, `/settings`, `/ollama`, `/icons`. Assembles the system prompt: active playbook → reference playbooks → memory facts → relevant past snippets. |
| **Memory service** | `synapse/memory/` | Separate FastAPI app (:8001). `/memories/extract` uses an Ollama prompt to decide what to persist. Shares the SQLite DB with the backend. |
| **Playbooks** | `synapse/playbooks/` + `data/playbooks/` | Ordered `{id}.yaml` records managed by `PlaybookManager`. |
@@ -142,6 +140,7 @@ management/ nexus-cli.sh, ncp API client, control panel, desktop theme
bin/ install, backup/restore, panel + provisioning scripts
assets/ branding: icons, boot splash, XFCE/GTK theme
data/playbooks/ active playbook YAML
Promethean/ Python venv (gitignored, built by the installer)
```
## Configuration
@@ -151,7 +150,7 @@ data/playbooks/ active playbook YAML
| Ollama host | `OLLAMA_HOST` env (default `http://127.0.0.1:11434`) |
| Filesystem paths | `synapse/nexus_config.py` |
| Frontend API base URL | `interface/web/src/config.js` |
| Python deps | `requirements-base.txt` + amd/nvidia GPU overlay; `requirements-wsl.txt` = standalone CPU runtime |
| Python deps | `requirements-base.txt` + amd/nvidia GPU overlay; `requirements-windows.txt` = standalone CPU runtime |
---
+1 -1
View File
@@ -41,7 +41,7 @@ cp -f "$NEXUS/management/panel/genmon-16.rc" "$PANEL_CFG/genmon-16.rc"
# ── Register the genmon applets into the XFCE panel ────────────────────────
# The network applet's genmon (plugin-13) was added by hand once; the Nexus
# (15) and Bluetooth (16) applets are wired up programmatically via xfconf so a
# fresh install picks them up. No-op on machines without XFCE (e.g. the WSL box).
# fresh install picks them up. No-op on machines without XFCE (e.g. the Windows box).
# place = before|after — where to insert relative to the network applet (13).
register_genmon() {
local id=$1 place=$2 anchor=13 panel=panel-1
+15 -6
View File
@@ -52,11 +52,20 @@ if [ "$stage" = "runtime" ]; then
echo "Warning: bin/fetch-ollama.sh not found — skipping Ollama fetch."
fi
# Shell wiring: the `ncp` function and the Promethean venv alias. This is the
# only place they get registered now that bin/install.sh is gone. Guarded by a
# grep so an in-place restore is a no-op and a scratch clone can't re-point an
# already-wired ~/.bashrc at itself.
if ! grep -q "management/nexus-cli.sh" "$HOME/.bashrc" 2>/dev/null; then
# `ncp` on PATH, not a shell function: /usr/local/bin is visible to sh, cron,
# systemd units and .desktop Exec lines, none of which read ~/.bashrc. The
# Windows installer does the matching thing with management\ncp.cmd on PATH.
# Falls back to the old ~/.bashrc function when there's no sudo (a shell
# function shadows the symlink anyway, so having both is harmless).
ncp_link="/usr/local/bin/ncp"
ncp_target="$NEXUS_ROOT/management/nexus-cli.sh"
if [ "$(readlink -f "$ncp_link" 2>/dev/null)" = "$ncp_target" ]; then
echo "ncp already on PATH at $ncp_link."
elif sudo ln -sfn "$ncp_target" "$ncp_link"; then
echo "Registered ncp at $ncp_link."
elif ! grep -q "management/nexus-cli.sh" "$HOME/.bashrc" 2>/dev/null; then
# Guarded by a grep so an in-place restore is a no-op and a scratch clone
# can't re-point an already-wired ~/.bashrc at itself.
cat >> "$HOME/.bashrc" <<EOF
# Nexus
@@ -64,7 +73,7 @@ ncp() {
$NEXUS_ROOT/management/nexus-cli.sh "\$@"
}
EOF
echo "Registered ncp in ~/.bashrc."
echo "No sudo for $ncp_link — registered ncp in ~/.bashrc instead."
fi
if ! grep -qF "alias promethean=" "$HOME/.bashrc" 2>/dev/null; then
+1 -1
View File
@@ -69,7 +69,7 @@ def venv_python() -> Path:
def requirements() -> str:
"""Pick the PyTorch overlay for this host."""
if os.name == "nt":
return "requirements-wsl.txt" # CPU / pure-Python, right for native Windows
return "requirements-windows.txt" # CPU / pure-Python, right for native Windows
if shutil.which("nvidia-smi"):
return "requirements-nvidia.txt"
lspci = shutil.which("lspci")
+33 -47
View File
@@ -1,7 +1,7 @@
#Requires -Version 5.1
<#
.SYNOPSIS
NexusOS installer for Windows (native - no WSL).
NexusOS installer for Windows (native).
.DESCRIPTION
Installs Python, Node.js, and Ollama via winget, builds the Promethean
virtualenv and the web UI, and drops a desktop shortcut. NexusOS then runs
@@ -11,17 +11,8 @@
Run from the nexus-core directory:
Right-click install-windows.ps1 -> "Run with PowerShell"
Requires Windows 10/11 with winget (App Installer). No WSL, no reboot.
.PARAMETER InvokerProfile
Internal. The PowerShell profile path of the user who launched the
installer, captured before self-elevation and passed to the elevated run.
If a standard account elevates with a DIFFERENT admin's credentials, the
elevated process sees that admin's $PROFILE - so ncp would be registered
for the wrong user. Not meant to be passed by hand.
Requires Windows 10/11 with winget (App Installer). No reboot needed.
#>
param(
[string]$InvokerProfile = ""
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
@@ -57,11 +48,7 @@ $IsAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIden
[Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $IsAdmin) {
Write-Host "Requesting administrator privileges..." -ForegroundColor Yellow
# Capture THIS user's profile path before elevating and hand it to the
# elevated run: if UAC prompts for another admin's credentials, that process
# would otherwise register ncp in the wrong user's profile.
$Invoker = $PROFILE.CurrentUserAllHosts
Start-Process powershell -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" -InvokerProfile `"$Invoker`"" -Verb RunAs
Start-Process powershell -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs
exit
}
@@ -100,7 +87,7 @@ if (-not (Test-Path $VenvPy)) {
if (-not (Test-Path $VenvPy)) { Write-Fail "venv creation failed at $Venv" }
& $VenvPy -m pip install --upgrade pip -q
$Req = Join-Path $RepoRoot "requirements-wsl.txt" # CPU / pure-Python overlay - right for native Windows too
$Req = Join-Path $RepoRoot "requirements-windows.txt" # CPU / pure-Python overlay - right for native Windows too
if (-not (Test-Path $Req)) { Write-Fail "requirements file not found: $Req" }
& $VenvPy -m pip install -r $Req
if ($LASTEXITCODE -ne 0) { Write-Fail "pip install failed (exit $LASTEXITCODE) - see output above" }
@@ -178,41 +165,40 @@ try {
}
# -- ncp command ---------------------------------------------------------------
# The Linux side registers ncp in ~/.bashrc from restore-linux.sh; this is the
# same idea for PowerShell. Guarded by a marker so re-running the installer is a
# no-op instead of stacking duplicate functions.
# management\ncp.cmd goes on the machine PATH rather than a `function ncp` in the
# PowerShell profile. Three reasons the profile route kept biting:
# 1. Execution policy defaults to Restricted, which blocks the profile itself -
# so the function was never defined, and ncp.ps1 could not have run anyway.
# 2. Profiles are a PowerShell thing. cmd, Win+R, Task Scheduler and .lnk
# targets all reported "ncp is not recognized".
# 3. We are elevated here, so $PROFILE is the ADMIN's profile - the whole
# -InvokerProfile dance existed to work around that. Machine PATH has no
# such split.
# The Linux side does the matching thing: a symlink in /usr/local/bin.
Write-Step "Registering the ncp command"
try {
# CurrentUserAllHosts (profile.ps1), not $PROFILE: $PROFILE is host-specific,
# so ncp would exist in the console but not in the VS Code terminal or ISE.
# Prefer the invoking user's path when we were elevated - see -InvokerProfile.
$ProfilePath = if ($InvokerProfile) { $InvokerProfile } else { $PROFILE.CurrentUserAllHosts }
$ProfileDir = Split-Path -Parent $ProfilePath
if (-not (Test-Path $ProfileDir)) {
New-Item -ItemType Directory -Path $ProfileDir -Force | Out-Null
}
$Marker = "# NexusOS ncp"
$Existing = ""
if (Test-Path $ProfilePath) {
$Existing = Get-Content -Path $ProfilePath -Raw -ErrorAction SilentlyContinue
}
if ($Existing -and $Existing.Contains($Marker)) {
Write-OK "ncp already registered in $ProfilePath"
$NcpCmd = Join-Path $RepoRoot "management\ncp.cmd"
if (-not (Test-Path $NcpCmd)) { throw "ncp.cmd not found at '$NcpCmd'" }
$BinDir = Split-Path -Parent $NcpCmd
# Machine scope needs admin, which the self-elevation above already gave us.
# Read-modify-write through .NET, NEVER setx: setx silently truncates PATH at
# 1024 characters and has permanently broken machines.
$Current = [Environment]::GetEnvironmentVariable("Path", "Machine")
$Entries = @($Current -split ';' | Where-Object { $_ -ne "" })
if ($Entries -contains $BinDir) {
Write-OK "ncp already on the machine PATH ($BinDir)"
} else {
$NcpPs1 = Join-Path $RepoRoot "management\ncp.ps1"
# Without this, a Join-Path that fails writes `function ncp { & "" }` -
# a broken profile with no error. Fail into the catch below instead.
if (-not (Test-Path $NcpPs1)) { throw "ncp.ps1 not found at '$NcpPs1'" }
# ASCII encoding: the content is ASCII anyway, and -Encoding UTF8 on
# PowerShell 5.1 can plant a BOM in the middle of an existing profile.
Add-Content -Path $ProfilePath -Encoding ASCII -Value ""
Add-Content -Path $ProfilePath -Encoding ASCII -Value $Marker
Add-Content -Path $ProfilePath -Encoding ASCII -Value "function ncp { & `"$NcpPs1`" @args }"
Write-OK "ncp registered in $ProfilePath"
$New = (@($Entries) + $BinDir) -join ';'
[Environment]::SetEnvironmentVariable("Path", $New, "Machine")
Write-OK "ncp registered on the machine PATH ($BinDir)"
}
# PATH is read at process start, so this shell and any already-open one still
# do not have it. Patch ours so the rest of the installer can call ncp.
$env:Path = "$env:Path;$BinDir"
} catch {
Write-Warn "Could not register ncp - add this line to your PowerShell profile:"
Write-Warn " function ncp { & '$RepoRoot\management\ncp.ps1' @args }"
Write-Warn "Could not put ncp on PATH - add this directory to PATH by hand:"
Write-Warn " $RepoRoot\management"
}
# -- Desktop shortcut ----------------------------------------------------------
+3 -2
View File
@@ -4,7 +4,8 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
// host: true binds 0.0.0.0 so the Windows browser can reach the dev server
// through WSL2 (loopback-only binding isn't reliably forwarded out of WSL).
// host: true binds 0.0.0.0 so the dev server is reachable from another
// machine on the LAN, not just localhost. Dev-only - production is the built
// dist/ served by the backend on :8000.
server: { host: true },
})
+1 -1
View File
@@ -1,4 +1,4 @@
# NexusOS launcher for Windows (native, no WSL, no Vite).
# NexusOS launcher for Windows (native, no Vite).
#
# Single-process app: the backend on :8000 serves the built web UI itself, so
# this starts only the memory service + backend, then opens the UI as an Edge
+2 -2
View File
@@ -80,7 +80,7 @@ class NexusControlPanel:
self.root = root
if os.name != 'nt' and not os.environ.get('DISPLAY'):
print("ERROR: No DISPLAY variable. Check WSLg.")
print("ERROR: No DISPLAY variable. Is an X session running?")
sys.exit(1)
self.root.title("Nexus Control Panel")
@@ -91,7 +91,7 @@ class NexusControlPanel:
print(f"Icon load failed: {e}")
if not sys.platform.startswith("linux"):
# Keep the custom borderless titlebar on Windows/WSL,
# Keep the custom borderless titlebar on Windows,
# but allow normal stacking on Linux Mint.
self.root.overrideredirect(True)
self.root.configure(background="#1e1e1e")
+16
View File
@@ -0,0 +1,16 @@
@echo off
REM ncp - PATH entry point on Windows. A .cmd, not a .ps1, on purpose: PowerShell's
REM execution policy governs .ps1 only, so this keeps working under the default
REM Restricted policy and from cmd, Win+R, Task Scheduler and .lnk targets - none
REM of which load a PowerShell profile. install-windows.ps1 puts this directory on
REM the machine PATH. ncp.ps1 stays for pwsh-on-Linux, where there is no PATH shim.
REM
REM ASCII only, same rule as the .ps1 files - a test in tests/test_smoke.py enforces it.
setlocal
set "ROOT=%~dp0.."
REM System Python fallback so backup/restore work before the venv exists; those
REM delegate to the stdlib-only bin/sync.py. Mirrors the same fallback in ncp.ps1.
set "PY=%ROOT%\Promethean\Scripts\python.exe"
if not exist "%PY%" set "PY=python"
"%PY%" "%ROOT%\management\ncp.py" %*
exit /b %ERRORLEVEL%
+4 -2
View File
@@ -2,8 +2,10 @@
# file the Linux shell wrapper runs; this only picks an interpreter and forwards
# the arguments.
#
# Windows: install-windows.ps1 registers this automatically. To do it by hand,
# or to get ncp inside PowerShell on Linux, add to $PROFILE.CurrentUserAllHosts:
# On Windows, ncp comes from management/ncp.cmd on the machine PATH instead (a
# .cmd is not subject to the execution policy and works outside PowerShell too) -
# install-windows.ps1 sets that up. This file is what gives ncp to pwsh on Linux,
# where restore-linux.sh registers it in $PROFILE.CurrentUserAllHosts as:
#
# function ncp { & "<repo>/management/ncp.ps1" @args }
#
+1 -1
View File
@@ -12,7 +12,7 @@ bin/restore-linux.sh, controlpanel.py, nexus-popup.py and nexus-app.sh all keep
calling what they always called.
psutil does the process work (cmdlines, pattern sweeps, port owners). It is
declared in requirements-base.txt AND requirements-wsl.txt, so both boxes have
declared in requirements-base.txt AND requirements-windows.txt, so both boxes have
it - but it is imported lazily so `ncp backup`/`restore` still run before the
venv exists, exactly as they did when they shelled out to sync.py.
"""
View File
+25 -5
View File
@@ -54,19 +54,39 @@ def test_default_models_have_one_source_of_truth():
assert hardcoded not in ps1, f"install-windows.ps1 hardcodes {hardcoded!r}"
def test_powershell_files_stay_ascii():
def test_windows_scripts_stay_ascii():
# PowerShell 5.1 decodes BOM-less files as ANSI: one stray Unicode dash
# eats a quote and the whole script dies at parse time. Globbed rather than
# listed by name so a newly added .ps1 is covered without editing this test.
# eats a quote and the whole script dies at parse time. cmd.exe is worse
# still - it decodes by the console codepage. Globbed rather than listed by
# name so a newly added script is covered without editing this test.
skip = {"Promethean", "node_modules", ".git", "dist"}
ps1s = [p for p in REPO_ROOT.rglob("*.ps1") if not skip & set(p.parts)]
assert ps1s, "no .ps1 files found - did the Windows path move?"
ps1s = [p for pat in ("*.ps1", "*.cmd") for p in REPO_ROOT.rglob(pat)
if not skip & set(p.parts)]
assert ps1s, "no .ps1/.cmd files found - did the Windows path move?"
for path in ps1s:
raw = path.read_bytes()
bad = [(i, b) for i, b in enumerate(raw) if b > 0x7F]
assert not bad, f"{path.relative_to(REPO_ROOT)} has non-ASCII bytes at {bad[:3]}"
def test_ncp_is_registered_on_path_not_in_a_shell_profile():
"""A `function ncp` in a shell profile is invisible to cron, .desktop Exec
lines, cmd.exe and Task Scheduler - and on Windows the default Restricted
execution policy blocks the profile outright. Both installers must put ncp
on PATH; the profile wiring only survives as a no-sudo fallback."""
linux = (REPO_ROOT / "bin" / "restore-linux.sh").read_text(encoding="utf-8")
runtime = linux.split('if [ "$stage" = "runtime" ]; then')[1].split("\n exit 0\nfi")[0]
assert "/usr/local/bin/ncp" in runtime
ps1 = (REPO_ROOT / "install-windows.ps1").read_text(encoding="utf-8")
assert "ncp.cmd" in ps1, "installer must register the .cmd shim, not a profile function"
# setx truncates PATH at 1024 characters and has permanently broken machines.
# Comments stripped so the code that explains the ban does not trip it.
code = "\n".join(ln for ln in ps1.splitlines() if not ln.strip().startswith("#"))
assert "setx" not in code.lower()
assert 'SetEnvironmentVariable("Path"' in code
def test_first_playbook_is_the_system_prompt(tmp_path, monkeypatch):
store = PlaybookFileStore(tmp_path)
store.add_playbook(PlaybookItem(id="ctx", title="Reference", goal="ref goal",