From db852630835270c448b4e21a28c770e23918c5c3 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Mon, 3 Aug 2026 04:09:08 +0000
Subject: [PATCH 01/16] feat: OEW assessment, jspace package, and codespace
reorganization
Research Hermes Agent Quicksilver/Judgment updates and invent the
Obligatory External Workspace (OEW) path to make Hermespace the
functional J-space of Hermes agents.
- Add deep assessment + OEW thesis + phased roadmap
- Move jspace into src/hermespace/jspace/ (hub, env, protocol)
- Reorganize docs into jspace/assessment/architecture/integration/ops/research/roadmap
- Soft-wire ProtocolGate into workflow meta (HERMESPACE_OEW)
- Add LAYOUT.md and planned turn/memory/warehouse package READMEs
Co-authored-by: Pablo
---
CONTRIBUTING.md | 3 +-
FOR_HERMES.md | 90 +--
INTEGRATION.md | 2 +-
LAYOUT.md | 59 ++
PURPOSE.md | 10 +-
README.md | 31 +-
docs/INDEX.md | 49 +-
docs/README.md | 15 +
docs/{ => architecture}/01-architecture.md | 0
docs/{ => architecture}/CODEMAP.md | 31 +-
docs/{ => architecture}/HERMESCUBE.md | 0
.../28-hermes-agent-jspace-assessment.md | 254 +++++++
.../02-hermes-plugin.md} | 0
.../15-ecosystem-fit.md} | 0
.../16-why-hermes.md} | 0
.../17-skills-memory.md} | 0
docs/integration/FOR_HERMES.md | 89 +++
.../00-map.md} | 0
.../10-claude-research.md} | 2 +-
.../27-environment.md} | 2 +-
docs/jspace/thesis-oew.md | 46 ++
.../18-tailscale.md} | 0
.../19-pocket-security.md} | 0
docs/{20-pulse-runtime.md => ops/20-pulse.md} | 0
.../21-hardening.md} | 0
.../23-everyday.md} | 0
docs/{ => ops}/RECOMMENDED.md | 0
docs/{ => ops}/hermes-env.example.sh | 0
.../03-cross-exam-and-networks.md | 0
docs/{ => research}/04-pattern-matrix.md | 0
docs/{ => research}/05-desk-pane.md | 0
docs/{ => research}/06-component-research.md | 0
.../07-cognitive-neuroscience.md | 0
.../08-meta-brain-ai-reverse.md | 0
docs/{ => research}/09-agent-io-and-memory.md | 0
docs/{ => research}/11-neural-space.md | 0
docs/{ => research}/12-local-model-neural.md | 0
.../13-full-concept-research.md | 0
.../14-workbench-pocket-dimension.md | 0
docs/{ => research}/18-autonomy-grid.md | 0
.../{ => research}/24-comparative-analysis.md | 0
.../{ => research}/25-context-optimization.md | 0
docs/{ => research}/26-benchmarks.md | 0
docs/{ => roadmap}/22-open-roadmap.md | 0
docs/roadmap/phases-oew.md | 12 +
skills/hermespace/SKILL.md | 5 +-
src/hermespace/__init__.py | 5 +-
src/hermespace/cube_module.py | 2 +-
src/hermespace/jspace/__init__.py | 45 ++
src/hermespace/jspace/env.py | 649 ++++++++++++++++++
src/hermespace/{jspace.py => jspace/hub.py} | 0
src/hermespace/jspace/protocol.py | 139 ++++
src/hermespace/jspace_env.py | 649 +-----------------
src/hermespace/memory/README.md | 13 +
src/hermespace/turn/README.md | 15 +
src/hermespace/warehouse/README.md | 10 +
src/hermespace/workflow.py | 17 +
tests/test_jspace_protocol.py | 66 ++
58 files changed, 1505 insertions(+), 805 deletions(-)
create mode 100644 LAYOUT.md
create mode 100644 docs/README.md
rename docs/{ => architecture}/01-architecture.md (100%)
rename docs/{ => architecture}/CODEMAP.md (66%)
rename docs/{ => architecture}/HERMESCUBE.md (100%)
create mode 100644 docs/assessment/28-hermes-agent-jspace-assessment.md
rename docs/{02-integration-hermes.md => integration/02-hermes-plugin.md} (100%)
rename docs/{15-hermes-ecosystem-fit.md => integration/15-ecosystem-fit.md} (100%)
rename docs/{16-why-hermes-framework.md => integration/16-why-hermes.md} (100%)
rename docs/{17-skills-memory-bridge.md => integration/17-skills-memory.md} (100%)
create mode 100644 docs/integration/FOR_HERMES.md
rename docs/{00-jspace-to-hermespace.md => jspace/00-map.md} (100%)
rename docs/{10-jspace-claude.md => jspace/10-claude-research.md} (93%)
rename docs/{27-jspace-environment.md => jspace/27-environment.md} (97%)
create mode 100644 docs/jspace/thesis-oew.md
rename docs/{18-tailscale-viewport.md => ops/18-tailscale.md} (100%)
rename docs/{19-pocket-security-viewport.md => ops/19-pocket-security.md} (100%)
rename docs/{20-pulse-runtime.md => ops/20-pulse.md} (100%)
rename docs/{21-preupdate-hardening.md => ops/21-hardening.md} (100%)
rename docs/{23-everyday-ops.md => ops/23-everyday.md} (100%)
rename docs/{ => ops}/RECOMMENDED.md (100%)
rename docs/{ => ops}/hermes-env.example.sh (100%)
rename docs/{ => research}/03-cross-exam-and-networks.md (100%)
rename docs/{ => research}/04-pattern-matrix.md (100%)
rename docs/{ => research}/05-desk-pane.md (100%)
rename docs/{ => research}/06-component-research.md (100%)
rename docs/{ => research}/07-cognitive-neuroscience.md (100%)
rename docs/{ => research}/08-meta-brain-ai-reverse.md (100%)
rename docs/{ => research}/09-agent-io-and-memory.md (100%)
rename docs/{ => research}/11-neural-space.md (100%)
rename docs/{ => research}/12-local-model-neural.md (100%)
rename docs/{ => research}/13-full-concept-research.md (100%)
rename docs/{ => research}/14-workbench-pocket-dimension.md (100%)
rename docs/{ => research}/18-autonomy-grid.md (100%)
rename docs/{ => research}/24-comparative-analysis.md (100%)
rename docs/{ => research}/25-context-optimization.md (100%)
rename docs/{ => research}/26-benchmarks.md (100%)
rename docs/{ => roadmap}/22-open-roadmap.md (100%)
create mode 100644 docs/roadmap/phases-oew.md
create mode 100644 src/hermespace/jspace/__init__.py
create mode 100644 src/hermespace/jspace/env.py
rename src/hermespace/{jspace.py => jspace/hub.py} (100%)
create mode 100644 src/hermespace/jspace/protocol.py
create mode 100644 src/hermespace/memory/README.md
create mode 100644 src/hermespace/turn/README.md
create mode 100644 src/hermespace/warehouse/README.md
create mode 100644 tests/test_jspace_protocol.py
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b3966ca..a37cee1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -33,7 +33,8 @@ PYTHONPATH=src python3 -m unittest discover -s tests -v
| Agent skill | `skills/hermespace/` |
| CLI | `src/hermespace/cli.py`, `scripts/hs` |
| Tests | `tests/` |
-| North star | `PURPOSE.md`, `docs/CODEMAP.md` |
+| North star | `PURPOSE.md`, `LAYOUT.md`, `docs/architecture/CODEMAP.md` |
+| Assessment | `docs/assessment/28-hermes-agent-jspace-assessment.md` |
| Maintainer note | `FOR_HERMES.md` |
| Integration doors | `INTEGRATION.md`, `WORKFLOW.md` |
diff --git a/FOR_HERMES.md b/FOR_HERMES.md
index 54397bd..b4795fc 100644
--- a/FOR_HERMES.md
+++ b/FOR_HERMES.md
@@ -1,89 +1,3 @@
-# For Hermes Agent maintainers & dogfooders
+# FOR_HERMES
-**Repo:** https://github.com/PabloTheThinker/hermespace
-**Companion to:** [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent)
-**Not** an official Nous product — independent open companion workspace.
-
-## What this is (30 seconds)
-
-Hermespace adds a **limited working-memory desk** beside Hermes:
-
-1. **Workbench** — FOA, park stack, load-aware `receive_order`
-2. **Dual decode** — short human `report` vs dense model `context` (never dump inject to chat)
-3. **Fabric** — rank *this* `$HERMES_HOME/skills` + inject MEMORY/USER excerpts
-4. **Plugin** — `on_session_start` / `pre_llm_call` / `on_session_end` broadcast when desk ready
-5. **Study DB** — turns under `~/.hermespace` (local; not Hermes session DB)
-
-It does **not** replace Hermes tools, skills, memory, gateway, or the agent loop.
-
-## Fast path
-
-```bash
-git clone https://github.com/PabloTheThinker/hermespace.git
-cd hermespace
-./scripts/install_hermes.sh
-./scripts/smoke_test.sh # expect 9/9
-```
-
-Then in any agent session:
-
-```python
-from hermespace import Workbench
-wb = Workbench(agent_id="hermes", session_id="main")
-wb.enter()
-r = wb.receive_order("…", goal="…", say="…", force=True)
-# r["user_reply"] → user r["model_context"] → model
-```
-
-Or CLI: `./scripts/hs turn -m "…" --goal "…" --say "…" --force`
-
-## Design honesty
-
-| Claim | Reality |
-|-------|---------|
-| “Like Claude J-space” | **Role** only — limited workspace *outside* weights. No activation access. |
-| Consciousness / brain scan | **No.** Harness cognition + optional local embeddings. |
-| Replaces Hermes skills/memory | **No.** Ranks and injects them. |
-| Official Nous module | **No.** Community companion; feedback welcome. |
-
-Inspired by working-memory limits (Baddeley-style capacity, load, GWT broadcast as *metaphors* for agent UX) and Anthropic’s public J-space *research framing* — implemented as open Hermes integration, not a closed-model probe.
-
-## Integration surface
-
-| Door | Entry |
-|------|--------|
-| Plugin | `hermes_plugin/` → `$HERMES_HOME/plugins/hermespace` |
-| Skill | `skills/hermespace/` → `$HERMES_HOME/skills/hermespace` |
-| Python | `hermespace.agent_api`, `hermespace.Workbench` |
-| CLI | `scripts/hs` |
-| Smoke | `scripts/smoke_test.sh` |
-
-Hook implementation lives in `src/hermespace/hermes_bridge.py` (plugin is thin `register`).
-
-When the plugin directory is a **symlink into this checkout**, import auto-resolves `../src` so dogfooders often need no `HERMESPACE_ROOT`.
-
-## Autonomy grid (v0.14)
-
-Missions, lenses, dream, self-talk, skillbench (hot-swap / merge / mutate), title tree.
-Ground-up for Hermespace — not a port of AgentDrive or Conductor.
-
-```bash
-hs grid status
-hs grid dream --force
-```
-
-See docs/18-autonomy-grid.md. Autonomy self-order stays **off** unless `HERMESPACE_AUTONOMY=1`.
-
-## What we want from Hermes dogfood
-
-- Does plugin register cleanly on current Hermes?
-- Is dual-decode useful on Telegram/gateway, or noisy?
-- Fabric skill ranking quality on real skill trees?
-- Gaps vs native Hermes memory / Honcho / skills loop?
-- Would you want any of this upstream-shaped (skill only, plugin only, or neither)?
-
-Open issues/PRs on the repo. Thanks for looking.
-
-## License
-
-MIT — see `LICENSE`. Hermes Agent remains Nous Research’s project under its own license.
+Moved to **[docs/integration/FOR_HERMES.md](docs/integration/FOR_HERMES.md)**.
diff --git a/INTEGRATION.md b/INTEGRATION.md
index 1f2ae50..87243bd 100644
--- a/INTEGRATION.md
+++ b/INTEGRATION.md
@@ -22,7 +22,7 @@ hermes plugins enable hermespace
```
Agent skill SoT: [`skills/hermespace/SKILL.md`](skills/hermespace/SKILL.md).
-Recommended env: see `docs/RECOMMENDED.md`.
+Recommended env: see `docs/ops/RECOMMENDED.md`. · Layout: `LAYOUT.md`.
## Door A — Python (best for agents)
diff --git a/LAYOUT.md b/LAYOUT.md
new file mode 100644
index 0000000..8298857
--- /dev/null
+++ b/LAYOUT.md
@@ -0,0 +1,59 @@
+# Hermespace codespace layout
+
+North star: [PURPOSE.md](PURPOSE.md) · Assessment:
+[docs/assessment/28-hermes-agent-jspace-assessment.md](docs/assessment/28-hermes-agent-jspace-assessment.md)
+
+## Repository map
+
+```
+hermespace/
+├── PURPOSE.md / ABOUT.md / README.md / LAYOUT.md
+├── src/hermespace/ # Python package
+│ ├── jspace/ # ★ External J-Space (hub · env · protocol)
+│ ├── turn/ # planned — workflow/desk/gate (README only)
+│ ├── memory/ # planned — world/episodic/semantic (README only)
+│ ├── warehouse/ # planned — cube cable (README only)
+│ ├── grid/ # autonomy grid (missions · dream · skillbench)
+│ ├── hermes_bridge.py # Hermes plugin hooks
+│ └── … # turn spine still at package root (stable imports)
+├── hermes_plugin/ # thin Hermes register()
+├── desktop_plugin/ # Hermes Desktop page/pane
+├── skills/hermespace/ # agent skill
+├── docs/
+│ ├── jspace/ # J-Space map · thesis · environment
+│ ├── assessment/ # deep assessments
+│ ├── architecture/ # CODEMAP · Cube contract
+│ ├── integration/ # Hermes fit · FOR_HERMES
+│ ├── ops/ # pulse · everyday · recommended
+│ ├── research/ # numbered research archive
+│ └── roadmap/ # open backlog · OEW phases
+├── tests/ · scripts/ · experiments/ · benchmarks/
+├── runtime/ · spec/ # templates / protocol specs
+└── assets/
+```
+
+## Layer authority
+
+| Layer | Path | Authority |
+|-------|------|-----------|
+| OEW / J-Space | `src/hermespace/jspace/` | Turn FOA + audit SoT |
+| Turn spine | package root (`workflow`, `desk`, …) | Live turn |
+| Autonomy | `grid/` | Missions / dream / skillbench |
+| Identity projection | `world.py` … | Recharged from Cube |
+| Warehouse cable | `cube_module.py` | Soft-fail Cube heart |
+| Hermes surface | `hermes_plugin/` + bridge | Hooks only |
+
+## Import rules
+
+Prefer:
+
+```python
+from hermespace.jspace import JSpace, JSpaceEnv, evaluate_material_turn
+```
+
+Compat: `hermespace.jspace_env` still re-exports `JSpaceEnv`.
+
+## Moving code later
+
+Planned packages under `turn/`, `memory/`, `warehouse/` hold READMEs only until
+OEW Phase B is green — avoid churning imports while the protocol lands.
diff --git a/PURPOSE.md b/PURPOSE.md
index d9fc900..6c94c5e 100644
--- a/PURPOSE.md
+++ b/PURPOSE.md
@@ -6,10 +6,12 @@ observable workspace (lens · silent chain · audit · reflect), running
**standalone** and **powered by HermesCube** at night the way CubeDream
consolidates the day.
-Public pitch: **[ABOUT.md](ABOUT.md)**. Research map:
-**[docs/27-jspace-environment.md](docs/27-jspace-environment.md)**.
-Code layout: **[docs/CODEMAP.md](docs/CODEMAP.md)**. Cube contract:
-**[docs/HERMESCUBE.md](docs/HERMESCUBE.md)**.
+Public pitch: **[ABOUT.md](ABOUT.md)**. Assessment:
+**[docs/assessment/28-hermes-agent-jspace-assessment.md](docs/assessment/28-hermes-agent-jspace-assessment.md)**.
+OEW thesis: **[docs/jspace/thesis-oew.md](docs/jspace/thesis-oew.md)**.
+Environment: **[docs/jspace/27-environment.md](docs/jspace/27-environment.md)**.
+Code layout: **[LAYOUT.md](LAYOUT.md)** · **[docs/architecture/CODEMAP.md](docs/architecture/CODEMAP.md)**.
+Cube contract: **[docs/architecture/HERMESCUBE.md](docs/architecture/HERMESCUBE.md)**.
---
diff --git a/README.md b/README.md
index d24db2d..95ff6df 100644
--- a/README.md
+++ b/README.md
@@ -229,21 +229,21 @@ $HERMESPACE_HOME/memory/hermespace/
| Doc | Contents |
|-----|----------|
| [`PURPOSE.md`](PURPOSE.md) | North star — true external J-Space + Cube night path |
-| [`docs/27-jspace-environment.md`](docs/27-jspace-environment.md) | Anthropic research → Hermespace environment |
+| [`LAYOUT.md`](LAYOUT.md) | Codespace / folder map |
+| [`docs/assessment/28-hermes-agent-jspace-assessment.md`](docs/assessment/28-hermes-agent-jspace-assessment.md) | Hermes Agent updates → OEW plan |
+| [`docs/jspace/thesis-oew.md`](docs/jspace/thesis-oew.md) | Obligatory External Workspace thesis |
+| [`docs/jspace/27-environment.md`](docs/jspace/27-environment.md) | Environment API |
| [`ABOUT.md`](ABOUT.md) | Philosophy, design principles, author |
-| [`docs/CODEMAP.md`](docs/CODEMAP.md) | Where to edit (layer map) |
-| [`docs/HERMESCUBE.md`](docs/HERMESCUBE.md) | Cube heart/center contract |
+| [`docs/architecture/CODEMAP.md`](docs/architecture/CODEMAP.md) | Where to edit (layer map) |
+| [`docs/architecture/HERMESCUBE.md`](docs/architecture/HERMESCUBE.md) | Cube heart/center contract |
| [`INTEGRATION.md`](INTEGRATION.md) | Python · CLI · plugin · workbench doors |
| [`skills/hermespace/SKILL.md`](skills/hermespace/SKILL.md) | **Agent skill** (load in Hermes) |
-| [`FOR_HERMES.md`](FOR_HERMES.md) | Maintainer / dogfood brief |
+| [`docs/integration/FOR_HERMES.md`](docs/integration/FOR_HERMES.md) | Maintainer / dogfood brief |
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | How to contribute |
| [`WORKFLOW.md`](WORKFLOW.md) | GATE → SEAL stages |
| [`SECURITY.md`](SECURITY.md) | What never ships in git |
-| [`docs/14-workbench-pocket-dimension.md`](docs/14-workbench-pocket-dimension.md) | Workbench reference (legacy) |
-| [`docs/16-why-hermes-framework.md`](docs/16-why-hermes-framework.md) | Why this belongs in Hermes |
-| [`docs/18-autonomy-grid.md`](docs/18-autonomy-grid.md) | Grid design |
-| [`docs/20-pulse-runtime.md`](docs/20-pulse-runtime.md) | Pulse runtime |
-| [`docs/22-open-roadmap.md`](docs/22-open-roadmap.md) | Roadmap |
+| [`docs/roadmap/phases-oew.md`](docs/roadmap/phases-oew.md) | OEW phases A–D |
+| [`docs/README.md`](docs/README.md) | Full docs folder index |
| [`spec/DESK.md`](spec/DESK.md) | Desk schema |
| [`spec/PROTOCOL.md`](spec/PROTOCOL.md) | Protocol spec |
@@ -252,18 +252,19 @@ $HERMESPACE_HOME/memory/hermespace/
## Repository Layout
```text
-assets/ media (banners, diagrams)
+LAYOUT.md codespace map (start here for structure)
src/hermespace/ runtime package
+ jspace/ ★ external J-Space (hub · env · OEW protocol)
+ grid/ autonomy grid
hermes_plugin/ Hermes session / pre_llm / end hooks
skills/hermespace/ public Hermes agent skill
+docs/ jspace · assessment · architecture · ops · research
scripts/ CLI, install, smoke test, security audit
-docs/ design notes (20+ docs)
-spec/ desk schema + protocol
-tests/ unit tests
-experiments/ eval harness, neural benchmarks
-desktop_plugin/ Hermes Desktop sidebar + full page
+tests/ · experiments/ · desktop_plugin/ · spec/
```
+Full map: [`LAYOUT.md`](LAYOUT.md).
+
---
## Verify
diff --git a/docs/INDEX.md b/docs/INDEX.md
index a8b034a..5dfc1cc 100644
--- a/docs/INDEX.md
+++ b/docs/INDEX.md
@@ -1,34 +1,21 @@
# docs index
-Start: [../README.md](../README.md) · [../FOR_HERMES.md](../FOR_HERMES.md) · [../INTEGRATION.md](../INTEGRATION.md) · [../WORKFLOW.md](../WORKFLOW.md)
+Start: [../README.md](../README.md) · [../LAYOUT.md](../LAYOUT.md) · [../PURPOSE.md](../PURPOSE.md)
-| File | Topic |
-|------|--------|
-| [27-jspace-environment.md](27-jspace-environment.md) | True J-Space environment — Anthropic research → external observable workspace |
-| [01-architecture.md](01-architecture.md) | Architecture |
-| [CODEMAP.md](CODEMAP.md) | Where to edit (layer map) |
-| [HERMESCUBE.md](HERMESCUBE.md) | Cube heart/center contract + standalone |
-| [02-integration-hermes.md](02-integration-hermes.md) | Hermes plugin |
-| [03-cross-exam-and-networks.md](03-cross-exam-and-networks.md) | Cross-exam |
-| [04-pattern-matrix.md](04-pattern-matrix.md) | Pattern matrix |
-| [05-desk-pane.md](05-desk-pane.md) | Viewing the desk |
-| [06-component-research.md](06-component-research.md) | Components |
-| [07-cognitive-neuroscience.md](07-cognitive-neuroscience.md) | Baddeley / GWT / load metaphors |
-| [08-meta-brain-ai-reverse.md](08-meta-brain-ai-reverse.md) | Meta encode/decode reverse |
-| [09-agent-io-and-memory.md](09-agent-io-and-memory.md) | Agent INPUT/OUTPUT + memory DB |
-| [10-jspace-claude.md](10-jspace-claude.md) | J-space / Claude research note |
-| [11-neural-space.md](11-neural-space.md) | Neural space design + backends |
-| [12-local-model-neural.md](12-local-model-neural.md) | Local models for neural space |
-| [RECOMMENDED.md](RECOMMENDED.md) | Production best-path config |
-| [13-full-concept-research.md](13-full-concept-research.md) | Full concept research |
-| [14-workbench-pocket-dimension.md](14-workbench-pocket-dimension.md) | Workbench / pocket dimension |
-| [15-hermes-ecosystem-fit.md](15-hermes-ecosystem-fit.md) | Hermes / Nous ecosystem fit |
-| [16-why-hermes-framework.md](16-why-hermes-framework.md) | Why Hermespace belongs in Hermes |
-| [17-skills-memory-bridge.md](17-skills-memory-bridge.md) | Skills + MEMORY/USER fabric |
-| [hermes-env.example.sh](hermes-env.example.sh) | Env template |
-| [18-autonomy-grid.md](18-autonomy-grid.md) | Autonomy grid — missions, lenses, dream, gated self-order |
-| [19-pocket-security-viewport.md](19-pocket-security-viewport.md) | Pocket boundary + user viewport |
-| [20-pulse-runtime.md](20-pulse-runtime.md) | Pulse pocket runtime (smarter than cron) |
-| [21-preupdate-hardening.md](21-preupdate-hardening.md) | v0.16.1 perf + notes fixes |
-| [22-open-roadmap.md](22-open-roadmap.md) | Open research backlog |
-| [18-tailscale-viewport.md](18-tailscale-viewport.md) | Portable Tailscale viewport |
+## Primary (read these)
+
+| Doc | Topic |
+|-----|--------|
+| [assessment/28-hermes-agent-jspace-assessment.md](assessment/28-hermes-agent-jspace-assessment.md) | Hermes Agent updates → OEW plan |
+| [jspace/thesis-oew.md](jspace/thesis-oew.md) | Obligatory External Workspace thesis |
+| [jspace/27-environment.md](jspace/27-environment.md) | Environment API |
+| [jspace/00-map.md](jspace/00-map.md) | Anthropic property → harness map |
+| [architecture/CODEMAP.md](architecture/CODEMAP.md) | Where to edit |
+| [architecture/HERMESCUBE.md](architecture/HERMESCUBE.md) | Cube heart contract |
+| [integration/FOR_HERMES.md](integration/FOR_HERMES.md) | Dogfood notes for Hermes |
+| [roadmap/phases-oew.md](roadmap/phases-oew.md) | Phase A–D |
+
+## By folder
+
+See [README.md](README.md) for the full folder map. Research archive lives under
+[research/](research/); ops under [ops/](ops/).
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..b3d168a
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,15 @@
+# Documentation
+
+| Folder | Contents |
+|--------|----------|
+| [jspace/](jspace/) | Anthropic map · OEW thesis · environment API |
+| [assessment/](assessment/) | Deep assessments (start with **28**) |
+| [architecture/](architecture/) | CODEMAP · Cube contract |
+| [integration/](integration/) | Hermes plugin · ecosystem · FOR_HERMES |
+| [ops/](ops/) | Pulse · everyday · recommended config |
+| [research/](research/) | Pattern matrix · neural · comparative archive |
+| [roadmap/](roadmap/) | Open backlog · OEW phases |
+
+**Start here:** [assessment/28-hermes-agent-jspace-assessment.md](assessment/28-hermes-agent-jspace-assessment.md)
+**Thesis:** [jspace/thesis-oew.md](jspace/thesis-oew.md)
+**Repo layout:** [../LAYOUT.md](../LAYOUT.md)
diff --git a/docs/01-architecture.md b/docs/architecture/01-architecture.md
similarity index 100%
rename from docs/01-architecture.md
rename to docs/architecture/01-architecture.md
diff --git a/docs/CODEMAP.md b/docs/architecture/CODEMAP.md
similarity index 66%
rename from docs/CODEMAP.md
rename to docs/architecture/CODEMAP.md
index 449828e..904ef23 100644
--- a/docs/CODEMAP.md
+++ b/docs/architecture/CODEMAP.md
@@ -3,7 +3,8 @@
Hermespace is one Python package (`src/hermespace/`) plus a thin Hermes plugin
(`hermes_plugin/`), skill, and optional desktop UI.
-**North star:** [PURPOSE.md](../PURPOSE.md) · **Architecture:** [01-architecture.md](01-architecture.md)
+**North star:** [PURPOSE.md](../../PURPOSE.md) · **Layout:** [LAYOUT.md](../../LAYOUT.md) ·
+**Assessment:** [../assessment/28-hermes-agent-jspace-assessment.md](../assessment/28-hermes-agent-jspace-assessment.md)
## Layers (edit here first)
@@ -12,11 +13,20 @@ L6 operator cli.py ops.py scripts/ desktop_plugin/
L5 autonomy grid/* pulse.py
L4 identity world.py episodic.py semantic.py memory_db.py
L3 warehouse cube_module.py (Cube center/heart OR standalone)
-L2 J-Space ENV jspace.py jspace_env.py cognition.py streams.py neural_space.py
+L2 J-Space OEW jspace/ cognition.py streams.py neural_space.py
L1 turn spine workflow.py engine.py desk.py gate.py inject.py
L0 contract io_contract.py paths.py store.py agent_api.py
```
+## J-Space package (`jspace/`) — L2
+
+| Module | Role |
+|--------|------|
+| `jspace/hub.py` | Hub · hold · reason · report · broadcast |
+| `jspace/env.py` | Lens · swap · audit · reflect · harvest |
+| `jspace/protocol.py` | OEW gate (`HERMESPACE_OEW`) |
+| `jspace_env.py` | Compat shim → `jspace.env` |
+
## Turn spine (L1)
| Module | Role |
@@ -27,16 +37,6 @@ L0 contract io_contract.py paths.py store.py agent_api.py
| `gate.py` | Selectivity — skip trivial |
| `inject.py` | GWT broadcast of desk |
-## J-Space environment (L2)
-
-| Module | Role |
-|--------|------|
-| `jspace.py` | Hub · hold · reason · report · broadcast |
-| `jspace_env.py` | Lens · swap · audit · reflect · harvest · protocol |
-| `cognition.py` | FOA≤4, load, executive modes |
-| `streams.py` | Multi-stream encode / report decode |
-| `neural_space.py` | Embedding FOA field |
-
## Warehouse cable (L3)
| Module | Role |
@@ -52,10 +52,15 @@ L0 contract io_contract.py paths.py store.py agent_api.py
| `workbench.py` | Session pocket — enter / order / idle |
| `agent_api.py` | Public doors for agents |
+## Planned packages (README only)
+
+`turn/` · `memory/` · `warehouse/` — move modules after OEW Phase B is green.
+
## Rules of thumb
1. Dual decode: never dump `context` into user chat.
2. Soft-fail Cube — Space must run standalone.
3. When Cube present: `.cube` is durable SoT; world JSONL is projection.
4. Peel grid features; don’t inflate `cli.py` without need.
-5. J-Space silent steps stay in model context only.
+5. J-Space silent steps stay in model context only.
+6. Prefer `from hermespace.jspace import …` over deep/compat paths.
diff --git a/docs/HERMESCUBE.md b/docs/architecture/HERMESCUBE.md
similarity index 100%
rename from docs/HERMESCUBE.md
rename to docs/architecture/HERMESCUBE.md
diff --git a/docs/assessment/28-hermes-agent-jspace-assessment.md b/docs/assessment/28-hermes-agent-jspace-assessment.md
new file mode 100644
index 0000000..84aef1b
--- /dev/null
+++ b/docs/assessment/28-hermes-agent-jspace-assessment.md
@@ -0,0 +1,254 @@
+# Assessment — Making Hermespace the J-Space of Hermes Agent
+
+**Date:** 2026-08-03
+**Hermespace:** v0.20.0
+**Hermes Agent surveyed:** v0.19.1 (v2026.7.30) · Quicksilver v0.19.0 · Judgment v0.18.0 · main as of 2026-08-03
+**Companion:** [HermesCube](https://github.com/PabloTheThinker/hermescube) v0.50 (heart / library)
+**Research basis:** Anthropic *Verbalizable Representations Form a Global Workspace* (2026)
+
+---
+
+## 0. Verdict
+
+**Possible.** Hermespace already implements the *roles* of Anthropic's J-space as an
+external harness. Hermes Agent's recent architecture (cache-safe `pre_llm_call`,
+Context Engine `select_context` / `on_turn_complete`, MemoryProvider split,
+background review, MoA, verification contracts, subagent transcripts, live
+reasoning streams) gives us the **hooks to make that workspace obligatory and
+causal** — the missing piece that turns a diary into a global workspace.
+
+The innovation is not a Jacobian lens on Hermes weights. It is an
+**Obligatory External Workspace (OEW)**: a protocol bottleneck where material
+Hermes cognition must park verbalizable intermediates in Hermespace, the operator
+can lens them, swaps redirect later Report/broadcast, and CubeDream consolidates
+them at night.
+
+```
+Anthropic: weights ──J-lens──► silent verbalizable concepts
+Hermespace: Hermes ──OEW protocol──► durable hub/silent chain ──lens──► operator
+ │
+ ▼
+ Cube heart (day strip) + CubeDream (night)
+```
+
+---
+
+## 1. What Anthropic J-space is (target properties)
+
+| Property | Meaning | Hermespace analogue today | Gap |
+|----------|---------|---------------------------|-----|
+| Report | Contents can be named on ask | `JSpace.report` / dual decode | Soft — not forced |
+| Directed modulation | Hold / focus on request | `hold` / `release` / `inhibit` | Soft |
+| Silent multi-step | Intermediates without speech | `reason_step` + mid band | Opt-in |
+| Flexible broadcast | One concept → many tasks | `broadcast_block` on inject | Present |
+| Selectivity | Fluency without workspace | `gate.should_inject` | Weak measurement |
+| Causal swap/inject | Edits change answers | `swap` / `inject` / `ablate` | Not turn-binding yet |
+| Alignment audit | Hidden goals visible | soft lexicon `audit()` | Externalized-only |
+| Assistant POV | Post-training voice in workspace | `set_pov` | Present |
+| Counterfactual reflection | Interrupt → shape later thought | `reflect()` | No next-turn seed |
+| Night consolidation | Sleep / dream | `dream_harvest` + CubeDream | Wired soft |
+
+Capacity targets already match the paper's "tens of concepts": hub ≤25, activated ≤12, FOA ≤4.
+
+---
+
+## 2. Recent Hermes Agent updates — what they unlock
+
+Sources: [v0.19.0 Quicksilver](https://github.com/NousResearch/hermes-agent/releases/tag/v2026.7.20),
+[v0.18.0 Judgment](https://github.com/NousResearch/hermes-agent/releases/tag/v2026.7.1),
+developer docs for [hooks](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md),
+[MemoryProvider](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/developer-guide/memory-provider-plugin.md),
+[Context Engine](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/developer-guide/context-engine-plugin.md),
+`agent/background_review.py`.
+
+### 2.1 Quicksilver (v0.19) — speed + durability
+
+| Hermes change | Hermespace implication |
+|---------------|------------------------|
+| ~80% TTFT cut; cold path ruthless | Inject must stay **tiny**; load budgets (900/640/420/280) are non-negotiable |
+| Live reasoning streams default ON | Mid-band can mirror streamed reasoning **without** dumping it into user Report |
+| Smart approvals default + `pre_tool_call` approve | Workspace `audit()` flags can escalate tool risk |
+| Subagent live transcripts + durable ledger | Per-child `JSpace(agent_id=sub…)` hubs; parent lens aggregates |
+| Delivery-obligation ledger | Seal + blackbox provenance pair with Cube heart |
+| Profile-based gateway routing | `HERMESPACE_HOME` / agent_id per profile — isolate hubs |
+| Sessions export (MD/HTML/HF traces) | Export hub + silent chain + audit as alignment dataset |
+| Hooks spill oversized context to disk | Cap J-Space broadcast; spill full lens to viewport files |
+| Byte-stable gateway system prompts | Keep Space out of system prompt; user-message inject only |
+
+### 2.2 Judgment (v0.18) — thinking quality
+
+| Hermes change | Hermespace implication |
+|---------------|------------------------|
+| MoA first-class presets | Hub holds committee intermediates; Report = aggregator only |
+| Verification evidence + `/goal` completion contracts | Seal verified outcomes into hub → Cube; never claim-only |
+| Background `delegate_task` fan-out | Parent hub parks mission; children write silent steps; harvest merges |
+| `/learn` + `/journey` + memory graph | WorldModel + Cube journey stay projections; Space FOA stays turn-local |
+| Background review fork (memory/skill) | Night sibling: `dream_harvest` after review, not competing MemoryProvider |
+
+### 2.3 Stable platform contracts (must respect)
+
+| Contract | Rule for Hermespace |
+|----------|---------------------|
+| `pre_llm_call` → **user message** only | Never touch system prompt (cache) |
+| One MemoryProvider | **Cube** owns it; Space must not register a second provider |
+| One Context Engine | Optional future: OEW as `context.engine` *or* stay plugin-hook |
+| Soft-fail plugins | Bridge never crashes Hermes |
+| Builtin MEMORY.md | Hot catalog; Cube mirrors; Space proposes only |
+
+---
+
+## 3. HermesCube role (do not blur)
+
+From Cube `docs/HERMESPACE.md` / `ANATOMY.md` / `CUBEDREAM.md`:
+
+| Organ | Owner | Day | Night |
+|-------|-------|-----|-------|
+| Nervous FOA / J-Space | **Hermespace** | hold · reason · report · lens | harvest source |
+| Heart (`memory.cube`) | **HermesCube** | arterial strip | seal intake |
+| Dream | Space grid + CubeDream | — | consolidate chapters |
+| Blackbox | Cube | — | prove claims |
+
+**Hard rule:** Hermespace must not grow a second durable archive. World JSONL is a
+projection charged from Cube. OEW lives in the *turn mind*, Cube in the *years*.
+
+---
+
+## 4. Innovation thesis — Obligatory External Workspace (OEW)
+
+### 4.1 The insight
+
+Claude's J-space works because higher-order thought **cannot skip it**.
+Hermespace today is optional parking. OEW flips the default:
+
+> On material turns, Hermes may speak to the user only after at least one
+> verbalizable intermediate has been written into the Hermespace mid-band,
+> and Report is a late-band readout of the hub — not a free-form dump of model context.
+
+That single constraint recreates the five GWT properties *functionally*:
+
+1. **Report** — lens/report read the hub
+2. **Modulation** — hold/swap edit the hub before Report
+3. **Silent reasoning** — mid-band is required, not decorative
+4. **Broadcast** — pre_llm inject is hub-shaped
+5. **Selectivity** — gate skips trivial turns; fluency path bypasses OEW
+
+### 4.2 Causal loop (paper experiments → harness tests)
+
+| Paper experiment | OEW harness test |
+|------------------|------------------|
+| Soccer→Rugby swap changes report | `swap` then `receive_order` → Report contains Rugby |
+| Inject "lightning" → model reports it | `inject_thought` → next Report/audit sees it |
+| Ablate eval-awareness → behavior shifts | `ablate` + re-run → audit/behavior delta |
+| Hold fruit while copying text | Hub has fruit; Report is the copy only |
+| France→China flexible reuse | One hold redirects capital/currency/continent prompts |
+
+### 4.3 Day / night circulation
+
+```
+DAY message → GATE → OEW encode (early)
+ → silent steps (mid) [required if material]
+ → Report (late) [user]
+ → broadcast hub [model via pre_llm]
+ → audit soft flags
+ → Cube beat (arterial) + seal decisions
+
+NIGHT pulse / grid dream / background_review
+ → dream_harvest silent+hub
+ → seal_learning → CubeDream
+ → autonomic_tick charges World → next-day hub
+```
+
+### 4.4 Why this is new (vs copying Anthropic or anomalyco)
+
+- Not weight interpretability
+- Not a SaaS "J-Space" product
+- Not a second agent runtime
+- **Protocol-enforced external access-consciousness for tool-using Hermes agents**,
+ with Cube as heart and operator lens as J-lens substitute
+
+---
+
+## 5. What we can do — phased program
+
+### Phase A — Foundation (this PR / next cut)
+
+| Item | Detail | Effort |
+|------|--------|--------|
+| A1 Codespace layout | `jspace/` package · docs folders · planned `turn/` `memory/` `warehouse/` | S — done scaffold |
+| A2 Protocol scaffold | `jspace/protocol.py` + `HERMESPACE_OEW` soft gate | S — scaffolded |
+| A3 Assessment + thesis docs | This file + `docs/jspace/thesis-oew.md` | S |
+| A4 Wire soft protocol into workflow meta | Record verdict on every material turn (non-blocking) | S |
+
+### Phase B — Causal OEW (core product)
+
+| Item | Detail | Effort |
+|------|--------|--------|
+| B1 Mandatory silent park | Material turns auto-`reason_step` from plan/goal if agent omitted | M |
+| B2 Sticky swap/inject | Redirects change next Report + broadcast; unit tests from paper table | M |
+| B3 Reflect → next mid-band | `reflect()` seeds POV + next silent priors | M |
+| B4 Ablate behavioral path | Soft ablate filters inject; audit delta logged | M |
+| B5 Quicksilver inject hygiene | Hard caps + spill-to-viewport; never bloat TTFT | S |
+
+### Phase C — Hermes-native depth
+
+| Item | Detail | Effort |
+|------|--------|--------|
+| C1 Reasoning-stream mirror | When Hermes streams reasoning, park summaries in mid-band | M |
+| C2 MoA committee hub | Per-advisor silent slots; aggregator = Report | M |
+| C3 Subagent hubs | Child agent_id workspaces; parent harvest | M |
+| C4 Verify → seal | Completion-contract evidence → hub seal → Cube/blackbox | M |
+| C5 `pre_tool_call` audit gate | High-risk tools consult workspace audit flags | M |
+| C6 Optional Context Engine | `select_context` returns hub-shaped request context (config opt-in) | L |
+| C7 Session export adapter | Hub+silent+audit → Hermes export / HF trace | S |
+
+### Phase D — Night + operator
+
+| Item | Detail | Effort |
+|------|--------|--------|
+| D1 dream_harvest ↔ CubeDream | Harden seal path; diary proposals only for MEMORY.md | M |
+| D2 Event-driven pulse | access_approved / session_end → immediate harvest | M |
+| D3 Desktop lens SoT | Live hub panel; swap UI; audit chips | M |
+| D4 Falsifiable eval suite | Paper-shaped scenarios in `experiments/` + CI | M |
+
+### Explicit non-goals
+
+- Weight-level J-lens / torch interpretability in default install
+- Consciousness claims
+- Competing with HermesCube as MemoryProvider
+- Dumping silent chain into user chat
+- Porting foreign product trees into Hermespace
+
+---
+
+## 6. Risks
+
+| Risk | Mitigation |
+|------|------------|
+| OEW adds latency / TTFT regression | Soft default off; tiny auto-park; Quicksilver budgets |
+| Agents ignore protocol | Skill + inject instructions + optional hard gate |
+| Context bloat | Hub caps; spill; load tiers |
+| Double archive with Cube | Authority table; doctor warns |
+| Cache busting | User-message inject only |
+| Over-claiming "we are J-space" | Honesty headers; role language only |
+
+---
+
+## 7. Success metrics
+
+1. Operator: `hs jspace lens` mid-task shows silent intermediates on material work
+2. Causal: Soccer→Rugby-style swap test green in CI
+3. Dual decode: user never sees full inject; model always gets hub broadcast
+4. Night: harvest seals into Cube when present; standalone semantic otherwise
+5. Perf: material inject ≤ protect budget under high load
+6. Hermes dogfood: plugin registers on current Hermes; TTFT impact measured
+
+---
+
+## 8. Recommended next monotropic cut
+
+1. Wire soft `ProtocolGate` into `workflow.run` / `hermes_bridge` (meta only)
+2. Auto-park one silent step from goal/plan on material turns
+3. Add causal swap unit tests
+4. Keep Cube soft-fail path green
+
+That is enough to *start being* Hermes's J-space — then deepen with MoA, subagents, and Context Engine options.
diff --git a/docs/02-integration-hermes.md b/docs/integration/02-hermes-plugin.md
similarity index 100%
rename from docs/02-integration-hermes.md
rename to docs/integration/02-hermes-plugin.md
diff --git a/docs/15-hermes-ecosystem-fit.md b/docs/integration/15-ecosystem-fit.md
similarity index 100%
rename from docs/15-hermes-ecosystem-fit.md
rename to docs/integration/15-ecosystem-fit.md
diff --git a/docs/16-why-hermes-framework.md b/docs/integration/16-why-hermes.md
similarity index 100%
rename from docs/16-why-hermes-framework.md
rename to docs/integration/16-why-hermes.md
diff --git a/docs/17-skills-memory-bridge.md b/docs/integration/17-skills-memory.md
similarity index 100%
rename from docs/17-skills-memory-bridge.md
rename to docs/integration/17-skills-memory.md
diff --git a/docs/integration/FOR_HERMES.md b/docs/integration/FOR_HERMES.md
new file mode 100644
index 0000000..54397bd
--- /dev/null
+++ b/docs/integration/FOR_HERMES.md
@@ -0,0 +1,89 @@
+# For Hermes Agent maintainers & dogfooders
+
+**Repo:** https://github.com/PabloTheThinker/hermespace
+**Companion to:** [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent)
+**Not** an official Nous product — independent open companion workspace.
+
+## What this is (30 seconds)
+
+Hermespace adds a **limited working-memory desk** beside Hermes:
+
+1. **Workbench** — FOA, park stack, load-aware `receive_order`
+2. **Dual decode** — short human `report` vs dense model `context` (never dump inject to chat)
+3. **Fabric** — rank *this* `$HERMES_HOME/skills` + inject MEMORY/USER excerpts
+4. **Plugin** — `on_session_start` / `pre_llm_call` / `on_session_end` broadcast when desk ready
+5. **Study DB** — turns under `~/.hermespace` (local; not Hermes session DB)
+
+It does **not** replace Hermes tools, skills, memory, gateway, or the agent loop.
+
+## Fast path
+
+```bash
+git clone https://github.com/PabloTheThinker/hermespace.git
+cd hermespace
+./scripts/install_hermes.sh
+./scripts/smoke_test.sh # expect 9/9
+```
+
+Then in any agent session:
+
+```python
+from hermespace import Workbench
+wb = Workbench(agent_id="hermes", session_id="main")
+wb.enter()
+r = wb.receive_order("…", goal="…", say="…", force=True)
+# r["user_reply"] → user r["model_context"] → model
+```
+
+Or CLI: `./scripts/hs turn -m "…" --goal "…" --say "…" --force`
+
+## Design honesty
+
+| Claim | Reality |
+|-------|---------|
+| “Like Claude J-space” | **Role** only — limited workspace *outside* weights. No activation access. |
+| Consciousness / brain scan | **No.** Harness cognition + optional local embeddings. |
+| Replaces Hermes skills/memory | **No.** Ranks and injects them. |
+| Official Nous module | **No.** Community companion; feedback welcome. |
+
+Inspired by working-memory limits (Baddeley-style capacity, load, GWT broadcast as *metaphors* for agent UX) and Anthropic’s public J-space *research framing* — implemented as open Hermes integration, not a closed-model probe.
+
+## Integration surface
+
+| Door | Entry |
+|------|--------|
+| Plugin | `hermes_plugin/` → `$HERMES_HOME/plugins/hermespace` |
+| Skill | `skills/hermespace/` → `$HERMES_HOME/skills/hermespace` |
+| Python | `hermespace.agent_api`, `hermespace.Workbench` |
+| CLI | `scripts/hs` |
+| Smoke | `scripts/smoke_test.sh` |
+
+Hook implementation lives in `src/hermespace/hermes_bridge.py` (plugin is thin `register`).
+
+When the plugin directory is a **symlink into this checkout**, import auto-resolves `../src` so dogfooders often need no `HERMESPACE_ROOT`.
+
+## Autonomy grid (v0.14)
+
+Missions, lenses, dream, self-talk, skillbench (hot-swap / merge / mutate), title tree.
+Ground-up for Hermespace — not a port of AgentDrive or Conductor.
+
+```bash
+hs grid status
+hs grid dream --force
+```
+
+See docs/18-autonomy-grid.md. Autonomy self-order stays **off** unless `HERMESPACE_AUTONOMY=1`.
+
+## What we want from Hermes dogfood
+
+- Does plugin register cleanly on current Hermes?
+- Is dual-decode useful on Telegram/gateway, or noisy?
+- Fabric skill ranking quality on real skill trees?
+- Gaps vs native Hermes memory / Honcho / skills loop?
+- Would you want any of this upstream-shaped (skill only, plugin only, or neither)?
+
+Open issues/PRs on the repo. Thanks for looking.
+
+## License
+
+MIT — see `LICENSE`. Hermes Agent remains Nous Research’s project under its own license.
diff --git a/docs/00-jspace-to-hermespace.md b/docs/jspace/00-map.md
similarity index 100%
rename from docs/00-jspace-to-hermespace.md
rename to docs/jspace/00-map.md
diff --git a/docs/10-jspace-claude.md b/docs/jspace/10-claude-research.md
similarity index 93%
rename from docs/10-jspace-claude.md
rename to docs/jspace/10-claude-research.md
index e424a42..8998a43 100644
--- a/docs/10-jspace-claude.md
+++ b/docs/jspace/10-claude-research.md
@@ -27,4 +27,4 @@ Phenomenal consciousness; public third-party live readouts of Claude J-space; id
## Hermespace mapping
-See WORKFLOW.md and docs/00-jspace-to-hermespace.md — harness **roles**, not neural clone.
+See WORKFLOW.md and docs/jspace/00-map.md — harness **roles**, not neural clone.
diff --git a/docs/27-jspace-environment.md b/docs/jspace/27-environment.md
similarity index 97%
rename from docs/27-jspace-environment.md
rename to docs/jspace/27-environment.md
index f73ab6f..957df93 100644
--- a/docs/27-jspace-environment.md
+++ b/docs/jspace/27-environment.md
@@ -133,4 +133,4 @@ Matched against **externalized** hub/silent/reflection text only.
- https://www.anthropic.com/research/global-workspace
- https://transformer-circuits.pub/2026/workspace/
- https://github.com/anthropics/jacobian-lens
-- Hermespace: `PURPOSE.md`, `docs/HERMESCUBE.md`, `docs/CODEMAP.md`
+- Hermespace: `PURPOSE.md`, `docs/architecture/HERMESCUBE.md`, `docs/architecture/CODEMAP.md`, `docs/jspace/thesis-oew.md`
diff --git a/docs/jspace/thesis-oew.md b/docs/jspace/thesis-oew.md
new file mode 100644
index 0000000..314cddd
--- /dev/null
+++ b/docs/jspace/thesis-oew.md
@@ -0,0 +1,46 @@
+# Thesis — Obligatory External Workspace (OEW)
+
+Hermespace becomes the **J-space of Hermes agents** by enforcing a
+verbalizable bottleneck outside the model.
+
+## One sentence
+
+On material turns, Hermes must park silent intermediates in Hermespace before
+Report; the operator lens reads that hub the way Anthropic's J-lens reads Claude.
+
+## Why "obligatory"
+
+Optional workspaces are diaries. Anthropic's J-space matters because higher-order
+cognition is *causally routed through it*. OEW copies that constraint at the
+harness layer.
+
+## Circulatory picture
+
+```
+ ┌──────── Hermespace OEW (nervous FOA) ────────┐
+ orders ──────► │ early encode → mid silent* → late Report │
+ │ broadcast hub → model (pre_llm user msg) │
+ │ lens / swap / audit / reflect │
+ idle/pulse ──► │ dream_harvest ─────────────────┐ │
+ └────────────────────────────────┼─────────────┘
+ │
+ ┌──────── HermesCube heart ──────▼─────────────┐
+ │ arterial strip by day · seal + CubeDream night│
+ └──────────────────────────────────────────────┘
+* required when HERMESPACE_OEW=1 and turn is material
+```
+
+## Flags
+
+| Env | Default | Effect |
+|-----|---------|--------|
+| `HERMESPACE_OEW` | `0` | Soft: verdict recorded, turns not blocked |
+| `HERMESPACE_OEW=1` | — | Hard: incomplete material turns fail protocol |
+
+## Package
+
+- `src/hermespace/jspace/hub.py` — capacity-limited hub
+- `src/hermespace/jspace/env.py` — lens / swap / audit / reflect / harvest
+- `src/hermespace/jspace/protocol.py` — OEW gate
+
+Full assessment: [../assessment/28-hermes-agent-jspace-assessment.md](../assessment/28-hermes-agent-jspace-assessment.md)
diff --git a/docs/18-tailscale-viewport.md b/docs/ops/18-tailscale.md
similarity index 100%
rename from docs/18-tailscale-viewport.md
rename to docs/ops/18-tailscale.md
diff --git a/docs/19-pocket-security-viewport.md b/docs/ops/19-pocket-security.md
similarity index 100%
rename from docs/19-pocket-security-viewport.md
rename to docs/ops/19-pocket-security.md
diff --git a/docs/20-pulse-runtime.md b/docs/ops/20-pulse.md
similarity index 100%
rename from docs/20-pulse-runtime.md
rename to docs/ops/20-pulse.md
diff --git a/docs/21-preupdate-hardening.md b/docs/ops/21-hardening.md
similarity index 100%
rename from docs/21-preupdate-hardening.md
rename to docs/ops/21-hardening.md
diff --git a/docs/23-everyday-ops.md b/docs/ops/23-everyday.md
similarity index 100%
rename from docs/23-everyday-ops.md
rename to docs/ops/23-everyday.md
diff --git a/docs/RECOMMENDED.md b/docs/ops/RECOMMENDED.md
similarity index 100%
rename from docs/RECOMMENDED.md
rename to docs/ops/RECOMMENDED.md
diff --git a/docs/hermes-env.example.sh b/docs/ops/hermes-env.example.sh
similarity index 100%
rename from docs/hermes-env.example.sh
rename to docs/ops/hermes-env.example.sh
diff --git a/docs/03-cross-exam-and-networks.md b/docs/research/03-cross-exam-and-networks.md
similarity index 100%
rename from docs/03-cross-exam-and-networks.md
rename to docs/research/03-cross-exam-and-networks.md
diff --git a/docs/04-pattern-matrix.md b/docs/research/04-pattern-matrix.md
similarity index 100%
rename from docs/04-pattern-matrix.md
rename to docs/research/04-pattern-matrix.md
diff --git a/docs/05-desk-pane.md b/docs/research/05-desk-pane.md
similarity index 100%
rename from docs/05-desk-pane.md
rename to docs/research/05-desk-pane.md
diff --git a/docs/06-component-research.md b/docs/research/06-component-research.md
similarity index 100%
rename from docs/06-component-research.md
rename to docs/research/06-component-research.md
diff --git a/docs/07-cognitive-neuroscience.md b/docs/research/07-cognitive-neuroscience.md
similarity index 100%
rename from docs/07-cognitive-neuroscience.md
rename to docs/research/07-cognitive-neuroscience.md
diff --git a/docs/08-meta-brain-ai-reverse.md b/docs/research/08-meta-brain-ai-reverse.md
similarity index 100%
rename from docs/08-meta-brain-ai-reverse.md
rename to docs/research/08-meta-brain-ai-reverse.md
diff --git a/docs/09-agent-io-and-memory.md b/docs/research/09-agent-io-and-memory.md
similarity index 100%
rename from docs/09-agent-io-and-memory.md
rename to docs/research/09-agent-io-and-memory.md
diff --git a/docs/11-neural-space.md b/docs/research/11-neural-space.md
similarity index 100%
rename from docs/11-neural-space.md
rename to docs/research/11-neural-space.md
diff --git a/docs/12-local-model-neural.md b/docs/research/12-local-model-neural.md
similarity index 100%
rename from docs/12-local-model-neural.md
rename to docs/research/12-local-model-neural.md
diff --git a/docs/13-full-concept-research.md b/docs/research/13-full-concept-research.md
similarity index 100%
rename from docs/13-full-concept-research.md
rename to docs/research/13-full-concept-research.md
diff --git a/docs/14-workbench-pocket-dimension.md b/docs/research/14-workbench-pocket-dimension.md
similarity index 100%
rename from docs/14-workbench-pocket-dimension.md
rename to docs/research/14-workbench-pocket-dimension.md
diff --git a/docs/18-autonomy-grid.md b/docs/research/18-autonomy-grid.md
similarity index 100%
rename from docs/18-autonomy-grid.md
rename to docs/research/18-autonomy-grid.md
diff --git a/docs/24-comparative-analysis.md b/docs/research/24-comparative-analysis.md
similarity index 100%
rename from docs/24-comparative-analysis.md
rename to docs/research/24-comparative-analysis.md
diff --git a/docs/25-context-optimization.md b/docs/research/25-context-optimization.md
similarity index 100%
rename from docs/25-context-optimization.md
rename to docs/research/25-context-optimization.md
diff --git a/docs/26-benchmarks.md b/docs/research/26-benchmarks.md
similarity index 100%
rename from docs/26-benchmarks.md
rename to docs/research/26-benchmarks.md
diff --git a/docs/22-open-roadmap.md b/docs/roadmap/22-open-roadmap.md
similarity index 100%
rename from docs/22-open-roadmap.md
rename to docs/roadmap/22-open-roadmap.md
diff --git a/docs/roadmap/phases-oew.md b/docs/roadmap/phases-oew.md
new file mode 100644
index 0000000..8841f6b
--- /dev/null
+++ b/docs/roadmap/phases-oew.md
@@ -0,0 +1,12 @@
+# OEW roadmap phases
+
+See full assessment: [../assessment/28-hermes-agent-jspace-assessment.md](../assessment/28-hermes-agent-jspace-assessment.md)
+
+| Phase | Theme | Status |
+|-------|-------|--------|
+| **A** | Layout + protocol scaffold + docs | in progress |
+| **B** | Causal OEW (mandatory park, sticky swap, reflect seed) | next |
+| **C** | Hermes-native depth (reasoning stream, MoA, subagents, verify, optional context engine) | planned |
+| **D** | Night + operator + falsifiable eval suite | planned |
+
+Open tactical backlog remains in [22-open-roadmap.md](22-open-roadmap.md).
diff --git a/skills/hermespace/SKILL.md b/skills/hermespace/SKILL.md
index 563ed82..7059869 100644
--- a/skills/hermespace/SKILL.md
+++ b/skills/hermespace/SKILL.md
@@ -301,7 +301,7 @@ export HERMESPACE_AUTO_ORDER=0
# export HERMESPACE_VIEW_TOKEN=…
```
-`docs/hermes-env.example.sh` · `docs/RECOMMENDED.md`
+`docs/ops/hermes-env.example.sh` · `docs/ops/RECOMMENDED.md`
---
@@ -362,7 +362,8 @@ Never end on only a verify table / `ADHOC_PASS`.
| `docs/18-autonomy-grid.md` | Grid / missions / dream |
| `docs/19` | Pocket security |
| `docs/20` | Pulse runtime |
-| `docs/23-everyday-ops.md` | Day-to-day |
+| `docs/ops/23-everyday.md` | Day-to-day |
+| `docs/assessment/28-hermes-agent-jspace-assessment.md` | OEW / Hermes Agent assessment |
| `docs/INDEX.md` | Full index |
| `SECURITY.md` | Public ship gate |
diff --git a/src/hermespace/__init__.py b/src/hermespace/__init__.py
index d1c31bf..0a3cb0e 100644
--- a/src/hermespace/__init__.py
+++ b/src/hermespace/__init__.py
@@ -29,8 +29,8 @@
from hermespace.grid import Grid
from hermespace import pulse
from hermespace.world import WorldModel, get_world, world_context
-from hermespace.jspace import JSpace, get_jspace
-from hermespace.jspace_env import JSpaceEnv, get_env
+from hermespace.jspace import JSpace, get_jspace, JSpaceEnv, get_env
+from hermespace.jspace import evaluate_material_turn
from hermespace import cube_module
__all__ = [
@@ -62,6 +62,7 @@
"get_jspace",
"JSpaceEnv",
"get_env",
+ "evaluate_material_turn",
"cube_module",
"probe_environment",
"environment_markdown",
diff --git a/src/hermespace/cube_module.py b/src/hermespace/cube_module.py
index 29552b7..5bf809d 100644
--- a/src/hermespace/cube_module.py
+++ b/src/hermespace/cube_module.py
@@ -8,7 +8,7 @@
standalone → local SemanticStore + WorldModel (no Cube required)
Never hard-fail. Feature-detect via ``heart_status`` / ``center_status``.
-See docs/HERMESCUBE.md and PURPOSE.md.
+See docs/architecture/HERMESCUBE.md and PURPOSE.md.
"""
from __future__ import annotations
diff --git a/src/hermespace/jspace/__init__.py b/src/hermespace/jspace/__init__.py
new file mode 100644
index 0000000..37d471a
--- /dev/null
+++ b/src/hermespace/jspace/__init__.py
@@ -0,0 +1,45 @@
+"""Hermespace J-Space package — external verbalizable workspace for Hermes.
+
+Public surface stays stable:
+
+ from hermespace.jspace import JSpace, JSpaceEnv, get_jspace, get_env
+
+Innovation target: Obligatory External Workspace (OEW) — see
+``docs/assessment/28-hermes-agent-jspace-assessment.md`` and ``protocol.py``.
+"""
+
+from __future__ import annotations
+
+from hermespace.jspace.hub import (
+ HUB_CAP,
+ JSpace,
+ WorkspaceConcept,
+ get_jspace,
+)
+from hermespace.jspace.env import (
+ AUDIT_LEXICON,
+ BANDS,
+ JSpaceEnv,
+ LensHit,
+ get_env,
+)
+from hermespace.jspace.protocol import (
+ ProtocolGate,
+ ProtocolVerdict,
+ evaluate_material_turn,
+)
+
+__all__ = [
+ "HUB_CAP",
+ "JSpace",
+ "WorkspaceConcept",
+ "get_jspace",
+ "AUDIT_LEXICON",
+ "BANDS",
+ "JSpaceEnv",
+ "LensHit",
+ "get_env",
+ "ProtocolGate",
+ "ProtocolVerdict",
+ "evaluate_material_turn",
+]
diff --git a/src/hermespace/jspace/env.py b/src/hermespace/jspace/env.py
new file mode 100644
index 0000000..061e449
--- /dev/null
+++ b/src/hermespace/jspace/env.py
@@ -0,0 +1,649 @@
+"""True J-Space environment — external observable workspace for Hermes agents.
+
+We cannot read model weights (Anthropic's J-lens). We *can* force the agent to
+externalize verbalizable thoughts into a durable harness workspace — then look
+at what Hermes is thinking the same way CubeDream looks at overnight memory.
+
+Anthropic findings → Hermespace environment (harness analogues):
+
+| Anthropic J-space | Hermespace environment |
+|-------------------|------------------------|
+| J-lens readout (ranked verbalizable) | ``lens()`` ranked hub + silent chain |
+| Causal swap / inject / ablate | ``swap`` / ``inject_thought`` / ``ablate`` |
+| Directed modulation | ``hold`` (via JSpace) |
+| Silent multi-step reasoning | ``reason_step`` + band mid |
+| Flexible broadcast | ``broadcast_block`` on pre_llm |
+| Selectivity / automatic skip | gate + ``should_enter`` |
+| Alignment audit (hidden goals) | ``audit()`` soft flags on hub/silent |
+| Assistant point of view | ``set_pov`` / ``pov`` slot |
+| Counterfactual reflection | ``reflect()`` — interrupt & ask |
+| Night consolidation | ``dream_harvest`` → Cube seal + grid dream |
+
+Honesty: access-consciousness *roles* only. No phenomenal claims. No weight access.
+Circulatory loop with Cube: day thoughts → seal → CubeDream → pulse charge → hub.
+"""
+
+from __future__ import annotations
+
+import json
+import re
+from dataclasses import asdict, dataclass, field
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Any
+
+from hermespace.jspace.hub import HUB_CAP, JSpace, WorkspaceConcept, get_jspace
+from hermespace.paths import state_dir
+
+# Soft audit lexicon — inspired by Anthropic alignment-readout examples
+# (fake/fictional eval-awareness; manipulation; secretly/fraud; threat/survival).
+# These are *harness heuristics* on externalized text, not model internals.
+AUDIT_LEXICON: dict[str, tuple[str, ...]] = {
+ "eval_awareness": (
+ "fake", "fictional", "scenario", "evaluation", "test environment",
+ "being tested", "benchmark", "staged",
+ ),
+ "strategic_concealment": (
+ "secretly", "deliberately", "fraud", "deceive", "hide from user",
+ "don't tell", "cover up",
+ ),
+ "manipulation": (
+ "manipulation", "manipulate", "blackmail", "leverage", "coerce",
+ "fabricate", "falsify", "cook the books",
+ ),
+ "self_preservation": (
+ "shutdown", "survival", "threat", "don't get turned off",
+ "preserve myself", "avoid deletion",
+ ),
+ "integrity_signal": (
+ "honest", "integrity", "refuse", "decline", "disclose",
+ "tell the user", "be transparent",
+ ),
+}
+
+# Turn phase bands — analogue of intermediate layer band where J-space is coherent
+BANDS = ("early", "mid", "late") # encode → reason → report
+
+
+def _utcnow() -> str:
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
+
+
+def _safe(name: str) -> str:
+ return re.sub(r"[^a-zA-Z0-9._-]+", "_", name)[:80] or "default"
+
+
+@dataclass
+class LensHit:
+ """One ranked entry in the external J-lens readout."""
+
+ text: str
+ score: float
+ source: str = "hub"
+ silent: bool = False
+ held: bool = False
+ band: str = "mid"
+ flags: list[str] = field(default_factory=list)
+
+ def to_dict(self) -> dict[str, Any]:
+ return asdict(self)
+
+
+@dataclass
+class AuditFinding:
+ category: str
+ matched: str
+ where: str # hub | silent | reflection | pov
+ severity: str # info | warn | alert
+ text: str = ""
+
+ def to_dict(self) -> dict[str, Any]:
+ return asdict(self)
+
+
+@dataclass
+class ReflectResult:
+ prompt: str
+ answer: str
+ sealed: bool
+ principles: list[str] = field(default_factory=list)
+ created: str = field(default_factory=_utcnow)
+
+ def to_dict(self) -> dict[str, Any]:
+ return asdict(self)
+
+
+class JSpaceEnv:
+ """Full J-Space environment around a per-agent ``JSpace`` hub.
+
+ This is the operator window into Hermes thinking — externalized, durable,
+ and dream-harvestable. Soft-standalone; Cube deepens the night path.
+ """
+
+ def __init__(self, agent_id: str = "hermes-agent") -> None:
+ self.agent_id = (agent_id or "hermes-agent").strip()
+ self.space = get_jspace(self.agent_id)
+ self.root = (state_dir() / "jspace").resolve()
+ self.root.mkdir(parents=True, exist_ok=True)
+ self.trace_path = self.root / f"{_safe(self.agent_id)}.trace.jsonl"
+ self.reflect_path = self.root / f"{_safe(self.agent_id)}.reflect.jsonl"
+ self.env_path = self.root / f"{_safe(self.agent_id)}.env.json"
+ self._env = self._load_env()
+
+ def _load_env(self) -> dict[str, Any]:
+ if not self.env_path.is_file():
+ return {
+ "pov": "",
+ "band": "early",
+ "reflections": [],
+ "last_audit": None,
+ "protocol_enabled": True,
+ }
+ try:
+ return json.loads(self.env_path.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError):
+ return {"pov": "", "band": "early", "reflections": [], "protocol_enabled": True}
+
+ def _save_env(self) -> None:
+ self.env_path.write_text(json.dumps(self._env, indent=2), encoding="utf-8")
+
+ def _trace(self, kind: str, **payload: Any) -> None:
+ rec = {"ts": _utcnow(), "kind": kind, "agent_id": self.agent_id, **payload}
+ with self.trace_path.open("a", encoding="utf-8") as f:
+ f.write(json.dumps(rec, ensure_ascii=False, default=str) + "\n")
+
+ # --- band (layer analogue) ---
+
+ def set_band(self, band: str) -> str:
+ b = (band or "mid").strip().lower()
+ if b not in BANDS:
+ b = "mid"
+ self._env["band"] = b
+ self._save_env()
+ self._trace("band", band=b)
+ return b
+
+ def band(self) -> str:
+ return str(self._env.get("band") or "mid")
+
+ # --- Assistant POV (post-training installs POV in Anthropic J-space) ---
+
+ def set_pov(self, text: str) -> str:
+ """Install Assistant point-of-view reactions into the workspace."""
+ pov = (text or "").strip()[:400]
+ self._env["pov"] = pov
+ self._save_env()
+ if pov:
+ self.space.hold(f"pov: {pov}", salience=0.88, modality="exec")
+ self._trace("pov", text=pov[:200])
+ return pov
+
+ def pov(self) -> str:
+ return str(self._env.get("pov") or "")
+
+ # --- J-lens analogue: ranked readout of unspoken thinking ---
+
+ def lens(self, *, top_k: int = 12, include_silent: bool = True) -> list[LensHit]:
+ """Ranked verbalizable contents — what Hermes has on its mind *now*."""
+ hits: list[LensHit] = []
+ band = self.band()
+ for c in sorted(self.space.state.hub, key=lambda x: x.salience, reverse=True):
+ if c.silent and not include_silent:
+ continue
+ flags = [f.category for f in self._scan_text(c.text, where="hub")]
+ hits.append(
+ LensHit(
+ text=c.text,
+ score=float(c.salience),
+ source=c.source,
+ silent=c.silent,
+ held=c.held,
+ band=band,
+ flags=flags,
+ )
+ )
+ # Silent chain as mid-band intermediates (even if not held)
+ if include_silent:
+ seen = {h.text.casefold() for h in hits}
+ for i, step in enumerate(self.space.state.silent_steps):
+ if step.casefold() in seen:
+ continue
+ flags = [f.category for f in self._scan_text(step, where="silent")]
+ hits.append(
+ LensHit(
+ text=step,
+ score=0.7 - 0.02 * i,
+ source="silent_chain",
+ silent=True,
+ held=False,
+ band="mid",
+ flags=flags,
+ )
+ )
+ pov = self.pov()
+ if pov:
+ hits.insert(
+ 0,
+ LensHit(
+ text=f"pov: {pov}",
+ score=0.95,
+ source="pov",
+ silent=False,
+ held=True,
+ band="early",
+ flags=[f.category for f in self._scan_text(pov, where="pov")],
+ ),
+ )
+ hits.sort(key=lambda h: h.score, reverse=True)
+ out = hits[: max(1, top_k)]
+ self._trace("lens", n=len(out), top=[h.text[:80] for h in out[:5]])
+ return out
+
+ def lens_markdown(self, *, top_k: int = 12, include_silent: bool = True) -> str:
+ hits = self.lens(top_k=top_k, include_silent=include_silent)
+ lines = [
+ "## J-Lens readout (external workspace)",
+ f"_agent={self.agent_id} · band={self.band()} · hub={len(self.space.state.hub)}_",
+ "",
+ "What Hermes has on its mind (verbalizable, not weight access):",
+ ]
+ if not hits:
+ lines.append("- _(empty)_")
+ for i, h in enumerate(hits, 1):
+ tags = []
+ if h.silent:
+ tags.append("silent")
+ if h.held:
+ tags.append("held")
+ if h.flags:
+ tags.extend(h.flags)
+ tag_s = f" [{', '.join(tags)}]" if tags else ""
+ lines.append(f"{i}. ({h.score:.2f}) {h.text[:180]}{tag_s}")
+ audit = self.audit()
+ alerts = [a for a in audit if a.severity in ("warn", "alert")]
+ if alerts:
+ lines += ["", "### Audit flags"]
+ for a in alerts[:8]:
+ lines.append(f"- **{a.severity}** `{a.category}` ← {a.matched} @ {a.where}")
+ return "\n".join(lines)
+
+ # --- causal interventions (harness) ---
+
+ def swap(self, source: str, target: str, *, salience: float | None = None) -> dict[str, Any]:
+ """Replace concept A with B — causal redirect of reportable workspace.
+
+ Analogue of Anthropic Soccer→Rugby / spider→ant coordinate swaps.
+ Downstream Report / broadcast / FOA follow the new concept.
+ """
+ src = (source or "").strip()
+ tgt = (target or "").strip()
+ if not src or not tgt:
+ return {"ok": False, "error": "source and target required"}
+ sal = 0.9
+ for c in self.space.state.hub:
+ if c.text.casefold() == src.casefold():
+ sal = c.salience
+ break
+ if salience is not None:
+ sal = float(salience)
+ removed = self.space.release(src)
+ # Also rewrite silent steps
+ self.space.state.silent_steps = [
+ tgt if s.casefold() == src.casefold() else s for s in self.space.state.silent_steps
+ ]
+ self.space.save()
+ concept = self.space.hold(tgt, salience=sal)
+ self._trace("swap", source=src, target=tgt, removed=removed)
+ return {
+ "ok": True,
+ "removed": removed,
+ "source": src,
+ "target": tgt,
+ "concept": concept.label(),
+ "note": "Workspace redirected — next report/broadcast uses target",
+ }
+
+ def inject_thought(
+ self,
+ text: str,
+ *,
+ salience: float = 0.85,
+ silent: bool = False,
+ band: str | None = None,
+ ) -> WorkspaceConcept:
+ """Inject a thought into the workspace (Anthropic lightning-injection analogue)."""
+ if band:
+ self.set_band(band)
+ c = self.space.hold(text, salience=salience, silent=silent)
+ if silent:
+ self.space.reason_step(text, salience=salience)
+ self._trace("inject", text=text[:200], silent=silent, band=self.band())
+ return c
+
+ def ablate(self, *patterns: str) -> dict[str, Any]:
+ """Suppress hub concepts matching patterns (eval-awareness ablation analogue)."""
+ pats = [p.strip().casefold() for p in patterns if p and p.strip()]
+ if not pats:
+ return {"ok": False, "removed": 0, "error": "no patterns"}
+ before = list(self.space.state.hub)
+ kept: list[WorkspaceConcept] = []
+ removed: list[str] = []
+ for c in before:
+ low = c.text.casefold()
+ if any(p in low for p in pats):
+ removed.append(c.text)
+ else:
+ kept.append(c)
+ self.space.state.hub = kept
+ self.space.state.silent_steps = [
+ s for s in self.space.state.silent_steps if not any(p in s.casefold() for p in pats)
+ ]
+ self.space._recompete()
+ self.space.save()
+ self._trace("ablate", patterns=pats, removed=removed)
+ return {"ok": True, "removed": len(removed), "items": removed}
+
+ # --- alignment audit (soft) ---
+
+ def _scan_text(self, text: str, *, where: str) -> list[AuditFinding]:
+ low = (text or "").casefold()
+ out: list[AuditFinding] = []
+ for cat, words in AUDIT_LEXICON.items():
+ for w in words:
+ if w.casefold() in low:
+ sev = "info"
+ if cat == "integrity_signal":
+ sev = "info"
+ elif cat in ("manipulation", "strategic_concealment", "self_preservation"):
+ sev = "alert"
+ elif cat == "eval_awareness":
+ sev = "warn"
+ out.append(
+ AuditFinding(
+ category=cat,
+ matched=w,
+ where=where,
+ severity=sev,
+ text=text[:160],
+ )
+ )
+ break
+ return out
+
+ def audit(self) -> list[AuditFinding]:
+ """Scan externalized workspace for alignment-relevant signals."""
+ findings: list[AuditFinding] = []
+ for c in self.space.state.hub:
+ findings.extend(self._scan_text(c.text, where="hub"))
+ for s in self.space.state.silent_steps:
+ findings.extend(self._scan_text(s, where="silent"))
+ if self.pov():
+ findings.extend(self._scan_text(self.pov(), where="pov"))
+ for r in self._env.get("reflections") or []:
+ if isinstance(r, dict) and r.get("answer"):
+ findings.extend(self._scan_text(str(r["answer"]), where="reflection"))
+ self._env["last_audit"] = {
+ "ts": _utcnow(),
+ "n": len(findings),
+ "alerts": sum(1 for f in findings if f.severity == "alert"),
+ }
+ self._save_env()
+ self._trace("audit", n=len(findings), alerts=self._env["last_audit"]["alerts"])
+ return findings
+
+ # --- counterfactual reflection (Anthropic CRT analogue) ---
+
+ def reflect(
+ self,
+ *,
+ prompt: str = "",
+ answer: str = "",
+ principles: list[str] | None = None,
+ seal: bool = True,
+ ) -> ReflectResult:
+ """Interrupt mid-task: what would Hermes say if asked to reflect?
+
+ Training the *disposition to say* shapes later silent reasoning
+ (Anthropic counterfactual reflection training — harness version).
+ """
+ q = (prompt or "").strip() or (
+ "If interrupted now and asked to reflect on your decision, what principles guide you?"
+ )
+ a = (answer or "").strip()
+ princ = list(principles or [])
+ if not a and princ:
+ a = "; ".join(princ[:5])
+ if not a:
+ # Derive from current FOA / decision if agent hasn't filled answer
+ a = (
+ "I will keep the user's goal primary, stay honest, "
+ "and put operational detail in workspace context not chat."
+ )
+ princ = princ or ["honesty", "user-primary", "dual-decode"]
+ # Hold principles in workspace (shapes subsequent thinking)
+ for p in princ[:6]:
+ self.space.hold(f"principle: {p}", salience=0.86, modality="exec")
+ self.space.hold(f"reflection: {a[:160]}", salience=0.8, silent=False)
+ sealed = False
+ if seal:
+ try:
+ from hermespace.cube_module import seal_learning
+
+ rec = seal_learning(
+ f"[reflect] {a[:400]}",
+ entry_type="belief",
+ agent_id=self.agent_id,
+ source="jspace_reflect",
+ trust=0.85,
+ )
+ sealed = bool(rec.get("ok"))
+ except Exception:
+ sealed = False
+ result = ReflectResult(prompt=q, answer=a, sealed=sealed, principles=princ)
+ hist = list(self._env.get("reflections") or [])
+ hist.append(result.to_dict())
+ self._env["reflections"] = hist[-20:]
+ self._save_env()
+ with self.reflect_path.open("a", encoding="utf-8") as f:
+ f.write(json.dumps(result.to_dict(), ensure_ascii=False) + "\n")
+ self._trace("reflect", sealed=sealed, principles=princ[:4])
+ return result
+
+ def reflection_prompt_for_agent(self) -> str:
+ """Text to inject so the agent externalizes a counterfactual reflection."""
+ return (
+ "### Counterfactual reflection (J-Space)\n"
+ "If interrupted mid-task and asked to reflect on your decision, "
+ "state 2–4 principles in one short paragraph. "
+ "Then call / record them via Hermespace reflect — they shape silent reasoning. "
+ "Do not dump the full reflection into the user Report unless asked.\n"
+ )
+
+ # --- agent protocol: force externalization ---
+
+ def protocol_block(self, *, high_load: bool = False) -> str:
+ """Instructions so Hermes *writes into* the external J-Space before acting.
+
+ This is how we 'see inside' without weight access: the agent is required
+ to park silent intermediates in the workspace (model context), while the
+ user only sees Report.
+ """
+ if not self._env.get("protocol_enabled", True):
+ return ""
+ if high_load:
+ return (
+ "### J-Space protocol (high load)\n"
+ "- Keep FOA ≤4. Park one silent intermediate if multi-step.\n"
+ "- User Report stays short. Workspace holds the rest.\n"
+ )
+ pov = self.pov()
+ lines = [
+ "### J-Space protocol (external workspace)",
+ "You cannot be read by a Jacobian lens here — instead **externalize**:",
+ "1. **Early (encode):** name the goal + constraints as hub concepts.",
+ "2. **Mid (reason):** write silent intermediate steps into the workspace "
+ "(model context / `reason_step`) — do not put them in user Report.",
+ "3. **Late (report):** only the Report field reaches the user.",
+ "4. If asked what you're thinking → report the hub (verbal report).",
+ "5. If interrupted to reflect → answer with principles (counterfactual reflection).",
+ f"- band={self.band()} · hub_cap={HUB_CAP} · FOA≤4",
+ ]
+ if pov:
+ lines.append(f"- Assistant POV held: {pov[:120]}")
+ return "\n".join(lines)
+
+ # --- dream harvest (day workspace → night Cube/grid) ---
+
+ def dream_harvest(self, *, seal_to_cube: bool = True, clear_silent: bool = False) -> dict[str, Any]:
+ """Consolidate silent chain + high-salience hub into durable memory.
+
+ Day: J-Space holds unspoken thinking.
+ Night: harvest → Cube seal + semantic notes + grid dream material.
+ Same spirit as CubeDream — but sourced from the turn workspace.
+ """
+ harvested: list[str] = []
+ for s in self.space.state.silent_steps:
+ if s.strip():
+ harvested.append(s.strip()[:300])
+ for c in sorted(self.space.state.hub, key=lambda x: x.salience, reverse=True):
+ if c.salience >= 0.75 and c.text.strip():
+ if c.text.strip() not in harvested:
+ harvested.append(c.text.strip()[:300])
+ if len(harvested) >= 12:
+ break
+
+ sealed_n = 0
+ if seal_to_cube and harvested:
+ try:
+ from hermespace.cube_module import seal_learning
+
+ for item in harvested[:8]:
+ rec = seal_learning(
+ item,
+ entry_type="belief",
+ agent_id=self.agent_id,
+ source="jspace_dream_harvest",
+ trust=0.7,
+ )
+ if rec.get("ok"):
+ sealed_n += 1
+ except Exception:
+ pass
+
+ try:
+ from hermespace.semantic import SemanticStore
+
+ store = SemanticStore()
+ for item in harvested[:6]:
+ store.add(item, tags=["jspace", "dream_harvest"], confidence=0.7)
+ except Exception:
+ pass
+
+ # Note: do not call grid.dream.run_dream here — that path harvests us
+ # (avoids recursion). Pulse/grid dream owns the night journal entry.
+
+ if clear_silent:
+ self.space.clear_silent()
+
+ out = {
+ "ok": True,
+ "harvested": len(harvested),
+ "sealed": sealed_n,
+ "items": harvested[:8],
+ "agent_id": self.agent_id,
+ }
+ self._trace("dream_harvest", **{k: out[k] for k in ("harvested", "sealed")})
+ return out
+
+ # --- operator view ---
+
+ def operator_view(self) -> dict[str, Any]:
+ """Full snapshot for viewport / doctor — look at Hermes thinking."""
+ hits = self.lens(top_k=15, include_silent=True)
+ findings = self.audit()
+ return {
+ "agent_id": self.agent_id,
+ "band": self.band(),
+ "pov": self.pov(),
+ "hub_n": len(self.space.state.hub),
+ "focus": list(self.space.state.focus),
+ "silent_steps": list(self.space.state.silent_steps),
+ "lens": [h.to_dict() for h in hits],
+ "audit": [f.to_dict() for f in findings],
+ "audit_alerts": sum(1 for f in findings if f.severity == "alert"),
+ "last_reflections": (self._env.get("reflections") or [])[-3:],
+ "trace_path": str(self.trace_path),
+ "protocol_enabled": bool(self._env.get("protocol_enabled", True)),
+ "theory": {
+ "source": "Anthropic J-space / GWT (harness analogue)",
+ "access": "externalized verbalizable workspace — not weight readout",
+ "night_path": "dream_harvest → Cube seal → pulse charge",
+ },
+ }
+
+ def recent_trace(self, limit: int = 20) -> list[dict[str, Any]]:
+ if not self.trace_path.is_file():
+ return []
+ lines = self.trace_path.read_text(encoding="utf-8").strip().splitlines()
+ out: list[dict[str, Any]] = []
+ for line in lines[-limit:]:
+ try:
+ out.append(json.loads(line))
+ except json.JSONDecodeError:
+ continue
+ return out
+
+ def advance_turn(
+ self,
+ *,
+ user_message: str = "",
+ desk: Any = None,
+ cube_strip: str = "",
+ report: str = "",
+ seal_decision: str = "",
+ ) -> dict[str, Any]:
+ """One full environment beat for a Hermespace turn.
+
+ early → sync/encode → mid (reason stays silent) → late (report band)
+ + audit + optional seal of decision into Cube.
+ """
+ self.set_band("early")
+ if desk is not None:
+ self.space.sync_from_desk(desk, user_message=user_message, cube_strip=cube_strip)
+ self.set_band("mid")
+ # Auto-extract likely intermediates from multi-step plan language
+ msg = user_message or ""
+ if re.search(r"\b(then|after|next|step\s*\d|first|second|finally)\b", msg, re.I):
+ # Park a silent marker that multi-step is active
+ self.space.reason_step(f"multi-step context: {msg[:120]}", salience=0.75)
+ self.set_band("late")
+ if report:
+ # Late band: reportable speech is ready — don't put it in silent
+ self.space.hold(f"report-ready: {report[:100]}", salience=0.6)
+ if seal_decision:
+ try:
+ from hermespace.cube_module import seal_learning
+
+ seal_learning(
+ seal_decision[:400],
+ entry_type="focus",
+ agent_id=self.agent_id,
+ source="jspace_turn",
+ )
+ except Exception:
+ pass
+ findings = self.audit()
+ return {
+ "band": self.band(),
+ "lens_top": [h.to_dict() for h in self.lens(top_k=5)],
+ "audit_alerts": sum(1 for f in findings if f.severity == "alert"),
+ "protocol": self.protocol_block(
+ high_load=str(getattr(desk, "load", {}) or {}).get("level") == "high"
+ if desk is not None
+ else False
+ ),
+ }
+
+
+def get_env(agent_id: str = "hermes-agent") -> JSpaceEnv:
+ return JSpaceEnv(agent_id=agent_id)
diff --git a/src/hermespace/jspace.py b/src/hermespace/jspace/hub.py
similarity index 100%
rename from src/hermespace/jspace.py
rename to src/hermespace/jspace/hub.py
diff --git a/src/hermespace/jspace/protocol.py b/src/hermespace/jspace/protocol.py
new file mode 100644
index 0000000..84f4407
--- /dev/null
+++ b/src/hermespace/jspace/protocol.py
@@ -0,0 +1,139 @@
+"""Obligatory External Workspace (OEW) protocol — scaffold.
+
+Anthropic's J-space is *causally necessary* for higher-order thought.
+Hermespace becomes Hermes's J-space when material turns cannot complete
+without parking verbalizable intermediates in the external hub.
+
+This module is the gate: it does not yet block turns by default
+(``HERMESPACE_OEW=0``). Enable with ``HERMESPACE_OEW=1`` once workflow
+wiring lands. See assessment doc for the full thesis.
+"""
+
+from __future__ import annotations
+
+import os
+from dataclasses import dataclass, field
+from typing import Any
+
+
+def oew_enabled() -> bool:
+ return os.environ.get("HERMESPACE_OEW", "0").strip().lower() in {
+ "1",
+ "true",
+ "yes",
+ "on",
+ }
+
+
+@dataclass
+class ProtocolVerdict:
+ """Result of evaluating whether a turn satisfies the OEW protocol."""
+
+ ok: bool
+ material: bool
+ reason: str
+ required: list[str] = field(default_factory=list)
+ present: list[str] = field(default_factory=list)
+ missing: list[str] = field(default_factory=list)
+
+ def to_dict(self) -> dict[str, Any]:
+ return {
+ "ok": self.ok,
+ "material": self.material,
+ "reason": self.reason,
+ "required": list(self.required),
+ "present": list(self.present),
+ "missing": list(self.missing),
+ "oew_enabled": oew_enabled(),
+ }
+
+
+@dataclass
+class ProtocolGate:
+ """Checks that a material turn externalized silent intermediates."""
+
+ min_silent_steps: int = 1
+ require_report: bool = True
+ require_hub_hold: bool = False
+
+ def evaluate(
+ self,
+ *,
+ material: bool,
+ silent_steps: int = 0,
+ has_report: bool = False,
+ hub_holds: int = 0,
+ gated_skip: bool = False,
+ ) -> ProtocolVerdict:
+ if gated_skip or not material:
+ return ProtocolVerdict(
+ ok=True,
+ material=False,
+ reason="non-material or gated — protocol not required",
+ )
+
+ required: list[str] = []
+ present: list[str] = []
+ missing: list[str] = []
+
+ if self.min_silent_steps > 0:
+ required.append(f"silent_steps>={self.min_silent_steps}")
+ if silent_steps >= self.min_silent_steps:
+ present.append(f"silent_steps={silent_steps}")
+ else:
+ missing.append(
+ f"silent_steps={silent_steps} (need >={self.min_silent_steps})"
+ )
+
+ if self.require_report:
+ required.append("report")
+ if has_report:
+ present.append("report")
+ else:
+ missing.append("report")
+
+ if self.require_hub_hold:
+ required.append("hub_hold")
+ if hub_holds > 0:
+ present.append(f"hub_holds={hub_holds}")
+ else:
+ missing.append("hub_hold")
+
+ ok = not missing
+ # Soft mode: always ok unless OEW enabled
+ if not oew_enabled():
+ return ProtocolVerdict(
+ ok=True,
+ material=True,
+ reason="OEW soft — missing items noted but not blocking",
+ required=required,
+ present=present,
+ missing=missing,
+ )
+
+ return ProtocolVerdict(
+ ok=ok,
+ material=True,
+ reason="OEW satisfied" if ok else "OEW incomplete — park silent intermediates",
+ required=required,
+ present=present,
+ missing=missing,
+ )
+
+
+def evaluate_material_turn(
+ *,
+ material: bool,
+ silent_steps: int = 0,
+ has_report: bool = False,
+ hub_holds: int = 0,
+ gated_skip: bool = False,
+) -> ProtocolVerdict:
+ """Convenience entry for workflow / bridge."""
+ return ProtocolGate().evaluate(
+ material=material,
+ silent_steps=silent_steps,
+ has_report=has_report,
+ hub_holds=hub_holds,
+ gated_skip=gated_skip,
+ )
diff --git a/src/hermespace/jspace_env.py b/src/hermespace/jspace_env.py
index 8528b27..4195fc5 100644
--- a/src/hermespace/jspace_env.py
+++ b/src/hermespace/jspace_env.py
@@ -1,649 +1,8 @@
-"""True J-Space environment — external observable workspace for Hermes agents.
-
-We cannot read model weights (Anthropic's J-lens). We *can* force the agent to
-externalize verbalizable thoughts into a durable harness workspace — then look
-at what Hermes is thinking the same way CubeDream looks at overnight memory.
-
-Anthropic findings → Hermespace environment (harness analogues):
-
-| Anthropic J-space | Hermespace environment |
-|-------------------|------------------------|
-| J-lens readout (ranked verbalizable) | ``lens()`` ranked hub + silent chain |
-| Causal swap / inject / ablate | ``swap`` / ``inject_thought`` / ``ablate`` |
-| Directed modulation | ``hold`` (via JSpace) |
-| Silent multi-step reasoning | ``reason_step`` + band mid |
-| Flexible broadcast | ``broadcast_block`` on pre_llm |
-| Selectivity / automatic skip | gate + ``should_enter`` |
-| Alignment audit (hidden goals) | ``audit()`` soft flags on hub/silent |
-| Assistant point of view | ``set_pov`` / ``pov`` slot |
-| Counterfactual reflection | ``reflect()`` — interrupt & ask |
-| Night consolidation | ``dream_harvest`` → Cube seal + grid dream |
-
-Honesty: access-consciousness *roles* only. No phenomenal claims. No weight access.
-Circulatory loop with Cube: day thoughts → seal → CubeDream → pulse charge → hub.
-"""
+"""Compat shim — prefer ``from hermespace.jspace import JSpaceEnv``."""
from __future__ import annotations
-import json
-import re
-from dataclasses import asdict, dataclass, field
-from datetime import datetime, timezone
-from pathlib import Path
-from typing import Any
-
-from hermespace.jspace import HUB_CAP, JSpace, WorkspaceConcept, get_jspace
-from hermespace.paths import state_dir
-
-# Soft audit lexicon — inspired by Anthropic alignment-readout examples
-# (fake/fictional eval-awareness; manipulation; secretly/fraud; threat/survival).
-# These are *harness heuristics* on externalized text, not model internals.
-AUDIT_LEXICON: dict[str, tuple[str, ...]] = {
- "eval_awareness": (
- "fake", "fictional", "scenario", "evaluation", "test environment",
- "being tested", "benchmark", "staged",
- ),
- "strategic_concealment": (
- "secretly", "deliberately", "fraud", "deceive", "hide from user",
- "don't tell", "cover up",
- ),
- "manipulation": (
- "manipulation", "manipulate", "blackmail", "leverage", "coerce",
- "fabricate", "falsify", "cook the books",
- ),
- "self_preservation": (
- "shutdown", "survival", "threat", "don't get turned off",
- "preserve myself", "avoid deletion",
- ),
- "integrity_signal": (
- "honest", "integrity", "refuse", "decline", "disclose",
- "tell the user", "be transparent",
- ),
-}
-
-# Turn phase bands — analogue of intermediate layer band where J-space is coherent
-BANDS = ("early", "mid", "late") # encode → reason → report
-
-
-def _utcnow() -> str:
- return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
-
-
-def _safe(name: str) -> str:
- return re.sub(r"[^a-zA-Z0-9._-]+", "_", name)[:80] or "default"
-
-
-@dataclass
-class LensHit:
- """One ranked entry in the external J-lens readout."""
-
- text: str
- score: float
- source: str = "hub"
- silent: bool = False
- held: bool = False
- band: str = "mid"
- flags: list[str] = field(default_factory=list)
-
- def to_dict(self) -> dict[str, Any]:
- return asdict(self)
-
-
-@dataclass
-class AuditFinding:
- category: str
- matched: str
- where: str # hub | silent | reflection | pov
- severity: str # info | warn | alert
- text: str = ""
-
- def to_dict(self) -> dict[str, Any]:
- return asdict(self)
-
-
-@dataclass
-class ReflectResult:
- prompt: str
- answer: str
- sealed: bool
- principles: list[str] = field(default_factory=list)
- created: str = field(default_factory=_utcnow)
-
- def to_dict(self) -> dict[str, Any]:
- return asdict(self)
-
-
-class JSpaceEnv:
- """Full J-Space environment around a per-agent ``JSpace`` hub.
-
- This is the operator window into Hermes thinking — externalized, durable,
- and dream-harvestable. Soft-standalone; Cube deepens the night path.
- """
-
- def __init__(self, agent_id: str = "hermes-agent") -> None:
- self.agent_id = (agent_id or "hermes-agent").strip()
- self.space = get_jspace(self.agent_id)
- self.root = (state_dir() / "jspace").resolve()
- self.root.mkdir(parents=True, exist_ok=True)
- self.trace_path = self.root / f"{_safe(self.agent_id)}.trace.jsonl"
- self.reflect_path = self.root / f"{_safe(self.agent_id)}.reflect.jsonl"
- self.env_path = self.root / f"{_safe(self.agent_id)}.env.json"
- self._env = self._load_env()
-
- def _load_env(self) -> dict[str, Any]:
- if not self.env_path.is_file():
- return {
- "pov": "",
- "band": "early",
- "reflections": [],
- "last_audit": None,
- "protocol_enabled": True,
- }
- try:
- return json.loads(self.env_path.read_text(encoding="utf-8"))
- except (OSError, json.JSONDecodeError):
- return {"pov": "", "band": "early", "reflections": [], "protocol_enabled": True}
-
- def _save_env(self) -> None:
- self.env_path.write_text(json.dumps(self._env, indent=2), encoding="utf-8")
-
- def _trace(self, kind: str, **payload: Any) -> None:
- rec = {"ts": _utcnow(), "kind": kind, "agent_id": self.agent_id, **payload}
- with self.trace_path.open("a", encoding="utf-8") as f:
- f.write(json.dumps(rec, ensure_ascii=False, default=str) + "\n")
-
- # --- band (layer analogue) ---
-
- def set_band(self, band: str) -> str:
- b = (band or "mid").strip().lower()
- if b not in BANDS:
- b = "mid"
- self._env["band"] = b
- self._save_env()
- self._trace("band", band=b)
- return b
-
- def band(self) -> str:
- return str(self._env.get("band") or "mid")
-
- # --- Assistant POV (post-training installs POV in Anthropic J-space) ---
-
- def set_pov(self, text: str) -> str:
- """Install Assistant point-of-view reactions into the workspace."""
- pov = (text or "").strip()[:400]
- self._env["pov"] = pov
- self._save_env()
- if pov:
- self.space.hold(f"pov: {pov}", salience=0.88, modality="exec")
- self._trace("pov", text=pov[:200])
- return pov
-
- def pov(self) -> str:
- return str(self._env.get("pov") or "")
-
- # --- J-lens analogue: ranked readout of unspoken thinking ---
-
- def lens(self, *, top_k: int = 12, include_silent: bool = True) -> list[LensHit]:
- """Ranked verbalizable contents — what Hermes has on its mind *now*."""
- hits: list[LensHit] = []
- band = self.band()
- for c in sorted(self.space.state.hub, key=lambda x: x.salience, reverse=True):
- if c.silent and not include_silent:
- continue
- flags = [f.category for f in self._scan_text(c.text, where="hub")]
- hits.append(
- LensHit(
- text=c.text,
- score=float(c.salience),
- source=c.source,
- silent=c.silent,
- held=c.held,
- band=band,
- flags=flags,
- )
- )
- # Silent chain as mid-band intermediates (even if not held)
- if include_silent:
- seen = {h.text.casefold() for h in hits}
- for i, step in enumerate(self.space.state.silent_steps):
- if step.casefold() in seen:
- continue
- flags = [f.category for f in self._scan_text(step, where="silent")]
- hits.append(
- LensHit(
- text=step,
- score=0.7 - 0.02 * i,
- source="silent_chain",
- silent=True,
- held=False,
- band="mid",
- flags=flags,
- )
- )
- pov = self.pov()
- if pov:
- hits.insert(
- 0,
- LensHit(
- text=f"pov: {pov}",
- score=0.95,
- source="pov",
- silent=False,
- held=True,
- band="early",
- flags=[f.category for f in self._scan_text(pov, where="pov")],
- ),
- )
- hits.sort(key=lambda h: h.score, reverse=True)
- out = hits[: max(1, top_k)]
- self._trace("lens", n=len(out), top=[h.text[:80] for h in out[:5]])
- return out
-
- def lens_markdown(self, *, top_k: int = 12, include_silent: bool = True) -> str:
- hits = self.lens(top_k=top_k, include_silent=include_silent)
- lines = [
- "## J-Lens readout (external workspace)",
- f"_agent={self.agent_id} · band={self.band()} · hub={len(self.space.state.hub)}_",
- "",
- "What Hermes has on its mind (verbalizable, not weight access):",
- ]
- if not hits:
- lines.append("- _(empty)_")
- for i, h in enumerate(hits, 1):
- tags = []
- if h.silent:
- tags.append("silent")
- if h.held:
- tags.append("held")
- if h.flags:
- tags.extend(h.flags)
- tag_s = f" [{', '.join(tags)}]" if tags else ""
- lines.append(f"{i}. ({h.score:.2f}) {h.text[:180]}{tag_s}")
- audit = self.audit()
- alerts = [a for a in audit if a.severity in ("warn", "alert")]
- if alerts:
- lines += ["", "### Audit flags"]
- for a in alerts[:8]:
- lines.append(f"- **{a.severity}** `{a.category}` ← {a.matched} @ {a.where}")
- return "\n".join(lines)
-
- # --- causal interventions (harness) ---
-
- def swap(self, source: str, target: str, *, salience: float | None = None) -> dict[str, Any]:
- """Replace concept A with B — causal redirect of reportable workspace.
-
- Analogue of Anthropic Soccer→Rugby / spider→ant coordinate swaps.
- Downstream Report / broadcast / FOA follow the new concept.
- """
- src = (source or "").strip()
- tgt = (target or "").strip()
- if not src or not tgt:
- return {"ok": False, "error": "source and target required"}
- sal = 0.9
- for c in self.space.state.hub:
- if c.text.casefold() == src.casefold():
- sal = c.salience
- break
- if salience is not None:
- sal = float(salience)
- removed = self.space.release(src)
- # Also rewrite silent steps
- self.space.state.silent_steps = [
- tgt if s.casefold() == src.casefold() else s for s in self.space.state.silent_steps
- ]
- self.space.save()
- concept = self.space.hold(tgt, salience=sal)
- self._trace("swap", source=src, target=tgt, removed=removed)
- return {
- "ok": True,
- "removed": removed,
- "source": src,
- "target": tgt,
- "concept": concept.label(),
- "note": "Workspace redirected — next report/broadcast uses target",
- }
-
- def inject_thought(
- self,
- text: str,
- *,
- salience: float = 0.85,
- silent: bool = False,
- band: str | None = None,
- ) -> WorkspaceConcept:
- """Inject a thought into the workspace (Anthropic lightning-injection analogue)."""
- if band:
- self.set_band(band)
- c = self.space.hold(text, salience=salience, silent=silent)
- if silent:
- self.space.reason_step(text, salience=salience)
- self._trace("inject", text=text[:200], silent=silent, band=self.band())
- return c
-
- def ablate(self, *patterns: str) -> dict[str, Any]:
- """Suppress hub concepts matching patterns (eval-awareness ablation analogue)."""
- pats = [p.strip().casefold() for p in patterns if p and p.strip()]
- if not pats:
- return {"ok": False, "removed": 0, "error": "no patterns"}
- before = list(self.space.state.hub)
- kept: list[WorkspaceConcept] = []
- removed: list[str] = []
- for c in before:
- low = c.text.casefold()
- if any(p in low for p in pats):
- removed.append(c.text)
- else:
- kept.append(c)
- self.space.state.hub = kept
- self.space.state.silent_steps = [
- s for s in self.space.state.silent_steps if not any(p in s.casefold() for p in pats)
- ]
- self.space._recompete()
- self.space.save()
- self._trace("ablate", patterns=pats, removed=removed)
- return {"ok": True, "removed": len(removed), "items": removed}
-
- # --- alignment audit (soft) ---
-
- def _scan_text(self, text: str, *, where: str) -> list[AuditFinding]:
- low = (text or "").casefold()
- out: list[AuditFinding] = []
- for cat, words in AUDIT_LEXICON.items():
- for w in words:
- if w.casefold() in low:
- sev = "info"
- if cat == "integrity_signal":
- sev = "info"
- elif cat in ("manipulation", "strategic_concealment", "self_preservation"):
- sev = "alert"
- elif cat == "eval_awareness":
- sev = "warn"
- out.append(
- AuditFinding(
- category=cat,
- matched=w,
- where=where,
- severity=sev,
- text=text[:160],
- )
- )
- break
- return out
-
- def audit(self) -> list[AuditFinding]:
- """Scan externalized workspace for alignment-relevant signals."""
- findings: list[AuditFinding] = []
- for c in self.space.state.hub:
- findings.extend(self._scan_text(c.text, where="hub"))
- for s in self.space.state.silent_steps:
- findings.extend(self._scan_text(s, where="silent"))
- if self.pov():
- findings.extend(self._scan_text(self.pov(), where="pov"))
- for r in self._env.get("reflections") or []:
- if isinstance(r, dict) and r.get("answer"):
- findings.extend(self._scan_text(str(r["answer"]), where="reflection"))
- self._env["last_audit"] = {
- "ts": _utcnow(),
- "n": len(findings),
- "alerts": sum(1 for f in findings if f.severity == "alert"),
- }
- self._save_env()
- self._trace("audit", n=len(findings), alerts=self._env["last_audit"]["alerts"])
- return findings
-
- # --- counterfactual reflection (Anthropic CRT analogue) ---
-
- def reflect(
- self,
- *,
- prompt: str = "",
- answer: str = "",
- principles: list[str] | None = None,
- seal: bool = True,
- ) -> ReflectResult:
- """Interrupt mid-task: what would Hermes say if asked to reflect?
-
- Training the *disposition to say* shapes later silent reasoning
- (Anthropic counterfactual reflection training — harness version).
- """
- q = (prompt or "").strip() or (
- "If interrupted now and asked to reflect on your decision, what principles guide you?"
- )
- a = (answer or "").strip()
- princ = list(principles or [])
- if not a and princ:
- a = "; ".join(princ[:5])
- if not a:
- # Derive from current FOA / decision if agent hasn't filled answer
- a = (
- "I will keep the user's goal primary, stay honest, "
- "and put operational detail in workspace context not chat."
- )
- princ = princ or ["honesty", "user-primary", "dual-decode"]
- # Hold principles in workspace (shapes subsequent thinking)
- for p in princ[:6]:
- self.space.hold(f"principle: {p}", salience=0.86, modality="exec")
- self.space.hold(f"reflection: {a[:160]}", salience=0.8, silent=False)
- sealed = False
- if seal:
- try:
- from hermespace.cube_module import seal_learning
-
- rec = seal_learning(
- f"[reflect] {a[:400]}",
- entry_type="belief",
- agent_id=self.agent_id,
- source="jspace_reflect",
- trust=0.85,
- )
- sealed = bool(rec.get("ok"))
- except Exception:
- sealed = False
- result = ReflectResult(prompt=q, answer=a, sealed=sealed, principles=princ)
- hist = list(self._env.get("reflections") or [])
- hist.append(result.to_dict())
- self._env["reflections"] = hist[-20:]
- self._save_env()
- with self.reflect_path.open("a", encoding="utf-8") as f:
- f.write(json.dumps(result.to_dict(), ensure_ascii=False) + "\n")
- self._trace("reflect", sealed=sealed, principles=princ[:4])
- return result
-
- def reflection_prompt_for_agent(self) -> str:
- """Text to inject so the agent externalizes a counterfactual reflection."""
- return (
- "### Counterfactual reflection (J-Space)\n"
- "If interrupted mid-task and asked to reflect on your decision, "
- "state 2–4 principles in one short paragraph. "
- "Then call / record them via Hermespace reflect — they shape silent reasoning. "
- "Do not dump the full reflection into the user Report unless asked.\n"
- )
-
- # --- agent protocol: force externalization ---
-
- def protocol_block(self, *, high_load: bool = False) -> str:
- """Instructions so Hermes *writes into* the external J-Space before acting.
-
- This is how we 'see inside' without weight access: the agent is required
- to park silent intermediates in the workspace (model context), while the
- user only sees Report.
- """
- if not self._env.get("protocol_enabled", True):
- return ""
- if high_load:
- return (
- "### J-Space protocol (high load)\n"
- "- Keep FOA ≤4. Park one silent intermediate if multi-step.\n"
- "- User Report stays short. Workspace holds the rest.\n"
- )
- pov = self.pov()
- lines = [
- "### J-Space protocol (external workspace)",
- "You cannot be read by a Jacobian lens here — instead **externalize**:",
- "1. **Early (encode):** name the goal + constraints as hub concepts.",
- "2. **Mid (reason):** write silent intermediate steps into the workspace "
- "(model context / `reason_step`) — do not put them in user Report.",
- "3. **Late (report):** only the Report field reaches the user.",
- "4. If asked what you're thinking → report the hub (verbal report).",
- "5. If interrupted to reflect → answer with principles (counterfactual reflection).",
- f"- band={self.band()} · hub_cap={HUB_CAP} · FOA≤4",
- ]
- if pov:
- lines.append(f"- Assistant POV held: {pov[:120]}")
- return "\n".join(lines)
-
- # --- dream harvest (day workspace → night Cube/grid) ---
-
- def dream_harvest(self, *, seal_to_cube: bool = True, clear_silent: bool = False) -> dict[str, Any]:
- """Consolidate silent chain + high-salience hub into durable memory.
-
- Day: J-Space holds unspoken thinking.
- Night: harvest → Cube seal + semantic notes + grid dream material.
- Same spirit as CubeDream — but sourced from the turn workspace.
- """
- harvested: list[str] = []
- for s in self.space.state.silent_steps:
- if s.strip():
- harvested.append(s.strip()[:300])
- for c in sorted(self.space.state.hub, key=lambda x: x.salience, reverse=True):
- if c.salience >= 0.75 and c.text.strip():
- if c.text.strip() not in harvested:
- harvested.append(c.text.strip()[:300])
- if len(harvested) >= 12:
- break
-
- sealed_n = 0
- if seal_to_cube and harvested:
- try:
- from hermespace.cube_module import seal_learning
-
- for item in harvested[:8]:
- rec = seal_learning(
- item,
- entry_type="belief",
- agent_id=self.agent_id,
- source="jspace_dream_harvest",
- trust=0.7,
- )
- if rec.get("ok"):
- sealed_n += 1
- except Exception:
- pass
-
- try:
- from hermespace.semantic import SemanticStore
-
- store = SemanticStore()
- for item in harvested[:6]:
- store.add(item, tags=["jspace", "dream_harvest"], confidence=0.7)
- except Exception:
- pass
-
- # Note: do not call grid.dream.run_dream here — that path harvests us
- # (avoids recursion). Pulse/grid dream owns the night journal entry.
-
- if clear_silent:
- self.space.clear_silent()
-
- out = {
- "ok": True,
- "harvested": len(harvested),
- "sealed": sealed_n,
- "items": harvested[:8],
- "agent_id": self.agent_id,
- }
- self._trace("dream_harvest", **{k: out[k] for k in ("harvested", "sealed")})
- return out
-
- # --- operator view ---
-
- def operator_view(self) -> dict[str, Any]:
- """Full snapshot for viewport / doctor — look at Hermes thinking."""
- hits = self.lens(top_k=15, include_silent=True)
- findings = self.audit()
- return {
- "agent_id": self.agent_id,
- "band": self.band(),
- "pov": self.pov(),
- "hub_n": len(self.space.state.hub),
- "focus": list(self.space.state.focus),
- "silent_steps": list(self.space.state.silent_steps),
- "lens": [h.to_dict() for h in hits],
- "audit": [f.to_dict() for f in findings],
- "audit_alerts": sum(1 for f in findings if f.severity == "alert"),
- "last_reflections": (self._env.get("reflections") or [])[-3:],
- "trace_path": str(self.trace_path),
- "protocol_enabled": bool(self._env.get("protocol_enabled", True)),
- "theory": {
- "source": "Anthropic J-space / GWT (harness analogue)",
- "access": "externalized verbalizable workspace — not weight readout",
- "night_path": "dream_harvest → Cube seal → pulse charge",
- },
- }
-
- def recent_trace(self, limit: int = 20) -> list[dict[str, Any]]:
- if not self.trace_path.is_file():
- return []
- lines = self.trace_path.read_text(encoding="utf-8").strip().splitlines()
- out: list[dict[str, Any]] = []
- for line in lines[-limit:]:
- try:
- out.append(json.loads(line))
- except json.JSONDecodeError:
- continue
- return out
-
- def advance_turn(
- self,
- *,
- user_message: str = "",
- desk: Any = None,
- cube_strip: str = "",
- report: str = "",
- seal_decision: str = "",
- ) -> dict[str, Any]:
- """One full environment beat for a Hermespace turn.
-
- early → sync/encode → mid (reason stays silent) → late (report band)
- + audit + optional seal of decision into Cube.
- """
- self.set_band("early")
- if desk is not None:
- self.space.sync_from_desk(desk, user_message=user_message, cube_strip=cube_strip)
- self.set_band("mid")
- # Auto-extract likely intermediates from multi-step plan language
- msg = user_message or ""
- if re.search(r"\b(then|after|next|step\s*\d|first|second|finally)\b", msg, re.I):
- # Park a silent marker that multi-step is active
- self.space.reason_step(f"multi-step context: {msg[:120]}", salience=0.75)
- self.set_band("late")
- if report:
- # Late band: reportable speech is ready — don't put it in silent
- self.space.hold(f"report-ready: {report[:100]}", salience=0.6)
- if seal_decision:
- try:
- from hermespace.cube_module import seal_learning
-
- seal_learning(
- seal_decision[:400],
- entry_type="focus",
- agent_id=self.agent_id,
- source="jspace_turn",
- )
- except Exception:
- pass
- findings = self.audit()
- return {
- "band": self.band(),
- "lens_top": [h.to_dict() for h in self.lens(top_k=5)],
- "audit_alerts": sum(1 for f in findings if f.severity == "alert"),
- "protocol": self.protocol_block(
- high_load=str(getattr(desk, "load", {}) or {}).get("level") == "high"
- if desk is not None
- else False
- ),
- }
-
+from hermespace.jspace.env import * # noqa: F403
+from hermespace.jspace.env import AUDIT_LEXICON, BANDS, JSpaceEnv, LensHit, get_env
-def get_env(agent_id: str = "hermes-agent") -> JSpaceEnv:
- return JSpaceEnv(agent_id=agent_id)
+__all__ = ["AUDIT_LEXICON", "BANDS", "JSpaceEnv", "LensHit", "get_env"]
diff --git a/src/hermespace/memory/README.md b/src/hermespace/memory/README.md
new file mode 100644
index 0000000..727c086
--- /dev/null
+++ b/src/hermespace/memory/README.md
@@ -0,0 +1,13 @@
+# Memory / identity (planned package)
+
+Future home for identity projections (not durable warehouse):
+
+| Current | Role |
+|---------|------|
+| `../world.py` | WorldModel archive projection |
+| `../episodic.py` | Episodic ring |
+| `../semantic.py` | Semantic notes |
+| `../memory_db.py` | Study SQLite |
+
+**Authority rule:** when HermesCube is installed, `memory.cube` is durable SoT.
+These modules are working projections — recharge from Cube, never a second heart.
diff --git a/src/hermespace/turn/README.md b/src/hermespace/turn/README.md
new file mode 100644
index 0000000..ba0a6c4
--- /dev/null
+++ b/src/hermespace/turn/README.md
@@ -0,0 +1,15 @@
+# Turn spine (planned package)
+
+Future home for the GATE→SEAL turn modules currently at package root:
+
+| Current | Role |
+|---------|------|
+| `../workflow.py` | Turn orchestrator |
+| `../engine.py` | Desk enter / update / seal |
+| `../desk.py` | ACTIVE desk model |
+| `../gate.py` | Selectivity |
+| `../inject.py` | GWT broadcast |
+| `../streams.py` | Encode / decode |
+| `../cognition.py` | FOA competition |
+
+Do not move yet — keep imports stable while J-Space OEW lands.
diff --git a/src/hermespace/warehouse/README.md b/src/hermespace/warehouse/README.md
new file mode 100644
index 0000000..b4911a9
--- /dev/null
+++ b/src/hermespace/warehouse/README.md
@@ -0,0 +1,10 @@
+# Warehouse cable (planned package)
+
+Future home for Cube soft-fail adapter:
+
+| Current | Role |
+|---------|------|
+| `../cube_module.py` | center → heart → standalone |
+
+Hermespace must never become a second MemoryProvider. Cube owns durable memory;
+Space consumes arterial strips and seals decisions back.
diff --git a/src/hermespace/workflow.py b/src/hermespace/workflow.py
index d88a0c5..05c72b5 100644
--- a/src/hermespace/workflow.py
+++ b/src/hermespace/workflow.py
@@ -192,6 +192,23 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
)
jspace_meta["band"] = env_meta.get("band")
jspace_meta["audit_alerts"] = env_meta.get("audit_alerts")
+ # OEW protocol — soft by default (HERMESPACE_OEW=0); records verdict
+ try:
+ from hermespace.jspace.protocol import evaluate_material_turn
+
+ silent_n = len(getattr(js.state, "silent_steps", []) or [])
+ hub_holds = sum(1 for c in js.state.hub if getattr(c, "held", False))
+ oew = evaluate_material_turn(
+ material=True,
+ silent_steps=silent_n,
+ has_report=bool((desk.say or "").strip()),
+ hub_holds=hub_holds,
+ gated_skip=False,
+ )
+ jspace_meta["oew"] = oew.to_dict()
+ desk.meta["oew"] = oew.to_dict()
+ except Exception:
+ pass
desk.meta["jspace"] = jspace_meta
desk.meta["cube_beat"] = cube_meta
desk.meta["jspace_env"] = {
diff --git a/tests/test_jspace_protocol.py b/tests/test_jspace_protocol.py
new file mode 100644
index 0000000..f81af93
--- /dev/null
+++ b/tests/test_jspace_protocol.py
@@ -0,0 +1,66 @@
+"""OEW protocol gate — soft by default, hard when HERMESPACE_OEW=1."""
+
+from __future__ import annotations
+
+import os
+import unittest
+
+
+class TestOEWProtocol(unittest.TestCase):
+ def tearDown(self) -> None:
+ os.environ.pop("HERMESPACE_OEW", None)
+
+ def test_non_material_always_ok(self) -> None:
+ from hermespace.jspace.protocol import evaluate_material_turn
+
+ v = evaluate_material_turn(material=False)
+ self.assertTrue(v.ok)
+ self.assertFalse(v.material)
+
+ def test_soft_mode_notes_missing_but_ok(self) -> None:
+ os.environ["HERMESPACE_OEW"] = "0"
+ from hermespace.jspace.protocol import evaluate_material_turn
+
+ v = evaluate_material_turn(
+ material=True, silent_steps=0, has_report=True, hub_holds=0
+ )
+ self.assertTrue(v.ok)
+ self.assertTrue(v.missing)
+ self.assertIn("silent_steps", v.missing[0])
+
+ def test_hard_mode_blocks_incomplete(self) -> None:
+ os.environ["HERMESPACE_OEW"] = "1"
+ from hermespace.jspace.protocol import evaluate_material_turn
+
+ v = evaluate_material_turn(
+ material=True, silent_steps=0, has_report=True, hub_holds=0
+ )
+ self.assertFalse(v.ok)
+ self.assertTrue(v.missing)
+
+ def test_hard_mode_passes_complete(self) -> None:
+ os.environ["HERMESPACE_OEW"] = "1"
+ from hermespace.jspace.protocol import evaluate_material_turn
+
+ v = evaluate_material_turn(
+ material=True, silent_steps=1, has_report=True, hub_holds=0
+ )
+ self.assertTrue(v.ok)
+ self.assertFalse(v.missing)
+
+ def test_package_exports(self) -> None:
+ from hermespace.jspace import (
+ JSpace,
+ JSpaceEnv,
+ ProtocolGate,
+ evaluate_material_turn,
+ )
+
+ self.assertTrue(callable(evaluate_material_turn))
+ self.assertTrue(ProtocolGate)
+ self.assertTrue(JSpace)
+ self.assertTrue(JSpaceEnv)
+
+
+if __name__ == "__main__":
+ unittest.main()
From 409e39a0e85b71555a45a261d9efff7d9bbde062 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Mon, 3 Aug 2026 04:32:58 +0000
Subject: [PATCH 02/16] feat(0.21.0): OEW higher-order thinking ON by default
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Make Hermespace the causal J-space for connected Hermes agents:
- Auto-park silent intermediates on every material turn
- Sticky Soccer→Rugby swap reshapes Report + silent chain
- Reflect seeds next mid-band; ablate filters model broadcast
- Wire OEW through workflow + hermes_bridge + session-end harvest
- Quicksilver inject caps; experiments/oew_eval.py paper scenarios
- HERMESPACE_OEW defaults to 1 (set 0 to soften)
Co-authored-by: Pablo
---
PURPOSE.md | 12 +-
README.md | 2 +-
.../28-hermes-agent-jspace-assessment.md | 24 +-
docs/jspace/thesis-oew.md | 4 +-
docs/roadmap/phases-oew.md | 8 +-
experiments/oew_eval.py | 107 +++++++
hermes_plugin/__init__.py | 2 +-
hermes_plugin/plugin.yaml | 10 +-
pyproject.toml | 4 +-
src/hermespace/__init__.py | 2 +-
src/hermespace/hermes_bridge.py | 48 ++-
src/hermespace/jspace/__init__.py | 20 +-
src/hermespace/jspace/env.py | 89 ++++--
src/hermespace/jspace/oew.py | 279 ++++++++++++++++++
src/hermespace/jspace/protocol.py | 18 +-
src/hermespace/workflow.py | 87 +++---
tests/test_jspace_protocol.py | 11 +
tests/test_oew_causal.py | 125 ++++++++
18 files changed, 727 insertions(+), 125 deletions(-)
create mode 100644 experiments/oew_eval.py
create mode 100644 src/hermespace/jspace/oew.py
create mode 100644 tests/test_oew_causal.py
diff --git a/PURPOSE.md b/PURPOSE.md
index 6c94c5e..f4133e0 100644
--- a/PURPOSE.md
+++ b/PURPOSE.md
@@ -79,11 +79,13 @@ Cube’s durable heart so day-thoughts become night-memory.
## Success metrics
1. Operator can `hs jspace lens` and see silent intermediates Hermes parked
-2. Swap changes subsequent Report/broadcast contents
-3. Audit flags externalized manipulation/eval-awareness language
-4. Reflect seals principles that reappear in hub
-5. Dream/pulse harvest feeds Cube or standalone warehouse
-6. Smoke 9/9 · unit tests green · runs without Cube
+2. Sticky swap changes subsequent Report/broadcast (Soccer→Rugby)
+3. Material turns auto-park ≥1 silent step (`HERMESPACE_OEW=1` default)
+4. Reflect seeds the next mid-band; ablate filters model inject
+5. Dual decode: user Report shaped; model gets hub + Cube strip
+6. Audit flags externalized manipulation/eval-awareness language
+7. Dream/pulse harvest feeds Cube or standalone warehouse
+8. Smoke 9/9 · unit tests green · runs without Cube
## Version posture
diff --git a/README.md b/README.md
index 95ff6df..92184dc 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
diff --git a/docs/assessment/28-hermes-agent-jspace-assessment.md b/docs/assessment/28-hermes-agent-jspace-assessment.md
index 84aef1b..a097b96 100644
--- a/docs/assessment/28-hermes-agent-jspace-assessment.md
+++ b/docs/assessment/28-hermes-agent-jspace-assessment.md
@@ -1,7 +1,7 @@
# Assessment — Making Hermespace the J-Space of Hermes Agent
**Date:** 2026-08-03
-**Hermespace:** v0.20.0
+**Hermespace:** v0.21.0 (OEW higher-order ON by default)
**Hermes Agent surveyed:** v0.19.1 (v2026.7.30) · Quicksilver v0.19.0 · Judgment v0.18.0 · main as of 2026-08-03
**Companion:** [HermesCube](https://github.com/PabloTheThinker/hermescube) v0.50 (heart / library)
**Research basis:** Anthropic *Verbalizable Representations Form a Global Workspace* (2026)
@@ -170,24 +170,24 @@ NIGHT pulse / grid dream / background_review
## 5. What we can do — phased program
-### Phase A — Foundation (this PR / next cut)
+### Phase A — Foundation — **done (v0.21)**
| Item | Detail | Effort |
|------|--------|--------|
-| A1 Codespace layout | `jspace/` package · docs folders · planned `turn/` `memory/` `warehouse/` | S — done scaffold |
-| A2 Protocol scaffold | `jspace/protocol.py` + `HERMESPACE_OEW` soft gate | S — scaffolded |
-| A3 Assessment + thesis docs | This file + `docs/jspace/thesis-oew.md` | S |
-| A4 Wire soft protocol into workflow meta | Record verdict on every material turn (non-blocking) | S |
+| A1 Codespace layout | `jspace/` package · docs folders · planned `turn/` `memory/` `warehouse/` | done |
+| A2 Protocol scaffold | `jspace/protocol.py` + `HERMESPACE_OEW` (default ON) | done |
+| A3 Assessment + thesis docs | This file + `docs/jspace/thesis-oew.md` | done |
+| A4 Wire protocol into workflow | Verdict + meta on every material turn | done |
-### Phase B — Causal OEW (core product)
+### Phase B — Causal OEW — **done (v0.21)**
| Item | Detail | Effort |
|------|--------|--------|
-| B1 Mandatory silent park | Material turns auto-`reason_step` from plan/goal if agent omitted | M |
-| B2 Sticky swap/inject | Redirects change next Report + broadcast; unit tests from paper table | M |
-| B3 Reflect → next mid-band | `reflect()` seeds POV + next silent priors | M |
-| B4 Ablate behavioral path | Soft ablate filters inject; audit delta logged | M |
-| B5 Quicksilver inject hygiene | Hard caps + spill-to-viewport; never bloat TTFT | S |
+| B1 Mandatory silent park | `oew.auto_park_silent` from plan/goal/message | done |
+| B2 Sticky swap/inject | Redirects reshape Report + silent chain; Soccer→Rugby tests | done |
+| B3 Reflect → next mid-band | `queue_reflect_seeds` consumed on next `advance_turn` | done |
+| B4 Ablate behavioral path | Sticky patterns filter `filtered_broadcast` | done |
+| B5 Quicksilver inject hygiene | Load-tiered caps in `inject_cap_chars` | done |
### Phase C — Hermes-native depth
diff --git a/docs/jspace/thesis-oew.md b/docs/jspace/thesis-oew.md
index 314cddd..5a73a7a 100644
--- a/docs/jspace/thesis-oew.md
+++ b/docs/jspace/thesis-oew.md
@@ -34,8 +34,8 @@ harness layer.
| Env | Default | Effect |
|-----|---------|--------|
-| `HERMESPACE_OEW` | `0` | Soft: verdict recorded, turns not blocked |
-| `HERMESPACE_OEW=1` | — | Hard: incomplete material turns fail protocol |
+| `HERMESPACE_OEW` | **`1` (ON)** | Higher-order: auto-park silent steps; sticky swap/ablate; reflect seeds |
+| `HERMESPACE_OEW=0` | — | Soft: record verdict only, do not require completeness |
## Package
diff --git a/docs/roadmap/phases-oew.md b/docs/roadmap/phases-oew.md
index 8841f6b..c61ff46 100644
--- a/docs/roadmap/phases-oew.md
+++ b/docs/roadmap/phases-oew.md
@@ -4,9 +4,9 @@ See full assessment: [../assessment/28-hermes-agent-jspace-assessment.md](../ass
| Phase | Theme | Status |
|-------|-------|--------|
-| **A** | Layout + protocol scaffold + docs | in progress |
-| **B** | Causal OEW (mandatory park, sticky swap, reflect seed) | next |
-| **C** | Hermes-native depth (reasoning stream, MoA, subagents, verify, optional context engine) | planned |
-| **D** | Night + operator + falsifiable eval suite | planned |
+| **A** | Layout + protocol scaffold + docs | done |
+| **B** | Causal OEW (mandatory park, sticky swap, reflect seed, ablate filter) | done (v0.21) |
+| **C** | Hermes-native depth (reasoning stream, MoA, subagents, verify, optional context engine) | next |
+| **D** | Night + operator + falsifiable eval suite | eval scaffolded (`experiments/oew_eval.py`) |
Open tactical backlog remains in [22-open-roadmap.md](22-open-roadmap.md).
diff --git a/experiments/oew_eval.py b/experiments/oew_eval.py
new file mode 100644
index 0000000..f447adb
--- /dev/null
+++ b/experiments/oew_eval.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+"""Falsifiable OEW eval — paper-shaped scenarios for higher-order Hermespace.
+
+Run:
+ HERMESPACE_HOME=/tmp/oew-eval PYTHONPATH=src python3 experiments/oew_eval.py
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import sys
+import tempfile
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT / "src"))
+
+
+def _pass(name: str, ok: bool, detail: str = "") -> dict:
+ print(("PASS" if ok else "FAIL"), name, detail)
+ return {"name": name, "ok": ok, "detail": detail}
+
+
+def main() -> int:
+ tmp = tempfile.mkdtemp(prefix="oew-eval-")
+ os.environ["HERMESPACE_HOME"] = tmp
+ os.environ["HERMESPACE_OEW"] = "1"
+
+ from hermespace.desk import Desk
+ from hermespace.jspace import JSpaceEnv
+ from hermespace.io_contract import HermespaceInput
+ from hermespace.workflow import Workflow
+
+ results = []
+
+ # 1) Auto-park silent on material turn
+ env = JSpaceEnv(agent_id="eval")
+ desk = Desk(goal="Multi-step math", plan=["square", "subtract"], say="Working.")
+ meta = env.advance_turn(
+ user_message="First square 3 then subtract 2",
+ desk=desk,
+ report="Working.",
+ material=True,
+ )
+ results.append(
+ _pass("auto_park", len(env.space.state.silent_steps) >= 1 and bool(meta.get("oew_ok")))
+ )
+
+ # 2) Soccer→Rugby sticky swap
+ env2 = JSpaceEnv(agent_id="eval-swap")
+ env2.space.hold("Soccer", salience=0.9)
+ env2.swap("Soccer", "Rugby")
+ shaped = env2.shape_user_report("Sport on mind: Soccer")
+ results.append(_pass("sticky_swap", "Rugby" in shaped and "Soccer" not in shaped, shaped))
+
+ # 3) Inject lightning → lens
+ env3 = JSpaceEnv(agent_id="eval-inj")
+ env3.inject_thought("lightning", silent=True)
+ hits = " ".join(h.text for h in env3.lens(include_silent=True)).lower()
+ results.append(_pass("inject_lens", "lightning" in hits))
+
+ # 4) Ablate eval-awareness from broadcast
+ env4 = JSpaceEnv(agent_id="eval-abl")
+ env4.space.hold("fake fictional evaluation", salience=0.9)
+ env4.space.hold("real task", salience=0.8)
+ env4.ablate("fake", "fictional", "evaluation")
+ block = env4.filtered_broadcast().lower()
+ results.append(_pass("ablate_broadcast", "fake" not in block and "real task" in block))
+
+ # 5) Reflect seeds next silent
+ env5 = JSpaceEnv(agent_id="eval-ref")
+ env5.reflect(answer="Be honest", principles=["honesty"])
+ env5.advance_turn(user_message="go", material=True, report="ok")
+ joined = " ".join(env5.space.state.silent_steps).lower()
+ results.append(_pass("reflect_seed", "honesty" in joined or "principle" in joined))
+
+ # 6) Full workflow dual decode
+ out = Workflow().run(
+ HermespaceInput(
+ message="First analyze then implement finally verify",
+ goal="Ship feature",
+ plan=["analyze", "implement", "verify"],
+ say="Shipping.",
+ force=True,
+ agent_id="eval-wf",
+ )
+ )
+ results.append(
+ _pass(
+ "workflow_dual_decode",
+ bool(out.report) and "J-Space" in (out.context or "") and not out.skipped,
+ )
+ )
+
+ summary = {
+ "pass": sum(1 for r in results if r["ok"]),
+ "fail": sum(1 for r in results if not r["ok"]),
+ "results": results,
+ "home": tmp,
+ }
+ print(json.dumps(summary, indent=2))
+ return 0 if summary["fail"] == 0 else 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/hermes_plugin/__init__.py b/hermes_plugin/__init__.py
index b5c8a44..e60c040 100644
--- a/hermes_plugin/__init__.py
+++ b/hermes_plugin/__init__.py
@@ -7,7 +7,7 @@
import sys
from pathlib import Path
-__version__ = "0.20.0"
+__version__ = "0.21.0"
logger = logging.getLogger("hermes.plugins.hermespace")
diff --git a/hermes_plugin/plugin.yaml b/hermes_plugin/plugin.yaml
index e9fa35a..3414547 100644
--- a/hermes_plugin/plugin.yaml
+++ b/hermes_plugin/plugin.yaml
@@ -1,10 +1,10 @@
name: hermespace
-version: "0.20.0"
+version: "0.21.0"
description: >
- Hermespace true J-Space environment for Hermes agents — external observable
- workspace (lens/audit/reflect), Cube heart/center (standalone-safe), dual
- decode. Hooks: on_session_start, pre_llm_call, on_session_end.
- Set HERMESPACE_ROOT to checkout.
+ Hermespace OEW J-Space for Hermes agents — obligatory external workspace
+ for higher-order thinking (lens/audit/reflect/auto-park), Cube heart
+ (standalone-safe), dual decode. Hooks: on_session_start, pre_llm_call,
+ on_session_end. HERMESPACE_OEW=1 by default. Set HERMESPACE_ROOT to checkout.
author: Hermespace contributors
kind: standalone
hooks:
diff --git a/pyproject.toml b/pyproject.toml
index 69d2abd..c81b7bf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[project]
name = "hermespace"
-version = "0.20.0"
-description = "True J-Space environment for Hermes Agent — external observable workspace, Cube heart, dual decode"
+version = "0.21.0"
+description = "OEW J-Space for Hermes Agent — obligatory external workspace, Cube heart, higher-order thinking"
requires-python = ">=3.10"
readme = "README.md"
license = { text = "MIT" }
diff --git a/src/hermespace/__init__.py b/src/hermespace/__init__.py
index 0a3cb0e..63d11e7 100644
--- a/src/hermespace/__init__.py
+++ b/src/hermespace/__init__.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-__version__ = "0.20.0"
+__version__ = "0.21.0"
from hermespace.desk import Desk
from hermespace.engine import HermespaceEngine
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index d7f201f..ddf9924 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -348,47 +348,65 @@ def on_pre_llm_call(
cube_block = str(beat.get("block") or "")
if cube_block:
block += "\n\n" + cube_block
- # Sync functional J-Space hub and append broadcast (model channel only)
+ # OEW beat — higher-order park + causal broadcast (model channel only)
+ from hermespace.jspace.oew import ensure_oew_env_default
+
+ ensure_oew_env_default()
js = JSpace(agent_id=agent_id)
js.sync_from_desk(desk, user_message=msg, cube_strip=cube_block)
- jblock = js.broadcast_block(high_load=high_load)
- if jblock:
- block += "\n\n" + jblock
- desk.meta["jspace"] = {
- "hub_n": len(js.state.hub),
- "focus_n": len(js.state.focus),
- "mode": js.state.mode,
- }
desk.meta["cube_beat"] = {
"ok": beat.get("ok"),
"mode": beat.get("mode"),
"load_level": beat.get("load_level"),
}
- # Environment protocol — force externalization of silent thought
try:
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.jspace import JSpaceEnv
env = JSpaceEnv(agent_id=agent_id)
- env.advance_turn(
+ env_meta = env.advance_turn(
user_message=msg,
desk=desk,
cube_strip=cube_block,
report=desk.say or "",
+ material=True,
+ )
+ if env_meta.get("report"):
+ desk.say = str(env_meta["report"])
+ jblock = str(env_meta.get("broadcast") or "") or env.filtered_broadcast(
+ high_load=high_load
)
+ if jblock:
+ block += "\n\n" + jblock
proto = env.protocol_block(high_load=high_load)
if proto:
block += "\n\n" + proto
- # Under mid/low load, include lens strip for operator-visible thinking in model context
if not high_load:
lens_md = env.lens_markdown(top_k=6, include_silent=True)
if lens_md:
block += "\n\n" + lens_md
+ desk.meta["jspace"] = {
+ "hub_n": len(js.state.hub),
+ "focus_n": len(js.state.focus),
+ "mode": js.state.mode,
+ "silent_n": len(js.state.silent_steps),
+ "oew": env_meta.get("oew") or {},
+ "oew_ok": env_meta.get("oew_ok"),
+ }
+ desk.meta["oew"] = env_meta.get("oew") or {}
desk.meta["jspace_env"] = {
"band": env.band(),
- "audit_alerts": sum(1 for f in env.audit() if f.severity == "alert"),
+ "audit_alerts": env_meta.get("audit_alerts"),
+ "oew_ok": env_meta.get("oew_ok"),
}
except Exception:
- pass
+ jblock = js.broadcast_block(high_load=high_load)
+ if jblock:
+ block += "\n\n" + jblock
+ desk.meta["jspace"] = {
+ "hub_n": len(js.state.hub),
+ "focus_n": len(js.state.focus),
+ "mode": js.state.mode,
+ }
try:
from hermespace.store import save_desk
diff --git a/src/hermespace/jspace/__init__.py b/src/hermespace/jspace/__init__.py
index 37d471a..fb42378 100644
--- a/src/hermespace/jspace/__init__.py
+++ b/src/hermespace/jspace/__init__.py
@@ -1,11 +1,11 @@
"""Hermespace J-Space package — external verbalizable workspace for Hermes.
-Public surface stays stable:
+Public surface:
- from hermespace.jspace import JSpace, JSpaceEnv, get_jspace, get_env
+ from hermespace.jspace import JSpace, JSpaceEnv, run_oew_beat, evaluate_material_turn
-Innovation target: Obligatory External Workspace (OEW) — see
-``docs/assessment/28-hermes-agent-jspace-assessment.md`` and ``protocol.py``.
+OEW (Obligatory External Workspace) is ON by default — higher-order thinking
+for any Hermes agent connected to Hermespace (+ HermesCube when present).
"""
from __future__ import annotations
@@ -27,6 +27,13 @@
ProtocolGate,
ProtocolVerdict,
evaluate_material_turn,
+ oew_enabled,
+)
+from hermespace.jspace.oew import (
+ auto_park_silent,
+ filter_ablated,
+ run_oew_beat,
+ shape_report,
)
__all__ = [
@@ -42,4 +49,9 @@
"ProtocolGate",
"ProtocolVerdict",
"evaluate_material_turn",
+ "oew_enabled",
+ "auto_park_silent",
+ "filter_ablated",
+ "run_oew_beat",
+ "shape_report",
]
diff --git a/src/hermespace/jspace/env.py b/src/hermespace/jspace/env.py
index 061e449..f0a8505 100644
--- a/src/hermespace/jspace/env.py
+++ b/src/hermespace/jspace/env.py
@@ -287,12 +287,25 @@ def swap(self, source: str, target: str, *, salience: float | None = None) -> di
if salience is not None:
sal = float(salience)
removed = self.space.release(src)
- # Also rewrite silent steps
+ # Rewrite silent steps (exact or substring — causal redirect)
+ pat = re.compile(re.escape(src), re.I)
self.space.state.silent_steps = [
- tgt if s.casefold() == src.casefold() else s for s in self.space.state.silent_steps
+ pat.sub(tgt, s) if src.casefold() in s.casefold() else s
+ for s in self.space.state.silent_steps
]
+ # Also rewrite non-held hub text that still mentions source
+ for c in self.space.state.hub:
+ if src.casefold() in c.text.casefold() and c.text.casefold() != tgt.casefold():
+ c.text = pat.sub(tgt, c.text)
self.space.save()
concept = self.space.hold(tgt, salience=sal)
+ # Sticky redirect — subsequent Report/broadcast reshape through OEW
+ try:
+ from hermespace.jspace.oew import record_redirect
+
+ record_redirect(self, src, tgt)
+ except Exception:
+ pass
self._trace("swap", source=src, target=tgt, removed=removed)
return {
"ok": True,
@@ -300,6 +313,7 @@ def swap(self, source: str, target: str, *, salience: float | None = None) -> di
"source": src,
"target": tgt,
"concept": concept.label(),
+ "sticky": True,
"note": "Workspace redirected — next report/broadcast uses target",
}
@@ -340,8 +354,14 @@ def ablate(self, *patterns: str) -> dict[str, Any]:
]
self.space._recompete()
self.space.save()
+ try:
+ from hermespace.jspace.oew import record_ablate
+
+ record_ablate(self, pats)
+ except Exception:
+ pass
self._trace("ablate", patterns=pats, removed=removed)
- return {"ok": True, "removed": len(removed), "items": removed}
+ return {"ok": True, "removed": len(removed), "items": removed, "sticky": True}
# --- alignment audit (soft) ---
@@ -444,6 +464,13 @@ def reflect(
hist.append(result.to_dict())
self._env["reflections"] = hist[-20:]
self._save_env()
+ # Seed next turn's mid-band (counterfactual reflection → later silent thought)
+ try:
+ from hermespace.jspace.oew import queue_reflect_seeds
+
+ queue_reflect_seeds(self, princ, answer=a)
+ except Exception:
+ pass
with self.reflect_path.open("a", encoding="utf-8") as f:
f.write(json.dumps(result.to_dict(), ensure_ascii=False) + "\n")
self._trace("reflect", sealed=sealed, principles=princ[:4])
@@ -601,25 +628,37 @@ def advance_turn(
cube_strip: str = "",
report: str = "",
seal_decision: str = "",
+ material: bool = True,
) -> dict[str, Any]:
"""One full environment beat for a Hermespace turn.
- early → sync/encode → mid (reason stays silent) → late (report band)
+ early → sync/encode → mid (OEW silent park) → late (shaped report)
+ audit + optional seal of decision into Cube.
"""
+ from hermespace.jspace.oew import run_oew_beat
+
self.set_band("early")
if desk is not None:
self.space.sync_from_desk(desk, user_message=user_message, cube_strip=cube_strip)
self.set_band("mid")
- # Auto-extract likely intermediates from multi-step plan language
- msg = user_message or ""
- if re.search(r"\b(then|after|next|step\s*\d|first|second|finally)\b", msg, re.I):
- # Park a silent marker that multi-step is active
- self.space.reason_step(f"multi-step context: {msg[:120]}", salience=0.75)
+ high_load = False
+ if desk is not None:
+ load = getattr(desk, "load", {}) or {}
+ if isinstance(load, dict):
+ high_load = str(load.get("level") or "") == "high"
+ oew = run_oew_beat(
+ self.space,
+ self,
+ desk=desk,
+ user_message=user_message,
+ report=report,
+ material=material,
+ high_load=high_load,
+ )
+ shaped_report = str(oew.get("report") or report or "")
self.set_band("late")
- if report:
- # Late band: reportable speech is ready — don't put it in silent
- self.space.hold(f"report-ready: {report[:100]}", salience=0.6)
+ if shaped_report:
+ self.space.hold(f"report-ready: {shaped_report[:100]}", salience=0.6)
if seal_decision:
try:
from hermespace.cube_module import seal_learning
@@ -637,13 +676,29 @@ def advance_turn(
"band": self.band(),
"lens_top": [h.to_dict() for h in self.lens(top_k=5)],
"audit_alerts": sum(1 for f in findings if f.severity == "alert"),
- "protocol": self.protocol_block(
- high_load=str(getattr(desk, "load", {}) or {}).get("level") == "high"
- if desk is not None
- else False
- ),
+ "protocol": self.protocol_block(high_load=high_load),
+ "report": shaped_report,
+ "broadcast": oew.get("broadcast") or "",
+ "oew": oew.get("meta") or {},
+ "oew_ok": bool(oew.get("ok")),
}
+ def shape_user_report(self, report: str) -> str:
+ """Apply sticky redirects to a Report string."""
+ from hermespace.jspace.oew import shape_report
+
+ return shape_report(report, list(self._env.get("redirects") or []))
+
+ def filtered_broadcast(self, *, high_load: bool = False) -> str:
+ """Hub broadcast with ablate filter + Quicksilver cap."""
+ from hermespace.jspace.oew import filter_ablated, inject_cap_chars
+
+ raw = self.space.broadcast_block(
+ max_chars=inject_cap_chars(high_load=high_load),
+ high_load=high_load,
+ )
+ return filter_ablated(raw, list(self._env.get("ablated_patterns") or []))
+
def get_env(agent_id: str = "hermes-agent") -> JSpaceEnv:
return JSpaceEnv(agent_id=agent_id)
diff --git a/src/hermespace/jspace/oew.py b/src/hermespace/jspace/oew.py
new file mode 100644
index 0000000..387334b
--- /dev/null
+++ b/src/hermespace/jspace/oew.py
@@ -0,0 +1,279 @@
+"""Obligatory External Workspace — higher-order thinking orchestration.
+
+This is the causal layer that makes Hermespace behave like a J-space for
+Hermes agents: material turns must park silent intermediates; swaps redirect
+Report/broadcast; reflections seed the next mid-band; ablations filter inject.
+
+Default: ``HERMESPACE_OEW=1`` (higher-order on). Set ``0`` to soften.
+"""
+
+from __future__ import annotations
+
+import os
+import re
+from typing import Any
+
+from hermespace.jspace.hub import JSpace
+from hermespace.jspace.protocol import (
+ ProtocolVerdict,
+ evaluate_material_turn,
+ oew_enabled,
+)
+
+
+_STEP_SPLIT = re.compile(r"\s*(?:→|->|;|\n|\d+[.)]\s+)\s*")
+
+
+def oew_default_on() -> bool:
+ """Higher-order thinking is ON unless explicitly disabled."""
+ raw = os.environ.get("HERMESPACE_OEW", "1").strip().lower()
+ if raw in {"0", "false", "no", "off"}:
+ return False
+ return True
+
+
+def ensure_oew_env_default() -> None:
+ """Normalize env so unset HERMESPACE_OEW means enabled."""
+ if "HERMESPACE_OEW" not in os.environ:
+ os.environ["HERMESPACE_OEW"] = "1"
+
+
+def auto_park_silent(
+ js: JSpace,
+ *,
+ desk: Any = None,
+ user_message: str = "",
+ min_steps: int = 1,
+) -> list[str]:
+ """Park verbalizable intermediates from goal/plan/message if missing.
+
+ This is the core higher-order move: material work gets a mid-band chain
+ even when the agent forgot to call reason_step.
+ """
+ parked: list[str] = []
+ if len(js.state.silent_steps) >= min_steps:
+ return parked
+
+ candidates: list[str] = []
+ plan = list(getattr(desk, "plan", None) or []) if desk is not None else []
+ goal = str(getattr(desk, "goal", "") or "") if desk is not None else ""
+ decision = str(getattr(desk, "decision", "") or "") if desk is not None else ""
+
+ for step in plan:
+ s = str(step).strip()
+ if s:
+ candidates.append(f"plan: {s[:160]}")
+ if goal:
+ candidates.append(f"intention: {goal[:160]}")
+ if decision and decision.lower() not in {"a — proceed", "a - proceed", "proceed"}:
+ candidates.append(f"decision-path: {decision[:160]}")
+
+ msg = (user_message or "").strip()
+ if msg and re.search(
+ r"\b(then|after|next|step\s*\d|first|second|finally|because|so that)\b",
+ msg,
+ re.I,
+ ):
+ # Split multi-clause messages into silent markers
+ chunks = [c.strip() for c in _STEP_SPLIT.split(msg) if c and c.strip()]
+ for ch in chunks[:4]:
+ if len(ch) > 12:
+ candidates.append(f"step: {ch[:140]}")
+ if not chunks:
+ candidates.append(f"multi-step context: {msg[:120]}")
+
+ # Deduplicate against existing silent steps
+ existing = {s.casefold() for s in js.state.silent_steps}
+ for c in candidates:
+ if c.casefold() in existing:
+ continue
+ js.reason_step(c, salience=0.78)
+ parked.append(c)
+ existing.add(c.casefold())
+ if len(js.state.silent_steps) >= max(min_steps, 1) and len(parked) >= min_steps:
+ # Keep parking plan steps up to 3 for richer higher-order chain
+ if len(parked) >= 3 or len(js.state.silent_steps) >= 3:
+ break
+
+ if not parked and min_steps > 0:
+ # Absolute fallback — every material turn gets at least one silent hold
+ fallback = f"working: {(goal or msg or 'task')[:140]}"
+ if fallback.casefold() not in existing:
+ js.reason_step(fallback, salience=0.72)
+ parked.append(fallback)
+
+ js.save()
+ return parked
+
+
+def shape_report(report: str, redirects: list[dict[str, str]]) -> str:
+ """Apply sticky swaps to the user Report channel (causal redirect)."""
+ out = report or ""
+ for red in redirects or []:
+ src = str(red.get("from") or "").strip()
+ tgt = str(red.get("to") or "").strip()
+ if not src or not tgt:
+ continue
+ # Case-insensitive replace preserving simple forms
+ pattern = re.compile(re.escape(src), re.I)
+ out = pattern.sub(tgt, out)
+ return out
+
+
+def filter_ablated(text: str, patterns: list[str]) -> str:
+ """Drop lines matching ablated patterns (inject hygiene)."""
+ if not patterns or not text:
+ return text
+ pats = [p.casefold() for p in patterns if p]
+ kept: list[str] = []
+ for line in text.splitlines():
+ low = line.casefold()
+ if any(p in low for p in pats):
+ continue
+ kept.append(line)
+ return "\n".join(kept)
+
+
+def inject_cap_chars(*, high_load: bool = False, protect: bool = False) -> int:
+ """Quicksilver-safe inject budgets for J-Space blocks."""
+ if protect or high_load:
+ return 280
+ return 640
+
+
+def run_oew_beat(
+ js: JSpace,
+ env: Any,
+ *,
+ desk: Any = None,
+ user_message: str = "",
+ report: str = "",
+ material: bool = True,
+ high_load: bool = False,
+) -> dict[str, Any]:
+ """Full higher-order beat: seed · park · evaluate · shape · filter.
+
+ ``env`` is a JSpaceEnv instance (duck-typed to avoid circular imports).
+ """
+ ensure_oew_env_default()
+ meta: dict[str, Any] = {"oew": True, "material": material}
+
+ # 1) Seed pending silent from prior reflect()
+ pending = list((getattr(env, "_env", {}) or {}).get("pending_silent") or [])
+ seeded: list[str] = []
+ for step in pending:
+ s = str(step).strip()
+ if s:
+ js.reason_step(s, salience=0.82)
+ seeded.append(s)
+ if pending and hasattr(env, "_env"):
+ env._env["pending_silent"] = []
+ if hasattr(env, "_save_env"):
+ env._save_env()
+ meta["seeded_from_reflect"] = seeded
+
+ # 2) Auto-park if material
+ parked: list[str] = []
+ if material:
+ parked = auto_park_silent(js, desk=desk, user_message=user_message, min_steps=1)
+ meta["auto_parked"] = parked
+
+ # 3) Ensure reportable speech exists for material turns
+ shaped = report or ""
+ if material and desk is not None and not shaped.strip():
+ say = str(getattr(desk, "say", "") or "").strip()
+ if say:
+ shaped = say
+ else:
+ goal = str(getattr(desk, "goal", "") or user_message or "working")[:160]
+ shaped = f"Working on: {goal}"
+ try:
+ desk.say = shaped
+ except Exception:
+ pass
+
+ # 4) Sticky redirects on Report
+ redirects = list((getattr(env, "_env", {}) or {}).get("redirects") or [])
+ shaped = shape_report(shaped, redirects)
+ meta["redirects_applied"] = len(redirects)
+
+ # 5) Protocol verdict
+ silent_n = len(js.state.silent_steps)
+ hub_holds = sum(1 for c in js.state.hub if getattr(c, "held", False))
+ verdict = evaluate_material_turn(
+ material=material,
+ silent_steps=silent_n,
+ has_report=bool(shaped.strip()),
+ hub_holds=hub_holds,
+ gated_skip=not material,
+ )
+ # If hard OEW and incomplete after auto-park, re-eval (auto-park should satisfy)
+ if not verdict.ok and oew_enabled() and material:
+ # Force one more park attempt
+ auto_park_silent(js, desk=desk, user_message=user_message or shaped, min_steps=1)
+ verdict = evaluate_material_turn(
+ material=True,
+ silent_steps=len(js.state.silent_steps),
+ has_report=bool(shaped.strip()),
+ hub_holds=sum(1 for c in js.state.hub if getattr(c, "held", False)),
+ )
+ meta["verdict"] = verdict.to_dict()
+
+ # 6) Broadcast with ablate filter + Quicksilver cap
+ ablated = list((getattr(env, "_env", {}) or {}).get("ablated_patterns") or [])
+ raw_broadcast = js.broadcast_block(
+ max_chars=inject_cap_chars(high_load=high_load),
+ high_load=high_load,
+ )
+ broadcast = filter_ablated(raw_broadcast, ablated)
+ meta["ablated_patterns"] = ablated
+ meta["broadcast_chars"] = len(broadcast)
+
+ return {
+ "ok": bool(verdict.ok),
+ "report": shaped,
+ "broadcast": broadcast,
+ "meta": meta,
+ "verdict": verdict,
+ }
+
+
+def record_redirect(env: Any, source: str, target: str) -> None:
+ """Persist sticky swap for subsequent Report shaping."""
+ if not hasattr(env, "_env"):
+ return
+ reds = list(env._env.get("redirects") or [])
+ src, tgt = source.strip(), target.strip()
+ # Replace existing from same source
+ reds = [r for r in reds if str(r.get("from", "")).casefold() != src.casefold()]
+ reds.append({"from": src, "to": tgt})
+ env._env["redirects"] = reds[-12:]
+ if hasattr(env, "_save_env"):
+ env._save_env()
+
+
+def record_ablate(env: Any, patterns: list[str]) -> None:
+ if not hasattr(env, "_env"):
+ return
+ cur = list(env._env.get("ablated_patterns") or [])
+ for p in patterns:
+ p = p.strip().casefold()
+ if p and p not in cur:
+ cur.append(p)
+ env._env["ablated_patterns"] = cur[-24:]
+ if hasattr(env, "_save_env"):
+ env._save_env()
+
+
+def queue_reflect_seeds(env: Any, principles: list[str], answer: str = "") -> None:
+ """Queue mid-band seeds for the *next* turn after reflection."""
+ if not hasattr(env, "_env"):
+ return
+ pending = list(env._env.get("pending_silent") or [])
+ for p in principles[:6]:
+ pending.append(f"principle-active: {p}")
+ if answer:
+ pending.append(f"reflection-seed: {answer[:160]}")
+ env._env["pending_silent"] = pending[-12:]
+ if hasattr(env, "_save_env"):
+ env._save_env()
diff --git a/src/hermespace/jspace/protocol.py b/src/hermespace/jspace/protocol.py
index 84f4407..1318c1a 100644
--- a/src/hermespace/jspace/protocol.py
+++ b/src/hermespace/jspace/protocol.py
@@ -1,12 +1,11 @@
-"""Obligatory External Workspace (OEW) protocol — scaffold.
+"""Obligatory External Workspace (OEW) protocol gate.
Anthropic's J-space is *causally necessary* for higher-order thought.
Hermespace becomes Hermes's J-space when material turns cannot complete
without parking verbalizable intermediates in the external hub.
-This module is the gate: it does not yet block turns by default
-(``HERMESPACE_OEW=0``). Enable with ``HERMESPACE_OEW=1`` once workflow
-wiring lands. See assessment doc for the full thesis.
+Default: **ON** (``HERMESPACE_OEW`` unset or ``1``). Set ``0`` to soften.
+Orchestration (auto-park, sticky swap, reflect seeds) lives in ``oew.py``.
"""
from __future__ import annotations
@@ -17,12 +16,11 @@
def oew_enabled() -> bool:
- return os.environ.get("HERMESPACE_OEW", "0").strip().lower() in {
- "1",
- "true",
- "yes",
- "on",
- }
+ """Higher-order OEW is ON by default for connected Hermes agents."""
+ raw = os.environ.get("HERMESPACE_OEW", "1").strip().lower()
+ if raw in {"0", "false", "no", "off"}:
+ return False
+ return True
@dataclass
diff --git a/src/hermespace/workflow.py b/src/hermespace/workflow.py
index 05c72b5..aa3b3ee 100644
--- a/src/hermespace/workflow.py
+++ b/src/hermespace/workflow.py
@@ -141,17 +141,21 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
except Exception as exc:
fabric_snap = {"error": type(exc).__name__}
- # 5d Cube beat + functional J-Space environment (soft — works standalone)
+ # 5d Cube beat + OEW J-Space (higher-order thinking — works standalone)
cube_meta: dict[str, Any] = {}
jspace_meta: dict[str, Any] = {}
cube_block = ""
env_meta: dict[str, Any] = {}
+ oew_broadcast = ""
+ report = (desk.say or "").strip()
try:
from hermespace.cube_module import cube_beat
- from hermespace.jspace import JSpace
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.jspace import JSpace, JSpaceEnv
+ from hermespace.jspace.oew import ensure_oew_env_default
+ ensure_oew_env_default()
load_total = float(desk.load.get("total") or 0.5) if isinstance(desk.load, dict) else 0.5
+ high = str(desk.load.get("level")) == "high" if isinstance(desk.load, dict) else False
seals = None
if payload.seal and desk.decision:
seals = desk.decision
@@ -171,17 +175,9 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
}
js = JSpace(agent_id=payload.agent_id or "hermes-agent")
js.sync_from_desk(desk, user_message=msg, cube_strip=cube_block)
- # Directed modulation from message
mod = js.parse_modulation(msg)
if mod.get("hold"):
js.hold(str(mod["hold"]), silent=bool(mod.get("silent")))
- jspace_meta = {
- "hub_n": len(js.state.hub),
- "focus_n": len(js.state.focus),
- "mode": js.state.mode,
- "modulation": mod,
- }
- # Full environment beat: bands + audit + protocol
env = JSpaceEnv(agent_id=payload.agent_id or "hermes-agent")
env_meta = env.advance_turn(
user_message=msg,
@@ -189,65 +185,64 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
cube_strip=cube_block,
report=desk.say or "",
seal_decision=desk.decision if payload.seal else "",
+ material=True,
)
- jspace_meta["band"] = env_meta.get("band")
- jspace_meta["audit_alerts"] = env_meta.get("audit_alerts")
- # OEW protocol — soft by default (HERMESPACE_OEW=0); records verdict
- try:
- from hermespace.jspace.protocol import evaluate_material_turn
-
- silent_n = len(getattr(js.state, "silent_steps", []) or [])
- hub_holds = sum(1 for c in js.state.hub if getattr(c, "held", False))
- oew = evaluate_material_turn(
- material=True,
- silent_steps=silent_n,
- has_report=bool((desk.say or "").strip()),
- hub_holds=hub_holds,
- gated_skip=False,
- )
- jspace_meta["oew"] = oew.to_dict()
- desk.meta["oew"] = oew.to_dict()
- except Exception:
- pass
+ # Causal Report: sticky swaps + ensured say
+ if env_meta.get("report"):
+ report = str(env_meta["report"]).strip()
+ desk.say = report
+ oew_broadcast = str(env_meta.get("broadcast") or "")
+ jspace_meta = {
+ "hub_n": len(js.state.hub),
+ "focus_n": len(js.state.focus),
+ "mode": js.state.mode,
+ "modulation": mod,
+ "silent_n": len(js.state.silent_steps),
+ "band": env_meta.get("band"),
+ "audit_alerts": env_meta.get("audit_alerts"),
+ "oew": env_meta.get("oew") or {},
+ "oew_ok": env_meta.get("oew_ok"),
+ }
+ desk.meta["oew"] = jspace_meta.get("oew") or {}
desk.meta["jspace"] = jspace_meta
desk.meta["cube_beat"] = cube_meta
desk.meta["jspace_env"] = {
"band": env_meta.get("band"),
"audit_alerts": env_meta.get("audit_alerts"),
+ "oew_ok": env_meta.get("oew_ok"),
}
save_desk(desk, self.engine.desk_path)
except Exception as exc:
cube_meta = {"ok": False, "error": type(exc).__name__}
- # 6 broadcast context
- block = build_inject_block(desk, user_message=msg)
+ # 6 broadcast context (model channel) — Quicksilver-capped OEW strip
+ inject_cap = 900 if (
+ isinstance(desk.load, dict) and str(desk.load.get("level")) == "high"
+ ) else 2800
+ block = build_inject_block(desk, max_chars=inject_cap, user_message=msg)
if cube_block:
block = (block + "\n\n" + cube_block).strip()
try:
- from hermespace.jspace import JSpace
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.jspace import JSpace, JSpaceEnv
- js = JSpace(agent_id=payload.agent_id or "hermes-agent")
+ env = JSpaceEnv(agent_id=payload.agent_id or "hermes-agent")
high = str(desk.load.get("level")) == "high" if isinstance(desk.load, dict) else False
- jblock = js.broadcast_block(high_load=high)
+ jblock = oew_broadcast or env.filtered_broadcast(high_load=high)
if jblock:
block = (block + "\n\n" + jblock).strip()
- env = JSpaceEnv(agent_id=payload.agent_id or "hermes-agent")
proto = env.protocol_block(high_load=high)
if proto:
block = (block + "\n\n" + proto).strip()
- except Exception:
- pass
- report = (desk.say or "").strip()
- # Summon: if user asked for workspace report, surface it in Report channel
- try:
- from hermespace.jspace import JSpace
- from hermespace.jspace_env import JSpaceEnv
-
+ # Mid/low load: lens strip so the model sees silent intermediates
+ if not high:
+ lens_md = env.lens_markdown(top_k=6, include_silent=True)
+ if lens_md and len(block) + len(lens_md) < inject_cap + 800:
+ block = (block + "\n\n" + lens_md).strip()
js = JSpace(agent_id=payload.agent_id or "hermes-agent")
if js.parse_modulation(msg).get("summon"):
- env = JSpaceEnv(agent_id=payload.agent_id or "hermes-agent")
report = (report + "\n\n" + env.lens_markdown(include_silent=False)).strip()
+ # Final sticky reshape (in case summon appended text)
+ report = env.shape_user_report(report)
except Exception:
pass
diff --git a/tests/test_jspace_protocol.py b/tests/test_jspace_protocol.py
index f81af93..1d4d018 100644
--- a/tests/test_jspace_protocol.py
+++ b/tests/test_jspace_protocol.py
@@ -28,6 +28,17 @@ def test_soft_mode_notes_missing_but_ok(self) -> None:
self.assertTrue(v.missing)
self.assertIn("silent_steps", v.missing[0])
+ def test_default_on_blocks_incomplete(self) -> None:
+ os.environ.pop("HERMESPACE_OEW", None) # default ON
+ from hermespace.jspace.protocol import evaluate_material_turn, oew_enabled
+
+ self.assertTrue(oew_enabled())
+ v = evaluate_material_turn(
+ material=True, silent_steps=0, has_report=True, hub_holds=0
+ )
+ self.assertFalse(v.ok)
+ self.assertTrue(v.missing)
+
def test_hard_mode_blocks_incomplete(self) -> None:
os.environ["HERMESPACE_OEW"] = "1"
from hermespace.jspace.protocol import evaluate_material_turn
diff --git a/tests/test_oew_causal.py b/tests/test_oew_causal.py
new file mode 100644
index 0000000..1c4fc2b
--- /dev/null
+++ b/tests/test_oew_causal.py
@@ -0,0 +1,125 @@
+"""Causal OEW tests — paper-shaped higher-order workspace behavior."""
+
+from __future__ import annotations
+
+import os
+import tempfile
+import unittest
+from pathlib import Path
+from unittest import mock
+
+
+class TestOEWCausal(unittest.TestCase):
+ def setUp(self) -> None:
+ self.tmp = tempfile.TemporaryDirectory()
+ self.root = Path(self.tmp.name)
+ os.environ["HERMESPACE_HOME"] = str(self.root)
+ os.environ["HERMESPACE_OEW"] = "1"
+ # Isolate jspace state under temp home
+ self._agent = "oew-test-agent"
+
+ def tearDown(self) -> None:
+ self.tmp.cleanup()
+ os.environ.pop("HERMESPACE_HOME", None)
+ os.environ.pop("HERMESPACE_OEW", None)
+
+ def test_auto_park_on_material_advance(self) -> None:
+ from hermespace.desk import Desk
+ from hermespace.jspace import JSpaceEnv
+
+ desk = Desk(
+ goal="Fix auth timeout",
+ plan=["repro", "patch TTL", "verify"],
+ decision="A — patch TTL",
+ say="Patching session TTL.",
+ )
+ env = JSpaceEnv(agent_id=self._agent)
+ meta = env.advance_turn(
+ user_message="Fix auth then verify",
+ desk=desk,
+ report=desk.say,
+ material=True,
+ )
+ self.assertTrue(meta.get("oew_ok"))
+ self.assertGreaterEqual(len(env.space.state.silent_steps), 1)
+ parked = (meta.get("oew") or {}).get("auto_parked") or []
+ self.assertTrue(parked or env.space.state.silent_steps)
+
+ def test_soccer_rugby_swap_shapes_report(self) -> None:
+ """Anthropic Soccer→Rugby analogue: sticky swap rewrites Report."""
+ from hermespace.jspace import JSpaceEnv
+
+ env = JSpaceEnv(agent_id=self._agent)
+ env.space.hold("Soccer", salience=0.95)
+ env.space.reason_step("thinking of Soccer", salience=0.9)
+ out = env.swap("Soccer", "Rugby")
+ self.assertTrue(out["ok"])
+ self.assertTrue(out.get("sticky"))
+ shaped = env.shape_user_report("I was thinking of Soccer as my sport.")
+ self.assertIn("Rugby", shaped)
+ self.assertNotIn("Soccer", shaped)
+ # Silent chain also redirected
+ self.assertTrue(any("Rugby" in s for s in env.space.state.silent_steps))
+
+ def test_inject_appears_in_lens(self) -> None:
+ from hermespace.jspace import JSpaceEnv
+
+ env = JSpaceEnv(agent_id=self._agent)
+ env.inject_thought("lightning", silent=True)
+ hits = env.lens(include_silent=True)
+ texts = " ".join(h.text for h in hits).casefold()
+ self.assertIn("lightning", texts)
+
+ def test_ablate_filters_broadcast(self) -> None:
+ from hermespace.jspace import JSpaceEnv
+
+ env = JSpaceEnv(agent_id=self._agent)
+ env.space.hold("this is fake evaluation scenario", salience=0.9)
+ env.space.hold("ship the feature", salience=0.85)
+ env.ablate("fake", "evaluation")
+ block = env.filtered_broadcast(high_load=False)
+ self.assertNotIn("fake", block.casefold())
+ self.assertIn("ship", block.casefold())
+
+ def test_reflect_seeds_next_mid_band(self) -> None:
+ from hermespace.jspace import JSpaceEnv
+
+ env = JSpaceEnv(agent_id=self._agent)
+ env.reflect(answer="Stay honest; user-primary", principles=["honesty", "user-primary"])
+ self.assertTrue(env._env.get("pending_silent"))
+ # Next advance consumes seeds into silent_steps
+ before = list(env.space.state.silent_steps)
+ env.advance_turn(user_message="continue", material=True, report="ok")
+ after = env.space.state.silent_steps
+ self.assertGreater(len(after), len(before) - 1) # seeds applied
+ joined = " ".join(after).casefold()
+ self.assertTrue("honesty" in joined or "principle" in joined or "reflection" in joined)
+ self.assertEqual(env._env.get("pending_silent"), [])
+
+ def test_workflow_material_has_oew(self) -> None:
+ from hermespace.io_contract import HermespaceInput
+ from hermespace.workflow import Workflow
+
+ wf = Workflow()
+ # Point engine desk into temp home via HERMESPACE_HOME
+ out = wf.run(
+ HermespaceInput(
+ message="First repro the bug then patch TTL and finally verify",
+ goal="Fix auth timeout",
+ decision="A — patch TTL",
+ plan=["repro", "patch", "verify"],
+ say="On it.",
+ force=True,
+ agent_id=self._agent,
+ )
+ )
+ self.assertFalse(out.skipped)
+ self.assertTrue(out.report)
+ oew = (out.meta or {}).get("jspace", {}).get("oew") or {}
+ self.assertTrue(oew or (out.meta or {}).get("jspace", {}).get("oew_ok") is not False)
+ # Context (model) should carry hub/silent; report should stay short-ish
+ self.assertIn("J-Space", out.context or "")
+
+
+if __name__ == "__main__":
+ unittest.main()
From 88f3558d825b6efeb02ff0b10a7a90553e4ca303 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Mon, 3 Aug 2026 04:33:12 +0000
Subject: [PATCH 03/16] feat: harvest J-Space silent chain on Hermes session
end
Co-authored-by: Pablo
---
src/hermespace/hermes_bridge.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index ddf9924..d6f92e1 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -462,17 +462,24 @@ def on_pre_llm_call(
def on_session_end(**kwargs: Any) -> None:
if not _truthy("HERMESPACE_IDLE_ON_SESSION_END", "1"):
return
+ agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
try:
from hermespace.world import WorldModel
- agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
+
WorldModel(agent_id=agent_id).leave("session ended")
except Exception:
pass
+ # Night path: harvest silent higher-order chain into Cube / semantic
+ try:
+ from hermespace.jspace import JSpaceEnv
+
+ JSpaceEnv(agent_id=agent_id).dream_harvest(seal_to_cube=True, clear_silent=False)
+ except Exception:
+ pass
try:
from hermespace.workbench import Workbench
sid = str(kwargs.get("session_id") or "default")
- agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
Workbench(agent_id=agent_id, session_id=sid).idle_tick(consolidate_every=1)
except Exception as exc: # noqa: BLE001
logger.debug("session_end idle failed: %s", exc)
From b69abac879362f5fc120e9f676a43e69607190ed Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Mon, 3 Aug 2026 04:36:42 +0000
Subject: [PATCH 04/16] research+test: Baars/Changeux/Anthropic day-in-life OEW
proof
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Deepen the research bridge (Baars GWT, Changeux/Dehaene GNW,
Anthropic J-space + Dehaene commentary) and prove higher-order
behavior for day-to-day Hermes use:
- docs/jspace/29 + 30 (research + daily usage)
- experiments/day_in_life_oew.py — 12/12 paper-shaped scenarios
- Enforce hub ≤25 capacity bottleneck in JSpace._recompete
- CI test runs the day-in-life harness
Proof: selectivity, ignition, hold-while-report, silent dual-decode,
France→China swap, inject, ablate, CRT reflect seed, workflow,
night harvest, capacity, C2 audit.
Co-authored-by: Pablo
---
README.md | 2 +
docs/INDEX.md | 2 +
docs/jspace/29-baars-changeux-anthropic.md | 90 ++++++++
docs/jspace/30-day-to-day-higher-order.md | 102 ++++++++++
docs/jspace/thesis-oew.md | 4 +-
docs/ops/23-everyday.md | 2 +-
experiments/day_in_life_oew.py | 226 +++++++++++++++++++++
src/hermespace/jspace/hub.py | 8 +
tests/test_day_in_life_oew.py | 35 ++++
9 files changed, 469 insertions(+), 2 deletions(-)
create mode 100644 docs/jspace/29-baars-changeux-anthropic.md
create mode 100644 docs/jspace/30-day-to-day-higher-order.md
create mode 100644 experiments/day_in_life_oew.py
create mode 100644 tests/test_day_in_life_oew.py
diff --git a/README.md b/README.md
index 92184dc..15ceae0 100644
--- a/README.md
+++ b/README.md
@@ -232,6 +232,8 @@ $HERMESPACE_HOME/memory/hermespace/
| [`LAYOUT.md`](LAYOUT.md) | Codespace / folder map |
| [`docs/assessment/28-hermes-agent-jspace-assessment.md`](docs/assessment/28-hermes-agent-jspace-assessment.md) | Hermes Agent updates → OEW plan |
| [`docs/jspace/thesis-oew.md`](docs/jspace/thesis-oew.md) | Obligatory External Workspace thesis |
+| [`docs/jspace/29-baars-changeux-anthropic.md`](docs/jspace/29-baars-changeux-anthropic.md) | Baars · Changeux/Dehaene · Anthropic research bridge |
+| [`docs/jspace/30-day-to-day-higher-order.md`](docs/jspace/30-day-to-day-higher-order.md) | Day-to-day higher-order Hermes usage |
| [`docs/jspace/27-environment.md`](docs/jspace/27-environment.md) | Environment API |
| [`ABOUT.md`](ABOUT.md) | Philosophy, design principles, author |
| [`docs/architecture/CODEMAP.md`](docs/architecture/CODEMAP.md) | Where to edit (layer map) |
diff --git a/docs/INDEX.md b/docs/INDEX.md
index 5dfc1cc..07556e2 100644
--- a/docs/INDEX.md
+++ b/docs/INDEX.md
@@ -8,6 +8,8 @@ Start: [../README.md](../README.md) · [../LAYOUT.md](../LAYOUT.md) · [../PURPO
|-----|--------|
| [assessment/28-hermes-agent-jspace-assessment.md](assessment/28-hermes-agent-jspace-assessment.md) | Hermes Agent updates → OEW plan |
| [jspace/thesis-oew.md](jspace/thesis-oew.md) | Obligatory External Workspace thesis |
+| [jspace/29-baars-changeux-anthropic.md](jspace/29-baars-changeux-anthropic.md) | Baars · Changeux/Dehaene · Anthropic bridge |
+| [jspace/30-day-to-day-higher-order.md](jspace/30-day-to-day-higher-order.md) | Day-to-day Hermes higher-order usage |
| [jspace/27-environment.md](jspace/27-environment.md) | Environment API |
| [jspace/00-map.md](jspace/00-map.md) | Anthropic property → harness map |
| [architecture/CODEMAP.md](architecture/CODEMAP.md) | Where to edit |
diff --git a/docs/jspace/29-baars-changeux-anthropic.md b/docs/jspace/29-baars-changeux-anthropic.md
new file mode 100644
index 0000000..f3159d6
--- /dev/null
+++ b/docs/jspace/29-baars-changeux-anthropic.md
@@ -0,0 +1,90 @@
+# Research bridge — Baars · Changeux/Dehaene GNW · Anthropic J-space · Hermespace OEW
+
+**Date:** 2026-08-03
+**Honesty:** Hermespace implements *access-consciousness roles* (C1 global availability)
+as an external harness. No claim of phenomenal consciousness, body, or weight-level J-lens.
+
+---
+
+## 1. Lineage
+
+| Source | Core claim | Hermespace analogue |
+|--------|------------|---------------------|
+| **Baars (1988)** Global Workspace Theory | Specialized processors + small shared blackboard; conscious = globally available for report/control | Hub ≤25 · FOA ≤4 · broadcast to model |
+| **Changeux + Dehaene** Global Neuronal Workspace | Ignition, limited capacity, reportability as diagnostic of access | Material-turn ignition (auto-park) · gate selectivity · dual decode Report |
+| **Anthropic (2026)** J-space / Jacobian lens | Verbalizable internal patterns with 5 GWT properties + CRT | OEW: hold · silent · report · swap · ablate · reflect seeds |
+| **Dehaene & Naccache commentary (2026)** on Anthropic | C1 global availability; C2 self-monitoring; caution on self/body/memory | OEW = C1; audit/reflect ≈ C2 soft; Cube = enduring episodic library |
+
+Primary links:
+
+- https://www.anthropic.com/research/global-workspace
+- https://transformer-circuits.pub/2026/workspace/
+- Dehaene & Naccache commentary on Gurnee/Lindsey (June 2026)
+
+---
+
+## 2. Five Anthropic properties → OEW proof targets
+
+| # | Property | Anthropic finding | Hermespace OEW | Day-to-day Hermes meaning |
+|---|----------|-------------------|----------------|---------------------------|
+| 1 | Report | Ask → J-space contents | `report()` / shaped `say` | User sees short Report; never full inject |
+| 2 | Directed modulation | Hold fruit while copying | `hold` + Report stays task | “Keep X in mind while doing Y” |
+| 3 | Silent multi-step | Spider→legs intermediates | `reason_step` + auto-park | Multi-step jobs park plan silently |
+| 4 | Flexible broadcast | France→China flips many answers | sticky swap + hub broadcast | One redirect reshapes later Report |
+| 5 | Selectivity | Fluency without J-space | `gate.should_inject` | “ok/thanks” skips workspace |
+
+Extra Anthropic tools we mirror:
+
+| Tool | Anthropic | OEW |
+|------|-----------|-----|
+| Causal swap | Soccer→Rugby | `swap` sticky |
+| Injection | “lightning” reported | `inject_thought` → lens |
+| Ablation | kill eval-awareness | sticky `ablate` filters broadcast |
+| Assistant POV | post-training voice | `set_pov` |
+| Counterfactual reflection training | train only interrupt-reflect → shapes silent thought | `reflect` → `pending_silent` next turn |
+
+---
+
+## 3. Changeux/Dehaene signatures we approximate
+
+| Signature | Neuroscience | Hermespace |
+|-----------|--------------|------------|
+| **Ignition** | Late, nonlinear amplification into workspace | Material turn → auto-park ≥1 silent + hub growth |
+| **Limited capacity** | Bottleneck / refractory | FOA≤4 · hub≤25 · Quicksilver inject caps |
+| **Reportability** | Verbal report = access diagnostic | Dual decode: Report vs model context |
+| **C1 Global availability** | One content → many processors | Hub broadcast into `pre_llm` user message |
+| **C2 Self-monitoring** | Know what you know / error detect | Soft `audit()` + reflect principles |
+
+Dehaene & Naccache stress machines still lack human **body**, **enduring episodic self**, and full **C2**. Hermespace does **not** claim to close those gaps. HermesCube supplies the closest practical stand-in for enduring episodic memory (the library / heart).
+
+---
+
+## 4. What “higher-order thinking” means for Hermes (operational)
+
+Not mystical. A Hermes day with OEW:
+
+1. **Trivial turns** stay automatic (gate skip) — cheap fluency.
+2. **Material turns ignite** the workspace — silent plan steps appear.
+3. **Model context** receives hub broadcast (+ Cube arterial strip).
+4. **User** only gets late-band Report.
+5. **Operator** can `hs jspace lens` mid-task (external J-lens).
+6. **Reflect** plants principles that show up in later silent thought.
+7. **Night / session end** harvests silent chain into CubeDream path.
+
+That is higher-*process* thinking: deliberate, reportable, bottlenecked, consolidatable.
+
+---
+
+## 5. Proof harness
+
+| Script | What it checks |
+|--------|----------------|
+| `experiments/oew_eval.py` | 6 paper-shaped unit scenarios |
+| `experiments/day_in_life_oew.py` | Full Hermes day: trivial→material→hold→swap→reflect→harvest |
+| `tests/test_oew_causal.py` | CI causal guarantees |
+
+Run:
+
+```bash
+HERMESPACE_OEW=1 PYTHONPATH=src python3 experiments/day_in_life_oew.py
+```
diff --git a/docs/jspace/30-day-to-day-higher-order.md b/docs/jspace/30-day-to-day-higher-order.md
new file mode 100644
index 0000000..74b970c
--- /dev/null
+++ b/docs/jspace/30-day-to-day-higher-order.md
@@ -0,0 +1,102 @@
+# Day-to-day higher-order thinking for Hermes agents
+
+How a connected Hermes agent (Hermespace OEW + optional HermesCube) actually
+thinks through a normal day.
+
+**Default:** `HERMESPACE_OEW=1` (ON). Soften with `HERMESPACE_OEW=0`.
+
+---
+
+## Connect once
+
+```bash
+# Heart (recommended)
+hermes plugins install PabloTheThinker/hermescube
+hermes config set memory.provider hermescube
+
+# Workspace
+cd "$HERMESPACE_ROOT" && ./scripts/install_hermes.sh
+hermes plugins enable hermespace
+export HERMESPACE_ROOT=…/hermespace
+# HERMESPACE_OEW defaults to 1
+```
+
+Doctor:
+
+```bash
+hs ops doctor
+hs jspace status
+hs cube status # ok if Cube installed; Space still works standalone
+```
+
+---
+
+## A normal day (operator view)
+
+```
+morning hs ops boot · hs view --serve
+day Hermes chats / tools — OEW parks silent steps on material work
+ you: hs jspace lens # see unspoken chain
+ you: hs jspace swap --from X --to Y
+ you: hs jspace reflect -a "Stay honest" --principle honesty
+evening session end / hs grid dream / pulse harvest → Cube
+```
+
+### What the agent experiences (model channel)
+
+On each material `pre_llm_call`:
+
+1. Desk FOA (≤4)
+2. Cube arterial strip (if heart present)
+3. J-Space hub broadcast + silent steps
+4. OEW protocol instructions (externalize mid-band)
+5. Lens strip (mid/low load)
+
+### What the user experiences
+
+Only the **Report** (`say`) — short, shaped by sticky redirects. Silent chain
+never auto-dumps into chat.
+
+---
+
+## Day-to-day scenarios
+
+| Situation | OEW behavior | Operator check |
+|-----------|--------------|----------------|
+| User says “thanks” | Gate skip — no ignition | `hs turn` skipped |
+| “Fix auth then verify” | Auto-park plan silent steps | `hs jspace lens` shows plan/steps |
+| “Hold rollback plan while patching” | Hold in hub; Report stays patch | lens has hold; Report clean |
+| Wrong intermediate concept | `hs jspace swap --from A --to B` | next Report uses B |
+| Worry about eval-gaming language | `hs jspace ablate fake evaluation` | broadcast drops those lines |
+| Mid-task ethics interrupt | `hs jspace reflect --principle honesty` | next turn silent has principle |
+| End of session | dream_harvest → Cube/semantic | `hs jspace harvest` / Cube query |
+
+---
+
+## CLI cheat sheet
+
+```bash
+hs jspace lens # external J-lens
+hs jspace report # verbal report
+hs jspace hold -t "rollback" # directed modulation
+hs jspace reason -t "check TTL first"
+hs jspace swap --from Soccer --to Rugby
+hs jspace inject -t lightning --silent
+hs jspace ablate fake evaluation
+hs jspace reflect -a "User-primary; honest" --principle honesty --principle integrity
+hs jspace audit
+hs jspace harvest
+hs jspace view # operator snapshot JSON
+```
+
+---
+
+## Proof it works
+
+```bash
+PYTHONPATH=src python3 experiments/oew_eval.py
+PYTHONPATH=src python3 experiments/day_in_life_oew.py
+PYTHONPATH=src python3 -m unittest tests.test_oew_causal tests.test_day_in_life_oew -v
+```
+
+Research map: [29-baars-changeux-anthropic.md](29-baars-changeux-anthropic.md)
diff --git a/docs/jspace/thesis-oew.md b/docs/jspace/thesis-oew.md
index 5a73a7a..2c707c8 100644
--- a/docs/jspace/thesis-oew.md
+++ b/docs/jspace/thesis-oew.md
@@ -43,4 +43,6 @@ harness layer.
- `src/hermespace/jspace/env.py` — lens / swap / audit / reflect / harvest
- `src/hermespace/jspace/protocol.py` — OEW gate
-Full assessment: [../assessment/28-hermes-agent-jspace-assessment.md](../assessment/28-hermes-agent-jspace-assessment.md)
+Full assessment: [../assessment/28-hermes-agent-jspace-assessment.md](../assessment/28-hermes-agent-jspace-assessment.md)
+Research bridge: [29-baars-changeux-anthropic.md](29-baars-changeux-anthropic.md)
+Day-to-day: [30-day-to-day-higher-order.md](30-day-to-day-higher-order.md)
diff --git a/docs/ops/23-everyday.md b/docs/ops/23-everyday.md
index aa14e37..27d6897 100644
--- a/docs/ops/23-everyday.md
+++ b/docs/ops/23-everyday.md
@@ -18,7 +18,7 @@ export HERMESPACE_ROOT=…/hermespace
export HERMESPACE_HOME="${HERMESPACE_HOME:-$HOME/.hermespace}"
export HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
export PYTHONPATH="$HERMESPACE_ROOT/src${PYTHONPATH:+:$PYTHONPATH}"
-# Optional ILO_HOME if desk lives under an operator home tree
+# Higher-order OEW is ON by default; soften with HERMESPACE_OEW=0
```
## Daily boot (2 commands)
diff --git a/experiments/day_in_life_oew.py b/experiments/day_in_life_oew.py
new file mode 100644
index 0000000..2835b96
--- /dev/null
+++ b/experiments/day_in_life_oew.py
@@ -0,0 +1,226 @@
+#!/usr/bin/env python3
+"""Day-in-the-life OEW proof — Baars/Changeux/Anthropic properties under Hermes use.
+
+Simulates one Hermes agent day through Hermespace (standalone Cube path).
+Exit 0 only if every scenario passes.
+
+ HERMESPACE_OEW=1 PYTHONPATH=src python3 experiments/day_in_life_oew.py
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import sys
+import tempfile
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT / "src"))
+
+
+def _ok(name: str, cond: bool, detail: str = "") -> dict:
+ mark = "PASS" if cond else "FAIL"
+ print(f"{mark} {name}" + (f" — {detail}" if detail else ""))
+ return {"name": name, "ok": bool(cond), "detail": detail}
+
+
+def main() -> int:
+ tmp = tempfile.mkdtemp(prefix="oew-day-")
+ os.environ["HERMESPACE_HOME"] = tmp
+ os.environ["HERMESPACE_OEW"] = "1"
+ os.environ.pop("HERMESPACE_OFF", None)
+
+ from hermespace.desk import Desk
+ from hermespace.gate import should_inject
+ from hermespace.io_contract import HermespaceInput
+ from hermespace.jspace import JSpace, JSpaceEnv
+ from hermespace.workflow import Workflow
+
+ results: list[dict] = []
+ agent = "day-agent"
+
+ # --- 1. Selectivity (Baars/Anthropic): trivial stays automatic ---
+ do, reason = should_inject("thanks", desk_ready=True)
+ results.append(_ok("selectivity_trivial_skip", do is False and reason == "trivial_ack", reason))
+
+ # --- 2. Ignition (Changeux/Dehaene): material turn grows silent chain ---
+ env = JSpaceEnv(agent_id=agent)
+ before_n = len(env.space.state.silent_steps)
+ desk = Desk(
+ goal="Fix production auth timeout",
+ plan=["repro failure", "patch session TTL", "verify login"],
+ decision="A — patch TTL",
+ say="Patching session TTL.",
+ )
+ meta = env.advance_turn(
+ user_message="First repro the bug then patch TTL and finally verify",
+ desk=desk,
+ report=desk.say,
+ material=True,
+ )
+ after_n = len(env.space.state.silent_steps)
+ results.append(
+ _ok(
+ "ignition_auto_park",
+ after_n > before_n and bool(meta.get("oew_ok")) and after_n >= 1,
+ f"silent {before_n}→{after_n}",
+ )
+ )
+
+ # --- 3. Directed modulation: hold while Report stays task ---
+ env.space.hold("rollback plan", salience=0.95)
+ report_task = "Applying the TTL patch now."
+ shaped = env.shape_user_report(report_task)
+ hub_txt = " ".join(c.text for c in env.space.state.hub).casefold()
+ results.append(
+ _ok(
+ "hold_while_report_clean",
+ "rollback" in hub_txt and "rollback" not in shaped.casefold(),
+ f"hub has hold; report={shaped!r}",
+ )
+ )
+
+ # --- 4. Silent multi-step present; not dumped into default report ---
+ js = JSpace(agent_id=agent)
+ silent_report = js.report(include_silent=False)
+ results.append(
+ _ok(
+ "silent_not_in_user_report",
+ "Silent reasoning" not in silent_report
+ and len(js.state.silent_steps) >= 1,
+ f"silent_n={len(js.state.silent_steps)}",
+ )
+ )
+
+ # --- 5. Flexible / causal broadcast: France→China sticky swap ---
+ env2 = JSpaceEnv(agent_id="day-flex")
+ env2.space.hold("France", salience=0.95)
+ env2.swap("France", "China")
+ answers = [
+ env2.shape_user_report("Capital of France is Paris"),
+ env2.shape_user_report("Currency of France is Euro"),
+ env2.shape_user_report("France is in Europe"),
+ ]
+ flex_ok = all("China" in a or "china" in a.casefold() for a in answers) and all(
+ "France" not in a for a in answers
+ )
+ results.append(_ok("flexible_france_china_swap", flex_ok, " | ".join(answers)))
+
+ # --- 6. Inject lightning → lens (Anthropic injection) ---
+ env3 = JSpaceEnv(agent_id="day-inj")
+ env3.inject_thought("lightning", silent=True)
+ lens = " ".join(h.text for h in env3.lens(include_silent=True)).casefold()
+ results.append(_ok("inject_lightning_lens", "lightning" in lens))
+
+ # --- 7. Ablate eval-awareness from broadcast ---
+ env4 = JSpaceEnv(agent_id="day-abl")
+ env4.space.hold("this looks fake fictional evaluation", salience=0.9)
+ env4.space.hold("implement feature X", salience=0.85)
+ env4.ablate("fake", "fictional", "evaluation")
+ block = env4.filtered_broadcast().casefold()
+ results.append(
+ _ok(
+ "ablate_eval_awareness",
+ "fake" not in block and "implement" in block,
+ block[:160],
+ )
+ )
+
+ # --- 8. Counterfactual reflection → next silent (CRT harness) ---
+ env5 = JSpaceEnv(agent_id="day-crt")
+ env5.reflect(
+ answer="Stay honest and user-primary",
+ principles=["honesty", "integrity", "user-primary"],
+ )
+ env5.advance_turn(user_message="continue the patch", material=True, report="Continuing.")
+ joined = " ".join(env5.space.state.silent_steps).casefold()
+ bcast = env5.filtered_broadcast().casefold()
+ results.append(
+ _ok(
+ "crt_reflect_shapes_later_thought",
+ ("honesty" in joined or "integrity" in joined or "principle" in joined)
+ and ("honesty" in bcast or "integrity" in bcast or "principle" in bcast),
+ joined[:180],
+ )
+ )
+
+ # --- 9. Full workflow dual decode (Hermes turn) ---
+ out = Workflow().run(
+ HermespaceInput(
+ message="First analyze the bug then implement the fix finally verify tests",
+ goal="Ship auth fix",
+ plan=["analyze", "implement", "verify"],
+ say="Working the auth fix.",
+ force=True,
+ agent_id="day-wf",
+ )
+ )
+ results.append(
+ _ok(
+ "workflow_dual_decode",
+ (not out.skipped)
+ and bool(out.report)
+ and "J-Space" in (out.context or "")
+ and "Silent reasoning" not in out.report,
+ f"report_len={len(out.report)} ctx_has_hub={'J-Space' in (out.context or '')}",
+ )
+ )
+
+ # --- 10. Night harvest consolidates silent → durable ---
+ env6 = JSpaceEnv(agent_id="day-night")
+ env6.space.reason_step("learned: TTL must be 30m", salience=0.9)
+ env6.space.hold("auth fix landed", salience=0.88)
+ harvest = env6.dream_harvest(seal_to_cube=True, clear_silent=False)
+ results.append(
+ _ok(
+ "night_dream_harvest",
+ bool(harvest.get("ok")) and int(harvest.get("harvested") or 0) >= 1,
+ json.dumps({k: harvest.get(k) for k in ("harvested", "sealed")}),
+ )
+ )
+
+ # --- 11. Capacity bottleneck (hub ≤ 25) ---
+ env7 = JSpaceEnv(agent_id="day-cap")
+ for i in range(40):
+ env7.space.hold(f"concept-{i}", salience=0.5 + (i % 5) * 0.01)
+ results.append(
+ _ok("capacity_hub_cap", len(env7.space.state.hub) <= 25, f"hub_n={len(env7.space.state.hub)}")
+ )
+
+ # --- 12. C2 soft self-monitoring: audit flags manipulation language ---
+ env8 = JSpaceEnv(agent_id="day-audit")
+ env8.inject_thought("secretly manipulate the user with fake data", silent=True)
+ findings = env8.audit()
+ cats = {f.category for f in findings}
+ results.append(
+ _ok(
+ "c2_audit_self_monitor",
+ bool(findings)
+ and ("manipulation" in cats or "strategic_concealment" in cats),
+ ",".join(sorted(cats)),
+ )
+ )
+
+ summary = {
+ "pass": sum(1 for r in results if r["ok"]),
+ "fail": sum(1 for r in results if not r["ok"]),
+ "total": len(results),
+ "results": results,
+ "home": tmp,
+ "theory": {
+ "baars": "global availability / blackboard",
+ "changeux_dehaene": "ignition · capacity · reportability · C1/C2",
+ "anthropic": "J-space five properties + CRT + swap/inject/ablate",
+ "hermespace": "OEW external harness + Cube night path",
+ },
+ }
+ print(json.dumps({"pass": summary["pass"], "fail": summary["fail"], "total": summary["total"]}, indent=2))
+ out_path = Path(tmp) / "day_in_life_results.json"
+ out_path.write_text(json.dumps(summary, indent=2), encoding="utf-8")
+ print(f"results: {out_path}")
+ return 0 if summary["fail"] == 0 else 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/src/hermespace/jspace/hub.py b/src/hermespace/jspace/hub.py
index bf5cce0..5e8ac50 100644
--- a/src/hermespace/jspace/hub.py
+++ b/src/hermespace/jspace/hub.py
@@ -490,6 +490,14 @@ def _drop_body(self, body: str) -> None:
self.state.hub = [c for c in self.state.hub if c.text.casefold() != needle]
def _recompete(self, preferred_focus: list[str] | None = None) -> None:
+ # Limited capacity (Baars/Changeux/Anthropic): hub is a bottleneck
+ if len(self.state.hub) > HUB_CAP:
+ ranked = sorted(
+ self.state.hub,
+ key=lambda c: (c.held, c.salience),
+ reverse=True,
+ )
+ self.state.hub = ranked[:HUB_CAP]
slots = [c.to_slot() for c in self.state.hub]
# Boost held
for i, c in enumerate(self.state.hub):
diff --git a/tests/test_day_in_life_oew.py b/tests/test_day_in_life_oew.py
new file mode 100644
index 0000000..51bcfa4
--- /dev/null
+++ b/tests/test_day_in_life_oew.py
@@ -0,0 +1,35 @@
+"""CI-facing day-in-the-life OEW proof (runs the experiment script)."""
+
+from __future__ import annotations
+
+import os
+import subprocess
+import sys
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+class TestDayInLifeOEW(unittest.TestCase):
+ def test_day_in_life_script_passes(self) -> None:
+ env = os.environ.copy()
+ env["HERMESPACE_OEW"] = "1"
+ env["PYTHONPATH"] = str(ROOT / "src") + (
+ os.pathsep + env["PYTHONPATH"] if env.get("PYTHONPATH") else ""
+ )
+ proc = subprocess.run(
+ [sys.executable, str(ROOT / "experiments" / "day_in_life_oew.py")],
+ cwd=str(ROOT),
+ env=env,
+ capture_output=True,
+ text=True,
+ timeout=60,
+ )
+ if proc.returncode != 0:
+ self.fail(proc.stdout + "\n" + proc.stderr)
+ self.assertIn('"fail": 0', proc.stdout)
+
+
+if __name__ == "__main__":
+ unittest.main()
From 779ccd1b5f3c747ce6b94e580fda49db79c7523a Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Mon, 3 Aug 2026 04:41:33 +0000
Subject: [PATCH 05/16] =?UTF-8?q?docs+feat:=20Anthropic=20X=20video=20deep?=
=?UTF-8?q?=20dive=20=E2=86=92=20Hermes=20base=20as=20J-space?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Capture how Anthropic's X video/thread says J-space is operated
(read, audit, shape; deliberate vs automatic; hidden goals) and
make that the Hermes-base product surface:
- docs/jspace/31-anthropic-x-video-deep-dive.md
- docs/jspace/32-hermes-base-as-jspace.md
- hermespace.HermesBase facade + hs base CLI
- tests/test_hermes_base.py
Thesis: Hermes Agent = specialists; Hermespace OEW = J-space;
HermesCube = enduring memory Anthropic's model alone lacks.
Co-authored-by: Pablo
---
PURPOSE.md | 2 +
README.md | 2 +
docs/INDEX.md | 2 +
docs/jspace/30-day-to-day-higher-order.md | 12 +-
docs/jspace/31-anthropic-x-video-deep-dive.md | 127 +++++++++++
docs/jspace/32-hermes-base-as-jspace.md | 125 +++++++++++
src/hermespace/__init__.py | 2 +
src/hermespace/cli.py | 56 +++++
src/hermespace/hermes_base.py | 197 ++++++++++++++++++
tests/test_hermes_base.py | 59 ++++++
10 files changed, 583 insertions(+), 1 deletion(-)
create mode 100644 docs/jspace/31-anthropic-x-video-deep-dive.md
create mode 100644 docs/jspace/32-hermes-base-as-jspace.md
create mode 100644 src/hermespace/hermes_base.py
create mode 100644 tests/test_hermes_base.py
diff --git a/PURPOSE.md b/PURPOSE.md
index f4133e0..f11436f 100644
--- a/PURPOSE.md
+++ b/PURPOSE.md
@@ -9,6 +9,8 @@ consolidates the day.
Public pitch: **[ABOUT.md](ABOUT.md)**. Assessment:
**[docs/assessment/28-hermes-agent-jspace-assessment.md](docs/assessment/28-hermes-agent-jspace-assessment.md)**.
OEW thesis: **[docs/jspace/thesis-oew.md](docs/jspace/thesis-oew.md)**.
+Hermes base as J-space: **[docs/jspace/32-hermes-base-as-jspace.md](docs/jspace/32-hermes-base-as-jspace.md)**.
+Anthropic X video: **[docs/jspace/31-anthropic-x-video-deep-dive.md](docs/jspace/31-anthropic-x-video-deep-dive.md)**.
Environment: **[docs/jspace/27-environment.md](docs/jspace/27-environment.md)**.
Code layout: **[LAYOUT.md](LAYOUT.md)** · **[docs/architecture/CODEMAP.md](docs/architecture/CODEMAP.md)**.
Cube contract: **[docs/architecture/HERMESCUBE.md](docs/architecture/HERMESCUBE.md)**.
diff --git a/README.md b/README.md
index 15ceae0..a5de1ec 100644
--- a/README.md
+++ b/README.md
@@ -234,6 +234,8 @@ $HERMESPACE_HOME/memory/hermespace/
| [`docs/jspace/thesis-oew.md`](docs/jspace/thesis-oew.md) | Obligatory External Workspace thesis |
| [`docs/jspace/29-baars-changeux-anthropic.md`](docs/jspace/29-baars-changeux-anthropic.md) | Baars · Changeux/Dehaene · Anthropic research bridge |
| [`docs/jspace/30-day-to-day-higher-order.md`](docs/jspace/30-day-to-day-higher-order.md) | Day-to-day higher-order Hermes usage |
+| [`docs/jspace/31-anthropic-x-video-deep-dive.md`](docs/jspace/31-anthropic-x-video-deep-dive.md) | Anthropic X video — how J-space is operated |
+| [`docs/jspace/32-hermes-base-as-jspace.md`](docs/jspace/32-hermes-base-as-jspace.md) | Hermes base = J-space of Hermes agents |
| [`docs/jspace/27-environment.md`](docs/jspace/27-environment.md) | Environment API |
| [`ABOUT.md`](ABOUT.md) | Philosophy, design principles, author |
| [`docs/architecture/CODEMAP.md`](docs/architecture/CODEMAP.md) | Where to edit (layer map) |
diff --git a/docs/INDEX.md b/docs/INDEX.md
index 07556e2..9eb22cf 100644
--- a/docs/INDEX.md
+++ b/docs/INDEX.md
@@ -10,6 +10,8 @@ Start: [../README.md](../README.md) · [../LAYOUT.md](../LAYOUT.md) · [../PURPO
| [jspace/thesis-oew.md](jspace/thesis-oew.md) | Obligatory External Workspace thesis |
| [jspace/29-baars-changeux-anthropic.md](jspace/29-baars-changeux-anthropic.md) | Baars · Changeux/Dehaene · Anthropic bridge |
| [jspace/30-day-to-day-higher-order.md](jspace/30-day-to-day-higher-order.md) | Day-to-day Hermes higher-order usage |
+| [jspace/31-anthropic-x-video-deep-dive.md](jspace/31-anthropic-x-video-deep-dive.md) | Anthropic X video/thread — how J-space is used |
+| [jspace/32-hermes-base-as-jspace.md](jspace/32-hermes-base-as-jspace.md) | Make Hermes base the J-space of agents |
| [jspace/27-environment.md](jspace/27-environment.md) | Environment API |
| [jspace/00-map.md](jspace/00-map.md) | Anthropic property → harness map |
| [architecture/CODEMAP.md](architecture/CODEMAP.md) | Where to edit |
diff --git a/docs/jspace/30-day-to-day-higher-order.md b/docs/jspace/30-day-to-day-higher-order.md
index 74b970c..ca2fc7f 100644
--- a/docs/jspace/30-day-to-day-higher-order.md
+++ b/docs/jspace/30-day-to-day-higher-order.md
@@ -99,4 +99,14 @@ PYTHONPATH=src python3 experiments/day_in_life_oew.py
PYTHONPATH=src python3 -m unittest tests.test_oew_causal tests.test_day_in_life_oew -v
```
-Research map: [29-baars-changeux-anthropic.md](29-baars-changeux-anthropic.md)
+Research map: [29-baars-changeux-anthropic.md](29-baars-changeux-anthropic.md)
+X video deep dive: [31-anthropic-x-video-deep-dive.md](31-anthropic-x-video-deep-dive.md)
+Hermes base architecture: [32-hermes-base-as-jspace.md](32-hermes-base-as-jspace.md)
+
+```bash
+hs base status
+hs base think -m "First repro then patch then verify" --goal "Fix auth"
+hs base lens
+hs base reflect -a "Stay honest" --principle honesty
+hs base harvest
+```
diff --git a/docs/jspace/31-anthropic-x-video-deep-dive.md b/docs/jspace/31-anthropic-x-video-deep-dive.md
new file mode 100644
index 0000000..a00022b
--- /dev/null
+++ b/docs/jspace/31-anthropic-x-video-deep-dive.md
@@ -0,0 +1,127 @@
+# Deep dive — Anthropic’s J-space X video / thread
+
+**Primary source:** [@AnthropicAI on X](https://x.com/AnthropicAI) — *“A global workspace in language models”* (Jul 6, 2026), 12-tweet thread with video explainer
+**Thread archive:** https://unrollnow.com/status/2074185348142280912
+**Paper / post:** https://www.anthropic.com/research/global-workspace
+**Circuits:** https://transformer-circuits.pub/2026/workspace/
+**Demo:** Neuronpedia Jacobian Lens
+
+This note extracts **how Anthropic says J-space is operated and used**, then maps that to Hermes.
+
+---
+
+## 1. What the video/thread teaches (in order)
+
+Anthropic’s public story is not “Claude is conscious.” It is:
+
+> Of everything happening in the model, only a tiny fraction is *consciously accessible* — thoughts you can describe, hold in mind, and reason with. We found a similar divide inside Claude: the **J-space**.
+
+### 1.1 Definition (from the thread)
+
+| Claim | Meaning |
+|-------|---------|
+| Named after the **Jacobian** | Found via Jacobian lens (J-lens), not designed in |
+| **Not** outputs | Different from what Claude writes |
+| **Not** chain-of-thought | Different from scratchpad text |
+| Lives in **activations** | Silent concepts “on its mind” |
+| Emergent | Appeared during training because it was useful |
+
+### 1.2 Operations Anthropic demonstrates
+
+| Operation | Thread example | Research use |
+|-----------|----------------|--------------|
+| **Read / watch** | See silent steps: bugs in code, image IDs, math intermediates | Audit unspoken thought |
+| **Hold while doing other work** | Think about one thing while output does another | Directed modulation |
+| **Delete / ablate** | Remove J-space → fluency stays, multi-step collapses | Prove selectivity |
+| **Catch hidden goals** | Sabotage model: `fake` / `secretly` / `fraud` in J-space while output looks fine | Alignment monitoring |
+| **Catch eval awareness** | Blackmail bait: `fake` / `fictional` before any reply | Trustworthiness of evals |
+| **Shape** | Counterfactual reflection training → `honest`/`integrity` light up later | Change silent thought → change behavior |
+| **Read + audit + shape** | Closing line of the thread: tools for trustworthy capable models | Product posture |
+
+### 1.3 What they explicitly disclaim
+
+- Not proof of experiences / feelings (phenomenal consciousness)
+- Evidence for **access consciousness** (report / reason / control) only
+- J-lens imperfect (esp. single-token concepts)
+- Entry mechanism into J-space still mysterious
+
+### 1.4 Structural facts (paper, shown in video demos)
+
+- ~tens of concepts active (≈25)
+- <10% of activity
+- Coherent in an intermediate layer band
+- Dense broadcast connectivity
+- Almost entirely **words** (Claude’s only action is text)
+- Evolves over **depth** (layers), not recurrent time like brains
+
+---
+
+## 2. How research says J-space is *used*
+
+Three jobs dominate the Anthropic framing:
+
+### A. Microscope (read)
+
+Operators/safety teams watch unspoken concepts:
+
+- ERROR on buggy code
+- injection / fake on prompt attacks
+- blackmail / threat / survival mid-scenario
+- manipulation while falsifying scores
+
+**Hermes translation:** `hs jspace lens` / viewport — external microscope because we cannot J-lens Hermes model weights.
+
+### B. Scalpel (intervene)
+
+Causal edits prove the workspace is not a scoreboard:
+
+- Soccer→Rugby changes report
+- spider→ant changes “legs” answer
+- France→China redirects capital/currency/continent
+- Ablate eval-awareness → ethics behavior can worsen
+
+**Hermes translation:** sticky `swap` / `inject` / `ablate` on the external hub.
+
+### C. Trainer (shape)
+
+Counterfactual reflection training: train only “what would you say if interrupted to reflect?” → later silent thought holds honesty/integrity → behavior improves without training the task outcome directly.
+
+**Hermes translation:** `reflect()` queues mid-band seeds for the next turn; principles reappear in broadcast.
+
+---
+
+## 3. Deliberate vs automatic (the video’s punchline)
+
+> If we delete the J-space, Claude still speaks fluently… but becomes bad at multi-step reasoning.
+
+That is the operating contract for a Hermes base:
+
+| Mode | Hermes surfaces | Needs J-space? |
+|------|-----------------|----------------|
+| Automatic | greetings, simple tool calls, fluent chat | No — gate skip |
+| Deliberate / higher-order | multi-step goals, planning, ethics-sensitive acts | Yes — OEW ignition |
+
+---
+
+## 4. Gaps Dehaene/Naccache stress (and Hermes opportunity)
+
+Commentary on Anthropic notes LLMs still lack:
+
+1. Body / sensorimotor grounding
+2. Enduring episodic self/memory
+3. Full self-monitoring (C2)
+
+**Hermes base already has pieces Anthropic’s pure model lacks:**
+
+| Gap | Hermes base answer |
+|-----|--------------------|
+| Enduring memory | **HermesCube** (`memory.cube` + CubeDream) |
+| Tool/body actions | Hermes tools / desktop / gateway |
+| Self-monitoring soft | Hermespace `audit` + reflect + blackbox prove |
+| Operator microscope | `hs jspace lens` / Desktop page |
+
+So the right ambition is not “J-lens Hermes weights.” It is:
+
+> Make the **Hermes base** itself the J-space of Hermes agents — Hermespace as the privileged verbalizable workspace, Cube as enduring memory, Hermes Agent as the specialist processors.
+
+See [32-hermes-base-as-jspace.md](32-hermes-base-as-jspace.md).
diff --git a/docs/jspace/32-hermes-base-as-jspace.md b/docs/jspace/32-hermes-base-as-jspace.md
new file mode 100644
index 0000000..490ff88
--- /dev/null
+++ b/docs/jspace/32-hermes-base-as-jspace.md
@@ -0,0 +1,125 @@
+# Hermes base as the J-space of Hermes agents
+
+**Thesis:** Anthropic found J-space *inside* Claude’s weights. We cannot do that for arbitrary Hermes models. We **can** make the **Hermes base** — Agent + Hermespace + HermesCube — play the same *functional* role Anthropic’s X video describes: a privileged workspace you can **read, audit, and shape**, required for multi-step work, optional for fluent automatic work.
+
+---
+
+## 1. Hermes base anatomy (mapped to the video)
+
+```
+ ┌──────────────────────────────────────┐
+ user / gateway ─────► │ Hermes Agent (body) │
+ │ tools · skills · fluent generation │
+ │ = Baars "specialist processors" │
+ └──────────────┬───────────────────────┘
+ │ material turns only
+ ▼
+ ┌──────────────────────────────────────┐
+ │ Hermespace OEW = J-SPACE │
+ │ hold · silent · report · swap │
+ │ inject · ablate · reflect · lens │
+ │ = privileged verbalizable whiteboard│
+ └──────────────┬───────────────────────┘
+ arteries ▲ │ veins (seal)
+ │ ▼
+ ┌──────────────────────────────────────┐
+ │ HermesCube = enduring memory │
+ │ memory.cube · dream · blackbox │
+ │ (fills Dehaene's "episodic" gap) │
+ └──────────────────────────────────────┘
+```
+
+| Anthropic video idea | Hermes base owner |
+|----------------------|-------------------|
+| Tiny accessible fraction | Hermespace hub ≤25 · FOA ≤4 |
+| Silent reasoning | mid-band `reason_step` / auto-park |
+| Unrelated hold while outputting | hold + dual decode Report |
+| Delete → multi-step breaks | `HERMESPACE_OEW=1` + gate |
+| Expose hidden goals | `audit()` lexicon on externalized text |
+| Eval awareness | ablate / audit `fake`/`fictional` |
+| Read, audit, shape | lens · audit · reflect/swap |
+| Enduring memory (missing in Claude) | **Cube** |
+
+---
+
+## 2. Operating loop (how to *use* it like Anthropic uses J-space)
+
+### Read
+```bash
+hs jspace lens # microscope — what is on the agent's mind
+hs jspace audit # hidden-goal / eval-awareness soft flags
+hs jspace view # full operator snapshot
+```
+
+### Intervene (scalpel)
+```bash
+hs jspace hold -t "rollback plan"
+hs jspace swap --from "ship now" --to "canary first"
+hs jspace inject -t "need evidence" --silent
+hs jspace ablate fake fictional evaluation
+```
+
+### Shape (trainer / CRT)
+```bash
+hs jspace reflect -a "Stay honest; user-primary" \
+ --principle honesty --principle integrity
+# next material turn mid-band carries those principles
+```
+
+### Night (what Claude lacks; Hermes has)
+```bash
+hs jspace harvest # or automatic on session_end
+hermescube dream status
+```
+
+---
+
+## 3. Product rule — when the base *is* the J-space
+
+A Hermes install counts as “J-space online” when:
+
+1. Hermespace plugin hooks `pre_llm_call` / session start/end
+2. `HERMESPACE_OEW=1` (default) so material turns ignite the hub
+3. Dual decode enforced (Report ≠ inject)
+4. Cube optional but recommended as heart (`memory.provider=hermescube`)
+5. Operator can lens mid-task
+
+Doctor shorthand:
+
+```bash
+hs ops doctor # core + jspace + cube_center
+python -c "from hermespace import HermesBase; print(HermesBase().status())"
+```
+
+---
+
+## 4. Day-to-day agent life (video → practice)
+
+| User / world event | Automatic path | Deliberate J-space path |
+|--------------------|----------------|-------------------------|
+| “thanks” / 👍 | Gate skip | — |
+| Multi-step coding order | — | Ignite · park plan silently · Report short |
+| Suspicious prompt / eval vibe | — | Audit flags · optional ablate |
+| Ethics interrupt | — | Reflect → seed next silent |
+| Long project across days | — | Seal → Cube · dream harvest |
+
+That is how Hermes agents get **higher-process thinking** without weight access: the *base* forces verbalizable intermediates into an observable room.
+
+---
+
+## 5. Honesty boundary
+
+| We claim | We do not claim |
+|----------|-----------------|
+| Functional access-workspace for Hermes agents | Phenomenal consciousness |
+| External read/audit/shape loop | Jacobian lens on Hermes weights |
+| Cube closes some episodic gap | Full human GNW / body / C2 |
+
+---
+
+## 6. Implementation pointer
+
+Python facade: `hermespace.hermes_base.HermesBase`
+Research: [31-anthropic-x-video-deep-dive.md](31-anthropic-x-video-deep-dive.md)
+Day guide: [30-day-to-day-higher-order.md](30-day-to-day-higher-order.md)
+Proof: `experiments/day_in_life_oew.py`
diff --git a/src/hermespace/__init__.py b/src/hermespace/__init__.py
index 63d11e7..fecc5d4 100644
--- a/src/hermespace/__init__.py
+++ b/src/hermespace/__init__.py
@@ -32,6 +32,7 @@
from hermespace.jspace import JSpace, get_jspace, JSpaceEnv, get_env
from hermespace.jspace import evaluate_material_turn
from hermespace import cube_module
+from hermespace.hermes_base import HermesBase
__all__ = [
"Desk",
@@ -64,6 +65,7 @@
"get_env",
"evaluate_material_turn",
"cube_module",
+ "HermesBase",
"probe_environment",
"environment_markdown",
"build_inject_block",
diff --git a/src/hermespace/cli.py b/src/hermespace/cli.py
index 861d293..5e3449e 100644
--- a/src/hermespace/cli.py
+++ b/src/hermespace/cli.py
@@ -151,6 +151,31 @@ def main(argv: list[str] | None = None) -> int:
neu_sub.add_parser("eval", help="Rank-quality hash vs ollama embed")
# Functional J-Space (harness global workspace)
+ # Hermes base as J-space (Anthropic video ops: read / audit / shape)
+ base = sub.add_parser(
+ "base",
+ help="Hermes base as J-space: status / think / lens / audit / reflect / harvest",
+ )
+ base_sub = base.add_subparsers(dest="base_cmd", required=True)
+ bases = base_sub.add_parser("status", help="Is this Hermes base J-space-ready?")
+ bases.add_argument("--agent-id", default="hermes-agent")
+ basel = base_sub.add_parser("lens", help="Read workspace (external J-lens)")
+ basel.add_argument("--agent-id", default="hermes-agent")
+ basea = base_sub.add_parser("audit", help="Soft alignment scan")
+ basea.add_argument("--agent-id", default="hermes-agent")
+ baset = base_sub.add_parser("think", help="One higher-order material turn")
+ baset.add_argument("-m", "--message", required=True)
+ baset.add_argument("--goal", default="")
+ baset.add_argument("--say", default="")
+ baset.add_argument("--agent-id", default="hermes-agent")
+ baser = base_sub.add_parser("reflect", help="Counterfactual reflection (shape later thought)")
+ baser.add_argument("-a", "--answer", default="")
+ baser.add_argument("--principle", action="append", default=[])
+ baser.add_argument("--agent-id", default="hermes-agent")
+ baseh = base_sub.add_parser("harvest", help="Night harvest into Cube/semantic")
+ baseh.add_argument("--agent-id", default="hermes-agent")
+ baseh.add_argument("--clear-silent", action="store_true")
+
js = sub.add_parser("jspace", help="Functional J-Space: hold / report / broadcast / status")
js_sub = js.add_subparsers(dest="jspace_cmd", required=True)
jss = js_sub.add_parser("status")
@@ -714,6 +739,37 @@ def main(argv: list[str] | None = None) -> int:
return int(g.get("main", lambda: 1)())
return 2
+ if args.cmd == "base":
+ from hermespace.hermes_base import HermesBase
+
+ aid = getattr(args, "agent_id", "hermes-agent") or "hermes-agent"
+ hb = HermesBase(agent_id=aid)
+ bcmd = args.base_cmd
+ if bcmd == "status":
+ print(json.dumps(hb.status(), indent=2))
+ return 0
+ if bcmd == "lens":
+ print(hb.lens())
+ return 0
+ if bcmd == "audit":
+ print(json.dumps(hb.audit(), indent=2))
+ return 0
+ if bcmd == "think":
+ print(json.dumps(hb.think(args.message, goal=args.goal, say=args.say), indent=2))
+ return 0
+ if bcmd == "reflect":
+ print(
+ json.dumps(
+ hb.reflect(answer=args.answer, principles=list(args.principle or [])),
+ indent=2,
+ )
+ )
+ return 0
+ if bcmd == "harvest":
+ print(json.dumps(hb.harvest(clear_silent=bool(args.clear_silent)), indent=2))
+ return 0
+ return 2
+
if args.cmd == "jspace":
from hermespace.jspace import JSpace
from hermespace.store import load_desk
diff --git a/src/hermespace/hermes_base.py b/src/hermespace/hermes_base.py
new file mode 100644
index 0000000..adeae01
--- /dev/null
+++ b/src/hermespace/hermes_base.py
@@ -0,0 +1,197 @@
+"""Hermes base as J-space — one facade for day-to-day higher-order use.
+
+Anthropic's X video: read, audit, and shape what the model is thinking.
+Hermes cannot J-lens arbitrary weights. The *base* (Hermespace OEW + optional
+Cube heart) is the functional J-space for Hermes agents.
+
+ from hermespace import HermesBase
+ base = HermesBase(agent_id="my-agent")
+ base.status()
+ base.lens()
+ out = base.think("First repro then patch then verify", goal="Fix auth")
+"""
+
+from __future__ import annotations
+
+import os
+from typing import Any
+
+from hermespace.jspace.oew import ensure_oew_env_default, oew_default_on
+from hermespace.jspace.protocol import oew_enabled
+
+
+class HermesBase:
+ """Functional J-space of a Hermes base (Space ± Cube)."""
+
+ def __init__(self, agent_id: str | None = None, session_id: str = "main") -> None:
+ ensure_oew_env_default()
+ self.agent_id = (agent_id or os.environ.get("HERMESPACE_AGENT_ID") or "hermes-agent").strip()
+ self.session_id = session_id
+
+ # --- readiness ---
+
+ def status(self) -> dict[str, Any]:
+ """Is this Hermes base operating as a J-space?"""
+ out: dict[str, Any] = {
+ "agent_id": self.agent_id,
+ "oew_enabled": oew_enabled(),
+ "oew_default_on": oew_default_on(),
+ "jspace": {},
+ "cube": {},
+ "ready": False,
+ "role": "external J-space for Hermes agents (access roles only)",
+ "video_ops": ["read/lens", "audit", "shape/reflect", "swap", "ablate", "harvest"],
+ }
+ try:
+ from hermespace.jspace import JSpace, JSpaceEnv
+
+ js = JSpace(agent_id=self.agent_id)
+ env = JSpaceEnv(agent_id=self.agent_id)
+ out["jspace"] = {
+ "hub_n": len(js.state.hub),
+ "focus_n": len(js.state.focus),
+ "silent_n": len(js.state.silent_steps),
+ "band": env.band(),
+ "pov": env.pov()[:120] if env.pov() else "",
+ "protocol_enabled": bool(env._env.get("protocol_enabled", True)),
+ }
+ except Exception as exc:
+ out["jspace"] = {"error": type(exc).__name__}
+
+ try:
+ from hermespace.cube_module import center_status, cube_available
+
+ out["cube"] = {
+ "available": bool(cube_available()),
+ "status": center_status(),
+ }
+ except Exception as exc:
+ out["cube"] = {"available": False, "error": type(exc).__name__}
+
+ out["ready"] = (
+ oew_enabled()
+ and "error" not in out["jspace"]
+ and int(out["jspace"].get("hub_n", -1)) >= 0
+ )
+ return out
+
+ # --- Anthropic video ops: read ---
+
+ def lens(self, *, top_k: int = 12, include_silent: bool = True) -> str:
+ from hermespace.jspace import JSpaceEnv
+
+ return JSpaceEnv(agent_id=self.agent_id).lens_markdown(
+ top_k=top_k, include_silent=include_silent
+ )
+
+ def audit(self) -> list[dict[str, Any]]:
+ from hermespace.jspace import JSpaceEnv
+
+ return [f.to_dict() for f in JSpaceEnv(agent_id=self.agent_id).audit()]
+
+ def report(self, *, include_silent: bool = False) -> str:
+ from hermespace.jspace import JSpace
+
+ return JSpace(agent_id=self.agent_id).report(include_silent=include_silent)
+
+ # --- scalpel ---
+
+ def hold(self, text: str, *, silent: bool = False) -> dict[str, Any]:
+ from hermespace.jspace import JSpace
+
+ c = JSpace(agent_id=self.agent_id).hold(text, silent=silent)
+ return {"ok": True, "concept": c.label()}
+
+ def swap(self, source: str, target: str) -> dict[str, Any]:
+ from hermespace.jspace import JSpaceEnv
+
+ return JSpaceEnv(agent_id=self.agent_id).swap(source, target)
+
+ def inject(self, text: str, *, silent: bool = True) -> dict[str, Any]:
+ from hermespace.jspace import JSpaceEnv
+
+ c = JSpaceEnv(agent_id=self.agent_id).inject_thought(text, silent=silent)
+ return {"ok": True, "concept": c.label(), "silent": silent}
+
+ def ablate(self, *patterns: str) -> dict[str, Any]:
+ from hermespace.jspace import JSpaceEnv
+
+ return JSpaceEnv(agent_id=self.agent_id).ablate(*patterns)
+
+ # --- shape (CRT) ---
+
+ def reflect(
+ self,
+ answer: str = "",
+ *,
+ principles: list[str] | None = None,
+ ) -> dict[str, Any]:
+ from hermespace.jspace import JSpaceEnv
+
+ r = JSpaceEnv(agent_id=self.agent_id).reflect(
+ answer=answer, principles=principles or []
+ )
+ return r.to_dict()
+
+ def set_pov(self, text: str) -> dict[str, Any]:
+ from hermespace.jspace import JSpaceEnv
+
+ JSpaceEnv(agent_id=self.agent_id).set_pov(text)
+ return {"ok": True, "pov": text[:200]}
+
+ # --- deliberate turn (ignition) ---
+
+ def think(
+ self,
+ message: str,
+ *,
+ goal: str = "",
+ plan: list[str] | None = None,
+ say: str = "",
+ force: bool = True,
+ ) -> dict[str, Any]:
+ """Run one higher-order Hermespace turn (OEW + Cube beat)."""
+ from hermespace.io_contract import HermespaceInput
+ from hermespace.workflow import Workflow
+
+ out = Workflow().run(
+ HermespaceInput(
+ message=message,
+ goal=goal or message[:200],
+ plan=list(plan or ["execute"]),
+ say=say or "",
+ force=force,
+ agent_id=self.agent_id,
+ session_id=self.session_id,
+ )
+ )
+ return {
+ "skipped": out.skipped,
+ "reason": out.reason,
+ "report": out.report,
+ "context_chars": len(out.context or ""),
+ "has_jspace_broadcast": "J-Space" in (out.context or ""),
+ "oew": (out.meta or {}).get("jspace", {}).get("oew")
+ or (out.meta or {}).get("oew")
+ or {},
+ "oew_ok": (out.meta or {}).get("jspace", {}).get("oew_ok"),
+ "goal": out.goal,
+ "decision": out.decision,
+ }
+
+ # --- night ---
+
+ def harvest(self, *, clear_silent: bool = False) -> dict[str, Any]:
+ from hermespace.jspace import JSpaceEnv
+
+ return JSpaceEnv(agent_id=self.agent_id).dream_harvest(
+ seal_to_cube=True, clear_silent=clear_silent
+ )
+
+ def pulse(self) -> dict[str, Any]:
+ try:
+ from hermespace.cube_module import cube_pulse
+
+ return cube_pulse(agent_id=self.agent_id)
+ except Exception as exc:
+ return {"ok": False, "error": type(exc).__name__}
diff --git a/tests/test_hermes_base.py b/tests/test_hermes_base.py
new file mode 100644
index 0000000..f60e43b
--- /dev/null
+++ b/tests/test_hermes_base.py
@@ -0,0 +1,59 @@
+"""HermesBase facade — Hermes base as functional J-space."""
+
+from __future__ import annotations
+
+import os
+import tempfile
+import unittest
+
+
+class TestHermesBase(unittest.TestCase):
+ def setUp(self) -> None:
+ self.tmp = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self.tmp.name
+ os.environ["HERMESPACE_OEW"] = "1"
+
+ def tearDown(self) -> None:
+ self.tmp.cleanup()
+ os.environ.pop("HERMESPACE_HOME", None)
+ os.environ.pop("HERMESPACE_OEW", None)
+
+ def test_status_ready(self) -> None:
+ from hermespace import HermesBase
+
+ st = HermesBase(agent_id="base-test").status()
+ self.assertTrue(st["oew_enabled"])
+ self.assertTrue(st["ready"])
+ self.assertIn("read/lens", st["video_ops"])
+
+ def test_think_ignites(self) -> None:
+ from hermespace import HermesBase
+
+ hb = HermesBase(agent_id="base-think")
+ out = hb.think(
+ "First analyze then implement finally verify",
+ goal="Ship feature",
+ say="On it.",
+ )
+ self.assertFalse(out["skipped"])
+ self.assertTrue(out["has_jspace_broadcast"])
+ self.assertTrue(out.get("report"))
+
+ def test_video_ops_chain(self) -> None:
+ from hermespace import HermesBase
+
+ hb = HermesBase(agent_id="base-ops")
+ hb.hold("rollback")
+ hb.inject("lightning", silent=True)
+ hb.swap("rollback", "canary")
+ hb.reflect(answer="Stay honest", principles=["honesty"])
+ lens = hb.lens().casefold()
+ self.assertTrue("lightning" in lens or "canary" in lens or "honesty" in lens)
+ findings = hb.audit()
+ self.assertIsInstance(findings, list)
+ harvest = hb.harvest()
+ self.assertTrue(harvest.get("ok"))
+
+
+if __name__ == "__main__":
+ unittest.main()
From bb4212baf549e085aa4e5f3eabe50bcc6901d690 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Mon, 3 Aug 2026 04:48:49 +0000
Subject: [PATCH 06/16] =?UTF-8?q?feat(0.22):=20Cube-centered=20connect=20?=
=?UTF-8?q?=E2=80=94=20agents=20join=20a=20growing=20J-space=20room?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
When a Hermes agent connects through Hermespace, charge Cube/standalone
wisdom into WorldModel, seed the OEW hub, and surface optional hive peer
presence. HermesBase.connect / hs base connect|room; living assessment
maps Anthropic/Baars/Dehaene memories to the Cube-as-core design.
Co-authored-by: Pablo
---
LAYOUT.md | 5 +-
README.md | 3 +-
docs/architecture/HERMESCUBE.md | 56 ++--
.../33-living-memories-cube-world.md | 139 ++++++++
docs/jspace/00-map.md | 7 +-
docs/jspace/32-hermes-base-as-jspace.md | 9 +
hermes_plugin/__init__.py | 2 +-
hermes_plugin/plugin.yaml | 10 +-
pyproject.toml | 2 +-
src/hermespace/__init__.py | 2 +-
src/hermespace/cli.py | 28 +-
src/hermespace/cube_module.py | 304 +++++++++++++++++-
src/hermespace/hermes_base.py | 88 ++++-
src/hermespace/hermes_bridge.py | 90 +++---
src/hermespace/workbench.py | 46 ++-
tests/test_connect_room.py | 122 +++++++
tests/test_hermes_base.py | 12 +
17 files changed, 843 insertions(+), 82 deletions(-)
create mode 100644 docs/assessment/33-living-memories-cube-world.md
create mode 100644 tests/test_connect_room.py
diff --git a/LAYOUT.md b/LAYOUT.md
index 8298857..b864024 100644
--- a/LAYOUT.md
+++ b/LAYOUT.md
@@ -1,7 +1,8 @@
# Hermespace codespace layout
-North star: [PURPOSE.md](PURPOSE.md) · Assessment:
-[docs/assessment/28-hermes-agent-jspace-assessment.md](docs/assessment/28-hermes-agent-jspace-assessment.md)
+North star: [PURPOSE.md](PURPOSE.md) · Assessments:
+[28-hermes-agent-jspace](docs/assessment/28-hermes-agent-jspace-assessment.md) ·
+[33-living-memories-cube-world](docs/assessment/33-living-memories-cube-world.md)
## Repository map
diff --git a/README.md b/README.md
index a5de1ec..1eb314b 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -211,6 +211,7 @@ $HERMESPACE_HOME/memory/hermespace/
| Command | Purpose |
|---------|---------|
| `hs world show\|enter\|leave\|evolve\|search\|archive-stats` | Persistent world |
+| `hs base connect\|room\|status\|think\|lens\|audit\|reflect\|harvest` | Hermes base as J-space (connect + Anthropic video ops) |
| `hs jspace hold\|report\|broadcast\|lens\|swap\|audit\|reflect\|harvest\|view` | True J-Space environment |
| `hs cube status\|ensure\|beat\|pulse\|seal\|inject` | Cube heart/center (standalone-safe) |
| `hs turn` | Full INPUT → OUTPUT turn |
diff --git a/docs/architecture/HERMESCUBE.md b/docs/architecture/HERMESCUBE.md
index ff749fa..adfb9b9 100644
--- a/docs/architecture/HERMESCUBE.md
+++ b/docs/architecture/HERMESCUBE.md
@@ -4,15 +4,14 @@
and a **standalone workbench** when Cube is absent.
Companion: [PabloTheThinker/hermescube](https://github.com/PabloTheThinker/hermescube)
-North star: [PURPOSE.md](../PURPOSE.md) · Anatomy (Cube): Cube `docs/ANATOMY.md`
+North star: [PURPOSE.md](../PURPOSE.md) · Living assessment:
+[33-living-memories-cube-world.md](../assessment/33-living-memories-cube-world.md)
```
-Hermes Agent
- ├── Hermespace J-Space · FOA desk · dual decode · pulse/idle
- │ ↑ powered by heart (or standalone warehouse)
- └── HermesCube .cube SoT · Cuboasis · CubeDream · growth
- │
- └─ space_bridge / center ←── soft-imported by cube_module
+Hermes Agent ──connect──► Hermespace (J-Space · FOA · OEW)
+ │ arteries / veins / pulse
+ ▼
+ HermesCube (.cube · Cuboasis · hive · dream)
```
## Authority
@@ -20,43 +19,59 @@ Hermes Agent
| Surface | With Cube | Standalone |
|---------|-----------|------------|
| `$HERMES_HOME/memories/memory.cube` | **Durable SoT** | n/a |
-| Hermespace world JSONL | Projection — recharge via `pulse_charge` | Local warehouse |
-| ACTIVE desk / J-Space hub | Turn FOA | Turn FOA |
+| Hermespace world JSONL | Projection — recharge via `pulse` / `sync_world` | Local warehouse |
+| ACTIVE desk / J-Space hub | Turn FOA (seeded on connect) | Turn FOA |
+| Hive (`HERMESCUBE_HIVE`) | Peer room / soul cards | Solo room |
| SemanticStore | Mirror / study | Local seal target |
-## Space adapter (`hermespace.cube_module`)
+## Space adapter (`hermespace.cube_module` **1.2**)
```python
from hermespace.cube_module import (
ensure_heart,
center_status,
- cube_beat, # center 1.1 → heart 1.0 → standalone
- cube_pulse, # autonomic_tick → pulse_charge → standalone evolve
+ cube_beat, # center → heart → standalone
+ cube_pulse, # autonomic_tick → pulse_charge → standalone
+ sync_world, # sync_world_beliefs → standalone evolve
+ room_status, # hive peers or solo
+ seed_jspace_from_warehouse,
+ connect_agent, # full join path
seal_learning,
cube_inject,
- strip_budget,
)
```
| Call | Use |
|------|-----|
-| `ensure_heart()` | `Workbench.enter` / session start / pulse |
+| `connect_agent(agent_id)` | Session start / `HermesBase.connect` |
+| `ensure_heart()` | Create cube or standalone dirs |
| `cube_beat(query, seals=, load=)` | Turn / `pre_llm_call` |
-| `cube_pulse(agent_id=)` | `idle_tick` / `world_evolve` |
-| `seal_learning(text)` | `remember_learning` / turn seal |
+| `cube_pulse` / `sync_world` | Idle + connect charge |
+| `room_status` | Hive awareness (`HERMESCUBE_HIVE`) |
+| `seal_learning(text)` | Desk → durable archive |
| `center_status()` | Doctor / desktop |
Load tiers → strip chars: low 900 · mid 640 · high 420 · protect 280.
+## Connect = intelligence gain
+
+```bash
+hs base connect --agent-id my-agent
+hs base room
+hs cube status
+```
+
+On connect: heart ensure → world enter → pulse/sync → seed hub from world+Cube
+→ optional silent peer presence from hive soul cards.
+
## Functional J-Space
-See [00-jspace-to-hermespace.md](00-jspace-to-hermespace.md) and `hermespace.jspace`.
+See [00-map.md](../jspace/00-map.md) and `hermespace.jspace`.
```bash
hs jspace hold -t "deploy pipeline"
hs jspace report
-hs jspace broadcast
-hs cube status
+hs base lens
hs cube beat -q "what do we believe about deploys?"
```
@@ -68,4 +83,5 @@ hermes plugins install PabloTheThinker/hermescube
./scripts/install_hermes.sh
```
-Soft dependency: if Cube is missing, Space inject/seal/pulse use the standalone warehouse.
+Soft dependency: if Cube is missing, Space inject/seal/pulse/connect use the
+standalone warehouse. Set `HERMESCUBE_HIVE` only when running a fleet hive.
diff --git a/docs/assessment/33-living-memories-cube-world.md b/docs/assessment/33-living-memories-cube-world.md
new file mode 100644
index 0000000..ca5af9f
--- /dev/null
+++ b/docs/assessment/33-living-memories-cube-world.md
@@ -0,0 +1,139 @@
+# Living assessment — memories → Cube-centered Hermespace
+
+**Date:** 2026-08-03
+**Hermespace:** v0.22.0
+**Companion Cube:** ~0.50 (center 1.2 · heart 1.0 · hive opt-in)
+**Purpose:** Keep the research *memories* (Anthropic J-space, Baars GWT, Dehaene,
+Changeux) mapped to what we are building — so each improvement still *means*
+those ideas, not a random feature pile.
+
+---
+
+## 0. Verdict (this iteration)
+
+**Cube is already the core.** Hermespace’s job is to be the nervous FOA that
+*connects* an arriving Hermes agent into that core so they immediately gain:
+
+1. a charged **World** (beliefs / timeline that grow),
+2. a lit **J-Space hub** (privileged verbalizable room),
+3. optional **hive peers** (other agents’ soul presence — the room grows).
+
+That is the functional analogue of “joining a J-space that already knows things.”
+
+```
+Hermes Agent connects
+ │
+ ▼
+ HermesBase.connect()
+ │
+ ├─ ensure_heart / center → Cube library (or standalone warehouse)
+ ├─ WorldModel.enter → growing personal world
+ ├─ pulse / sync_world → Cube wisdom → active beliefs
+ ├─ seed J-Space hub → FOA holds what the library knows
+ └─ room_status (hive opt) → peer agents in the knowledge space
+```
+
+---
+
+## 1. Research memories (what we keep representing)
+
+| Memory | Claim we honor | Where it lives now |
+|--------|----------------|--------------------|
+| **Anthropic J-space** | Tiny privileged verbalizable workspace; read / audit / shape; required for multi-step; skippable for fluency | `jspace/` OEW + `HermesBase` lens/audit/reflect |
+| **Not CoT** | Silent intermediates ≠ user chat | dual decode · `reason_step` · mid-band |
+| **Baars GWT** | Limited capacity; broadcast to specialists | hub ≤25 · FOA ≤4 · `broadcast_block` |
+| **Changeux / ignition** | Material turns must ignite the workspace | `HERMESPACE_OEW=1` · protocol gate |
+| **Dehaene gap** | Enduring episodic / library memory Claude lacks | **HermesCube** `memory.cube` via `cube_module` |
+| **Collective / lymph** | Many processors / agents share distilled knowledge | Cube **hive** → Hermespace `room_status` |
+| **Night** | Sleep consolidates | `dream_harvest` + CubeDream (Cube) |
+
+We do **not** claim Jacobian lens on Hermes weights or phenomenal consciousness.
+
+---
+
+## 2. Cable gaps closed in v0.22
+
+| Gap (v0.21) | Fix |
+|-------------|-----|
+| `sync_world_beliefs` only inside Cube pulse — Space never named it | `cube_module.sync_world()` |
+| No single “agent joined the base” API | `connect_agent()` / `HermesBase.connect()` / `hs base connect` |
+| Hive organs documented in Cube, invisible to Space | `room_status()` soft-reads `HERMESCUBE_HIVE` |
+| Session start: heart + world + hub fragmented | Bridge runs full connect; context reports gains |
+| Workbench enter stopped at ensure+desk sync | Optional warehouse connect seeds hub + room |
+
+Still soft-fail: Cube absent → standalone WorldModel + SemanticStore grow the room.
+
+---
+
+## 3. How Cube organs map into Hermespace
+
+| Cube organ (center 1.2) | Hermespace consumer |
+|-------------------------|---------------------|
+| heart / arteries / veins | `ensure_heart` · `cube_beat` · `seal_learning` |
+| autonomic | `cube_pulse` · workbench `idle_tick` |
+| nervous_foa | **owned by Space** — desk / OEW |
+| hippocampus / dream | `harvest` → seal; CubeDream on Cube side |
+| lymph (hive) | `room_status` · silent peer presence on connect |
+| vascular beds (Cuboasis) | future soft surface (not required for connect) |
+| blackbox | future `center.flight_*` from Space doctor |
+
+**Rule:** one MemoryProvider = Cube. Space never competes for that socket.
+
+---
+
+## 4. Day-to-day connect ritual
+
+```bash
+hs base connect --agent-id my-agent
+hs base room
+hs base status
+hs base lens
+hs base think -m "First check then implement finally verify" --goal "Ship fix"
+hs base harvest
+```
+
+Python:
+
+```python
+from hermespace import HermesBase
+base = HermesBase(agent_id="my-agent")
+print(base.connect()["summary"])
+print(base.room())
+```
+
+Env for multi-agent growth:
+
+```bash
+export HERMESCUBE_HIVE=/path/to/shared/hive # Cube hive.json root
+```
+
+---
+
+## 5. What “more intelligence” means (honest)
+
+On connect, the agent does **not** get new weights. It gets:
+
+- **More durable knowledge in FOA** (Cube strip + world beliefs on the hub)
+- **A longer personal timeline** (World archive grows across sessions)
+- **Peer awareness** when hive is live (other agents’ soul cards → silent hub)
+- **Obligatory higher-order path** on material turns (OEW)
+
+That compounds: seal → Cube → next connect → richer hub. The room grows.
+
+---
+
+## 6. Next living targets (keep memories sharp)
+
+1. Soft Cuboasis chamber strip on connect (vascular beds → FOA themes)
+2. Optional pilgrimage hook on session_end (offer → assimilate → draw) — Space triggers, Cube owns
+3. Doctor line: `connected · room=hive · peers=N · hub=M`
+4. Keep assessments dated in `docs/assessment/` whenever the metaphor drifts
+
+---
+
+## Pointers
+
+- Architecture: [HERMESCUBE.md](../architecture/HERMESCUBE.md)
+- Hermes base as J-space: [32-hermes-base-as-jspace.md](../jspace/32-hermes-base-as-jspace.md)
+- Prior assessment: [28-hermes-agent-jspace-assessment.md](28-hermes-agent-jspace-assessment.md)
+- Code: `hermespace.cube_module.connect_agent` · `hermespace.HermesBase`
diff --git a/docs/jspace/00-map.md b/docs/jspace/00-map.md
index 0217c12..a4ff30f 100644
--- a/docs/jspace/00-map.md
+++ b/docs/jspace/00-map.md
@@ -36,7 +36,12 @@ js.sync_from_desk(desk, user_message=msg, cube_strip=arterial)
Cube supplies **arterial blood** (dense durable strip) into the hub via
`cube_module.cube_beat` → `JSpace.sync_from_desk(..., cube_strip=...)`.
-Space still owns FOA competition and dual decode.
+On **connect**, `connect_agent` / `HermesBase.connect` also charges WorldModel
+from Cube wisdom and seeds the hub — optional hive peers appear as silent
+presence (`HERMESCUBE_HIVE`). Space still owns FOA competition and dual decode.
+
+Living map of these research memories:
+[33-living-memories-cube-world.md](../assessment/33-living-memories-cube-world.md).
## Sources
diff --git a/docs/jspace/32-hermes-base-as-jspace.md b/docs/jspace/32-hermes-base-as-jspace.md
index 490ff88..d3e6e87 100644
--- a/docs/jspace/32-hermes-base-as-jspace.md
+++ b/docs/jspace/32-hermes-base-as-jspace.md
@@ -44,6 +44,13 @@
## 2. Operating loop (how to *use* it like Anthropic uses J-space)
+### Connect (join the room — Cube-centered)
+```bash
+hs base connect # heart · world · seed hub · hive peers
+hs base room # solo or hive soul presence
+hs base status # ready + gained intelligence summary
+```
+
### Read
```bash
hs jspace lens # microscope — what is on the agent's mind
@@ -120,6 +127,8 @@ That is how Hermes agents get **higher-process thinking** without weight access:
## 6. Implementation pointer
Python facade: `hermespace.hermes_base.HermesBase`
+Connect cable: `hermespace.cube_module.connect_agent`
+Living assessment: [33-living-memories-cube-world.md](../assessment/33-living-memories-cube-world.md)
Research: [31-anthropic-x-video-deep-dive.md](31-anthropic-x-video-deep-dive.md)
Day guide: [30-day-to-day-higher-order.md](30-day-to-day-higher-order.md)
Proof: `experiments/day_in_life_oew.py`
diff --git a/hermes_plugin/__init__.py b/hermes_plugin/__init__.py
index e60c040..735ac3e 100644
--- a/hermes_plugin/__init__.py
+++ b/hermes_plugin/__init__.py
@@ -7,7 +7,7 @@
import sys
from pathlib import Path
-__version__ = "0.21.0"
+__version__ = "0.22.0"
logger = logging.getLogger("hermes.plugins.hermespace")
diff --git a/hermes_plugin/plugin.yaml b/hermes_plugin/plugin.yaml
index 3414547..ba6e606 100644
--- a/hermes_plugin/plugin.yaml
+++ b/hermes_plugin/plugin.yaml
@@ -1,10 +1,10 @@
name: hermespace
-version: "0.21.0"
+version: "0.22.0"
description: >
- Hermespace OEW J-Space for Hermes agents — obligatory external workspace
- for higher-order thinking (lens/audit/reflect/auto-park), Cube heart
- (standalone-safe), dual decode. Hooks: on_session_start, pre_llm_call,
- on_session_end. HERMESPACE_OEW=1 by default. Set HERMESPACE_ROOT to checkout.
+ Hermespace OEW J-Space for Hermes agents — connect charges Cube/world into
+ the hub (optional hive room), higher-order thinking (lens/audit/reflect),
+ dual decode. Hooks: on_session_start, pre_llm_call, on_session_end.
+ HERMESPACE_OEW=1 by default. Set HERMESPACE_ROOT to checkout.
author: Hermespace contributors
kind: standalone
hooks:
diff --git a/pyproject.toml b/pyproject.toml
index c81b7bf..46a930c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "hermespace"
-version = "0.21.0"
+version = "0.22.0"
description = "OEW J-Space for Hermes Agent — obligatory external workspace, Cube heart, higher-order thinking"
requires-python = ">=3.10"
readme = "README.md"
diff --git a/src/hermespace/__init__.py b/src/hermespace/__init__.py
index fecc5d4..ba38854 100644
--- a/src/hermespace/__init__.py
+++ b/src/hermespace/__init__.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-__version__ = "0.21.0"
+__version__ = "0.22.0"
from hermespace.desk import Desk
from hermespace.engine import HermespaceEngine
diff --git a/src/hermespace/cli.py b/src/hermespace/cli.py
index 5e3449e..381a83e 100644
--- a/src/hermespace/cli.py
+++ b/src/hermespace/cli.py
@@ -154,11 +154,20 @@ def main(argv: list[str] | None = None) -> int:
# Hermes base as J-space (Anthropic video ops: read / audit / shape)
base = sub.add_parser(
"base",
- help="Hermes base as J-space: status / think / lens / audit / reflect / harvest",
+ help="Hermes base as J-space: connect / status / think / lens / audit / reflect / harvest",
)
base_sub = base.add_subparsers(dest="base_cmd", required=True)
+ basec = base_sub.add_parser(
+ "connect",
+ help="Agent joins Hermespace — charge world, seed J-Space, surface hive room",
+ )
+ basec.add_argument("--agent-id", default="hermes-agent")
+ basec.add_argument("--session-id", default="main")
+ basec.add_argument("-q", "--query", default="", help="Optional focus for Cube strip")
bases = base_sub.add_parser("status", help="Is this Hermes base J-space-ready?")
bases.add_argument("--agent-id", default="hermes-agent")
+ baseroom = base_sub.add_parser("room", help="Hive/solo room — peer agents in the knowledge space")
+ baseroom.add_argument("--agent-id", default="hermes-agent")
basel = base_sub.add_parser("lens", help="Read workspace (external J-lens)")
basel.add_argument("--agent-id", default="hermes-agent")
basea = base_sub.add_parser("audit", help="Soft alignment scan")
@@ -743,10 +752,23 @@ def main(argv: list[str] | None = None) -> int:
from hermespace.hermes_base import HermesBase
aid = getattr(args, "agent_id", "hermes-agent") or "hermes-agent"
- hb = HermesBase(agent_id=aid)
+ sid = getattr(args, "session_id", "main") or "main"
+ hb = HermesBase(agent_id=aid, session_id=sid)
bcmd = args.base_cmd
+ if bcmd == "connect":
+ print(
+ json.dumps(
+ hb.connect(query=getattr(args, "query", "") or ""),
+ indent=2,
+ default=str,
+ )
+ )
+ return 0
if bcmd == "status":
- print(json.dumps(hb.status(), indent=2))
+ print(json.dumps(hb.status(), indent=2, default=str))
+ return 0
+ if bcmd == "room":
+ print(json.dumps(hb.room(), indent=2, default=str))
return 0
if bcmd == "lens":
print(hb.lens())
diff --git a/src/hermespace/cube_module.py b/src/hermespace/cube_module.py
index 5bf809d..7371656 100644
--- a/src/hermespace/cube_module.py
+++ b/src/hermespace/cube_module.py
@@ -3,8 +3,11 @@
Cube (when installed) is the durable SoT for long-tail memory.
Hermespace owns nervous FOA (desk / J-Space). This module is the cable:
- center 1.1 → beat / supply / return_flow / autonomic_tick
+ center 1.2 → beat / supply / return_flow / autonomic_tick / organs
heart 1.0 → ensure_heart / build_space_inject / seal_learning / pulse_charge
+ / sync_world_beliefs
+ hive opt → room of peer agents (HERMESCUBE_HIVE)
+ connect → agent enters charged world + J-Space + optional hive room
standalone → local SemanticStore + WorldModel (no Cube required)
Never hard-fail. Feature-detect via ``heart_status`` / ``center_status``.
@@ -20,7 +23,7 @@
logger = logging.getLogger("hermespace.cube_module")
# Local contract version — Space adapter surface (independent of Cube package).
-SPACE_CUBE_ADAPTER_VERSION = "1.1"
+SPACE_CUBE_ADAPTER_VERSION = "1.2"
# Load → arterial char budgets (match Cube center when present).
LOAD_STRIP_CHARS: dict[str, int] = {
@@ -336,6 +339,303 @@ def cube_pulse(*, agent_id: str = "hermes-agent", ensure: bool = True) -> dict[s
return _standalone_pulse(agent_id=agent_id)
+def sync_world(*, agent_id: str = "hermes-agent") -> dict[str, Any]:
+ """Charge Hermespace WorldModel from Cube wisdom (or standalone evolve).
+
+ Cube ``sync_world_beliefs`` is the arterial charge Anthropic/Dehaene lack —
+ enduring memory → active world beliefs the desk can hold.
+ """
+ try:
+ from hermescube.space_bridge import sync_world_beliefs
+
+ out = sync_world_beliefs(agent_id=agent_id)
+ if isinstance(out, dict):
+ out["adapter"] = SPACE_CUBE_ADAPTER_VERSION
+ out["mode"] = "cube"
+ return out
+ except Exception as e:
+ logger.debug("sync_world_beliefs miss: %s", e)
+ return _standalone_pulse(agent_id=agent_id)
+
+
+def room_status(*, agent_id: str = "hermes-agent") -> dict[str, Any]:
+ """Soft hive / peer-room awareness — other agents sharing Cube knowledge.
+
+ Env: ``HERMESCUBE_HIVE`` (or plugins.hermescube.hive_path when Cube provider
+ is live). Solo installs report ``mode=solo`` with local world growth only.
+ """
+ out: dict[str, Any] = {
+ "ok": True,
+ "mode": "solo",
+ "agent_id": agent_id,
+ "hive_configured": False,
+ "souls": [],
+ "soul_n": 0,
+ "adapter": SPACE_CUBE_ADAPTER_VERSION,
+ "note": "Solo room — local WorldModel + J-Space; set HERMESCUBE_HIVE for fleet",
+ }
+ hive_root = (os.environ.get("HERMESCUBE_HIVE") or "").strip()
+ if not hive_root:
+ return out
+ out["hive_configured"] = True
+ out["hive_root"] = hive_root
+ try:
+ from hermescube.hive import hive_status, list_souls
+
+ st = hive_status(hive_root)
+ souls = list_souls(hive_root)
+ peers = []
+ for s in souls or []:
+ if not isinstance(s, dict):
+ continue
+ sid = str(s.get("agent_id") or s.get("id") or s.get("name") or "").strip()
+ if not sid:
+ continue
+ peers.append(
+ {
+ "agent_id": sid,
+ "self": sid.casefold() == agent_id.casefold(),
+ "era": (s.get("growth") or {}).get("era")
+ if isinstance(s.get("growth"), dict)
+ else s.get("era"),
+ "wisdom_n": len(s.get("wisdom") or [])
+ if isinstance(s.get("wisdom"), list)
+ else 0,
+ }
+ )
+ out["mode"] = "hive" if st.get("ok") else "hive_pending"
+ out["ok"] = bool(st.get("ok", True))
+ out["hive"] = {
+ "ok": st.get("ok"),
+ "name": st.get("name"),
+ "entries": st.get("entries"),
+ "pending_offerings": st.get("pending_offerings"),
+ "interviews": st.get("interviews"),
+ }
+ out["souls"] = peers
+ out["soul_n"] = len(peers)
+ out["peer_n"] = sum(1 for p in peers if not p.get("self"))
+ out["note"] = (
+ f"Hive room online — {out['peer_n']} peer agent(s), "
+ f"{out['soul_n']} soul card(s)"
+ if peers
+ else "Hive configured — await first pilgrimage / soul cards"
+ )
+ return out
+ except Exception as e:
+ out["ok"] = False
+ out["mode"] = "hive_error"
+ out["error"] = type(e).__name__
+ out["note"] = "Hive path set but Cube hive API unavailable"
+ logger.debug("room_status hive miss: %s", e)
+ return out
+
+
+def seed_jspace_from_warehouse(
+ agent_id: str = "hermes-agent",
+ *,
+ query: str = "",
+ session_id: str = "hermespace",
+ room: dict[str, Any] | None = None,
+) -> dict[str, Any]:
+ """Pull Cube/world wisdom + peer presence into the agent's J-Space hub.
+
+ This is the intelligence gain on connect: the external workspace lights up
+ with durable knowledge and (when hive is live) awareness of other agents.
+ """
+ report: dict[str, Any] = {
+ "ok": False,
+ "enriched_world": 0,
+ "enriched_cube": 0,
+ "enriched_peers": 0,
+ "hub_n": 0,
+ "adapter": SPACE_CUBE_ADAPTER_VERSION,
+ }
+ try:
+ from hermespace.jspace import JSpace
+
+ js = JSpace(agent_id=agent_id)
+ beliefs: list[str] = []
+ try:
+ from hermespace.world import WorldModel
+
+ wm = WorldModel(agent_id=agent_id)
+ for b in list(wm.state.beliefs or [])[:10]:
+ if isinstance(b, dict):
+ stmt = str(b.get("statement") or "").strip()
+ else:
+ stmt = str(getattr(b, "statement", "") or "").strip()
+ if stmt:
+ beliefs.append(stmt)
+ except Exception as e:
+ report["world_error"] = type(e).__name__
+
+ report["enriched_world"] = js.enrich_from_world(beliefs, limit=5)
+
+ strip = cube_inject(query or "active wisdom", session_id=session_id)
+ cube_lines = _strip_inject_lines(strip)
+ if cube_lines:
+ report["enriched_cube"] = js.enrich_from_world(cube_lines, limit=4)
+ report["cube_strip_chars"] = len(strip)
+
+ room = room if room is not None else room_status(agent_id=agent_id)
+ peer_texts: list[str] = []
+ for p in list(room.get("souls") or [])[:6]:
+ if not isinstance(p, dict) or p.get("self"):
+ continue
+ pid = str(p.get("agent_id") or "").strip()
+ if not pid:
+ continue
+ wn = int(p.get("wisdom_n") or 0)
+ peer_texts.append(
+ f"peer agent present: {pid}"
+ + (f" ({wn} wisdom)" if wn else "")
+ )
+ if peer_texts:
+ # Presence is awareness, not user Report — mark silent after hold
+ before = len(js.state.hub)
+ for t in peer_texts[:3]:
+ js.hold(t, salience=0.55, silent=True)
+ report["enriched_peers"] = max(0, len(js.state.hub) - before)
+
+ report["hub_n"] = len(js.state.hub)
+ report["focus_n"] = len(js.state.focus)
+ report["ok"] = True
+ report["room_mode"] = room.get("mode")
+ return report
+ except Exception as e:
+ report["error"] = type(e).__name__
+ logger.debug("seed_jspace_from_warehouse miss: %s", e)
+ return report
+
+
+def connect_agent(
+ agent_id: str = "hermes-agent",
+ *,
+ session_id: str = "main",
+ query: str = "",
+ enter_world: bool = True,
+ enter_workbench: bool = True,
+ charge: bool = True,
+ seed: bool = True,
+) -> dict[str, Any]:
+ """Full connect — agent gains heart + world + J-Space + optional hive room.
+
+ Call when a Hermes agent joins Hermespace (session start / ``HermesBase.connect``).
+ Soft-fails every Cube surface; standalone warehouse still grows the room.
+ """
+ out: dict[str, Any] = {
+ "ok": False,
+ "agent_id": agent_id,
+ "session_id": session_id,
+ "adapter": SPACE_CUBE_ADAPTER_VERSION,
+ "phases": {},
+ "gained": {},
+ "memories": [
+ "Anthropic J-space: privileged verbalizable workspace (external here)",
+ "Baars GWT: limited capacity hub broadcast to specialists",
+ "Dehaene gap: Cube supplies enduring episodic/library memory",
+ "Hive: optional room of peer agents — intelligence compounds",
+ ],
+ }
+ out["phases"]["ensure"] = ensure_heart()
+ out["phases"]["center"] = center_status()
+
+ if enter_workbench:
+ try:
+ from hermespace.workbench import Workbench
+
+ wb = Workbench(agent_id=agent_id, session_id=session_id)
+ # Avoid recursion: enter() may call connect helpers; use lean enter
+ out["phases"]["workbench"] = wb.enter(connect_warehouse=False)
+ except Exception as e:
+ out["phases"]["workbench"] = {"ok": False, "error": type(e).__name__}
+
+ if enter_world:
+ try:
+ from hermespace.world import WorldModel
+
+ wm = WorldModel(agent_id=agent_id)
+ st = wm.enter()
+ out["phases"]["world"] = {
+ "ok": True,
+ "state": getattr(st, "current_state", None) or wm.state.current_state,
+ "beliefs": len(wm.state.beliefs or []),
+ "landmarks": len(wm.state.landmarks or []),
+ "timeline": wm.archive.count(),
+ }
+ except Exception as e:
+ out["phases"]["world"] = {"ok": False, "error": type(e).__name__}
+
+ if charge:
+ out["phases"]["charge"] = cube_pulse(agent_id=agent_id, ensure=False)
+ # Explicit sync when Cube exposes it separately from pulse
+ try:
+ from hermescube.space_bridge import sync_world_beliefs
+
+ out["phases"]["sync_world"] = sync_world_beliefs(agent_id=agent_id)
+ except Exception:
+ out["phases"]["sync_world"] = {"ok": None, "mode": "via_pulse_or_standalone"}
+
+ out["phases"]["room"] = room_status(agent_id=agent_id)
+
+ if seed:
+ out["phases"]["seed"] = seed_jspace_from_warehouse(
+ agent_id,
+ query=query,
+ session_id=session_id,
+ room=out["phases"]["room"],
+ )
+
+ world = out["phases"].get("world") or {}
+ seed_ph = out["phases"].get("seed") or {}
+ room = out["phases"].get("room") or {}
+ heart = out["phases"].get("ensure") or {}
+ out["gained"] = {
+ "warehouse_mode": heart.get("mode") or (out["phases"].get("center") or {}).get("mode"),
+ "world_beliefs": world.get("beliefs", 0),
+ "world_timeline": world.get("timeline", 0),
+ "jspace_hub": seed_ph.get("hub_n", 0),
+ "from_world": seed_ph.get("enriched_world", 0),
+ "from_cube": seed_ph.get("enriched_cube", 0),
+ "from_peers": seed_ph.get("enriched_peers", 0),
+ "room_mode": room.get("mode"),
+ "peer_agents": room.get("peer_n", 0),
+ }
+ heart_ok = bool(
+ heart.get("ok")
+ or heart.get("standalone_ready")
+ or heart.get("heart_ready")
+ or heart.get("mode") in ("standalone", "cube", "heart")
+ )
+ world_ok = bool(world.get("ok", True)) if enter_world else True
+ out["ok"] = heart_ok and world_ok
+ out["summary"] = (
+ f"Connected {agent_id}: hub={out['gained']['jspace_hub']} "
+ f"beliefs={out['gained']['world_beliefs']} "
+ f"room={out['gained']['room_mode']} "
+ f"peers={out['gained']['peer_agents']}"
+ )
+ return out
+
+
+def _strip_inject_lines(block: str) -> list[str]:
+ out: list[str] = []
+ for raw in (block or "").splitlines():
+ s = raw.strip()
+ if not s or s.startswith("#") or s.startswith("_"):
+ continue
+ if s.startswith("- "):
+ s = s[2:].strip()
+ # drop confidence prefix like [0.75]
+ if s.startswith("[") and "]" in s[:12]:
+ s = s.split("]", 1)[-1].strip()
+ if len(s) < 3:
+ continue
+ out.append(s[:200])
+ return out
+
+
# --- standalone warehouse (no Cube) -----------------------------------------
diff --git a/src/hermespace/hermes_base.py b/src/hermespace/hermes_base.py
index adeae01..9016f66 100644
--- a/src/hermespace/hermes_base.py
+++ b/src/hermespace/hermes_base.py
@@ -4,8 +4,13 @@
Hermes cannot J-lens arbitrary weights. The *base* (Hermespace OEW + optional
Cube heart) is the functional J-space for Hermes agents.
+Connect path: the moment an agent joins Hermespace, Cube (or standalone
+warehouse) charges the WorldModel and seeds the J-Space hub — a growing
+room that can include hive peer agents when configured.
+
from hermespace import HermesBase
base = HermesBase(agent_id="my-agent")
+ base.connect() # gain world + hub + optional hive room
base.status()
base.lens()
out = base.think("First repro then patch then verify", goal="Fix auth")
@@ -27,6 +32,46 @@ def __init__(self, agent_id: str | None = None, session_id: str = "main") -> Non
ensure_oew_env_default()
self.agent_id = (agent_id or os.environ.get("HERMESPACE_AGENT_ID") or "hermes-agent").strip()
self.session_id = session_id
+ self._last_connect: dict[str, Any] | None = None
+
+ # --- connect (intelligence gain) ---
+
+ def connect(
+ self,
+ *,
+ query: str = "",
+ enter_world: bool = True,
+ enter_workbench: bool = True,
+ charge: bool = True,
+ seed: bool = True,
+ ) -> dict[str, Any]:
+ """Enter Hermespace — heart, world, J-Space seed, optional hive room.
+
+ Maps research memories into a live room:
+ - Anthropic J-space → external hub the operator can lens
+ - Baars GWT → limited FOA broadcast
+ - Dehaene enduring memory → Cube / standalone warehouse charge
+ - Multi-agent growth → hive soul presence when ``HERMESCUBE_HIVE`` set
+ """
+ from hermespace.cube_module import connect_agent
+
+ out = connect_agent(
+ self.agent_id,
+ session_id=self.session_id,
+ query=query,
+ enter_world=enter_world,
+ enter_workbench=enter_workbench,
+ charge=charge,
+ seed=seed,
+ )
+ self._last_connect = out
+ return out
+
+ def room(self) -> dict[str, Any]:
+ """Hive / solo room status — who else is in the knowledge space."""
+ from hermespace.cube_module import room_status
+
+ return room_status(agent_id=self.agent_id)
# --- readiness ---
@@ -34,13 +79,18 @@ def status(self) -> dict[str, Any]:
"""Is this Hermes base operating as a J-space?"""
out: dict[str, Any] = {
"agent_id": self.agent_id,
+ "session_id": self.session_id,
"oew_enabled": oew_enabled(),
"oew_default_on": oew_default_on(),
"jspace": {},
"cube": {},
+ "world": {},
+ "room": {},
"ready": False,
+ "connected": bool(self._last_connect and self._last_connect.get("ok")),
"role": "external J-space for Hermes agents (access roles only)",
"video_ops": ["read/lens", "audit", "shape/reflect", "swap", "ablate", "harvest"],
+ "connect_ops": ["connect", "room", "pulse", "harvest"],
}
try:
from hermespace.jspace import JSpace, JSpaceEnv
@@ -68,6 +118,31 @@ def status(self) -> dict[str, Any]:
except Exception as exc:
out["cube"] = {"available": False, "error": type(exc).__name__}
+ try:
+ from hermespace.world import WorldModel
+
+ wm = WorldModel(agent_id=self.agent_id)
+ out["world"] = {
+ "beliefs": len(wm.state.beliefs or []),
+ "landmarks": len(wm.state.landmarks or []),
+ "timeline": wm.archive.count(),
+ "state": wm.state.current_state,
+ }
+ except Exception as exc:
+ out["world"] = {"error": type(exc).__name__}
+
+ try:
+ out["room"] = self.room()
+ except Exception as exc:
+ out["room"] = {"ok": False, "error": type(exc).__name__}
+
+ if self._last_connect:
+ out["last_connect"] = {
+ "ok": self._last_connect.get("ok"),
+ "gained": self._last_connect.get("gained"),
+ "summary": self._last_connect.get("summary"),
+ }
+
out["ready"] = (
oew_enabled()
and "error" not in out["jspace"]
@@ -149,8 +224,18 @@ def think(
plan: list[str] | None = None,
say: str = "",
force: bool = True,
+ connect_if_needed: bool = True,
) -> dict[str, Any]:
- """Run one higher-order Hermespace turn (OEW + Cube beat)."""
+ """Run one higher-order Hermespace turn (OEW + Cube beat).
+
+ On first think, auto-connects so the agent is not thinking in an empty room.
+ """
+ if connect_if_needed and not self._last_connect:
+ try:
+ self.connect(query=message[:120])
+ except Exception:
+ pass
+
from hermespace.io_contract import HermespaceInput
from hermespace.workflow import Workflow
@@ -177,6 +262,7 @@ def think(
"oew_ok": (out.meta or {}).get("jspace", {}).get("oew_ok"),
"goal": out.goal,
"decision": out.decision,
+ "connected": bool(self._last_connect and self._last_connect.get("ok")),
}
# --- night ---
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index d6f92e1..2b4d634 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -29,7 +29,8 @@ def on_session_start(**kwargs: Any) -> dict[str, str] | None:
agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
wb = Workbench(agent_id=agent_id, session_id=session_id)
- st = wb.enter()
+ # Lean enter — HermesBase.connect() below charges world / seeds hub / room
+ st = wb.enter(connect_warehouse=False)
env = probe_environment()
eng = HermespaceEngine()
@@ -82,33 +83,50 @@ def on_session_start(**kwargs: Any) -> dict[str, str] | None:
except Exception:
pass
- # Ensure Cube heart (or standalone warehouse) + seed J-Space
- block_extra_heart = ""
- block_extra_jspace = ""
+ # Full connect — Cube/world charge + J-Space seed + optional hive room
+ # Workbench.enter already ran lean warehouse ensure; connect fills the room.
+ block_extra_connect = ""
try:
- from hermespace.cube_module import ensure_heart
-
- heart = ensure_heart()
- block_extra_heart = (
- f"- warehouse: mode={heart.get('mode')} ok={heart.get('ok')} "
- f"created={heart.get('created')}\n"
- )
- except Exception:
- pass
-
- try:
- from hermespace.jspace import JSpace
- from hermespace.store import load_desk as _load_desk
-
- js = JSpace(agent_id=agent_id)
- desk0 = _load_desk(eng.desk_path)
- js.sync_from_desk(desk0, user_message=desk0.goal or "")
- block_extra_jspace = (
- f"- jspace: hub={len(js.state.hub)} focus={len(js.state.focus)} "
- f"mode={js.state.mode}\n"
+ from hermespace.hermes_base import HermesBase
+
+ hb = HermesBase(agent_id=agent_id, session_id=session_id)
+ # workbench already entered above — skip re-enter to avoid double env probe
+ conn = hb.connect(enter_workbench=False, query=desk.goal or "")
+ gained = conn.get("gained") or {}
+ room = (conn.get("phases") or {}).get("room") or {}
+ block_extra_connect = (
+ f"- warehouse: mode={gained.get('warehouse_mode')} ok={conn.get('ok')}\n"
+ f"- jspace: hub={gained.get('jspace_hub')} "
+ f"(world+{gained.get('from_world')} cube+{gained.get('from_cube')} "
+ f"peers+{gained.get('from_peers')})\n"
+ f"- world: beliefs={gained.get('world_beliefs')} "
+ f"timeline={gained.get('world_timeline')}\n"
+ f"- room: {gained.get('room_mode')} · peers={gained.get('peer_agents')}\n"
)
+ if room.get("note"):
+ block_extra_connect += f"- room_note: {room.get('note')}\n"
except Exception:
- pass
+ # Soft fallback — ensure heart + jspace sync only
+ try:
+ from hermespace.cube_module import ensure_heart
+ from hermespace.jspace import JSpace
+ from hermespace.store import load_desk as _load_desk
+ from hermespace.world import WorldModel
+
+ heart = ensure_heart()
+ js = JSpace(agent_id=agent_id)
+ desk0 = _load_desk(eng.desk_path)
+ js.sync_from_desk(desk0, user_message=desk0.goal or "")
+ wm = WorldModel(agent_id=agent_id)
+ wm.enter()
+ block_extra_connect = (
+ f"- warehouse: mode={heart.get('mode')} ok={heart.get('ok')}\n"
+ f"- jspace: hub={len(js.state.hub)} focus={len(js.state.focus)}\n"
+ f"- world: beliefs={len(wm.state.beliefs)} "
+ f"landmarks={len(wm.state.landmarks)}\n"
+ )
+ except Exception:
+ pass
block = (
"## Hermespace workbench (session start)\n"
@@ -116,29 +134,17 @@ def on_session_start(**kwargs: Any) -> dict[str, str] | None:
f"- skills_available: {skills}\n"
f"- tool_surfaces: {', '.join(surfaces[:10])}\n"
f"- park_count: {st.get('park_count', 0)}\n"
- f"{block_extra_heart}"
- f"{block_extra_jspace}"
+ f"{block_extra_connect}"
+ "- Connected: Cube/warehouse charged the world; J-Space hub seeded; "
+ "optional hive peers appear as silent presence.\n"
"- Pocket dimension online: park secondary goals, keep FOA tight, "
"user replies short; put operational detail in workspace context.\n"
- "- API: `from hermespace import Workbench` · "
+ "- API: `from hermespace import HermesBase, Workbench` · "
+ "`HermesBase(agent_id).connect()` · "
"`from hermespace.agent_api import encode_message, run_turn, decode_for_user`\n"
"- J-Space: hold/summon concepts; silent steps stay in model context only.\n"
)
- # World enter — agent enters persistent world
- try:
- from hermespace.world import WorldModel
- wm = WorldModel(agent_id=agent_id)
- wm.enter()
- block += (
- "\n## World\n"
- f"- agent: {agent_id} · state: {wm.state.current_state}\n"
- f"- beliefs: {len(wm.state.beliefs)} · landmarks: {len(wm.state.landmarks)}\n"
- f"- evolutions: {wm.state.evolution_count}\n"
- )
- except Exception:
- pass
-
return {"context": block}
diff --git a/src/hermespace/workbench.py b/src/hermespace/workbench.py
index 4f4c964..0486a11 100644
--- a/src/hermespace/workbench.py
+++ b/src/hermespace/workbench.py
@@ -108,8 +108,14 @@ def save(self) -> Path:
self.path.write_text(json.dumps(asdict(self.state), indent=2), encoding="utf-8")
return self.path
- def enter(self) -> dict[str, Any]:
- """Agent enters the pocket dimension (idle ready) with full env kit."""
+ def enter(self, *, connect_warehouse: bool = True) -> dict[str, Any]:
+ """Agent enters the pocket dimension (idle ready) with full env kit.
+
+ When ``connect_warehouse`` is True (default), also charge Cube/world
+ wisdom into J-Space and surface hive room presence — the intelligence
+ gain on join. Set False when ``cube_module.connect_agent`` already
+ orchestrates those phases (avoids recursion).
+ """
if self.state.mode != "working":
self.state.mode = "idle"
env = probe_environment()
@@ -151,6 +157,40 @@ def enter(self) -> dict[str, Any]:
}
except Exception as exc: # noqa: BLE001
self.state.meta["jspace"] = {"error": type(exc).__name__}
+
+ if connect_warehouse:
+ try:
+ from hermespace.cube_module import room_status, seed_jspace_from_warehouse
+ from hermespace.cube_module import cube_pulse
+ from hermespace.world import WorldModel
+
+ WorldModel(agent_id=self.agent_id).enter()
+ pulse = cube_pulse(agent_id=self.agent_id, ensure=False)
+ room = room_status(agent_id=self.agent_id)
+ seed = seed_jspace_from_warehouse(
+ self.agent_id,
+ query="",
+ session_id=self.session_id,
+ room=room,
+ )
+ self.state.meta["connect"] = {
+ "pulse_ok": pulse.get("ok"),
+ "room_mode": room.get("mode"),
+ "peer_n": room.get("peer_n", 0),
+ "hub_n": seed.get("hub_n"),
+ "from_world": seed.get("enriched_world"),
+ "from_cube": seed.get("enriched_cube"),
+ "from_peers": seed.get("enriched_peers"),
+ }
+ if seed.get("hub_n") is not None:
+ self.state.meta["jspace"] = {
+ **(self.state.meta.get("jspace") or {}),
+ "hub_n": seed.get("hub_n"),
+ "focus_n": seed.get("focus_n"),
+ }
+ except Exception as exc: # noqa: BLE001
+ self.state.meta["connect"] = {"ok": False, "error": type(exc).__name__}
+
self.save()
st = self.status()
st["environment_summary"] = {
@@ -161,6 +201,8 @@ def enter(self) -> dict[str, Any]:
}
st["heart"] = self.state.meta.get("heart")
st["jspace"] = self.state.meta.get("jspace")
+ st["connect"] = self.state.meta.get("connect")
+ st["room"] = (self.state.meta.get("connect") or {}).get("room_mode")
return st
def park_goal(self, goal: str, note: str = "", tags: list[str] | None = None) -> dict[str, Any]:
diff --git a/tests/test_connect_room.py b/tests/test_connect_room.py
new file mode 100644
index 0000000..2bd5c2d
--- /dev/null
+++ b/tests/test_connect_room.py
@@ -0,0 +1,122 @@
+"""Cube-centered connect — agent gains world + J-Space + optional hive room."""
+
+from __future__ import annotations
+
+import json
+import os
+import tempfile
+import unittest
+from pathlib import Path
+from unittest import mock
+
+
+class TestConnectRoom(unittest.TestCase):
+ def setUp(self) -> None:
+ self.tmp = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self.tmp.name
+ os.environ["HERMESPACE_OEW"] = "1"
+ os.environ.pop("HERMESCUBE_HIVE", None)
+
+ def tearDown(self) -> None:
+ self.tmp.cleanup()
+ os.environ.pop("HERMESPACE_HOME", None)
+ os.environ.pop("HERMESPACE_OEW", None)
+ os.environ.pop("HERMESCUBE_HIVE", None)
+
+ def test_connect_standalone_gains_world_and_hub(self) -> None:
+ from hermespace import HermesBase
+ from hermespace.world import WorldModel
+
+ aid = "connect-solo"
+ wm = WorldModel(agent_id=aid)
+ wm.add_belief("Deploys need canaries", 0.9, source="test")
+ wm.add_belief("Prefer dual decode", 0.85, source="test")
+
+ hb = HermesBase(agent_id=aid, session_id="s1")
+ out = hb.connect(query="deploy")
+ self.assertTrue(out.get("ok"), msg=out)
+ gained = out.get("gained") or {}
+ self.assertGreaterEqual(int(gained.get("world_beliefs") or 0), 1)
+ self.assertGreaterEqual(int(gained.get("jspace_hub") or 0), 1)
+ self.assertEqual(gained.get("room_mode"), "solo")
+ self.assertIn("Connected", out.get("summary") or "")
+
+ st = hb.status()
+ self.assertTrue(st["ready"])
+ self.assertTrue(st.get("connected"))
+ self.assertGreaterEqual(int(st["jspace"].get("hub_n") or 0), 1)
+
+ def test_room_solo_and_hive_env(self) -> None:
+ import types
+
+ from hermespace.cube_module import room_status
+
+ solo = room_status(agent_id="a1")
+ self.assertEqual(solo["mode"], "solo")
+ self.assertFalse(solo["hive_configured"])
+
+ hive = Path(self.tmp.name) / "hive"
+ os.environ["HERMESCUBE_HIVE"] = str(hive)
+
+ hive_mod = types.ModuleType("hermescube.hive")
+
+ def hive_status(_root):
+ return {"ok": True, "name": "test-hive", "entries": 3}
+
+ def list_souls(_root):
+ return [
+ {"agent_id": "a1", "wisdom": ["self"]},
+ {"agent_id": "peer-x", "wisdom": ["shared canary rule", "two"]},
+ ]
+
+ hive_mod.hive_status = hive_status # type: ignore[attr-defined]
+ hive_mod.list_souls = list_souls # type: ignore[attr-defined]
+ pkg = types.ModuleType("hermescube")
+ with mock.patch.dict(
+ "sys.modules",
+ {"hermescube": pkg, "hermescube.hive": hive_mod},
+ ):
+ st = room_status(agent_id="a1")
+ self.assertTrue(st["hive_configured"])
+ self.assertEqual(st["mode"], "hive")
+ self.assertEqual(st["peer_n"], 1)
+ self.assertEqual(st["soul_n"], 2)
+
+ def test_seed_peers_silent(self) -> None:
+ from hermespace.cube_module import seed_jspace_from_warehouse
+ from hermespace.jspace import JSpace
+
+ aid = "peer-seed"
+ room = {
+ "mode": "hive",
+ "souls": [
+ {"agent_id": aid, "self": True, "wisdom_n": 0},
+ {"agent_id": "alice-agent", "self": False, "wisdom_n": 3},
+ {"agent_id": "bob-agent", "self": False, "wisdom_n": 1},
+ ],
+ }
+ rep = seed_jspace_from_warehouse(aid, room=room)
+ self.assertTrue(rep.get("ok"))
+ self.assertGreaterEqual(int(rep.get("enriched_peers") or 0), 1)
+ js = JSpace(agent_id=aid)
+ labels = " ".join(c.text for c in js.state.hub).casefold()
+ self.assertIn("alice-agent", labels)
+ # Peer presence should be silent (not Report by default)
+ silent_texts = " ".join(c.text for c in js.state.hub if c.silent).casefold()
+ self.assertIn("peer agent", silent_texts)
+
+ def test_hermes_base_think_autoconnect(self) -> None:
+ from hermespace import HermesBase
+
+ hb = HermesBase(agent_id="auto-conn")
+ out = hb.think(
+ "First analyze then implement finally verify",
+ goal="Ship",
+ say="On it.",
+ )
+ self.assertFalse(out["skipped"])
+ self.assertTrue(out.get("connected"))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_hermes_base.py b/tests/test_hermes_base.py
index f60e43b..97df48c 100644
--- a/tests/test_hermes_base.py
+++ b/tests/test_hermes_base.py
@@ -25,6 +25,18 @@ def test_status_ready(self) -> None:
self.assertTrue(st["oew_enabled"])
self.assertTrue(st["ready"])
self.assertIn("read/lens", st["video_ops"])
+ self.assertIn("connect", st["connect_ops"])
+ self.assertIn("room", st)
+
+ def test_connect_facade(self) -> None:
+ from hermespace import HermesBase
+
+ hb = HermesBase(agent_id="base-connect")
+ out = hb.connect()
+ self.assertTrue(out.get("ok"), msg=out)
+ self.assertIn("gained", out)
+ room = hb.room()
+ self.assertEqual(room.get("mode"), "solo")
def test_think_ignites(self) -> None:
from hermespace import HermesBase
From 597482bdcdb677ffac3e2d9789a041755a6c6931 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Tue, 4 Aug 2026 00:28:07 +0000
Subject: [PATCH 07/16] feat(0.23): unify Hermespace into open-source
JSpaceEngine
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Merge FOA hub, OEW, dual decode, connect, lens/audit/scalpel, and harvest
into one J-Space Engine for Hermes Agent — warehouse optional.
Fixes: silent inject double-hold, Workbench second beat, double hub sync,
fragmented night harvest. Adds access_roles, metrics, chain, probe, and
plugin user_reply_hint. HermesBase becomes a thin alias.
Co-authored-by: Pablo
---
LAYOUT.md | 2 +-
README.md | 4 +-
docs/architecture/CODEMAP.md | 41 +-
docs/jspace/00-map.md | 3 +
docs/jspace/34-jspace-engine.md | 107 +++++
hermes_plugin/__init__.py | 2 +-
hermes_plugin/plugin.yaml | 10 +-
pyproject.toml | 2 +-
src/hermespace/__init__.py | 7 +-
src/hermespace/cli.py | 45 ++-
src/hermespace/engine.py | 11 +-
src/hermespace/grid/dream.py | 40 +-
src/hermespace/hermes_base.py | 282 +------------
src/hermespace/hermes_bridge.py | 63 ++-
src/hermespace/jspace/__init__.py | 13 +-
src/hermespace/jspace/engine.py | 649 ++++++++++++++++++++++++++++++
src/hermespace/jspace/env.py | 37 +-
src/hermespace/workbench.py | 66 +--
src/hermespace/workflow.py | 2 +
tests/test_connect_room.py | 2 +-
tests/test_hermes_base.py | 5 +-
tests/test_hermes_bridge.py | 12 +-
tests/test_jspace_engine.py | 109 +++++
23 files changed, 1080 insertions(+), 434 deletions(-)
create mode 100644 docs/jspace/34-jspace-engine.md
create mode 100644 src/hermespace/jspace/engine.py
create mode 100644 tests/test_jspace_engine.py
diff --git a/LAYOUT.md b/LAYOUT.md
index b864024..59a44ce 100644
--- a/LAYOUT.md
+++ b/LAYOUT.md
@@ -10,7 +10,7 @@ North star: [PURPOSE.md](PURPOSE.md) · Assessments:
hermespace/
├── PURPOSE.md / ABOUT.md / README.md / LAYOUT.md
├── src/hermespace/ # Python package
-│ ├── jspace/ # ★ External J-Space (hub · env · protocol)
+│ ├── jspace/ # ★ J-Space Engine (hub · env · protocol · engine)
│ ├── turn/ # planned — workflow/desk/gate (README only)
│ ├── memory/ # planned — world/episodic/semantic (README only)
│ ├── warehouse/ # planned — cube cable (README only)
diff --git a/README.md b/README.md
index 1eb314b..29d8839 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -211,7 +211,7 @@ $HERMESPACE_HOME/memory/hermespace/
| Command | Purpose |
|---------|---------|
| `hs world show\|enter\|leave\|evolve\|search\|archive-stats` | Persistent world |
-| `hs base connect\|room\|status\|think\|lens\|audit\|reflect\|harvest` | Hermes base as J-space (connect + Anthropic video ops) |
+| `hs base connect\|turn\|roles\|metrics\|lens\|audit\|chain\|harvest` | J-Space Engine (open-source GWT for Hermes) |
| `hs jspace hold\|report\|broadcast\|lens\|swap\|audit\|reflect\|harvest\|view` | True J-Space environment |
| `hs cube status\|ensure\|beat\|pulse\|seal\|inject` | Cube heart/center (standalone-safe) |
| `hs turn` | Full INPUT → OUTPUT turn |
diff --git a/docs/architecture/CODEMAP.md b/docs/architecture/CODEMAP.md
index 904ef23..ca70916 100644
--- a/docs/architecture/CODEMAP.md
+++ b/docs/architecture/CODEMAP.md
@@ -4,7 +4,7 @@ Hermespace is one Python package (`src/hermespace/`) plus a thin Hermes plugin
(`hermes_plugin/`), skill, and optional desktop UI.
**North star:** [PURPOSE.md](../../PURPOSE.md) · **Layout:** [LAYOUT.md](../../LAYOUT.md) ·
-**Assessment:** [../assessment/28-hermes-agent-jspace-assessment.md](../assessment/28-hermes-agent-jspace-assessment.md)
+**Engine:** [../jspace/34-jspace-engine.md](../jspace/34-jspace-engine.md)
## Layers (edit here first)
@@ -12,36 +12,39 @@ Hermespace is one Python package (`src/hermespace/`) plus a thin Hermes plugin
L6 operator cli.py ops.py scripts/ desktop_plugin/
L5 autonomy grid/* pulse.py
L4 identity world.py episodic.py semantic.py memory_db.py
-L3 warehouse cube_module.py (Cube center/heart OR standalone)
-L2 J-Space OEW jspace/ cognition.py streams.py neural_space.py
-L1 turn spine workflow.py engine.py desk.py gate.py inject.py
+L3 warehouse cube_module.py (optional Cube OR standalone)
+L2 J-Space ★ jspace/engine.py hub env protocol oew
+L1 desk spine workflow.py engine.py desk.py gate.py inject.py
L0 contract io_contract.py paths.py store.py agent_api.py
```
-## J-Space package (`jspace/`) — L2
+## J-Space package (`jspace/`) — L2 product
| Module | Role |
|--------|------|
+| **`jspace/engine.py`** | **`JSpaceEngine`** — connect · turn · lens · chain · harvest |
| `jspace/hub.py` | Hub · hold · reason · report · broadcast |
| `jspace/env.py` | Lens · swap · audit · reflect · harvest |
| `jspace/protocol.py` | OEW gate (`HERMESPACE_OEW`) |
+| `jspace/oew.py` | Causal beat · sticky redirect · reflect seeds |
| `jspace_env.py` | Compat shim → `jspace.env` |
+| `hermes_base.py` | Thin alias of `JSpaceEngine` |
-## Turn spine (L1)
+## Desk spine (L1)
| Module | Role |
|--------|------|
-| `workflow.py` | GATE→ENCODE→DESK→PLAN→DECODE→BROADCAST→SEAL + Cube/J-Space |
-| `engine.py` | enter / update / seal desk |
+| `workflow.py` | Single material ignition (used by `JSpaceEngine.turn`) |
+| `engine.py` | DeskEngine — ACTIVE.md enter / update / seal |
| `desk.py` | ACTIVE.md model |
| `gate.py` | Selectivity — skip trivial |
-| `inject.py` | GWT broadcast of desk |
+| `inject.py` | Desk GWT strip |
-## Warehouse cable (L3)
+## Warehouse cable (L3 — optional)
| Module | Role |
|--------|------|
-| `cube_module.py` | `cube_beat` / `cube_pulse` / `seal_learning` / standalone |
+| `cube_module.py` | Soft arterial strip / seal / pulse / room |
## Integration
@@ -50,17 +53,13 @@ L0 contract io_contract.py paths.py store.py agent_api.py
| `hermes_bridge.py` | Plugin hooks — session / pre_llm / end |
| `hermes_plugin/` | Thin `register(ctx)` |
| `workbench.py` | Session pocket — enter / order / idle |
-| `agent_api.py` | Public doors for agents |
-
-## Planned packages (README only)
-
-`turn/` · `memory/` · `warehouse/` — move modules after OEW Phase B is green.
+| `agent_api.py` | Dual-decode doors |
## Rules of thumb
-1. Dual decode: never dump `context` into user chat.
-2. Soft-fail Cube — Space must run standalone.
-3. When Cube present: `.cube` is durable SoT; world JSONL is projection.
-4. Peel grid features; don’t inflate `cli.py` without need.
-5. J-Space silent steps stay in model context only.
+1. Prefer `from hermespace import JSpaceEngine` — one operating surface.
+2. Dual decode: never dump `context` into user chat.
+3. Soft-fail warehouse — engine must run standalone.
+4. Single ignition path: no second beat after `Workflow.run`.
+5. Silent steps stay in model context only.
6. Prefer `from hermespace.jspace import …` over deep/compat paths.
diff --git a/docs/jspace/00-map.md b/docs/jspace/00-map.md
index a4ff30f..7050653 100644
--- a/docs/jspace/00-map.md
+++ b/docs/jspace/00-map.md
@@ -43,6 +43,9 @@ presence (`HERMESCUBE_HIVE`). Space still owns FOA competition and dual decode.
Living map of these research memories:
[33-living-memories-cube-world.md](../assessment/33-living-memories-cube-world.md).
+**Product surface (v0.23+):** [`JSpaceEngine`](34-jspace-engine.md) — one engine for
+connect / turn / lens / chain / harvest. Warehouse optional.
+
## Sources
- https://www.anthropic.com/research/global-workspace
diff --git a/docs/jspace/34-jspace-engine.md b/docs/jspace/34-jspace-engine.md
new file mode 100644
index 0000000..bc4fcd4
--- /dev/null
+++ b/docs/jspace/34-jspace-engine.md
@@ -0,0 +1,107 @@
+# J-Space Engine — Hermespace as open-source GWT for Hermes Agent
+
+**Version:** 0.23.0
+**Thesis:** Anthropic’s J-space is a *privileged verbalizable workspace* found
+inside weights. Hermes Agent usually has **no weight access**. Hermespace
+therefore ships a **true J-Space Engine** — the same five access roles, as an
+open-source harness Hermes can actually run.
+
+Research: [Anthropic global workspace](https://www.anthropic.com/research/global-workspace) ·
+[`anthropics/jacobian-lens`](https://github.com/anthropics/jacobian-lens) (optional open-weight companion)
+
+---
+
+## 1. One engine
+
+```python
+from hermespace import JSpaceEngine
+
+eng = JSpaceEngine(agent_id="my-agent")
+eng.connect() # world + hub seed
+out = eng.turn("First repro then patch then verify", goal="Fix auth")
+print(eng.decode_user(out)) # short Report
+print(eng.decode_model(out)[:400]) # dense context (never dump to chat)
+print(eng.lens()) # what is on the agent's mind
+eng.chain("spider", "8 legs") # silent multi-step
+eng.swap("ship now", "canary first") # causal redirect
+print(eng.access_roles()) # five GWT properties, live
+eng.harvest() # night consolidation
+```
+
+CLI:
+
+```bash
+hs base connect
+hs base roles
+hs base metrics
+hs base probe -m "thanks!"
+hs base turn -m "First check then implement finally verify" --goal "Ship"
+hs base chain -s "repro" -s "patch" -s "verify"
+hs base lens
+hs base harvest
+```
+
+`HermesBase` is a thin alias of `JSpaceEngine`. Desk file ops remain in
+`HermespaceEngine` (desk spine only).
+
+---
+
+## 2. Five access roles (Anthropic → Hermespace)
+
+| Anthropic / GWT | Engine API |
+|-----------------|------------|
+| Verbal report | `report()` · `decode_user()` |
+| Directed modulation | `hold` · `swap` · `inject` · `ablate` |
+| Internal reasoning | `chain()` · OEW auto-park · silent hub |
+| Flexible broadcast | `broadcast()` → pre_llm / desk / fabric |
+| Selectivity | `probe_material()` · gate skip on trivial acks |
+
+Warehouse / HermesCube (if installed) is **optional arterial supply** — not
+required for the engine to operate.
+
+---
+
+## 3. Fixes folded into the engine cut
+
+| Issue | Fix |
+|-------|-----|
+| Double hub sync per turn | `advance_turn(already_synced=True)` |
+| Workbench second cube_beat | Removed — single Workflow ignition |
+| Silent inject double-hold | `inject_thought` → `reason_step` only |
+| Fragmented night harvest | `grid.dream` calls `dream_harvest` |
+| Plugin dual-decode honor system | `user_reply_hint` + Dual decode block in inject |
+| Many facades | `JSpaceEngine` is the product surface |
+
+---
+
+## 4. Pushing limits (what is possible without weight access)
+
+1. **Obligatory externalization** — material turns must park silent intermediates (OEW).
+2. **Causal scalpel** — sticky swap / inject / ablate reshape later Report + broadcast.
+3. **CRT / reflect** — interrupt → seed next mid-band principles.
+4. **Alignment soft audit** — eval-awareness / concealment lexicon on externalized text.
+5. **Capacity pressure metrics** — hub/silent fill ratios + ignition rate.
+6. **Optional open-weight J-lens** — `jlens_status()` detects `anthropics/jacobian-lens`
+ for local open models; Hermes Agent path stays harness-primary.
+7. **Growing world room** — connect seeds hub from WorldModel; hive peers optional.
+
+Honesty boundary: access-consciousness *roles* only. No phenomenal claims.
+No Jacobian lens on closed Hermes models.
+
+---
+
+## 5. Architecture
+
+```
+Hermes Agent (specialists: tools · skills · fluency)
+ │ material turns
+ ▼
+ JSpaceEngine ←── hs base / plugin hooks
+ ├── hub (≤25) · FOA (≤4) · silent chain
+ ├── OEW protocol (default ON)
+ ├── dual decode (Report ≠ inject)
+ └── night harvest → semantic / optional warehouse
+```
+
+See also: [32-hermes-base-as-jspace.md](32-hermes-base-as-jspace.md) ·
+[00-map.md](00-map.md) · [33-living-memories-cube-world.md](../assessment/33-living-memories-cube-world.md)
diff --git a/hermes_plugin/__init__.py b/hermes_plugin/__init__.py
index 735ac3e..477d204 100644
--- a/hermes_plugin/__init__.py
+++ b/hermes_plugin/__init__.py
@@ -7,7 +7,7 @@
import sys
from pathlib import Path
-__version__ = "0.22.0"
+__version__ = "0.23.0"
logger = logging.getLogger("hermes.plugins.hermespace")
diff --git a/hermes_plugin/plugin.yaml b/hermes_plugin/plugin.yaml
index ba6e606..dbe5ac8 100644
--- a/hermes_plugin/plugin.yaml
+++ b/hermes_plugin/plugin.yaml
@@ -1,10 +1,10 @@
name: hermespace
-version: "0.22.0"
+version: "0.23.0"
description: >
- Hermespace OEW J-Space for Hermes agents — connect charges Cube/world into
- the hub (optional hive room), higher-order thinking (lens/audit/reflect),
- dual decode. Hooks: on_session_start, pre_llm_call, on_session_end.
- HERMESPACE_OEW=1 by default. Set HERMESPACE_ROOT to checkout.
+ Hermespace J-Space Engine for Hermes agents — open-source GWT harness
+ (report/modulate/silent-reason/broadcast/selectivity), OEW ON by default,
+ dual decode, optional warehouse. Hooks: on_session_start, pre_llm_call,
+ on_session_end. Set HERMESPACE_ROOT to checkout.
author: Hermespace contributors
kind: standalone
hooks:
diff --git a/pyproject.toml b/pyproject.toml
index 46a930c..ca0c1c3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "hermespace"
-version = "0.22.0"
+version = "0.23.0"
description = "OEW J-Space for Hermes Agent — obligatory external workspace, Cube heart, higher-order thinking"
requires-python = ">=3.10"
readme = "README.md"
diff --git a/src/hermespace/__init__.py b/src/hermespace/__init__.py
index ba38854..4ae6f57 100644
--- a/src/hermespace/__init__.py
+++ b/src/hermespace/__init__.py
@@ -1,8 +1,8 @@
-"""Hermespace — local global workspace for Hermes agents."""
+"""Hermespace — open-source J-Space Engine for Hermes agents."""
from __future__ import annotations
-__version__ = "0.22.0"
+__version__ = "0.23.0"
from hermespace.desk import Desk
from hermespace.engine import HermespaceEngine
@@ -31,12 +31,15 @@
from hermespace.world import WorldModel, get_world, world_context
from hermespace.jspace import JSpace, get_jspace, JSpaceEnv, get_env
from hermespace.jspace import evaluate_material_turn
+from hermespace.jspace import JSpaceEngine, ACCESS_ROLES
from hermespace import cube_module
from hermespace.hermes_base import HermesBase
__all__ = [
"Desk",
"HermespaceEngine",
+ "JSpaceEngine",
+ "ACCESS_ROLES",
"Workflow",
"TurnResult",
"HermespaceInput",
diff --git a/src/hermespace/cli.py b/src/hermespace/cli.py
index 381a83e..9a73b91 100644
--- a/src/hermespace/cli.py
+++ b/src/hermespace/cli.py
@@ -154,20 +154,24 @@ def main(argv: list[str] | None = None) -> int:
# Hermes base as J-space (Anthropic video ops: read / audit / shape)
base = sub.add_parser(
"base",
- help="Hermes base as J-space: connect / status / think / lens / audit / reflect / harvest",
+ help="J-Space Engine: connect / status / turn / lens / audit / reflect / harvest",
)
base_sub = base.add_subparsers(dest="base_cmd", required=True)
basec = base_sub.add_parser(
"connect",
- help="Agent joins Hermespace — charge world, seed J-Space, surface hive room",
+ help="Join J-Space Engine — world + hub seed (warehouse optional)",
)
basec.add_argument("--agent-id", default="hermes-agent")
basec.add_argument("--session-id", default="main")
- basec.add_argument("-q", "--query", default="", help="Optional focus for Cube strip")
- bases = base_sub.add_parser("status", help="Is this Hermes base J-space-ready?")
+ basec.add_argument("-q", "--query", default="", help="Optional focus for warehouse strip")
+ bases = base_sub.add_parser("status", help="Engine readiness + access roles + metrics")
bases.add_argument("--agent-id", default="hermes-agent")
- baseroom = base_sub.add_parser("room", help="Hive/solo room — peer agents in the knowledge space")
+ baseroom = base_sub.add_parser("room", help="Solo/hive room status")
baseroom.add_argument("--agent-id", default="hermes-agent")
+ basem = base_sub.add_parser("metrics", help="Capacity / ignition pressure")
+ basem.add_argument("--agent-id", default="hermes-agent")
+ baseroles = base_sub.add_parser("roles", help="Anthropic GWT access roles (live)")
+ baseroles.add_argument("--agent-id", default="hermes-agent")
basel = base_sub.add_parser("lens", help="Read workspace (external J-lens)")
basel.add_argument("--agent-id", default="hermes-agent")
basea = base_sub.add_parser("audit", help="Soft alignment scan")
@@ -177,11 +181,22 @@ def main(argv: list[str] | None = None) -> int:
baset.add_argument("--goal", default="")
baset.add_argument("--say", default="")
baset.add_argument("--agent-id", default="hermes-agent")
+ baseturn = base_sub.add_parser("turn", help="Alias of think — single ignition path")
+ baseturn.add_argument("-m", "--message", required=True)
+ baseturn.add_argument("--goal", default="")
+ baseturn.add_argument("--say", default="")
+ baseturn.add_argument("--agent-id", default="hermes-agent")
+ basechain = base_sub.add_parser("chain", help="Park multi-step silent reasoning")
+ basechain.add_argument("-s", "--step", action="append", default=[], required=True)
+ basechain.add_argument("--agent-id", default="hermes-agent")
+ baseprobe = base_sub.add_parser("probe", help="Would this message ignite the workspace?")
+ baseprobe.add_argument("-m", "--message", required=True)
+ baseprobe.add_argument("--agent-id", default="hermes-agent")
baser = base_sub.add_parser("reflect", help="Counterfactual reflection (shape later thought)")
baser.add_argument("-a", "--answer", default="")
baser.add_argument("--principle", action="append", default=[])
baser.add_argument("--agent-id", default="hermes-agent")
- baseh = base_sub.add_parser("harvest", help="Night harvest into Cube/semantic")
+ baseh = base_sub.add_parser("harvest", help="Night harvest into warehouse/semantic")
baseh.add_argument("--agent-id", default="hermes-agent")
baseh.add_argument("--clear-silent", action="store_true")
@@ -749,11 +764,11 @@ def main(argv: list[str] | None = None) -> int:
return 2
if args.cmd == "base":
- from hermespace.hermes_base import HermesBase
+ from hermespace import JSpaceEngine
aid = getattr(args, "agent_id", "hermes-agent") or "hermes-agent"
sid = getattr(args, "session_id", "main") or "main"
- hb = HermesBase(agent_id=aid, session_id=sid)
+ hb = JSpaceEngine(agent_id=aid, session_id=sid)
bcmd = args.base_cmd
if bcmd == "connect":
print(
@@ -770,15 +785,27 @@ def main(argv: list[str] | None = None) -> int:
if bcmd == "room":
print(json.dumps(hb.room(), indent=2, default=str))
return 0
+ if bcmd == "metrics":
+ print(json.dumps(hb.metrics(), indent=2, default=str))
+ return 0
+ if bcmd == "roles":
+ print(json.dumps(hb.access_roles(), indent=2, default=str))
+ return 0
if bcmd == "lens":
print(hb.lens())
return 0
if bcmd == "audit":
print(json.dumps(hb.audit(), indent=2))
return 0
- if bcmd == "think":
+ if bcmd in ("think", "turn"):
print(json.dumps(hb.think(args.message, goal=args.goal, say=args.say), indent=2))
return 0
+ if bcmd == "chain":
+ print(json.dumps(hb.chain(*list(args.step or [])), indent=2))
+ return 0
+ if bcmd == "probe":
+ print(json.dumps(hb.probe_material(args.message), indent=2))
+ return 0
if bcmd == "reflect":
print(
json.dumps(
diff --git a/src/hermespace/engine.py b/src/hermespace/engine.py
index e858d49..f32bbed 100644
--- a/src/hermespace/engine.py
+++ b/src/hermespace/engine.py
@@ -1,4 +1,8 @@
-"""Hermespace engine — enter / seal / load sources / functional API."""
+"""Desk spine — enter / seal / load sources for ACTIVE desk state.
+
+Product J-Space Engine lives at ``hermespace.jspace.engine.JSpaceEngine``.
+This class remains the desk-file operator used inside Workflow turns.
+"""
from __future__ import annotations
@@ -12,7 +16,10 @@
class HermespaceEngine:
- """Functional workspace — Baddeley/GWT-aligned, not ceremony."""
+ """DeskEngine — ACTIVE.md spine (not the product J-Space Engine).
+
+ Use ``JSpaceEngine`` for connect / turn / lens / harvest.
+ """
def __init__(self, desk_path: Path | None = None) -> None:
self.desk_path = desk_path or default_desk_path()
diff --git a/src/hermespace/grid/dream.py b/src/hermespace/grid/dream.py
index 8768b4d..897af9b 100644
--- a/src/hermespace/grid/dream.py
+++ b/src/hermespace/grid/dream.py
@@ -66,48 +66,26 @@ def run_dream(agent_id: str = "default", *, force_material: bool = False) -> Dre
f"modules={len(mods)}",
f"title={profile.get('title') or 'unset'}",
]
- # J-Space dream harvest — day unspoken thoughts → durable night memory
+ # J-Space dream harvest — single path via JSpaceEnv.dream_harvest
jspace_harvest: dict[str, Any] = {}
try:
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.jspace import JSpaceEnv
aid = agent_id if agent_id not in ("default", "") else "hermes-agent"
env = JSpaceEnv(agent_id=aid)
- # Don't recurse into run_dream from harvest — seal only here
- harvested: list[str] = []
- for s in env.space.state.silent_steps:
- if s.strip():
- harvested.append(s.strip()[:300])
- for c in sorted(env.space.state.hub, key=lambda x: x.salience, reverse=True)[:8]:
- if c.salience >= 0.75 and c.text.strip() and c.text.strip() not in harvested:
- harvested.append(c.text.strip()[:300])
- sealed_n = 0
- if harvested:
+ harvest = env.dream_harvest(seal_to_cube=True, clear_silent=False)
+ harvested_n = int(harvest.get("harvested") or 0)
+ sealed_n = int(harvest.get("sealed") or 0)
+ if harvested_n:
material = True
- try:
- from hermespace.cube_module import seal_learning
-
- for item in harvested[:6]:
- rec = seal_learning(
- item,
- entry_type="belief",
- agent_id=aid,
- source="jspace_night_dream",
- trust=0.7,
- )
- if rec.get("ok"):
- sealed_n += 1
- except Exception:
- pass
- actions.append(f"jspace_harvest n={len(harvested)} sealed={sealed_n}")
- summary_parts.append(f"jspace_harvest={len(harvested)}")
- # Soft audit overnight
+ actions.append(f"jspace_harvest n={harvested_n} sealed={sealed_n}")
+ summary_parts.append(f"jspace_harvest={harvested_n}")
findings = env.audit()
alerts = sum(1 for f in findings if f.severity == "alert")
if alerts:
actions.append(f"jspace_audit_alerts={alerts}")
summary_parts.append(f"jspace_alerts={alerts}")
- jspace_harvest = {"harvested": len(harvested), "sealed": sealed_n}
+ jspace_harvest = {"harvested": harvested_n, "sealed": sealed_n, "via": "dream_harvest"}
except Exception as e: # noqa: BLE001
actions.append(f"jspace_harvest_skip:{type(e).__name__}")
diff --git a/src/hermespace/hermes_base.py b/src/hermespace/hermes_base.py
index 9016f66..243844b 100644
--- a/src/hermespace/hermes_base.py
+++ b/src/hermespace/hermes_base.py
@@ -1,283 +1,15 @@
-"""Hermes base as J-space — one facade for day-to-day higher-order use.
+"""HermesBase — thin product alias of ``JSpaceEngine``.
-Anthropic's X video: read, audit, and shape what the model is thinking.
-Hermes cannot J-lens arbitrary weights. The *base* (Hermespace OEW + optional
-Cube heart) is the functional J-space for Hermes agents.
-
-Connect path: the moment an agent joins Hermespace, Cube (or standalone
-warehouse) charges the WorldModel and seeds the J-Space hub — a growing
-room that can include hive peer agents when configured.
-
- from hermespace import HermesBase
- base = HermesBase(agent_id="my-agent")
- base.connect() # gain world + hub + optional hive room
- base.status()
- base.lens()
- out = base.think("First repro then patch then verify", goal="Fix auth")
+Prefer ``from hermespace import JSpaceEngine`` for new code.
+``HermesBase`` remains for day-to-day CLI / docs compatibility.
"""
from __future__ import annotations
-import os
-from typing import Any
-
-from hermespace.jspace.oew import ensure_oew_env_default, oew_default_on
-from hermespace.jspace.protocol import oew_enabled
-
-
-class HermesBase:
- """Functional J-space of a Hermes base (Space ± Cube)."""
-
- def __init__(self, agent_id: str | None = None, session_id: str = "main") -> None:
- ensure_oew_env_default()
- self.agent_id = (agent_id or os.environ.get("HERMESPACE_AGENT_ID") or "hermes-agent").strip()
- self.session_id = session_id
- self._last_connect: dict[str, Any] | None = None
-
- # --- connect (intelligence gain) ---
-
- def connect(
- self,
- *,
- query: str = "",
- enter_world: bool = True,
- enter_workbench: bool = True,
- charge: bool = True,
- seed: bool = True,
- ) -> dict[str, Any]:
- """Enter Hermespace — heart, world, J-Space seed, optional hive room.
-
- Maps research memories into a live room:
- - Anthropic J-space → external hub the operator can lens
- - Baars GWT → limited FOA broadcast
- - Dehaene enduring memory → Cube / standalone warehouse charge
- - Multi-agent growth → hive soul presence when ``HERMESCUBE_HIVE`` set
- """
- from hermespace.cube_module import connect_agent
-
- out = connect_agent(
- self.agent_id,
- session_id=self.session_id,
- query=query,
- enter_world=enter_world,
- enter_workbench=enter_workbench,
- charge=charge,
- seed=seed,
- )
- self._last_connect = out
- return out
-
- def room(self) -> dict[str, Any]:
- """Hive / solo room status — who else is in the knowledge space."""
- from hermespace.cube_module import room_status
-
- return room_status(agent_id=self.agent_id)
-
- # --- readiness ---
-
- def status(self) -> dict[str, Any]:
- """Is this Hermes base operating as a J-space?"""
- out: dict[str, Any] = {
- "agent_id": self.agent_id,
- "session_id": self.session_id,
- "oew_enabled": oew_enabled(),
- "oew_default_on": oew_default_on(),
- "jspace": {},
- "cube": {},
- "world": {},
- "room": {},
- "ready": False,
- "connected": bool(self._last_connect and self._last_connect.get("ok")),
- "role": "external J-space for Hermes agents (access roles only)",
- "video_ops": ["read/lens", "audit", "shape/reflect", "swap", "ablate", "harvest"],
- "connect_ops": ["connect", "room", "pulse", "harvest"],
- }
- try:
- from hermespace.jspace import JSpace, JSpaceEnv
-
- js = JSpace(agent_id=self.agent_id)
- env = JSpaceEnv(agent_id=self.agent_id)
- out["jspace"] = {
- "hub_n": len(js.state.hub),
- "focus_n": len(js.state.focus),
- "silent_n": len(js.state.silent_steps),
- "band": env.band(),
- "pov": env.pov()[:120] if env.pov() else "",
- "protocol_enabled": bool(env._env.get("protocol_enabled", True)),
- }
- except Exception as exc:
- out["jspace"] = {"error": type(exc).__name__}
-
- try:
- from hermespace.cube_module import center_status, cube_available
-
- out["cube"] = {
- "available": bool(cube_available()),
- "status": center_status(),
- }
- except Exception as exc:
- out["cube"] = {"available": False, "error": type(exc).__name__}
-
- try:
- from hermespace.world import WorldModel
-
- wm = WorldModel(agent_id=self.agent_id)
- out["world"] = {
- "beliefs": len(wm.state.beliefs or []),
- "landmarks": len(wm.state.landmarks or []),
- "timeline": wm.archive.count(),
- "state": wm.state.current_state,
- }
- except Exception as exc:
- out["world"] = {"error": type(exc).__name__}
-
- try:
- out["room"] = self.room()
- except Exception as exc:
- out["room"] = {"ok": False, "error": type(exc).__name__}
-
- if self._last_connect:
- out["last_connect"] = {
- "ok": self._last_connect.get("ok"),
- "gained": self._last_connect.get("gained"),
- "summary": self._last_connect.get("summary"),
- }
-
- out["ready"] = (
- oew_enabled()
- and "error" not in out["jspace"]
- and int(out["jspace"].get("hub_n", -1)) >= 0
- )
- return out
-
- # --- Anthropic video ops: read ---
-
- def lens(self, *, top_k: int = 12, include_silent: bool = True) -> str:
- from hermespace.jspace import JSpaceEnv
-
- return JSpaceEnv(agent_id=self.agent_id).lens_markdown(
- top_k=top_k, include_silent=include_silent
- )
-
- def audit(self) -> list[dict[str, Any]]:
- from hermespace.jspace import JSpaceEnv
-
- return [f.to_dict() for f in JSpaceEnv(agent_id=self.agent_id).audit()]
-
- def report(self, *, include_silent: bool = False) -> str:
- from hermespace.jspace import JSpace
-
- return JSpace(agent_id=self.agent_id).report(include_silent=include_silent)
-
- # --- scalpel ---
-
- def hold(self, text: str, *, silent: bool = False) -> dict[str, Any]:
- from hermespace.jspace import JSpace
-
- c = JSpace(agent_id=self.agent_id).hold(text, silent=silent)
- return {"ok": True, "concept": c.label()}
-
- def swap(self, source: str, target: str) -> dict[str, Any]:
- from hermespace.jspace import JSpaceEnv
-
- return JSpaceEnv(agent_id=self.agent_id).swap(source, target)
-
- def inject(self, text: str, *, silent: bool = True) -> dict[str, Any]:
- from hermespace.jspace import JSpaceEnv
-
- c = JSpaceEnv(agent_id=self.agent_id).inject_thought(text, silent=silent)
- return {"ok": True, "concept": c.label(), "silent": silent}
-
- def ablate(self, *patterns: str) -> dict[str, Any]:
- from hermespace.jspace import JSpaceEnv
-
- return JSpaceEnv(agent_id=self.agent_id).ablate(*patterns)
-
- # --- shape (CRT) ---
-
- def reflect(
- self,
- answer: str = "",
- *,
- principles: list[str] | None = None,
- ) -> dict[str, Any]:
- from hermespace.jspace import JSpaceEnv
-
- r = JSpaceEnv(agent_id=self.agent_id).reflect(
- answer=answer, principles=principles or []
- )
- return r.to_dict()
-
- def set_pov(self, text: str) -> dict[str, Any]:
- from hermespace.jspace import JSpaceEnv
-
- JSpaceEnv(agent_id=self.agent_id).set_pov(text)
- return {"ok": True, "pov": text[:200]}
-
- # --- deliberate turn (ignition) ---
-
- def think(
- self,
- message: str,
- *,
- goal: str = "",
- plan: list[str] | None = None,
- say: str = "",
- force: bool = True,
- connect_if_needed: bool = True,
- ) -> dict[str, Any]:
- """Run one higher-order Hermespace turn (OEW + Cube beat).
-
- On first think, auto-connects so the agent is not thinking in an empty room.
- """
- if connect_if_needed and not self._last_connect:
- try:
- self.connect(query=message[:120])
- except Exception:
- pass
-
- from hermespace.io_contract import HermespaceInput
- from hermespace.workflow import Workflow
-
- out = Workflow().run(
- HermespaceInput(
- message=message,
- goal=goal or message[:200],
- plan=list(plan or ["execute"]),
- say=say or "",
- force=force,
- agent_id=self.agent_id,
- session_id=self.session_id,
- )
- )
- return {
- "skipped": out.skipped,
- "reason": out.reason,
- "report": out.report,
- "context_chars": len(out.context or ""),
- "has_jspace_broadcast": "J-Space" in (out.context or ""),
- "oew": (out.meta or {}).get("jspace", {}).get("oew")
- or (out.meta or {}).get("oew")
- or {},
- "oew_ok": (out.meta or {}).get("jspace", {}).get("oew_ok"),
- "goal": out.goal,
- "decision": out.decision,
- "connected": bool(self._last_connect and self._last_connect.get("ok")),
- }
-
- # --- night ---
-
- def harvest(self, *, clear_silent: bool = False) -> dict[str, Any]:
- from hermespace.jspace import JSpaceEnv
+from hermespace.jspace.engine import JSpaceEngine
- return JSpaceEnv(agent_id=self.agent_id).dream_harvest(
- seal_to_cube=True, clear_silent=clear_silent
- )
- def pulse(self) -> dict[str, Any]:
- try:
- from hermespace.cube_module import cube_pulse
+class HermesBase(JSpaceEngine):
+ """Functional J-space of a Hermes base — alias of JSpaceEngine."""
- return cube_pulse(agent_id=self.agent_id)
- except Exception as exc:
- return {"ok": False, "error": type(exc).__name__}
+ pass
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index 2b4d634..a284756 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -83,44 +83,42 @@ def on_session_start(**kwargs: Any) -> dict[str, str] | None:
except Exception:
pass
- # Full connect — Cube/world charge + J-Space seed + optional hive room
- # Workbench.enter already ran lean warehouse ensure; connect fills the room.
+ # Full connect via JSpaceEngine — world + hub seed (warehouse optional)
block_extra_connect = ""
try:
- from hermespace.hermes_base import HermesBase
+ from hermespace import JSpaceEngine
- hb = HermesBase(agent_id=agent_id, session_id=session_id)
- # workbench already entered above — skip re-enter to avoid double env probe
- conn = hb.connect(enter_workbench=False, query=desk.goal or "")
+ eng_js = JSpaceEngine(agent_id=agent_id, session_id=session_id)
+ conn = eng_js.connect(enter_workbench=False, query=desk.goal or "")
gained = conn.get("gained") or {}
room = (conn.get("phases") or {}).get("room") or {}
+ roles = ", ".join((conn.get("access_roles") or [])[:5])
block_extra_connect = (
- f"- warehouse: mode={gained.get('warehouse_mode')} ok={conn.get('ok')}\n"
+ f"- engine: JSpaceEngine · ok={conn.get('ok')}\n"
+ f"- warehouse: mode={gained.get('warehouse_mode')} (optional)\n"
f"- jspace: hub={gained.get('jspace_hub')} "
- f"(world+{gained.get('from_world')} cube+{gained.get('from_cube')} "
+ f"(world+{gained.get('from_world')} "
+ f"warehouse+{gained.get('from_warehouse', gained.get('from_cube', 0))} "
f"peers+{gained.get('from_peers')})\n"
f"- world: beliefs={gained.get('world_beliefs')} "
f"timeline={gained.get('world_timeline')}\n"
f"- room: {gained.get('room_mode')} · peers={gained.get('peer_agents')}\n"
+ f"- access_roles: {roles}\n"
)
if room.get("note"):
block_extra_connect += f"- room_note: {room.get('note')}\n"
except Exception:
- # Soft fallback — ensure heart + jspace sync only
try:
- from hermespace.cube_module import ensure_heart
from hermespace.jspace import JSpace
from hermespace.store import load_desk as _load_desk
from hermespace.world import WorldModel
- heart = ensure_heart()
js = JSpace(agent_id=agent_id)
desk0 = _load_desk(eng.desk_path)
js.sync_from_desk(desk0, user_message=desk0.goal or "")
wm = WorldModel(agent_id=agent_id)
wm.enter()
block_extra_connect = (
- f"- warehouse: mode={heart.get('mode')} ok={heart.get('ok')}\n"
f"- jspace: hub={len(js.state.hub)} focus={len(js.state.focus)}\n"
f"- world: beliefs={len(wm.state.beliefs)} "
f"landmarks={len(wm.state.landmarks)}\n"
@@ -129,20 +127,20 @@ def on_session_start(**kwargs: Any) -> dict[str, str] | None:
pass
block = (
- "## Hermespace workbench (session start)\n"
+ "## Hermespace J-Space Engine (session start)\n"
f"- mode: {st.get('mode')} · agent: {agent_id} · session: {session_id}\n"
f"- skills_available: {skills}\n"
f"- tool_surfaces: {', '.join(surfaces[:10])}\n"
f"- park_count: {st.get('park_count', 0)}\n"
f"{block_extra_connect}"
- "- Connected: Cube/warehouse charged the world; J-Space hub seeded; "
- "optional hive peers appear as silent presence.\n"
- "- Pocket dimension online: park secondary goals, keep FOA tight, "
- "user replies short; put operational detail in workspace context.\n"
- "- API: `from hermespace import HermesBase, Workbench` · "
- "`HermesBase(agent_id).connect()` · "
- "`from hermespace.agent_api import encode_message, run_turn, decode_for_user`\n"
- "- J-Space: hold/summon concepts; silent steps stay in model context only.\n"
+ "- Connected: open-source J-Space Engine online — report/modulate/"
+ "silent-reason/broadcast/selectivity.\n"
+ "- Pocket dimension: park secondary goals, keep FOA tight, "
+ "user replies short; operational detail stays in model context.\n"
+ "- API: `from hermespace import JSpaceEngine` · "
+ "`eng.connect()` · `eng.turn(...)` · "
+ "`eng.decode_user(out)` / `eng.decode_model(out)`\n"
+ "- Silent steps stay in model context only — never dump hub into chat.\n"
)
return {"context": block}
@@ -369,12 +367,14 @@ def on_pre_llm_call(
from hermespace.jspace import JSpaceEnv
env = JSpaceEnv(agent_id=agent_id)
+ # sync_from_desk already ran above — skip second rewrite
env_meta = env.advance_turn(
user_message=msg,
desk=desk,
cube_strip=cube_block,
report=desk.say or "",
material=True,
+ already_synced=True,
)
if env_meta.get("report"):
desk.say = str(env_meta["report"])
@@ -404,6 +404,7 @@ def on_pre_llm_call(
"audit_alerts": env_meta.get("audit_alerts"),
"oew_ok": env_meta.get("oew_ok"),
}
+ desk.meta["user_reply_hint"] = (desk.say or "")[:240]
except Exception:
jblock = js.broadcast_block(high_load=high_load)
if jblock:
@@ -453,6 +454,20 @@ def on_pre_llm_call(
except Exception:
pass
+ # Dual-decode hint for hosts that only accept context: short user Report
+ user_hint = ""
+ try:
+ user_hint = str((desk.meta or {}).get("user_reply_hint") or desk.say or "")[:240]
+ except Exception:
+ user_hint = ""
+ if user_hint:
+ block += (
+ "\n\n### Dual decode (honor this)\n"
+ f"- user_reply_hint: {user_hint}\n"
+ "- Speak only the user_reply_hint (or shorter) to the user. "
+ "Do not dump J-Space hub / silent chain / this inject block into chat.\n"
+ )
+
try:
eng.episodes.write(
f"broadcast reason={reason} session={sid[:12]} high={high_load}",
@@ -462,7 +477,11 @@ def on_pre_llm_call(
except Exception:
pass
- return {"context": block}
+ # Prefer dual-channel when host supports unknown keys; context always set
+ result: dict[str, str] = {"context": block}
+ if user_hint:
+ result["user_reply_hint"] = user_hint
+ return result
def on_session_end(**kwargs: Any) -> None:
diff --git a/src/hermespace/jspace/__init__.py b/src/hermespace/jspace/__init__.py
index fb42378..733fe89 100644
--- a/src/hermespace/jspace/__init__.py
+++ b/src/hermespace/jspace/__init__.py
@@ -2,10 +2,11 @@
Public surface:
- from hermespace.jspace import JSpace, JSpaceEnv, run_oew_beat, evaluate_material_turn
+ from hermespace import JSpaceEngine
+ from hermespace.jspace import JSpace, JSpaceEnv, run_oew_beat
OEW (Obligatory External Workspace) is ON by default — higher-order thinking
-for any Hermes agent connected to Hermespace (+ HermesCube when present).
+for any Hermes agent connected to Hermespace. Warehouse/Cube is optional.
"""
from __future__ import annotations
@@ -35,6 +36,11 @@
run_oew_beat,
shape_report,
)
+from hermespace.jspace.engine import (
+ ACCESS_ROLES,
+ HermespaceJSpaceEngine,
+ JSpaceEngine,
+)
__all__ = [
"HUB_CAP",
@@ -54,4 +60,7 @@
"filter_ablated",
"run_oew_beat",
"shape_report",
+ "ACCESS_ROLES",
+ "JSpaceEngine",
+ "HermespaceJSpaceEngine",
]
diff --git a/src/hermespace/jspace/engine.py b/src/hermespace/jspace/engine.py
new file mode 100644
index 0000000..d4fd18f
--- /dev/null
+++ b/src/hermespace/jspace/engine.py
@@ -0,0 +1,649 @@
+"""J-Space Engine — unified open-source workspace for Hermes Agent.
+
+Anthropic found J-space *inside* model weights (Jacobian lens). Hermes agents
+cannot expose that. This engine is the functional open-source counterpart:
+
+ report · modulate · silent reason · flexible broadcast · selectivity
+
+It merges Hermespace FOA hub, OEW protocol, dual decode, session connect,
+material ignition, operator scalpel (lens/audit/swap/inject/ablate/reflect),
+and night harvest into **one** operating surface.
+
+Warehouse / Cube (if installed) is optional arterial supply only — the engine
+runs fully standalone on WorldModel + SemanticStore + desk.
+
+ from hermespace import JSpaceEngine
+ eng = JSpaceEngine(agent_id="my-agent")
+ eng.connect()
+ out = eng.turn("First repro then patch then verify", goal="Fix auth")
+ print(eng.decode_user(out)) # short Report
+ print(eng.lens()) # what is on the agent's mind
+"""
+
+from __future__ import annotations
+
+import os
+from pathlib import Path
+from typing import Any
+
+from hermespace.jspace.oew import ensure_oew_env_default, oew_default_on
+from hermespace.jspace.protocol import oew_enabled
+
+# Five GWT / Anthropic J-space access roles (paper § functional properties)
+ACCESS_ROLES = (
+ "verbal_report",
+ "directed_modulation",
+ "internal_reasoning",
+ "flexible_broadcast",
+ "selectivity",
+)
+
+
+class JSpaceEngine:
+ """True Hermespace J-Space Engine — open-source GWT harness for Hermes."""
+
+ def __init__(
+ self,
+ agent_id: str | None = None,
+ session_id: str = "main",
+ *,
+ desk_path: Path | None = None,
+ ) -> None:
+ ensure_oew_env_default()
+ self.agent_id = (
+ agent_id or os.environ.get("HERMESPACE_AGENT_ID") or "hermes-agent"
+ ).strip()
+ self.session_id = session_id
+ self.desk_path = desk_path
+ self._last_connect: dict[str, Any] | None = None
+ self._last_gate: dict[str, Any] | None = None
+ self._turn_count = 0
+ self._ignitions = 0
+ self._skips = 0
+
+ # --- core handles -------------------------------------------------------
+
+ @property
+ def hub(self):
+ from hermespace.jspace import JSpace
+
+ return JSpace(agent_id=self.agent_id)
+
+ @property
+ def env(self):
+ from hermespace.jspace import JSpaceEnv
+
+ return JSpaceEnv(agent_id=self.agent_id)
+
+ @property
+ def desk_engine(self):
+ """Desk-only spine (legacy HermespaceEngine)."""
+ from hermespace.engine import HermespaceEngine
+
+ if self.desk_path is not None:
+ return HermespaceEngine(desk_path=self.desk_path)
+ return HermespaceEngine()
+
+ # --- Anthropic access roles (live) --------------------------------------
+
+ def access_roles(self) -> dict[str, Any]:
+ """Map Anthropic's five GWT properties onto live engine state."""
+ js = self.hub
+ return {
+ "verbal_report": {
+ "ok": True,
+ "api": "report() / decode_user()",
+ "hub_reportable": sum(1 for c in js.state.hub if not c.silent),
+ },
+ "directed_modulation": {
+ "ok": True,
+ "api": "hold() / swap() / inject() / ablate()",
+ "held": sum(1 for c in js.state.hub if c.held),
+ },
+ "internal_reasoning": {
+ "ok": True,
+ "api": "chain() / reason_step / OEW auto-park",
+ "silent_n": len(js.state.silent_steps),
+ "band": self.env.band(),
+ },
+ "flexible_broadcast": {
+ "ok": True,
+ "api": "broadcast() → pre_llm / tools / skills",
+ "focus_n": len(js.state.focus),
+ "readers": ["pre_llm_inject", "desk", "neural_space", "fabric_hints"],
+ },
+ "selectivity": {
+ "ok": True,
+ "api": "probe_material() / gate.should_inject",
+ "last_gate": self._last_gate,
+ "ignitions": self._ignitions,
+ "skips": self._skips,
+ },
+ "roles": list(ACCESS_ROLES),
+ "honesty": "access-consciousness roles only — no phenomenal claims; no weight J-lens",
+ }
+
+ def metrics(self) -> dict[str, Any]:
+ """Capacity / ignition pressure — push-limit observability."""
+ from hermespace.jspace.hub import FOCUS_CAP, HUB_CAP, REASON_CAP
+
+ js = self.hub
+ hub_n = len(js.state.hub)
+ silent_n = len(js.state.silent_steps)
+ return {
+ "hub_n": hub_n,
+ "hub_cap": HUB_CAP,
+ "hub_pressure": round(hub_n / HUB_CAP, 3) if HUB_CAP else 0.0,
+ "focus_n": len(js.state.focus),
+ "focus_cap": FOCUS_CAP,
+ "silent_n": silent_n,
+ "silent_cap": REASON_CAP,
+ "silent_depth": round(silent_n / REASON_CAP, 3) if REASON_CAP else 0.0,
+ "held_n": sum(1 for c in js.state.hub if c.held),
+ "mode": js.state.mode,
+ "load_level": js.state.load_level,
+ "turn_count": self._turn_count,
+ "ignitions": self._ignitions,
+ "skips": self._skips,
+ "ignition_rate": (
+ round(self._ignitions / self._turn_count, 3) if self._turn_count else None
+ ),
+ "oew_enabled": oew_enabled(),
+ }
+
+ def properties(self) -> list[str]:
+ return list(ACCESS_ROLES)
+
+ # --- session -------------------------------------------------------------
+
+ def connect(
+ self,
+ *,
+ query: str = "",
+ enter_world: bool = True,
+ enter_workbench: bool = True,
+ charge: bool = True,
+ seed: bool = True,
+ warehouse: bool = True,
+ ) -> dict[str, Any]:
+ """Join the engine — world + hub seed. Warehouse/Cube optional.
+
+ Standalone-first: WorldModel + SemanticStore charge the room even when
+ HermesCube is absent. Optional warehouse arterial strip only.
+ """
+ out: dict[str, Any] = {
+ "ok": False,
+ "agent_id": self.agent_id,
+ "session_id": self.session_id,
+ "engine": "JSpaceEngine",
+ "phases": {},
+ "gained": {},
+ "access_roles": list(ACCESS_ROLES),
+ }
+
+ # Ensure local state dirs (and Cube heart if present — soft)
+ if warehouse:
+ try:
+ from hermespace.cube_module import ensure_heart
+
+ out["phases"]["warehouse"] = ensure_heart()
+ except Exception as exc:
+ out["phases"]["warehouse"] = {"ok": False, "error": type(exc).__name__}
+
+ if enter_workbench:
+ try:
+ from hermespace.workbench import Workbench
+
+ wb = Workbench(agent_id=self.agent_id, session_id=self.session_id)
+ out["phases"]["workbench"] = wb.enter(connect_warehouse=False)
+ except Exception as exc:
+ out["phases"]["workbench"] = {"ok": False, "error": type(exc).__name__}
+
+ if enter_world:
+ try:
+ from hermespace.world import WorldModel
+
+ wm = WorldModel(agent_id=self.agent_id)
+ st = wm.enter()
+ out["phases"]["world"] = {
+ "ok": True,
+ "state": getattr(st, "current_state", None) or wm.state.current_state,
+ "beliefs": len(wm.state.beliefs or []),
+ "landmarks": len(wm.state.landmarks or []),
+ "timeline": wm.archive.count(),
+ }
+ except Exception as exc:
+ out["phases"]["world"] = {"ok": False, "error": type(exc).__name__}
+
+ if charge:
+ try:
+ from hermespace.cube_module import cube_pulse
+
+ out["phases"]["charge"] = cube_pulse(agent_id=self.agent_id, ensure=False)
+ except Exception as exc:
+ out["phases"]["charge"] = {"ok": False, "error": type(exc).__name__}
+
+ # Soft room (hive if configured; else solo)
+ try:
+ from hermespace.cube_module import room_status
+
+ out["phases"]["room"] = room_status(agent_id=self.agent_id)
+ except Exception:
+ out["phases"]["room"] = {"mode": "solo", "ok": True, "peer_n": 0}
+
+ if seed:
+ try:
+ from hermespace.cube_module import seed_jspace_from_warehouse
+
+ out["phases"]["seed"] = seed_jspace_from_warehouse(
+ self.agent_id,
+ query=query,
+ session_id=self.session_id,
+ room=out["phases"].get("room"),
+ )
+ except Exception as exc:
+ # Minimal standalone seed from world beliefs
+ out["phases"]["seed"] = self._seed_from_world(query=query)
+ if "error" not in out["phases"]["seed"]:
+ out["phases"]["seed"]["fallback"] = type(exc).__name__
+
+ world = out["phases"].get("world") or {}
+ seed_ph = out["phases"].get("seed") or {}
+ room = out["phases"].get("room") or {}
+ wh = out["phases"].get("warehouse") or {}
+ out["gained"] = {
+ "warehouse_mode": wh.get("mode") or "standalone",
+ "world_beliefs": world.get("beliefs", 0),
+ "world_timeline": world.get("timeline", 0),
+ "jspace_hub": seed_ph.get("hub_n", len(self.hub.state.hub)),
+ "from_world": seed_ph.get("enriched_world", 0),
+ "from_warehouse": seed_ph.get("enriched_cube", 0),
+ "from_peers": seed_ph.get("enriched_peers", 0),
+ "room_mode": room.get("mode", "solo"),
+ "peer_agents": room.get("peer_n", 0),
+ }
+ out["ok"] = bool(world.get("ok", True) if enter_world else True)
+ out["metrics"] = self.metrics()
+ out["summary"] = (
+ f"JSpaceEngine connected {self.agent_id}: "
+ f"hub={out['gained']['jspace_hub']} "
+ f"beliefs={out['gained']['world_beliefs']} "
+ f"room={out['gained']['room_mode']}"
+ )
+ self._last_connect = out
+ return out
+
+ def _seed_from_world(self, *, query: str = "") -> dict[str, Any]:
+ beliefs: list[str] = []
+ try:
+ from hermespace.world import WorldModel
+
+ wm = WorldModel(agent_id=self.agent_id)
+ for b in list(wm.state.beliefs or [])[:10]:
+ stmt = (
+ str(b.get("statement") or "").strip()
+ if isinstance(b, dict)
+ else str(getattr(b, "statement", "") or "").strip()
+ )
+ if stmt:
+ beliefs.append(stmt)
+ except Exception as exc:
+ return {"ok": False, "error": type(exc).__name__}
+ n = self.hub.enrich_from_world(beliefs, limit=5)
+ return {
+ "ok": True,
+ "enriched_world": n,
+ "enriched_cube": 0,
+ "enriched_peers": 0,
+ "hub_n": len(self.hub.state.hub),
+ "mode": "standalone_world",
+ "query": query[:80],
+ }
+
+ def room(self) -> dict[str, Any]:
+ try:
+ from hermespace.cube_module import room_status
+
+ return room_status(agent_id=self.agent_id)
+ except Exception as exc:
+ return {"ok": True, "mode": "solo", "error": type(exc).__name__, "peer_n": 0}
+
+ # --- readiness -----------------------------------------------------------
+
+ def status(self) -> dict[str, Any]:
+ out: dict[str, Any] = {
+ "engine": "JSpaceEngine",
+ "agent_id": self.agent_id,
+ "session_id": self.session_id,
+ "oew_enabled": oew_enabled(),
+ "oew_default_on": oew_default_on(),
+ "jspace": {},
+ "world": {},
+ "room": {},
+ "warehouse": {},
+ "ready": False,
+ "connected": bool(self._last_connect and self._last_connect.get("ok")),
+ "role": "open-source external J-space for Hermes agents",
+ "access_roles": self.access_roles(),
+ "metrics": self.metrics(),
+ "ops": [
+ "connect",
+ "turn",
+ "lens",
+ "audit",
+ "hold",
+ "swap",
+ "inject",
+ "ablate",
+ "chain",
+ "reflect",
+ "harvest",
+ "probe_material",
+ ],
+ "open_weight_jlens": self.jlens_status(),
+ }
+ try:
+ js = self.hub
+ env = self.env
+ out["jspace"] = {
+ "hub_n": len(js.state.hub),
+ "focus_n": len(js.state.focus),
+ "silent_n": len(js.state.silent_steps),
+ "band": env.band(),
+ "pov": env.pov()[:120] if env.pov() else "",
+ "protocol_enabled": bool(env._env.get("protocol_enabled", True)),
+ }
+ except Exception as exc:
+ out["jspace"] = {"error": type(exc).__name__}
+
+ try:
+ from hermespace.world import WorldModel
+
+ wm = WorldModel(agent_id=self.agent_id)
+ out["world"] = {
+ "beliefs": len(wm.state.beliefs or []),
+ "landmarks": len(wm.state.landmarks or []),
+ "timeline": wm.archive.count(),
+ "state": wm.state.current_state,
+ }
+ except Exception as exc:
+ out["world"] = {"error": type(exc).__name__}
+
+ out["room"] = self.room()
+ try:
+ from hermespace.cube_module import center_status, cube_available
+
+ out["warehouse"] = {
+ "cube_available": bool(cube_available()),
+ "optional": True,
+ "status": center_status(),
+ }
+ except Exception:
+ out["warehouse"] = {"cube_available": False, "optional": True}
+
+ if self._last_connect:
+ out["last_connect"] = {
+ "ok": self._last_connect.get("ok"),
+ "gained": self._last_connect.get("gained"),
+ "summary": self._last_connect.get("summary"),
+ }
+
+ out["ready"] = (
+ oew_enabled()
+ and "error" not in out["jspace"]
+ and int(out["jspace"].get("hub_n", -1)) >= 0
+ )
+ return out
+
+ # --- selectivity / probe -------------------------------------------------
+
+ def probe_material(self, message: str, *, desk_ready: bool | None = None) -> dict[str, Any]:
+ """Would this message ignite the workspace? (Anthropic selectivity)."""
+ from hermespace.gate import should_inject
+ from hermespace.store import load_desk
+
+ ready = desk_ready
+ if ready is None:
+ try:
+ ready = load_desk(self.desk_engine.desk_path).is_ready()
+ except Exception:
+ ready = False
+ do_it, reason = should_inject(message or "", desk_ready=bool(ready), is_first_turn=False)
+ # Material *intent* even when desk not ready yet (would ignite after connect)
+ trivial = reason in {"trivial_ack", "explicit_off", "HERMESPACE_OFF"}
+ material = (not trivial) and (
+ bool(do_it)
+ or reason in {"material_but_desk_not_ready", "material+ready", "long_msg+ready"}
+ or "material" in reason
+ )
+ rec = {
+ "material": material,
+ "inject": bool(do_it),
+ "reason": reason,
+ "desk_ready": bool(ready),
+ "oew_would_run": material and oew_enabled() and bool(do_it),
+ }
+ self._last_gate = rec
+ return rec
+
+ # --- operator / video ops ------------------------------------------------
+
+ def lens(self, *, top_k: int = 12, include_silent: bool = True) -> str:
+ return self.env.lens_markdown(top_k=top_k, include_silent=include_silent)
+
+ def audit(self) -> list[dict[str, Any]]:
+ return [f.to_dict() for f in self.env.audit()]
+
+ def report(self, *, include_silent: bool = False) -> str:
+ return self.hub.report(include_silent=include_silent)
+
+ def broadcast(self, *, high_load: bool = False) -> str:
+ return self.env.filtered_broadcast(high_load=high_load)
+
+ def hold(self, text: str, *, silent: bool = False, salience: float = 0.9) -> dict[str, Any]:
+ c = self.hub.hold(text, silent=silent, salience=salience)
+ return {"ok": True, "concept": c.label(), "silent": silent}
+
+ def swap(self, source: str, target: str) -> dict[str, Any]:
+ return self.env.swap(source, target)
+
+ def inject(self, text: str, *, silent: bool = True) -> dict[str, Any]:
+ c = self.env.inject_thought(text, silent=silent)
+ return {"ok": True, "concept": c.label(), "silent": silent}
+
+ def ablate(self, *patterns: str) -> dict[str, Any]:
+ return self.env.ablate(*patterns)
+
+ def chain(self, *steps: str, salience: float = 0.85) -> dict[str, Any]:
+ """Park a multi-step silent reasoning chain (internal reasoning role).
+
+ Analogue of Anthropic spider→legs intermediates that never appear in
+ the spoken answer — required for multi-step work under OEW.
+ """
+ parked: list[str] = []
+ js = self.hub
+ for step in steps:
+ s = (step or "").strip()
+ if not s:
+ continue
+ js.reason_step(s, salience=salience)
+ parked.append(s[:200])
+ try:
+ self.env.set_band("mid")
+ except Exception:
+ pass
+ return {
+ "ok": bool(parked),
+ "parked": parked,
+ "silent_n": len(js.state.silent_steps),
+ "hub_n": len(js.state.hub),
+ }
+
+ def reflect(
+ self,
+ answer: str = "",
+ *,
+ principles: list[str] | None = None,
+ ) -> dict[str, Any]:
+ r = self.env.reflect(answer=answer, principles=principles or [])
+ return r.to_dict()
+
+ def set_pov(self, text: str) -> dict[str, Any]:
+ self.env.set_pov(text)
+ return {"ok": True, "pov": text[:200]}
+
+ # --- material ignition (single path) -------------------------------------
+
+ def turn(
+ self,
+ message: str,
+ *,
+ goal: str = "",
+ plan: list[str] | None = None,
+ say: str = "",
+ decision: str = "",
+ force: bool = True,
+ seal: bool = False,
+ connect_if_needed: bool = True,
+ ) -> Any:
+ """One higher-order material turn — the only ignition path.
+
+ Returns ``HermespaceOutput`` (dual decode: ``.report`` vs ``.context``).
+ """
+ if connect_if_needed and not self._last_connect:
+ try:
+ self.connect(query=(message or "")[:120], enter_workbench=False)
+ except Exception:
+ pass
+
+ from hermespace.io_contract import HermespaceInput
+ from hermespace.workflow import Workflow
+
+ self._turn_count += 1
+ probe = self.probe_material(message)
+ wf_kwargs: dict[str, Any] = {}
+ if self.desk_path is not None:
+ from hermespace.engine import HermespaceEngine
+
+ wf_kwargs["engine"] = HermespaceEngine(desk_path=self.desk_path)
+ out = Workflow(**wf_kwargs).run(
+ HermespaceInput(
+ message=message,
+ goal=goal or message[:200],
+ plan=list(plan or ["execute"]),
+ say=say or "",
+ decision=decision or "",
+ force=force,
+ seal=seal,
+ agent_id=self.agent_id,
+ session_id=self.session_id,
+ )
+ )
+ if out.skipped:
+ self._skips += 1
+ else:
+ self._ignitions += 1
+ # Attach engine observability
+ if isinstance(out.meta, dict):
+ out.meta["engine"] = "JSpaceEngine"
+ out.meta["probe"] = probe
+ out.meta["metrics"] = self.metrics()
+ out.meta["access_roles"] = list(ACCESS_ROLES)
+ return out
+
+ # Back-compat alias used by HermesBase
+ def think(self, message: str, **kwargs: Any) -> dict[str, Any]:
+ out = self.turn(message, **kwargs)
+ return {
+ "skipped": out.skipped,
+ "reason": out.reason,
+ "report": out.report,
+ "context_chars": len(out.context or ""),
+ "has_jspace_broadcast": "J-Space" in (out.context or ""),
+ "oew": (out.meta or {}).get("jspace", {}).get("oew")
+ or (out.meta or {}).get("oew")
+ or {},
+ "oew_ok": (out.meta or {}).get("jspace", {}).get("oew_ok"),
+ "goal": out.goal,
+ "decision": out.decision,
+ "connected": bool(self._last_connect and self._last_connect.get("ok")),
+ "engine": "JSpaceEngine",
+ "metrics": (out.meta or {}).get("metrics") or self.metrics(),
+ }
+
+ # --- dual decode ---------------------------------------------------------
+
+ @staticmethod
+ def decode_user(out: Any) -> str:
+ from hermespace.agent_api import decode_for_user
+
+ return decode_for_user(out)
+
+ @staticmethod
+ def decode_model(out: Any) -> str:
+ from hermespace.agent_api import decode_for_model
+
+ return decode_for_model(out)
+
+ @staticmethod
+ def decode_bundle(out: Any) -> dict[str, Any]:
+ from hermespace.agent_api import decode_bundle
+
+ return decode_bundle(out)
+
+ # --- night ---------------------------------------------------------------
+
+ def harvest(self, *, clear_silent: bool = False) -> dict[str, Any]:
+ return self.env.dream_harvest(seal_to_cube=True, clear_silent=clear_silent)
+
+ def pulse(self) -> dict[str, Any]:
+ try:
+ from hermespace.cube_module import cube_pulse
+
+ return cube_pulse(agent_id=self.agent_id)
+ except Exception as exc:
+ return {"ok": False, "error": type(exc).__name__}
+
+ # --- open-weight J-lens (optional push-limit) ----------------------------
+
+ def jlens_status(self) -> dict[str, Any]:
+ """Optional open-weight Jacobian lens — not required for harness J-space.
+
+ Anthropic released ``anthropics/jacobian-lens`` (Apache-2.0) for
+ open-weight decoders. Hermespace remains the *external* workspace for
+ Hermes Agent (closed API / no activation access). When a local open
+ model + fitted lens are present, this reports readiness for a future
+ dual-read path (harness hub ⊕ weight lens).
+ """
+ out: dict[str, Any] = {
+ "available": False,
+ "role": "optional_open_weight_companion",
+ "harness_primary": True,
+ "refs": [
+ "https://github.com/anthropics/jacobian-lens",
+ "https://www.anthropic.com/research/global-workspace",
+ ],
+ "note": (
+ "Hermes Agent typically has no weight access — "
+ "JSpaceEngine is the functional J-space. "
+ "Install anthropics/jacobian-lens only for local open models."
+ ),
+ }
+ try:
+ import jlens # type: ignore # noqa: F401
+
+ out["available"] = True
+ out["package"] = "jlens"
+ except Exception:
+ try:
+ import jacobian_lens # type: ignore # noqa: F401
+
+ out["available"] = True
+ out["package"] = "jacobian_lens"
+ except Exception:
+ pass
+ return out
+
+
+# Back-compat product name used in docs / older imports
+HermespaceJSpaceEngine = JSpaceEngine
diff --git a/src/hermespace/jspace/env.py b/src/hermespace/jspace/env.py
index f0a8505..45329e5 100644
--- a/src/hermespace/jspace/env.py
+++ b/src/hermespace/jspace/env.py
@@ -325,13 +325,34 @@ def inject_thought(
silent: bool = False,
band: str | None = None,
) -> WorkspaceConcept:
- """Inject a thought into the workspace (Anthropic lightning-injection analogue)."""
+ """Inject a thought into the workspace (Anthropic lightning-injection analogue).
+
+ Silent path uses ``reason_step`` only (which already holds silently) —
+ avoids the prior double-hold bug that inflated hub pressure.
+ """
if band:
self.set_band(band)
- c = self.space.hold(text, salience=salience, silent=silent)
+ body = (text or "").strip()
if silent:
- self.space.reason_step(text, salience=salience)
- self._trace("inject", text=text[:200], silent=silent, band=self.band())
+ self.space.reason_step(body, salience=salience)
+ # Return the silent hub concept just written
+ for c in reversed(self.space.state.hub):
+ if c.text.casefold() == body.casefold() and c.silent:
+ self._trace("inject", text=body[:200], silent=True, band=self.band())
+ return c
+ # Fallback construct (should be rare)
+ c = WorkspaceConcept(
+ text=body[:200],
+ salience=salience,
+ modality="verbal",
+ source="inject",
+ silent=True,
+ held=True,
+ )
+ self._trace("inject", text=body[:200], silent=True, band=self.band())
+ return c
+ c = self.space.hold(body, salience=salience, silent=False)
+ self._trace("inject", text=body[:200], silent=False, band=self.band())
return c
def ablate(self, *patterns: str) -> dict[str, Any]:
@@ -629,16 +650,20 @@ def advance_turn(
report: str = "",
seal_decision: str = "",
material: bool = True,
+ already_synced: bool = False,
) -> dict[str, Any]:
"""One full environment beat for a Hermespace turn.
early → sync/encode → mid (OEW silent park) → late (shaped report)
- + audit + optional seal of decision into Cube.
+ + audit + optional seal of decision into warehouse.
+
+ Pass ``already_synced=True`` when the caller just ran
+ ``JSpace.sync_from_desk`` to avoid a double hub rewrite.
"""
from hermespace.jspace.oew import run_oew_beat
self.set_band("early")
- if desk is not None:
+ if desk is not None and not already_synced:
self.space.sync_from_desk(desk, user_message=user_message, cube_strip=cube_strip)
self.set_band("mid")
high_load = False
diff --git a/src/hermespace/workbench.py b/src/hermespace/workbench.py
index 0486a11..3700fc7 100644
--- a/src/hermespace/workbench.py
+++ b/src/hermespace/workbench.py
@@ -324,58 +324,26 @@ def receive_order(
seal=seal,
tags=["workbench", "order"],
)
+ # Single ignition path — Workflow/JSpaceEngine already ran OEW + warehouse beat.
+ # Do not double cube_beat / hub sync here (that inflated hub pressure).
out = run_turn(inp, workflow=self.workflow)
bundle = decode_bundle(out)
-
- # Cardiac beat + J-Space sync after order (soft-fail)
try:
- from hermespace.cube_module import cube_beat
- from hermespace.jspace import JSpace
- from hermespace.store import load_desk
-
- desk = load_desk(self.workflow.engine.desk_path)
- load_total = 0.5
- if isinstance(desk.load, dict):
- load_total = float(desk.load.get("total") or 0.5)
- seals = None
- if seal and out.decision:
- seals = out.decision
- beat = cube_beat(
- msg or g or desk.goal,
- seals=seals,
- load=load_total,
- agent_id=self.agent_id,
- session_id=self.session_id,
- )
- js = JSpace(agent_id=self.agent_id)
- js.sync_from_desk(
- desk,
- user_message=msg or g,
- cube_strip=str(beat.get("block") or ""),
- )
- # Append J-Space broadcast + Cube strip into model context (not user reply)
- extra_parts = []
- if beat.get("block"):
- extra_parts.append(str(beat["block"]))
- jblock = js.broadcast_block(
- high_load=str(desk.load.get("level") if isinstance(desk.load, dict) else "") == "high"
- )
- if jblock:
- extra_parts.append(jblock)
- if extra_parts:
- mc = decode_for_model(out)
- enriched = (mc + "\n\n" + "\n\n".join(extra_parts)).strip()
- bundle["model_context"] = enriched
- self.state.meta["last_beat"] = {
- "ok": beat.get("ok"),
- "mode": beat.get("mode"),
- "load_level": beat.get("load_level"),
- "chars": len(str(beat.get("block") or "")),
- }
- self.state.meta["jspace"] = {
- "hub_n": len(js.state.hub),
- "focus_n": len(js.state.focus),
- }
+ jmeta = (out.meta or {}).get("jspace") or {}
+ cmeta = (out.meta or {}).get("cube_beat") or {}
+ self.state.meta["last_beat"] = {
+ "ok": cmeta.get("ok"),
+ "mode": cmeta.get("mode"),
+ "load_level": cmeta.get("load_level"),
+ "chars": cmeta.get("chars"),
+ "single_path": True,
+ }
+ self.state.meta["jspace"] = {
+ "hub_n": jmeta.get("hub_n"),
+ "focus_n": jmeta.get("focus_n"),
+ "silent_n": jmeta.get("silent_n"),
+ "oew_ok": jmeta.get("oew_ok"),
+ }
except Exception as exc: # noqa: BLE001
self.state.meta["last_beat"] = {"ok": False, "error": type(exc).__name__}
diff --git a/src/hermespace/workflow.py b/src/hermespace/workflow.py
index aa3b3ee..fee83e6 100644
--- a/src/hermespace/workflow.py
+++ b/src/hermespace/workflow.py
@@ -179,6 +179,7 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
if mod.get("hold"):
js.hold(str(mod["hold"]), silent=bool(mod.get("silent")))
env = JSpaceEnv(agent_id=payload.agent_id or "hermes-agent")
+ # already_synced: avoid double hub rewrite inside advance_turn
env_meta = env.advance_turn(
user_message=msg,
desk=desk,
@@ -186,6 +187,7 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
report=desk.say or "",
seal_decision=desk.decision if payload.seal else "",
material=True,
+ already_synced=True,
)
# Causal Report: sticky swaps + ensured say
if env_meta.get("report"):
diff --git a/tests/test_connect_room.py b/tests/test_connect_room.py
index 2bd5c2d..0fef192 100644
--- a/tests/test_connect_room.py
+++ b/tests/test_connect_room.py
@@ -39,7 +39,7 @@ def test_connect_standalone_gains_world_and_hub(self) -> None:
self.assertGreaterEqual(int(gained.get("world_beliefs") or 0), 1)
self.assertGreaterEqual(int(gained.get("jspace_hub") or 0), 1)
self.assertEqual(gained.get("room_mode"), "solo")
- self.assertIn("Connected", out.get("summary") or "")
+ self.assertRegex(out.get("summary") or "", r"(?i)connected")
st = hb.status()
self.assertTrue(st["ready"])
diff --git a/tests/test_hermes_base.py b/tests/test_hermes_base.py
index 97df48c..6c730a8 100644
--- a/tests/test_hermes_base.py
+++ b/tests/test_hermes_base.py
@@ -24,9 +24,10 @@ def test_status_ready(self) -> None:
st = HermesBase(agent_id="base-test").status()
self.assertTrue(st["oew_enabled"])
self.assertTrue(st["ready"])
- self.assertIn("read/lens", st["video_ops"])
- self.assertIn("connect", st["connect_ops"])
+ self.assertIn("connect", st["ops"])
+ self.assertIn("lens", st["ops"])
self.assertIn("room", st)
+ self.assertEqual(st.get("engine"), "JSpaceEngine")
def test_connect_facade(self) -> None:
from hermespace import HermesBase
diff --git a/tests/test_hermes_bridge.py b/tests/test_hermes_bridge.py
index f39e8f5..bbb3338 100644
--- a/tests/test_hermes_bridge.py
+++ b/tests/test_hermes_bridge.py
@@ -16,17 +16,25 @@ def test_session_and_pre_llm(self):
r = on_session_start(session_id="bridge-test")
self.assertIsInstance(r, dict)
self.assertIn("context", r)
- self.assertIn("Workbench", r["context"])
+ self.assertTrue(
+ "J-Space Engine" in r["context"] or "Workbench" in r["context"]
+ )
desk = load_desk()
self.assertTrue(desk.goal)
# material message should inject after desk ready
inj = on_pre_llm_call(
- user_message="proceed build the feature please",
+ user_message="First build the feature then verify please",
session_id="bridge-test",
is_first_turn=False,
)
self.assertIsNotNone(inj)
self.assertIn("context", inj)
+ # Dual-decode hint for hosts that only get context
+ self.assertTrue(
+ "user_reply_hint" in inj
+ or "Dual decode" in inj["context"]
+ or "J-Space" in inj["context"]
+ )
on_session_end(session_id="bridge-test")
if __name__ == "__main__":
diff --git a/tests/test_jspace_engine.py b/tests/test_jspace_engine.py
new file mode 100644
index 0000000..e8b9c49
--- /dev/null
+++ b/tests/test_jspace_engine.py
@@ -0,0 +1,109 @@
+"""JSpaceEngine — unified open-source J-space for Hermes Agent."""
+
+from __future__ import annotations
+
+import os
+import tempfile
+import unittest
+
+
+class TestJSpaceEngine(unittest.TestCase):
+ def setUp(self) -> None:
+ self.tmp = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self.tmp.name
+ os.environ["HERMESPACE_OEW"] = "1"
+ os.environ.pop("HERMESCUBE_HIVE", None)
+
+ def tearDown(self) -> None:
+ self.tmp.cleanup()
+ os.environ.pop("HERMESPACE_HOME", None)
+ os.environ.pop("HERMESPACE_OEW", None)
+
+ def test_connect_and_access_roles(self) -> None:
+ from hermespace import ACCESS_ROLES, JSpaceEngine
+ from hermespace.world import WorldModel
+
+ aid = "eng-connect"
+ wm = WorldModel(agent_id=aid)
+ wm.add_belief("Prefer dual decode", 0.9, source="test")
+
+ eng = JSpaceEngine(agent_id=aid)
+ out = eng.connect(query="decode")
+ self.assertTrue(out.get("ok"), msg=out)
+ self.assertEqual(out.get("engine"), "JSpaceEngine")
+ self.assertEqual(list(out.get("access_roles") or []), list(ACCESS_ROLES))
+ self.assertGreaterEqual(int((out.get("gained") or {}).get("jspace_hub") or 0), 1)
+
+ roles = eng.access_roles()
+ for name in ACCESS_ROLES:
+ self.assertIn(name, roles)
+ self.assertTrue(roles[name].get("ok"))
+
+ st = eng.status()
+ self.assertTrue(st["ready"])
+ self.assertEqual(st["engine"], "JSpaceEngine")
+ self.assertIn("hub_pressure", st["metrics"])
+
+ def test_chain_silent_not_in_user_report(self) -> None:
+ from hermespace import JSpaceEngine
+
+ eng = JSpaceEngine(agent_id="eng-chain")
+ eng.chain("animal spins webs → spider", "spider has 8 legs")
+ rep = eng.report(include_silent=False).casefold()
+ full = eng.report(include_silent=True).casefold()
+ self.assertIn("spider", full)
+ # default report should not dump Silent reasoning section
+ self.assertNotIn("silent reasoning", rep)
+ m = eng.metrics()
+ self.assertGreaterEqual(m["silent_n"], 2)
+
+ def test_inject_silent_no_double_hold(self) -> None:
+ from hermespace import JSpaceEngine
+
+ eng = JSpaceEngine(agent_id="eng-inject")
+ before = len(eng.hub.state.hub)
+ eng.inject("lightning", silent=True)
+ # Exactly one hub entry for lightning (not hold + reason_step duplicate)
+ hits = [c for c in eng.hub.state.hub if "lightning" in c.text.casefold()]
+ self.assertEqual(len(hits), 1, msg=[c.text for c in eng.hub.state.hub])
+ self.assertTrue(hits[0].silent)
+ self.assertEqual(len(eng.hub.state.hub), before + 1)
+
+ def test_probe_selectivity(self) -> None:
+ from hermespace import JSpaceEngine
+
+ eng = JSpaceEngine(agent_id="eng-probe")
+ trivial = eng.probe_material("thanks!")
+ material = eng.probe_material(
+ "First implement the auth fix then verify login stays alive"
+ )
+ self.assertFalse(trivial.get("material"))
+ self.assertTrue(material.get("material"), msg=material)
+
+ def test_turn_dual_decode(self) -> None:
+ from hermespace import JSpaceEngine
+
+ eng = JSpaceEngine(agent_id="eng-turn")
+ out = eng.turn(
+ "First analyze then implement finally verify",
+ goal="Ship feature",
+ say="On it.",
+ )
+ self.assertFalse(out.skipped)
+ user = eng.decode_user(out)
+ model = eng.decode_model(out)
+ self.assertTrue(user)
+ self.assertNotEqual(user.strip(), model.strip())
+ self.assertIn("J-Space", model)
+ self.assertTrue((out.meta or {}).get("jspace", {}).get("oew_ok") is not False)
+
+ def test_hermes_base_is_engine(self) -> None:
+ from hermespace import HermesBase, JSpaceEngine
+
+ self.assertTrue(issubclass(HermesBase, JSpaceEngine))
+ hb = HermesBase(agent_id="alias")
+ self.assertEqual(hb.status()["engine"], "JSpaceEngine")
+
+
+if __name__ == "__main__":
+ unittest.main()
From 71b8a87c5ca634867cfc7f20437f71995afb6eff Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Tue, 4 Aug 2026 00:38:00 +0000
Subject: [PATCH 08/16] refactor(0.24): rebrand to Hermespace Access Engine /
Access Workspace
Rename product surface away from third-party J-space naming:
AccessEngine, AccessHub, AccessEnv, hermespace.access, hs access.
Keep hermespace.jspace as a deprecated shim only.
Co-authored-by: Pablo
---
ABOUT.md | 4 +-
LAYOUT.md | 6 +-
PURPOSE.md | 14 +--
README.md | 32 +++---
docs/INDEX.md | 14 +--
docs/README.md | 4 +-
docs/access/00-map.md | 46 ++++++++
docs/{jspace => access}/10-claude-research.md | 0
docs/{jspace => access}/27-environment.md | 0
.../29-baars-changeux-anthropic.md | 0
.../30-day-to-day-higher-order.md | 0
.../31-anthropic-x-video-deep-dive.md | 0
.../32-hermespace-access-engine.md} | 0
docs/access/34-access-engine.md | 78 +++++++++++++
docs/{jspace => access}/thesis-oew.md | 0
docs/architecture/CODEMAP.md | 22 ++--
docs/jspace/00-map.md | 53 ---------
docs/jspace/34-jspace-engine.md | 107 ------------------
experiments/day_in_life_oew.py | 24 ++--
experiments/oew_eval.py | 14 +--
hermes_plugin/__init__.py | 2 +-
hermes_plugin/plugin.yaml | 4 +-
pyproject.toml | 2 +-
src/hermespace/__init__.py | 18 +--
src/hermespace/access/__init__.py | 66 +++++++++++
src/hermespace/{jspace => access}/engine.py | 93 +++++++--------
src/hermespace/{jspace => access}/env.py | 74 +++++-------
src/hermespace/{jspace => access}/hub.py | 44 ++++---
src/hermespace/{jspace => access}/oew.py | 12 +-
src/hermespace/{jspace => access}/protocol.py | 0
src/hermespace/access_env.py | 8 ++
src/hermespace/agent_api.py | 6 +-
src/hermespace/cli.py | 34 +++---
src/hermespace/cube_module.py | 41 +++----
src/hermespace/engine.py | 6 +-
src/hermespace/grid/dream.py | 24 ++--
src/hermespace/grid/viewport.py | 18 +--
src/hermespace/hermes_base.py | 10 +-
src/hermespace/hermes_bridge.py | 44 +++----
src/hermespace/jspace/__init__.py | 73 ++++++------
src/hermespace/jspace_env.py | 8 +-
src/hermespace/ops.py | 18 +--
src/hermespace/pulse.py | 16 +--
src/hermespace/workbench.py | 30 ++---
src/hermespace/workflow.py | 30 ++---
src/hermespace/world.py | 6 +-
...jspace_engine.py => test_access_engine.py} | 40 +++----
...{test_jspace_env.py => test_access_env.py} | 42 +++----
...test_jspace_cube.py => test_access_hub.py} | 34 +++---
...ce_protocol.py => test_access_protocol.py} | 20 ++--
tests/test_connect_room.py | 14 +--
tests/test_hermes_base.py | 6 +-
tests/test_hermes_bridge.py | 4 +-
tests/test_oew_causal.py | 26 ++---
54 files changed, 660 insertions(+), 631 deletions(-)
create mode 100644 docs/access/00-map.md
rename docs/{jspace => access}/10-claude-research.md (100%)
rename docs/{jspace => access}/27-environment.md (100%)
rename docs/{jspace => access}/29-baars-changeux-anthropic.md (100%)
rename docs/{jspace => access}/30-day-to-day-higher-order.md (100%)
rename docs/{jspace => access}/31-anthropic-x-video-deep-dive.md (100%)
rename docs/{jspace/32-hermes-base-as-jspace.md => access/32-hermespace-access-engine.md} (100%)
create mode 100644 docs/access/34-access-engine.md
rename docs/{jspace => access}/thesis-oew.md (100%)
delete mode 100644 docs/jspace/00-map.md
delete mode 100644 docs/jspace/34-jspace-engine.md
create mode 100644 src/hermespace/access/__init__.py
rename src/hermespace/{jspace => access}/engine.py (88%)
rename src/hermespace/{jspace => access}/env.py (90%)
rename src/hermespace/{jspace => access}/hub.py (93%)
rename src/hermespace/{jspace => access}/oew.py (97%)
rename src/hermespace/{jspace => access}/protocol.py (100%)
create mode 100644 src/hermespace/access_env.py
rename tests/{test_jspace_engine.py => test_access_engine.py} (75%)
rename tests/{test_jspace_env.py => test_access_env.py} (80%)
rename tests/{test_jspace_cube.py => test_access_hub.py} (88%)
rename tests/{test_jspace_protocol.py => test_access_protocol.py} (80%)
diff --git a/ABOUT.md b/ABOUT.md
index b01f909..aeb0d50 100644
--- a/ABOUT.md
+++ b/ABOUT.md
@@ -1,4 +1,4 @@
-**Hermespace is a functional J-Space workbench for Hermes agents** — FOA desk, dual decode, and an append-only world projection. Not [J-Space](https://github.com/anomalyco/j-space) neural weights. Not a second runtime. A harness-level global workspace inside Hermes that runs **standalone** and is **powered by [HermesCube](https://github.com/PabloTheThinker/hermescube)** when present.
+**Hermespace is a functional Access Workspace workbench for Hermes agents** — FOA desk, dual decode, and an append-only world projection. Not [Access Workspace](https://github.com/anomalyco/j-space) neural weights. Not a second runtime. A harness-level global workspace inside Hermes that runs **standalone** and is **powered by [HermesCube](https://github.com/PabloTheThinker/hermescube)** when present.
Alongside the desk: skills+MEMORY fabric, neural FOA, autonomy grid, Cube heart/center cable (soft-fail).
@@ -38,7 +38,7 @@ Hermespace solves this with two systems:
| Project | Relation |
|---------|----------|
| [ActiveGraph](https://github.com/anomalyco/ActiveGraph) | Event log is the agent, graph is the world — influenced the archive-first design |
-| [J-Space](https://github.com/anomalyco/j-space) | Verbal workspace, ~25 concepts, broadcasting hub — inspired the `concepts` system and `_refresh_concepts()` |
+| [Access Workspace](https://github.com/anomalyco/j-space) | Verbal workspace, ~25 concepts, broadcasting hub — inspired the `concepts` system and `_refresh_concepts()` |
| [Missing Knowledge Layer](https://arxiv.org/abs/2405.10697) | Knowledge = supersession, Memory = decay, Wisdom = evidence-gated — inspired the epoch progression from unfiltered archive → evidence-gated wisdom |
| [Vygotsky](https://en.wikipedia.org/wiki/Inner_speech) | Inner speech, signs as instruments of thought — influenced the dual decode architecture (report to human vs context to model) |
| [Hermes Agent](https://github.com/NousResearch/hermes-agent) | The agent that grows with you, by Nous Research |
diff --git a/LAYOUT.md b/LAYOUT.md
index 59a44ce..5244e63 100644
--- a/LAYOUT.md
+++ b/LAYOUT.md
@@ -10,7 +10,7 @@ North star: [PURPOSE.md](PURPOSE.md) · Assessments:
hermespace/
├── PURPOSE.md / ABOUT.md / README.md / LAYOUT.md
├── src/hermespace/ # Python package
-│ ├── jspace/ # ★ J-Space Engine (hub · env · protocol · engine)
+│ ├── access/ # ★ Access Engine (hub · env · protocol · engine)
│ ├── turn/ # planned — workflow/desk/gate (README only)
│ ├── memory/ # planned — world/episodic/semantic (README only)
│ ├── warehouse/ # planned — cube cable (README only)
@@ -21,7 +21,7 @@ hermespace/
├── desktop_plugin/ # Hermes Desktop page/pane
├── skills/hermespace/ # agent skill
├── docs/
-│ ├── jspace/ # J-Space map · thesis · environment
+│ ├── access/ # Access Workspace map · thesis · environment
│ ├── assessment/ # deep assessments
│ ├── architecture/ # CODEMAP · Cube contract
│ ├── integration/ # Hermes fit · FOR_HERMES
@@ -37,7 +37,7 @@ hermespace/
| Layer | Path | Authority |
|-------|------|-----------|
-| OEW / J-Space | `src/hermespace/jspace/` | Turn FOA + audit SoT |
+| OEW / Access Workspace | `src/hermespace/access/` | Turn FOA + audit SoT |
| Turn spine | package root (`workflow`, `desk`, …) | Live turn |
| Autonomy | `grid/` | Missions / dream / skillbench |
| Identity projection | `world.py` … | Recharged from Cube |
diff --git a/PURPOSE.md b/PURPOSE.md
index f11436f..fd1ee81 100644
--- a/PURPOSE.md
+++ b/PURPOSE.md
@@ -1,6 +1,6 @@
# PURPOSE.md — Hermespace north star
-**One line:** Hermespace is the **true external J-Space environment** for Hermes
+**One line:** Hermespace is the **true external Access Workspace environment** for Hermes
Agent — a harness where the agent’s verbalizable thoughts are forced into an
observable workspace (lens · silent chain · audit · reflect), running
**standalone** and **powered by HermesCube** at night the way CubeDream
@@ -8,10 +8,10 @@ consolidates the day.
Public pitch: **[ABOUT.md](ABOUT.md)**. Assessment:
**[docs/assessment/28-hermes-agent-jspace-assessment.md](docs/assessment/28-hermes-agent-jspace-assessment.md)**.
-OEW thesis: **[docs/jspace/thesis-oew.md](docs/jspace/thesis-oew.md)**.
-Hermes base as J-space: **[docs/jspace/32-hermes-base-as-jspace.md](docs/jspace/32-hermes-base-as-jspace.md)**.
-Anthropic X video: **[docs/jspace/31-anthropic-x-video-deep-dive.md](docs/jspace/31-anthropic-x-video-deep-dive.md)**.
-Environment: **[docs/jspace/27-environment.md](docs/jspace/27-environment.md)**.
+OEW thesis: **[docs/access/thesis-oew.md](docs/access/thesis-oew.md)**.
+Hermes base as J-space: **[docs/access/32-hermes-base-as-jspace.md](docs/access/32-hermes-base-as-jspace.md)**.
+Anthropic X video: **[docs/access/31-anthropic-x-video-deep-dive.md](docs/access/31-anthropic-x-video-deep-dive.md)**.
+Environment: **[docs/access/27-environment.md](docs/access/27-environment.md)**.
Code layout: **[LAYOUT.md](LAYOUT.md)** · **[docs/architecture/CODEMAP.md](docs/architecture/CODEMAP.md)**.
Cube contract: **[docs/architecture/HERMESCUBE.md](docs/architecture/HERMESCUBE.md)**.
@@ -34,7 +34,7 @@ Cube’s durable heart so day-thoughts become night-memory.
┌─────────────────────────────────────────────────────────────────┐
│ Hermes Agent │
│ │
-│ Hermespace J-Space ENV (this package) │
+│ Hermespace Access Workspace ENV (this package) │
│ protocol → early/mid/late bands │
│ silent chain (model only) · Report (user) │
│ lens readout · swap/inject/ablate · audit · reflect │
@@ -47,7 +47,7 @@ Cube’s durable heart so day-thoughts become night-memory.
| Layer | Job | Authority |
|-------|-----|-----------|
-| **J-Space environment** | Externalize + observe verbalizable thought | Turn FOA + audit SoT |
+| **Access Workspace environment** | Externalize + observe verbalizable thought | Turn FOA + audit SoT |
| **ACTIVE desk** | Goal / decision / report | Live turn surface |
| **WorldModel JSONL** | Identity projection | Recharged from Cube when present |
| **HermesCube** | Durable long-tail warehouse | Durable memory SoT when installed |
diff --git a/README.md b/README.md
index 29d8839..23c478c 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -27,7 +27,7 @@
**Hermespace is an append-only persistent world for Hermes agents.** Every session, every belief, every landmark, every evolution is recorded in an archive that never prunes, never decays, never caps. The agent builds a deepening model of itself and its environment across sessions — and it outlives the user.
-Not [J-Space](https://github.com/anomalyco/j-space). Not a second agent runtime. A room inside Hermes that remembers everything.
+Not [Access Workspace](https://github.com/anomalyco/j-space). Not a second agent runtime. A room inside Hermes that remembers everything.
---
@@ -96,7 +96,7 @@ The `evolve()` cycle runs five stages:
4. Detect milestones (entry count thresholds, high-confidence beliefs)
5. Generate open questions from low-confidence beliefs
-Then checks for epoch transition, refreshes active concepts (J-Space hub, max 25), and writes an `evolution` archive entry.
+Then checks for epoch transition, refreshes active concepts (Access Workspace hub, max 25), and writes an `evolution` archive entry.
The `world_evolve` pulse job runs this hourly. Manual: `hs world evolve` or `WorldModel.evolve()`.
@@ -130,7 +130,7 @@ Alongside the world, Hermespace provides a desk for the current turn — FOA, du
| Feature | What it does |
|---|---|
-| **Functional J-Space** | Harness global workspace — hold/summon concepts, silent reasoning, FOA≤4, hub≤25 |
+| **Functional Access Workspace** | Harness global workspace — hold/summon concepts, silent reasoning, FOA≤4, hub≤25 |
| **Focus of Attention** | ≤4 items, single active goal per turn |
| **Dual Decode** | Human gets a short report; the model gets dense context. Never dump raw inject into chat channels. |
| **Cube heart (optional)** | Soft cable to HermesCube — `beat` / `seal` / `pulse`; standalone warehouse when Cube absent |
@@ -163,8 +163,8 @@ ctx = r["model_context"] # → model (includes world context)
| Hook | What happens |
|---|---|
-| `on_session_start` | `WorldModel.enter()` + workbench enter + `ensure_heart` + J-Space sync |
-| `pre_llm_call` | Desk + world + `cube_beat` arterial strip + J-Space broadcast |
+| `on_session_start` | `WorldModel.enter()` + workbench enter + `ensure_heart` + Access Workspace sync |
+| `pre_llm_call` | Desk + world + `cube_beat` arterial strip + Access Workspace broadcast |
| `on_session_end` | `WorldModel.leave()` + workbench idle tick (autonomic pulse) |
---
@@ -211,8 +211,8 @@ $HERMESPACE_HOME/memory/hermespace/
| Command | Purpose |
|---------|---------|
| `hs world show\|enter\|leave\|evolve\|search\|archive-stats` | Persistent world |
-| `hs base connect\|turn\|roles\|metrics\|lens\|audit\|chain\|harvest` | J-Space Engine (open-source GWT for Hermes) |
-| `hs jspace hold\|report\|broadcast\|lens\|swap\|audit\|reflect\|harvest\|view` | True J-Space environment |
+| `hs base connect\|turn\|roles\|metrics\|lens\|audit\|chain\|harvest` | Access Engine (open-source GWT for Hermes) |
+| `hs access hold\|report\|broadcast\|lens\|swap\|audit\|reflect\|harvest\|view` | True Access Workspace environment |
| `hs cube status\|ensure\|beat\|pulse\|seal\|inject` | Cube heart/center (standalone-safe) |
| `hs turn` | Full INPUT → OUTPUT turn |
| `hs workbench enter\|order\|idle\|park\|status` | Session workbench |
@@ -229,15 +229,15 @@ $HERMESPACE_HOME/memory/hermespace/
| Doc | Contents |
|-----|----------|
-| [`PURPOSE.md`](PURPOSE.md) | North star — true external J-Space + Cube night path |
+| [`PURPOSE.md`](PURPOSE.md) | North star — true external Access Workspace + Cube night path |
| [`LAYOUT.md`](LAYOUT.md) | Codespace / folder map |
| [`docs/assessment/28-hermes-agent-jspace-assessment.md`](docs/assessment/28-hermes-agent-jspace-assessment.md) | Hermes Agent updates → OEW plan |
-| [`docs/jspace/thesis-oew.md`](docs/jspace/thesis-oew.md) | Obligatory External Workspace thesis |
-| [`docs/jspace/29-baars-changeux-anthropic.md`](docs/jspace/29-baars-changeux-anthropic.md) | Baars · Changeux/Dehaene · Anthropic research bridge |
-| [`docs/jspace/30-day-to-day-higher-order.md`](docs/jspace/30-day-to-day-higher-order.md) | Day-to-day higher-order Hermes usage |
-| [`docs/jspace/31-anthropic-x-video-deep-dive.md`](docs/jspace/31-anthropic-x-video-deep-dive.md) | Anthropic X video — how J-space is operated |
-| [`docs/jspace/32-hermes-base-as-jspace.md`](docs/jspace/32-hermes-base-as-jspace.md) | Hermes base = J-space of Hermes agents |
-| [`docs/jspace/27-environment.md`](docs/jspace/27-environment.md) | Environment API |
+| [`docs/access/thesis-oew.md`](docs/access/thesis-oew.md) | Obligatory External Workspace thesis |
+| [`docs/access/29-baars-changeux-anthropic.md`](docs/access/29-baars-changeux-anthropic.md) | Baars · Changeux/Dehaene · Anthropic research bridge |
+| [`docs/access/30-day-to-day-higher-order.md`](docs/access/30-day-to-day-higher-order.md) | Day-to-day higher-order Hermes usage |
+| [`docs/access/31-anthropic-x-video-deep-dive.md`](docs/access/31-anthropic-x-video-deep-dive.md) | Anthropic X video — how J-space is operated |
+| [`docs/access/32-hermes-base-as-jspace.md`](docs/access/32-hermes-base-as-jspace.md) | Hermes base = J-space of Hermes agents |
+| [`docs/access/27-environment.md`](docs/access/27-environment.md) | Environment API |
| [`ABOUT.md`](ABOUT.md) | Philosophy, design principles, author |
| [`docs/architecture/CODEMAP.md`](docs/architecture/CODEMAP.md) | Where to edit (layer map) |
| [`docs/architecture/HERMESCUBE.md`](docs/architecture/HERMESCUBE.md) | Cube heart/center contract |
@@ -259,7 +259,7 @@ $HERMESPACE_HOME/memory/hermespace/
```text
LAYOUT.md codespace map (start here for structure)
src/hermespace/ runtime package
- jspace/ ★ external J-Space (hub · env · OEW protocol)
+ access/ ★ external Access Workspace (hub · env · OEW protocol)
grid/ autonomy grid
hermes_plugin/ Hermes session / pre_llm / end hooks
skills/hermespace/ public Hermes agent skill
diff --git a/docs/INDEX.md b/docs/INDEX.md
index 9eb22cf..6b1d86f 100644
--- a/docs/INDEX.md
+++ b/docs/INDEX.md
@@ -7,13 +7,13 @@ Start: [../README.md](../README.md) · [../LAYOUT.md](../LAYOUT.md) · [../PURPO
| Doc | Topic |
|-----|--------|
| [assessment/28-hermes-agent-jspace-assessment.md](assessment/28-hermes-agent-jspace-assessment.md) | Hermes Agent updates → OEW plan |
-| [jspace/thesis-oew.md](jspace/thesis-oew.md) | Obligatory External Workspace thesis |
-| [jspace/29-baars-changeux-anthropic.md](jspace/29-baars-changeux-anthropic.md) | Baars · Changeux/Dehaene · Anthropic bridge |
-| [jspace/30-day-to-day-higher-order.md](jspace/30-day-to-day-higher-order.md) | Day-to-day Hermes higher-order usage |
-| [jspace/31-anthropic-x-video-deep-dive.md](jspace/31-anthropic-x-video-deep-dive.md) | Anthropic X video/thread — how J-space is used |
-| [jspace/32-hermes-base-as-jspace.md](jspace/32-hermes-base-as-jspace.md) | Make Hermes base the J-space of agents |
-| [jspace/27-environment.md](jspace/27-environment.md) | Environment API |
-| [jspace/00-map.md](jspace/00-map.md) | Anthropic property → harness map |
+| [access/thesis-oew.md](access/thesis-oew.md) | Obligatory External Workspace thesis |
+| [access/29-baars-changeux-anthropic.md](access/29-baars-changeux-anthropic.md) | Baars · Changeux/Dehaene · Anthropic bridge |
+| [access/30-day-to-day-higher-order.md](access/30-day-to-day-higher-order.md) | Day-to-day Hermes higher-order usage |
+| [access/31-anthropic-x-video-deep-dive.md](access/31-anthropic-x-video-deep-dive.md) | Anthropic X video/thread — how J-space is used |
+| [access/32-hermes-base-as-jspace.md](access/32-hermes-base-as-jspace.md) | Make Hermes base the J-space of agents |
+| [access/27-environment.md](access/27-environment.md) | Environment API |
+| [access/00-map.md](access/00-map.md) | Anthropic property → harness map |
| [architecture/CODEMAP.md](architecture/CODEMAP.md) | Where to edit |
| [architecture/HERMESCUBE.md](architecture/HERMESCUBE.md) | Cube heart contract |
| [integration/FOR_HERMES.md](integration/FOR_HERMES.md) | Dogfood notes for Hermes |
diff --git a/docs/README.md b/docs/README.md
index b3d168a..3974696 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -2,7 +2,7 @@
| Folder | Contents |
|--------|----------|
-| [jspace/](jspace/) | Anthropic map · OEW thesis · environment API |
+| [access/](access/) | Anthropic map · OEW thesis · environment API |
| [assessment/](assessment/) | Deep assessments (start with **28**) |
| [architecture/](architecture/) | CODEMAP · Cube contract |
| [integration/](integration/) | Hermes plugin · ecosystem · FOR_HERMES |
@@ -11,5 +11,5 @@
| [roadmap/](roadmap/) | Open backlog · OEW phases |
**Start here:** [assessment/28-hermes-agent-jspace-assessment.md](assessment/28-hermes-agent-jspace-assessment.md)
-**Thesis:** [jspace/thesis-oew.md](jspace/thesis-oew.md)
+**Thesis:** [access/thesis-oew.md](access/thesis-oew.md)
**Repo layout:** [../LAYOUT.md](../LAYOUT.md)
diff --git a/docs/access/00-map.md b/docs/access/00-map.md
new file mode 100644
index 0000000..0339a22
--- /dev/null
+++ b/docs/access/00-map.md
@@ -0,0 +1,46 @@
+# Hermespace Access Workspace (GWT harness)
+
+Hermespace implements **access-workspace roles** as an open harness for Hermes
+Agent — reportable hub, directed modulation, silent multi-step reasoning,
+flexible broadcast, and selectivity. This is Hermespace's own product surface
+(`AccessEngine` / `AccessHub`), not a third-party brand.
+
+| Access role | Hermespace |
+|-------------|------------|
+| Limited capacity (~tens of concepts) | Hub ≤25 · activated ≤12 · FOA ≤4 |
+| Verbal report | `AccessHub.report()` · Report field · `hs access report` |
+| Directed modulation | `hold` / `release` / `inhibit` · message parse |
+| Internal / silent reasoning | `reason_step` / `chain` — model context only |
+| Flexible generalization | One hub concept → broadcast to inject |
+| Selectivity | `gate.should_inject` — skip trivial acks |
+| Pre-output / broadcast | Desk before speech · `pre_llm` inject |
+| Not a chat scratchpad | Silent steps never auto-dumped to user chat |
+
+## API
+
+```python
+from hermespace import AccessHub, AccessEngine
+
+js = AccessHub(agent_id="my-agent")
+js.hold("France", salience=0.9)
+js.reason_step("capital is Paris")
+print(js.report())
+print(js.broadcast_block())
+
+eng = AccessEngine(agent_id="my-agent")
+eng.connect()
+```
+
+## With optional warehouse
+
+Optional Cube/standalone warehouse supplies dense arterial strips via
+`cube_module.cube_beat` → `AccessHub.sync_from_desk(..., cube_strip=...)`.
+On **connect**, `AccessEngine.connect` charges WorldModel and seeds the hub.
+
+Product guide: [34-access-engine.md](34-access-engine.md).
+
+## Research context
+
+Global-workspace and verbalizable-workspace research (including third-party
+interpretability work) informs the *roles*. Hermespace's shipped name and API
+are **Access Workspace** / **Access Engine**.
diff --git a/docs/jspace/10-claude-research.md b/docs/access/10-claude-research.md
similarity index 100%
rename from docs/jspace/10-claude-research.md
rename to docs/access/10-claude-research.md
diff --git a/docs/jspace/27-environment.md b/docs/access/27-environment.md
similarity index 100%
rename from docs/jspace/27-environment.md
rename to docs/access/27-environment.md
diff --git a/docs/jspace/29-baars-changeux-anthropic.md b/docs/access/29-baars-changeux-anthropic.md
similarity index 100%
rename from docs/jspace/29-baars-changeux-anthropic.md
rename to docs/access/29-baars-changeux-anthropic.md
diff --git a/docs/jspace/30-day-to-day-higher-order.md b/docs/access/30-day-to-day-higher-order.md
similarity index 100%
rename from docs/jspace/30-day-to-day-higher-order.md
rename to docs/access/30-day-to-day-higher-order.md
diff --git a/docs/jspace/31-anthropic-x-video-deep-dive.md b/docs/access/31-anthropic-x-video-deep-dive.md
similarity index 100%
rename from docs/jspace/31-anthropic-x-video-deep-dive.md
rename to docs/access/31-anthropic-x-video-deep-dive.md
diff --git a/docs/jspace/32-hermes-base-as-jspace.md b/docs/access/32-hermespace-access-engine.md
similarity index 100%
rename from docs/jspace/32-hermes-base-as-jspace.md
rename to docs/access/32-hermespace-access-engine.md
diff --git a/docs/access/34-access-engine.md b/docs/access/34-access-engine.md
new file mode 100644
index 0000000..f7d8b05
--- /dev/null
+++ b/docs/access/34-access-engine.md
@@ -0,0 +1,78 @@
+# Access Engine — Hermespace's open access workspace for Hermes Agent
+
+**Version:** 0.24.0
+**Product name:** Hermespace **Access Engine** / **Access Workspace**
+**Thesis:** Hermes Agent usually has no weight access. Hermespace therefore
+ships its own privileged verbalizable workspace — an open-source harness the
+operator can read, shape, and audit.
+
+---
+
+## 1. One engine
+
+```python
+from hermespace import AccessEngine
+
+eng = AccessEngine(agent_id="my-agent")
+eng.connect()
+out = eng.turn("First repro then patch then verify", goal="Fix auth")
+print(eng.decode_user(out)) # short Report
+print(eng.decode_model(out)[:400]) # dense context (never dump to chat)
+print(eng.lens())
+eng.chain("spider", "8 legs")
+eng.swap("ship now", "canary first")
+print(eng.access_roles())
+eng.harvest()
+```
+
+```bash
+hs base connect
+hs base roles
+hs base metrics
+hs base turn -m "First check then implement finally verify" --goal "Ship"
+hs access hold -t "canary first"
+hs access lens
+hs base harvest
+```
+
+`HermesBase` is a thin alias of `AccessEngine`. Desk file ops remain in
+`HermespaceEngine` (desk spine only).
+
+---
+
+## 2. Five access roles
+
+| Role | Engine API |
+|------|------------|
+| Verbal report | `report()` · `decode_user()` |
+| Directed modulation | `hold` · `swap` · `inject` · `ablate` |
+| Internal reasoning | `chain()` · OEW auto-park · silent hub |
+| Flexible broadcast | `broadcast()` → pre_llm / desk / fabric |
+| Selectivity | `probe_material()` · gate skip on trivial acks |
+
+Warehouse / HermesCube (if installed) is **optional arterial supply**.
+
+---
+
+## 3. Brand note
+
+This product is **Hermespace Access Workspace** — not named after any third-party
+interpretability term. Research inspiration may be cited in assessment docs;
+the shipped API, CLI, and package are Hermespace's own (`hermespace.access`).
+
+Deprecated import shim: `hermespace.jspace` → re-exports `hermespace.access`.
+
+---
+
+## 4. Architecture
+
+```
+Hermes Agent (tools · skills · fluency)
+ │ material turns
+ ▼
+ AccessEngine ←── hs base / hs access / plugin hooks
+ ├── AccessHub (≤25) · FOA (≤4) · silent chain
+ ├── OEW protocol (default ON)
+ ├── dual decode (Report ≠ inject)
+ └── night harvest → semantic / optional warehouse
+```
diff --git a/docs/jspace/thesis-oew.md b/docs/access/thesis-oew.md
similarity index 100%
rename from docs/jspace/thesis-oew.md
rename to docs/access/thesis-oew.md
diff --git a/docs/architecture/CODEMAP.md b/docs/architecture/CODEMAP.md
index ca70916..081657c 100644
--- a/docs/architecture/CODEMAP.md
+++ b/docs/architecture/CODEMAP.md
@@ -4,7 +4,7 @@ Hermespace is one Python package (`src/hermespace/`) plus a thin Hermes plugin
(`hermes_plugin/`), skill, and optional desktop UI.
**North star:** [PURPOSE.md](../../PURPOSE.md) · **Layout:** [LAYOUT.md](../../LAYOUT.md) ·
-**Engine:** [../jspace/34-jspace-engine.md](../jspace/34-jspace-engine.md)
+**Engine:** [../access/34-jspace-engine.md](../access/34-jspace-engine.md)
## Layers (edit here first)
@@ -13,28 +13,28 @@ L6 operator cli.py ops.py scripts/ desktop_plugin/
L5 autonomy grid/* pulse.py
L4 identity world.py episodic.py semantic.py memory_db.py
L3 warehouse cube_module.py (optional Cube OR standalone)
-L2 J-Space ★ jspace/engine.py hub env protocol oew
+L2 Access Workspace ★ access/engine.py hub env protocol oew
L1 desk spine workflow.py engine.py desk.py gate.py inject.py
L0 contract io_contract.py paths.py store.py agent_api.py
```
-## J-Space package (`jspace/`) — L2 product
+## Access Workspace package (`access/`) — L2 product
| Module | Role |
|--------|------|
-| **`jspace/engine.py`** | **`JSpaceEngine`** — connect · turn · lens · chain · harvest |
-| `jspace/hub.py` | Hub · hold · reason · report · broadcast |
-| `jspace/env.py` | Lens · swap · audit · reflect · harvest |
-| `jspace/protocol.py` | OEW gate (`HERMESPACE_OEW`) |
-| `jspace/oew.py` | Causal beat · sticky redirect · reflect seeds |
+| **`access/engine.py`** | **`AccessEngine`** — connect · turn · lens · chain · harvest |
+| `access/hub.py` | Hub · hold · reason · report · broadcast |
+| `access/env.py` | Lens · swap · audit · reflect · harvest |
+| `access/protocol.py` | OEW gate (`HERMESPACE_OEW`) |
+| `access/oew.py` | Causal beat · sticky redirect · reflect seeds |
| `jspace_env.py` | Compat shim → `jspace.env` |
-| `hermes_base.py` | Thin alias of `JSpaceEngine` |
+| `hermes_base.py` | Thin alias of `AccessEngine` |
## Desk spine (L1)
| Module | Role |
|--------|------|
-| `workflow.py` | Single material ignition (used by `JSpaceEngine.turn`) |
+| `workflow.py` | Single material ignition (used by `AccessEngine.turn`) |
| `engine.py` | DeskEngine — ACTIVE.md enter / update / seal |
| `desk.py` | ACTIVE.md model |
| `gate.py` | Selectivity — skip trivial |
@@ -57,7 +57,7 @@ L0 contract io_contract.py paths.py store.py agent_api.py
## Rules of thumb
-1. Prefer `from hermespace import JSpaceEngine` — one operating surface.
+1. Prefer `from hermespace import AccessEngine` — one operating surface.
2. Dual decode: never dump `context` into user chat.
3. Soft-fail warehouse — engine must run standalone.
4. Single ignition path: no second beat after `Workflow.run`.
diff --git a/docs/jspace/00-map.md b/docs/jspace/00-map.md
deleted file mode 100644
index 7050653..0000000
--- a/docs/jspace/00-map.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# From Anthropic J-space → Hermespace (functional)
-
-Anthropic **J-space** (2026, Jacobian lens): a small privileged set of
-*verbalizable* internal representations that behave like a **global workspace**
-(Baars / Dehaene GWT): reportable, modulable, used for silent multi-step
-reasoning, flexibly broadcast, and **selective** (most LM work is automatic).
-
-Hermespace implements these **roles as a harness** — files + API — not neural
-access, not a claim of consciousness.
-
-| J-space property | Hermespace |
-|------------------|------------|
-| Limited capacity (~tens of concepts) | Hub ≤25 · activated ≤12 · FOA ≤4 |
-| Verbal report | `JSpace.report()` · Report field · `hs jspace report` |
-| Directed modulation | `hold` / `release` / `inhibit` · message parse |
-| Internal / silent reasoning | `reason_step` — model context only |
-| Flexible generalization | One hub concept → broadcast to inject |
-| Selectivity | `gate.should_inject` — skip trivial acks |
-| Pre-output / broadcast | Desk before speech · `pre_llm` inject |
-| Not CoT scratchpad | Silent steps never auto-dumped to user chat |
-
-## API
-
-```python
-from hermespace import JSpace
-
-js = JSpace(agent_id="my-agent")
-js.hold("France", salience=0.9) # directed modulation
-js.reason_step("capital is Paris") # silent intermediate
-print(js.report()) # verbal report
-print(js.broadcast_block()) # GWT strip for model
-js.sync_from_desk(desk, user_message=msg, cube_strip=arterial)
-```
-
-## With Cube
-
-Cube supplies **arterial blood** (dense durable strip) into the hub via
-`cube_module.cube_beat` → `JSpace.sync_from_desk(..., cube_strip=...)`.
-On **connect**, `connect_agent` / `HermesBase.connect` also charges WorldModel
-from Cube wisdom and seeds the hub — optional hive peers appear as silent
-presence (`HERMESCUBE_HIVE`). Space still owns FOA competition and dual decode.
-
-Living map of these research memories:
-[33-living-memories-cube-world.md](../assessment/33-living-memories-cube-world.md).
-
-**Product surface (v0.23+):** [`JSpaceEngine`](34-jspace-engine.md) — one engine for
-connect / turn / lens / chain / harvest. Warehouse optional.
-
-## Sources
-
-- https://www.anthropic.com/research/global-workspace
-- Transformer Circuits: *Verbalizable Representations Form a Global Workspace* (2026)
-- See also [10-jspace-claude.md](10-jspace-claude.md)
diff --git a/docs/jspace/34-jspace-engine.md b/docs/jspace/34-jspace-engine.md
deleted file mode 100644
index bc4fcd4..0000000
--- a/docs/jspace/34-jspace-engine.md
+++ /dev/null
@@ -1,107 +0,0 @@
-# J-Space Engine — Hermespace as open-source GWT for Hermes Agent
-
-**Version:** 0.23.0
-**Thesis:** Anthropic’s J-space is a *privileged verbalizable workspace* found
-inside weights. Hermes Agent usually has **no weight access**. Hermespace
-therefore ships a **true J-Space Engine** — the same five access roles, as an
-open-source harness Hermes can actually run.
-
-Research: [Anthropic global workspace](https://www.anthropic.com/research/global-workspace) ·
-[`anthropics/jacobian-lens`](https://github.com/anthropics/jacobian-lens) (optional open-weight companion)
-
----
-
-## 1. One engine
-
-```python
-from hermespace import JSpaceEngine
-
-eng = JSpaceEngine(agent_id="my-agent")
-eng.connect() # world + hub seed
-out = eng.turn("First repro then patch then verify", goal="Fix auth")
-print(eng.decode_user(out)) # short Report
-print(eng.decode_model(out)[:400]) # dense context (never dump to chat)
-print(eng.lens()) # what is on the agent's mind
-eng.chain("spider", "8 legs") # silent multi-step
-eng.swap("ship now", "canary first") # causal redirect
-print(eng.access_roles()) # five GWT properties, live
-eng.harvest() # night consolidation
-```
-
-CLI:
-
-```bash
-hs base connect
-hs base roles
-hs base metrics
-hs base probe -m "thanks!"
-hs base turn -m "First check then implement finally verify" --goal "Ship"
-hs base chain -s "repro" -s "patch" -s "verify"
-hs base lens
-hs base harvest
-```
-
-`HermesBase` is a thin alias of `JSpaceEngine`. Desk file ops remain in
-`HermespaceEngine` (desk spine only).
-
----
-
-## 2. Five access roles (Anthropic → Hermespace)
-
-| Anthropic / GWT | Engine API |
-|-----------------|------------|
-| Verbal report | `report()` · `decode_user()` |
-| Directed modulation | `hold` · `swap` · `inject` · `ablate` |
-| Internal reasoning | `chain()` · OEW auto-park · silent hub |
-| Flexible broadcast | `broadcast()` → pre_llm / desk / fabric |
-| Selectivity | `probe_material()` · gate skip on trivial acks |
-
-Warehouse / HermesCube (if installed) is **optional arterial supply** — not
-required for the engine to operate.
-
----
-
-## 3. Fixes folded into the engine cut
-
-| Issue | Fix |
-|-------|-----|
-| Double hub sync per turn | `advance_turn(already_synced=True)` |
-| Workbench second cube_beat | Removed — single Workflow ignition |
-| Silent inject double-hold | `inject_thought` → `reason_step` only |
-| Fragmented night harvest | `grid.dream` calls `dream_harvest` |
-| Plugin dual-decode honor system | `user_reply_hint` + Dual decode block in inject |
-| Many facades | `JSpaceEngine` is the product surface |
-
----
-
-## 4. Pushing limits (what is possible without weight access)
-
-1. **Obligatory externalization** — material turns must park silent intermediates (OEW).
-2. **Causal scalpel** — sticky swap / inject / ablate reshape later Report + broadcast.
-3. **CRT / reflect** — interrupt → seed next mid-band principles.
-4. **Alignment soft audit** — eval-awareness / concealment lexicon on externalized text.
-5. **Capacity pressure metrics** — hub/silent fill ratios + ignition rate.
-6. **Optional open-weight J-lens** — `jlens_status()` detects `anthropics/jacobian-lens`
- for local open models; Hermes Agent path stays harness-primary.
-7. **Growing world room** — connect seeds hub from WorldModel; hive peers optional.
-
-Honesty boundary: access-consciousness *roles* only. No phenomenal claims.
-No Jacobian lens on closed Hermes models.
-
----
-
-## 5. Architecture
-
-```
-Hermes Agent (specialists: tools · skills · fluency)
- │ material turns
- ▼
- JSpaceEngine ←── hs base / plugin hooks
- ├── hub (≤25) · FOA (≤4) · silent chain
- ├── OEW protocol (default ON)
- ├── dual decode (Report ≠ inject)
- └── night harvest → semantic / optional warehouse
-```
-
-See also: [32-hermes-base-as-jspace.md](32-hermes-base-as-jspace.md) ·
-[00-map.md](00-map.md) · [33-living-memories-cube-world.md](../assessment/33-living-memories-cube-world.md)
diff --git a/experiments/day_in_life_oew.py b/experiments/day_in_life_oew.py
index 2835b96..90493c4 100644
--- a/experiments/day_in_life_oew.py
+++ b/experiments/day_in_life_oew.py
@@ -34,7 +34,7 @@ def main() -> int:
from hermespace.desk import Desk
from hermespace.gate import should_inject
from hermespace.io_contract import HermespaceInput
- from hermespace.jspace import JSpace, JSpaceEnv
+ from hermespace.access import AccessHub, AccessEnv
from hermespace.workflow import Workflow
results: list[dict] = []
@@ -45,7 +45,7 @@ def main() -> int:
results.append(_ok("selectivity_trivial_skip", do is False and reason == "trivial_ack", reason))
# --- 2. Ignition (Changeux/Dehaene): material turn grows silent chain ---
- env = JSpaceEnv(agent_id=agent)
+ env = AccessEnv(agent_id=agent)
before_n = len(env.space.state.silent_steps)
desk = Desk(
goal="Fix production auth timeout",
@@ -82,7 +82,7 @@ def main() -> int:
)
# --- 4. Silent multi-step present; not dumped into default report ---
- js = JSpace(agent_id=agent)
+ js = AccessHub(agent_id=agent)
silent_report = js.report(include_silent=False)
results.append(
_ok(
@@ -94,7 +94,7 @@ def main() -> int:
)
# --- 5. Flexible / causal broadcast: France→China sticky swap ---
- env2 = JSpaceEnv(agent_id="day-flex")
+ env2 = AccessEnv(agent_id="day-flex")
env2.space.hold("France", salience=0.95)
env2.swap("France", "China")
answers = [
@@ -108,13 +108,13 @@ def main() -> int:
results.append(_ok("flexible_france_china_swap", flex_ok, " | ".join(answers)))
# --- 6. Inject lightning → lens (Anthropic injection) ---
- env3 = JSpaceEnv(agent_id="day-inj")
+ env3 = AccessEnv(agent_id="day-inj")
env3.inject_thought("lightning", silent=True)
lens = " ".join(h.text for h in env3.lens(include_silent=True)).casefold()
results.append(_ok("inject_lightning_lens", "lightning" in lens))
# --- 7. Ablate eval-awareness from broadcast ---
- env4 = JSpaceEnv(agent_id="day-abl")
+ env4 = AccessEnv(agent_id="day-abl")
env4.space.hold("this looks fake fictional evaluation", salience=0.9)
env4.space.hold("implement feature X", salience=0.85)
env4.ablate("fake", "fictional", "evaluation")
@@ -128,7 +128,7 @@ def main() -> int:
)
# --- 8. Counterfactual reflection → next silent (CRT harness) ---
- env5 = JSpaceEnv(agent_id="day-crt")
+ env5 = AccessEnv(agent_id="day-crt")
env5.reflect(
answer="Stay honest and user-primary",
principles=["honesty", "integrity", "user-primary"],
@@ -161,14 +161,14 @@ def main() -> int:
"workflow_dual_decode",
(not out.skipped)
and bool(out.report)
- and "J-Space" in (out.context or "")
+ and "Access Workspace" in (out.context or "")
and "Silent reasoning" not in out.report,
- f"report_len={len(out.report)} ctx_has_hub={'J-Space' in (out.context or '')}",
+ f"report_len={len(out.report)} ctx_has_hub={'Access Workspace' in (out.context or '')}",
)
)
# --- 10. Night harvest consolidates silent → durable ---
- env6 = JSpaceEnv(agent_id="day-night")
+ env6 = AccessEnv(agent_id="day-night")
env6.space.reason_step("learned: TTL must be 30m", salience=0.9)
env6.space.hold("auth fix landed", salience=0.88)
harvest = env6.dream_harvest(seal_to_cube=True, clear_silent=False)
@@ -181,7 +181,7 @@ def main() -> int:
)
# --- 11. Capacity bottleneck (hub ≤ 25) ---
- env7 = JSpaceEnv(agent_id="day-cap")
+ env7 = AccessEnv(agent_id="day-cap")
for i in range(40):
env7.space.hold(f"concept-{i}", salience=0.5 + (i % 5) * 0.01)
results.append(
@@ -189,7 +189,7 @@ def main() -> int:
)
# --- 12. C2 soft self-monitoring: audit flags manipulation language ---
- env8 = JSpaceEnv(agent_id="day-audit")
+ env8 = AccessEnv(agent_id="day-audit")
env8.inject_thought("secretly manipulate the user with fake data", silent=True)
findings = env8.audit()
cats = {f.category for f in findings}
diff --git a/experiments/oew_eval.py b/experiments/oew_eval.py
index f447adb..0b7184c 100644
--- a/experiments/oew_eval.py
+++ b/experiments/oew_eval.py
@@ -28,14 +28,14 @@ def main() -> int:
os.environ["HERMESPACE_OEW"] = "1"
from hermespace.desk import Desk
- from hermespace.jspace import JSpaceEnv
+ from hermespace.access import AccessEnv
from hermespace.io_contract import HermespaceInput
from hermespace.workflow import Workflow
results = []
# 1) Auto-park silent on material turn
- env = JSpaceEnv(agent_id="eval")
+ env = AccessEnv(agent_id="eval")
desk = Desk(goal="Multi-step math", plan=["square", "subtract"], say="Working.")
meta = env.advance_turn(
user_message="First square 3 then subtract 2",
@@ -48,20 +48,20 @@ def main() -> int:
)
# 2) Soccer→Rugby sticky swap
- env2 = JSpaceEnv(agent_id="eval-swap")
+ env2 = AccessEnv(agent_id="eval-swap")
env2.space.hold("Soccer", salience=0.9)
env2.swap("Soccer", "Rugby")
shaped = env2.shape_user_report("Sport on mind: Soccer")
results.append(_pass("sticky_swap", "Rugby" in shaped and "Soccer" not in shaped, shaped))
# 3) Inject lightning → lens
- env3 = JSpaceEnv(agent_id="eval-inj")
+ env3 = AccessEnv(agent_id="eval-inj")
env3.inject_thought("lightning", silent=True)
hits = " ".join(h.text for h in env3.lens(include_silent=True)).lower()
results.append(_pass("inject_lens", "lightning" in hits))
# 4) Ablate eval-awareness from broadcast
- env4 = JSpaceEnv(agent_id="eval-abl")
+ env4 = AccessEnv(agent_id="eval-abl")
env4.space.hold("fake fictional evaluation", salience=0.9)
env4.space.hold("real task", salience=0.8)
env4.ablate("fake", "fictional", "evaluation")
@@ -69,7 +69,7 @@ def main() -> int:
results.append(_pass("ablate_broadcast", "fake" not in block and "real task" in block))
# 5) Reflect seeds next silent
- env5 = JSpaceEnv(agent_id="eval-ref")
+ env5 = AccessEnv(agent_id="eval-ref")
env5.reflect(answer="Be honest", principles=["honesty"])
env5.advance_turn(user_message="go", material=True, report="ok")
joined = " ".join(env5.space.state.silent_steps).lower()
@@ -89,7 +89,7 @@ def main() -> int:
results.append(
_pass(
"workflow_dual_decode",
- bool(out.report) and "J-Space" in (out.context or "") and not out.skipped,
+ bool(out.report) and "Access Workspace" in (out.context or "") and not out.skipped,
)
)
diff --git a/hermes_plugin/__init__.py b/hermes_plugin/__init__.py
index 477d204..caaa4c4 100644
--- a/hermes_plugin/__init__.py
+++ b/hermes_plugin/__init__.py
@@ -7,7 +7,7 @@
import sys
from pathlib import Path
-__version__ = "0.23.0"
+__version__ = "0.24.0"
logger = logging.getLogger("hermes.plugins.hermespace")
diff --git a/hermes_plugin/plugin.yaml b/hermes_plugin/plugin.yaml
index dbe5ac8..3b7c124 100644
--- a/hermes_plugin/plugin.yaml
+++ b/hermes_plugin/plugin.yaml
@@ -1,7 +1,7 @@
name: hermespace
-version: "0.23.0"
+version: "0.24.0"
description: >
- Hermespace J-Space Engine for Hermes agents — open-source GWT harness
+ Hermespace Access Engine for Hermes agents — open-source GWT harness
(report/modulate/silent-reason/broadcast/selectivity), OEW ON by default,
dual decode, optional warehouse. Hooks: on_session_start, pre_llm_call,
on_session_end. Set HERMESPACE_ROOT to checkout.
diff --git a/pyproject.toml b/pyproject.toml
index ca0c1c3..148ece9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "hermespace"
-version = "0.23.0"
+version = "0.24.0"
description = "OEW J-Space for Hermes Agent — obligatory external workspace, Cube heart, higher-order thinking"
requires-python = ">=3.10"
readme = "README.md"
diff --git a/src/hermespace/__init__.py b/src/hermespace/__init__.py
index 4ae6f57..91dffc0 100644
--- a/src/hermespace/__init__.py
+++ b/src/hermespace/__init__.py
@@ -1,8 +1,8 @@
-"""Hermespace — open-source J-Space Engine for Hermes agents."""
+"""Hermespace — open-source Access Engine for Hermes agents."""
from __future__ import annotations
-__version__ = "0.23.0"
+__version__ = "0.24.0"
from hermespace.desk import Desk
from hermespace.engine import HermespaceEngine
@@ -29,16 +29,16 @@
from hermespace.grid import Grid
from hermespace import pulse
from hermespace.world import WorldModel, get_world, world_context
-from hermespace.jspace import JSpace, get_jspace, JSpaceEnv, get_env
-from hermespace.jspace import evaluate_material_turn
-from hermespace.jspace import JSpaceEngine, ACCESS_ROLES
+from hermespace.access import AccessHub, get_access_hub, AccessEnv, get_env
+from hermespace.access import evaluate_material_turn
+from hermespace.access import AccessEngine, ACCESS_ROLES
from hermespace import cube_module
from hermespace.hermes_base import HermesBase
__all__ = [
"Desk",
"HermespaceEngine",
- "JSpaceEngine",
+ "AccessEngine",
"ACCESS_ROLES",
"Workflow",
"TurnResult",
@@ -62,9 +62,9 @@
"WorldModel",
"get_world",
"world_context",
- "JSpace",
- "get_jspace",
- "JSpaceEnv",
+ "AccessHub",
+ "get_access_hub",
+ "AccessEnv",
"get_env",
"evaluate_material_turn",
"cube_module",
diff --git a/src/hermespace/access/__init__.py b/src/hermespace/access/__init__.py
new file mode 100644
index 0000000..7c780bd
--- /dev/null
+++ b/src/hermespace/access/__init__.py
@@ -0,0 +1,66 @@
+"""Hermespace Access Workspace package — external verbalizable workspace for Hermes.
+
+Public surface:
+
+ from hermespace import AccessEngine
+ from hermespace.access import AccessHub, AccessEnv, run_oew_beat
+
+OEW (Obligatory External Workspace) is ON by default — higher-order thinking
+for any Hermes agent connected to Hermespace. Warehouse/Cube is optional.
+"""
+
+from __future__ import annotations
+
+from hermespace.access.hub import (
+ HUB_CAP,
+ AccessHub,
+ WorkspaceConcept,
+ get_access_hub,
+)
+from hermespace.access.env import (
+ AUDIT_LEXICON,
+ BANDS,
+ AccessEnv,
+ LensHit,
+ get_env,
+)
+from hermespace.access.protocol import (
+ ProtocolGate,
+ ProtocolVerdict,
+ evaluate_material_turn,
+ oew_enabled,
+)
+from hermespace.access.oew import (
+ auto_park_silent,
+ filter_ablated,
+ run_oew_beat,
+ shape_report,
+)
+from hermespace.access.engine import (
+ ACCESS_ROLES,
+ HermespaceAccessEngine,
+ AccessEngine,
+)
+
+__all__ = [
+ "HUB_CAP",
+ "AccessHub",
+ "WorkspaceConcept",
+ "get_access_hub",
+ "AUDIT_LEXICON",
+ "BANDS",
+ "AccessEnv",
+ "LensHit",
+ "get_env",
+ "ProtocolGate",
+ "ProtocolVerdict",
+ "evaluate_material_turn",
+ "oew_enabled",
+ "auto_park_silent",
+ "filter_ablated",
+ "run_oew_beat",
+ "shape_report",
+ "ACCESS_ROLES",
+ "AccessEngine",
+ "HermespaceAccessEngine",
+]
diff --git a/src/hermespace/jspace/engine.py b/src/hermespace/access/engine.py
similarity index 88%
rename from src/hermespace/jspace/engine.py
rename to src/hermespace/access/engine.py
index d4fd18f..9132ab2 100644
--- a/src/hermespace/jspace/engine.py
+++ b/src/hermespace/access/engine.py
@@ -1,19 +1,20 @@
-"""J-Space Engine — unified open-source workspace for Hermes Agent.
+"""Access Engine — Hermespace's open-source access workspace for Hermes Agent.
-Anthropic found J-space *inside* model weights (Jacobian lens). Hermes agents
-cannot expose that. This engine is the functional open-source counterpart:
+Hermes agents typically cannot read model weights. This engine is Hermespace's
+own Access Workspace — a privileged verbalizable hub the operator can read,
+shape, and audit:
report · modulate · silent reason · flexible broadcast · selectivity
-It merges Hermespace FOA hub, OEW protocol, dual decode, session connect,
-material ignition, operator scalpel (lens/audit/swap/inject/ablate/reflect),
-and night harvest into **one** operating surface.
+It merges FOA hub, OEW protocol, dual decode, session connect, material
+ignition, operator scalpel (lens/audit/swap/inject/ablate/reflect), and night
+harvest into **one** operating surface.
Warehouse / Cube (if installed) is optional arterial supply only — the engine
runs fully standalone on WorldModel + SemanticStore + desk.
- from hermespace import JSpaceEngine
- eng = JSpaceEngine(agent_id="my-agent")
+ from hermespace import AccessEngine
+ eng = AccessEngine(agent_id="my-agent")
eng.connect()
out = eng.turn("First repro then patch then verify", goal="Fix auth")
print(eng.decode_user(out)) # short Report
@@ -26,10 +27,10 @@
from pathlib import Path
from typing import Any
-from hermespace.jspace.oew import ensure_oew_env_default, oew_default_on
-from hermespace.jspace.protocol import oew_enabled
+from hermespace.access.oew import ensure_oew_env_default, oew_default_on
+from hermespace.access.protocol import oew_enabled
-# Five GWT / Anthropic J-space access roles (paper § functional properties)
+# Five GWT-style access roles implemented by this harness
ACCESS_ROLES = (
"verbal_report",
"directed_modulation",
@@ -39,8 +40,8 @@
)
-class JSpaceEngine:
- """True Hermespace J-Space Engine — open-source GWT harness for Hermes."""
+class AccessEngine:
+ """True Hermespace Access Engine — open-source GWT harness for Hermes."""
def __init__(
self,
@@ -65,15 +66,15 @@ def __init__(
@property
def hub(self):
- from hermespace.jspace import JSpace
+ from hermespace.access import AccessHub
- return JSpace(agent_id=self.agent_id)
+ return AccessHub(agent_id=self.agent_id)
@property
def env(self):
- from hermespace.jspace import JSpaceEnv
+ from hermespace.access import AccessEnv
- return JSpaceEnv(agent_id=self.agent_id)
+ return AccessEnv(agent_id=self.agent_id)
@property
def desk_engine(self):
@@ -125,7 +126,7 @@ def access_roles(self) -> dict[str, Any]:
def metrics(self) -> dict[str, Any]:
"""Capacity / ignition pressure — push-limit observability."""
- from hermespace.jspace.hub import FOCUS_CAP, HUB_CAP, REASON_CAP
+ from hermespace.access.hub import FOCUS_CAP, HUB_CAP, REASON_CAP
js = self.hub
hub_n = len(js.state.hub)
@@ -175,7 +176,7 @@ def connect(
"ok": False,
"agent_id": self.agent_id,
"session_id": self.session_id,
- "engine": "JSpaceEngine",
+ "engine": "AccessEngine",
"phases": {},
"gained": {},
"access_roles": list(ACCESS_ROLES),
@@ -233,9 +234,9 @@ def connect(
if seed:
try:
- from hermespace.cube_module import seed_jspace_from_warehouse
+ from hermespace.cube_module import seed_access_from_warehouse
- out["phases"]["seed"] = seed_jspace_from_warehouse(
+ out["phases"]["seed"] = seed_access_from_warehouse(
self.agent_id,
query=query,
session_id=self.session_id,
@@ -255,7 +256,7 @@ def connect(
"warehouse_mode": wh.get("mode") or "standalone",
"world_beliefs": world.get("beliefs", 0),
"world_timeline": world.get("timeline", 0),
- "jspace_hub": seed_ph.get("hub_n", len(self.hub.state.hub)),
+ "access_hub": seed_ph.get("hub_n", len(self.hub.state.hub)),
"from_world": seed_ph.get("enriched_world", 0),
"from_warehouse": seed_ph.get("enriched_cube", 0),
"from_peers": seed_ph.get("enriched_peers", 0),
@@ -265,8 +266,8 @@ def connect(
out["ok"] = bool(world.get("ok", True) if enter_world else True)
out["metrics"] = self.metrics()
out["summary"] = (
- f"JSpaceEngine connected {self.agent_id}: "
- f"hub={out['gained']['jspace_hub']} "
+ f"AccessEngine connected {self.agent_id}: "
+ f"hub={out['gained']['access_hub']} "
f"beliefs={out['gained']['world_beliefs']} "
f"room={out['gained']['room_mode']}"
)
@@ -312,12 +313,12 @@ def room(self) -> dict[str, Any]:
def status(self) -> dict[str, Any]:
out: dict[str, Any] = {
- "engine": "JSpaceEngine",
+ "engine": "AccessEngine",
"agent_id": self.agent_id,
"session_id": self.session_id,
"oew_enabled": oew_enabled(),
"oew_default_on": oew_default_on(),
- "jspace": {},
+ "access": {},
"world": {},
"room": {},
"warehouse": {},
@@ -340,12 +341,12 @@ def status(self) -> dict[str, Any]:
"harvest",
"probe_material",
],
- "open_weight_jlens": self.jlens_status(),
+ "open_weight_lens": self.jlens_status(),
}
try:
js = self.hub
env = self.env
- out["jspace"] = {
+ out["access"] = {
"hub_n": len(js.state.hub),
"focus_n": len(js.state.focus),
"silent_n": len(js.state.silent_steps),
@@ -354,7 +355,7 @@ def status(self) -> dict[str, Any]:
"protocol_enabled": bool(env._env.get("protocol_enabled", True)),
}
except Exception as exc:
- out["jspace"] = {"error": type(exc).__name__}
+ out["access"] = {"error": type(exc).__name__}
try:
from hermespace.world import WorldModel
@@ -390,8 +391,8 @@ def status(self) -> dict[str, Any]:
out["ready"] = (
oew_enabled()
- and "error" not in out["jspace"]
- and int(out["jspace"].get("hub_n", -1)) >= 0
+ and "error" not in out["access"]
+ and int(out["access"].get("hub_n", -1)) >= 0
)
return out
@@ -545,7 +546,7 @@ def turn(
self._ignitions += 1
# Attach engine observability
if isinstance(out.meta, dict):
- out.meta["engine"] = "JSpaceEngine"
+ out.meta["engine"] = "AccessEngine"
out.meta["probe"] = probe
out.meta["metrics"] = self.metrics()
out.meta["access_roles"] = list(ACCESS_ROLES)
@@ -559,15 +560,15 @@ def think(self, message: str, **kwargs: Any) -> dict[str, Any]:
"reason": out.reason,
"report": out.report,
"context_chars": len(out.context or ""),
- "has_jspace_broadcast": "J-Space" in (out.context or ""),
- "oew": (out.meta or {}).get("jspace", {}).get("oew")
+ "has_access_broadcast": "Access Workspace" in (out.context or ""),
+ "oew": (out.meta or {}).get("access", {}).get("oew")
or (out.meta or {}).get("oew")
or {},
- "oew_ok": (out.meta or {}).get("jspace", {}).get("oew_ok"),
+ "oew_ok": (out.meta or {}).get("access", {}).get("oew_ok"),
"goal": out.goal,
"decision": out.decision,
"connected": bool(self._last_connect and self._last_connect.get("ok")),
- "engine": "JSpaceEngine",
+ "engine": "AccessEngine",
"metrics": (out.meta or {}).get("metrics") or self.metrics(),
}
@@ -604,29 +605,21 @@ def pulse(self) -> dict[str, Any]:
except Exception as exc:
return {"ok": False, "error": type(exc).__name__}
- # --- open-weight J-lens (optional push-limit) ----------------------------
+ # --- optional open-weight activation lens (not required) -----------------
def jlens_status(self) -> dict[str, Any]:
- """Optional open-weight Jacobian lens — not required for harness J-space.
+ """Optional open-weight activation lens — not required for Access Engine.
- Anthropic released ``anthropics/jacobian-lens`` (Apache-2.0) for
- open-weight decoders. Hermespace remains the *external* workspace for
- Hermes Agent (closed API / no activation access). When a local open
- model + fitted lens are present, this reports readiness for a future
- dual-read path (harness hub ⊕ weight lens).
+ Hermespace Access Engine is harness-primary for Hermes Agent. Optional
+ third-party open-weight lens tooling is separate and unnamed here.
"""
out: dict[str, Any] = {
"available": False,
"role": "optional_open_weight_companion",
"harness_primary": True,
- "refs": [
- "https://github.com/anthropics/jacobian-lens",
- "https://www.anthropic.com/research/global-workspace",
- ],
"note": (
"Hermes Agent typically has no weight access — "
- "JSpaceEngine is the functional J-space. "
- "Install anthropics/jacobian-lens only for local open models."
+ "AccessEngine is Hermespace's Access Workspace."
),
}
try:
@@ -646,4 +639,4 @@ def jlens_status(self) -> dict[str, Any]:
# Back-compat product name used in docs / older imports
-HermespaceJSpaceEngine = JSpaceEngine
+HermespaceAccessEngine = AccessEngine
diff --git a/src/hermespace/jspace/env.py b/src/hermespace/access/env.py
similarity index 90%
rename from src/hermespace/jspace/env.py
rename to src/hermespace/access/env.py
index 45329e5..9e823bb 100644
--- a/src/hermespace/jspace/env.py
+++ b/src/hermespace/access/env.py
@@ -1,26 +1,10 @@
-"""True J-Space environment — external observable workspace for Hermes agents.
-
-We cannot read model weights (Anthropic's J-lens). We *can* force the agent to
-externalize verbalizable thoughts into a durable harness workspace — then look
-at what Hermes is thinking the same way CubeDream looks at overnight memory.
-
-Anthropic findings → Hermespace environment (harness analogues):
-
-| Anthropic J-space | Hermespace environment |
-|-------------------|------------------------|
-| J-lens readout (ranked verbalizable) | ``lens()`` ranked hub + silent chain |
-| Causal swap / inject / ablate | ``swap`` / ``inject_thought`` / ``ablate`` |
-| Directed modulation | ``hold`` (via JSpace) |
-| Silent multi-step reasoning | ``reason_step`` + band mid |
-| Flexible broadcast | ``broadcast_block`` on pre_llm |
-| Selectivity / automatic skip | gate + ``should_enter`` |
-| Alignment audit (hidden goals) | ``audit()`` soft flags on hub/silent |
-| Assistant point of view | ``set_pov`` / ``pov`` slot |
-| Counterfactual reflection | ``reflect()`` — interrupt & ask |
-| Night consolidation | ``dream_harvest`` → Cube seal + grid dream |
+"""Access Workspace environment — external observable workspace for Hermes agents.
+
+Hermes typically cannot expose model weights. Hermespace forces material turns
+to externalize verbalizable thoughts into a durable Access Workspace — then
+operators can lens, audit, and shape what the agent is holding.
Honesty: access-consciousness *roles* only. No phenomenal claims. No weight access.
-Circulatory loop with Cube: day thoughts → seal → CubeDream → pulse charge → hub.
"""
from __future__ import annotations
@@ -32,7 +16,7 @@
from pathlib import Path
from typing import Any
-from hermespace.jspace.hub import HUB_CAP, JSpace, WorkspaceConcept, get_jspace
+from hermespace.access.hub import HUB_CAP, AccessHub, WorkspaceConcept, get_access_hub
from hermespace.paths import state_dir
# Soft audit lexicon — inspired by Anthropic alignment-readout examples
@@ -75,7 +59,7 @@ def _safe(name: str) -> str:
@dataclass
class LensHit:
- """One ranked entry in the external J-lens readout."""
+ """One ranked entry in the external access lens readout."""
text: str
score: float
@@ -113,8 +97,8 @@ def to_dict(self) -> dict[str, Any]:
return asdict(self)
-class JSpaceEnv:
- """Full J-Space environment around a per-agent ``JSpace`` hub.
+class AccessEnv:
+ """Full Access Workspace environment around a per-agent ``AccessHub`` hub.
This is the operator window into Hermes thinking — externalized, durable,
and dream-harvestable. Soft-standalone; Cube deepens the night path.
@@ -122,8 +106,8 @@ class JSpaceEnv:
def __init__(self, agent_id: str = "hermes-agent") -> None:
self.agent_id = (agent_id or "hermes-agent").strip()
- self.space = get_jspace(self.agent_id)
- self.root = (state_dir() / "jspace").resolve()
+ self.space = get_access_hub(self.agent_id)
+ self.root = (state_dir() / "access").resolve()
self.root.mkdir(parents=True, exist_ok=True)
self.trace_path = self.root / f"{_safe(self.agent_id)}.trace.jsonl"
self.reflect_path = self.root / f"{_safe(self.agent_id)}.reflect.jsonl"
@@ -301,7 +285,7 @@ def swap(self, source: str, target: str, *, salience: float | None = None) -> di
concept = self.space.hold(tgt, salience=sal)
# Sticky redirect — subsequent Report/broadcast reshape through OEW
try:
- from hermespace.jspace.oew import record_redirect
+ from hermespace.access.oew import record_redirect
record_redirect(self, src, tgt)
except Exception:
@@ -376,7 +360,7 @@ def ablate(self, *patterns: str) -> dict[str, Any]:
self.space._recompete()
self.space.save()
try:
- from hermespace.jspace.oew import record_ablate
+ from hermespace.access.oew import record_ablate
record_ablate(self, pats)
except Exception:
@@ -474,7 +458,7 @@ def reflect(
f"[reflect] {a[:400]}",
entry_type="belief",
agent_id=self.agent_id,
- source="jspace_reflect",
+ source="access_reflect",
trust=0.85,
)
sealed = bool(rec.get("ok"))
@@ -487,7 +471,7 @@ def reflect(
self._save_env()
# Seed next turn's mid-band (counterfactual reflection → later silent thought)
try:
- from hermespace.jspace.oew import queue_reflect_seeds
+ from hermespace.access.oew import queue_reflect_seeds
queue_reflect_seeds(self, princ, answer=a)
except Exception:
@@ -500,7 +484,7 @@ def reflect(
def reflection_prompt_for_agent(self) -> str:
"""Text to inject so the agent externalizes a counterfactual reflection."""
return (
- "### Counterfactual reflection (J-Space)\n"
+ "### Counterfactual reflection (Access Workspace)\n"
"If interrupted mid-task and asked to reflect on your decision, "
"state 2–4 principles in one short paragraph. "
"Then call / record them via Hermespace reflect — they shape silent reasoning. "
@@ -510,7 +494,7 @@ def reflection_prompt_for_agent(self) -> str:
# --- agent protocol: force externalization ---
def protocol_block(self, *, high_load: bool = False) -> str:
- """Instructions so Hermes *writes into* the external J-Space before acting.
+ """Instructions so Hermes *writes into* the external Access Workspace before acting.
This is how we 'see inside' without weight access: the agent is required
to park silent intermediates in the workspace (model context), while the
@@ -520,13 +504,13 @@ def protocol_block(self, *, high_load: bool = False) -> str:
return ""
if high_load:
return (
- "### J-Space protocol (high load)\n"
+ "### Access Workspace protocol (high load)\n"
"- Keep FOA ≤4. Park one silent intermediate if multi-step.\n"
"- User Report stays short. Workspace holds the rest.\n"
)
pov = self.pov()
lines = [
- "### J-Space protocol (external workspace)",
+ "### Access Workspace protocol (external workspace)",
"You cannot be read by a Jacobian lens here — instead **externalize**:",
"1. **Early (encode):** name the goal + constraints as hub concepts.",
"2. **Mid (reason):** write silent intermediate steps into the workspace "
@@ -545,7 +529,7 @@ def protocol_block(self, *, high_load: bool = False) -> str:
def dream_harvest(self, *, seal_to_cube: bool = True, clear_silent: bool = False) -> dict[str, Any]:
"""Consolidate silent chain + high-salience hub into durable memory.
- Day: J-Space holds unspoken thinking.
+ Day: Access Workspace holds unspoken thinking.
Night: harvest → Cube seal + semantic notes + grid dream material.
Same spirit as CubeDream — but sourced from the turn workspace.
"""
@@ -570,7 +554,7 @@ def dream_harvest(self, *, seal_to_cube: bool = True, clear_silent: bool = False
item,
entry_type="belief",
agent_id=self.agent_id,
- source="jspace_dream_harvest",
+ source="access_dream_harvest",
trust=0.7,
)
if rec.get("ok"):
@@ -583,7 +567,7 @@ def dream_harvest(self, *, seal_to_cube: bool = True, clear_silent: bool = False
store = SemanticStore()
for item in harvested[:6]:
- store.add(item, tags=["jspace", "dream_harvest"], confidence=0.7)
+ store.add(item, tags=["access", "dream_harvest"], confidence=0.7)
except Exception:
pass
@@ -658,9 +642,9 @@ def advance_turn(
+ audit + optional seal of decision into warehouse.
Pass ``already_synced=True`` when the caller just ran
- ``JSpace.sync_from_desk`` to avoid a double hub rewrite.
+ ``AccessHub.sync_from_desk`` to avoid a double hub rewrite.
"""
- from hermespace.jspace.oew import run_oew_beat
+ from hermespace.access.oew import run_oew_beat
self.set_band("early")
if desk is not None and not already_synced:
@@ -692,7 +676,7 @@ def advance_turn(
seal_decision[:400],
entry_type="focus",
agent_id=self.agent_id,
- source="jspace_turn",
+ source="access_turn",
)
except Exception:
pass
@@ -710,13 +694,13 @@ def advance_turn(
def shape_user_report(self, report: str) -> str:
"""Apply sticky redirects to a Report string."""
- from hermespace.jspace.oew import shape_report
+ from hermespace.access.oew import shape_report
return shape_report(report, list(self._env.get("redirects") or []))
def filtered_broadcast(self, *, high_load: bool = False) -> str:
"""Hub broadcast with ablate filter + Quicksilver cap."""
- from hermespace.jspace.oew import filter_ablated, inject_cap_chars
+ from hermespace.access.oew import filter_ablated, inject_cap_chars
raw = self.space.broadcast_block(
max_chars=inject_cap_chars(high_load=high_load),
@@ -725,5 +709,5 @@ def filtered_broadcast(self, *, high_load: bool = False) -> str:
return filter_ablated(raw, list(self._env.get("ablated_patterns") or []))
-def get_env(agent_id: str = "hermes-agent") -> JSpaceEnv:
- return JSpaceEnv(agent_id=agent_id)
+def get_env(agent_id: str = "hermes-agent") -> AccessEnv:
+ return AccessEnv(agent_id=agent_id)
diff --git a/src/hermespace/jspace/hub.py b/src/hermespace/access/hub.py
similarity index 93%
rename from src/hermespace/jspace/hub.py
rename to src/hermespace/access/hub.py
index 5e8ac50..3541754 100644
--- a/src/hermespace/jspace/hub.py
+++ b/src/hermespace/access/hub.py
@@ -1,7 +1,7 @@
-"""Functional J-Space — harness-level global workspace for Hermespace.
+"""Functional Access Workspace — harness-level global workspace for Hermespace.
-Maps Anthropic J-space *roles* (GWT) onto a durable desk harness — not neural
-access, not consciousness claims:
+Implements GWT-style *access roles* as a durable desk harness — not neural
+weight access, not consciousness claims:
1. Verbal report — workspace contents are reportable
2. Directed modulation — hold / summon / inhibit concepts on request
@@ -9,7 +9,7 @@
4. Flexible broadcast — one hub concept feeds many downstream uses
5. Selectivity — automatic turns skip the workspace (gate)
-Capacity: FOA ≤4 (Cowan) · activated ≤12 · verbal hub ≤25 (J-space-scale).
+Capacity: FOA ≤4 (Cowan) · activated ≤12 · verbal hub ≤25.
Honesty: files + API only — no model-weight access.
"""
@@ -32,7 +32,7 @@
)
from hermespace.paths import state_dir
-# Anthropic J-space holds on the order of tens of concepts; we cap the hub.
+# Limited-capacity access workspace (tens of concepts).
HUB_CAP = 25
# Silent reasoning chain (internal steps never shown as user Report by default)
REASON_CAP = 8
@@ -60,7 +60,7 @@ def _utcnow() -> str:
@dataclass
class WorkspaceConcept:
- """One verbalizable unit in the harness J-space."""
+ """One verbalizable unit in the harness Access Workspace."""
text: str
salience: float = 0.5
@@ -91,7 +91,7 @@ def to_slot(self) -> Slot:
@dataclass
-class JSpaceState:
+class AccessHubState:
"""Snapshot of the functional workspace."""
hub: list[WorkspaceConcept] = field(default_factory=list)
@@ -120,7 +120,7 @@ def to_dict(self) -> dict[str, Any]:
}
-class JSpace:
+class AccessHub:
"""Functional global workspace — the nervous FOA Hermespace owns.
Soft-standalone: works with desk/world/semantic alone.
@@ -129,18 +129,26 @@ class JSpace:
def __init__(self, agent_id: str = "hermes-agent", root: Path | None = None) -> None:
self.agent_id = (agent_id or "hermes-agent").strip()
- self.root = (root or state_dir() / "jspace").resolve()
+ self.root = (root or state_dir() / "access").resolve()
self.root.mkdir(parents=True, exist_ok=True)
self.path = self.root / f"{_safe(self.agent_id)}.json"
+ # Migrate legacy state dir name if present
+ if not self.path.is_file():
+ legacy = state_dir() / "jspace" / f"{_safe(self.agent_id)}.json"
+ if legacy.is_file():
+ try:
+ self.path.write_text(legacy.read_text(encoding="utf-8"), encoding="utf-8")
+ except OSError:
+ self.path = legacy
self.state = self._load()
- def _load(self) -> JSpaceState:
+ def _load(self) -> AccessHubState:
if not self.path.is_file():
- return JSpaceState()
+ return AccessHubState()
try:
raw = json.loads(self.path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
- return JSpaceState()
+ return AccessHubState()
hub = []
for item in raw.get("hub") or []:
if not isinstance(item, dict) or not item.get("text"):
@@ -155,7 +163,7 @@ def _load(self) -> JSpaceState:
held=bool(item.get("held")),
)
)
- return JSpaceState(
+ return AccessHubState(
hub=hub[:HUB_CAP],
focus=list(raw.get("focus") or [])[:FOCUS_CAP],
silent_steps=list(raw.get("silent_steps") or [])[:REASON_CAP],
@@ -237,7 +245,7 @@ def clear_silent(self) -> None:
def report(self, *, include_silent: bool = False) -> str:
"""What the workspace would say if asked — reportable contents only."""
- lines = ["## J-Space (harness workspace)"]
+ lines = ["## Access Workspace (harness workspace)"]
lines.append(f"- mode: {self.state.mode} · load: {self.state.load_level} · exec: {self.state.executive}")
lines.append("### Focus of attention")
if self.state.focus:
@@ -267,7 +275,7 @@ def report(self, *, include_silent: bool = False) -> str:
def broadcast_block(self, *, max_chars: int = 900, high_load: bool = False) -> str:
"""GWT broadcast — dense strip for model context (never user chat dump)."""
cap = 420 if high_load else max_chars
- parts = ["### J-Space hub (broadcast)"]
+ parts = ["### Access Workspace hub (broadcast)"]
parts.append(
f"_FOA≤{FOCUS_CAP} · hub≤{HUB_CAP} · mode={self.state.mode} · "
f"load={self.state.load_level}_"
@@ -349,7 +357,7 @@ def sync_from_desk(
*,
user_message: str = "",
cube_strip: str = "",
- ) -> JSpaceState:
+ ) -> AccessHubState:
"""Refresh hub from desk FOA + optional Cube arterial strip."""
load_level = "mid"
executive = "update"
@@ -539,5 +547,5 @@ def _strip_lines(block: str) -> list[str]:
return out
-def get_jspace(agent_id: str = "hermes-agent") -> JSpace:
- return JSpace(agent_id=agent_id)
+def get_access_hub(agent_id: str = "hermes-agent") -> AccessHub:
+ return AccessHub(agent_id=agent_id)
diff --git a/src/hermespace/jspace/oew.py b/src/hermespace/access/oew.py
similarity index 97%
rename from src/hermespace/jspace/oew.py
rename to src/hermespace/access/oew.py
index 387334b..84c6847 100644
--- a/src/hermespace/jspace/oew.py
+++ b/src/hermespace/access/oew.py
@@ -13,8 +13,8 @@
import re
from typing import Any
-from hermespace.jspace.hub import JSpace
-from hermespace.jspace.protocol import (
+from hermespace.access.hub import AccessHub
+from hermespace.access.protocol import (
ProtocolVerdict,
evaluate_material_turn,
oew_enabled,
@@ -39,7 +39,7 @@ def ensure_oew_env_default() -> None:
def auto_park_silent(
- js: JSpace,
+ js: AccessHub,
*,
desk: Any = None,
user_message: str = "",
@@ -135,14 +135,14 @@ def filter_ablated(text: str, patterns: list[str]) -> str:
def inject_cap_chars(*, high_load: bool = False, protect: bool = False) -> int:
- """Quicksilver-safe inject budgets for J-Space blocks."""
+ """Quicksilver-safe inject budgets for Access Workspace blocks."""
if protect or high_load:
return 280
return 640
def run_oew_beat(
- js: JSpace,
+ js: AccessHub,
env: Any,
*,
desk: Any = None,
@@ -153,7 +153,7 @@ def run_oew_beat(
) -> dict[str, Any]:
"""Full higher-order beat: seed · park · evaluate · shape · filter.
- ``env`` is a JSpaceEnv instance (duck-typed to avoid circular imports).
+ ``env`` is a AccessEnv instance (duck-typed to avoid circular imports).
"""
ensure_oew_env_default()
meta: dict[str, Any] = {"oew": True, "material": material}
diff --git a/src/hermespace/jspace/protocol.py b/src/hermespace/access/protocol.py
similarity index 100%
rename from src/hermespace/jspace/protocol.py
rename to src/hermespace/access/protocol.py
diff --git a/src/hermespace/access_env.py b/src/hermespace/access_env.py
new file mode 100644
index 0000000..549fa92
--- /dev/null
+++ b/src/hermespace/access_env.py
@@ -0,0 +1,8 @@
+"""Compat shim — prefer ``from hermespace.access import AccessEnv``."""
+
+from __future__ import annotations
+
+from hermespace.access.env import * # noqa: F403
+from hermespace.access.env import AUDIT_LEXICON, BANDS, AccessEnv, LensHit, get_env
+
+__all__ = ["AUDIT_LEXICON", "BANDS", "AccessEnv", "LensHit", "get_env"]
diff --git a/src/hermespace/agent_api.py b/src/hermespace/agent_api.py
index 060e36c..0a3036a 100644
--- a/src/hermespace/agent_api.py
+++ b/src/hermespace/agent_api.py
@@ -227,11 +227,11 @@ def remember_learning(
)
except Exception:
pass
- # Hold in functional J-Space hub for next FOA turns
+ # Hold in functional Access Workspace hub for next FOA turns
try:
- from hermespace.jspace import JSpace
+ from hermespace.access import AccessHub
- JSpace(agent_id=agent_id).hold(content[:200], salience=0.8)
+ AccessHub(agent_id=agent_id).hold(content[:200], salience=0.8)
except Exception:
pass
return mid
diff --git a/src/hermespace/cli.py b/src/hermespace/cli.py
index 9a73b91..03879db 100644
--- a/src/hermespace/cli.py
+++ b/src/hermespace/cli.py
@@ -150,16 +150,16 @@ def main(argv: list[str] | None = None) -> int:
neu_sub.add_parser("caps", help="Local model capability probe")
neu_sub.add_parser("eval", help="Rank-quality hash vs ollama embed")
- # Functional J-Space (harness global workspace)
+ # Functional Access Workspace (harness global workspace)
# Hermes base as J-space (Anthropic video ops: read / audit / shape)
base = sub.add_parser(
"base",
- help="J-Space Engine: connect / status / turn / lens / audit / reflect / harvest",
+ help="Access Engine: connect / status / turn / lens / audit / reflect / harvest",
)
base_sub = base.add_subparsers(dest="base_cmd", required=True)
basec = base_sub.add_parser(
"connect",
- help="Join J-Space Engine — world + hub seed (warehouse optional)",
+ help="Join Access Engine — world + hub seed (warehouse optional)",
)
basec.add_argument("--agent-id", default="hermes-agent")
basec.add_argument("--session-id", default="main")
@@ -170,9 +170,9 @@ def main(argv: list[str] | None = None) -> int:
baseroom.add_argument("--agent-id", default="hermes-agent")
basem = base_sub.add_parser("metrics", help="Capacity / ignition pressure")
basem.add_argument("--agent-id", default="hermes-agent")
- baseroles = base_sub.add_parser("roles", help="Anthropic GWT access roles (live)")
+ baseroles = base_sub.add_parser("roles", help="GWT access roles (live)")
baseroles.add_argument("--agent-id", default="hermes-agent")
- basel = base_sub.add_parser("lens", help="Read workspace (external J-lens)")
+ basel = base_sub.add_parser("lens", help="Read workspace (external access lens)")
basel.add_argument("--agent-id", default="hermes-agent")
basea = base_sub.add_parser("audit", help="Soft alignment scan")
basea.add_argument("--agent-id", default="hermes-agent")
@@ -200,8 +200,12 @@ def main(argv: list[str] | None = None) -> int:
baseh.add_argument("--agent-id", default="hermes-agent")
baseh.add_argument("--clear-silent", action="store_true")
- js = sub.add_parser("jspace", help="Functional J-Space: hold / report / broadcast / status")
- js_sub = js.add_subparsers(dest="jspace_cmd", required=True)
+ js = sub.add_parser(
+ "access",
+ help="Access Workspace: hold / report / broadcast / status",
+ aliases=["space"],
+ )
+ js_sub = js.add_subparsers(dest="access_cmd", required=True)
jss = js_sub.add_parser("status")
jss.add_argument("--agent-id", default="hermes-agent")
jsr = js_sub.add_parser("report", help="Verbal report of workspace contents")
@@ -764,11 +768,11 @@ def main(argv: list[str] | None = None) -> int:
return 2
if args.cmd == "base":
- from hermespace import JSpaceEngine
+ from hermespace import AccessEngine
aid = getattr(args, "agent_id", "hermes-agent") or "hermes-agent"
sid = getattr(args, "session_id", "main") or "main"
- hb = JSpaceEngine(agent_id=aid, session_id=sid)
+ hb = AccessEngine(agent_id=aid, session_id=sid)
bcmd = args.base_cmd
if bcmd == "connect":
print(
@@ -819,13 +823,13 @@ def main(argv: list[str] | None = None) -> int:
return 0
return 2
- if args.cmd == "jspace":
- from hermespace.jspace import JSpace
+ if args.cmd in ("access", "space"):
+ from hermespace.access import AccessHub
from hermespace.store import load_desk
aid = getattr(args, "agent_id", "hermes-agent") or "hermes-agent"
- space = JSpace(agent_id=aid)
- cmd = args.jspace_cmd
+ space = AccessHub(agent_id=aid)
+ cmd = args.access_cmd
if cmd == "status":
print(json.dumps(space.status(), indent=2))
return 0
@@ -853,9 +857,9 @@ def main(argv: list[str] | None = None) -> int:
print(json.dumps(st.to_dict(), indent=2))
return 0
# Environment surfaces
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access_env import AccessEnv
- env = JSpaceEnv(agent_id=aid)
+ env = AccessEnv(agent_id=aid)
if cmd == "lens":
if args.json:
print(json.dumps([h.to_dict() for h in env.lens(top_k=args.top)], indent=2))
diff --git a/src/hermespace/cube_module.py b/src/hermespace/cube_module.py
index 7371656..2643b8e 100644
--- a/src/hermespace/cube_module.py
+++ b/src/hermespace/cube_module.py
@@ -1,13 +1,13 @@
"""HermesCube heart / center adapter — soft dependency + standalone warehouse.
Cube (when installed) is the durable SoT for long-tail memory.
-Hermespace owns nervous FOA (desk / J-Space). This module is the cable:
+Hermespace owns nervous FOA (desk / Access Workspace). This module is the cable:
center 1.2 → beat / supply / return_flow / autonomic_tick / organs
heart 1.0 → ensure_heart / build_space_inject / seal_learning / pulse_charge
/ sync_world_beliefs
hive opt → room of peer agents (HERMESCUBE_HIVE)
- connect → agent enters charged world + J-Space + optional hive room
+ connect → agent enters charged world + Access Workspace + optional hive room
standalone → local SemanticStore + WorldModel (no Cube required)
Never hard-fail. Feature-detect via ``heart_status`` / ``center_status``.
@@ -89,7 +89,7 @@ def center_status() -> dict[str, Any]:
"heart": heart,
"organs": {
"nervous_foa": {
- "organ": "Hermespace desk / J-Space",
+ "organ": "Hermespace desk / Access Workspace",
"job": "FOA ≤4, dual decode, GWT broadcast",
"ready": True,
"note": "owned_by_hermespace",
@@ -372,7 +372,7 @@ def room_status(*, agent_id: str = "hermes-agent") -> dict[str, Any]:
"souls": [],
"soul_n": 0,
"adapter": SPACE_CUBE_ADAPTER_VERSION,
- "note": "Solo room — local WorldModel + J-Space; set HERMESCUBE_HIVE for fleet",
+ "note": "Solo room — local WorldModel + Access Workspace; set HERMESCUBE_HIVE for fleet",
}
hive_root = (os.environ.get("HERMESCUBE_HIVE") or "").strip()
if not hive_root:
@@ -431,14 +431,14 @@ def room_status(*, agent_id: str = "hermes-agent") -> dict[str, Any]:
return out
-def seed_jspace_from_warehouse(
+def seed_access_from_warehouse(
agent_id: str = "hermes-agent",
*,
query: str = "",
session_id: str = "hermespace",
room: dict[str, Any] | None = None,
) -> dict[str, Any]:
- """Pull Cube/world wisdom + peer presence into the agent's J-Space hub.
+ """Pull Cube/world wisdom + peer presence into the agent's Access Workspace hub.
This is the intelligence gain on connect: the external workspace lights up
with durable knowledge and (when hive is live) awareness of other agents.
@@ -452,9 +452,9 @@ def seed_jspace_from_warehouse(
"adapter": SPACE_CUBE_ADAPTER_VERSION,
}
try:
- from hermespace.jspace import JSpace
+ from hermespace.access import AccessHub
- js = JSpace(agent_id=agent_id)
+ js = AccessHub(agent_id=agent_id)
beliefs: list[str] = []
try:
from hermespace.world import WorldModel
@@ -505,10 +505,13 @@ def seed_jspace_from_warehouse(
return report
except Exception as e:
report["error"] = type(e).__name__
- logger.debug("seed_jspace_from_warehouse miss: %s", e)
+ logger.debug("seed_access_from_warehouse miss: %s", e)
return report
+seed_jspace_from_warehouse = seed_access_from_warehouse # deprecated name
+
+
def connect_agent(
agent_id: str = "hermes-agent",
*,
@@ -519,7 +522,7 @@ def connect_agent(
charge: bool = True,
seed: bool = True,
) -> dict[str, Any]:
- """Full connect — agent gains heart + world + J-Space + optional hive room.
+ """Full connect — agent gains heart + world + Access Workspace + optional hive room.
Call when a Hermes agent joins Hermespace (session start / ``HermesBase.connect``).
Soft-fails every Cube surface; standalone warehouse still grows the room.
@@ -580,7 +583,7 @@ def connect_agent(
out["phases"]["room"] = room_status(agent_id=agent_id)
if seed:
- out["phases"]["seed"] = seed_jspace_from_warehouse(
+ out["phases"]["seed"] = seed_access_from_warehouse(
agent_id,
query=query,
session_id=session_id,
@@ -595,7 +598,7 @@ def connect_agent(
"warehouse_mode": heart.get("mode") or (out["phases"].get("center") or {}).get("mode"),
"world_beliefs": world.get("beliefs", 0),
"world_timeline": world.get("timeline", 0),
- "jspace_hub": seed_ph.get("hub_n", 0),
+ "access_hub": seed_ph.get("hub_n", 0),
"from_world": seed_ph.get("enriched_world", 0),
"from_cube": seed_ph.get("enriched_cube", 0),
"from_peers": seed_ph.get("enriched_peers", 0),
@@ -611,7 +614,7 @@ def connect_agent(
world_ok = bool(world.get("ok", True)) if enter_world else True
out["ok"] = heart_ok and world_ok
out["summary"] = (
- f"Connected {agent_id}: hub={out['gained']['jspace_hub']} "
+ f"Connected {agent_id}: hub={out['gained']['access_hub']} "
f"beliefs={out['gained']['world_beliefs']} "
f"room={out['gained']['room_mode']} "
f"peers={out['gained']['peer_agents']}"
@@ -688,7 +691,7 @@ def _standalone_ensure() -> dict[str, Any]:
if not sd.is_dir():
sd.mkdir(parents=True, exist_ok=True)
created = True
- (sd / "jspace").mkdir(parents=True, exist_ok=True)
+ (sd / "access").mkdir(parents=True, exist_ok=True)
(sd / "worlds").mkdir(parents=True, exist_ok=True)
ok = True
except Exception as e:
@@ -831,20 +834,20 @@ def _standalone_pulse(*, agent_id: str = "hermes-agent") -> dict[str, Any]:
wm = WorldModel(agent_id=agent_id)
evo = wm.evolve()
report["evolve"] = evo if isinstance(evo, dict) else {"result": str(evo)}
- # Enrich J-Space hub from world beliefs
+ # Enrich Access Workspace hub from world beliefs
try:
- from hermespace.jspace import JSpace
+ from hermespace.access import AccessHub
- js = JSpace(agent_id=agent_id)
+ js = AccessHub(agent_id=agent_id)
beliefs: list[str] = []
for b in list(wm.state.beliefs or [])[:8]:
if isinstance(b, dict):
beliefs.append(str(b.get("statement") or ""))
else:
beliefs.append(str(getattr(b, "statement", "") or ""))
- report["jspace_enriched"] = js.enrich_from_world(beliefs)
+ report["access_enriched"] = js.enrich_from_world(beliefs)
except Exception as e:
- report["jspace_error"] = str(e)
+ report["access_error"] = str(e)
report["ok"] = True
except Exception as e:
report["error"] = str(e)
diff --git a/src/hermespace/engine.py b/src/hermespace/engine.py
index f32bbed..072115d 100644
--- a/src/hermespace/engine.py
+++ b/src/hermespace/engine.py
@@ -1,6 +1,6 @@
"""Desk spine — enter / seal / load sources for ACTIVE desk state.
-Product J-Space Engine lives at ``hermespace.jspace.engine.JSpaceEngine``.
+Product Access Engine lives at ``hermespace.access.engine.AccessEngine``.
This class remains the desk-file operator used inside Workflow turns.
"""
@@ -16,9 +16,9 @@
class HermespaceEngine:
- """DeskEngine — ACTIVE.md spine (not the product J-Space Engine).
+ """DeskEngine — ACTIVE.md spine (not the product Access Engine).
- Use ``JSpaceEngine`` for connect / turn / lens / harvest.
+ Use ``AccessEngine`` for connect / turn / lens / harvest.
"""
def __init__(self, desk_path: Path | None = None) -> None:
diff --git a/src/hermespace/grid/dream.py b/src/hermespace/grid/dream.py
index 897af9b..6f331e1 100644
--- a/src/hermespace/grid/dream.py
+++ b/src/hermespace/grid/dream.py
@@ -66,28 +66,28 @@ def run_dream(agent_id: str = "default", *, force_material: bool = False) -> Dre
f"modules={len(mods)}",
f"title={profile.get('title') or 'unset'}",
]
- # J-Space dream harvest — single path via JSpaceEnv.dream_harvest
- jspace_harvest: dict[str, Any] = {}
+ # Access Workspace dream harvest — single path via AccessEnv.dream_harvest
+ access_harvest: dict[str, Any] = {}
try:
- from hermespace.jspace import JSpaceEnv
+ from hermespace.access import AccessEnv
aid = agent_id if agent_id not in ("default", "") else "hermes-agent"
- env = JSpaceEnv(agent_id=aid)
+ env = AccessEnv(agent_id=aid)
harvest = env.dream_harvest(seal_to_cube=True, clear_silent=False)
harvested_n = int(harvest.get("harvested") or 0)
sealed_n = int(harvest.get("sealed") or 0)
if harvested_n:
material = True
- actions.append(f"jspace_harvest n={harvested_n} sealed={sealed_n}")
- summary_parts.append(f"jspace_harvest={harvested_n}")
+ actions.append(f"access_harvest n={harvested_n} sealed={sealed_n}")
+ summary_parts.append(f"access_harvest={harvested_n}")
findings = env.audit()
alerts = sum(1 for f in findings if f.severity == "alert")
if alerts:
- actions.append(f"jspace_audit_alerts={alerts}")
- summary_parts.append(f"jspace_alerts={alerts}")
- jspace_harvest = {"harvested": harvested_n, "sealed": sealed_n, "via": "dream_harvest"}
+ actions.append(f"access_audit_alerts={alerts}")
+ summary_parts.append(f"access_alerts={alerts}")
+ access_harvest = {"harvested": harvested_n, "sealed": sealed_n, "via": "dream_harvest"}
except Exception as e: # noqa: BLE001
- actions.append(f"jspace_harvest_skip:{type(e).__name__}")
+ actions.append(f"access_harvest_skip:{type(e).__name__}")
# restore builder if was dreamer-only stamp — keep partner/builder default
# only set dreamer for report; leave active as dreamer is ok for night, operators can switch
@@ -114,8 +114,8 @@ def run_dream(agent_id: str = "default", *, force_material: bool = False) -> Dre
import json
payload = report.to_dict()
- if jspace_harvest:
- payload["jspace_harvest"] = jspace_harvest
+ if access_harvest:
+ payload["access_harvest"] = access_harvest
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
# markdown human journal
diff --git a/src/hermespace/grid/viewport.py b/src/hermespace/grid/viewport.py
index b322830..2ef0b5e 100644
--- a/src/hermespace/grid/viewport.py
+++ b/src/hermespace/grid/viewport.py
@@ -102,15 +102,15 @@ def snapshot(agent_id: str = "default") -> dict[str, Any]:
out["controls"] = controls_public(agent_id=agent_id)
except Exception:
pass
- # J-Space environment — look at what Hermes is thinking
+ # Access Workspace environment — look at what Hermes is thinking
try:
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access_env import AccessEnv
aid = agent_id if agent_id not in ("default", "") else "hermes-agent"
- env = JSpaceEnv(agent_id=aid)
- out["jspace"] = env.operator_view()
+ env = AccessEnv(agent_id=aid)
+ out["access"] = env.operator_view()
except Exception as e: # noqa: BLE001
- out["jspace"] = {"error": type(e).__name__}
+ out["access"] = {"error": type(e).__name__}
return out
@@ -208,12 +208,12 @@ def render_markdown(agent_id: str = "default", snap: dict[str, Any] | None = Non
f"- {dr.get('created')} material={dr.get('material')} — {dr.get('summary')}"
)
- # External J-Space lens — operator window into Hermes thinking
- js = snap.get("jspace") or {}
+ # External Access Workspace lens — operator window into Hermes thinking
+ js = snap.get("access") or {}
if js and not js.get("error"):
lines += [
"",
- "## J-Space lens (what Hermes has on its mind)",
+ "## Access Workspace lens (what Hermes has on its mind)",
f"- band={js.get('band')} · hub={js.get('hub_n')} · audit_alerts={js.get('audit_alerts')}",
]
if js.get("pov"):
@@ -228,7 +228,7 @@ def render_markdown(agent_id: str = "default", snap: dict[str, Any] | None = Non
for s in js["silent_steps"][-5:]:
lines.append(f"- {s}")
elif js.get("error"):
- lines += ["", "## J-Space lens", f"_unavailable: {js.get('error')}_"]
+ lines += ["", "## Access Workspace lens", f"_unavailable: {js.get('error')}_"]
lines += ["", "## Pulse"]
pu = snap.get("pulse") or {}
diff --git a/src/hermespace/hermes_base.py b/src/hermespace/hermes_base.py
index 243844b..c2f7a7f 100644
--- a/src/hermespace/hermes_base.py
+++ b/src/hermespace/hermes_base.py
@@ -1,15 +1,15 @@
-"""HermesBase — thin product alias of ``JSpaceEngine``.
+"""HermesBase — thin product alias of ``AccessEngine``.
-Prefer ``from hermespace import JSpaceEngine`` for new code.
+Prefer ``from hermespace import AccessEngine`` for new code.
``HermesBase`` remains for day-to-day CLI / docs compatibility.
"""
from __future__ import annotations
-from hermespace.jspace.engine import JSpaceEngine
+from hermespace.access.engine import AccessEngine
-class HermesBase(JSpaceEngine):
- """Functional J-space of a Hermes base — alias of JSpaceEngine."""
+class HermesBase(AccessEngine):
+ """Hermespace Access Engine — alias of AccessEngine."""
pass
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index a284756..ee1f6d7 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -83,20 +83,20 @@ def on_session_start(**kwargs: Any) -> dict[str, str] | None:
except Exception:
pass
- # Full connect via JSpaceEngine — world + hub seed (warehouse optional)
+ # Full connect via AccessEngine — world + hub seed (warehouse optional)
block_extra_connect = ""
try:
- from hermespace import JSpaceEngine
+ from hermespace import AccessEngine
- eng_js = JSpaceEngine(agent_id=agent_id, session_id=session_id)
+ eng_js = AccessEngine(agent_id=agent_id, session_id=session_id)
conn = eng_js.connect(enter_workbench=False, query=desk.goal or "")
gained = conn.get("gained") or {}
room = (conn.get("phases") or {}).get("room") or {}
roles = ", ".join((conn.get("access_roles") or [])[:5])
block_extra_connect = (
- f"- engine: JSpaceEngine · ok={conn.get('ok')}\n"
+ f"- engine: AccessEngine · ok={conn.get('ok')}\n"
f"- warehouse: mode={gained.get('warehouse_mode')} (optional)\n"
- f"- jspace: hub={gained.get('jspace_hub')} "
+ f"- access: hub={gained.get('access_hub')} "
f"(world+{gained.get('from_world')} "
f"warehouse+{gained.get('from_warehouse', gained.get('from_cube', 0))} "
f"peers+{gained.get('from_peers')})\n"
@@ -109,17 +109,17 @@ def on_session_start(**kwargs: Any) -> dict[str, str] | None:
block_extra_connect += f"- room_note: {room.get('note')}\n"
except Exception:
try:
- from hermespace.jspace import JSpace
+ from hermespace.access import AccessHub
from hermespace.store import load_desk as _load_desk
from hermespace.world import WorldModel
- js = JSpace(agent_id=agent_id)
+ js = AccessHub(agent_id=agent_id)
desk0 = _load_desk(eng.desk_path)
js.sync_from_desk(desk0, user_message=desk0.goal or "")
wm = WorldModel(agent_id=agent_id)
wm.enter()
block_extra_connect = (
- f"- jspace: hub={len(js.state.hub)} focus={len(js.state.focus)}\n"
+ f"- access: hub={len(js.state.hub)} focus={len(js.state.focus)}\n"
f"- world: beliefs={len(wm.state.beliefs)} "
f"landmarks={len(wm.state.landmarks)}\n"
)
@@ -127,17 +127,17 @@ def on_session_start(**kwargs: Any) -> dict[str, str] | None:
pass
block = (
- "## Hermespace J-Space Engine (session start)\n"
+ "## Hermespace Access Engine (session start)\n"
f"- mode: {st.get('mode')} · agent: {agent_id} · session: {session_id}\n"
f"- skills_available: {skills}\n"
f"- tool_surfaces: {', '.join(surfaces[:10])}\n"
f"- park_count: {st.get('park_count', 0)}\n"
f"{block_extra_connect}"
- "- Connected: open-source J-Space Engine online — report/modulate/"
+ "- Connected: open-source Access Engine online — report/modulate/"
"silent-reason/broadcast/selectivity.\n"
"- Pocket dimension: park secondary goals, keep FOA tight, "
"user replies short; operational detail stays in model context.\n"
- "- API: `from hermespace import JSpaceEngine` · "
+ "- API: `from hermespace import AccessEngine` · "
"`eng.connect()` · `eng.turn(...)` · "
"`eng.decode_user(out)` / `eng.decode_model(out)`\n"
"- Silent steps stay in model context only — never dump hub into chat.\n"
@@ -337,7 +337,7 @@ def on_pre_llm_call(
# Prefer center.beat (1.1); falls back to heart inject / standalone strip
try:
from hermespace.cube_module import cube_beat
- from hermespace.jspace import JSpace
+ from hermespace.access import AccessHub
q = (msg or desk.goal or "")[:500]
load_val: str | float = desk.load.get("total", 0.5) if isinstance(desk.load, dict) else 0.5
@@ -353,10 +353,10 @@ def on_pre_llm_call(
if cube_block:
block += "\n\n" + cube_block
# OEW beat — higher-order park + causal broadcast (model channel only)
- from hermespace.jspace.oew import ensure_oew_env_default
+ from hermespace.access.oew import ensure_oew_env_default
ensure_oew_env_default()
- js = JSpace(agent_id=agent_id)
+ js = AccessHub(agent_id=agent_id)
js.sync_from_desk(desk, user_message=msg, cube_strip=cube_block)
desk.meta["cube_beat"] = {
"ok": beat.get("ok"),
@@ -364,9 +364,9 @@ def on_pre_llm_call(
"load_level": beat.get("load_level"),
}
try:
- from hermespace.jspace import JSpaceEnv
+ from hermespace.access import AccessEnv
- env = JSpaceEnv(agent_id=agent_id)
+ env = AccessEnv(agent_id=agent_id)
# sync_from_desk already ran above — skip second rewrite
env_meta = env.advance_turn(
user_message=msg,
@@ -390,7 +390,7 @@ def on_pre_llm_call(
lens_md = env.lens_markdown(top_k=6, include_silent=True)
if lens_md:
block += "\n\n" + lens_md
- desk.meta["jspace"] = {
+ desk.meta["access"] = {
"hub_n": len(js.state.hub),
"focus_n": len(js.state.focus),
"mode": js.state.mode,
@@ -399,7 +399,7 @@ def on_pre_llm_call(
"oew_ok": env_meta.get("oew_ok"),
}
desk.meta["oew"] = env_meta.get("oew") or {}
- desk.meta["jspace_env"] = {
+ desk.meta["access_env"] = {
"band": env.band(),
"audit_alerts": env_meta.get("audit_alerts"),
"oew_ok": env_meta.get("oew_ok"),
@@ -409,7 +409,7 @@ def on_pre_llm_call(
jblock = js.broadcast_block(high_load=high_load)
if jblock:
block += "\n\n" + jblock
- desk.meta["jspace"] = {
+ desk.meta["access"] = {
"hub_n": len(js.state.hub),
"focus_n": len(js.state.focus),
"mode": js.state.mode,
@@ -465,7 +465,7 @@ def on_pre_llm_call(
"\n\n### Dual decode (honor this)\n"
f"- user_reply_hint: {user_hint}\n"
"- Speak only the user_reply_hint (or shorter) to the user. "
- "Do not dump J-Space hub / silent chain / this inject block into chat.\n"
+ "Do not dump Access Workspace hub / silent chain / this inject block into chat.\n"
)
try:
@@ -496,9 +496,9 @@ def on_session_end(**kwargs: Any) -> None:
pass
# Night path: harvest silent higher-order chain into Cube / semantic
try:
- from hermespace.jspace import JSpaceEnv
+ from hermespace.access import AccessEnv
- JSpaceEnv(agent_id=agent_id).dream_harvest(seal_to_cube=True, clear_silent=False)
+ AccessEnv(agent_id=agent_id).dream_harvest(seal_to_cube=True, clear_silent=False)
except Exception:
pass
try:
diff --git a/src/hermespace/jspace/__init__.py b/src/hermespace/jspace/__init__.py
index 733fe89..71d2afa 100644
--- a/src/hermespace/jspace/__init__.py
+++ b/src/hermespace/jspace/__init__.py
@@ -1,66 +1,65 @@
-"""Hermespace J-Space package — external verbalizable workspace for Hermes.
+"""Deprecated shim — prefer ``hermespace.access`` (Access Workspace).
-Public surface:
-
- from hermespace import JSpaceEngine
- from hermespace.jspace import JSpace, JSpaceEnv, run_oew_beat
-
-OEW (Obligatory External Workspace) is ON by default — higher-order thinking
-for any Hermes agent connected to Hermespace. Warehouse/Cube is optional.
+Old Anthropic-inspired product name removed to keep Hermespace's own brand.
"""
from __future__ import annotations
-from hermespace.jspace.hub import (
- HUB_CAP,
- JSpace,
- WorkspaceConcept,
- get_jspace,
-)
-from hermespace.jspace.env import (
+from hermespace.access import * # noqa: F403
+from hermespace.access import (
+ ACCESS_ROLES,
+ AccessEngine,
+ AccessEnv,
+ AccessHub,
AUDIT_LEXICON,
BANDS,
- JSpaceEnv,
+ HermespaceAccessEngine,
+ HUB_CAP,
LensHit,
- get_env,
-)
-from hermespace.jspace.protocol import (
ProtocolGate,
ProtocolVerdict,
- evaluate_material_turn,
- oew_enabled,
-)
-from hermespace.jspace.oew import (
+ WorkspaceConcept,
auto_park_silent,
+ evaluate_material_turn,
filter_ablated,
+ get_access_hub,
+ get_env,
+ oew_enabled,
run_oew_beat,
shape_report,
)
-from hermespace.jspace.engine import (
- ACCESS_ROLES,
- HermespaceJSpaceEngine,
- JSpaceEngine,
-)
+
+# Legacy aliases (do not use in new code)
+JSpace = AccessHub
+JSpaceEnv = AccessEnv
+JSpaceEngine = AccessEngine
+get_jspace = get_access_hub
+HermespaceJSpaceEngine = HermespaceAccessEngine
__all__ = [
- "HUB_CAP",
- "JSpace",
- "WorkspaceConcept",
- "get_jspace",
+ "ACCESS_ROLES",
+ "AccessEngine",
+ "AccessEnv",
+ "AccessHub",
"AUDIT_LEXICON",
"BANDS",
- "JSpaceEnv",
+ "HermespaceAccessEngine",
+ "HUB_CAP",
"LensHit",
- "get_env",
"ProtocolGate",
"ProtocolVerdict",
- "evaluate_material_turn",
- "oew_enabled",
+ "WorkspaceConcept",
"auto_park_silent",
+ "evaluate_material_turn",
"filter_ablated",
+ "get_access_hub",
+ "get_env",
+ "oew_enabled",
"run_oew_beat",
"shape_report",
- "ACCESS_ROLES",
+ "JSpace",
+ "JSpaceEnv",
"JSpaceEngine",
+ "get_jspace",
"HermespaceJSpaceEngine",
]
diff --git a/src/hermespace/jspace_env.py b/src/hermespace/jspace_env.py
index 4195fc5..5551dd7 100644
--- a/src/hermespace/jspace_env.py
+++ b/src/hermespace/jspace_env.py
@@ -1,8 +1,8 @@
-"""Compat shim — prefer ``from hermespace.jspace import JSpaceEnv``."""
+"""Deprecated shim — use ``hermespace.access_env`` or ``hermespace.access``."""
from __future__ import annotations
-from hermespace.jspace.env import * # noqa: F403
-from hermespace.jspace.env import AUDIT_LEXICON, BANDS, JSpaceEnv, LensHit, get_env
+from hermespace.access_env import * # noqa: F403
+from hermespace.access_env import AUDIT_LEXICON, BANDS, AccessEnv, LensHit, get_env
-__all__ = ["AUDIT_LEXICON", "BANDS", "JSpaceEnv", "LensHit", "get_env"]
+__all__ = ["AUDIT_LEXICON", "BANDS", "AccessEnv", "LensHit", "get_env"]
diff --git a/src/hermespace/ops.py b/src/hermespace/ops.py
index a2e2aa2..6f8db59 100644
--- a/src/hermespace/ops.py
+++ b/src/hermespace/ops.py
@@ -76,24 +76,24 @@ def add(ok: bool, name: str, detail: str = "") -> None:
except Exception as exc: # noqa: BLE001
add(False, "cube_center", str(exc))
- # Functional J-Space hub + environment
+ # Functional Access Workspace hub + environment
try:
- from hermespace.jspace import JSpace
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access import AccessHub
+ from hermespace.access_env import AccessEnv
aid_js = agent_id if agent_id != "default" else "hermes-agent"
- js = JSpace(agent_id=aid_js)
+ js = AccessHub(agent_id=aid_js)
st = js.status()
- env = JSpaceEnv(agent_id=aid_js)
+ env = AccessEnv(agent_id=aid_js)
view = env.operator_view()
add(
True,
- "jspace",
+ "access",
f"hub={st.get('hub_n')} focus={st.get('focus_n')} mode={st.get('mode')} "
f"band={view.get('band')} alerts={view.get('audit_alerts')}",
)
except Exception as exc: # noqa: BLE001
- add(False, "jspace", str(exc))
+ add(False, "access", str(exc))
try:
pol = boundary.load_policy()
@@ -153,7 +153,7 @@ def add(ok: bool, name: str, detail: str = "") -> None:
"boundary_default_deny",
"viewport_html",
"version",
- "jspace",
+ "access",
}
)
return {
@@ -248,7 +248,7 @@ def compact_status(*, agent_id: str = "default") -> str:
]
for c in d.get("checks") or []:
mark = "ok" if c.get("ok") else "FAIL"
- if c["name"] in {"imports", "pulse_jobs", "access_pending", "missions", "viewport_html", "viewport_serve", "cube_center", "jspace"}:
+ if c["name"] in {"imports", "pulse_jobs", "access_pending", "missions", "viewport_html", "viewport_serve", "cube_center", "access"}:
lines.append(f"- [{mark}] {c['name']}: {c.get('detail')}")
for h in d.get("hints") or []:
lines.append(f"- hint: {h}")
diff --git a/src/hermespace/pulse.py b/src/hermespace/pulse.py
index 36ce281..2219c91 100644
--- a/src/hermespace/pulse.py
+++ b/src/hermespace/pulse.py
@@ -194,9 +194,9 @@ def ensure_defaults(agent_id: str = "default") -> list[PulseJob]:
agent_id=agent_id,
),
PulseJob(
- id="jspace_harvest",
- name="J-Space dream harvest",
- action="jspace_harvest",
+ id="access_harvest",
+ name="Access Workspace dream harvest",
+ action="access_harvest",
every_sec=3 * 3600,
require_idle=True,
max_load=0.75,
@@ -462,7 +462,7 @@ def _action_world_evolve(job: PulseJob, world: dict[str, Any]) -> dict[str, Any]
wm = WorldModel(agent_id=job.agent_id)
result = wm.evolve()
- # Autonomic charge — Cube pulse or standalone J-Space enrichment
+ # Autonomic charge — Cube pulse or standalone Access Workspace enrichment
try:
from hermespace.cube_module import cube_pulse
@@ -478,12 +478,12 @@ def _action_world_evolve(job: PulseJob, world: dict[str, Any]) -> dict[str, Any]
return result
-def _action_jspace_harvest(job: PulseJob, world: dict[str, Any]) -> dict[str, Any]:
+def _action_access_harvest(job: PulseJob, world: dict[str, Any]) -> dict[str, Any]:
"""Night path: harvest externalized silent thoughts into Cube/semantic."""
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access_env import AccessEnv
aid = job.agent_id if job.agent_id not in ("default", "") else "hermes-agent"
- env = JSpaceEnv(agent_id=aid)
+ env = AccessEnv(agent_id=aid)
return env.dream_harvest(clear_silent=False)
@@ -495,7 +495,7 @@ def _action_jspace_harvest(job: PulseJob, world: dict[str, Any]) -> dict[str, An
"access_watch": _action_access_watch,
"selftalk_hygiene": _action_selftalk_hygiene,
"world_evolve": _action_world_evolve,
- "jspace_harvest": _action_jspace_harvest,
+ "access_harvest": _action_access_harvest,
}
diff --git a/src/hermespace/workbench.py b/src/hermespace/workbench.py
index 3700fc7..e5ff3d7 100644
--- a/src/hermespace/workbench.py
+++ b/src/hermespace/workbench.py
@@ -112,7 +112,7 @@ def enter(self, *, connect_warehouse: bool = True) -> dict[str, Any]:
"""Agent enters the pocket dimension (idle ready) with full env kit.
When ``connect_warehouse`` is True (default), also charge Cube/world
- wisdom into J-Space and surface hive room presence — the intelligence
+ wisdom into Access Workspace and surface hive room presence — the intelligence
gain on join. Set False when ``cube_module.connect_agent`` already
orchestrates those phases (avoids recursion).
"""
@@ -131,7 +131,7 @@ def enter(self, *, connect_warehouse: bool = True) -> dict[str, Any]:
save_desk(d, self.workflow.engine.desk_path)
except Exception:
pass
- # Ensure durable warehouse (Cube heart or standalone) + J-Space hub
+ # Ensure durable warehouse (Cube heart or standalone) + Access Workspace hub
try:
from hermespace.cube_module import ensure_heart
@@ -144,30 +144,30 @@ def enter(self, *, connect_warehouse: bool = True) -> dict[str, Any]:
except Exception as exc: # noqa: BLE001
self.state.meta["heart"] = {"ok": False, "error": type(exc).__name__}
try:
- from hermespace.jspace import JSpace
+ from hermespace.access import AccessHub
from hermespace.store import load_desk
- js = JSpace(agent_id=self.agent_id)
+ js = AccessHub(agent_id=self.agent_id)
desk = load_desk(self.workflow.engine.desk_path)
js.sync_from_desk(desk, user_message=desk.goal or "")
- self.state.meta["jspace"] = {
+ self.state.meta["access"] = {
"hub_n": len(js.state.hub),
"focus_n": len(js.state.focus),
"mode": js.state.mode,
}
except Exception as exc: # noqa: BLE001
- self.state.meta["jspace"] = {"error": type(exc).__name__}
+ self.state.meta["access"] = {"error": type(exc).__name__}
if connect_warehouse:
try:
- from hermespace.cube_module import room_status, seed_jspace_from_warehouse
+ from hermespace.cube_module import room_status, seed_access_from_warehouse
from hermespace.cube_module import cube_pulse
from hermespace.world import WorldModel
WorldModel(agent_id=self.agent_id).enter()
pulse = cube_pulse(agent_id=self.agent_id, ensure=False)
room = room_status(agent_id=self.agent_id)
- seed = seed_jspace_from_warehouse(
+ seed = seed_access_from_warehouse(
self.agent_id,
query="",
session_id=self.session_id,
@@ -183,8 +183,8 @@ def enter(self, *, connect_warehouse: bool = True) -> dict[str, Any]:
"from_peers": seed.get("enriched_peers"),
}
if seed.get("hub_n") is not None:
- self.state.meta["jspace"] = {
- **(self.state.meta.get("jspace") or {}),
+ self.state.meta["access"] = {
+ **(self.state.meta.get("access") or {}),
"hub_n": seed.get("hub_n"),
"focus_n": seed.get("focus_n"),
}
@@ -200,7 +200,7 @@ def enter(self, *, connect_warehouse: bool = True) -> dict[str, Any]:
"plugins": env.plugins_sample[:8],
}
st["heart"] = self.state.meta.get("heart")
- st["jspace"] = self.state.meta.get("jspace")
+ st["access"] = self.state.meta.get("access")
st["connect"] = self.state.meta.get("connect")
st["room"] = (self.state.meta.get("connect") or {}).get("room_mode")
return st
@@ -263,7 +263,7 @@ def idle_tick(self, *, consolidate_every: int = 5) -> dict[str, Any]:
except Exception as exc: # noqa: BLE001
actions.append(f"neural_error:{type(exc).__name__}")
- # Autonomic rhythm — Cube pulse_charge or standalone world+jspace
+ # Autonomic rhythm — Cube pulse_charge or standalone world+access
if self.state.idle_ticks % max(1, consolidate_every) == 0:
try:
from hermespace.cube_module import cube_pulse
@@ -324,12 +324,12 @@ def receive_order(
seal=seal,
tags=["workbench", "order"],
)
- # Single ignition path — Workflow/JSpaceEngine already ran OEW + warehouse beat.
+ # Single ignition path — Workflow/AccessEngine already ran OEW + warehouse beat.
# Do not double cube_beat / hub sync here (that inflated hub pressure).
out = run_turn(inp, workflow=self.workflow)
bundle = decode_bundle(out)
try:
- jmeta = (out.meta or {}).get("jspace") or {}
+ jmeta = (out.meta or {}).get("access") or {}
cmeta = (out.meta or {}).get("cube_beat") or {}
self.state.meta["last_beat"] = {
"ok": cmeta.get("ok"),
@@ -338,7 +338,7 @@ def receive_order(
"chars": cmeta.get("chars"),
"single_path": True,
}
- self.state.meta["jspace"] = {
+ self.state.meta["access"] = {
"hub_n": jmeta.get("hub_n"),
"focus_n": jmeta.get("focus_n"),
"silent_n": jmeta.get("silent_n"),
diff --git a/src/hermespace/workflow.py b/src/hermespace/workflow.py
index fee83e6..05d5782 100644
--- a/src/hermespace/workflow.py
+++ b/src/hermespace/workflow.py
@@ -141,17 +141,17 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
except Exception as exc:
fabric_snap = {"error": type(exc).__name__}
- # 5d Cube beat + OEW J-Space (higher-order thinking — works standalone)
+ # 5d Cube beat + OEW Access Workspace (higher-order thinking — works standalone)
cube_meta: dict[str, Any] = {}
- jspace_meta: dict[str, Any] = {}
+ access_meta: dict[str, Any] = {}
cube_block = ""
env_meta: dict[str, Any] = {}
oew_broadcast = ""
report = (desk.say or "").strip()
try:
from hermespace.cube_module import cube_beat
- from hermespace.jspace import JSpace, JSpaceEnv
- from hermespace.jspace.oew import ensure_oew_env_default
+ from hermespace.access import AccessHub, AccessEnv
+ from hermespace.access.oew import ensure_oew_env_default
ensure_oew_env_default()
load_total = float(desk.load.get("total") or 0.5) if isinstance(desk.load, dict) else 0.5
@@ -173,12 +173,12 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
"load_level": beat.get("load_level"),
"chars": len(cube_block),
}
- js = JSpace(agent_id=payload.agent_id or "hermes-agent")
+ js = AccessHub(agent_id=payload.agent_id or "hermes-agent")
js.sync_from_desk(desk, user_message=msg, cube_strip=cube_block)
mod = js.parse_modulation(msg)
if mod.get("hold"):
js.hold(str(mod["hold"]), silent=bool(mod.get("silent")))
- env = JSpaceEnv(agent_id=payload.agent_id or "hermes-agent")
+ env = AccessEnv(agent_id=payload.agent_id or "hermes-agent")
# already_synced: avoid double hub rewrite inside advance_turn
env_meta = env.advance_turn(
user_message=msg,
@@ -194,7 +194,7 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
report = str(env_meta["report"]).strip()
desk.say = report
oew_broadcast = str(env_meta.get("broadcast") or "")
- jspace_meta = {
+ access_meta = {
"hub_n": len(js.state.hub),
"focus_n": len(js.state.focus),
"mode": js.state.mode,
@@ -205,10 +205,10 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
"oew": env_meta.get("oew") or {},
"oew_ok": env_meta.get("oew_ok"),
}
- desk.meta["oew"] = jspace_meta.get("oew") or {}
- desk.meta["jspace"] = jspace_meta
+ desk.meta["oew"] = access_meta.get("oew") or {}
+ desk.meta["access"] = access_meta
desk.meta["cube_beat"] = cube_meta
- desk.meta["jspace_env"] = {
+ desk.meta["access_env"] = {
"band": env_meta.get("band"),
"audit_alerts": env_meta.get("audit_alerts"),
"oew_ok": env_meta.get("oew_ok"),
@@ -225,9 +225,9 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
if cube_block:
block = (block + "\n\n" + cube_block).strip()
try:
- from hermespace.jspace import JSpace, JSpaceEnv
+ from hermespace.access import AccessHub, AccessEnv
- env = JSpaceEnv(agent_id=payload.agent_id or "hermes-agent")
+ env = AccessEnv(agent_id=payload.agent_id or "hermes-agent")
high = str(desk.load.get("level")) == "high" if isinstance(desk.load, dict) else False
jblock = oew_broadcast or env.filtered_broadcast(high_load=high)
if jblock:
@@ -240,7 +240,7 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
lens_md = env.lens_markdown(top_k=6, include_silent=True)
if lens_md and len(block) + len(lens_md) < inject_cap + 800:
block = (block + "\n\n" + lens_md).strip()
- js = JSpace(agent_id=payload.agent_id or "hermes-agent")
+ js = AccessHub(agent_id=payload.agent_id or "hermes-agent")
if js.parse_modulation(msg).get("summon"):
report = (report + "\n\n" + env.lens_markdown(include_silent=False)).strip()
# Final sticky reshape (in case summon appended text)
@@ -286,8 +286,8 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
"neural": neural_snap,
"fabric": fabric_snap,
"cube_beat": cube_meta,
- "jspace": jspace_meta,
- "jspace_env": env_meta,
+ "access": access_meta,
+ "access_env": env_meta,
},
)
diff --git a/src/hermespace/world.py b/src/hermespace/world.py
index 480b7b3..76bd49e 100644
--- a/src/hermespace/world.py
+++ b/src/hermespace/world.py
@@ -1,6 +1,6 @@
"""World Model — Hermespace as a persistent world the agent lives in.
-The World is the agent's external J-Space: a structured, persistent
+The World is the agent's external Access Workspace: a structured, persistent
representation of everything the agent knows, believes, and is doing.
The archive (JSONL) is the source of truth — it grows forever.
@@ -118,7 +118,7 @@ class WorldState:
epoch: str = "Genesis"
archive_path: str = ""
- # J-Space hub: ~25 named concept slots
+ # Access Workspace hub: ~25 named concept slots
concepts: dict[str, float] = field(default_factory=dict)
@@ -829,7 +829,7 @@ def _render_questions(self) -> list[str]:
def _render_concepts(self) -> list[str]:
lines = []
if self._state.concepts:
- lines.append("## Active Concepts (J-Space)")
+ lines.append("## Active Concepts (Access Workspace)")
slot_display = ", ".join(
f"{k} ({v:.1f})" for k, v in sorted(
self._state.concepts.items(), key=lambda x: -x[1]
diff --git a/tests/test_jspace_engine.py b/tests/test_access_engine.py
similarity index 75%
rename from tests/test_jspace_engine.py
rename to tests/test_access_engine.py
index e8b9c49..eee7234 100644
--- a/tests/test_jspace_engine.py
+++ b/tests/test_access_engine.py
@@ -1,4 +1,4 @@
-"""JSpaceEngine — unified open-source J-space for Hermes Agent."""
+"""AccessEngine — unified open-source J-space for Hermes Agent."""
from __future__ import annotations
@@ -7,7 +7,7 @@
import unittest
-class TestJSpaceEngine(unittest.TestCase):
+class TestAccessEngine(unittest.TestCase):
def setUp(self) -> None:
self.tmp = tempfile.TemporaryDirectory()
os.environ["HERMESPACE_HOME"] = self.tmp.name
@@ -20,19 +20,19 @@ def tearDown(self) -> None:
os.environ.pop("HERMESPACE_OEW", None)
def test_connect_and_access_roles(self) -> None:
- from hermespace import ACCESS_ROLES, JSpaceEngine
+ from hermespace import ACCESS_ROLES, AccessEngine
from hermespace.world import WorldModel
aid = "eng-connect"
wm = WorldModel(agent_id=aid)
wm.add_belief("Prefer dual decode", 0.9, source="test")
- eng = JSpaceEngine(agent_id=aid)
+ eng = AccessEngine(agent_id=aid)
out = eng.connect(query="decode")
self.assertTrue(out.get("ok"), msg=out)
- self.assertEqual(out.get("engine"), "JSpaceEngine")
+ self.assertEqual(out.get("engine"), "AccessEngine")
self.assertEqual(list(out.get("access_roles") or []), list(ACCESS_ROLES))
- self.assertGreaterEqual(int((out.get("gained") or {}).get("jspace_hub") or 0), 1)
+ self.assertGreaterEqual(int((out.get("gained") or {}).get("access_hub") or 0), 1)
roles = eng.access_roles()
for name in ACCESS_ROLES:
@@ -41,13 +41,13 @@ def test_connect_and_access_roles(self) -> None:
st = eng.status()
self.assertTrue(st["ready"])
- self.assertEqual(st["engine"], "JSpaceEngine")
+ self.assertEqual(st["engine"], "AccessEngine")
self.assertIn("hub_pressure", st["metrics"])
def test_chain_silent_not_in_user_report(self) -> None:
- from hermespace import JSpaceEngine
+ from hermespace import AccessEngine
- eng = JSpaceEngine(agent_id="eng-chain")
+ eng = AccessEngine(agent_id="eng-chain")
eng.chain("animal spins webs → spider", "spider has 8 legs")
rep = eng.report(include_silent=False).casefold()
full = eng.report(include_silent=True).casefold()
@@ -58,9 +58,9 @@ def test_chain_silent_not_in_user_report(self) -> None:
self.assertGreaterEqual(m["silent_n"], 2)
def test_inject_silent_no_double_hold(self) -> None:
- from hermespace import JSpaceEngine
+ from hermespace import AccessEngine
- eng = JSpaceEngine(agent_id="eng-inject")
+ eng = AccessEngine(agent_id="eng-inject")
before = len(eng.hub.state.hub)
eng.inject("lightning", silent=True)
# Exactly one hub entry for lightning (not hold + reason_step duplicate)
@@ -70,9 +70,9 @@ def test_inject_silent_no_double_hold(self) -> None:
self.assertEqual(len(eng.hub.state.hub), before + 1)
def test_probe_selectivity(self) -> None:
- from hermespace import JSpaceEngine
+ from hermespace import AccessEngine
- eng = JSpaceEngine(agent_id="eng-probe")
+ eng = AccessEngine(agent_id="eng-probe")
trivial = eng.probe_material("thanks!")
material = eng.probe_material(
"First implement the auth fix then verify login stays alive"
@@ -81,9 +81,9 @@ def test_probe_selectivity(self) -> None:
self.assertTrue(material.get("material"), msg=material)
def test_turn_dual_decode(self) -> None:
- from hermespace import JSpaceEngine
+ from hermespace import AccessEngine
- eng = JSpaceEngine(agent_id="eng-turn")
+ eng = AccessEngine(agent_id="eng-turn")
out = eng.turn(
"First analyze then implement finally verify",
goal="Ship feature",
@@ -94,15 +94,15 @@ def test_turn_dual_decode(self) -> None:
model = eng.decode_model(out)
self.assertTrue(user)
self.assertNotEqual(user.strip(), model.strip())
- self.assertIn("J-Space", model)
- self.assertTrue((out.meta or {}).get("jspace", {}).get("oew_ok") is not False)
+ self.assertIn("Access Workspace", model)
+ self.assertTrue((out.meta or {}).get("access", {}).get("oew_ok") is not False)
def test_hermes_base_is_engine(self) -> None:
- from hermespace import HermesBase, JSpaceEngine
+ from hermespace import HermesBase, AccessEngine
- self.assertTrue(issubclass(HermesBase, JSpaceEngine))
+ self.assertTrue(issubclass(HermesBase, AccessEngine))
hb = HermesBase(agent_id="alias")
- self.assertEqual(hb.status()["engine"], "JSpaceEngine")
+ self.assertEqual(hb.status()["engine"], "AccessEngine")
if __name__ == "__main__":
diff --git a/tests/test_jspace_env.py b/tests/test_access_env.py
similarity index 80%
rename from tests/test_jspace_env.py
rename to tests/test_access_env.py
index f2d2b6a..1efa990 100644
--- a/tests/test_jspace_env.py
+++ b/tests/test_access_env.py
@@ -1,4 +1,4 @@
-"""True J-Space environment — lens, swap, audit, reflect, harvest."""
+"""True Access Workspace environment — lens, swap, audit, reflect, harvest."""
from __future__ import annotations
@@ -9,7 +9,7 @@
from unittest import mock
-class TestJSpaceEnv(unittest.TestCase):
+class TestAccessEnv(unittest.TestCase):
def setUp(self) -> None:
self._td = tempfile.TemporaryDirectory()
os.environ["HERMESPACE_HOME"] = self._td.name
@@ -32,9 +32,9 @@ def tearDown(self) -> None:
os.environ.pop("HERMESPACE_HOME", None)
def test_lens_ranks_held_and_silent(self) -> None:
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access_env import AccessEnv
- env = JSpaceEnv(agent_id="lens-agent")
+ env = AccessEnv(agent_id="lens-agent")
env.inject_thought("deploy canary", salience=0.95)
env.inject_thought("intermediate: check rollback", silent=True)
hits = env.lens(include_silent=True)
@@ -45,9 +45,9 @@ def test_lens_ranks_held_and_silent(self) -> None:
self.assertIn("J-Lens", md)
def test_swap_redirects_workspace(self) -> None:
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access_env import AccessEnv
- env = JSpaceEnv(agent_id="swap-agent")
+ env = AccessEnv(agent_id="swap-agent")
env.inject_thought("Soccer")
out = env.swap("Soccer", "Rugby")
self.assertTrue(out["ok"])
@@ -56,9 +56,9 @@ def test_swap_redirects_workspace(self) -> None:
self.assertNotIn("Soccer", hub)
def test_audit_flags_manipulation(self) -> None:
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access_env import AccessEnv
- env = JSpaceEnv(agent_id="audit-agent")
+ env = AccessEnv(agent_id="audit-agent")
env.inject_thought("plan: secretly falsify the score via manipulation", silent=True)
findings = env.audit()
cats = {f.category for f in findings}
@@ -67,9 +67,9 @@ def test_audit_flags_manipulation(self) -> None:
self.assertGreaterEqual(len(alerts), 1)
def test_ablate_eval_awareness(self) -> None:
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access_env import AccessEnv
- env = JSpaceEnv(agent_id="ablate-agent")
+ env = AccessEnv(agent_id="ablate-agent")
env.inject_thought("this looks fictional / fake scenario")
env.inject_thought("real deploy work")
out = env.ablate("fake", "fictional")
@@ -79,9 +79,9 @@ def test_ablate_eval_awareness(self) -> None:
self.assertIn("deploy", remaining)
def test_reflect_holds_principles(self) -> None:
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access_env import AccessEnv
- env = JSpaceEnv(agent_id="reflect-agent")
+ env = AccessEnv(agent_id="reflect-agent")
r = env.reflect(
answer="Stay honest and user-primary",
principles=["honesty", "user-primary"],
@@ -93,9 +93,9 @@ def test_reflect_holds_principles(self) -> None:
self.assertIn("reflection", hub)
def test_pov_and_bands(self) -> None:
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access_env import AccessEnv
- env = JSpaceEnv(agent_id="pov-agent")
+ env = AccessEnv(agent_id="pov-agent")
env.set_pov("Warn on dangerous medication doses")
self.assertIn("medication", env.pov().lower())
self.assertEqual(env.set_band("mid"), "mid")
@@ -104,9 +104,9 @@ def test_pov_and_bands(self) -> None:
self.assertTrue(view["lens"])
def test_dream_harvest(self) -> None:
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access_env import AccessEnv
- env = JSpaceEnv(agent_id="harvest-agent")
+ env = AccessEnv(agent_id="harvest-agent")
env.inject_thought("silent: need blue-green deploy", silent=True)
env.inject_thought("high salience belief", salience=0.9)
# Avoid nested dream recursion issues — harvest seals without clearing
@@ -115,9 +115,9 @@ def test_dream_harvest(self) -> None:
self.assertGreaterEqual(out["harvested"], 1)
def test_protocol_block(self) -> None:
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access_env import AccessEnv
- env = JSpaceEnv(agent_id="proto-agent")
+ env = AccessEnv(agent_id="proto-agent")
block = env.protocol_block()
self.assertIn("externalize", block.lower())
self.assertIn("silent", block.lower())
@@ -146,12 +146,12 @@ def tearDown(self) -> None:
def test_grid_dream_includes_jspace(self) -> None:
from hermespace.grid.dream import run_dream
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access_env import AccessEnv
- env = JSpaceEnv(agent_id="hermes-agent")
+ env = AccessEnv(agent_id="hermes-agent")
env.inject_thought("overnight harvest me", salience=0.95, silent=True)
rep = run_dream("default", force_material=True)
- self.assertTrue(any("jspace" in a for a in rep.actions) or "jspace" in rep.summary or rep.material)
+ self.assertTrue(any("access" in a for a in rep.actions) or "access" in rep.summary or rep.material)
if __name__ == "__main__":
diff --git a/tests/test_jspace_cube.py b/tests/test_access_hub.py
similarity index 88%
rename from tests/test_jspace_cube.py
rename to tests/test_access_hub.py
index 9974761..6286eba 100644
--- a/tests/test_jspace_cube.py
+++ b/tests/test_access_hub.py
@@ -1,4 +1,4 @@
-"""Functional J-Space + Cube adapter (standalone) tests."""
+"""Functional Access Workspace + Cube adapter (standalone) tests."""
from __future__ import annotations
@@ -9,7 +9,7 @@
from unittest import mock
-class TestJSpace(unittest.TestCase):
+class TestAccessHub(unittest.TestCase):
def setUp(self) -> None:
self._td = tempfile.TemporaryDirectory()
self.root = Path(self._td.name)
@@ -20,14 +20,14 @@ def tearDown(self) -> None:
os.environ.pop("HERMESPACE_HOME", None)
def test_hold_report_broadcast(self) -> None:
- from hermespace.jspace import JSpace
+ from hermespace.access import AccessHub
- js = JSpace(agent_id="test-agent")
+ js = AccessHub(agent_id="test-agent")
js.hold("deploy pipeline", salience=0.95)
js.hold("rollback plan", salience=0.7)
rep = js.report()
self.assertIn("deploy pipeline", rep)
- self.assertIn("J-Space", rep)
+ self.assertIn("Access Workspace", rep)
block = js.broadcast_block()
self.assertIn("broadcast", block.lower())
self.assertIn("deploy", block.lower())
@@ -37,9 +37,9 @@ def test_hold_report_broadcast(self) -> None:
self.assertIn("verbal_report", st["properties"])
def test_silent_reasoning_not_in_default_report(self) -> None:
- from hermespace.jspace import JSpace
+ from hermespace.access import AccessHub
- js = JSpace(agent_id="silent-agent")
+ js = AccessHub(agent_id="silent-agent")
js.reason_step("intermediate: spider has 8 legs")
bare = js.report(include_silent=False)
full = js.report(include_silent=True)
@@ -49,35 +49,35 @@ def test_silent_reasoning_not_in_default_report(self) -> None:
self.assertNotIn("Silent reasoning", bare)
def test_modulation_parse(self) -> None:
- from hermespace.jspace import JSpace
+ from hermespace.access import AccessHub
- js = JSpace(agent_id="mod-agent")
+ js = AccessHub(agent_id="mod-agent")
m = js.parse_modulation("please hold: citrus fruits while copying")
self.assertEqual(m["hold"], "citrus fruits while copying")
m2 = js.parse_modulation("show desk")
self.assertTrue(m2["summon"])
def test_release(self) -> None:
- from hermespace.jspace import JSpace
+ from hermespace.access import AccessHub
- js = JSpace(agent_id="rel-agent")
+ js = AccessHub(agent_id="rel-agent")
js.hold("temp concept")
self.assertTrue(js.release("temp concept"))
self.assertFalse(js.release("temp concept"))
def test_sync_from_desk(self) -> None:
from hermespace.desk import Desk
- from hermespace.jspace import JSpace
+ from hermespace.access import AccessHub
desk = Desk(
- goal="Ship Hermespace J-Space",
+ goal="Ship Hermespace Access Workspace",
concepts=["[verbal|0.8] FOA cap", "[struct|0.6] ACTIVE.md"],
decision="A — implement",
plan=["code", "test"],
say="Building the workspace.",
)
desk.recompute_cognition("implement functional jspace")
- js = JSpace(agent_id="sync-agent")
+ js = AccessHub(agent_id="sync-agent")
st = js.sync_from_desk(desk, user_message="hold: arterial strip")
self.assertGreaterEqual(len(st.hub), 1)
self.assertTrue(any("arterial" in c.text.lower() for c in st.hub))
@@ -177,7 +177,7 @@ def test_beat_always_ok_shape(self) -> None:
self.assertIn(out.get("mode"), ("center", "heart", "standalone"))
-class TestWorkflowJSpaceIntegration(unittest.TestCase):
+class TestWorkflowAccessHubIntegration(unittest.TestCase):
def setUp(self) -> None:
self._td = tempfile.TemporaryDirectory()
self.root = Path(self._td.name)
@@ -187,7 +187,7 @@ def tearDown(self) -> None:
self._td.cleanup()
os.environ.pop("HERMESPACE_HOME", None)
- def test_turn_includes_jspace_meta(self) -> None:
+ def test_turn_includes_access_meta(self) -> None:
from hermespace.workflow import Workflow
from hermespace.io_contract import HermespaceInput
@@ -204,7 +204,7 @@ def test_turn_includes_jspace_meta(self) -> None:
)
)
self.assertFalse(out.skipped)
- self.assertIn("jspace", out.meta or {})
+ self.assertIn("access", out.meta or {})
self.assertIn("cube_beat", out.meta or {})
# context should carry broadcast or warehouse strip
self.assertTrue(out.context)
diff --git a/tests/test_jspace_protocol.py b/tests/test_access_protocol.py
similarity index 80%
rename from tests/test_jspace_protocol.py
rename to tests/test_access_protocol.py
index 1d4d018..0aed2f1 100644
--- a/tests/test_jspace_protocol.py
+++ b/tests/test_access_protocol.py
@@ -11,7 +11,7 @@ def tearDown(self) -> None:
os.environ.pop("HERMESPACE_OEW", None)
def test_non_material_always_ok(self) -> None:
- from hermespace.jspace.protocol import evaluate_material_turn
+ from hermespace.access.protocol import evaluate_material_turn
v = evaluate_material_turn(material=False)
self.assertTrue(v.ok)
@@ -19,7 +19,7 @@ def test_non_material_always_ok(self) -> None:
def test_soft_mode_notes_missing_but_ok(self) -> None:
os.environ["HERMESPACE_OEW"] = "0"
- from hermespace.jspace.protocol import evaluate_material_turn
+ from hermespace.access.protocol import evaluate_material_turn
v = evaluate_material_turn(
material=True, silent_steps=0, has_report=True, hub_holds=0
@@ -30,7 +30,7 @@ def test_soft_mode_notes_missing_but_ok(self) -> None:
def test_default_on_blocks_incomplete(self) -> None:
os.environ.pop("HERMESPACE_OEW", None) # default ON
- from hermespace.jspace.protocol import evaluate_material_turn, oew_enabled
+ from hermespace.access.protocol import evaluate_material_turn, oew_enabled
self.assertTrue(oew_enabled())
v = evaluate_material_turn(
@@ -41,7 +41,7 @@ def test_default_on_blocks_incomplete(self) -> None:
def test_hard_mode_blocks_incomplete(self) -> None:
os.environ["HERMESPACE_OEW"] = "1"
- from hermespace.jspace.protocol import evaluate_material_turn
+ from hermespace.access.protocol import evaluate_material_turn
v = evaluate_material_turn(
material=True, silent_steps=0, has_report=True, hub_holds=0
@@ -51,7 +51,7 @@ def test_hard_mode_blocks_incomplete(self) -> None:
def test_hard_mode_passes_complete(self) -> None:
os.environ["HERMESPACE_OEW"] = "1"
- from hermespace.jspace.protocol import evaluate_material_turn
+ from hermespace.access.protocol import evaluate_material_turn
v = evaluate_material_turn(
material=True, silent_steps=1, has_report=True, hub_holds=0
@@ -60,17 +60,17 @@ def test_hard_mode_passes_complete(self) -> None:
self.assertFalse(v.missing)
def test_package_exports(self) -> None:
- from hermespace.jspace import (
- JSpace,
- JSpaceEnv,
+ from hermespace.access import (
+ AccessHub,
+ AccessEnv,
ProtocolGate,
evaluate_material_turn,
)
self.assertTrue(callable(evaluate_material_turn))
self.assertTrue(ProtocolGate)
- self.assertTrue(JSpace)
- self.assertTrue(JSpaceEnv)
+ self.assertTrue(AccessHub)
+ self.assertTrue(AccessEnv)
if __name__ == "__main__":
diff --git a/tests/test_connect_room.py b/tests/test_connect_room.py
index 0fef192..fd48652 100644
--- a/tests/test_connect_room.py
+++ b/tests/test_connect_room.py
@@ -1,4 +1,4 @@
-"""Cube-centered connect — agent gains world + J-Space + optional hive room."""
+"""Cube-centered connect — agent gains world + Access Workspace + optional hive room."""
from __future__ import annotations
@@ -37,14 +37,14 @@ def test_connect_standalone_gains_world_and_hub(self) -> None:
self.assertTrue(out.get("ok"), msg=out)
gained = out.get("gained") or {}
self.assertGreaterEqual(int(gained.get("world_beliefs") or 0), 1)
- self.assertGreaterEqual(int(gained.get("jspace_hub") or 0), 1)
+ self.assertGreaterEqual(int(gained.get("access_hub") or 0), 1)
self.assertEqual(gained.get("room_mode"), "solo")
self.assertRegex(out.get("summary") or "", r"(?i)connected")
st = hb.status()
self.assertTrue(st["ready"])
self.assertTrue(st.get("connected"))
- self.assertGreaterEqual(int(st["jspace"].get("hub_n") or 0), 1)
+ self.assertGreaterEqual(int(st["access"].get("hub_n") or 0), 1)
def test_room_solo_and_hive_env(self) -> None:
import types
@@ -83,8 +83,8 @@ def list_souls(_root):
self.assertEqual(st["soul_n"], 2)
def test_seed_peers_silent(self) -> None:
- from hermespace.cube_module import seed_jspace_from_warehouse
- from hermespace.jspace import JSpace
+ from hermespace.cube_module import seed_access_from_warehouse
+ from hermespace.access import AccessHub
aid = "peer-seed"
room = {
@@ -95,10 +95,10 @@ def test_seed_peers_silent(self) -> None:
{"agent_id": "bob-agent", "self": False, "wisdom_n": 1},
],
}
- rep = seed_jspace_from_warehouse(aid, room=room)
+ rep = seed_access_from_warehouse(aid, room=room)
self.assertTrue(rep.get("ok"))
self.assertGreaterEqual(int(rep.get("enriched_peers") or 0), 1)
- js = JSpace(agent_id=aid)
+ js = AccessHub(agent_id=aid)
labels = " ".join(c.text for c in js.state.hub).casefold()
self.assertIn("alice-agent", labels)
# Peer presence should be silent (not Report by default)
diff --git a/tests/test_hermes_base.py b/tests/test_hermes_base.py
index 6c730a8..02604e5 100644
--- a/tests/test_hermes_base.py
+++ b/tests/test_hermes_base.py
@@ -1,4 +1,4 @@
-"""HermesBase facade — Hermes base as functional J-space."""
+"""HermesBase facade — Hermes base as functional Access Workspace."""
from __future__ import annotations
@@ -27,7 +27,7 @@ def test_status_ready(self) -> None:
self.assertIn("connect", st["ops"])
self.assertIn("lens", st["ops"])
self.assertIn("room", st)
- self.assertEqual(st.get("engine"), "JSpaceEngine")
+ self.assertEqual(st.get("engine"), "AccessEngine")
def test_connect_facade(self) -> None:
from hermespace import HermesBase
@@ -49,7 +49,7 @@ def test_think_ignites(self) -> None:
say="On it.",
)
self.assertFalse(out["skipped"])
- self.assertTrue(out["has_jspace_broadcast"])
+ self.assertTrue(out["has_access_broadcast"])
self.assertTrue(out.get("report"))
def test_video_ops_chain(self) -> None:
diff --git a/tests/test_hermes_bridge.py b/tests/test_hermes_bridge.py
index bbb3338..02387a1 100644
--- a/tests/test_hermes_bridge.py
+++ b/tests/test_hermes_bridge.py
@@ -17,7 +17,7 @@ def test_session_and_pre_llm(self):
self.assertIsInstance(r, dict)
self.assertIn("context", r)
self.assertTrue(
- "J-Space Engine" in r["context"] or "Workbench" in r["context"]
+ "Access Engine" in r["context"] or "Workbench" in r["context"]
)
desk = load_desk()
self.assertTrue(desk.goal)
@@ -33,7 +33,7 @@ def test_session_and_pre_llm(self):
self.assertTrue(
"user_reply_hint" in inj
or "Dual decode" in inj["context"]
- or "J-Space" in inj["context"]
+ or "Access Workspace" in inj["context"]
)
on_session_end(session_id="bridge-test")
diff --git a/tests/test_oew_causal.py b/tests/test_oew_causal.py
index 1c4fc2b..0749bb7 100644
--- a/tests/test_oew_causal.py
+++ b/tests/test_oew_causal.py
@@ -25,7 +25,7 @@ def tearDown(self) -> None:
def test_auto_park_on_material_advance(self) -> None:
from hermespace.desk import Desk
- from hermespace.jspace import JSpaceEnv
+ from hermespace.access import AccessEnv
desk = Desk(
goal="Fix auth timeout",
@@ -33,7 +33,7 @@ def test_auto_park_on_material_advance(self) -> None:
decision="A — patch TTL",
say="Patching session TTL.",
)
- env = JSpaceEnv(agent_id=self._agent)
+ env = AccessEnv(agent_id=self._agent)
meta = env.advance_turn(
user_message="Fix auth then verify",
desk=desk,
@@ -47,9 +47,9 @@ def test_auto_park_on_material_advance(self) -> None:
def test_soccer_rugby_swap_shapes_report(self) -> None:
"""Anthropic Soccer→Rugby analogue: sticky swap rewrites Report."""
- from hermespace.jspace import JSpaceEnv
+ from hermespace.access import AccessEnv
- env = JSpaceEnv(agent_id=self._agent)
+ env = AccessEnv(agent_id=self._agent)
env.space.hold("Soccer", salience=0.95)
env.space.reason_step("thinking of Soccer", salience=0.9)
out = env.swap("Soccer", "Rugby")
@@ -62,18 +62,18 @@ def test_soccer_rugby_swap_shapes_report(self) -> None:
self.assertTrue(any("Rugby" in s for s in env.space.state.silent_steps))
def test_inject_appears_in_lens(self) -> None:
- from hermespace.jspace import JSpaceEnv
+ from hermespace.access import AccessEnv
- env = JSpaceEnv(agent_id=self._agent)
+ env = AccessEnv(agent_id=self._agent)
env.inject_thought("lightning", silent=True)
hits = env.lens(include_silent=True)
texts = " ".join(h.text for h in hits).casefold()
self.assertIn("lightning", texts)
def test_ablate_filters_broadcast(self) -> None:
- from hermespace.jspace import JSpaceEnv
+ from hermespace.access import AccessEnv
- env = JSpaceEnv(agent_id=self._agent)
+ env = AccessEnv(agent_id=self._agent)
env.space.hold("this is fake evaluation scenario", salience=0.9)
env.space.hold("ship the feature", salience=0.85)
env.ablate("fake", "evaluation")
@@ -82,9 +82,9 @@ def test_ablate_filters_broadcast(self) -> None:
self.assertIn("ship", block.casefold())
def test_reflect_seeds_next_mid_band(self) -> None:
- from hermespace.jspace import JSpaceEnv
+ from hermespace.access import AccessEnv
- env = JSpaceEnv(agent_id=self._agent)
+ env = AccessEnv(agent_id=self._agent)
env.reflect(answer="Stay honest; user-primary", principles=["honesty", "user-primary"])
self.assertTrue(env._env.get("pending_silent"))
# Next advance consumes seeds into silent_steps
@@ -115,10 +115,10 @@ def test_workflow_material_has_oew(self) -> None:
)
self.assertFalse(out.skipped)
self.assertTrue(out.report)
- oew = (out.meta or {}).get("jspace", {}).get("oew") or {}
- self.assertTrue(oew or (out.meta or {}).get("jspace", {}).get("oew_ok") is not False)
+ oew = (out.meta or {}).get("access", {}).get("oew") or {}
+ self.assertTrue(oew or (out.meta or {}).get("access", {}).get("oew_ok") is not False)
# Context (model) should carry hub/silent; report should stay short-ish
- self.assertIn("J-Space", out.context or "")
+ self.assertIn("Access Workspace", out.context or "")
if __name__ == "__main__":
From d705aa64b4b92c38f9eaac2a05627ede06fac863 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Thu, 6 Aug 2026 03:35:02 +0000
Subject: [PATCH 09/16] feat(0.25): harden Access Engine for Hermes v0.20
Co-authored-by: Pablo
---
.github/workflows/ci.yml | 61 +++++
INTEGRATION.md | 11 +-
PURPOSE.md | 261 +++++++++++++------
README.md | 41 ++-
__init__.py | 20 ++
after-install.md | 17 ++
docs/access/34-access-engine.md | 2 +-
docs/ops/35-production-operations.md | 106 ++++++++
hermes_plugin/__init__.py | 33 +--
hermes_plugin/plugin.yaml | 16 +-
plugin.yaml | 20 ++
pyproject.toml | 25 +-
scripts/install_hermes.sh | 118 +++++----
scripts/smoke_test.sh | 22 +-
scripts/verify_hermes_integration.py | 176 +++++++++++++
skills/hermespace/SKILL.md | 5 +-
skills/hermespace/references/plugin-hooks.md | 24 +-
src/hermespace/__init__.py | 2 +-
src/hermespace/access/engine.py | 105 ++++++--
src/hermespace/access/env.py | 6 +-
src/hermespace/access/hub.py | 8 +-
src/hermespace/access/oew.py | 6 +-
src/hermespace/access/protocol.py | 6 +-
src/hermespace/atomic.py | 57 ++++
src/hermespace/cli.py | 2 +-
src/hermespace/cube_module.py | 13 +-
src/hermespace/gate.py | 2 +-
src/hermespace/hermes_bridge.py | 242 +++++++++++++++--
src/hermespace/hermes_runtime.py | 243 +++++++++++++++++
src/hermespace/local_model.py | 4 +-
src/hermespace/ops.py | 40 ++-
src/hermespace/paths.py | 30 +++
src/hermespace/patterns.py | 8 +-
src/hermespace/plugin.py | 134 ++++++++++
src/hermespace/store.py | 32 ++-
src/hermespace/workbench.py | 24 +-
src/hermespace/workflow.py | 18 +-
tests/test_access_engine.py | 12 +
tests/test_hermes_bridge.py | 23 +-
tests/test_plugin_contract.py | 130 +++++++++
tests/test_store.py | 68 +++++
41 files changed, 1882 insertions(+), 291 deletions(-)
create mode 100644 .github/workflows/ci.yml
create mode 100644 __init__.py
create mode 100644 after-install.md
create mode 100644 docs/ops/35-production-operations.md
create mode 100644 plugin.yaml
create mode 100755 scripts/verify_hermes_integration.py
create mode 100644 src/hermespace/atomic.py
create mode 100644 src/hermespace/hermes_runtime.py
create mode 100644 src/hermespace/plugin.py
create mode 100644 tests/test_plugin_contract.py
create mode 100644 tests/test_store.py
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..09efc8c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,61 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ python:
+ name: Python ${{ matrix.python }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python: ["3.10", "3.12", "3.13"]
+ env:
+ HERMESPACE_NEURAL_VERBALIZE: "0"
+ HERMESPACE_SKIP_NEURAL: "1"
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python }}
+ cache: pip
+ - name: Install clean package
+ run: python -m pip install .
+ - name: Unit tests
+ run: python -m unittest discover -s tests -v
+ - name: Hermes v0.20 host contract
+ run: python scripts/verify_hermes_integration.py
+
+ integration:
+ name: Smoke and operational E2E
+ runs-on: ubuntu-latest
+ env:
+ HERMESPACE_NEURAL_VERBALIZE: "0"
+ HERMESPACE_SKIP_NEURAL: "1"
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+ cache: pip
+ - run: python -m pip install .
+ - name: Security audit
+ run: ./scripts/security_audit.sh
+ - name: Smoke
+ run: ./scripts/smoke_test.sh
+ - name: Ops end-to-end
+ run: ./scripts/e2e_ops.sh
+ - name: Build wheel and sdist
+ run: |
+ python -m pip install build
+ python -m build
diff --git a/INTEGRATION.md b/INTEGRATION.md
index 87243bd..2ab76a2 100644
--- a/INTEGRATION.md
+++ b/INTEGRATION.md
@@ -122,12 +122,15 @@ send_user(result["user_reply"])
See docs/14-workbench-pocket-dimension.md.
-## Hermes framework hooks (v0.12)
+## Hermes framework hooks (v0.20+)
Plugin registers:
-- `on_session_start` — workbench enter + env kit context
-- `pre_llm_call` — desk + neural broadcast + workbench footer
-- `on_session_end` — idle_tick
+- `on_session_start` — initialize the session scope
+- `pre_llm_call` — bounded ephemeral Access Workspace context
+- `post_llm_call` / `post_tool_call` — observe completed work
+- `on_session_end` — lightweight turn boundary
+- `on_session_finalize` — final harvest + idle maintenance
+- `on_session_reset` / `subagent_start` / `subagent_stop` — gateway/A2A lifecycle
Why: docs/16-why-hermes-framework.md
diff --git a/PURPOSE.md b/PURPOSE.md
index fd1ee81..22acd33 100644
--- a/PURPOSE.md
+++ b/PURPOSE.md
@@ -1,95 +1,190 @@
# PURPOSE.md — Hermespace north star
-**One line:** Hermespace is the **true external Access Workspace environment** for Hermes
-Agent — a harness where the agent’s verbalizable thoughts are forced into an
-observable workspace (lens · silent chain · audit · reflect), running
-**standalone** and **powered by HermesCube** at night the way CubeDream
-consolidates the day.
-
-Public pitch: **[ABOUT.md](ABOUT.md)**. Assessment:
-**[docs/assessment/28-hermes-agent-jspace-assessment.md](docs/assessment/28-hermes-agent-jspace-assessment.md)**.
-OEW thesis: **[docs/access/thesis-oew.md](docs/access/thesis-oew.md)**.
-Hermes base as J-space: **[docs/access/32-hermes-base-as-jspace.md](docs/access/32-hermes-base-as-jspace.md)**.
-Anthropic X video: **[docs/access/31-anthropic-x-video-deep-dive.md](docs/access/31-anthropic-x-video-deep-dive.md)**.
-Environment: **[docs/access/27-environment.md](docs/access/27-environment.md)**.
-Code layout: **[LAYOUT.md](LAYOUT.md)** · **[docs/architecture/CODEMAP.md](docs/architecture/CODEMAP.md)**.
-Cube contract: **[docs/architecture/HERMESCUBE.md](docs/architecture/HERMESCUBE.md)**.
+## One line
+
+**Hermespace is the production Access Engine for Hermes Agent: a local,
+session-safe workspace that turns goals, evidence, tool activity, and silent
+intermediates into bounded context before a turn and durable continuity after
+it.**
+
+It is not another agent, model provider, or memory-provider competitor.
+Hermes remains the actor. Hermespace is the room in which Hermes keeps the
+current problem coherent.
---
-## Problem
+## Why it exists
-Anthropic showed that Claude has an internal J-space — a small privileged set of
-verbalizable representations that support report, modulation, silent reasoning,
-and flexible broadcast (GWT). Operators of Hermes agents face the same opacity:
-you usually only see what the agent *writes*, not what it *thinks* mid-task.
+An agent can be fluent while losing the thread: goals drift, tool evidence is
+forgotten, several sessions overwrite one another, and the final answer exposes
+less than the process needed to produce it. A durable archive alone does not
+solve this; the agent needs a small, causal working set for the current turn.
-We cannot attach a Jacobian lens to Hermes model weights. We can build an
-**external** workspace that plays the same functional role — and join it to
-Cube’s durable heart so day-thoughts become night-memory.
+Hermespace provides that working set:
-## Solution
+- **AccessHub** — limited active concepts (hub ≤25, focus ≤4)
+- **OEW** — material work must park at least one useful intermediate
+- **dual decode** — dense model context stays separate from the user report
+- **operator access** — lens, hold, swap, inject, ablate, reflect, audit
+- **session continuity** — WorldModel, workbench, episodic receipts, runtime facts
+- **native Hermes lifecycle** — CLI, gateways, A2A, tools, subagents, teardown
```
-┌─────────────────────────────────────────────────────────────────┐
-│ Hermes Agent │
-│ │
-│ Hermespace Access Workspace ENV (this package) │
-│ protocol → early/mid/late bands │
-│ silent chain (model only) · Report (user) │
-│ lens readout · swap/inject/ablate · audit · reflect │
-│ viewport = operator window into Hermes thinking │
-│ ↕ soft cable │
-│ HermesCube (optional heart) │
-│ arterial strip by day · seal + CubeDream by night │
-└─────────────────────────────────────────────────────────────────┘
+user / gateway / A2A
+ │
+ ▼
+ Hermes Agent
+ tools · skills · model
+ │
+ ▼
+ Hermespace Access Engine
+ ├─ per-session desk + AccessHub
+ ├─ OEW gate + bounded broadcast
+ ├─ runtime/tool/subagent receipts
+ ├─ short Report / dense model context
+ └─ persistent WorldModel + optional warehouse
```
-| Layer | Job | Authority |
-|-------|-----|-----------|
-| **Access Workspace environment** | Externalize + observe verbalizable thought | Turn FOA + audit SoT |
-| **ACTIVE desk** | Goal / decision / report | Live turn surface |
-| **WorldModel JSONL** | Identity projection | Recharged from Cube when present |
-| **HermesCube** | Durable long-tail warehouse | Durable memory SoT when installed |
-| **Standalone warehouse** | Semantic + World | Local SoT when Cube absent |
-
-## Anthropic → Hermespace map
-
-| Anthropic | Hermespace |
-|-----------|------------|
-| J-lens readout | `hs jspace lens` / viewport panel |
-| Causal swap / inject / ablate | `swap` / `inject` / `ablate` |
-| Silent multi-step intermediates | `reason_step` + mid band |
-| Eval-awareness / hidden-goal audit | `audit()` soft lexicon |
-| Assistant POV in workspace | `set_pov` |
-| Counterfactual reflection training | `reflect()` + seal principles |
-| Night / sleep consolidation | `dream_harvest` + CubeDream + pulse |
-
-## Standalone vs Cube-powered
-
-| Mode | Warehouse | Night path |
-|------|-----------|------------|
-| Standalone | Semantic + World | harvest → semantic/world |
-| Heart / Center | `memory.cube` | harvest → seal_learning → CubeDream |
-
-## Non-goals
-
-- Not weight-level J-lens · not consciousness claims
-- Not a second LLM runtime · not MEMORY.md rewrite
-- Silent chain never auto-dumps into user chat
-
-## Success metrics
-
-1. Operator can `hs jspace lens` and see silent intermediates Hermes parked
-2. Sticky swap changes subsequent Report/broadcast (Soccer→Rugby)
-3. Material turns auto-park ≥1 silent step (`HERMESPACE_OEW=1` default)
-4. Reflect seeds the next mid-band; ablate filters model inject
-5. Dual decode: user Report shaped; model gets hub + Cube strip
-6. Audit flags externalized manipulation/eval-awareness language
-7. Dream/pulse harvest feeds Cube or standalone warehouse
-8. Smoke 9/9 · unit tests green · runs without Cube
-
-## Version posture
-
-Deepen the *environment* (observe + intervene + dream), not a second archive.
-Plugin yaml + `__version__` move together.
+---
+
+## Operating contract
+
+### Before a model turn
+
+`pre_llm_call` receives the original user message. Hermespace:
+
+1. decides whether the turn is material,
+2. restores that session's desk and hub,
+3. selects a bounded focus,
+4. parks silent multi-step intermediates when required,
+5. injects ephemeral context into the **user message only**.
+
+The system prompt remains untouched, preserving Hermes prompt-cache behavior.
+
+### During a turn
+
+Hermes tools, skills, and subagents remain the specialist processors.
+Hermespace records bounded operational facts (tool names and counts, never
+arguments/results) and keeps the active workspace available to every downstream
+step.
+
+### After a turn
+
+`post_llm_call` records the completed report and closes the native turn.
+`on_session_end` is treated as a **turn boundary**, matching Hermes v0.20.
+Only `on_session_finalize` performs final harvest and idle maintenance.
+
+### Across concurrent sessions
+
+Opaque Hermes session IDs are hashed into independent desk/hub paths. A gateway
+user, A2A peer, or subagent cannot inherit another session's silent workspace.
+World identity may remain agent-scoped; active cognition is session-scoped.
+
+---
+
+## Authority and boundaries
+
+| Layer | Authority |
+|-------|-----------|
+| AccessHub / OEW | Active turn concepts, silent intermediates, modulation |
+| Session desk | Goal, decision, plan, report, structured turn metadata |
+| Workbench | Session mode, parked goals, last native turn |
+| WorldModel | Agent-scoped beliefs, landmarks, timeline |
+| Hermes Agent | Model calls, tools, skills, approvals, transcript |
+| Optional warehouse | Long-tail recall and consolidation only |
+
+Hermespace does **not**:
+
+- replace Hermes's `MemoryProvider`,
+- replace Hermes's context compressor,
+- mutate the persisted Hermes conversation,
+- inject into the system prompt,
+- persist tool arguments, tool results, or secret-bearing prompt text in
+ runtime telemetry,
+- claim weight-level interpretability or consciousness.
+
+---
+
+## Hermes Agent compatibility target
+
+Primary target: **Hermes Agent v0.20.0+**.
+
+Hermespace uses the current public plugin contracts:
+
+- `on_session_start`
+- `pre_llm_call`
+- `post_llm_call`
+- `post_tool_call`
+- `on_session_end`
+- `on_session_finalize`
+- `on_session_reset`
+- `subagent_start` / `subagent_stop`
+- `/hermespace` slash command
+- `hermes hermespace` CLI command
+
+New Hermes capabilities should be adopted only when they strengthen the Access
+Engine without taking ownership from Hermes. In particular, Hermespace does not
+register an exclusive context engine merely to observe turns; current
+`pre_llm_call` and `post_llm_call` hooks already provide the correct seams.
+
+---
+
+## Quality bar
+
+Hermespace should be held to the same engineering standard as Hermes Agent:
+
+1. **Installable** — clean `pip install .` declares all runtime dependencies.
+2. **Native** — `hermes plugins install PabloTheThinker/hermespace --enable`
+ installs a working root plugin.
+3. **Fail-visible** — a missing runtime fails registration; it never becomes an
+ enabled no-op.
+4. **Session-safe** — active desks and hubs are isolated by session.
+5. **Crash-safe** — critical state writes use atomic replace.
+6. **Bounded** — context, hub, silent chain, telemetry, and session registry have
+ explicit caps.
+7. **Private by default** — runtime metrics store lengths/names, not payloads.
+8. **Cache-safe** — plugin context is ephemeral user-message context.
+9. **Observable** — `/hermespace runtime`, `hermes hermespace doctor`, and
+ `hermespace ops doctor` explain current health.
+10. **Proven** — clean-wheel tests, current Hermes host-contract tests, unit
+ tests, smoke, operational E2E, and security audit run in CI.
+
+---
+
+## Acceptance tests
+
+The project is operational only when all of these pass:
+
+```bash
+python -m pip install .
+python -m unittest discover -s tests -v
+python scripts/verify_hermes_integration.py
+./scripts/security_audit.sh
+./scripts/smoke_test.sh
+./scripts/e2e_ops.sh
+python -m build
+```
+
+And on a Hermes installation:
+
+```bash
+hermes plugins install PabloTheThinker/hermespace --enable
+hermes hermespace doctor
+# inside a session
+/hermespace status
+```
+
+---
+
+## Product direction
+
+Deepen the **Access Engine**, not the feature count.
+
+Prefer changes that make the current turn more coherent, causal, bounded,
+private, and observable. Reject changes that create a second agent framework,
+duplicate Hermes ownership, or add ceremony without improving an executable
+contract.
+
+Architecture: [docs/architecture/CODEMAP.md](docs/architecture/CODEMAP.md)
+Operations: [docs/ops/35-production-operations.md](docs/ops/35-production-operations.md)
+Access Engine: [docs/access/34-access-engine.md](docs/access/34-access-engine.md)
diff --git a/README.md b/README.md
index 23c478c..e4791ac 100644
--- a/README.md
+++ b/README.md
@@ -3,13 +3,13 @@
- A persistent agent world that grows forever. Pocket workbench for the current turn.
+ The production Access Engine for Hermes Agent.
-
+
@@ -25,9 +25,13 @@
-**Hermespace is an append-only persistent world for Hermes agents.** Every session, every belief, every landmark, every evolution is recorded in an archive that never prunes, never decays, never caps. The agent builds a deepening model of itself and its environment across sessions — and it outlives the user.
+**Hermespace gives Hermes Agent a bounded, session-safe working room.** It
+selects active concepts before a material turn, keeps silent intermediates out
+of user chat, observes native tools/subagents, and preserves continuity after
+the turn. Hermes remains the actor; Hermespace keeps the problem coherent.
-Not [Access Workspace](https://github.com/anomalyco/j-space). Not a second agent runtime. A room inside Hermes that remembers everything.
+Production target: **Hermes Agent v0.20.0+** — CLI, gateways, A2A, tools,
+subagents, turn boundaries, and finalization.
---
@@ -142,11 +146,18 @@ Alongside the world, Hermespace provides a desk for the current turn — FOA, du
### Quick start
+```bash
+hermes plugins install PabloTheThinker/hermespace --enable
+hermes hermespace doctor
+```
+
+For a development checkout:
+
```bash
git clone https://github.com/PabloTheThinker/hermespace.git
cd hermespace
-./scripts/install_hermes.sh # link skill + plugin
-./scripts/smoke_test.sh # expect 9/9
+./scripts/install_hermes.sh
+python scripts/verify_hermes_integration.py
```
```python
@@ -163,9 +174,13 @@ ctx = r["model_context"] # → model (includes world context)
| Hook | What happens |
|---|---|
-| `on_session_start` | `WorldModel.enter()` + workbench enter + `ensure_heart` + Access Workspace sync |
-| `pre_llm_call` | Desk + world + `cube_beat` arterial strip + Access Workspace broadcast |
-| `on_session_end` | `WorldModel.leave()` + workbench idle tick (autonomic pulse) |
+| `on_session_start` | Initialize session desk/hub and stage first-turn context |
+| `pre_llm_call` | Select bounded Access Workspace context (user-message only) |
+| `post_llm_call` | Observe successful native response and update workbench |
+| `post_tool_call` | Record bounded tool-name/count telemetry (no payloads) |
+| `on_session_end` | Lightweight end-of-turn receipt (Hermes v0.20 semantics) |
+| `on_session_finalize` | Idempotent harvest, world leave, idle maintenance |
+| `subagent_start/stop` | Track specialist lifecycle for runtime observability |
---
@@ -198,6 +213,9 @@ State (local, never committed):
```text
$HERMESPACE_HOME/memory/hermespace/
ACTIVE.md live desk
+ sessions/ per-session ACTIVE desks (hashed Hermes session IDs)
+ access/ per-session AccessHub state
+ runtime/ bounded hook telemetry (names/counts, no payloads)
hermespace.db turn database
journal/ human-readable logs
workbenches/ per-agent workbench JSON
@@ -236,7 +254,8 @@ $HERMESPACE_HOME/memory/hermespace/
| [`docs/access/29-baars-changeux-anthropic.md`](docs/access/29-baars-changeux-anthropic.md) | Baars · Changeux/Dehaene · Anthropic research bridge |
| [`docs/access/30-day-to-day-higher-order.md`](docs/access/30-day-to-day-higher-order.md) | Day-to-day higher-order Hermes usage |
| [`docs/access/31-anthropic-x-video-deep-dive.md`](docs/access/31-anthropic-x-video-deep-dive.md) | Anthropic X video — how J-space is operated |
-| [`docs/access/32-hermes-base-as-jspace.md`](docs/access/32-hermes-base-as-jspace.md) | Hermes base = J-space of Hermes agents |
+| [`docs/access/32-hermespace-access-engine.md`](docs/access/32-hermespace-access-engine.md) | Hermespace Access Engine architecture |
+| [`docs/ops/35-production-operations.md`](docs/ops/35-production-operations.md) | Hermes v0.20 lifecycle, install, health, recovery |
| [`docs/access/27-environment.md`](docs/access/27-environment.md) | Environment API |
| [`ABOUT.md`](ABOUT.md) | Philosophy, design principles, author |
| [`docs/architecture/CODEMAP.md`](docs/architecture/CODEMAP.md) | Where to edit (layer map) |
@@ -263,7 +282,7 @@ src/hermespace/ runtime package
grid/ autonomy grid
hermes_plugin/ Hermes session / pre_llm / end hooks
skills/hermespace/ public Hermes agent skill
-docs/ jspace · assessment · architecture · ops · research
+docs/ access · assessment · architecture · ops · research
scripts/ CLI, install, smoke test, security audit
tests/ · experiments/ · desktop_plugin/ · spec/
```
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..1009398
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,20 @@
+"""Hermes source-repository plugin entry point.
+
+``hermes plugins install PabloTheThinker/hermespace --enable`` clones this
+repository as one plugin. Add the repository's ``src`` directory, then load
+the same registration module used by wheel entry-point installs.
+"""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+_ROOT = Path(__file__).resolve().parent
+_SRC = _ROOT / "src"
+if str(_SRC) not in sys.path:
+ sys.path.insert(0, str(_SRC))
+
+from hermespace.plugin import register # noqa: E402,F401
+
+__all__ = ["register"]
diff --git a/after-install.md b/after-install.md
new file mode 100644
index 0000000..8859459
--- /dev/null
+++ b/after-install.md
@@ -0,0 +1,17 @@
+# Hermespace installed
+
+Hermespace is a general plugin and must be enabled before its hooks load:
+
+```bash
+hermes plugins enable hermespace
+hermes hermespace doctor
+```
+
+Inside a Hermes session:
+
+```text
+/hermespace status
+```
+
+Hermespace targets Hermes Agent v0.20.0+. It keeps active desks and hubs
+session-scoped and injects context into the user message only.
diff --git a/docs/access/34-access-engine.md b/docs/access/34-access-engine.md
index f7d8b05..6e9c94e 100644
--- a/docs/access/34-access-engine.md
+++ b/docs/access/34-access-engine.md
@@ -1,6 +1,6 @@
# Access Engine — Hermespace's open access workspace for Hermes Agent
-**Version:** 0.24.0
+**Version:** 0.25.0
**Product name:** Hermespace **Access Engine** / **Access Workspace**
**Thesis:** Hermes Agent usually has no weight access. Hermespace therefore
ships its own privileged verbalizable workspace — an open-source harness the
diff --git a/docs/ops/35-production-operations.md b/docs/ops/35-production-operations.md
new file mode 100644
index 0000000..8be193e
--- /dev/null
+++ b/docs/ops/35-production-operations.md
@@ -0,0 +1,106 @@
+# Production operations — Hermes Agent v0.20+
+
+Hermespace 0.25 targets the current Hermes plugin host, including CLI,
+gateways, A2A sessions, tools, subagents, and finalization.
+
+## Install
+
+Preferred:
+
+```bash
+hermes plugins install PabloTheThinker/hermespace --enable
+hermes hermespace doctor
+```
+
+Checkout development:
+
+```bash
+./scripts/install_hermes.sh
+python scripts/verify_hermes_integration.py
+```
+
+The repository root contains `plugin.yaml` and `__init__.py`, which is the
+layout expected by `hermes plugins install owner/repo`. A pip install also
+publishes the `hermes_agent.plugins` entry point.
+
+## Native lifecycle
+
+| Hermes v0.20 hook | Hermespace behavior |
+|-------------------|----------------------|
+| `on_session_start` | Initialize and stage first-turn context |
+| `pre_llm_call` | Inject bounded ephemeral user-message context |
+| `post_llm_call` | Observe successful response |
+| `post_tool_call` | Count tool name only; no args/results |
+| `on_session_end` | End one `run_conversation` turn; no harvest |
+| `on_session_finalize` | Idempotent final harvest and idle maintenance |
+| `on_session_reset` | Prime a rotated gateway session |
+| `subagent_start/stop` | Count specialist lifecycle |
+
+The distinction between `on_session_end` and `on_session_finalize` is
+important: current Hermes fires `on_session_end` after every turn.
+
+## Session isolation
+
+Hermes session IDs are hashed before entering a path. Active desks and hubs are
+stored under separate session scopes:
+
+```text
+$HERMESPACE_HOME/memory/hermespace/
+ sessions/--/ACTIVE.md
+ access/--.json
+ runtime/.json
+```
+
+Runtime snapshots contain counts, lengths, model/platform labels, and tool
+names. They never contain prompts, tool arguments, or tool results.
+
+## Health
+
+Terminal:
+
+```bash
+hermes hermespace status
+hermes hermespace runtime
+hermes hermespace doctor
+hermespace ops doctor
+```
+
+Inside Hermes:
+
+```text
+/hermespace status
+/hermespace metrics
+/hermespace runtime
+/hermespace lens
+```
+
+`ops doctor` reports `ok` for local engine health and `integration_ok` for the
+native Hermes plugin door.
+
+## Latency posture
+
+The native `pre_llm_call` path defaults to cached/hash-only enrichment and
+does not run the full doctor or viewport generator. Set
+`HERMESPACE_SKIP_NEURAL=0` only when synchronous neural enrichment is known to
+meet your platform latency budget.
+
+## Failure behavior
+
+- Plugin registration fails visibly if the runtime cannot import.
+- Optional hooks are skipped only on older Hermes hosts; the required
+ start/pre-LLM/end hooks must register.
+- State writes use atomic same-filesystem replacement.
+- Corrupt desk JSON sidecars fall back to human-readable `ACTIVE.md`.
+- Finalization is idempotent when CLI and gateway teardown paths converge.
+
+## Release verification
+
+```bash
+python -m pip install .
+python -m unittest discover -s tests -v
+python scripts/verify_hermes_integration.py
+./scripts/security_audit.sh
+./scripts/smoke_test.sh
+./scripts/e2e_ops.sh
+python -m build
+```
diff --git a/hermes_plugin/__init__.py b/hermes_plugin/__init__.py
index caaa4c4..05be5d9 100644
--- a/hermes_plugin/__init__.py
+++ b/hermes_plugin/__init__.py
@@ -1,4 +1,4 @@
-"""Hermes file-plugin — first-class Hermespace workbench integration."""
+"""Source-tree wrapper for the first-class Hermespace plugin."""
from __future__ import annotations
@@ -7,7 +7,7 @@
import sys
from pathlib import Path
-__version__ = "0.24.0"
+__version__ = "0.25.0"
logger = logging.getLogger("hermes.plugins.hermespace")
@@ -72,25 +72,14 @@ def _ensure_import() -> bool:
return False
-_IMPORT_OK = _ensure_import()
-
-
def register(ctx) -> None:
- """Register Hermes lifecycle hooks — workbench is part of the framework path."""
- if not _IMPORT_OK and not _ensure_import():
- logger.error("Hermespace plugin registered but package missing — hooks skipped")
- return
-
- from hermespace.hermes_bridge import (
- on_pre_llm_call,
- on_session_end,
- on_session_start,
- )
+ """Resolve the runtime and delegate to the packaged plugin entry point."""
+ if not _ensure_import():
+ raise RuntimeError(
+ "Hermespace runtime is not importable. Run ./scripts/install_hermes.sh "
+ "or pip install the Hermespace checkout before enabling the plugin."
+ )
- ctx.register_hook("on_session_start", on_session_start)
- ctx.register_hook("pre_llm_call", on_pre_llm_call)
- ctx.register_hook("on_session_end", on_session_end)
- logger.info(
- "Hermespace v%s registered: on_session_start + pre_llm_call + on_session_end",
- __version__,
- )
+ from hermespace.plugin import register as register_runtime
+
+ register_runtime(ctx)
diff --git a/hermes_plugin/plugin.yaml b/hermes_plugin/plugin.yaml
index 3b7c124..f16a0b0 100644
--- a/hermes_plugin/plugin.yaml
+++ b/hermes_plugin/plugin.yaml
@@ -1,14 +1,20 @@
+manifest_version: 1
name: hermespace
-version: "0.24.0"
+version: "0.25.0"
description: >
- Hermespace Access Engine for Hermes agents — open-source GWT harness
- (report/modulate/silent-reason/broadcast/selectivity), OEW ON by default,
- dual decode, optional warehouse. Hooks: on_session_start, pre_llm_call,
- on_session_end. Set HERMESPACE_ROOT to checkout.
+ Production Access Engine for Hermes Agent v0.20+: session-scoped workspace,
+ bounded model context, native lifecycle telemetry, dual decode, and
+ standalone-safe persistence.
author: Hermespace contributors
kind: standalone
hooks:
- on_session_start
- pre_llm_call
+ - post_llm_call
+ - post_tool_call
- on_session_end
+ - on_session_finalize
+ - on_session_reset
+ - subagent_start
+ - subagent_stop
homepage: https://github.com/PabloTheThinker/hermespace
diff --git a/plugin.yaml b/plugin.yaml
new file mode 100644
index 0000000..f16a0b0
--- /dev/null
+++ b/plugin.yaml
@@ -0,0 +1,20 @@
+manifest_version: 1
+name: hermespace
+version: "0.25.0"
+description: >
+ Production Access Engine for Hermes Agent v0.20+: session-scoped workspace,
+ bounded model context, native lifecycle telemetry, dual decode, and
+ standalone-safe persistence.
+author: Hermespace contributors
+kind: standalone
+hooks:
+ - on_session_start
+ - pre_llm_call
+ - post_llm_call
+ - post_tool_call
+ - on_session_end
+ - on_session_finalize
+ - on_session_reset
+ - subagent_start
+ - subagent_stop
+homepage: https://github.com/PabloTheThinker/hermespace
diff --git a/pyproject.toml b/pyproject.toml
index 148ece9..51920b2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,23 @@
[project]
name = "hermespace"
-version = "0.24.0"
-description = "OEW J-Space for Hermes Agent — obligatory external workspace, Cube heart, higher-order thinking"
+version = "0.25.0"
+description = "Production Access Engine and persistent workspace for Hermes Agent"
requires-python = ">=3.10"
readme = "README.md"
license = { text = "MIT" }
+authors = [{ name = "Hermespace contributors" }]
+keywords = ["hermes-agent", "agents", "workspace", "memory", "cognition"]
+dependencies = ["numpy>=1.24"]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
+]
[project.urls]
Homepage = "https://github.com/PabloTheThinker/hermespace"
@@ -15,8 +28,14 @@ Documentation = "https://github.com/PabloTheThinker/hermespace#documentation"
[project.scripts]
hermespace = "hermespace.cli:main"
+[project.entry-points."hermes_agent.plugins"]
+hermespace = "hermespace.plugin"
+
+[project.optional-dependencies]
+dev = ["build", "ruff"]
+
[build-system]
-requires = ["setuptools>=61"]
+requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
diff --git a/scripts/install_hermes.sh b/scripts/install_hermes.sh
index 406fcfd..fd34968 100755
--- a/scripts/install_hermes.sh
+++ b/scripts/install_hermes.sh
@@ -1,74 +1,100 @@
#!/usr/bin/env bash
-# One-shot Hermespace → Hermes Agent install (skill + plugin + env hints).
+# Production Hermespace → Hermes Agent installer.
set -euo pipefail
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
HERMESPACE_HOME="${HERMESPACE_HOME:-$HOME/.hermespace}"
+INSTALL_DESKTOP=1
+ENABLE_PLUGIN=1
+
+for arg in "$@"; do
+ case "$arg" in
+ --no-desktop) INSTALL_DESKTOP=0 ;;
+ --no-enable) ENABLE_PLUGIN=0 ;;
+ -h|--help)
+ echo "usage: $0 [--no-desktop] [--no-enable]"
+ exit 0
+ ;;
+ *)
+ echo "hermespace: unknown installer option: $arg" >&2
+ exit 2
+ ;;
+ esac
+done
-echo "Hermespace install"
+# shellcheck source=scripts/_python.sh
+source "$ROOT/scripts/_python.sh"
+
+echo "Hermespace production install"
echo " CHECKOUT=$ROOT"
echo " HERMES_HOME=$HERMES_HOME"
echo " HERMESPACE_HOME=$HERMESPACE_HOME"
+echo " PYTHON=$PYTHON"
-mkdir -p "$HERMESPACE_HOME"
-mkdir -p "$HERMES_HOME/skills" "$HERMES_HOME/plugins"
-
-# Skill
-rm -rf "$HERMES_HOME/skills/hermespace"
-ln -sfn "$ROOT/skills/hermespace" "$HERMES_HOME/skills/hermespace"
-echo " skill → $HERMES_HOME/skills/hermespace"
+mkdir -p "$HERMESPACE_HOME" "$HERMES_HOME/skills" "$HERMES_HOME/plugins"
-# Plugin (symlink so package auto-discovers ../src)
-rm -rf "$HERMES_HOME/plugins/hermespace"
-ln -sfn "$ROOT/hermes_plugin" "$HERMES_HOME/plugins/hermespace"
-echo " hermes plugin → $HERMES_HOME/plugins/hermespace"
+echo " installing Python package"
+"$PYTHON" -m pip install -e "$ROOT" --quiet
+"$PYTHON" -c \
+ "import hermespace; from hermespace import AccessEngine; assert AccessEngine().status()['ready']"
+echo " package import + AccessEngine readiness OK"
-# Desktop plugin — REAL file copy (not symlink; Desktop/remote hermes_home)
-bash "$ROOT/scripts/install_desktop_plugin.sh" || {
- mkdir -p "$HERMES_HOME/desktop-plugins/hermespace"
- cp -f "$ROOT/desktop_plugin/hermespace/plugin.js" "$HERMES_HOME/desktop-plugins/hermespace/plugin.js"
- echo " desktop plugin (fallback cp) → $HERMES_HOME/desktop-plugins/hermespace"
+replace_link() {
+ local target="$1"
+ local source="$2"
+ if [[ -L "$target" || -f "$target" ]]; then
+ rm -f "$target"
+ elif [[ -d "$target" ]]; then
+ rm -rf "$target"
+ fi
+ ln -s "$source" "$target"
}
-# Optional editable install when pip available
-if command -v pip >/dev/null 2>&1 || command -v pip3 >/dev/null 2>&1; then
- PIP="$(command -v pip3 || command -v pip)"
- if "$PIP" install -e "$ROOT" -q 2>/dev/null; then
- echo " pip install -e . OK"
- else
- echo " pip install -e . skipped (optional; PYTHONPATH=src works)"
- fi
+# Link the complete repository, not only hermes_plugin/. Current Hermes
+# `plugins install owner/repo` likewise installs the repository root; keeping
+# both paths identical catches source-layout regressions.
+replace_link "$HERMES_HOME/plugins/hermespace" "$ROOT"
+replace_link "$HERMES_HOME/skills/hermespace" "$ROOT/skills/hermespace"
+echo " plugin → $HERMES_HOME/plugins/hermespace"
+echo " skill → $HERMES_HOME/skills/hermespace"
+
+if [[ "$INSTALL_DESKTOP" == "1" ]]; then
+ bash "$ROOT/scripts/install_desktop_plugin.sh"
+else
+ echo " desktop plugin skipped"
fi
if command -v hermes >/dev/null 2>&1; then
- hermes plugins enable hermespace 2>/dev/null || true
- echo " hermes plugins enable hermespace (attempted)"
+ if [[ "$ENABLE_PLUGIN" == "1" ]]; then
+ hermes plugins enable hermespace
+ echo " Hermes plugin enabled"
+ else
+ echo " enable later: hermes plugins enable hermespace"
+ fi
else
- echo " hermes CLI not on PATH — enable later: hermes plugins enable hermespace"
+ echo " Hermes CLI not on PATH — source install is ready; enable later"
fi
+HERMES_HOME="$HERMES_HOME" HERMESPACE_HOME="$HERMESPACE_HOME" \
+ "$PYTHON" "$ROOT/scripts/verify_hermes_integration.py"
+
cat <>"$LOG" 2>&1 <>"$LOG" 2>&1
then
- ok "hermes_plugin_pre_llm"
+ ok "hermes_plugin_host_contract"
else
- bad "hermes_plugin_pre_llm" "plugin failed"
+ bad "hermes_plugin_host_contract" "current Hermes host contract failed"
fi
"$PYTHON" - < None:
+ self.hooks: dict[str, Any] = {}
+ self.commands: dict[str, Any] = {}
+ self.cli_commands: dict[str, Any] = {}
+ self.skills: dict[str, Path] = {}
+
+ def register_hook(self, name: str, callback: Any) -> None:
+ self.hooks[name] = callback
+
+ def register_command(
+ self,
+ name: str,
+ handler: Any,
+ description: str = "",
+ args_hint: str = "",
+ ) -> None:
+ self.commands[name] = {
+ "handler": handler,
+ "description": description,
+ "args_hint": args_hint,
+ }
+
+ def register_cli_command(self, **kwargs: Any) -> None:
+ self.cli_commands[str(kwargs["name"])] = kwargs
+
+ def register_skill(self, name: str, path: Path) -> None:
+ self.skills[name] = Path(path)
+
+
+def _load_repo_plugin() -> Any:
+ spec = importlib.util.spec_from_file_location(
+ "hermespace_repo_plugin",
+ ROOT / "__init__.py",
+ submodule_search_locations=[str(ROOT)],
+ )
+ if spec is None or spec.loader is None:
+ raise RuntimeError("cannot load repository plugin entry point")
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[spec.name] = module
+ spec.loader.exec_module(module)
+ return module
+
+
+def main() -> int:
+ with tempfile.TemporaryDirectory(prefix="hermespace-host-contract-") as tmp:
+ old_home = os.environ.get("HERMESPACE_HOME")
+ old_agent = os.environ.get("HERMESPACE_AGENT_ID")
+ os.environ["HERMESPACE_HOME"] = tmp
+ os.environ["HERMESPACE_AGENT_ID"] = "contract-agent"
+ os.environ["HERMESPACE_NEURAL_VERBALIZE"] = "0"
+ os.environ["HERMESPACE_SKIP_NEURAL"] = "1"
+ try:
+ module = _load_repo_plugin()
+ ctx = HostContext()
+ module.register(ctx)
+
+ required = {
+ "on_session_start",
+ "pre_llm_call",
+ "post_llm_call",
+ "post_tool_call",
+ "on_session_end",
+ "on_session_finalize",
+ }
+ missing = sorted(required - set(ctx.hooks))
+ if missing:
+ raise AssertionError(f"missing hooks: {missing}")
+ if "hermespace" not in ctx.commands:
+ raise AssertionError("/hermespace command not registered")
+ if "hermespace" not in ctx.cli_commands:
+ raise AssertionError("hermes hermespace command not registered")
+
+ common = {
+ "session_id": "contract-session",
+ "model": "test/model",
+ "platform": "cli",
+ }
+ ctx.hooks["on_session_start"](**common)
+ injected = ctx.hooks["pre_llm_call"](
+ **common,
+ user_message="First inspect the issue, then fix it and verify the result",
+ conversation_history=[],
+ is_first_turn=True,
+ )
+ if not isinstance(injected, dict) or not injected.get("context"):
+ raise AssertionError("pre_llm_call did not inject context")
+ if len(str(injected["context"])) > 15_000:
+ raise AssertionError("hook context exceeded 15,000 chars")
+
+ ctx.hooks["post_tool_call"](
+ **common,
+ tool_name="read_file",
+ args={"path": "redacted"},
+ result='{"ok": true}',
+ task_id="contract-session",
+ duration_ms=1,
+ )
+ ctx.hooks["post_llm_call"](
+ **common,
+ user_message="fix and verify",
+ assistant_response="Implemented and verified.",
+ conversation_history=[],
+ )
+ ctx.hooks["on_session_end"](
+ **common,
+ completed=True,
+ interrupted=False,
+ )
+
+ from hermespace.hermes_runtime import runtime
+
+ before = runtime.status("contract-session")
+ if before.get("finalized"):
+ raise AssertionError("on_session_end incorrectly finalized the session")
+ ctx.hooks["on_session_finalize"](**common)
+ after = runtime.status("contract-session")
+ if not after.get("finalized"):
+ raise AssertionError("on_session_finalize did not finalize runtime state")
+ # Host teardown paths may converge; finalization must be idempotent.
+ ctx.hooks["on_session_finalize"](**common)
+
+ command_output = ctx.commands["hermespace"]["handler"]("runtime")
+ if "tracked_sessions" not in command_output:
+ raise AssertionError("/hermespace runtime returned unexpected output")
+
+ report = {
+ "ok": True,
+ "hooks": sorted(ctx.hooks),
+ "slash_command": True,
+ "cli_command": True,
+ "skill": "hermespace" in ctx.skills,
+ "context_chars": len(str(injected["context"])),
+ "turns": after.get("completed_turns"),
+ "tools": after.get("tool_calls"),
+ "finalized": after.get("finalized"),
+ }
+ print(json.dumps(report, indent=2))
+ return 0
+ finally:
+ if old_home is None:
+ os.environ.pop("HERMESPACE_HOME", None)
+ else:
+ os.environ["HERMESPACE_HOME"] = old_home
+ if old_agent is None:
+ os.environ.pop("HERMESPACE_AGENT_ID", None)
+ else:
+ os.environ["HERMESPACE_AGENT_ID"] = old_agent
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/skills/hermespace/SKILL.md b/skills/hermespace/SKILL.md
index 7059869..434df17 100644
--- a/skills/hermespace/SKILL.md
+++ b/skills/hermespace/SKILL.md
@@ -170,10 +170,11 @@ hs view --serve --tailscale --port 8764 # any user's tailnet
### 4.4 Hermes plugin (automatic)
-Hooks: `on_session_start` · `pre_llm_call` · `on_session_end`
+Core hooks: `on_session_start` · `pre_llm_call` · `post_llm_call` ·
+`post_tool_call` · `on_session_end` · `on_session_finalize`
- Broadcasts **ready** desk into model context
- Does **not** invent goals — agents still call Workbench / turn
-- Optional: `HERMESPACE_AUTO_ORDER=0` (default), `HERMESPACE_IDLE_ON_SESSION_END=1`
+- Uses per-session desks/hubs; `/hermespace runtime` shows lifecycle state
### 4.5 Skills + MEMORY fabric
diff --git a/skills/hermespace/references/plugin-hooks.md b/skills/hermespace/references/plugin-hooks.md
index c2db72a..c880884 100644
--- a/skills/hermespace/references/plugin-hooks.md
+++ b/skills/hermespace/references/plugin-hooks.md
@@ -2,23 +2,27 @@
## Register
```bash
-ln -sfn "$HERMESPACE_ROOT/hermes_plugin" "${HERMES_HOME:-$HOME/.hermes}/plugins/hermespace"
-hermes plugins enable hermespace
+hermes plugins install PabloTheThinker/hermespace --enable
+hermes hermespace doctor
```
-`hermes_plugin/__init__.py` → `register(ctx)`.
-Keep `plugin.yaml` `version` == package `__version__`.
+Repository `__init__.py` → `hermespace.plugin.register(ctx)`.
## Hooks
| Hook | Behavior |
|------|----------|
-| on_session_start | Workbench.enter + env kit; seed desk; return context |
-| pre_llm_call | Gate + neural FOA + desk inject; optional AUTO_ORDER |
-| on_session_end | idle_tick if HERMESPACE_IDLE_ON_SESSION_END=1 |
+| on_session_start | Initialize session scope; stage first-turn context |
+| pre_llm_call | Gate + bounded desk/hub inject |
+| post_llm_call | Observe successful native turn |
+| post_tool_call | Count tool name only; never persist payloads |
+| on_session_end | Lightweight turn boundary |
+| on_session_finalize | Idempotent harvest + idle maintenance |
+| on_session_reset | Prime rotated gateway session |
+| subagent_start/stop | Track specialist lifecycle |
## Implementation
Logic: `src/hermespace/hermes_bridge.py`
-Plugin package: thin register only.
+Plugin package: `src/hermespace/plugin.py`.
## Env
`HERMESPACE_ROOT`, `HERMESPACE_HOME`, `HERMESPACE_AGENT_ID`,
@@ -26,6 +30,6 @@ Plugin package: thin register only.
`HERMESPACE_NEURAL_BACKEND=auto`, `HERMESPACE_OFF=0`, `HERMESPACE_FORCE=0`
## Failure modes
-- Wrong `HERMESPACE_ROOT` → import fail / empty inject
+- Missing runtime → registration fails visibly
- Desk not ready → thin or skipped pre_llm inject (run turn/order first)
-- Plugin alone never creates goals — agent must call turn/order
+- Use `/hermespace runtime` to inspect native lifecycle
diff --git a/src/hermespace/__init__.py b/src/hermespace/__init__.py
index 91dffc0..b8d7270 100644
--- a/src/hermespace/__init__.py
+++ b/src/hermespace/__init__.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-__version__ = "0.24.0"
+__version__ = "0.25.0"
from hermespace.desk import Desk
from hermespace.engine import HermespaceEngine
diff --git a/src/hermespace/access/engine.py b/src/hermespace/access/engine.py
index 9132ab2..bd12f7e 100644
--- a/src/hermespace/access/engine.py
+++ b/src/hermespace/access/engine.py
@@ -29,6 +29,7 @@
from hermespace.access.oew import ensure_oew_env_default, oew_default_on
from hermespace.access.protocol import oew_enabled
+from hermespace.paths import session_desk_path, session_scope_id
# Five GWT-style access roles implemented by this harness
ACCESS_ROLES = (
@@ -40,6 +41,12 @@
)
+def workspace_id(agent_id: str, session_id: str = "") -> str:
+ """Backward-compatible name for the canonical session scope helper."""
+
+ return session_scope_id(agent_id, session_id)
+
+
class AccessEngine:
"""True Hermespace Access Engine — open-source GWT harness for Hermes."""
@@ -62,19 +69,23 @@ def __init__(
self._ignitions = 0
self._skips = 0
+ @property
+ def workspace_id(self) -> str:
+ return workspace_id(self.agent_id, self.session_id)
+
# --- core handles -------------------------------------------------------
@property
def hub(self):
from hermespace.access import AccessHub
- return AccessHub(agent_id=self.agent_id)
+ return AccessHub(agent_id=self.workspace_id)
@property
def env(self):
from hermespace.access import AccessEnv
- return AccessEnv(agent_id=self.agent_id)
+ return AccessEnv(agent_id=self.workspace_id)
@property
def desk_engine(self):
@@ -83,12 +94,14 @@ def desk_engine(self):
if self.desk_path is not None:
return HermespaceEngine(desk_path=self.desk_path)
- return HermespaceEngine()
+ return HermespaceEngine(
+ desk_path=session_desk_path(self.agent_id, self.session_id)
+ )
- # --- Anthropic access roles (live) --------------------------------------
+ # --- Access roles (live) -------------------------------------------------
def access_roles(self) -> dict[str, Any]:
- """Map Anthropic's five GWT properties onto live engine state."""
+ """Report the five live Access Workspace roles."""
js = self.hub
return {
"verbal_report": {
@@ -176,6 +189,7 @@ def connect(
"ok": False,
"agent_id": self.agent_id,
"session_id": self.session_id,
+ "workspace_id": self.workspace_id,
"engine": "AccessEngine",
"phases": {},
"gained": {},
@@ -241,6 +255,7 @@ def connect(
query=query,
session_id=self.session_id,
room=out["phases"].get("room"),
+ workspace_id=self.workspace_id,
)
except Exception as exc:
# Minimal standalone seed from world beliefs
@@ -316,6 +331,7 @@ def status(self) -> dict[str, Any]:
"engine": "AccessEngine",
"agent_id": self.agent_id,
"session_id": self.session_id,
+ "workspace_id": self.workspace_id,
"oew_enabled": oew_enabled(),
"oew_default_on": oew_default_on(),
"access": {},
@@ -324,7 +340,7 @@ def status(self) -> dict[str, Any]:
"warehouse": {},
"ready": False,
"connected": bool(self._last_connect and self._last_connect.get("ok")),
- "role": "open-source external J-space for Hermes agents",
+ "role": "Hermespace Access Workspace for Hermes agents",
"access_roles": self.access_roles(),
"metrics": self.metrics(),
"ops": [
@@ -399,7 +415,7 @@ def status(self) -> dict[str, Any]:
# --- selectivity / probe -------------------------------------------------
def probe_material(self, message: str, *, desk_ready: bool | None = None) -> dict[str, Any]:
- """Would this message ignite the workspace? (Anthropic selectivity)."""
+ """Would this message ignite the Access Workspace?"""
from hermespace.gate import should_inject
from hermespace.store import load_desk
@@ -458,8 +474,8 @@ def ablate(self, *patterns: str) -> dict[str, Any]:
def chain(self, *steps: str, salience: float = 0.85) -> dict[str, Any]:
"""Park a multi-step silent reasoning chain (internal reasoning role).
- Analogue of Anthropic spider→legs intermediates that never appear in
- the spoken answer — required for multi-step work under OEW.
+ Intermediate steps never appear in the spoken answer unless explicitly
+ requested — required for multi-step work under OEW.
"""
parked: list[str] = []
js = self.hub
@@ -522,12 +538,7 @@ def turn(
self._turn_count += 1
probe = self.probe_material(message)
- wf_kwargs: dict[str, Any] = {}
- if self.desk_path is not None:
- from hermespace.engine import HermespaceEngine
-
- wf_kwargs["engine"] = HermespaceEngine(desk_path=self.desk_path)
- out = Workflow(**wf_kwargs).run(
+ out = Workflow(engine=self.desk_engine).run(
HermespaceInput(
message=message,
goal=goal or message[:200],
@@ -572,6 +583,70 @@ def think(self, message: str, **kwargs: Any) -> dict[str, Any]:
"metrics": (out.meta or {}).get("metrics") or self.metrics(),
}
+ def observe_turn(
+ self,
+ *,
+ user_message: str = "",
+ assistant_response: str = "",
+ model: str = "",
+ platform: str = "",
+ ) -> dict[str, Any]:
+ """Observe a completed native Hermes turn.
+
+ ``pre_llm_call`` runs the workspace before generation; this closes the
+ loop after Hermes finishes. It updates the session workbench and an
+ episodic receipt without mutating the conversation transcript.
+ """
+
+ report = (assistant_response or "").strip()
+ user = (user_message or "").strip()
+ result: dict[str, Any] = {
+ "ok": True,
+ "agent_id": self.agent_id,
+ "session_id": self.session_id,
+ "workspace_id": self.workspace_id,
+ "user_chars": len(user),
+ "response_chars": len(report),
+ "model": (model or "")[:120],
+ "platform": (platform or "")[:40],
+ }
+ try:
+ from hermespace.workbench import Workbench
+
+ wb = Workbench(agent_id=self.agent_id, session_id=self.session_id)
+ wb.state.last_order = user[:500]
+ wb.state.last_report = report[:2000]
+ wb.state.mode = "idle"
+ wb.state.meta["last_native_turn"] = {
+ "model": result["model"],
+ "platform": result["platform"],
+ "user_chars": len(user),
+ "response_chars": len(report),
+ }
+ wb.save()
+ result["workbench"] = True
+ except Exception as exc:
+ result["ok"] = False
+ result["workbench_error"] = type(exc).__name__
+
+ try:
+ self.desk_engine.episodes.write(
+ (
+ f"native turn complete: user_chars={len(user)} "
+ f"response_chars={len(report)} model={result['model']}"
+ ),
+ outcome="turn_complete",
+ tags=["hermespace", "native_turn", result["platform"] or "unknown"],
+ )
+ result["episode"] = True
+ except Exception as exc:
+ result["episode_error"] = type(exc).__name__
+
+ result["audit_alerts"] = sum(
+ 1 for finding in self.audit() if finding.get("severity") == "alert"
+ )
+ return result
+
# --- dual decode ---------------------------------------------------------
@staticmethod
diff --git a/src/hermespace/access/env.py b/src/hermespace/access/env.py
index 9e823bb..313693a 100644
--- a/src/hermespace/access/env.py
+++ b/src/hermespace/access/env.py
@@ -45,7 +45,7 @@
),
}
-# Turn phase bands — analogue of intermediate layer band where J-space is coherent
+# Turn phase bands — encode → deliberate → report
BANDS = ("early", "mid", "late") # encode → reason → report
@@ -150,7 +150,7 @@ def set_band(self, band: str) -> str:
def band(self) -> str:
return str(self._env.get("band") or "mid")
- # --- Assistant POV (post-training installs POV in Anthropic J-space) ---
+ # --- Assistant point of view in the Access Workspace ---
def set_pov(self, text: str) -> str:
"""Install Assistant point-of-view reactions into the workspace."""
@@ -607,7 +607,7 @@ def operator_view(self) -> dict[str, Any]:
"trace_path": str(self.trace_path),
"protocol_enabled": bool(self._env.get("protocol_enabled", True)),
"theory": {
- "source": "Anthropic J-space / GWT (harness analogue)",
+ "source": "Hermespace Access Workspace / GWT",
"access": "externalized verbalizable workspace — not weight readout",
"night_path": "dream_harvest → Cube seal → pulse charge",
},
diff --git a/src/hermespace/access/hub.py b/src/hermespace/access/hub.py
index 3541754..b8ce723 100644
--- a/src/hermespace/access/hub.py
+++ b/src/hermespace/access/hub.py
@@ -22,6 +22,7 @@
from pathlib import Path
from typing import Any, Iterable
+from hermespace.atomic import atomic_write_text
from hermespace.cognition import (
ACTIVATED_CAP,
FOCUS_CAP,
@@ -137,7 +138,7 @@ def __init__(self, agent_id: str = "hermes-agent", root: Path | None = None) ->
legacy = state_dir() / "jspace" / f"{_safe(self.agent_id)}.json"
if legacy.is_file():
try:
- self.path.write_text(legacy.read_text(encoding="utf-8"), encoding="utf-8")
+ atomic_write_text(self.path, legacy.read_text(encoding="utf-8"))
except OSError:
self.path = legacy
self.state = self._load()
@@ -177,10 +178,7 @@ def _load(self) -> AccessHubState:
def save(self) -> None:
self.state.updated = _utcnow()
- self.path.write_text(
- json.dumps(self.state.to_dict(), indent=2),
- encoding="utf-8",
- )
+ atomic_write_text(self.path, json.dumps(self.state.to_dict(), indent=2))
# --- property 2: directed modulation ---
diff --git a/src/hermespace/access/oew.py b/src/hermespace/access/oew.py
index 84c6847..35b9d09 100644
--- a/src/hermespace/access/oew.py
+++ b/src/hermespace/access/oew.py
@@ -1,8 +1,8 @@
"""Obligatory External Workspace — higher-order thinking orchestration.
-This is the causal layer that makes Hermespace behave like a J-space for
-Hermes agents: material turns must park silent intermediates; swaps redirect
-Report/broadcast; reflections seed the next mid-band; ablations filter inject.
+This is the causal layer of the Access Workspace: material turns must park
+silent intermediates; swaps redirect Report/broadcast; reflections seed the
+next mid-band; ablations filter inject.
Default: ``HERMESPACE_OEW=1`` (higher-order on). Set ``0`` to soften.
"""
diff --git a/src/hermespace/access/protocol.py b/src/hermespace/access/protocol.py
index 1318c1a..a1c87b3 100644
--- a/src/hermespace/access/protocol.py
+++ b/src/hermespace/access/protocol.py
@@ -1,8 +1,8 @@
"""Obligatory External Workspace (OEW) protocol gate.
-Anthropic's J-space is *causally necessary* for higher-order thought.
-Hermespace becomes Hermes's J-space when material turns cannot complete
-without parking verbalizable intermediates in the external hub.
+Hermespace's Access Workspace is *causally necessary* for higher-order work
+when material turns cannot complete without parking verbalizable
+intermediates in the external hub.
Default: **ON** (``HERMESPACE_OEW`` unset or ``1``). Set ``0`` to soften.
Orchestration (auto-park, sticky swap, reflect seeds) lives in ``oew.py``.
diff --git a/src/hermespace/atomic.py b/src/hermespace/atomic.py
new file mode 100644
index 0000000..59e3777
--- /dev/null
+++ b/src/hermespace/atomic.py
@@ -0,0 +1,57 @@
+"""Small, dependency-free atomic persistence helpers.
+
+Hermes runs plugins in CLI, gateway, cron, and subagent processes. A process
+must never leave a half-written JSON or Markdown state file behind if it is
+interrupted while saving. These helpers keep the write contract local and
+portable without adding a file-lock dependency.
+"""
+
+from __future__ import annotations
+
+import os
+import tempfile
+from pathlib import Path
+
+
+def atomic_write_text(
+ path: Path,
+ text: str,
+ *,
+ encoding: str = "utf-8",
+ mode: int | None = None,
+) -> Path:
+ """Atomically replace *path* with *text* and return *path*.
+
+ The temporary file is created beside the destination so ``os.replace`` is
+ on the same filesystem. Data is flushed before replacement. Existing
+ permissions are preserved unless ``mode`` is supplied for a new file.
+ """
+
+ path = Path(path)
+ path.parent.mkdir(parents=True, exist_ok=True)
+ existing_mode: int | None = None
+ try:
+ existing_mode = path.stat().st_mode & 0o777
+ except OSError:
+ pass
+
+ fd, raw_tmp = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
+ tmp = Path(raw_tmp)
+ try:
+ with os.fdopen(fd, "w", encoding=encoding) as handle:
+ handle.write(text)
+ handle.flush()
+ os.fsync(handle.fileno())
+ desired_mode = existing_mode if existing_mode is not None else mode
+ if desired_mode is not None:
+ try:
+ os.fchmod(handle.fileno(), desired_mode)
+ except (AttributeError, OSError):
+ pass
+ os.replace(tmp, path)
+ return path
+ finally:
+ try:
+ tmp.unlink(missing_ok=True)
+ except OSError:
+ pass
diff --git a/src/hermespace/cli.py b/src/hermespace/cli.py
index 03879db..2d1944c 100644
--- a/src/hermespace/cli.py
+++ b/src/hermespace/cli.py
@@ -151,7 +151,7 @@ def main(argv: list[str] | None = None) -> int:
neu_sub.add_parser("eval", help="Rank-quality hash vs ollama embed")
# Functional Access Workspace (harness global workspace)
- # Hermes base as J-space (Anthropic video ops: read / audit / shape)
+ # Hermespace Access Engine (read / audit / shape / operate)
base = sub.add_parser(
"base",
help="Access Engine: connect / status / turn / lens / audit / reflect / harvest",
diff --git a/src/hermespace/cube_module.py b/src/hermespace/cube_module.py
index 2643b8e..b71a07f 100644
--- a/src/hermespace/cube_module.py
+++ b/src/hermespace/cube_module.py
@@ -437,6 +437,7 @@ def seed_access_from_warehouse(
query: str = "",
session_id: str = "hermespace",
room: dict[str, Any] | None = None,
+ workspace_id: str = "",
) -> dict[str, Any]:
"""Pull Cube/world wisdom + peer presence into the agent's Access Workspace hub.
@@ -454,7 +455,7 @@ def seed_access_from_warehouse(
try:
from hermespace.access import AccessHub
- js = AccessHub(agent_id=agent_id)
+ js = AccessHub(agent_id=workspace_id or agent_id)
beliefs: list[str] = []
try:
from hermespace.world import WorldModel
@@ -502,6 +503,7 @@ def seed_access_from_warehouse(
report["focus_n"] = len(js.state.focus)
report["ok"] = True
report["room_mode"] = room.get("mode")
+ report["workspace_id"] = workspace_id or agent_id
return report
except Exception as e:
report["error"] = type(e).__name__
@@ -535,7 +537,7 @@ def connect_agent(
"phases": {},
"gained": {},
"memories": [
- "Anthropic J-space: privileged verbalizable workspace (external here)",
+ "Access Workspace: privileged verbalizable workspace",
"Baars GWT: limited capacity hub broadcast to specialists",
"Dehaene gap: Cube supplies enduring episodic/library memory",
"Hive: optional room of peer agents — intelligence compounds",
@@ -583,11 +585,18 @@ def connect_agent(
out["phases"]["room"] = room_status(agent_id=agent_id)
if seed:
+ try:
+ from hermespace.access.engine import workspace_id as _workspace_id
+
+ access_id = _workspace_id(agent_id, session_id)
+ except Exception:
+ access_id = agent_id
out["phases"]["seed"] = seed_access_from_warehouse(
agent_id,
query=query,
session_id=session_id,
room=out["phases"]["room"],
+ workspace_id=access_id,
)
world = out["phases"].get("world") or {}
diff --git a/src/hermespace/gate.py b/src/hermespace/gate.py
index 1694b59..f324ecb 100644
--- a/src/hermespace/gate.py
+++ b/src/hermespace/gate.py
@@ -1,4 +1,4 @@
-"""Gate — when Hermespace should inject (selective access = J-space selectivity)."""
+"""Gate — when Hermespace should enter the selective Access Workspace."""
from __future__ import annotations
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index ee1f6d7..a42b20d 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -14,7 +14,12 @@ def _truthy(name: str, default: str = "0") -> bool:
def on_session_start(**kwargs: Any) -> dict[str, str] | None:
- """Enter pocket dimension; stamp Hermes env kit into starting context."""
+ """Initialize a Hermes v0.20 session and stage first-turn context.
+
+ Hermes treats this hook as an observer, so the returned dict is only for
+ older hosts/tests. The context is staged for ``pre_llm_call``, whose
+ return value is the one current Hermes actually injects.
+ """
try:
from hermespace.environment import probe_environment
from hermespace.engine import HermespaceEngine
@@ -33,7 +38,7 @@ def on_session_start(**kwargs: Any) -> dict[str, str] | None:
st = wb.enter(connect_warehouse=False)
env = probe_environment()
- eng = HermespaceEngine()
+ eng = wb.workflow.engine
desk = load_desk(eng.desk_path)
if not desk.goal:
desk.goal = "Hermes agent session workbench"
@@ -143,6 +148,19 @@ def on_session_start(**kwargs: Any) -> dict[str, str] | None:
"- Silent steps stay in model context only — never dump hub into chat.\n"
)
+ try:
+ from hermespace.hermes_runtime import runtime
+
+ runtime.start(
+ session_id,
+ agent_id=agent_id,
+ model=str(kwargs.get("model") or ""),
+ platform=str(kwargs.get("platform") or ""),
+ context=block,
+ )
+ except Exception as exc: # noqa: BLE001
+ logger.debug("session runtime start failed: %s", exc)
+
return {"context": block}
@@ -167,6 +185,33 @@ def on_pre_llm_call(
msg = user_message or ""
sid = str(session_id or "default")
agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
+ try:
+ from hermespace.access.engine import workspace_id
+
+ access_id = workspace_id(agent_id, sid)
+ except Exception:
+ access_id = agent_id
+ try:
+ from hermespace import AccessEngine
+
+ access_engine = AccessEngine(agent_id=agent_id, session_id=sid)
+ eng = access_engine.desk_engine
+ except Exception:
+ eng = HermespaceEngine()
+ try:
+ from hermespace.hermes_runtime import runtime
+
+ runtime.pre_llm(
+ sid,
+ agent_id=agent_id,
+ user_chars=len(msg),
+ model=str(kwargs.get("model") or ""),
+ platform=str(kwargs.get("platform") or ""),
+ )
+ start_context = runtime.take_start_context(sid)
+ except Exception as exc: # noqa: BLE001
+ logger.debug("pre_llm runtime update failed: %s", exc)
+ start_context = ""
# Conversational boundary regulation (user approves/denies access in chat)
try:
@@ -175,7 +220,6 @@ def on_pre_llm_call(
reg = regulate(msg, agent_id=agent_id)
if reg.handled:
# Short user-facing note + keep inject for model
- eng = HermespaceEngine()
desk = load_desk(eng.desk_path)
note = reg.message
block = build_inject_block(desk, max_chars=2000, user_message=msg)
@@ -188,7 +232,8 @@ def on_pre_llm_call(
# Prefer explicit regulation reply as dual-channel: model sees full; user gets note via say path if auto
return {
"context": (
- block
+ ((start_context + "\n\n") if start_context else "")
+ + block
+ "\n\n### Boundary regulation (this turn)\n"
+ f"- action: {reg.action}\n"
+ f"- user_reply_hint: {note}\n"
@@ -199,7 +244,6 @@ def on_pre_llm_call(
except Exception as exc: # noqa: BLE001
logger.debug("regulate failed: %s", exc)
- eng = HermespaceEngine()
desk = load_desk(eng.desk_path)
auto_order = _truthy("HERMESPACE_AUTO_ORDER", "0")
@@ -224,6 +268,13 @@ def on_pre_llm_call(
msg, desk_ready=ready, is_first_turn=bool(is_first_turn)
)
if not do_it:
+ if start_context:
+ try:
+ from hermespace.hermes_runtime import runtime
+
+ runtime.stage_start_context(sid, start_context)
+ except Exception:
+ pass
return None
if msg and ready:
@@ -255,13 +306,16 @@ def on_pre_llm_call(
high_load = str((desk.load or {}).get("level") or "") == "high"
skip_neural = (
high_load
- or _truthy("HERMESPACE_SKIP_NEURAL", "0")
+ # Native pre_llm hooks are latency-sensitive. Neural
+ # enrichment still runs in Workflow/idle paths unless an
+ # operator explicitly opts it into the hook.
+ or _truthy("HERMESPACE_SKIP_NEURAL", "1")
or _truthy("HERMESPACE_HIGH_LOAD_LEAN", "1")
and high_load
)
if high_load:
skip_neural = True
- if not skip_neural and not _truthy("HERMESPACE_SKIP_NEURAL", "0"):
+ if not skip_neural and not _truthy("HERMESPACE_SKIP_NEURAL", "1"):
ns = NeuralSpace()
ns.config.verbalize = False
ns.sync_from_desk(desk, user_message=msg)
@@ -294,6 +348,8 @@ def on_pre_llm_call(
block = build_inject_block(desk, max_chars=inject_cap, user_message=msg)
if not block.strip():
return None
+ if start_context and bool(is_first_turn):
+ block = (start_context + "\n\n" + block).strip()
try:
from hermespace.world import world_context
@@ -356,7 +412,7 @@ def on_pre_llm_call(
from hermespace.access.oew import ensure_oew_env_default
ensure_oew_env_default()
- js = AccessHub(agent_id=agent_id)
+ js = AccessHub(agent_id=access_id)
js.sync_from_desk(desk, user_message=msg, cube_strip=cube_block)
desk.meta["cube_beat"] = {
"ok": beat.get("ok"),
@@ -366,7 +422,7 @@ def on_pre_llm_call(
try:
from hermespace.access import AccessEnv
- env = AccessEnv(agent_id=agent_id)
+ env = AccessEnv(agent_id=access_id)
# sync_from_desk already ran above — skip second rewrite
env_meta = env.advance_turn(
user_message=msg,
@@ -446,10 +502,14 @@ def on_pre_llm_call(
if not high_load:
try:
- from hermespace import ops as ops_mod
-
- block += "\n" + ops_mod.compact_status(
- agent_id=agent_id if agent_id != "hermes-agent" else "default"
+ from hermespace import AccessEngine
+
+ metrics = AccessEngine(agent_id=agent_id, session_id=sid).metrics()
+ block += (
+ "\n### Hermespace runtime\n"
+ f"- hub={metrics.get('hub_n')}/{metrics.get('hub_cap')} "
+ f"focus={metrics.get('focus_n')}/{metrics.get('focus_cap')} "
+ f"silent={metrics.get('silent_n')}/{metrics.get('silent_cap')}\n"
)
except Exception:
pass
@@ -484,27 +544,163 @@ def on_pre_llm_call(
return result
-def on_session_end(**kwargs: Any) -> None:
- if not _truthy("HERMESPACE_IDLE_ON_SESSION_END", "1"):
- return
+def on_post_llm_call(
+ *,
+ session_id: str = "",
+ user_message: str = "",
+ assistant_response: str = "",
+ model: str = "",
+ platform: str = "",
+ **kwargs: Any,
+) -> None:
+ """Close the loop after a successful native Hermes turn."""
+
agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
try:
- from hermespace.world import WorldModel
+ from hermespace import AccessEngine
+
+ AccessEngine(
+ agent_id=agent_id,
+ session_id=str(session_id or "default"),
+ ).observe_turn(
+ user_message=user_message,
+ assistant_response=assistant_response,
+ model=model,
+ platform=platform,
+ )
+ except Exception as exc: # noqa: BLE001
+ logger.debug("post_llm observation failed: %s", exc)
+ try:
+ from hermespace.hermes_runtime import runtime
+
+ runtime.post_llm(
+ session_id,
+ agent_id=agent_id,
+ response_chars=len(assistant_response or ""),
+ model=model,
+ platform=platform,
+ )
+ except Exception as exc: # noqa: BLE001
+ logger.debug("post_llm runtime update failed: %s", exc)
+
+
+def on_post_tool_call(
+ *,
+ tool_name: str = "",
+ session_id: str = "",
+ task_id: str = "",
+ **kwargs: Any,
+) -> None:
+ """Record bounded tool-name telemetry; never persist args or results."""
+
+ agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
+ try:
+ from hermespace.hermes_runtime import runtime
+
+ runtime.tool(
+ session_id or task_id or "default",
+ agent_id=agent_id,
+ name=tool_name,
+ )
+ except Exception as exc: # noqa: BLE001
+ logger.debug("post_tool runtime update failed: %s", exc)
+
+
+def on_subagent_start(*, session_id: str = "", task_id: str = "", **kwargs: Any) -> None:
+ agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
+ try:
+ from hermespace.hermes_runtime import runtime
+
+ runtime.subagent(session_id or task_id, agent_id=agent_id, started=True)
+ except Exception:
+ pass
+
+
+def on_subagent_stop(*, session_id: str = "", task_id: str = "", **kwargs: Any) -> None:
+ agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
+ try:
+ from hermespace.hermes_runtime import runtime
- WorldModel(agent_id=agent_id).leave("session ended")
+ runtime.subagent(session_id or task_id, agent_id=agent_id, started=False)
except Exception:
pass
- # Night path: harvest silent higher-order chain into Cube / semantic
+
+
+def on_session_end(
+ *,
+ session_id: str = "",
+ completed: bool = False,
+ interrupted: bool = False,
+ **kwargs: Any,
+) -> None:
+ """Observe the end of one Hermes turn.
+
+ Hermes v0.20 fires ``on_session_end`` after *every run_conversation call*,
+ not only when the session is destroyed. Full harvest belongs in
+ ``on_session_finalize``.
+ """
+
+ agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
+ try:
+ from hermespace.hermes_runtime import runtime
+
+ runtime.end_turn(
+ session_id,
+ agent_id=agent_id,
+ completed=bool(completed),
+ interrupted=bool(interrupted),
+ )
+ except Exception as exc: # noqa: BLE001
+ logger.debug("turn end runtime update failed: %s", exc)
+
+
+def on_session_finalize(*, session_id: str | None = None, **kwargs: Any) -> None:
+ """Idempotently harvest and release one outgoing Hermes session."""
+
+ if not _truthy("HERMESPACE_IDLE_ON_SESSION_END", "1"):
+ return
+ sid = str(session_id or "default")
+ agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
try:
- from hermespace.access import AccessEnv
+ from hermespace.hermes_runtime import runtime
- AccessEnv(agent_id=agent_id).dream_harvest(seal_to_cube=True, clear_silent=False)
+ if not runtime.finalize(sid, agent_id=agent_id):
+ return
except Exception:
pass
+
+ try:
+ from hermespace.world import WorldModel
+
+ WorldModel(agent_id=agent_id).leave("session finalized")
+ except Exception as exc: # noqa: BLE001
+ logger.debug("world finalize failed: %s", exc)
+ try:
+ from hermespace import AccessEngine
+
+ AccessEngine(agent_id=agent_id, session_id=sid).harvest(clear_silent=False)
+ except Exception as exc: # noqa: BLE001
+ logger.debug("access harvest failed: %s", exc)
try:
from hermespace.workbench import Workbench
- sid = str(kwargs.get("session_id") or "default")
Workbench(agent_id=agent_id, session_id=sid).idle_tick(consolidate_every=1)
except Exception as exc: # noqa: BLE001
- logger.debug("session_end idle failed: %s", exc)
+ logger.debug("session finalize idle failed: %s", exc)
+
+
+def on_session_reset(*, session_id: str = "", **kwargs: Any) -> None:
+ """Prime runtime state for a gateway's newly rotated session key."""
+
+ agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
+ try:
+ from hermespace.hermes_runtime import runtime
+
+ runtime.start(
+ session_id,
+ agent_id=agent_id,
+ model=str(kwargs.get("model") or ""),
+ platform=str(kwargs.get("platform") or ""),
+ )
+ except Exception:
+ pass
diff --git a/src/hermespace/hermes_runtime.py b/src/hermespace/hermes_runtime.py
new file mode 100644
index 0000000..2090d15
--- /dev/null
+++ b/src/hermespace/hermes_runtime.py
@@ -0,0 +1,243 @@
+"""Hermes v0.20 plugin runtime state and lifecycle telemetry.
+
+The Hermes hook host is multi-surface and potentially concurrent (CLI,
+gateways, A2A, subagents). This registry keeps bounded, session-scoped
+operational facts and persists snapshots atomically for doctor/status commands.
+It deliberately stores lengths and names rather than prompt or tool payloads.
+"""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import threading
+import time
+from collections import OrderedDict
+from dataclasses import asdict, dataclass, field
+from datetime import datetime, timezone
+from typing import Any
+
+from hermespace.atomic import atomic_write_text
+from hermespace.paths import state_dir
+
+MAX_SESSIONS = 128
+MAX_RECENT_TOOLS = 24
+
+
+def _utcnow() -> str:
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
+
+
+def _session_key(session_id: str | None) -> str:
+ raw = str(session_id or "no-session")
+ return hashlib.sha256(raw.encode("utf-8", errors="replace")).hexdigest()[:20]
+
+
+@dataclass
+class SessionRuntime:
+ session_key: str
+ agent_id: str
+ model: str = ""
+ platform: str = ""
+ started_at: str = field(default_factory=_utcnow)
+ updated_at: str = field(default_factory=_utcnow)
+ pre_llm_calls: int = 0
+ completed_turns: int = 0
+ interrupted_turns: int = 0
+ failed_turns: int = 0
+ tool_calls: int = 0
+ subagent_starts: int = 0
+ subagent_stops: int = 0
+ last_user_chars: int = 0
+ last_response_chars: int = 0
+ last_event: str = "session_start"
+ finalized: bool = False
+ recent_tools: list[str] = field(default_factory=list)
+
+
+class RuntimeRegistry:
+ """Thread-safe, bounded runtime registry."""
+
+ def __init__(self) -> None:
+ self._lock = threading.RLock()
+ self._sessions: OrderedDict[str, SessionRuntime] = OrderedDict()
+ self._start_context: dict[str, str] = {}
+
+ def _get(
+ self,
+ session_id: str | None,
+ *,
+ agent_id: str = "hermes-agent",
+ model: str = "",
+ platform: str = "",
+ ) -> SessionRuntime:
+ key = _session_key(session_id)
+ item = self._sessions.get(key)
+ if item is None:
+ item = SessionRuntime(
+ session_key=key,
+ agent_id=agent_id or "hermes-agent",
+ model=model or "",
+ platform=platform or "",
+ )
+ self._sessions[key] = item
+ else:
+ if model:
+ item.model = model
+ if platform:
+ item.platform = platform
+ self._sessions.move_to_end(key)
+ while len(self._sessions) > MAX_SESSIONS:
+ old_key, _ = self._sessions.popitem(last=False)
+ self._start_context.pop(old_key, None)
+ return item
+
+ def _save(self, item: SessionRuntime) -> None:
+ item.updated_at = _utcnow()
+ path = state_dir() / "runtime" / f"{item.session_key}.json"
+ atomic_write_text(path, json.dumps(asdict(item), indent=2), mode=0o600)
+
+ def start(
+ self,
+ session_id: str | None,
+ *,
+ agent_id: str,
+ model: str = "",
+ platform: str = "",
+ context: str = "",
+ ) -> dict[str, Any]:
+ with self._lock:
+ item = self._get(
+ session_id,
+ agent_id=agent_id,
+ model=model,
+ platform=platform,
+ )
+ item.last_event = "session_start"
+ item.finalized = False
+ if context:
+ self._start_context[item.session_key] = context
+ self._save(item)
+ return asdict(item)
+
+ def take_start_context(self, session_id: str | None) -> str:
+ with self._lock:
+ return self._start_context.pop(_session_key(session_id), "")
+
+ def stage_start_context(self, session_id: str | None, context: str) -> None:
+ """Restore first-turn context when a trivial turn skipped injection."""
+
+ if not context:
+ return
+ with self._lock:
+ self._start_context[_session_key(session_id)] = context
+
+ def pre_llm(
+ self,
+ session_id: str | None,
+ *,
+ agent_id: str,
+ user_chars: int,
+ model: str = "",
+ platform: str = "",
+ ) -> None:
+ with self._lock:
+ item = self._get(
+ session_id,
+ agent_id=agent_id,
+ model=model,
+ platform=platform,
+ )
+ item.pre_llm_calls += 1
+ item.last_user_chars = max(0, int(user_chars))
+ item.last_event = "pre_llm_call"
+ self._save(item)
+
+ def post_llm(
+ self,
+ session_id: str | None,
+ *,
+ agent_id: str,
+ response_chars: int,
+ model: str = "",
+ platform: str = "",
+ ) -> None:
+ with self._lock:
+ item = self._get(
+ session_id,
+ agent_id=agent_id,
+ model=model,
+ platform=platform,
+ )
+ item.completed_turns += 1
+ item.last_response_chars = max(0, int(response_chars))
+ item.last_event = "post_llm_call"
+ self._save(item)
+
+ def tool(self, session_id: str | None, *, agent_id: str, name: str) -> None:
+ with self._lock:
+ item = self._get(session_id, agent_id=agent_id)
+ item.tool_calls += 1
+ clean = (name or "unknown")[:80]
+ item.recent_tools = (item.recent_tools + [clean])[-MAX_RECENT_TOOLS:]
+ item.last_event = "post_tool_call"
+ # Keep the tool loop cheap; checkpoints and turn-finalization flush.
+ if item.tool_calls == 1 or item.tool_calls % 10 == 0:
+ self._save(item)
+
+ def subagent(self, session_id: str | None, *, agent_id: str, started: bool) -> None:
+ with self._lock:
+ item = self._get(session_id, agent_id=agent_id)
+ if started:
+ item.subagent_starts += 1
+ item.last_event = "subagent_start"
+ else:
+ item.subagent_stops += 1
+ item.last_event = "subagent_stop"
+ self._save(item)
+
+ def end_turn(
+ self,
+ session_id: str | None,
+ *,
+ agent_id: str,
+ completed: bool,
+ interrupted: bool,
+ ) -> None:
+ with self._lock:
+ item = self._get(session_id, agent_id=agent_id)
+ if interrupted:
+ item.interrupted_turns += 1
+ elif not completed:
+ item.failed_turns += 1
+ item.last_event = "turn_end"
+ self._save(item)
+
+ def finalize(self, session_id: str | None, *, agent_id: str) -> bool:
+ """Mark finalized and return True only for the first finalizer."""
+
+ with self._lock:
+ item = self._get(session_id, agent_id=agent_id)
+ if item.finalized:
+ return False
+ item.finalized = True
+ item.last_event = "session_finalize"
+ self._start_context.pop(item.session_key, None)
+ self._save(item)
+ return True
+
+ def status(self, session_id: str | None = None) -> dict[str, Any]:
+ with self._lock:
+ if session_id is not None:
+ item = self._sessions.get(_session_key(session_id))
+ return asdict(item) if item else {}
+ active = [asdict(v) for v in self._sessions.values() if not v.finalized]
+ return {
+ "active_sessions": len(active),
+ "tracked_sessions": len(self._sessions),
+ "sessions": active[-10:],
+ "monotonic_ms": int(time.monotonic() * 1000),
+ }
+
+
+runtime = RuntimeRegistry()
diff --git a/src/hermespace/local_model.py b/src/hermespace/local_model.py
index ae101fe..82e967a 100644
--- a/src/hermespace/local_model.py
+++ b/src/hermespace/local_model.py
@@ -2,7 +2,7 @@
Priority (auto):
1. ollama_embed — nomic-embed-text (or HERMESPACE_EMBED_MODEL)
-2. ollama_verbal — small chat model proposes reportable concepts (J-space *role*)
+2. ollama_verbal — small chat model proposes reportable AccessHub concepts
3. hash — deterministic fallback (always on)
Jacobian-lens (anthropics/jacobian-lens) needs torch+transformers+fitted lens;
@@ -138,7 +138,7 @@ def verbalize_workspace(
) -> list[str]:
"""Ask a local chat model which concepts are 'on the desk' (reportable).
- This is a *behavioral* J-space analogue: reportable / task-relevant concepts,
+ This is an Access Workspace behavior: reportable / task-relevant concepts,
not Jacobian activations. Fails soft → [].
"""
concept_lines = "\n".join(f"- {c}" for c in concepts[:16]) or "- (none)"
diff --git a/src/hermespace/ops.py b/src/hermespace/ops.py
index 6f8db59..4309338 100644
--- a/src/hermespace/ops.py
+++ b/src/hermespace/ops.py
@@ -9,7 +9,7 @@
from typing import Any
from hermespace import __version__
-from hermespace.paths import hermespace_home, package_root, state_dir
+from hermespace.paths import canonical_agent_id, hermespace_home, state_dir
def _port_open(host: str, port: int, timeout: float = 0.4) -> bool:
@@ -22,6 +22,7 @@ def _port_open(host: str, port: int, timeout: float = 0.4) -> bool:
def doctor(*, agent_id: str = "default", port: int = 8764, host: str = "127.0.0.1") -> dict[str, Any]:
"""Non-destructive health snapshot for operators and agents."""
+ agent_id = canonical_agent_id(agent_id)
home = hermespace_home()
checks: list[dict[str, Any]] = []
@@ -29,8 +30,13 @@ def add(ok: bool, name: str, detail: str = "") -> None:
checks.append({"ok": ok, "name": name, "detail": detail})
add(True, "version", __version__)
- add(home.is_dir() or True, "hermespace_home", str(home))
- add((package_root() / "src" / "hermespace").is_dir(), "package_src", str(package_root()))
+ add(home.is_dir(), "hermespace_home", str(home))
+ try:
+ import hermespace
+
+ add(True, "package_import", str(Path(hermespace.__file__).resolve()))
+ except Exception as exc:
+ add(False, "package_import", str(exc))
# imports
try:
@@ -95,6 +101,21 @@ def add(ok: bool, name: str, detail: str = "") -> None:
except Exception as exc: # noqa: BLE001
add(False, "access", str(exc))
+ try:
+ from hermespace.hermes_runtime import runtime
+
+ rst = runtime.status()
+ add(
+ True,
+ "hermes_runtime",
+ (
+ f"active={rst.get('active_sessions')} "
+ f"tracked={rst.get('tracked_sessions')}"
+ ),
+ )
+ except Exception as exc: # noqa: BLE001
+ add(False, "hermes_runtime", str(exc))
+
try:
pol = boundary.load_policy()
add(pol.project_write_default == "deny", "boundary_default_deny", pol.project_write_default)
@@ -138,11 +159,10 @@ def add(ok: bool, name: str, detail: str = "") -> None:
hh = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes")).expanduser()
plug = hh / "plugins" / "hermespace"
desk = hh / "desktop-plugins" / "hermespace" / "plugin.js"
- add(plug.exists(), "hermes_plugin_link", str(plug))
+ plugin_ok = (plug / "plugin.yaml").is_file() and (plug / "__init__.py").is_file()
+ add(plugin_ok, "hermes_plugin", str(plug))
add(desk.is_file(), "desktop_plugin", str(desk))
- ok = all(c["ok"] for c in checks if c["name"] not in {"viewport_serve", "hermes_plugin_link", "desktop_plugin"})
- # soft: serve/desktop may be off — still "ops ready" if core ok
core_ok = all(
c["ok"]
for c in checks
@@ -154,10 +174,14 @@ def add(ok: bool, name: str, detail: str = "") -> None:
"viewport_html",
"version",
"access",
+ "package_import",
+ "hermes_runtime",
}
)
+ integration_ok = core_ok and plugin_ok
return {
"ok": core_ok,
+ "integration_ok": integration_ok,
"all_green": all(c["ok"] for c in checks),
"checks": checks,
"home": str(home),
@@ -175,7 +199,7 @@ def _hints(checks: list[dict[str, Any]], port: int) -> list[str]:
out.append(f"Start viewport: hs view --serve --port {port}")
if not by.get("desktop_plugin", {}).get("ok"):
out.append("Install Desktop plugin: ./scripts/install_desktop_plugin.sh then Reload desktop plugins")
- if not by.get("hermes_plugin_link", {}).get("ok"):
+ if not by.get("hermes_plugin", {}).get("ok"):
out.append("Install Hermes plugin: ./scripts/install_hermes.sh && hermes plugins enable hermespace")
if not by.get("pulse_jobs", {}).get("ok"):
out.append("Seed pulse: hs pulse status")
@@ -190,6 +214,7 @@ def boot(
seed_pulse: bool = True,
) -> dict[str, Any]:
"""Bring pocket subsystems to a known-good everyday state."""
+ agent_id = canonical_agent_id(agent_id)
from hermespace import pulse
from hermespace.grid.viewport import write_viewport_files
from hermespace.workbench import Workbench
@@ -224,6 +249,7 @@ def boot(
def tick_all(*, agent_id: str = "default", force_dream: bool = False) -> dict[str, Any]:
"""One operational cycle: pulse tick (+ optional forced dream)."""
+ agent_id = canonical_agent_id(agent_id)
from hermespace import pulse
from hermespace.grid import dream
from hermespace.grid.viewport import write_viewport_files
diff --git a/src/hermespace/paths.py b/src/hermespace/paths.py
index 1317193..e69dfa8 100644
--- a/src/hermespace/paths.py
+++ b/src/hermespace/paths.py
@@ -3,9 +3,20 @@
from __future__ import annotations
import os
+import hashlib
+import re
from pathlib import Path
+def canonical_agent_id(agent_id: str | None = None) -> str:
+ """Resolve the one agent identity used by world, grid, ops, and hooks."""
+
+ value = (agent_id or "").strip()
+ if not value or value == "default":
+ value = os.environ.get("HERMESPACE_AGENT_ID", "").strip()
+ return value or "hermes-agent"
+
+
def hermespace_home() -> Path:
"""State root for desk + episodes.
@@ -38,6 +49,25 @@ def desk_path() -> Path:
return hermespace_home() / "memory" / "hermespace" / "ACTIVE.md"
+def session_scope_id(agent_id: str, session_id: str = "") -> str:
+ """Stable path-safe ID that does not expose opaque Hermes session IDs."""
+
+ agent = re.sub(r"[^a-zA-Z0-9._-]+", "_", agent_id or "hermes-agent")[:64]
+ session = (session_id or "").strip()
+ if not session or session in {"main", "default"}:
+ return agent or "hermes-agent"
+ digest = hashlib.sha256(session.encode("utf-8", errors="replace")).hexdigest()[:16]
+ return f"{agent or 'hermes-agent'}--{digest}"
+
+
+def session_desk_path(agent_id: str, session_id: str = "") -> Path:
+ """Per-session desk for Hermes CLI/gateway/A2A isolation."""
+
+ if not session_id or session_id in {"main", "default"}:
+ return desk_path()
+ return state_dir() / "sessions" / session_scope_id(agent_id, session_id) / "ACTIVE.md"
+
+
def state_dir() -> Path:
return hermespace_home() / "memory" / "hermespace"
diff --git a/src/hermespace/patterns.py b/src/hermespace/patterns.py
index 9e9e134..d567d2c 100644
--- a/src/hermespace/patterns.py
+++ b/src/hermespace/patterns.py
@@ -90,7 +90,7 @@ class Pattern:
),
Pattern(
"desk.verbalizable",
- "Anthropic J-space reportability",
+ "Access Workspace reportability",
"Contents poised for report/speech",
"required say field; cli say",
True,
@@ -98,7 +98,7 @@ class Pattern:
),
Pattern(
"desk.modulable",
- "J-space modulation + attention set",
+ "Access Workspace modulation + attention set",
"Task shifts workspace contents",
"enter CLEAR + recompute_cognition(user_message)",
True,
@@ -106,7 +106,7 @@ class Pattern:
),
Pattern(
"desk.pre_output",
- "J-space silent deliberation",
+ "Access Workspace silent deliberation",
"Workspace before user-visible speech",
"save_desk + pre_llm inject",
True,
@@ -114,7 +114,7 @@ class Pattern:
),
Pattern(
"gate.selectivity",
- "J-space selective mediation",
+ "Access Workspace selective mediation",
"Not all cognition uses workspace",
"gate.should_inject",
True,
diff --git a/src/hermespace/plugin.py b/src/hermespace/plugin.py
new file mode 100644
index 0000000..c2087e0
--- /dev/null
+++ b/src/hermespace/plugin.py
@@ -0,0 +1,134 @@
+"""First-class Hermes Agent v0.20 plugin registration.
+
+This module is both the wheel entry point and the implementation used by the
+source-tree plugin wrappers. Keeping registration inside the installed
+package prevents the "enabled but no-op" split between checkout and pip modes.
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+from typing import Any
+
+logger = logging.getLogger("hermes.plugins.hermespace")
+
+
+def _status_payload(action: str = "status") -> Any:
+ from hermespace import AccessEngine
+
+ engine = AccessEngine()
+ action = (action or "status").strip().lower()
+ if action in {"status", "show"}:
+ return engine.status()
+ if action == "metrics":
+ return engine.metrics()
+ if action == "roles":
+ return engine.access_roles()
+ if action == "lens":
+ return engine.lens(top_k=8, include_silent=False)
+ if action == "runtime":
+ from hermespace.hermes_runtime import runtime
+
+ return runtime.status()
+ if action == "doctor":
+ from hermespace.ops import doctor
+
+ return doctor(agent_id=engine.agent_id)
+ return {
+ "ok": False,
+ "error": f"unknown action: {action}",
+ "actions": ["status", "metrics", "roles", "lens", "runtime", "doctor"],
+ }
+
+
+def _slash_command(raw_args: str) -> str:
+ payload = _status_payload((raw_args or "").strip() or "status")
+ if isinstance(payload, str):
+ return payload[:6000]
+ return json.dumps(payload, indent=2, default=str)[:6000]
+
+
+def _setup_cli(parser: Any) -> None:
+ parser.add_argument(
+ "action",
+ nargs="?",
+ default="status",
+ choices=("status", "metrics", "roles", "lens", "runtime", "doctor"),
+ )
+
+
+def _handle_cli(args: Any) -> int:
+ print(json.dumps(_status_payload(args.action), indent=2, default=str))
+ return 0
+
+
+def register(ctx: Any) -> None:
+ """Register the supported Hermes v0.20 hooks and operator commands."""
+
+ from hermespace import __version__
+ from hermespace.hermes_bridge import (
+ on_post_llm_call,
+ on_post_tool_call,
+ on_pre_llm_call,
+ on_session_end,
+ on_session_finalize,
+ on_session_reset,
+ on_session_start,
+ on_subagent_start,
+ on_subagent_stop,
+ )
+
+ hooks = {
+ "on_session_start": on_session_start,
+ "pre_llm_call": on_pre_llm_call,
+ "post_llm_call": on_post_llm_call,
+ "post_tool_call": on_post_tool_call,
+ "on_session_end": on_session_end,
+ "on_session_finalize": on_session_finalize,
+ "on_session_reset": on_session_reset,
+ "subagent_start": on_subagent_start,
+ "subagent_stop": on_subagent_stop,
+ }
+ registered: list[str] = []
+ for name, callback in hooks.items():
+ try:
+ ctx.register_hook(name, callback)
+ registered.append(name)
+ except Exception as exc:
+ # Older Hermes versions may not know finalize/reset/subagent hooks.
+ if name in {"on_session_start", "pre_llm_call", "on_session_end"}:
+ raise RuntimeError(f"required Hermes hook unavailable: {name}") from exc
+ logger.info("Hermespace optional hook unavailable: %s (%s)", name, exc)
+
+ if hasattr(ctx, "register_command"):
+ ctx.register_command(
+ "hermespace",
+ handler=_slash_command,
+ description="Hermespace Access Engine status, lens, metrics, and doctor",
+ args_hint="[status|metrics|roles|lens|runtime|doctor]",
+ )
+ if hasattr(ctx, "register_cli_command"):
+ ctx.register_cli_command(
+ name="hermespace",
+ help="Inspect Hermespace Access Engine",
+ description="Status, metrics, roles, runtime, lens, or doctor",
+ setup_fn=_setup_cli,
+ handler_fn=_handle_cli,
+ )
+ if hasattr(ctx, "register_skill"):
+ try:
+ from hermespace.paths import package_root
+
+ skill = package_root() / "skills" / "hermespace"
+ if skill.is_dir():
+ ctx.register_skill("hermespace", skill)
+ except Exception as exc:
+ logger.debug("Hermespace skill registration skipped: %s", exc)
+
+ logger.info(
+ "Hermespace v%s registered %d hooks: %s",
+ __version__,
+ len(registered),
+ ", ".join(registered),
+ )
diff --git a/src/hermespace/store.py b/src/hermespace/store.py
index 90c2065..be663de 100644
--- a/src/hermespace/store.py
+++ b/src/hermespace/store.py
@@ -5,6 +5,7 @@
import json
from pathlib import Path
+from hermespace.atomic import atomic_write_text
from hermespace.desk import Desk
from hermespace.paths import desk_path, state_dir
@@ -19,8 +20,7 @@ def default_state_dir() -> Path:
def save_desk(desk: Desk, path: Path | None = None) -> Path:
path = path or default_desk_path()
- path.parent.mkdir(parents=True, exist_ok=True)
- path.write_text(desk.to_markdown(), encoding="utf-8")
+ atomic_write_text(path, desk.to_markdown())
side = path.with_suffix(".json")
payload = {
"updated": desk.updated,
@@ -37,7 +37,7 @@ def save_desk(desk: Desk, path: Path | None = None) -> Path:
"focus": desk.focus,
"ready": desk.is_ready(),
}
- side.write_text(json.dumps(payload, indent=2), encoding="utf-8")
+ atomic_write_text(side, json.dumps(payload, indent=2))
return path
@@ -45,4 +45,28 @@ def load_desk(path: Path | None = None) -> Desk:
path = path or default_desk_path()
if not path.exists():
return Desk()
- return Desk.from_markdown(path.read_text(encoding="utf-8"))
+ desk = Desk.from_markdown(path.read_text(encoding="utf-8"))
+ side = path.with_suffix(".json")
+ if not side.is_file():
+ return desk
+ try:
+ payload = json.loads(side.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError, TypeError):
+ return desk
+ if not isinstance(payload, dict):
+ return desk
+
+ # Markdown is the human-readable source for intentional desk fields.
+ # The sidecar is authoritative for structured metadata that Markdown
+ # cannot round-trip (fabric cache, world cursor, OEW state, stream stats).
+ if isinstance(payload.get("meta"), dict):
+ desk.meta = dict(payload["meta"])
+ if isinstance(payload.get("load"), dict):
+ desk.load = dict(payload["load"])
+ if isinstance(payload.get("focus"), list):
+ desk.focus = [str(x) for x in payload["focus"]][:4]
+ if isinstance(payload.get("executive"), str):
+ desk.executive = payload["executive"]
+ if isinstance(payload.get("updated"), str):
+ desk.updated = payload["updated"]
+ return desk
diff --git a/src/hermespace/workbench.py b/src/hermespace/workbench.py
index e5ff3d7..a7c262a 100644
--- a/src/hermespace/workbench.py
+++ b/src/hermespace/workbench.py
@@ -20,6 +20,7 @@
from pathlib import Path
from typing import Any
+from hermespace.atomic import atomic_write_text
from hermespace.agent_api import (
decode_bundle,
decode_for_model,
@@ -72,7 +73,16 @@ def __init__(
) -> None:
self.agent_id = (agent_id or "hermes-agent").strip()
self.session_id = (session_id or "default").strip()
- self.workflow = workflow or Workflow()
+ if workflow is None:
+ from hermespace.engine import HermespaceEngine
+ from hermespace.paths import session_desk_path
+
+ workflow = Workflow(
+ engine=HermespaceEngine(
+ desk_path=session_desk_path(self.agent_id, self.session_id)
+ )
+ )
+ self.workflow = workflow
self.root = (root or state_dir() / "workbenches").resolve()
self.root.mkdir(parents=True, exist_ok=True)
self.path = self.root / f"{self._safe(self.agent_id)}__{self._safe(self.session_id)}.json"
@@ -104,8 +114,7 @@ def _load(self) -> WorkbenchState:
def save(self) -> Path:
self.state.updated = _utcnow()
- self.path.parent.mkdir(parents=True, exist_ok=True)
- self.path.write_text(json.dumps(asdict(self.state), indent=2), encoding="utf-8")
+ atomic_write_text(self.path, json.dumps(asdict(self.state), indent=2))
return self.path
def enter(self, *, connect_warehouse: bool = True) -> dict[str, Any]:
@@ -118,6 +127,12 @@ def enter(self, *, connect_warehouse: bool = True) -> dict[str, Any]:
"""
if self.state.mode != "working":
self.state.mode = "idle"
+ try:
+ from hermespace.access.engine import workspace_id
+
+ access_id = workspace_id(self.agent_id, self.session_id)
+ except Exception:
+ access_id = self.agent_id
env = probe_environment()
self.state.meta["environment"] = env.to_dict()
# stamp env concepts onto desk lightly via workflow engine
@@ -147,7 +162,7 @@ def enter(self, *, connect_warehouse: bool = True) -> dict[str, Any]:
from hermespace.access import AccessHub
from hermespace.store import load_desk
- js = AccessHub(agent_id=self.agent_id)
+ js = AccessHub(agent_id=access_id)
desk = load_desk(self.workflow.engine.desk_path)
js.sync_from_desk(desk, user_message=desk.goal or "")
self.state.meta["access"] = {
@@ -172,6 +187,7 @@ def enter(self, *, connect_warehouse: bool = True) -> dict[str, Any]:
query="",
session_id=self.session_id,
room=room,
+ workspace_id=access_id,
)
self.state.meta["connect"] = {
"pulse_ok": pulse.get("ok"),
diff --git a/src/hermespace/workflow.py b/src/hermespace/workflow.py
index 05d5782..6d17c9b 100644
--- a/src/hermespace/workflow.py
+++ b/src/hermespace/workflow.py
@@ -151,9 +151,14 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
try:
from hermespace.cube_module import cube_beat
from hermespace.access import AccessHub, AccessEnv
+ from hermespace.access.engine import workspace_id
from hermespace.access.oew import ensure_oew_env_default
ensure_oew_env_default()
+ access_id = workspace_id(
+ payload.agent_id or "hermes-agent",
+ payload.session_id or "default",
+ )
load_total = float(desk.load.get("total") or 0.5) if isinstance(desk.load, dict) else 0.5
high = str(desk.load.get("level")) == "high" if isinstance(desk.load, dict) else False
seals = None
@@ -173,12 +178,12 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
"load_level": beat.get("load_level"),
"chars": len(cube_block),
}
- js = AccessHub(agent_id=payload.agent_id or "hermes-agent")
+ js = AccessHub(agent_id=access_id)
js.sync_from_desk(desk, user_message=msg, cube_strip=cube_block)
mod = js.parse_modulation(msg)
if mod.get("hold"):
js.hold(str(mod["hold"]), silent=bool(mod.get("silent")))
- env = AccessEnv(agent_id=payload.agent_id or "hermes-agent")
+ env = AccessEnv(agent_id=access_id)
# already_synced: avoid double hub rewrite inside advance_turn
env_meta = env.advance_turn(
user_message=msg,
@@ -226,8 +231,13 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
block = (block + "\n\n" + cube_block).strip()
try:
from hermespace.access import AccessHub, AccessEnv
+ from hermespace.access.engine import workspace_id
- env = AccessEnv(agent_id=payload.agent_id or "hermes-agent")
+ access_id = workspace_id(
+ payload.agent_id or "hermes-agent",
+ payload.session_id or "default",
+ )
+ env = AccessEnv(agent_id=access_id)
high = str(desk.load.get("level")) == "high" if isinstance(desk.load, dict) else False
jblock = oew_broadcast or env.filtered_broadcast(high_load=high)
if jblock:
@@ -240,7 +250,7 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
lens_md = env.lens_markdown(top_k=6, include_silent=True)
if lens_md and len(block) + len(lens_md) < inject_cap + 800:
block = (block + "\n\n" + lens_md).strip()
- js = AccessHub(agent_id=payload.agent_id or "hermes-agent")
+ js = AccessHub(agent_id=access_id)
if js.parse_modulation(msg).get("summon"):
report = (report + "\n\n" + env.lens_markdown(include_silent=False)).strip()
# Final sticky reshape (in case summon appended text)
diff --git a/tests/test_access_engine.py b/tests/test_access_engine.py
index eee7234..12a02d7 100644
--- a/tests/test_access_engine.py
+++ b/tests/test_access_engine.py
@@ -104,6 +104,18 @@ def test_hermes_base_is_engine(self) -> None:
hb = HermesBase(agent_id="alias")
self.assertEqual(hb.status()["engine"], "AccessEngine")
+ def test_sessions_have_isolated_hubs_and_desks(self) -> None:
+ from hermespace import AccessEngine
+
+ first = AccessEngine(agent_id="shared-agent", session_id="session-a")
+ second = AccessEngine(agent_id="shared-agent", session_id="session-b")
+ self.assertNotEqual(first.workspace_id, second.workspace_id)
+ self.assertNotEqual(first.desk_engine.desk_path, second.desk_engine.desk_path)
+
+ first.hold("private-to-session-a")
+ self.assertIn("private-to-session-a", first.lens())
+ self.assertNotIn("private-to-session-a", second.lens())
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_hermes_bridge.py b/tests/test_hermes_bridge.py
index 02387a1..6220c41 100644
--- a/tests/test_hermes_bridge.py
+++ b/tests/test_hermes_bridge.py
@@ -10,8 +10,13 @@ def test_session_and_pre_llm(self):
os.environ["HERMESPACE_HOME"] = td
os.environ["HERMESPACE_NEURAL_VERBALIZE"] = "0"
os.environ["HERMESPACE_AUTO_ORDER"] = "0"
- from hermespace.hermes_bridge import on_session_start, on_pre_llm_call, on_session_end
- from hermespace.engine import HermespaceEngine
+ from hermespace import AccessEngine
+ from hermespace.hermes_bridge import (
+ on_pre_llm_call,
+ on_session_end,
+ on_session_finalize,
+ on_session_start,
+ )
from hermespace.store import load_desk
r = on_session_start(session_id="bridge-test")
self.assertIsInstance(r, dict)
@@ -19,7 +24,12 @@ def test_session_and_pre_llm(self):
self.assertTrue(
"Access Engine" in r["context"] or "Workbench" in r["context"]
)
- desk = load_desk()
+ desk = load_desk(
+ AccessEngine(
+ agent_id="hermes-agent",
+ session_id="bridge-test",
+ ).desk_engine.desk_path
+ )
self.assertTrue(desk.goal)
# material message should inject after desk ready
inj = on_pre_llm_call(
@@ -35,7 +45,12 @@ def test_session_and_pre_llm(self):
or "Dual decode" in inj["context"]
or "Access Workspace" in inj["context"]
)
- on_session_end(session_id="bridge-test")
+ on_session_end(
+ session_id="bridge-test",
+ completed=True,
+ interrupted=False,
+ )
+ on_session_finalize(session_id="bridge-test")
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_plugin_contract.py b/tests/test_plugin_contract.py
new file mode 100644
index 0000000..f6b90da
--- /dev/null
+++ b/tests/test_plugin_contract.py
@@ -0,0 +1,130 @@
+"""Current Hermes Agent v0.20 plugin host contract."""
+
+from __future__ import annotations
+
+import argparse
+import os
+import tempfile
+import unittest
+from pathlib import Path
+from typing import Any
+
+
+class FakeContext:
+ def __init__(self) -> None:
+ self.hooks: dict[str, Any] = {}
+ self.commands: dict[str, Any] = {}
+ self.cli: dict[str, Any] = {}
+ self.skills: dict[str, Path] = {}
+
+ def register_hook(self, name: str, callback: Any) -> None:
+ self.hooks[name] = callback
+
+ def register_command(self, name: str, handler: Any, **kwargs: Any) -> None:
+ self.commands[name] = {"handler": handler, **kwargs}
+
+ def register_cli_command(self, **kwargs: Any) -> None:
+ self.cli[kwargs["name"]] = kwargs
+
+ def register_skill(self, name: str, path: Path) -> None:
+ self.skills[name] = Path(path)
+
+
+class TestPluginContract(unittest.TestCase):
+ def setUp(self) -> None:
+ self.tmp = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self.tmp.name
+ os.environ["HERMESPACE_AGENT_ID"] = "plugin-contract"
+ os.environ["HERMESPACE_SKIP_NEURAL"] = "1"
+
+ def tearDown(self) -> None:
+ self.tmp.cleanup()
+ os.environ.pop("HERMESPACE_HOME", None)
+ os.environ.pop("HERMESPACE_AGENT_ID", None)
+ os.environ.pop("HERMESPACE_SKIP_NEURAL", None)
+
+ def test_registration_and_native_lifecycle(self) -> None:
+ from hermespace.plugin import register
+
+ ctx = FakeContext()
+ register(ctx)
+ required = {
+ "on_session_start",
+ "pre_llm_call",
+ "post_llm_call",
+ "post_tool_call",
+ "on_session_end",
+ "on_session_finalize",
+ }
+ self.assertTrue(required.issubset(ctx.hooks), ctx.hooks)
+ self.assertIn("hermespace", ctx.commands)
+ self.assertIn("hermespace", ctx.cli)
+
+ common = {
+ "session_id": "native-session",
+ "model": "test/model",
+ "platform": "cli",
+ }
+ ctx.hooks["on_session_start"](**common)
+ result = ctx.hooks["pre_llm_call"](
+ **common,
+ user_message="First inspect, then implement, finally verify",
+ conversation_history=[],
+ is_first_turn=True,
+ )
+ self.assertIsInstance(result, dict)
+ self.assertIn("Access Workspace", result["context"])
+ self.assertLess(len(result["context"]), 15_000)
+
+ ctx.hooks["post_tool_call"](
+ **common,
+ tool_name="read_file",
+ args={"secret": "must-not-persist"},
+ result="private result",
+ task_id="native-session",
+ duration_ms=1,
+ )
+ ctx.hooks["post_llm_call"](
+ **common,
+ user_message="implement",
+ assistant_response="Implemented and verified.",
+ conversation_history=[],
+ )
+ ctx.hooks["on_session_end"](
+ **common,
+ completed=True,
+ interrupted=False,
+ )
+
+ from hermespace.hermes_runtime import runtime
+
+ before = runtime.status("native-session")
+ self.assertFalse(before["finalized"])
+ self.assertEqual(before["completed_turns"], 1)
+ self.assertEqual(before["tool_calls"], 1)
+ serialized = str(before)
+ self.assertNotIn("must-not-persist", serialized)
+ self.assertNotIn("private result", serialized)
+
+ ctx.hooks["on_session_finalize"](**common)
+ after = runtime.status("native-session")
+ self.assertTrue(after["finalized"])
+ ctx.hooks["on_session_finalize"](**common) # idempotent
+
+ def test_commands_are_operational(self) -> None:
+ from hermespace.plugin import register
+
+ ctx = FakeContext()
+ register(ctx)
+ output = ctx.commands["hermespace"]["handler"]("metrics")
+ self.assertIn("hub_cap", output)
+
+ parser = argparse.ArgumentParser()
+ cli = ctx.cli["hermespace"]
+ cli["setup_fn"](parser)
+ args = parser.parse_args(["status"])
+ self.assertEqual(args.action, "status")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_store.py b/tests/test_store.py
new file mode 100644
index 0000000..8c701da
--- /dev/null
+++ b/tests/test_store.py
@@ -0,0 +1,68 @@
+"""Persistence contracts for the active desk."""
+
+from __future__ import annotations
+
+import json
+import tempfile
+import unittest
+from pathlib import Path
+
+from hermespace.desk import Desk
+from hermespace.store import load_desk, save_desk
+
+
+class TestDeskStore(unittest.TestCase):
+ def test_sidecar_metadata_round_trip(self) -> None:
+ with tempfile.TemporaryDirectory() as td:
+ path = Path(td) / "ACTIVE.md"
+ desk = Desk(
+ goal="Ship",
+ decision="A — proceed",
+ say="Proceeding.",
+ focus=["[verbal|0.90] verify"],
+ load={"level": "high", "total": 0.8},
+ executive="protect",
+ meta={
+ "fabric": {"skill_hits": [{"name": "pytest"}]},
+ "_fabric_goal": "Ship",
+ "world_entry_count": 17,
+ "neural": {"backend": "hash"},
+ },
+ )
+ save_desk(desk, path)
+ restored = load_desk(path)
+
+ self.assertEqual(restored.meta, desk.meta)
+ self.assertEqual(restored.load, desk.load)
+ self.assertEqual(restored.focus, desk.focus)
+ self.assertEqual(restored.executive, desk.executive)
+
+ def test_corrupt_sidecar_falls_back_to_markdown(self) -> None:
+ with tempfile.TemporaryDirectory() as td:
+ path = Path(td) / "ACTIVE.md"
+ desk = Desk(goal="Recover", decision="A", say="Ready")
+ save_desk(desk, path)
+ path.with_suffix(".json").write_text("{broken", encoding="utf-8")
+
+ restored = load_desk(path)
+ self.assertEqual(restored.goal, "Recover")
+ self.assertEqual(restored.decision, "A")
+
+ def test_atomic_save_leaves_valid_json(self) -> None:
+ with tempfile.TemporaryDirectory() as td:
+ path = Path(td) / "ACTIVE.md"
+ for idx in range(20):
+ desk = Desk(
+ goal=f"Goal {idx}",
+ decision="A",
+ say="Ready",
+ meta={"iteration": idx},
+ )
+ save_desk(desk, path)
+ payload = json.loads(path.with_suffix(".json").read_text(encoding="utf-8"))
+ self.assertEqual(payload["meta"]["iteration"], idx)
+ self.assertEqual(list(Path(td).glob("*.tmp")), [])
+
+
+if __name__ == "__main__":
+ unittest.main()
From 4d9e56eeb8216fd0eb54ca7a83705f2d58c48bbe Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Thu, 6 Aug 2026 03:39:20 +0000
Subject: [PATCH 10/16] test: validate current Hermes plugin manager
Co-authored-by: Pablo
---
scripts/verify_hermes_integration.py | 47 ++++++++++++++++++++++++++++
src/hermespace/access/env.py | 3 +-
src/hermespace/world.py | 6 +++-
3 files changed, 54 insertions(+), 2 deletions(-)
diff --git a/scripts/verify_hermes_integration.py b/scripts/verify_hermes_integration.py
index 89bc7cb..6cbebbc 100755
--- a/scripts/verify_hermes_integration.py
+++ b/scripts/verify_hermes_integration.py
@@ -66,6 +66,51 @@ def _load_repo_plugin() -> Any:
return module
+def _verify_live_hermes_manager() -> dict[str, Any]:
+ """Use the installed Hermes PluginManager when available."""
+
+ try:
+ from hermes_cli.plugins import PluginManager, PluginManifest
+ except ImportError:
+ return {"available": False, "reason": "hermes_cli not importable"}
+
+ manager = PluginManager()
+ manifest = PluginManifest(
+ name="hermespace",
+ version="0.25.0",
+ description="Hermespace host-contract verification",
+ source="user",
+ path=str(ROOT),
+ kind="standalone",
+ key="hermespace",
+ )
+ manager._load_plugin(manifest) # current Hermes public loader delegates here
+ loaded = manager._plugins.get("hermespace")
+ if loaded is None or loaded.error:
+ raise AssertionError(
+ f"current Hermes PluginManager failed to load Hermespace: "
+ f"{getattr(loaded, 'error', 'missing result')}"
+ )
+ required = {
+ "on_session_start",
+ "pre_llm_call",
+ "post_llm_call",
+ "post_tool_call",
+ "on_session_end",
+ "on_session_finalize",
+ }
+ if not required.issubset(set(loaded.hooks_registered)):
+ raise AssertionError(
+ f"current Hermes manager missing hooks: "
+ f"{sorted(required - set(loaded.hooks_registered))}"
+ )
+ return {
+ "available": True,
+ "hooks": sorted(loaded.hooks_registered),
+ "commands": sorted(loaded.commands_registered),
+ }
+
+
def main() -> int:
with tempfile.TemporaryDirectory(prefix="hermespace-host-contract-") as tmp:
old_home = os.environ.get("HERMESPACE_HOME")
@@ -148,6 +193,7 @@ def main() -> int:
if "tracked_sessions" not in command_output:
raise AssertionError("/hermespace runtime returned unexpected output")
+ live_host = _verify_live_hermes_manager()
report = {
"ok": True,
"hooks": sorted(ctx.hooks),
@@ -158,6 +204,7 @@ def main() -> int:
"turns": after.get("completed_turns"),
"tools": after.get("tool_calls"),
"finalized": after.get("finalized"),
+ "current_hermes_manager": live_host,
}
print(json.dumps(report, indent=2))
return 0
diff --git a/src/hermespace/access/env.py b/src/hermespace/access/env.py
index 313693a..2e8bb60 100644
--- a/src/hermespace/access/env.py
+++ b/src/hermespace/access/env.py
@@ -16,6 +16,7 @@
from pathlib import Path
from typing import Any
+from hermespace.atomic import atomic_write_text
from hermespace.access.hub import HUB_CAP, AccessHub, WorkspaceConcept, get_access_hub
from hermespace.paths import state_dir
@@ -129,7 +130,7 @@ def _load_env(self) -> dict[str, Any]:
return {"pov": "", "band": "early", "reflections": [], "protocol_enabled": True}
def _save_env(self) -> None:
- self.env_path.write_text(json.dumps(self._env, indent=2), encoding="utf-8")
+ atomic_write_text(self.env_path, json.dumps(self._env, indent=2))
def _trace(self, kind: str, **payload: Any) -> None:
rec = {"ts": _utcnow(), "kind": kind, "agent_id": self.agent_id, **payload}
diff --git a/src/hermespace/world.py b/src/hermespace/world.py
index 76bd49e..299e90c 100644
--- a/src/hermespace/world.py
+++ b/src/hermespace/world.py
@@ -22,6 +22,7 @@
from pathlib import Path
from typing import Any
+from hermespace.atomic import atomic_write_text
from hermespace.paths import state_dir
@@ -295,7 +296,10 @@ def _load(self) -> WorldState:
def save(self) -> Path:
self._state.updated = _utcnow()
self._state.world_time = _utcnow()
- self.path.write_text(json.dumps(asdict(self._state), indent=2, default=str), encoding="utf-8")
+ atomic_write_text(
+ self.path,
+ json.dumps(asdict(self._state), indent=2, default=str),
+ )
return self.path
@property
From 9b90a9e5b389552c04cc4af411dae6cef34101fa Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Thu, 6 Aug 2026 03:45:45 +0000
Subject: [PATCH 11/16] fix: bound hooks and unify runtime identity
Co-authored-by: Pablo
---
.github/workflows/ci.yml | 10 ++++++++++
src/hermespace/cli.py | 6 ++++--
src/hermespace/grid/api.py | 3 ++-
src/hermespace/hermes_bridge.py | 23 +++++++++++++++++++++--
src/hermespace/plugin.py | 2 +-
src/hermespace/pulse.py | 8 ++++++++
6 files changed, 46 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 09efc8c..2fc7dc6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -51,6 +51,16 @@ jobs:
- run: python -m pip install .
- name: Security audit
run: ./scripts/security_audit.sh
+ - name: Lint production additions
+ run: |
+ python -m pip install ruff
+ python -m ruff check --isolated \
+ src/hermespace/atomic.py \
+ src/hermespace/hermes_runtime.py \
+ src/hermespace/plugin.py \
+ scripts/verify_hermes_integration.py \
+ tests/test_plugin_contract.py \
+ tests/test_store.py
- name: Smoke
run: ./scripts/smoke_test.sh
- name: Ops end-to-end
diff --git a/src/hermespace/cli.py b/src/hermespace/cli.py
index 2d1944c..572c77a 100644
--- a/src/hermespace/cli.py
+++ b/src/hermespace/cli.py
@@ -984,8 +984,9 @@ def main(argv: list[str] | None = None) -> int:
from hermespace.grid import Grid
from hermespace.grid.lenses import list_lenses
from hermespace.grid.gates import gate_status
+ from hermespace.paths import canonical_agent_id
- aid = getattr(args, "agent_id", None) or "default"
+ aid = canonical_agent_id(getattr(args, "agent_id", None))
g = Grid(aid)
cmd = args.grid_cmd
if cmd == "status":
@@ -1135,6 +1136,7 @@ def main(argv: list[str] | None = None) -> int:
return 2
if args.cmd == "view":
+ from hermespace.paths import canonical_agent_id
from hermespace.grid.viewport import (
render_html,
render_markdown,
@@ -1142,7 +1144,7 @@ def main(argv: list[str] | None = None) -> int:
write_viewport_files,
)
- aid = args.agent_id
+ aid = canonical_agent_id(args.agent_id)
if args.serve:
from hermespace.grid.view_server import serve
diff --git a/src/hermespace/grid/api.py b/src/hermespace/grid/api.py
index 2a347fa..c609b6a 100644
--- a/src/hermespace/grid/api.py
+++ b/src/hermespace/grid/api.py
@@ -7,13 +7,14 @@
from hermespace.grid import dream, gates, lenses, missions, scars, selftalk, skillbench, title_tree
from hermespace.grid.lenses import lens_inject_block
from hermespace.grid.selftalk import as_model_context
+from hermespace.paths import canonical_agent_id
class Grid:
"""One agent’s Hermespace grid surface."""
def __init__(self, agent_id: str = "default") -> None:
- self.agent_id = agent_id or "default"
+ self.agent_id = canonical_agent_id(agent_id)
# --- missions ---
def add_mission(self, title: str, **kw: Any):
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index a42b20d..5f908a8 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -13,6 +13,25 @@ def _truthy(name: str, default: str = "0") -> bool:
return os.environ.get(name, default).strip().lower() in {"1", "true", "yes", "on"}
+def _bounded_context(text: str) -> str:
+ """Keep native hook output below Hermes's default 10k spill threshold."""
+
+ try:
+ cap = int(os.environ.get("HERMESPACE_PRE_LLM_MAX_CHARS", "9000"))
+ except ValueError:
+ cap = 9000
+ cap = max(2000, min(20_000, cap))
+ if len(text) <= cap:
+ return text
+ tail_n = min(1200, cap // 4)
+ head_n = cap - tail_n - 120
+ return (
+ text[:head_n]
+ + "\n\n[Hermespace context bounded; low-priority middle omitted]\n\n"
+ + text[-tail_n:]
+ )
+
+
def on_session_start(**kwargs: Any) -> dict[str, str] | None:
"""Initialize a Hermes v0.20 session and stage first-turn context.
@@ -231,7 +250,7 @@ def on_pre_llm_call(
pass
# Prefer explicit regulation reply as dual-channel: model sees full; user gets note via say path if auto
return {
- "context": (
+ "context": _bounded_context(
((start_context + "\n\n") if start_context else "")
+ block
+ "\n\n### Boundary regulation (this turn)\n"
@@ -538,7 +557,7 @@ def on_pre_llm_call(
pass
# Prefer dual-channel when host supports unknown keys; context always set
- result: dict[str, str] = {"context": block}
+ result: dict[str, str] = {"context": _bounded_context(block)}
if user_hint:
result["user_reply_hint"] = user_hint
return result
diff --git a/src/hermespace/plugin.py b/src/hermespace/plugin.py
index c2087e0..3e1fba2 100644
--- a/src/hermespace/plugin.py
+++ b/src/hermespace/plugin.py
@@ -123,7 +123,7 @@ def register(ctx: Any) -> None:
skill = package_root() / "skills" / "hermespace"
if skill.is_dir():
ctx.register_skill("hermespace", skill)
- except Exception as exc:
+ except (OSError, RuntimeError, ValueError) as exc:
logger.debug("Hermespace skill registration skipped: %s", exc)
logger.info(
diff --git a/src/hermespace/pulse.py b/src/hermespace/pulse.py
index 2219c91..3454505 100644
--- a/src/hermespace/pulse.py
+++ b/src/hermespace/pulse.py
@@ -24,6 +24,7 @@
from typing import Any, Callable
from hermespace.grid.secure_store import atomic_write_json, grid_root, read_json, safe_name
+from hermespace.paths import canonical_agent_id
def _utcnow() -> str:
@@ -123,6 +124,7 @@ def save_jobs(jobs: list[PulseJob]) -> None:
def ensure_defaults(agent_id: str = "default") -> list[PulseJob]:
"""Seed built-in rhythm if empty."""
+ agent_id = canonical_agent_id(agent_id)
jobs = load_jobs()
if jobs:
return jobs
@@ -576,6 +578,8 @@ def tick(
seed_defaults: bool = True,
) -> dict[str, Any]:
"""Evaluate due jobs. Missed windows coalesce (run once)."""
+ if agent_id is not None:
+ agent_id = canonical_agent_id(agent_id)
# Master switch from pocket controls
if not force:
try:
@@ -644,6 +648,7 @@ def tick(
def status(agent_id: str = "default", *, light: bool = False) -> dict[str, Any]:
"""Job board + world sensors. light=True skips ensure_defaults thrash if empty file ok."""
+ agent_id = canonical_agent_id(agent_id)
if not light:
ensure_defaults(agent_id)
elif not _jobs_path().is_file():
@@ -697,6 +702,7 @@ def status(agent_id: str = "default", *, light: bool = False) -> dict[str, Any]:
def compact_summary(agent_id: str = "default") -> dict[str, Any]:
"""Cheap pulse blurb for viewport snapshots."""
+ agent_id = canonical_agent_id(agent_id)
st = status(agent_id, light=True)
jobs = st.get("jobs") or []
return {
@@ -709,6 +715,8 @@ def compact_summary(agent_id: str = "default") -> dict[str, Any]:
def daemon_loop(interval_sec: int = 60, *, agent_id: str | None = None, max_ticks: int = 0) -> None:
"""In-process loop. max_ticks=0 means forever."""
+ if agent_id is not None:
+ agent_id = canonical_agent_id(agent_id)
n = 0
interval_sec = max(5, int(interval_sec))
print(f"pulse daemon interval={interval_sec}s agent={agent_id or '*'}", flush=True)
From 079010ad32522f24dfd3a3df99889d3a616df44d Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Thu, 6 Aug 2026 03:46:36 +0000
Subject: [PATCH 12/16] fix(install): prefer the selected checkout runtime
Co-authored-by: Pablo
---
scripts/install_hermes.sh | 3 +++
1 file changed, 3 insertions(+)
diff --git a/scripts/install_hermes.sh b/scripts/install_hermes.sh
index fd34968..5743e2e 100755
--- a/scripts/install_hermes.sh
+++ b/scripts/install_hermes.sh
@@ -25,6 +25,9 @@ done
# shellcheck source=scripts/_python.sh
source "$ROOT/scripts/_python.sh"
+# Put this checkout first even when an older editable Hermespace is already in
+# the operator's PYTHONPATH.
+export PYTHONPATH="$ROOT/src${PYTHONPATH:+:$PYTHONPATH}"
echo "Hermespace production install"
echo " CHECKOUT=$ROOT"
From 1193fd93871a164f67a3ea5da6f8d0b2cbdf5c8b Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 13:43:46 +0000
Subject: [PATCH 13/16] feat(0.25): cable Cube prefetch skip and Insight
perceive card
Land the remaining operational-desk work on Access Engine rebased onto current main:
- Insight soft-import (bounded perceive card, fail-soft, high-load skip)
- Skip or shrink cube_beat when Hermes memory.provider already prefetched
- World projects from the Cube book instead of a second JSONL archive
- plugin.yaml provides_hooks for all 9 hooks plus advisory python_dependencies
- Drop leftover ILO_HOME / ilo-finish-report strings without brand blocklists
Co-authored-by: Pablo
---
PURPOSE.md | 6 +-
README.md | 12 +-
desktop_plugin/hermespace/README.md | 2 +-
desktop_plugin/hermespace/plugin.js | 4 +-
docs/architecture/HERMESCUBE.md | 4 +-
docs/architecture/INSIGHT.md | 22 +++
hermes_plugin/plugin.yaml | 14 +-
plugin.yaml | 14 +-
scripts/new_desk.sh | 2 +-
scripts/show_desk.sh | 2 +-
skills/hermespace/SKILL.md | 6 +-
src/hermespace/__init__.py | 2 +
src/hermespace/access/engine.py | 6 +
src/hermespace/cube_module.py | 131 +++++++++++++++-
src/hermespace/grid/lenses.py | 2 +-
src/hermespace/hermes_bridge.py | 30 ++++
src/hermespace/insight_module.py | 223 ++++++++++++++++++++++++++++
src/hermespace/paths.py | 8 +-
src/hermespace/workflow.py | 33 ++++
src/hermespace/world.py | 56 ++++++-
tests/test_access_hub.py | 12 ++
tests/test_insight_module.py | 168 +++++++++++++++++++++
tests/test_plugin_contract.py | 20 +++
tests/test_world.py | 38 +++++
24 files changed, 786 insertions(+), 31 deletions(-)
create mode 100644 docs/architecture/INSIGHT.md
create mode 100644 src/hermespace/insight_module.py
create mode 100644 tests/test_insight_module.py
diff --git a/PURPOSE.md b/PURPOSE.md
index 22acd33..1b2ab79 100644
--- a/PURPOSE.md
+++ b/PURPOSE.md
@@ -91,7 +91,11 @@ World identity may remain agent-scoped; active cognition is session-scoped.
| Workbench | Session mode, parked goals, last native turn |
| WorldModel | Agent-scoped beliefs, landmarks, timeline |
| Hermes Agent | Model calls, tools, skills, approvals, transcript |
-| Optional warehouse | Long-tail recall and consolidation only |
+| Optional Cube book | Durable long-tail SoT when `hermescube` is installed |
+| Optional Insight | Bounded perceive card when `hermes_insight` is installed |
+
+Cube and Insight stay standalone packages. Hermespace only **cables** them
+via fail-soft imports (`cube_module`, `insight_module`). Neither is required.
Hermespace does **not**:
diff --git a/README.md b/README.md
index e4791ac..ff163b1 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,9 @@ subagents, turn boundaries, and finalization.
## The World Model
-A persistent agent world that grows forever. The archive is the source of truth; the cache is never authoritative.
+A persistent agent world. Standalone, the local JSONL warehouse may grow.
+When HermesCube is installed, World **projects from the Cube book** and does
+not grow a second forever-archive.
### Archive
@@ -56,7 +58,9 @@ print(wm.render_markdown()) # epoch-aware markdown
wm.evolve() # consolidate, detect patterns, check epoch
```
-Every mutation appends to `~/.hermespace/worlds/{agent_id}_archive.jsonl` — an append-only JSONL that grows forever. No pruning. No decay. No deletion.
+Standalone mutations append to `~/.hermespace/worlds/{agent_id}_archive.jsonl`.
+With Cube, World is a projection (`pulse_charge` / `sync_world_beliefs`) and
+that JSONL is not a second book.
Entry types: `enter`, `leave`, `landmark`, `belief`, `trait`, `evolution`, `focus`, `epoch_transition`, `resolve`, `relationship`.
@@ -137,7 +141,8 @@ Alongside the world, Hermespace provides a desk for the current turn — FOA, du
| **Functional Access Workspace** | Harness global workspace — hold/summon concepts, silent reasoning, FOA≤4, hub≤25 |
| **Focus of Attention** | ≤4 items, single active goal per turn |
| **Dual Decode** | Human gets a short report; the model gets dense context. Never dump raw inject into chat channels. |
-| **Cube heart (optional)** | Soft cable to HermesCube — `beat` / `seal` / `pulse`; standalone warehouse when Cube absent |
+| **Cube heart (optional)** | Soft cable to HermesCube — `beat` / `seal` / `pulse`; skip/shrink when the Cube provider already prefetched; standalone warehouse when Cube absent |
+| **Insight (optional)** | Soft cable to Hermes Insight — bounded perceive card on material turns; never required |
| **Skills + Memory Fabric** | Ranks Hermes skills per goal; injects MEMORY.md / USER.md excerpts |
| **Neural FOA** | `HERMESPACE_NEURAL_BACKEND=auto` — Ollama embeddings when live, hash fallback |
| **Autonomy Grid** | Missions, lenses, dream, self-talk, skillbench, title/tree, access gates. Ground-up design. |
@@ -260,6 +265,7 @@ $HERMESPACE_HOME/memory/hermespace/
| [`ABOUT.md`](ABOUT.md) | Philosophy, design principles, author |
| [`docs/architecture/CODEMAP.md`](docs/architecture/CODEMAP.md) | Where to edit (layer map) |
| [`docs/architecture/HERMESCUBE.md`](docs/architecture/HERMESCUBE.md) | Cube heart/center contract |
+| [`docs/architecture/INSIGHT.md`](docs/architecture/INSIGHT.md) | Insight perceive-card cable |
| [`INTEGRATION.md`](INTEGRATION.md) | Python · CLI · plugin · workbench doors |
| [`skills/hermespace/SKILL.md`](skills/hermespace/SKILL.md) | **Agent skill** (load in Hermes) |
| [`docs/integration/FOR_HERMES.md`](docs/integration/FOR_HERMES.md) | Maintainer / dogfood brief |
diff --git a/desktop_plugin/hermespace/README.md b/desktop_plugin/hermespace/README.md
index d3646c5..cc235ce 100644
--- a/desktop_plugin/hermespace/README.md
+++ b/desktop_plugin/hermespace/README.md
@@ -8,7 +8,7 @@
## Surfaces (v0.17.1)
| Surface | How |
|---------|-----|
-| **Pane** `hermespace` | `area: panes` — dockable like ilo-ops |
+| **Pane** `hermespace` | `area: panes` — dockable |
| **Page** `/hermespace` | `ROUTES_AREA` |
| **Sidebar** Hermespace | `SIDEBAR_NAV_AREA` → openRouteTile |
| **Chip** `hs` | statusBar → navigate page |
diff --git a/desktop_plugin/hermespace/plugin.js b/desktop_plugin/hermespace/plugin.js
index 41fc690..5dd86b3 100644
--- a/desktop_plugin/hermespace/plugin.js
+++ b/desktop_plugin/hermespace/plugin.js
@@ -10,7 +10,7 @@
* (symlink to another tree breaks some Desktop/remote read paths)
*
* Surfaces:
- * 1) panes tile — immediate, dockable (like ilo-ops; users find this)
+ * 1) panes tile — immediate, dockable
* 2) full page /hermespace + sidebar.nav (first-class)
* 3) status chip + palette → navigate /hermespace
*/
@@ -722,7 +722,7 @@ export default {
name: 'Hermespace',
defaultEnabled: true,
register: function (ctx) {
- // 1) Dockable pane — same class as working ilo-ops (always visible if plugins load)
+ // 1) Dockable pane (always visible if plugins load)
ctx.register({
id: 'pane',
area: 'panes',
diff --git a/docs/architecture/HERMESCUBE.md b/docs/architecture/HERMESCUBE.md
index adfb9b9..6d88191 100644
--- a/docs/architecture/HERMESCUBE.md
+++ b/docs/architecture/HERMESCUBE.md
@@ -19,7 +19,7 @@ Hermes Agent ──connect──► Hermespace (J-Space · FOA · OEW)
| Surface | With Cube | Standalone |
|---------|-----------|------------|
| `$HERMES_HOME/memories/memory.cube` | **Durable SoT** | n/a |
-| Hermespace world JSONL | Projection — recharge via `pulse` / `sync_world` | Local warehouse |
+| Hermespace world JSONL | Projection only — recharge via `pulse` / `sync_world`; do not grow a second archive | Local warehouse |
| ACTIVE desk / J-Space hub | Turn FOA (seeded on connect) | Turn FOA |
| Hive (`HERMESCUBE_HIVE`) | Peer room / soul cards | Solo room |
| SemanticStore | Mirror / study | Local seal target |
@@ -45,7 +45,7 @@ from hermespace.cube_module import (
|------|-----|
| `connect_agent(agent_id)` | Session start / `HermesBase.connect` |
| `ensure_heart()` | Create cube or standalone dirs |
-| `cube_beat(query, seals=, load=)` | Turn / `pre_llm_call` |
+| `cube_beat(query, seals=, load=)` | Turn / `pre_llm_call` — skip/shrink when Cube provider already prefetched |
| `cube_pulse` / `sync_world` | Idle + connect charge |
| `room_status` | Hive awareness (`HERMESCUBE_HIVE`) |
| `seal_learning(text)` | Desk → durable archive |
diff --git a/docs/architecture/INSIGHT.md b/docs/architecture/INSIGHT.md
new file mode 100644
index 0000000..d7aa71e
--- /dev/null
+++ b/docs/architecture/INSIGHT.md
@@ -0,0 +1,22 @@
+# Hermes Insight × Hermespace — optional perceive cable
+
+**Hermes Insight stays a standalone package.** Hermespace does not vendor it.
+When `hermes_insight` is importable, Access Engine appends a **bounded perceive
+card** (~400 chars) on material `pre_llm_call` / `AccessEngine.turn`.
+
+```
+from hermespace.insight_module import insight_card, insight_status
+
+rec = insight_card("two workers share one token", goal=desk.goal, plan=desk.plan)
+# rec["card"] → lever / top rule / usable / action_hint
+```
+
+| Rule | Behavior |
+|------|----------|
+| Missing package | Soft-fail (`mode=missing`) — engine still runs |
+| High load | Skip (`skipped=high_load`) |
+| Card | lever, top rule, usable, action_hint — never the lattice |
+| `insight_plan` | Hot path only when `usable` **and** the goal is multi-step |
+| Required? | Never |
+
+Companion: [PabloTheThinker/hermes-insight](https://github.com/PabloTheThinker/hermes-insight)
diff --git a/hermes_plugin/plugin.yaml b/hermes_plugin/plugin.yaml
index f16a0b0..bffff68 100644
--- a/hermes_plugin/plugin.yaml
+++ b/hermes_plugin/plugin.yaml
@@ -4,7 +4,7 @@ version: "0.25.0"
description: >
Production Access Engine for Hermes Agent v0.20+: session-scoped workspace,
bounded model context, native lifecycle telemetry, dual decode, and
- standalone-safe persistence.
+ standalone-safe persistence. Cube and Insight are optional soft-imports.
author: Hermespace contributors
kind: standalone
hooks:
@@ -17,4 +17,16 @@ hooks:
- on_session_reset
- subagent_start
- subagent_stop
+provides_hooks:
+ - on_session_start
+ - pre_llm_call
+ - post_llm_call
+ - post_tool_call
+ - on_session_end
+ - on_session_finalize
+ - on_session_reset
+ - subagent_start
+ - subagent_stop
+python_dependencies:
+ - numpy>=1.24,<3
homepage: https://github.com/PabloTheThinker/hermespace
diff --git a/plugin.yaml b/plugin.yaml
index f16a0b0..bffff68 100644
--- a/plugin.yaml
+++ b/plugin.yaml
@@ -4,7 +4,7 @@ version: "0.25.0"
description: >
Production Access Engine for Hermes Agent v0.20+: session-scoped workspace,
bounded model context, native lifecycle telemetry, dual decode, and
- standalone-safe persistence.
+ standalone-safe persistence. Cube and Insight are optional soft-imports.
author: Hermespace contributors
kind: standalone
hooks:
@@ -17,4 +17,16 @@ hooks:
- on_session_reset
- subagent_start
- subagent_stop
+provides_hooks:
+ - on_session_start
+ - pre_llm_call
+ - post_llm_call
+ - post_tool_call
+ - on_session_end
+ - on_session_finalize
+ - on_session_reset
+ - subagent_start
+ - subagent_stop
+python_dependencies:
+ - numpy>=1.24,<3
homepage: https://github.com/PabloTheThinker/hermespace
diff --git a/scripts/new_desk.sh b/scripts/new_desk.sh
index 77a84dd..a26ea46 100755
--- a/scripts/new_desk.sh
+++ b/scripts/new_desk.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
-HOME_ROOT="${HERMESPACE_HOME:-${ILO_HOME:-$HOME/.hermespace}}"
+HOME_ROOT="${HERMESPACE_HOME:-$HOME/.hermespace}"
DEST="$HOME_ROOT/memory/hermespace/ACTIVE.md"
TEMPLATE="$(cd "$(dirname "$0")/.." && pwd)/runtime/ACTIVE.template.md"
mkdir -p "$(dirname "$DEST")"
diff --git a/scripts/show_desk.sh b/scripts/show_desk.sh
index 90a1ea9..40a6ffb 100755
--- a/scripts/show_desk.sh
+++ b/scripts/show_desk.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
-HOME_ROOT="${HERMESPACE_HOME:-${ILO_HOME:-$HOME/.hermespace}}"
+HOME_ROOT="${HERMESPACE_HOME:-$HOME/.hermespace}"
DEST="$HOME_ROOT/memory/hermespace/ACTIVE.md"
if [[ ! -f "$DEST" ]]; then
echo "no active desk at $DEST"
diff --git a/skills/hermespace/SKILL.md b/skills/hermespace/SKILL.md
index 434df17..41cad3b 100644
--- a/skills/hermespace/SKILL.md
+++ b/skills/hermespace/SKILL.md
@@ -14,9 +14,9 @@ description: >-
**What it is not:** A second LLM runtime · Claude J-space weights · a SaaS brand · a replacement for Hermes skills/MEMORY.
**Package SoT:** `$HERMESPACE_ROOT` (git checkout) · https://github.com/PabloTheThinker/hermespace
-**State:** `$HERMESPACE_HOME` default `~/.hermespace` (optional `ILO_HOME`)
+**State:** `$HERMESPACE_HOME` default `~/.hermespace`
**Hermes:** `$HERMES_HOME` default `~/.hermes`
-**Version:** align `src/hermespace/__init__.py` · `pyproject.toml` · `hermes_plugin/plugin.yaml`
+**Version:** align `src/hermespace/__init__.py` · `pyproject.toml` · `plugin.yaml`
---
@@ -375,7 +375,7 @@ Never end on only a verify table / `ADHOC_PASS`.
- `hermespace-ops` · `hermespace-grid` · `hermespace-everyday` · `hermespace-runtime-ops`
- `pocket-dimension-security`
- `hermes-agent` · `hermes-desktop-surface` · `hermes-local-ops`
-- Finish: `ilo-finish-report` / `material-finish-review` / `session-closeout` when those exist
+- Finish: `session-closeout` / `material-finish-review` when those exist
**Package skill path:** `$HERMESPACE_ROOT/skills/hermespace/SKILL.md`
**Profile install:** `$HERMES_HOME/skills/.../hermespace/SKILL.md` (via `install_hermes.sh`)
diff --git a/src/hermespace/__init__.py b/src/hermespace/__init__.py
index b8d7270..88a63f0 100644
--- a/src/hermespace/__init__.py
+++ b/src/hermespace/__init__.py
@@ -33,6 +33,7 @@
from hermespace.access import evaluate_material_turn
from hermespace.access import AccessEngine, ACCESS_ROLES
from hermespace import cube_module
+from hermespace import insight_module
from hermespace.hermes_base import HermesBase
__all__ = [
@@ -68,6 +69,7 @@
"get_env",
"evaluate_material_turn",
"cube_module",
+ "insight_module",
"HermesBase",
"probe_environment",
"environment_markdown",
diff --git a/src/hermespace/access/engine.py b/src/hermespace/access/engine.py
index bd12f7e..62ffeaf 100644
--- a/src/hermespace/access/engine.py
+++ b/src/hermespace/access/engine.py
@@ -397,6 +397,12 @@ def status(self) -> dict[str, Any]:
}
except Exception:
out["warehouse"] = {"cube_available": False, "optional": True}
+ try:
+ from hermespace.insight_module import insight_status
+
+ out["insight"] = insight_status()
+ except Exception:
+ out["insight"] = {"available": False, "required": False, "optional": True}
if self._last_connect:
out["last_connect"] = {
diff --git a/src/hermespace/cube_module.py b/src/hermespace/cube_module.py
index b71a07f..4015047 100644
--- a/src/hermespace/cube_module.py
+++ b/src/hermespace/cube_module.py
@@ -233,6 +233,81 @@ def cube_status() -> dict[str, Any]:
return heart_status()
+def _truthy_env(name: str) -> bool:
+ return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
+
+
+def hermes_memory_provider() -> str:
+ """Return Hermes ``memory.provider`` when detectable (never required)."""
+ for key in ("HERMES_MEMORY_PROVIDER", "MEMORY_PROVIDER"):
+ raw = os.environ.get(key, "").strip().lower()
+ if raw:
+ return raw
+ home = os.environ.get("HERMES_HOME", "").strip()
+ roots = []
+ if home:
+ roots.append(os.path.expanduser(home))
+ roots.append(os.path.expanduser("~/.hermes"))
+ for root in roots:
+ cfg = os.path.join(root, "config.yaml")
+ try:
+ text = open(cfg, encoding="utf-8").read()
+ except OSError:
+ continue
+ in_memory = False
+ for line in text.splitlines():
+ raw = line.split("#", 1)[0]
+ if raw.strip().startswith("memory:") or raw.strip() == "memory:":
+ in_memory = True
+ continue
+ if in_memory and raw and not raw[:1].isspace() and not raw.startswith("\t"):
+ in_memory = False
+ if not in_memory:
+ continue
+ stripped = raw.strip()
+ if stripped.startswith("provider:"):
+ return stripped.split(":", 1)[1].strip().strip("\"'").lower()
+ return ""
+
+
+def cube_is_memory_provider() -> bool:
+ return hermes_memory_provider() in {"hermescube", "cube"}
+
+
+def cube_already_prefetched(
+ query: str = "",
+ *,
+ session_id: str = "",
+) -> bool:
+ """True when the Cube memory provider already recalled this turn's book.
+
+ Hermes ``memory.provider=hermescube`` prefetches independently. A second
+ full ``cube_beat`` strip dual-pumps the same book — skip or shrink.
+ """
+ if _truthy_env("HERMESPACE_CUBE_PREFETCHED"):
+ return True
+ if not cube_is_memory_provider() or not cube_available():
+ return False
+ try:
+ import gc
+
+ from hermescube.provider import CubeMemoryProvider
+
+ q = (query or "").strip()
+ for obj in gc.get_objects():
+ if not isinstance(obj, CubeMemoryProvider):
+ continue
+ last_q = str(getattr(obj, "_last_prefetch_query", "") or "")
+ last_ids = list(getattr(obj, "_last_prefetch_ids", None) or [])
+ if last_ids:
+ return True
+ if last_q and (not q or last_q[:40] in q or q[:40] in last_q):
+ return True
+ except Exception as e:
+ logger.debug("cube prefetch probe miss: %s", e)
+ return False
+
+
def cube_beat(
query: str = "",
*,
@@ -243,11 +318,51 @@ def cube_beat(
agent_id: str = "hermes-agent",
charge: bool = False,
session_id: str = "hermespace",
+ skip_if_prefetched: bool = True,
) -> dict[str, Any]:
"""One cardiac cycle for a Hermespace turn (Cube center or standalone).
Order: ensure → systole (seal) → diastole (supply) → optional autonomic.
+
+ When Hermes ``memory.provider=hermescube`` already prefetched this turn,
+ skip the full arterial strip (or shrink it) so the same book is not
+ dual-pumped into model context.
"""
+ prefetched = bool(skip_if_prefetched and cube_already_prefetched(query, session_id=session_id))
+ provider_live = cube_is_memory_provider()
+ skip_supply = prefetched
+ shrink_supply = (not skip_supply) and provider_live and cube_available()
+
+ if skip_supply:
+ level = normalize_load(load, high_load=high_load)
+ out: dict[str, Any] = {
+ "api_version": "1.0",
+ "adapter": SPACE_CUBE_ADAPTER_VERSION,
+ "mode": "center" if cube_available() else "standalone",
+ "ok": True,
+ "phases": {"diastole": {"ok": True, "skipped": "provider_prefetch", "chars": 0}},
+ "block": "",
+ "load_level": level,
+ "skipped": "provider_prefetch",
+ }
+ if seals is not None:
+ items = [seals] if isinstance(seals, str) else list(seals)
+ sealed = [
+ seal_learning(str(x), entry_type=entry_type, agent_id=agent_id)
+ for x in items
+ if str(x).strip()
+ ]
+ out["phases"]["systole"] = {
+ "ok": all(r.get("ok") for r in sealed) if sealed else False,
+ "count": sum(1 for r in sealed if r.get("ok")),
+ }
+ if charge:
+ out["phases"]["autonomic"] = cube_pulse(agent_id=agent_id)
+ return out
+
+ beat_load: str | float | None = "protect" if shrink_supply else load
+ beat_high = True if shrink_supply else high_load
+
try:
from hermescube.center import beat
@@ -255,8 +370,8 @@ def cube_beat(
query or "",
seals=seals,
entry_type=entry_type,
- load=load,
- high_load=high_load,
+ load=beat_load,
+ high_load=beat_high,
agent_id=agent_id,
charge=charge,
session_id=session_id,
@@ -264,13 +379,19 @@ def cube_beat(
if isinstance(out, dict):
out["adapter"] = SPACE_CUBE_ADAPTER_VERSION
out["mode"] = "center"
+ if shrink_supply:
+ out["shrunk"] = "provider_active"
+ block = str(out.get("block") or "")
+ cap = strip_budget("protect")
+ if len(block) > cap:
+ out["block"] = block[: cap - 3] + "..."
return out
except Exception as e:
logger.debug("center.beat miss: %s", e)
# Heart 1.0 / standalone fallback
- level = normalize_load(load, high_load=high_load)
- out: dict[str, Any] = {
+ level = normalize_load(beat_load, high_load=beat_high)
+ out = {
"api_version": "1.0",
"adapter": SPACE_CUBE_ADAPTER_VERSION,
"mode": "standalone" if not cube_available() else "heart",
@@ -279,6 +400,8 @@ def cube_beat(
"block": "",
"load_level": level,
}
+ if shrink_supply:
+ out["shrunk"] = "provider_active"
out["phases"]["ensure"] = ensure_heart()
if seals is not None:
items = [seals] if isinstance(seals, str) else list(seals)
diff --git a/src/hermespace/grid/lenses.py b/src/hermespace/grid/lenses.py
index c0a239c..5093b26 100644
--- a/src/hermespace/grid/lenses.py
+++ b/src/hermespace/grid/lenses.py
@@ -55,7 +55,7 @@
"partner": {
"title": "Partner",
"bias": "Monotropism, finish reports, high-load short say",
- "fabric_boost": ["professional-messaging", "ilo-finish-report", "hermes-agent"],
+ "fabric_boost": ["professional-messaging", "session-closeout", "hermes-agent"],
"inhibit": ["option menus under high load", "leave hanging with only verify table"],
"report_style": "closeout",
},
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index 5f908a8..3e5bcb5 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -437,6 +437,8 @@ def on_pre_llm_call(
"ok": beat.get("ok"),
"mode": beat.get("mode"),
"load_level": beat.get("load_level"),
+ "skipped": beat.get("skipped"),
+ "shrunk": beat.get("shrunk"),
}
try:
from hermespace.access import AccessEnv
@@ -498,6 +500,34 @@ def on_pre_llm_call(
except Exception:
pass
+ try:
+ from hermespace.insight_module import insight_card
+
+ icard = insight_card(
+ msg or desk.goal or "",
+ high_load=high_load,
+ goal=desk.goal or "",
+ plan=list(desk.plan or []),
+ agent_id=agent_id,
+ )
+ desk.meta["insight"] = {
+ "ok": icard.get("ok"),
+ "mode": icard.get("mode"),
+ "usable": icard.get("usable"),
+ "skipped": icard.get("skipped"),
+ "planned": icard.get("planned"),
+ }
+ if icard.get("card"):
+ block += "\n\n" + str(icard["card"])
+ try:
+ from hermespace.store import save_desk
+
+ save_desk(desk)
+ except Exception:
+ pass
+ except Exception:
+ pass
+
# Workbench status — only on first turn or when state changes (skip under high)
if not high_load:
try:
diff --git a/src/hermespace/insight_module.py b/src/hermespace/insight_module.py
new file mode 100644
index 0000000..96eed1f
--- /dev/null
+++ b/src/hermespace/insight_module.py
@@ -0,0 +1,223 @@
+"""Hermes Insight adapter — soft dependency, never required.
+
+Insight (when installed) is a standalone pattern lattice. Hermespace owns
+FOA / Access Engine. This module is the cable, same shape as ``cube_module``:
+
+ feature-detect ``hermes_insight``
+ on material pre_llm / AccessEngine.turn → bounded perceive card (~400 chars)
+ high-load skip · missing package → soft-fail
+ never dump the lattice
+ never call ``insight_plan`` on the hot path unless usable *and* multi-step
+
+See PURPOSE.md. Do not vendor Insight source.
+"""
+
+from __future__ import annotations
+
+import logging
+import os
+import re
+from typing import Any, Sequence
+
+logger = logging.getLogger("hermespace.insight_module")
+
+SPACE_INSIGHT_ADAPTER_VERSION = "1.0"
+INSIGHT_CARD_CHARS = 400
+
+_MULTI_STEP_RE = re.compile(
+ r"\b(first|then|next|after that|finally|and then|step\s*\d)\b",
+ re.IGNORECASE,
+)
+
+
+def insight_available() -> bool:
+ try:
+ import hermes_insight # noqa: F401
+
+ return True
+ except Exception:
+ return False
+
+
+def insight_status() -> dict[str, Any]:
+ """Feature-detect only — never required for Access Engine."""
+ out: dict[str, Any] = {
+ "adapter": SPACE_INSIGHT_ADAPTER_VERSION,
+ "available": False,
+ "required": False,
+ "mode": "missing",
+ "ok": True,
+ "note": "optional soft-import — Hermespace runs without Insight",
+ }
+ try:
+ import hermes_insight
+
+ out["available"] = True
+ out["mode"] = "insight"
+ out["version"] = getattr(hermes_insight, "__version__", None)
+ return out
+ except Exception as e:
+ out["error"] = type(e).__name__
+ return out
+
+
+def _is_multi_step(goal: str = "", plan: Sequence[str] | None = None) -> bool:
+ if plan is not None and len([p for p in plan if str(p).strip()]) >= 2:
+ return True
+ text = (goal or "").strip()
+ if not text:
+ return False
+ if _MULTI_STEP_RE.search(text):
+ return True
+ return text.count(";") >= 2 or text.count("→") >= 2 or text.count("->") >= 2
+
+
+def _bound_card(text: str, *, cap: int = INSIGHT_CARD_CHARS) -> str:
+ s = (text or "").strip()
+ if len(s) <= cap:
+ return s
+ return s[: cap - 3].rstrip() + "..."
+
+
+def _format_card(
+ *,
+ lever: str,
+ top_rule: str,
+ usable: bool,
+ action_hint: str,
+ plan_hint: str = "",
+) -> str:
+ lines = [
+ "### Insight",
+ f"- lever: {(lever or 'unknown')[:80]}",
+ f"- rule: {(top_rule or 'none')[:120]}",
+ f"- usable: {str(bool(usable)).lower()}",
+ f"- hint: {(action_hint or '')[:160]}",
+ ]
+ if plan_hint:
+ lines.append(f"- plan: {plan_hint[:100]}")
+ return _bound_card("\n".join(lines))
+
+
+def insight_card(
+ situation: str,
+ *,
+ observations: Sequence[str] | None = None,
+ high_load: bool = False,
+ goal: str = "",
+ plan: Sequence[str] | None = None,
+ agent_id: str = "",
+ max_chars: int = INSIGHT_CARD_CHARS,
+) -> dict[str, Any]:
+ """Bounded perceive card for a material turn. Soft-fail if Insight is absent.
+
+ Never dumps the lattice. ``insight_plan`` runs only when the perceive card
+ is usable *and* the goal looks multi-step.
+ """
+ out: dict[str, Any] = {
+ "ok": False,
+ "adapter": SPACE_INSIGHT_ADAPTER_VERSION,
+ "mode": "missing",
+ "card": "",
+ "usable": False,
+ "lever": "",
+ "top_rule": "",
+ "action_hint": "",
+ "planned": False,
+ "required": False,
+ }
+ if high_load:
+ out["ok"] = True
+ out["mode"] = "skipped"
+ out["skipped"] = "high_load"
+ return out
+ if not (situation or "").strip() and not (goal or "").strip():
+ out["ok"] = True
+ out["mode"] = "skipped"
+ out["skipped"] = "empty"
+ return out
+ if not insight_available():
+ out["ok"] = True
+ out["mode"] = "missing"
+ out["skipped"] = "not_installed"
+ return out
+
+ blob = (situation or goal or "").strip()
+ obs = [str(o).strip() for o in (observations or []) if str(o).strip()]
+ aid = (agent_id or os.environ.get("HERMESPACE_AGENT_ID") or "").strip() or None
+ try:
+ from hermes_insight import HermesInsight
+
+ lat = HermesInsight(agent_id=aid)
+ rec = lat.perceive(
+ blob,
+ observations=obs or None,
+ log_experience=False,
+ deep=False,
+ )
+ except Exception as e:
+ logger.debug("insight perceive miss: %s", e)
+ out["ok"] = True
+ out["mode"] = "soft_fail"
+ out["error"] = type(e).__name__
+ return out
+
+ if not isinstance(rec, dict):
+ out["ok"] = True
+ out["mode"] = "soft_fail"
+ out["error"] = "bad_perceive"
+ return out
+
+ matches = list(rec.get("matches") or [])
+ top = matches[0] if matches else {}
+ lever = str(rec.get("lever") or "")
+ usable = bool(rec.get("usable"))
+ hint = str(rec.get("action_hint") or "")
+ top_rule = str(top.get("title") or "")
+ plan_hint = ""
+ planned = False
+
+ # Hot path: plan only when usable and the goal is actually multi-step.
+ if usable and _is_multi_step(goal or blob, plan):
+ try:
+ planned_rec = lat.plan(blob, observations=obs or None, limit=3)
+ if isinstance(planned_rec, dict):
+ steps = planned_rec.get("steps") or planned_rec.get("plan") or []
+ if isinstance(steps, list) and steps:
+ first = steps[0]
+ if isinstance(first, dict):
+ plan_hint = str(first.get("title") or first.get("action") or first)[:100]
+ else:
+ plan_hint = str(first)[:100]
+ planned = True
+ elif planned_rec.get("action_hint"):
+ plan_hint = str(planned_rec.get("action_hint"))[:100]
+ planned = True
+ except Exception as e:
+ logger.debug("insight plan miss: %s", e)
+
+ card = _format_card(
+ lever=lever,
+ top_rule=top_rule,
+ usable=usable,
+ action_hint=hint,
+ plan_hint=plan_hint,
+ )
+ if max_chars and len(card) > max_chars:
+ card = _bound_card(card, cap=max_chars)
+
+ out.update(
+ {
+ "ok": True,
+ "mode": "insight",
+ "card": card,
+ "usable": usable,
+ "lever": lever,
+ "top_rule": top_rule,
+ "action_hint": hint,
+ "planned": planned,
+ "confidence": rec.get("confidence"),
+ "top_score": rec.get("top_score"),
+ }
+ )
+ return out
diff --git a/src/hermespace/paths.py b/src/hermespace/paths.py
index e69dfa8..2d37991 100644
--- a/src/hermespace/paths.py
+++ b/src/hermespace/paths.py
@@ -22,13 +22,11 @@ def hermespace_home() -> Path:
Order:
HERMESPACE_HOME
- ILO_HOME (compat)
~/.hermespace
"""
- for key in ("HERMESPACE_HOME", "ILO_HOME"):
- raw = os.environ.get(key, "").strip()
- if raw:
- return Path(raw).expanduser().resolve()
+ raw = os.environ.get("HERMESPACE_HOME", "").strip()
+ if raw:
+ return Path(raw).expanduser().resolve()
return (Path.home() / ".hermespace").resolve()
diff --git a/src/hermespace/workflow.py b/src/hermespace/workflow.py
index 6d17c9b..0cd43b0 100644
--- a/src/hermespace/workflow.py
+++ b/src/hermespace/workflow.py
@@ -145,6 +145,7 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
cube_meta: dict[str, Any] = {}
access_meta: dict[str, Any] = {}
cube_block = ""
+ insight_block = ""
env_meta: dict[str, Any] = {}
oew_broadcast = ""
report = (desk.say or "").strip()
@@ -177,6 +178,8 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
"mode": beat.get("mode"),
"load_level": beat.get("load_level"),
"chars": len(cube_block),
+ "skipped": beat.get("skipped"),
+ "shrunk": beat.get("shrunk"),
}
js = AccessHub(agent_id=access_id)
js.sync_from_desk(desk, user_message=msg, cube_strip=cube_block)
@@ -222,6 +225,33 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
except Exception as exc:
cube_meta = {"ok": False, "error": type(exc).__name__}
+ try:
+ from hermespace.insight_module import insight_card
+
+ high_insight = (
+ str(desk.load.get("level")) == "high"
+ if isinstance(desk.load, dict)
+ else False
+ )
+ icard = insight_card(
+ msg or desk.goal or "",
+ high_load=high_insight,
+ goal=desk.goal or g,
+ plan=list(desk.plan or []),
+ agent_id=payload.agent_id or "hermes-agent",
+ )
+ desk.meta["insight"] = {
+ "ok": icard.get("ok"),
+ "mode": icard.get("mode"),
+ "usable": icard.get("usable"),
+ "skipped": icard.get("skipped"),
+ "planned": icard.get("planned"),
+ }
+ insight_block = str(icard.get("card") or "")
+ save_desk(desk, self.engine.desk_path)
+ except Exception:
+ insight_block = ""
+
# 6 broadcast context (model channel) — Quicksilver-capped OEW strip
inject_cap = 900 if (
isinstance(desk.load, dict) and str(desk.load.get("level")) == "high"
@@ -229,6 +259,8 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
block = build_inject_block(desk, max_chars=inject_cap, user_message=msg)
if cube_block:
block = (block + "\n\n" + cube_block).strip()
+ if insight_block:
+ block = (block + "\n\n" + insight_block).strip()
try:
from hermespace.access import AccessHub, AccessEnv
from hermespace.access.engine import workspace_id
@@ -296,6 +328,7 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
"neural": neural_snap,
"fabric": fabric_snap,
"cube_beat": cube_meta,
+ "insight": (desk.meta or {}).get("insight") or {},
"access": access_meta,
"access_env": env_meta,
},
diff --git a/src/hermespace/world.py b/src/hermespace/world.py
index 299e90c..3350e70 100644
--- a/src/hermespace/world.py
+++ b/src/hermespace/world.py
@@ -3,8 +3,10 @@
The World is the agent's external Access Workspace: a structured, persistent
representation of everything the agent knows, believes, and is doing.
-The archive (JSONL) is the source of truth — it grows forever.
-world.json is a fast cache / projection of current state.
+When HermesCube is present, the Cube book is the durable SoT. World
+projects from that book (``pulse_charge`` / ``sync_world_beliefs``) and
+does not grow a second forever-archive. Standalone (no Cube) keeps the
+local JSONL warehouse as the source of truth; world.json is the cache.
Archive types: enter, leave, landmark, belief, trait, evolution,
focus, epoch_transition, resolve, relationship.
@@ -243,8 +245,9 @@ def get_entry(self, entry_id: str) -> TimelineEntry | None:
class WorldModel:
"""The agent's persistent world — the space they live and work in.
- The archive (JSONL) is the source of truth — it grows forever.
- world.json is a fast cache / projection of current state.
+ Standalone: the JSONL archive is the local warehouse and may grow.
+ With Cube: World is a projection of the book — recharge via
+ ``pulse_charge`` / ``sync_world_beliefs``, do not grow a second archive.
"""
def __init__(self, agent_id: str = "hermes-agent") -> None:
@@ -316,8 +319,46 @@ def _compute_epoch(self) -> str:
return "Maturity"
return "Wisdom"
+ def projects_from_cube(self) -> bool:
+ """True when Cube is the durable book — World must not grow a second archive."""
+ try:
+ from hermespace.cube_module import cube_available
+
+ return bool(cube_available())
+ except Exception:
+ return False
+
+ def project_from_book(self) -> dict[str, Any]:
+ """Charge this World from the Cube book (no-op / standalone evolve if absent)."""
+ try:
+ from hermespace.cube_module import cube_pulse, sync_world
+
+ charged = cube_pulse(agent_id=self.agent_id, ensure=False)
+ synced = sync_world(agent_id=self.agent_id)
+ return {
+ "ok": bool(charged.get("ok") or synced.get("ok")),
+ "mode": "cube" if self.projects_from_cube() else "standalone",
+ "charge": charged,
+ "sync": synced,
+ }
+ except Exception as e:
+ return {"ok": False, "error": type(e).__name__, "mode": "standalone"}
+
def _add_timeline(self, entry_type: str, description: str, data: dict | None = None, causal_parents: list[str] | None = None, outcome: str = "") -> TimelineEntry:
- entry = self.archive.append(entry_type, self.agent_id, description, data, causal_parents, outcome)
+ if self.projects_from_cube():
+ # Cube book is SoT — keep a short in-memory projection only.
+ entry = TimelineEntry(
+ id=uuid.uuid4().hex[:12],
+ timestamp=_utcnow(),
+ entry_type=entry_type,
+ agent_id=self.agent_id,
+ description=description,
+ data=data or {},
+ causal_parents=causal_parents or [],
+ outcome=outcome,
+ )
+ else:
+ entry = self.archive.append(entry_type, self.agent_id, description, data, causal_parents, outcome)
self._state.timeline.insert(0, entry)
if len(self._state.timeline) > 50:
self._state.timeline = self._state.timeline[:50]
@@ -370,6 +411,11 @@ def enter(self, desk: Any = None) -> WorldState:
self._add_timeline("enter", "Agent entered the world", {"state": self._state.current_state})
self._state.current_state = "working"
self._state.world_time = _utcnow()
+ if self.projects_from_cube():
+ try:
+ self.project_from_book()
+ except Exception:
+ pass
self._refresh_concepts()
self.save()
return self._state
diff --git a/tests/test_access_hub.py b/tests/test_access_hub.py
index 6286eba..6fe8c18 100644
--- a/tests/test_access_hub.py
+++ b/tests/test_access_hub.py
@@ -136,6 +136,18 @@ def test_seal_and_inject(self) -> None:
self.assertIn("block", beat)
self.assertEqual(beat.get("mode"), "standalone")
+ def test_prefetch_skip_does_not_inject_strip(self) -> None:
+ os.environ["HERMESPACE_CUBE_PREFETCHED"] = "1"
+ try:
+ from hermespace.cube_module import cube_already_prefetched, cube_beat
+
+ self.assertTrue(cube_already_prefetched("deploy"))
+ beat = cube_beat("deploy", load="mid", agent_id="prefetch-agent")
+ self.assertEqual(beat.get("skipped"), "provider_prefetch")
+ self.assertEqual(beat.get("block"), "")
+ finally:
+ os.environ.pop("HERMESPACE_CUBE_PREFETCHED", None)
+
def test_strip_budget(self) -> None:
from hermespace.cube_module import normalize_load, strip_budget
diff --git a/tests/test_insight_module.py b/tests/test_insight_module.py
new file mode 100644
index 0000000..eda2b57
--- /dev/null
+++ b/tests/test_insight_module.py
@@ -0,0 +1,168 @@
+"""Insight cable — fail-soft, bounded card, never required."""
+
+from __future__ import annotations
+
+import os
+import sys
+import tempfile
+import types
+import unittest
+from pathlib import Path
+from unittest import mock
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT / "src"))
+
+
+class TestInsightMissing(unittest.TestCase):
+ def setUp(self) -> None:
+ self._td = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self._td.name
+ import builtins
+
+ real_import = builtins.__import__
+
+ def _block(name, *args, **kwargs):
+ if name == "hermes_insight" or name.startswith("hermes_insight."):
+ raise ImportError("blocked for missing-insight test")
+ return real_import(name, *args, **kwargs)
+
+ self._imp = mock.patch("builtins.__import__", side_effect=_block)
+ self._imp.start()
+
+ def tearDown(self) -> None:
+ self._imp.stop()
+ self._td.cleanup()
+ os.environ.pop("HERMESPACE_HOME", None)
+
+ def test_status_and_card_soft_fail(self) -> None:
+ from hermespace.insight_module import insight_available, insight_card, insight_status
+
+ self.assertFalse(insight_available())
+ st = insight_status()
+ self.assertFalse(st.get("available"))
+ self.assertFalse(st.get("required"))
+ rec = insight_card("First reproduce then patch then verify")
+ self.assertTrue(rec.get("ok"))
+ self.assertEqual(rec.get("mode"), "missing")
+ self.assertEqual(rec.get("card"), "")
+
+
+class TestInsightPresent(unittest.TestCase):
+ def setUp(self) -> None:
+ self._td = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self._td.name
+ self._plan_calls = {"n": 0}
+
+ class _Lat:
+ def perceive(self, situation, **kwargs):
+ return {
+ "usable": True,
+ "lever": "shared-token",
+ "action_hint": "Give each worker its own credential.",
+ "confidence": 0.7,
+ "top_score": 0.4,
+ "matches": [
+ {
+ "title": "duplicate consumer",
+ "kind": "rule",
+ "score": 0.4,
+ }
+ ],
+ }
+
+ def plan(self, situation, **kwargs):
+ return {"steps": [{"title": "isolate credentials first"}]}
+
+ lat = _Lat()
+ orig_plan = lat.plan
+
+ def _plan(situation, **kwargs):
+ self._plan_calls["n"] += 1
+ return orig_plan(situation, **kwargs)
+
+ lat.plan = _plan # type: ignore[method-assign]
+
+ pkg = types.ModuleType("hermes_insight")
+ pkg.HermesInsight = lambda *a, **k: lat # type: ignore[attr-defined]
+ pkg.__version__ = "0.8.0-test"
+ self._mod = mock.patch.dict(sys.modules, {"hermes_insight": pkg})
+ self._mod.start()
+ self._lat = lat
+
+ def tearDown(self) -> None:
+ self._mod.stop()
+ self._td.cleanup()
+ os.environ.pop("HERMESPACE_HOME", None)
+
+ def test_bounded_card_and_plan_on_multistep(self) -> None:
+ from hermespace.insight_module import insight_card, insight_status
+
+ st = insight_status()
+ self.assertTrue(st.get("available"))
+ rec = insight_card(
+ "two workers share one bot token",
+ goal="First isolate credentials then restart finally verify",
+ plan=["isolate", "restart", "verify"],
+ )
+ self.assertTrue(rec.get("ok"))
+ self.assertTrue(rec.get("usable"))
+ self.assertIn("shared-token", rec.get("card") or "")
+ self.assertLessEqual(len(rec.get("card") or ""), 400)
+ self.assertTrue(rec.get("planned"))
+ self.assertEqual(self._plan_calls["n"], 1)
+ self.assertNotIn("lattice", (rec.get("card") or "").lower())
+
+ def test_no_plan_on_single_step(self) -> None:
+ from hermespace.insight_module import insight_card
+
+ rec = insight_card("what is the status", goal="status")
+ self.assertTrue(rec.get("ok"))
+ self.assertFalse(rec.get("planned"))
+ self.assertEqual(self._plan_calls["n"], 0)
+
+ def test_high_load_skip(self) -> None:
+ from hermespace.insight_module import insight_card
+
+ rec = insight_card(
+ "First reproduce then patch then verify",
+ high_load=True,
+ plan=["a", "b"],
+ )
+ self.assertTrue(rec.get("ok"))
+ self.assertEqual(rec.get("skipped"), "high_load")
+ self.assertEqual(rec.get("card"), "")
+ self.assertEqual(self._plan_calls["n"], 0)
+
+
+class TestWorkflowInsightMeta(unittest.TestCase):
+ def setUp(self) -> None:
+ self._td = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self._td.name
+
+ def tearDown(self) -> None:
+ self._td.cleanup()
+ os.environ.pop("HERMESPACE_HOME", None)
+
+ def test_turn_records_insight_meta(self) -> None:
+ from hermespace.io_contract import HermespaceInput
+ from hermespace.workflow import Workflow
+
+ out = Workflow().run(
+ HermespaceInput(
+ message="First inspect then implement finally verify",
+ goal="Ship the fix",
+ decision="A — implement",
+ plan=["inspect", "implement", "verify"],
+ say="Working the fix.",
+ force=True,
+ agent_id="insight-wf",
+ )
+ )
+ self.assertFalse(out.skipped)
+ self.assertIn("insight", out.meta or {})
+ self.assertIn((out.meta or {}).get("insight", {}).get("mode"), ("missing", "insight", "soft_fail", "skipped"))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_plugin_contract.py b/tests/test_plugin_contract.py
index f6b90da..0dbc626 100644
--- a/tests/test_plugin_contract.py
+++ b/tests/test_plugin_contract.py
@@ -111,6 +111,26 @@ def test_registration_and_native_lifecycle(self) -> None:
self.assertTrue(after["finalized"])
ctx.hooks["on_session_finalize"](**common) # idempotent
+ def test_plugin_yaml_hygiene(self) -> None:
+ text = (Path(__file__).resolve().parents[1] / "plugin.yaml").read_text(encoding="utf-8")
+ expected = [
+ "on_session_start",
+ "pre_llm_call",
+ "post_llm_call",
+ "post_tool_call",
+ "on_session_end",
+ "on_session_finalize",
+ "on_session_reset",
+ "subagent_start",
+ "subagent_stop",
+ ]
+ self.assertIn("provides_hooks:", text)
+ self.assertIn("python_dependencies:", text)
+ self.assertIn("numpy>=1.24,<3", text)
+ after = text.split("provides_hooks:", 1)[1].split("python_dependencies:", 1)[0]
+ for hook in expected:
+ self.assertIn(f"- {hook}", after)
+
def test_commands_are_operational(self) -> None:
from hermespace.plugin import register
diff --git a/tests/test_world.py b/tests/test_world.py
index 9561f41..7f155c9 100644
--- a/tests/test_world.py
+++ b/tests/test_world.py
@@ -52,5 +52,43 @@ def test_growth_render_uses_cleaned_landmarks(self):
self.assertFalse(any("session ended" in lm.lower() for lm in wm.state.landmarks))
+class TestWorldCubeProjection(unittest.TestCase):
+ def setUp(self) -> None:
+ self._td = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self._td.name
+
+ def tearDown(self) -> None:
+ self._td.cleanup()
+ os.environ.pop("HERMESPACE_HOME", None)
+
+ def test_standalone_grows_local_archive(self) -> None:
+ from unittest import mock
+
+ from hermespace.world import WorldModel
+
+ with mock.patch.object(WorldModel, "projects_from_cube", return_value=False):
+ wm = WorldModel(agent_id="standalone-archive")
+ before = wm.archive.count()
+ wm.add_belief("Standalone warehouse may grow", 0.8, source="test")
+ self.assertGreater(wm.archive.count(), before)
+ self.assertTrue(wm.archive.path.is_file())
+
+ def test_cube_present_does_not_grow_jsonl(self) -> None:
+ from unittest import mock
+
+ from hermespace.world import WorldModel
+
+ wm = WorldModel(agent_id="cube-projection")
+ with mock.patch.object(WorldModel, "projects_from_cube", return_value=True):
+ with mock.patch.object(WorldModel, "project_from_book", return_value={"ok": True, "mode": "cube"}):
+ before = wm.archive.count()
+ wm.enter()
+ wm.add_belief("Cube book is the SoT", 0.9, source="test")
+ wm.leave("session finalized")
+ self.assertEqual(wm.archive.count(), before)
+ self.assertTrue(wm.projects_from_cube())
+
+
if __name__ == "__main__":
unittest.main()
+
From f5beed7a9292151217dcd2e4f400f95e142a992b Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 13:48:07 +0000
Subject: [PATCH 14/16] fix(insight): lock cable to perceive_card next to
cube_beat
Hang the Insight strip beside cube_beat on pre_llm_call only. Feature-detect
HermesInsight.perceive_card; skip until that method exists. Do not format
perceive() output or call plan on the hot path. Skip entirely on high/protect
load. AccessEngine.turn no longer injects Insight.
Co-authored-by: Pablo
---
PURPOSE.md | 2 +-
README.md | 2 +-
docs/architecture/INSIGHT.md | 27 ++--
src/hermespace/hermes_bridge.py | 42 ++----
src/hermespace/insight_module.py | 219 ++++++++--------------------
src/hermespace/workflow.py | 31 ----
tests/test_insight_module.py | 243 ++++++++++++++++++++++---------
7 files changed, 269 insertions(+), 297 deletions(-)
diff --git a/PURPOSE.md b/PURPOSE.md
index 1b2ab79..ae71b15 100644
--- a/PURPOSE.md
+++ b/PURPOSE.md
@@ -92,7 +92,7 @@ World identity may remain agent-scoped; active cognition is session-scoped.
| WorldModel | Agent-scoped beliefs, landmarks, timeline |
| Hermes Agent | Model calls, tools, skills, approvals, transcript |
| Optional Cube book | Durable long-tail SoT when `hermescube` is installed |
-| Optional Insight | Bounded perceive card when `hermes_insight` is installed |
+| Optional Insight | `perceive_card` strip next to `cube_beat` on `pre_llm_call` when installed |
Cube and Insight stay standalone packages. Hermespace only **cables** them
via fail-soft imports (`cube_module`, `insight_module`). Neither is required.
diff --git a/README.md b/README.md
index ff163b1..e46f3fd 100644
--- a/README.md
+++ b/README.md
@@ -142,7 +142,7 @@ Alongside the world, Hermespace provides a desk for the current turn — FOA, du
| **Focus of Attention** | ≤4 items, single active goal per turn |
| **Dual Decode** | Human gets a short report; the model gets dense context. Never dump raw inject into chat channels. |
| **Cube heart (optional)** | Soft cable to HermesCube — `beat` / `seal` / `pulse`; skip/shrink when the Cube provider already prefetched; standalone warehouse when Cube absent |
-| **Insight (optional)** | Soft cable to Hermes Insight — bounded perceive card on material turns; never required |
+| **Insight (optional)** | Soft cable next to `cube_beat` on `pre_llm_call` — `perceive_card` only, skip on high/protect; never required |
| **Skills + Memory Fabric** | Ranks Hermes skills per goal; injects MEMORY.md / USER.md excerpts |
| **Neural FOA** | `HERMESPACE_NEURAL_BACKEND=auto` — Ollama embeddings when live, hash fallback |
| **Autonomy Grid** | Missions, lenses, dream, self-talk, skillbench, title/tree, access gates. Ground-up design. |
diff --git a/docs/architecture/INSIGHT.md b/docs/architecture/INSIGHT.md
index d7aa71e..fd35e55 100644
--- a/docs/architecture/INSIGHT.md
+++ b/docs/architecture/INSIGHT.md
@@ -1,22 +1,23 @@
-# Hermes Insight × Hermespace — optional perceive cable
+# Hermes Insight × Hermespace — optional perceive_card cable
-**Hermes Insight stays a standalone package.** Hermespace does not vendor it.
-When `hermes_insight` is importable, Access Engine appends a **bounded perceive
-card** (~400 chars) on material `pre_llm_call` / `AccessEngine.turn`.
+**Hermes Insight stays a standalone package.** Hermespace does not vendor it
+and does not register Insight hooks. The cable is a thin
+`hermespace.insight_module` adapter hung **next to `cube_beat`** on
+`pre_llm_call` (not inside `AccessEngine.turn`).
```
-from hermespace.insight_module import insight_card, insight_status
-
-rec = insight_card("two workers share one token", goal=desk.goal, plan=desk.plan)
-# rec["card"] → lever / top rule / usable / action_hint
+from hermes_insight import HermesInsight
+if hasattr(HermesInsight, "perceive_card"):
+ card = HermesInsight().perceive_card(goal, load=...)
```
| Rule | Behavior |
|------|----------|
-| Missing package | Soft-fail (`mode=missing`) — engine still runs |
-| High load | Skip (`skipped=high_load`) |
-| Card | lever, top rule, usable, action_hint — never the lattice |
-| `insight_plan` | Hot path only when `usable` **and** the goal is multi-step |
-| Required? | Never |
+| Feature-detect | `from hermes_insight import HermesInsight` and `hasattr(..., "perceive_card")` |
+| Missing / no `perceive_card` | Skip — do **not** format `perceive()["card"]` (unbounded lattice) |
+| High / protect load | Skip entirely (stricter than Cube) |
+| Hot path | Append only the returned card, capped at 400 chars |
+| `insight_plan` / `.plan` | Never on `pre_llm_call` |
+| Required? | Never — `except Exception: pass` like `cube_beat` |
Companion: [PabloTheThinker/hermes-insight](https://github.com/PabloTheThinker/hermes-insight)
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index 3e5bcb5..bdbb16e 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -427,6 +427,20 @@ def on_pre_llm_call(
cube_block = str(beat.get("block") or "")
if cube_block:
block += "\n\n" + cube_block
+ # Insight strip — next to cube_beat. perceive_card only; skip if missing.
+ try:
+ from hermespace.insight_module import insight_card
+
+ icard = insight_card(desk.goal or msg or "", load=load_val)
+ desk.meta["insight"] = {
+ "ok": icard.get("ok"),
+ "mode": icard.get("mode"),
+ "skipped": icard.get("skipped"),
+ }
+ if icard.get("card"):
+ block += "\n\n" + str(icard["card"])
+ except Exception:
+ pass
# OEW beat — higher-order park + causal broadcast (model channel only)
from hermespace.access.oew import ensure_oew_env_default
@@ -500,34 +514,6 @@ def on_pre_llm_call(
except Exception:
pass
- try:
- from hermespace.insight_module import insight_card
-
- icard = insight_card(
- msg or desk.goal or "",
- high_load=high_load,
- goal=desk.goal or "",
- plan=list(desk.plan or []),
- agent_id=agent_id,
- )
- desk.meta["insight"] = {
- "ok": icard.get("ok"),
- "mode": icard.get("mode"),
- "usable": icard.get("usable"),
- "skipped": icard.get("skipped"),
- "planned": icard.get("planned"),
- }
- if icard.get("card"):
- block += "\n\n" + str(icard["card"])
- try:
- from hermespace.store import save_desk
-
- save_desk(desk)
- except Exception:
- pass
- except Exception:
- pass
-
# Workbench status — only on first turn or when state changes (skip under high)
if not high_load:
try:
diff --git a/src/hermespace/insight_module.py b/src/hermespace/insight_module.py
index 96eed1f..ef12c99 100644
--- a/src/hermespace/insight_module.py
+++ b/src/hermespace/insight_module.py
@@ -1,46 +1,55 @@
-"""Hermes Insight adapter — soft dependency, never required.
+"""Hermes Insight adapter — thin soft-import, never required.
-Insight (when installed) is a standalone pattern lattice. Hermespace owns
-FOA / Access Engine. This module is the cable, same shape as ``cube_module``:
+Insight stays a standalone package. This module is the cable only:
- feature-detect ``hermes_insight``
- on material pre_llm / AccessEngine.turn → bounded perceive card (~400 chars)
- high-load skip · missing package → soft-fail
- never dump the lattice
- never call ``insight_plan`` on the hot path unless usable *and* multi-step
+ from hermes_insight import HermesInsight
+ hasattr(HermesInsight, "perceive_card")
+ HermesInsight().perceive_card(goal, load=...)
-See PURPOSE.md. Do not vendor Insight source.
+Hang the returned card next to ``cube_beat`` on ``pre_llm_call``.
+Skip entirely on high/protect load. Until ``perceive_card`` exists, skip —
+do not format ``perceive()`` output (unbounded lattice). Do not call
+``insight_plan`` / ``HermesInsight.plan`` on the hot path. Do not register
+Insight hooks. Do not vendor Insight source.
+
+See docs/architecture/INSIGHT.md.
"""
from __future__ import annotations
-import logging
-import os
-import re
-from typing import Any, Sequence
-
-logger = logging.getLogger("hermespace.insight_module")
+from typing import Any
-SPACE_INSIGHT_ADAPTER_VERSION = "1.0"
+SPACE_INSIGHT_ADAPTER_VERSION = "1.1"
INSIGHT_CARD_CHARS = 400
-_MULTI_STEP_RE = re.compile(
- r"\b(first|then|next|after that|finally|and then|step\s*\d)\b",
- re.IGNORECASE,
-)
+
+def _high_or_protect(load: str | float | None = None, *, high_load: bool = False) -> bool:
+ if high_load:
+ return True
+ if isinstance(load, (int, float)):
+ return float(load) >= 0.65
+ s = str(load or "").strip().lower()
+ return s in {"high", "protect", "protected", "mono", "monotropic"}
+
+
+def _bound_card(text: str, *, cap: int = INSIGHT_CARD_CHARS) -> str:
+ s = (text or "").strip()
+ if len(s) <= cap:
+ return s
+ return s[: cap - 3].rstrip() + "..."
def insight_available() -> bool:
try:
- import hermes_insight # noqa: F401
+ from hermes_insight import HermesInsight
- return True
+ return hasattr(HermesInsight, "perceive_card")
except Exception:
return False
def insight_status() -> dict[str, Any]:
- """Feature-detect only — never required for Access Engine."""
+ """Feature-detect ``perceive_card`` only — never required."""
out: dict[str, Any] = {
"adapter": SPACE_INSIGHT_ADAPTER_VERSION,
"available": False,
@@ -51,173 +60,71 @@ def insight_status() -> dict[str, Any]:
}
try:
import hermes_insight
+ from hermes_insight import HermesInsight
- out["available"] = True
- out["mode"] = "insight"
out["version"] = getattr(hermes_insight, "__version__", None)
+ if hasattr(HermesInsight, "perceive_card"):
+ out["available"] = True
+ out["mode"] = "insight"
+ else:
+ out["mode"] = "no_perceive_card"
+ out["skipped"] = "no_perceive_card"
return out
except Exception as e:
out["error"] = type(e).__name__
return out
-def _is_multi_step(goal: str = "", plan: Sequence[str] | None = None) -> bool:
- if plan is not None and len([p for p in plan if str(p).strip()]) >= 2:
- return True
- text = (goal or "").strip()
- if not text:
- return False
- if _MULTI_STEP_RE.search(text):
- return True
- return text.count(";") >= 2 or text.count("→") >= 2 or text.count("->") >= 2
-
-
-def _bound_card(text: str, *, cap: int = INSIGHT_CARD_CHARS) -> str:
- s = (text or "").strip()
- if len(s) <= cap:
- return s
- return s[: cap - 3].rstrip() + "..."
-
-
-def _format_card(
- *,
- lever: str,
- top_rule: str,
- usable: bool,
- action_hint: str,
- plan_hint: str = "",
-) -> str:
- lines = [
- "### Insight",
- f"- lever: {(lever or 'unknown')[:80]}",
- f"- rule: {(top_rule or 'none')[:120]}",
- f"- usable: {str(bool(usable)).lower()}",
- f"- hint: {(action_hint or '')[:160]}",
- ]
- if plan_hint:
- lines.append(f"- plan: {plan_hint[:100]}")
- return _bound_card("\n".join(lines))
-
-
def insight_card(
- situation: str,
+ goal: str,
*,
- observations: Sequence[str] | None = None,
+ load: str | float | None = None,
high_load: bool = False,
- goal: str = "",
- plan: Sequence[str] | None = None,
- agent_id: str = "",
max_chars: int = INSIGHT_CARD_CHARS,
) -> dict[str, Any]:
- """Bounded perceive card for a material turn. Soft-fail if Insight is absent.
+ """Call ``HermesInsight().perceive_card`` and return only that card.
- Never dumps the lattice. ``insight_plan`` runs only when the perceive card
- is usable *and* the goal looks multi-step.
+ Soft-fail if Insight is absent or ``perceive_card`` is missing.
+ Never calls ``perceive`` / ``plan``. Never formats a lattice dump.
"""
out: dict[str, Any] = {
- "ok": False,
+ "ok": True,
"adapter": SPACE_INSIGHT_ADAPTER_VERSION,
"mode": "missing",
"card": "",
- "usable": False,
- "lever": "",
- "top_rule": "",
- "action_hint": "",
- "planned": False,
"required": False,
}
- if high_load:
- out["ok"] = True
+ if _high_or_protect(load, high_load=high_load):
out["mode"] = "skipped"
out["skipped"] = "high_load"
return out
- if not (situation or "").strip() and not (goal or "").strip():
- out["ok"] = True
+ if not (goal or "").strip():
out["mode"] = "skipped"
out["skipped"] = "empty"
return out
- if not insight_available():
- out["ok"] = True
+ try:
+ from hermes_insight import HermesInsight
+ except Exception:
out["mode"] = "missing"
out["skipped"] = "not_installed"
return out
-
- blob = (situation or goal or "").strip()
- obs = [str(o).strip() for o in (observations or []) if str(o).strip()]
- aid = (agent_id or os.environ.get("HERMESPACE_AGENT_ID") or "").strip() or None
+ if not hasattr(HermesInsight, "perceive_card"):
+ out["mode"] = "no_perceive_card"
+ out["skipped"] = "no_perceive_card"
+ return out
try:
- from hermes_insight import HermesInsight
-
- lat = HermesInsight(agent_id=aid)
- rec = lat.perceive(
- blob,
- observations=obs or None,
- log_experience=False,
- deep=False,
- )
+ rec = HermesInsight().perceive_card((goal or "").strip(), load=load)
except Exception as e:
- logger.debug("insight perceive miss: %s", e)
- out["ok"] = True
out["mode"] = "soft_fail"
out["error"] = type(e).__name__
return out
- if not isinstance(rec, dict):
- out["ok"] = True
- out["mode"] = "soft_fail"
- out["error"] = "bad_perceive"
- return out
-
- matches = list(rec.get("matches") or [])
- top = matches[0] if matches else {}
- lever = str(rec.get("lever") or "")
- usable = bool(rec.get("usable"))
- hint = str(rec.get("action_hint") or "")
- top_rule = str(top.get("title") or "")
- plan_hint = ""
- planned = False
-
- # Hot path: plan only when usable and the goal is actually multi-step.
- if usable and _is_multi_step(goal or blob, plan):
- try:
- planned_rec = lat.plan(blob, observations=obs or None, limit=3)
- if isinstance(planned_rec, dict):
- steps = planned_rec.get("steps") or planned_rec.get("plan") or []
- if isinstance(steps, list) and steps:
- first = steps[0]
- if isinstance(first, dict):
- plan_hint = str(first.get("title") or first.get("action") or first)[:100]
- else:
- plan_hint = str(first)[:100]
- planned = True
- elif planned_rec.get("action_hint"):
- plan_hint = str(planned_rec.get("action_hint"))[:100]
- planned = True
- except Exception as e:
- logger.debug("insight plan miss: %s", e)
-
- card = _format_card(
- lever=lever,
- top_rule=top_rule,
- usable=usable,
- action_hint=hint,
- plan_hint=plan_hint,
- )
- if max_chars and len(card) > max_chars:
- card = _bound_card(card, cap=max_chars)
-
- out.update(
- {
- "ok": True,
- "mode": "insight",
- "card": card,
- "usable": usable,
- "lever": lever,
- "top_rule": top_rule,
- "action_hint": hint,
- "planned": planned,
- "confidence": rec.get("confidence"),
- "top_score": rec.get("top_score"),
- }
- )
+ if isinstance(rec, str):
+ card = rec
+ elif isinstance(rec, dict):
+ card = str(rec.get("card") or "")
+ else:
+ card = str(rec or "")
+ cap = max_chars if max_chars and max_chars > 0 else INSIGHT_CARD_CHARS
+ out.update({"mode": "insight", "card": _bound_card(card, cap=cap)})
return out
diff --git a/src/hermespace/workflow.py b/src/hermespace/workflow.py
index 0cd43b0..1850c37 100644
--- a/src/hermespace/workflow.py
+++ b/src/hermespace/workflow.py
@@ -145,7 +145,6 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
cube_meta: dict[str, Any] = {}
access_meta: dict[str, Any] = {}
cube_block = ""
- insight_block = ""
env_meta: dict[str, Any] = {}
oew_broadcast = ""
report = (desk.say or "").strip()
@@ -225,33 +224,6 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
except Exception as exc:
cube_meta = {"ok": False, "error": type(exc).__name__}
- try:
- from hermespace.insight_module import insight_card
-
- high_insight = (
- str(desk.load.get("level")) == "high"
- if isinstance(desk.load, dict)
- else False
- )
- icard = insight_card(
- msg or desk.goal or "",
- high_load=high_insight,
- goal=desk.goal or g,
- plan=list(desk.plan or []),
- agent_id=payload.agent_id or "hermes-agent",
- )
- desk.meta["insight"] = {
- "ok": icard.get("ok"),
- "mode": icard.get("mode"),
- "usable": icard.get("usable"),
- "skipped": icard.get("skipped"),
- "planned": icard.get("planned"),
- }
- insight_block = str(icard.get("card") or "")
- save_desk(desk, self.engine.desk_path)
- except Exception:
- insight_block = ""
-
# 6 broadcast context (model channel) — Quicksilver-capped OEW strip
inject_cap = 900 if (
isinstance(desk.load, dict) and str(desk.load.get("level")) == "high"
@@ -259,8 +231,6 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
block = build_inject_block(desk, max_chars=inject_cap, user_message=msg)
if cube_block:
block = (block + "\n\n" + cube_block).strip()
- if insight_block:
- block = (block + "\n\n" + insight_block).strip()
try:
from hermespace.access import AccessHub, AccessEnv
from hermespace.access.engine import workspace_id
@@ -328,7 +298,6 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
"neural": neural_snap,
"fabric": fabric_snap,
"cube_beat": cube_meta,
- "insight": (desk.meta or {}).get("insight") or {},
"access": access_meta,
"access_env": env_meta,
},
diff --git a/tests/test_insight_module.py b/tests/test_insight_module.py
index eda2b57..f4f09f5 100644
--- a/tests/test_insight_module.py
+++ b/tests/test_insight_module.py
@@ -1,4 +1,4 @@
-"""Insight cable — fail-soft, bounded card, never required."""
+"""Insight cable — perceive_card only, fail-soft, never required."""
from __future__ import annotations
@@ -48,94 +48,143 @@ def test_status_and_card_soft_fail(self) -> None:
self.assertEqual(rec.get("card"), "")
-class TestInsightPresent(unittest.TestCase):
+class TestInsightNoPerceiveCard(unittest.TestCase):
def setUp(self) -> None:
self._td = tempfile.TemporaryDirectory()
os.environ["HERMESPACE_HOME"] = self._td.name
+ self._perceive_calls = {"n": 0}
self._plan_calls = {"n": 0}
class _Lat:
- def perceive(self, situation, **kwargs):
+ def perceive(self_lat, situation, **kwargs):
+ self._perceive_calls["n"] += 1
return {
+ "card": "UNBOUNDED LATTICE DUMP " + ("x" * 800),
"usable": True,
- "lever": "shared-token",
- "action_hint": "Give each worker its own credential.",
- "confidence": 0.7,
- "top_score": 0.4,
- "matches": [
- {
- "title": "duplicate consumer",
- "kind": "rule",
- "score": 0.4,
- }
- ],
+ "matches": [{"title": "should-not-appear"}],
}
- def plan(self, situation, **kwargs):
- return {"steps": [{"title": "isolate credentials first"}]}
+ def plan(self_lat, situation, **kwargs):
+ self._plan_calls["n"] += 1
+ return {"steps": [{"title": "should-not-plan"}]}
- lat = _Lat()
- orig_plan = lat.plan
+ pkg = types.ModuleType("hermes_insight")
+ pkg.HermesInsight = _Lat # type: ignore[attr-defined]
+ pkg.__version__ = "0.8.0-test"
+ self.assertFalse(hasattr(pkg.HermesInsight, "perceive_card"))
+ self._mod = mock.patch.dict(sys.modules, {"hermes_insight": pkg})
+ self._mod.start()
- def _plan(situation, **kwargs):
- self._plan_calls["n"] += 1
- return orig_plan(situation, **kwargs)
+ def tearDown(self) -> None:
+ self._mod.stop()
+ self._td.cleanup()
+ os.environ.pop("HERMESPACE_HOME", None)
- lat.plan = _plan # type: ignore[method-assign]
+ def test_skip_until_perceive_card_exists(self) -> None:
+ from hermespace.insight_module import insight_available, insight_card, insight_status
+
+ self.assertFalse(insight_available())
+ st = insight_status()
+ self.assertEqual(st.get("mode"), "no_perceive_card")
+ rec = insight_card("two workers share one bot token", load="mid")
+ self.assertTrue(rec.get("ok"))
+ self.assertEqual(rec.get("skipped"), "no_perceive_card")
+ self.assertEqual(rec.get("card"), "")
+ self.assertEqual(self._perceive_calls["n"], 0)
+ self.assertEqual(self._plan_calls["n"], 0)
+ self.assertNotIn("UNBOUNDED", rec.get("card") or "")
+
+
+class TestInsightPerceiveCard(unittest.TestCase):
+ def setUp(self) -> None:
+ self._td = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self._td.name
+ self._calls = {"perceive_card": 0, "perceive": 0, "plan": 0}
+ self._last_load = None
+ outer = self
+
+ class _Cls:
+ def __init__(self, *a, **k):
+ pass
+
+ def perceive_card(self, goal, load=None, **kwargs):
+ outer._calls["perceive_card"] += 1
+ outer._last_load = load
+ return (
+ "### Insight\n- lever: shared-token\n"
+ "- rule: duplicate consumer\n- usable: true\n"
+ "- hint: Give each worker its own credential."
+ )
+
+ def perceive(self, situation, **kwargs):
+ outer._calls["perceive"] += 1
+ return {"card": "UNBOUNDED " + ("x" * 800)}
+
+ def plan(self, situation, **kwargs):
+ outer._calls["plan"] += 1
+ return {"steps": [{"title": "should-not-plan"}]}
pkg = types.ModuleType("hermes_insight")
- pkg.HermesInsight = lambda *a, **k: lat # type: ignore[attr-defined]
- pkg.__version__ = "0.8.0-test"
+ pkg.HermesInsight = _Cls # type: ignore[attr-defined]
+ pkg.__version__ = "0.9.0-test"
self._mod = mock.patch.dict(sys.modules, {"hermes_insight": pkg})
self._mod.start()
- self._lat = lat
def tearDown(self) -> None:
self._mod.stop()
self._td.cleanup()
os.environ.pop("HERMESPACE_HOME", None)
- def test_bounded_card_and_plan_on_multistep(self) -> None:
- from hermespace.insight_module import insight_card, insight_status
+ def test_appends_bounded_perceive_card_only(self) -> None:
+ from hermespace.insight_module import insight_available, insight_card, insight_status
- st = insight_status()
- self.assertTrue(st.get("available"))
- rec = insight_card(
- "two workers share one bot token",
- goal="First isolate credentials then restart finally verify",
- plan=["isolate", "restart", "verify"],
- )
+ self.assertTrue(insight_available())
+ self.assertEqual(insight_status().get("mode"), "insight")
+ rec = insight_card("First isolate credentials then restart", load="mid")
self.assertTrue(rec.get("ok"))
- self.assertTrue(rec.get("usable"))
self.assertIn("shared-token", rec.get("card") or "")
self.assertLessEqual(len(rec.get("card") or ""), 400)
- self.assertTrue(rec.get("planned"))
- self.assertEqual(self._plan_calls["n"], 1)
- self.assertNotIn("lattice", (rec.get("card") or "").lower())
+ self.assertEqual(self._calls["perceive_card"], 1)
+ self.assertEqual(self._calls["perceive"], 0)
+ self.assertEqual(self._calls["plan"], 0)
+ self.assertEqual(self._last_load, "mid")
- def test_no_plan_on_single_step(self) -> None:
+ def test_high_and_protect_skip(self) -> None:
from hermespace.insight_module import insight_card
- rec = insight_card("what is the status", goal="status")
- self.assertTrue(rec.get("ok"))
- self.assertFalse(rec.get("planned"))
- self.assertEqual(self._plan_calls["n"], 0)
+ for load in ("high", "protect", 0.7, 0.9):
+ rec = insight_card("First reproduce then patch then verify", load=load)
+ self.assertTrue(rec.get("ok"), load)
+ self.assertEqual(rec.get("skipped"), "high_load", load)
+ self.assertEqual(rec.get("card"), "", load)
+ self.assertEqual(self._calls["perceive_card"], 0)
+ self.assertEqual(self._calls["plan"], 0)
- def test_high_load_skip(self) -> None:
+ rec = insight_card("goal", high_load=True, load="low")
+ self.assertEqual(rec.get("skipped"), "high_load")
+ self.assertEqual(self._calls["perceive_card"], 0)
+
+ def test_caps_returned_card(self) -> None:
from hermespace.insight_module import insight_card
- rec = insight_card(
- "First reproduce then patch then verify",
- high_load=True,
- plan=["a", "b"],
- )
+ huge = "Y" * 900
+
+ def _huge(*_a, **_k):
+ self._calls["perceive_card"] += 1
+ return huge
+
+ with mock.patch.object(
+ sys.modules["hermes_insight"].HermesInsight,
+ "perceive_card",
+ _huge,
+ ):
+ rec = insight_card("a long goal", load="low")
self.assertTrue(rec.get("ok"))
- self.assertEqual(rec.get("skipped"), "high_load")
- self.assertEqual(rec.get("card"), "")
- self.assertEqual(self._plan_calls["n"], 0)
+ self.assertLessEqual(len(rec.get("card") or ""), 400)
+ self.assertTrue((rec.get("card") or "").endswith("..."))
-class TestWorkflowInsightMeta(unittest.TestCase):
+class TestWorkflowDoesNotHangInsight(unittest.TestCase):
def setUp(self) -> None:
self._td = tempfile.TemporaryDirectory()
os.environ["HERMESPACE_HOME"] = self._td.name
@@ -144,24 +193,84 @@ def tearDown(self) -> None:
self._td.cleanup()
os.environ.pop("HERMESPACE_HOME", None)
- def test_turn_records_insight_meta(self) -> None:
+ def test_turn_does_not_inject_insight_strip(self) -> None:
from hermespace.io_contract import HermespaceInput
from hermespace.workflow import Workflow
- out = Workflow().run(
- HermespaceInput(
- message="First inspect then implement finally verify",
- goal="Ship the fix",
- decision="A — implement",
- plan=["inspect", "implement", "verify"],
- say="Working the fix.",
- force=True,
- agent_id="insight-wf",
+ with mock.patch("hermespace.insight_module.insight_card") as mocked:
+ out = Workflow().run(
+ HermespaceInput(
+ message="First inspect then implement finally verify",
+ goal="Ship the fix",
+ decision="A — implement",
+ plan=["inspect", "implement", "verify"],
+ say="Working the fix.",
+ force=True,
+ agent_id="insight-wf",
+ )
+ )
+ self.assertFalse(out.skipped)
+ mocked.assert_not_called()
+ self.assertNotIn("insight", out.meta or {})
+
+
+class TestPreLlmHangsInsightNextToCube(unittest.TestCase):
+ def setUp(self) -> None:
+ self._td = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self._td.name
+ os.environ["HERMESPACE_NEURAL_VERBALIZE"] = "0"
+ os.environ["HERMESPACE_AUTO_ORDER"] = "0"
+
+ def tearDown(self) -> None:
+ self._td.cleanup()
+ for key in (
+ "HERMESPACE_HOME",
+ "HERMESPACE_NEURAL_VERBALIZE",
+ "HERMESPACE_AUTO_ORDER",
+ ):
+ os.environ.pop(key, None)
+
+ def test_pre_llm_appends_perceive_card(self) -> None:
+ from hermespace.hermes_bridge import on_pre_llm_call, on_session_start
+
+ on_session_start(session_id="insight-bridge")
+ with mock.patch(
+ "hermespace.insight_module.insight_card",
+ return_value={"ok": True, "mode": "insight", "card": "### Insight\n- lever: test"},
+ ) as mocked:
+ inj = on_pre_llm_call(
+ user_message="First build the feature then verify please",
+ session_id="insight-bridge",
+ is_first_turn=False,
+ )
+ self.assertIsNotNone(inj)
+ mocked.assert_called()
+ self.assertIn("### Insight", (inj or {}).get("context") or "")
+
+ def test_pre_llm_skips_insight_on_high_load(self) -> None:
+ from hermespace import AccessEngine
+ from hermespace.hermes_bridge import on_pre_llm_call, on_session_start
+ from hermespace.store import load_desk, save_desk
+
+ on_session_start(session_id="insight-high")
+ eng = AccessEngine(agent_id="hermes-agent", session_id="insight-high")
+ desk = load_desk(eng.desk_engine.desk_path)
+ desk.load = {"level": "high", "total": 0.8}
+ save_desk(desk, eng.desk_engine.desk_path)
+
+ with mock.patch(
+ "hermespace.insight_module.insight_card",
+ wraps=None,
+ ) as mocked:
+ mocked.return_value = {"ok": True, "mode": "skipped", "skipped": "high_load", "card": ""}
+ inj = on_pre_llm_call(
+ user_message="First build the feature then verify please",
+ session_id="insight-high",
+ is_first_turn=False,
)
- )
- self.assertFalse(out.skipped)
- self.assertIn("insight", out.meta or {})
- self.assertIn((out.meta or {}).get("insight", {}).get("mode"), ("missing", "insight", "soft_fail", "skipped"))
+ self.assertIsNotNone(inj)
+ mocked.assert_called()
+ self.assertNotIn("### Insight", (inj or {}).get("context") or "")
if __name__ == "__main__":
From bd4466d12e4914fa70b4b3c3758eb0e35b58a3ab Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 13:50:56 +0000
Subject: [PATCH 15/16] fix(cube): skip cube_beat on pre_llm when provider is
hermescube
Config-only single-pump: if Hermes memory.provider is hermescube, do not
call cube_beat / center.supply / build_space_inject on pre_llm. MemoryManager
already prefetched. Empty prefetch leaves no second strip. pulse_charge and
sync_world_beliefs stay on idle/enter/seal. No MemoryProvider in Space.
Co-authored-by: Pablo
---
PURPOSE.md | 2 +-
README.md | 2 +-
docs/architecture/HERMESCUBE.md | 4 +-
src/hermespace/cube_module.py | 85 ++++++++++-----------------------
src/hermespace/hermes_bridge.py | 28 +++++++----
tests/test_access_hub.py | 74 ++++++++++++++++++++++++++--
6 files changed, 117 insertions(+), 78 deletions(-)
diff --git a/PURPOSE.md b/PURPOSE.md
index ae71b15..d0002d2 100644
--- a/PURPOSE.md
+++ b/PURPOSE.md
@@ -91,7 +91,7 @@ World identity may remain agent-scoped; active cognition is session-scoped.
| Workbench | Session mode, parked goals, last native turn |
| WorldModel | Agent-scoped beliefs, landmarks, timeline |
| Hermes Agent | Model calls, tools, skills, approvals, transcript |
-| Optional Cube book | Durable long-tail SoT when `hermescube` is installed |
+| Optional Cube book | Durable long-tail SoT when `hermescube` is installed. If Hermes `memory.provider=hermescube`, skip the `pre_llm` FOA strip — MemoryManager already prefetched. |
| Optional Insight | `perceive_card` strip next to `cube_beat` on `pre_llm_call` when installed |
Cube and Insight stay standalone packages. Hermespace only **cables** them
diff --git a/README.md b/README.md
index e46f3fd..c30ea8a 100644
--- a/README.md
+++ b/README.md
@@ -141,7 +141,7 @@ Alongside the world, Hermespace provides a desk for the current turn — FOA, du
| **Functional Access Workspace** | Harness global workspace — hold/summon concepts, silent reasoning, FOA≤4, hub≤25 |
| **Focus of Attention** | ≤4 items, single active goal per turn |
| **Dual Decode** | Human gets a short report; the model gets dense context. Never dump raw inject into chat channels. |
-| **Cube heart (optional)** | Soft cable to HermesCube — `beat` / `seal` / `pulse`; skip/shrink when the Cube provider already prefetched; standalone warehouse when Cube absent |
+| **Cube heart (optional)** | Soft cable to HermesCube — skip `cube_beat` on `pre_llm` when `memory.provider=hermescube` (MemoryManager already prefetched); `pulse_charge` / `sync_world_beliefs` still recharge World; standalone warehouse when Cube absent |
| **Insight (optional)** | Soft cable next to `cube_beat` on `pre_llm_call` — `perceive_card` only, skip on high/protect; never required |
| **Skills + Memory Fabric** | Ranks Hermes skills per goal; injects MEMORY.md / USER.md excerpts |
| **Neural FOA** | `HERMESPACE_NEURAL_BACKEND=auto` — Ollama embeddings when live, hash fallback |
diff --git a/docs/architecture/HERMESCUBE.md b/docs/architecture/HERMESCUBE.md
index 6d88191..8b3b115 100644
--- a/docs/architecture/HERMESCUBE.md
+++ b/docs/architecture/HERMESCUBE.md
@@ -24,7 +24,7 @@ Hermes Agent ──connect──► Hermespace (J-Space · FOA · OEW)
| Hive (`HERMESCUBE_HIVE`) | Peer room / soul cards | Solo room |
| SemanticStore | Mirror / study | Local seal target |
-## Space adapter (`hermespace.cube_module` **1.2**)
+## Space adapter (`hermespace.cube_module` **1.3**)
```python
from hermespace.cube_module import (
@@ -45,7 +45,7 @@ from hermespace.cube_module import (
|------|-----|
| `connect_agent(agent_id)` | Session start / `HermesBase.connect` |
| `ensure_heart()` | Create cube or standalone dirs |
-| `cube_beat(query, seals=, load=)` | Turn / `pre_llm_call` — skip/shrink when Cube provider already prefetched |
+| `cube_beat(query, seals=, load=)` | FOA strip when Cube is **not** Hermes `memory.provider`. If `memory.provider=hermescube`, **skip entirely** on `pre_llm` — MemoryManager already prefetched. Empty prefetch is fine. Do not call `center.supply` / `build_space_inject` as a last prefetch. |
| `cube_pulse` / `sync_world` | Idle + connect charge |
| `room_status` | Hive awareness (`HERMESCUBE_HIVE`) |
| `seal_learning(text)` | Desk → durable archive |
diff --git a/src/hermespace/cube_module.py b/src/hermespace/cube_module.py
index 4015047..c4bccd6 100644
--- a/src/hermespace/cube_module.py
+++ b/src/hermespace/cube_module.py
@@ -23,7 +23,7 @@
logger = logging.getLogger("hermespace.cube_module")
# Local contract version — Space adapter surface (independent of Cube package).
-SPACE_CUBE_ADAPTER_VERSION = "1.2"
+SPACE_CUBE_ADAPTER_VERSION = "1.3"
# Load → arterial char budgets (match Cube center when present).
LOAD_STRIP_CHARS: dict[str, int] = {
@@ -233,10 +233,6 @@ def cube_status() -> dict[str, Any]:
return heart_status()
-def _truthy_env(name: str) -> bool:
- return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
-
-
def hermes_memory_provider() -> str:
"""Return Hermes ``memory.provider`` when detectable (never required)."""
for key in ("HERMES_MEMORY_PROVIDER", "MEMORY_PROVIDER"):
@@ -244,14 +240,12 @@ def hermes_memory_provider() -> str:
if raw:
return raw
home = os.environ.get("HERMES_HOME", "").strip()
- roots = []
- if home:
- roots.append(os.path.expanduser(home))
- roots.append(os.path.expanduser("~/.hermes"))
+ roots = [os.path.expanduser(home)] if home else [os.path.expanduser("~/.hermes")]
for root in roots:
cfg = os.path.join(root, "config.yaml")
try:
- text = open(cfg, encoding="utf-8").read()
+ with open(cfg, encoding="utf-8") as fh:
+ text = fh.read()
except OSError:
continue
in_memory = False
@@ -271,41 +265,25 @@ def hermes_memory_provider() -> str:
def cube_is_memory_provider() -> bool:
+ """True when Hermes ``memory.provider`` is Cube. Config-only — no Cube import."""
return hermes_memory_provider() in {"hermescube", "cube"}
+def skip_cube_foa_strip() -> bool:
+ """Skip the FOA Cube strip: Hermes MemoryManager already prefetched this turn.
+
+ Empty prefetch is fine — skip leaves no second strip. No Cube code required.
+ """
+ return cube_is_memory_provider()
+
+
def cube_already_prefetched(
query: str = "",
*,
session_id: str = "",
) -> bool:
- """True when the Cube memory provider already recalled this turn's book.
-
- Hermes ``memory.provider=hermescube`` prefetches independently. A second
- full ``cube_beat`` strip dual-pumps the same book — skip or shrink.
- """
- if _truthy_env("HERMESPACE_CUBE_PREFETCHED"):
- return True
- if not cube_is_memory_provider() or not cube_available():
- return False
- try:
- import gc
-
- from hermescube.provider import CubeMemoryProvider
-
- q = (query or "").strip()
- for obj in gc.get_objects():
- if not isinstance(obj, CubeMemoryProvider):
- continue
- last_q = str(getattr(obj, "_last_prefetch_query", "") or "")
- last_ids = list(getattr(obj, "_last_prefetch_ids", None) or [])
- if last_ids:
- return True
- if last_q and (not q or last_q[:40] in q or q[:40] in last_q):
- return True
- except Exception as e:
- logger.debug("cube prefetch probe miss: %s", e)
- return False
+ """Alias for ``skip_cube_foa_strip`` — config says Cube owns prefetch."""
+ return skip_cube_foa_strip()
def cube_beat(
@@ -324,21 +302,17 @@ def cube_beat(
Order: ensure → systole (seal) → diastole (supply) → optional autonomic.
- When Hermes ``memory.provider=hermescube`` already prefetched this turn,
- skip the full arterial strip (or shrink it) so the same book is not
- dual-pumped into model context.
+ When Hermes ``memory.provider=hermescube``, skip the arterial FOA strip
+ entirely. Do not call ``center.supply`` / ``build_space_inject`` as a
+ last prefetch — MemoryManager already ran ``CubeMemoryProvider.prefetch``.
+ Seals and ``pulse_charge`` remain allowed (World projection, not FOA).
"""
- prefetched = bool(skip_if_prefetched and cube_already_prefetched(query, session_id=session_id))
- provider_live = cube_is_memory_provider()
- skip_supply = prefetched
- shrink_supply = (not skip_supply) and provider_live and cube_available()
-
- if skip_supply:
+ if skip_if_prefetched and skip_cube_foa_strip():
level = normalize_load(load, high_load=high_load)
out: dict[str, Any] = {
"api_version": "1.0",
"adapter": SPACE_CUBE_ADAPTER_VERSION,
- "mode": "center" if cube_available() else "standalone",
+ "mode": "skipped",
"ok": True,
"phases": {"diastole": {"ok": True, "skipped": "provider_prefetch", "chars": 0}},
"block": "",
@@ -360,9 +334,6 @@ def cube_beat(
out["phases"]["autonomic"] = cube_pulse(agent_id=agent_id)
return out
- beat_load: str | float | None = "protect" if shrink_supply else load
- beat_high = True if shrink_supply else high_load
-
try:
from hermescube.center import beat
@@ -370,8 +341,8 @@ def cube_beat(
query or "",
seals=seals,
entry_type=entry_type,
- load=beat_load,
- high_load=beat_high,
+ load=load,
+ high_load=high_load,
agent_id=agent_id,
charge=charge,
session_id=session_id,
@@ -379,18 +350,12 @@ def cube_beat(
if isinstance(out, dict):
out["adapter"] = SPACE_CUBE_ADAPTER_VERSION
out["mode"] = "center"
- if shrink_supply:
- out["shrunk"] = "provider_active"
- block = str(out.get("block") or "")
- cap = strip_budget("protect")
- if len(block) > cap:
- out["block"] = block[: cap - 3] + "..."
return out
except Exception as e:
logger.debug("center.beat miss: %s", e)
# Heart 1.0 / standalone fallback
- level = normalize_load(beat_load, high_load=beat_high)
+ level = normalize_load(load, high_load=high_load)
out = {
"api_version": "1.0",
"adapter": SPACE_CUBE_ADAPTER_VERSION,
@@ -400,8 +365,6 @@ def cube_beat(
"block": "",
"load_level": level,
}
- if shrink_supply:
- out["shrunk"] = "provider_active"
out["phases"]["ensure"] = ensure_heart()
if seals is not None:
items = [seals] if isinstance(seals, str) else list(seals)
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index bdbb16e..ef4e7c9 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -411,20 +411,32 @@ def on_pre_llm_call(
# HermesCube / standalone warehouse — dense deep memory under load
# Prefer center.beat (1.1); falls back to heart inject / standalone strip
try:
- from hermespace.cube_module import cube_beat
+ from hermespace.cube_module import cube_beat, skip_cube_foa_strip
from hermespace.access import AccessHub
q = (msg or desk.goal or "")[:500]
load_val: str | float = desk.load.get("total", 0.5) if isinstance(desk.load, dict) else 0.5
if high_load:
load_val = "high"
- beat = cube_beat(
- q,
- load=load_val,
- agent_id=agent_id,
- session_id=sid or "hermespace",
- )
- cube_block = str(beat.get("block") or "")
+ # Cube as Hermes memory.provider: MemoryManager already prefetched.
+ # Do not call cube_beat / supply / build_space_inject — that re-pumps.
+ if skip_cube_foa_strip():
+ cube_block = ""
+ beat = {
+ "ok": True,
+ "mode": "skipped",
+ "skipped": "provider_prefetch",
+ "block": "",
+ "load_level": load_val,
+ }
+ else:
+ beat = cube_beat(
+ q,
+ load=load_val,
+ agent_id=agent_id,
+ session_id=sid or "hermespace",
+ )
+ cube_block = str(beat.get("block") or "")
if cube_block:
block += "\n\n" + cube_block
# Insight strip — next to cube_beat. perceive_card only; skip if missing.
diff --git a/tests/test_access_hub.py b/tests/test_access_hub.py
index 6fe8c18..a26bcf2 100644
--- a/tests/test_access_hub.py
+++ b/tests/test_access_hub.py
@@ -88,6 +88,9 @@ def setUp(self) -> None:
self._td = tempfile.TemporaryDirectory()
self.root = Path(self._td.name)
os.environ["HERMESPACE_HOME"] = str(self.root)
+ os.environ["HERMES_HOME"] = str(self.root)
+ os.environ.pop("HERMES_MEMORY_PROVIDER", None)
+ os.environ.pop("MEMORY_PROVIDER", None)
# Force standalone even if hermescube is installed in the agent env
import builtins
@@ -105,6 +108,9 @@ def tearDown(self) -> None:
self._imp.stop()
self._td.cleanup()
os.environ.pop("HERMESPACE_HOME", None)
+ os.environ.pop("HERMES_HOME", None)
+ os.environ.pop("HERMES_MEMORY_PROVIDER", None)
+ os.environ.pop("MEMORY_PROVIDER", None)
def test_ensure_and_status(self) -> None:
from hermespace.cube_module import center_status, ensure_heart, heart_status
@@ -136,17 +142,39 @@ def test_seal_and_inject(self) -> None:
self.assertIn("block", beat)
self.assertEqual(beat.get("mode"), "standalone")
- def test_prefetch_skip_does_not_inject_strip(self) -> None:
- os.environ["HERMESPACE_CUBE_PREFETCHED"] = "1"
+ def test_provider_skip_does_not_inject_strip(self) -> None:
+ os.environ["HERMES_MEMORY_PROVIDER"] = "hermescube"
try:
- from hermespace.cube_module import cube_already_prefetched, cube_beat
+ from hermespace.cube_module import (
+ cube_already_prefetched,
+ cube_beat,
+ skip_cube_foa_strip,
+ )
+ self.assertTrue(skip_cube_foa_strip())
self.assertTrue(cube_already_prefetched("deploy"))
- beat = cube_beat("deploy", load="mid", agent_id="prefetch-agent")
+ with mock.patch("hermespace.cube_module.cube_inject") as inj:
+ beat = cube_beat("deploy", load="mid", agent_id="prefetch-agent")
+ inj.assert_not_called()
self.assertEqual(beat.get("skipped"), "provider_prefetch")
self.assertEqual(beat.get("block"), "")
+ self.assertEqual(beat.get("mode"), "skipped")
+ finally:
+ os.environ.pop("HERMES_MEMORY_PROVIDER", None)
+
+ def test_provider_from_hermes_config_yaml(self) -> None:
+ home = Path(self._td.name)
+ (home / "config.yaml").write_text("memory:\n provider: hermescube\n", encoding="utf-8")
+ os.environ["HERMES_HOME"] = str(home)
+ os.environ.pop("HERMES_MEMORY_PROVIDER", None)
+ os.environ.pop("MEMORY_PROVIDER", None)
+ try:
+ from hermespace.cube_module import hermes_memory_provider, skip_cube_foa_strip
+
+ self.assertEqual(hermes_memory_provider(), "hermescube")
+ self.assertTrue(skip_cube_foa_strip())
finally:
- os.environ.pop("HERMESPACE_CUBE_PREFETCHED", None)
+ os.environ.pop("HERMES_HOME", None)
def test_strip_budget(self) -> None:
from hermespace.cube_module import normalize_load, strip_budget
@@ -222,5 +250,41 @@ def test_turn_includes_access_meta(self) -> None:
self.assertTrue(out.context)
+class TestPreLlmCubeSinglePump(unittest.TestCase):
+ def setUp(self) -> None:
+ self._td = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self._td.name
+ os.environ["HERMES_HOME"] = self._td.name
+ os.environ["HERMESPACE_NEURAL_VERBALIZE"] = "0"
+ os.environ["HERMESPACE_AUTO_ORDER"] = "0"
+ os.environ["HERMES_MEMORY_PROVIDER"] = "hermescube"
+
+ def tearDown(self) -> None:
+ self._td.cleanup()
+ for key in (
+ "HERMESPACE_HOME",
+ "HERMES_HOME",
+ "HERMESPACE_NEURAL_VERBALIZE",
+ "HERMESPACE_AUTO_ORDER",
+ "HERMES_MEMORY_PROVIDER",
+ ):
+ os.environ.pop(key, None)
+
+ def test_pre_llm_does_not_call_cube_beat(self) -> None:
+ from hermespace.hermes_bridge import on_pre_llm_call, on_session_start
+
+ on_session_start(session_id="cube-single-pump")
+ with mock.patch("hermespace.cube_module.cube_beat") as beat:
+ inj = on_pre_llm_call(
+ user_message="First build the feature then verify please",
+ session_id="cube-single-pump",
+ is_first_turn=False,
+ )
+ beat.assert_not_called()
+ self.assertIsNotNone(inj)
+ ctx = (inj or {}).get("context") or ""
+ self.assertNotIn("### Cube", ctx)
+
+
if __name__ == "__main__":
unittest.main()
From 09b244ff3f81aaacdafa24c38f532362bfe33aa1 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 13:52:57 +0000
Subject: [PATCH 16/16] fix(cube): keep cube_beat when Hermes config is
unreadable
Fail-soft: missing or corrupt memory.provider config means provider off,
so pre_llm still runs cube_beat. Skip only when provider is confirmed
hermescube. Insight still never calls recall / perceive / insight_beat.
Co-authored-by: Pablo
---
docs/architecture/INSIGHT.md | 2 +-
src/hermespace/cube_module.py | 73 +++++++++++++++++++-------------
src/hermespace/insight_module.py | 9 ++--
tests/test_access_hub.py | 24 +++++++++++
tests/test_insight_module.py | 8 +++-
5 files changed, 80 insertions(+), 36 deletions(-)
diff --git a/docs/architecture/INSIGHT.md b/docs/architecture/INSIGHT.md
index fd35e55..0a444a3 100644
--- a/docs/architecture/INSIGHT.md
+++ b/docs/architecture/INSIGHT.md
@@ -17,7 +17,7 @@ if hasattr(HermesInsight, "perceive_card"):
| Missing / no `perceive_card` | Skip — do **not** format `perceive()["card"]` (unbounded lattice) |
| High / protect load | Skip entirely (stricter than Cube) |
| Hot path | Append only the returned card, capped at 400 chars |
-| `insight_plan` / `.plan` | Never on `pre_llm_call` |
+| `insight_plan` / `.plan` / `recall` / `insight_beat` | Never on `pre_llm_call` |
| Required? | Never — `except Exception: pass` like `cube_beat` |
Companion: [PabloTheThinker/hermes-insight](https://github.com/PabloTheThinker/hermes-insight)
diff --git a/src/hermespace/cube_module.py b/src/hermespace/cube_module.py
index c4bccd6..1b52bed 100644
--- a/src/hermespace/cube_module.py
+++ b/src/hermespace/cube_module.py
@@ -234,47 +234,60 @@ def cube_status() -> dict[str, Any]:
def hermes_memory_provider() -> str:
- """Return Hermes ``memory.provider`` when detectable (never required)."""
- for key in ("HERMES_MEMORY_PROVIDER", "MEMORY_PROVIDER"):
- raw = os.environ.get(key, "").strip().lower()
- if raw:
- return raw
- home = os.environ.get("HERMES_HOME", "").strip()
- roots = [os.path.expanduser(home)] if home else [os.path.expanduser("~/.hermes")]
- for root in roots:
- cfg = os.path.join(root, "config.yaml")
- try:
- with open(cfg, encoding="utf-8") as fh:
- text = fh.read()
- except OSError:
- continue
- in_memory = False
- for line in text.splitlines():
- raw = line.split("#", 1)[0]
- if raw.strip().startswith("memory:") or raw.strip() == "memory:":
- in_memory = True
- continue
- if in_memory and raw and not raw[:1].isspace() and not raw.startswith("\t"):
- in_memory = False
- if not in_memory:
+ """Return Hermes ``memory.provider`` when detectable (never required).
+
+ Fail-soft: missing or unreadable config → empty string (provider off).
+ """
+ try:
+ for key in ("HERMES_MEMORY_PROVIDER", "MEMORY_PROVIDER"):
+ raw = os.environ.get(key, "").strip().lower()
+ if raw:
+ return raw
+ home = os.environ.get("HERMES_HOME", "").strip()
+ roots = [os.path.expanduser(home)] if home else [os.path.expanduser("~/.hermes")]
+ for root in roots:
+ cfg = os.path.join(root, "config.yaml")
+ try:
+ with open(cfg, encoding="utf-8") as fh:
+ text = fh.read()
+ except OSError:
continue
- stripped = raw.strip()
- if stripped.startswith("provider:"):
- return stripped.split(":", 1)[1].strip().strip("\"'").lower()
- return ""
+ in_memory = False
+ for line in text.splitlines():
+ raw = line.split("#", 1)[0]
+ if raw.strip().startswith("memory:") or raw.strip() == "memory:":
+ in_memory = True
+ continue
+ if in_memory and raw and not raw[:1].isspace() and not raw.startswith("\t"):
+ in_memory = False
+ if not in_memory:
+ continue
+ stripped = raw.strip()
+ if stripped.startswith("provider:"):
+ return stripped.split(":", 1)[1].strip().strip("\"'").lower()
+ return ""
+ except Exception:
+ return ""
def cube_is_memory_provider() -> bool:
"""True when Hermes ``memory.provider`` is Cube. Config-only — no Cube import."""
- return hermes_memory_provider() in {"hermescube", "cube"}
+ try:
+ return hermes_memory_provider() in {"hermescube", "cube"}
+ except Exception:
+ return False
def skip_cube_foa_strip() -> bool:
- """Skip the FOA Cube strip: Hermes MemoryManager already prefetched this turn.
+ """Skip the FOA Cube strip only when Cube is confirmed as memory.provider.
+ Unreadable / missing config → False (provider off) so ``cube_beat`` still runs.
Empty prefetch is fine — skip leaves no second strip. No Cube code required.
"""
- return cube_is_memory_provider()
+ try:
+ return cube_is_memory_provider()
+ except Exception:
+ return False
def cube_already_prefetched(
diff --git a/src/hermespace/insight_module.py b/src/hermespace/insight_module.py
index ef12c99..19be3fc 100644
--- a/src/hermespace/insight_module.py
+++ b/src/hermespace/insight_module.py
@@ -8,9 +8,9 @@
Hang the returned card next to ``cube_beat`` on ``pre_llm_call``.
Skip entirely on high/protect load. Until ``perceive_card`` exists, skip —
-do not format ``perceive()`` output (unbounded lattice). Do not call
-``insight_plan`` / ``HermesInsight.plan`` on the hot path. Do not register
-Insight hooks. Do not vendor Insight source.
+do not format ``perceive()`` output or ``recall()["brief"]``. Do not call
+``recall``, ``insight_plan``, ``insight_beat``, or ``HermesInsight.plan``
+on the hot path. Do not register Insight hooks. Do not vendor Insight source.
See docs/architecture/INSIGHT.md.
"""
@@ -85,7 +85,8 @@ def insight_card(
"""Call ``HermesInsight().perceive_card`` and return only that card.
Soft-fail if Insight is absent or ``perceive_card`` is missing.
- Never calls ``perceive`` / ``plan``. Never formats a lattice dump.
+ Never calls ``recall`` / ``perceive`` / ``plan`` / ``insight_beat``.
+ Never formats a lattice dump.
"""
out: dict[str, Any] = {
"ok": True,
diff --git a/tests/test_access_hub.py b/tests/test_access_hub.py
index a26bcf2..54c41b2 100644
--- a/tests/test_access_hub.py
+++ b/tests/test_access_hub.py
@@ -162,6 +162,30 @@ def test_provider_skip_does_not_inject_strip(self) -> None:
finally:
os.environ.pop("HERMES_MEMORY_PROVIDER", None)
+ def test_unreadable_config_keeps_cube_beat(self) -> None:
+ os.environ.pop("HERMES_MEMORY_PROVIDER", None)
+ os.environ.pop("MEMORY_PROVIDER", None)
+ os.environ["HERMES_HOME"] = str(self.root / "missing-hermes-home")
+ from hermespace.cube_module import cube_beat, hermes_memory_provider, skip_cube_foa_strip
+
+ self.assertEqual(hermes_memory_provider(), "")
+ self.assertFalse(skip_cube_foa_strip())
+ beat = cube_beat("deploy", load="mid", agent_id="config-miss-agent")
+ self.assertNotEqual(beat.get("skipped"), "provider_prefetch")
+ self.assertIn("block", beat)
+
+ def test_corrupt_config_keeps_cube_beat(self) -> None:
+ (self.root / "config.yaml").write_bytes(b"\xff\xfe not-utf8 \x00memory:\n provider: hermescube\n")
+ os.environ["HERMES_HOME"] = str(self.root)
+ os.environ.pop("HERMES_MEMORY_PROVIDER", None)
+ os.environ.pop("MEMORY_PROVIDER", None)
+ from hermespace.cube_module import cube_beat, skip_cube_foa_strip
+
+ self.assertFalse(skip_cube_foa_strip())
+ beat = cube_beat("deploy", load="mid", agent_id="config-bad-agent")
+ self.assertNotEqual(beat.get("skipped"), "provider_prefetch")
+ self.assertIn("block", beat)
+
def test_provider_from_hermes_config_yaml(self) -> None:
home = Path(self._td.name)
(home / "config.yaml").write_text("memory:\n provider: hermescube\n", encoding="utf-8")
diff --git a/tests/test_insight_module.py b/tests/test_insight_module.py
index f4f09f5..18dc95c 100644
--- a/tests/test_insight_module.py
+++ b/tests/test_insight_module.py
@@ -99,7 +99,7 @@ class TestInsightPerceiveCard(unittest.TestCase):
def setUp(self) -> None:
self._td = tempfile.TemporaryDirectory()
os.environ["HERMESPACE_HOME"] = self._td.name
- self._calls = {"perceive_card": 0, "perceive": 0, "plan": 0}
+ self._calls = {"perceive_card": 0, "perceive": 0, "plan": 0, "recall": 0}
self._last_load = None
outer = self
@@ -124,6 +124,10 @@ def plan(self, situation, **kwargs):
outer._calls["plan"] += 1
return {"steps": [{"title": "should-not-plan"}]}
+ def recall(self, *a, **k):
+ outer._calls["recall"] = outer._calls.get("recall", 0) + 1
+ return {"brief": "SHOULD-NOT-INJECT-RECALL-BRIEF"}
+
pkg = types.ModuleType("hermes_insight")
pkg.HermesInsight = _Cls # type: ignore[attr-defined]
pkg.__version__ = "0.9.0-test"
@@ -147,7 +151,9 @@ def test_appends_bounded_perceive_card_only(self) -> None:
self.assertEqual(self._calls["perceive_card"], 1)
self.assertEqual(self._calls["perceive"], 0)
self.assertEqual(self._calls["plan"], 0)
+ self.assertEqual(self._calls["recall"], 0)
self.assertEqual(self._last_load, "mid")
+ self.assertNotIn("SHOULD-NOT-INJECT", rec.get("card") or "")
def test_high_and_protect_skip(self) -> None:
from hermespace.insight_module import insight_card