Files
Bo c6558508d1 Consolidate AgentOps into a 34-skill engineering menu (#1133)
AgentOps' 55-skill catalog contained overlapping entry points, stale
routes and descriptions that could lose meaningful guidance in the Codex
projection. This change consolidates 21 roots into existing owners,
leaving 34 distinct skills and a generated, task-oriented menu. README
documents every retired name and its replacement.

Planning now establishes observable behavior in the caller's existing
intent, using proportional Given/When/Then examples and domain language.
Implementation and final validation carry those same examples forward.
Original adaptations informed by Matt Pocock's engineering skills
strengthen existing owners rather than adding a new workflow. Routine
edits need no mandatory plan, coverage report, mutation exercise or
learning artifact.

Codex retains complete source descriptions and translates explicit-only
invocation policy. All descriptions fit the existing 180-character
limit; the root instructions retain their 250-line limit. Generated
catalogs, projections, routers, moved references/helpers and their live
consumers are updated together. RPI remains explicitly selected.

Validation passed: projection/conformance checks, the local aggregate
(10 passed; one existing optional-directory skip), and exact-commit CI
covering the complete gate registry, Bats, Go build/vet/race/coverage,
Windows and security. A fresh author-distinct reviewer passed all
acceptance criteria over the complete 573-path subject at
aa642a55d6, including the installed-link
and protected-backup changes. Review findings were repaired and
revalidated. Existing ranker goldens are regression checks, not
model-quality measurements. A fixed six-case fresh-context pilot
supplied an exact candidate menu: three of four targeted cases loaded
expected guidance, a simple refactor selected no skill, and both
no-skill controls selected none. No wrong owner was selected. This pilot
preceded final wording repairs for existing ranker/context limits; it
does not establish installed automatic activation, coding benefit or
savings. No live coding task was run in that pilot.
2026-09-10 22:18:05 -04:00

739 lines
31 KiB
Bash
Executable File

#!/usr/bin/env bash
# codex-sync.sh — generate parity_only Codex twins from their source skills.
#
# A parity_only twin is a SELF-CONTAINED runtime artifact derived from its
# source skill. The Codex runtime ships skills-codex/ ONLY (never skills/ source
# Codex may still consume a generated skills-codex projection for archive/
# marketplace artifacts. Live installs use `ao skills link` into runtime skill
# roots — not a plugin-cache installer.
# twin must carry its own body + references; a bare pointer to skills/<name>
# would dangle at runtime (docs/contracts/codex-skill-api.md). The generated twin is therefore:
# - SKILL.md: frontmatter carrying the complete source description
# (whitespace normalized) + the source body transformed
# runtime-native
# (slash-command invocations of known skills -> `$` prefix, but never the H1
# title; the RUNTIME_REWRITES table below for ~/.claude -> ~/.codex and
# "Claude Code" -> "Codex", longest phrase first);
# - references/ + scripts/: copied byte-identical (lint scans only SKILL.md);
# - agents/openai.yaml: source metadata preserved, with explicit-only source
# invocation policy mapped to Codex policy.allow_implicit_invocation;
# - prompt.md: the standard codex pointer-to-sibling-SKILL.md template,
# optionally plus catalog-declared operator-contract markers.
# Because the twin is GENERATED from source, source edits never require a hand
# mirror — re-running this (via regen-all) reproduces a correct twin, killing the
# "add/touch a skill -> chase ~5 codex gates serially" whack-a-mole (regen-all.sh
# historically only rehashed EXISTING twins; it could not author one).
#
# This generator authors the COMPLETE twin for any source skill that lacks one:
# the body files + references, the per-skill marker, and all three catalog
# surfaces (manifest .skills[], manifest .codex_override_catalog.skills[], and
# skills-codex-overrides/catalog.json .skills[]), then fixes every hash. It is
# idempotent: a source skill that already matches the generated parity form is
# left untouched; a complete but stale parity twin is refreshed from source.
#
# bespoke twins (hand-authored Codex profiles) are the opt-out: they are never
# generated or overwritten — body AND references/scripts are hand-maintained, so
# even --force skips them. Source reference/body edits do NOT auto-propagate to a
# bespoke twin (many bespoke references are deliberate Codex rewrites of source);
# refreshing one is a deliberate human edit. Auto-mirroring source over a bespoke
# twin would clobber the hand-authored copy (age-0js4). Accidental drift is the
# divergence gate's job (age-odv), not this generator's.
#
# Usage:
# scripts/codex-sync.sh # generate any missing parity twin (writes)
# scripts/codex-sync.sh --check # report missing/incomplete twins; exit 1 on drift (no writes)
# scripts/codex-sync.sh --only a,b # scope to skills a and b
# scripts/codex-sync.sh <name> # scope to a single skill
#
# Wired into scripts/regen-all.sh ahead of the codex-hash step.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CHECK_ONLY=false
FORCE=false
ONLY=""
while [[ $# -gt 0 ]]; do
case "$1" in
--check) CHECK_ONLY=true; shift ;;
--force) FORCE=true; shift ;;
--only) ONLY="$2"; shift 2 ;;
-h|--help)
sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0 ;;
-*) echo "Unknown flag: $1" >&2; exit 2 ;;
*) ONLY="$1"; shift ;;
esac
done
# --force regenerates EXISTING twins (overwrite body + exact-mirror references/
# scripts from source). It must be scoped (--only / a skill name) so it cannot
# silently clobber the ~75 existing hand-tended twins in one shot.
if [[ "$FORCE" == "true" && -z "$ONLY" ]]; then
echo "Refusing --force without scope: pass --only <skill[,...]> or a skill name." >&2
echo "(--force rewrites existing twins from source; an unscoped run would clobber all of them.)" >&2
exit 2
fi
export ROOT CHECK_ONLY FORCE ONLY
python3 - <<'PY'
import hashlib
import json
import os
import pathlib
import re
import shutil
import sys
import yaml
# Runtime-name rewrites for the Codex projection, applied LONGEST-MATCH-FIRST in
# a single pass (one entry per rewrite; add new ones here, not as another
# str.replace call site).
#
# These are correct ONLY for a skill that documents a single runtime. A skill
# whose text is genuinely cross-runtime — it names Claude Code AND Codex CLI, or
# tells the operator to check both ~/.claude/skills and ~/.codex/skills — must
# go in scripts/lint/codex-cross-runtime-skills.txt instead, which skips these
# rewrites entirely for that skill. Do NOT try to repair such a skill by adding
# a longer phrase here: patching one sentence leaves every other sentence in the
# same body corrupted. using-flywheel is exactly that case (its trio collapsed
# to two names and its two verification paths collapsed to one, listed twice),
# and the exemption list is its fix.
RUNTIME_REWRITES: tuple[tuple[str, str], ...] = (
("Claude Code", "Codex"),
("~/.claude/", "~/.codex/"),
("~/.claude", "~/.codex"),
(".claude/", ".codex/"),
)
_RUNTIME_REWRITE_MAP = dict(RUNTIME_REWRITES)
_RUNTIME_REWRITE_RE = re.compile(
"|".join(
re.escape(pattern)
for pattern, _ in sorted(RUNTIME_REWRITES, key=lambda kv: len(kv[0]), reverse=True)
)
)
def apply_runtime_rewrites(text: str) -> str:
"""Rewrite runtime names/paths for Codex, longest phrase winning."""
return _RUNTIME_REWRITE_RE.sub(lambda m: _RUNTIME_REWRITE_MAP[m.group(0)], text)
root = pathlib.Path(os.environ["ROOT"]).resolve()
check_only = os.environ.get("CHECK_ONLY") == "true"
force = os.environ.get("FORCE") == "true"
scope = {s.strip() for s in os.environ.get("ONLY", "").split(",") if s.strip()}
source_root = root / "skills"
codex_root = root / "skills-codex"
manifest_path = codex_root / ".agentops-manifest.json"
overrides_catalog_path = root / "skills-codex-overrides" / "catalog.json"
marker_name = ".agentops-generated.json"
if not manifest_path.exists():
print(f"FATAL: missing manifest {manifest_path}", file=sys.stderr)
sys.exit(1)
if not overrides_catalog_path.exists():
print(f"FATAL: missing overrides catalog {overrides_catalog_path}", file=sys.stderr)
sys.exit(1)
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
overrides_catalog = json.loads(overrides_catalog_path.read_text(encoding="utf-8"))
manifest_skills = manifest.setdefault("skills", [])
manifest_catalog = manifest.setdefault("codex_override_catalog", {})
manifest_catalog_skills = manifest_catalog.setdefault("skills", [])
overrides_skills = overrides_catalog.setdefault("skills", [])
# bespoke = the opt-out set (never generate/overwrite). Read from both catalogs.
bespoke = {
e.get("name")
for e in (manifest_catalog_skills + overrides_skills)
if e.get("treatment") == "bespoke"
}
# excluded = the drop-the-twin set (age-focus-membrane-bookkeeper-m1wg.19). A
# spine-excluded source skill (e.g. a legacy corpus skill demoted to the
# experimental tier) ships NO Codex twin: the skills-codex/<name>/ dir is deleted
# and MUST NOT be regenerated. Like bespoke it is skipped entirely — never
# generated, never checked, never restained — but unlike bespoke there is no twin
# on disk at all. The catalog keeps the entry (treatment: excluded) so
# validate-codex-override-coverage.sh treats the source skill as covered-by-
# exclusion rather than "missing from Codex catalog". Read from both catalogs.
excluded = {
e.get("name")
for e in (manifest_catalog_skills + overrides_skills)
if e.get("treatment") == "excluded"
}
# Cross-runtime skills: exempt from the Claude->Codex / ~/.claude->~/.codex body
# rewrites (they legitimately document non-Codex runtimes). Single source of truth
# shared with the gates: scripts/lint/codex-cross-runtime-skills.txt.
cross_runtime_path = root / "scripts" / "lint" / "codex-cross-runtime-skills.txt"
cross_runtime = set()
if cross_runtime_path.exists():
for line in cross_runtime_path.read_text(encoding="utf-8").splitlines():
s = line.strip()
if s and not s.startswith("#"):
cross_runtime.add(s)
def sha256_bytes(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def hash_tree_with(root_dir: pathlib.Path, overlay: dict[str, bytes]) -> str:
"""Tree-hash of a skill dir, with `overlay` (relpath -> bytes) substituted
in. Mirrors regen-codex-hashes.sh hash_tree exactly (excludes manifest,
marker, .DS_Store, __pycache__, *.pyc)."""
files: dict[str, bytes] = {}
if root_dir.is_dir():
for path in root_dir.rglob("*"):
if not path.is_file():
continue
if path.name in {".agentops-manifest.json", marker_name, ".DS_Store"}:
continue
if "__pycache__" in path.parts or path.suffix == ".pyc":
continue
files[path.relative_to(root_dir).as_posix()] = path.read_bytes()
files.update(overlay)
rows = [f"{rel}\t{sha256_bytes(data)}\n" for rel, data in sorted(files.items())]
return sha256_bytes("".join(rows).encode("utf-8"))
def parse_frontmatter(skill_md: pathlib.Path) -> dict:
text = skill_md.read_text(encoding="utf-8")
if not text.startswith("---"):
return {}
parts = text.split("---", 2)
if len(parts) < 3:
return {}
return yaml.safe_load(parts[1]) or {}
def split_frontmatter(skill_md: pathlib.Path) -> str:
"""Return the markdown BODY of a SKILL.md (everything after the leading
--- ... --- frontmatter block)."""
text = skill_md.read_text(encoding="utf-8")
if not text.startswith("---"):
return text
parts = text.split("---", 2)
return parts[2].lstrip("\n") if len(parts) >= 3 else text
def transform_body(body: str, known_skills: set[str], exempt: bool = False) -> str:
"""Make a source skill body runtime-native for Codex (the lint-codex-native
contract): slash-command invocations of KNOWN skills -> `$` prefix, Claude
paths -> Codex paths, "Claude Code" -> "Codex". References are copied
byte-identical (lint scans only SKILL.md), so only the body is transformed.
exempt=True (cross-runtime skill, see scripts/lint/codex-cross-runtime-skills.txt):
apply ONLY the slash->$ rewrite (Codex execution syntax is universal) and
PRESERVE runtime names/paths verbatim — the twin legitimately documents
Claude/AGY/etc., so rewriting "Claude Code"->"Codex" or ~/.claude->~/.codex
would make it inaccurate."""
# The H1 title is the document's NAME, not an invocation: a source titled
# `# /route` must stay `# /route` in the twin, because `# $route` is not a
# heading anyone reads. Hold the title line out of the slash rewrites and
# put it back afterwards; the rest of the body still gets them.
title = ""
if body.startswith("# "):
newline = body.find("\n")
if newline == -1:
title, body = body, ""
else:
title, body = body[: newline + 1], body[newline + 1 :]
# Skill(skill="known", args="...") -> $known ... for declarative skill
# invocations in source skills. Preserve args when present so inline examples
# remain actionable in Codex.
known_alt = "|".join(re.escape(skill) for skill in sorted(known_skills, key=len, reverse=True))
if known_alt:
def repl_skill_call(match: re.Match) -> str:
skill = match.group(1)
args = match.group(2)
return f"${skill}{(' ' + args) if args else ''}"
body = re.sub(
rf'Skill\(skill="({known_alt})"(?:,\s*args="([^"]*)")?\)',
repl_skill_call,
body,
)
# /<known-skill> -> $<known-skill> for slash-COMMAND invocations only — never
# a path segment. Longest names first (so /premortem wins over /pre). Exclude
# when preceded by a path char (word/./-/_/slash, e.g. ../research/, foo/plan)
# or followed by '/' (a path like /research/SKILL.md), so markdown links and
# file paths are left intact (the bug that turned ../foo/ into ..$foo/). A
# closing '>' counts as a path char too: `<run-id>/codebase-recon.json` is a
# path whose placeholder segment happens to precede a known skill name.
for skill in sorted(known_skills, key=len, reverse=True):
body = re.sub(rf"(?<![\w./_>-])/{re.escape(skill)}\b(?!/)", f"${skill}", body)
body = re.sub(r"(?<![\w./_>-])/skill\b(?!/)", "$skill", body)
body = title + body
if exempt:
return body
return apply_runtime_rewrites(body)
def render_operator_contract_block(name: str, operator_contract: dict | None) -> str:
if not operator_contract:
return ""
sections = operator_contract.get("required_sections") or []
markers = operator_contract.get("required_markers") or []
if not sections or not markers:
return ""
out: list[str] = [
"<!-- BEGIN AGENTOPS OPERATOR CONTRACT -->",
f"<!-- Generated from skills-codex-overrides/catalog.json for {name}. -->",
"",
]
marker_index = 0
remaining_markers = len(markers)
section_count = len(sections)
for section_index, section in enumerate(sections):
out.extend([str(section), ""])
sections_left = section_count - section_index
if remaining_markers == 0:
count = 0
elif sections_left == 1:
count = remaining_markers
else:
count = remaining_markers - (sections_left - 1)
if count < 1:
count = 1
for bullet_index in range(count):
out.append(f"{bullet_index + 1}. {markers[marker_index]}")
marker_index += 1
remaining_markers -= 1
if section_index < section_count - 1:
out.append("")
out.extend(["", "<!-- END AGENTOPS OPERATOR CONTRACT -->"])
return "\n".join(out)
def codex_catalog_description(name: str, source_description: str) -> str:
"""Preserve the complete source routing signal, including all use cases,
preconditions, exclusions and triggers. Source authors own concision;
catalog budgets must not silently delete meaning during projection.
"""
# Whitespace only. parse_frontmatter runs the frontmatter through
# yaml.safe_load, so the value handed in here is ALREADY the unquoted
# scalar with any doubled '' unescaped; stripping quote characters again
# ate legitimate leading/trailing quotes, e.g. a description ending in
# `Triggers: "validate"` lost its final quote.
desc = re.sub(r"\s+", " ", source_description).strip()
return desc or f"Run {name}."
def codex_payload_overrides(src_dir: pathlib.Path, fm: dict) -> dict[str, bytes]:
"""Map host frontmatter policy without losing caller-owned source metadata.
Always start from source, never the previous twin. Removing the source flag
therefore restores source YAML (or removes a generated-only policy file).
With neither a flag nor source policy, Codex defaults to implicit invocation.
"""
disabled = fm.get("disable-model-invocation", False)
if not isinstance(disabled, bool):
raise ValueError(f"{src_dir}/SKILL.md: disable-model-invocation must be boolean")
if not disabled:
return {}
relpath = "agents/openai.yaml"
source_yaml = src_dir / relpath
metadata = yaml.safe_load(source_yaml.read_text(encoding="utf-8")) if source_yaml.exists() else {}
if metadata is None:
metadata = {}
if not isinstance(metadata, dict):
raise ValueError(f"{source_yaml}: metadata must be a mapping")
policy = metadata.setdefault("policy", {})
if not isinstance(policy, dict):
raise ValueError(f"{source_yaml}: policy must be a mapping")
policy["allow_implicit_invocation"] = False
return {relpath: yaml.safe_dump(metadata, sort_keys=False, allow_unicode=True).encode("utf-8")}
def twin_skill_md(
name: str,
description: str,
source_body: str,
known_skills: set[str],
exempt: bool = False,
) -> bytes:
"""A self-contained Codex twin: slim (name + terse catalog description)
frontmatter + the source body transformed runtime-native. Self-contained
because the Codex runtime ships skills-codex/ ONLY (never skills/ source) —
a twin must carry its own body + references (docs/contracts/codex-skill-api.md)."""
fm = {"name": name, "description": description}
front = yaml.safe_dump(fm, sort_keys=False, allow_unicode=True, width=10_000).strip()
body = transform_body(source_body, known_skills, exempt)
return f"---\n{front}\n---\n{body.rstrip()}\n".encode("utf-8")
def twin_prompt_md(
name: str, description: str, operator_contract: dict | None = None
) -> bytes:
prompt = (
f"# {name}\n\n"
f"{description}\n\n"
f"## Instructions\n\n"
f"Load and follow the skill instructions from the sibling `SKILL.md` file "
f"for this skill.\n"
f"Then read local files in `references/` and `scripts/` when needed.\n"
)
contract = render_operator_contract_block(name, operator_contract)
if contract:
prompt = f"{prompt.rstrip()}\n\n\n{contract}\n"
return prompt.encode("utf-8")
def upsert(entries: list, name: str, entry: dict) -> bool:
"""Insert or replace by name, keeping EXACTLY ONE row per name. Returns True
if the list changed. Replaces in place (no global re-sort) to keep the diff
minimal — matches the append-only behavior of register-new-codex-skill.sh
and avoids reordering the existing catalog on every new skill. Any later
duplicate rows for the same name are dropped: historical syncs updated one
row of a duplicated pair in place, so drift was masked or misreported
depending on which row a reader's name-keyed dict happened to keep."""
replaced_at = None
removed = False
for i in range(len(entries) - 1, -1, -1):
if entries[i].get("name") != name:
continue
if replaced_at is None:
replaced_at = i
else:
# entries[i] is an EARLIER duplicate (we scan backwards): keep the
# first position for the row, drop the later one.
entries[replaced_at : replaced_at + 1] = []
replaced_at = i
removed = True
if replaced_at is None:
entries.append(entry)
return True
if not removed and entries[replaced_at] == entry:
return False
entries[replaced_at] = entry
return True
# Discover source skills (ground truth), skip non-skill dirs and bespoke.
def mirror_reasons(
src_dir: pathlib.Path, twin_dir: pathlib.Path, overrides: dict[str, bytes]
) -> list[str]:
"""Drift reasons for a twin's mirrored content (references/scripts/fixtures/
etc.) vs source — missing, stale (content mismatch), or extra files. SKILL.md
is transformed (verified separately); prompt.md + marker are twin-only."""
twin_only = {"SKILL.md", "prompt.md", marker_name, ".agentops-manifest.json", ".DS_Store"}
def tree(root_dir: pathlib.Path) -> dict[str, bytes]:
out: dict[str, bytes] = {}
if not root_dir.is_dir():
return out
for p in root_dir.rglob("*"):
if not p.is_file() or p.name in twin_only:
continue
if "__pycache__" in p.parts or p.suffix == ".pyc":
continue
out[p.relative_to(root_dir).as_posix()] = p.read_bytes()
return out
src = tree(src_dir)
src.update(overrides)
twin = tree(twin_dir)
reasons = [f"missing {r}" for r in src if r not in twin]
reasons += [f"stale {r}" for r in src if r in twin and twin[r] != src[r]]
reasons += [f"extra {r}" for r in twin if r not in src]
return reasons
def exact_mirror_source_payload(
src_dir: pathlib.Path, twin_dir: pathlib.Path, overrides: dict[str, bytes]
) -> None:
"""Exact-copy source sibling content into a parity twin, excluding SKILL.md
and twin-only bookkeeping. This keeps references/scripts/fixtures generated
from source and removes stale copied files when source deletes them."""
twin_only = {"SKILL.md", "prompt.md", marker_name, ".agentops-manifest.json", ".DS_Store"}
source_names = {
entry.name
for entry in src_dir.iterdir()
if entry.name not in {"SKILL.md", ".agentops-manifest.json", marker_name, ".DS_Store"}
}
for entry in sorted(twin_dir.iterdir()):
if entry.name in twin_only:
continue
if entry.name not in source_names:
shutil.rmtree(entry) if entry.is_dir() else entry.unlink()
for entry in sorted(src_dir.iterdir()):
if entry.name in {"SKILL.md", ".agentops-manifest.json", marker_name, ".DS_Store"}:
continue
dst_entry = twin_dir / entry.name
if dst_entry.exists():
shutil.rmtree(dst_entry) if dst_entry.is_dir() else dst_entry.unlink()
shutil.copytree(entry, dst_entry) if entry.is_dir() else shutil.copy2(entry, dst_entry)
for relpath, content in overrides.items():
destination = twin_dir / relpath
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(content)
source_skills = sorted(
p.name
for p in source_root.iterdir()
if p.is_dir()
and not p.name.startswith("_")
and (p / "SKILL.md").exists()
)
known_skills = set(source_skills)
drift = []
generated = []
# Source metadata owns the installed set. Retired source roots must not leave
# empty directories, stale generated twins, or override rows that continue to
# advertise removed skills.
retired_twin_dirs = sorted(
p for p in codex_root.iterdir()
if p.is_dir() and not p.name.startswith("_") and p.name not in known_skills
)
if check_only:
drift.extend((p.name, ["retired twin directory remains"]) for p in retired_twin_dirs)
else:
for path in retired_twin_dirs:
shutil.rmtree(path)
overrides_skills[:] = [
entry for entry in overrides_skills if entry.get("name") in known_skills
]
for name in source_skills:
if name in bespoke:
continue
if name in excluded:
# Spine-excluded: the twin was intentionally dropped; never regenerate it.
continue
if scope and name not in scope:
continue
twin_dir = codex_root / name
skill_md = twin_dir / "SKILL.md"
prompt_md = twin_dir / "prompt.md"
marker_path = twin_dir / marker_name
catalog_source_entry = next((e for e in overrides_skills if e.get("name") == name), {})
operator_contract = catalog_source_entry.get("operator_contract")
fm = parse_frontmatter(source_root / name / "SKILL.md")
source_description = str(fm.get("description", "")).strip()
codex_description = codex_catalog_description(name, source_description)
source_body = split_frontmatter(source_root / name / "SKILL.md")
desired_skill = twin_skill_md(
name,
codex_description,
source_body,
known_skills,
name in cross_runtime,
)
desired_prompt = twin_prompt_md(name, source_description, operator_contract)
payload_overrides = codex_payload_overrides(source_root / name, fm)
# A twin is "complete" iff its body files + marker exist AND it is registered
# in the gate-enforced 1:1 surface (skills-codex-overrides/catalog.json — the
# surface validate-codex-override-coverage.sh holds to source skills exactly).
# Complete parity twins are still checked against the generated shape below;
# stale generated bodies, prompts, or mirrored references are refreshed.
# Bespoke twins opt out via the catalog and are skipped before this point.
# The bloated manifest .codex_override_catalog is downstream and is NOT a
# generation trigger.
in_ocat = any(e.get("name") == name for e in overrides_skills)
if check_only:
# THE single drift gate for parity twins: the on-disk twin must EXACTLY
# match what the generator would emit — presence + registration +
# byte-identical transformed SKILL.md + generated prompt.md + mirrored
# references. Any drift forces a regen. This guarantee is what lets the
# content validators (lint-native, api-conformance, runtime-sections,
# audit-parity) skip parity twins entirely: a generated artifact is
# verified by regenerate-and-diff, not by re-checking content rules.
reasons = []
if not in_ocat:
reasons.append("unregistered in catalog.json")
if not marker_path.exists():
reasons.append("missing marker")
if not skill_md.exists() or skill_md.read_bytes() != desired_skill:
reasons.append("SKILL.md")
if not prompt_md.exists() or prompt_md.read_bytes() != desired_prompt:
reasons.append("prompt.md")
reasons += mirror_reasons(source_root / name, twin_dir, payload_overrides)
if reasons:
drift.append((name, reasons))
continue
regen_reasons = []
if not in_ocat:
regen_reasons.append("unregistered in catalog.json")
if not marker_path.exists():
regen_reasons.append("missing marker")
if not skill_md.exists() or skill_md.read_bytes() != desired_skill:
regen_reasons.append("SKILL.md")
if not prompt_md.exists() or prompt_md.read_bytes() != desired_prompt:
regen_reasons.append("prompt.md")
regen_reasons += mirror_reasons(source_root / name, twin_dir, payload_overrides)
if not force and not regen_reasons:
continue
# --- Author/refresh the parity twin (self-contained: body + references) ---
twin_dir.mkdir(parents=True, exist_ok=True)
if force or not skill_md.exists() or skill_md.read_bytes() != desired_skill:
skill_md.write_bytes(desired_skill)
if force or not prompt_md.exists() or prompt_md.read_bytes() != desired_prompt:
prompt_md.write_bytes(desired_prompt)
# Mirror ALL source content (references/, scripts/, fixtures/, templates/,
# agents/, any sibling files) EXCEPT SKILL.md, then apply the invocation
# policy mapping. Other payload files stay byte-identical so every local
# link resolves and the Codex artifact is fully self-contained.
exact_mirror_source_payload(source_root / name, twin_dir, payload_overrides)
source_hash = hash_tree_with(source_root / name, {})
generated_hash = hash_tree_with(twin_dir, {})
marker_path.write_text(
json.dumps(
{
"generator": "codex-sync",
"source_skill": f"skills/{name}",
"layout": "modular",
"source_hash": source_hash,
"generated_hash": generated_hash,
},
indent=2,
)
+ "\n",
encoding="utf-8",
)
upsert(
manifest_skills,
name,
{
"name": name,
"source_skill": f"skills/{name}",
"source_hash": source_hash,
"generated_hash": generated_hash,
},
)
catalog_entry = {
"name": name,
"treatment": "parity_only",
"wave": "catalog-parity",
"reason": (
f"Auto-generated parity twin (codex-sync): skills/{name} is the source "
f"of truth; no durable Codex-specific divergence yet."
),
}
# Catalog entries are ADD-ONLY: an already-registered skill keeps its existing
# (often hand-written) reason/wave in the authoritative
# skills-codex-overrides/catalog.json. The manifest embeds that catalog for
# the shipped runtime artifact, so mirror the authoritative entry for skills
# touched by this generator.
if catalog_source_entry:
upsert(manifest_catalog_skills, name, catalog_source_entry)
elif not any(e.get("name") == name for e in manifest_catalog_skills):
manifest_catalog_skills.append(catalog_entry)
if not any(e.get("name") == name for e in overrides_skills):
overrides_skills.append(catalog_entry)
generated.append(name)
# Rebuild the runtime artifact inventory from the actual generated directories.
# Source retirement can delete a twin before this generator runs; an append-only
# manifest would otherwise retain a ghost row forever. The embedded treatment
# catalog is likewise a projection of the authoritative overrides catalog, not
# a second hand-maintained store.
desired_manifest_skills = []
existing_manifest_by_name = {
entry.get("name"): entry for entry in manifest_skills if entry.get("name")
}
package_count = sum(
1
for package_dir in codex_root.iterdir()
if package_dir.is_dir() and (package_dir / "SKILL.md").exists()
)
for twin_dir in sorted(
p
for p in codex_root.iterdir()
if p.is_dir()
and (p / "SKILL.md").exists()
):
marker_path = twin_dir / marker_name
if not marker_path.exists():
continue # the manifest validator reports the missing marker fail-closed
marker = json.loads(marker_path.read_text(encoding="utf-8"))
entry = dict(existing_manifest_by_name.get(twin_dir.name, {}))
entry.update(
name=twin_dir.name,
source_skill=marker.get("source_skill", f"skills/{twin_dir.name}"),
source_hash=marker.get("source_hash", ""),
generated_hash=marker.get("generated_hash", ""),
)
desired_manifest_skills.append(entry)
manifest_inventory_drift = manifest_skills != desired_manifest_skills
embedded_catalog_drift = manifest_catalog_skills != overrides_skills
package_count_drift = manifest.get("package_count") != package_count
if check_only:
if manifest_inventory_drift:
drift.append(("<manifest>", ["artifact inventory differs from skills-codex directories"]))
if embedded_catalog_drift:
drift.append(("<manifest-catalog>", ["embedded treatment catalog differs from authoritative overrides catalog"]))
if package_count_drift:
drift.append(("<manifest>", [f"package_count differs from {package_count} installable skill directories"]))
if drift:
print(f"codex-sync drift: {len(drift)} parity twin(s) differ from generator output:")
for n, reasons in drift:
print(f" - {n}: {', '.join(reasons)}")
print("Fix: scripts/codex-sync.sh --force --only <name> (then regen hashes).")
sys.exit(1)
print("codex-sync: all parity twins match generator output.")
sys.exit(0)
manifest_skills[:] = desired_manifest_skills
manifest_catalog_skills[:] = [dict(entry) for entry in overrides_skills]
manifest["package_count"] = package_count
if generated or manifest_inventory_drift or embedded_catalog_drift or package_count_drift:
# Recompute the embedded catalog hash (same algorithm as
# register-new-codex-skill.sh) so the manifest catalog stays self-consistent.
catalog_for_hash = json.dumps(
{k: v for k, v in manifest_catalog.items() if k != "skills"}
| {"skills": manifest_catalog_skills},
sort_keys=True,
).encode("utf-8")
manifest["codex_override_catalog_hash"] = sha256_bytes(catalog_for_hash)
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
overrides_catalog_path.write_text(
json.dumps(overrides_catalog, indent=2) + "\n", encoding="utf-8"
)
if generated:
print(f"codex-sync: generated {len(generated)} twin(s): {', '.join(generated)}")
else:
print("codex-sync: refreshed manifest projections")
else:
print("codex-sync: nothing to generate (all parity twins present).")
PY