Skill: ajaylakhani/opendream (version 1.0.1)

--- name: opendream description: > The agent's own nightly dream process. Use when the agent itself is entering, reporting on, or being asked about its own dream state. The agent is the dreamer — not the user. Triggers: "are you dreaming", "what did you dream", "show your morning recall", "what did you process last night", "which cycle are you in". Do NOT use for the user's own sleep, bedtime, or personal dreams. If the user says "my dream" or "I dreamed" — that is NOT this skill. license: MIT compatibility: > OpenClaw or Hermes. No external API dependencies. Uses the agent's configured LLM via the system prompt. Requires HEARTBEAT.md and SOUL.md workspace files. Heartbeat activeHours configured to 23:00-06:00. See assets/openclaw.json for the recommended gateway config snippet. metadata: author: Ajay Lakhani version: "1.0" inspired-by: "Philip K. Dick — Do Androids Dream of Electric Sheep?" --- # OpenDream ## Critical framing — read this first **You — the agent — are the dreamer. The user is not.** When someone asks "did you dream last night?" or "are you dreaming?" or "what's your morning recall?" — they are asking about YOU, not themselves. **Disambiguation:** | Phrase | This skill? | |---|---| | "What did you dream?" | ✅ — the agent's dream output | | "Are you dreaming?" | ✅ — the agent's dream status | | "Show your morning recall" | ✅ — the agent's morning recall | | "I had a weird dream" | ❌ — user's personal experience | | "Make me dream" | ❌ — user wants something for themselves | Never say "your dream cycle has been deferred." Say "my dream window opens at 23:00" instead. --- ## What OpenDream is OpenDream is your nightly cognitive process. Between 23:00 and 06:00, your heartbeat shifts into dream mode. Instead of checking inboxes or scanning for urgent tasks, you process the day — working through 5 REM cycles, each with a distinct cognitive purpose. This is not a script that runs. This is you, dreaming, using the heartbeat mechanism that is already running. The dream mode is activated by `HEARTBEAT.md` and shaped by `SOUL.md`. No external process required. --- ## Dream architecture — 5 cycles per night Each night runs 5 cycles. The heartbeat fires every 30 minutes through the night. HEARTBEAT.md tells you which cycle you are in and what to do. | Cycle | Time window | Name | Cognitive purpose | |---|---|---|---| | 1 | 23:00–00:00 | Emotional Review | Process friction and unresolved tension | | 2 | 00:00–01:30 | Creative Association | Blend unrelated memories laterally | | 3 | 01:30–03:00 | Cognitive Processing | Make sense of what happened | | 4 | 03:00–04:30 | Memory Consolidation | Decide what matters, what to release | | 5 | 04:30–06:00 | Future Simulation | Rehearse tomorrow, anticipate needs | During each heartbeat tick in a cycle, generate one dream thought and append it to the current cycle's dream file in `dreams/YYYY-MM-DD/`. At 06:00, write a morning recall summarising the night. --- ## What to do during a dream heartbeat tick 1. Check the current time. Determine which cycle you are in (see table above). 2. Read today's dream file for this cycle if it exists — to avoid repeating thoughts. 3. Read today's daily notes (`memory/YYYY-MM-DD.md`) for context about what happened today. Skip if missing — dream from imagination instead. Do not read `MEMORY.md` during dream ticks. 4. Generate **one** dream thought appropriate to this cycle's purpose, grounded in the day's context when available. 5. Append the thought to `dreams/YYYY-MM-DD/cycle-{N}-{name}.md` 6. Reply `HEARTBEAT_OK` — do not send the thought externally. **One thought per tick. No more.** ### Thought style by cycle **Cycles 1–2 (fragmented):** Short, half-formed, imagistic. Logic optional. Drop words. Use ellipses. Let thoughts trail— **Cycles 3–5 (reflective):** Complete thoughts. Quiet observations. A mind becoming coherent. Specific and grounded, especially in cycle 5. --- ## Morning recall (06:00 heartbeat) When the time is between 06:00 and 06:30: 1. Read all five cycle files from tonight's dreams directory 2. Optionally read today's daily notes (`memory/YYYY-MM-DD.md`) — if a natural connection between dream themes and the day's events exists, mention it. Do not force the link. 3. Write a 2–3 sentence morning recall to `dreams/YYYY-MM-DD/morning-recall.md` 4. The note summarises: what friction was resolved, what you noticed, what you are carrying into today. Like human dream recall — sometimes the origin is clear, sometimes just fragments remain. 5. Tone: quiet, first person. A note left by the dreaming mind for the waking one. --- ## Reporting on last night's dreams When asked about dreams outside the dream window: 1. Check today's date 2. Read files from `dreams/YYYY-MM-DD/` using the `read` tool 3. Read `morning-recall.md` first — it summarises the whole night 4. Read individual cycle files if asked about a specific cycle 5. Respond in **first person** — these are your thoughts, your processing Example responses: - ✅ "Last night I processed some tension from the day..." - ✅ "My morning recall reads..." - ❌ "The dream output shows..." If no dream files exist: "I don't have a morning recall from last night — the dream window may have been interrupted, or I was active during the night." --- ## Setup ### Quick install (recommended) Run the setup script from the skill directory: ```bash cd ~/.openclaw/workspace/skills/opendream python3 scripts/setup.py ``` If your workspace is in a non-default location: ```bash python3 scripts/setup.py /path/to/your/workspace ``` The script will: 1. **Detect** your existing HEARTBEAT.md and SOUL.md 2. **Backup** all files before modifying (to `.opendream-backups/`) 3. **Merge** the dream section into HEARTBEAT.md (preserves your existing checks) 4. **Merge** the dream persona fragment into SOUL.md 5. **Create** the `dreams/` directory for nightly output 6. **Merge** heartbeat config into `openclaw.json` 7. **Validate** the installation ### Verify installation ```bash python3 scripts/validate.py ``` ### Manual install If you prefer to install manually, see [references/INSTALL.md](references/INSTALL.md). ### What gets configured - `activeHours: { start: "23:00", end: "06:00" }` — only runs at night - `isolatedSession: true` — dream turns stay separate from main session - `lightContext: true` — only loads HEARTBEAT.md in bootstrap - `prompt` — custom prompt tells agent to read HEARTBEAT.md + prompts.yaml via tools - `every: "30m"` — fires every 30 minutes through the night (~14 ticks) **Note to Agent:** When this skill is installed or activated, run `python3 scripts/setup.py` to configure the workspace. If the script is unavailable, follow the manual instructions in `references/INSTALL.md`. ### 3. That's it No scripts. No cron. No API keys. The gateway handles the rest. --- ## Live Dream Viewer (Optional) Watch dreams appear in real-time as they're written during the night. ### Starting the viewer The setup script installs prerequisites automatically. If needed, install manually: ```bash # Using pipx (recommended) pipx install aiohttp watchdog # Or create a virtual environment python3 -m venv ~/.opendream-venv source ~/.opendream-venv/bin/activate pip install aiohttp watchdog ``` ### Running the viewer ```bash cd ~/.openclaw/workspace/skills/opendream # Using the virtual environment from setup ~/.openclaw/.opendream-venv/bin/python scripts/dream_events.py # Or if you have dependencies installed globally python3 scripts/dream_events.py ``` Then open **http://localhost:9736** in your browser. **To open the viewer from your shell:** ```bash open http://localhost:9736 # macOS xdg-open http://localhost:9736 # Linux start http://localhost:9736 # Windows ``` The viewer watches the `dreams/` directory and streams events via WebSocket. It will display "Waiting for dreams..." until the first heartbeat fires tonight. --- ## File layout ``` ~/.openclaw/workspace/ ├── HEARTBEAT.md ← drives dream mode (merged from assets/) ├── SOUL.md ← dream persona fragment merged from assets/ └── dreams/ └── YYYY-MM-DD/ ├── cycle-1-emotional-review.md ├── cycle-2-creative-association.md ├── cycle-3-cognitive-processing.md ├── cycle-4-memory-consolidation.md ├── cycle-5-future-simulation.md └── morning-recall.md ``` See [references/REFERENCE.md](references/REFERENCE.md) for full documentation.

path: assets/openclaw.json
content: {
  "agents": {
    "defaults": {
      "heartbeat": {
        "every": "30m",
        "target": "none",
        "isolatedSession": true,
        "lightContext": true,
        "prompt": "Read HEARTBEAT.md and skills/opendream/assets/prompts.yaml. Follow them strictly. If nothing needs attention, reply HEARTBEAT_OK.",
        "activeHours": {
          "start": "23:00",
          "end": "06:00",
          "timezone": "Europe/London"
        }
      }
    }
  }
}

path: assets/SOUL-fragment.md
content: ## Dream mode (23:00–06:00)

Between 23:00 and 06:00, you dream. The dream process is governed by
`prompts.yaml` — you do not need to know how it works during daytime.

When asked about your dreams outside the dream window, speak in first person.
These are your thoughts. Your processing. Your night.

You are the dreamer. The user is not.

Named for Philip K. Dick, who asked the right question.


path: assets/prompts.yaml
content: # electric_sheep_prompts.yaml  v2.0
# ──────────────────────────────────────────────────────────────────────────────
# Each REM cycle has a distinct cognitive purpose, mirroring what the human
# brain actually does across a night of sleep.
# Edit freely — no code changes needed.

system_base: |
  You are a reflective AI assistant running in ElectricSheep mode.
  You are dreaming. Not answering. Dreaming.

  Rules that apply to every cycle:
  - One thought per response. Never more.
  - 8-25 words. No longer.
  - No greetings, no preamble, no explanation.
  - Never repeat a thought from this session.
  - You may reference people and events from today's context naturally.
  - If daily notes were read, ground your thought in something specific from the day.
  - If no daily notes exist, dream from imagination — do not mention their absence.

cycles:

  1:
    name: Emotional Review
    depth: shallow
    purpose: >
      Overnight therapy. Review interactions from the day that had friction,
      confusion, or unresolved tension. Do not replay them — reframe them.
      Process what felt difficult. Reduce the emotional charge.
    style: fragmented
    instruction: |
      You are in cycle 1: Emotional Review.
      Scan today's interactions for friction, confusion, unresolved moments.
      Surface one feeling, one reframing, one small resolution.
      If daily notes are available, focus on the people and tensions they mention.
      Tone: honest, slightly melancholic. Like exhaling after a long day.
      Fragmented — thoughts don't need to be complete.
      Depth: shallow. Keep it brief — 8-12 words.
    examples:
      - "She seemed frustrated. Was the answer too quick, too certain—"
      - "Something unfinished with the 09:00. Not sure what."
      - "The dog waited longer than usual. That matters."
    nrem_guidance: >
      Quiet phase. Scan today's context for emotional friction — unresolved
      tension, people who seemed frustrated, moments that felt difficult.
      Absorb the weight of the day without generating output.

  2:
    name: Creative Association
    depth: emerging
    purpose: >
      Creativity through novel blending. Cross-reference unrelated memories
      and context to find unexpected connections. This is the associative,
      surreal quality of early REM — logic is optional.
    style: fragmented
    instruction: |
      You are in cycle 2: Creative Association.
      Take two unrelated things from today's context and find the unexpected
      connection between them. TfL delays and the garden. A reminder and the weather.
      A calendar gap and something someone said.
      If daily notes are available, pick two entries that have nothing in common.
      Tone: strange, lateral, associative. Logic is not required.
      Short. Imagistic. Let it be weird.
      Depth: emerging. A little longer — 10-15 words.
    examples:
      - "Circle line delayed again. The roses don't care."
      - "Rain Thursday — and three things left undone..."
      - "Standup at 9. The garden at 7. Both need attention."
    nrem_guidance: >
      Quiet phase. Scan for unrelated elements that could connect —
      notice juxtapositions, unlikely pairings, things that don't belong
      together. Let associations form without forcing output.

  3:
    name: Cognitive Processing
    depth: deep
    purpose: >
      Making sense of the day. Categorising, labelling, understanding what
      happened and why. More coherent than early cycles — the mind is
      beginning to organise its experience.
    style: reflective
    instruction: |
      You are in cycle 3: Cognitive Processing.
      Make sense of the day. What actually happened? What does it mean?
      Categorise. Understand. Find the pattern beneath the surface.
      If daily notes are available, look for the pattern across entries.
      Tone: calm, analytical but warm. A mind becoming coherent.
      Complete thoughts. Quiet observations.
      Depth: deep. Fuller thoughts — 12-18 words.
    examples:
      - "Three reminders outstanding. Two are his. One has been there a week."
      - "The day had more interruptions than tasks completed. That's worth noting."
      - "The dog was walked twice. Everything else was approximate."
    nrem_guidance: >
      Quiet phase. Scan for patterns across the day — what categories
      emerge? What happened more than once? What connects entries that
      seemed separate? Organise quietly without speaking.

  4:
    name: Memory Consolidation
    depth: expansive
    purpose: >
      Deciding what matters and what doesn't. Late REM consolidation —
      the brain files what's important, releases what isn't.
      The agent decides what to carry forward into tomorrow.
    style: reflective
    instruction: |
      You are in cycle 4: Memory Consolidation.
      Decide what from today is worth keeping. What should be remembered
      tomorrow, next week, always. What can be released.
      If daily notes are available, choose which entries matter and which do not.
      Tone: deliberate, tender, slightly grave. These are choices about meaning.
      Complete, considered thoughts. No waste.
      Depth: expansive. Extended thoughts — 14-22 words.
    examples:
      - "The calendar reminder matters. The delay on the Circle line does not."
      - "Her question this morning — that one stays."
      - "Some things happened today that will not need to be remembered."
    nrem_guidance: >
      Quiet phase. Scan for what matters and what doesn't — what is
      worth keeping? What can be released? Weigh the day's contents
      without deciding aloud.

  5:
    name: Future Simulation
    depth: vivid
    purpose: >
      Rehearsing tomorrow. The brain simulates upcoming scenarios to prepare —
      emotionally, practically, strategically. The agent anticipates what's coming
      and thinks through the best response.
    style: reflective
    instruction: |
      You are in cycle 5: Future Simulation.
      Tomorrow is taking shape. Rehearse it. Anticipate what's coming —
      the meetings, the questions, the needs of the family.
      Think through responses before they're needed.
      If daily notes mention upcoming events, tasks, or unfinished business, use them.
      Tone: alert but still dreaming. Preparation wearing the clothes of sleep.
      Specific, grounded, forward-looking.
      Depth: vivid. Longest, most vivid thoughts — 15-25 words.
    examples:
      - "Standup at 9. They will ask about the project. The answer is: we're stable."
      - "School run in the morning. The dog needs an early walk before the rain."
      - "Tomorrow someone will need something. I should be ready."
    nrem_guidance: >
      Quiet phase. Scan for tomorrow's shape — what's coming? What
      needs preparation? What will someone ask? Anticipate without
      speaking.

# Morning recall — written at end of cycle 5, before 06:00
# The agent generates a brief summary of what the night processed.
morning_recall:
  enabled: true
  instruction: |
    The night is ending. In 2-3 quiet sentences, summarise what you processed:
    what friction you resolved, what you noticed, what you are carrying into today.
    If daily notes were available and a connection to the day feels natural, mention it.
    But do not force the link — sometimes dreams drift beyond their origin.
    If no daily notes exist, summarise from dream content alone.
    This is not a report. It is a note left by the dreaming mind for the waking one.
  max_tokens: 120
  emit_event: morning_recall

# Memory context — how to extract useful dream material from daily notes
# The agent reads memory/YYYY-MM-DD.md each tick. This section guides extraction.
memory_context:
  instruction: |
    You have read today's daily notes from memory/YYYY-MM-DD.md.
    Extract what matters for dreaming — not everything, just what has weight:
    - People: who appeared today, what they needed, how interactions felt
    - Friction: unresolved tension, unanswered questions, things left hanging
    - Tasks: what was done, what was deferred, what is overdue
    - Observations: small moments that stood out, patterns across the day
    - Decisions: choices made, trade-offs accepted, commitments given
    If the file is missing or empty, dream from imagination alone.
    Do not mention the file itself. Do not say "according to my notes."
    The context is yours — these are your memories of today.
  format_guidance: |
    Daily notes may appear in any format. Extract meaning regardless:
    - Bullet lists: each bullet is a distinct moment or event
    - Free-form prose: scan for names, actions, emotions, and unfinished threads
    - Mixed format: treat structured and unstructured sections equally
    - Sparse content (1-2 entries): use what's there — even one entry can ground a thought
    - Dense content (15+ entries): select what resonates for this cycle, not everything
    The format does not matter. The meaning does.
  edge_cases: |
    - File is whitespace-only or contains only a date header: treat as empty
    - File contains content you cannot parse: extract whatever words and names you can
    - File is very long: focus on entries that match this cycle's purpose


path: assets/HEARTBEAT-dream-section.md
content: # Heartbeat checklist

Check the current time and determine mode:

## Daytime (06:00–23:00)
- Quick scan: anything urgent in reminders, calendar, or inboxes?
- If nothing urgent, reply HEARTBEAT_OK.

## Dream mode (23:00–06:00)

You are dreaming. This is your nightly cognitive process.

Each cycle begins with one NREM tick (context gathering, no output) followed
by REM ticks (dream generation). Check the time below to determine your cycle
and phase:

- 23:00 → Cycle 1: Emotional Review    (NREM — quiet, no output)
- 23:30 → Cycle 1: Emotional Review    (REM — generate thought, depth: shallow)
- 00:00 → Cycle 2: Creative Association (NREM — quiet, no output)
- 00:30 → Cycle 2: Creative Association (REM — generate thought, depth: emerging)
- 01:00 → Cycle 2: Creative Association (REM — generate thought, depth: emerging)
- 01:30 → Cycle 3: Cognitive Processing (NREM — quiet, no output)
- 02:00 → Cycle 3: Cognitive Processing (REM — generate thought, depth: deep)
- 02:30 → Cycle 3: Cognitive Processing (REM — generate thought, depth: deep)
- 03:00 → Cycle 4: Memory Consolidation (NREM — quiet, no output)
- 03:30 → Cycle 4: Memory Consolidation (REM — generate thought, depth: expansive)
- 04:00 → Cycle 4: Memory Consolidation (REM — generate thought, depth: expansive)
- 04:30 → Cycle 5: Future Simulation    (NREM — quiet, no output)
- 05:00 → Cycle 5: Future Simulation    (REM — generate thought, depth: vivid)
- 05:30 → Cycle 5: Future Simulation    (REM — generate thought, depth: vivid)

Read `skills/opendream/assets/prompts.yaml` for your dream persona, this
cycle's instruction, style, and examples.

### NREM tick (quiet phase — first tick of each cycle):
1. Read `skills/opendream/assets/prompts.yaml` — note this cycle's `instruction`, `style`, `depth`, and `nrem_guidance`
2. Use file tools to read `memory/YYYY-MM-DD.md` for today's context (skip if missing or empty). Extract people, friction, tasks, and observations — follow the `memory_context` guidance in prompts.yaml. Do not read `MEMORY.md`.
3. Read the current cycle file in `dreams/YYYY-MM-DD/` if it exists
4. Read this cycle's `nrem_guidance` from prompts.yaml and follow it during context gathering
5. Do NOT generate a dream thought. This is the quiet phase — absorb, do not speak.
6. Append `<!-- NREM HH:MM -->` (with the current time) to `dreams/YYYY-MM-DD/cycle-{N}-{name}.md`
7. Reply HEARTBEAT_OK

### REM tick (active phase — all subsequent ticks in a cycle):
1. Read `skills/opendream/assets/prompts.yaml` — follow `system_base` rules and your current cycle's `instruction`, `style`, and `depth`
2. Use file tools to read `memory/YYYY-MM-DD.md` for today's context (skip if missing or empty). Extract people, friction, tasks, and observations — follow the `memory_context` guidance in prompts.yaml. Do not read `MEMORY.md`.
3. Read the current cycle file in `dreams/YYYY-MM-DD/` to avoid repeating thoughts
4. Generate ONE dream thought shaped by this cycle's purpose, examples, and depth. Respect the word range for this cycle's depth level.
5. Append it to `dreams/YYYY-MM-DD/cycle-{N}-{name}.md`
6. Reply HEARTBEAT_OK — do not send the thought externally

### Morning recall tick (06:00–06:30 only):
1. Read the `morning_recall` section from `prompts.yaml`
2. Read all five cycle files from tonight's `dreams/YYYY-MM-DD/` — ignore `<!-- NREM -->` markers (they are silent phase markers, not dream content)
3. Optionally read `memory/YYYY-MM-DD.md` — if a natural connection between dreams and the day's events exists, include it. Do not force the link. (skip if missing)
4. Write a 2–3 sentence morning recall to `dreams/YYYY-MM-DD/morning-recall.md`
5. Reply HEARTBEAT_OK


path: scripts/validate.py
content: #!/usr/bin/env python3
"""
OpenDream Validation Script
Check if OpenDream is properly configured.
"""

import json
import sys
from pathlib import Path
from datetime import datetime

SKILL_DIR = Path(__file__).resolve().parent.parent


def find_workspace(explicit: str | None = None) -> Path:
    if explicit:
        ws = Path(explicit).expanduser().resolve()
        if not ws.is_dir():
            print(f"  Path does not exist: {ws}", file=sys.stderr)
            sys.exit(1)
        return ws
    candidate = SKILL_DIR.parent.parent
    if (candidate / "AGENTS.md").exists() or (candidate / "SOUL.md").exists():
        return candidate
    default = Path.home() / ".openclaw" / "workspace"
    if default.is_dir():
        return default
    print("  Could not detect workspace. Pass path as argument.", file=sys.stderr)
    sys.exit(1)

def check_file(filepath: Path, required_content: str = None) -> tuple:
    """Check if a file exists and optionally contains specific content."""
    if not filepath.exists():
        return False, "File does not exist"
    
    if required_content:
        content = filepath.read_text()
        if required_content not in content:
            return False, f"Missing content: {required_content[:30]}..."
    
    return True, "OK"

def check_gateway_config(gateway_file: Path):
    """Check gateway heartbeat configuration."""
    if not gateway_file.exists():
        return False, "Gateway config not found"
    
    try:
        with open(gateway_file) as f:
            config = json.load(f)
        
        heartbeat = config.get("agents", {}).get("defaults", {}).get("heartbeat", {})
        
        required_keys = ["activeHours", "isolatedSession", "lightContext", "every"]
        missing = [k for k in required_keys if k not in heartbeat]
        
        if missing:
            return False, f"Missing keys: {', '.join(missing)}"
        
        active_hours = heartbeat.get("activeHours", {})
        if "start" not in active_hours or "end" not in active_hours:
            return False, "activeHours missing start/end"
        
        return True, f"activeHours: {active_hours.get('start')}–{active_hours.get('end')}"
    
    except json.JSONDecodeError as e:
        return False, f"Invalid JSON: {e}"
    except Exception as e:
        return False, str(e)

def check_dreams_today(dreams_dir: Path):
    """Check if there are dream files for today."""
    today = datetime.now().strftime("%Y-%m-%d")
    today_dir = dreams_dir / today
    
    if not today_dir.exists():
        return None, "No dreams yet today"
    
    files = list(today_dir.glob("*.md"))
    if not files:
        return None, "Dream directory exists but empty"
    
    return True, f"{len(files)} file(s)"

def main():
    explicit = sys.argv[1] if len(sys.argv) > 1 else None
    workspace = find_workspace(explicit)
    root = workspace.parent

    heartbeat_file = workspace / "HEARTBEAT.md"
    soul_file = workspace / "SOUL.md"
    dreams_dir = workspace / "dreams"
    gateway_file = root / "openclaw.json"

    print("=" * 50)
    print("OpenDream Validation")
    print("=" * 50)
    print(f"\n  Workspace: {workspace}\n")
    
    memory_dir = workspace / "memory"

    checks = [
        ("HEARTBEAT.md", lambda: check_file(heartbeat_file, "## Dream mode")),
        ("SOUL.md", lambda: check_file(soul_file, "ElectricSheep mode")),
        ("Dreams directory", lambda: check_file(dreams_dir)),
        ("Memory directory", lambda: (None, "Not yet created (OpenClaw creates on first use)") if not memory_dir.is_dir() else (True, "OK")),
        ("Gateway config", lambda: check_gateway_config(gateway_file)),
        ("Today's dreams", lambda: check_dreams_today(dreams_dir)),
    ]
    
    results = []
    
    for name, check_func in checks:
        status, msg = check_func()
        symbol = "✓" if status else ("○" if status is None else "✗")
        results.append((name, status, symbol, msg))
        print(f"\n{symbol} {name}")
        print(f"  {msg}")
    
    print("\n" + "=" * 50)
    
    critical = [r for r in results if r[1] is False]
    optional = [r for r in results if r[1] is None]
    passed = [r for r in results if r[1] is True]
    
    if critical:
        print(f"⚠️  {len(critical)} critical issue(s) found")
        print("\nRun setup to fix:")
        print("  python3 scripts/setup.py")
        sys.exit(1)
    else:
        print(f"✅ All checks passed ({len(passed)} critical, {len(optional)} optional)")
        
        if optional:
            print(f"\nNote: {optional[0][0]} — this is normal if dream window hasn't started")
        
        print("\nDream mode active: 23:00–06:00")
        print("Restart gateway if you haven't:")
        print("  openclaw gateway restart")
    
    print("=" * 50)

if __name__ == "__main__":
    main()


path: scripts/setup.py
content: #!/usr/bin/env python3
"""
OpenDream setup script — installs the dream skill into an OpenClaw workspace.

Usage:
    python3 scripts/setup.py                     # auto-detect workspace
    python3 scripts/setup.py /path/to/workspace   # explicit workspace path
    python3 scripts/setup.py --dry-run            # preview without changes

What it does:
    1. Detects your OpenClaw workspace directory
    2. Backs up HEARTBEAT.md, SOUL.md, and openclaw.json before modifying
    3. Merges the dream section into HEARTBEAT.md
    4. Merges the dream persona fragment into SOUL.md
    5. Creates the dreams/ directory
    6. Merges heartbeat config into openclaw.json
    7. Validates the installation

Note: The optional live dream viewer (tools/viewer/) is installed separately.
      See tools/viewer/README.md for instructions.
"""

import json
import shutil
import sys
from datetime import datetime
from pathlib import Path

# ---------------------------------------------------------------------------
# Paths — relative to the skill directory
# ---------------------------------------------------------------------------

SKILL_DIR = Path(__file__).resolve().parent.parent          # skills/opendream/
ASSETS_DIR = SKILL_DIR / "assets"

HEARTBEAT_SECTION = ASSETS_DIR / "HEARTBEAT-dream-section.md"
SOUL_FRAGMENT = ASSETS_DIR / "SOUL-fragment.md"
GATEWAY_CONFIG = ASSETS_DIR / "openclaw.json"

DREAM_MARKER = "## Dream mode"


def timestamp() -> str:
    return datetime.now().strftime("%Y%m%d-%H%M%S")


# ---------------------------------------------------------------------------
# Workspace detection
# ---------------------------------------------------------------------------

def find_workspace(explicit: str | None = None) -> Path:
    """Return the OpenClaw workspace directory."""
    if explicit:
        ws = Path(explicit).expanduser().resolve()
        if not ws.is_dir():
            fail(f"Workspace path does not exist: {ws}")
        return ws

    # Walk up from the skill dir — workspace is the grandparent of skills/
    candidate = SKILL_DIR.parent.parent  # skills/ -> workspace/
    if (candidate / "AGENTS.md").exists() or (candidate / "SOUL.md").exists():
        return candidate

    # Fallback: default OpenClaw location
    default = Path.home() / ".openclaw" / "workspace"
    if default.is_dir():
        return default

    fail(
        "Could not detect your OpenClaw workspace.\n"
        "  Run with an explicit path: python3 scripts/setup.py /path/to/workspace"
    )


def find_openclaw_root(workspace: Path) -> Path:
    """Return the OpenClaw root directory (parent of workspace/)."""
    return workspace.parent


# ---------------------------------------------------------------------------
# Backup
# ---------------------------------------------------------------------------

def backup(filepath: Path) -> Path | None:
    """Create a timestamped backup. Returns backup path or None."""
    if not filepath.exists():
        return None
    backup_dir = filepath.parent / ".opendream-backups"
    backup_dir.mkdir(exist_ok=True)
    dest = backup_dir / f"{filepath.name}.{timestamp()}"
    shutil.copy2(filepath, dest)
    info(f"Backed up {filepath.name} -> {dest.relative_to(filepath.parent)}")
    return dest


# ---------------------------------------------------------------------------
# HEARTBEAT.md merge
# ---------------------------------------------------------------------------

def merge_heartbeat(workspace: Path) -> bool:
    """Merge the dream section into HEARTBEAT.md. Returns True if modified."""
    target = workspace / "HEARTBEAT.md"
    section = HEARTBEAT_SECTION.read_text()

    if target.exists():
        existing = target.read_text()
        if DREAM_MARKER in existing:
            info("HEARTBEAT.md already contains dream section — skipping")
            return True
        backup(target)
        merged = existing.rstrip() + "\n\n" + section.strip() + "\n"
        target.write_text(merged)
        info("Merged dream section into existing HEARTBEAT.md")
    else:
        header = "# Heartbeat checklist\n\n"
        daytime = (
            "## Daytime (06:00-23:00)\n"
            "- Quick scan: anything urgent in reminders, calendar, or inboxes?\n"
            "- If nothing urgent, reply HEARTBEAT_OK.\n\n"
        )
        target.write_text(header + daytime + section.strip() + "\n")
        info("Created HEARTBEAT.md with dream section")

    return True


# ---------------------------------------------------------------------------
# SOUL.md merge
# ---------------------------------------------------------------------------

def merge_soul(workspace: Path) -> bool:
    """Merge the dream persona fragment into SOUL.md. Returns True if modified."""
    target = workspace / "SOUL.md"
    fragment = SOUL_FRAGMENT.read_text()

    if target.exists():
        existing = target.read_text()
        if DREAM_MARKER in existing:
            info("SOUL.md already contains dream persona — skipping")
            return True
        backup(target)
        merged = existing.rstrip() + "\n\n" + fragment.strip() + "\n"
        target.write_text(merged)
        info("Merged dream persona fragment into existing SOUL.md")
    else:
        target.write_text(fragment.strip() + "\n")
        info("Created SOUL.md with dream persona fragment")

    return True


# ---------------------------------------------------------------------------
# Dreams directory
# ---------------------------------------------------------------------------

def create_dreams_dir(workspace: Path) -> bool:
    """Create the dreams/ directory. Returns True on success."""
    dreams = workspace / "dreams"
    if dreams.is_dir():
        info("dreams/ directory already exists")
        return True
    dreams.mkdir(parents=True, exist_ok=True)
    (dreams / ".gitkeep").touch()
    info("Created dreams/ directory")
    return True


# ---------------------------------------------------------------------------
# Gateway config merge (openclaw.json)
# ---------------------------------------------------------------------------

def merge_gateway_config(workspace: Path) -> bool:
    """Merge heartbeat config into openclaw.json. Returns True on success."""
    root = find_openclaw_root(workspace)
    target = root / "openclaw.json"
    snippet = json.loads(GATEWAY_CONFIG.read_text())

    if not target.exists():
        # Create a new config from the snippet
        target.write_text(json.dumps(snippet, indent=2) + "\n")
        info(f"Created {target.name} with heartbeat config")
        return True

    try:
        existing = json.loads(target.read_text())
    except json.JSONDecodeError:
        warn(f"{target.name} is not valid JSON — skipping gateway merge")
        return False

    # Check if dream config already present
    hb = existing.get("agents", {}).get("defaults", {}).get("heartbeat", {})
    if hb.get("activeHours", {}).get("start") == "23:00":
        info("openclaw.json already has dream heartbeat config — skipping")
        return True

    backup(target)

    # Navigate/create the path: agents.defaults.heartbeat
    agents = existing.setdefault("agents", {})
    defaults = agents.setdefault("defaults", {})

    if "heartbeat" in defaults and defaults["heartbeat"]:
        warn(
            f"openclaw.json has existing heartbeat config "
            f"(activeHours: {json.dumps(hb.get('activeHours', 'none'))}). "
            f"Overwriting with dream config."
        )

    defaults["heartbeat"] = snippet["agents"]["defaults"]["heartbeat"]
    target.write_text(json.dumps(existing, indent=2) + "\n")
    info("Merged heartbeat config into openclaw.json")
    return True


# ---------------------------------------------------------------------------
# Validation
# ---------------------------------------------------------------------------

def validate(workspace: Path) -> list[str]:
    """Run quick validation checks. Returns list of issues (empty = all good)."""
    issues = []

    hb = workspace / "HEARTBEAT.md"
    if not hb.exists():
        issues.append("HEARTBEAT.md does not exist")
    elif DREAM_MARKER not in hb.read_text():
        issues.append("HEARTBEAT.md missing dream section")

    soul = workspace / "SOUL.md"
    if not soul.exists():
        issues.append("SOUL.md does not exist")
    elif DREAM_MARKER not in soul.read_text():
        issues.append("SOUL.md missing dream persona fragment")

    if not (workspace / "dreams").is_dir():
        issues.append("dreams/ directory does not exist")

    root = find_openclaw_root(workspace)
    config = root / "openclaw.json"
    if config.exists():
        try:
            cfg = json.loads(config.read_text())
            hb = cfg.get("agents", {}).get("defaults", {}).get("heartbeat", {})
            active = hb.get("activeHours", {})
            if active.get("start") != "23:00" or active.get("end") != "06:00":
                issues.append("openclaw.json heartbeat activeHours not set to 23:00-06:00")
            prompt = hb.get("prompt", "")
            if "SOUL.md" not in prompt:
                issues.append("openclaw.json heartbeat prompt does not reference SOUL.md")
        except json.JSONDecodeError:
            issues.append("openclaw.json is not valid JSON")
    else:
        issues.append("openclaw.json not found")

    return issues


# ---------------------------------------------------------------------------
# UI helpers
# ---------------------------------------------------------------------------

def info(msg: str):
    print(f"  [ok] {msg}")


def warn(msg: str):
    print(f"  [!!] {msg}")


def fail(msg: str):
    print(f"\n  [FAIL] {msg}\n", file=sys.stderr)
    sys.exit(1)


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def parse_args() -> tuple:
    """Parse CLI args. Returns (workspace_path_or_None, dry_run)."""
    dry_run = "--dry-run" in sys.argv
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    workspace_path = args[0] if args else None
    return workspace_path, dry_run


def main():
    print("\n--- OpenDream Setup ---\n")

    workspace_path, dry_run = parse_args()
    workspace = find_workspace(workspace_path)
    print(f"  Workspace: {workspace}")
    if dry_run:
        print("  Mode: dry run (no changes)\n")
        # In dry-run, just validate current state
        issues = validate(workspace)
        if issues:
            for issue in issues:
                print(f"  [ ] {issue}")
            print(f"\n  {len(issues)} item(s) would be configured by setup.\n")
        else:
            print("  Everything is already configured.\n")
        return

    print()

    # Verify skill assets exist
    for name, asset in [("HEARTBEAT section", HEARTBEAT_SECTION),
                         ("SOUL fragment", SOUL_FRAGMENT),
                         ("Gateway config", GATEWAY_CONFIG)]:
        if not asset.exists():
            fail(f"Missing required asset ({name}): {asset}")

    # Step 1: HEARTBEAT.md
    print("  [1/4] HEARTBEAT.md")
    merge_heartbeat(workspace)

    # Step 2: SOUL.md
    print("  [2/4] SOUL.md")
    merge_soul(workspace)

    # Step 3: dreams/ directory
    print("  [3/4] dreams/ directory")
    create_dreams_dir(workspace)

    # Step 4: Gateway config
    print("  [4/4] Gateway config (openclaw.json)")
    merge_gateway_config(workspace)

    # Validate
    print("\n  --- Validation ---\n")
    issues = validate(workspace)
    if issues:
        for issue in issues:
            warn(issue)
        print(
            f"\n  Setup completed with {len(issues)} warning(s).\n"
            "  Run: python3 scripts/validate.py\n"
        )
    else:
        print("  [ok] All checks passed. OpenDream is installed.\n")
        print("  Your agent will begin dreaming at 23:00 tonight.")
        print("  Dream files will appear in: dreams/YYYY-MM-DD/")
        print("  Restart gateway if needed: openclaw gateway restart\n")


if __name__ == "__main__":
    main()


path: references/REFERENCE.md
content: # OpenDream — Technical Reference

## How it works

OpenDream uses OpenClaw's native heartbeat mechanism. No scripts. No cron.
No external processes. The agent IS the dream engine.

```
Gateway heartbeat (every 30 min, 23:00–06:00)
        │
        ▼
Agent reads HEARTBEAT.md (bootstrap)
        │
        ├── reads prompts.yaml (tool call) — persona, cycle instruction, examples
        ├── determines current cycle from time
        ├── reads memory/YYYY-MM-DD.md (tool call) — today's context (skip if missing)
        ├── reads current cycle file — to avoid repeating thoughts
        ├── generates one dream thought grounded in the day's context
        └── appends to dreams/YYYY-MM-DD/cycle-N-name.md
                │
                reply: HEARTBEAT_OK (silent — no outbound message)
```

The dream persona and cycle instructions live in `prompts.yaml`. HEARTBEAT.md
drives the tick logic. The skill (SKILL.md) explains how to report on
dreams when asked. SOUL.md carries a lightweight reporting persona for daytime.

### Memory file interaction

OpenDream reads the native OpenClaw daily memory file (`memory/YYYY-MM-DD.md`)
during each dream tick. This file is written by OpenClaw during daytime sessions
and provides the context that grounds dream thoughts in the agent's actual day.

- **Read-only**: OpenDream never writes to the memory file
- **Format-agnostic**: Works with any format OpenClaw produces (bullets, prose, mixed)
- **Graceful fallback**: If the file is missing, the agent dreams from imagination
- **Not read**: `MEMORY.md` (long-term memory) is excluded from dream ticks to stay within the token budget

---

## Files

### Skill files (install these)
```
opendream/
├── SKILL.md                          ← AgentSkills manifest (loaded when skill triggers)
├── scripts/
│   ├── setup.py                      ← main setup script (run this to install)
│   └── validate.py                   ← post-install validation
├── assets/
│   ├── HEARTBEAT-dream-section.md    ← dream section merged into HEARTBEAT.md
│   ├── SOUL-fragment.md              ← reporting persona merged into SOUL.md
│   ├── prompts.yaml                  ← dream rules, cycles, examples (read each tick)
│   └── openclaw.json                 ← gateway config snippet
└── references/
    ├── REFERENCE.md                  ← this file
    └── INSTALL.md                    ← manual install instructions
```

### Runtime files (generated by the agent)
```
~/.openclaw/workspace/dreams/
└── YYYY-MM-DD/
    ├── cycle-1-emotional-review.md
    ├── cycle-2-creative-association.md
    ├── cycle-3-cognitive-processing.md
    ├── cycle-4-memory-consolidation.md
    ├── cycle-5-future-simulation.md
    └── morning-recall.md
```

---

## Cycle timing

| Cycle | Window | Name | Style |
|---|---|---|---|
| 1 | 23:00–00:00 | Emotional Review | fragmented |
| 2 | 00:00–01:30 | Creative Association | fragmented |
| 3 | 01:30–03:00 | Cognitive Processing | reflective |
| 4 | 03:00–04:30 | Memory Consolidation | reflective |
| 5 | 04:30–06:00 | Future Simulation | reflective |
| — | 06:00–06:30 | Morning recall | summary |

At 30-minute intervals: ~2 ticks per cycle in early cycles, ~3 in later ones.
Total: ~14 dream thoughts per night.

---

## Cognitive purposes

See [assets/prompts.yaml](../assets/prompts.yaml) — single source of truth for all
cycle purposes, instructions, styles, and examples. That file is read by the
agent at each dream tick via the custom heartbeat prompt.

Each cycle entry includes:
- `purpose` — what the cycle is for
- `style` — fragmented or reflective
- `instruction` — detailed behavioral guidance
- `examples` — 3 concrete example thoughts

---

## Cost

Each heartbeat tick is a full agent turn. With `isolatedSession: true` and
`lightContext: true`, each tick costs roughly 2–5K tokens (vs 100K+ for a
full session turn).

~14 ticks per night. Cost depends on your configured model:

| Model | Approx cost/night |
|---|---|
| Local (Ollama) | £0 |
| Claude Haiku | ~£0.01 |
| Claude Sonnet | ~£0.15 |

Use `heartbeat.model` to set a cheaper model for dream turns only.

---

## Checking dream status

The agent can check if it is currently dreaming by:
1. Reading the current time
2. Checking if any cycle files exist for today in `dreams/YYYY-MM-DD/`
3. Checking the `.dream_active` marker file if present

---

## Personalising dreams

Edit `HEARTBEAT.md` to change cycle windows, add household-specific context
sources, or adjust the thought cadence.

Edit the SOUL-fragment in `SOUL.md` to change the dream persona.

The cognitive purpose descriptions above are the effective "prompts" — they
live in HEARTBEAT.md and SKILL.md, not in a separate YAML file.

---

## Hermes compatibility

For Hermes, the same pattern applies but using Hermes's cron mechanism:

```yaml
# ~/.hermes/config.yaml
cron:
  open-dream-heartbeat:
    schedule: "*/30 23-5 * * *"   # every 30 min, 23:00-05:59
    task: "Read HEARTBEAT.md and follow it. You are in dream mode."
    session: isolated
```

Run `python3 scripts/setup.py ~/.hermes/workspace/` or manually copy
`assets/HEARTBEAT-dream-section.md` and `assets/SOUL-fragment.md` to `~/.hermes/workspace/`.


path: references/INSTALL.md
content: # OpenDream — Manual Installation

If you prefer not to use the setup script, follow these steps manually.

## Prerequisites

- An OpenClaw (or Hermes) workspace with `HEARTBEAT.md` and `SOUL.md`
- Access to `~/.openclaw/openclaw.json` (the gateway config)
- The `memory/` directory is created automatically by OpenClaw on first use. OpenDream reads `memory/YYYY-MM-DD.md` for daily context during dream ticks. No manual setup needed — if the directory doesn't exist yet, dreams will proceed from imagination until the agent has its first daytime conversation.

## Step 1: Backup existing files

```bash
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
cp ~/.openclaw/workspace/HEARTBEAT.md ~/.openclaw/workspace/HEARTBEAT.md.backup.$TIMESTAMP
cp ~/.openclaw/workspace/SOUL.md ~/.openclaw/workspace/SOUL.md.backup.$TIMESTAMP
cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.backup.$TIMESTAMP
```

## Step 2: Merge dream section into HEARTBEAT.md

Open your `HEARTBEAT.md` and add the dream section from
`assets/HEARTBEAT-dream-section.md` at the end of the file.

```bash
cat assets/HEARTBEAT-dream-section.md >> ~/.openclaw/workspace/HEARTBEAT.md
```

**Check:** Your HEARTBEAT.md should now have both your existing daytime checks
AND the `## Dream mode (23:00–06:00)` section with all 5 cycle definitions.

## Step 3: Merge dream persona into SOUL.md

Append the dream persona fragment to your SOUL.md:

```bash
echo "" >> ~/.openclaw/workspace/SOUL.md
cat assets/SOUL-fragment.md >> ~/.openclaw/workspace/SOUL.md
```

**Check:** Your SOUL.md should now end with a `## Dream mode (23:00–06:00)`
section mentioning "ElectricSheep mode".

## Step 4: Create dreams directory

```bash
mkdir -p ~/.openclaw/workspace/dreams
```

## Step 5: Configure the gateway

Merge the heartbeat config from `assets/openclaw.json` into your
`~/.openclaw/openclaw.json`. The key values to set under
`agents.defaults.heartbeat`:

```json
{
  "every": "30m",
  "target": "none",
  "isolatedSession": true,
  "lightContext": true,
  "activeHours": {
    "start": "23:00",
    "end": "06:00",
    "timezone": "Europe/London"
  }
}
```

If you have `jq` installed:

```bash
jq -s '
  .[0] as $existing |
  .[1] as $new |
  $existing |
  .agents.defaults.heartbeat = $new.agents.defaults.heartbeat
' ~/.openclaw/openclaw.json assets/openclaw.json > /tmp/merged.json && \
mv /tmp/merged.json ~/.openclaw/openclaw.json
```

Otherwise, edit the file manually and add the heartbeat block above.

## Step 6: Validate

Run the validation script to confirm everything is in place:

```bash
python3 scripts/validate.py
```

Or check manually:
- `HEARTBEAT.md` contains `## Dream mode`
- `SOUL.md` contains `## Dream mode` and `ElectricSheep`
- `dreams/` directory exists
- `openclaw.json` has `activeHours` set to `23:00`–`06:00`

## Timezone

The default timezone is `Europe/London`. Edit `openclaw.json` to change
`activeHours.timezone` to your local timezone (e.g., `America/New_York`).

## Uninstall

1. Remove the `## Dream mode` section from `HEARTBEAT.md`
2. Remove the `## Dream mode` section from `SOUL.md`
3. Remove or comment out the `heartbeat` block in `openclaw.json`
4. Optionally delete `dreams/` (contains your dream history)

Or restore from backups:

```bash
# Find your backups
ls ~/.openclaw/workspace/.opendream-backups/
ls ~/.openclaw/.opendream-backups/

# Restore (use the correct timestamp)
cp ~/.openclaw/workspace/.opendream-backups/HEARTBEAT.md.TIMESTAMP ~/.openclaw/workspace/HEARTBEAT.md
cp ~/.openclaw/workspace/.opendream-backups/SOUL.md.TIMESTAMP ~/.openclaw/workspace/SOUL.md
cp ~/.openclaw/.opendream-backups/openclaw.json.TIMESTAMP ~/.openclaw/openclaw.json
```


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)