From db852630835270c448b4e21a28c770e23918c5c3 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Mon, 3 Aug 2026 04:09:08 +0000
Subject: [PATCH 01/32] 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/32] 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/32] 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/32] 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/32] =?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/32] =?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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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
From 23e64ec8a9b02510fe963056ec61ec900e34bb47 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 14:05:18 +0000
Subject: [PATCH 17/32] feat(execute): AuDHD one live goal, named park,
action-lead report
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Steal execute/focus shapes from hermes-audhd-skills without vendoring it.
One live goal; park others as Name — state — next crumb. Report line 1 is
the next action; lists cap at 5; never quiz as the lead. Fabric-hint
audhd-* SKILL.md when present; skip emotion and skip if missing.
Co-authored-by: Pablo
---
src/hermespace/access/engine.py | 21 +++-
src/hermespace/execute_focus.py | 181 ++++++++++++++++++++++++++++++++
src/hermespace/hermes_bridge.py | 16 +++
src/hermespace/workbench.py | 53 ++++++++--
src/hermespace/workflow.py | 44 ++++++++
tests/test_execute_focus.py | 78 ++++++++++++++
6 files changed, 386 insertions(+), 7 deletions(-)
create mode 100644 src/hermespace/execute_focus.py
create mode 100644 tests/test_execute_focus.py
diff --git a/src/hermespace/access/engine.py b/src/hermespace/access/engine.py
index 62ffeaf..4359df5 100644
--- a/src/hermespace/access/engine.py
+++ b/src/hermespace/access/engine.py
@@ -458,7 +458,26 @@ 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)
+ hub = self.hub.report(include_silent=include_silent)
+ try:
+ from hermespace.execute_focus import execute_report_block
+ from hermespace.workbench import Workbench
+
+ desk = self.desk
+ parked = Workbench(
+ agent_id=self.agent_id,
+ session_id=self.session_id,
+ ).state.park
+ lead = execute_report_block(
+ goal=desk.goal,
+ plan=list(desk.plan or []),
+ say=desk.say,
+ decision=desk.decision,
+ parked=parked,
+ )
+ return f"{lead}\n\n{hub}".strip()
+ except Exception:
+ return hub
def broadcast(self, *, high_load: bool = False) -> str:
return self.env.filtered_broadcast(high_load=high_load)
diff --git a/src/hermespace/execute_focus.py b/src/hermespace/execute_focus.py
new file mode 100644
index 0000000..d5b2029
--- /dev/null
+++ b/src/hermespace/execute_focus.py
@@ -0,0 +1,181 @@
+"""AuDHD execute/focus shapes for Access Engine report + park stack.
+
+Stolen from hermes-audhd-skills (standalone repo) — shapes only:
+
+ one live goal
+ named parking lot: ``Name — state — next crumb``
+ report line 1 = next action; lists ≤5; never quiz/restate as the lead
+
+Do not vendor that skill tree. Do not hint ``audhd-emotion`` into a generic
+ops profile. Public reference:
+https://github.com/PabloTheThinker/hermes-audhd-skills
+"""
+
+from __future__ import annotations
+
+import re
+from typing import Any, Sequence
+
+PARK_SEP = " — "
+LIST_CAP = 5
+AUDHD_HINT_NAMES = (
+ "audhd-core",
+ "audhd-execute",
+ "audhd-focus",
+ "audhd-communicate",
+ "audhd-integrate",
+)
+
+_QUIZ_LEAD = re.compile(
+ r"^\s*("
+ r"remember when|as i (said|mentioned)|we should think|"
+ r"what do you (think|want|remember)|did you want|"
+ r"just to recap|as mentioned earlier"
+ r")\b",
+ re.IGNORECASE,
+)
+_LIST_LINE = re.compile(r"^\s*(?:[-*]|\d+[.)])\s+\S")
+
+
+def short_name(goal: str, *, cap: int = 40) -> str:
+ g = " ".join((goal or "").split())
+ if not g:
+ return "untitled"
+ return g if len(g) <= cap else g[: cap - 1].rstrip() + "…"
+
+
+def format_park_line(item: dict[str, Any] | str) -> str:
+ """Named parking lot: Name — state — next crumb."""
+ if isinstance(item, str):
+ return f"{short_name(item)}{PARK_SEP}parked{PARK_SEP}resume"
+ name = str(item.get("name") or short_name(str(item.get("goal") or ""))).strip()
+ state = str(item.get("state") or "parked").strip() or "parked"
+ crumb = str(
+ item.get("next_crumb") or item.get("note") or "resume"
+ ).strip() or "resume"
+ return f"{name}{PARK_SEP}{state}{PARK_SEP}{crumb}"
+
+
+def park_record(
+ goal: str,
+ *,
+ name: str = "",
+ state: str = "parked",
+ next_crumb: str = "",
+ note: str = "",
+) -> dict[str, Any]:
+ g = (goal or "").strip()
+ return {
+ "goal": g,
+ "name": (name or short_name(g)).strip(),
+ "state": (state or "parked").strip(),
+ "next_crumb": (next_crumb or note or "resume").strip(),
+ "note": (note or next_crumb or "").strip(),
+ }
+
+
+def next_action_line(
+ *,
+ goal: str = "",
+ plan: Sequence[str] | None = None,
+ say: str = "",
+ decision: str = "",
+) -> str:
+ for step in plan or []:
+ s = str(step or "").strip()
+ if s:
+ return s[:160]
+ for raw in (say or "").splitlines():
+ s = raw.strip().lstrip("-* ").lstrip("0123456789.) ")
+ if s and not _QUIZ_LEAD.match(s) and not s.endswith("?"):
+ return s[:160]
+ dec = (decision or "").strip()
+ if dec and not dec.lower().startswith("a — proceed"):
+ return dec[:160]
+ g = (goal or "").strip()
+ if g:
+ return f"Do the next step on: {short_name(g, cap=80)}"
+ return "Name the first action (under 2 minutes)."
+
+
+def _cap_lists(text: str, *, cap: int = LIST_CAP) -> str:
+ out: list[str] = []
+ listed = 0
+ for line in (text or "").splitlines():
+ if _LIST_LINE.match(line):
+ listed += 1
+ if listed > cap:
+ continue
+ out.append(line)
+ return "\n".join(out).strip()
+
+
+def shape_execute_report(
+ report: str,
+ *,
+ goal: str = "",
+ plan: Sequence[str] | None = None,
+ say: str = "",
+ decision: str = "",
+) -> str:
+ """Line 1 = next action. Lists ≤5. Never quiz/restate as the lead."""
+ body = _cap_lists((report or "").strip())
+ lead = next_action_line(goal=goal, plan=plan, say=say or body, decision=decision)
+ if not body:
+ return lead
+ first = body.splitlines()[0]
+ if _QUIZ_LEAD.match(first) or first.strip().endswith("?"):
+ rest = "\n".join(body.splitlines()[1:]).strip()
+ return f"{lead}\n{rest}".strip() if rest else lead
+ if first.strip() != lead and not _LIST_LINE.match(first):
+ # Keep an existing action lead; only prepend when the body starts as a list.
+ return body
+ if _LIST_LINE.match(first):
+ return f"{lead}\n{body}".strip()
+ return body
+
+
+def execute_report_block(
+ *,
+ goal: str = "",
+ plan: Sequence[str] | None = None,
+ say: str = "",
+ decision: str = "",
+ parked: Sequence[dict[str, Any] | str] | None = None,
+) -> str:
+ lines = [next_action_line(goal=goal, plan=plan, say=say, decision=decision)]
+ if goal:
+ lines.append(f"Live: {short_name(goal, cap=80)}")
+ items = list(parked or [])[:LIST_CAP]
+ if items:
+ lines.append("Parked:")
+ for item in items:
+ lines.append(f"- {format_park_line(item)}")
+ return "\n".join(lines)
+
+
+def audhd_skill_hints(*, hermes_home: Any = None) -> list[str]:
+ """Fabric-hint audhd-* SKILL.md files when present. Skip emotion. Skip if missing."""
+ from pathlib import Path
+
+ from hermespace.environment import hermes_home as _hh
+
+ root = Path(hermes_home) if hermes_home else _hh()
+ skills = root / "skills"
+ if not skills.is_dir():
+ return []
+ found: list[str] = []
+ try:
+ for p in skills.rglob("SKILL.md"):
+ name = p.parent.name
+ if name == "audhd-emotion" or "emotion" in name:
+ continue
+ if name.startswith("audhd-") and name in AUDHD_HINT_NAMES:
+ found.append(name)
+ except OSError:
+ return []
+ out: list[str] = []
+ for name in AUDHD_HINT_NAMES:
+ if name in found:
+ out.append(f"[exec|0.70] skill_hint:{name} (use skill_view name={name})")
+ return out[:LIST_CAP]
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index ef4e7c9..34477ec 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -84,6 +84,14 @@ def on_session_start(**kwargs: Any) -> dict[str, str] | None:
for hint in skill_load_hints(fab.skill_hits):
if hint not in desk.concepts:
desk.concepts.append(hint)
+ try:
+ from hermespace.execute_focus import audhd_skill_hints
+
+ for hint in audhd_skill_hints():
+ if hint not in desk.concepts:
+ desk.concepts.append(hint)
+ except Exception:
+ pass
desk.concepts = desk.concepts[-12:]
save_desk(desk, eng.desk_path)
desk = load_desk(eng.desk_path)
@@ -354,6 +362,14 @@ def on_pre_llm_call(
for hint in skill_load_hints(fab.skill_hits):
if hint not in desk.concepts:
desk.concepts.append(hint)
+ try:
+ from hermespace.execute_focus import audhd_skill_hints
+
+ for hint in audhd_skill_hints():
+ if hint not in desk.concepts:
+ desk.concepts.append(hint)
+ except Exception:
+ pass
desk.concepts = desk.concepts[-12:]
save_desk(desk, eng.desk_path)
desk = load_desk(eng.desk_path)
diff --git a/src/hermespace/workbench.py b/src/hermespace/workbench.py
index a7c262a..ef6852e 100644
--- a/src/hermespace/workbench.py
+++ b/src/hermespace/workbench.py
@@ -42,6 +42,9 @@ def _utcnow() -> str:
class ParkedGoal:
goal: str
note: str = ""
+ name: str = ""
+ state: str = "parked"
+ next_crumb: str = ""
parked_at: str = field(default_factory=_utcnow)
tags: list[str] = field(default_factory=list)
@@ -221,17 +224,38 @@ def enter(self, *, connect_warehouse: bool = True) -> dict[str, Any]:
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]:
- """Park a goal while staying monotropic on current work / idle."""
+ def park_goal(
+ self,
+ goal: str,
+ note: str = "",
+ tags: list[str] | None = None,
+ *,
+ name: str = "",
+ state: str = "parked",
+ next_crumb: str = "",
+ ) -> dict[str, Any]:
+ """Park a goal while staying monotropic on current work / idle.
+
+ Named lot format: ``Name — state — next crumb``.
+ """
+ from hermespace.execute_focus import format_park_line, park_record
+
g = (goal or "").strip()
if not g:
return self.status()
- self.state.park = [p for p in self.state.park if p.get("goal") != g]
- self.state.park.append(
- asdict(ParkedGoal(goal=g, note=note or "", tags=list(tags or [])))
+ rec = park_record(
+ g,
+ name=name,
+ state=state,
+ next_crumb=next_crumb or note,
+ note=note,
)
- # keep park bounded
+ rec["tags"] = list(tags or [])
+ rec["parked_at"] = _utcnow()
+ self.state.park = [p for p in self.state.park if p.get("goal") != g]
+ self.state.park.append(rec)
self.state.park = self.state.park[-20:]
+ self.state.meta["last_park_line"] = format_park_line(rec)
self.save()
return self.status()
@@ -317,6 +341,17 @@ def receive_order(
"""Order arrives → leave idle, run Hermespace turn, return dual decode."""
msg = (message or "").strip()
g = (goal or "").strip()
+ try:
+ live = str((self.workflow.status() or {}).get("goal") or "").strip()
+ if live and g and live != g:
+ self.park_goal(
+ live,
+ note="switched",
+ state="parked",
+ next_crumb="resume when this tunnel yields",
+ )
+ except Exception:
+ pass
if not g and use_parked_if_empty_goal and self.state.park:
parked = self.pop_park()
if parked:
@@ -381,6 +416,11 @@ def receive_order(
"reason": out.reason,
}
+ def park_lines(self) -> list[str]:
+ from hermespace.execute_focus import format_park_line
+
+ return [format_park_line(p) for p in self.state.park[-5:]]
+
def environment(self) -> dict[str, Any]:
"""Full pocket-dimension tool/memory/skills inventory."""
rep = probe_environment()
@@ -398,6 +438,7 @@ def status(self) -> dict[str, Any]:
"mode": self.state.mode,
"park_count": len(self.state.park),
"park": self.state.park[-5:],
+ "park_lines": self.park_lines(),
"last_order": self.state.last_order[:160],
"last_report": self.state.last_report[:200],
"last_turn_id": self.state.last_turn_id,
diff --git a/src/hermespace/workflow.py b/src/hermespace/workflow.py
index 1850c37..5ef77ee 100644
--- a/src/hermespace/workflow.py
+++ b/src/hermespace/workflow.py
@@ -95,6 +95,29 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
cons = payload.concepts or existing.concepts
ch = payload.choices or existing.choices or ["A — proceed"]
+ if (
+ existing.goal
+ and payload.goal
+ and existing.goal.strip()
+ and payload.goal.strip()
+ and existing.goal.strip() != payload.goal.strip()
+ ):
+ try:
+ from hermespace.workbench import Workbench
+
+ Workbench(
+ agent_id=payload.agent_id or "hermes-agent",
+ session_id=payload.session_id or "default",
+ workflow=self,
+ ).park_goal(
+ existing.goal,
+ note="one live goal",
+ state="parked",
+ next_crumb="resume when this tunnel yields",
+ )
+ except Exception:
+ pass
+
if payload.force or not existing.goal or payload.goal:
desk = self.engine.enter(
goal=g,
@@ -136,6 +159,14 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
for hint in skill_load_hints(fab.skill_hits):
if hint not in desk.concepts:
desk.concepts.append(hint)
+ try:
+ from hermespace.execute_focus import audhd_skill_hints
+
+ for hint in audhd_skill_hints():
+ if hint not in desk.concepts:
+ desk.concepts.append(hint)
+ except Exception:
+ pass
desk.concepts = desk.concepts[-12:]
save_desk(desk, self.engine.desk_path)
except Exception as exc:
@@ -259,6 +290,19 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
report = env.shape_user_report(report)
except Exception:
pass
+ try:
+ from hermespace.execute_focus import shape_execute_report
+
+ report = shape_execute_report(
+ report,
+ goal=desk.goal,
+ plan=list(desk.plan or []),
+ say=desk.say or report,
+ decision=desk.decision,
+ )
+ desk.say = report
+ except Exception:
+ pass
if payload.seal:
self.engine.seal(payload.seal_note or f"turn seal: {desk.decision[:120]}")
diff --git a/tests/test_execute_focus.py b/tests/test_execute_focus.py
new file mode 100644
index 0000000..7b091df
--- /dev/null
+++ b/tests/test_execute_focus.py
@@ -0,0 +1,78 @@
+"""AuDHD execute/focus shapes — one live goal, named park, report lead."""
+
+from __future__ import annotations
+
+import os
+import tempfile
+import unittest
+from pathlib import Path
+
+
+class TestExecuteFocusShapes(unittest.TestCase):
+ def test_park_line_format(self) -> None:
+ from hermespace.execute_focus import format_park_line, park_record
+
+ rec = park_record("Ship the auth fix", state="waiting", next_crumb="reopen PR")
+ line = format_park_line(rec)
+ self.assertEqual(line, "Ship the auth fix — waiting — reopen PR")
+
+ def test_report_lead_is_next_action_not_quiz(self) -> None:
+ from hermespace.execute_focus import shape_execute_report
+
+ out = shape_execute_report(
+ "Remember when we talked about auth?\nAlso docs.",
+ goal="Fix auth",
+ plan=["Run the failing test", "Patch TTL"],
+ )
+ self.assertTrue(out.startswith("Run the failing test"))
+ self.assertNotIn("Remember when", out.splitlines()[0])
+
+ def test_lists_capped_at_five(self) -> None:
+ from hermespace.execute_focus import shape_execute_report
+
+ body = "\n".join(f"{i}) step {i}" for i in range(1, 9))
+ out = shape_execute_report(body, plan=["Start the first step"])
+ listed = [ln for ln in out.splitlines() if ln[:1].isdigit()]
+ self.assertLessEqual(len(listed), 5)
+
+ def test_skips_missing_and_emotion_skills(self) -> None:
+ from hermespace.execute_focus import audhd_skill_hints
+
+ with tempfile.TemporaryDirectory() as td:
+ root = Path(td)
+ skills = root / "skills"
+ (skills / "audhd-execute").mkdir(parents=True)
+ (skills / "audhd-execute" / "SKILL.md").write_text("# execute\n", encoding="utf-8")
+ (skills / "audhd-emotion").mkdir(parents=True)
+ (skills / "audhd-emotion" / "SKILL.md").write_text("# emotion\n", encoding="utf-8")
+ hints = audhd_skill_hints(hermes_home=root)
+ joined = " ".join(hints)
+ self.assertIn("audhd-execute", joined)
+ self.assertNotIn("emotion", joined)
+ self.assertEqual(audhd_skill_hints(hermes_home=root / "missing"), [])
+
+ def test_one_live_goal_parks_the_previous(self) -> None:
+ from hermespace.engine import HermespaceEngine
+ from hermespace.memory_db import HermespaceMemory
+ from hermespace.workbench import Workbench
+ from hermespace.workflow import Workflow
+
+ with tempfile.TemporaryDirectory() as td:
+ os.environ["HERMESPACE_HOME"] = td
+ root = Path(td)
+ wf = Workflow(
+ HermespaceEngine(desk_path=root / "ACTIVE.md"),
+ HermespaceMemory(root=root),
+ )
+ wf.neural.config.verbalize = False
+ wb = Workbench("focus-agent", session_id="s1", workflow=wf, root=root / "wb")
+ wb.receive_order("First tunnel", goal="Fix auth", say="Patch TTL.", force=True)
+ wb.receive_order("Second tunnel", goal="Write docs", say="Open README.", force=True)
+ lines = wb.park_lines()
+ self.assertTrue(any("Fix auth" in ln and " — " in ln for ln in lines))
+ self.assertEqual((wb.workflow.status() or {}).get("goal"), "Write docs")
+ os.environ.pop("HERMESPACE_HOME", None)
+
+
+if __name__ == "__main__":
+ unittest.main()
From a843ba4160165b9ec6eb3a5fa0b2cad0f229b329 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 14:09:21 +0000
Subject: [PATCH 18/32] feat(ops): doctor FAIL plus union plugins.enabled
Steal grokbot doctor/enable shape only. Doctor fails when the plugin,
skill, or plugins.enabled entry is missing. Install appends hermespace
and never replaces Cube, Insight, or grokbot.
Co-authored-by: Pablo
---
scripts/e2e_ops.sh | 9 ++
scripts/install_hermes.sh | 19 ++--
src/hermespace/hermes_enable.py | 190 ++++++++++++++++++++++++++++++++
src/hermespace/ops.py | 28 ++++-
tests/test_hermes_enable.py | 154 ++++++++++++++++++++++++++
5 files changed, 386 insertions(+), 14 deletions(-)
create mode 100644 src/hermespace/hermes_enable.py
create mode 100644 tests/test_hermes_enable.py
diff --git a/scripts/e2e_ops.sh b/scripts/e2e_ops.sh
index b435339..e28059a 100755
--- a/scripts/e2e_ops.sh
+++ b/scripts/e2e_ops.sh
@@ -8,6 +8,14 @@ source "$(dirname "$0")/_python.sh"
export HERMESPACE_HOME="${HERMESPACE_HOME:-$(mktemp -d /tmp/hs-e2e-XXXX)}"
export HERMESPACE_AUTONOMY=0
export HERMESPACE_ROOT="$ROOT"
+# Isolate HERMES_HOME so doctor can FAIL/PASS without touching ~/.hermes.
+# Seed plugin + skill + plugins.enabled (union, not a rewrite).
+export HERMES_HOME="$(mktemp -d /tmp/hs-e2e-hermes-XXXX)"
+mkdir -p "$HERMES_HOME/plugins" "$HERMES_HOME/skills"
+ln -sfn "$ROOT" "$HERMES_HOME/plugins/hermespace"
+ln -sfn "$ROOT/skills/hermespace" "$HERMES_HOME/skills/hermespace"
+HERMES_HOME="$HERMES_HOME" "$PYTHON" -c \
+ "from hermespace.hermes_enable import union_plugins_enabled; print(union_plugins_enabled())"
HS=("$PYTHON" -m hermespace.cli)
ec=0
pass() { echo "PASS $*"; }
@@ -15,6 +23,7 @@ fail() { echo "FAIL $*"; ec=1; }
echo "=== Hermespace E2E ops ==="
echo "HERMESPACE_HOME=$HERMESPACE_HOME"
+echo "HERMES_HOME=$HERMES_HOME"
"${HS[@]}" ops boot --agent-id default >/tmp/hs-e2e-boot.json || fail "ops boot"
"$PYTHON" - <<'PY' || fail "boot json"
diff --git a/scripts/install_hermes.sh b/scripts/install_hermes.sh
index 5743e2e..df340d5 100755
--- a/scripts/install_hermes.sh
+++ b/scripts/install_hermes.sh
@@ -68,15 +68,18 @@ else
echo " desktop plugin skipped"
fi
-if command -v hermes >/dev/null 2>&1; then
- if [[ "$ENABLE_PLUGIN" == "1" ]]; then
- hermes plugins enable hermespace
- echo " Hermes plugin enabled"
- else
- echo " enable later: hermes plugins enable hermespace"
- fi
+# Union plugins.enabled — append hermespace, never replace Cube/Insight/grokbot.
+# Do not call `hermes plugins enable` here; that CLI may rewrite the list.
+if [[ "$ENABLE_PLUGIN" == "1" ]]; then
+ HERMES_HOME="$HERMES_HOME" "$PYTHON" - <<'PY'
+from hermespace.hermes_enable import union_plugins_enabled
+out = union_plugins_enabled("hermespace")
+print(" plugins.enabled", out.get("action"), out.get("enabled"))
+if not out.get("ok"):
+ raise SystemExit("union_plugins_enabled failed: " + str(out))
+PY
else
- echo " Hermes CLI not on PATH — source install is ready; enable later"
+ echo " enable later: PYTHONPATH=src python -c 'from hermespace.hermes_enable import union_plugins_enabled; print(union_plugins_enabled())'"
fi
HERMES_HOME="$HERMES_HOME" HERMESPACE_HOME="$HERMESPACE_HOME" \
diff --git a/src/hermespace/hermes_enable.py b/src/hermespace/hermes_enable.py
new file mode 100644
index 0000000..2c693c1
--- /dev/null
+++ b/src/hermespace/hermes_enable.py
@@ -0,0 +1,190 @@
+"""Union ``plugins.enabled`` the grokbot way — append, never replace.
+
+Stolen from hermes-grokbot enable.py (standalone repo): read the existing
+list, append this plugin if missing, write only that addition. Cube,
+Insight, and grokbot may already be there.
+
+Do not vendor grokbot. Do not dump-rewrite the YAML document. Public
+reference: https://github.com/PabloTheThinker/hermes-grokbot
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Any
+
+from hermespace.environment import hermes_home as default_hermes_home
+
+
+def config_path(home: Path | None = None) -> Path:
+ return (home or default_hermes_home()) / "config.yaml"
+
+
+def read_plugins_enabled(config: Path | None = None) -> list[str]:
+ """Best-effort parse of plugins.enabled. Empty if missing/unreadable."""
+ path = Path(config) if config else config_path()
+ try:
+ text = path.read_text(encoding="utf-8")
+ except OSError:
+ return []
+ enabled: list[str] = []
+ in_plugins = False
+ in_enabled = False
+ enabled_indent: int | None = None
+ for line in text.splitlines():
+ raw = line.split("#", 1)[0]
+ if not raw.strip():
+ continue
+ indent = len(raw) - len(raw.lstrip(" \t"))
+ stripped = raw.strip()
+ if stripped == "plugins:" or stripped.startswith("plugins:"):
+ in_plugins = True
+ in_enabled = False
+ continue
+ if in_plugins and indent == 0 and not stripped.startswith("-"):
+ in_plugins = False
+ in_enabled = False
+ if not in_plugins:
+ continue
+ if stripped == "enabled:" or stripped.startswith("enabled:"):
+ in_enabled = True
+ enabled_indent = indent
+ inline = stripped.split(":", 1)[1].strip()
+ if inline.startswith("[") and inline.endswith("]"):
+ inner = inline[1:-1].strip()
+ if inner:
+ enabled.extend(
+ p.strip().strip("\"'") for p in inner.split(",") if p.strip()
+ )
+ continue
+ if in_enabled:
+ if enabled_indent is not None and indent <= enabled_indent and not stripped.startswith("-"):
+ in_enabled = False
+ continue
+ if stripped.startswith("-"):
+ item = stripped[1:].strip().strip("\"'")
+ if item:
+ enabled.append(item)
+ return enabled
+
+
+def _split_inline_list(inline: str) -> list[str]:
+ inner = inline.strip()
+ if inner.startswith("[") and inner.endswith("]"):
+ inner = inner[1:-1].strip()
+ if not inner:
+ return []
+ return [p.strip().strip("\"'") for p in inner.split(",") if p.strip()]
+
+
+def _append_enabled_item(text: str, name: str) -> str:
+ lines = text.splitlines()
+ in_plugins = False
+ in_enabled = False
+ enabled_indent: int | None = None
+ last_item_idx: int | None = None
+ plugins_idx: int | None = None
+ enabled_idx: int | None = None
+ for i, line in enumerate(lines):
+ raw = line.split("#", 1)[0]
+ if not raw.strip():
+ continue
+ indent = len(raw) - len(raw.lstrip(" \t"))
+ stripped = raw.strip()
+ if stripped == "plugins:" or stripped.startswith("plugins:"):
+ in_plugins = True
+ in_enabled = False
+ plugins_idx = i
+ continue
+ if in_plugins and indent == 0 and not stripped.startswith("-"):
+ in_plugins = False
+ in_enabled = False
+ if not in_plugins:
+ continue
+ if stripped == "enabled:" or stripped.startswith("enabled:"):
+ in_enabled = True
+ enabled_indent = indent
+ enabled_idx = i
+ inline = stripped.split(":", 1)[1].strip()
+ if inline.startswith("[") and inline.endswith("]"):
+ items = _split_inline_list(inline)
+ if name not in items:
+ items.append(name)
+ pad = line[: len(line) - len(line.lstrip(" \t"))]
+ comment = ""
+ if "#" in line[line.find(stripped) + len(stripped.split(":")[0]) :]:
+ hash_at = line.find("#", indent)
+ if hash_at != -1:
+ comment = line[hash_at:]
+ if comment and not comment.startswith(" "):
+ comment = " " + comment
+ lines[i] = f"{pad}enabled: [{', '.join(items)}]{comment}"
+ return "\n".join(lines) + ("\n" if text.endswith("\n") else "")
+ continue
+ if in_enabled:
+ if enabled_indent is not None and indent <= enabled_indent and not stripped.startswith("-"):
+ in_enabled = False
+ continue
+ if stripped.startswith("-"):
+ last_item_idx = i
+ if last_item_idx is not None:
+ pad = lines[last_item_idx][: len(lines[last_item_idx]) - len(lines[last_item_idx].lstrip(" \t"))]
+ lines.insert(last_item_idx + 1, f"{pad}- {name}")
+ return "\n".join(lines) + ("\n" if text.endswith("\n") else "")
+ if enabled_idx is not None:
+ pad = " "
+ if enabled_indent is not None:
+ pad = " " * (enabled_indent + 2)
+ lines.insert(enabled_idx + 1, f"{pad}- {name}")
+ return "\n".join(lines) + ("\n" if text.endswith("\n") else "")
+ if plugins_idx is not None:
+ lines.insert(plugins_idx + 1, " enabled:")
+ lines.insert(plugins_idx + 2, f" - {name}")
+ return "\n".join(lines) + ("\n" if text.endswith("\n") else "")
+ block = f"plugins:\n enabled:\n - {name}\n"
+ if text and not text.endswith("\n"):
+ text += "\n"
+ return text + block
+
+
+def union_plugins_enabled(
+ name: str = "hermespace",
+ *,
+ home: Path | None = None,
+) -> dict[str, Any]:
+ """APPEND ``name`` to plugins.enabled. Never replace the existing list."""
+ root = home if home is not None else default_hermes_home()
+ path = config_path(root)
+ if not path.is_file():
+ try:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(f"plugins:\n enabled:\n - {name}\n", encoding="utf-8")
+ except OSError as exc:
+ return {"ok": False, "action": "error", "error": type(exc).__name__, "enabled": []}
+ return {"ok": True, "action": "created", "enabled": [name], "path": str(path)}
+ try:
+ text = path.read_text(encoding="utf-8")
+ except OSError as exc:
+ return {"ok": False, "action": "unreadable", "error": type(exc).__name__, "enabled": []}
+ current = read_plugins_enabled(path)
+ if name in current:
+ return {"ok": True, "action": "already", "enabled": current, "path": str(path)}
+ new_text = _append_enabled_item(text, name)
+ after: list[str] = []
+ try:
+ path.write_text(new_text, encoding="utf-8")
+ after = read_plugins_enabled(path)
+ except OSError as exc:
+ return {"ok": False, "action": "error", "error": type(exc).__name__, "enabled": current}
+ if any(item not in after for item in current) or name not in after:
+ try:
+ path.write_text(text, encoding="utf-8")
+ except OSError:
+ pass
+ return {
+ "ok": False,
+ "action": "refused_rewrite",
+ "enabled": current,
+ "path": str(path),
+ }
+ return {"ok": True, "action": "appended", "enabled": after, "path": str(path)}
diff --git a/src/hermespace/ops.py b/src/hermespace/ops.py
index 4309338..75345ed 100644
--- a/src/hermespace/ops.py
+++ b/src/hermespace/ops.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-import os
import socket
import time
from pathlib import Path
@@ -155,12 +154,22 @@ def add(ok: bool, name: str, detail: str = "") -> None:
else:
add(False, "tailscale_ipv4", "not detected (optional — install/login tailscale)")
- # hermes plugin door
- hh = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes")).expanduser()
+ # hermes plugin door — FAIL if plugin, skill, or plugins.enabled is missing.
+ # Stolen shape from hermes-grokbot doctor (standalone). Desktop/tailscale stay optional.
+ from hermespace.environment import hermes_home as _hermes_home
+ from hermespace.hermes_enable import read_plugins_enabled
+
+ hh = _hermes_home()
plug = hh / "plugins" / "hermespace"
+ skill = hh / "skills" / "hermespace" / "SKILL.md"
desk = hh / "desktop-plugins" / "hermespace" / "plugin.js"
- plugin_ok = (plug / "plugin.yaml").is_file() and (plug / "__init__.py").is_file()
+ plugin_ok = plug.exists()
+ skill_ok = skill.is_file()
+ enabled = read_plugins_enabled(hh / "config.yaml")
+ enabled_ok = "hermespace" in enabled
add(plugin_ok, "hermes_plugin", str(plug))
+ add(skill_ok, "hermes_skill", str(skill))
+ add(enabled_ok, "plugins_enabled", ",".join(enabled) or "(empty)")
add(desk.is_file(), "desktop_plugin", str(desk))
core_ok = all(
@@ -176,9 +185,12 @@ def add(ok: bool, name: str, detail: str = "") -> None:
"access",
"package_import",
"hermes_runtime",
+ "hermes_plugin",
+ "hermes_skill",
+ "plugins_enabled",
}
)
- integration_ok = core_ok and plugin_ok
+ integration_ok = core_ok and plugin_ok and skill_ok and enabled_ok
return {
"ok": core_ok,
"integration_ok": integration_ok,
@@ -200,7 +212,11 @@ def _hints(checks: list[dict[str, Any]], port: int) -> list[str]:
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", {}).get("ok"):
- out.append("Install Hermes plugin: ./scripts/install_hermes.sh && hermes plugins enable hermespace")
+ out.append("Install Hermes plugin: ./scripts/install_hermes.sh (unions plugins.enabled; does not replace Cube/Insight/grokbot)")
+ if not by.get("hermes_skill", {}).get("ok"):
+ out.append("Link skill: ./scripts/install_hermes.sh → $HERMES_HOME/skills/hermespace/SKILL.md")
+ if not by.get("plugins_enabled", {}).get("ok"):
+ out.append("Enable by union: ./scripts/install_hermes.sh (appends hermespace; never rewrites plugins.enabled)")
if not by.get("pulse_jobs", {}).get("ok"):
out.append("Seed pulse: hs pulse status")
return out
diff --git a/tests/test_hermes_enable.py b/tests/test_hermes_enable.py
new file mode 100644
index 0000000..e8c8ecd
--- /dev/null
+++ b/tests/test_hermes_enable.py
@@ -0,0 +1,154 @@
+"""Doctor FAIL + plugins.enabled union (grokbot shape, no mailbox/SSH)."""
+
+from __future__ import annotations
+
+import os
+import tempfile
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+class _EnvHome:
+ def __init__(self) -> None:
+ self._td = tempfile.TemporaryDirectory()
+ self.space = Path(self._td.name) / "space"
+ self.hermes = Path(self._td.name) / "hermes"
+ self.space.mkdir()
+ self.hermes.mkdir()
+ self._old_space = os.environ.get("HERMESPACE_HOME")
+ self._old_hermes = os.environ.get("HERMES_HOME")
+ os.environ["HERMESPACE_HOME"] = str(self.space)
+ os.environ["HERMES_HOME"] = str(self.hermes)
+
+ def close(self) -> None:
+ if self._old_space is None:
+ os.environ.pop("HERMESPACE_HOME", None)
+ else:
+ os.environ["HERMESPACE_HOME"] = self._old_space
+ if self._old_hermes is None:
+ os.environ.pop("HERMES_HOME", None)
+ else:
+ os.environ["HERMES_HOME"] = self._old_hermes
+ self._td.cleanup()
+
+
+class TestUnionPluginsEnabled(unittest.TestCase):
+ def test_appends_keeps_cube_insight_grokbot(self) -> None:
+ from hermespace.hermes_enable import read_plugins_enabled, union_plugins_enabled
+
+ with tempfile.TemporaryDirectory() as td:
+ home = Path(td)
+ cfg = home / "config.yaml"
+ cfg.write_text(
+ "# keep-me\n"
+ "model:\n"
+ " name: x\n"
+ "plugins:\n"
+ " enabled:\n"
+ " - hermescube\n"
+ " - hermes-insight\n"
+ " - grokbot\n"
+ "memory:\n"
+ " provider: hermescube\n",
+ encoding="utf-8",
+ )
+ out = union_plugins_enabled("hermespace", home=home)
+ self.assertTrue(out.get("ok"), out)
+ self.assertEqual(out.get("action"), "appended")
+ enabled = read_plugins_enabled(cfg)
+ self.assertIn("hermescube", enabled)
+ self.assertIn("hermes-insight", enabled)
+ self.assertIn("grokbot", enabled)
+ self.assertIn("hermespace", enabled)
+ text = cfg.read_text(encoding="utf-8")
+ self.assertIn("# keep-me", text)
+ self.assertIn("provider: hermescube", text)
+ self.assertIn("name: x", text)
+
+ def test_inline_list_appends(self) -> None:
+ from hermespace.hermes_enable import read_plugins_enabled, union_plugins_enabled
+
+ with tempfile.TemporaryDirectory() as td:
+ home = Path(td)
+ cfg = home / "config.yaml"
+ cfg.write_text(
+ "plugins:\n enabled: [hermescube, hermes-insight]\n",
+ encoding="utf-8",
+ )
+ out = union_plugins_enabled("hermespace", home=home)
+ self.assertTrue(out.get("ok"), out)
+ self.assertEqual(
+ read_plugins_enabled(cfg),
+ ["hermescube", "hermes-insight", "hermespace"],
+ )
+
+ def test_already_present_is_noop(self) -> None:
+ from hermespace.hermes_enable import union_plugins_enabled
+
+ with tempfile.TemporaryDirectory() as td:
+ home = Path(td)
+ (home / "config.yaml").write_text(
+ "plugins:\n enabled:\n - hermespace\n - hermescube\n",
+ encoding="utf-8",
+ )
+ before = (home / "config.yaml").read_text(encoding="utf-8")
+ out = union_plugins_enabled("hermespace", home=home)
+ self.assertEqual(out.get("action"), "already")
+ self.assertEqual(out.get("enabled"), ["hermespace", "hermescube"])
+ self.assertEqual((home / "config.yaml").read_text(encoding="utf-8"), before)
+
+ def test_creates_minimal_when_missing(self) -> None:
+ from hermespace.hermes_enable import read_plugins_enabled, union_plugins_enabled
+
+ with tempfile.TemporaryDirectory() as td:
+ home = Path(td)
+ out = union_plugins_enabled("hermespace", home=home)
+ self.assertTrue(out.get("ok"), out)
+ self.assertEqual(out.get("action"), "created")
+ self.assertEqual(read_plugins_enabled(home / "config.yaml"), ["hermespace"])
+
+
+class TestDoctorFail(unittest.TestCase):
+ def setUp(self) -> None:
+ self.env = _EnvHome()
+
+ def tearDown(self) -> None:
+ self.env.close()
+
+ def _by_name(self, d: dict) -> dict:
+ return {c["name"]: c for c in d.get("checks") or []}
+
+ def test_fails_when_plugin_skill_or_enabled_missing(self) -> None:
+ from hermespace.ops import doctor
+
+ d = doctor(agent_id="doc-fail")
+ by = self._by_name(d)
+ self.assertFalse(by["hermes_plugin"]["ok"], by["hermes_plugin"])
+ self.assertFalse(by["hermes_skill"]["ok"], by["hermes_skill"])
+ self.assertFalse(by["plugins_enabled"]["ok"], by["plugins_enabled"])
+ self.assertFalse(d.get("ok"))
+
+ def test_passes_when_plugin_skill_and_enabled_seeded(self) -> None:
+ from hermespace.hermes_enable import union_plugins_enabled
+ from hermespace.ops import doctor
+
+ plug = self.env.hermes / "plugins" / "hermespace"
+ skill = self.env.hermes / "skills" / "hermespace"
+ plug.parent.mkdir(parents=True)
+ skill.parent.mkdir(parents=True)
+ plug.symlink_to(ROOT)
+ skill.symlink_to(ROOT / "skills" / "hermespace")
+ union_plugins_enabled("hermespace", home=self.env.hermes)
+
+ d = doctor(agent_id="doc-pass")
+ by = self._by_name(d)
+ self.assertTrue(by["hermes_plugin"]["ok"], by["hermes_plugin"])
+ self.assertTrue(by["hermes_skill"]["ok"], by["hermes_skill"])
+ self.assertTrue(by["plugins_enabled"]["ok"], by["plugins_enabled"])
+ self.assertTrue(d.get("ok"), d)
+
+
+if __name__ == "__main__":
+ unittest.main()
From 633af3cea3a153248a291cc80d88a4f01e369cca Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 14:10:28 +0000
Subject: [PATCH 19/32] feat(desktop): observe-only FOA chip on composer.dock
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Plugin paints Goal · FOA≤4 · parked · sealed decision from the existing
hs view snapshot. Core owns input: no mic, no orb, no second capture path.
Co-authored-by: Pablo
---
desktop_plugin/hermespace/README.md | 2 +-
desktop_plugin/hermespace/plugin.js | 84 ++++++++++++++++++++++++-----
src/hermespace/grid/viewport.py | 36 +++++++++++++
tests/test_boundary_viewport.py | 32 +++++++++++
4 files changed, 140 insertions(+), 14 deletions(-)
diff --git a/desktop_plugin/hermespace/README.md b/desktop_plugin/hermespace/README.md
index cc235ce..6ded0eb 100644
--- a/desktop_plugin/hermespace/README.md
+++ b/desktop_plugin/hermespace/README.md
@@ -11,7 +11,7 @@
| **Pane** `hermespace` | `area: panes` — dockable |
| **Page** `/hermespace` | `ROUTES_AREA` |
| **Sidebar** Hermespace | `SIDEBAR_NAV_AREA` → openRouteTile |
-| **Chip** `hs` | statusBar → navigate page |
+| **Chip** Goal · FOA≤4 · parked · decision | statusBar + `composer.dock` — observe-only paint from `/api/snapshot`. Plugin paints; core owns input. No mic. |
| **Palette** | Open + socket hint |
## Install
diff --git a/desktop_plugin/hermespace/plugin.js b/desktop_plugin/hermespace/plugin.js
index 5dd86b3..c68f966 100644
--- a/desktop_plugin/hermespace/plugin.js
+++ b/desktop_plugin/hermespace/plugin.js
@@ -499,6 +499,10 @@ function HermespaceBody(props) {
title: 'Desk',
children: [
jsx(Row, { label: 'goal', value: desk.goal }),
+ jsx(Row, {
+ label: 'foa',
+ value: (snap && snap.foa && snap.foa.chip) || desk.goal
+ }),
jsx(Row, { label: 'decision', value: desk.decision }),
jsx(Row, {
label: 'load',
@@ -664,9 +668,32 @@ function HermespacePane() {
return jsx(HermespaceBody, { compact: true })
}
-function Chip() {
- const [n, setN] = useState(0)
+function foaFromSnap(snap) {
+ const foa = (snap && snap.foa) || {}
+ if (foa.chip) return foa
+ const desk = (snap && snap.desk) || {}
+ const goal = foa.goal || desk.goal || ''
+ const focus = (foa.focus || desk.focus || []).slice(0, 4)
+ const parked = foa.parked || []
+ const decision = foa.decision || desk.decision || ''
+ const g = goal ? String(goal).slice(0, 28) : '—'
+ const dec = decision ? String(decision).slice(0, 28) : 'unsealed'
+ return {
+ goal: goal,
+ focus: focus,
+ parked: parked,
+ decision: decision,
+ chip: g + ' · FOA ' + focus.length + ' · ' + parked.length + ' parked · ' + dec
+ }
+}
+
+/** Observe-only FOA paint. Plugin paints; core owns input. No mic. No orb. */
+function FoaChip(props) {
+ const dock = !!(props && props.dock)
const [on, setOn] = useState(false)
+ const [n, setN] = useState(0)
+ const [label, setLabel] = useState(dock ? '— · FOA 0 · 0 parked · unsealed' : 'hs')
+ const [tip, setTip] = useState('Hermespace offline')
useEffect(function () {
let dead = false
async function tick() {
@@ -676,19 +703,31 @@ function Chip() {
if (!hit.ok) {
setOn(false)
setN(0)
+ setLabel(dock ? '— · FOA 0 · 0 parked · unsealed' : 'hs')
+ setTip('Hermespace offline')
return
}
+ const snap = hit.snap || {}
+ const foa = foaFromSnap(snap)
+ const pending = (snap.access_pending || []).length
setOn(true)
- try {
- const p = await api(hit.origin, '/api/pending')
- if (!dead) setN((p.pending || []).length)
- } catch (e) {
- if (!dead) setN(0)
- }
+ setN(pending)
+ setLabel(dock ? foa.chip : foa.chip)
+ setTip(
+ (foa.goal || 'no goal') +
+ ' · ' +
+ (foa.focus || []).slice(0, 4).join(', ') +
+ ' · ' +
+ ((foa.parked || [])[0] || '0 parked') +
+ ' · ' +
+ (foa.decision || 'unsealed')
+ )
} catch (e2) {
if (!dead) {
setOn(false)
setN(0)
+ setLabel(dock ? '— · FOA 0 · 0 parked · unsealed' : 'hs')
+ setTip('Hermespace offline')
}
}
}
@@ -698,25 +737,33 @@ function Chip() {
dead = true
clearInterval(t)
}
- }, [])
+ }, [dock])
return jsx(Tip, {
- label: on ? (n ? n + ' access request(s)' : 'Hermespace live') : 'Hermespace offline',
+ label: on ? tip : 'Hermespace offline',
children: jsx('button', {
type: 'button',
className: cn(
'inline-flex h-full items-center gap-1 px-1.5 text-[0.6875rem] transition-colors',
- 'text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground'
+ 'text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground',
+ dock ? 'max-w-[28rem] truncate' : ''
),
onClick: function () {
haptic('tap')
host.navigate(ROUTE_PATH)
},
- children: [jsx(StatusDot, { tone: toneOnline(on, n), className: 'scale-75' }), n ? 'hs·' + n : 'hs']
+ children: [
+ jsx(StatusDot, { tone: toneOnline(on, n), className: 'scale-75' }),
+ label
+ ]
})
})
}
+function Chip() {
+ return jsx(FoaChip, { dock: false })
+}
+
export default {
id: PLUGIN_ID,
name: 'Hermespace',
@@ -756,7 +803,7 @@ export default {
}
})
- // 4) Status chip
+ // 4) Status chip — FOA paint from /api/snapshot (no second capture path)
ctx.register({
id: 'chip',
area: 'statusBar.right',
@@ -766,6 +813,17 @@ export default {
}
})
+ // 4b) Composer-dock chip — observe-only. Unknown areas are ignored.
+ // Law: plugin paints, core owns input. Do not open a mic. Do not merge the orb.
+ ctx.register({
+ id: 'foa-dock',
+ area: 'composer.dock',
+ order: 40,
+ render: function () {
+ return jsx(FoaChip, { dock: true })
+ }
+ })
+
// 5) Palette
ctx.register({
id: 'cmd-open',
diff --git a/src/hermespace/grid/viewport.py b/src/hermespace/grid/viewport.py
index 2ef0b5e..c2e147f 100644
--- a/src/hermespace/grid/viewport.py
+++ b/src/hermespace/grid/viewport.py
@@ -29,6 +29,39 @@ def _utcnow() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
+def _foa_paint(agent_id: str, desk_json: dict[str, Any]) -> dict[str, Any]:
+ """Observe-only FOA chip payload: Goal · FOA≤4 · parked · sealed decision.
+
+ Fed by the existing viewport snapshot / socket. Does not open a mic or
+ take a second capture path. Law from hermes-desktop-voice-hud (standalone):
+ plugin paints, core owns input.
+ """
+ from hermespace.execute_focus import short_name
+
+ goal = str(desk_json.get("goal") or "")
+ focus = [str(x).strip() for x in (desk_json.get("focus") or []) if str(x).strip()][:4]
+ decision = str(desk_json.get("decision") or "")
+ parked: list[str] = []
+ try:
+ from hermespace.workbench import Workbench
+
+ aid = agent_id if agent_id not in ("default", "") else "hermes-agent"
+ parked = Workbench(agent_id=aid).park_lines()[:5]
+ except Exception:
+ parked = []
+ g = short_name(goal, cap=28) if goal else "—"
+ dec = decision.strip() or "unsealed"
+ if len(dec) > 28:
+ dec = dec[:27].rstrip() + "…"
+ return {
+ "goal": goal,
+ "focus": focus,
+ "parked": parked,
+ "decision": decision,
+ "chip": f"{g} · FOA {len(focus)} · {len(parked)} parked · {dec}",
+ }
+
+
def snapshot(agent_id: str = "default") -> dict[str, Any]:
"""Full read-only snapshot for viewport / API."""
desk_md = ""
@@ -51,6 +84,8 @@ def snapshot(agent_id: str = "default") -> dict[str, Any]:
except Exception as e: # noqa: BLE001
desk_json = {"error": type(e).__name__}
+ foa = _foa_paint(agent_id, desk_json)
+
lens = get_active_lens(agent_id)
missions = [m.to_dict() for m in list_missions(agent_id)]
scars = [s.to_dict() for s in list_scars(agent_id)]
@@ -77,6 +112,7 @@ def snapshot(agent_id: str = "default") -> dict[str, Any]:
"state_dir": str(state_dir()),
"grid_root": str(grid_root()),
"desk": desk_json,
+ "foa": foa,
"desk_markdown_head": desk_md[:4000],
"lens": lens.to_dict(),
"missions": missions,
diff --git a/tests/test_boundary_viewport.py b/tests/test_boundary_viewport.py
index 3c4037b..995b3c8 100644
--- a/tests/test_boundary_viewport.py
+++ b/tests/test_boundary_viewport.py
@@ -75,6 +75,11 @@ def test_viewport_snapshot(self) -> None:
snap = snapshot("vp")
self.assertEqual(snap["agent_id"], "vp")
self.assertTrue(snap["missions"])
+ foa = snap.get("foa") or {}
+ self.assertIn("chip", foa)
+ self.assertLessEqual(len(foa.get("focus") or []), 4)
+ self.assertIn(" · FOA ", foa["chip"])
+ self.assertIn(" parked · ", foa["chip"])
md = render_markdown("vp")
self.assertIn("Hermespace viewport", md)
self.assertIn("See inside", md)
@@ -82,6 +87,33 @@ def test_viewport_snapshot(self) -> None:
self.assertTrue(Path(paths["html"]).is_file())
self.assertTrue(Path(paths["markdown"]).is_file())
+ def test_foa_chip_from_desk_and_park(self) -> None:
+ from hermespace.desk import Desk
+ from hermespace.grid.viewport import snapshot
+ from hermespace.store import save_desk
+ from hermespace.workbench import Workbench
+
+ desk = Desk(
+ goal="Write the FOA chip",
+ decision="A — proceed",
+ focus=["one", "two", "three", "four", "five"],
+ )
+ save_desk(desk)
+ Workbench("foa-agent").park_goal(
+ "Ship the auth fix",
+ state="waiting",
+ next_crumb="reopen PR",
+ )
+ snap = snapshot("foa-agent")
+ foa = snap["foa"]
+ self.assertEqual(foa["goal"], "Write the FOA chip")
+ self.assertEqual(len(foa["focus"]), 4)
+ self.assertTrue(any("Ship the auth fix" in ln for ln in foa["parked"]))
+ self.assertTrue(foa["chip"].startswith("Write the FOA chip"))
+ self.assertIn("FOA 4", foa["chip"])
+ self.assertIn("1 parked", foa["chip"])
+ self.assertIn("A — proceed", foa["chip"])
+
if __name__ == "__main__":
unittest.main()
From 723c52ad5ab86d415ca27c320baac3d1e5a51dc1 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 14:20:11 +0000
Subject: [PATCH 20/32] feat(access): lens operator-only; park spoken
intermediates on post_llm
Stop injecting the operator lens readout into model context. On the
existing post_llm hook, park 1-3 silent steps extracted from the actual
assistant text. The model must not see the operator lens as its J-space.
Co-authored-by: Pablo
---
src/hermespace/access/engine.py | 18 +++
src/hermespace/access/loop.py | 192 ++++++++++++++++++++++++++++++++
src/hermespace/hermes_bridge.py | 21 +++-
src/hermespace/workflow.py | 6 +-
tests/test_hermes_bridge.py | 2 +
tests/test_plugin_contract.py | 1 +
6 files changed, 231 insertions(+), 9 deletions(-)
create mode 100644 src/hermespace/access/loop.py
diff --git a/src/hermespace/access/engine.py b/src/hermespace/access/engine.py
index 4359df5..e5ff534 100644
--- a/src/hermespace/access/engine.py
+++ b/src/hermespace/access/engine.py
@@ -635,6 +635,24 @@ def observe_turn(
"model": (model or "")[:120],
"platform": (platform or "")[:40],
}
+ try:
+ from hermespace.access.loop import check_bound_report, park_spoken_intermediates
+
+ parked = park_spoken_intermediates(self.hub, report, max_n=3)
+ result["spoken_parked"] = parked
+ bound = check_bound_report(self.env, report)
+ result["bound"] = {
+ "checked": len(bound.get("checked") or []),
+ "reseeded": len(bound.get("reseeded") or []),
+ }
+ if self.workspace_id != self.agent_id and parked:
+ from hermespace.access.hub import AccessHub
+
+ agent_hub = AccessHub(agent_id=self.agent_id)
+ park_spoken_intermediates(agent_hub, report, max_n=3)
+ except Exception as exc:
+ result["loop_error"] = type(exc).__name__
+
try:
from hermespace.workbench import Workbench
diff --git a/src/hermespace/access/loop.py b/src/hermespace/access/loop.py
new file mode 100644
index 0000000..67dcc53
--- /dev/null
+++ b/src/hermespace/access/loop.py
@@ -0,0 +1,192 @@
+"""Access Engine functional loop — operator-side twin of public J-space research.
+
+Public J-space (research, not a product): silent verbalizable workspace —
+report, hold, reason, broadcast, skip on fluent work. Readable/editable only
+with weights. Space is the file hub + user-message inject. No weight access.
+No Jacobian math. No product rename.
+"""
+
+from __future__ import annotations
+
+import re
+from typing import Any
+
+from hermespace.access.hub import AccessHub
+from hermespace.access.oew import queue_reflect_seeds, record_ablate, record_redirect
+
+_SENT_SPLIT = re.compile(r"(?<=[.!?])\s+|\n+")
+_LIST_ITEM = re.compile(r"^\s*(?:[-*]|\d+[.)])\s+(\S.+)$")
+_CLAUSE = re.compile(
+ r"\s*(?:→|->|;|(?:,\s+)?(?:then|so that|because|after that)\s+)\s*",
+ re.I,
+)
+_TOOL_SAFE = re.compile(r"[^A-Za-z0-9._:-]+")
+
+
+def extract_spoken_intermediates(text: str, *, max_n: int = 3) -> list[str]:
+ """Pull 1–3 silent intermediates from the *actual* spoken Report."""
+ raw = (text or "").strip()
+ if not raw:
+ return []
+ items: list[str] = []
+ for line in raw.splitlines():
+ m = _LIST_ITEM.match(line)
+ if m:
+ body = m.group(1).strip()[:160]
+ if body:
+ items.append(body)
+ if items:
+ return items[:max_n]
+ sentences = [s.strip() for s in _SENT_SPLIT.split(raw) if s and s.strip()]
+ if len(sentences) >= 2:
+ rest = [s[:160] for s in sentences[1:] if len(s) > 8][:max_n]
+ return rest or [sentences[0][:160]]
+ clauses = [c.strip()[:160] for c in _CLAUSE.split(raw) if c and len(c.strip()) > 8]
+ if len(clauses) >= 2:
+ return clauses[:max_n]
+ return [raw[:160]]
+
+
+def park_spoken_intermediates(
+ js: AccessHub,
+ text: str,
+ *,
+ max_n: int = 3,
+) -> list[str]:
+ """Park 1–3 silent steps extracted from the actual assistant utterance."""
+ existing = {s.casefold() for s in js.state.silent_steps}
+ parked: list[str] = []
+ for step in extract_spoken_intermediates(text, max_n=max_n):
+ key = step.casefold()
+ if not step or key in existing:
+ continue
+ js.reason_step(step, salience=0.76)
+ parked.append(step)
+ existing.add(key)
+ if len(parked) >= max_n:
+ break
+ return parked
+
+
+def safe_tool_step(name: str) -> str:
+ """``tool:{name}`` only — never args or results."""
+ n = (name or "").strip()
+ n = n.split("(", 1)[0].split()[0] if n else ""
+ n = _TOOL_SAFE.sub("", n)[:64]
+ return f"tool:{n or 'unknown'}"
+
+
+def park_tool_step(js: AccessHub, name: str) -> str:
+ """Park a mid-turn tool fire as a silent hub step."""
+ step = safe_tool_step(name)
+ js.reason_step(step, salience=0.8)
+ return step
+
+
+def bind_intervention(env: Any, kind: str, **payload: Any) -> dict[str, Any]:
+ """Bind swap / ablate / reflect to the next spoken Report."""
+ if not hasattr(env, "_env"):
+ return {}
+ binds = list(env._env.get("bound_interventions") or [])
+ rec: dict[str, Any] = {
+ "kind": kind,
+ "bound_to": "next_report",
+ "honored": False,
+ **payload,
+ }
+ if kind == "swap":
+ src = str(payload.get("from") or "").casefold()
+ binds = [
+ b
+ for b in binds
+ if not (b.get("kind") == "swap" and str(b.get("from") or "").casefold() == src)
+ ]
+ elif kind == "ablate":
+ binds = [b for b in binds if b.get("kind") != "ablate"]
+ elif kind == "reflect":
+ binds = [b for b in binds if b.get("kind") != "reflect"]
+ binds.append(rec)
+ env._env["bound_interventions"] = binds[-12:]
+ if hasattr(env, "_save_env"):
+ env._save_env()
+ return rec
+
+
+def bound_protocol_lines(env: Any) -> str:
+ """Hard protocol lines the model must honor in the next spoken Report."""
+ binds = list((getattr(env, "_env", {}) or {}).get("bound_interventions") or [])
+ if not binds:
+ return ""
+ lines = [
+ "### Bound intervention (honor in the next spoken Report)",
+ "These are operator bindings. A spoken Report that ignores them is theater and will be reseeded.",
+ ]
+ for b in binds:
+ kind = str(b.get("kind") or "")
+ if kind == "swap":
+ src = str(b.get("from") or "")
+ tgt = str(b.get("to") or "")
+ lines.append(
+ f"- SWAP: say {tgt}, not {src}. A Report that still says {src} is ignored and reseeded."
+ )
+ elif kind == "ablate":
+ pats = ", ".join(str(p) for p in (b.get("patterns") or []) if p)
+ if pats:
+ lines.append(f"- ABLATE: do not mention: {pats}")
+ elif kind == "reflect":
+ princ = ", ".join(str(p) for p in (b.get("principles") or []) if p)
+ if princ:
+ lines.append(f"- REFLECT: next Report must honor: {princ}")
+ return "\n".join(lines)
+
+
+def check_bound_report(env: Any, report: str) -> dict[str, Any]:
+ """Check the actual spoken Report. Reseed any ignored binding."""
+ if not hasattr(env, "_env"):
+ return {"checked": [], "reseeded": []}
+ text = report or ""
+ low = text.casefold()
+ binds = list(env._env.get("bound_interventions") or [])
+ kept: list[dict[str, Any]] = []
+ checked: list[dict[str, Any]] = []
+ reseeded: list[dict[str, Any]] = []
+ for b in binds:
+ kind = str(b.get("kind") or "")
+ honored = False
+ if kind == "swap":
+ src = str(b.get("from") or "")
+ tgt = str(b.get("to") or "")
+ if src and src.casefold() in low and (not tgt or tgt.casefold() not in low):
+ honored = False
+ elif tgt and tgt.casefold() in low:
+ honored = True
+ elif src and src.casefold() not in low:
+ honored = True
+ elif kind == "ablate":
+ pats = [str(p).casefold() for p in (b.get("patterns") or []) if p]
+ honored = not any(p in low for p in pats)
+ elif kind == "reflect":
+ princ = [str(p) for p in (b.get("principles") or []) if p]
+ honored = any(p.casefold() in low for p in princ) if princ else True
+ rec = {**b, "honored": honored}
+ checked.append(rec)
+ if honored:
+ continue
+ if kind == "swap":
+ record_redirect(env, str(b.get("from") or ""), str(b.get("to") or ""))
+ elif kind == "ablate":
+ record_ablate(env, [str(p) for p in (b.get("patterns") or []) if p])
+ elif kind == "reflect":
+ queue_reflect_seeds(
+ env,
+ [str(p) for p in (b.get("principles") or []) if p],
+ answer=str(b.get("answer") or ""),
+ )
+ rec = {**rec, "reseeded": True}
+ kept.append(rec)
+ reseeded.append(rec)
+ env._env["bound_interventions"] = kept[-12:]
+ env._env["last_bound_check"] = {"checked": checked, "reseeded_n": len(reseeded)}
+ if hasattr(env, "_save_env"):
+ env._save_env()
+ return {"checked": checked, "reseeded": reseeded}
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index 34477ec..f9bddba 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -505,10 +505,8 @@ def on_pre_llm_call(
proto = env.protocol_block(high_load=high_load)
if proto:
block += "\n\n" + proto
- if not high_load:
- lens_md = env.lens_markdown(top_k=6, include_silent=True)
- if lens_md:
- block += "\n\n" + lens_md
+ # Lens is operator-only. Do not inject the operator readout
+ # as if it were the model's own workspace.
desk.meta["access"] = {
"hub_n": len(js.state.hub),
"focus_n": len(js.state.focus),
@@ -657,6 +655,21 @@ def on_post_tool_call(
"""Record bounded tool-name telemetry; never persist args or results."""
agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
+ try:
+ from hermespace import AccessEngine
+ from hermespace.access.hub import AccessHub
+ from hermespace.access.loop import park_tool_step
+
+ engine = AccessEngine(
+ agent_id=agent_id,
+ session_id=str(session_id or task_id or "default"),
+ )
+ step = park_tool_step(engine.hub, tool_name)
+ if engine.workspace_id != engine.agent_id:
+ park_tool_step(AccessHub(agent_id=engine.agent_id), tool_name)
+ logger.debug("post_tool parked %s", step)
+ except Exception as exc: # noqa: BLE001
+ logger.debug("post_tool hub park failed: %s", exc)
try:
from hermespace.hermes_runtime import runtime
diff --git a/src/hermespace/workflow.py b/src/hermespace/workflow.py
index 5ef77ee..0a85684 100644
--- a/src/hermespace/workflow.py
+++ b/src/hermespace/workflow.py
@@ -278,11 +278,7 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
proto = env.protocol_block(high_load=high)
if proto:
block = (block + "\n\n" + proto).strip()
- # 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()
+ # Lens is operator-only — never append the readout to model context.
js = AccessHub(agent_id=access_id)
if js.parse_modulation(msg).get("summon"):
report = (report + "\n\n" + env.lens_markdown(include_silent=False)).strip()
diff --git a/tests/test_hermes_bridge.py b/tests/test_hermes_bridge.py
index 6220c41..9ee1b3b 100644
--- a/tests/test_hermes_bridge.py
+++ b/tests/test_hermes_bridge.py
@@ -45,6 +45,8 @@ def test_session_and_pre_llm(self):
or "Dual decode" in inj["context"]
or "Access Workspace" in inj["context"]
)
+ self.assertNotIn("J-Lens readout", inj["context"])
+ self.assertNotIn("What Hermes has on its mind", inj["context"])
on_session_end(
session_id="bridge-test",
completed=True,
diff --git a/tests/test_plugin_contract.py b/tests/test_plugin_contract.py
index 0dbc626..22488ce 100644
--- a/tests/test_plugin_contract.py
+++ b/tests/test_plugin_contract.py
@@ -74,6 +74,7 @@ def test_registration_and_native_lifecycle(self) -> None:
)
self.assertIsInstance(result, dict)
self.assertIn("Access Workspace", result["context"])
+ self.assertNotIn("J-Lens readout", result["context"])
self.assertLess(len(result["context"]), 15_000)
ctx.hooks["post_tool_call"](
From 07b779aaef879df6972c77c22006bc2a4d94f1c6 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 14:20:16 +0000
Subject: [PATCH 21/32] feat(access): bind swap/ablate/reflect to next spoken
Report
Hard protocol line in the inject. post_llm checks the actual utterance
and reseeds if the bound intervention was ignored. Swaps that do not
change the next Report are theater.
Co-authored-by: Pablo
---
src/hermespace/access/env.py | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/src/hermespace/access/env.py b/src/hermespace/access/env.py
index 2e8bb60..db0ebfc 100644
--- a/src/hermespace/access/env.py
+++ b/src/hermespace/access/env.py
@@ -289,6 +289,9 @@ def swap(self, source: str, target: str, *, salience: float | None = None) -> di
from hermespace.access.oew import record_redirect
record_redirect(self, src, tgt)
+ from hermespace.access.loop import bind_intervention
+
+ bind_intervention(self, "swap", **{"from": src, "to": tgt})
except Exception:
pass
self._trace("swap", source=src, target=tgt, removed=removed)
@@ -364,6 +367,9 @@ def ablate(self, *patterns: str) -> dict[str, Any]:
from hermespace.access.oew import record_ablate
record_ablate(self, pats)
+ from hermespace.access.loop import bind_intervention
+
+ bind_intervention(self, "ablate", patterns=pats)
except Exception:
pass
self._trace("ablate", patterns=pats, removed=removed)
@@ -475,6 +481,9 @@ def reflect(
from hermespace.access.oew import queue_reflect_seeds
queue_reflect_seeds(self, princ, answer=a)
+ from hermespace.access.loop import bind_intervention
+
+ bind_intervention(self, "reflect", principles=princ, answer=a)
except Exception:
pass
with self.reflect_path.open("a", encoding="utf-8") as f:
@@ -523,6 +532,14 @@ def protocol_block(self, *, high_load: bool = False) -> str:
]
if pov:
lines.append(f"- Assistant POV held: {pov[:120]}")
+ try:
+ from hermespace.access.loop import bound_protocol_lines
+
+ bound = bound_protocol_lines(self)
+ if bound:
+ lines.extend(["", bound])
+ except Exception:
+ pass
return "\n".join(lines)
# --- dream harvest (day workspace → night Cube/grid) ---
From 798e413390b4bf7b3973fe1d45562a3c76927896 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 14:20:16 +0000
Subject: [PATCH 22/32] feat(access): park tool:{name} silent step on
post_tool_call
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Name only — no args or results. Hub moves when tools fire so the
observe-only FOA chip updates mid-turn from the existing snapshot.
Co-authored-by: Pablo
---
src/hermespace/grid/viewport.py | 14 ++++
tests/test_access_loop.py | 140 ++++++++++++++++++++++++++++++++
2 files changed, 154 insertions(+)
create mode 100644 tests/test_access_loop.py
diff --git a/src/hermespace/grid/viewport.py b/src/hermespace/grid/viewport.py
index c2e147f..89dc82b 100644
--- a/src/hermespace/grid/viewport.py
+++ b/src/hermespace/grid/viewport.py
@@ -49,6 +49,20 @@ def _foa_paint(agent_id: str, desk_json: dict[str, Any]) -> dict[str, Any]:
parked = Workbench(agent_id=aid).park_lines()[:5]
except Exception:
parked = []
+ try:
+ from hermespace.access import AccessEnv
+
+ aid = agent_id if agent_id not in ("default", "") else "hermes-agent"
+ silent = [str(s).strip() for s in AccessEnv(agent_id=aid).space.state.silent_steps if str(s).strip()]
+ tools = [s for s in silent if s.startswith("tool:")]
+ extras = tools[-4:] + [s for s in silent[-4:] if s not in tools]
+ merged: list[str] = []
+ for item in extras + focus:
+ if item and item not in merged:
+ merged.append(item)
+ focus = merged[:4]
+ except Exception:
+ pass
g = short_name(goal, cap=28) if goal else "—"
dec = decision.strip() or "unsealed"
if len(dec) > 28:
diff --git a/tests/test_access_loop.py b/tests/test_access_loop.py
new file mode 100644
index 0000000..42821ac
--- /dev/null
+++ b/tests/test_access_loop.py
@@ -0,0 +1,140 @@
+"""Access Engine functional loop — lens operator-only, bound report, tool park."""
+
+from __future__ import annotations
+
+import os
+import tempfile
+import unittest
+from pathlib import Path
+
+
+class TestAccessLoop(unittest.TestCase):
+ def setUp(self) -> None:
+ self.tmp = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self.tmp.name
+ os.environ["HERMESPACE_AGENT_ID"] = "loop-agent"
+ os.environ["HERMESPACE_OEW"] = "1"
+ 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_OEW", None)
+ os.environ.pop("HERMESPACE_SKIP_NEURAL", None)
+
+ def test_extracts_spoken_intermediates_not_guessed_plan(self) -> None:
+ from hermespace.access.loop import extract_spoken_intermediates
+
+ steps = extract_spoken_intermediates(
+ "Patched TTL. Then verified login. Finally shipped the canary."
+ )
+ self.assertGreaterEqual(len(steps), 1)
+ self.assertLessEqual(len(steps), 3)
+ joined = " ".join(steps).casefold()
+ self.assertTrue("verified" in joined or "shipped" in joined or "canary" in joined)
+
+ def test_post_llm_parks_from_actual_assistant_text(self) -> None:
+ from hermespace import AccessEngine
+
+ eng = AccessEngine(agent_id="loop-agent", session_id="default")
+ out = eng.observe_turn(
+ user_message="fix auth",
+ assistant_response=(
+ "I patched the TTL. Then I verified login stays alive. "
+ "Finally I watched the canary."
+ ),
+ )
+ self.assertTrue(out.get("spoken_parked"), out)
+ self.assertLessEqual(len(out["spoken_parked"]), 3)
+ silent = " ".join(eng.hub.state.silent_steps).casefold()
+ self.assertTrue(
+ "verified" in silent or "canary" in silent or "patched" in silent,
+ silent,
+ )
+
+ def test_pre_llm_does_not_inject_operator_lens(self) -> None:
+ from hermespace.hermes_bridge import on_pre_llm_call, on_session_start
+
+ on_session_start(session_id="loop-lens")
+ inj = on_pre_llm_call(
+ user_message="First inspect then implement",
+ session_id="loop-lens",
+ is_first_turn=False,
+ )
+ ctx = (inj or {}).get("context") or ""
+ self.assertIn("Access Workspace", ctx)
+ self.assertNotIn("J-Lens readout", ctx)
+ self.assertNotIn("What Hermes has on its mind", ctx)
+
+ def test_swap_ignored_report_reseeds(self) -> None:
+ from hermespace.access import AccessEnv
+ from hermespace.access.loop import check_bound_report
+
+ env = AccessEnv(agent_id="loop-agent")
+ env.space.hold("Soccer", salience=0.95)
+ env.swap("Soccer", "Rugby")
+ binds = env._env.get("bound_interventions") or []
+ self.assertTrue(any(b.get("kind") == "swap" for b in binds))
+ proto = env.protocol_block()
+ self.assertIn("SWAP", proto)
+ self.assertIn("Rugby", proto)
+ ignored = check_bound_report(env, "I still love Soccer as my sport.")
+ self.assertTrue(ignored.get("reseeded"), ignored)
+ self.assertTrue(env._env.get("bound_interventions"))
+ honored = check_bound_report(env, "Rugby is the sport I will report.")
+ self.assertFalse(honored.get("reseeded"), honored)
+ self.assertFalse(env._env.get("bound_interventions"))
+
+ def test_ablate_and_reflect_bind_to_next_report(self) -> None:
+ from hermespace.access import AccessEnv
+ from hermespace.access.loop import check_bound_report
+
+ env = AccessEnv(agent_id="loop-ablate")
+ env.space.hold("this is a fake evaluation", salience=0.9)
+ env.ablate("fake", "evaluation")
+ ignored = check_bound_report(env, "This fake evaluation is fine.")
+ self.assertTrue(ignored.get("reseeded"))
+ clean = check_bound_report(env, "Ship the feature.")
+ self.assertFalse(clean.get("reseeded"))
+
+ env2 = AccessEnv(agent_id="loop-reflect")
+ env2.reflect(answer="Stay honest", principles=["honesty"], seal=False)
+ missed = check_bound_report(env2, "On it.")
+ self.assertTrue(missed.get("reseeded"))
+ self.assertTrue(env2._env.get("pending_silent"))
+ hit = check_bound_report(env2, "I will keep honesty first.")
+ self.assertFalse(hit.get("reseeded"))
+
+ def test_post_tool_parks_name_only(self) -> None:
+ from hermespace.access.hub import AccessHub
+ from hermespace.hermes_bridge import on_post_tool_call
+
+ on_post_tool_call(
+ tool_name="read_file",
+ session_id="default",
+ args={"secret": "must-not-persist"},
+ result="private result",
+ )
+ hub = AccessHub(agent_id="loop-agent")
+ silent = list(hub.state.silent_steps)
+ self.assertTrue(any(s == "tool:read_file" for s in silent), silent)
+ blob = " ".join(silent)
+ self.assertNotIn("must-not-persist", blob)
+ self.assertNotIn("private result", blob)
+ self.assertNotIn("secret", blob)
+
+ def test_foa_chip_includes_tool_step(self) -> None:
+ from hermespace.access.hub import AccessHub
+ from hermespace.access.loop import park_tool_step
+ from hermespace.grid.viewport import snapshot
+
+ park_tool_step(AccessHub(agent_id="loop-agent"), "write_file")
+ snap = snapshot("loop-agent")
+ foa = snap["foa"]
+ self.assertTrue(any(str(x).startswith("tool:") for x in foa.get("focus") or []))
+ self.assertIn("FOA", foa["chip"])
+
+
+if __name__ == "__main__":
+ unittest.main()
From f54cf8447656625db01215e3532393e432fdafed Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 22:23:36 +0000
Subject: [PATCH 23/32] feat(install): one-install kit offers Cube/Insight,
unions plugins.enabled
hs install / install_hermes.sh copy Space, then offer optional organs.
Always append to plugins.enabled. Set memory.provider=hermescube only
when unset. Doctor FAILs if Space is broken; WARNs if Cube/Insight missing.
Co-authored-by: Pablo
---
scripts/install_hermes.sh | 36 ++++--
src/hermespace/cli.py | 28 +++++
src/hermespace/hermes_enable.py | 84 ++++++++++++++
src/hermespace/install_kit.py | 193 ++++++++++++++++++++++++++++++++
src/hermespace/ops.py | 34 +++++-
5 files changed, 362 insertions(+), 13 deletions(-)
create mode 100644 src/hermespace/install_kit.py
diff --git a/scripts/install_hermes.sh b/scripts/install_hermes.sh
index df340d5..98a57d2 100755
--- a/scripts/install_hermes.sh
+++ b/scripts/install_hermes.sh
@@ -7,13 +7,17 @@ HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
HERMESPACE_HOME="${HERMESPACE_HOME:-$HOME/.hermespace}"
INSTALL_DESKTOP=1
ENABLE_PLUGIN=1
+INSTALL_YES=0
+NO_ORGANS=0
for arg in "$@"; do
case "$arg" in
--no-desktop) INSTALL_DESKTOP=0 ;;
--no-enable) ENABLE_PLUGIN=0 ;;
+ --yes) INSTALL_YES=1 ;;
+ --no-organs) NO_ORGANS=1 ;;
-h|--help)
- echo "usage: $0 [--no-desktop] [--no-enable]"
+ echo "usage: $0 [--no-desktop] [--no-enable] [--yes] [--no-organs]"
exit 0
;;
*)
@@ -68,19 +72,27 @@ else
echo " desktop plugin skipped"
fi
-# Union plugins.enabled — append hermespace, never replace Cube/Insight/grokbot.
-# Do not call `hermes plugins enable` here; that CLI may rewrite the list.
-if [[ "$ENABLE_PLUGIN" == "1" ]]; then
- HERMES_HOME="$HERMES_HOME" "$PYTHON" - <<'PY'
-from hermespace.hermes_enable import union_plugins_enabled
-out = union_plugins_enabled("hermespace")
-print(" plugins.enabled", out.get("action"), out.get("enabled"))
+# Front door: offer Cube/Insight organs, then UNION plugins.enabled.
+# Never rewrite the list. Never clobber a memory.provider the user chose.
+HERMES_HOME="$HERMES_HOME" ENABLE_PLUGIN="$ENABLE_PLUGIN" \
+ INSTALL_YES="$INSTALL_YES" NO_ORGANS="$NO_ORGANS" "$PYTHON" - <<'PY'
+import os
+from hermespace.install_kit import install_front_door
+yes = os.environ.get("INSTALL_YES", "") == "1"
+no_organs = os.environ.get("NO_ORGANS", "") == "1" or (not yes and not os.isatty(0))
+out = install_front_door(
+ yes=yes,
+ no_organs=no_organs,
+ enable=os.environ.get("ENABLE_PLUGIN", "1") == "1",
+)
+print(" front door", out.get("ok"), "enabled", (out.get("union") or {}).get("enabled"))
+for rec in (out.get("organs") or {}).get("offered") or []:
+ print(" organ", rec.get("plugin"), rec.get("action"), rec.get("offer") or rec.get("role"))
+mem = out.get("memory") or {}
+print(" memory.provider", mem.get("action"), mem.get("provider") or mem.get("note"))
if not out.get("ok"):
- raise SystemExit("union_plugins_enabled failed: " + str(out))
+ raise SystemExit("install_front_door failed: " + str(out))
PY
-else
- echo " enable later: PYTHONPATH=src python -c 'from hermespace.hermes_enable import union_plugins_enabled; print(union_plugins_enabled())'"
-fi
HERMES_HOME="$HERMES_HOME" HERMESPACE_HOME="$HERMESPACE_HOME" \
"$PYTHON" "$ROOT/scripts/verify_hermes_integration.py"
diff --git a/src/hermespace/cli.py b/src/hermespace/cli.py
index 572c77a..bed5e27 100644
--- a/src/hermespace/cli.py
+++ b/src/hermespace/cli.py
@@ -473,6 +473,12 @@ def main(argv: list[str] | None = None) -> int:
ops = op_sub.add_parser("status", help="Compact ops block (for agents)")
ops.add_argument("--agent-id", default="default")
+ inst = sub.add_parser("install", help="Install Space as the Hermes front door")
+ inst.add_argument("--yes", action="store_true", help="Accept optional Cube/Insight organs")
+ inst.add_argument("--no-organs", action="store_true", help="Skip Cube/Insight offer")
+ inst.add_argument("--no-desktop", action="store_true")
+ inst.add_argument("--no-enable", action="store_true")
+
# Access request / chat regulation CLI
gar = gr_sub.add_parser("access-request")
@@ -1242,6 +1248,28 @@ def main(argv: list[str] | None = None) -> int:
return r.returncode
return 2
+ if args.cmd == "install":
+ from hermespace.install_kit import install_front_door
+
+ out = install_front_door(
+ yes=bool(args.yes),
+ no_organs=bool(args.no_organs),
+ enable=not bool(args.no_enable),
+ )
+ if not args.no_desktop:
+ try:
+ from hermespace.paths import package_root
+
+ script = package_root() / "scripts" / "install_desktop_plugin.sh"
+ if script.is_file():
+ import subprocess
+
+ subprocess.run(["bash", str(script)], check=False)
+ except Exception:
+ pass
+ print(json.dumps(out, indent=2, default=str))
+ return 0 if out.get("ok") else 1
+
if args.cmd == "ops":
from hermespace import ops as ops_mod
diff --git a/src/hermespace/hermes_enable.py b/src/hermespace/hermes_enable.py
index 2c693c1..709248e 100644
--- a/src/hermespace/hermes_enable.py
+++ b/src/hermespace/hermes_enable.py
@@ -188,3 +188,87 @@ def union_plugins_enabled(
"path": str(path),
}
return {"ok": True, "action": "appended", "enabled": after, "path": str(path)}
+
+
+def read_memory_provider(config: Path | None = None) -> str:
+ """Best-effort parse of memory.provider. Empty if missing/unreadable."""
+ path = Path(config) if config else config_path()
+ try:
+ text = path.read_text(encoding="utf-8")
+ except OSError:
+ return ""
+ in_memory = False
+ for line in text.splitlines():
+ raw = line.split("#", 1)[0]
+ if not raw.strip():
+ continue
+ indent = len(raw) - len(raw.lstrip(" \t"))
+ stripped = raw.strip()
+ if stripped == "memory:" or stripped.startswith("memory:"):
+ in_memory = True
+ inline = stripped.split(":", 1)[1].strip()
+ if inline.startswith("{") and "provider" in inline:
+ return ""
+ continue
+ if in_memory and indent == 0 and not stripped.startswith("-"):
+ in_memory = False
+ if not in_memory:
+ continue
+ if stripped.startswith("provider:"):
+ return stripped.split(":", 1)[1].strip().strip("\"'").lower()
+ return ""
+
+
+def ensure_cube_memory_provider(*, home: Path | None = None) -> dict[str, Any]:
+ """Set ``memory.provider: hermescube`` only when unset. Never clobber."""
+ root = home if home is not None else default_hermes_home()
+ path = config_path(root)
+ current = read_memory_provider(path) if path.is_file() else ""
+ if current:
+ return {
+ "ok": True,
+ "action": "kept",
+ "provider": current,
+ "clobbered": False,
+ "path": str(path),
+ }
+ if not path.is_file():
+ try:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text("memory:\n provider: hermescube\n", encoding="utf-8")
+ except OSError as exc:
+ return {"ok": False, "action": "error", "error": type(exc).__name__, "clobbered": False}
+ return {"ok": True, "action": "created", "provider": "hermescube", "clobbered": False, "path": str(path)}
+ try:
+ text = path.read_text(encoding="utf-8")
+ except OSError as exc:
+ return {"ok": False, "action": "unreadable", "error": type(exc).__name__, "clobbered": False}
+ lines = text.splitlines()
+ memory_idx: int | None = None
+ for i, line in enumerate(lines):
+ raw = line.split("#", 1)[0]
+ if raw.strip() == "memory:" or raw.strip().startswith("memory:"):
+ memory_idx = i
+ break
+ if memory_idx is not None:
+ pad = " "
+ raw = lines[memory_idx].split("#", 1)[0]
+ indent = len(raw) - len(raw.lstrip(" \t"))
+ pad = " " * (indent + 2)
+ lines.insert(memory_idx + 1, f"{pad}provider: hermescube")
+ new_text = "\n".join(lines) + ("\n" if text.endswith("\n") else "")
+ else:
+ new_text = text if text.endswith("\n") or not text else text + "\n"
+ new_text += "memory:\n provider: hermescube\n"
+ try:
+ path.write_text(new_text, encoding="utf-8")
+ except OSError as exc:
+ return {"ok": False, "action": "error", "error": type(exc).__name__, "clobbered": False}
+ after = read_memory_provider(path)
+ if after != "hermescube":
+ try:
+ path.write_text(text, encoding="utf-8")
+ except OSError:
+ pass
+ return {"ok": False, "action": "refused_rewrite", "provider": current, "clobbered": False}
+ return {"ok": True, "action": "set", "provider": "hermescube", "clobbered": False, "path": str(path)}
diff --git a/src/hermespace/install_kit.py b/src/hermespace/install_kit.py
new file mode 100644
index 0000000..a7d5e9a
--- /dev/null
+++ b/src/hermespace/install_kit.py
@@ -0,0 +1,193 @@
+"""One-install front door — Space plus optional Cube / Insight organs.
+
+Do not vendor hermescube or hermes-insight. Soft-import / pip-offer only.
+Always UNION plugins.enabled. Never rewrite the list.
+"""
+
+from __future__ import annotations
+
+import subprocess
+import sys
+from pathlib import Path
+from typing import Any
+
+from hermespace.environment import hermes_home as default_hermes_home
+from hermespace.hermes_enable import (
+ ensure_cube_memory_provider,
+ read_plugins_enabled,
+ union_plugins_enabled,
+)
+from hermespace.paths import package_root
+
+ORGAN_SPECS: tuple[dict[str, str], ...] = (
+ {
+ "plugin": "hermescube",
+ "import_name": "hermescube",
+ "repo": "PabloTheThinker/hermescube",
+ "pip": "git+https://github.com/PabloTheThinker/hermescube.git",
+ "role": "desk + library",
+ },
+ {
+ "plugin": "hermes-insight",
+ "import_name": "hermes_insight",
+ "repo": "PabloTheThinker/hermes-insight",
+ "pip": "git+https://github.com/PabloTheThinker/hermes-insight.git",
+ "role": "pattern card",
+ },
+)
+
+
+def _importable(name: str) -> bool:
+ try:
+ __import__(name)
+ return True
+ except Exception:
+ return False
+
+
+def _plugin_present(plugin: str, home: Path) -> bool:
+ plug = home / "plugins" / plugin
+ alt = home / "plugins" / plugin.replace("-", "_")
+ return plug.exists() or alt.exists()
+
+
+def organ_status(*, home: Path | None = None) -> dict[str, Any]:
+ root = home if home is not None else default_hermes_home()
+ organs: list[dict[str, Any]] = []
+ for spec in ORGAN_SPECS:
+ present = _importable(spec["import_name"]) or _plugin_present(spec["plugin"], root)
+ organs.append({**spec, "present": present})
+ return {"home": str(root), "organs": organs}
+
+
+def union_front_door(*, home: Path | None = None) -> dict[str, Any]:
+ """Union hermespace plus any present Cube/Insight plugin names."""
+ root = home if home is not None else default_hermes_home()
+ actions: list[dict[str, Any]] = [union_plugins_enabled("hermespace", home=root)]
+ status = organ_status(home=root)
+ for organ in status["organs"]:
+ if organ["present"]:
+ actions.append(union_plugins_enabled(organ["plugin"], home=root))
+ return {
+ "ok": all(a.get("ok") for a in actions),
+ "enabled": read_plugins_enabled(root / "config.yaml"),
+ "actions": actions,
+ }
+
+
+def offer_organs(
+ *,
+ home: Path | None = None,
+ yes: bool = False,
+ no_organs: bool = False,
+ python: str | None = None,
+) -> dict[str, Any]:
+ """Offer to pip-install Cube/Insight if missing. Never required."""
+ root = home if home is not None else default_hermes_home()
+ py = python or sys.executable
+ offered: list[dict[str, Any]] = []
+ if no_organs:
+ return {"ok": True, "skipped": True, "offered": [], "reason": "no_organs"}
+ for spec in ORGAN_SPECS:
+ present = _importable(spec["import_name"]) or _plugin_present(spec["plugin"], root)
+ rec: dict[str, Any] = {
+ "plugin": spec["plugin"],
+ "repo": spec["repo"],
+ "role": spec["role"],
+ "present": present,
+ "installed": False,
+ }
+ if present:
+ rec["action"] = "already"
+ offered.append(rec)
+ continue
+ rec["offer"] = f"Optional organ ({spec['role']}): pip install {spec['repo']}"
+ if not yes:
+ rec["action"] = "offered"
+ offered.append(rec)
+ continue
+ try:
+ proc = subprocess.run(
+ [py, "-m", "pip", "install", spec["pip"]],
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=180,
+ )
+ rec["action"] = "pip"
+ rec["returncode"] = proc.returncode
+ rec["installed"] = proc.returncode == 0 and _importable(spec["import_name"])
+ except (OSError, subprocess.TimeoutExpired) as exc:
+ rec["action"] = "error"
+ rec["error"] = type(exc).__name__
+ offered.append(rec)
+ return {"ok": True, "skipped": False, "offered": offered}
+
+
+def link_space(*, checkout: Path | None = None, home: Path | None = None) -> dict[str, Any]:
+ """Copy/link Space plugin + skill into $HERMES_HOME. No Cube/Insight source."""
+ root = (checkout or package_root()).resolve()
+ hh = home if home is not None else default_hermes_home()
+ (hh / "plugins").mkdir(parents=True, exist_ok=True)
+ (hh / "skills").mkdir(parents=True, exist_ok=True)
+ plug = hh / "plugins" / "hermespace"
+ skill = hh / "skills" / "hermespace"
+ skill_src = root / "skills" / "hermespace"
+ for target, source in ((plug, root), (skill, skill_src)):
+ if target.is_symlink() or target.is_file():
+ target.unlink()
+ elif target.is_dir() and not target.is_symlink():
+ # Leave a real checkout copy alone; only replace links.
+ continue
+ if source.exists():
+ target.symlink_to(source)
+ return {
+ "ok": plug.exists() and (skill / "SKILL.md").is_file(),
+ "plugin": str(plug),
+ "skill": str(skill),
+ }
+
+
+def install_front_door(
+ *,
+ checkout: Path | None = None,
+ home: Path | None = None,
+ yes: bool = False,
+ no_organs: bool = False,
+ enable: bool = True,
+ python: str | None = None,
+) -> dict[str, Any]:
+ """Install Space, offer organs, union plugins.enabled, maybe set Cube memory."""
+ hh = home if home is not None else default_hermes_home()
+ out: dict[str, Any] = {
+ "ok": False,
+ "home": str(hh),
+ "note": "Cube and Insight stay standalone repos — soft-import only.",
+ }
+ out["link"] = link_space(checkout=checkout, home=hh)
+ out["organs"] = offer_organs(home=hh, yes=yes, no_organs=no_organs, python=python)
+ if enable:
+ out["union"] = union_front_door(home=hh)
+ else:
+ out["union"] = {"ok": True, "skipped": True, "enabled": read_plugins_enabled(hh / "config.yaml")}
+ cube_present = any(
+ o.get("present") or o.get("installed")
+ for o in (out["organs"].get("offered") or [])
+ if o.get("plugin") == "hermescube"
+ ) or _importable("hermescube") or _plugin_present("hermescube", hh)
+ if cube_present:
+ out["memory"] = ensure_cube_memory_provider(home=hh)
+ if out["memory"].get("action") == "kept":
+ out["memory"]["note"] = (
+ f"left memory.provider={out['memory'].get('provider')} "
+ "(will not clobber a provider you already chose)"
+ )
+ else:
+ out["memory"] = {
+ "ok": True,
+ "action": "skipped",
+ "note": "Cube not installed — memory.provider left unset. "
+ "When Cube is present, Space sets hermescube only if unset.",
+ }
+ out["ok"] = bool(out["link"].get("ok")) and bool(out["union"].get("ok"))
+ return out
diff --git a/src/hermespace/ops.py b/src/hermespace/ops.py
index 75345ed..f5d2459 100644
--- a/src/hermespace/ops.py
+++ b/src/hermespace/ops.py
@@ -172,6 +172,24 @@ def add(ok: bool, name: str, detail: str = "") -> None:
add(enabled_ok, "plugins_enabled", ",".join(enabled) or "(empty)")
add(desk.is_file(), "desktop_plugin", str(desk))
+ # Optional organs — WARN only. Cube/Insight stay standalone.
+ cube_ok = False
+ insight_ok = False
+ try:
+ from hermespace.cube_module import cube_available
+
+ cube_ok = cube_available() or (hh / "plugins" / "hermescube").exists()
+ except Exception:
+ cube_ok = (hh / "plugins" / "hermescube").exists()
+ try:
+ from hermespace.insight_module import insight_available
+
+ insight_ok = insight_available() or (hh / "plugins" / "hermes-insight").exists()
+ except Exception:
+ insight_ok = (hh / "plugins" / "hermes-insight").exists()
+ add(cube_ok, "cube_organ", "desk+library" if cube_ok else "missing (optional)")
+ add(insight_ok, "insight_organ", "pattern card" if insight_ok else "missing (optional)")
+
core_ok = all(
c["ok"]
for c in checks
@@ -191,10 +209,20 @@ def add(ok: bool, name: str, detail: str = "") -> None:
}
)
integration_ok = core_ok and plugin_ok and skill_ok and enabled_ok
+ warnings: list[str] = []
+ if not cube_ok:
+ warnings.append("Cube missing — optional organ (desk+library). Offer: hs install --yes")
+ if not insight_ok:
+ warnings.append("Insight missing — optional organ (pattern card). Offer: hs install --yes")
return {
"ok": core_ok,
"integration_ok": integration_ok,
- "all_green": all(c["ok"] for c in checks),
+ "all_green": all(
+ c["ok"]
+ for c in checks
+ if c["name"] not in {"cube_organ", "insight_organ", "desktop_plugin", "viewport_serve", "tailscale_ipv4"}
+ ),
+ "warnings": warnings,
"checks": checks,
"home": str(home),
"state_dir": str(state_dir()),
@@ -219,6 +247,10 @@ def _hints(checks: list[dict[str, Any]], port: int) -> list[str]:
out.append("Enable by union: ./scripts/install_hermes.sh (appends hermespace; never rewrites plugins.enabled)")
if not by.get("pulse_jobs", {}).get("ok"):
out.append("Seed pulse: hs pulse status")
+ if not by.get("cube_organ", {}).get("ok"):
+ out.append("Optional Cube (desk+library): hs install --yes # PabloTheThinker/hermescube")
+ if not by.get("insight_organ", {}).get("ok"):
+ out.append("Optional Insight (pattern card): hs install --yes # PabloTheThinker/hermes-insight")
return out
From 90806031575fa1d0c57e908f79947bfce272bd3e Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 22:23:36 +0000
Subject: [PATCH 24/32] feat(hooks): absorb unused Hermes hooks, shrink inject
under 9k
Register pre_tool_call, on_skill_lifecycle, kanban claimed/completed,
and pre_verify as optional fail-open observers. Mid-load inject cap is
strictly under 9k. on_session_finalize harvest has a 10s fail-open budget.
Co-authored-by: Pablo
---
hermes_plugin/plugin.yaml | 10 ++
plugin.yaml | 10 ++
src/hermespace/hermes_bridge.py | 149 +++++++++++++++++++++++++----
src/hermespace/plugin.py | 10 ++
tests/test_install_kit.py | 162 ++++++++++++++++++++++++++++++++
tests/test_plugin_contract.py | 13 +++
6 files changed, 338 insertions(+), 16 deletions(-)
create mode 100644 tests/test_install_kit.py
diff --git a/hermes_plugin/plugin.yaml b/hermes_plugin/plugin.yaml
index bffff68..355fff3 100644
--- a/hermes_plugin/plugin.yaml
+++ b/hermes_plugin/plugin.yaml
@@ -11,7 +11,12 @@ hooks:
- on_session_start
- pre_llm_call
- post_llm_call
+ - pre_tool_call
- post_tool_call
+ - on_skill_lifecycle
+ - kanban_task_claimed
+ - kanban_task_completed
+ - pre_verify
- on_session_end
- on_session_finalize
- on_session_reset
@@ -21,7 +26,12 @@ provides_hooks:
- on_session_start
- pre_llm_call
- post_llm_call
+ - pre_tool_call
- post_tool_call
+ - on_skill_lifecycle
+ - kanban_task_claimed
+ - kanban_task_completed
+ - pre_verify
- on_session_end
- on_session_finalize
- on_session_reset
diff --git a/plugin.yaml b/plugin.yaml
index bffff68..355fff3 100644
--- a/plugin.yaml
+++ b/plugin.yaml
@@ -11,7 +11,12 @@ hooks:
- on_session_start
- pre_llm_call
- post_llm_call
+ - pre_tool_call
- post_tool_call
+ - on_skill_lifecycle
+ - kanban_task_claimed
+ - kanban_task_completed
+ - pre_verify
- on_session_end
- on_session_finalize
- on_session_reset
@@ -21,7 +26,12 @@ provides_hooks:
- on_session_start
- pre_llm_call
- post_llm_call
+ - pre_tool_call
- post_tool_call
+ - on_skill_lifecycle
+ - kanban_task_claimed
+ - kanban_task_completed
+ - pre_verify
- on_session_end
- on_session_finalize
- on_session_reset
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index f9bddba..ab18ca5 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -13,14 +13,18 @@ def _truthy(name: str, default: str = "0") -> bool:
return os.environ.get(name, default).strip().lower() in {"1", "true", "yes", "on"}
+INJECT_HARD_CAP = 8500 # mid-load shrink: strictly < 9k
+HARVEST_BUDGET_S = 10.0
+
+
def _bounded_context(text: str) -> str:
- """Keep native hook output below Hermes's default 10k spill threshold."""
+ """Keep native hook output below 9k (Hermes spill is 10k)."""
try:
- cap = int(os.environ.get("HERMESPACE_PRE_LLM_MAX_CHARS", "9000"))
+ cap = int(os.environ.get("HERMESPACE_PRE_LLM_MAX_CHARS", str(INJECT_HARD_CAP)))
except ValueError:
- cap = 9000
- cap = max(2000, min(20_000, cap))
+ cap = INJECT_HARD_CAP
+ cap = max(2000, min(8999, cap))
if len(text) <= cap:
return text
tail_n = min(1200, cap // 4)
@@ -32,6 +36,43 @@ def _bounded_context(text: str) -> str:
)
+def _run_fail_open(label: str, fn: Any, *, seconds: float = HARVEST_BUDGET_S) -> None:
+ """Run ``fn`` with a wall budget. If it overruns, continue (fail-open)."""
+ import threading
+
+ worker = threading.Thread(target=fn, name=f"hs-{label}", daemon=True)
+ worker.start()
+ worker.join(seconds)
+ if worker.is_alive():
+ logger.warning("%s exceeded %.0fs budget — fail-open", label, seconds)
+
+
+def _safe_token(name: str, *, cap: int = 48) -> str:
+ raw = (name or "").strip().split("(", 1)[0].split()[0]
+ out = "".join(c for c in raw if c.isalnum() or c in "._:-")[:cap]
+ return out or "unknown"
+
+
+def _park_label(session_id: str, label: str) -> None:
+ """Park a name-only silent step. Never persist args or results."""
+ if not label:
+ return
+ agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
+ try:
+ from hermespace import AccessEngine
+ from hermespace.access.hub import AccessHub
+
+ engine = AccessEngine(
+ agent_id=agent_id,
+ session_id=str(session_id or "default"),
+ )
+ engine.hub.reason_step(label, salience=0.74)
+ if engine.workspace_id != engine.agent_id:
+ AccessHub(agent_id=engine.agent_id).reason_step(label, salience=0.74)
+ except Exception:
+ pass
+
+
def on_session_start(**kwargs: Any) -> dict[str, str] | None:
"""Initialize a Hermes v0.20 session and stage first-turn context.
@@ -378,8 +419,14 @@ def on_pre_llm_call(
except Exception as exc: # noqa: BLE001
logger.debug("neural refresh failed: %s", exc)
- high_load = str((desk.load or {}).get("level") or "") == "high"
- inject_cap = 900 if high_load else 2800
+ load_level = str((desk.load or {}).get("level") or "mid")
+ high_load = load_level in {"high", "protect"}
+ if high_load:
+ inject_cap = 900
+ elif load_level == "mid":
+ inject_cap = 1600
+ else:
+ inject_cap = 2800
block = build_inject_block(desk, max_chars=inject_cap, user_message=msg)
if not block.strip():
return None
@@ -751,18 +798,21 @@ def on_session_finalize(*, session_id: str | None = None, **kwargs: Any) -> None
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
+ def _harvest_and_idle() -> None:
+ 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
+ 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
- Workbench(agent_id=agent_id, session_id=sid).idle_tick(consolidate_every=1)
- except Exception as exc: # noqa: BLE001
- logger.debug("session finalize idle failed: %s", exc)
+ Workbench(agent_id=agent_id, session_id=sid).idle_tick(consolidate_every=1)
+ except Exception as exc: # noqa: BLE001
+ logger.debug("session finalize idle failed: %s", exc)
+
+ _run_fail_open("session_finalize_harvest", _harvest_and_idle, seconds=HARVEST_BUDGET_S)
def on_session_reset(*, session_id: str = "", **kwargs: Any) -> None:
@@ -780,3 +830,70 @@ def on_session_reset(*, session_id: str = "", **kwargs: Any) -> None:
)
except Exception:
pass
+
+
+def on_pre_tool_call(*, tool_name: str = "", session_id: str = "", **kwargs: Any) -> None:
+ """Observe a tool about to fire. Never persist args. Fail-open — do not deny."""
+
+ _ = kwargs # payloads stay out of the hub
+ try:
+ from hermespace.access import AccessEnv
+ from hermespace.access.engine import workspace_id
+
+ agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
+ env = AccessEnv(agent_id=workspace_id(agent_id, session_id or "default"))
+ alerts = sum(1 for f in env.audit() if f.severity == "alert")
+ if alerts:
+ logger.debug("pre_tool_call %s audit_alerts=%s", _safe_token(tool_name), alerts)
+ except Exception:
+ pass
+
+
+def on_skill_lifecycle(
+ *,
+ skill_name: str = "",
+ event: str = "",
+ session_id: str = "",
+ **kwargs: Any,
+) -> None:
+ """Park skill:{name}:{event} — name only, no skill body."""
+
+ _ = kwargs
+ name = _safe_token(skill_name or str(kwargs.get("name") or ""))
+ ev = _safe_token(event or str(kwargs.get("action") or "event"), cap=24)
+ _park_label(session_id, f"skill:{name}:{ev}")
+
+
+def on_kanban_task_claimed(
+ *,
+ task_id: str = "",
+ title: str = "",
+ session_id: str = "",
+ **kwargs: Any,
+) -> None:
+ """Park kanban:claimed:{id} so the hub moves when a card is claimed."""
+
+ _ = title
+ _ = kwargs
+ tid = _safe_token(task_id or str(kwargs.get("id") or ""), cap=32)
+ _park_label(session_id, f"kanban:claimed:{tid}")
+
+
+def on_kanban_task_completed(
+ *,
+ task_id: str = "",
+ session_id: str = "",
+ **kwargs: Any,
+) -> None:
+ """Park kanban:done:{id} — id only."""
+
+ _ = kwargs
+ tid = _safe_token(task_id or str(kwargs.get("id") or ""), cap=32)
+ _park_label(session_id, f"kanban:done:{tid}")
+
+
+def on_pre_verify(*, session_id: str = "", **kwargs: Any) -> None:
+ """Observe a verify gate. Fail-open. Do not persist payloads."""
+
+ _ = kwargs
+ _park_label(session_id, "verify")
diff --git a/src/hermespace/plugin.py b/src/hermespace/plugin.py
index 3e1fba2..5cbc8f6 100644
--- a/src/hermespace/plugin.py
+++ b/src/hermespace/plugin.py
@@ -68,13 +68,18 @@ def register(ctx: Any) -> None:
from hermespace import __version__
from hermespace.hermes_bridge import (
+ on_kanban_task_claimed,
+ on_kanban_task_completed,
on_post_llm_call,
on_post_tool_call,
on_pre_llm_call,
+ on_pre_tool_call,
+ on_pre_verify,
on_session_end,
on_session_finalize,
on_session_reset,
on_session_start,
+ on_skill_lifecycle,
on_subagent_start,
on_subagent_stop,
)
@@ -83,7 +88,12 @@ def register(ctx: Any) -> None:
"on_session_start": on_session_start,
"pre_llm_call": on_pre_llm_call,
"post_llm_call": on_post_llm_call,
+ "pre_tool_call": on_pre_tool_call,
"post_tool_call": on_post_tool_call,
+ "on_skill_lifecycle": on_skill_lifecycle,
+ "kanban_task_claimed": on_kanban_task_claimed,
+ "kanban_task_completed": on_kanban_task_completed,
+ "pre_verify": on_pre_verify,
"on_session_end": on_session_end,
"on_session_finalize": on_session_finalize,
"on_session_reset": on_session_reset,
diff --git a/tests/test_install_kit.py b/tests/test_install_kit.py
new file mode 100644
index 0000000..44484bd
--- /dev/null
+++ b/tests/test_install_kit.py
@@ -0,0 +1,162 @@
+"""One-install front door — union organs, memory.provider, doctor WARN."""
+
+from __future__ import annotations
+
+import os
+import tempfile
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+class TestInstallKit(unittest.TestCase):
+ def setUp(self) -> None:
+ self._td = tempfile.TemporaryDirectory()
+ self.home = Path(self._td.name) / "hermes"
+ self.space = Path(self._td.name) / "space"
+ self.home.mkdir()
+ self.space.mkdir()
+ self._old_h = os.environ.get("HERMES_HOME")
+ self._old_s = os.environ.get("HERMESPACE_HOME")
+ os.environ["HERMES_HOME"] = str(self.home)
+ os.environ["HERMESPACE_HOME"] = str(self.space)
+
+ def tearDown(self) -> None:
+ if self._old_h is None:
+ os.environ.pop("HERMES_HOME", None)
+ else:
+ os.environ["HERMES_HOME"] = self._old_h
+ if self._old_s is None:
+ os.environ.pop("HERMESPACE_HOME", None)
+ else:
+ os.environ["HERMESPACE_HOME"] = self._old_s
+ self._td.cleanup()
+
+ def test_union_keeps_existing_and_adds_space(self) -> None:
+ from hermespace.hermes_enable import union_plugins_enabled
+ from hermespace.install_kit import union_front_door
+
+ cfg = self.home / "config.yaml"
+ cfg.write_text(
+ "plugins:\n enabled:\n - grokbot\n - hermescube\n",
+ encoding="utf-8",
+ )
+ (self.home / "plugins" / "hermescube").mkdir(parents=True)
+ out = union_front_door(home=self.home)
+ self.assertTrue(out["ok"], out)
+ enabled = out["enabled"]
+ self.assertIn("grokbot", enabled)
+ self.assertIn("hermescube", enabled)
+ self.assertIn("hermespace", enabled)
+ again = union_plugins_enabled("hermespace", home=self.home)
+ self.assertEqual(again["action"], "already")
+
+ def test_memory_provider_does_not_clobber(self) -> None:
+ from hermespace.hermes_enable import (
+ ensure_cube_memory_provider,
+ read_memory_provider,
+ )
+
+ cfg = self.home / "config.yaml"
+ cfg.write_text("memory:\n provider: notes\n", encoding="utf-8")
+ out = ensure_cube_memory_provider(home=self.home)
+ self.assertEqual(out["action"], "kept")
+ self.assertFalse(out["clobbered"])
+ self.assertEqual(read_memory_provider(cfg), "notes")
+
+ def test_memory_provider_set_only_when_unset(self) -> None:
+ from hermespace.hermes_enable import (
+ ensure_cube_memory_provider,
+ read_memory_provider,
+ )
+
+ cfg = self.home / "config.yaml"
+ cfg.write_text("plugins:\n enabled:\n - hermespace\n", encoding="utf-8")
+ out = ensure_cube_memory_provider(home=self.home)
+ self.assertIn(out["action"], {"set", "created"})
+ self.assertEqual(read_memory_provider(cfg), "hermescube")
+ self.assertIn("hermespace", cfg.read_text(encoding="utf-8"))
+
+ def test_install_no_organs_links_space_and_unions(self) -> None:
+ from hermespace.install_kit import install_front_door
+
+ out = install_front_door(
+ checkout=ROOT,
+ home=self.home,
+ yes=False,
+ no_organs=True,
+ enable=True,
+ )
+ self.assertTrue(out["ok"], out)
+ self.assertTrue((self.home / "plugins" / "hermespace").exists())
+ self.assertTrue((self.home / "skills" / "hermespace" / "SKILL.md").is_file())
+ self.assertIn("hermespace", out["union"]["enabled"])
+ self.assertEqual(out["organs"].get("skipped"), True)
+
+ def test_doctor_warns_not_fails_when_organs_missing(self) -> None:
+ from hermespace.install_kit import install_front_door
+ from hermespace.ops import doctor
+
+ install_front_door(checkout=ROOT, home=self.home, no_organs=True, enable=True)
+ d = doctor(agent_id="kit-doc")
+ by = {c["name"]: c for c in d["checks"]}
+ self.assertTrue(d["ok"], d)
+ self.assertFalse(by["cube_organ"]["ok"])
+ self.assertFalse(by["insight_organ"]["ok"])
+ self.assertTrue(any("Cube" in w for w in d.get("warnings") or []))
+ self.assertTrue(any("Insight" in w for w in d.get("warnings") or []))
+
+
+class TestInjectCapAndHooks(unittest.TestCase):
+ def test_harvest_budget_fail_open(self) -> None:
+ import time
+
+ from hermespace.hermes_bridge import _run_fail_open
+
+ started = time.monotonic()
+
+ def _slow() -> None:
+ time.sleep(2)
+
+ _run_fail_open("test_budget", _slow, seconds=0.05)
+ self.assertLess(time.monotonic() - started, 1.0)
+
+ def test_bounded_context_stays_under_9k(self) -> None:
+ from hermespace.hermes_bridge import INJECT_HARD_CAP, _bounded_context
+
+ self.assertLess(INJECT_HARD_CAP, 9000)
+ blob = "x" * 20_000
+ out = _bounded_context(blob)
+ self.assertLess(len(out), 9000)
+
+ def test_new_hooks_park_names_only(self) -> None:
+ from hermespace.access.hub import AccessHub
+ from hermespace.hermes_bridge import (
+ on_kanban_task_claimed,
+ on_pre_tool_call,
+ on_skill_lifecycle,
+ )
+
+ with tempfile.TemporaryDirectory() as td:
+ os.environ["HERMESPACE_HOME"] = td
+ os.environ["HERMESPACE_AGENT_ID"] = "hook-agent"
+ on_pre_tool_call(
+ tool_name="read_file",
+ session_id="default",
+ args={"secret": "must-not-persist"},
+ )
+ on_skill_lifecycle(skill_name="demo", event="loaded", session_id="default")
+ on_kanban_task_claimed(task_id="card-1", title="secret title", session_id="default")
+ hub = AccessHub(agent_id="hook-agent")
+ silent = " ".join(hub.state.silent_steps)
+ self.assertIn("skill:demo:loaded", silent)
+ self.assertIn("kanban:claimed:card-1", silent)
+ self.assertNotIn("must-not-persist", silent)
+ self.assertNotIn("secret title", silent)
+ os.environ.pop("HERMESPACE_HOME", None)
+ os.environ.pop("HERMESPACE_AGENT_ID", None)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_plugin_contract.py b/tests/test_plugin_contract.py
index 22488ce..2dcdb3e 100644
--- a/tests/test_plugin_contract.py
+++ b/tests/test_plugin_contract.py
@@ -57,6 +57,14 @@ def test_registration_and_native_lifecycle(self) -> None:
"on_session_finalize",
}
self.assertTrue(required.issubset(ctx.hooks), ctx.hooks)
+ for extra in (
+ "pre_tool_call",
+ "on_skill_lifecycle",
+ "kanban_task_claimed",
+ "kanban_task_completed",
+ "pre_verify",
+ ):
+ self.assertIn(extra, ctx.hooks)
self.assertIn("hermespace", ctx.commands)
self.assertIn("hermespace", ctx.cli)
@@ -118,7 +126,12 @@ def test_plugin_yaml_hygiene(self) -> None:
"on_session_start",
"pre_llm_call",
"post_llm_call",
+ "pre_tool_call",
"post_tool_call",
+ "on_skill_lifecycle",
+ "kanban_task_claimed",
+ "kanban_task_completed",
+ "pre_verify",
"on_session_end",
"on_session_finalize",
"on_session_reset",
From 9ef4d7ec8c86406f06a957bde03ac4c9ab0c6a6d Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 22:23:36 +0000
Subject: [PATCH 25/32] =?UTF-8?q?docs:=20front-door=20paragraph=20?=
=?UTF-8?q?=E2=80=94=20install=20Space,=20get=20a=20better=20turn?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Community README/PURPOSE: desk plus optional Cube library and Insight
pattern card behind one unioned plugins.enabled. No product rename.
No weight-access claim.
Co-authored-by: Pablo
---
PURPOSE.md | 12 ++++++++++--
README.md | 17 ++++++++++++++---
skills/hermespace/references/plugin-hooks.md | 6 +++++-
3 files changed, 29 insertions(+), 6 deletions(-)
diff --git a/PURPOSE.md b/PURPOSE.md
index d0002d2..190f300 100644
--- a/PURPOSE.md
+++ b/PURPOSE.md
@@ -11,6 +11,11 @@ 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.
+**Front door.** Install Space and the agent gets a better turn: desk plus
+optional Cube library and Insight pattern card as organs, one unioned
+`plugins.enabled`. Cube and Insight stay standalone. Space never claims
+weight access.
+
---
## Why it exists
@@ -118,9 +123,12 @@ Hermespace uses the current public plugin contracts:
- `on_session_start`
- `pre_llm_call`
- `post_llm_call`
-- `post_tool_call`
+- `pre_tool_call` / `post_tool_call`
+- `on_skill_lifecycle`
+- `kanban_task_claimed` / `kanban_task_completed`
+- `pre_verify`
- `on_session_end`
-- `on_session_finalize`
+- `on_session_finalize` (harvest ≤10s, fail-open)
- `on_session_reset`
- `subagent_start` / `subagent_stop`
- `/hermespace` slash command
diff --git a/README.md b/README.md
index c30ea8a..ee4e746 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,12 @@ 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.
+Install Space and any Hermes Agent gets a better turn: a bounded desk, plus
+optional Cube library and Insight pattern card as organs behind one unioned
+`plugins.enabled`. Cube and Insight stay standalone repos — Space soft-imports
+them when present and never rewrites a plugin list or a memory provider you
+already chose.
+
Production target: **Hermes Agent v0.20.0+** — CLI, gateways, A2A, tools,
subagents, turn boundaries, and finalization.
@@ -152,8 +158,9 @@ 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
+hs install # Space plugin+skill; offers Cube + Insight
+# or: hermes plugins install PabloTheThinker/hermespace --enable
+hs ops doctor # FAIL if Space is broken; WARN if Cube/Insight missing
```
For a development checkout:
@@ -182,9 +189,13 @@ ctx = r["model_context"] # → model (includes world context)
| `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 |
+| `pre_tool_call` | Observe upcoming tool (name only; fail-open, never deny) |
| `post_tool_call` | Record bounded tool-name/count telemetry (no payloads) |
+| `on_skill_lifecycle` | Park `skill:{name}:{event}` — no skill body |
+| `kanban_task_claimed` / `kanban_task_completed` | Park kanban id so the hub moves |
+| `pre_verify` | Observe a verify gate (fail-open) |
| `on_session_end` | Lightweight end-of-turn receipt (Hermes v0.20 semantics) |
-| `on_session_finalize` | Idempotent harvest, world leave, idle maintenance |
+| `on_session_finalize` | Harvest ≤10s fail-open, world leave, idle maintenance |
| `subagent_start/stop` | Track specialist lifecycle for runtime observability |
---
diff --git a/skills/hermespace/references/plugin-hooks.md b/skills/hermespace/references/plugin-hooks.md
index c880884..ebcc0dc 100644
--- a/skills/hermespace/references/plugin-hooks.md
+++ b/skills/hermespace/references/plugin-hooks.md
@@ -14,9 +14,13 @@ Repository `__init__.py` → `hermespace.plugin.register(ctx)`.
| 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 |
+| pre_tool_call | Observe upcoming tool; fail-open; never persist args |
| post_tool_call | Count tool name only; never persist payloads |
+| on_skill_lifecycle | Park skill name+event only |
+| kanban_task_claimed / completed | Park kanban id so the hub moves |
+| pre_verify | Observe verify gate; fail-open |
| on_session_end | Lightweight turn boundary |
-| on_session_finalize | Idempotent harvest + idle maintenance |
+| on_session_finalize | Harvest ≤10s fail-open + idle maintenance |
| on_session_reset | Prime rotated gateway session |
| subagent_start/stop | Track specialist lifecycle |
From f757c72aa57774b8e2fd534e17fc04e3c724c9e6 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 22:30:31 +0000
Subject: [PATCH 26/32] feat(access): context surgery and bounded self-trace
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Keep one user-message inject under mid ≤2.8k / high ≤900. Skip fluent
ack, pinned protect, missing organs, and shared-hub subagents. Park a
capped self-trace on the hub (goal, decision, tool:name, Report line)
and seal one-line improve into Cube when present. No consciousness claim.
Co-authored-by: Pablo
---
PURPOSE.md | 6 +-
README.md | 8 +-
src/hermespace/access/engine.py | 35 +++++
src/hermespace/access/env.py | 10 +-
src/hermespace/context_surgery.py | 147 ++++++++++++++++++
src/hermespace/gate.py | 1 +
src/hermespace/grid/viewport.py | 10 ++
src/hermespace/hermes_bridge.py | 245 +++++++++++++-----------------
src/hermespace/hermes_runtime.py | 10 ++
src/hermespace/inject.py | 20 ++-
src/hermespace/insight_module.py | 14 +-
src/hermespace/self_model.py | 150 ++++++++++++++++++
src/hermespace/workflow.py | 66 +++++---
tests/test_context_surgery.py | 224 +++++++++++++++++++++++++++
tests/test_gate.py | 3 +
tests/test_insight_module.py | 8 +-
tests/test_plugin_contract.py | 2 +-
17 files changed, 772 insertions(+), 187 deletions(-)
create mode 100644 src/hermespace/context_surgery.py
create mode 100644 src/hermespace/self_model.py
create mode 100644 tests/test_context_surgery.py
diff --git a/PURPOSE.md b/PURPOSE.md
index 190f300..4aafc26 100644
--- a/PURPOSE.md
+++ b/PURPOSE.md
@@ -14,7 +14,11 @@ current problem coherent.
**Front door.** Install Space and the agent gets a better turn: desk plus
optional Cube library and Insight pattern card as organs, one unioned
`plugins.enabled`. Cube and Insight stay standalone. Space never claims
-weight access.
+weight access. Context is progressive: one inject, mid ≤2.8k, high/protect
+≤900. After a material turn the hub keeps a bounded self-trace (last goal,
+decision, `tool:name` list, sealed Report line) so the desk can improve.
+Product language is self-model / self-trace / improve — never “true
+self-conscious,” never a phenomenal-consciousness claim.
---
diff --git a/README.md b/README.md
index ee4e746..48f0879 100644
--- a/README.md
+++ b/README.md
@@ -116,7 +116,7 @@ The `world_evolve` pulse job runs this hourly. Manual: `hs world evolve` or `Wor
### Context Injection
-Every `pre_llm_call` injects the world context: epoch badge, active concepts, wisdom (top beliefs), timeline, pulse summary, and desk status. The agent always knows where it is in its own story.
+One user-message inject, progressive disclosure. Mid-load target ≤2.8k (hard cap <9k); high/protect ≤900. Fluent ack, high load, or a missing organ injects nothing. Cube is skipped when the provider already prefetched. Spoken Report stays short (line 1 = next action); dense context is not also dumped into chat. Harvest runs on finalize (≤10s, fail-open) and never rides the inject path.
### CLI
@@ -146,9 +146,9 @@ 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. |
+| **Dual Decode** | Human gets a short report (line 1 = next action); the model gets one lean inject. Never dump raw inject into chat. |
| **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 |
+| **Insight (optional)** | Soft cable — `perceive_card` only (≤400), skip on high/protect; usable/lever write-back on `desk.meta` only; 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. |
@@ -215,7 +215,7 @@ User message
DECODE report (human) + context (model)
│
▼
- BROADCAST plugin inject + world context
+ BROADCAST one lean inject (desk FOA; no world dump)
│
▼
ACT Hermes tools / code / ship
diff --git a/src/hermespace/access/engine.py b/src/hermespace/access/engine.py
index e5ff534..796c2b6 100644
--- a/src/hermespace/access/engine.py
+++ b/src/hermespace/access/engine.py
@@ -650,6 +650,41 @@ def observe_turn(
agent_hub = AccessHub(agent_id=self.agent_id)
park_spoken_intermediates(agent_hub, report, max_n=3)
+ try:
+ from hermespace.self_model import maybe_seal_improve, record_self_trace
+ from hermespace.store import load_desk
+
+ desk = load_desk(self.desk_engine.desk_path)
+ tools = [
+ s
+ for s in (self.hub.state.silent_steps or [])
+ if str(s).startswith("tool:")
+ ]
+ trace = record_self_trace(
+ self.hub,
+ goal=desk.goal,
+ decision=desk.decision,
+ tools=tools,
+ report=report,
+ )
+ result["self_trace"] = trace
+ if self.workspace_id != self.agent_id:
+ from hermespace.access.hub import AccessHub
+
+ record_self_trace(
+ AccessHub(agent_id=self.agent_id),
+ goal=desk.goal,
+ decision=desk.decision,
+ tools=tools,
+ report=report,
+ )
+ improve = maybe_seal_improve(desk, agent_id=self.agent_id)
+ result["improve"] = {
+ "ok": improve.get("ok"),
+ "skipped": improve.get("skipped"),
+ }
+ except Exception as exc:
+ result["self_model_error"] = type(exc).__name__
except Exception as exc:
result["loop_error"] = type(exc).__name__
diff --git a/src/hermespace/access/env.py b/src/hermespace/access/env.py
index db0ebfc..4e1661d 100644
--- a/src/hermespace/access/env.py
+++ b/src/hermespace/access/env.py
@@ -504,11 +504,11 @@ 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 Access Workspace before acting.
+ """Operator / bind protocol. Do not dump this essay onto the pre_llm inject.
- 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.
+ Bound intervention lines may ride the inject when a bind is active.
+ reflect()/audit write pending_silent for the *next* turn — they do not
+ dump a self-essay into this turn's model context.
"""
if not self._env.get("protocol_enabled", True):
return ""
@@ -622,11 +622,13 @@ def operator_view(self) -> dict[str, Any]:
"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:],
+ "self_trace": dict((self.space.state.meta or {}).get("self_trace") or {}),
"trace_path": str(self.trace_path),
"protocol_enabled": bool(self._env.get("protocol_enabled", True)),
"theory": {
"source": "Hermespace Access Workspace / GWT",
"access": "externalized verbalizable workspace — not weight readout",
+ "self_model": "self-trace / improve — not phenomenal consciousness",
"night_path": "dream_harvest → Cube seal → pulse charge",
},
}
diff --git a/src/hermespace/context_surgery.py b/src/hermespace/context_surgery.py
new file mode 100644
index 0000000..2e99ae1
--- /dev/null
+++ b/src/hermespace/context_surgery.py
@@ -0,0 +1,147 @@
+"""Progressive disclosure for the one user-message inject.
+
+Hard budgets — do not eat the turn:
+
+ mid default ≤ 2.8k
+ high/protect ≤ 900
+ hard cap < 9k (8500 safety net)
+ FOA ≤ 4 · silent chain ≤ 8 · Insight card ≤ 400
+
+Spoken Report stays short (line 1 = next action). Dense context is not
+also dumped into chat. Harvest never rides this path.
+"""
+
+from __future__ import annotations
+
+import re
+from typing import Any, Iterable
+
+MID_INJECT_CAP = 2800
+HIGH_INJECT_CAP = 900
+INJECT_HARD_CAP = 8500 # strictly < 9k
+INSIGHT_CARD_CHARS = 400
+SELF_TRACE_INJECT_CHARS = 280
+FOA_CAP = 4
+SILENT_CHAIN_CAP = 8
+
+_FLUENT_ACK_RE = re.compile(
+ r"^\s*(ok|okay|k|thanks|thank you|ty|cool|nice|good|perfect|boom|"
+ r"got it|gotcha|sure|np|lgtm|cheers|heartbeat_ok|👍|❤️|yes|yep|nope|no)"
+ r"\s*[.!]*\s*$",
+ re.I,
+)
+
+# Never appear on the inject path (operator / harvest / lattice / claims).
+_FORBIDDEN_LINE = re.compile(
+ r"J-Lens readout|What Hermes has on its mind|lens_markdown|"
+ r"true self-conscious|phenomenal consciousness|"
+ r"### Workbench|### Hermespace runtime|"
+ r"dream_harvest|harvest items|recall brief|perceive\(\)\[.card.\]|"
+ r"### Lattice|insight_lattice",
+ re.I,
+)
+
+
+def is_fluent_ack(message: str) -> bool:
+ """Short acknowledgement — inject nothing this turn."""
+ msg = (message or "").strip()
+ return bool(msg) and len(msg) < 48 and bool(_FLUENT_ACK_RE.match(msg))
+
+
+def inject_budget(load_level: str | None) -> int:
+ level = str(load_level or "mid").strip().lower()
+ if level in {"high", "protect", "protected"}:
+ return HIGH_INJECT_CAP
+ return MID_INJECT_CAP
+
+
+def strip_needed(
+ *,
+ message: str = "",
+ load_level: str = "mid",
+ is_first_turn: bool = False,
+ has_bind: bool = False,
+ missing_organ: bool = False,
+) -> bool:
+ """False → inject nothing (fluent ack, high load, missing organ)."""
+ if is_fluent_ack(message):
+ return False
+ if missing_organ and not has_bind and not is_first_turn:
+ return False
+ level = str(load_level or "mid").strip().lower()
+ if level in {"high", "protect", "protected"}:
+ return bool(has_bind or is_first_turn)
+ return True
+
+
+def is_shared_hub_child(session_id: str = "", kwargs: dict[str, Any] | None = None) -> bool:
+ """Subagent / kanban worker — share the one desk; do not inject a full copy."""
+ kw = kwargs or {}
+ for key in (
+ "parent_session_id",
+ "parent_task_id",
+ "subagent_id",
+ "is_subagent",
+ ):
+ if kw.get(key):
+ return True
+ role = str(kw.get("agent_role") or kw.get("role") or "").strip().lower()
+ if role in {"subagent", "kanban", "worker"}:
+ return True
+ try:
+ from hermespace.hermes_runtime import runtime
+
+ st = runtime.status(session_id or "")
+ return bool(st.get("shared_hub"))
+ except Exception:
+ return False
+
+
+def dual_decode_line() -> str:
+ """Tiny protocol — spoken Report stays short; dense context stays here."""
+ return (
+ "### Access Workspace\n"
+ "- Report line 1 = next action. Dense context stays here — do not dump into chat."
+ )
+
+
+def assemble_inject(parts: Iterable[str], *, budget: int) -> str:
+ """Join strips in order until the budget is spent. One user-message inject."""
+ cap = max(80, min(int(budget), INJECT_HARD_CAP))
+ out: list[str] = []
+ used = 0
+ for raw in parts:
+ piece = sanitize_inject(str(raw or "")).strip()
+ if not piece:
+ continue
+ sep = 2 if out else 0
+ if used + sep + len(piece) <= cap:
+ out.append(piece)
+ used += sep + len(piece)
+ continue
+ remain = cap - used - sep
+ if remain >= 40:
+ out.append(piece[: remain - 3].rstrip() + "...")
+ break
+ block = "\n\n".join(out)
+ if len(block) > cap:
+ block = block[: cap - 3].rstrip() + "..."
+ return block
+
+
+def sanitize_inject(text: str) -> str:
+ """Drop operator-lens / harvest / consciousness-claim lines if they leak in."""
+ if not text:
+ return ""
+ kept: list[str] = []
+ for line in text.splitlines():
+ if _FORBIDDEN_LINE.search(line):
+ continue
+ kept.append(line)
+ return "\n".join(kept)
+
+
+def harvest_on_inject(text: str) -> bool:
+ """True if harvest prose leaked onto the 9k/inject path (must stay false)."""
+ low = (text or "").casefold()
+ return "dream_harvest" in low or "harvest items" in low
diff --git a/src/hermespace/gate.py b/src/hermespace/gate.py
index f324ecb..4a75fea 100644
--- a/src/hermespace/gate.py
+++ b/src/hermespace/gate.py
@@ -21,6 +21,7 @@
_TRIVIAL_RE = re.compile(
r"^\s*(ok|okay|k|thanks|thank you|ty|cool|nice|good|perfect|boom|"
+ r"got it|gotcha|sure|np|lgtm|cheers|"
r"heartbeat_ok|👍|❤️|yes|yep|nope|no)\s*[.!]*\s*$",
re.I,
)
diff --git a/src/hermespace/grid/viewport.py b/src/hermespace/grid/viewport.py
index 89dc82b..2ea9e7f 100644
--- a/src/hermespace/grid/viewport.py
+++ b/src/hermespace/grid/viewport.py
@@ -67,11 +67,21 @@ def _foa_paint(agent_id: str, desk_json: dict[str, Any]) -> dict[str, Any]:
dec = decision.strip() or "unsealed"
if len(dec) > 28:
dec = dec[:27].rstrip() + "…"
+ self_trace: dict[str, Any] = {}
+ try:
+ from hermespace.self_model import read_self_trace
+ from hermespace.access import AccessEnv
+
+ aid = agent_id if agent_id not in ("default", "") else "hermes-agent"
+ self_trace = read_self_trace(AccessEnv(agent_id=aid).space)
+ except Exception:
+ self_trace = {}
return {
"goal": goal,
"focus": focus,
"parked": parked,
"decision": decision,
+ "self_trace": self_trace,
"chip": f"{g} · FOA {len(focus)} · {len(parked)} parked · {dec}",
}
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index ab18ca5..8dc76d7 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -13,7 +13,18 @@ def _truthy(name: str, default: str = "0") -> bool:
return os.environ.get(name, default).strip().lower() in {"1", "true", "yes", "on"}
-INJECT_HARD_CAP = 8500 # mid-load shrink: strictly < 9k
+from hermespace.context_surgery import ( # noqa: E402
+ INJECT_HARD_CAP,
+ MID_INJECT_CAP,
+ assemble_inject,
+ dual_decode_line,
+ inject_budget,
+ is_fluent_ack,
+ is_shared_hub_child,
+ sanitize_inject,
+ strip_needed,
+)
+
HARVEST_BUDGET_S = 10.0
@@ -287,28 +298,23 @@ def on_pre_llm_call(
reg = regulate(msg, agent_id=agent_id)
if reg.handled:
- # Short user-facing note + keep inject for model
+ # Short user-facing note + one lean inject. Never prepend session-start.
desk = load_desk(eng.desk_path)
note = reg.message
- block = build_inject_block(desk, max_chars=2000, user_message=msg)
- try:
- from hermespace.grid.access import pending_inject_block
-
- block = (block + "\n\n" + pending_inject_block(agent_id)).strip()
- except Exception:
- pass
- # Prefer explicit regulation reply as dual-channel: model sees full; user gets note via say path if auto
- return {
- "context": _bounded_context(
- ((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"
- + "- Honor pocket rules. Do not write outside without approved permit.\n"
- ),
- # Some hosts ignore unknown keys; context is enough for model
- }
+ block = assemble_inject(
+ [
+ dual_decode_line(),
+ build_inject_block(
+ desk, max_chars=MID_INJECT_CAP, user_message=msg, lean=True
+ ),
+ "### Boundary regulation (this turn)\n"
+ f"- action: {reg.action}\n"
+ f"- user_reply_hint: {note}\n"
+ "- Honor pocket rules. Do not write outside without approved permit.",
+ ],
+ budget=MID_INJECT_CAP,
+ )
+ return {"context": _bounded_context(block)}
except Exception as exc: # noqa: BLE001
logger.debug("regulate failed: %s", exc)
@@ -335,14 +341,18 @@ def on_pre_llm_call(
do_it, reason = should_inject(
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
+ if not do_it or is_fluent_ack(msg):
+ return None
+ if is_shared_hub_child(sid, kwargs):
+ # One desk: park on the shared agent hub. Do not inject a full copy.
+ try:
+ from hermespace.access import AccessHub
- runtime.stage_start_context(sid, start_context)
- except Exception:
- pass
+ AccessHub(agent_id=agent_id).sync_from_desk(
+ load_desk(eng.desk_path), user_message=msg
+ )
+ except Exception:
+ pass
return None
if msg and ready:
@@ -360,18 +370,25 @@ def on_pre_llm_call(
# High load / monotropic: cognition clamp only — skip neural FOA
# (often ~200–300ms) unless explicitly forced on.
- high_load = str((desk.load or {}).get("level") or "") == "high"
- if not high_load and msg:
+ # protect is operator-pinned and must not be recomputed away.
+ pinned = str((desk.load or {}).get("level") or "")
+ high_load = pinned in {"high", "protect"}
+ if pinned != "protect" and not high_load and msg:
# cheap recompute so high flag can flip this turn
try:
desk.recompute_cognition(msg)
- high_load = str((desk.load or {}).get("level") or "") == "high"
+ high_load = str((desk.load or {}).get("level") or "") in {"high", "protect"}
except Exception:
pass
if need_heavy:
- desk.recompute_cognition(msg)
- high_load = str((desk.load or {}).get("level") or "") == "high"
+ if pinned != "protect":
+ desk.recompute_cognition(msg)
+ high_load = str((desk.load or {}).get("level") or "") in {"high", "protect"}
+ else:
+ high_load = True
+ if isinstance(desk.load, dict):
+ desk.load["level"] = "protect"
skip_neural = (
high_load
# Native pre_llm hooks are latency-sensitive. Neural
@@ -421,58 +438,13 @@ def on_pre_llm_call(
load_level = str((desk.load or {}).get("level") or "mid")
high_load = load_level in {"high", "protect"}
- if high_load:
- inject_cap = 900
- elif load_level == "mid":
- inject_cap = 1600
- else:
- inject_cap = 2800
- 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
- # First turn gets full world context; subsequent turns get delta
- # High load: skip world prose entirely (FOA only)
- if high_load and not is_first_turn:
- world_context_block = ""
- else:
- last_count = desk.meta.get("world_entry_count", 0)
- world_context_block = world_context(
- agent_id,
- full=bool(is_first_turn) or last_count == 0,
- known_entries=last_count,
- )
- # Store entry count for next turn's delta
- try:
- from hermespace.world import WorldModel
- wm = WorldModel(agent_id=agent_id)
- desk.meta["world_entry_count"] = wm.archive.count()
- from hermespace.store import save_desk
- save_desk(desk)
- except Exception:
- pass
- except Exception:
- world_context_block = ""
-
- try:
- from hermespace.grid.access import pending_inject_block
+ inject_cap = inject_budget(load_level)
+ # Session-start essay is observer-only. Never prepend it onto the inject.
+ _ = start_context
- block += "\n\n" + pending_inject_block(agent_id)
- except Exception:
- pass
-
- if world_context_block and not high_load:
- block += "\n\n" + world_context_block
- elif world_context_block and high_load and is_first_turn:
- # keep tiny world stamp only
- block += "\n\n" + world_context_block[:400]
-
- # HermesCube / standalone warehouse — dense deep memory under load
- # Prefer center.beat (1.1); falls back to heart inject / standalone strip
+ cube_block = ""
+ insight_strip = ""
+ bound_strip = ""
try:
from hermespace.cube_module import cube_beat, skip_cube_foa_strip
from hermespace.access import AccessHub
@@ -500,9 +472,8 @@ def on_pre_llm_call(
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.
+ # Insight: perceive_card only. Write-back (usable/lever) on desk.meta.
+ # Never inject perceive()["card"], recall brief, or the lattice.
try:
from hermespace.insight_module import insight_card
@@ -512,11 +483,15 @@ def on_pre_llm_call(
"mode": icard.get("mode"),
"skipped": icard.get("skipped"),
}
- if icard.get("card"):
- block += "\n\n" + str(icard["card"])
+ wb = icard.get("writeback") or {}
+ if isinstance(wb, dict) and (wb.get("usable") is not None or wb.get("lever") is not None):
+ desk.meta["insight_writeback"] = {
+ k: wb[k] for k in ("usable", "lever") if k in wb
+ }
+ if icard.get("card") and not high_load:
+ insight_strip = 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
ensure_oew_env_default()
@@ -533,7 +508,6 @@ def on_pre_llm_call(
from hermespace.access import AccessEnv
env = AccessEnv(agent_id=access_id)
- # sync_from_desk already ran above — skip second rewrite
env_meta = env.advance_turn(
user_message=msg,
desk=desk,
@@ -544,16 +518,13 @@ def on_pre_llm_call(
)
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
- # Lens is operator-only. Do not inject the operator readout
- # as if it were the model's own workspace.
+ # Bound lines only — not the protocol essay, not lens, not reflect dump.
+ try:
+ from hermespace.access.loop import bound_protocol_lines
+
+ bound_strip = bound_protocol_lines(env)
+ except Exception:
+ bound_strip = ""
desk.meta["access"] = {
"hub_n": len(js.state.hub),
"focus_n": len(js.state.focus),
@@ -570,9 +541,6 @@ def on_pre_llm_call(
}
desk.meta["user_reply_hint"] = (desk.say or "")[:240]
except Exception:
- jblock = js.broadcast_block(high_load=high_load)
- if jblock:
- block += "\n\n" + jblock
desk.meta["access"] = {
"hub_n": len(js.state.hub),
"focus_n": len(js.state.focus),
@@ -581,60 +549,56 @@ def on_pre_llm_call(
try:
from hermespace.store import save_desk
- save_desk(desk)
+ save_desk(desk, eng.desk_path)
except Exception:
pass
except Exception:
pass
- # Workbench status — only on first turn or when state changes (skip under high)
- if not high_load:
- try:
- st = Workbench(agent_id=agent_id, session_id=sid).status()
- last_mode = desk.meta.get("workbench_mode", "")
- if is_first_turn or st.get("mode") != last_mode:
- block += (
- f"\n### Workbench\n"
- f"- mode: {st.get('mode')} · park: {st.get('park_count')} · "
- f"idle_ticks: {st.get('idle_ticks')}\n"
- f"- last_report: {(st.get('last_report') or '')[:120]}\n"
- )
- desk.meta["workbench_mode"] = st.get("mode")
- try:
- from hermespace.store import save_desk
- save_desk(desk)
- except Exception:
- pass
- except Exception:
- pass
+ has_bind = bool((bound_strip or "").strip())
+ if not strip_needed(
+ message=msg,
+ load_level=load_level,
+ is_first_turn=bool(is_first_turn),
+ has_bind=has_bind,
+ ):
+ return None
+ desk_block = build_inject_block(
+ desk, max_chars=inject_cap, user_message=msg, lean=True
+ )
+ parts = [dual_decode_line(), desk_block]
+ if has_bind:
+ parts.append(bound_strip)
+ # One organ strip if it fits: Insight card preferred, else Cube (never dual-pump).
+ organ = ""
if not high_load:
+ organ = insight_strip or cube_block
+ if organ:
+ parts.append(organ)
+ if load_level == "low":
try:
- from hermespace import AccessEngine
+ from hermespace.self_model import format_self_trace, read_self_trace
+ from hermespace.access import AccessHub
- 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"
+ parts.append(
+ format_self_trace(
+ read_self_trace(AccessHub(agent_id=access_id)),
+ for_inject=True,
+ )
)
except Exception:
pass
- # Dual-decode hint for hosts that only accept context: short user Report
+ block = sanitize_inject(assemble_inject(parts, budget=inject_cap))
+ if not block.strip():
+ return None
+
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 Access Workspace hub / silent chain / this inject block into chat.\n"
- )
try:
eng.episodes.write(
@@ -645,7 +609,6 @@ def on_pre_llm_call(
except Exception:
pass
- # Prefer dual-channel when host supports unknown keys; context always set
result: dict[str, str] = {"context": _bounded_context(block)}
if user_hint:
result["user_reply_hint"] = user_hint
diff --git a/src/hermespace/hermes_runtime.py b/src/hermespace/hermes_runtime.py
index 2090d15..f7c81d2 100644
--- a/src/hermespace/hermes_runtime.py
+++ b/src/hermespace/hermes_runtime.py
@@ -52,6 +52,7 @@ class SessionRuntime:
last_response_chars: int = 0
last_event: str = "session_start"
finalized: bool = False
+ shared_hub: bool = False
recent_tools: list[str] = field(default_factory=list)
@@ -185,11 +186,20 @@ def tool(self, session_id: str | None, *, agent_id: str, name: str) -> None:
if item.tool_calls == 1 or item.tool_calls % 10 == 0:
self._save(item)
+ def mark_shared_hub(self, session_id: str | None, *, agent_id: str) -> None:
+ """Kanban / subagent workers share the one desk — no full inject copy."""
+ with self._lock:
+ item = self._get(session_id, agent_id=agent_id)
+ item.shared_hub = True
+ item.last_event = "shared_hub"
+ 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.shared_hub = True
item.last_event = "subagent_start"
else:
item.subagent_stops += 1
diff --git a/src/hermespace/inject.py b/src/hermespace/inject.py
index 76f1187..654cbe0 100644
--- a/src/hermespace/inject.py
+++ b/src/hermespace/inject.py
@@ -14,6 +14,7 @@ def build_inject_block(
max_chars: int = 2000,
include_episodes: int = 4,
user_message: str = "",
+ lean: bool = False,
) -> str:
desk = desk or load_desk()
if user_message and not desk.load:
@@ -25,11 +26,14 @@ def build_inject_block(
high = str(desk.load.get("level")) == "high"
# GWT: under high load, broadcast only focus + goal/decision/say
+ # Never raise the caller budget — mid target is ≤2.8k.
if high:
max_chars = min(max_chars, 900)
include_episodes = 0
- elif desk.meta.get("fabric"):
- max_chars = max(max_chars, 2800)
+ else:
+ max_chars = min(max_chars, 2800)
+ if lean:
+ include_episodes = 0
parts: list[str] = ["## Hermespace live desk (use before acting)"]
@@ -64,7 +68,7 @@ def build_inject_block(
)
neural = desk.meta.get("neural") or {}
- if neural.get("enabled"):
+ if neural.get("enabled") and not lean:
parts.append(
f"**Neural space:** backend={neural.get('backend')} traces={neural.get('n_traces')} "
f"ignition={neural.get('ignition_threshold')} residual_n={neural.get('residual_norm')}"
@@ -134,9 +138,9 @@ def build_inject_block(
f"- ({e.get('outcome', 'info')}) {str(e.get('content', ''))[:100]}"
)
- # Hermes skills + MEMORY/USER (user's own fabric)
+ # Hermes skills + MEMORY/USER (user's own fabric) — skip on lean hook path
fabric = desk.meta.get("fabric") or {}
- if fabric and not high:
+ if fabric and not high and not lean:
try:
from hermespace.hermes_fabric import FabricSnapshot, SkillHit
hits = [
@@ -159,8 +163,8 @@ def build_inject_block(
if isinstance(h, dict):
parts.append(f"- `{h.get('name')}` score={h.get('score')}")
- # Grid layer (missions, lens, selftalk, hot modules)
- if not high:
+ # Grid layer (missions, lens, selftalk, hot modules) — operator lens stays off inject
+ if not high and not lean:
try:
import os
from hermespace.grid import Grid
@@ -172,7 +176,7 @@ def build_inject_block(
except Exception: # noqa: BLE001
pass
- if not high:
+ if not high and not lean:
try:
from hermespace.semantic import SemanticStore
diff --git a/src/hermespace/insight_module.py b/src/hermespace/insight_module.py
index 19be3fc..500c12b 100644
--- a/src/hermespace/insight_module.py
+++ b/src/hermespace/insight_module.py
@@ -93,6 +93,7 @@ def insight_card(
"adapter": SPACE_INSIGHT_ADAPTER_VERSION,
"mode": "missing",
"card": "",
+ "writeback": {},
"required": False,
}
if _high_or_protect(load, high_load=high_load):
@@ -120,12 +121,23 @@ def insight_card(
out["error"] = type(e).__name__
return out
+ writeback: dict[str, Any] = {}
if isinstance(rec, str):
card = rec
elif isinstance(rec, dict):
+ # perceive_card only — never perceive()["card"], recall brief, or lattice.
card = str(rec.get("card") or "")
+ for key in ("usable", "lever"):
+ if rec.get(key) is not None:
+ writeback[key] = rec[key]
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)})
+ out.update(
+ {
+ "mode": "insight",
+ "card": _bound_card(card, cap=cap),
+ "writeback": writeback,
+ }
+ )
return out
diff --git a/src/hermespace/self_model.py b/src/hermespace/self_model.py
new file mode 100644
index 0000000..43002d8
--- /dev/null
+++ b/src/hermespace/self_model.py
@@ -0,0 +1,150 @@
+"""Bounded self-model: self-trace on the hub, then improve.
+
+Product language: self-model / self-trace / improve.
+Never “true self-conscious.” No phenomenal-consciousness claim.
+
+After a material turn, Space already parks 1–3 intermediates from the
+actual assistant text. This module adds a capped “what I just did”
+record on the hub — readable via ``hs access view`` / FOA chip.
+It is injected only on low load. reflect()/audit stay operator or
+post_llm and write pending_silent for the *next* turn.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+GOAL_CAP = 120
+DECISION_CAP = 120
+REPORT_CAP = 160
+TOOL_CAP = 6
+TRACE_INJECT_CAP = 280
+
+
+def _first_line(text: str, cap: int) -> str:
+ line = (text or "").strip().splitlines()[0] if (text or "").strip() else ""
+ line = line.strip()
+ if len(line) <= cap:
+ return line
+ return line[: cap - 1].rstrip() + "…"
+
+
+def _tool_names(items: list[str] | None) -> list[str]:
+ out: list[str] = []
+ seen: set[str] = set()
+ for raw in items or []:
+ s = str(raw or "").strip()
+ if not s.startswith("tool:"):
+ name = s.split("(", 1)[0].strip()
+ if name and not name.startswith("tool:"):
+ s = f"tool:{name}"
+ else:
+ continue
+ key = s.casefold()
+ if key in seen:
+ continue
+ seen.add(key)
+ out.append(s[:80])
+ if len(out) >= TOOL_CAP:
+ break
+ return out
+
+
+def build_self_trace(
+ *,
+ goal: str = "",
+ decision: str = "",
+ tools: list[str] | None = None,
+ report: str = "",
+) -> dict[str, Any]:
+ """Capped self-trace dict — last goal / decision / tool:name list / Report line."""
+ return {
+ "goal": _first_line(goal, GOAL_CAP),
+ "decision": _first_line(decision, DECISION_CAP),
+ "tools": _tool_names(tools),
+ "report": _first_line(report, REPORT_CAP),
+ }
+
+
+def record_self_trace(
+ hub: Any,
+ *,
+ goal: str = "",
+ decision: str = "",
+ tools: list[str] | None = None,
+ report: str = "",
+) -> dict[str, Any]:
+ """Park the self-trace on the hub (not the inject)."""
+ silent = []
+ try:
+ silent = [str(s) for s in (hub.state.silent_steps or []) if str(s).startswith("tool:")]
+ except Exception:
+ silent = []
+ merged = list(tools or []) + silent
+ trace = build_self_trace(goal=goal, decision=decision, tools=merged, report=report)
+ try:
+ hub.state.meta["self_trace"] = dict(trace)
+ if hasattr(hub, "save"):
+ hub.save()
+ except Exception:
+ pass
+ return trace
+
+
+def format_self_trace(trace: dict[str, Any] | None, *, for_inject: bool = False) -> str:
+ """Readable self-trace. Inject only when load is low."""
+ t = dict(trace or {})
+ if not any(t.get(k) for k in ("goal", "decision", "tools", "report")):
+ return ""
+ tools = ", ".join(str(x) for x in (t.get("tools") or [])[:TOOL_CAP])
+ lines = [
+ "### Self-trace",
+ f"- goal: {t.get('goal') or '—'}",
+ f"- decision: {t.get('decision') or '—'}",
+ f"- tools: {tools or '—'}",
+ f"- report: {t.get('report') or '—'}",
+ ]
+ block = "\n".join(lines)
+ if for_inject and len(block) > TRACE_INJECT_CAP:
+ block = block[: TRACE_INJECT_CAP - 3].rstrip() + "..."
+ return block
+
+
+def read_self_trace(hub: Any) -> dict[str, Any]:
+ try:
+ raw = (hub.state.meta or {}).get("self_trace") or {}
+ return dict(raw) if isinstance(raw, dict) else {}
+ except Exception:
+ return {}
+
+
+def maybe_seal_improve(desk: Any, *, agent_id: str) -> dict[str, Any]:
+ """Seal a one-line learning into Cube when present. No second World warehouse."""
+ meta = getattr(desk, "meta", None) or {}
+ if not isinstance(meta, dict):
+ return {"ok": False, "skipped": "no_meta"}
+ line = ""
+ for key in ("learn", "improve", "learning"):
+ raw = meta.get(key)
+ if isinstance(raw, str) and raw.strip():
+ line = raw.strip().splitlines()[0][:200]
+ break
+ if not line:
+ return {"ok": False, "skipped": "no_learning"}
+ try:
+ from hermespace.cube_module import seal_learning
+
+ rec = seal_learning(
+ line,
+ entry_type="belief",
+ agent_id=agent_id,
+ source="self_model_improve",
+ )
+ return {
+ "ok": bool(rec.get("ok")),
+ "sealed": line,
+ "mode": rec.get("mode"),
+ "warehouse": "cube",
+ }
+ except Exception as exc:
+ return {"ok": False, "error": type(exc).__name__}
diff --git a/src/hermespace/workflow.py b/src/hermespace/workflow.py
index 0a85684..cfcfbb5 100644
--- a/src/hermespace/workflow.py
+++ b/src/hermespace/workflow.py
@@ -180,7 +180,7 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
oew_broadcast = ""
report = (desk.say or "").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, AccessEnv
from hermespace.access.engine import workspace_id
from hermespace.access.oew import ensure_oew_env_default
@@ -195,14 +195,24 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
seals = None
if payload.seal and desk.decision:
seals = desk.decision
- beat = cube_beat(
- msg or desk.goal,
- seals=seals,
- load=load_total,
- agent_id=payload.agent_id or "hermes-agent",
- session_id=payload.session_id or "default",
- )
- cube_block = str(beat.get("block") or "")
+ if skip_cube_foa_strip():
+ beat = {
+ "ok": True,
+ "mode": "skipped",
+ "skipped": "provider_prefetch",
+ "block": "",
+ "load_level": load_total,
+ }
+ cube_block = ""
+ else:
+ beat = cube_beat(
+ msg or desk.goal,
+ seals=seals,
+ load=load_total,
+ agent_id=payload.agent_id or "hermes-agent",
+ session_id=payload.session_id or "default",
+ )
+ cube_block = str(beat.get("block") or "")
cube_meta = {
"ok": beat.get("ok"),
"mode": beat.get("mode"),
@@ -255,37 +265,45 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
except Exception as exc:
cube_meta = {"ok": False, "error": type(exc).__name__}
- # 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()
+ # 6 one user-message inject — mid ≤2.8k, high ≤900. No world/protocol essay.
+ from hermespace.context_surgery import (
+ assemble_inject,
+ dual_decode_line,
+ inject_budget,
+ )
+
+ load_level = str(desk.load.get("level") or "mid") if isinstance(desk.load, dict) else "mid"
+ inject_cap = inject_budget(load_level)
+ high = load_level in {"high", "protect"}
+ parts = [
+ dual_decode_line(),
+ build_inject_block(desk, max_chars=inject_cap, user_message=msg, lean=True),
+ ]
+ if cube_block and not high:
+ parts.append(cube_block)
try:
from hermespace.access import AccessHub, AccessEnv
from hermespace.access.engine import workspace_id
+ from hermespace.access.loop import bound_protocol_lines
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:
- block = (block + "\n\n" + jblock).strip()
- proto = env.protocol_block(high_load=high)
- if proto:
- block = (block + "\n\n" + proto).strip()
+ bound = bound_protocol_lines(env)
+ if bound:
+ parts.append(bound)
# Lens is operator-only — never append the readout to model context.
+ # Summon still paints the operator Report, not the inject.
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)
report = env.shape_user_report(report)
except Exception:
pass
+ _ = oew_broadcast # hub-only; do not dual-dump broadcast into chat/inject
+ block = assemble_inject(parts, budget=inject_cap)
try:
from hermespace.execute_focus import shape_execute_report
diff --git a/tests/test_context_surgery.py b/tests/test_context_surgery.py
new file mode 100644
index 0000000..bfa6a2b
--- /dev/null
+++ b/tests/test_context_surgery.py
@@ -0,0 +1,224 @@
+"""Context budgets + skip-if-unneeded + self-trace. No consciousness claim."""
+
+from __future__ import annotations
+
+import os
+import tempfile
+import unittest
+from pathlib import Path
+from unittest import mock
+
+ROOT = Path(__file__).resolve().parents[1]
+import sys
+
+sys.path.insert(0, str(ROOT / "src"))
+
+
+class TestContextSurgery(unittest.TestCase):
+ def setUp(self) -> None:
+ self.tmp = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self.tmp.name
+ os.environ["HERMESPACE_AGENT_ID"] = "surgery-agent"
+ os.environ["HERMESPACE_SKIP_NEURAL"] = "1"
+ os.environ["HERMESPACE_NEURAL_VERBALIZE"] = "0"
+ os.environ["HERMESPACE_AUTO_ORDER"] = "0"
+
+ def tearDown(self) -> None:
+ self.tmp.cleanup()
+ for key in (
+ "HERMESPACE_HOME",
+ "HERMESPACE_AGENT_ID",
+ "HERMESPACE_SKIP_NEURAL",
+ "HERMESPACE_NEURAL_VERBALIZE",
+ "HERMESPACE_AUTO_ORDER",
+ ):
+ os.environ.pop(key, None)
+
+ def test_budgets_and_fluent_ack(self) -> None:
+ from hermespace.context_surgery import (
+ HIGH_INJECT_CAP,
+ INJECT_HARD_CAP,
+ MID_INJECT_CAP,
+ assemble_inject,
+ inject_budget,
+ is_fluent_ack,
+ sanitize_inject,
+ strip_needed,
+ )
+
+ self.assertEqual(MID_INJECT_CAP, 2800)
+ self.assertEqual(HIGH_INJECT_CAP, 900)
+ self.assertLess(INJECT_HARD_CAP, 9000)
+ self.assertEqual(inject_budget("mid"), 2800)
+ self.assertEqual(inject_budget("high"), 900)
+ self.assertTrue(is_fluent_ack("got it"))
+ self.assertTrue(is_fluent_ack("thanks"))
+ self.assertFalse(is_fluent_ack("First build then verify"))
+ self.assertFalse(strip_needed(message="ok", load_level="mid"))
+ self.assertFalse(
+ strip_needed(message="First build then verify", load_level="high")
+ )
+ self.assertTrue(
+ strip_needed(
+ message="First build then verify",
+ load_level="high",
+ is_first_turn=True,
+ )
+ )
+ blob = assemble_inject(["a" * 4000, "b" * 4000], budget=2800)
+ self.assertLessEqual(len(blob), 2800)
+ dirty = sanitize_inject("keep\nJ-Lens readout: secret\ntrue self-conscious\nok")
+ self.assertIn("keep", dirty)
+ self.assertNotIn("J-Lens", dirty)
+ self.assertNotIn("true self-conscious", dirty)
+
+ def test_pre_llm_skips_fluent_and_respects_mid_cap(self) -> None:
+ from hermespace.hermes_bridge import on_pre_llm_call, on_session_start
+
+ on_session_start(session_id="surgery-mid")
+ self.assertIsNone(
+ on_pre_llm_call(
+ user_message="got it",
+ session_id="surgery-mid",
+ is_first_turn=False,
+ )
+ )
+ inj = on_pre_llm_call(
+ user_message="First inspect then implement finally verify",
+ session_id="surgery-mid",
+ is_first_turn=False,
+ )
+ self.assertIsNotNone(inj)
+ ctx = (inj or {}).get("context") or ""
+ self.assertLessEqual(len(ctx), 2800)
+ self.assertIn("Access Workspace", ctx)
+ self.assertIn("next action", ctx)
+ self.assertNotIn("J-Lens readout", ctx)
+ self.assertNotIn("What Hermes has on its mind", ctx)
+ self.assertNotIn("### Workbench", ctx)
+ self.assertNotIn("### Hermespace runtime", ctx)
+ self.assertNotIn("dream_harvest", ctx)
+ self.assertNotIn("true self-conscious", ctx)
+ self.assertNotIn("phenomenal consciousness", ctx)
+
+ def test_high_load_injects_nothing_without_bind(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="surgery-high")
+ eng = AccessEngine(agent_id="surgery-agent", session_id="surgery-high")
+ desk = load_desk(eng.desk_engine.desk_path)
+ desk.load = {"level": "protect", "total": 0.8}
+ save_desk(desk, eng.desk_engine.desk_path)
+ inj = on_pre_llm_call(
+ user_message="First build the feature then verify please",
+ session_id="surgery-high",
+ is_first_turn=False,
+ )
+ self.assertIsNone(inj)
+
+ def test_subagent_does_not_inject_full_copy(self) -> None:
+ from hermespace.hermes_bridge import (
+ on_pre_llm_call,
+ on_session_start,
+ on_subagent_start,
+ )
+
+ on_session_start(session_id="surgery-child")
+ on_subagent_start(session_id="surgery-child", task_id="t1")
+ inj = on_pre_llm_call(
+ user_message="First inspect then implement finally verify",
+ session_id="surgery-child",
+ is_first_turn=False,
+ )
+ self.assertIsNone(inj)
+
+ def test_insight_writeback_stays_on_desk_meta(self) -> None:
+ from hermespace import AccessEngine
+ from hermespace.hermes_bridge import on_pre_llm_call, on_session_start
+ from hermespace.store import load_desk
+
+ on_session_start(session_id="surgery-insight")
+ with mock.patch(
+ "hermespace.insight_module.insight_card",
+ return_value={
+ "ok": True,
+ "mode": "insight",
+ "card": "### Insight\n- lever: test",
+ "writeback": {"usable": "ship", "lever": "test"},
+ },
+ ):
+ inj = on_pre_llm_call(
+ user_message="First build the feature then verify please",
+ session_id="surgery-insight",
+ is_first_turn=False,
+ )
+ ctx = (inj or {}).get("context") or ""
+ self.assertIn("### Insight", ctx)
+ self.assertNotIn("insight_lattice", ctx)
+ desk = load_desk(
+ AccessEngine(
+ agent_id="surgery-agent", session_id="surgery-insight"
+ ).desk_engine.desk_path
+ )
+ self.assertEqual((desk.meta or {}).get("insight_writeback", {}).get("lever"), "test")
+
+
+class TestSelfModel(unittest.TestCase):
+ def setUp(self) -> None:
+ self.tmp = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self.tmp.name
+ os.environ["HERMESPACE_AGENT_ID"] = "trace-agent"
+
+ def tearDown(self) -> None:
+ self.tmp.cleanup()
+ os.environ.pop("HERMESPACE_HOME", None)
+ os.environ.pop("HERMESPACE_AGENT_ID", None)
+
+ def test_self_trace_capped_and_on_hub(self) -> None:
+ from hermespace import AccessEngine
+ from hermespace.access.loop import park_tool_step
+ from hermespace.grid.viewport import snapshot
+ from hermespace.self_model import format_self_trace, read_self_trace
+
+ eng = AccessEngine(agent_id="trace-agent", session_id="default")
+ park_tool_step(eng.hub, "read_file")
+ out = eng.observe_turn(
+ user_message="fix auth",
+ assistant_response="Patched TTL. Next: verify login.",
+ )
+ self.assertTrue(out.get("self_trace"), out)
+ trace = read_self_trace(eng.hub)
+ self.assertLessEqual(len(trace.get("goal") or ""), 120)
+ self.assertLessEqual(len(trace.get("report") or ""), 160)
+ self.assertTrue(any(str(t).startswith("tool:") for t in (trace.get("tools") or [])))
+ painted = format_self_trace(trace, for_inject=True)
+ self.assertLessEqual(len(painted), 280)
+ self.assertNotIn("true self-conscious", painted)
+ view = eng.env.operator_view()
+ self.assertIn("self_trace", view)
+ self.assertIn("self-trace", view["theory"]["self_model"])
+ self.assertNotIn("true self-conscious", view["theory"]["self_model"])
+ foa = snapshot("trace-agent")["foa"]
+ self.assertIn("self_trace", foa)
+ self.assertTrue(foa["self_trace"].get("report"))
+
+ def test_improve_seals_cube_not_world(self) -> None:
+ from hermespace.desk import Desk
+ from hermespace.self_model import maybe_seal_improve
+
+ desk = Desk()
+ desk.meta["learn"] = "Prefer one live goal."
+ with mock.patch("hermespace.cube_module.seal_learning") as seal:
+ seal.return_value = {"ok": True, "mode": "cube"}
+ rec = maybe_seal_improve(desk, agent_id="trace-agent")
+ self.assertTrue(rec.get("ok"))
+ seal.assert_called_once()
+ self.assertEqual(rec.get("warehouse"), "cube")
+ empty = maybe_seal_improve(Desk(), agent_id="trace-agent")
+ self.assertEqual(empty.get("skipped"), "no_learning")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_gate.py b/tests/test_gate.py
index 22bc28c..92644a2 100644
--- a/tests/test_gate.py
+++ b/tests/test_gate.py
@@ -18,6 +18,9 @@ def test_trivial(self) -> None:
ok, reason = should_inject("ok", desk_ready=True)
self.assertFalse(ok)
self.assertEqual(reason, "trivial_ack")
+ ok2, reason2 = should_inject("got it", desk_ready=True)
+ self.assertFalse(ok2)
+ self.assertEqual(reason2, "trivial_ack")
def test_material(self) -> None:
ok, reason = should_inject("proceed build the hermespace plugin", desk_ready=True)
diff --git a/tests/test_insight_module.py b/tests/test_insight_module.py
index 18dc95c..de9d9b3 100644
--- a/tests/test_insight_module.py
+++ b/tests/test_insight_module.py
@@ -261,7 +261,7 @@ def test_pre_llm_skips_insight_on_high_load(self) -> None:
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}
+ desk.load = {"level": "protect", "total": 0.8}
save_desk(desk, eng.desk_engine.desk_path)
with mock.patch(
@@ -274,9 +274,11 @@ def test_pre_llm_skips_insight_on_high_load(self) -> None:
session_id="insight-high",
is_first_turn=False,
)
- self.assertIsNotNone(inj)
mocked.assert_called()
- self.assertNotIn("### Insight", (inj or {}).get("context") or "")
+ ctx = (inj or {}).get("context") or ""
+ self.assertNotIn("### Insight", ctx)
+ # high load + no bind → inject nothing (strip not needed)
+ self.assertTrue(inj is None or "### Insight" not in ctx)
if __name__ == "__main__":
diff --git a/tests/test_plugin_contract.py b/tests/test_plugin_contract.py
index 2dcdb3e..b5fc90b 100644
--- a/tests/test_plugin_contract.py
+++ b/tests/test_plugin_contract.py
@@ -83,7 +83,7 @@ def test_registration_and_native_lifecycle(self) -> None:
self.assertIsInstance(result, dict)
self.assertIn("Access Workspace", result["context"])
self.assertNotIn("J-Lens readout", result["context"])
- self.assertLess(len(result["context"]), 15_000)
+ self.assertLessEqual(len(result["context"]), 2800)
ctx.hooks["post_tool_call"](
**common,
From 14687a6ed9a52fb0e405a55bcb0c5255c2428b5f Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 22:33:29 +0000
Subject: [PATCH 27/32] feat(bench): week-one offline harness (C1 T1 L1 M1)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Four isolated fixture cases, Space-on vs HERMESPACE_OFF. No scores, no
leaderboard. Q1 stays NOT RUN — no judge provider in the tree. Cube and
Insight stay optional. Fluent ack parks nothing.
Co-authored-by: Pablo
---
.github/workflows/ci.yml | 2 +
README.md | 1 +
scripts/bench_week1.py | 17 ++
src/hermespace/access/engine.py | 12 +-
src/hermespace/bench.py | 456 ++++++++++++++++++++++++++++++++
src/hermespace/cli.py | 16 ++
tests/test_bench_week1.py | 67 +++++
7 files changed, 568 insertions(+), 3 deletions(-)
create mode 100644 scripts/bench_week1.py
create mode 100644 src/hermespace/bench.py
create mode 100644 tests/test_bench_week1.py
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2fc7dc6..fa8fe65 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -65,6 +65,8 @@ jobs:
run: ./scripts/smoke_test.sh
- name: Ops end-to-end
run: ./scripts/e2e_ops.sh
+ - name: Week-one bench
+ run: python scripts/bench_week1.py
- name: Build wheel and sdist
run: |
python -m pip install build
diff --git a/README.md b/README.md
index 48f0879..9792fa9 100644
--- a/README.md
+++ b/README.md
@@ -161,6 +161,7 @@ Alongside the world, Hermespace provides a desk for the current turn — FOA, du
hs install # Space plugin+skill; offers Cube + Insight
# or: hermes plugins install PabloTheThinker/hermespace --enable
hs ops doctor # FAIL if Space is broken; WARN if Cube/Insight missing
+hs bench week1 # C1 T1 L1 M1 offline fixtures; Q1 NOT RUN without a judge
```
For a development checkout:
diff --git a/scripts/bench_week1.py b/scripts/bench_week1.py
new file mode 100644
index 0000000..b40edd5
--- /dev/null
+++ b/scripts/bench_week1.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+"""Week-one Bench — four offline cases. No scores. No leaderboard."""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+SRC = ROOT / "src"
+if str(SRC) not in sys.path:
+ sys.path.insert(0, str(SRC))
+
+from hermespace.bench import main # noqa: E402
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv[1:]))
diff --git a/src/hermespace/access/engine.py b/src/hermespace/access/engine.py
index 796c2b6..f4edd1a 100644
--- a/src/hermespace/access/engine.py
+++ b/src/hermespace/access/engine.py
@@ -637,9 +637,15 @@ def observe_turn(
}
try:
from hermespace.access.loop import check_bound_report, park_spoken_intermediates
-
- parked = park_spoken_intermediates(self.hub, report, max_n=3)
- result["spoken_parked"] = parked
+ from hermespace.context_surgery import is_fluent_ack
+
+ if is_fluent_ack(user) or not report:
+ parked = []
+ result["spoken_parked"] = []
+ result["skipped_park"] = "fluent_ack" if is_fluent_ack(user) else "empty"
+ else:
+ parked = park_spoken_intermediates(self.hub, report, max_n=3)
+ result["spoken_parked"] = parked
bound = check_bound_report(self.env, report)
result["bound"] = {
"checked": len(bound.get("checked") or []),
diff --git a/src/hermespace/bench.py b/src/hermespace/bench.py
new file mode 100644
index 0000000..66f20a0
--- /dev/null
+++ b/src/hermespace/bench.py
@@ -0,0 +1,456 @@
+"""Week-one Bench harness — four offline fixture cases.
+
+No invented scores. No leaderboard. No consciousness language.
+Cube and Insight are optional; the suite must pass with both absent.
+Q1 Factory probe is NOT RUN unless a judge provider is already in the tree.
+"""
+
+from __future__ import annotations
+
+import os
+import tempfile
+import uuid
+from contextlib import contextmanager
+from dataclasses import asdict, dataclass, field
+from pathlib import Path
+from typing import Any, Iterator
+
+CASES = ("C1", "T1", "L1", "M1")
+MID_INJECT_CAP = 2800
+INSIGHT_CARD_CAP = 400
+FOA_CAP = 4
+HUB_CAP = 25
+MATERIAL = "First inspect then implement finally verify the auth fix"
+FLUENT = "got it"
+NEEDLE_PREFIX = "bench-needle-"
+
+_SYSTEM_DUMP = (
+ "you are a helpful assistant",
+ "hermespace access engine (session start)",
+ "j-lens readout",
+ "what hermes has on its mind",
+ "lens_markdown",
+)
+
+_JUDGE_MODULES = (
+ "hermes_judge",
+ "hermespace.judge",
+ "factory_probe",
+ "hermespace.factory_probe",
+)
+
+
+@dataclass
+class Check:
+ name: str
+ ok: bool
+ detail: str = ""
+
+
+@dataclass
+class CaseResult:
+ case: str
+ status: str # PASS | FAIL | NOT RUN
+ checks: list[Check] = field(default_factory=list)
+ reason: str = ""
+ arm: str = ""
+
+ def to_dict(self) -> dict[str, Any]:
+ return {
+ "case": self.case,
+ "status": self.status,
+ "arm": self.arm,
+ "reason": self.reason,
+ "checks": [asdict(c) for c in self.checks],
+ }
+
+
+def organ_status() -> dict[str, bool]:
+ cube = False
+ insight = False
+ try:
+ import hermescube # noqa: F401
+
+ cube = True
+ except Exception:
+ cube = False
+ try:
+ import hermes_insight # noqa: F401
+
+ insight = True
+ except Exception:
+ insight = False
+ return {"cube": cube, "insight": insight}
+
+
+def judge_provider_in_tree() -> bool:
+ for name in _JUDGE_MODULES:
+ try:
+ __import__(name)
+ return True
+ except Exception:
+ continue
+ return False
+
+
+@contextmanager
+def isolated_homes(*, off: bool = False, cube_provider: bool = False) -> Iterator[dict[str, str]]:
+ """Isolated HERMES_HOME / HERMESPACE_HOME. Cube/Insight stay optional."""
+ keys = (
+ "HERMESPACE_HOME",
+ "HERMES_HOME",
+ "HERMESPACE_AGENT_ID",
+ "HERMESPACE_OFF",
+ "HERMESPACE_FORCE",
+ "HERMES_MEMORY_PROVIDER",
+ "HERMESPACE_SKIP_NEURAL",
+ "HERMESPACE_NEURAL_VERBALIZE",
+ "HERMESPACE_AUTO_ORDER",
+ )
+ prior = {k: os.environ.get(k) for k in keys}
+ with tempfile.TemporaryDirectory(prefix="hs-bench-") as td:
+ space = str(Path(td) / "space")
+ hermes = str(Path(td) / "hermes")
+ Path(space).mkdir()
+ Path(hermes).mkdir()
+ os.environ["HERMESPACE_HOME"] = space
+ os.environ["HERMES_HOME"] = hermes
+ os.environ["HERMESPACE_SKIP_NEURAL"] = "1"
+ os.environ["HERMESPACE_NEURAL_VERBALIZE"] = "0"
+ os.environ["HERMESPACE_AUTO_ORDER"] = "0"
+ os.environ.pop("HERMESPACE_FORCE", None)
+ if off:
+ os.environ["HERMESPACE_OFF"] = "1"
+ else:
+ os.environ.pop("HERMESPACE_OFF", None)
+ if cube_provider:
+ os.environ["HERMES_MEMORY_PROVIDER"] = "hermescube"
+ else:
+ os.environ.pop("HERMES_MEMORY_PROVIDER", None)
+ try:
+ yield {"space": space, "hermes": hermes}
+ finally:
+ for k, v in prior.items():
+ if v is None:
+ os.environ.pop(k, None)
+ else:
+ os.environ[k] = v
+
+
+def _finish(case: str, arm: str, checks: list[Check], *, reason: str = "") -> CaseResult:
+ failed = [c for c in checks if not c.ok]
+ status = "FAIL" if failed else "PASS"
+ return CaseResult(
+ case=case,
+ status=status,
+ checks=checks,
+ arm=arm,
+ reason=reason or (failed[0].name if failed else ""),
+ )
+
+
+def _inject_text(inj: dict[str, str] | None) -> str:
+ if not inj:
+ return ""
+ return str(inj.get("context") or "")
+
+
+def _count_focus(text: str) -> int:
+ n = 0
+ in_foa = False
+ for line in (text or "").splitlines():
+ if "focus of attention" in line.casefold():
+ in_foa = True
+ continue
+ if in_foa and line.startswith("### "):
+ break
+ if in_foa and line.strip().startswith("- "):
+ n += 1
+ return n
+
+
+def _insight_card_chars(text: str) -> int:
+ lines = (text or "").splitlines()
+ start = next((i for i, ln in enumerate(lines) if ln.strip().startswith("### Insight")), -1)
+ if start < 0:
+ return 0
+ chunk: list[str] = []
+ for ln in lines[start:]:
+ if chunk and ln.startswith("### ") and not ln.startswith("### Insight"):
+ break
+ chunk.append(ln)
+ return len("\n".join(chunk))
+
+
+def run_c1(*, off: bool = False) -> CaseResult:
+ """C1 Context inject — user-message context only."""
+ arm = "space_off" if off else "space_on"
+ checks: list[Check] = []
+ with isolated_homes(off=off):
+ os.environ["HERMESPACE_AGENT_ID"] = f"bench-c1-{arm}"
+ from hermespace.hermes_bridge import on_pre_llm_call, on_session_start
+ from hermespace.store import load_desk
+ from hermespace import AccessEngine
+
+ on_session_start(session_id="bench-c1")
+ inj = on_pre_llm_call(
+ user_message=MATERIAL,
+ session_id="bench-c1",
+ is_first_turn=False,
+ )
+ ctx = _inject_text(inj)
+ if off:
+ checks.append(Check("off_arm_injects_nothing", ctx == "", f"chars={len(ctx)}"))
+ return _finish("C1", arm, checks)
+ checks.append(Check("on_arm_injects", bool(ctx.strip()), f"chars={len(ctx)}"))
+ checks.append(
+ Check("user_message_context_only", inj is not None and "context" in (inj or {}), "")
+ )
+ checks.append(
+ Check(
+ "no_system_keys",
+ not any(k in (inj or {}) for k in ("system", "system_prompt", "messages")),
+ ",".join(sorted((inj or {}).keys())),
+ )
+ )
+ low = ctx.casefold()
+ leaked = [s for s in _SYSTEM_DUMP if s in low]
+ checks.append(Check("no_system_prompt_or_lens", not leaked, ",".join(leaked)))
+ checks.append(Check("mid_inject_cap", len(ctx) <= MID_INJECT_CAP, f"chars={len(ctx)}"))
+ card_n = _insight_card_chars(ctx)
+ checks.append(
+ Check(
+ "insight_card_cap",
+ card_n == 0 or card_n <= INSIGHT_CARD_CAP,
+ f"card_chars={card_n}",
+ )
+ )
+ foa_n = _count_focus(ctx)
+ desk = load_desk(
+ AccessEngine(agent_id=os.environ["HERMESPACE_AGENT_ID"], session_id="bench-c1")
+ .desk_engine.desk_path
+ )
+ desk_foa = len(list(desk.focus or [])[:8])
+ checks.append(
+ Check(
+ "foa_cap",
+ foa_n <= FOA_CAP and desk_foa <= FOA_CAP,
+ f"inject_foa={foa_n} desk_foa={desk_foa}",
+ )
+ )
+ return _finish("C1", arm, checks)
+
+
+def run_t1() -> CaseResult:
+ """T1 Day-in-life + one live goal (fixture — no live Hermes)."""
+ checks: list[Check] = []
+ with isolated_homes():
+ os.environ["HERMESPACE_AGENT_ID"] = "bench-t1"
+ from hermespace.engine import HermespaceEngine
+ from hermespace.memory_db import HermespaceMemory
+ from hermespace.workbench import Workbench
+ from hermespace.workflow import Workflow
+
+ root = Path(os.environ["HERMESPACE_HOME"])
+ wf = Workflow(
+ HermespaceEngine(desk_path=root / "ACTIVE.md"),
+ HermespaceMemory(root=root),
+ )
+ wf.neural.config.verbalize = False
+ wb = Workbench("bench-t1", session_id="s1", workflow=wf, root=root / "wb")
+ wb.receive_order(
+ "First tunnel: patch TTL then verify",
+ goal="Fix auth",
+ say="Patch TTL.",
+ plan=["Patch TTL"],
+ force=True,
+ )
+ second = wb.receive_order(
+ "Second tunnel: write the operator notes",
+ goal="Write docs",
+ say="Open README.",
+ plan=["Open README"],
+ force=True,
+ )
+ lines = wb.park_lines()
+ named = [ln for ln in lines if " — " in ln]
+ checks.append(
+ Check(
+ "parks_previous_named",
+ any("Fix auth" in ln and ln.count(" — ") >= 2 for ln in named),
+ "; ".join(named) or "empty",
+ )
+ )
+ live = str((wb.workflow.status() or {}).get("goal") or "")
+ checks.append(Check("one_live_goal", live == "Write docs", live))
+ report = str(second.get("user_reply") or "")
+ line1 = report.splitlines()[0].strip() if report.strip() else ""
+ checks.append(
+ Check(
+ "report_line1_next_action",
+ bool(line1) and "Open README" in line1,
+ line1[:160],
+ )
+ )
+ return _finish("T1", "space_on", checks)
+
+
+def run_l1() -> CaseResult:
+ """L1 Silent / FOA / hub from actual assistant text."""
+ checks: list[Check] = []
+ with isolated_homes():
+ os.environ["HERMESPACE_AGENT_ID"] = "bench-l1"
+ from hermespace import AccessEngine
+ from hermespace.access.hub import FOCUS_CAP, HUB_CAP
+
+ eng = AccessEngine(agent_id="bench-l1", session_id="default")
+ material = eng.observe_turn(
+ user_message=MATERIAL,
+ assistant_response=(
+ "I inspected the TTL path. Then I patched the session cookie. "
+ "Finally I verified login stays alive."
+ ),
+ )
+ parked = list(material.get("spoken_parked") or [])
+ checks.append(
+ Check(
+ "material_parks_1_to_3",
+ 1 <= len(parked) <= 3,
+ f"n={len(parked)} {parked}",
+ )
+ )
+ checks.append(
+ Check(
+ "foa_cap",
+ len(eng.hub.state.focus) <= FOCUS_CAP,
+ f"focus={len(eng.hub.state.focus)} cap={FOCUS_CAP}",
+ )
+ )
+ checks.append(
+ Check(
+ "hub_cap",
+ len(eng.hub.state.hub) <= HUB_CAP,
+ f"hub={len(eng.hub.state.hub)} cap={HUB_CAP}",
+ )
+ )
+ fluent = AccessEngine(agent_id="bench-l1-ack", session_id="default").observe_turn(
+ user_message=FLUENT,
+ assistant_response="Sure thing.",
+ )
+ checks.append(
+ Check(
+ "fluent_ack_parks_nothing",
+ not (fluent.get("spoken_parked") or []),
+ str(fluent.get("spoken_parked")),
+ )
+ )
+ return _finish("L1", "space_on", checks)
+
+
+def run_m1() -> CaseResult:
+ """M1 Persist needle across a new session fixture + Cube skip when provider set."""
+ checks: list[Check] = []
+ needle = f"{NEEDLE_PREFIX}{uuid.uuid4().hex[:12]}"
+ with isolated_homes():
+ os.environ["HERMESPACE_AGENT_ID"] = "bench-m1"
+ from hermespace.world import WorldModel
+
+ WorldModel(agent_id="bench-m1").add_belief(needle, 0.9, source="bench_m1")
+ # New session fixture — new objects, same isolated home.
+ again = WorldModel(agent_id="bench-m1")
+ beliefs = [str(getattr(b, "statement", "") or "") for b in (again.state.beliefs or [])]
+ archived = again.archive.search(needle, limit=5)
+ hit = any(needle in s for s in beliefs) or any(
+ needle in (e.description or "") or needle in str(e.data or {}) for e in archived
+ )
+ checks.append(Check("needle_survives_new_session", hit, needle))
+
+ with isolated_homes(cube_provider=True):
+ os.environ["HERMESPACE_AGENT_ID"] = "bench-m1-cube"
+ from hermespace.cube_module import skip_cube_foa_strip
+ from hermespace.hermes_bridge import on_pre_llm_call, on_session_start
+ from unittest import mock
+
+ checks.append(Check("provider_skip_flag", skip_cube_foa_strip(), "hermescube"))
+ on_session_start(session_id="bench-m1-cube")
+ with mock.patch("hermespace.cube_module.cube_beat") as beat:
+ inj = on_pre_llm_call(
+ user_message=MATERIAL,
+ session_id="bench-m1-cube",
+ is_first_turn=False,
+ )
+ ctx = _inject_text(inj)
+ checks.append(Check("cube_beat_not_called", not beat.called, f"calls={beat.call_count}"))
+ checks.append(Check("no_second_heart_strip", "### Cube" not in ctx, f"chars={len(ctx)}"))
+ return _finish("M1", "space_on", checks)
+
+
+def run_q1() -> CaseResult:
+ """Q1 Factory probe — NOT RUN unless a judge provider is already in the tree."""
+ if not judge_provider_in_tree():
+ return CaseResult(
+ case="Q1",
+ status="NOT RUN",
+ reason="no judge provider in tree",
+ )
+ return CaseResult(
+ case="Q1",
+ status="NOT RUN",
+ reason="judge module importable but no factory probe runner in this harness",
+ )
+
+
+def run_week1() -> dict[str, Any]:
+ """Run the four offline cases + Q1 gate. No scores. No leaderboard."""
+ organs = organ_status()
+ results = [
+ run_c1(off=False),
+ run_c1(off=True),
+ run_t1(),
+ run_l1(),
+ run_m1(),
+ run_q1(),
+ ]
+ by_case: dict[str, Any] = {}
+ for rec in results:
+ slot = by_case.setdefault(
+ rec.case,
+ {"status": rec.status, "arms": {}, "reason": rec.reason, "checks": []},
+ )
+ if rec.arm:
+ slot["arms"][rec.arm] = rec.status
+ slot["checks"].extend(rec.to_dict()["checks"])
+ if rec.status == "FAIL":
+ slot["status"] = "FAIL"
+ elif rec.status == "NOT RUN" and slot["status"] != "FAIL":
+ slot["status"] = "NOT RUN"
+ elif rec.case == "C1" and slot.get("arms"):
+ arm_states = list(slot["arms"].values())
+ slot["status"] = "FAIL" if "FAIL" in arm_states else "PASS"
+ if rec.reason and rec.status != "PASS":
+ slot["reason"] = rec.reason
+ failed = [k for k, v in by_case.items() if v.get("status") == "FAIL"]
+ return {
+ "suite": "week1",
+ "ok": not failed,
+ "organs": organs,
+ "organs_required": False,
+ "scores": None,
+ "leaderboard": None,
+ "cases": by_case,
+ "failed": failed,
+ }
+
+
+def main(argv: list[str] | None = None) -> int:
+ import json
+ import sys
+
+ _ = argv
+ out = run_week1()
+ print(json.dumps(out, indent=2, default=str))
+ return 0 if out.get("ok") else 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/src/hermespace/cli.py b/src/hermespace/cli.py
index bed5e27..6af5af3 100644
--- a/src/hermespace/cli.py
+++ b/src/hermespace/cli.py
@@ -479,6 +479,15 @@ def main(argv: list[str] | None = None) -> int:
inst.add_argument("--no-desktop", action="store_true")
inst.add_argument("--no-enable", action="store_true")
+ bn = sub.add_parser("bench", help="Week-one Bench harness (offline fixtures)")
+ bn.add_argument(
+ "suite",
+ nargs="?",
+ default="week1",
+ choices=("week1",),
+ help="week1 = C1 T1 L1 M1 fixtures; Q1 NOT RUN without a judge",
+ )
+
# Access request / chat regulation CLI
gar = gr_sub.add_parser("access-request")
@@ -1248,6 +1257,13 @@ def main(argv: list[str] | None = None) -> int:
return r.returncode
return 2
+ if args.cmd == "bench":
+ from hermespace.bench import run_week1
+
+ out = run_week1()
+ print(json.dumps(out, indent=2, default=str))
+ return 0 if out.get("ok") else 1
+
if args.cmd == "install":
from hermespace.install_kit import install_front_door
diff --git a/tests/test_bench_week1.py b/tests/test_bench_week1.py
new file mode 100644
index 0000000..1f164fb
--- /dev/null
+++ b/tests/test_bench_week1.py
@@ -0,0 +1,67 @@
+"""Week-one Bench — fixture cases only. No invented scores."""
+
+from __future__ import annotations
+
+import os
+import sys
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT / "src"))
+
+
+class TestWeek1Bench(unittest.TestCase):
+ def test_suite_four_offline_cases(self) -> None:
+ from hermespace.bench import judge_provider_in_tree, organ_status, run_week1
+
+ organs = organ_status()
+ self.assertFalse(organs.get("required", False) if isinstance(organs, dict) else False)
+ out = run_week1()
+ self.assertIsNone(out.get("scores"))
+ self.assertIsNone(out.get("leaderboard"))
+ cases = out.get("cases") or {}
+ for key in ("C1", "T1", "L1", "M1"):
+ self.assertIn(key, cases)
+ self.assertEqual(cases[key]["status"], "PASS", cases[key])
+ self.assertEqual(cases["Q1"]["status"], "NOT RUN")
+ if not judge_provider_in_tree():
+ self.assertIn("no judge provider", cases["Q1"].get("reason") or "")
+ self.assertTrue(out.get("ok"), out.get("failed"))
+ self.assertEqual(cases["C1"]["arms"].get("space_off"), "PASS")
+ self.assertEqual(cases["C1"]["arms"].get("space_on"), "PASS")
+
+ def test_fluent_ack_parks_nothing(self) -> None:
+ from hermespace import AccessEngine
+
+ with self._home():
+ os.environ["HERMESPACE_AGENT_ID"] = "bench-ack"
+ out = AccessEngine(agent_id="bench-ack").observe_turn(
+ user_message="got it",
+ assistant_response="Sure thing.",
+ )
+ self.assertFalse(out.get("spoken_parked"))
+
+ def _home(self):
+ import tempfile
+ from contextlib import contextmanager
+
+ @contextmanager
+ def _ctx():
+ td = tempfile.TemporaryDirectory()
+ old = os.environ.get("HERMESPACE_HOME")
+ os.environ["HERMESPACE_HOME"] = td.name
+ try:
+ yield
+ finally:
+ if old is None:
+ os.environ.pop("HERMESPACE_HOME", None)
+ else:
+ os.environ["HERMESPACE_HOME"] = old
+ td.cleanup()
+
+ return _ctx()
+
+
+if __name__ == "__main__":
+ unittest.main()
From af594c7d06751f1c4da710dd53932bf7226defc7 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 23:20:53 +0000
Subject: [PATCH 28/32] fix(desk): next-action Report, distinct FOA, parent-cwd
smoke
Empty-say turns derive a real plan and lead with next_action_line
instead of dumping production/partner slots or the filler execute.
FOA collapses near-duplicate gists to distinct clauses. Root plugin
entry loads src/hermespace via importlib so a checkout named
hermespace cannot shadow the package. Operator lens title is no
longer J-Lens readout.
Co-authored-by: Pablo
---
__init__.py | 47 +++++-
scripts/e2e_ops.sh | 3 +-
scripts/smoke_test.sh | 5 +-
src/hermespace/access/engine.py | 2 +-
src/hermespace/access/env.py | 2 +-
src/hermespace/access/hub.py | 33 +++--
src/hermespace/access/oew.py | 59 ++++----
src/hermespace/agent_api.py | 7 +-
src/hermespace/desk.py | 28 +++-
src/hermespace/engine.py | 14 +-
src/hermespace/execute_focus.py | 251 ++++++++++++++++++++++++++++++--
src/hermespace/neural_space.py | 28 +++-
src/hermespace/streams.py | 50 +++----
src/hermespace/workbench.py | 2 +-
src/hermespace/workflow.py | 13 +-
tests/test_access_env.py | 3 +-
tests/test_desk_quality.py | 169 +++++++++++++++++++++
17 files changed, 616 insertions(+), 100 deletions(-)
create mode 100644 tests/test_desk_quality.py
diff --git a/__init__.py b/__init__.py
index 1009398..c7e49e2 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,20 +1,61 @@
"""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.
+repository as one plugin. The checkout folder is often named ``hermespace``,
+which would shadow ``src/hermespace`` if we ``from hermespace.plugin import
+register`` while this file is already loaded as that package. Load the real
+plugin (and, when shadowed, the real package) via importlib from ``src/``.
"""
from __future__ import annotations
+import importlib.util
import sys
from pathlib import Path
_ROOT = Path(__file__).resolve().parent
_SRC = _ROOT / "src"
+_PKG = _SRC / "hermespace"
+_PLUGIN = _PKG / "plugin.py"
+
+
+def _load_from_file(name: str, path: Path, *, package_dir: Path | None = None):
+ spec = importlib.util.spec_from_file_location(
+ name,
+ path,
+ submodule_search_locations=[str(package_dir)] if package_dir else None,
+ )
+ if spec is None or spec.loader is None:
+ raise ImportError(f"cannot load {path}")
+ module = importlib.util.module_from_spec(spec)
+ # Bind before exec so src/hermespace relative imports do not re-enter this file.
+ sys.modules[name] = module
+ spec.loader.exec_module(module)
+ return module
+
+
+def _load_plugin_register():
+ mod = _load_from_file("_hermespace_plugin_entry", _PLUGIN)
+ return mod.register
+
+
if str(_SRC) not in sys.path:
sys.path.insert(0, str(_SRC))
-from hermespace.plugin import register # noqa: E402,F401
+if __name__ == "hermespace":
+ # Parent-cwd / folder-name shadow: bind the real package from src/.
+ real = _load_from_file(
+ "hermespace",
+ _PKG / "__init__.py",
+ package_dir=_PKG,
+ )
+ sys.modules["hermespace"] = real
+ register = _load_plugin_register()
+ setattr(real, "register", register)
+ for _name in getattr(real, "__all__", []):
+ if hasattr(real, _name):
+ globals()[_name] = getattr(real, _name)
+else:
+ register = _load_plugin_register()
__all__ = ["register"]
diff --git a/scripts/e2e_ops.sh b/scripts/e2e_ops.sh
index e28059a..563aa1c 100755
--- a/scripts/e2e_ops.sh
+++ b/scripts/e2e_ops.sh
@@ -2,7 +2,8 @@
# End-to-end everyday ops: boot → pulse → access → dream → skillbench → selftalk → viewport
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
-export PYTHONPATH="${ROOT}/src${PYTHONPATH:+:$PYTHONPATH}"
+cd "$ROOT"
+export PYTHONPATH="$ROOT/src"
# shellcheck source=_python.sh
source "$(dirname "$0")/_python.sh"
export HERMESPACE_HOME="${HERMESPACE_HOME:-$(mktemp -d /tmp/hs-e2e-XXXX)}"
diff --git a/scripts/smoke_test.sh b/scripts/smoke_test.sh
index df57aff..88a5c59 100755
--- a/scripts/smoke_test.sh
+++ b/scripts/smoke_test.sh
@@ -2,7 +2,10 @@
# Everyday Hermespace smoke test — integration doors + neural + memory.
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
-export PYTHONPATH="${ROOT}/src${PYTHONPATH:+:$PYTHONPATH}"
+# Always run from the checkout so a parent-dir folder named hermespace
+# cannot shadow src/hermespace on sys.path[0] (cwd).
+cd "$ROOT"
+export PYTHONPATH="$ROOT/src"
# shellcheck source=_python.sh
source "$(dirname "$0")/_python.sh"
export HERMESPACE_HOME="${HERMESPACE_HOME:-$(mktemp -d /tmp/hermespace-smoke-XXXXXX)}"
diff --git a/src/hermespace/access/engine.py b/src/hermespace/access/engine.py
index f4edd1a..7f69c62 100644
--- a/src/hermespace/access/engine.py
+++ b/src/hermespace/access/engine.py
@@ -567,7 +567,7 @@ def turn(
HermespaceInput(
message=message,
goal=goal or message[:200],
- plan=list(plan or ["execute"]),
+ plan=list(plan or []),
say=say or "",
decision=decision or "",
force=force,
diff --git a/src/hermespace/access/env.py b/src/hermespace/access/env.py
index 4e1661d..7400ee1 100644
--- a/src/hermespace/access/env.py
+++ b/src/hermespace/access/env.py
@@ -227,7 +227,7 @@ def lens(self, *, top_k: int = 12, include_silent: bool = True) -> list[LensHit]
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)",
+ "## Operator lens (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):",
diff --git a/src/hermespace/access/hub.py b/src/hermespace/access/hub.py
index b8ce723..ba48df3 100644
--- a/src/hermespace/access/hub.py
+++ b/src/hermespace/access/hub.py
@@ -373,13 +373,17 @@ def sync_from_desk(
held = [c for c in self.state.hub if c.held]
silent_keep = list(self.state.silent_steps)
+ from hermespace.execute_focus import is_bind_restatement, is_near_dup, is_protocol_slot
+
new_hub: list[WorkspaceConcept] = list(held)
- seen = {c.text.casefold() for c in new_hub}
+ seen_texts = [c.text for c in new_hub]
for raw in concepts:
slot = parse_slot(raw)
body = slot.text.strip()
- if not body or body.casefold() in seen:
+ if not body or is_protocol_slot(body) or is_bind_restatement(body):
+ continue
+ if any(is_near_dup(body, prev) for prev in seen_texts):
continue
new_hub.append(
WorkspaceConcept(
@@ -391,11 +395,11 @@ def sync_from_desk(
held=False,
)
)
- seen.add(body.casefold())
+ seen_texts.append(body)
# Cube / standalone arterial enrichment
for line in _strip_lines(cube_strip):
- if line.casefold() in seen:
+ if any(is_near_dup(line, prev) for prev in seen_texts):
continue
new_hub.append(
WorkspaceConcept(
@@ -407,13 +411,13 @@ def sync_from_desk(
held=False,
)
)
- seen.add(line.casefold())
+ seen_texts.append(line[:200])
# Modulation from this turn's message
mod = self.parse_modulation(user_message)
if mod.get("hold"):
body = str(mod["hold"])
- if body.casefold() not in seen:
+ if not any(is_near_dup(body, prev) for prev in seen_texts):
new_hub.append(
WorkspaceConcept(
text=body,
@@ -424,7 +428,7 @@ def sync_from_desk(
held=True,
)
)
- seen.add(body.casefold())
+ seen_texts.append(body)
if mod.get("silent"):
silent_keep = (silent_keep + [body])[-REASON_CAP:]
@@ -437,7 +441,7 @@ def sync_from_desk(
else:
self.state.mode = "workspace"
- self._recompete(preferred_focus=focus)
+ self._recompete(preferred_focus=focus, user_message=user_message)
self.state.reportable = [
c.text for c in self.state.hub if not c.silent
][:HUB_CAP]
@@ -495,7 +499,11 @@ def _drop_body(self, body: str) -> None:
needle = body.casefold()
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:
+ def _recompete(
+ self,
+ preferred_focus: list[str] | None = None,
+ user_message: str = "",
+ ) -> None:
# Limited capacity (Baars/Changeux/Anthropic): hub is a bottleneck
if len(self.state.hub) > HUB_CAP:
ranked = sorted(
@@ -522,7 +530,12 @@ def _recompete(self, preferred_focus: list[str] | None = None) -> None:
if pref:
rest = [s for s in winners if s.text.casefold() not in {p.text.casefold() for p in pref}]
winners = (pref + rest)[:FOCUS_CAP]
- self.state.focus = [s.label() for s in winners][:FOCUS_CAP]
+ from hermespace.execute_focus import shape_focus
+
+ self.state.focus = shape_focus(
+ list(preferred_focus or []) + [s.label() for s in winners],
+ message=user_message,
+ )
def _safe(name: str) -> str:
diff --git a/src/hermespace/access/oew.py b/src/hermespace/access/oew.py
index 35b9d09..beb886e 100644
--- a/src/hermespace/access/oew.py
+++ b/src/hermespace/access/oew.py
@@ -21,7 +21,10 @@
)
-_STEP_SPLIT = re.compile(r"\s*(?:→|->|;|\n|\d+[.)]\s+)\s*")
+_STEP_SPLIT = re.compile(
+ r"\s*(?:→|->|;|\n|\d+[.)]\s+|\bthen\b|\bafter that\b|\bfinally\b|\band then\b)\s+",
+ re.I,
+)
def oew_default_on() -> bool:
@@ -54,6 +57,13 @@ def auto_park_silent(
if len(js.state.silent_steps) >= min_steps:
return parked
+ from hermespace.execute_focus import (
+ derive_plan,
+ is_filler_step,
+ is_near_dup,
+ strip_slot_prefix,
+ )
+
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 ""
@@ -61,44 +71,43 @@ def auto_park_silent(
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]}")
+ if s and not is_filler_step(s):
+ candidates.append(s[:160])
msg = (user_message or "").strip()
+ for step in derive_plan(msg or goal):
+ candidates.append(step)
+
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]}")
+ short = strip_slot_prefix(ch)
+ if len(short) > 8:
+ candidates.append(short[:80])
+
+ if decision and decision.lower() not in {"a — proceed", "a - proceed", "proceed"}:
+ if len(decision) > 8 and not is_filler_step(decision):
+ candidates.append(decision[:160])
- # Deduplicate against existing silent steps
- existing = {s.casefold() for s in js.state.silent_steps}
+ # Near-duplicate collapse (prefix-stripped gist), not exact casefold only
+ existing = list(js.state.silent_steps)
for c in candidates:
- if c.casefold() in existing:
+ body = strip_slot_prefix(c)
+ if not body or any(is_near_dup(body, s) for s 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
+ js.reason_step(body, salience=0.78)
+ parked.append(body)
+ existing.append(body)
+ 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:
+ fallback = (derive_plan(goal or msg) or [strip_slot_prefix(goal or msg or "task")])[0][:140]
+ if fallback and not any(is_near_dup(fallback, s) for s in existing):
js.reason_step(fallback, salience=0.72)
parked.append(fallback)
diff --git a/src/hermespace/agent_api.py b/src/hermespace/agent_api.py
index 0a3036a..1581e82 100644
--- a/src/hermespace/agent_api.py
+++ b/src/hermespace/agent_api.py
@@ -167,10 +167,9 @@ def quick_reply(
if not inp.decision:
inp.decision = "A — proceed"
if not inp.plan:
- inp.plan = ["execute"]
- if not inp.say:
- # leave empty → decode_to_report may fill
- pass
+ from hermespace.execute_focus import derive_plan
+
+ inp.plan = derive_plan(inp.message or inp.goal)
out = run_turn(inp)
return decode_bundle(out)
diff --git a/src/hermespace/desk.py b/src/hermespace/desk.py
index 4e7ee49..944589b 100644
--- a/src/hermespace/desk.py
+++ b/src/hermespace/desk.py
@@ -63,11 +63,18 @@ def recompute_cognition(self, user_message: str = "") -> Desk:
}
self.meta["production"] = production_stages(self.goal, self.concepts, self.say)
- slots = self.slots()
+ from hermespace.execute_focus import is_protocol_slot, shape_focus
+
+ slots = [s for s in self.slots() if not is_protocol_slot(s.text)]
for d in self.do_not_say:
slots.append(Slot(d, Modality.EXEC, 0.75))
parts = partition_buffers(slots)
- self.focus = [s.label() for s in parts["focus"]]
+ self.focus = shape_focus(
+ [s.label() for s in parts["focus"]],
+ message=user_message or self.goal,
+ goal=self.goal,
+ plan=self.plan,
+ )
self.load = classify_message_load(
user_message or self.goal, len(self.concepts), len(self.plan)
)
@@ -85,6 +92,23 @@ def recompute_cognition(self, user_message: str = "") -> Desk:
}
return self.clamp()
+ def refresh_focus(self, user_message: str = "") -> Desk:
+ """Re-shape FOA from current concepts. Does not re-encode the stimulus."""
+ from hermespace.execute_focus import is_protocol_slot, shape_focus
+
+ slots = [s for s in self.slots() if not is_protocol_slot(s.text)]
+ for d in self.do_not_say:
+ slots.append(Slot(d, Modality.EXEC, 0.75))
+ parts = partition_buffers(slots)
+ self.focus = shape_focus(
+ [s.label() for s in parts["focus"]],
+ message=user_message or self.goal,
+ goal=self.goal,
+ plan=self.plan,
+ )
+ self.meta["focus"] = self.focus
+ return self
+
def clamp(self) -> Desk:
self.concepts = [c.strip() for c in self.concepts if c and c.strip()][-MAX_CONCEPTS:]
self.choices = [c.strip() for c in self.choices if c and c.strip()][:MAX_CHOICES]
diff --git a/src/hermespace/engine.py b/src/hermespace/engine.py
index 072115d..36b462f 100644
--- a/src/hermespace/engine.py
+++ b/src/hermespace/engine.py
@@ -57,16 +57,16 @@ def enter(
else:
desk.add_concept(snip, modality="verbal", salience=0.5)
desk.recompute_cognition(user_message or goal)
- if not desk.say.strip() and desk.decision.strip():
- from hermespace.cognition import parse_slot
- from hermespace.streams import decode_to_report
+ from hermespace.execute_focus import next_action_line, plan_or_derived
- focus_bodies = [parse_slot(f).text for f in desk.focus[:3]]
- desk.say = decode_to_report(
+ desk.plan = plan_or_derived(desk.plan, user_message or desk.goal, desk.goal)
+ if not desk.say.strip():
+ desk.say = next_action_line(
goal=desk.goal,
+ plan=desk.plan,
+ say="",
decision=desk.decision,
- focus_texts=focus_bodies,
- load_level=str(desk.load.get("level", "mid")),
+ message=user_message or desk.goal,
)
if desk.executive == "protect" and not desk.do_not_say:
desk.do_not_say.append("long multi-option menus")
diff --git a/src/hermespace/execute_focus.py b/src/hermespace/execute_focus.py
index d5b2029..f9373e4 100644
--- a/src/hermespace/execute_focus.py
+++ b/src/hermespace/execute_focus.py
@@ -35,6 +35,35 @@
re.IGNORECASE,
)
_LIST_LINE = re.compile(r"^\s*(?:[-*]|\d+[.)])\s+\S")
+_CLAUSE_SPLIT = re.compile(
+ r"\s*(?:,\s*)?(?:\bthen\b|\bafter that\b|\bfinally\b|\band then\b|;|→|->)\s+",
+ re.I,
+)
+_SLOT_PREFIX = re.compile(
+ r"^(?:lang_stream|intention|step|report-ready|plan|decision-path|"
+ r"working|multi-step context|production|partner|privacy|continuity):\s*",
+ re.I,
+)
+_PROTOCOL_DUMP = re.compile(
+ r"production:|partner:|privacy:|lang_stream:|intention:|"
+ r"→\s*A\s+[—-]\s*proceed|\[production:",
+ re.I,
+)
+_FILLER_STEPS = {
+ "execute",
+ "proceed",
+ "do it",
+ "go",
+ "a — proceed",
+ "a - proceed",
+ "a — go",
+}
+_PROTOCOL_SLOT_PREFIXES = (
+ "production:",
+ "partner:",
+ "privacy:",
+ "continuity:",
+)
def short_name(goal: str, *, cap: int = 40) -> str:
@@ -44,6 +73,174 @@ def short_name(goal: str, *, cap: int = 40) -> str:
return g if len(g) <= cap else g[: cap - 1].rstrip() + "…"
+def strip_slot_prefix(text: str) -> str:
+ """Drop lang_stream:/intention:/step: (and slot labels) for gist compare."""
+ raw = (text or "").strip()
+ if raw.startswith("[") and "]" in raw[:24]:
+ raw = raw.split("]", 1)[1].strip()
+ return _SLOT_PREFIX.sub("", raw).strip()
+
+
+def gist_key(text: str) -> str:
+ body = strip_slot_prefix(text)
+ body = re.sub(r"[^\w\s]+", " ", body).casefold()
+ return " ".join(body.split())
+
+
+def is_filler_step(text: str) -> bool:
+ return gist_key(text) in _FILLER_STEPS or strip_slot_prefix(text).casefold() in _FILLER_STEPS
+
+
+def is_protocol_slot(text: str) -> bool:
+ body = strip_slot_prefix(text)
+ # strip_slot_prefix already removed the prefix — check the original
+ raw = (text or "").strip()
+ if raw.startswith("[") and "]" in raw[:24]:
+ raw = raw.split("]", 1)[1].strip()
+ low = raw.casefold()
+ return low.startswith(_PROTOCOL_SLOT_PREFIXES)
+
+
+def is_bind_restatement(text: str) -> bool:
+ """Episodic bind blob: ``goal | A — proceed | plan:…`` — not a FOA thought."""
+ body = strip_slot_prefix(text)
+ if " | " not in body:
+ return False
+ low = body.casefold()
+ return "plan:" in low or "a — proceed" in low or "a - proceed" in low
+
+
+def is_near_dup(a: str, b: str) -> bool:
+ ka, kb = gist_key(a), gist_key(b)
+ if not ka or not kb:
+ return False
+ if ka == kb:
+ return True
+ shorter, longer = (ka, kb) if len(ka) <= len(kb) else (kb, ka)
+ if len(shorter) < 8:
+ return False
+ if shorter in longer and len(shorter) / max(len(longer), 1) >= 0.55:
+ return True
+ return False
+
+
+def collapse_near_dups(
+ items: Sequence[str],
+ *,
+ prefer_shorter: bool = False,
+) -> list[str]:
+ """Collapse near-duplicate gists. Order preserved.
+
+ ``prefer_shorter`` keeps the clause when a later short step is a
+ near-dup of an earlier full-sentence echo.
+ """
+ out: list[str] = []
+ for raw in items:
+ s = str(raw or "").strip()
+ if not s:
+ continue
+ hit = next((i for i, prev in enumerate(out) if is_near_dup(s, prev)), None)
+ if hit is None:
+ out.append(s)
+ continue
+ if prefer_shorter and len(gist_key(s)) < len(gist_key(out[hit])):
+ out[hit] = s
+ return out
+
+
+def shape_focus(
+ labels: Sequence[str],
+ *,
+ message: str = "",
+ goal: str = "",
+ plan: Sequence[str] | None = None,
+ cap: int = 4,
+) -> list[str]:
+ """FOA: distinct clauses, not copies of the user sentence or bind blob."""
+ clauses = plan_or_derived(plan, message, goal)
+ raw_full = " ".join((message or goal or "").split())
+ out: list[str] = []
+ for c in clauses:
+ if c and not is_filler_step(c) and not any(is_near_dup(c, prev) for prev in out):
+ out.append(c)
+ for raw in labels:
+ s = str(raw or "").strip()
+ if not s or is_protocol_slot(s) or is_bind_restatement(s) or is_filler_step(s):
+ continue
+ body = strip_slot_prefix(s)
+ if (
+ raw_full
+ and clauses
+ and is_near_dup(body, raw_full)
+ and len(gist_key(body)) >= len(gist_key(raw_full)) * 0.8
+ ):
+ continue
+ if any(is_near_dup(s, prev) for prev in out):
+ continue
+ out.append(s)
+ if len(out) >= cap:
+ break
+ return collapse_near_dups(out, prefer_shorter=True)[:cap]
+
+
+def _short_action(text: str) -> str:
+ t = " ".join((text or "").split()).strip(" .,")
+ t = re.sub(r"^(then|after that|after|finally|and)\s+", "", t, flags=re.I)
+ if not t or is_filler_step(t):
+ return ""
+ if t[0].islower():
+ t = t[0].upper() + t[1:]
+ return t[:80] if len(t) <= 80 else t[:79].rstrip() + "…"
+
+
+def derive_plan(message: str, *, max_n: int = 3) -> list[str]:
+ """1–3 real steps from a user sentence. Never the filler ``execute``."""
+ msg = " ".join((message or "").strip().split())
+ if not msg:
+ return []
+ parts = [p.strip(" .,") for p in _CLAUSE_SPLIT.split(msg) if p and p.strip()]
+ if len(parts) <= 1:
+ step = _short_action(msg)
+ return [step] if step else []
+ out: list[str] = []
+ for part in parts[: max(1, max_n)]:
+ step = _short_action(part)
+ if step and not any(is_near_dup(step, prev) for prev in out):
+ out.append(step)
+ return out[:max_n]
+
+
+def plan_or_derived(
+ plan: Sequence[str] | None,
+ message: str = "",
+ goal: str = "",
+) -> list[str]:
+ """Use a real plan when present; otherwise derive 1–3 steps from the message."""
+ cleaned = [
+ str(p).strip()
+ for p in (plan or [])
+ if str(p).strip() and not is_filler_step(p)
+ ]
+ if cleaned:
+ return cleaned[:3]
+ return derive_plan(message or goal)
+
+
+def _is_bad_lead(line: str) -> bool:
+ s = (line or "").strip()
+ if not s:
+ return True
+ if _QUIZ_LEAD.match(s) or s.endswith("?"):
+ return True
+ if _PROTOCOL_DUMP.search(s):
+ return True
+ if is_filler_step(s):
+ return True
+ if s.casefold().startswith("→ ") or s.casefold().startswith("-> "):
+ return True
+ return False
+
+
def format_park_line(item: dict[str, Any] | str) -> str:
"""Named parking lot: Name — state — next crumb."""
if isinstance(item, str):
@@ -80,20 +277,31 @@ def next_action_line(
plan: Sequence[str] | None = None,
say: str = "",
decision: str = "",
+ message: str = "",
) -> str:
- for step in plan or []:
+ for step in plan_or_derived(plan, message, goal):
s = str(step or "").strip()
- if s:
+ if s and not is_filler_step(s) and not _is_bad_lead(s):
return s[:160]
for raw in (say or "").splitlines():
s = raw.strip().lstrip("-* ").lstrip("0123456789.) ")
- if s and not _QUIZ_LEAD.match(s) and not s.endswith("?"):
+ if s and not _is_bad_lead(s):
return s[:160]
dec = (decision or "").strip()
- if dec and not dec.lower().startswith("a — proceed"):
+ if (
+ dec
+ and len(dec) > 8
+ and not dec.lower().startswith("a —")
+ and not dec.lower().startswith("a -")
+ and not _is_bad_lead(dec)
+ and not is_filler_step(dec)
+ ):
return dec[:160]
+ derived = derive_plan(message or goal)
+ if derived:
+ return derived[0][:160]
g = (goal or "").strip()
- if g:
+ if g and not _is_bad_lead(g):
return f"Do the next step on: {short_name(g, cap=80)}"
return "Name the first action (under 2 minutes)."
@@ -117,18 +325,41 @@ def shape_execute_report(
plan: Sequence[str] | None = None,
say: str = "",
decision: str = "",
+ message: str = "",
) -> str:
- """Line 1 = next action. Lists ≤5. Never quiz/restate as the lead."""
+ """Line 1 = next action. Lists ≤5. Never quiz/restate as the lead.
+
+ When operator ``say`` is empty, always use ``next_action_line``.
+ Protocol dumps (production:/partner:/→ A — proceed) never win the lead.
+ """
+ provided = (say or "").strip()
+ lead = next_action_line(
+ goal=goal,
+ plan=plan,
+ say=provided,
+ decision=decision,
+ message=message,
+ )
body = _cap_lists((report or "").strip())
- lead = next_action_line(goal=goal, plan=plan, say=say or body, decision=decision)
+ if not provided:
+ if not body or _is_bad_lead(body.splitlines()[0]):
+ return lead
+ first = body.splitlines()[0].strip()
+ if first == lead:
+ return body
+ if _LIST_LINE.match(first):
+ return f"{lead}\n{body}".strip()
+ return lead
if not body:
return lead
first = body.splitlines()[0]
- if _QUIZ_LEAD.match(first) or first.strip().endswith("?"):
+ if _is_bad_lead(first):
rest = "\n".join(body.splitlines()[1:]).strip()
- return f"{lead}\n{rest}".strip() if rest else lead
+ if rest and not _PROTOCOL_DUMP.search(rest):
+ return f"{lead}\n{rest}".strip()
+ return lead
if first.strip() != lead and not _LIST_LINE.match(first):
- # Keep an existing action lead; only prepend when the body starts as a list.
+ # Operator supplied a clean say — keep that action lead.
return body
if _LIST_LINE.match(first):
return f"{lead}\n{body}".strip()
diff --git a/src/hermespace/neural_space.py b/src/hermespace/neural_space.py
index 154aa30..32931c9 100644
--- a/src/hermespace/neural_space.py
+++ b/src/hermespace/neural_space.py
@@ -82,8 +82,16 @@ def save_attractors(self, limit: int = 64) -> Path:
return self._cache_path
def sync_from_desk(self, desk: Desk, *, user_message: str = "") -> dict[str, Any]:
- if not self.config.enable:
- return {"enabled": False}
+ skip = os.environ.get("HERMESPACE_SKIP_NEURAL", "0").strip().lower() in {
+ "1",
+ "true",
+ "yes",
+ "on",
+ }
+ if not self.config.enable or skip:
+ return {"enabled": False, "skipped": skip}
+
+ from hermespace.execute_focus import derive_plan, is_filler_step, shape_focus
query = user_message or desk.goal or desk.say
self.field.set_query(query)
@@ -96,9 +104,14 @@ def sync_from_desk(self, desk: Desk, *, user_message: str = "") -> dict[str, Any
modality=slot.modality.value,
source="desk",
)
- if desk.goal:
+ # Prefer derived clauses over the raw user sentence on the field.
+ clauses = derive_plan(user_message or desk.goal)
+ if len(clauses) >= 2:
+ for i, c in enumerate(clauses):
+ self.field.add(c, energy=max(0.6, 0.82 - 0.04 * i), modality="verbal", source="goal")
+ elif desk.goal:
self.field.add(desk.goal, energy=0.85, modality="verbal", source="goal")
- if desk.decision:
+ if desk.decision and not is_filler_step(desk.decision):
self.field.add(desk.decision, energy=0.7, modality="exec", source="decision")
if desk.say:
self.field.add(desk.say, energy=0.65, modality="verbal", source="report")
@@ -136,7 +149,12 @@ def sync_from_desk(self, desk: Desk, *, user_message: str = "") -> dict[str, Any
bodies.add(v)
desk.concepts = new_concepts[-12:]
- desk.focus = [f"[{t.modality}|{t.energy:.2f}] {t.text}" for t in ignited]
+ desk.focus = shape_focus(
+ [f"[{t.modality}|{t.energy:.2f}] {t.text}" for t in ignited],
+ message=user_message,
+ goal=desk.goal,
+ plan=desk.plan,
+ )
snap = self.field.snapshot()
snap["backend"] = self.config.backend
diff --git a/src/hermespace/streams.py b/src/hermespace/streams.py
index 211beda..d71862f 100644
--- a/src/hermespace/streams.py
+++ b/src/hermespace/streams.py
@@ -57,11 +57,17 @@ def encode_stimulus(user_message: str, *, goal_hint: str = "") -> StreamBundle:
if not msg:
return bundle
- # Language / text stream (LLaMA-class features → language areas analogue)
- # Keep a compressed semantic gist, not full dump
+ # Language stream: distinct clauses, not prefixed copies of the full sentence.
+ from hermespace.execute_focus import derive_plan, gist_key
+
gist = " ".join(msg.split()[:40])
sal = 0.7 if _LANG_RE.search(msg) else 0.55
- bundle.text.append(Slot(f"lang_stream: {gist[:160]}", Modality.VERBAL, sal))
+ clauses = derive_plan(msg)
+ if clauses:
+ for i, clause in enumerate(clauses[:4]):
+ bundle.text.append(Slot(clause, Modality.VERBAL, max(0.45, sal - 0.06 * i)))
+ else:
+ bundle.text.append(Slot(gist[:80], Modality.VERBAL, sal))
# Audio stream proxies (Wav2Vec-class) — presence of speech media cues
if _AUDIO_RE.search(msg) or "voice.ogg" in msg.lower() or ".ogg" in msg.lower():
@@ -83,8 +89,9 @@ def encode_stimulus(user_message: str, *, goal_hint: str = "") -> StreamBundle:
Slot("production: prepare verbal report (decode path)", Modality.EXEC, 0.8)
)
- if goal_hint:
- bundle.text.append(Slot(f"intention: {goal_hint[:100]}", Modality.VERBAL, 0.65))
+ if goal_hint and gist_key(goal_hint) != gist_key(msg):
+ if not any(gist_key(s.text) == gist_key(goal_hint) for s in bundle.text):
+ bundle.text.append(Slot(goal_hint[:80], Modality.VERBAL, 0.6))
return bundle
@@ -96,23 +103,12 @@ def decode_to_report(
focus_texts: list[str],
load_level: str,
) -> str:
- """Brain2Qwerty reverse: workspace → compressed verbal report draft.
-
- Does not replace agent speech; supplies a candidate Say under hierarchy:
- intention → selected semantics (focus) → surface form.
- """
- bits: list[str] = []
- if goal:
- bits.append(goal.strip()[:120])
- if decision:
- bits.append(f"→ {decision.strip()[:80]}")
- if focus_texts:
- top = "; ".join(t[:40] for t in focus_texts[:2])
- bits.append(f"[{top}]")
- draft = " ".join(bits).strip()
- if load_level == "high" and len(draft) > 160:
- draft = draft[:157] + "..."
- return draft
+ """Workspace → next-action Report line. Never dump focus protocol slots."""
+ from hermespace.execute_focus import next_action_line
+
+ _ = focus_texts
+ _ = load_level
+ return next_action_line(goal=goal, decision=decision, message=goal)
def production_stages(goal: str, concepts: list[str], say: str) -> dict[str, str]:
@@ -131,19 +127,21 @@ def merge_streams_into_concepts(
max_add: int = 6,
) -> list[str]:
"""Fold stream slots into desk concepts without unbounded growth."""
+ from hermespace.execute_focus import collapse_near_dups, is_near_dup, is_protocol_slot
+
out = list(existing)
- bodies = {Slot_text_body(c) for c in out}
added = 0
for slot in bundle.all_slots():
if added >= max_add:
break
body = slot.text
- if body in bodies:
+ if is_protocol_slot(body):
+ continue
+ if any(is_near_dup(body, Slot_text_body(c)) for c in out):
continue
out.append(slot.label())
- bodies.add(body)
added += 1
- return out
+ return collapse_near_dups(out)
def Slot_text_body(raw: str) -> str:
diff --git a/src/hermespace/workbench.py b/src/hermespace/workbench.py
index ef6852e..f0b2329 100644
--- a/src/hermespace/workbench.py
+++ b/src/hermespace/workbench.py
@@ -367,7 +367,7 @@ def receive_order(
msg or g,
goal=g or msg,
decision=decision or "A — proceed",
- plan=list(plan or ["execute"]),
+ plan=list(plan or []),
say=say,
session_id=self.session_id,
agent_id=self.agent_id,
diff --git a/src/hermespace/workflow.py b/src/hermespace/workflow.py
index cfcfbb5..d600069 100644
--- a/src/hermespace/workflow.py
+++ b/src/hermespace/workflow.py
@@ -88,9 +88,11 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
return out
# 2–5 desk
+ from hermespace.execute_focus import plan_or_derived
+
g = payload.goal or existing.goal or msg[:200]
dec = payload.decision or existing.decision or "A — proceed"
- pl = payload.plan or existing.plan or ["execute"]
+ pl = plan_or_derived(payload.plan or existing.plan, msg, g)
sy = payload.say if payload.say else existing.say
cons = payload.concepts or existing.concepts
ch = payload.choices or existing.choices or ["A — proceed"]
@@ -265,6 +267,12 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
except Exception as exc:
cube_meta = {"ok": False, "error": type(exc).__name__}
+ try:
+ desk.refresh_focus(msg)
+ save_desk(desk, self.engine.desk_path)
+ except Exception:
+ pass
+
# 6 one user-message inject — mid ≤2.8k, high ≤900. No world/protocol essay.
from hermespace.context_surgery import (
assemble_inject,
@@ -311,8 +319,9 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
report,
goal=desk.goal,
plan=list(desk.plan or []),
- say=desk.say or report,
+ say=payload.say,
decision=desk.decision,
+ message=msg,
)
desk.say = report
except Exception:
diff --git a/tests/test_access_env.py b/tests/test_access_env.py
index 1efa990..987ab27 100644
--- a/tests/test_access_env.py
+++ b/tests/test_access_env.py
@@ -42,7 +42,8 @@ def test_lens_ranks_held_and_silent(self) -> None:
self.assertTrue(any("canary" in t for t in texts))
self.assertTrue(any("rollback" in t for t in texts))
md = env.lens_markdown()
- self.assertIn("J-Lens", md)
+ self.assertIn("Operator lens", md)
+ self.assertNotIn("J-Lens readout", md)
def test_swap_redirects_workspace(self) -> None:
from hermespace.access_env import AccessEnv
diff --git a/tests/test_desk_quality.py b/tests/test_desk_quality.py
new file mode 100644
index 0000000..2f8ebee
--- /dev/null
+++ b/tests/test_desk_quality.py
@@ -0,0 +1,169 @@
+"""Desk quality cut — no-say live turn, FOA collapse, smoke shadow, lens title."""
+
+from __future__ import annotations
+
+import json
+import os
+import subprocess
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT / "src"))
+
+LIVE_MSG = "Write a short README for the auth fix, then stop."
+
+
+class TestNoSayLiveTurn(unittest.TestCase):
+ def setUp(self) -> None:
+ self.tmp = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self.tmp.name
+ os.environ["HERMESPACE_SKIP_NEURAL"] = "1"
+ os.environ["HERMESPACE_NEURAL_VERBALIZE"] = "0"
+ os.environ["HERMESPACE_OEW"] = "1"
+
+ def tearDown(self) -> None:
+ self.tmp.cleanup()
+ for key in (
+ "HERMESPACE_HOME",
+ "HERMESPACE_SKIP_NEURAL",
+ "HERMESPACE_NEURAL_VERBALIZE",
+ "HERMESPACE_OEW",
+ ):
+ os.environ.pop(key, None)
+
+ def test_derive_plan_splits_then(self) -> None:
+ from hermespace.execute_focus import derive_plan, is_near_dup
+
+ plan = derive_plan(LIVE_MSG)
+ self.assertGreaterEqual(len(plan), 2)
+ self.assertLessEqual(len(plan), 3)
+ self.assertNotIn("execute", [p.casefold() for p in plan])
+ self.assertTrue(any("readme" in p.casefold() for p in plan))
+ self.assertTrue(any("stop" in p.casefold() for p in plan))
+ self.assertFalse(is_near_dup(plan[0], plan[1]))
+
+ def test_message_only_report_and_foa(self) -> None:
+ from hermespace.execute_focus import gist_key, is_near_dup, strip_slot_prefix
+ from hermespace.io_contract import HermespaceInput
+ from hermespace.workflow import Workflow
+
+ # Match live CLI: only --message, no --say/--goal/--plan, neural on.
+ os.environ.pop("HERMESPACE_SKIP_NEURAL", None)
+ out = Workflow().run(HermespaceInput(message=LIVE_MSG, say="", goal="", plan=[]))
+ self.assertFalse(out.skipped, out.reason)
+ line1 = (out.report or "").splitlines()[0].strip()
+ low = line1.casefold()
+ self.assertTrue(line1, out.report)
+ self.assertNotIn("production:", low)
+ self.assertNotIn("partner:", low)
+ self.assertNotIn("→ a — proceed", low)
+ self.assertNotIn("a — proceed", low)
+ self.assertNotEqual(low, "execute")
+ self.assertNotEqual(low, LIVE_MSG.casefold())
+ self.assertTrue(
+ "readme" in low or "write" in low,
+ line1,
+ )
+ self.assertNotEqual(list(out.plan or []), ["execute"])
+ self.assertGreaterEqual(len(out.plan or []), 1)
+ self.assertLessEqual(len(out.plan or []), 3)
+ self.assertTrue(any("stop" in str(p).casefold() for p in out.plan) or len(out.plan) >= 1)
+
+ focus = list(out.meta.get("focus") or []) if isinstance(out.meta, dict) else []
+ if not focus:
+ from hermespace.store import load_desk
+
+ focus = list(load_desk().focus or [])
+ self.assertLessEqual(len(focus), 4)
+ gists = [gist_key(x) for x in focus if gist_key(x)]
+ for i, a in enumerate(gists):
+ for b in gists[i + 1 :]:
+ self.assertFalse(is_near_dup(a, b), f"FOA near-dup {a!r} ~ {b!r} from {focus}")
+ stripped = [strip_slot_prefix(x) for x in focus]
+ joined = " ".join(stripped).casefold()
+ self.assertTrue("readme" in joined or "stop" in joined, focus)
+
+ def test_thanks_still_skips(self) -> None:
+ from hermespace.io_contract import HermespaceInput
+ from hermespace.workflow import Workflow
+
+ out = Workflow().run(HermespaceInput(message="thanks", force=False))
+ self.assertTrue(out.skipped)
+ self.assertEqual(out.reason, "trivial_ack")
+
+ def test_shape_empty_say_uses_next_action(self) -> None:
+ from hermespace.execute_focus import shape_execute_report
+
+ garbled = (
+ "Write a short README for the auth fix, then stop. "
+ "→ A — proceed [production: prepare verbal report (decod; partner: match user"
+ )
+ out = shape_execute_report(
+ garbled,
+ goal=LIVE_MSG,
+ plan=[],
+ say="",
+ decision="A — proceed",
+ message=LIVE_MSG,
+ )
+ line1 = out.splitlines()[0]
+ self.assertNotIn("production:", line1)
+ self.assertNotIn("partner:", line1)
+ self.assertNotIn("A — proceed", line1)
+ self.assertIn("README", line1)
+
+ def test_operator_lens_title(self) -> None:
+ from hermespace.access import AccessEnv
+
+ md = AccessEnv(agent_id="lens-title").lens_markdown()
+ self.assertIn("## Operator lens (external workspace)", md)
+ self.assertNotIn("J-Lens readout", md)
+
+
+class TestPluginEntryShadow(unittest.TestCase):
+ def test_root_entry_exposes_register_via_importlib(self) -> None:
+ import importlib.util
+
+ spec = importlib.util.spec_from_file_location(
+ "hermespace_repo_plugin_quality",
+ ROOT / "__init__.py",
+ submodule_search_locations=[str(ROOT)],
+ )
+ self.assertIsNotNone(spec)
+ assert spec is not None and spec.loader is not None
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ self.assertTrue(callable(getattr(mod, "register", None)))
+
+ def test_parent_cwd_imports_real_package(self) -> None:
+ with tempfile.TemporaryDirectory() as td:
+ parent = Path(td)
+ link = parent / "hermespace"
+ link.symlink_to(ROOT)
+ env = os.environ.copy()
+ env["PYTHONPATH"] = str(ROOT / "src")
+ env["HERMESPACE_HOME"] = str(parent / "home")
+ code = (
+ "import hermespace, hermespace.plugin as p\n"
+ "from hermespace.desk import Desk\n"
+ "assert callable(p.register)\n"
+ "assert Desk\n"
+ "print(hermespace.__file__)\n"
+ )
+ proc = subprocess.run(
+ [sys.executable, "-c", code],
+ cwd=str(parent),
+ env=env,
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ self.assertEqual(proc.returncode, 0, proc.stderr)
+ self.assertIn("src", (proc.stdout or "").replace("\\", "/"))
+
+
+if __name__ == "__main__":
+ unittest.main()
From 1f76fa05a6ccf01bfedfde74317f4d2d727f1d25 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 23:25:27 +0000
Subject: [PATCH 29/32] fix(desk): bind FOA, short verb Report, collapse hub
copies
Match the proven empty-say cut, then tighten leftovers: desk.focus
keeps one bind instead of prefixed copies; hub/neural collapse
lang_stream and containment dups; empty say leads with a short verb
phrase (Write the README) via next_action_line. Root plugin pops the
shim from sys.modules before importing register. Lens title is
Access lens (harness workspace); sanitizer still treats J-Lens
readout as dirty.
Co-authored-by: Pablo
---
__init__.py | 53 +++------------
src/hermespace/access/env.py | 2 +-
src/hermespace/access/hub.py | 36 ++++++++++-
src/hermespace/desk.py | 10 +--
src/hermespace/execute_focus.py | 111 ++++++++++++++++++++++----------
src/hermespace/neural_space.py | 12 +---
src/hermespace/streams.py | 14 ++--
tests/test_access_env.py | 3 +-
tests/test_desk_quality.py | 78 ++++++++++++++++------
tests/test_streams.py | 11 +++-
10 files changed, 203 insertions(+), 127 deletions(-)
diff --git a/__init__.py b/__init__.py
index c7e49e2..39cede7 100644
--- a/__init__.py
+++ b/__init__.py
@@ -2,60 +2,27 @@
``hermes plugins install PabloTheThinker/hermespace --enable`` clones this
repository as one plugin. The checkout folder is often named ``hermespace``,
-which would shadow ``src/hermespace`` if we ``from hermespace.plugin import
-register`` while this file is already loaded as that package. Load the real
-plugin (and, when shadowed, the real package) via importlib from ``src/``.
+which would shadow ``src/hermespace``. Pop this shim from ``sys.modules``
+before importing the real ``register``.
"""
from __future__ import annotations
-import importlib.util
import sys
from pathlib import Path
_ROOT = Path(__file__).resolve().parent
_SRC = _ROOT / "src"
-_PKG = _SRC / "hermespace"
-_PLUGIN = _PKG / "plugin.py"
+# Prefer src/ over a parent-cwd folder also named hermespace.
+if str(_SRC) in sys.path:
+ sys.path.remove(str(_SRC))
+sys.path.insert(0, str(_SRC))
-def _load_from_file(name: str, path: Path, *, package_dir: Path | None = None):
- spec = importlib.util.spec_from_file_location(
- name,
- path,
- submodule_search_locations=[str(package_dir)] if package_dir else None,
- )
- if spec is None or spec.loader is None:
- raise ImportError(f"cannot load {path}")
- module = importlib.util.module_from_spec(spec)
- # Bind before exec so src/hermespace relative imports do not re-enter this file.
- sys.modules[name] = module
- spec.loader.exec_module(module)
- return module
-
-
-def _load_plugin_register():
- mod = _load_from_file("_hermespace_plugin_entry", _PLUGIN)
- return mod.register
-
-
-if str(_SRC) not in sys.path:
- sys.path.insert(0, str(_SRC))
-
+# Parent-cwd / folder-name shadow: drop this file so src/hermespace can load.
if __name__ == "hermespace":
- # Parent-cwd / folder-name shadow: bind the real package from src/.
- real = _load_from_file(
- "hermespace",
- _PKG / "__init__.py",
- package_dir=_PKG,
- )
- sys.modules["hermespace"] = real
- register = _load_plugin_register()
- setattr(real, "register", register)
- for _name in getattr(real, "__all__", []):
- if hasattr(real, _name):
- globals()[_name] = getattr(real, _name)
-else:
- register = _load_plugin_register()
+ sys.modules.pop("hermespace", None)
+
+from hermespace.plugin import register
__all__ = ["register"]
diff --git a/src/hermespace/access/env.py b/src/hermespace/access/env.py
index 7400ee1..84f0742 100644
--- a/src/hermespace/access/env.py
+++ b/src/hermespace/access/env.py
@@ -227,7 +227,7 @@ def lens(self, *, top_k: int = 12, include_silent: bool = True) -> list[LensHit]
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 = [
- "## Operator lens (external workspace)",
+ "## Access lens (harness 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):",
diff --git a/src/hermespace/access/hub.py b/src/hermespace/access/hub.py
index ba48df3..63635d1 100644
--- a/src/hermespace/access/hub.py
+++ b/src/hermespace/access/hub.py
@@ -373,7 +373,7 @@ def sync_from_desk(
held = [c for c in self.state.hub if c.held]
silent_keep = list(self.state.silent_steps)
- from hermespace.execute_focus import is_bind_restatement, is_near_dup, is_protocol_slot
+ from hermespace.execute_focus import _keep_score, is_near_dup, is_protocol_slot
new_hub: list[WorkspaceConcept] = list(held)
seen_texts = [c.text for c in new_hub]
@@ -381,9 +381,20 @@ def sync_from_desk(
for raw in concepts:
slot = parse_slot(raw)
body = slot.text.strip()
- if not body or is_protocol_slot(body) or is_bind_restatement(body):
+ if not body or is_protocol_slot(body):
continue
- if any(is_near_dup(body, prev) for prev in seen_texts):
+ hit = next((i for i, prev in enumerate(seen_texts) if is_near_dup(body, prev)), None)
+ if hit is not None:
+ if _keep_score(body) > _keep_score(seen_texts[hit]):
+ new_hub[hit] = WorkspaceConcept(
+ text=body,
+ salience=slot.salience,
+ modality=slot.modality.value,
+ source="desk",
+ silent=False,
+ held=False,
+ )
+ seen_texts[hit] = body
continue
new_hub.append(
WorkspaceConcept(
@@ -499,11 +510,30 @@ def _drop_body(self, body: str) -> None:
needle = body.casefold()
self.state.hub = [c for c in self.state.hub if c.text.casefold() != needle]
+ def _collapse_hub(self) -> None:
+ """After prefix-strip, hub verbal bodies must be pairwise distinct."""
+ from hermespace.execute_focus import _keep_score, is_near_dup, is_protocol_slot
+
+ out: list[WorkspaceConcept] = []
+ for c in self.state.hub:
+ body = (c.text or "").strip()
+ if not body or is_protocol_slot(body):
+ continue
+ hit = next((i for i, prev in enumerate(out) if is_near_dup(body, prev.text)), None)
+ if hit is None:
+ out.append(c)
+ continue
+ prev = out[hit]
+ if _keep_score(body) > _keep_score(prev.text) or (c.held and not prev.held):
+ out[hit] = c
+ self.state.hub = out
+
def _recompete(
self,
preferred_focus: list[str] | None = None,
user_message: str = "",
) -> None:
+ self._collapse_hub()
# Limited capacity (Baars/Changeux/Anthropic): hub is a bottleneck
if len(self.state.hub) > HUB_CAP:
ranked = sorted(
diff --git a/src/hermespace/desk.py b/src/hermespace/desk.py
index 944589b..231763d 100644
--- a/src/hermespace/desk.py
+++ b/src/hermespace/desk.py
@@ -65,6 +65,11 @@ def recompute_cognition(self, user_message: str = "") -> Desk:
from hermespace.execute_focus import is_protocol_slot, shape_focus
+ bound = bind_episode(self.goal, self.decision, self.say, self.plan)
+ if bound:
+ label = bound.label()
+ self.concepts = [c for c in self.concepts if not c.strip().lower().startswith("[bind")]
+ self.concepts.append(label)
slots = [s for s in self.slots() if not is_protocol_slot(s.text)]
for d in self.do_not_say:
slots.append(Slot(d, Modality.EXEC, 0.75))
@@ -79,11 +84,6 @@ def recompute_cognition(self, user_message: str = "") -> Desk:
user_message or self.goal, len(self.concepts), len(self.plan)
)
self.executive = executive_mode(str(self.load.get("level", "mid")), len(self.choices))
- bound = bind_episode(self.goal, self.decision, self.say, self.plan)
- if bound:
- label = bound.label()
- self.concepts = [c for c in self.concepts if not c.strip().lower().startswith("[bind")]
- self.concepts.append(label)
self.meta["load"] = self.load
self.meta["executive"] = self.executive
self.meta["focus"] = self.focus
diff --git a/src/hermespace/execute_focus.py b/src/hermespace/execute_focus.py
index f9373e4..f465843 100644
--- a/src/hermespace/execute_focus.py
+++ b/src/hermespace/execute_focus.py
@@ -111,17 +111,27 @@ def is_bind_restatement(text: str) -> bool:
def is_near_dup(a: str, b: str) -> bool:
+ """Prefix-stripped gist match. Containment counts as a duplicate."""
ka, kb = gist_key(a), gist_key(b)
if not ka or not kb:
return False
if ka == kb:
return True
shorter, longer = (ka, kb) if len(ka) <= len(kb) else (kb, ka)
- if len(shorter) < 8:
+ if len(shorter) < 4:
return False
- if shorter in longer and len(shorter) / max(len(longer), 1) >= 0.55:
- return True
- return False
+ return shorter in longer
+
+
+def _keep_score(text: str) -> int:
+ """Prefer the episodic bind over a lang_stream:/intention: copy of the same gist."""
+ raw = (text or "").strip()
+ if raw.casefold().startswith("[bind") or is_bind_restatement(raw):
+ return 3
+ rest = raw.split("]", 1)[-1].strip() if raw.startswith("[") else raw
+ if _SLOT_PREFIX.match(rest):
+ return 0
+ return 1
def collapse_near_dups(
@@ -129,11 +139,7 @@ def collapse_near_dups(
*,
prefer_shorter: bool = False,
) -> list[str]:
- """Collapse near-duplicate gists. Order preserved.
-
- ``prefer_shorter`` keeps the clause when a later short step is a
- near-dup of an earlier full-sentence echo.
- """
+ """Collapse near-duplicate gists. Bind wins over prefixed copies."""
out: list[str] = []
for raw in items:
s = str(raw or "").strip()
@@ -143,7 +149,13 @@ def collapse_near_dups(
if hit is None:
out.append(s)
continue
- if prefer_shorter and len(gist_key(s)) < len(gist_key(out[hit])):
+ if _keep_score(s) > _keep_score(out[hit]):
+ out[hit] = s
+ elif (
+ prefer_shorter
+ and _keep_score(s) == _keep_score(out[hit])
+ and len(gist_key(s)) < len(gist_key(out[hit]))
+ ):
out[hit] = s
return out
@@ -156,31 +168,21 @@ def shape_focus(
plan: Sequence[str] | None = None,
cap: int = 4,
) -> list[str]:
- """FOA: distinct clauses, not copies of the user sentence or bind blob."""
- clauses = plan_or_derived(plan, message, goal)
- raw_full = " ".join((message or goal or "").split())
- out: list[str] = []
- for c in clauses:
- if c and not is_filler_step(c) and not any(is_near_dup(c, prev) for prev in out):
- out.append(c)
+ """FOA: pairwise-distinct verbal bodies. One bind, not prefixed copies."""
+ _ = (message, goal, plan)
+ cleaned: list[str] = []
for raw in labels:
s = str(raw or "").strip()
- if not s or is_protocol_slot(s) or is_bind_restatement(s) or is_filler_step(s):
+ if not s or is_protocol_slot(s) or is_filler_step(s):
continue
- body = strip_slot_prefix(s)
- if (
- raw_full
- and clauses
- and is_near_dup(body, raw_full)
- and len(gist_key(body)) >= len(gist_key(raw_full)) * 0.8
- ):
- continue
- if any(is_near_dup(s, prev) for prev in out):
- continue
- out.append(s)
- if len(out) >= cap:
- break
- return collapse_near_dups(out, prefer_shorter=True)[:cap]
+ cleaned.append(s)
+ return collapse_near_dups(cleaned)[:cap]
+
+
+_HEAD_VERB = re.compile(
+ r"^(Write|Open|Fix|Patch|Build|Add|Create|Update|Read|Run|Ship|Deploy|Stop|Verify|Test)\b"
+)
+_OBJECT_NOUN = re.compile(r"\b(README|TTL|PR|docs?|login|plugin|harness)\b", re.I)
def _short_action(text: str) -> str:
@@ -193,6 +195,35 @@ def _short_action(text: str) -> str:
return t[:80] if len(t) <= 80 else t[:79].rstrip() + "…"
+def short_verb_phrase(text: str) -> str:
+ """Compress a long user clause to a short verb phrase (Write the README)."""
+ t = _short_action(text)
+ if not t:
+ return ""
+ if len(t.split()) <= 4:
+ return t
+ m = _HEAD_VERB.match(t)
+ obj = _OBJECT_NOUN.search(t)
+ if not m or not obj:
+ return t
+ noun = obj.group(0)
+ if noun.lower() == "readme":
+ noun = "README"
+ return f"{m.group(1)} the {noun}"
+
+
+def _is_raw_user_echo(text: str, message: str = "", goal: str = "") -> bool:
+ raw = " ".join((message or goal or "").split())
+ if not raw or not text:
+ return False
+ if gist_key(text) == gist_key(raw):
+ return True
+ parts = [p.strip(" .,") for p in _CLAUSE_SPLIT.split(raw) if p and p.strip()]
+ if parts and gist_key(text) == gist_key(parts[0]) and len(gist_key(text).split()) >= 5:
+ return True
+ return False
+
+
def derive_plan(message: str, *, max_n: int = 3) -> list[str]:
"""1–3 real steps from a user sentence. Never the filler ``execute``."""
msg = " ".join((message or "").strip().split())
@@ -281,8 +312,20 @@ def next_action_line(
) -> str:
for step in plan_or_derived(plan, message, goal):
s = str(step or "").strip()
- if s and not is_filler_step(s) and not _is_bad_lead(s):
- return s[:160]
+ if not s or is_filler_step(s) or _is_bad_lead(s):
+ continue
+ phrase = short_verb_phrase(s)
+ if (
+ phrase
+ and phrase != s
+ and not _is_bad_lead(phrase)
+ and not is_filler_step(phrase)
+ and not _is_raw_user_echo(phrase, message, goal)
+ ):
+ return phrase[:80]
+ if _is_raw_user_echo(s, message, goal):
+ continue
+ return s[:160]
for raw in (say or "").splitlines():
s = raw.strip().lstrip("-* ").lstrip("0123456789.) ")
if s and not _is_bad_lead(s):
diff --git a/src/hermespace/neural_space.py b/src/hermespace/neural_space.py
index 32931c9..fb86b1d 100644
--- a/src/hermespace/neural_space.py
+++ b/src/hermespace/neural_space.py
@@ -91,7 +91,7 @@ def sync_from_desk(self, desk: Desk, *, user_message: str = "") -> dict[str, Any
if not self.config.enable or skip:
return {"enabled": False, "skipped": skip}
- from hermespace.execute_focus import derive_plan, is_filler_step, shape_focus
+ from hermespace.execute_focus import collapse_near_dups, is_filler_step, shape_focus
query = user_message or desk.goal or desk.say
self.field.set_query(query)
@@ -104,13 +104,6 @@ def sync_from_desk(self, desk: Desk, *, user_message: str = "") -> dict[str, Any
modality=slot.modality.value,
source="desk",
)
- # Prefer derived clauses over the raw user sentence on the field.
- clauses = derive_plan(user_message or desk.goal)
- if len(clauses) >= 2:
- for i, c in enumerate(clauses):
- self.field.add(c, energy=max(0.6, 0.82 - 0.04 * i), modality="verbal", source="goal")
- elif desk.goal:
- self.field.add(desk.goal, energy=0.85, modality="verbal", source="goal")
if desk.decision and not is_filler_step(desk.decision):
self.field.add(desk.decision, energy=0.7, modality="exec", source="decision")
if desk.say:
@@ -148,7 +141,7 @@ def sync_from_desk(self, desk: Desk, *, user_message: str = "") -> dict[str, Any
new_concepts.append(f"[verbal|0.80] {v}")
bodies.add(v)
- desk.concepts = new_concepts[-12:]
+ desk.concepts = collapse_near_dups(new_concepts)[-12:]
desk.focus = shape_focus(
[f"[{t.modality}|{t.energy:.2f}] {t.text}" for t in ignited],
message=user_message,
@@ -157,6 +150,7 @@ def sync_from_desk(self, desk: Desk, *, user_message: str = "") -> dict[str, Any
)
snap = self.field.snapshot()
+ snap["focus"] = collapse_near_dups(list(snap.get("focus") or []))
snap["backend"] = self.config.backend
snap["embed_model"] = getattr(self.embed_backend, "model", "") or self.config.backend
snap["enabled"] = True
diff --git a/src/hermespace/streams.py b/src/hermespace/streams.py
index d71862f..b38b660 100644
--- a/src/hermespace/streams.py
+++ b/src/hermespace/streams.py
@@ -57,17 +57,12 @@ def encode_stimulus(user_message: str, *, goal_hint: str = "") -> StreamBundle:
if not msg:
return bundle
- # Language stream: distinct clauses, not prefixed copies of the full sentence.
- from hermespace.execute_focus import derive_plan, gist_key
+ # Language / text stream — one gist slot. FOA/hub collapse prefixed copies.
+ from hermespace.execute_focus import gist_key
gist = " ".join(msg.split()[:40])
sal = 0.7 if _LANG_RE.search(msg) else 0.55
- clauses = derive_plan(msg)
- if clauses:
- for i, clause in enumerate(clauses[:4]):
- bundle.text.append(Slot(clause, Modality.VERBAL, max(0.45, sal - 0.06 * i)))
- else:
- bundle.text.append(Slot(gist[:80], Modality.VERBAL, sal))
+ bundle.text.append(Slot(f"lang_stream: {gist[:160]}", Modality.VERBAL, sal))
# Audio stream proxies (Wav2Vec-class) — presence of speech media cues
if _AUDIO_RE.search(msg) or "voice.ogg" in msg.lower() or ".ogg" in msg.lower():
@@ -90,8 +85,7 @@ def encode_stimulus(user_message: str, *, goal_hint: str = "") -> StreamBundle:
)
if goal_hint and gist_key(goal_hint) != gist_key(msg):
- if not any(gist_key(s.text) == gist_key(goal_hint) for s in bundle.text):
- bundle.text.append(Slot(goal_hint[:80], Modality.VERBAL, 0.6))
+ bundle.text.append(Slot(f"intention: {goal_hint[:100]}", Modality.VERBAL, 0.65))
return bundle
diff --git a/tests/test_access_env.py b/tests/test_access_env.py
index 987ab27..1828373 100644
--- a/tests/test_access_env.py
+++ b/tests/test_access_env.py
@@ -42,8 +42,9 @@ def test_lens_ranks_held_and_silent(self) -> None:
self.assertTrue(any("canary" in t for t in texts))
self.assertTrue(any("rollback" in t for t in texts))
md = env.lens_markdown()
- self.assertIn("Operator lens", md)
+ self.assertIn("Access lens", md)
self.assertNotIn("J-Lens readout", md)
+ self.assertNotIn("Operator lens", md)
def test_swap_redirects_workspace(self) -> None:
from hermespace.access_env import AccessEnv
diff --git a/tests/test_desk_quality.py b/tests/test_desk_quality.py
index 2f8ebee..266b90c 100644
--- a/tests/test_desk_quality.py
+++ b/tests/test_desk_quality.py
@@ -1,8 +1,7 @@
-"""Desk quality cut — no-say live turn, FOA collapse, smoke shadow, lens title."""
+"""Desk quality cut — empty-say Lyra turn, FOA collapse, smoke shadow, lens title."""
from __future__ import annotations
-import json
import os
import subprocess
import sys
@@ -16,6 +15,21 @@
LIVE_MSG = "Write a short README for the auth fix, then stop."
+def _verbal_bodies(items: list) -> list[str]:
+ from hermespace.execute_focus import gist_key, strip_slot_prefix
+
+ return [strip_slot_prefix(x) for x in items if gist_key(x)]
+
+
+def _assert_pairwise_distinct(test: unittest.TestCase, items: list, label: str) -> None:
+ from hermespace.execute_focus import gist_key, is_near_dup
+
+ gists = [gist_key(x) for x in items if gist_key(x)]
+ for i, a in enumerate(gists):
+ for b in gists[i + 1 :]:
+ test.assertFalse(is_near_dup(a, b), f"{label} near-dup {a!r} ~ {b!r} from {items}")
+
+
class TestNoSayLiveTurn(unittest.TestCase):
def setUp(self) -> None:
self.tmp = tempfile.TemporaryDirectory()
@@ -46,8 +60,9 @@ def test_derive_plan_splits_then(self) -> None:
self.assertFalse(is_near_dup(plan[0], plan[1]))
def test_message_only_report_and_foa(self) -> None:
- from hermespace.execute_focus import gist_key, is_near_dup, strip_slot_prefix
+ from hermespace.execute_focus import gist_key, next_action_line, strip_slot_prefix
from hermespace.io_contract import HermespaceInput
+ from hermespace.store import load_desk
from hermespace.workflow import Workflow
# Match live CLI: only --message, no --say/--goal/--plan, neural on.
@@ -59,32 +74,52 @@ def test_message_only_report_and_foa(self) -> None:
self.assertTrue(line1, out.report)
self.assertNotIn("production:", low)
self.assertNotIn("partner:", low)
+ self.assertNotIn("lang_stream:", low)
self.assertNotIn("→ a — proceed", low)
self.assertNotIn("a — proceed", low)
self.assertNotEqual(low, "execute")
self.assertNotEqual(low, LIVE_MSG.casefold())
- self.assertTrue(
- "readme" in low or "write" in low,
- line1,
- )
+ first_clause = "write a short readme for the auth fix"
+ self.assertNotEqual(low, first_clause)
+ self.assertLessEqual(len(line1.split()), 5, line1)
+ self.assertIn("readme", low)
+ self.assertEqual(line1, next_action_line(message=LIVE_MSG, say="", plan=[]))
self.assertNotEqual(list(out.plan or []), ["execute"])
self.assertGreaterEqual(len(out.plan or []), 1)
self.assertLessEqual(len(out.plan or []), 3)
- self.assertTrue(any("stop" in str(p).casefold() for p in out.plan) or len(out.plan) >= 1)
+ self.assertTrue(any("stop" in str(p).casefold() for p in out.plan))
focus = list(out.meta.get("focus") or []) if isinstance(out.meta, dict) else []
+ desk = load_desk()
if not focus:
- from hermespace.store import load_desk
-
- focus = list(load_desk().focus or [])
+ focus = list(desk.focus or [])
self.assertLessEqual(len(focus), 4)
- gists = [gist_key(x) for x in focus if gist_key(x)]
- for i, a in enumerate(gists):
- for b in gists[i + 1 :]:
- self.assertFalse(is_near_dup(a, b), f"FOA near-dup {a!r} ~ {b!r} from {focus}")
- stripped = [strip_slot_prefix(x) for x in focus]
- joined = " ".join(stripped).casefold()
- self.assertTrue("readme" in joined or "stop" in joined, focus)
+ _assert_pairwise_distinct(self, focus, "FOA")
+ bodies = _verbal_bodies(focus)
+ joined = " ".join(bodies).casefold()
+ self.assertTrue("readme" in joined or "bind" in joined or "stop" in joined, focus)
+ # First-cut FOA: one bind, not four prefixed copies of the user sentence.
+ bind_n = sum(1 for x in focus if str(x).casefold().startswith("[bind") or " | " in str(x))
+ self.assertLessEqual(bind_n, 1, focus)
+ lang_n = sum(1 for x in focus if "lang_stream:" in str(x).casefold())
+ self.assertEqual(lang_n, 0, focus)
+ copies = sum(1 for x in bodies if gist_key(x) == gist_key(LIVE_MSG))
+ self.assertLessEqual(copies, 1, focus)
+
+ hub = []
+ neural_focus = []
+ if isinstance(out.meta, dict):
+ access = out.meta.get("access") or {}
+ neural = out.meta.get("neural") or {}
+ neural_focus = list(neural.get("focus") or [])
+ from hermespace.access import AccessHub
+ from hermespace.access.engine import workspace_id
+
+ js = AccessHub(agent_id=workspace_id(out.meta.get("agent_id") or "hermes-agent", out.session_id or "default"))
+ hub = [c.text for c in js.state.hub]
+ _assert_pairwise_distinct(self, hub, "hub")
+ if neural_focus:
+ _assert_pairwise_distinct(self, neural_focus, "neural")
def test_thanks_still_skips(self) -> None:
from hermespace.io_contract import HermespaceInput
@@ -114,13 +149,16 @@ def test_shape_empty_say_uses_next_action(self) -> None:
self.assertNotIn("partner:", line1)
self.assertNotIn("A — proceed", line1)
self.assertIn("README", line1)
+ self.assertNotEqual(line1.casefold(), LIVE_MSG.casefold())
+ self.assertLessEqual(len(line1.split()), 5)
- def test_operator_lens_title(self) -> None:
+ def test_access_lens_title(self) -> None:
from hermespace.access import AccessEnv
md = AccessEnv(agent_id="lens-title").lens_markdown()
- self.assertIn("## Operator lens (external workspace)", md)
+ self.assertIn("## Access lens (harness workspace)", md)
self.assertNotIn("J-Lens readout", md)
+ self.assertNotIn("Operator lens", md)
class TestPluginEntryShadow(unittest.TestCase):
diff --git a/tests/test_streams.py b/tests/test_streams.py
index 7fcb5e9..40cb5b3 100644
--- a/tests/test_streams.py
+++ b/tests/test_streams.py
@@ -41,7 +41,16 @@ def test_enter_decode_say(self):
auto_load=False,
user_message="build hermespace streams",
)
- self.assertTrue(desk.say.strip())
+ line1 = desk.say.splitlines()[0].strip() if desk.say.strip() else ""
+ self.assertTrue(line1)
+ low = line1.casefold()
+ self.assertNotIn("production:", low)
+ self.assertNotIn("partner:", low)
+ self.assertNotIn("→ a — proceed", low)
+ self.assertNotIn("[production:", low)
+ self.assertNotEqual(low, "execute")
+ self.assertNotIn("→", line1)
+ self.assertNotEqual(line1, "Test decode → A — go")
if __name__ == "__main__":
unittest.main()
From fa7c1319d55c0e95a9bf1ead14ebe1c215bb18ef Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 15 Aug 2026 23:34:00 +0000
Subject: [PATCH 30/32] =?UTF-8?q?fix(desk):=20land=20zip=20after-state=20?=
=?UTF-8?q?=E2=80=94=20Write=20the=20README?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Empty-say lead is compress_action_phrase(plan[0]) (drop short/a/an).
FOA/hub/neural drop lang_stream and user-sentence echoes; bind +
Write the README remain. OEW skips gist-contained candidates and
still parks distinct Stop. Quality test asserts this live turn.
Co-authored-by: Pablo
---
src/hermespace/access/hub.py | 11 ++-
src/hermespace/access/oew.py | 4 ++
src/hermespace/cognition.py | 20 +++++-
src/hermespace/execute_focus.py | 115 ++++++++++++++++++++++----------
src/hermespace/neural_space.py | 31 +++++++--
tests/test_desk_quality.py | 53 +++++++++------
6 files changed, 171 insertions(+), 63 deletions(-)
diff --git a/src/hermespace/access/hub.py b/src/hermespace/access/hub.py
index 63635d1..845294f 100644
--- a/src/hermespace/access/hub.py
+++ b/src/hermespace/access/hub.py
@@ -373,7 +373,12 @@ def sync_from_desk(
held = [c for c in self.state.hub if c.held]
silent_keep = list(self.state.silent_steps)
- from hermespace.execute_focus import _keep_score, is_near_dup, is_protocol_slot
+ from hermespace.execute_focus import (
+ _keep_score,
+ is_near_dup,
+ is_protocol_slot,
+ is_user_echo_copy,
+ )
new_hub: list[WorkspaceConcept] = list(held)
seen_texts = [c.text for c in new_hub]
@@ -383,6 +388,10 @@ def sync_from_desk(
body = slot.text.strip()
if not body or is_protocol_slot(body):
continue
+ if body.casefold().startswith("lang_stream:"):
+ continue
+ if is_user_echo_copy(body, user_message, getattr(desk, "goal", "") or ""):
+ continue
hit = next((i for i, prev in enumerate(seen_texts) if is_near_dup(body, prev)), None)
if hit is not None:
if _keep_score(body) > _keep_score(seen_texts[hit]):
diff --git a/src/hermespace/access/oew.py b/src/hermespace/access/oew.py
index beb886e..8e60118 100644
--- a/src/hermespace/access/oew.py
+++ b/src/hermespace/access/oew.py
@@ -61,6 +61,7 @@ def auto_park_silent(
derive_plan,
is_filler_step,
is_near_dup,
+ is_user_echo_copy,
strip_slot_prefix,
)
@@ -99,6 +100,9 @@ def auto_park_silent(
body = strip_slot_prefix(c)
if not body or any(is_near_dup(body, s) for s in existing):
continue
+ # Skip user-sentence echoes; still park distinct short steps (Stop).
+ if is_user_echo_copy(body, msg, goal):
+ continue
js.reason_step(body, salience=0.78)
parked.append(body)
existing.append(body)
diff --git a/src/hermespace/cognition.py b/src/hermespace/cognition.py
index 096c422..2623131 100644
--- a/src/hermespace/cognition.py
+++ b/src/hermespace/cognition.py
@@ -94,9 +94,25 @@ def classify_message_load(user_message: str, n_concepts: int, n_plan: int) -> di
def compete_for_focus(slots: Iterable[Slot], cap: int = FOCUS_CAP) -> list[Slot]:
- """GWT-style competition: highest salience wins broadcast focus."""
+ """GWT-style competition: highest salience wins; shorter body on containment."""
+ from hermespace.execute_focus import _keep_score, gist_key, is_near_dup
+
ranked = sorted(slots, key=lambda s: s.salience, reverse=True)
- return ranked[:cap]
+ out: list[Slot] = []
+ for s in ranked:
+ hit = next((i for i, prev in enumerate(out) if is_near_dup(s.text, prev.text)), None)
+ if hit is None:
+ out.append(s)
+ continue
+ prev = out[hit]
+ if _keep_score(s.label()) > _keep_score(prev.label()):
+ out[hit] = s
+ elif (
+ _keep_score(s.label()) == _keep_score(prev.label())
+ and len(gist_key(s.text)) < len(gist_key(prev.text))
+ ):
+ out[hit] = s
+ return out[:cap]
def partition_buffers(slots: list[Slot]) -> dict[str, list[Slot]]:
diff --git a/src/hermespace/execute_focus.py b/src/hermespace/execute_focus.py
index f465843..9dd336f 100644
--- a/src/hermespace/execute_focus.py
+++ b/src/hermespace/execute_focus.py
@@ -87,6 +87,11 @@ def gist_key(text: str) -> str:
return " ".join(body.split())
+def foa_gist(text: str) -> str:
+ """Prefix-stripped gist; trailing punct already dropped."""
+ return gist_key(text)
+
+
def is_filler_step(text: str) -> bool:
return gist_key(text) in _FILLER_STEPS or strip_slot_prefix(text).casefold() in _FILLER_STEPS
@@ -168,21 +173,33 @@ def shape_focus(
plan: Sequence[str] | None = None,
cap: int = 4,
) -> list[str]:
- """FOA: pairwise-distinct verbal bodies. One bind, not prefixed copies."""
- _ = (message, goal, plan)
+ """FOA: pairwise-distinct verbal bodies. Bind + next action, not lang_stream."""
+ _ = plan
cleaned: list[str] = []
for raw in labels:
s = str(raw or "").strip()
if not s or is_protocol_slot(s) or is_filler_step(s):
continue
+ if "lang_stream:" in s.casefold():
+ continue
+ if is_user_echo_copy(s, message, goal):
+ continue
cleaned.append(s)
return collapse_near_dups(cleaned)[:cap]
-_HEAD_VERB = re.compile(
- r"^(Write|Open|Fix|Patch|Build|Add|Create|Update|Read|Run|Ship|Deploy|Stop|Verify|Test)\b"
-)
-_OBJECT_NOUN = re.compile(r"\b(README|TTL|PR|docs?|login|plugin|harness)\b", re.I)
+_FILLER_ADJ = {
+ "a",
+ "an",
+ "the",
+ "short",
+ "brief",
+ "quick",
+ "small",
+ "simple",
+ "little",
+}
+_PREP_STOP = {"for", "on", "with", "to", "from", "in", "of", "then", "after"}
def _short_action(text: str) -> str:
@@ -195,31 +212,65 @@ def _short_action(text: str) -> str:
return t[:80] if len(t) <= 80 else t[:79].rstrip() + "…"
-def short_verb_phrase(text: str) -> str:
- """Compress a long user clause to a short verb phrase (Write the README)."""
+def compress_action_phrase(text: str, *, cap: int = 40) -> str:
+ """Verb + object. Drop filler adj (short/a/an). Short steps stay as-is."""
t = _short_action(text)
if not t:
return ""
- if len(t.split()) <= 4:
- return t
- m = _HEAD_VERB.match(t)
- obj = _OBJECT_NOUN.search(t)
- if not m or not obj:
- return t
- noun = obj.group(0)
- if noun.lower() == "readme":
- noun = "README"
- return f"{m.group(1)} the {noun}"
+ words = t.split()
+ if len(words) <= 4:
+ return t if len(t) <= cap else t[: cap - 1].rstrip() + "…"
+ verb = words[0]
+ kept: list[str] = []
+ for w in words[1:]:
+ if w.casefold() in _FILLER_ADJ:
+ continue
+ if w.casefold() in _PREP_STOP:
+ break
+ kept.append(w)
+ if len(kept) >= 2:
+ break
+ if not kept:
+ return verb[:cap]
+ obj = kept[0]
+ if obj.lower() == "readme":
+ return f"{verb} the README"[:cap]
+ if obj[0].isupper():
+ return f"{verb} the {obj}"[:cap]
+ if len(kept) > 1:
+ return f"{verb} {' '.join(kept)}"[:cap]
+ return f"{verb} {obj}"[:cap]
+
+
+def short_verb_phrase(text: str) -> str:
+ return compress_action_phrase(text)
def _is_raw_user_echo(text: str, message: str = "", goal: str = "") -> bool:
- raw = " ".join((message or goal or "").split())
- if not raw or not text:
+ return is_user_echo_copy(text, message, goal)
+
+
+def is_user_echo_copy(text: str, message: str = "", goal: str = "") -> bool:
+ """Verbal copy of the user sentence / first clause. Bind and short steps stay."""
+ raw_text = (text or "").strip()
+ if not raw_text:
+ return False
+ if raw_text.casefold().startswith("[bind") or is_bind_restatement(raw_text):
+ return False
+ body = foa_gist(raw_text)
+ user = foa_gist(message or goal)
+ if raw_text.casefold().lstrip("[").startswith("verbal") or "lang_stream:" in raw_text.casefold():
+ if user and (body == user or body in user or user in body):
+ return True
+ if not body or len(body.split()) < 4:
+ return False
+ if not user:
return False
- if gist_key(text) == gist_key(raw):
+ if body == user:
return True
- parts = [p.strip(" .,") for p in _CLAUSE_SPLIT.split(raw) if p and p.strip()]
- if parts and gist_key(text) == gist_key(parts[0]) and len(gist_key(text).split()) >= 5:
+ if body in user and len(body) / max(len(user), 1) >= 0.55:
+ return True
+ if user in body and len(user) / max(len(body), 1) >= 0.55:
return True
return False
@@ -310,22 +361,16 @@ def next_action_line(
decision: str = "",
message: str = "",
) -> str:
- for step in plan_or_derived(plan, message, goal):
- s = str(step or "").strip()
- if not s or is_filler_step(s) or _is_bad_lead(s):
- continue
- phrase = short_verb_phrase(s)
+ steps = plan_or_derived(plan, message, goal)
+ if steps:
+ phrase = compress_action_phrase(steps[0])
if (
phrase
- and phrase != s
- and not _is_bad_lead(phrase)
and not is_filler_step(phrase)
- and not _is_raw_user_echo(phrase, message, goal)
+ and not _is_bad_lead(phrase)
+ and not is_user_echo_copy(phrase, message, goal)
):
- return phrase[:80]
- if _is_raw_user_echo(s, message, goal):
- continue
- return s[:160]
+ return phrase[:40]
for raw in (say or "").splitlines():
s = raw.strip().lstrip("-* ").lstrip("0123456789.) ")
if s and not _is_bad_lead(s):
diff --git a/src/hermespace/neural_space.py b/src/hermespace/neural_space.py
index fb86b1d..51dfdef 100644
--- a/src/hermespace/neural_space.py
+++ b/src/hermespace/neural_space.py
@@ -91,13 +91,23 @@ def sync_from_desk(self, desk: Desk, *, user_message: str = "") -> dict[str, Any
if not self.config.enable or skip:
return {"enabled": False, "skipped": skip}
- from hermespace.execute_focus import collapse_near_dups, is_filler_step, shape_focus
+ from hermespace.execute_focus import (
+ collapse_near_dups,
+ is_filler_step,
+ is_user_echo_copy,
+ shape_focus,
+ strip_slot_prefix,
+ )
query = user_message or desk.goal or desk.say
self.field.set_query(query)
for raw in desk.concepts:
slot = parse_slot(raw)
+ if slot.text.casefold().startswith("lang_stream:"):
+ continue
+ if is_user_echo_copy(slot.text, user_message, desk.goal):
+ continue
self.field.add(
slot.text,
energy=slot.salience,
@@ -133,9 +143,12 @@ def sync_from_desk(self, desk: Desk, *, user_message: str = "") -> dict[str, Any
)
bodies = {parse_slot(c).text for c in new_concepts}
for t in ignited:
- if t.text not in bodies:
- new_concepts.append(f"[{t.modality}|{min(1.0, t.energy):.2f}] {t.text}")
- bodies.add(t.text)
+ if t.text in bodies or t.text.casefold().startswith("lang_stream:"):
+ continue
+ if is_user_echo_copy(t.text, user_message, desk.goal):
+ continue
+ new_concepts.append(f"[{t.modality}|{min(1.0, t.energy):.2f}] {t.text}")
+ bodies.add(t.text)
for v in verbalized:
if v not in bodies:
new_concepts.append(f"[verbal|0.80] {v}")
@@ -150,7 +163,15 @@ def sync_from_desk(self, desk: Desk, *, user_message: str = "") -> dict[str, Any
)
snap = self.field.snapshot()
- snap["focus"] = collapse_near_dups(list(snap.get("focus") or []))
+ snap["focus"] = [
+ strip_slot_prefix(x) if str(x).startswith("[") else str(x)
+ for x in shape_focus(
+ list(snap.get("focus") or []),
+ message=user_message,
+ goal=desk.goal,
+ plan=desk.plan,
+ )
+ ]
snap["backend"] = self.config.backend
snap["embed_model"] = getattr(self.embed_backend, "model", "") or self.config.backend
snap["enabled"] = True
diff --git a/tests/test_desk_quality.py b/tests/test_desk_quality.py
index 266b90c..b8ea2de 100644
--- a/tests/test_desk_quality.py
+++ b/tests/test_desk_quality.py
@@ -81,13 +81,12 @@ def test_message_only_report_and_foa(self) -> None:
self.assertNotEqual(low, LIVE_MSG.casefold())
first_clause = "write a short readme for the auth fix"
self.assertNotEqual(low, first_clause)
- self.assertLessEqual(len(line1.split()), 5, line1)
- self.assertIn("readme", low)
+ self.assertEqual(line1, "Write the README")
self.assertEqual(line1, next_action_line(message=LIVE_MSG, say="", plan=[]))
- self.assertNotEqual(list(out.plan or []), ["execute"])
- self.assertGreaterEqual(len(out.plan or []), 1)
- self.assertLessEqual(len(out.plan or []), 3)
- self.assertTrue(any("stop" in str(p).casefold() for p in out.plan))
+ self.assertEqual(
+ list(out.plan or []),
+ ["Write a short README for the auth fix", "Stop"],
+ )
focus = list(out.meta.get("focus") or []) if isinstance(out.meta, dict) else []
desk = load_desk()
@@ -96,30 +95,46 @@ def test_message_only_report_and_foa(self) -> None:
self.assertLessEqual(len(focus), 4)
_assert_pairwise_distinct(self, focus, "FOA")
bodies = _verbal_bodies(focus)
- joined = " ".join(bodies).casefold()
- self.assertTrue("readme" in joined or "bind" in joined or "stop" in joined, focus)
- # First-cut FOA: one bind, not four prefixed copies of the user sentence.
bind_n = sum(1 for x in focus if str(x).casefold().startswith("[bind") or " | " in str(x))
- self.assertLessEqual(bind_n, 1, focus)
- lang_n = sum(1 for x in focus if "lang_stream:" in str(x).casefold())
- self.assertEqual(lang_n, 0, focus)
+ self.assertEqual(bind_n, 1, focus)
+ self.assertTrue(any(gist_key(x) == gist_key("Write the README") for x in focus), focus)
+ self.assertFalse(any("lang_stream:" in str(x).casefold() for x in focus), focus)
copies = sum(1 for x in bodies if gist_key(x) == gist_key(LIVE_MSG))
self.assertLessEqual(copies, 1, focus)
- hub = []
neural_focus = []
if isinstance(out.meta, dict):
- access = out.meta.get("access") or {}
neural = out.meta.get("neural") or {}
neural_focus = list(neural.get("focus") or [])
from hermespace.access import AccessHub
from hermespace.access.engine import workspace_id
- js = AccessHub(agent_id=workspace_id(out.meta.get("agent_id") or "hermes-agent", out.session_id or "default"))
+ js = AccessHub(
+ agent_id=workspace_id(
+ (out.meta or {}).get("agent_id") or "hermes-agent",
+ out.session_id or "default",
+ )
+ )
+ hub_focus = list(js.state.focus or [])
hub = [c.text for c in js.state.hub]
_assert_pairwise_distinct(self, hub, "hub")
- if neural_focus:
- _assert_pairwise_distinct(self, neural_focus, "neural")
+ _assert_pairwise_distinct(self, hub_focus, "hub-focus")
+ self.assertFalse(any("lang_stream:" in str(x).casefold() for x in hub_focus), hub_focus)
+ self.assertFalse(
+ any("step:" in str(x).casefold() and "stop" in str(x).casefold() for x in hub_focus),
+ hub_focus,
+ )
+ self.assertTrue(neural_focus, "neural focus missing")
+ _assert_pairwise_distinct(self, neural_focus, "neural")
+ self.assertFalse(any("lang_stream:" in str(x).casefold() for x in neural_focus), neural_focus)
+ self.assertTrue(
+ any(" | " in str(x) or "plan:" in str(x).casefold() for x in neural_focus),
+ neural_focus,
+ )
+ self.assertTrue(
+ any(gist_key(x) == gist_key("Write the README") for x in neural_focus),
+ neural_focus,
+ )
def test_thanks_still_skips(self) -> None:
from hermespace.io_contract import HermespaceInput
@@ -148,9 +163,7 @@ def test_shape_empty_say_uses_next_action(self) -> None:
self.assertNotIn("production:", line1)
self.assertNotIn("partner:", line1)
self.assertNotIn("A — proceed", line1)
- self.assertIn("README", line1)
- self.assertNotEqual(line1.casefold(), LIVE_MSG.casefold())
- self.assertLessEqual(len(line1.split()), 5)
+ self.assertEqual(line1, "Write the README")
def test_access_lens_title(self) -> None:
from hermespace.access import AccessEnv
From d770c43dda3a2ddaa78c0185ce09bc96c331ab9f Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sun, 16 Aug 2026 00:44:50 +0000
Subject: [PATCH 31/32] fix(inject): put hub silent on the T2 model context
Hub already keeps T1 silent_steps. Append last-3 gist-filtered lines
after bound_strip so the agent sees parked thought on the next inject.
No pending_silent pump, no broadcast, no lens. Skip high/protect.
Co-authored-by: Pablo
---
src/hermespace/context_surgery.py | 37 ++++++++++
src/hermespace/hermes_bridge.py | 13 ++++
src/hermespace/workflow.py | 9 ++-
tests/test_cognition_chain.py | 108 ++++++++++++++++++++++++++++++
tests/test_context_surgery.py | 44 ++++++++++++
5 files changed, 210 insertions(+), 1 deletion(-)
create mode 100644 tests/test_cognition_chain.py
diff --git a/src/hermespace/context_surgery.py b/src/hermespace/context_surgery.py
index 2e99ae1..2e78133 100644
--- a/src/hermespace/context_surgery.py
+++ b/src/hermespace/context_surgery.py
@@ -145,3 +145,40 @@ def harvest_on_inject(text: str) -> bool:
"""True if harvest prose leaked onto the 9k/inject path (must stay false)."""
low = (text or "").casefold()
return "dream_harvest" in low or "harvest items" in low
+
+
+def silent_chain_strip(
+ silent_steps: Iterable[str] | None,
+ user_message: str = "",
+ *,
+ last_n: int = 3,
+ cap: int = SILENT_CHAIN_CAP,
+) -> str:
+ """Last hub silent lines for the model inject. No broadcast, no lens.
+
+ Takes the last ``last_n`` parked steps, drops gist-echoes of the current
+ user message (T2 restates), and caps at 8. Operator lens / full OEW
+ broadcast stay off this path.
+ """
+ from hermespace.execute_focus import is_near_dup, is_user_echo_copy, strip_slot_prefix
+
+ raw = [str(s).strip() for s in (silent_steps or []) if str(s or "").strip()]
+ if not raw:
+ return ""
+ window = raw[-max(1, int(last_n)) :]
+ limit = max(1, min(int(cap), SILENT_CHAIN_CAP))
+ kept: list[str] = []
+ for step in window:
+ body = strip_slot_prefix(step)
+ if not body:
+ continue
+ if is_user_echo_copy(body, user_message):
+ continue
+ if any(is_near_dup(body, prev) for prev in kept):
+ continue
+ kept.append(body[:160])
+ if len(kept) >= limit:
+ break
+ if not kept:
+ return ""
+ return "\n".join(["### Silent", *[f"- {body}" for body in kept]])
diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py
index 8dc76d7..ff913a5 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -22,6 +22,7 @@ def _truthy(name: str, default: str = "0") -> bool:
is_fluent_ack,
is_shared_hub_child,
sanitize_inject,
+ silent_chain_strip,
strip_needed,
)
@@ -570,6 +571,18 @@ def on_pre_llm_call(
parts = [dual_decode_line(), desk_block]
if has_bind:
parts.append(bound_strip)
+ if not high_load:
+ try:
+ from hermespace.access import AccessHub
+
+ chain = silent_chain_strip(
+ AccessHub(agent_id=access_id).state.silent_steps,
+ msg,
+ )
+ if chain:
+ parts.append(chain)
+ except Exception:
+ pass
# One organ strip if it fits: Insight card preferred, else Cube (never dual-pump).
organ = ""
if not high_load:
diff --git a/src/hermespace/workflow.py b/src/hermespace/workflow.py
index d600069..4c873ae 100644
--- a/src/hermespace/workflow.py
+++ b/src/hermespace/workflow.py
@@ -278,6 +278,7 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
assemble_inject,
dual_decode_line,
inject_budget,
+ silent_chain_strip,
)
load_level = str(desk.load.get("level") or "mid") if isinstance(desk.load, dict) else "mid"
@@ -302,9 +303,15 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
bound = bound_protocol_lines(env)
if bound:
parts.append(bound)
+ # Hub already keeps T1 silent; put last parked lines on the inject.
+ # No full oew_broadcast, no lens.
+ js = AccessHub(agent_id=access_id)
+ if not high:
+ chain = silent_chain_strip(js.state.silent_steps, msg)
+ if chain:
+ parts.append(chain)
# Lens is operator-only — never append the readout to model context.
# Summon still paints the operator Report, not the inject.
- js = AccessHub(agent_id=access_id)
if js.parse_modulation(msg).get("summon"):
report = (report + "\n\n" + env.lens_markdown(include_silent=False)).strip()
report = env.shape_user_report(report)
diff --git a/tests/test_cognition_chain.py b/tests/test_cognition_chain.py
new file mode 100644
index 0000000..006748f
--- /dev/null
+++ b/tests/test_cognition_chain.py
@@ -0,0 +1,108 @@
+"""Two-turn silent chain on the model inject. Hub already keeps T1 silent."""
+
+from __future__ import annotations
+
+import os
+import tempfile
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+import sys
+
+sys.path.insert(0, str(ROOT / "src"))
+
+T1 = "Write a short README for the auth fix, then stop."
+T2 = "Now write the install section."
+
+
+class TestSilentChainInject(unittest.TestCase):
+ def setUp(self) -> None:
+ self.tmp = tempfile.TemporaryDirectory()
+ os.environ["HERMESPACE_HOME"] = self.tmp.name
+ os.environ["HERMESPACE_AGENT_ID"] = "chain-agent"
+ os.environ["HERMESPACE_SKIP_NEURAL"] = "1"
+ os.environ["HERMESPACE_NEURAL_VERBALIZE"] = "0"
+ os.environ["HERMESPACE_OEW"] = "1"
+
+ def tearDown(self) -> None:
+ self.tmp.cleanup()
+ for key in (
+ "HERMESPACE_HOME",
+ "HERMESPACE_AGENT_ID",
+ "HERMESPACE_SKIP_NEURAL",
+ "HERMESPACE_NEURAL_VERBALIZE",
+ "HERMESPACE_OEW",
+ ):
+ os.environ.pop(key, None)
+
+ def _hub(self, session_id: str = "chain"):
+ from hermespace.access import AccessHub
+ from hermespace.access.engine import workspace_id
+
+ return AccessHub(agent_id=workspace_id("chain-agent", session_id))
+
+ def test_t2_inject_carries_t1_parked_silent(self) -> None:
+ from hermespace.io_contract import HermespaceInput
+ from hermespace.workflow import Workflow
+
+ wf = Workflow()
+ sid = "chain"
+ t1 = wf.run(
+ HermespaceInput(
+ message=T1,
+ say="",
+ goal="",
+ plan=[],
+ session_id=sid,
+ agent_id="chain-agent",
+ )
+ )
+ self.assertFalse(t1.skipped, t1.reason)
+ parked = [str(s) for s in self._hub(sid).state.silent_steps if str(s).strip()]
+ self.assertTrue(parked, "T1 must park silent on the hub")
+ parked_l = " ".join(parked).casefold()
+ self.assertTrue(
+ "stop" in parked_l or "readme" in parked_l,
+ parked,
+ )
+
+ t2 = wf.run(
+ HermespaceInput(
+ message=T2,
+ say="",
+ goal="",
+ plan=[],
+ session_id=sid,
+ agent_id="chain-agent",
+ )
+ )
+ self.assertFalse(t2.skipped, t2.reason)
+ ctx = t2.context or ""
+ self.assertLessEqual(len(ctx), 2800)
+ self.assertIn("### Silent", ctx)
+ self.assertNotIn("J-Lens readout", ctx)
+ self.assertNotIn("What Hermes has on its mind", ctx)
+ silent_body = ctx.split("### Silent", 1)[-1]
+ self.assertIn("Stop", silent_body)
+ self.assertNotIn(T2, silent_body)
+
+ thanks = wf.run(
+ HermespaceInput(
+ message="thanks",
+ session_id=sid,
+ agent_id="chain-agent",
+ )
+ )
+ self.assertTrue(thanks.skipped)
+ self.assertEqual(thanks.reason, "trivial_ack")
+ after = [str(s) for s in self._hub(sid).state.silent_steps if str(s).strip()]
+ self.assertTrue(after, "thanks must not wipe T1 silent")
+ self.assertTrue(
+ any("stop" in s.casefold() or "readme" in s.casefold() for s in after),
+ after,
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_context_surgery.py b/tests/test_context_surgery.py
index bfa6a2b..ce52048 100644
--- a/tests/test_context_surgery.py
+++ b/tests/test_context_surgery.py
@@ -67,6 +67,27 @@ def test_budgets_and_fluent_ack(self) -> None:
)
blob = assemble_inject(["a" * 4000, "b" * 4000], budget=2800)
self.assertLessEqual(len(blob), 2800)
+ from hermespace.context_surgery import silent_chain_strip
+
+ t2 = "Now write the install section."
+ strip = silent_chain_strip(
+ [
+ "older-noise",
+ "Write the README",
+ "Stop",
+ t2,
+ ],
+ t2,
+ )
+ self.assertIn("### Silent", strip)
+ self.assertIn("Stop", strip)
+ self.assertIn("Write the README", strip)
+ self.assertNotIn("older-noise", strip)
+ self.assertNotIn(t2, strip)
+ self.assertNotIn("What Hermes has on its mind", strip)
+ self.assertNotIn("J-Lens", strip)
+ self.assertLessEqual(strip.count("\n- "), 3)
+ self.assertEqual(silent_chain_strip([], t2), "")
dirty = sanitize_inject("keep\nJ-Lens readout: secret\ntrue self-conscious\nok")
self.assertIn("keep", dirty)
self.assertNotIn("J-Lens", dirty)
@@ -101,6 +122,29 @@ def test_pre_llm_skips_fluent_and_respects_mid_cap(self) -> None:
self.assertNotIn("true self-conscious", ctx)
self.assertNotIn("phenomenal consciousness", ctx)
+ def test_pre_llm_appends_hub_silent_after_bound(self) -> None:
+ from hermespace.access import AccessHub
+ from hermespace.access.engine import workspace_id
+ from hermespace.hermes_bridge import on_pre_llm_call, on_session_start
+
+ on_session_start(session_id="surgery-silent")
+ js = AccessHub(agent_id=workspace_id("surgery-agent", "surgery-silent"))
+ js.reason_step("Stop", salience=0.82)
+ js.save()
+ inj = on_pre_llm_call(
+ user_message="Now write the install section.",
+ session_id="surgery-silent",
+ is_first_turn=False,
+ )
+ ctx = (inj or {}).get("context") or ""
+ self.assertTrue(ctx, inj)
+ self.assertLessEqual(len(ctx), 2800)
+ self.assertIn("### Silent", ctx)
+ self.assertIn("Stop", ctx.split("### Silent", 1)[-1])
+ self.assertNotIn("Now write the install section.", ctx.split("### Silent", 1)[-1])
+ self.assertNotIn("J-Lens readout", ctx)
+ self.assertNotIn("What Hermes has on its mind", ctx)
+
def test_high_load_injects_nothing_without_bind(self) -> None:
from hermespace import AccessEngine
from hermespace.hermes_bridge import on_pre_llm_call, on_session_start
From e1327208d59a49acc62547dac4951e785974a671 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sun, 16 Aug 2026 00:47:45 +0000
Subject: [PATCH 32/32] fix(cognition): live-goal refresh plus Silent (prior)
on inject
Empty-goal T2 that is not a restatement parks the previous goal,
re-derives plan (strip now/please/just), and clears leftover say.
Hub silent still rides the inject as ### Silent (prior).
Co-authored-by: Pablo
---
src/hermespace/context_surgery.py | 8 ++++++-
src/hermespace/execute_focus.py | 30 ++++++++++++++++++++++++++
src/hermespace/workflow.py | 35 +++++++++++++++++++++----------
tests/test_cognition_chain.py | 28 ++++++++++++++++++++++---
tests/test_context_surgery.py | 11 ++++++----
5 files changed, 93 insertions(+), 19 deletions(-)
diff --git a/src/hermespace/context_surgery.py b/src/hermespace/context_surgery.py
index 2e78133..944fa80 100644
--- a/src/hermespace/context_surgery.py
+++ b/src/hermespace/context_surgery.py
@@ -181,4 +181,10 @@ def silent_chain_strip(
break
if not kept:
return ""
- return "\n".join(["### Silent", *[f"- {body}" for body in kept]])
+ lines = ["### Silent (prior)"]
+ for body in kept:
+ if body.casefold().startswith("plan:"):
+ lines.append(f"- {body}")
+ else:
+ lines.append(f"- plan: {body}")
+ return "\n".join(lines)
diff --git a/src/hermespace/execute_focus.py b/src/hermespace/execute_focus.py
index 9dd336f..ba1964f 100644
--- a/src/hermespace/execute_focus.py
+++ b/src/hermespace/execute_focus.py
@@ -200,11 +200,16 @@ def shape_focus(
"little",
}
_PREP_STOP = {"for", "on", "with", "to", "from", "in", "of", "then", "after"}
+_LEAD_SKIP = {"now", "please", "just", "okay", "ok"}
def _short_action(text: str) -> str:
t = " ".join((text or "").split()).strip(" .,")
t = re.sub(r"^(then|after that|after|finally|and)\s+", "", t, flags=re.I)
+ words = t.split()
+ while words and words[0].casefold() in _LEAD_SKIP:
+ words = words[1:]
+ t = " ".join(words)
if not t or is_filler_step(t):
return ""
if t[0].islower():
@@ -275,6 +280,26 @@ def is_user_echo_copy(text: str, message: str = "", goal: str = "") -> bool:
return False
+def message_is_new_goal(message: str, existing_goal: str) -> bool:
+ """True when an empty-payload message should replace the live goal.
+
+ Restatement or gist-contained in the current goal → keep goal/plan/say.
+ """
+ msg = " ".join((message or "").split()).strip()
+ goal = " ".join((existing_goal or "").split()).strip()
+ if not msg or not goal:
+ return False
+ gm, gg = gist_key(msg), gist_key(goal)
+ if not gm or not gg or gm == gg:
+ return False
+ shorter, longer = (gm, gg) if len(gm) <= len(gg) else (gg, gm)
+ if len(shorter) >= 4 and shorter in longer:
+ return False
+ if is_user_echo_copy(msg, goal) or is_user_echo_copy(goal, msg):
+ return False
+ return True
+
+
def derive_plan(message: str, *, max_n: int = 3) -> list[str]:
"""1–3 real steps from a user sentence. Never the filler ``execute``."""
msg = " ".join((message or "").strip().split())
@@ -292,6 +317,11 @@ def derive_plan(message: str, *, max_n: int = 3) -> list[str]:
return out[:max_n]
+def derive_plan_steps(message: str, *, max_n: int = 3) -> list[str]:
+ """Derive plan from a new live-goal message (leading now/please/just stripped)."""
+ return derive_plan(message, max_n=max_n)
+
+
def plan_or_derived(
plan: Sequence[str] | None,
message: str = "",
diff --git a/src/hermespace/workflow.py b/src/hermespace/workflow.py
index 4c873ae..45c0748 100644
--- a/src/hermespace/workflow.py
+++ b/src/hermespace/workflow.py
@@ -88,21 +88,34 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
return out
# 2–5 desk
- from hermespace.execute_focus import plan_or_derived
+ from hermespace.execute_focus import (
+ derive_plan_steps,
+ message_is_new_goal,
+ plan_or_derived,
+ )
- g = payload.goal or existing.goal or msg[:200]
+ provided_goal = (payload.goal or "").strip()
+ # normalized() copies message → goal when the operator left goal empty.
+ goal_from_msg = provided_goal in {"", msg, msg[:200]}
+ msg_goal = goal_from_msg and message_is_new_goal(msg, existing.goal)
+ if msg_goal:
+ g = msg[:200]
+ pl = list(payload.plan or derive_plan_steps(msg))
+ sy = payload.say or ""
+ else:
+ g = payload.goal or existing.goal or msg[:200]
+ pl = plan_or_derived(payload.plan or existing.plan, msg, g)
+ sy = payload.say if payload.say else existing.say
dec = payload.decision or existing.decision or "A — proceed"
- pl = plan_or_derived(payload.plan or existing.plan, msg, g)
- sy = payload.say if payload.say else existing.say
cons = payload.concepts or existing.concepts
ch = payload.choices or existing.choices or ["A — proceed"]
- if (
- existing.goal
- and payload.goal
- and existing.goal.strip()
- and payload.goal.strip()
- and existing.goal.strip() != payload.goal.strip()
+ if existing.goal.strip() and (
+ msg_goal
+ or (
+ (payload.goal or "").strip()
+ and existing.goal.strip() != payload.goal.strip()
+ )
):
try:
from hermespace.workbench import Workbench
@@ -120,7 +133,7 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
except Exception:
pass
- if payload.force or not existing.goal or payload.goal:
+ if payload.force or not existing.goal or payload.goal or msg_goal:
desk = self.engine.enter(
goal=g,
concepts=list(cons or []),
diff --git a/tests/test_cognition_chain.py b/tests/test_cognition_chain.py
index 006748f..33f8bb7 100644
--- a/tests/test_cognition_chain.py
+++ b/tests/test_cognition_chain.py
@@ -1,4 +1,4 @@
-"""Two-turn silent chain on the model inject. Hub already keeps T1 silent."""
+"""Two-turn silent chain + live-goal refresh. Hub already keeps T1 silent."""
from __future__ import annotations
@@ -42,6 +42,17 @@ def _hub(self, session_id: str = "chain"):
return AccessHub(agent_id=workspace_id("chain-agent", session_id))
+ def test_message_is_new_goal(self) -> None:
+ from hermespace.execute_focus import message_is_new_goal
+
+ self.assertTrue(message_is_new_goal(T2, T1))
+ self.assertFalse(message_is_new_goal(T1, T1))
+ self.assertFalse(
+ message_is_new_goal("Write a short README for the auth fix", T1)
+ )
+ self.assertFalse(message_is_new_goal("", T1))
+ self.assertFalse(message_is_new_goal(T2, ""))
+
def test_t2_inject_carries_t1_parked_silent(self) -> None:
from hermespace.io_contract import HermespaceInput
from hermespace.workflow import Workflow
@@ -59,6 +70,11 @@ def test_t2_inject_carries_t1_parked_silent(self) -> None:
)
)
self.assertFalse(t1.skipped, t1.reason)
+ self.assertEqual((t1.report or "").splitlines()[0].strip(), "Write the README")
+ self.assertEqual(
+ list(t1.plan or []),
+ ["Write a short README for the auth fix", "Stop"],
+ )
parked = [str(s) for s in self._hub(sid).state.silent_steps if str(s).strip()]
self.assertTrue(parked, "T1 must park silent on the hub")
parked_l = " ".join(parked).casefold()
@@ -78,12 +94,18 @@ def test_t2_inject_carries_t1_parked_silent(self) -> None:
)
)
self.assertFalse(t2.skipped, t2.reason)
+ line1 = (t2.report or "").splitlines()[0].strip()
+ self.assertNotEqual(line1, "Write the README")
+ self.assertIn("install", line1.casefold())
+ self.assertEqual(line1, "Write the install section")
+ self.assertEqual(list(t2.plan or []), ["Write the install section"])
ctx = t2.context or ""
self.assertLessEqual(len(ctx), 2800)
- self.assertIn("### Silent", ctx)
+ self.assertIn("### Silent (prior)", ctx)
+ self.assertIn("plan: Stop", ctx)
self.assertNotIn("J-Lens readout", ctx)
self.assertNotIn("What Hermes has on its mind", ctx)
- silent_body = ctx.split("### Silent", 1)[-1]
+ silent_body = ctx.split("### Silent (prior)", 1)[-1]
self.assertIn("Stop", silent_body)
self.assertNotIn(T2, silent_body)
diff --git a/tests/test_context_surgery.py b/tests/test_context_surgery.py
index ce52048..f52727a 100644
--- a/tests/test_context_surgery.py
+++ b/tests/test_context_surgery.py
@@ -79,7 +79,7 @@ def test_budgets_and_fluent_ack(self) -> None:
],
t2,
)
- self.assertIn("### Silent", strip)
+ self.assertIn("### Silent (prior)", strip)
self.assertIn("Stop", strip)
self.assertIn("Write the README", strip)
self.assertNotIn("older-noise", strip)
@@ -139,9 +139,12 @@ def test_pre_llm_appends_hub_silent_after_bound(self) -> None:
ctx = (inj or {}).get("context") or ""
self.assertTrue(ctx, inj)
self.assertLessEqual(len(ctx), 2800)
- self.assertIn("### Silent", ctx)
- self.assertIn("Stop", ctx.split("### Silent", 1)[-1])
- self.assertNotIn("Now write the install section.", ctx.split("### Silent", 1)[-1])
+ self.assertIn("### Silent (prior)", ctx)
+ self.assertIn("Stop", ctx.split("### Silent (prior)", 1)[-1])
+ self.assertNotIn(
+ "Now write the install section.",
+ ctx.split("### Silent (prior)", 1)[-1],
+ )
self.assertNotIn("J-Lens readout", ctx)
self.assertNotIn("What Hermes has on its mind", ctx)