Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .devcontainer/memories/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Hermes memory files are versioned (MEMORY.md, USER.md).

# Ephemeral artifacts Hermes writes into the memories dir — never version these.
*.lock
*.log
7 changes: 7 additions & 0 deletions .devcontainer/memories/MEMORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
SKILL-LOADING RULE: Before ANY GitHub/Git operation in a Codespace, ALWAYS load skill `github-codespace` first. This includes: git push, gh pr create, gh run watch, API calls, PR reviews, CI monitoring, auth setup. The skill covers the full workflow (auth -> push -> CI -> PR -> merge). Skipping it causes wasted turns re-deriving known patterns (token extraction from /proc/PID/environ, credential helpers, gh run watch). Also load `codespace-gh-auth` alongside when the immediate task is auth-only.
§
WIKI-SKILL SYNC RULE: When updating a skill in `.devcontainer/skills/`, always check if any wiki article in `.devcontainer/wiki/` references the same topic and needs a corresponding update (or cross-reference). User explicitly asked: "check whether the change in this skill deserve some changes in some LM wiki that we have stored." Wiki = reference knowledge; skill = procedural. They should stay in sync on the same topic.
§
CI path-filter convention (hermes-codespace, user-validated): .devcontainer/memories/** and .devcontainer/skills/** are CONTENT, stay in the runtime group -> 30s lint-check only (lint-check carries a standalone 'Validate symlink persistence' step asserting both symlinks). Only boot scripts (.devcontainer/*.sh, devcontainer.json, workflows) are infrastructure -> full-build. Never move markdown content into infrastructure: user rejected 15-min full-builds for content that doesn't affect install/startup. Self-check Persistence section = 9a+9b only; 9c (tracked-content-exists) removed as redundant with git checkout.
§
gh pr create right after pushing a new branch can fail with GraphQL 'Head sha can't be blank / No commits between main and <branch>' even when commits exist (GitHub ref cache lags the push). Diagnose first: `gh api repos/O/R/compare/main...<branch> --jq .ahead_by` (>=1 => refs fine, cache lag). Reliable fallback: `gh api repos/O/R/pulls --method POST` with JSON {title, head, base, body} — bypasses the GraphQL createPullRequest mutation.
3 changes: 3 additions & 0 deletions .devcontainer/memories/USER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Developer/engineer on Hermes-CodeSpace (repo owner intricko). Wants concise practical responses, working artifacts, honesty w/ caveats. PR-to-main + merge gate; conservative CI (infra full build vs docs/runtime lint). Root-cause fixes (don't weaken tests). Wiki=reference vs skill=procedural; keeps skill+wiki+mnemon in sync. Loads relevant skills proactively. Dislikes convoluted/defensive boot logic — prefers spare, single-responsibility design (chose Option A: authoritative post-create + slim start-hermes repair guard). Wants infra invariants AUTO-VERIFIED in CI, not just documented. Understands Codespace idle = client activity/billing implications.
§
Use Mnemon (mnemon_remember / mnemon_recall) as the primary memory provider instead of the standard memory() tool. Mnemon has no char limit. Only fall back to memory() for structured preference data (that is, target=user or target=memory).
42 changes: 41 additions & 1 deletion .devcontainer/mnemon/seed.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,46 @@
"tags": ["keepalive", "idle-timeout", "platform-idle", "layer-1", "layer-2", "terminal-activity"],
"entities": ["keepalive.sh", "start-hermes.sh", "layer-1", "layer-2", "terminal-activity", "delay-shutdown", "platform"],
"source": "agent"
},
{
"content": "Persistent Memory Option A (validated 2026-08): post-create-cmd.sh is authoritative for symlink creation (runs once on fresh container, right after Hermes install, before Hermes instantiates ~/.hermes/memories). start-hermes.sh keeps only a slim repair guard (~12 lines) for pre-existing containers where postCreateCommand doesn't re-run. Symlink: ~/.hermes/memories → .devcontainer/memories/ (whole folder, skills pattern). .gitignore in tracked dir ignores *.lock *.log. Mnemon primary rule preserved in tracked USER.md.",
"category": "decision",
"importance": 5,
"tags": ["persistent-memory", "option-a", "symlink", "post-create", "start-hermes", "architecture"],
"entities": ["post-create-cmd.sh", "start-hermes.sh", "memories", "symlink", "mnemon"],
"source": "agent"
},
{
"content": "Self-check.sh Persistence section (section 9) validates both memories and skills symlinks: 9a) ~/.hermes/memories → .devcontainer/memories, 9b) ~/.hermes/skills/codespace → .devcontainer/skills. Each handles 3 cases: correct symlink (ok), real dir (fail), missing (fail). 9c (tracked content existence) removed as redundant with git checkout. CI lint-check also validates symlinks via standalone step for runtime changes.",
"category": "decision",
"importance": 4,
"tags": ["self-check", "persistence", "symlink-validation", "ci", "lint-check"],
"entities": ["self-check.sh", "persistence", "memories", "skills", "lint-check"],
"source": "agent"
},
{
"content": "CI path-filter for persistence: .devcontainer/memories/** and .devcontainer/skills/** are in runtime (not infrastructure) so content changes trigger 30s lint-check, not 15min full-build. lint-check now includes 'Validate symlink persistence' step asserting both symlinks. infrastructure remains boot scripts only (post-create-cmd.sh, start-hermes.sh, self-check.sh, devcontainer.json, workflows). This keeps CI fast for content edits while still gating symlink correctness.",
"category": "decision",
"importance": 5,
"tags": ["ci", "path-filter", "runtime", "infrastructure", "lint-check", "full-build"],
"entities": ["devcontainer-ci.yml", "dorny/paths-filter", "memories", "skills", "full-build"],
"source": "agent"
},
{
"content": "Wiki: persistent-memory-proposal.md — Architecture decision document for Hermes persistent memory (MEMORY.md/USER.md) versioning via whole-folder symlink. Covers Option A split (post-create authoritative + start-hermes guard), self-check validation, CI wiring, .gitignore for lock/log files, Mnemon primary rule in USER.md. Read .devcontainer/wiki/persistent-memory-proposal.md for full details.",
"category": "context",
"importance": 4,
"tags": ["wiki", "architecture", "persistent-memory", "proposal", "symlink"],
"entities": ["persistent-memory-proposal", ".devcontainer/wiki", "memories", "symlink", "mnemon"],
"source": "agent"
},
{
"content": "Skill: codespace-persistent-symlinks — Procedural skill for persisting Hermes state (memories + skills) across Codespace rebuilds via whole-folder symlinks. Documents Option A placement, self-check verification, 3-case guard logic, pitfalls (seed block removal, head truncation bug, verification cases). Symlink pattern mirrors skills: ~/.hermes/memories → .devcontainer/memories/, ~/.hermes/skills/codespace → .devcontainer/skills/. Read .devcontainer/skills/codespace-persistent-symlinks/SKILL.md for full procedure.",
"category": "context",
"importance": 4,
"tags": ["skill", "persistent-memory", "symlink", "codespace", "procedure"],
"entities": ["codespace-persistent-symlinks", "skills", "memories", "symlink", "start-hermes.sh"],
"source": "agent"
}
]
}
}
17 changes: 17 additions & 0 deletions .devcontainer/post-create-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,23 @@ if ! command -v hermes &>/dev/null; then
sudo rm -rf /var/lib/apt/lists/*
fi

# Symlink the Hermes memories dir into the repo (skills-style folder symlink).
# postCreateCommand runs exactly once on a FRESH container, so this is the
# authoritative, cleanest place to set it up — before Hermes first instantiates
# ~/.hermes/memories. MEMORY.md/USER.md are guaranteed present in git at this
# point, so we never need to migrate/seed content here. Ephemeral .lock/.log files
# Hermes writes inside are gitignored (see .devcontainer/memories/.gitignore).
HERMES_MEMORIES="$HOME/.hermes/memories"
TRACKED_MEMORIES="${SCRIPT_DIR}/memories"
mkdir -p "$TRACKED_MEMORIES"
if [ "$(readlink "$HERMES_MEMORIES" 2>/dev/null)" != "$TRACKED_MEMORIES" ]; then
rm -rf "$HERMES_MEMORIES"
ln -s "$TRACKED_MEMORIES" "$HERMES_MEMORIES"
echo "[$SCRIPT_NAME] Created memories folder symlink: $HERMES_MEMORIES -> $TRACKED_MEMORIES"
else
echo "[$SCRIPT_NAME] Memories folder symlink already correct"
fi

# Ensure agent-client-protocol (ACP) is installed
echo "[$SCRIPT_NAME] Checking agent-client-protocol (ACP)..."
if command -v hermes &>/dev/null; then
Expand Down
63 changes: 63 additions & 0 deletions .devcontainer/self-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,69 @@ else
echo " (skipped)"
fi

# ── 9. Persistence (symlinks) ──────────────────────────────────────────────────
section "Persistence"

if ! should_skip "persistence"; then
# Resolve repo root from script location (works in CI and local)
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PERSIST_FAIL=0

# 9a. Memories folder symlink
MEM_RUNTIME="$HOME/.hermes/memories"
MEM_TRACKED="$REPO_ROOT/.devcontainer/memories"
if [ -L "$MEM_RUNTIME" ]; then
target="$(readlink "$MEM_RUNTIME")"
if [ "$target" = "$MEM_TRACKED" ]; then
_ok "Memories" "symlink correct -> $MEM_TRACKED"
json_add "memories_symlink" "ok" "symlink correct" "{\"runtime\":\"$MEM_RUNTIME\",\"target\":\"$target\"}"
else
_fail "Memories" "symlink points to '$target' (expected '$MEM_TRACKED')"
json_add "memories_symlink" "fail" "symlink points to $target" "{\"runtime\":\"$MEM_RUNTIME\",\"target\":\"$target\",\"expected\":\"$MEM_TRACKED\"}"
PERSIST_FAIL=1
fi
elif [ -d "$MEM_RUNTIME" ]; then
_fail "Memories" "runtime is a real directory, not a symlink"
json_add "memories_symlink" "fail" "runtime is real directory" "{\"runtime\":\"$MEM_RUNTIME\"}"
PERSIST_FAIL=1
else
_fail "Memories" "runtime missing entirely"
json_add "memories_symlink" "fail" "runtime missing" "{\"runtime\":\"$MEM_RUNTIME\"}"
PERSIST_FAIL=1
fi

# 9b. Skills folder symlink
SKILLS_RUNTIME="$HOME/.hermes/skills/codespace"
SKILLS_TRACKED="$REPO_ROOT/.devcontainer/skills"
if [ -L "$SKILLS_RUNTIME" ]; then
target="$(readlink "$SKILLS_RUNTIME")"
if [ "$target" = "$SKILLS_TRACKED" ]; then
_ok "Skills" "symlink correct -> $SKILLS_TRACKED"
json_add "skills_symlink" "ok" "symlink correct" "{\"runtime\":\"$SKILLS_RUNTIME\",\"target\":\"$target\"}"
else
_fail "Skills" "symlink points to '$target' (expected '$SKILLS_TRACKED')"
json_add "skills_symlink" "fail" "symlink points to $target" "{\"runtime\":\"$SKILLS_RUNTIME\",\"target\":\"$target\",\"expected\":\"$SKILLS_TRACKED\"}"
PERSIST_FAIL=1
fi
elif [ -d "$SKILLS_RUNTIME" ]; then
_fail "Skills" "runtime is a real directory, not a symlink"
json_add "skills_symlink" "fail" "runtime is real directory" "{\"runtime\":\"$SKILLS_RUNTIME\"}"
PERSIST_FAIL=1
else
_fail "Skills" "runtime missing entirely"
json_add "skills_symlink" "fail" "runtime missing" "{\"runtime\":\"$SKILLS_RUNTIME\"}"
PERSIST_FAIL=1
fi

if [ "$PERSIST_FAIL" -eq 0 ]; then
json_add "persistence" "ok" "all symlinks valid" "{}"
else
json_add "persistence" "fail" "one or more persistence checks failed" "{}"
fi
else
echo " (skipped)"
fi

# ── Summary ──────────────────────────────────────────────────────────────────
section "Summary"
echo ""
Expand Down
70 changes: 70 additions & 0 deletions .devcontainer/skills/codespace-persistent-symlinks/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
name: codespace-persistent-symlinks
description: "Persist Hermes state across Codespace rebuilds via symlinks."
---

# Persisting Hermes runtime state via symlinks (Codespace)

Hermes knowledge/memory (`~/.hermes/memories/MEMORY.md`, `USER.md`) and skills die on
Codespace rebuild unless persisted. The durable technique mirrors how the repo already
persists skills: a **whole-folder symlink** from runtime to a git-tracked repo dir.

## The validated pattern

```
runtime: ~/.hermes/memories (a symlink → the tracked folder)
target: .devcontainer/memories/ (git-tracked: MEMORY.md, USER.md, .gitignore)
.gitignore: *.lock *.log (Hermes writes lock/log beside the memories)
```

- Whole-folder symlink (skills-style), NOT per-file links: single point of truth,
no copy-back between `~/.hermes` and the repo, edits flow both ways instantly.
- Hermes memory writes are symlink-safe: `atomic_replace` (utils.py) re-solves the
symlink and writes the real git file — no Hermes change needed.
- Memory path is `~/.hermes/memories/`, NOT `profiles/default/memories/`.

## Placement decision (Option A — validated 2026-08)

Two layers, keep `start-hermes.sh` trivial:

1. **Authoritative = `post-create-cmd.sh`** (`postCreateCommand`, runs once on a FRESH
container). Create the symlink right after Hermes is installed, before Hermes first
instantiates `~/.hermes/memories`. Cleanest moment — nothing to migrate.
2. **Repair guard = `start-hermes.sh`** — keep under ~12 lines. `postCreateCommand`
never re-runs on later boots or on containers created before the feature shipped,
so a guard (`if [ "$(readlink runtime)" != "$target" ]; then ln -s ...`) catches
them. Handle 3 cases: already-correct link (no-op), real dir (repair via
`rm -rf` + `ln -s`), missing entirely (just link).

Do NOT port a first-run migration/seed block into start-hermes.sh. If the tracked files
are committed, that code is dead weight and reads as convoluted.

## Verification (automated)

`self-check.sh` asserts both symlinks in its `Persistence` section (section 9), so CI
fails loudly if either drifts:

- `~/.hermes/memories` → `$REPO_ROOT/.devcontainer/memories`
- `~/.hermes/skills/codespace` → `$REPO_ROOT/.devcontainer/skills`

Handles 3 cases per link: correct symlink (ok), real dir (fail), missing (fail), plus
checks tracked MEMORY.md / USER.md / SKILL.md exist. To enforce it, the CI
`detect-changes` path filter lists `.devcontainer/memories/**` and `.devcontainer/skills/**`
under `infrastructure`, so persistence changes trigger `full-build` (which runs
self-check.sh). Run locally:
`HERMES_WEBTOP_SKIP_CHECKS=services,models,disk,cron,ollama,memory bash .devcontainer/self-check.sh`

## Pitfalls

- **Removing a seed block can silently lose a behavioral nudge.** Before deleting a
start-hermes.sh seed that writes USER.md, fold its content into the tracked USER.md
first so the rule survives (the "use Mnemon as primary memory provider" nudge was
preserved this way).
- **Undo-by-`head -n -1` corrupts memory files.** When testing write-through, remove
ONLY the injected marker line (perl/grep), never tail-trim — you can truncate a real
directive and the file shrinks unexpectedly (observed 1297B → 504B).
- Verify the guard against ALL THREE cases, not just the happy path.

## Sync note (wiki)
- `persistent-memory-proposal.md` — reference/proposal doc vs this procedural skill.
Keep the Option-A split and the self-check wiring (below) mirrored in both.
26 changes: 19 additions & 7 deletions .devcontainer/start-hermes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,6 @@ else
setsid hermes dashboard --port 9119 --no-open > ~/.hermes/logs/dashboard.log 2>&1 &
fi

# Remind Hermes on Mnemon setup if needed
if [ ! -f "$HOME/.hermes/memories/USER.md" ]; then
echo "[$SCRIPT_NAME] Setting up Mnemon USER.md..."
echo 'Always use Mnemon (mnemon_remember / mnemon_recall) as primary memory provider instead of the standard memory() tool. Mnemon has no char limit. Only fall back to memory() for structured preference data (target=user or memory).' > "$HOME/.hermes/memories/USER.md"
fi

# Wait for Hermes dashboard to be ready (replaces brittle sleep 15)
echo "[$SCRIPT_NAME] Waiting for Hermes dashboard to become healthy..."
for i in $(seq 1 20); do
Expand All @@ -141,7 +135,25 @@ else
echo "[$SCRIPT_NAME] Skills symlink already exists"
fi

# 6. Start keepalive (idempotent) — keeps codespace from idle-shutting-down
# 5.5.5 Memories folder symlink — REPAIR GUARD ONLY.
# post-create-cmd.sh is the authoritative creator (runs once on a fresh container,
# before Hermes instantiates ~/.hermes/memories). This guard is a cheap safety net
# for pre-existing containers created before that shipped, where the dir may still
# be a real folder. MEMORY.md/USER.md are assumed committed in .devcontainer/memories/
# (their ephemeral .lock/.log siblings are gitignored there).
MEMORIES_RUNTIME="$HOME/.hermes/memories"
MEMORIES_TRACKED="$WORKSPACE_ROOT/.devcontainer/memories"

if [ "$(readlink "$MEMORIES_RUNTIME" 2>/dev/null)" != "$MEMORIES_TRACKED" ]; then
mkdir -p "$MEMORIES_TRACKED"
rm -rf "$MEMORIES_RUNTIME"
ln -s "$MEMORIES_TRACKED" "$MEMORIES_RUNTIME"
echo "[$SCRIPT_NAME] Repaired memories symlink: $MEMORIES_RUNTIME -> $MEMORIES_TRACKED"
else
echo "[$SCRIPT_NAME] Memories folder symlink already correct"
fi

# 5.6. Starting keepalive (idempotent) — keeps codespace from idle-shutting-down
if ! pgrep -f "keepalive.sh" > /dev/null; then
echo "[$SCRIPT_NAME] Starting keepalive..."
setsid nohup "${SCRIPT_DIR}/keepalive.sh" >> /tmp/keepalive.log 2>&1 &
Expand Down
3 changes: 2 additions & 1 deletion .devcontainer/wiki/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
| [repository-analysis.md](repository-analysis.md) | Repository deep dive — architecture, startup flow, verification, what's used vs unused | architecture, startup, verification, ci |
| [github-actions-testing-plan.md](github-actions-testing-plan.md) | CI/CD testing plan — phased approach, workflow design, service smoke tests, integration tests | ci, testing, github-actions, workflow |
| [persistent-knowledge-proposal.md](persistent-knowledge-proposal.md) | Architecture decision: persistent knowledge system via Git — symlinks, skills, wiki, Mnemon seeding | architecture, knowledge-persistence, symlink, devcontainer |
| [persistent-memory-proposal.md](persistent-memory-proposal.md) | Proposal for versioning Hermes MEMORY.md / USER.md via a symlink architecture (runtime vs tracked). | architecture, memory, persistence, symlink |
| [keepalive-proposal.md](keepalive-proposal.md) | Proposal: Codespace keepalive to mimic client activity and avoid idle shutdown (A: terminal heartbeat, B: /delay-shutdown pinger) | codespace, keepalive, idle-timeout, lifecycle, proposal |
| [codespace-lifecycle.md](codespace-lifecycle.md) | Reference: how Codespaces detects idle & shuts down, diagnosing container death, keeping a codespace alive | codespace, lifecycle, idle, keep-alive, shutdown, reference |

Expand All @@ -33,4 +34,4 @@

---

*Last updated: 2026-08-02*
*Last updated: 2026-08-02*
Loading
Loading