Compare commits
1 Commits
fix/570-an
...
fix/530
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f53409614 |
27
ansible/inventory/laptops.ini
Normal file
27
ansible/inventory/laptops.ini
Normal file
@@ -0,0 +1,27 @@
|
||||
[laptop_anchor]
|
||||
# 24/7 anchor agents — lowest idle wattage, reliable adapters
|
||||
timmy-anchor-a ansible_host=TIMMY_ANCHOR_A_IP ansible_user=timmy
|
||||
|
||||
[laptop_daylight]
|
||||
# Daylight compute nodes — peak solar hours only
|
||||
timmy-daylight-a ansible_host=TIMMY_DAYLIGHT_A_IP ansible_user=timmy
|
||||
timmy-daylight-b ansible_host=TIMMY_DAYLIGHT_B_IP ansible_user=timmy
|
||||
|
||||
[laptop_pending]
|
||||
# Machines awaiting hardware repair before production duty
|
||||
timmy-daylight-c ansible_host=TIMMY_DAYLIGHT_C_IP ansible_user=timmy
|
||||
|
||||
[desktop_nas]
|
||||
# Heavy compute + 4TB SSD NAS — daylight only due to power draw
|
||||
timmy-desktop-nas ansible_host=TIMMY_DESKTOP_NAS_IP ansible_user=timmy
|
||||
|
||||
[laptops:children]
|
||||
laptop_anchor
|
||||
laptop_daylight
|
||||
laptop_pending
|
||||
desktop_nas
|
||||
|
||||
[laptops:vars]
|
||||
ansible_python_interpreter=/usr/bin/python3
|
||||
timmy_home=/home/timmy/timmy
|
||||
timmy_repo=https://forge.alexanderwhitestone.com/Timmy_Foundation/timmy-home.git
|
||||
137
ansible/playbooks/deploy_laptop_fleet.yml
Normal file
137
ansible/playbooks/deploy_laptop_fleet.yml
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
- name: Deploy Hermes agent fleet on available laptops
|
||||
hosts: laptops
|
||||
gather_facts: true
|
||||
vars:
|
||||
timmy_user: "{{ ansible_user }}"
|
||||
timmy_dir: "/home/{{ timmy_user }}/timmy"
|
||||
hermes_repo: "https://forge.alexanderwhitestone.com/Timmy_Foundation/timmy-home.git"
|
||||
hermes_agent_repo: "https://forge.alexanderwhitestone.com/Timmy_Foundation/hermes-agent.git"
|
||||
|
||||
tasks:
|
||||
- name: Ensure required packages are installed
|
||||
ansible.builtin.package:
|
||||
name:
|
||||
- git
|
||||
- python3
|
||||
- python3-pip
|
||||
- python3-venv
|
||||
- tmux
|
||||
- curl
|
||||
- jq
|
||||
- sqlite3
|
||||
state: present
|
||||
become: true
|
||||
when: ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
|
||||
|
||||
- name: Ensure timmy directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ timmy_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Clone timmy-home repository
|
||||
ansible.builtin.git:
|
||||
repo: "{{ hermes_repo }}"
|
||||
dest: "{{ timmy_dir }}/timmy-home"
|
||||
version: main
|
||||
depth: 1
|
||||
|
||||
- name: Clone hermes-agent repository
|
||||
ansible.builtin.git:
|
||||
repo: "{{ hermes_agent_repo }}"
|
||||
dest: "{{ timmy_dir }}/hermes-agent"
|
||||
version: main
|
||||
depth: 1
|
||||
|
||||
- name: Create Python virtual environment
|
||||
ansible.builtin.command:
|
||||
cmd: "python3 -m venv {{ timmy_dir }}/venv"
|
||||
creates: "{{ timmy_dir }}/venv/bin/python"
|
||||
|
||||
- name: Install Python dependencies
|
||||
ansible.builtin.pip:
|
||||
name:
|
||||
- requests
|
||||
- pyyaml
|
||||
virtualenv: "{{ timmy_dir }}/venv"
|
||||
|
||||
- name: Ensure systemd user directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ ansible_env.HOME | default('/home/' + timmy_user) }}/.config/systemd/user"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
|
||||
|
||||
- name: Deploy anchor agent systemd user service
|
||||
ansible.builtin.template:
|
||||
src: "../../configs/hermes-laptop-anchor.service"
|
||||
dest: "{{ ansible_env.HOME | default('/home/' + timmy_user) }}/.config/systemd/user/hermes-laptop-anchor.service"
|
||||
mode: "0644"
|
||||
when:
|
||||
- inventory_hostname in groups['laptop_anchor']
|
||||
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
|
||||
notify: Reload user systemd
|
||||
|
||||
- name: Deploy daylight agent systemd user service
|
||||
ansible.builtin.template:
|
||||
src: "../../configs/hermes-laptop-daylight.service"
|
||||
dest: "{{ ansible_env.HOME | default('/home/' + timmy_user) }}/.config/systemd/user/hermes-laptop-daylight.service"
|
||||
mode: "0644"
|
||||
when:
|
||||
- inventory_hostname in groups['laptop_daylight']
|
||||
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
|
||||
notify: Reload user systemd
|
||||
|
||||
- name: Deploy daylight agent systemd timer
|
||||
ansible.builtin.template:
|
||||
src: "../../configs/hermes-laptop-daylight.timer"
|
||||
dest: "{{ ansible_env.HOME | default('/home/' + timmy_user) }}/.config/systemd/user/hermes-laptop-daylight.timer"
|
||||
mode: "0644"
|
||||
when:
|
||||
- inventory_hostname in groups['laptop_daylight']
|
||||
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
|
||||
notify: Reload user systemd
|
||||
|
||||
- name: Enable and start anchor agent service
|
||||
ansible.builtin.systemd:
|
||||
name: hermes-laptop-anchor.service
|
||||
state: started
|
||||
enabled: true
|
||||
scope: user
|
||||
when:
|
||||
- inventory_hostname in groups['laptop_anchor']
|
||||
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
|
||||
|
||||
- name: Enable daylight agent timer
|
||||
ansible.builtin.systemd:
|
||||
name: hermes-laptop-daylight.timer
|
||||
state: started
|
||||
enabled: true
|
||||
scope: user
|
||||
when:
|
||||
- inventory_hostname in groups['laptop_daylight']
|
||||
- ansible_os_family in ['Debian', 'RedHat', 'Archlinux']
|
||||
|
||||
- name: Create fleet status script
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ timmy_dir }}/scripts/status.sh"
|
||||
content: |
|
||||
#!/bin/bash
|
||||
echo "=== {{ inventory_hostname }} Status ==="
|
||||
echo ""
|
||||
echo "Services:"
|
||||
systemctl --user is-active hermes-laptop-anchor.service 2>/dev/null && echo " anchor: RUNNING" || true
|
||||
systemctl --user is-active hermes-laptop-daylight.service 2>/dev/null && echo " daylight: RUNNING" || true
|
||||
echo ""
|
||||
echo "Disk Usage:"
|
||||
df -h $HOME | tail -1
|
||||
echo ""
|
||||
echo "Memory:"
|
||||
free -h 2>/dev/null | grep Mem || vm_stat 2>/dev/null | head -5
|
||||
mode: "0755"
|
||||
|
||||
handlers:
|
||||
- name: Reload user systemd
|
||||
ansible.builtin.command: systemctl --user daemon-reload
|
||||
changed_when: true
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
# ansible/playbooks/deploy_mempalace.yml — Deploy MemPalace v3.0.0 to fleet wizards.
|
||||
#
|
||||
# Usage:
|
||||
# ansible-playbook -i inventory/hosts.ini playbooks/deploy_mempalace.yml --limit ezra
|
||||
# ansible-playbook -i inventory/hosts.ini playbooks/deploy_mempalace.yml
|
||||
#
|
||||
# Refs: Issue #570
|
||||
|
||||
- name: Deploy MemPalace v3.0.0 to wizard hosts
|
||||
hosts: fleet
|
||||
become: false
|
||||
gather_facts: false
|
||||
vars:
|
||||
mempalace_hermes_home: "{{ ansible_env.HOME }}/.hermes"
|
||||
mempalace_sessions_dir: "{{ mempalace_hermes_home }}/sessions"
|
||||
mempalace_palace_path: "{{ ansible_env.HOME }}/.mempalace/palace"
|
||||
mempalace_wing: "{{ inventory_hostname }}_home"
|
||||
roles:
|
||||
- role: ../roles/mempalace
|
||||
vars:
|
||||
mempalace_venv_path: "{{ ansible_env.HOME }}/.mempalace-venv"
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
# MemPalace role defaults
|
||||
mempalace_package_spec: "mempalace==3.0.0"
|
||||
mempalace_hermes_home: "{{ ansible_env.HOME }}/.hermes"
|
||||
mempalace_sessions_dir: "{{ mempalace_hermes_home }}/sessions"
|
||||
mempalace_palace_path: "{{ ansible_env.HOME }}/.mempalace/palace"
|
||||
mempalace_wing: "{{ inventory_hostname }}_home"
|
||||
mempalace_wakeup_dir: "{{ mempalace_hermes_home }}/wakeups"
|
||||
mempalace_wakeup_file: "{{ mempalace_wakeup_dir }}/{{ mempalace_wing }}.txt"
|
||||
mempalace_venv_path: "{{ ansible_env.HOME }}/.mempalace-venv"
|
||||
mempalace_config_path: "{{ mempalace_hermes_home }}/mempalace.yaml"
|
||||
mempalace_mcp_config_path: "{{ mempalace_hermes_home }}/hermes-mcp-mempalace.yaml"
|
||||
mempalace_session_hook_path: "{{ mempalace_hermes_home }}/session-start-mempalace.sh"
|
||||
mempalace_run_mining: true
|
||||
mempalace_run_search_test: true
|
||||
mempalace_run_wake_up: true
|
||||
@@ -1,2 +0,0 @@
|
||||
---
|
||||
dependencies: []
|
||||
@@ -1,119 +0,0 @@
|
||||
---
|
||||
# MemPalace v3.0.0 deployment role for fleet wizards.
|
||||
# Refs: Issue #570
|
||||
|
||||
- name: Ensure mempalace venv directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ mempalace_venv_path }}"
|
||||
state: directory
|
||||
mode: '0750'
|
||||
|
||||
- name: Create mempalace virtual environment
|
||||
ansible.builtin.command:
|
||||
cmd: "python3 -m venv {{ mempalace_venv_path }}"
|
||||
creates: "{{ mempalace_venv_path }}/bin/python"
|
||||
|
||||
- name: Install mempalace package
|
||||
ansible.builtin.pip:
|
||||
name: "{{ mempalace_package_spec }}"
|
||||
virtualenv: "{{ mempalace_venv_path }}"
|
||||
virtualenv_command: "{{ mempalace_venv_path }}/bin/python -m venv"
|
||||
|
||||
- name: Ensure Hermes home directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ mempalace_hermes_home }}"
|
||||
state: directory
|
||||
mode: '0750'
|
||||
|
||||
- name: Ensure sessions directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ mempalace_sessions_dir }}"
|
||||
state: directory
|
||||
mode: '0750'
|
||||
|
||||
- name: Ensure wakeup directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ mempalace_wakeup_dir }}"
|
||||
state: directory
|
||||
mode: '0750'
|
||||
|
||||
- name: Ensure palace directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ mempalace_palace_path }}"
|
||||
state: directory
|
||||
mode: '0750'
|
||||
|
||||
- name: Deploy mempalace.yaml configuration
|
||||
ansible.builtin.template:
|
||||
src: mempalace.yaml.j2
|
||||
dest: "{{ mempalace_config_path }}"
|
||||
mode: '0640'
|
||||
|
||||
- name: Deploy Hermes MCP mempalace config
|
||||
ansible.builtin.template:
|
||||
src: hermes-mcp-mempalace.yaml.j2
|
||||
dest: "{{ mempalace_mcp_config_path }}"
|
||||
mode: '0640'
|
||||
|
||||
- name: Deploy session-start wake-up hook
|
||||
ansible.builtin.template:
|
||||
src: session-start-mempalace.sh.j2
|
||||
dest: "{{ mempalace_session_hook_path }}"
|
||||
mode: '0750'
|
||||
|
||||
- name: Mine Hermes home directory
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
echo "" | {{ mempalace_venv_path }}/bin/mempalace mine {{ mempalace_hermes_home }} --config {{ mempalace_config_path }}
|
||||
args:
|
||||
executable: /bin/bash
|
||||
when: mempalace_run_mining | bool
|
||||
register: mine_home_result
|
||||
changed_when: mine_home_result.rc == 0
|
||||
|
||||
- name: Mine session history
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
echo "" | {{ mempalace_venv_path }}/bin/mempalace mine {{ mempalace_sessions_dir }} --mode convos --config {{ mempalace_config_path }}
|
||||
args:
|
||||
executable: /bin/bash
|
||||
when: mempalace_run_mining | bool
|
||||
register: mine_sessions_result
|
||||
changed_when: mine_sessions_result.rc == 0
|
||||
|
||||
- name: Run search test
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
{{ mempalace_venv_path }}/bin/mempalace search "common queries" --config {{ mempalace_config_path }} | head -20
|
||||
args:
|
||||
executable: /bin/bash
|
||||
when: mempalace_run_search_test | bool
|
||||
register: search_test_result
|
||||
changed_when: false
|
||||
|
||||
- name: Generate wake-up context
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
{{ mempalace_venv_path }}/bin/mempalace wake-up --config {{ mempalace_config_path }} > {{ mempalace_wakeup_file }}
|
||||
export HERMES_MEMPALACE_WAKEUP_FILE="{{ mempalace_wakeup_file }}"
|
||||
printf '[MemPalace] wake-up context refreshed: %s\n' "$HERMES_MEMPALACE_WAKEUP_FILE"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
when: mempalace_run_wake_up | bool
|
||||
register: wake_up_result
|
||||
changed_when: wake_up_result.rc == 0
|
||||
|
||||
- name: Report MemPalace deployment summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "MemPalace deployed for {{ inventory_hostname }}"
|
||||
- "Package: {{ mempalace_package_spec }}"
|
||||
- "Config: {{ mempalace_config_path }}"
|
||||
- "Palace: {{ mempalace_palace_path }}"
|
||||
- "Wake-up: {{ mempalace_wakeup_file }}"
|
||||
- "MCP config: {{ mempalace_mcp_config_path }}"
|
||||
- "Session hook: {{ mempalace_session_hook_path }}"
|
||||
- "Home mine: {{ 'OK' if mine_home_result.rc | default(1) == 0 else 'SKIPPED' }}"
|
||||
- "Sessions mine: {{ 'OK' if mine_sessions_result.rc | default(1) == 0 else 'SKIPPED' }}"
|
||||
- "Search test: {{ 'OK' if search_test_result.rc | default(1) == 0 else 'SKIPPED' }}"
|
||||
- "Wake-up: {{ 'OK' if wake_up_result.rc | default(1) == 0 else 'SKIPPED' }}"
|
||||
@@ -1,6 +0,0 @@
|
||||
mcp_servers:
|
||||
mempalace:
|
||||
command: "{{ mempalace_venv_path }}/bin/python"
|
||||
args:
|
||||
- -m
|
||||
- mempalace.mcp_server
|
||||
@@ -1,21 +0,0 @@
|
||||
wing: {{ mempalace_wing }}
|
||||
palace: {{ mempalace_palace_path }}
|
||||
rooms:
|
||||
- name: sessions
|
||||
description: Conversation history and durable agent transcripts
|
||||
globs:
|
||||
- "*.json"
|
||||
- "*.jsonl"
|
||||
- name: config
|
||||
description: Hermes configuration and runtime settings
|
||||
globs:
|
||||
- "*.yaml"
|
||||
- "*.yml"
|
||||
- "*.toml"
|
||||
- name: docs
|
||||
description: Notes, markdown docs, and operating reports
|
||||
globs:
|
||||
- "*.md"
|
||||
- "*.txt"
|
||||
people: []
|
||||
projects: []
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if command -v {{ mempalace_venv_path }}/bin/mempalace >/dev/null 2>&1; then
|
||||
mkdir -p "{{ mempalace_wakeup_dir }}"
|
||||
{{ mempalace_venv_path }}/bin/mempalace wake-up --config {{ mempalace_config_path }} > "{{ mempalace_wakeup_file }}"
|
||||
export HERMES_MEMPALACE_WAKEUP_FILE="{{ mempalace_wakeup_file }}"
|
||||
printf '[MemPalace] wake-up context refreshed: %s\n' "$HERMES_MEMPALACE_WAKEUP_FILE"
|
||||
fi
|
||||
15
configs/hermes-laptop-anchor.service
Normal file
15
configs/hermes-laptop-anchor.service
Normal file
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Hermes Laptop Anchor Agent (24/7)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=%h/timmy/hermes-agent
|
||||
ExecStart=%h/timmy/venv/bin/python %h/timmy/hermes-agent/run_agent.py
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
Environment="HOME=%h"
|
||||
Environment="HERMES_HOME=%h/.hermes"
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
16
configs/hermes-laptop-daylight.service
Normal file
16
configs/hermes-laptop-daylight.service
Normal file
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Hermes Laptop Daylight Agent
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=%h/timmy/hermes-agent
|
||||
ExecStart=%h/timmy/venv/bin/python %h/timmy/hermes-agent/run_agent.py
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
RuntimeMaxSec=6h
|
||||
Environment="HOME=%h"
|
||||
Environment="HERMES_HOME=%h/.hermes"
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
9
configs/hermes-laptop-daylight.timer
Normal file
9
configs/hermes-laptop-daylight.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Run Hermes daylight agent during peak solar hours
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 10:00:00
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
67
configs/laptop-fleet-manifest.yaml
Normal file
67
configs/laptop-fleet-manifest.yaml
Normal file
@@ -0,0 +1,67 @@
|
||||
# LAB-005: Laptop Fleet Manifest
|
||||
# Production manifest for the 6-machine Timmy Foundation laptop fleet.
|
||||
# Edit this file when hardware changes, then regenerate the deployment plan:
|
||||
# python3 scripts/plan_laptop_fleet.py configs/laptop-fleet-manifest.yaml --markdown > docs/LAB-005-laptop-fleet-deployment.md
|
||||
|
||||
fleet_name: timmy-laptop-fleet
|
||||
machines:
|
||||
- hostname: timmy-anchor-a
|
||||
machine_type: laptop
|
||||
ram_gb: 16
|
||||
cpu_cores: 8
|
||||
os: macOS
|
||||
adapter_condition: good
|
||||
idle_watts: 11
|
||||
always_on_capable: true
|
||||
notes: candidate 24/7 anchor agent
|
||||
|
||||
- hostname: timmy-anchor-b
|
||||
machine_type: laptop
|
||||
ram_gb: 8
|
||||
cpu_cores: 4
|
||||
os: Linux
|
||||
adapter_condition: good
|
||||
idle_watts: 13
|
||||
always_on_capable: true
|
||||
notes: candidate 24/7 anchor agent
|
||||
|
||||
- hostname: timmy-daylight-a
|
||||
machine_type: laptop
|
||||
ram_gb: 32
|
||||
cpu_cores: 10
|
||||
os: macOS
|
||||
adapter_condition: ok
|
||||
idle_watts: 22
|
||||
always_on_capable: true
|
||||
notes: higher-performance daylight compute
|
||||
|
||||
- hostname: timmy-daylight-b
|
||||
machine_type: laptop
|
||||
ram_gb: 16
|
||||
cpu_cores: 8
|
||||
os: Linux
|
||||
adapter_condition: ok
|
||||
idle_watts: 19
|
||||
always_on_capable: true
|
||||
notes: daylight compute node
|
||||
|
||||
- hostname: timmy-daylight-c
|
||||
machine_type: laptop
|
||||
ram_gb: 8
|
||||
cpu_cores: 4
|
||||
os: Windows
|
||||
adapter_condition: needs_replacement
|
||||
idle_watts: 17
|
||||
always_on_capable: false
|
||||
notes: repair power adapter before production duty
|
||||
|
||||
- hostname: timmy-desktop-nas
|
||||
machine_type: desktop
|
||||
ram_gb: 64
|
||||
cpu_cores: 12
|
||||
os: Linux
|
||||
adapter_condition: good
|
||||
idle_watts: 58
|
||||
always_on_capable: false
|
||||
has_4tb_ssd: true
|
||||
notes: desktop plus 4TB SSD NAS and heavy compute during peak sun
|
||||
30
docs/LAB-005-laptop-fleet-deployment.md
Normal file
30
docs/LAB-005-laptop-fleet-deployment.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Laptop Fleet Deployment Plan
|
||||
|
||||
Fleet: timmy-laptop-fleet
|
||||
Machine count: 6
|
||||
24/7 anchor agents: timmy-anchor-a, timmy-anchor-b
|
||||
Desktop/NAS: timmy-desktop-nas
|
||||
Daylight schedule: 10:00-16:00
|
||||
|
||||
## Role mapping
|
||||
|
||||
| Hostname | Role | Schedule | Duty cycle |
|
||||
|---|---|---|---|
|
||||
| timmy-anchor-a | anchor_agent | 24/7 | continuous |
|
||||
| timmy-anchor-b | anchor_agent | 24/7 | continuous |
|
||||
| timmy-daylight-a | daylight_agent | 10:00-16:00 | peak_solar |
|
||||
| timmy-daylight-b | daylight_agent | 10:00-16:00 | peak_solar |
|
||||
| timmy-daylight-c | daylight_agent | 10:00-16:00 | peak_solar |
|
||||
| timmy-desktop-nas | desktop_nas | 10:00-16:00 | daylight_only |
|
||||
|
||||
## Machine inventory
|
||||
|
||||
| Hostname | Type | RAM | CPU cores | OS | Adapter | Idle watts | Notes |
|
||||
|---|---|---:|---:|---|---|---:|---|
|
||||
| timmy-anchor-a | laptop | 16 | 8 | macOS | good | 11 | candidate 24/7 anchor agent |
|
||||
| timmy-anchor-b | laptop | 8 | 4 | Linux | good | 13 | candidate 24/7 anchor agent |
|
||||
| timmy-daylight-a | laptop | 32 | 10 | macOS | ok | 22 | higher-performance daylight compute |
|
||||
| timmy-daylight-b | laptop | 16 | 8 | Linux | ok | 19 | daylight compute node |
|
||||
| timmy-daylight-c | laptop | 8 | 4 | Windows | needs_replacement | 17 | repair power adapter before production duty |
|
||||
| timmy-desktop-nas | desktop | 64 | 12 | Linux | good | 58 | desktop plus 4TB SSD NAS and heavy compute during peak sun |
|
||||
|
||||
@@ -146,23 +146,6 @@ That bundle writes:
|
||||
- `session-start-mempalace.sh`
|
||||
- `issue-568-comment-template.md`
|
||||
|
||||
## Fleet Ansible deployment
|
||||
|
||||
Deploy MemPalace to Ezra (or the whole fleet) with the Ansible playbook:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/deploy_mempalace.yml --limit ezra
|
||||
```
|
||||
|
||||
This playbook:
|
||||
1. Creates a dedicated venv and installs `mempalace==3.0.0`
|
||||
2. Deploys `mempalace.yaml`, MCP config, and session-start hook
|
||||
3. Mines the Hermes home and sessions directories
|
||||
4. Runs a search smoke test
|
||||
5. Generates the wake-up context file
|
||||
|
||||
Set `mempalace_run_mining=false` to skip mining on hosts where the corpus is already populated.
|
||||
|
||||
## Why this shape
|
||||
|
||||
- `wing: ezra_home` matches the issue's Ezra-specific integration target.
|
||||
|
||||
@@ -50,3 +50,43 @@ def test_manifest_template_is_valid_yaml() -> None:
|
||||
data = yaml.safe_load(Path("docs/laptop-fleet-manifest.example.yaml").read_text())
|
||||
assert data["fleet_name"] == "timmy-laptop-fleet"
|
||||
assert len(data["machines"]) == 6
|
||||
|
||||
|
||||
def test_production_manifest_exists_and_is_valid() -> None:
|
||||
assert Path("configs/laptop-fleet-manifest.yaml").exists()
|
||||
data = yaml.safe_load(Path("configs/laptop-fleet-manifest.yaml").read_text())
|
||||
assert data["fleet_name"] == "timmy-laptop-fleet"
|
||||
assert len(data["machines"]) == 6
|
||||
plan = build_plan(data)
|
||||
assert plan["desktop_nas"] == "timmy-desktop-nas"
|
||||
assert len(plan["anchor_agents"]) == 2
|
||||
|
||||
|
||||
def test_deployment_plan_generated() -> None:
|
||||
assert Path("docs/LAB-005-laptop-fleet-deployment.md").exists()
|
||||
content = Path("docs/LAB-005-laptop-fleet-deployment.md").read_text()
|
||||
assert "24/7 anchor agents: timmy-anchor-a, timmy-anchor-b" in content
|
||||
assert "Daylight schedule: 10:00-16:00" in content
|
||||
assert "desktop_nas" in content
|
||||
|
||||
|
||||
def test_ansible_playbook_exists() -> None:
|
||||
assert Path("ansible/playbooks/deploy_laptop_fleet.yml").exists()
|
||||
|
||||
|
||||
def test_ansible_laptop_inventory_exists() -> None:
|
||||
assert Path("ansible/inventory/laptops.ini").exists()
|
||||
content = Path("ansible/inventory/laptops.ini").read_text()
|
||||
assert "[laptop_anchor]" in content
|
||||
assert "[laptop_daylight]" in content
|
||||
assert "[desktop_nas]" in content
|
||||
|
||||
|
||||
def test_systemd_service_templates_exist() -> None:
|
||||
assert Path("configs/hermes-laptop-anchor.service").exists()
|
||||
assert Path("configs/hermes-laptop-daylight.service").exists()
|
||||
assert Path("configs/hermes-laptop-daylight.timer").exists()
|
||||
anchor = Path("configs/hermes-laptop-anchor.service").read_text()
|
||||
daylight = Path("configs/hermes-laptop-daylight.service").read_text()
|
||||
assert "Restart=always" in anchor
|
||||
assert "RuntimeMaxSec=6h" in daylight
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
ROLE_PATH = ROOT / "ansible" / "roles" / "mempalace"
|
||||
PLAYBOOK_PATH = ROOT / "ansible" / "playbooks" / "deploy_mempalace.yml"
|
||||
|
||||
|
||||
class TestMempalaceAnsibleRole(unittest.TestCase):
|
||||
def test_role_directory_structure_exists(self):
|
||||
self.assertTrue(ROLE_PATH.exists(), "mempalace role directory missing")
|
||||
for subdir in ["tasks", "templates", "defaults", "meta"]:
|
||||
self.assertTrue(
|
||||
(ROLE_PATH / subdir).exists(),
|
||||
f"mempalace role subdir missing: {subdir}",
|
||||
)
|
||||
|
||||
def test_role_defaults_contains_required_variables(self):
|
||||
defaults_path = ROLE_PATH / "defaults" / "main.yml"
|
||||
self.assertTrue(defaults_path.exists())
|
||||
text = defaults_path.read_text(encoding="utf-8")
|
||||
required_vars = [
|
||||
"mempalace_package_spec",
|
||||
"mempalace_hermes_home",
|
||||
"mempalace_sessions_dir",
|
||||
"mempalace_palace_path",
|
||||
"mempalace_wing",
|
||||
"mempalace_wakeup_dir",
|
||||
"mempalace_wakeup_file",
|
||||
"mempalace_venv_path",
|
||||
"mempalace_config_path",
|
||||
"mempalace_mcp_config_path",
|
||||
"mempalace_session_hook_path",
|
||||
"mempalace_run_mining",
|
||||
"mempalace_run_search_test",
|
||||
"mempalace_run_wake_up",
|
||||
]
|
||||
for var in required_vars:
|
||||
self.assertIn(var, text, f"missing default var: {var}")
|
||||
|
||||
def test_role_tasks_contain_required_steps(self):
|
||||
tasks_path = ROLE_PATH / "tasks" / "main.yml"
|
||||
self.assertTrue(tasks_path.exists())
|
||||
text = tasks_path.read_text(encoding="utf-8")
|
||||
required_steps = [
|
||||
"Create mempalace virtual environment",
|
||||
"Install mempalace package",
|
||||
"Deploy mempalace.yaml configuration",
|
||||
"Deploy Hermes MCP mempalace config",
|
||||
"Deploy session-start wake-up hook",
|
||||
"Mine Hermes home directory",
|
||||
"Mine session history",
|
||||
"Run search test",
|
||||
"Generate wake-up context",
|
||||
]
|
||||
for step in required_steps:
|
||||
self.assertIn(step, text, f"missing task: {step}")
|
||||
|
||||
def test_role_templates_are_valid(self):
|
||||
yaml_template = ROLE_PATH / "templates" / "mempalace.yaml.j2"
|
||||
mcp_template = ROLE_PATH / "templates" / "hermes-mcp-mempalace.yaml.j2"
|
||||
hook_template = ROLE_PATH / "templates" / "session-start-mempalace.sh.j2"
|
||||
|
||||
self.assertTrue(yaml_template.exists())
|
||||
self.assertTrue(mcp_template.exists())
|
||||
self.assertTrue(hook_template.exists())
|
||||
|
||||
yaml_text = yaml_template.read_text(encoding="utf-8")
|
||||
self.assertIn("wing: {{ mempalace_wing }}", yaml_text)
|
||||
self.assertIn("palace: {{ mempalace_palace_path }}", yaml_text)
|
||||
self.assertIn("rooms:", yaml_text)
|
||||
|
||||
mcp_text = mcp_template.read_text(encoding="utf-8")
|
||||
self.assertIn("mcp_servers:", mcp_text)
|
||||
self.assertIn("mempalace:", mcp_text)
|
||||
self.assertIn("mempalace.mcp_server", mcp_text)
|
||||
|
||||
hook_text = hook_template.read_text(encoding="utf-8")
|
||||
self.assertIn("mempalace wake-up", hook_text)
|
||||
self.assertIn("HERMES_MEMPALACE_WAKEUP_FILE", hook_text)
|
||||
|
||||
def test_playbook_exists_and_targets_fleet(self):
|
||||
self.assertTrue(PLAYBOOK_PATH.exists(), "deploy_mempalace.yml playbook missing")
|
||||
text = PLAYBOOK_PATH.read_text(encoding="utf-8")
|
||||
self.assertIn("hosts: fleet", text)
|
||||
self.assertIn("../roles/mempalace", text)
|
||||
self.assertIn("mempalace_venv_path", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -85,8 +85,6 @@ class TestMempalaceEzraIntegration(unittest.TestCase):
|
||||
"mcp_servers:",
|
||||
"HERMES_MEMPALACE_WAKEUP_FILE",
|
||||
"Metrics reply for #568",
|
||||
"Fleet Ansible deployment",
|
||||
"ansible-playbook",
|
||||
]
|
||||
for snippet in required:
|
||||
self.assertIn(snippet, text)
|
||||
|
||||
Reference in New Issue
Block a user