chore(plugins): sync generated content mirrors
Regenerates `plugins/*/skills`, `plugins/*/agents`, both per-plugin `plugin.json` manifests and the two marketplace mirrors from `.apm/` per ADR-0017, via `scripts/sync-plugin-content.sh --all`. The manifests matter beyond tidiness here: `plugin.json` carries the plugin version and wins over the marketplace entry at install time (calculatePluginVersion precedence). Until this ran, the patch bumps in the preceding commit were inert for anyone installing these plugins. ADR: 0017 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EeH8SCbcrCAQrtymkNuhKP
This commit is contained in:
@@ -15,7 +15,8 @@ Arguments:
|
||||
a long-lived branch, a mirror with a different remote name).
|
||||
The VALIDATE_PROVENANCE_BASE_REF environment variable is an
|
||||
equivalent, lower-precedence way to set it — the flag wins
|
||||
if both are given.
|
||||
if both are given, including when the flag is given empty
|
||||
(\`--base-ref=\`), which selects the default resolution.
|
||||
|
||||
Exit codes:
|
||||
0 All checks passed (or nothing to validate)
|
||||
@@ -47,10 +48,13 @@ Checks performed:
|
||||
8 Extracted non-(none) slug in research doc present in sources.md
|
||||
9 Description or Contributing files text changed since --base-ref (INFO
|
||||
only — a bash script cannot verify the claim is still TRUE, only that it
|
||||
changed; the auditor reads the named files to check that). A slug absent
|
||||
at the base ref is a creation, not a change, and is not flagged. When the
|
||||
base ref cannot be resolved at all, this is announced as ONE INFO for the
|
||||
whole check, never a silent skip.
|
||||
changed; the auditor reads the named files to check that). Wrapped values
|
||||
are joined before comparison, so a re-wrap alone is not a change and a
|
||||
rewrite of any line of one is. A slug absent at the base ref is a
|
||||
creation, not a change, and is not flagged; a field that WAS there and is
|
||||
now gone is announced as a removal. When the base ref cannot be resolved,
|
||||
or references/sources.md is not tracked under this path at that ref, this
|
||||
is announced as ONE INFO for the whole check, never a silent skip.
|
||||
|
||||
Checks 7 and 8 apply ONLY when the Research doc value names a research SOURCE
|
||||
INDEX — a file whose basename is sources.md, whose H2 headings ARE source
|
||||
@@ -70,8 +74,15 @@ fi
|
||||
# never counts against them — a caller passing it alongside skill-dir sees
|
||||
# the same argument-count behaviour as one who does not pass it at all, and a
|
||||
# genuinely extra positional argument is still rejected.
|
||||
#
|
||||
# BASE_REF_OVERRIDE is deliberately left UNSET here rather than initialised to
|
||||
# the empty string. `--base-ref=` (given, but empty) and "no flag at all" are
|
||||
# different instructions — the first says "use the default resolution, ignoring
|
||||
# the environment", the second says "fall back to the environment" — and an
|
||||
# empty-string initialiser collapsed them: `${BASE_REF_OVERRIDE:-$ENV}` treats
|
||||
# an empty flag value as absent, so the environment variable won and the usage
|
||||
# text's "the flag wins if both are given" was false for exactly that spelling.
|
||||
declare -a POSITIONAL_ARGS=()
|
||||
BASE_REF_OVERRIDE=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--base-ref=*)
|
||||
@@ -107,10 +118,15 @@ fi
|
||||
|
||||
SKILL_DIR_ARG="${POSITIONAL_ARGS[0]}"
|
||||
|
||||
# The flag wins over the environment variable when both are given; either is
|
||||
# empty-string when unset, and an empty string tells the Python body to fall
|
||||
# The flag wins over the environment variable whenever the flag was GIVEN —
|
||||
# `+x` tests for presence, not for a non-empty value, which is the distinction
|
||||
# `:-` could not make. An empty result either way tells the Python body to fall
|
||||
# back to `git merge-base HEAD origin/main`.
|
||||
BASE_REF="${BASE_REF_OVERRIDE:-${VALIDATE_PROVENANCE_BASE_REF:-}}"
|
||||
if [[ -n "${BASE_REF_OVERRIDE+x}" ]]; then
|
||||
BASE_REF="$BASE_REF_OVERRIDE"
|
||||
else
|
||||
BASE_REF="${VALIDATE_PROVENANCE_BASE_REF:-}"
|
||||
fi
|
||||
|
||||
# python3 is a HARD dependency. Without this preflight a missing interpreter
|
||||
# produced 'line NN: python3: command not found' and exit 127 — an exit code no
|
||||
@@ -491,10 +507,21 @@ def find_repo_root(start_dir):
|
||||
# --- Check 9 helpers ---------------------------------------------------
|
||||
# Check 9 needs a raw field VALUE (as text, to diff against an earlier
|
||||
# version), not the parsed structure parse_contributing_files() and
|
||||
# parse_status() return — a Contributing files list that reordered its
|
||||
# entries without changing them is not what this check is looking for, but
|
||||
# neither is normalizing so hard that a genuine rewrite disappears. Raw text,
|
||||
# whitespace-normalized, is the middle ground.
|
||||
# parse_status() return. The ONE normalization applied is whitespace
|
||||
# collapsing, which is what makes a re-wrap or a re-indent invisible; nothing
|
||||
# else is normalized away.
|
||||
#
|
||||
# In particular a REORDERED Contributing files list DOES fire this check, and
|
||||
# that is deliberate — the header here used to claim the opposite, which the
|
||||
# code never did. Order-insensitivity cannot be had for one field without
|
||||
# distorting the other: the two fields share this parser, and the only way to
|
||||
# ignore order is to split the value into items and sort them, which for a
|
||||
# prose Description means splitting on commas and would then hide a genuine
|
||||
# rewrite that merely permuted its clauses. Check 9 is always an INFO whose
|
||||
# whole job is to point a human at a place to read; a reordered list costs
|
||||
# that human one glance to dismiss, whereas a hidden rewrite is the exact
|
||||
# failure #118 exists to catch. False positive over false negative, on this
|
||||
# check, on purpose.
|
||||
|
||||
def run_git(args, cwd):
|
||||
"""Run `git <args>` in cwd. Returns (returncode, stdout, stderr) — never
|
||||
@@ -523,14 +550,44 @@ def find_slug_block(content, slug):
|
||||
m = pattern.search(content)
|
||||
return m.group(1) if m else None
|
||||
|
||||
# A field value ENDS at the next field, the next heading, or a blank line.
|
||||
# Every other non-blank line is a continuation of the value the author wrapped
|
||||
# across physical lines.
|
||||
#
|
||||
# This boundary is what the old `(.+)$` regex did not have. `.` does not cross
|
||||
# a newline, so only the FIRST physical line of a wrapped value was ever
|
||||
# compared — and a rewrite confined to a continuation line produced no finding
|
||||
# at all. That is verbatim the hedge-to-confident-claim regression #118 exists
|
||||
# to catch, invisible to the check written to catch it. The bullet branch had
|
||||
# the same defect one level down: a wrapped bullet's continuation does not
|
||||
# start with '- ', so the loop broke there and silently dropped every
|
||||
# remaining bullet.
|
||||
#
|
||||
# A continuation line that itself opens with bold text ('**note** — ...') is
|
||||
# read as a boundary and truncates the value. That is a known, narrow
|
||||
# false-negative, accepted because the alternative — no boundary at all —
|
||||
# is what produced the wide one above.
|
||||
FIELD_BOUNDARY_RE = re.compile(r'^(?:- )?\*\*|^#{1,6} ')
|
||||
|
||||
|
||||
def _is_field_boundary(stripped_line):
|
||||
"""True when a stripped line starts a new field, bullet-less heading or H2."""
|
||||
return bool(FIELD_BOUNDARY_RE.match(stripped_line))
|
||||
|
||||
|
||||
def parse_field_raw(content, slug, field_name):
|
||||
"""Raw text of a '**<field_name>:**' field under a slug H2.
|
||||
"""Raw text of a '**<field_name>:**' field under a slug H2, wrapping joined.
|
||||
|
||||
Mirrors the two authored shapes parse_contributing_files() and
|
||||
parse_status() already handle (inline value on the same line, or a
|
||||
bare heading followed by '- ' bullets), but returns text rather than a
|
||||
parsed structure, because check 9 diffs wording, not semantics.
|
||||
|
||||
Continuation lines are joined into the value they belong to before the
|
||||
caller normalizes and compares, so a value wrapped across two lines and
|
||||
the same value on one line are the same text — and a change made on any
|
||||
line of a wrapped value is visible, not just one made on the first.
|
||||
|
||||
Returns None when the H2 itself is absent (the slug did not exist at
|
||||
this content's revision) or the field is absent — both read as "no
|
||||
earlier claim to compare against" to the caller, which is deliberate:
|
||||
@@ -539,28 +596,49 @@ def parse_field_raw(content, slug, field_name):
|
||||
block = find_slug_block(content, slug)
|
||||
if block is None:
|
||||
return None
|
||||
inline_re = re.compile(r'^\- \*\*' + re.escape(field_name) + r':\*\* (.+)$', re.MULTILINE)
|
||||
im = inline_re.search(block)
|
||||
if im:
|
||||
return im.group(1).strip()
|
||||
heading_re = re.compile(r'^\*\*' + re.escape(field_name) + r':\*\*\s*$', re.MULTILINE)
|
||||
hm = heading_re.search(block)
|
||||
if not hm:
|
||||
return None
|
||||
lines = []
|
||||
for line in block[hm.end():].splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
if lines:
|
||||
break
|
||||
continue
|
||||
if not line.startswith("- "):
|
||||
break
|
||||
lines.append(line[2:].strip())
|
||||
return ", ".join(lines) if lines else None
|
||||
lines = block.splitlines()
|
||||
inline_re = re.compile(r'^\- \*\*' + re.escape(field_name) + r':\*\*[ \t]*(.*)$')
|
||||
heading_re = re.compile(r'^\*\*' + re.escape(field_name) + r':\*\*[ \t]*$')
|
||||
|
||||
for idx, line in enumerate(lines):
|
||||
im = inline_re.match(line)
|
||||
if im:
|
||||
parts = [im.group(1).strip()]
|
||||
for cont in lines[idx + 1:]:
|
||||
stripped = cont.strip()
|
||||
if not stripped or stripped.startswith("- ") or _is_field_boundary(stripped):
|
||||
break
|
||||
parts.append(stripped)
|
||||
joined = " ".join(p for p in parts if p).strip()
|
||||
return joined or None
|
||||
if heading_re.match(line):
|
||||
entries = []
|
||||
for cont in lines[idx + 1:]:
|
||||
stripped = cont.strip()
|
||||
if not stripped:
|
||||
if entries:
|
||||
break
|
||||
continue
|
||||
if _is_field_boundary(stripped):
|
||||
break
|
||||
if stripped.startswith("- "):
|
||||
entries.append(stripped[2:].strip())
|
||||
elif entries:
|
||||
# A wrapped bullet: fold it back into the bullet it
|
||||
# continues rather than ending the list here.
|
||||
entries[-1] = (entries[-1] + " " + stripped).strip()
|
||||
else:
|
||||
break
|
||||
return ", ".join(e for e in entries if e) or None
|
||||
return None
|
||||
|
||||
def normalize_field_text(value):
|
||||
"""Collapse whitespace so reformatting alone never registers as a change."""
|
||||
"""Collapse whitespace so reformatting alone never registers as a change.
|
||||
|
||||
True only because parse_field_raw() joins wrapped continuation lines
|
||||
first: collapsing whitespace inside a value that had already been
|
||||
truncated at its first newline normalized nothing a re-wrap could change.
|
||||
"""
|
||||
return re.sub(r'\s+', ' ', value).strip()
|
||||
|
||||
findings = []
|
||||
@@ -1025,28 +1103,82 @@ else:
|
||||
["show", f"{resolved_base_ref}:{sources_md_relpath}"], repo_root
|
||||
)
|
||||
if rc != 0:
|
||||
# The base ref resolved fine, but references/sources.md did not
|
||||
# exist there at all — the whole file is new. Every entry in it
|
||||
# is therefore a creation, not a change: nothing to flag, and
|
||||
# this is not a structural failure of the check, so no INFO
|
||||
# either. Same reasoning applies per-slug below when the ref
|
||||
# resolved but a given '## <slug>' heading did not exist yet.
|
||||
# The base ref resolved fine but `git show <ref>:<path>` did not.
|
||||
# That single return code covers two situations this check cannot
|
||||
# tell apart, and only one of them is harmless:
|
||||
#
|
||||
# the file genuinely did not exist at the base ref — the whole
|
||||
# sources.md is new, every entry in it is a creation, and there
|
||||
# is nothing check 9 could have flagged;
|
||||
#
|
||||
# the path is not TRACKED under that name at the base ref — a
|
||||
# renamed skill directory, or a copy of the skill living
|
||||
# somewhere untracked or gitignored (an installed .claude/skills
|
||||
# tree is the everyday case).
|
||||
#
|
||||
# Treating both as "creation, nothing to flag" made the second one
|
||||
# a silent, whole-skill skip: the same directory audited at its
|
||||
# authoring path reported changed claims and at its deployed path
|
||||
# reported nothing, with no way to tell that from a clean run.
|
||||
# That is the exact fail-open this script's own header forbids —
|
||||
# "never a silent skip" — so announce it once for the whole check
|
||||
# and hand over git's own stderr, which is the only diagnostic
|
||||
# that separates the two cases.
|
||||
detail = show_err.strip().splitlines()
|
||||
detail = detail[0] if detail else "git gave no reason"
|
||||
emit_info(
|
||||
f"Check 9 skipped — '{sources_md_relpath}' is not tracked at {resolved_base_ref}",
|
||||
"references/sources.md",
|
||||
f"`git show {resolved_base_ref}:{sources_md_relpath}` failed ({detail}). "
|
||||
f"Either the file did not exist at that ref — in which case every entry is a "
|
||||
f"creation and there was nothing to flag — or this path is not tracked under "
|
||||
f"that name there: a renamed skill directory, or an untracked or gitignored copy "
|
||||
f"of the skill such as a deployed .claude/skills/ tree. "
|
||||
f"Check 9 did not run for any slug in this skill. "
|
||||
f"Re-run against the tracked authoring path, or pass --base-ref=<ref> naming a "
|
||||
f"commit where this path exists."
|
||||
)
|
||||
old_sources_content = None
|
||||
|
||||
if old_sources_content is not None:
|
||||
for slug in unique_slugs:
|
||||
changed_fields = []
|
||||
removed_fields = []
|
||||
for field_name in ("Description", "Contributing files"):
|
||||
old_value = parse_field_raw(old_sources_content, slug, field_name)
|
||||
new_value = parse_field_raw(sources_content, slug, field_name)
|
||||
if old_value is None or new_value is None:
|
||||
if old_value is None and new_value is None:
|
||||
continue
|
||||
if old_value is None:
|
||||
# No earlier claim to compare against — a brand-new
|
||||
# entry, or a field that did not exist yet at the
|
||||
# base ref. That is a creation, not a change, and is
|
||||
# never flagged.
|
||||
continue
|
||||
if new_value is None:
|
||||
# The field existed at the base ref and is gone now.
|
||||
# This was folded into the creation skip above, which
|
||||
# justified only the other half: deleting a whole
|
||||
# '- **Description:**' line left NO finding anywhere —
|
||||
# no other check in this script requires the field, so
|
||||
# a claim could be removed as invisibly as it could be
|
||||
# strengthened. Announce it; the auditor decides
|
||||
# whether the removal was intended.
|
||||
removed_fields.append(field_name)
|
||||
continue
|
||||
if normalize_field_text(old_value) != normalize_field_text(new_value):
|
||||
changed_fields.append(field_name)
|
||||
if removed_fields:
|
||||
removed_list = " and ".join(removed_fields)
|
||||
emit_info(
|
||||
f"'{removed_list}' removed for '{slug}' since {resolved_base_ref}",
|
||||
f"references/sources.md (## {slug})",
|
||||
f"The '## {slug}' entry had {removed_list} at {resolved_base_ref} and has "
|
||||
f"none now. Nothing else in this script requires the field, so the removal "
|
||||
f"is otherwise invisible. Confirm it was deliberate — a provenance claim "
|
||||
f"withdrawn is as much a change to the chain as one rewritten — and "
|
||||
f"restore the field if it was lost to an edit."
|
||||
)
|
||||
if changed_fields:
|
||||
field_list = " and ".join(changed_fields)
|
||||
emit_info(
|
||||
|
||||
Reference in New Issue
Block a user