diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..fa8fe65
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,73 @@
+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: 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
+ 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
+ python -m build
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/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..2ab76a2 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)
@@ -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/LAYOUT.md b/LAYOUT.md
new file mode 100644
index 0000000..5244e63
--- /dev/null
+++ b/LAYOUT.md
@@ -0,0 +1,60 @@
+# Hermespace codespace layout
+
+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
+
+```
+hermespace/
+├── PURPOSE.md / ABOUT.md / README.md / LAYOUT.md
+├── src/hermespace/ # Python package
+│ ├── 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)
+│ ├── 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/
+│ ├── access/ # Access Workspace 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 / 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 |
+| 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..4aafc26 100644
--- a/PURPOSE.md
+++ b/PURPOSE.md
@@ -1,89 +1,206 @@
# PURPOSE.md — Hermespace north star
-**One line:** Hermespace is the **true external J-Space 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)**. 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)**.
+## 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.
+
+**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. 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.
---
-## 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 J-Space 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 |
-|-------|-----|-----------|
-| **J-Space 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 |
+---
+
+## 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.
-## Anthropic → Hermespace map
+### After a turn
-| 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 |
+`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.
-## Standalone vs Cube-powered
+### Across concurrent sessions
-| Mode | Warehouse | Night path |
-|------|-----------|------------|
-| Standalone | Semantic + World | harvest → semantic/world |
-| Heart / Center | `memory.cube` | harvest → seal_learning → CubeDream |
+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 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
+via fail-soft imports (`cube_module`, `insight_module`). Neither is required.
+
+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.
+
+---
-## Non-goals
+## Hermes Agent compatibility target
-- 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
+Primary target: **Hermes Agent v0.20.0+**.
+
+Hermespace uses the current public plugin contracts:
+
+- `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` (harvest ≤10s, fail-open)
+- `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
+```
+
+---
-## Success metrics
+## Product direction
-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
+Deepen the **Access Engine**, not the feature count.
-## Version posture
+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.
-Deepen the *environment* (observe + intervene + dream), not a second archive.
-Plugin yaml + `__version__` move together.
+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 d24db2d..9792fa9 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,15 +25,27 @@
-**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 [J-Space](https://github.com/anomalyco/j-space). Not a second agent runtime. A room inside Hermes that remembers everything.
+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.
---
## 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
@@ -52,7 +64,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`.
@@ -96,13 +110,13 @@ 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()`.
### 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
@@ -130,10 +144,11 @@ 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 |
+| **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 — `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. |
@@ -142,11 +157,20 @@ Alongside the world, Hermespace provides a desk for the current turn — FOA, du
### Quick start
+```bash
+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:
+
```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 +187,17 @@ 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_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 |
+| `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` | Harvest ≤10s fail-open, world leave, idle maintenance |
+| `subagent_start/stop` | Track specialist lifecycle for runtime observability |
---
@@ -184,7 +216,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
@@ -198,6 +230,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
@@ -211,7 +246,8 @@ $HERMESPACE_HOME/memory/hermespace/
| Command | Purpose |
|---------|---------|
| `hs world show\|enter\|leave\|evolve\|search\|archive-stats` | Persistent world |
-| `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 |
@@ -228,22 +264,28 @@ $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 |
+| [`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/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-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/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 |
+| [`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) |
-| [`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 +294,19 @@ $HERMESPACE_HOME/memory/hermespace/
## Repository Layout
```text
-assets/ media (banners, diagrams)
+LAYOUT.md codespace map (start here for structure)
src/hermespace/ runtime package
+ 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
+docs/ access · 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/__init__.py b/__init__.py
new file mode 100644
index 0000000..39cede7
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,28 @@
+"""Hermes source-repository plugin entry point.
+
+``hermes plugins install PabloTheThinker/hermespace --enable`` clones this
+repository as one plugin. The checkout folder is often named ``hermespace``,
+which would shadow ``src/hermespace``. Pop this shim from ``sys.modules``
+before importing the real ``register``.
+"""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+_ROOT = Path(__file__).resolve().parent
+_SRC = _ROOT / "src"
+
+# 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))
+
+# Parent-cwd / folder-name shadow: drop this file so src/hermespace can load.
+if __name__ == "hermespace":
+ sys.modules.pop("hermespace", None)
+
+from hermespace.plugin import register
+
+__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/desktop_plugin/hermespace/README.md b/desktop_plugin/hermespace/README.md
index d3646c5..6ded0eb 100644
--- a/desktop_plugin/hermespace/README.md
+++ b/desktop_plugin/hermespace/README.md
@@ -8,10 +8,10 @@
## 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 |
+| **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 41fc690..c68f966 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
*/
@@ -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,31 +737,39 @@ 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',
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',
@@ -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/docs/00-jspace-to-hermespace.md b/docs/00-jspace-to-hermespace.md
deleted file mode 100644
index 0217c12..0000000
--- a/docs/00-jspace-to-hermespace.md
+++ /dev/null
@@ -1,45 +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=...)`.
-Space still owns FOA competition and dual decode.
-
-## 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/CODEMAP.md b/docs/CODEMAP.md
deleted file mode 100644
index 449828e..0000000
--- a/docs/CODEMAP.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# Code map — where to edit
-
-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)
-
-## Layers (edit here first)
-
-```
-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
-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
-```
-
-## Turn spine (L1)
-
-| Module | Role |
-|--------|------|
-| `workflow.py` | GATE→ENCODE→DESK→PLAN→DECODE→BROADCAST→SEAL + Cube/J-Space |
-| `engine.py` | enter / update / seal desk |
-| `desk.py` | ACTIVE.md model |
-| `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 |
-|--------|------|
-| `cube_module.py` | `cube_beat` / `cube_pulse` / `seal_learning` / standalone |
-
-## Integration
-
-| Path | Role |
-|------|------|
-| `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 |
-
-## 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.
diff --git a/docs/HERMESCUBE.md b/docs/HERMESCUBE.md
deleted file mode 100644
index ff749fa..0000000
--- a/docs/HERMESCUBE.md
+++ /dev/null
@@ -1,71 +0,0 @@
-# HermesCube × Hermespace — heart / generator contract
-
-**HermesCube is the heart** (when installed). **Hermespace is the nervous FOA** —
-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`
-
-```
-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
-```
-
-## Authority
-
-| 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 |
-| SemanticStore | Mirror / study | Local seal target |
-
-## Space adapter (`hermespace.cube_module`)
-
-```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
- seal_learning,
- cube_inject,
- strip_budget,
-)
-```
-
-| Call | Use |
-|------|-----|
-| `ensure_heart()` | `Workbench.enter` / session start / pulse |
-| `cube_beat(query, seals=, load=)` | Turn / `pre_llm_call` |
-| `cube_pulse(agent_id=)` | `idle_tick` / `world_evolve` |
-| `seal_learning(text)` | `remember_learning` / turn seal |
-| `center_status()` | Doctor / desktop |
-
-Load tiers → strip chars: low 900 · mid 640 · high 420 · protect 280.
-
-## Functional J-Space
-
-See [00-jspace-to-hermespace.md](00-jspace-to-hermespace.md) and `hermespace.jspace`.
-
-```bash
-hs jspace hold -t "deploy pipeline"
-hs jspace report
-hs jspace broadcast
-hs cube status
-hs cube beat -q "what do we believe about deploys?"
-```
-
-## Install both
-
-```bash
-hermes plugins install PabloTheThinker/hermescube
-# … then Hermespace install
-./scripts/install_hermes.sh
-```
-
-Soft dependency: if Cube is missing, Space inject/seal/pulse use the standalone warehouse.
diff --git a/docs/INDEX.md b/docs/INDEX.md
index a8b034a..6b1d86f 100644
--- a/docs/INDEX.md
+++ b/docs/INDEX.md
@@ -1,34 +1,25 @@
# 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 |
+| [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 |
+| [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..3974696
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,15 @@
+# Documentation
+
+| Folder | Contents |
+|--------|----------|
+| [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 |
+| [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:** [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/10-jspace-claude.md b/docs/access/10-claude-research.md
similarity index 93%
rename from docs/10-jspace-claude.md
rename to docs/access/10-claude-research.md
index e424a42..8998a43 100644
--- a/docs/10-jspace-claude.md
+++ b/docs/access/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/access/27-environment.md
similarity index 97%
rename from docs/27-jspace-environment.md
rename to docs/access/27-environment.md
index f73ab6f..957df93 100644
--- a/docs/27-jspace-environment.md
+++ b/docs/access/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/access/29-baars-changeux-anthropic.md b/docs/access/29-baars-changeux-anthropic.md
new file mode 100644
index 0000000..f3159d6
--- /dev/null
+++ b/docs/access/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/access/30-day-to-day-higher-order.md b/docs/access/30-day-to-day-higher-order.md
new file mode 100644
index 0000000..ca2fc7f
--- /dev/null
+++ b/docs/access/30-day-to-day-higher-order.md
@@ -0,0 +1,112 @@
+# 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)
+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/access/31-anthropic-x-video-deep-dive.md b/docs/access/31-anthropic-x-video-deep-dive.md
new file mode 100644
index 0000000..a00022b
--- /dev/null
+++ b/docs/access/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/access/32-hermespace-access-engine.md b/docs/access/32-hermespace-access-engine.md
new file mode 100644
index 0000000..d3e6e87
--- /dev/null
+++ b/docs/access/32-hermespace-access-engine.md
@@ -0,0 +1,134 @@
+# 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)
+
+### 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
+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`
+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/docs/access/34-access-engine.md b/docs/access/34-access-engine.md
new file mode 100644
index 0000000..6e9c94e
--- /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.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
+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/access/thesis-oew.md b/docs/access/thesis-oew.md
new file mode 100644
index 0000000..2c707c8
--- /dev/null
+++ b/docs/access/thesis-oew.md
@@ -0,0 +1,48 @@
+# 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` | **`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
+
+- `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)
+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/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/architecture/CODEMAP.md b/docs/architecture/CODEMAP.md
new file mode 100644
index 0000000..081657c
--- /dev/null
+++ b/docs/architecture/CODEMAP.md
@@ -0,0 +1,65 @@
+# Code map — where to edit
+
+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:** [../access/34-jspace-engine.md](../access/34-jspace-engine.md)
+
+## Layers (edit here first)
+
+```
+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 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
+```
+
+## Access Workspace package (`access/`) — L2 product
+
+| Module | Role |
+|--------|------|
+| **`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 `AccessEngine` |
+
+## Desk spine (L1)
+
+| Module | Role |
+|--------|------|
+| `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 |
+| `inject.py` | Desk GWT strip |
+
+## Warehouse cable (L3 — optional)
+
+| Module | Role |
+|--------|------|
+| `cube_module.py` | Soft arterial strip / seal / pulse / room |
+
+## Integration
+
+| Path | Role |
+|------|------|
+| `hermes_bridge.py` | Plugin hooks — session / pre_llm / end |
+| `hermes_plugin/` | Thin `register(ctx)` |
+| `workbench.py` | Session pocket — enter / order / idle |
+| `agent_api.py` | Dual-decode doors |
+
+## Rules of thumb
+
+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`.
+5. Silent steps stay in model context only.
+6. Prefer `from hermespace.jspace import …` over deep/compat paths.
diff --git a/docs/architecture/HERMESCUBE.md b/docs/architecture/HERMESCUBE.md
new file mode 100644
index 0000000..8b3b115
--- /dev/null
+++ b/docs/architecture/HERMESCUBE.md
@@ -0,0 +1,87 @@
+# HermesCube × Hermespace — heart / generator contract
+
+**HermesCube is the heart** (when installed). **Hermespace is the nervous FOA** —
+and a **standalone workbench** when Cube is absent.
+
+Companion: [PabloTheThinker/hermescube](https://github.com/PabloTheThinker/hermescube)
+North star: [PURPOSE.md](../PURPOSE.md) · Living assessment:
+[33-living-memories-cube-world.md](../assessment/33-living-memories-cube-world.md)
+
+```
+Hermes Agent ──connect──► Hermespace (J-Space · FOA · OEW)
+ │ arteries / veins / pulse
+ ▼
+ HermesCube (.cube · Cuboasis · hive · dream)
+```
+
+## Authority
+
+| Surface | With Cube | Standalone |
+|---------|-----------|------------|
+| `$HERMES_HOME/memories/memory.cube` | **Durable SoT** | n/a |
+| 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 |
+
+## Space adapter (`hermespace.cube_module` **1.3**)
+
+```python
+from hermespace.cube_module import (
+ ensure_heart,
+ center_status,
+ 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,
+)
+```
+
+| Call | Use |
+|------|-----|
+| `connect_agent(agent_id)` | Session start / `HermesBase.connect` |
+| `ensure_heart()` | Create cube or standalone dirs |
+| `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 |
+| `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-map.md](../jspace/00-map.md) and `hermespace.jspace`.
+
+```bash
+hs jspace hold -t "deploy pipeline"
+hs jspace report
+hs base lens
+hs cube beat -q "what do we believe about deploys?"
+```
+
+## Install both
+
+```bash
+hermes plugins install PabloTheThinker/hermescube
+# … then Hermespace install
+./scripts/install_hermes.sh
+```
+
+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/architecture/INSIGHT.md b/docs/architecture/INSIGHT.md
new file mode 100644
index 0000000..0a444a3
--- /dev/null
+++ b/docs/architecture/INSIGHT.md
@@ -0,0 +1,23 @@
+# Hermes Insight × Hermespace — optional perceive_card cable
+
+**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 hermes_insight import HermesInsight
+if hasattr(HermesInsight, "perceive_card"):
+ card = HermesInsight().perceive_card(goal, load=...)
+```
+
+| Rule | Behavior |
+|------|----------|
+| 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` / `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/docs/assessment/28-hermes-agent-jspace-assessment.md b/docs/assessment/28-hermes-agent-jspace-assessment.md
new file mode 100644
index 0000000..a097b96
--- /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.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)
+
+---
+
+## 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 — **done (v0.21)**
+
+| Item | Detail | Effort |
+|------|--------|--------|
+| 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 — **done (v0.21)**
+
+| Item | Detail | Effort |
+|------|--------|--------|
+| 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
+
+| 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/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/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/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 98%
rename from docs/23-everyday-ops.md
rename to docs/ops/23-everyday.md
index aa14e37..27d6897 100644
--- a/docs/23-everyday-ops.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/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/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..c61ff46
--- /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 | 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/day_in_life_oew.py b/experiments/day_in_life_oew.py
new file mode 100644
index 0000000..90493c4
--- /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.access import AccessHub, AccessEnv
+ 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 = AccessEnv(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 = AccessHub(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 = AccessEnv(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 = 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 = 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")
+ 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 = AccessEnv(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 "Access Workspace" in (out.context or "")
+ and "Silent reasoning" not in out.report,
+ f"report_len={len(out.report)} ctx_has_hub={'Access Workspace' in (out.context or '')}",
+ )
+ )
+
+ # --- 10. Night harvest consolidates silent → durable ---
+ 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)
+ 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 = 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(
+ _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 = 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}
+ 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/experiments/oew_eval.py b/experiments/oew_eval.py
new file mode 100644
index 0000000..0b7184c
--- /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.access import AccessEnv
+ from hermespace.io_contract import HermespaceInput
+ from hermespace.workflow import Workflow
+
+ results = []
+
+ # 1) Auto-park silent on material turn
+ 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",
+ 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 = 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 = 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 = 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")
+ 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 = 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()
+ 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 "Access Workspace" 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..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.20.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 e9fa35a..355fff3 100644
--- a/hermes_plugin/plugin.yaml
+++ b/hermes_plugin/plugin.yaml
@@ -1,14 +1,42 @@
+manifest_version: 1
name: hermespace
-version: "0.20.0"
+version: "0.25.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.
+ Production Access Engine for Hermes Agent v0.20+: session-scoped workspace,
+ bounded model context, native lifecycle telemetry, dual decode, and
+ standalone-safe persistence. Cube and Insight are optional soft-imports.
author: Hermespace contributors
kind: standalone
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
+ - subagent_start
+ - subagent_stop
+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
+ - subagent_start
+ - subagent_stop
+python_dependencies:
+ - numpy>=1.24,<3
homepage: https://github.com/PabloTheThinker/hermespace
diff --git a/plugin.yaml b/plugin.yaml
new file mode 100644
index 0000000..355fff3
--- /dev/null
+++ b/plugin.yaml
@@ -0,0 +1,42 @@
+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. Cube and Insight are optional soft-imports.
+author: Hermespace contributors
+kind: standalone
+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
+ - subagent_start
+ - subagent_stop
+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
+ - subagent_start
+ - subagent_stop
+python_dependencies:
+ - numpy>=1.24,<3
+homepage: https://github.com/PabloTheThinker/hermespace
diff --git a/pyproject.toml b/pyproject.toml
index 69d2abd..51920b2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,23 @@
[project]
name = "hermespace"
-version = "0.20.0"
-description = "True J-Space environment for Hermes Agent — external observable workspace, Cube heart, dual decode"
+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/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/scripts/e2e_ops.sh b/scripts/e2e_ops.sh
index b435339..563aa1c 100755
--- a/scripts/e2e_ops.sh
+++ b/scripts/e2e_ops.sh
@@ -2,12 +2,21 @@
# 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)}"
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 +24,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 406fcfd..98a57d2 100755
--- a/scripts/install_hermes.sh
+++ b/scripts/install_hermes.sh
@@ -1,74 +1,118 @@
#!/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
+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] [--yes] [--no-organs]"
+ exit 0
+ ;;
+ *)
+ echo "hermespace: unknown installer option: $arg" >&2
+ exit 2
+ ;;
+ esac
+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 install"
+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"
+mkdir -p "$HERMESPACE_HOME" "$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"
+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"
-# 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"
-
-# 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
-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 command -v hermes >/dev/null 2>&1; then
- hermes plugins enable hermespace 2>/dev/null || true
- echo " hermes plugins enable hermespace (attempted)"
+if [[ "$INSTALL_DESKTOP" == "1" ]]; then
+ bash "$ROOT/scripts/install_desktop_plugin.sh"
else
- echo " hermes CLI not on PATH — enable later: hermes plugins enable hermespace"
+ echo " desktop plugin skipped"
fi
+# 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("install_front_door failed: " + str(out))
+PY
+
+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 _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")
+ 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")
+
+ live_host = _verify_live_hermes_manager()
+ 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"),
+ "current_hermes_manager": live_host,
+ }
+ 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 563ed82..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`
---
@@ -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
@@ -301,7 +302,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 +363,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 |
@@ -373,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/skills/hermespace/references/plugin-hooks.md b/skills/hermespace/references/plugin-hooks.md
index c2db72a..ebcc0dc 100644
--- a/skills/hermespace/references/plugin-hooks.md
+++ b/skills/hermespace/references/plugin-hooks.md
@@ -2,23 +2,31 @@
## 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 |
+| 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 | Harvest ≤10s fail-open + 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 +34,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 d1c31bf..88a63f0 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 Access Engine for Hermes agents."""
from __future__ import annotations
-__version__ = "0.20.0"
+__version__ = "0.25.0"
from hermespace.desk import Desk
from hermespace.engine import HermespaceEngine
@@ -29,13 +29,18 @@
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.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 import insight_module
+from hermespace.hermes_base import HermesBase
__all__ = [
"Desk",
"HermespaceEngine",
+ "AccessEngine",
+ "ACCESS_ROLES",
"Workflow",
"TurnResult",
"HermespaceInput",
@@ -58,11 +63,14 @@
"WorldModel",
"get_world",
"world_context",
- "JSpace",
- "get_jspace",
- "JSpaceEnv",
+ "AccessHub",
+ "get_access_hub",
+ "AccessEnv",
"get_env",
+ "evaluate_material_turn",
"cube_module",
+ "insight_module",
+ "HermesBase",
"probe_environment",
"environment_markdown",
"build_inject_block",
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/access/engine.py b/src/hermespace/access/engine.py
new file mode 100644
index 0000000..7f69c62
--- /dev/null
+++ b/src/hermespace/access/engine.py
@@ -0,0 +1,801 @@
+"""Access Engine — Hermespace's open-source access workspace for Hermes Agent.
+
+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 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 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.lens()) # what is on the agent's mind
+"""
+
+from __future__ import annotations
+
+import os
+from pathlib import Path
+from typing import Any
+
+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 = (
+ "verbal_report",
+ "directed_modulation",
+ "internal_reasoning",
+ "flexible_broadcast",
+ "selectivity",
+)
+
+
+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."""
+
+ 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
+
+ @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.workspace_id)
+
+ @property
+ def env(self):
+ from hermespace.access import AccessEnv
+
+ return AccessEnv(agent_id=self.workspace_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(
+ desk_path=session_desk_path(self.agent_id, self.session_id)
+ )
+
+ # --- Access roles (live) -------------------------------------------------
+
+ def access_roles(self) -> dict[str, Any]:
+ """Report the five live Access Workspace roles."""
+ 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.access.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,
+ "workspace_id": self.workspace_id,
+ "engine": "AccessEngine",
+ "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_access_from_warehouse
+
+ out["phases"]["seed"] = seed_access_from_warehouse(
+ self.agent_id,
+ 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
+ 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),
+ "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),
+ "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"AccessEngine connected {self.agent_id}: "
+ f"hub={out['gained']['access_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": "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": {},
+ "world": {},
+ "room": {},
+ "warehouse": {},
+ "ready": False,
+ "connected": bool(self._last_connect and self._last_connect.get("ok")),
+ "role": "Hermespace Access Workspace 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_lens": self.jlens_status(),
+ }
+ try:
+ js = self.hub
+ env = self.env
+ out["access"] = {
+ "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["access"] = {"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}
+ 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"] = {
+ "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["access"]
+ and int(out["access"].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 Access Workspace?"""
+ 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:
+ 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)
+
+ 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).
+
+ Intermediate steps never appear in the spoken answer unless explicitly
+ requested — 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)
+ out = Workflow(engine=self.desk_engine).run(
+ HermespaceInput(
+ message=message,
+ goal=goal or message[:200],
+ plan=list(plan or []),
+ 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"] = "AccessEngine"
+ 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_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("access", {}).get("oew_ok"),
+ "goal": out.goal,
+ "decision": out.decision,
+ "connected": bool(self._last_connect and self._last_connect.get("ok")),
+ "engine": "AccessEngine",
+ "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.access.loop import check_bound_report, park_spoken_intermediates
+ 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 []),
+ "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)
+ 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__
+
+ 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
+ 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__}
+
+ # --- optional open-weight activation lens (not required) -----------------
+
+ def jlens_status(self) -> dict[str, Any]:
+ """Optional open-weight activation lens — not required for Access Engine.
+
+ 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,
+ "note": (
+ "Hermes Agent typically has no weight access — "
+ "AccessEngine is Hermespace's Access Workspace."
+ ),
+ }
+ 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
+HermespaceAccessEngine = AccessEngine
diff --git a/src/hermespace/access/env.py b/src/hermespace/access/env.py
new file mode 100644
index 0000000..84f0742
--- /dev/null
+++ b/src/hermespace/access/env.py
@@ -0,0 +1,733 @@
+"""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.
+"""
+
+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.atomic import atomic_write_text
+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
+# (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 — encode → deliberate → report
+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 access 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 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.
+ """
+
+ def __init__(self, agent_id: str = "hermes-agent") -> None:
+ self.agent_id = (agent_id or "hermes-agent").strip()
+ 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"
+ 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:
+ 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}
+ 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 point of view in the Access Workspace ---
+
+ 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 = [
+ "## 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):",
+ ]
+ 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)
+ # Rewrite silent steps (exact or substring — causal redirect)
+ pat = re.compile(re.escape(src), re.I)
+ 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.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)
+ return {
+ "ok": True,
+ "removed": removed,
+ "source": src,
+ "target": tgt,
+ "concept": concept.label(),
+ "sticky": True,
+ "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).
+
+ 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)
+ body = (text or "").strip()
+ if silent:
+ 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]:
+ """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()
+ try:
+ 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)
+ return {"ok": True, "removed": len(removed), "items": removed, "sticky": True}
+
+ # --- 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="access_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()
+ # Seed next turn's mid-band (counterfactual reflection → later silent thought)
+ try:
+ 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:
+ 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 (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. "
+ "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:
+ """Operator / bind protocol. Do not dump this essay onto the pre_llm inject.
+
+ 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 ""
+ if high_load:
+ return (
+ "### 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 = [
+ "### 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 "
+ "(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]}")
+ 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) ---
+
+ 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: Access Workspace 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="access_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=["access", "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:],
+ "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",
+ },
+ }
+
+ 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 = "",
+ 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 warehouse.
+
+ Pass ``already_synced=True`` when the caller just ran
+ ``AccessHub.sync_from_desk`` to avoid a double hub rewrite.
+ """
+ from hermespace.access.oew import run_oew_beat
+
+ self.set_band("early")
+ 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
+ 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 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
+
+ seal_learning(
+ seal_decision[:400],
+ entry_type="focus",
+ agent_id=self.agent_id,
+ source="access_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=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.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.access.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") -> AccessEnv:
+ return AccessEnv(agent_id=agent_id)
diff --git a/src/hermespace/jspace.py b/src/hermespace/access/hub.py
similarity index 81%
rename from src/hermespace/jspace.py
rename to src/hermespace/access/hub.py
index bf5cce0..845294f 100644
--- a/src/hermespace/jspace.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.
"""
@@ -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,
@@ -32,7 +33,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 +61,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 +92,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 +121,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 +130,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:
+ atomic_write_text(self.path, legacy.read_text(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 +164,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],
@@ -169,10 +178,7 @@ def _load(self) -> JSpaceState:
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 ---
@@ -237,7 +243,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 +273,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 +355,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"
@@ -367,13 +373,37 @@ 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,
+ is_user_echo_copy,
+ )
+
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):
+ 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]):
+ 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(
@@ -385,11 +415,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(
@@ -401,13 +431,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,
@@ -418,7 +448,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:]
@@ -431,7 +461,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]
@@ -489,7 +519,38 @@ 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 _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(
+ 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):
@@ -508,7 +569,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:
@@ -531,5 +597,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/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/access/oew.py b/src/hermespace/access/oew.py
new file mode 100644
index 0000000..8e60118
--- /dev/null
+++ b/src/hermespace/access/oew.py
@@ -0,0 +1,292 @@
+"""Obligatory External Workspace — higher-order thinking orchestration.
+
+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.
+"""
+
+from __future__ import annotations
+
+import os
+import re
+from typing import Any
+
+from hermespace.access.hub import AccessHub
+from hermespace.access.protocol import (
+ ProtocolVerdict,
+ evaluate_material_turn,
+ oew_enabled,
+)
+
+
+_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:
+ """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: AccessHub,
+ *,
+ 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
+
+ from hermespace.execute_focus import (
+ derive_plan,
+ is_filler_step,
+ is_near_dup,
+ is_user_echo_copy,
+ 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 ""
+ decision = str(getattr(desk, "decision", "") or "") if desk is not None else ""
+
+ for step in plan:
+ s = str(step).strip()
+ 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,
+ ):
+ chunks = [c.strip() for c in _STEP_SPLIT.split(msg) if c and c.strip()]
+ for ch in chunks[:4]:
+ 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])
+
+ # Near-duplicate collapse (prefix-stripped gist), not exact casefold only
+ existing = list(js.state.silent_steps)
+ for c in candidates:
+ 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)
+ if len(parked) >= 3 or len(js.state.silent_steps) >= 3:
+ break
+
+ if not parked and min_steps > 0:
+ 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)
+
+ 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 Access Workspace blocks."""
+ if protect or high_load:
+ return 280
+ return 640
+
+
+def run_oew_beat(
+ js: AccessHub,
+ 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 AccessEnv 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/access/protocol.py b/src/hermespace/access/protocol.py
new file mode 100644
index 0000000..a1c87b3
--- /dev/null
+++ b/src/hermespace/access/protocol.py
@@ -0,0 +1,137 @@
+"""Obligatory External Workspace (OEW) protocol gate.
+
+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``.
+"""
+
+from __future__ import annotations
+
+import os
+from dataclasses import dataclass, field
+from typing import Any
+
+
+def oew_enabled() -> bool:
+ """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
+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/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..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)
@@ -227,11 +226,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/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/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 861d293..6af5af3 100644
--- a/src/hermespace/cli.py
+++ b/src/hermespace/cli.py
@@ -150,9 +150,62 @@ 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)
- js = sub.add_parser("jspace", help="Functional J-Space: hold / report / broadcast / status")
- js_sub = js.add_subparsers(dest="jspace_cmd", required=True)
+ # Functional Access Workspace (harness global workspace)
+ # Hermespace Access Engine (read / audit / shape / operate)
+ base = sub.add_parser(
+ "base",
+ 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 Access 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 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="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="GWT access roles (live)")
+ baseroles.add_argument("--agent-id", default="hermes-agent")
+ 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")
+ 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")
+ 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 warehouse/semantic")
+ baseh.add_argument("--agent-id", default="hermes-agent")
+ baseh.add_argument("--clear-silent", action="store_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")
@@ -420,6 +473,21 @@ 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")
+
+ 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")
@@ -714,13 +782,69 @@ def main(argv: list[str] | None = None) -> int:
return int(g.get("main", lambda: 1)())
return 2
- if args.cmd == "jspace":
- from hermespace.jspace import JSpace
+ if args.cmd == "base":
+ from hermespace import AccessEngine
+
+ aid = getattr(args, "agent_id", "hermes-agent") or "hermes-agent"
+ sid = getattr(args, "session_id", "main") or "main"
+ hb = AccessEngine(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, default=str))
+ return 0
+ 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 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(
+ 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 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
@@ -748,9 +872,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))
@@ -875,8 +999,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":
@@ -1026,6 +1151,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,
@@ -1033,7 +1159,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
@@ -1131,6 +1257,35 @@ 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
+
+ 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/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/context_surgery.py b/src/hermespace/context_surgery.py
new file mode 100644
index 0000000..944fa80
--- /dev/null
+++ b/src/hermespace/context_surgery.py
@@ -0,0 +1,190 @@
+"""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
+
+
+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 ""
+ 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/cube_module.py b/src/hermespace/cube_module.py
index 29552b7..1b52bed 100644
--- a/src/hermespace/cube_module.py
+++ b/src/hermespace/cube_module.py
@@ -1,14 +1,17 @@
"""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.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 + Access Workspace + optional hive room
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
@@ -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.3"
# Load → arterial char budgets (match Cube center when present).
LOAD_STRIP_CHARS: dict[str, int] = {
@@ -86,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",
@@ -230,6 +233,72 @@ def cube_status() -> dict[str, Any]:
return heart_status()
+def hermes_memory_provider() -> str:
+ """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
+ 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."""
+ try:
+ return hermes_memory_provider() in {"hermescube", "cube"}
+ except Exception:
+ return False
+
+
+def skip_cube_foa_strip() -> bool:
+ """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.
+ """
+ try:
+ return cube_is_memory_provider()
+ except Exception:
+ return False
+
+
+def cube_already_prefetched(
+ query: str = "",
+ *,
+ session_id: str = "",
+) -> bool:
+ """Alias for ``skip_cube_foa_strip`` — config says Cube owns prefetch."""
+ return skip_cube_foa_strip()
+
+
def cube_beat(
query: str = "",
*,
@@ -240,11 +309,44 @@ 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``, 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).
"""
+ 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": "skipped",
+ "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
+
try:
from hermescube.center import beat
@@ -267,7 +369,7 @@ def cube_beat(
# Heart 1.0 / standalone fallback
level = normalize_load(load, high_load=high_load)
- out: dict[str, Any] = {
+ out = {
"api_version": "1.0",
"adapter": SPACE_CUBE_ADAPTER_VERSION,
"mode": "standalone" if not cube_available() else "heart",
@@ -336,6 +438,315 @@ 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 + Access Workspace; 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_access_from_warehouse(
+ agent_id: str = "hermes-agent",
+ *,
+ 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.
+
+ 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.access import AccessHub
+
+ js = AccessHub(agent_id=workspace_id or 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")
+ report["workspace_id"] = workspace_id or agent_id
+ return report
+ except Exception as e:
+ report["error"] = type(e).__name__
+ 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",
+ *,
+ 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 + 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.
+ """
+ out: dict[str, Any] = {
+ "ok": False,
+ "agent_id": agent_id,
+ "session_id": session_id,
+ "adapter": SPACE_CUBE_ADAPTER_VERSION,
+ "phases": {},
+ "gained": {},
+ "memories": [
+ "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",
+ ],
+ }
+ 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:
+ 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 {}
+ 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),
+ "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),
+ "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']['access_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) -----------------------------------------
@@ -388,7 +799,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:
@@ -531,20 +942,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/desk.py b/src/hermespace/desk.py
index 4e7ee49..231763d 100644
--- a/src/hermespace/desk.py
+++ b/src/hermespace/desk.py
@@ -63,20 +63,27 @@ 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
+
+ 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))
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)
)
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
@@ -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 e858d49..36b462f 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 Access Engine lives at ``hermespace.access.engine.AccessEngine``.
+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 Access Engine).
+
+ Use ``AccessEngine`` for connect / turn / lens / harvest.
+ """
def __init__(self, desk_path: Path | None = None) -> None:
self.desk_path = desk_path or default_desk_path()
@@ -50,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
new file mode 100644
index 0000000..ba1964f
--- /dev/null
+++ b/src/hermespace/execute_focus.py
@@ -0,0 +1,530 @@
+"""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")
+_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:
+ g = " ".join((goal or "").split())
+ if not g:
+ return "untitled"
+ 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 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
+
+
+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:
+ """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) < 4:
+ 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(
+ items: Sequence[str],
+ *,
+ prefer_shorter: bool = False,
+) -> list[str]:
+ """Collapse near-duplicate gists. Bind wins over prefixed copies."""
+ 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 _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
+
+
+def shape_focus(
+ labels: Sequence[str],
+ *,
+ message: str = "",
+ goal: str = "",
+ plan: Sequence[str] | None = None,
+ cap: int = 4,
+) -> list[str]:
+ """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]
+
+
+_FILLER_ADJ = {
+ "a",
+ "an",
+ "the",
+ "short",
+ "brief",
+ "quick",
+ "small",
+ "simple",
+ "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():
+ t = t[0].upper() + t[1:]
+ return t[:80] if len(t) <= 80 else t[:79].rstrip() + "…"
+
+
+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 ""
+ 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:
+ 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 body == user:
+ return True
+ 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
+
+
+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())
+ 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 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 = "",
+ 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):
+ 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 = "",
+ message: str = "",
+) -> str:
+ steps = plan_or_derived(plan, message, goal)
+ if steps:
+ phrase = compress_action_phrase(steps[0])
+ if (
+ phrase
+ and not is_filler_step(phrase)
+ and not _is_bad_lead(phrase)
+ and not is_user_echo_copy(phrase, message, goal)
+ ):
+ return phrase[:40]
+ for raw in (say or "").splitlines():
+ s = raw.strip().lstrip("-* ").lstrip("0123456789.) ")
+ if s and not _is_bad_lead(s):
+ return s[:160]
+ dec = (decision or "").strip()
+ 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 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)."
+
+
+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 = "",
+ message: str = "",
+) -> str:
+ """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())
+ 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 _is_bad_lead(first):
+ rest = "\n".join(body.splitlines()[1:]).strip()
+ 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):
+ # Operator supplied a clean say — keep that action lead.
+ 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/gate.py b/src/hermespace/gate.py
index 1694b59..4a75fea 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
@@ -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/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/grid/dream.py b/src/hermespace/grid/dream.py
index 8768b4d..6f331e1 100644
--- a/src/hermespace/grid/dream.py
+++ b/src/hermespace/grid/dream.py
@@ -66,50 +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 — day unspoken thoughts → durable night memory
- jspace_harvest: dict[str, Any] = {}
+ # Access Workspace dream harvest — single path via AccessEnv.dream_harvest
+ access_harvest: dict[str, Any] = {}
try:
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access import AccessEnv
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:
+ 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
- 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"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": len(harvested), "sealed": sealed_n}
+ 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
@@ -136,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/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/grid/viewport.py b/src/hermespace/grid/viewport.py
index b322830..2ea9e7f 100644
--- a/src/hermespace/grid/viewport.py
+++ b/src/hermespace/grid/viewport.py
@@ -29,6 +29,63 @@ 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 = []
+ 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:
+ 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}",
+ }
+
+
def snapshot(agent_id: str = "default") -> dict[str, Any]:
"""Full read-only snapshot for viewport / API."""
desk_md = ""
@@ -51,6 +108,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 +136,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,
@@ -102,15 +162,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 +268,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 +288,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
new file mode 100644
index 0000000..c2f7a7f
--- /dev/null
+++ b/src/hermespace/hermes_base.py
@@ -0,0 +1,15 @@
+"""HermesBase — thin product alias of ``AccessEngine``.
+
+Prefer ``from hermespace import AccessEngine`` for new code.
+``HermesBase`` remains for day-to-day CLI / docs compatibility.
+"""
+
+from __future__ import annotations
+
+from hermespace.access.engine import AccessEngine
+
+
+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 d7f201f..ff913a5 100644
--- a/src/hermespace/hermes_bridge.py
+++ b/src/hermespace/hermes_bridge.py
@@ -13,8 +13,85 @@ def _truthy(name: str, default: str = "0") -> bool:
return os.environ.get(name, default).strip().lower() in {"1", "true", "yes", "on"}
+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,
+ silent_chain_strip,
+ strip_needed,
+)
+
+HARVEST_BUDGET_S = 10.0
+
+
+def _bounded_context(text: str) -> str:
+ """Keep native hook output below 9k (Hermes spill is 10k)."""
+
+ try:
+ cap = int(os.environ.get("HERMESPACE_PRE_LLM_MAX_CHARS", str(INJECT_HARD_CAP)))
+ except ValueError:
+ cap = INJECT_HARD_CAP
+ cap = max(2000, min(8999, 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 _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:
- """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
@@ -29,10 +106,11 @@ 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()
+ eng = wb.workflow.engine
desk = load_desk(eng.desk_path)
if not desk.goal:
desk.goal = "Hermes agent session workbench"
@@ -59,6 +137,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)
@@ -82,62 +168,78 @@ 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 via AccessEngine — world + hub seed (warehouse optional)
+ 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"
+ from hermespace import AccessEngine
+
+ 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: AccessEngine · ok={conn.get('ok')}\n"
+ f"- warehouse: mode={gained.get('warehouse_mode')} (optional)\n"
+ 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"
+ 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:
- 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"
- )
- except Exception:
- pass
+ try:
+ from hermespace.access import AccessHub
+ from hermespace.store import load_desk as _load_desk
+ from hermespace.world import WorldModel
+
+ 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"- 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"
+ )
+ except Exception:
+ pass
block = (
- "## Hermespace workbench (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_heart}"
- f"{block_extra_jspace}"
- "- Pocket dimension online: park secondary goals, keep FOA tight, "
- "user replies short; put operational detail in workspace context.\n"
- "- API: `from hermespace import Workbench` · "
- "`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"
+ f"{block_extra_connect}"
+ "- 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 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"
)
- # 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"
+ 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:
- pass
+ except Exception as exc: # noqa: BLE001
+ logger.debug("session runtime start failed: %s", exc)
return {"context": block}
@@ -163,6 +265,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:
@@ -170,32 +299,26 @@ 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()
+ # 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": (
- 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)
- eng = HermespaceEngine()
desk = load_desk(eng.desk_path)
auto_order = _truthy("HERMESPACE_AUTO_ORDER", "0")
@@ -219,7 +342,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 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
+
+ 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:
@@ -237,27 +371,37 @@ 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
- 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)
@@ -277,6 +421,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)
@@ -285,150 +437,182 @@ 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
- block = build_inject_block(desk, max_chars=inject_cap, user_message=msg)
- if not block.strip():
- return None
-
- 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
-
- block += "\n\n" + pending_inject_block(agent_id)
- except Exception:
- pass
+ load_level = str((desk.load or {}).get("level") or "mid")
+ high_load = load_level in {"high", "protect"}
+ inject_cap = inject_budget(load_level)
+ # Session-start essay is observer-only. Never prepend it onto the inject.
+ _ = start_context
- 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
- from hermespace.jspace import JSpace
+ 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 "")
- if cube_block:
- block += "\n\n" + cube_block
- # Sync functional J-Space hub and append broadcast (model channel only)
- js = JSpace(agent_id=agent_id)
+ # 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 "")
+ # 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
+
+ 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"),
+ }
+ 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
+ from hermespace.access.oew import ensure_oew_env_default
+
+ ensure_oew_env_default()
+ js = AccessHub(agent_id=access_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"),
+ "skipped": beat.get("skipped"),
+ "shrunk": beat.get("shrunk"),
}
- # Environment protocol — force externalization of silent thought
try:
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access import AccessEnv
- env = JSpaceEnv(agent_id=agent_id)
- env.advance_turn(
+ env = AccessEnv(agent_id=access_id)
+ env_meta = env.advance_turn(
user_message=msg,
desk=desk,
cube_strip=cube_block,
report=desk.say or "",
+ material=True,
+ already_synced=True,
)
- 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_env"] = {
+ if env_meta.get("report"):
+ desk.say = str(env_meta["report"])
+ # 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),
+ "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["access_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"),
}
+ desk.meta["user_reply_hint"] = (desk.say or "")[:240]
except Exception:
- pass
+ desk.meta["access"] = {
+ "hub_n": len(js.state.hub),
+ "focus_n": len(js.state.focus),
+ "mode": js.state.mode,
+ }
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)
+ 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)
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
+ 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:
+ organ = insight_strip or cube_block
+ if organ:
+ parts.append(organ)
+ if load_level == "low":
try:
- from hermespace import ops as ops_mod
+ from hermespace.self_model import format_self_trace, read_self_trace
+ from hermespace.access import AccessHub
- block += "\n" + ops_mod.compact_status(
- agent_id=agent_id if agent_id != "hermes-agent" else "default"
+ parts.append(
+ format_self_trace(
+ read_self_trace(AccessHub(agent_id=access_id)),
+ for_inject=True,
+ )
)
except Exception:
pass
+ 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 = ""
+
try:
eng.episodes.write(
f"broadcast reason={reason} session={sid[:12]} high={high_load}",
@@ -438,23 +622,254 @@ def on_pre_llm_call(
except Exception:
pass
- return {"context": block}
+ result: dict[str, str] = {"context": _bounded_context(block)}
+ if user_hint:
+ result["user_reply_hint"] = user_hint
+ return result
+
+
+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 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 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
+
+ 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
+
+ runtime.subagent(session_id or task_id, agent_id=agent_id, started=False)
+ except Exception:
+ pass
+
+
+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."""
-def on_session_end(**kwargs: Any) -> None:
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.hermes_runtime import runtime
+
+ if not runtime.finalize(sid, agent_id=agent_id):
+ return
+ except Exception:
+ pass
+
try:
from hermespace.world import WorldModel
- agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent")
- WorldModel(agent_id=agent_id).leave("session ended")
+
+ WorldModel(agent_id=agent_id).leave("session finalized")
+ except Exception as exc: # noqa: BLE001
+ logger.debug("world finalize failed: %s", exc)
+ 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
+
+ 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:
+ """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
+
+
+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.workbench import Workbench
+ from hermespace.access import AccessEnv
+ from hermespace.access.engine import workspace_id
- 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)
+ 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/hermes_enable.py b/src/hermespace/hermes_enable.py
new file mode 100644
index 0000000..709248e
--- /dev/null
+++ b/src/hermespace/hermes_enable.py
@@ -0,0 +1,274 @@
+"""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)}
+
+
+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/hermes_runtime.py b/src/hermespace/hermes_runtime.py
new file mode 100644
index 0000000..f7c81d2
--- /dev/null
+++ b/src/hermespace/hermes_runtime.py
@@ -0,0 +1,253 @@
+"""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
+ shared_hub: 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 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
+ 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/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
new file mode 100644
index 0000000..500c12b
--- /dev/null
+++ b/src/hermespace/insight_module.py
@@ -0,0 +1,143 @@
+"""Hermes Insight adapter — thin soft-import, never required.
+
+Insight stays a standalone package. This module is the cable only:
+
+ from hermes_insight import HermesInsight
+ hasattr(HermesInsight, "perceive_card")
+ HermesInsight().perceive_card(goal, load=...)
+
+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 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.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+SPACE_INSIGHT_ADAPTER_VERSION = "1.1"
+INSIGHT_CARD_CHARS = 400
+
+
+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:
+ from hermes_insight import HermesInsight
+
+ return hasattr(HermesInsight, "perceive_card")
+ except Exception:
+ return False
+
+
+def insight_status() -> dict[str, Any]:
+ """Feature-detect ``perceive_card`` only — never required."""
+ 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
+ from hermes_insight import HermesInsight
+
+ 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 insight_card(
+ goal: str,
+ *,
+ load: str | float | None = None,
+ high_load: bool = False,
+ max_chars: int = INSIGHT_CARD_CHARS,
+) -> dict[str, Any]:
+ """Call ``HermesInsight().perceive_card`` and return only that card.
+
+ Soft-fail if Insight is absent or ``perceive_card`` is missing.
+ Never calls ``recall`` / ``perceive`` / ``plan`` / ``insight_beat``.
+ Never formats a lattice dump.
+ """
+ out: dict[str, Any] = {
+ "ok": True,
+ "adapter": SPACE_INSIGHT_ADAPTER_VERSION,
+ "mode": "missing",
+ "card": "",
+ "writeback": {},
+ "required": False,
+ }
+ if _high_or_protect(load, high_load=high_load):
+ out["mode"] = "skipped"
+ out["skipped"] = "high_load"
+ return out
+ if not (goal or "").strip():
+ out["mode"] = "skipped"
+ out["skipped"] = "empty"
+ return out
+ try:
+ from hermes_insight import HermesInsight
+ except Exception:
+ out["mode"] = "missing"
+ out["skipped"] = "not_installed"
+ return out
+ if not hasattr(HermesInsight, "perceive_card"):
+ out["mode"] = "no_perceive_card"
+ out["skipped"] = "no_perceive_card"
+ return out
+ try:
+ rec = HermesInsight().perceive_card((goal or "").strip(), load=load)
+ except Exception as e:
+ out["mode"] = "soft_fail"
+ 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),
+ "writeback": writeback,
+ }
+ )
+ return out
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/jspace/__init__.py b/src/hermespace/jspace/__init__.py
new file mode 100644
index 0000000..71d2afa
--- /dev/null
+++ b/src/hermespace/jspace/__init__.py
@@ -0,0 +1,65 @@
+"""Deprecated shim — prefer ``hermespace.access`` (Access Workspace).
+
+Old Anthropic-inspired product name removed to keep Hermespace's own brand.
+"""
+
+from __future__ import annotations
+
+from hermespace.access import * # noqa: F403
+from hermespace.access import (
+ ACCESS_ROLES,
+ AccessEngine,
+ AccessEnv,
+ AccessHub,
+ AUDIT_LEXICON,
+ BANDS,
+ HermespaceAccessEngine,
+ HUB_CAP,
+ LensHit,
+ ProtocolGate,
+ ProtocolVerdict,
+ WorkspaceConcept,
+ auto_park_silent,
+ evaluate_material_turn,
+ filter_ablated,
+ get_access_hub,
+ get_env,
+ oew_enabled,
+ run_oew_beat,
+ shape_report,
+)
+
+# Legacy aliases (do not use in new code)
+JSpace = AccessHub
+JSpaceEnv = AccessEnv
+JSpaceEngine = AccessEngine
+get_jspace = get_access_hub
+HermespaceJSpaceEngine = HermespaceAccessEngine
+
+__all__ = [
+ "ACCESS_ROLES",
+ "AccessEngine",
+ "AccessEnv",
+ "AccessHub",
+ "AUDIT_LEXICON",
+ "BANDS",
+ "HermespaceAccessEngine",
+ "HUB_CAP",
+ "LensHit",
+ "ProtocolGate",
+ "ProtocolVerdict",
+ "WorkspaceConcept",
+ "auto_park_silent",
+ "evaluate_material_turn",
+ "filter_ablated",
+ "get_access_hub",
+ "get_env",
+ "oew_enabled",
+ "run_oew_beat",
+ "shape_report",
+ "JSpace",
+ "JSpaceEnv",
+ "JSpaceEngine",
+ "get_jspace",
+ "HermespaceJSpaceEngine",
+]
diff --git a/src/hermespace/jspace_env.py b/src/hermespace/jspace_env.py
index 8528b27..5551dd7 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.
-"""
+"""Deprecated shim — use ``hermespace.access_env`` or ``hermespace.access``."""
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.access_env import * # noqa: F403
+from hermespace.access_env import AUDIT_LEXICON, BANDS, AccessEnv, LensHit, get_env
-def get_env(agent_id: str = "hermes-agent") -> JSpaceEnv:
- return JSpaceEnv(agent_id=agent_id)
+__all__ = ["AUDIT_LEXICON", "BANDS", "AccessEnv", "LensHit", "get_env"]
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/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/neural_space.py b/src/hermespace/neural_space.py
index 154aa30..51dfdef 100644
--- a/src/hermespace/neural_space.py
+++ b/src/hermespace/neural_space.py
@@ -82,23 +82,39 @@ 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 (
+ 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,
modality=slot.modality.value,
source="desk",
)
- if 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")
@@ -127,18 +143,35 @@ 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}")
bodies.add(v)
- desk.concepts = new_concepts[-12:]
- desk.focus = [f"[{t.modality}|{t.energy:.2f}] {t.text}" for t in ignited]
+ 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,
+ goal=desk.goal,
+ plan=desk.plan,
+ )
snap = self.field.snapshot()
+ 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/src/hermespace/ops.py b/src/hermespace/ops.py
index a2e2aa2..f5d2459 100644
--- a/src/hermespace/ops.py
+++ b/src/hermespace/ops.py
@@ -2,14 +2,13 @@
from __future__ import annotations
-import os
import socket
import time
from pathlib import Path
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 +21,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 +29,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:
@@ -76,24 +81,39 @@ 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:
+ 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()
@@ -134,15 +154,42 @@ 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"
- add(plug.exists(), "hermes_plugin_link", str(plug))
+ 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))
- 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
+ # 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
@@ -153,12 +200,29 @@ def add(ok: bool, name: str, detail: str = "") -> None:
"boundary_default_deny",
"viewport_html",
"version",
- "jspace",
+ "access",
+ "package_import",
+ "hermes_runtime",
+ "hermes_plugin",
+ "hermes_skill",
+ "plugins_enabled",
}
)
+ 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,
- "all_green": all(c["ok"] for c in checks),
+ "integration_ok": integration_ok,
+ "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()),
@@ -175,10 +239,18 @@ 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"):
- out.append("Install Hermes plugin: ./scripts/install_hermes.sh && hermes plugins enable hermespace")
+ if not by.get("hermes_plugin", {}).get("ok"):
+ 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")
+ 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
@@ -190,6 +262,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 +297,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
@@ -248,7 +322,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/paths.py b/src/hermespace/paths.py
index 1317193..2d37991 100644
--- a/src/hermespace/paths.py
+++ b/src/hermespace/paths.py
@@ -3,21 +3,30 @@
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.
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()
@@ -38,6 +47,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..5cbc8f6
--- /dev/null
+++ b/src/hermespace/plugin.py
@@ -0,0 +1,144 @@
+"""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_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,
+ )
+
+ hooks = {
+ "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,
+ "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 (OSError, RuntimeError, ValueError) 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/pulse.py b/src/hermespace/pulse.py
index 36ce281..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
@@ -194,9 +196,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 +464,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 +480,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 +497,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,
}
@@ -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)
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/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/streams.py b/src/hermespace/streams.py
index 211beda..b38b660 100644
--- a/src/hermespace/streams.py
+++ b/src/hermespace/streams.py
@@ -57,8 +57,9 @@ 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 / 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
bundle.text.append(Slot(f"lang_stream: {gist[:160]}", Modality.VERBAL, sal))
@@ -83,7 +84,7 @@ def encode_stimulus(user_message: str, *, goal_hint: str = "") -> StreamBundle:
Slot("production: prepare verbal report (decode path)", Modality.EXEC, 0.8)
)
- if goal_hint:
+ if goal_hint and gist_key(goal_hint) != gist_key(msg):
bundle.text.append(Slot(f"intention: {goal_hint[:100]}", Modality.VERBAL, 0.65))
return bundle
@@ -96,23 +97,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 +121,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/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/workbench.py b/src/hermespace/workbench.py
index 4f4c964..f0b2329 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,
@@ -41,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)
@@ -72,7 +76,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,14 +117,25 @@ 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) -> 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 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).
+ """
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
@@ -125,7 +149,7 @@ def enter(self) -> 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
@@ -138,19 +162,54 @@ def enter(self) -> 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=access_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_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_access_from_warehouse(
+ self.agent_id,
+ query="",
+ session_id=self.session_id,
+ room=room,
+ workspace_id=access_id,
+ )
+ 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["access"] = {
+ **(self.state.meta.get("access") 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"] = {
@@ -160,20 +219,43 @@ def enter(self) -> 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
- 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()
@@ -221,7 +303,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
@@ -259,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:
@@ -274,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,
@@ -282,58 +375,26 @@ def receive_order(
seal=seal,
tags=["workbench", "order"],
)
+ # 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)
-
- # 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("access") 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["access"] = {
+ "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__}
@@ -355,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()
@@ -372,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 d88a0c5..45c0748 100644
--- a/src/hermespace/workflow.py
+++ b/src/hermespace/workflow.py
@@ -88,14 +88,52 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput:
return out
# 2–5 desk
- g = payload.goal or existing.goal or msg[:200]
+ from hermespace.execute_focus import (
+ derive_plan_steps,
+ message_is_new_goal,
+ plan_or_derived,
+ )
+
+ 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 = payload.plan or existing.plan or ["execute"]
- 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 payload.force or not existing.goal or payload.goal:
+ 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
+
+ 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 or msg_goal:
desk = self.engine.enter(
goal=g,
concepts=list(cons or []),
@@ -136,101 +174,176 @@ 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:
fabric_snap = {"error": type(exc).__name__}
- # 5d Cube beat + functional J-Space environment (soft — 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
- from hermespace.jspace_env import JSpaceEnv
+ 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
+ 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
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"),
"load_level": beat.get("load_level"),
"chars": len(cube_block),
+ "skipped": beat.get("skipped"),
+ "shrunk": beat.get("shrunk"),
}
- js = JSpace(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)
- # 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 = AccessEnv(agent_id=access_id)
+ # already_synced: avoid double hub rewrite inside advance_turn
env_meta = env.advance_turn(
user_message=msg,
desk=desk,
cube_strip=cube_block,
report=desk.say or "",
seal_decision=desk.decision if payload.seal else "",
+ material=True,
+ already_synced=True,
)
- jspace_meta["band"] = env_meta.get("band")
- jspace_meta["audit_alerts"] = env_meta.get("audit_alerts")
- desk.meta["jspace"] = jspace_meta
+ # 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 "")
+ access_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"] = 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"),
}
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)
- if cube_block:
- block = (block + "\n\n" + cube_block).strip()
try:
- from hermespace.jspace import JSpace
- from hermespace.jspace_env import JSpaceEnv
-
- js = JSpace(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)
- 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()
+ desk.refresh_focus(msg)
+ save_desk(desk, self.engine.desk_path)
except Exception:
pass
- report = (desk.say or "").strip()
- # Summon: if user asked for workspace report, surface it in Report channel
+
+ # 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,
+ silent_chain_strip,
+ )
+
+ 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.jspace import JSpace
- from hermespace.jspace_env import JSpaceEnv
+ from hermespace.access import AccessHub, AccessEnv
+ from hermespace.access.engine import workspace_id
+ from hermespace.access.loop import bound_protocol_lines
- js = JSpace(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)
+ 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.
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()
+ 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
+
+ report = shape_execute_report(
+ report,
+ goal=desk.goal,
+ plan=list(desk.plan or []),
+ say=payload.say,
+ decision=desk.decision,
+ message=msg,
+ )
+ desk.say = report
except Exception:
pass
@@ -272,8 +385,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..3350e70 100644
--- a/src/hermespace/world.py
+++ b/src/hermespace/world.py
@@ -1,10 +1,12 @@
"""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.
-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.
@@ -22,6 +24,7 @@
from pathlib import Path
from typing import Any
+from hermespace.atomic import atomic_write_text
from hermespace.paths import state_dir
@@ -118,7 +121,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)
@@ -242,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:
@@ -295,7 +299,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
@@ -312,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]
@@ -366,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
@@ -829,7 +879,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_access_engine.py b/tests/test_access_engine.py
new file mode 100644
index 0000000..12a02d7
--- /dev/null
+++ b/tests/test_access_engine.py
@@ -0,0 +1,121 @@
+"""AccessEngine — unified open-source J-space for Hermes Agent."""
+
+from __future__ import annotations
+
+import os
+import tempfile
+import unittest
+
+
+class TestAccessEngine(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, 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 = AccessEngine(agent_id=aid)
+ out = eng.connect(query="decode")
+ self.assertTrue(out.get("ok"), msg=out)
+ 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("access_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"], "AccessEngine")
+ self.assertIn("hub_pressure", st["metrics"])
+
+ def test_chain_silent_not_in_user_report(self) -> None:
+ from hermespace import AccessEngine
+
+ 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()
+ 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 AccessEngine
+
+ 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)
+ 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 AccessEngine
+
+ 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"
+ )
+ self.assertFalse(trivial.get("material"))
+ self.assertTrue(material.get("material"), msg=material)
+
+ def test_turn_dual_decode(self) -> None:
+ from hermespace import AccessEngine
+
+ eng = AccessEngine(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("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, AccessEngine
+
+ self.assertTrue(issubclass(HermesBase, AccessEngine))
+ 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_jspace_env.py b/tests/test_access_env.py
similarity index 78%
rename from tests/test_jspace_env.py
rename to tests/test_access_env.py
index f2d2b6a..1828373 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)
@@ -42,12 +42,14 @@ 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("Access lens", md)
+ self.assertNotIn("J-Lens readout", md)
+ self.assertNotIn("Operator 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 +58,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 +69,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 +81,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 +95,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 +106,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 +117,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 +148,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 56%
rename from tests/test_jspace_cube.py
rename to tests/test_access_hub.py
index 9974761..54c41b2 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))
@@ -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,6 +142,64 @@ def test_seal_and_inject(self) -> None:
self.assertIn("block", beat)
self.assertEqual(beat.get("mode"), "standalone")
+ 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,
+ skip_cube_foa_strip,
+ )
+
+ self.assertTrue(skip_cube_foa_strip())
+ self.assertTrue(cube_already_prefetched("deploy"))
+ 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_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")
+ 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("HERMES_HOME", None)
+
def test_strip_budget(self) -> None:
from hermespace.cube_module import normalize_load, strip_budget
@@ -177,7 +241,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 +251,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,11 +268,47 @@ 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)
+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()
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()
diff --git a/tests/test_access_protocol.py b/tests/test_access_protocol.py
new file mode 100644
index 0000000..0aed2f1
--- /dev/null
+++ b/tests/test_access_protocol.py
@@ -0,0 +1,77 @@
+"""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.access.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.access.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_default_on_blocks_incomplete(self) -> None:
+ os.environ.pop("HERMESPACE_OEW", None) # default ON
+ from hermespace.access.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.access.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.access.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.access import (
+ AccessHub,
+ AccessEnv,
+ ProtocolGate,
+ evaluate_material_turn,
+ )
+
+ self.assertTrue(callable(evaluate_material_turn))
+ self.assertTrue(ProtocolGate)
+ self.assertTrue(AccessHub)
+ self.assertTrue(AccessEnv)
+
+
+if __name__ == "__main__":
+ unittest.main()
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()
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()
diff --git a/tests/test_cognition_chain.py b/tests/test_cognition_chain.py
new file mode 100644
index 0000000..33f8bb7
--- /dev/null
+++ b/tests/test_cognition_chain.py
@@ -0,0 +1,130 @@
+"""Two-turn silent chain + live-goal refresh. 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_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
+
+ 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)
+ 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()
+ 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)
+ 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 (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 (prior)", 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_connect_room.py b/tests/test_connect_room.py
new file mode 100644
index 0000000..fd48652
--- /dev/null
+++ b/tests/test_connect_room.py
@@ -0,0 +1,122 @@
+"""Cube-centered connect — agent gains world + Access Workspace + 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("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["access"].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_access_from_warehouse
+ from hermespace.access import AccessHub
+
+ 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_access_from_warehouse(aid, room=room)
+ self.assertTrue(rep.get("ok"))
+ self.assertGreaterEqual(int(rep.get("enriched_peers") or 0), 1)
+ 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)
+ 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_context_surgery.py b/tests/test_context_surgery.py
new file mode 100644
index 0000000..f52727a
--- /dev/null
+++ b/tests/test_context_surgery.py
@@ -0,0 +1,271 @@
+"""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)
+ 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 (prior)", 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)
+ 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_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 (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)
+
+ 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_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()
diff --git a/tests/test_desk_quality.py b/tests/test_desk_quality.py
new file mode 100644
index 0000000..b8ea2de
--- /dev/null
+++ b/tests/test_desk_quality.py
@@ -0,0 +1,220 @@
+"""Desk quality cut — empty-say Lyra turn, FOA collapse, smoke shadow, lens title."""
+
+from __future__ import annotations
+
+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."
+
+
+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()
+ 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, 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.
+ 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("lang_stream:", low)
+ self.assertNotIn("→ a — proceed", low)
+ self.assertNotIn("a — proceed", low)
+ self.assertNotEqual(low, "execute")
+ self.assertNotEqual(low, LIVE_MSG.casefold())
+ first_clause = "write a short readme for the auth fix"
+ self.assertNotEqual(low, first_clause)
+ self.assertEqual(line1, "Write the README")
+ self.assertEqual(line1, next_action_line(message=LIVE_MSG, say="", 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()
+ if not focus:
+ focus = list(desk.focus or [])
+ self.assertLessEqual(len(focus), 4)
+ _assert_pairwise_distinct(self, focus, "FOA")
+ bodies = _verbal_bodies(focus)
+ bind_n = sum(1 for x in focus if str(x).casefold().startswith("[bind") or " | " in str(x))
+ 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)
+
+ neural_focus = []
+ if isinstance(out.meta, dict):
+ 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 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")
+ _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
+ 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.assertEqual(line1, "Write the README")
+
+ def test_access_lens_title(self) -> None:
+ from hermespace.access import AccessEnv
+
+ md = AccessEnv(agent_id="lens-title").lens_markdown()
+ self.assertIn("## Access lens (harness workspace)", md)
+ self.assertNotIn("J-Lens readout", md)
+ self.assertNotIn("Operator lens", 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()
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()
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_hermes_base.py b/tests/test_hermes_base.py
new file mode 100644
index 0000000..02604e5
--- /dev/null
+++ b/tests/test_hermes_base.py
@@ -0,0 +1,72 @@
+"""HermesBase facade — Hermes base as functional Access Workspace."""
+
+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("connect", st["ops"])
+ self.assertIn("lens", st["ops"])
+ self.assertIn("room", st)
+ self.assertEqual(st.get("engine"), "AccessEngine")
+
+ 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
+
+ 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_access_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()
diff --git a/tests/test_hermes_bridge.py b/tests/test_hermes_bridge.py
index f39e8f5..9ee1b3b 100644
--- a/tests/test_hermes_bridge.py
+++ b/tests/test_hermes_bridge.py
@@ -10,24 +10,49 @@ 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)
self.assertIn("context", r)
- self.assertIn("Workbench", r["context"])
- desk = load_desk()
+ self.assertTrue(
+ "Access Engine" in r["context"] or "Workbench" in r["context"]
+ )
+ 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(
- 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)
- on_session_end(session_id="bridge-test")
+ # Dual-decode hint for hosts that only get context
+ self.assertTrue(
+ "user_reply_hint" in inj
+ 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,
+ interrupted=False,
+ )
+ on_session_finalize(session_id="bridge-test")
if __name__ == "__main__":
unittest.main()
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()
diff --git a/tests/test_insight_module.py b/tests/test_insight_module.py
new file mode 100644
index 0000000..de9d9b3
--- /dev/null
+++ b/tests/test_insight_module.py
@@ -0,0 +1,285 @@
+"""Insight cable — perceive_card only, fail-soft, 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 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_lat, situation, **kwargs):
+ self._perceive_calls["n"] += 1
+ return {
+ "card": "UNBOUNDED LATTICE DUMP " + ("x" * 800),
+ "usable": True,
+ "matches": [{"title": "should-not-appear"}],
+ }
+
+ def plan(self_lat, situation, **kwargs):
+ self._plan_calls["n"] += 1
+ return {"steps": [{"title": "should-not-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 tearDown(self) -> None:
+ self._mod.stop()
+ self._td.cleanup()
+ os.environ.pop("HERMESPACE_HOME", None)
+
+ 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, "recall": 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"}]}
+
+ 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"
+ self._mod = mock.patch.dict(sys.modules, {"hermes_insight": pkg})
+ self._mod.start()
+
+ def tearDown(self) -> None:
+ self._mod.stop()
+ self._td.cleanup()
+ os.environ.pop("HERMESPACE_HOME", None)
+
+ def test_appends_bounded_perceive_card_only(self) -> None:
+ from hermespace.insight_module import insight_available, insight_card, insight_status
+
+ 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.assertIn("shared-token", rec.get("card") or "")
+ self.assertLessEqual(len(rec.get("card") or ""), 400)
+ 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
+
+ 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)
+
+ 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
+
+ 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.assertLessEqual(len(rec.get("card") or ""), 400)
+ self.assertTrue((rec.get("card") or "").endswith("..."))
+
+
+class TestWorkflowDoesNotHangInsight(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_does_not_inject_insight_strip(self) -> None:
+ from hermespace.io_contract import HermespaceInput
+ from hermespace.workflow import Workflow
+
+ 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": "protect", "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,
+ )
+ mocked.assert_called()
+ 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__":
+ unittest.main()
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_oew_causal.py b/tests/test_oew_causal.py
new file mode 100644
index 0000000..0749bb7
--- /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.access import AccessEnv
+
+ desk = Desk(
+ goal="Fix auth timeout",
+ plan=["repro", "patch TTL", "verify"],
+ decision="A — patch TTL",
+ say="Patching session TTL.",
+ )
+ env = AccessEnv(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.access import AccessEnv
+
+ 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")
+ 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.access import AccessEnv
+
+ 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.access import AccessEnv
+
+ 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")
+ 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.access import AccessEnv
+
+ 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
+ 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("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("Access Workspace", out.context or "")
+
+
+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..b5fc90b
--- /dev/null
+++ b/tests/test_plugin_contract.py
@@ -0,0 +1,164 @@
+"""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)
+ 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)
+
+ 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.assertNotIn("J-Lens readout", result["context"])
+ self.assertLessEqual(len(result["context"]), 2800)
+
+ 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_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",
+ "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",
+ "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
+
+ 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()
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()
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()
+