This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git
The following commit(s) were added to refs/heads/main by this push:
new 6e560f1 feat(skill-validator): enforce Claude Code's 1,536-char
metadata budget (#104)
6e560f1 is described below
commit 6e560f100d961bfd379121fd2b68b43ee4ac2841
Author: Yeonguk Choo <[email protected]>
AuthorDate: Mon May 11 02:09:00 2026 +0900
feat(skill-validator): enforce Claude Code's 1,536-char metadata budget
(#104)
* skills/pr-management-code-review: trim frontmatter to fit metadata budget
* feat(skill-validator): enforce Claude Code's 1,536-char metadata budget
---
.claude/skills/pr-management-code-review/SKILL.md | 27 ++++++++--------------
.../src/skill_validator/__init__.py | 23 +++++++++++++++++-
tools/skill-validator/tests/test_validator.py | 24 +++++++++++++++++++
3 files changed, 55 insertions(+), 19 deletions(-)
diff --git a/.claude/skills/pr-management-code-review/SKILL.md
b/.claude/skills/pr-management-code-review/SKILL.md
index 0fdc490..79fc7f4 100644
--- a/.claude/skills/pr-management-code-review/SKILL.md
+++ b/.claude/skills/pr-management-code-review/SKILL.md
@@ -2,25 +2,16 @@
name: maintainer-review
mode: Triage
description: |
- Walk a maintainer through deep code review of open pull requests on the
configured `<upstream>` repo (default: read from `<project-config>/project.md →
upstream_repo`). The
- default working list — referred to throughout the docs as **"my reviews"** —
is the union of five signals on the
- authenticated maintainer: PRs where review is requested from them, PRs that
touch files they recently modified, PRs
- whose changed files they own per `CODEOWNERS`, PRs that `@`-mention them,
and PRs they already submitted a real
- review on (triage comments do not count). Filters can narrow by area label,
collaborator status, or to a single PR.
- For each PR the skill reads the diff, applies the project's
- review criteria (the source files declared in
- `<project-config>/pr-management-code-review-criteria.md`, plus
- the project's repo-wide `AGENTS.md`), runs any
- locally-configured adversarial reviewer (e.g. the OpenAI
- Codex plugin), surfaces findings, drafts an `approve` / `request-changes` /
`comment` review with
- inline comments proposed by default, and — on the maintainer's confirmation
— posts it via the
- `addPullRequestReview` mutation. This is the deep-review counterpart to the
triage skill.
+ Walk a maintainer through deep, sequential code review of open pull requests
on the configured `<upstream>` repo.
+ Defaults to the **"my reviews"** queue (the union of five maintainer signals
— see the Inputs table); selectors can
+ narrow to a single PR, an area label, or a collaborator subset. Drafts an
`approve` / `request-changes` / `comment`
+ review per PR and posts on the maintainer's confirmation. Deep-review
counterpart to the triage skill.
when_to_use: |
- Invoke when a maintainer says "review my PRs", "go through the PRs assigned
to me", "review my queue", "review the
- area:scheduler PRs", "review PR NNN", "do my review pass", or any variation
on "look over the code on PRs I'm
- responsible for, one at a time." Distinct from `pr-management-triage`, which
decides *whether* to engage with a PR. This skill is
- invoked **after** triage has produced PRs marked `ready for maintainer
review` (or any other curated selector) and a
- human reviewer is doing the actual code review.
+ Invoke when a maintainer says "review my PRs", "go through my review queue",
"review PR NNN", "review the
+ area:scheduler PRs", "do my review pass", or any variation on "look over PRs
I'm responsible for, one at a time."
+ Also fires on "review my CODEOWNER PRs", "pair this PR with Codex /
adversarial review", and "review the
+ ready-for-maintainer-review queue". Distinct from `pr-management-triage`
(which decides *whether* to engage);
+ this skill runs **after** triage has produced reviewable PRs.
license: Apache-2.0
---
<!-- SPDX-License-Identifier: Apache-2.0
diff --git a/tools/skill-validator/src/skill_validator/__init__.py
b/tools/skill-validator/src/skill_validator/__init__.py
index 3f4cb57..40b50ba 100644
--- a/tools/skill-validator/src/skill_validator/__init__.py
+++ b/tools/skill-validator/src/skill_validator/__init__.py
@@ -106,6 +106,14 @@ FRAMEWORK_PLACEHOLDERS: tuple[str, ...] = (
"<repo>",
)
+# YAML block-scalar headers — must not be stored as scalar content,
+# else MAX_METADATA_CHARS measurements get inflated.
+YAML_BLOCK_SCALAR_HEADERS = {"|", ">", "|-", "|+", ">-", ">+"}
+
+# Per-skill description + when_to_use budget; Claude Code truncates past this.
+# https://code.claude.com/docs/en/skills#frontmatter-reference
+MAX_METADATA_CHARS = 1536
+
# Markdown link pattern: [text](url)
LINK_PATTERN = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
@@ -184,7 +192,8 @@ def parse_frontmatter(text: str) -> dict[str, str] | None:
result[current_key] =
"\n".join(current_value_lines).strip()
key, _, value = line.partition(":")
current_key = key.strip()
- current_value_lines = [value.strip()] if value.strip() else []
+ inline = value.strip()
+ current_value_lines = [inline] if inline and inline not in
YAML_BLOCK_SCALAR_HEADERS else []
continue
# Line without colon that is not indented — treat as folded scalar
if current_key is not None:
@@ -229,6 +238,18 @@ def validate_frontmatter(path: Path, text: str) ->
Iterable[Violation]:
f"frontmatter mode '{fm['mode']}' not in {sorted(ALLOWED_MODES)}
(see docs/modes.md)",
)
+ desc_len = len(fm.get("description", ""))
+ wtu_len = len(fm.get("when_to_use", ""))
+ total = desc_len + wtu_len
+ if total > MAX_METADATA_CHARS:
+ yield Violation(
+ path,
+ 1,
+ f"description + when_to_use is {total} chars; "
+ f"Claude Code truncates past {MAX_METADATA_CHARS} "
+ f"(description={desc_len}, when_to_use={wtu_len})",
+ )
+
# ---------------------------------------------------------------------------
# Link validation
diff --git a/tools/skill-validator/tests/test_validator.py
b/tools/skill-validator/tests/test_validator.py
index d231729..bbc157f 100644
--- a/tools/skill-validator/tests/test_validator.py
+++ b/tools/skill-validator/tests/test_validator.py
@@ -25,6 +25,7 @@ import pytest
from skill_validator import (
FORBIDDEN_PATTERNS,
+ MAX_METADATA_CHARS,
extract_headings,
find_repo_root,
parse_frontmatter,
@@ -132,6 +133,29 @@ class TestValidateFrontmatter:
violations = list(validate_frontmatter(path, text))
assert violations == []
+ def test_metadata_under_limit(self, tmp_path: Path) -> None:
+ path = tmp_path / "SKILL.md"
+ desc = "a" * 800
+ wtu = "b" * 700
+ text = f"---\nname: foo\ndescription: {desc}\nwhen_to_use:
{wtu}\nlicense: Apache-2.0\n---\n"
+ violations = list(validate_frontmatter(path, text))
+ assert violations == []
+
+ def test_metadata_over_limit(self, tmp_path: Path) -> None:
+ path = tmp_path / "SKILL.md"
+ desc = "a" * 1000
+ wtu = "b" * (MAX_METADATA_CHARS - 1000 + 1)
+ text = f"---\nname: foo\ndescription: {desc}\nwhen_to_use:
{wtu}\nlicense: Apache-2.0\n---\n"
+ violations = list(validate_frontmatter(path, text))
+ assert any("truncates" in v.message and str(MAX_METADATA_CHARS) in
v.message for v in violations)
+
+ def test_metadata_block_scalar_indicator_not_counted(self) -> None:
+ text = f"---\nname: foo\ndescription: |\n {'a' * 100}\nlicense:
Apache-2.0\n---\n"
+ fm = parse_frontmatter(text)
+ assert fm is not None
+ assert not fm["description"].startswith("|")
+ assert len(fm["description"]) == 100
+
# ---------------------------------------------------------------------------
# Heading / anchor helpers