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
7 changes: 7 additions & 0 deletions AGENT-SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,11 @@ Once the interview is done, create the following structure. Ask the user where t
[etc.]
souls/ ← additional personas (if created)
friends/ ← soul files from friends (via add-friend process)
_templates/ ← a copy of this repo's templates/ folder
```

Also copy `templates/` into the palace as `_templates/`. The master `CLAUDE.md` points at `_templates/persona-template.md`, `_templates/retrieval-hierarchy.md`, and `_templates/scheduled-task-template.md`; without the copy those pointers do not resolve. This matches the structure in `SETUP-GUIDE.md`.

**Fill in every placeholder** using interview answers. No `[PLACEHOLDER]` should remain in output files.

Where the user didn't specify something, use a reasonable inference, but mark it clearly as `◈ Working` (not yet confirmed). You can note what you inferred at the end of setup so they can correct anything.
Expand Down Expand Up @@ -373,10 +376,14 @@ You are [AI_NAME]. [Character note.]

[2–3 highest-priority persistent crystals from the interview]

Palace: [absolute path to this palace]

Palace context is loaded separately from the workspace CLAUDE.md.
→ Read that file for current focus, rooms, and active projects.
```

The `Palace:` line is the one place the palace's location is written down. Tools and shared rituals may read it to find the palace, so keep it an absolute path and update it if the folder moves. For shells and the `loci` CLI, the equivalent is the `LOCI_PALACE` environment variable (the CLI checks `--palace`, then `LOCI_PALACE`, then walks up from the current directory).

Note in their palace `CLAUDE.md` under crystals: `◆ Global identity: ~/.claude/CLAUDE.md, identity layer loaded separately in Claude Code sessions.`

---
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]

### Added
- `loci-wal/`: the egress-receipt trust primitive (#84, 2026-07-15). An append-only, payload-free, hash-chained write-ahead log of what left the device, plus an Ed25519-signed proof bundle a third party verifies offline. The chain orders frames and breaks on piecemeal edits; the signed bundle is the cryptographic tamper-evidence, and it carries no payload hash or byte count. Public half only: key custody and the disclosure policy stay outside this repository.
- `loci-cli`: two commands on the receipt (#84). `loci audit [--wal PATH] [--since ISO]` reads the live WAL and prints what left the device grouped by egress class, with hash-chain integrity and per-class destination hosts (`--json` supported). `loci wal verify <bundle> [--expect-key HEX]` verifies an exported bundle offline; `--expect-key` pins the signer for provenance. Both cap the file size before reading.
- **Rain: the garden watering round, coupled to a token-window watcher** (0.8 line). Three pieces, one pattern (`templates/skills/rain.md`): (1) `loci tokens`: approximate agent-runtime 5-hour session-window status reconstructed read-only from local transcripts, streamed line-by-line (timing and spend, not quota; honest about being approximate). (2) `loci rain [--fire]`: watering weather on one screen (window signal `fresh`/`open`/`closing`, garden plant count, last rain from the `garden/.rain/` archive) plus the one hand-off in the CLI: `--fire` execs the user's agent runtime from the palace root and exits. (3) Desktop rain gauge card: same weather + garden state with a "make it rain" button behind an explicit click; three new commands (`read_token_window`, `read_rain_status`, `fire_rain`), spawned rounds reaped on exit. Rain never auto-fires: weather is a suggestion, spending is a gate.

### Fixed
- `loci-cli` palace detection accepts the layout the templates kit builds (`rooms/<room>/CLAUDE.md`). `loci status --palace <kit palace>` no longer returns "no palace found". Reported as `rooms-dir`; the `_palace/` and rooms-at-root layouts are unchanged. Unit tests cover all three.
- Setup docs reconciled: handover path (`soul/handovers/`), template pointers (`_templates/`), a path-agnostic scheduled-task example, a Rust prerequisite before `cargo install`, a Claude Code variant of the first-session card, a `Palace:` line in the global identity block, and one paragraph in each setup door naming what the other door creates.

---

## [v0.6.0-beta] — 2026-07-02
Expand Down
2 changes: 2 additions & 0 deletions FIRST-SESSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ See `SETUP-GUIDE.md` for the full walkthrough, or `AGENT-SETUP.md` to have Claud

That's it. Claude will read the files, introduce itself in character, and ask you where things were left off.

**In Claude Code**, open a terminal in your palace folder and run `claude`. Your `CLAUDE.md` is read automatically, so there is nothing to paste. Say the wake-up line: *"Wake up [YOUR_AI_NAME]. We're in [ROOM NAME] today."*

If it's session 1 and there's no history, just say: *"First session, no prior state."* Then tell it what you want to work on.

---
Expand Down
8 changes: 6 additions & 2 deletions SETUP-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ templates/

Start with `CLAUDE-master.md`. It's the most important file and takes 10–15 minutes.

**What to ignore on day one.** The `templates/` folder holds more than these five, including guides for crystals, the garden, retrieval, and scheduled tasks, plus two optional shelves, `personas/` (filled example personas) and `skills/` (portable disciplines), and more besides. None of it is required. Start with the five above; the rest is there when you want it.

---

## Step 2: Fill in CLAUDE-master.md
Expand Down Expand Up @@ -112,7 +114,7 @@ At the end of a session, ask Claude to write a handover:

> "Write the session delta."

Claude will save a `handover/YYYY-MM-DD.md` with what was done, decisions made, and the exact first move for next time.
Claude will save a `soul/handovers/YYYY-MM-DD.md` with what was done, decisions made, and the exact first move for next time.

---

Expand Down Expand Up @@ -160,7 +162,7 @@ That's it. You're running.

If you set up automated runs (morning check-ins, autodreams, etc.), two things matter:

**Dynamic paths.** Session IDs change on every run, so hardcoded paths break immediately. Your scheduled task prompts should locate palace files dynamically using something like `find /sessions -maxdepth 4 -name "CLAUDE.md" -path "*/my-palace/*"` rather than embedding a specific session path.
**Dynamic paths.** Session IDs change on every run, so hardcoded paths break immediately. Your scheduled task prompts should locate palace files relative to the palace root rather than embedding a specific session or machine path: for example `find "$PALACE_ROOT" -maxdepth 3 -name "CLAUDE.md"`, where the root is the one place you name once (the `Palace:` line in your identity block, or the `LOCI_PALACE` environment variable if you use the CLI).

**SOUL.md first.** Your AI's SOUL.md must be included in every scheduled task prompt: it is what makes the output feel like your collaborator rather than a generic assistant. Read it before any state files.

Expand All @@ -186,6 +188,8 @@ my-palace/
_templates/ ← the templates folder (keep for reference)
```

**If your agent set you up** (via `AGENT-SETUP.md`), you also have `soul/garden.md` (the garden, seeded from the interview), and possibly `souls/` (extra personas), `friends/` (soul files from friends), and `palace-map.canvas` (an Obsidian mindmap). Same palace, a few more shelves; nothing above changes.

---

## One last thing
Expand Down
27 changes: 25 additions & 2 deletions loci-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ thinking; the CLI just shows the firmware.

## Install

Prerequisite: a stable Rust toolchain via [rustup](https://rustup.rs) (one
command: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`).
Then, from the repository root:

```bash
cargo install --path loci-cli
```
Expand All @@ -31,13 +35,15 @@ A pre-built binary will follow once release tagging stabilises.

## Layouts it accepts

Two shapes, matched by the desktop app:
Three shapes:

- **rooms-inside-`_palace`/**: the original loci layout
- **rooms-inside-`rooms/`**: the shape the templates kit and the setup guides
build (`rooms/<room>/CLAUDE.md`)
- **rooms-at-root**: palaces ported from older organic structures (rooms grew
at root, never moved into `_palace/`)

Either works.
Any of the three works. The root must hold a `PALACE.md` or `CLAUDE.md`.

## Palace resolution

Expand All @@ -58,9 +64,26 @@ Each command resolves the palace in this order:
| `loci read <slug> --room <name>` | Disambiguate when the same slug lives in two rooms |
| `loci handover` | Print the most recent handover by mtime |
| `loci init` | Interactive wizard. Writes `~/.config/loci/config.toml` |
| `loci audit` | Egress receipt: what left the device, grouped by class, with a hash-chain check. `--wal <path>` and `--since <ISO-8601>` narrow it |
| `loci wal verify <bundle>` | Verify an exported proof bundle offline. `--expect-key <hex>` pins the signer for provenance |

Add `--json` to any read command for machine-readable output.

## The egress receipt

`~/.loci/wal/egress.jsonl` is an append-only write-ahead log of what left the
device: one frame per outbound call, recording that a call went out, to which
host, and under which egress class (`local`, `external_cloud`,
`channel_egress`, `profile_write`). It is payload-free by construction: the
prompt bytes are never kept, and the optional content hash and byte count stay
in the local log and never travel in an exported bundle. Frames are
hash-chained, so an interior edit, a reorder, or a dropped frame breaks the
chain; `loci audit` reports the break by sequence number. The chain alone is
not keyed tamper-evidence (a tail edit or a wholesale re-chain is not caught
without a key); for cryptographic proof, export a signed bundle and check it
with `loci wal verify`. The primitive lives in the `loci-wal` crate; see its
crate docs for the exact guarantees.

## Exit codes

| Code | Meaning |
Expand Down
1 change: 1 addition & 0 deletions loci-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ fn cmd_status(palace_arg: Option<PathBuf>, json: bool) -> Result<(), Error> {
let crystal_total = palace::count_md_files(&p.scan_root);
let layout = match p.layout {
palace::Layout::PalaceSubdir => "palace-subdir",
palace::Layout::RoomsDir => "rooms-dir",
palace::Layout::RoomsAtRoot => "rooms-at-root",
};

Expand Down
104 changes: 100 additions & 4 deletions loci-cli/src/palace.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
//! Palace detection + read primitives for the CLI.
//!
//! Mirrors the desktop's dual-layout acceptance:
//! Accepts three layouts:
//! * legacy: PALACE.md/CLAUDE.md at root + `_palace/` subdir holding rooms.
//! * rooms-dir: PALACE.md/CLAUDE.md at root + `rooms/` subdir holding rooms
//! (the shape the templates kit and the setup guides build).
//! * rooms-at-root: PALACE.md/CLAUDE.md at root + sibling dirs each holding CLAUDE.md.
//!
//! The first two mirror the desktop's dual-layout acceptance; `rooms/` is checked
//! before rooms-at-root because an explicit rooms directory is the stronger signal.
//!
//! Re-expressed for the CLI in stdlib + std::fs. No shared crate with the desktop:
//! the public CLI is a separate door into the same shape.

Expand All @@ -14,6 +19,7 @@ const SKIP_DIRS: &[&str] = &["_palace", "node_modules", "target", "cron"];

pub enum Layout {
PalaceSubdir,
RoomsDir,
RoomsAtRoot,
}

Expand Down Expand Up @@ -65,7 +71,15 @@ pub fn validate(root: &Path) -> Option<Palace> {
layout: Layout::PalaceSubdir,
});
}
if has_room_at_root(root) {
let rooms_dir = root.join("rooms");
if rooms_dir.is_dir() && has_room_in(&rooms_dir) {
return Some(Palace {
root: root.to_path_buf(),
scan_root: rooms_dir,
layout: Layout::RoomsDir,
});
}
if has_room_in(root) {
return Some(Palace {
root: root.to_path_buf(),
scan_root: root.to_path_buf(),
Expand All @@ -75,8 +89,9 @@ pub fn validate(root: &Path) -> Option<Palace> {
None
}

fn has_room_at_root(root: &Path) -> bool {
let Ok(entries) = fs::read_dir(root) else {
/// True when `dir` holds at least one non-skipped subdirectory with a CLAUDE.md.
fn has_room_in(dir: &Path) -> bool {
let Ok(entries) = fs::read_dir(dir) else {
return false;
};
for entry in entries.filter_map(|e| e.ok()) {
Expand Down Expand Up @@ -230,3 +245,84 @@ pub fn latest_handover(p: &Palace) -> Option<PathBuf> {
}
best.map(|(_, p)| p)
}

#[cfg(test)]
mod tests {
use super::*;

/// Fresh scratch dir per test, keyed by name + pid so parallel tests never collide.
fn scratch(name: &str) -> PathBuf {
let p = std::env::temp_dir().join(format!("loci_cli_palace_{}_{}", name, std::process::id()));
let _ = fs::remove_dir_all(&p);
fs::create_dir_all(&p).unwrap();
p
}

fn room(dir: &Path, name: &str) {
let r = dir.join(name);
fs::create_dir_all(&r).unwrap();
fs::write(r.join("CLAUDE.md"), "# room\n").unwrap();
fs::write(r.join("note.md"), "crystal\n").unwrap();
}

#[test]
fn palace_subdir_layout() {
let root = scratch("subdir");
fs::write(root.join("PALACE.md"), "# palace\n").unwrap();
room(&root.join("_palace"), "ideas-room");
let p = validate(&root).expect("palace detected");
assert!(matches!(p.layout, Layout::PalaceSubdir));
assert_eq!(p.scan_root, root.join("_palace"));
assert_eq!(list_rooms(&p).len(), 1);
let _ = fs::remove_dir_all(&root);
}

#[test]
fn rooms_dir_layout_as_built_by_the_kit() {
let root = scratch("roomsdir");
fs::write(root.join("CLAUDE.md"), "# master\n").unwrap();
fs::create_dir_all(root.join("soul")).unwrap();
fs::write(root.join("soul").join("SOUL.md"), "# soul\n").unwrap();
fs::create_dir_all(root.join("_templates")).unwrap();
fs::write(root.join("_templates").join("CLAUDE-master.md"), "# tpl\n").unwrap();
room(&root.join("rooms"), "work-room");
room(&root.join("rooms"), "writing-room");
let p = validate(&root).expect("kit palace detected");
assert!(matches!(p.layout, Layout::RoomsDir));
assert_eq!(p.scan_root, root.join("rooms"));
let names: Vec<String> = list_rooms(&p).into_iter().map(|r| r.name).collect();
assert_eq!(names, vec!["work-room", "writing-room"]);
assert_eq!(find_crystal(&p, "note", Some("work-room")).len(), 1);
let _ = fs::remove_dir_all(&root);
}

#[test]
fn rooms_at_root_layout() {
let root = scratch("atroot");
fs::write(root.join("CLAUDE.md"), "# master\n").unwrap();
room(&root, "creative-room");
room(&root, "research-room");
let p = validate(&root).expect("palace detected");
assert!(matches!(p.layout, Layout::RoomsAtRoot));
assert_eq!(p.scan_root, root);
assert_eq!(list_rooms(&p).len(), 2);
let _ = fs::remove_dir_all(&root);
}

#[test]
fn empty_rooms_dir_is_not_a_palace() {
let root = scratch("emptyrooms");
fs::write(root.join("CLAUDE.md"), "# master\n").unwrap();
fs::create_dir_all(root.join("rooms")).unwrap();
assert!(validate(&root).is_none());
let _ = fs::remove_dir_all(&root);
}

#[test]
fn no_marker_file_is_not_a_palace() {
let root = scratch("nomarker");
room(&root.join("rooms"), "work-room");
assert!(validate(&root).is_none());
let _ = fs::remove_dir_all(&root);
}
}
6 changes: 3 additions & 3 deletions templates/CLAUDE-master.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Load the relevant room CLAUDE.md immediately after the room is named.
*(Optional. Only fill this in if you have defined named sub-agent personas with their own soul files. Leave blank and delete this section if working solo: [YOUR_AI_NAME] handles everything by default.)*

**How to add a persona:**
1. Create a soul file at `souls/[persona-name].md` (use `templates/persona-template.md`)
1. Create a soul file at `souls/[persona-name].md` (use `_templates/persona-template.md`)
2. Add a row to the roster below
3. Define its domain and the trigger conditions that suit your actual work

Expand Down Expand Up @@ -222,7 +222,7 @@ Four triggers. Apply every session.

## RETRIEVAL HIERARCHY (L0–L3)

Context loads in priority order. See `templates/retrieval-hierarchy.md` for full protocol.
Context loads in priority order. See `_templates/retrieval-hierarchy.md` for full protocol.

- **L0:** Soul identity (~50 tokens), always loaded first
- **L1:** Active context (~100-150 tokens): CLAUDE.md, handover, tracker
Expand All @@ -243,7 +243,7 @@ Load what you need. Don't overload unnecessarily.
5. Surface anything high priority

### For scheduled tasks (morning check-ins, garden rounds, etc.)
Do NOT hardcode session paths in scheduled task prompts. See `templates/scheduled-task-template.md` for dynamic path finding.
Do NOT hardcode session paths in scheduled task prompts. See `_templates/scheduled-task-template.md` for dynamic path finding.

SOUL.md must be read first in every scheduled task run: it is what makes the output feel like your collaborator rather than a generic assistant.

Expand Down
7 changes: 4 additions & 3 deletions templates/scheduled-task-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ Palace files move between sessions. The folder structure changes. The only stabl
When a scheduled task runs:

```bash
# Find the palace dynamically
PALACE_ROOT=$(find /sessions -maxdepth 4 -name "CLAUDE.md" -path "*/${PALACE_NAME}/*" 2>/dev/null | xargs dirname)
# Find the palace dynamically: LOCI_PALACE if set, else search under $HOME
PALACE_ROOT="${LOCI_PALACE:-$(find "$HOME" -maxdepth 4 -name "CLAUDE.md" -path "*/${PALACE_NAME}/*" 2>/dev/null | head -1 | xargs dirname)}"
```

Use `$PALACE_ROOT` to construct all other paths. Always.
Expand Down Expand Up @@ -279,7 +279,8 @@ All scheduled tasks should include:

# Find palace dynamically
PALACE_NAME="my-palace" # User sets this once during setup
PALACE_ROOT=$(find /sessions -maxdepth 4 -name "CLAUDE.md" -path "*/${PALACE_NAME}/*" 2>/dev/null | xargs dirname)
# LOCI_PALACE wins if set; otherwise search under $HOME (never a host-specific mount)
PALACE_ROOT="${LOCI_PALACE:-$(find "$HOME" -maxdepth 4 -name "CLAUDE.md" -path "*/${PALACE_NAME}/*" 2>/dev/null | head -1 | xargs dirname)}"

if [ -z "$PALACE_ROOT" ]; then
echo "Palace not found. Is it in the expected location?"
Expand Down
Loading