diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..18869fb --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "tree-ring-memory", + "interface": { + "displayName": "Tree Ring Memory" + }, + "plugins": [ + { + "name": "tree-ring-memory", + "source": { + "source": "local", + "path": "./" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Developer Tools" + } + ] +} diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 16d4add..3033644 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "tree-ring-memory", - "version": "0.3.3", + "version": "0.3.6", "description": "Local-first memory lifecycle, project bootstrap, and receipt-backed harness guidance for coding agents using Tree Ring Memory v0.15+.", "author": { "name": "TerminallyLazy", @@ -22,6 +22,7 @@ "skills" ], "skills": "./skills/", + "hooks": "./hooks/codex-hooks.json", "interface": { "displayName": "Tree Ring Memory", "shortDescription": "Local-first memory lifecycle guidance for Codex agents.", @@ -34,8 +35,8 @@ "Interactive" ], "websiteURL": "https://terminallylazy.github.io/Tree-Ring-Memory/", - "privacyPolicyURL": "https://github.com/TerminallyLazy/tree-ring-memory-codex-plugin/blob/main/PRIVACY.md", - "termsOfServiceURL": "https://github.com/TerminallyLazy/tree-ring-memory-codex-plugin/blob/main/TERMS.md", + "privacyPolicyURL": "https://github.com/TerminallyLazy/Tree-Ring-Memory/blob/main/plugins/tree-ring-memory/PRIVACY.md", + "termsOfServiceURL": "https://github.com/TerminallyLazy/Tree-Ring-Memory/blob/main/plugins/tree-ring-memory/TERMS.md", "defaultPrompt": [ "Recall durable project context before changing release behavior.", "Capture this validated lesson without storing a transcript.", diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 69d48e4..013fd42 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -10,8 +10,8 @@ permissions: contents: read env: - TREE_RING_VERSION: "0.15.0" - TREE_RING_LINUX_X86_64_SHA256: "9b47873268dbb94712a49b02bd785cc69507facee1e879e46e6922778b4afbe6" + TREE_RING_VERSION: "0.15.7" + TREE_RING_LINUX_X86_64_SHA256: "5c9da887e212b1a7222205d2a661910276171a927d7677f09ec097f2cddd26b7" jobs: validate: diff --git a/PRIVACY.md b/PRIVACY.md index 1668030..20adcca 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -2,15 +2,33 @@ Effective August 23, 2026 -The Tree Ring Memory plugin is an instruction package for AI coding agents. It -does not operate a hosted service, create a user account, collect analytics, -send telemetry, or include a remote MCP server. +The Tree Ring Memory repository plugin packages instructions and local +lifecycle-hook registrations for AI coding agents. It does not operate a hosted +service, create a user account, collect analytics, send telemetry, or include a +remote MCP server. -When an agent runs the separately installed Tree Ring Memory CLI, the CLI stores -the memory content the user chooses in a local SQLite database under the -configured Tree Ring root. The project does not receive that database or its -contents. Data leaves the local environment only when the user or another tool -explicitly exports, syncs, publishes, or otherwise transmits it. +The repository hooks run only when a session or subagent starts or stops. They +forward the host's lifecycle JSON through standard input to the separately +installed Tree Ring Memory CLI and wait synchronously for at most 10 seconds. +They do not register for user prompts, tool calls, or `SessionEnd`; persist hook +input; capture prompts or transcripts; or run in the background. The lifecycle +parser never inspects or persists `transcript_path`, `last_assistant_message`, +prompts, or transcript content. + +Each stop event enforces one agent-mediated memory checkpoint. It asks the +active agent to evaluate already-grounded work rather than deriving a summary +from hook input. Only a concise, durable candidate classified as normal +sensitivity may be written automatically with strict `tree-ring capture`. +That command fixes agent scope, requires identity and provenance, tags the +memory as automatic capture, and rejects sensitive content. If no candidate +passes, no durable memory is created. + +When an agent runs an explicit command in the separately installed Tree Ring +Memory CLI, including an identity-bound strict capture approved by the +checkpoint gates, the CLI stores accepted memory content in a local SQLite +database under the configured Tree Ring root. The project does not receive that +database or its contents. Data leaves the local environment only when the user +or another tool explicitly exports, syncs, publishes, or otherwise transmits it. The plugin instructs agents to avoid transcripts, credentials, secrets, private keys, raw chain-of-thought, and unnecessary sensitive personal data. It also diff --git a/README.md b/README.md index ed6191c..a2e76a6 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,15 @@ This plugin packages one Codex skill that teaches agents when to recall, write, audit, consolidate, and forget project memory using the open-source [Tree Ring Memory](https://github.com/TerminallyLazy/Tree-Ring-Memory) CLI. -Plugin `0.3.3` requires Tree Ring Memory CLI **>= 0.15.0**. The minimum adds +Plugin `0.3.6` requires Tree Ring Memory CLI **>= 0.15.0**. The minimum adds verified project-local bootstrap and scope-preserving CLI updates on top of the receipt-backed harness, schema-v3, coordinated-write, and idempotency contracts used by this guidance. -The public ZIP is a skills-only package. It intentionally omits +The repository plugin includes native lifecycle hooks and requires CLI `0.15.6` +or newer for automatic recall. Build the separate public-directory ZIP with +`python3 packaging/build-codex-skills-only.py tree-ring-memory-codex-skills-only.zip`. +That generated ZIP is a skills-only package. It intentionally omits `interface.screenshots`, which OpenAI's ZIP ingestion does not accept for this package type; the logo and composer icon remain available. @@ -201,3 +204,70 @@ webhooks, analytics, credentials, or networked runtime code. See [PRIVACY.md](PRIVACY.md), [TERMS.md](TERMS.md), and [SECURITY.md](SECURITY.md) for data handling, use terms, and disclosures. + +## Install The Hook-Capable Plugin + +```bash +codex plugin marketplace add TerminallyLazy/tree-ring-memory-codex-plugin +codex plugin add tree-ring-memory@tree-ring-memory +``` + +Review and trust the Tree Ring hook definitions in Codex, then start a new session. + + +## Automatic Lifecycle Hooks + +The repository plugin registers exactly `SessionStart`, `SubagentStart`, +`Stop`, and `SubagentStop`. Each hook forwards its event JSON directly to the +local CLI and waits synchronously for at most 10 seconds. It does not register +prompt, tool, compaction, or `SessionEnd` hooks; run a background service; +scrape chats; or ship an MCP server. + +Session start covers startup, resume, and compaction rehydration when the host +reports those sources. Subagent start gives each worker an independent, +receipt-backed preflight. Codex requires review and trust of the installed hook +definition before it runs. Claude Code loads the hook with the enabled plugin. + +Startup recall loads a bounded brief of shared project guidance and this +agent's durable memories, including captures from earlier sessions. Workflow +and session memories remain limited to their matching scope. It does not +depend on memories containing special startup keywords. Use targeted recall +when the task changes; the startup brief is not an exhaustive search. + +Stop and subagent-stop enforce one agent-mediated memory checkpoint. The +lifecycle parser uses only stable harness identity and project fields; it never +inspects or persists `transcript_path`, `last_assistant_message`, prompts, or +transcript content. The checkpoint asks the active agent to evaluate its +already-grounded work. If and only if that evaluation yields a concise, +durable, normal-sensitivity candidate, the agent automatically runs the exact +strict `tree-ring capture` command template returned by the lifecycle handler. +Strict capture fixes agent scope, requires identity and provenance, adds an +automatic-capture tag, and rejects sensitive candidates. No candidate means no +memory write. This is one bounded checkpoint, not a recorder or automatic +summary of every turn. + +The hook wrapper resolves the Git project root when available, prefers that +project's `.tree-ring/bin/tree-ring`, and otherwise uses `tree-ring` from +`PATH`. It then invokes the shared lifecycle entry point with the project-local +`.tree-ring` root. An unavailable or incompatible CLI is not active-harness +proof and cannot be reported as a successful checkpoint or capture. + +When project activation has already installed the managed lifecycle definition +in `.codex/hooks.json` or `.claude/settings.json`, that project definition owns +recall and stop checkpoints. The marketplace wrapper detects the exact managed +marker and exits without invoking the CLI, preventing duplicate context, +receipts, checkpoint continuations, or capture attempts when the host merges +project and plugin hooks. + +`integrations status --verbose` reports the last validated recall's result +count and query class. A zero-result receipt proves the check ran; it does not +prove that useful context was found. A skills-only plugin installation has no +automatic lifecycle hooks; enable the repository plugin or configure the +project with the CLI to obtain them. A newly configured Codex hook still needs +the host's trust flow and a new session before automatic execution can be +verified. + +Installer onboarding requires CLI 0.15.7 or newer to create the activation +manifest and native project hooks in the same install action. On earlier 0.15 +CLIs, run `tree-ring init` explicitly after the installer. Onboarding readiness +is configuration; a fresh host receipt is required to prove automatic recall. diff --git a/SECURITY.md b/SECURITY.md index d7edd00..4a39fbb 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Supported Versions -The `main` branch is the supported version of this Codex plugin wrapper. +The `main` branch is the supported version of this agent plugin package. The Tree Ring Memory framework and CLI are maintained in the canonical repository: @@ -25,8 +25,19 @@ or personal data in a public issue. ## Data Handling -This wrapper plugin contains guidance files only. It does not run a background -service, include remote MCP servers, collect telemetry, or store credentials. +This wrapper plugin contains guidance files and bounded local lifecycle-hook +registrations. It does not run a background service, include remote MCP +servers, collect telemetry, or store credentials. The hooks run only at +`SessionStart`, `SubagentStart`, `Stop`, and `SubagentStop`, forward standard +input directly to the local Tree Ring CLI, and do not persist prompts, +transcripts, `last_assistant_message`, or hook input. They are synchronous, +bounded to 10 seconds, and never run as a `SessionEnd` or background recorder. + +Stop hooks enforce one agent-mediated checkpoint. They may supply an exact +strict `tree-ring capture` template only for concise durable candidates. Strict +capture fixes agent scope, requires harness identity and provenance, accepts +only normal sensitivity, and tags the result as automatic capture. A missing, +sensitive, ambiguous, or ungrounded candidate must not be stored. Tree Ring Memory is designed for explicit agent-mediated memory actions. Store only concise decisions, lessons, warnings, and evidence references that are diff --git a/SUBMISSION.md b/SUBMISSION.md index 995d271..a64a1e3 100644 --- a/SUBMISSION.md +++ b/SUBMISSION.md @@ -77,7 +77,7 @@ Long description: ## Release Notes -Version 0.3.3 of the v0.15-compatible skills-only package adds verified +Version 0.3.6 of the v0.15-compatible skills-only package adds verified project-local bootstrap, project-root-safe initialization, and scope-preserving CLI update guidance. It retains the ZIP-ingestion fix that omits unsupported `interface.screenshots`, plus receipt-backed harness readiness, same-host @@ -94,3 +94,12 @@ skill remains guidance-only and explicitly forbids claims that a command ran. OpenAI's Claude-plugin migration guide asks local-execution plugins to contact their OpenAI partner; disclose this boundary in the submission rather than representing the package as a hosted integration. + +## Repository Lifecycle Distribution + +The Git marketplace package now includes SessionStart, SubagentStart, Stop, and +SubagentStop hooks backed by CLI 0.15.6. The public-directory upload must use +`packaging/build-codex-skills-only.py`; it excludes hooks and does not promise +automatic invocation. Updating the Git repository does not update a previously +reviewed directory snapshot. Submit the versioned ZIP through the directory +review channel separately. diff --git a/TERMS.md b/TERMS.md index bf25c83..74e39b7 100644 --- a/TERMS.md +++ b/TERMS.md @@ -5,12 +5,13 @@ Effective August 23, 2026 The Tree Ring Memory plugin is open-source software distributed under the MIT License. By using it, you agree to the license and these operational terms. -The plugin provides agent instructions only. It is not a hosted memory service, -backup service, access-control system, medical or legal record system, or -guarantee that an AI host will follow every instruction. The user is responsible -for choosing what to store, maintaining backups, controlling local filesystem -access, reviewing agent actions, and complying with applicable policies and -law. +The repository plugin provides agent instructions and local lifecycle-hook +registrations. The separate OpenAI upload artifact is skills-only. Neither is a +hosted memory service, backup service, access-control system, medical or legal +record system, or guarantee that an AI host will follow every instruction. The +user is responsible for choosing what to store, maintaining backups, controlling +local filesystem access, reviewing agent actions and hook trust, and complying +with applicable policies and law. Tree Ring Memory is provided without warranty, including warranties of fitness, availability, accuracy, non-infringement, or data durability, to the maximum diff --git a/hooks/codex-hook.sh b/hooks/codex-hook.sh new file mode 100755 index 0000000..0e60bff --- /dev/null +++ b/hooks/codex-hook.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -eu + +if command -v git >/dev/null 2>&1; then + project_root=$(git rev-parse --show-toplevel 2>/dev/null || true) + if [ -n "$project_root" ]; then + cd "$project_root" + fi +fi + +# Project activation owns lifecycle recall and checkpoints when its managed hook +# is present. The marketplace hook stands down to prevent duplicate handling. +if [ -f .codex/hooks.json ] && { + grep -Fq 'Tree Ring Memory managed lifecycle v2"' .codex/hooks.json || + grep -Fq 'Tree Ring Memory managed lifecycle v3"' .codex/hooks.json || + grep -Fq 'Tree Ring Memory managed lifecycle v4"' .codex/hooks.json +}; then + exit 0 +fi + +tree_ring=tree-ring +if [ -x .tree-ring/bin/tree-ring ]; then + tree_ring=.tree-ring/bin/tree-ring +fi + +exec "$tree_ring" --root .tree-ring integrations hook --harness codex --input-json-stdin diff --git a/hooks/codex-hooks.json b/hooks/codex-hooks.json new file mode 100644 index 0000000..1cc7933 --- /dev/null +++ b/hooks/codex-hooks.json @@ -0,0 +1,55 @@ +{ + "description": "Load bounded Tree Ring context at startup and enforce one agent-mediated memory checkpoint at stop.", + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "\"${PLUGIN_ROOT}/hooks/codex-hook.sh\"", + "timeout": 10, + "statusMessage": "Loading Tree Ring context", + "additionalContextLimit": 6000 + } + ] + } + ], + "SubagentStart": [ + { + "hooks": [ + { + "type": "command", + "command": "\"${PLUGIN_ROOT}/hooks/codex-hook.sh\"", + "timeout": 10, + "statusMessage": "Loading Tree Ring worker context", + "additionalContextLimit": 6000 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "\"${PLUGIN_ROOT}/hooks/codex-hook.sh\"", + "timeout": 10, + "statusMessage": "Checking Tree Ring memory" + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "\"${PLUGIN_ROOT}/hooks/codex-hook.sh\"", + "timeout": 10, + "statusMessage": "Checking Tree Ring worker memory" + } + ] + } + ] + } +} diff --git a/packaging/build-codex-skills-only.py b/packaging/build-codex-skills-only.py new file mode 100755 index 0000000..ebf327b --- /dev/null +++ b/packaging/build-codex-skills-only.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Build the OpenAI upload artifact without repository lifecycle hooks.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo + + +PLUGIN = Path(__file__).resolve().parents[1] +PROFILE = PLUGIN / "packaging" / "codex-skills-only" +PACKAGE_ROOT = Path("tree-ring-memory") +FIXED_TIMESTAMP = (2026, 1, 1, 0, 0, 0) + + +def write_file(archive: ZipFile, source: Path, destination: Path) -> None: + info = ZipInfo(str(PACKAGE_ROOT / destination), FIXED_TIMESTAMP) + info.compress_type = ZIP_DEFLATED + info.external_attr = 0o100644 << 16 + archive.writestr(info, source.read_bytes()) + + +def build(destination: Path) -> None: + destination.parent.mkdir(parents=True, exist_ok=True) + with ZipFile(destination, "w") as archive: + write_file( + archive, + PROFILE / ".codex-plugin" / "plugin.json", + Path(".codex-plugin/plugin.json"), + ) + for path in sorted((PLUGIN / "skills").rglob("*")): + if path.is_file(): + write_file(archive, path, path.relative_to(PLUGIN)) + for path in sorted((PLUGIN / "assets").rglob("*")): + if path.is_file(): + write_file(archive, path, path.relative_to(PLUGIN)) + for name in ("LICENSE", "PRIVACY.md", "SECURITY.md", "TERMS.md"): + write_file(archive, PLUGIN / name, Path(name)) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("output", type=Path, help="Destination ZIP path") + args = parser.parse_args() + build(args.output.resolve()) + + +if __name__ == "__main__": + main() diff --git a/packaging/codex-skills-only/.codex-plugin/plugin.json b/packaging/codex-skills-only/.codex-plugin/plugin.json new file mode 100644 index 0000000..98935e4 --- /dev/null +++ b/packaging/codex-skills-only/.codex-plugin/plugin.json @@ -0,0 +1,48 @@ +{ + "name": "tree-ring-memory", + "version": "0.3.6", + "description": "Local-first memory lifecycle, project bootstrap, and receipt-backed harness guidance for coding agents using Tree Ring Memory v0.15+.", + "author": { + "name": "TerminallyLazy", + "url": "https://github.com/TerminallyLazy" + }, + "homepage": "https://github.com/TerminallyLazy/Tree-Ring-Memory", + "repository": "https://github.com/TerminallyLazy/Tree-Ring-Memory", + "license": "MIT", + "keywords": [ + "codex", + "memory", + "agent-memory", + "multi-agent", + "harness-activation", + "idempotency", + "local-first", + "recall", + "sqlite", + "skills" + ], + "skills": "./skills/", + "interface": { + "displayName": "Tree Ring Memory", + "shortDescription": "Local-first memory lifecycle guidance for Codex agents.", + "longDescription": "Tree Ring Memory gives coding agents a lifecycle-aware practice for verified project-local setup, project recall, durable decisions, receipt-backed harness readiness, same-host fan-out/fan-in, idempotent worker writes, coordinator-authorized shared publication, explicit forgetting, privacy-safe memory capture, and scope-preserving CLI updates using Tree Ring Memory v0.15 or newer.", + "developerName": "TerminallyLazy", + "category": "Developer Tools", + "capabilities": [ + "Read", + "Write", + "Interactive" + ], + "websiteURL": "https://terminallylazy.github.io/Tree-Ring-Memory/", + "privacyPolicyURL": "https://github.com/TerminallyLazy/Tree-Ring-Memory/blob/main/plugins/tree-ring-memory/PRIVACY.md", + "termsOfServiceURL": "https://github.com/TerminallyLazy/Tree-Ring-Memory/blob/main/plugins/tree-ring-memory/TERMS.md", + "defaultPrompt": [ + "Recall durable project context before changing release behavior.", + "Capture this validated lesson without storing a transcript.", + "Check Tree Ring harness readiness and explain any non-active state." + ], + "brandColor": "#2F7D5C", + "composerIcon": "./assets/tree-ring-memory-logo.png", + "logo": "./assets/tree-ring-memory-logo.png" + } +} diff --git a/scripts/smoke_v015.sh b/scripts/smoke_v015.sh index dbfe4a0..12fbd0a 100755 --- a/scripts/smoke_v015.sh +++ b/scripts/smoke_v015.sh @@ -7,7 +7,7 @@ if [[ "${tree_ring_bin}" == */* ]]; then else command -v "${tree_ring_bin}" >/dev/null fi -test "$("${tree_ring_bin}" --version)" = "tree-ring 0.15.0" +test "$("${tree_ring_bin}" --version)" = "tree-ring 0.15.7" smoke_base="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" smoke_base="${smoke_base%/}" diff --git a/scripts/validate-lifecycle.py b/scripts/validate-lifecycle.py new file mode 100644 index 0000000..a874f7e --- /dev/null +++ b/scripts/validate-lifecycle.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Verify packaged lifecycle events, stdin forwarding, and managed-hook ownership.""" +import json, os, subprocess, tempfile +from pathlib import Path +from typing import Any +ROOT = Path(__file__).resolve().parents[1] +LIFECYCLE_EVENTS = {"SessionStart", "SubagentStart", "Stop", "SubagentStop"} + +def load_json(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise SystemExit(f"{path.relative_to(ROOT)} must contain a JSON object") + return value + +def require(condition: bool, message: str) -> None: + if not condition: + raise SystemExit(message) + +def validate_hook_config(path: Path, *, command: str, expect_exec_form: bool) -> None: + config = load_json(path) + events = config.get("hooks") + require(isinstance(events, dict), f"{path.relative_to(ROOT)} hooks object is required") + require(set(events) == LIFECYCLE_EVENTS, f"{path.relative_to(ROOT)} must register the exact lifecycle contract") + + for event in sorted(LIFECYCLE_EVENTS): + groups = events[event] + require(isinstance(groups, list) and len(groups) == 1, f"{path.relative_to(ROOT)} {event} group is invalid") + require("matcher" not in groups[0], f"{path.relative_to(ROOT)} {event} must handle every start source") + handlers = groups[0].get("hooks") + require(isinstance(handlers, list) and len(handlers) == 1, f"{path.relative_to(ROOT)} {event} handler is invalid") + handler = handlers[0] + require(handler.get("type") == "command", f"{path.relative_to(ROOT)} {event} must use a command hook") + require(handler.get("command") == command, f"{path.relative_to(ROOT)} {event} command is stale") + require(handler.get("timeout") == 10, f"{path.relative_to(ROOT)} {event} timeout must remain bounded") + require(handler.get("async") in (None, False), f"{path.relative_to(ROOT)} {event} must not run in the background") + if expect_exec_form: + require(handler.get("args") == [], f"{path.relative_to(ROOT)} {event} must use safe exec form") + else: + require("args" not in handler, f"{path.relative_to(ROOT)} {event} uses unsupported Codex args") + require( + handler.get("additionalContextLimit") == (6000 if event in {"SessionStart", "SubagentStart"} else None), + f"{path.relative_to(ROOT)} {event} context limit is stale", + ) + +def validate_hook_script(path: Path, harness: str) -> None: + text = path.read_text(encoding="utf-8") + require(os.access(path, os.X_OK), f"{path.relative_to(ROOT)} must be executable") + require(".tree-ring/bin/tree-ring" in text, f"{path.relative_to(ROOT)} must prefer the project-local CLI") + require("git rev-parse --show-toplevel" in text, f"{path.relative_to(ROOT)} must resolve the project root") + managed_hook = ".codex/hooks.json" if harness == "codex" else ".claude/settings.json" + require(managed_hook in text, f"{path.relative_to(ROOT)} must detect the project-managed hook") + for version in (2, 3, 4): + require( + f'Tree Ring Memory managed lifecycle v{version}"' in text, + f"{path.relative_to(ROOT)} must recognize managed lifecycle v{version}", + ) + require( + text.index(managed_hook) < text.index('exec "$tree_ring"'), + f"{path.relative_to(ROOT)} must enforce ownership before invoking the CLI", + ) + require( + f'--root .tree-ring integrations hook --harness {harness} --input-json-stdin' in text, + f"{path.relative_to(ROOT)} does not invoke the {harness} lifecycle entry point", + ) + require("PLUGIN_DATA" not in text, f"{path.relative_to(ROOT)} must not persist lifecycle input") + require("CLAUDE_PLUGIN_DATA" not in text, f"{path.relative_to(ROOT)} must not persist lifecycle input") + require(">>" not in text and "tee " not in text, f"{path.relative_to(ROOT)} must not append lifecycle input") + + events = { + "SessionStart": b'{"hook_event_name":"SessionStart","session_id":"validation-session"}\n', + "SubagentStart": b'{"hook_event_name":"SubagentStart","session_id":"validation-session","agent_id":"worker-1","agent_type":"worker"}\n', + "Stop": b'{"hook_event_name":"Stop","session_id":"validation-session","stop_hook_active":false,"transcript_path":"/private/transcript.jsonl","last_assistant_message":"must remain opaque"}\n', + "SubagentStop": b'{"hook_event_name":"SubagentStop","session_id":"validation-session","agent_id":"worker-1","agent_type":"worker","transcript_path":"/private/worker.jsonl","last_assistant_message":"must remain opaque"}\n', + } + with tempfile.TemporaryDirectory() as temporary: + project = Path(temporary) + cli = project / ".tree-ring" / "bin" / "tree-ring" + cli.parent.mkdir(parents=True) + cli.write_text( + "#!/bin/sh\n" + "printf '%s\\n' \"$@\" > \"$TREE_RING_TEST_ARGS\"\n" + "cat > \"$TREE_RING_TEST_STDIN\"\n" + "printf '%s\\n' '{\"hookSpecificOutput\":{\"additionalContext\":\"validated\"}}'\n", + encoding="utf-8", + ) + cli.chmod(0o755) + args_capture = project / "args" + stdin_capture = project / "stdin" + environment = os.environ.copy() + environment["TREE_RING_TEST_ARGS"] = str(args_capture) + environment["TREE_RING_TEST_STDIN"] = str(stdin_capture) + for event_name in sorted(LIFECYCLE_EVENTS): + event = events[event_name] + result = subprocess.run( + [str(path)], + cwd=project, + env=environment, + input=event, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + require(stdin_capture.read_bytes() == event, f"{path.relative_to(ROOT)} changed {event_name} JSON on stdin") + require( + args_capture.read_text(encoding="utf-8").splitlines() + == ["--root", ".tree-ring", "integrations", "hook", "--harness", harness, "--input-json-stdin"], + f"{path.relative_to(ROOT)} passed unexpected lifecycle arguments", + ) + require(b"validated" in result.stdout, f"{path.relative_to(ROOT)} did not forward CLI output") + args_capture.unlink() + stdin_capture.unlink() + + managed_path = project / managed_hook + managed_path.parent.mkdir(parents=True, exist_ok=True) + for version in (2, 3, 4): + managed_path.write_text( + f'{{"description":"Tree Ring Memory managed lifecycle v{version}"}}\n', + encoding="utf-8", + ) + duplicate = subprocess.run( + [str(path)], + cwd=project, + env=environment, + input=events["Stop"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + require(duplicate.stdout == b"", f"{path.relative_to(ROOT)} emitted duplicate v{version} context") + require(not args_capture.exists(), f"{path.relative_to(ROOT)} invoked the CLI for managed v{version}") + require(not stdin_capture.exists(), f"{path.relative_to(ROOT)} persisted managed v{version} input") + + managed_path.write_text( + '{"description":"Tree Ring Memory managed lifecycle v5"}\n', + encoding="utf-8", + ) + unsupported = subprocess.run( + [str(path)], + cwd=project, + env=environment, + input=events["SessionStart"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + require(args_capture.exists(), f"{path.relative_to(ROOT)} incorrectly accepted managed lifecycle v5") + require(stdin_capture.read_bytes() == events["SessionStart"], f"{path.relative_to(ROOT)} dropped v5 fallback input") + require(b"validated" in unsupported.stdout, f"{path.relative_to(ROOT)} did not run the v5 fallback") + +validate_hook_config(ROOT / "hooks/codex-hooks.json", command='"${PLUGIN_ROOT}/hooks/codex-hook.sh"', expect_exec_form=False) +validate_hook_script(ROOT / "hooks/codex-hook.sh", 'codex') +print("Lifecycle hook package verified.") diff --git a/scripts/validate-plugin.sh b/scripts/validate-plugin.sh index 93eb239..594e2c6 100755 --- a/scripts/validate-plugin.sh +++ b/scripts/validate-plugin.sh @@ -30,8 +30,8 @@ root = Path(".") manifest = json.loads((root / ".codex-plugin/plugin.json").read_text()) if manifest.get("name") != "tree-ring-memory": raise SystemExit("plugin name must remain tree-ring-memory") -if manifest.get("version") != "0.3.3": - raise SystemExit("wrapper version must be 0.3.3") +if manifest.get("version") != "0.3.6": + raise SystemExit("wrapper version must be 0.3.6") interface = manifest.get("interface", {}) prompts = interface.get("defaultPrompt", []) @@ -90,3 +90,5 @@ assert_contains "$SKILL" 'maintenance with apply or repair flags' assert_contains "$SKILL" 'launch every ordinary worker with `TREE_RING_COORDINATOR_TOKEN` unset' printf 'Tree Ring Memory Codex wrapper contract is valid.\n' + +python3 scripts/validate-lifecycle.py diff --git a/skills/tree-ring-memory/SKILL.md b/skills/tree-ring-memory/SKILL.md index 9a82aa4..9953ac3 100644 --- a/skills/tree-ring-memory/SKILL.md +++ b/skills/tree-ring-memory/SKILL.md @@ -31,6 +31,9 @@ working directory by accident. this project. Otherwise check `command -v tree-ring` and run `tree-ring --version`. 2. Read existing `/.tree-ring/SKILL.md` and `CLI.md` when present. + Lifecycle hooks need CLI 0.15.6 or newer; a skills-only plugin package has no + automatic hooks. Use `integrations status --verbose` to inspect the last + recall count and query class, and distinguish no receipt from zero results. 3. This package targets Tree Ring Memory CLI 0.15.0 or newer. If no compatible CLI is available and the user's request already authorizes Tree Ring setup, install the verified current release project-locally from the project root. @@ -91,8 +94,10 @@ Use this sequence for meaningful project work: changed and defer to current source files, tests, policies, and user input. 4. Do the work. Do not write memory merely because a session is active. 5. At a natural checkpoint or closeout, capture only durable decisions, - corrections, validated lessons, warnings, or future seeds. Never store raw - transcripts, secrets, or sensitive data. + corrections, validated lessons, warnings, preferences, or future seeds. + Never store raw transcripts, secrets, or sensitive data. Repository + lifecycle integrations enforce one agent-mediated checkpoint at `Stop` or + `SubagentStop`; they do not derive memory from hook payloads. 6. Observe the command result and report the actual outcome. A proposed memory, dry run, bridge file, or generated marker is not proof that a durable write, sync, activation, correction, or deletion occurred. @@ -484,6 +489,23 @@ Tree Ring Memory does not autonomously scrape chats or write durable memory in the background. The active agent is responsible for deciding when a Tree Ring command is warranted, then calling the CLI deliberately. +Repository lifecycle integrations register exactly `SessionStart`, +`SubagentStart`, `Stop`, and `SubagentStop`. Start events perform bounded recall. +Each stop event enforces one synchronous, agent-mediated memory checkpoint. The +hook parser uses stable harness identity and project fields only; it never +inspects or persists `transcript_path`, `last_assistant_message`, prompts, or +transcript content. It does not register a prompt, tool, or `SessionEnd` hook. + +At the checkpoint, evaluate the grounded work already in your context. If there +are no durable candidates, finish without a memory write. For up to three +concise candidates that are durable, source-grounded, and normal sensitivity, +automatically run the exact strict `tree-ring capture` command template supplied +by the lifecycle handler. Do not edit its project, agent, workflow, session, +checkpoint, operation, or source identity fields. Strict capture fixes +`scope=agent`, adds the automatic-capture tag, and rejects sensitive content. +Never substitute `remember`, `evidence`, an import, or a transcript summary for +that checkpoint command. + Use bridge files only to discover Tree Ring and its command reference: - project-level bridges should point to `.tree-ring/SKILL.md` and @@ -513,7 +535,8 @@ Never keep known-wrong memory merely because it was previously recalled. ## Closeout Habit -At the end of meaningful work, ask: +At the end of meaningful work, or when a stop hook requests the single +agent-mediated checkpoint, ask: - What did we decide? - What did we learn? @@ -522,4 +545,6 @@ At the end of meaningful work, ask: - Is there a future seed worth revisiting? - Is any memory sensitive and better left unstored? -Only remember the answers that will materially improve future work. +Only remember the answers that will materially improve future work and pass the +normal-sensitivity gate. During a lifecycle checkpoint, use only the supplied +strict `tree-ring capture` template.