"Sweep stale `<placeholder>` syntax in 134 production SKILL.md files (metadata.stage + description + allowed_memory_scopes)"
§1 — Description (BCP-14 normative)
This task catalogues, classifies, and substitutes the stale <placeholder> syntax that survives in 134 production SKILL.md frontmatter fields across modules/skill/. These placeholders were inherited from earlier scaffold runs (pre-2026-05-19 template work) and never substituted with real values. They block Anthropic-host portability (Phase B transpilers fail on Reference B's bracket prohibition) and degrade operator UX in audit + runtime error messages.
- The detection script
tools/sweep-placeholders/detect.pyMUST walk everySKILL.mdundermodules/skill/, parse the YAML frontmatter, and identify every field whose value contains a literal<word>pattern that is NOT the now-correctedwrap_in_marker:(per TASK-SKILL-113). Output: a structured JSON dump grouped by skill_path, field_path, and placeholder_token. - Known stale-placeholder fields (from the 2026-05-19 sweep audit) MUST be covered:
metadata.stage(134 hits —<SDP §2 stage letter or "cross">),description(28 hits —<input>,<artifact>etc.),allowed_memory_scopes.write[*](16 hits —<scope-glob>),name(2 hits —<artifact>-author/<artifact>-auditin_template/*scaffolds only), anddepends_on_contracts[*].{id, pin_path}(4 hits —<artifact>). - The substitution engine
tools/sweep-placeholders/suggest.pyMUST propose a concrete substitution per placeholder by reading: (a) the skill'sSKILL.mdbody's CONTRACT_ECHO block (which often names the real artefact type), (b)STANDALONE_INTERVIEW.md(which lists real input field names), (c)references/MANIFEST_SCHEMA.md(which carries the real artefact id), (d) the persona'sMODULE.mdentry (which lists the SDP stage letter). Suggestions are advisory; operator review is mandatory. - The substitution engine MUST NOT auto-apply suggestions. The right substitute depends on operator domain knowledge that the engine can approximate but not guarantee. The engine emits a single
tools/sweep-placeholders/report.mdlisting every skill + every placeholder + the recommended fix; operator reviews + edits the report; the sweep step then applies the operator-approved values. - The auditor rule MUST be
SKB-030 placeholder-free-frontmatter(added toSKILL_BUNDLE_RUBRIC.md) with severitywarningforstatus: draftskills and severityerrorforstatus: acceptedor higher. Auto-fix: never (operator-attestation required). - The Python validator
cuo.placeholder_check.scan(skill_path) -> ScanResultMUST return a list ofPlaceholderHit(field_path: str, value: str, suggested_substitution: str | None). The CI gate runspython -m cuo.placeholder_check --catalog modules/skill/and exits non-zero if any production skill has hits. - The sweep timing SHOULD be done in persona-grouped batches rather than one mega-commit (134 files in one commit makes diff review impractical). Recommended batches: P0 personas first (cpo, cto — ~12 skills), then P1 personas (ceo, coo, cfo, chro, cseco, clo, caio — ~30 skills), then P2+ in order. Each batch ships as a separate commit with
chore(skill): SKB-030 sweep — <persona-name> (<N> skills)message. - The body XML form
<untrusted_content source="..." page="...">…</untrusted_content>MUST NOT be touched anywhere. SKB-030 fires only on frontmatter values; body markup is preserved per TASK-SKILL-113 §1 #3. - Each per-skill substitution MUST preserve the SKILL.md's audit hash chain compatibility. The substitution is a frontmatter-only edit; body bytes are unchanged. Where a fine-tune signal triggers as a side effect (e.g.
skill_versionMINOR bump per AGENTS.md §11), the operator follows the standard fine-tune cycle. - Per-skill substitutions MUST be operator-attested via a
chore(skill):commit message containing a one-line rationale per field (e.g.metadata.stage: 'b' (SDP §2 Requirements — body §3 PLAN phase references stage b)). - The CI gate MUST run
cuo.placeholder_check --catalog modules/skill/ --fail-on-erroron every PR after the sweep is complete. Until the sweep finishes, the gate runs as--fail-on-error-status-accepted-onlyso scaffold/draft skills don't block PRs. - Pre-existing exceptions: the
_template/author/SKILL.mdand_template/audit/SKILL.mdfiles are intentional scaffolds and MUST retain their<artifact>placeholders (they're literal substitution tokens forcpoperations, not real values). The detector exempts paths under_template/. - The post-sweep verify script
tools/sweep-placeholders/verify.pyMUST assert: zero stale-placeholder hits in any non-_template/SKILL.md; the placeholder_check validator agrees; every modified file still parses as valid YAML. - The sweep report MUST be committed alongside the substitutions (lives at
tools/sweep-placeholders/report-<YYYY-MM-DD>.md). Future operators reviewing the sweep can reconstruct the decision chain. - Registry version: this task is a v0.2.6 increment (post-v0.2.5 introduced by TASK-SKILL-113). The bump is documented in the repo-root
CHANGELOG.md[SKILL]section.
§2 — Why this design (rationale for humans)
Why a separate task rather than fold into TASK-SKILL-113 (§1 #1)? TASK-SKILL-113 specifically migrated the wrap_in field via a mechanical pattern match. The stale placeholders span DOZENS of distinct fields with NO common pattern. Each one needs operator domain knowledge to substitute correctly. Bundling the work into 113 would have either inflated 113 from 12h to 28h (wrong scope grouping) or shipped 113 incomplete (leaving 134 portability bugs). Separating the tasks honours the audit-fix-audit discipline — 113 closed one specific bug class; 115 closes the residual.
Why operator-attested rather than auto-substituted (§1 #4)? Each placeholder asks a domain question. metadata.stage: <SDP §2 stage letter or "cross"> — is this skill stage b (requirements), c (design), e (delivery), or cross-cutting? The right answer is in the skill's body, but extracting it requires understanding what the skill actually does. A pattern-matching auto-substituter would get this wrong 30-50% of the time (the body sometimes mentions multiple stages; some skills genuinely span multiple stages and should be "cross"). Operator review at 3.5 min/skill × 134 skills = 8 hours is the right tradeoff.
Why suggestions rather than fully manual (§1 #3)? Pure-manual would cost 15-20 min/skill × 134 = 30-45 hours. With suggestions, operator reads the suggestion + verifies via body skim (3-5 min/skill). 4× speedup with negligible correctness loss because the suggestion engine cites its sources (CONTRACT_ECHO, MANIFEST_SCHEMA, MODULE.md) — operator verifies by skim, not by re-derivation.
Why severity warning on draft + error on accepted (§1 #5)? Drafting is iterative; forcing the rule on every draft commit would slow first-pass authoring. Production skills are routed by the supervisor and shipped to hosts — they MUST conform. Same severity scheme as SKB-020..023 + SKB-050..057 (TASK-SKILL-111 + 112) for consistency.
Why persona-grouped batches (§1 #7)? Three reasons. (1) Diff review: a 134-file commit is unreviewable; 12-30 file batches are. (2) Risk isolation: a mistake in one batch is fixable without rolling back the rest. (3) Schedule fit: persona owners can review their own persona's sweep — distributes the review load.
Why body XML preserved (§1 #8)? Restates TASK-SKILL-113's invariant. The body XML is the runtime wrapper; the frontmatter is the declaration. Touching body XML would break wrapping semantics for every production skill that processes external bytes (i.e. all of them).
Why operator-attested commit messages (§1 #10)? Audit-chain integrity. Six months from now, when an operator wonders "why does this skill have metadata.stage: c instead of b?", the commit message has the one-line rationale. Without it, the substitution is unprovenanced — operators have to re-derive the reason from skill body context, defeating the speed-up.
Why _template/ exemption (§1 #12)? The template files DELIBERATELY carry <artifact> placeholders. They're scaffolds; the placeholders are part of the contract for cp -r _template/author/ <new-skill>/ followed by sed-substitution. Sweeping them would break the scaffold-and-substitute workflow.
Why a v0.2.6 increment (§1 #15)? Per DEC-182, frontmatter changes that consumers can detect at load time are MINOR-compatible. The placeholder sweep doesn't change schema (no field renames); it just substitutes literal values. Strictly, this is a content edit, not a registry change — but bumping registry to v0.2.6 makes the change traceable in changelogs and signals to downstream consumers that the catalog has been audit-swept.
§3 — API contract
Detection script — tools/sweep-placeholders/detect.py
"""Detect stale <placeholder> syntax in modules/skill/**/SKILL.md frontmatter."""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
import yaml
PLACEHOLDER_RE = re.compile(r"<([a-zA-Z][a-zA-Z0-9_§ /\"|.()-]*)>")
EXEMPT_PATHS = ("_template/",)
def find_hits_in_value(field_path: str, value, hits: list) -> None:
if isinstance(value, str):
for m in PLACEHOLDER_RE.finditer(value):
tok = m.group(1)
# Whitelist <br> only (mermaid line-break in body; never expected in frontmatter)
if tok.lower() == "br":
continue
hits.append({"field": field_path, "value": value[:120], "token": tok})
elif isinstance(value, dict):
for k, v in value.items():
find_hits_in_value(f"{field_path}.{k}", v, hits)
elif isinstance(value, list):
for i, v in enumerate(value):
find_hits_in_value(f"{field_path}[{i}]", v, hits)
def scan(skill_path: Path) -> dict:
text = skill_path.read_text(encoding="utf-8")
if not text.startswith("---\n"):
return {"skill": str(skill_path), "hits": [], "error": "no_frontmatter"}
end = text.index("\n---\n", 4)
fm = yaml.safe_load(text[4:end])
hits: list = []
find_hits_in_value("root", fm, hits)
return {"skill": str(skill_path), "hits": hits}
def main() -> int:
catalog_root = Path("modules/skill")
results = []
for f in sorted(catalog_root.glob("**/SKILL.md")):
if any(part in str(f) for part in EXEMPT_PATHS):
continue
r = scan(f)
if r.get("hits"):
results.append(r)
print(json.dumps({"total_skills_with_hits": len(results), "skills": results}, indent=2))
return 0 if not results else 1
if __name__ == "__main__":
sys.exit(main())
Suggestion engine — tools/sweep-placeholders/suggest.py
"""Per-skill suggestion engine: propose substitutions based on body + sibling files."""
import re
from pathlib import Path
def suggest_for_metadata_stage(skill_path: Path) -> str | None:
"""Read skill body, find SDP stage references, return the most-frequent stage letter."""
text = (skill_path / "SKILL.md").read_text(encoding="utf-8")
stages = re.findall(r"stage[s]?\s+([a-h])\b", text, re.IGNORECASE)
if not stages:
return None
from collections import Counter
counts = Counter(s.lower() for s in stages)
most_common = counts.most_common(1)[0][0]
return most_common
def suggest_for_description(skill_path: Path, field_value: str) -> str | None:
"""Find <input>, <artifact> placeholders + propose concrete substitutions."""
# Read CONTRACT_ECHO block for artefact type
text = (skill_path / "SKILL.md").read_text(encoding="utf-8")
m = re.search(r"template_version:\s+([a-z][a-z_0-9-]*)@1", text)
artefact = m.group(1) if m else None
if not artefact:
return None
# Substitute <input> → artefact, <artifact> → artefact in description
new_value = field_value
if "<input>" in new_value and artefact:
new_value = new_value.replace("<input>", f"{artefact} source")
if "<artifact>" in new_value:
new_value = new_value.replace("<artifact>", artefact)
return new_value
# Full suggest() function dispatches by field path; per-field heuristics.
Auditor rule — addition to SKILL_BUNDLE_RUBRIC.md
### SKB-030 — placeholder-free-frontmatter
**Statement:** No SKILL.md frontmatter field value may contain literal placeholder syntax like `<word>` (excluding the explicitly-allowed `<br>` if it ever appears, which it shouldn't in frontmatter). Per TASK-SKILL-115. This is distinct from SKB-040 (no-xml-in-frontmatter, which targets the security boundary); SKB-030 targets the operator-UX + portability boundary.
**Severity:** error on `status: accepted | building | shipped`; warning on `status: draft`. Exempt: any path under `_template/`.
**Auto-fix:** never (operator-attestation required — see TASK-SKILL-115 §1 #4).
**Check:** `python -m cuo.placeholder_check <skill_path>`; exit 0 if clean, exit 1 with structured error otherwise.
§4 — Acceptance criteria
- detect.py finds known stale placeholders —
python3 tools/sweep-placeholders/detect.pyon the catalog as-of-2026-05-19 reports exactly the 188 occurrences across 134 files (per the ANTHROPIC_GUIDE_DIGEST.md verification). - detect.py exempts _template/ — running detect.py reports zero hits in
_template/author/SKILL.mdor_template/audit/SKILL.md(those are scaffolds with intentional placeholders). - detect.py whitelists
<br>— a<br>in any frontmatter value (theoretical; shouldn't happen but defended) does not register as a hit. - suggest.py proposes a stage letter — for
task-author(body references SDP §2(b) Requirements), suggest_for_metadata_stage returns'b'. - suggest.py reads CONTRACT_ECHO for artefact type — for
task-author, the description's<input>→task source. - placeholder_check.scan returns structured PlaceholderHit list — given a skill with
metadata.stage: <SDP §2 stage letter or "cross">, returns one hit withfield_path='root.metadata.stage'+value='<SDP §2 stage letter or "cross">'+suggested_substitution='b'(or None if suggest fails). - placeholder_check exit code 1 on hits — running on a skill with stale placeholders returns exit code 1; running on a clean skill returns 0.
- SKB-030 rule fires on production skill with placeholders — given a
status: acceptedskill with stale placeholders, the auditor reports one SKB-030 issue (severity error, status needs_human). - SKB-030 rule fires as warning on draft skill — same input but
status: draft→ severity warning. - Body XML preserved post-sweep — after running the full sweep, every body's
<untrusted_content source="..." page="...">…</untrusted_content>markup is unchanged byte-for-byte. - All 134 production skills are placeholder-free post-sweep —
python3 tools/sweep-placeholders/detect.pyexits 0; verify.py confirms. - Persona-batch commits are reviewable — each batch commit's diff is ≤30 files; commit message follows
chore(skill): SKB-030 sweep — <persona-name> (<N> skills)format. - Operator-attestation in commit message — every batch commit includes one-line rationale per field-type substitution (e.g. "metadata.stage values picked from each skill's body SDP-stage references; cross-cutting skills marked 'cross'").
- verify.py asserts post-sweep invariants — running verify.py exits 0; checks (a) detect.py reports zero hits, (b) every modified file parses as valid YAML, (c) every modified file's
wrap_in_marker:field is still"untrusted_content"(TASK-SKILL-113 invariant preserved), (d) body XML form unchanged via SHA256 comparison. - CI gate integration —
python -m cuo.placeholder_check --catalog modules/skill/ --fail-on-errorruns as part of the existing CUO test suite; PRs touching SKILL.md frontmatter are gated. - task-audit skill §3.13 rule 38f added — references TASK-SKILL-115 + SKB-030.
- Registry version bumped — v0.2.5 → v0.2.6 in CHANGELOG.md
[SKILL]section. - Sweep report committed —
tools/sweep-placeholders/report-2026-05-19.md(or later date) lists every skill + every substitution decision. - Idempotency check — running detect.py + sweep + detect.py again yields the same zero-hit state; no flapping.
- Cross-task reciprocity — TASK-SKILL-113's
blocks:list updated to include TASK-SKILL-115; reciprocity sweep passes.
§5 — Verification
# modules/cuo/tests/test_placeholder_check.py
import pytest
from pathlib import Path
from cuo.placeholder_check import scan, PlaceholderHit
def test_detects_metadata_stage_placeholder(tmp_path):
skill = tmp_path / "SKILL.md"
skill.write_text("""---
name: foo-author
metadata:
stage: <SDP §2 stage letter or "cross">
wrap_in_marker: "untrusted_content"
---
body
""", encoding="utf-8")
result = scan(skill)
assert len(result.hits) >= 1
assert any(h.field_path.endswith(".stage") for h in result.hits)
def test_ignores_template_path(tmp_path):
tpath = tmp_path / "_template" / "author"
tpath.mkdir(parents=True)
skill = tpath / "SKILL.md"
skill.write_text("""---
name: <artefact>-author
metadata:
stage: <SDP §2 stage letter>
---
body
""", encoding="utf-8")
result = scan(skill)
# Template path → return empty hits (exempt)
assert result.exempt is True
def test_whitelists_br_tag(tmp_path):
skill = tmp_path / "SKILL.md"
skill.write_text("""---
name: foo
description: "Line 1<br>Line 2"
---
""", encoding="utf-8")
result = scan(skill)
assert len(result.hits) == 0 # <br> exempt
def test_wrap_in_marker_not_flagged(tmp_path):
skill = tmp_path / "SKILL.md"
skill.write_text("""---
name: foo
metadata:
stage: b
wrap_in_marker: "untrusted_content"
---
body
""", encoding="utf-8")
result = scan(skill)
assert len(result.hits) == 0
§6 — Implementation skeleton
§3 covers the surface. Wiring:
tools/sweep-placeholders/detect.pyis the catalog scanner.tools/sweep-placeholders/suggest.pyis the per-skill suggestion engine.tools/sweep-placeholders/report.mdis the auto-generated report; operator edits to approve.modules/cuo/cuo/placeholder_check.pyis the runtime validator (re-exported fromcuopackage).modules/cuo/tests/test_placeholder_check.pyintegrates with the existing CUO test suite.- SKILL_BUNDLE_RUBRIC.md gains SKB-030.
- Task-audit skill gains §3.13 rule 38f.
- Per-persona batches commit independently (P0 cpo + cto first, then P1, then P2+).
§7 — Dependencies
Depends on:
- TASK-SKILL-113 (XML-free frontmatter) — provides the
wrap_in_marker:rename so the detector can correctly distinguish the now-fixed marker from stale placeholders.
Blocks: none (independent of TASK-SKILL-114; orthogonal to TASK-SKILL-111 + 112).
Related: all of TASK-SKILL-111 / 112 / 113 / 114 — together they form the v0.2.5/v0.2.6 portability + foundation-discipline bundle.
§8 — Example payloads
Example 1 — detect.py output (truncated)
{
"total_skills_with_hits": 134,
"skills": [
{
"skill": "modules/skill/transformation-roadmap-audit/SKILL.md",
"hits": [
{"field": "root.metadata.stage", "value": "<SDP §2 stage letter or \"cross\">", "token": "SDP §2 stage letter or \"cross\""}
]
}
]
}
Example 2 — suggest.py output
$ python3 tools/sweep-placeholders/suggest.py modules/skill/transformation-roadmap-audit
Field: root.metadata.stage
Current: <SDP §2 stage letter or "cross">
Suggested: "cross"
Rationale: Body references stages b, d, e — multi-stage skill; choose "cross".
Example 3 — Audit issue (SKB-030 firing)
ISSUE
id: ISS-014
rule_id: SKB-030
severity: error
category: placeholder_in_frontmatter
location: frontmatter root.metadata.stage
evidence: "metadata.stage: <SDP §2 stage letter or \"cross\">"
description: "Frontmatter field metadata.stage contains stale template placeholder syntax. Per TASK-SKILL-115 §1 #1, every production skill must carry concrete substituted values."
suggestion: "Run 'python3 tools/sweep-placeholders/suggest.py modules/skill/transformation-roadmap-audit' for a context-aware recommendation. The body references SDP stages b/d/e — suggest 'cross'."
auto_fix_applied: false
resolution: null
opened_at: "2026-05-19T17:00:00Z"
updated_at: "2026-05-19T17:00:00Z"
§9 — Open questions
All resolved during authoring.
Deferred:
- Per-persona owner sign-off at batch-commit time — out of scope for this task; handled by normal PR review process.
- Automated batch-commit grouping by persona — out of scope; the operator manually groups via
git add modules/skill/<persona>/.
§10 — Failure modes inventory
| Failure | Detection | Outcome | Recovery |
|---|---|---|---|
| suggest.py proposes wrong value | Operator review catches at report.md stage | No bad substitution shipped | Edit report.md; re-run sweep |
| Operator approves wrong substitution despite review | Audit log captures the wrong value; next fine-tune cycle on the skill surfaces the issue | Bad value lives until fine-tune | Standard fine-tune cycle corrects |
| Sweep accidentally touches body XML | verify.py SHA256-compares pre/post body bytes; mismatch → exit 1 | Sweep batch rejected | Re-run with operator review of diff |
| 134-skill sweep done in one mega-commit | Diff review fails (too large) | PR blocked | Split into persona batches |
| Migration runs while a fine-tune is mid-flight on the same skill | Conflict at commit; standard git merge resolves | Operator resolves conflict per file | Reapply suggested substitution + re-run verify |
| SKB-030 rule fires on draft skill mid-authoring | Severity warning only — author iterates without friction | No production impact | Substitute placeholder when promoting to accepted |
| _template/ exemption mis-fires (skips non-template skill) | detect.py output is incomplete | False clean signal | EXEMPT_PATHS path-prefix check is anchored to _template/; verify with grep -l '_template/' tools/sweep-placeholders/detect.py |
| Registry version bump conflicts with concurrent TASK-SKILL-117 marker namespace work | Operator coordinates timing | Version bump skipped | Bump on TASK-SKILL-117 commit instead |
| Placeholder appears in body XML (false positive) | Detector only scans frontmatter; body bytes untouched | No false positive | Detector splits at \n---\n and only parses frontmatter |
| Operator misreads a multi-line YAML folded scalar as a placeholder | suggest.py shows full flattened context; operator confirms before approval | Edge case caught at review | Adjust report.md template to render flattened equivalent alongside raw |
| CI gate fires before sweep finishes | --fail-on-error-status-accepted-only flag protects scaffold/draft skills during transition | PR not blocked on legitimate draft work | Operator transitions flag to --fail-on-error post-sweep completion |
wrap_in_marker: accidentally regressed during sweep | verify.py invariant (c) catches: marker still must equal "untrusted_content" | Sweep batch rejected | Re-run with corrected substitution |
| Multi-stage skill ambiguity (which letter to pick?) | suggest.py recommends "cross"; operator can override | No silent miscategorisation | Operator review at report.md stage |
| Body references stage letter that doesn't exist in SDP (typo) | suggest.py returns None; operator must hand-edit | No silent bad value | Hand-edit + commit |
§11 — Implementation notes
- Why persona-grouped batches over single mega-commit? Three reasons (already in §2): diff reviewability, risk isolation, schedule fit. Persona batches also align with the existing CHANGELOG taxonomy.
- Why preserve
_template/placeholders deliberately? They're scaffolds. Removing them would breakcp -r _template/author/ <new-skill>/+ sed-substitute workflow. The template files DOCUMENT what placeholders go where — they're contract artefacts, not stale leakage. - Why the broad placeholder regex (allowing §, /, ", etc. inside)? The actual stale values in the catalog include
<SDP §2 stage letter or "cross">— quite verbose. A naive<\w+>regex would miss the multi-word placeholder. The chosen regex matches anything that LOOKS like an angle-bracket placeholder text fragment. - Why suggest.py reads multiple sibling files (CONTRACT_ECHO, MANIFEST_SCHEMA, MODULE.md)? Different fields' correct substitutions come from different sources. metadata.stage comes from body SDP-stage references; description placeholders come from CONTRACT_ECHO template_version; depends_on_contracts come from MANIFEST_SCHEMA. One source-of-truth per field-type.
- Why operator-attested commit messages instead of an automated audit row? Both happen. The commit message is human-readable rationale; the audit row (per AGENTS.md §7) is the machine-readable record. Together they form the chain of custody for each substitution.
- Why 8 hours of mechanical sweep across 134 skills? Math: 3.5 minutes per skill = ~12 skills/hour. 134 / 12 ≈ 11 hours pre-suggestions; with suggest.py shortening review to ~2 min/skill, 8 hours is realistic.
- Why dependency on TASK-SKILL-113 instead of independent? TASK-SKILL-113 introduced
wrap_in_marker:— without that rename in place, the detector might confuse the OLDwrap_in: <untrusted_content/>form (which 113 already migrated) with stale placeholders. TASK-SKILL-113 first; then TASK-SKILL-115 picks up the residual.
End of TASK-SKILL-115.