diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 3033644..5c24b08 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "tree-ring-memory", - "version": "0.3.6", + "version": "0.3.7", "description": "Local-first memory lifecycle, project bootstrap, and receipt-backed harness guidance for coding agents using Tree Ring Memory v0.15+.", "author": { "name": "TerminallyLazy", diff --git a/README.md b/README.md index a2e76a6..0ecb6de 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,11 @@ used by this guidance. 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. +That generated ZIP includes skills and native Codex lifecycle hooks. The portal +calls the route "Skills only" because this plugin has no MCP server. The upload +preserves executable hook scripts and excludes Claude metadata and commands. +Hooks require Codex or ChatGPT Work, an available CLI, and host trust; ordinary +Chat remains guidance-only. See [OpenAI's current compatibility guidance](https://developers.openai.com/plugins/guides/submit-claude-plugin). It does not run a background service, scrape chats, or capture transcripts. The active agent chooses when a memory action is useful, source-linked, and @@ -261,9 +263,8 @@ 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 +prove that useful context was found. Older skills-only packages omitted +automatic lifecycle hooks. Current Git and public upload packages include them. A newly configured Codex hook still needs the host's trust flow and a new session before automatic execution can be verified. diff --git a/SUBMISSION.md b/SUBMISSION.md index a64a1e3..929d92e 100644 --- a/SUBMISSION.md +++ b/SUBMISSION.md @@ -77,7 +77,7 @@ Long description: ## Release Notes -Version 0.3.6 of the v0.15-compatible skills-only package adds verified +Version 0.3.7 retains native lifecycle hooks in the public upload and 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 @@ -91,15 +91,21 @@ requirement. The core workflow invokes a separately installed local CLI and reads project files when the host supports those capabilities. On hosts without them, the 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. +Native command hooks require the Codex runtime (including ChatGPT Work), an +available CLI, and host trust. Ordinary Chat does not run these hooks. Disclose +this boundary and follow [OpenAI compatibility guidance](https://developers.openai.com/plugins/guides/submit-claude-plugin). + +## Native Lifecycle Distribution + +Both the Git package and public-directory ZIP include SessionStart, +SubagentStart, Stop, and SubagentStop hooks. Build the public upload using +`packaging/build-codex-skills-only.py`. The portal's "Skills only" route means +there is no MCP server; it does not require stripping native command hooks. +CLI 0.15.7 or newer repairs installer bootstrap. Host trust and a fresh recall +receipt are required before claiming activation. Updating Git does not update +a reviewed directory snapshot; submit the versioned ZIP separately. + +Package validation covers deterministic ZIP bytes, canonical hook definitions, +and executable permissions. Older local scaffold validators that reject all +`hooks` fields are incompatible with the current published contract. Use native +host validation and the [current manifest specification](https://developers.openai.com/plugins/build/plugins). diff --git a/packaging/build-codex-skills-only.py b/packaging/build-codex-skills-only.py index ebf327b..c87708b 100755 --- a/packaging/build-codex-skills-only.py +++ b/packaging/build-codex-skills-only.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Build the OpenAI upload artifact without repository lifecycle hooks.""" +"""Build the OpenAI skills-only upload, including native Codex lifecycle hooks.""" from __future__ import annotations @@ -17,7 +17,9 @@ 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 + info.create_system = 3 + mode = 0o100755 if source.stat().st_mode & 0o111 else 0o100644 + info.external_attr = mode << 16 archive.writestr(info, source.read_bytes()) @@ -35,6 +37,9 @@ def build(destination: Path) -> None: for path in sorted((PLUGIN / "assets").rglob("*")): if path.is_file(): write_file(archive, path, path.relative_to(PLUGIN)) + for name in ("codex-hooks.json", "codex-hook.sh"): + path = PLUGIN / "hooks" / name + 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)) diff --git a/packaging/codex-skills-only/.codex-plugin/plugin.json b/packaging/codex-skills-only/.codex-plugin/plugin.json index 98935e4..bbd0f65 100644 --- a/packaging/codex-skills-only/.codex-plugin/plugin.json +++ b/packaging/codex-skills-only/.codex-plugin/plugin.json @@ -1,13 +1,13 @@ { "name": "tree-ring-memory", - "version": "0.3.6", + "version": "0.3.7", "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", + "repository": "https://github.com/TerminallyLazy/tree-ring-memory-codex-plugin", "license": "MIT", "keywords": [ "codex", @@ -44,5 +44,6 @@ "brandColor": "#2F7D5C", "composerIcon": "./assets/tree-ring-memory-logo.png", "logo": "./assets/tree-ring-memory-logo.png" - } + }, + "hooks": "./hooks/codex-hooks.json" } diff --git a/scripts/validate-plugin.sh b/scripts/validate-plugin.sh index 594e2c6..0465f09 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.6": - raise SystemExit("wrapper version must be 0.3.6") +if manifest.get("version") != "0.3.7": + raise SystemExit("wrapper version must be 0.3.7") interface = manifest.get("interface", {}) prompts = interface.get("defaultPrompt", []) @@ -92,3 +92,5 @@ assert_contains "$SKILL" 'launch every ordinary worker with `TREE_RING_COORDINAT printf 'Tree Ring Memory Codex wrapper contract is valid.\n' python3 scripts/validate-lifecycle.py + +python3 scripts/validate-upload.py diff --git a/scripts/validate-upload.py b/scripts/validate-upload.py new file mode 100644 index 0000000..069d0f4 --- /dev/null +++ b/scripts/validate-upload.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Validate the deterministic public upload and its native lifecycle hooks.""" +import json +import subprocess +import sys +import tempfile +from pathlib import Path +from zipfile import ZipFile + +ROOT = Path(__file__).resolve().parents[1] +PLUGIN = ROOT +CODEX_SKILLS_ONLY = ROOT / "packaging/codex-skills-only" +CODEX_SKILLS_BUILDER = ROOT / "packaging/build-codex-skills-only.py" + + +def require(condition, message): + if not condition: + raise SystemExit(message) + + +def load_json(path): + return json.loads(path.read_text()) + + +def validate_codex_skills_only() -> None: + repository_manifest = load_json(PLUGIN / ".codex-plugin" / "plugin.json") + skills_manifest = load_json(CODEX_SKILLS_ONLY / ".codex-plugin" / "plugin.json") + expected_manifest = dict(repository_manifest) + require(skills_manifest == expected_manifest, "skills-only Codex manifest drifted from repository metadata") + for unsupported in ("mcpServers", "apps"): + require(unsupported not in skills_manifest, f"skills-only Codex ZIP must not declare {unsupported}") + require("screenshots" not in skills_manifest.get("interface", {}), "skills-only Codex ZIP must not declare screenshots") + + with tempfile.TemporaryDirectory() as temporary: + first = Path(temporary) / "first.zip" + second = Path(temporary) / "second.zip" + for destination in (first, second): + subprocess.run([sys.executable, str(CODEX_SKILLS_BUILDER), str(destination)], check=True) + require(first.read_bytes() == second.read_bytes(), "skills-only Codex ZIP must be deterministic") + with ZipFile(first) as archive: + names = set(archive.namelist()) + prefix = "tree-ring-memory/" + require(names and all(name.startswith(prefix) for name in names), "skills-only Codex ZIP needs one package root") + manifest_name = prefix + ".codex-plugin/plugin.json" + require(manifest_name in names, "skills-only Codex ZIP manifest is missing") + built_manifest = json.loads(archive.read(manifest_name)) + require(built_manifest == skills_manifest, "skills-only Codex ZIP manifest is stale") + require( + not any( + marker in name + for name in names + for marker in ("/commands/", "/.claude-plugin/", "/packaging/") + ), + "skills-only Codex ZIP contains repository-only components", + ) + for hook_name in ("codex-hooks.json", "codex-hook.sh"): + archive_path = prefix + "hooks/" + hook_name + require(archive_path in names, "public upload is missing its native lifecycle hook") + require(archive.read(archive_path) == (PLUGIN / "hooks" / hook_name).read_bytes(), "public upload hook differs from the validated runtime hook") + hook_mode = archive.getinfo(prefix + "hooks/codex-hook.sh").external_attr >> 16 + require(hook_mode & 0o111 != 0, "public upload hook script must remain executable") + require( + prefix + "skills/tree-ring-memory/SKILL.md" in names, + "skills-only Codex ZIP is missing its skill", + ) + require( + prefix + "assets/tree-ring-memory-logo.png" in names, + "skills-only Codex ZIP is missing its declared assets", + ) + + +validate_codex_skills_only() +print("Public upload retains validated executable lifecycle hooks") diff --git a/skills/tree-ring-memory/SKILL.md b/skills/tree-ring-memory/SKILL.md index 9953ac3..b5b3a40 100644 --- a/skills/tree-ring-memory/SKILL.md +++ b/skills/tree-ring-memory/SKILL.md @@ -31,8 +31,8 @@ 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 + Lifecycle hooks need CLI 0.15.6 or newer; older packages may omit + automatic hooks. Current Codex packages include them, including the public upload. 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,