From bcd6e19d4da98edde944eaa38b2a87e8f8309e54 Mon Sep 17 00:00:00 2001 From: lazy Date: Thu, 17 Sep 2026 03:30:36 -0400 Subject: [PATCH 1/2] Fix public Codex skill interface packaging --- plugins/AGENTS.md | 2 +- .../.codex-plugin/plugin.json | 2 +- plugins/tree-ring-memory/README.md | 18 ++++- .../packaging/build-codex-skills-only.py | 41 +++++++++++- .../.codex-plugin/plugin.json | 4 +- .../tree-ring-memory/agents/openai.yaml | 7 ++ scripts/AGENTS.md | 2 +- scripts/validate-plugin-packages.py | 66 ++++++++++++++++++- 8 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 plugins/tree-ring-memory/packaging/codex-skills-only/skills/tree-ring-memory/agents/openai.yaml diff --git a/plugins/AGENTS.md b/plugins/AGENTS.md index 9137dcf..c449e29 100644 --- a/plugins/AGENTS.md +++ b/plugins/AGENTS.md @@ -10,7 +10,7 @@ tree-ring-memory contains Codex/Claude manifests, commands, skills, hooks, legal ## Local Contracts -Ship all four native lifecycle hooks. Codex skills-only means no MCP dependency; retain hooks and executable ZIP permissions. Effective host-owned project hooks cause plugin hooks to stand down; Codex linked-worktree root layers use the proven primary hook source, while Claude keeps local ownership checks. A genuinely absent project-local .tree-ring is a quiet skip; an existing entry, including a dangling symlink, must retain runtime diagnostics. Never redirect worktree memory to the primary checkout's store. +The public OpenAI upload renders skill front matter without legacy metadata, preserves the skill body, and includes supported interface fields in skills//agents/openai.yaml. Keep the public listing shortDescription within 30 characters. Native/shared skill source remains separate. Ship all four native lifecycle hooks. Codex skills-only means no MCP dependency; retain hooks and executable ZIP permissions. Effective host-owned project hooks cause plugin hooks to stand down; Codex linked-worktree root layers use the proven primary hook source, while Claude keeps local ownership checks. A genuinely absent project-local .tree-ring is a quiet skip; an existing entry, including a dangling symlink, must retain runtime diagnostics. Never redirect worktree memory to the primary checkout's store. ## Work Guidance diff --git a/plugins/tree-ring-memory/.codex-plugin/plugin.json b/plugins/tree-ring-memory/.codex-plugin/plugin.json index c9a4c59..7b27d77 100644 --- a/plugins/tree-ring-memory/.codex-plugin/plugin.json +++ b/plugins/tree-ring-memory/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "tree-ring-memory", - "version": "0.3.9", + "version": "0.3.10", "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/plugins/tree-ring-memory/README.md b/plugins/tree-ring-memory/README.md index 05c9a81..df8a4b5 100644 --- a/plugins/tree-ring-memory/README.md +++ b/plugins/tree-ring-memory/README.md @@ -5,7 +5,7 @@ ChatGPT/Codex and Claude Code. It packages reviewed instructions plus thin lifecycle-hook registrations; the local Tree Ring Memory CLI remains the runtime and data owner. -The Codex manifest is version `0.3.9`. The Claude Code manifest is version +The Codex manifest is version `0.3.10`. The Claude Code manifest is version `0.3.7`. Both share the same reviewed wrapper skill. Manual guidance supports CLI `0.15.0` or newer; the lifecycle hooks require CLI `0.15.6` or newer for project-local runtime resolution and cross-session recall. DOX persistence @@ -125,9 +125,23 @@ skills-only artifact explicitly instead of uploading the repository plugin: ```bash python3 plugins/tree-ring-memory/packaging/build-codex-skills-only.py \ - tree-ring-memory-codex-skills-only.zip + tree-ring-memory-codex-skills-only-0.3.10.zip ``` +Upload `tree-ring-memory-codex-skills-only-0.3.10.zip` from the Codex plugin +release to `platform.openai.com/plugins` using **Skills only**. The similarly +named `tree-ring-memory-codex-0.3.10.zip` is the full repository package. + +The public profile omits the ignored `metadata` mapping from skill front matter +and supplies supported interface settings in +`skills/tree-ring-memory/agents/openai.yaml`. This file uses JSON syntax, which +is valid YAML. The skill body, name, description, and license stay unchanged. +Plugin listing text is separate: the public profile's `interface.shortDescription` +fits the directory's 30-character limit. The upstream +[submission error reference](https://developers.openai.com/plugins/deploy/submission-errors) +classifies `skill_metadata_ignored` as a warning, not a blocking error; inspect +any additional portal messages if submission remains blocked. + The generated ZIP has one `tree-ring-memory/` package root. It includes the skill, legal notices, logo, composer icon, manifest, and executable native Codex lifecycle hooks. The portal calls this route "Skills only" because it has no MCP diff --git a/plugins/tree-ring-memory/packaging/build-codex-skills-only.py b/plugins/tree-ring-memory/packaging/build-codex-skills-only.py index c87708b..160d10a 100755 --- a/plugins/tree-ring-memory/packaging/build-codex-skills-only.py +++ b/plugins/tree-ring-memory/packaging/build-codex-skills-only.py @@ -14,13 +14,41 @@ FIXED_TIMESTAMP = (2026, 1, 1, 0, 0, 0) -def write_file(archive: ZipFile, source: Path, destination: Path) -> None: +def public_skill(content: str) -> str: + """Remove our canonical block-style metadata without changing instructions. + + This handles the checked-in front matter, not arbitrary YAML. Interface + settings come from the public profile's agents/openai.yaml instead. + """ + if not content.startswith("---\n"): + raise ValueError("skill must start with YAML front matter") + frontmatter, separator, body = content[4:].partition("\n---\n") + if not separator: + raise ValueError("skill front matter must end before the body") + lines = [] + in_metadata = False + for line in frontmatter.splitlines(): + if line.startswith("metadata:"): + if line != "metadata:": + raise ValueError("expected canonical block-style metadata") + in_metadata = True + elif in_metadata and (not line.strip() or line.startswith((" ", "\t"))): + continue + else: + in_metadata = False + lines.append(line) + return "---\n" + "\n".join(lines) + "\n---\n" + body + + +def write_file( + archive: ZipFile, source: Path, destination: Path, *, data: bytes | None = None, +) -> None: info = ZipInfo(str(PACKAGE_ROOT / destination), FIXED_TIMESTAMP) info.compress_type = ZIP_DEFLATED 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()) + archive.writestr(info, source.read_bytes() if data is None else data) def build(destination: Path) -> None: @@ -33,7 +61,14 @@ def build(destination: Path) -> None: ) for path in sorted((PLUGIN / "skills").rglob("*")): if path.is_file(): - write_file(archive, path, path.relative_to(PLUGIN)) + relative = path.relative_to(PLUGIN) + if (PROFILE / relative).exists(): + raise ValueError(f"public skill profile collides with source: {relative}") + data = public_skill(path.read_text(encoding="utf-8")).encode("utf-8") if path.name == "SKILL.md" else None + write_file(archive, path, relative, data=data) + for path in sorted((PROFILE / "skills").rglob("*")): + if path.is_file(): + write_file(archive, path, path.relative_to(PROFILE)) for path in sorted((PLUGIN / "assets").rglob("*")): if path.is_file(): write_file(archive, path, path.relative_to(PLUGIN)) diff --git a/plugins/tree-ring-memory/packaging/codex-skills-only/.codex-plugin/plugin.json b/plugins/tree-ring-memory/packaging/codex-skills-only/.codex-plugin/plugin.json index 34e54d1..9f88eca 100644 --- a/plugins/tree-ring-memory/packaging/codex-skills-only/.codex-plugin/plugin.json +++ b/plugins/tree-ring-memory/packaging/codex-skills-only/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "tree-ring-memory", - "version": "0.3.9", + "version": "0.3.10", "description": "Local-first memory lifecycle, project bootstrap, and receipt-backed harness guidance for coding agents using Tree Ring Memory v0.15+.", "author": { "name": "TerminallyLazy", @@ -24,7 +24,7 @@ "skills": "./skills/", "interface": { "displayName": "Tree Ring Memory", - "shortDescription": "Local-first memory lifecycle guidance for Codex agents.", + "shortDescription": "Durable memory for 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", diff --git a/plugins/tree-ring-memory/packaging/codex-skills-only/skills/tree-ring-memory/agents/openai.yaml b/plugins/tree-ring-memory/packaging/codex-skills-only/skills/tree-ring-memory/agents/openai.yaml new file mode 100644 index 0000000..f26bda5 --- /dev/null +++ b/plugins/tree-ring-memory/packaging/codex-skills-only/skills/tree-ring-memory/agents/openai.yaml @@ -0,0 +1,7 @@ +{ + "interface": { + "display_name": "Tree Ring Memory", + "short_description": "Recall and capture durable project memory", + "default_prompt": "Use $tree-ring-memory to recall durable project context before making changes." + } +} diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index 7200c25..6a4a70e 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -10,7 +10,7 @@ Package validator, release archive builder and certification script; coordinate ## Local Contracts -Release tag and binary version must agree. Published tarballs and plugin ZIPs require checksums and expected contents. Preserve platform constraints and installation scope. +Release tag and binary version must agree. Published tarballs and plugin ZIPs require checksums and expected contents. Public plugin checks cover skill interface schema, omission of ignored front-matter metadata, unchanged instruction bodies, and directory listing limits. Preserve platform constraints and installation scope. ## Work Guidance diff --git a/scripts/validate-plugin-packages.py b/scripts/validate-plugin-packages.py index 7a2f2f0..f6c5825 100644 --- a/scripts/validate-plugin-packages.py +++ b/scripts/validate-plugin-packages.py @@ -3,6 +3,8 @@ from __future__ import annotations +import copy +import importlib.util import json import os import subprocess @@ -68,7 +70,7 @@ def validate_codex() -> None: manifest = load_json(PLUGIN / ".codex-plugin" / "plugin.json") require(manifest.get("name") == "tree-ring-memory", "Codex manifest name is stale") - require(manifest.get("version") == "0.3.9", "Codex manifest version is stale") + require(manifest.get("version") == "0.3.10", "Codex manifest version is stale") require(manifest.get("skills") == "./skills/", "Codex skills path is stale") require(manifest.get("hooks") == "./hooks/codex-hooks.json", "Codex lifecycle hook path is stale") for unsupported in ("mcpServers", "apps"): @@ -455,14 +457,62 @@ def check(label: str, *, skip: bool) -> None: check("unproven primary ownership", skip=False) +def validate_public_skill(content: str) -> None: + require(content.startswith("---\n") and "\n---\n" in content, "public skill requires front matter") + header = content.split("\n---\n", 1)[0] + keys = [line.split(":", 1)[0] for line in header.splitlines()[1:] if line and not line[0].isspace()] + require(set(keys) == {"name", "description", "license"} and len(keys) == 3, "public skill must omit ignored metadata and interface front matter") + + +def validate_public_skill_interface(content: bytes) -> None: + # JSON is a YAML subset; the profile uses it so checks can validate structure + # and types without introducing a YAML dependency into package tooling. + agent = json.loads(content) + require(isinstance(agent, dict) and set(agent) == {"interface"}, "public skill agent file needs only interface") + interface = agent["interface"] + require(isinstance(interface, dict) and set(interface) == {"display_name", "short_description", "default_prompt"}, "public skill interface must use supported snake_case fields") + for key, value in interface.items(): + require(isinstance(value, str) and bool(value.strip()), f"public skill interface {key} must be a nonempty string") + require(25 <= len(interface["short_description"]) <= 64, "public skill description must fit Codex skill UI") + require("$tree-ring-memory" in interface["default_prompt"], "skill prompt must name the skill") + + +def validate_public_skill_renderer() -> None: + spec = importlib.util.spec_from_file_location("public_builder", CODEX_SKILLS_BUILDER) + builder = importlib.util.module_from_spec(spec) + spec.loader.exec_module(builder) + source = "---\nname: tree-ring-memory\nmetadata:\n version: legacy\n nested:\n ignored: true\nlicense: MIT\ndescription: Example\n---\n# Body\nmetadata: keep this body text\n" + rendered = builder.public_skill(source) + require(rendered == "---\nname: tree-ring-memory\nlicense: MIT\ndescription: Example\n---\n# Body\nmetadata: keep this body text\n", "metadata filtering must preserve later fields and body") + require(builder.public_skill(rendered) == rendered, "already-clean public front matter must remain unchanged") + validate_public_skill(rendered) + for invalid in (source, rendered.replace("license: MIT\n", "interface: invalid\n"), rendered.replace("name: tree-ring-memory\n", "name: tree-ring-memory\nname: duplicate\n")): + try: + validate_public_skill(invalid) + except SystemExit: + pass + else: + raise SystemExit("public skill validator accepted legacy, unknown or duplicate front matter") + for invalid in (b'{}', b'{"interface":{"displayName":"wrong"}}', b'{"interface":{"display_name":"Tree Ring Memory","short_description":false,"default_prompt":"test"}}'): + try: + validate_public_skill_interface(invalid) + except SystemExit: + pass + else: + raise SystemExit("public skill validator accepted a missing or invalid interface") + + 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) + expected_manifest = copy.deepcopy(repository_manifest) + expected_manifest["interface"]["shortDescription"] = "Durable memory for agents" 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") + require(1 <= len(skills_manifest["interface"]["shortDescription"]) <= 30, "public directory shortDescription must fit its 30-character limit") + validate_public_skill_renderer() with tempfile.TemporaryDirectory() as temporary: first = Path(temporary) / "first.zip" @@ -496,6 +546,18 @@ def validate_codex_skills_only() -> None: prefix + "skills/tree-ring-memory/SKILL.md" in names, "skills-only Codex ZIP is missing its skill", ) + source_skill = (PLUGIN / "skills/tree-ring-memory/SKILL.md").read_text(encoding="utf-8") + built_skill = archive.read(prefix + "skills/tree-ring-memory/SKILL.md").decode("utf-8") + validate_public_skill(built_skill) + require(source_skill.split("\n---\n", 1)[1] == built_skill.split("\n---\n", 1)[1], "public upload must preserve the complete skill body") + for key in ("name", "description", "license"): + source_line = next(line for line in source_skill.split("\n---\n", 1)[0].splitlines() if line.startswith(key + ":")) + require(source_line in built_skill.split("\n---\n", 1)[0].splitlines(), f"public upload changed skill {key}") + agent_path = prefix + "skills/tree-ring-memory/agents/openai.yaml" + require(agent_path in names, "public upload is missing its skill interface") + agent_bytes = archive.read(agent_path) + require(agent_bytes == (CODEX_SKILLS_ONLY / "skills/tree-ring-memory/agents/openai.yaml").read_bytes(), "public skill interface drifted from its profile") + validate_public_skill_interface(agent_bytes) require( prefix + "assets/tree-ring-memory-logo.png" in names, "skills-only Codex ZIP is missing its declared assets", From 3b985c34fb3bbf9fc71474e2d76747298d8e093e Mon Sep 17 00:00:00 2001 From: lazy Date: Thu, 17 Sep 2026 03:32:44 -0400 Subject: [PATCH 2/2] Refresh root Codex plugin release reference --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0040c4..8052ccb 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ hooks for project-local installs; see the The current public-directory upload also includes native Codex lifecycle hooks; ordinary Chat hosts without the Codex hook runtime remain guidance-only. -CLI 0.15.13 and the Codex 0.3.9 / Claude 0.3.7 plugins quietly skip lifecycle +CLI 0.15.13 and the Codex 0.3.10 / Claude 0.3.7 plugins quietly skip lifecycle hooks in uninitialized projects and linked worktrees. Each checkout keeps its own memory root; inherited hooks never initialize it or reuse another checkout's store. Existing roots with broken activation still report errors.