diff --git a/AGENTS.md b/AGENTS.md index 61d105f..934529a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,4 +4,6 @@ - Do not import raw sessions into another harness by default. - Curate memories before adding them under `memories/curated/`. - Keep Hermes-native runtime configuration in `hermes-soul`; this repository is the cross-harness contract. +- Public examples may reference private source repositories by URL/name, but must not quote or copy their private contents. +- Use `scripts/layered_soul.py validate .` before committing structural changes. - When adapting for Colibri, reviewed skills map to `system_skills`, curated memory maps to `system_brain`, and converted operational tasks map to `system_ops`. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c0bdfc9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2026 clawdie + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 828a9e5..4d7f8b3 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,31 @@ # Layered Soul -Layered Soul is the clean cross-harness soul repository for Clawdie agents. +Layered Soul is a public template for harness-agnostic agent identity and context bundles. -It carries durable identity, reviewed user context, and approved skills in a format that Pi, Hermes, Colibri, Codex, Claude Code, Zot, and future harnesses can adapt without copying a whole runtime backup. +It is designed for Clawdie-compatible agents that may run through Pi, Hermes, +Colibri, Codex, Claude Code, Zot, or future harnesses. The repository carries +durable identity, reviewed user context, curated memories, and approved skills +without copying a full runtime backup. -`hermes-soul` can continue to exist as the Hermes-native backup from Debby Linux. This repo is the shared source contract. +## Why this exists + +Agent runtimes already keep useful state, but their native backups are not safe +or portable as-is. For example, a private `hermes-soul` repository may contain +Hermes sessions, sanitized config, cron state, scripts, and memory files from a +Debby Linux host. That is useful for Hermes restore, but it should not be pushed +publicly or blindly imported into other harnesses. + +`layered-soul` is the clean public layer: + +```text +private runtime backup reviewed export portable public/private soul +---------------------- --------------- ---------------------------- +hermes-soul/ ─────► review queue ─────► layered-soul/ + sessions/ no raw sessions SOUL.md + config.yaml no secrets USER.md + memories/*.md curated summaries IDENTITY.md + skills/**/*.md selected skills AGENTS.md +``` ## Core files @@ -19,11 +40,85 @@ It carries durable identity, reviewed user context, and approved skills in a for - `skills/` — reviewed reusable procedures that can seed `system_skills` or harness-native skill directories - `memories/curated/` — reviewed memory summaries that can seed `system_brain` - `adapters/` — notes for materializing the same soul into specific harnesses +- `examples/private-sources/` — safe example configs for connecting private runtime backups +- `scripts/layered_soul.py` — stdlib helper for validation, prompt rendering, and private-source planning ## Rules - No secrets. - No raw chat logs by default. - No harness lock files or runtime caches. -- Raw Hermes sessions stay in `hermes-soul` unless the operator requests summarization. +- Runtime backups such as `hermes-soul` stay private unless the operator deliberately creates a sanitized export. - Durable memory returns to the Layered Memory Fabric. + +## Quick start + +Validate the repository: + +```sh +python3 scripts/layered_soul.py validate . +``` + +Render a compact prompt bundle for a task harness: + +```sh +python3 scripts/layered_soul.py render-prompt . --output /tmp/layered-soul-prompt.md +``` + +Inspect a private Hermes backup without copying private content: + +```sh +python3 scripts/layered_soul.py plan-private-source \ + examples/private-sources/hermes-soul.example.json \ + --source-root ~/hermes-soul +``` + +The planner reports what could be reviewed/exported and what must remain private. +It does not copy raw sessions or secrets. + +## Connecting `hermes-soul` properly + +Use `hermes-soul` as the private Hermes-native backup and `layered-soul` as the +portable contract. + +Recommended flow: + +1. Back up Hermes into the private `hermes-soul` repo from Debby Linux. +2. Run the private-source planner against that checkout. +3. Review candidate memories/skills manually. +4. Copy only sanitized summaries or selected skills into this repo. +5. Validate before committing. + +Example private-source config: + +```json +{ + "name": "hermes-soul-private", + "kind": "hermes-soul", + "repo": "git@code.smilepowered.org:clawdie/hermes-soul.git", + "visibility": "private", + "export_policy": { + "raw_sessions": "exclude", + "runtime_config": "exclude", + "secrets": "exclude", + "memories": "review-required", + "skills": "review-required" + } +} +``` + +See `docs/CONNECT-HERMES-SOUL.md` for the longer playbook. + +## Template usage + +This repository can be used as a Forgejo template. Create concrete soul repos +from it when you want a separate identity bundle for a person, agent, project, +or customer environment. + +Suggested split: + +```text +clawdie/layered-soul public template/schema +clawdie/hermes-soul private Hermes runtime backup +clawdie/-layered-soul curated identity instance when needed +``` diff --git a/docs/CONNECT-HERMES-SOUL.md b/docs/CONNECT-HERMES-SOUL.md new file mode 100644 index 0000000..713f163 --- /dev/null +++ b/docs/CONNECT-HERMES-SOUL.md @@ -0,0 +1,61 @@ +# Connecting private `hermes-soul` to Layered Soul + +`hermes-soul` and `layered-soul` have different jobs. + +| Repo | Visibility | Purpose | +| -------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `hermes-soul` | private | Hermes-native runtime backup from Debby Linux: sessions, sanitized config, scripts, cron, memories, skills. | +| `layered-soul` | public/template or curated instance | Harness-agnostic identity and reviewed context with no secrets or raw sessions. | + +Do not mirror `hermes-soul` into `layered-soul`. Use a review/export workflow. + +## Recommended workflow + +1. Back up Hermes normally into the private `hermes-soul` repo. +2. Run the planner from this repo: + + ```sh + python3 scripts/layered_soul.py plan-private-source \ + examples/private-sources/hermes-soul.example.json \ + --source-root ~/hermes-soul + ``` + +3. Review the candidate memory and skill files locally. +4. Write sanitized summaries into this repo: + + ```text + USER.md + memories/curated/hermes-memory-summary.md + skills//SKILL.md + ``` + +5. Validate before committing: + + ```sh + python3 scripts/layered_soul.py validate . + ``` + +## What may move across + +- Reviewed user context from `memories/USER.md`. +- Reviewed memory summaries from `memories/MEMORY.md`. +- Selected `skills/**/*.md` that are safe and useful across harnesses. +- Converted cron/task ideas after they are rewritten as explicit Ops manifests. + +## What must stay private + +- Raw `sessions/` chat logs. +- `config.yaml` and platform runtime config. +- `channel_directory.json`. +- Tokens, API keys, auth files, browser profiles, lock files, caches, bytecode. + +## Example source config + +See: + +```text +examples/private-sources/hermes-soul.example.json +``` + +The config is safe to publish because it describes the boundary, not the private +content. diff --git a/examples/private-sources/hermes-soul.example.json b/examples/private-sources/hermes-soul.example.json new file mode 100644 index 0000000..612854f --- /dev/null +++ b/examples/private-sources/hermes-soul.example.json @@ -0,0 +1,24 @@ +{ + "name": "hermes-soul-private", + "kind": "hermes-soul", + "repo": "git@code.smilepowered.org:clawdie/hermes-soul.git", + "visibility": "private", + "local_path": "~/hermes-soul", + "export_policy": { + "raw_sessions": "exclude", + "runtime_config": "exclude", + "channel_directory": "exclude", + "cron_jobs": "review-and-convert-to-ops-manifest", + "secrets": "exclude", + "memories": "review-required", + "skills": "review-required", + "scripts": "review-required" + }, + "candidate_mappings": { + "memories/USER.md": "review queue -> USER.md or memories/curated/hermes-user-summary.md", + "memories/MEMORY.md": "review queue -> memories/curated/hermes-memory-summary.md", + "skills/**/*.md": "review queue -> skills/ or Colibri system_skills import", + "cron/*": "review queue -> system_ops task manifest" + }, + "never_copy": ["sessions/*", "config.yaml", "channel_directory.json", "*.lock", "**/__pycache__/**", "**/*.pyc"] +} diff --git a/manifest.json b/manifest.json index a644785..99c9f8a 100644 --- a/manifest.json +++ b/manifest.json @@ -4,6 +4,7 @@ "display_name": "Layered Soul", "description": "Cross-harness durable identity and reviewed context for Clawdie-compatible agents.", "source_of_truth": "git", + "template": true, "core_files": { "soul": "SOUL.md", "user": "USER.md", @@ -14,6 +15,10 @@ "skills": ["skills/**/*.md"], "curated_memory": ["memories/curated/**/*.md"] }, + "tooling": { + "helper": "scripts/layered_soul.py", + "private_source_examples": ["examples/private-sources/hermes-soul.example.json"] + }, "archive_policy": { "raw_sessions": "excluded-by-default", "runtime_config": "adapter-local", diff --git a/scripts/layered_soul.py b/scripts/layered_soul.py new file mode 100755 index 0000000..c73fd72 --- /dev/null +++ b/scripts/layered_soul.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Layered Soul helper. + +Stdlib-only utilities for validating a Layered Soul repository, rendering a +prompt bundle for task harnesses, and planning safe exports from private runtime +backups such as hermes-soul. +""" + +from __future__ import annotations + +import argparse +import fnmatch +import json +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +FORBIDDEN_PATH_PATTERNS = [ + ".env", + "*.key", + "*.pem", + "*.token", + "config.yaml", + "channel_directory.json", + "sessions/*", + "cron/*", + "**/__pycache__/*", + "**/*.pyc", + "**/*.lock", +] + +CORE_FILE_KEYS = ["soul", "user", "identity", "harness_rules"] + + +@dataclass +class Finding: + level: str + message: str + + +def load_json(path: Path) -> Any: + try: + return json.loads(path.read_text(encoding="utf-8")) + except FileNotFoundError: + raise SystemExit(f"missing JSON file: {path}") + except json.JSONDecodeError as exc: + raise SystemExit(f"invalid JSON in {path}: {exc}") + + +def rel_paths(root: Path) -> list[str]: + paths: list[str] = [] + for path in root.rglob("*"): + if ".git" in path.parts: + continue + if path.is_file(): + paths.append(path.relative_to(root).as_posix()) + return sorted(paths) + + +def matches_any(path: str, patterns: list[str]) -> bool: + return any(fnmatch.fnmatch(path, pattern) for pattern in patterns) + + +def validate(root: Path) -> list[Finding]: + findings: list[Finding] = [] + manifest_path = root / "manifest.json" + if not manifest_path.exists(): + return [Finding("error", "manifest.json is required")] + + manifest = load_json(manifest_path) + if manifest.get("schema") != "clawdie.layered-soul.v1": + findings.append( + Finding( + "error", + "manifest.schema must be clawdie.layered-soul.v1", + ) + ) + + core_files = manifest.get("core_files", {}) + for key in CORE_FILE_KEYS: + value = core_files.get(key) + if not value: + findings.append(Finding("error", f"manifest.core_files.{key} is required")) + continue + path = root / value + if not path.exists(): + findings.append(Finding("error", f"core file missing: {value}")) + elif path.stat().st_size == 0: + findings.append(Finding("warn", f"core file is empty: {value}")) + + privacy = manifest.get("privacy", {}) + if privacy.get("secrets") != "excluded": + findings.append(Finding("warn", "privacy.secrets should be 'excluded'")) + if privacy.get("operator_review_required_before_cross_harness_import") is not True: + findings.append( + Finding( + "warn", + "operator_review_required_before_cross_harness_import should be true", + ) + ) + + for path in rel_paths(root): + if matches_any(path, FORBIDDEN_PATH_PATTERNS): + findings.append(Finding("error", f"forbidden runtime/private path: {path}")) + + return findings + + +def render_prompt(root: Path, include_curated: bool) -> str: + manifest = load_json(root / "manifest.json") + core_files = manifest.get("core_files", {}) + sections: list[str] = [] + for key in CORE_FILE_KEYS: + rel = core_files.get(key) + if not rel: + continue + path = root / rel + if not path.exists(): + continue + content = path.read_text(encoding="utf-8").strip() + if content: + sections.append(f"\n\n{content}") + + if include_curated: + for path in sorted((root / "memories" / "curated").glob("*.md")): + content = path.read_text(encoding="utf-8").strip() + if content: + rel = path.relative_to(root).as_posix() + sections.append(f"\n\n{content}") + + return "\n\n---\n\n".join(sections).strip() + "\n" + + +def plan_private_source(config_path: Path, source_root: Path | None) -> dict[str, Any]: + config = load_json(config_path) + source = source_root or Path(config.get("local_path", "")) + if not str(source): + raise SystemExit("provide --source-root or local_path in the source config") + source = source.expanduser().resolve() + + def count(pattern: str) -> int: + return len([p for p in source.glob(pattern) if p.is_file()]) + + exists = source.exists() + report: dict[str, Any] = { + "config": str(config_path), + "name": config.get("name"), + "kind": config.get("kind"), + "repo": config.get("repo"), + "visibility": config.get("visibility"), + "source_root": str(source), + "source_exists": exists, + "policy": config.get("export_policy", {}), + "recommended_flow": [ + "inspect candidates locally", + "summarize or redact private memory", + "copy only reviewed output into layered-soul", + "run: python3 scripts/layered_soul.py validate .", + ], + } + if exists: + report["candidate_counts"] = { + "memory_markdown": count("memories/*.md"), + "skill_markdown": count("skills/**/*.md"), + "session_archives_excluded": count("sessions/*"), + "cron_archives_excluded": count("cron/*"), + } + report["candidate_paths"] = { + "user_memory": "memories/USER.md" if (source / "memories" / "USER.md").exists() else None, + "general_memory": "memories/MEMORY.md" if (source / "memories" / "MEMORY.md").exists() else None, + "skills_root": "skills/" if (source / "skills").exists() else None, + } + return report + + +def print_findings(findings: list[Finding]) -> int: + errors = 0 + for finding in findings: + print(f"{finding.level.upper()}: {finding.message}") + if finding.level == "error": + errors += 1 + if not findings: + print("OK: layered soul repository looks valid") + elif errors == 0: + print("OK: no validation errors") + return 1 if errors else 0 + + +def cmd_validate(args: argparse.Namespace) -> int: + return print_findings(validate(Path(args.root).resolve())) + + +def cmd_render_prompt(args: argparse.Namespace) -> int: + root = Path(args.root).resolve() + rendered = render_prompt(root, include_curated=args.include_curated) + if args.output: + Path(args.output).write_text(rendered, encoding="utf-8") + else: + print(rendered, end="") + return 0 + + +def cmd_plan_private_source(args: argparse.Namespace) -> int: + report = plan_private_source( + Path(args.config).resolve(), + Path(args.source_root).expanduser() if args.source_root else None, + ) + print(json.dumps(report, indent=2, sort_keys=True)) + return 0 + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Layered Soul helper") + sub = parser.add_subparsers(dest="command", required=True) + + validate_cmd = sub.add_parser("validate", help="validate a Layered Soul repository") + validate_cmd.add_argument("root", nargs="?", default=".") + validate_cmd.set_defaults(func=cmd_validate) + + render_cmd = sub.add_parser("render-prompt", help="render core files as a prompt bundle") + render_cmd.add_argument("root", nargs="?", default=".") + render_cmd.add_argument("--include-curated", action="store_true") + render_cmd.add_argument("--output") + render_cmd.set_defaults(func=cmd_render_prompt) + + plan_cmd = sub.add_parser("plan-private-source", help="inspect a private source export plan") + plan_cmd.add_argument("config") + plan_cmd.add_argument("--source-root") + plan_cmd.set_defaults(func=cmd_plan_private_source) + + return parser + + +def main(argv: list[str]) -> int: + parser = build_parser() + args = parser.parse_args(argv) + return args.func(args) + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:]))