Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion plugins/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<skill>/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

Expand Down
2 changes: 1 addition & 1 deletion plugins/tree-ring-memory/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tree-ring-memory",
"version": "0.3.9",
"version": "0.3.10",
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Public plugin repo stays broken 🔗 Cross-repo conflict ≡ Correctness

plugin.json advances the bundled Codex package to 0.3.10 and the release instructions identify its
skills-only ZIP as the corrected public artifact, but the separately published
tree-ring-memory-codex-plugin repository remains at 0.3.9. Anyone building or submitting from that
wrapper still gets a ZIP that copies SKILL.md verbatim with metadata, omits
agents/openai.yaml, and retains the over-limit listing subtitle, so the fix does not reach that
distribution channel.
Agent Prompt
## Issue description
The standalone `tree-ring-memory-codex-plugin` repository still publishes version 0.3.9 using the obsolete public ZIP builder, despite Tree_Ring_Memory declaring the corrected package as 0.3.10.

## Fix Focus Areas
- plugins/tree-ring-memory/.codex-plugin/plugin.json[1-4]
- plugins/tree-ring-memory/packaging/build-codex-skills-only.py[61-71]
- /cross_repos/tree-ring-memory-codex-plugin/.codex-plugin/plugin.json[1-4]
- /cross_repos/tree-ring-memory-codex-plugin/packaging/build-codex-skills-only.py[26-39]

## Recommended Fix
Coordinate a 0.3.10 update in `tree-ring-memory-codex-plugin`: port the public front-matter renderer, add `skills/tree-ring-memory/agents/openai.yaml`, shorten the public manifest subtitle, update validation and documentation, and publish both repositories or their release artifacts together.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

"description": "Local-first memory lifecycle, project bootstrap, and receipt-backed harness guidance for coding agents using Tree Ring Memory v0.15+.",
"author": {
"name": "TerminallyLazy",
Expand Down
18 changes: 16 additions & 2 deletions plugins/tree-ring-memory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
41 changes: 38 additions & 3 deletions plugins/tree-ring-memory/packaging/build-codex-skills-only.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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."
}
}
2 changes: 1 addition & 1 deletion scripts/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
66 changes: 64 additions & 2 deletions scripts/validate-plugin-packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from __future__ import annotations

import copy
import importlib.util
import json
import os
import subprocess
Expand Down Expand Up @@ -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"):
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down