diff --git a/AGENT-SETUP.md b/AGENT-SETUP.md index 083a0e2..e659c8b 100644 --- a/AGENT-SETUP.md +++ b/AGENT-SETUP.md @@ -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. @@ -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.` --- diff --git a/CHANGELOG.md b/CHANGELOG.md index f1b4b99..b8f44a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 [--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//CLAUDE.md`). `loci status --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 diff --git a/FIRST-SESSION.md b/FIRST-SESSION.md index c75d811..5518e0b 100644 --- a/FIRST-SESSION.md +++ b/FIRST-SESSION.md @@ -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. --- diff --git a/SETUP-GUIDE.md b/SETUP-GUIDE.md index 018c40e..d472ad5 100644 --- a/SETUP-GUIDE.md +++ b/SETUP-GUIDE.md @@ -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 @@ -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. --- @@ -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. @@ -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 diff --git a/loci-cli/README.md b/loci-cli/README.md index ab72714..b317558 100644 --- a/loci-cli/README.md +++ b/loci-cli/README.md @@ -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 ``` @@ -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//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 @@ -58,9 +64,26 @@ Each command resolves the palace in this order: | `loci read --room ` | 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 ` and `--since ` narrow it | +| `loci wal verify ` | Verify an exported proof bundle offline. `--expect-key ` 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 | diff --git a/loci-cli/src/main.rs b/loci-cli/src/main.rs index 86e043e..ccb55db 100644 --- a/loci-cli/src/main.rs +++ b/loci-cli/src/main.rs @@ -154,6 +154,7 @@ fn cmd_status(palace_arg: Option, 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", }; diff --git a/loci-cli/src/palace.rs b/loci-cli/src/palace.rs index b4cf065..2d04675 100644 --- a/loci-cli/src/palace.rs +++ b/loci-cli/src/palace.rs @@ -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. @@ -14,6 +19,7 @@ const SKIP_DIRS: &[&str] = &["_palace", "node_modules", "target", "cron"]; pub enum Layout { PalaceSubdir, + RoomsDir, RoomsAtRoot, } @@ -65,7 +71,15 @@ pub fn validate(root: &Path) -> Option { 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(), @@ -75,8 +89,9 @@ pub fn validate(root: &Path) -> Option { 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()) { @@ -230,3 +245,84 @@ pub fn latest_handover(p: &Palace) -> Option { } 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 = 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); + } +} diff --git a/templates/CLAUDE-master.md b/templates/CLAUDE-master.md index c87b5e4..c7a7711 100644 --- a/templates/CLAUDE-master.md +++ b/templates/CLAUDE-master.md @@ -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 @@ -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 @@ -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. diff --git a/templates/scheduled-task-template.md b/templates/scheduled-task-template.md index 4ec49a4..86e1da9 100644 --- a/templates/scheduled-task-template.md +++ b/templates/scheduled-task-template.md @@ -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. @@ -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?"