@ikon85/agent-workflow-kit 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/ask-matt/SKILL.md +139 -0
- package/.agents/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/audit-skills/SKILL.md +145 -0
- package/.agents/skills/board-to-waves/SKILL.md +135 -0
- package/.agents/skills/code-review/SKILL.md +64 -0
- package/.agents/skills/codebase-design/DEEPENING.md +37 -0
- package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/.agents/skills/codebase-design/SKILL.md +114 -0
- package/.agents/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/codex-adapter-sync/SKILL.md +151 -0
- package/.agents/skills/decision-gate/SKILL.md +67 -0
- package/.agents/skills/diagnose/SKILL.md +136 -0
- package/.agents/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.agents/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/domain-modeling/SKILL.md +76 -0
- package/.agents/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/git-worktree-recover/SKILL.md +176 -0
- package/.agents/skills/grill-me/SKILL.md +54 -0
- package/.agents/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/grill-with-docs/SKILL.md +137 -0
- package/.agents/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/implement/SKILL.md +15 -0
- package/.agents/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
- package/.agents/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
- package/.agents/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/.agents/skills/improve-codebase-architecture/SKILL.md +88 -0
- package/.agents/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/kit-release/SKILL.md +56 -0
- package/.agents/skills/local-ci/SKILL.md +94 -0
- package/.agents/skills/orchestrate-wave/SKILL.md +261 -0
- package/.agents/skills/orchestrate-wave/references/builder-contract.md +67 -0
- package/.agents/skills/prototype/LOGIC.md +79 -0
- package/.agents/skills/prototype/SKILL.md +30 -0
- package/.agents/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/prototype/UI.md +112 -0
- package/.agents/skills/research/SKILL.md +12 -0
- package/.agents/skills/research/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/resolving-merge-conflicts/SKILL.md +14 -0
- package/.agents/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/retro/SKILL.md +209 -0
- package/.agents/skills/scale-check/SKILL.md +141 -0
- package/.agents/skills/security-audit/SKILL.md +67 -0
- package/.agents/skills/setup-workflow/SKILL.md +187 -0
- package/.agents/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/setup-workflow/board-sync.md +111 -0
- package/.agents/skills/setup-workflow/code-review.md +26 -0
- package/.agents/skills/setup-workflow/domain.md +51 -0
- package/.agents/skills/setup-workflow/issue-tracker-github.md +35 -0
- package/.agents/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
- package/.agents/skills/setup-workflow/issue-tracker-local.md +30 -0
- package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
- package/.agents/skills/setup-workflow/spec-completeness-seed.md +44 -0
- package/.agents/skills/setup-workflow/spec-self-critique-seed.md +43 -0
- package/.agents/skills/setup-workflow/triage-labels.md +18 -0
- package/.agents/skills/setup-workflow/workflow-overview.md +33 -0
- package/.agents/skills/spec-self-critique/SKILL.md +152 -0
- package/.agents/skills/spec-self-critique/scenarios.md +82 -0
- package/.agents/skills/tdd/SKILL.md +120 -0
- package/.agents/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/tdd/deep-modules.md +33 -0
- package/.agents/skills/tdd/interface-design.md +31 -0
- package/.agents/skills/tdd/mocking.md +59 -0
- package/.agents/skills/tdd/refactoring.md +10 -0
- package/.agents/skills/tdd/tests.md +61 -0
- package/.agents/skills/to-issues/SKILL.md +325 -0
- package/.agents/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
- package/.agents/skills/to-prd/SKILL.md +180 -0
- package/.agents/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/to-waves/SKILL.md +260 -0
- package/.agents/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
- package/.agents/skills/triage/AGENT-BRIEF.md +168 -0
- package/.agents/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/.agents/skills/triage/SKILL.md +122 -0
- package/.agents/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/verify-spike/SKILL.md +39 -0
- package/.agents/skills/wayfinder/SKILL.md +127 -0
- package/.agents/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/wrapup/SKILL.md +71 -0
- package/.agents/skills/wrapup/SOURCES.txt +6 -0
- package/.agents/skills/zoom-out/SKILL.md +7 -0
- package/.agents/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/hooks/_hook_utils.py +184 -0
- package/.claude/hooks/drift-guard.py +176 -0
- package/.claude/hooks/skill-drift-hint.py +122 -0
- package/.claude/hooks/sync-board-status.py +191 -0
- package/.claude/skills/ask-matt/SKILL.md +139 -0
- package/.claude/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/audit-skills/SKILL.md +145 -0
- package/.claude/skills/board-to-waves/SKILL.md +148 -0
- package/.claude/skills/code-review/SKILL.md +64 -0
- package/.claude/skills/codebase-design/DEEPENING.md +37 -0
- package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/.claude/skills/codebase-design/SKILL.md +114 -0
- package/.claude/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/codex-build/SKILL.md +126 -0
- package/.claude/skills/codex-build/THIRD-PARTY-NOTICES.md +35 -0
- package/.claude/skills/codex-review/SKILL.md +145 -0
- package/.claude/skills/codex-review/THIRD-PARTY-NOTICES.md +31 -0
- package/.claude/skills/decision-gate/SKILL.md +67 -0
- package/.claude/skills/diagnose/SKILL.md +136 -0
- package/.claude/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/.claude/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.claude/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/domain-modeling/SKILL.md +76 -0
- package/.claude/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/git-guardrails-claude-code/SKILL.md +96 -0
- package/.claude/skills/git-guardrails-claude-code/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh +28 -0
- package/.claude/skills/git-worktree-recover/SKILL.md +182 -0
- package/.claude/skills/grill-me/SKILL.md +54 -0
- package/.claude/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/grill-me-codex/SKILL.md +158 -0
- package/.claude/skills/grill-me-codex/THIRD-PARTY-NOTICES.md +32 -0
- package/.claude/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/.claude/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/grill-with-docs/SKILL.md +137 -0
- package/.claude/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/grill-with-docs-codex/ADR-FORMAT.md +47 -0
- package/.claude/skills/grill-with-docs-codex/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/grill-with-docs-codex/SKILL.md +233 -0
- package/.claude/skills/grill-with-docs-codex/THIRD-PARTY-NOTICES.md +33 -0
- package/.claude/skills/implement/SKILL.md +15 -0
- package/.claude/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
- package/.claude/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
- package/.claude/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +88 -0
- package/.claude/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/kit-release/SKILL.md +56 -0
- package/.claude/skills/local-ci/SKILL.md +94 -0
- package/.claude/skills/orchestrate-wave/SKILL.md +269 -0
- package/.claude/skills/orchestrate-wave/references/builder-contract.md +67 -0
- package/.claude/skills/prototype/LOGIC.md +79 -0
- package/.claude/skills/prototype/SKILL.md +30 -0
- package/.claude/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/prototype/UI.md +112 -0
- package/.claude/skills/research/SKILL.md +12 -0
- package/.claude/skills/research/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/resolving-merge-conflicts/SKILL.md +14 -0
- package/.claude/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/retro/SKILL.md +209 -0
- package/.claude/skills/scale-check/SKILL.md +141 -0
- package/.claude/skills/security-audit/SKILL.md +67 -0
- package/.claude/skills/setup-pre-commit/SKILL.md +104 -0
- package/.claude/skills/setup-pre-commit/scripts/pre-commit.template.sh +35 -0
- package/.claude/skills/setup-workflow/SKILL.md +187 -0
- package/.claude/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/setup-workflow/board-sync.md +111 -0
- package/.claude/skills/setup-workflow/code-review.md +26 -0
- package/.claude/skills/setup-workflow/domain.md +51 -0
- package/.claude/skills/setup-workflow/issue-tracker-github.md +35 -0
- package/.claude/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
- package/.claude/skills/setup-workflow/issue-tracker-local.md +30 -0
- package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
- package/.claude/skills/setup-workflow/spec-completeness-seed.md +44 -0
- package/.claude/skills/setup-workflow/spec-self-critique-seed.md +43 -0
- package/.claude/skills/setup-workflow/triage-labels.md +18 -0
- package/.claude/skills/setup-workflow/workflow-overview.md +33 -0
- package/.claude/skills/skill-manifest.json +389 -0
- package/.claude/skills/spec-self-critique/SKILL.md +152 -0
- package/.claude/skills/spec-self-critique/scenarios.md +82 -0
- package/.claude/skills/tdd/SKILL.md +120 -0
- package/.claude/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/tdd/deep-modules.md +33 -0
- package/.claude/skills/tdd/interface-design.md +31 -0
- package/.claude/skills/tdd/mocking.md +59 -0
- package/.claude/skills/tdd/refactoring.md +10 -0
- package/.claude/skills/tdd/tests.md +61 -0
- package/.claude/skills/to-issues/SKILL.md +325 -0
- package/.claude/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
- package/.claude/skills/to-prd/SKILL.md +180 -0
- package/.claude/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/to-waves/SKILL.md +260 -0
- package/.claude/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
- package/.claude/skills/triage/AGENT-BRIEF.md +168 -0
- package/.claude/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/.claude/skills/triage/SKILL.md +122 -0
- package/.claude/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/verify-spike/SKILL.md +39 -0
- package/.claude/skills/wayfinder/SKILL.md +127 -0
- package/.claude/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/wrapup/SKILL.md +81 -0
- package/.claude/skills/wrapup/SOURCES.txt +6 -0
- package/.claude/skills/write-a-skill/GLOSSARY.md +201 -0
- package/.claude/skills/write-a-skill/SKILL.md +88 -0
- package/.claude/skills/write-a-skill/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/zoom-out/SKILL.md +7 -0
- package/.claude/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
- package/LICENSE +28 -0
- package/PROVENANCE.md +39 -0
- package/README.md +619 -0
- package/agent-workflow-kit.package.json +1939 -0
- package/docs/agents/board-sync.md +71 -0
- package/docs/agents/code-review.md +14 -0
- package/docs/agents/domain.md +7 -0
- package/docs/agents/issue-tracker.md +12 -0
- package/docs/agents/security-audit-runbook-template.md +205 -0
- package/docs/agents/skills/orchestrate-wave.md +13 -0
- package/docs/agents/skills/spec-self-critique.md +18 -0
- package/docs/agents/triage-labels.md +10 -0
- package/docs/agents/wave-anchor-template.md +88 -0
- package/docs/conventions/spec-completeness.md +23 -0
- package/docs/index.html +56 -0
- package/docs/methodology.html +488 -0
- package/docs/methodology.svg +93 -0
- package/docs/workflow.png +0 -0
- package/package.json +40 -0
- package/scripts/anchor_table.py +343 -0
- package/scripts/board-sync.py +1232 -0
- package/scripts/board_config.py +147 -0
- package/scripts/board_fields.py +192 -0
- package/scripts/build-kit.mjs +116 -0
- package/scripts/build-kit.test.mjs +59 -0
- package/scripts/check-kit-staleness.mjs +45 -0
- package/scripts/check-kit-staleness.test.mjs +19 -0
- package/scripts/execute-ready-check.py +516 -0
- package/scripts/issue_deps.py +79 -0
- package/scripts/kit-release.mjs +100 -0
- package/scripts/kit-release.test.mjs +67 -0
- package/scripts/lib/audit-refs.mjs +146 -0
- package/scripts/lib/scrub.mjs +113 -0
- package/scripts/lib/scrub.test.mjs +187 -0
- package/scripts/loc_offender_core.py +196 -0
- package/scripts/loc_offender_gate.py +182 -0
- package/scripts/node_kind.py +68 -0
- package/scripts/portability_profile_scan.py +53 -0
- package/scripts/pr-body-check.py +263 -0
- package/scripts/program_graph.py +125 -0
- package/scripts/program_graph_parse.py +275 -0
- package/scripts/program_graph_validate.py +256 -0
- package/scripts/program_sync.py +160 -0
- package/scripts/release-delta-guard.mjs +96 -0
- package/scripts/release-delta-guard.test.mjs +53 -0
- package/scripts/test_board_sync_create_idempotency.py +104 -0
- package/scripts/test_dist_kit_smoke.py +199 -0
- package/scripts/test_kit_docs_language_census.py +178 -0
- package/scripts/test_skill_code_review_seed.py +89 -0
- package/scripts/test_skill_frontmatter_lint.py +226 -0
- package/scripts/test_skill_gh_lint.py +124 -0
- package/scripts/test_skill_language_census.py +254 -0
- package/scripts/test_skill_portability_lint.py +773 -0
- package/scripts/test_skill_publish_audit.py +300 -0
- package/scripts/test_skill_selfcontainment_lint.py +255 -0
- package/scripts/test_skill_setup_workflow_seeds.py +200 -0
- package/scripts/test_skill_stale_name_lint.py +126 -0
- package/scripts/test_skill_surface_refs.py +300 -0
- package/scripts/test_skill_trailing_artifact_lint.py +71 -0
- package/scripts/wrapup-land.py +573 -0
- package/src/cli.mjs +70 -0
- package/src/commands/diff.mjs +6 -0
- package/src/commands/init.mjs +59 -0
- package/src/commands/uninstall.mjs +43 -0
- package/src/commands/update.mjs +95 -0
- package/src/lib/atomicWrite.mjs +49 -0
- package/src/lib/bundle.mjs +133 -0
- package/src/lib/hash.mjs +13 -0
- package/src/lib/manifest.mjs +58 -0
- package/src/lib/sentinel.mjs +20 -0
- package/src/lib/settings.mjs +23 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fail-closed publish audit for dist-kit/ (Welle 26 / Slice 6 / #990, Step 13).
|
|
3
|
+
|
|
4
|
+
The scrub (scripts/lib/scrub.mjs) removes project-private tokens at build time;
|
|
5
|
+
THIS audit is the BACKSTOP — it scans the assembled dist-kit/ and DENIES the
|
|
6
|
+
publish if anything private survived. Scrub may be imperfect; this fails loud so
|
|
7
|
+
nothing private ever ships silently.
|
|
8
|
+
|
|
9
|
+
Deny classes (each with a negative fixture below):
|
|
10
|
+
- private repo slug `iKon85/Testreporter` + the bare project name
|
|
11
|
+
- board node/field IDs (`PVT…`) + status/wave/cluster option hashes
|
|
12
|
+
- private deploy domains (`*.iverra.de`), Coolify
|
|
13
|
+
- email addresses
|
|
14
|
+
- absolute home paths (`/home/<user>`)
|
|
15
|
+
- residual issue refs (`#NNN`) / hard-rule refs (`HRn`)
|
|
16
|
+
- unresolvable provenance cross-refs (`ADR-####` / `Welle N` / `Slice N`),
|
|
17
|
+
with a documented fixture/example allowlist (PROVENANCE_FIXTURE_SUFFIXES)
|
|
18
|
+
- `../` cross-skill reaches (skills/scripts/docs — NOT the CLI src, which
|
|
19
|
+
legitimately imports `../lib/…`)
|
|
20
|
+
- bare owner/maintainer names OUTSIDE the generated credit files
|
|
21
|
+
- high-entropy secrets (after exempting the manifest's own sha256 file-hashes)
|
|
22
|
+
|
|
23
|
+
Allowlisted: the PUBLIC repo slug `iKon85/agent-workflow-kit`.
|
|
24
|
+
File-exempt (credits legitimately name people): LICENSE, README.md, PROVENANCE.md,
|
|
25
|
+
THIRD-PARTY-NOTICES.md — but these still may NOT carry board IDs / domains /
|
|
26
|
+
secrets / the private repo slug.
|
|
27
|
+
|
|
28
|
+
Run: python3 scripts/test_skill_publish_audit.py
|
|
29
|
+
"""
|
|
30
|
+
import json
|
|
31
|
+
import re
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
import tempfile
|
|
35
|
+
import unittest
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
39
|
+
PUBLIC_SLUG = "iKon85/agent-workflow-kit"
|
|
40
|
+
CREDIT_FILES = {"LICENSE", "README.md", "PROVENANCE.md", "THIRD-PARTY-NOTICES.md"}
|
|
41
|
+
PRIVATE_SKILLS = {
|
|
42
|
+
"drizzle", "migrations", "forecast-logic", "bug-bucketing", "iverra-brand",
|
|
43
|
+
"ui-ux-pro-max",
|
|
44
|
+
}
|
|
45
|
+
# pin SHAs are legitimate attribution (full forms exempted from high-entropy)
|
|
46
|
+
KNOWN_PIN_HASHES = {
|
|
47
|
+
"2bf70051928429983de3b5718d277150926f8c89",
|
|
48
|
+
"ba71f82e8469395d3f7c4ed824334b16676e87a0",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# (label, regex) — denied EVERYWHERE, even in credit files.
|
|
52
|
+
HARD_DENY = [
|
|
53
|
+
("private repo slug", re.compile(r"iKon85/Testreporter")),
|
|
54
|
+
("board node/field id", re.compile(r"PVT(?:SSF|F)?_[A-Za-z0-9]")),
|
|
55
|
+
("private deploy domain", re.compile(r"[A-Za-z0-9.-]*\.iverra\.de")),
|
|
56
|
+
("Coolify reference", re.compile(r"\bCoolify\b")),
|
|
57
|
+
("email address", re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")),
|
|
58
|
+
("absolute home path", re.compile(r"/home/[A-Za-z0-9._-]+")),
|
|
59
|
+
("home-encoded project slug", re.compile(r"-home-[a-z0-9]+-projects")),
|
|
60
|
+
("issue ref", re.compile(r"#\d{3,5}\b")),
|
|
61
|
+
("hard-rule ref", re.compile(r"\bHR\d+\b")),
|
|
62
|
+
("project name", re.compile(r"[Tt]estreporter")),
|
|
63
|
+
("kit issue ref", re.compile(r"\bkit#\d+\b")),
|
|
64
|
+
]
|
|
65
|
+
# Provenance cross-refs a kit consumer cannot resolve (no docs/adr dir, no wave
|
|
66
|
+
# history). Denied EVERYWHERE except the documented fixture/example files below,
|
|
67
|
+
# where a Welle/Slice/ADR token is legitimate CONTENT — synthetic PRD scenarios,
|
|
68
|
+
# a template placeholder, illustrative ADR names, or board-sync's wave-prefix
|
|
69
|
+
# parser examples — not project provenance. Keep in sync with scrub.mjs's PROV
|
|
70
|
+
# class (which strips citation-shaped provenance from every other body/script).
|
|
71
|
+
PROVENANCE_DENY = [
|
|
72
|
+
("ADR ref", re.compile(r"\bADR-\d{3,4}\b")),
|
|
73
|
+
("Welle ref", re.compile(r"\bWelle \d+\b")),
|
|
74
|
+
("Slice ref", re.compile(r"\bSlice \d+[a-z]?\b")),
|
|
75
|
+
]
|
|
76
|
+
# Matched by dest-path SUFFIX so both surfaces (.claude/skills, .agents/skills)
|
|
77
|
+
# resolve with one entry each.
|
|
78
|
+
PROVENANCE_FIXTURE_SUFFIXES = (
|
|
79
|
+
"spec-self-critique/scenarios.md", # synthetic Program-PRD fixtures
|
|
80
|
+
"to-issues/SKILL.md", # `Welle $WAVE / Slice 1a` title example
|
|
81
|
+
"improve-codebase-architecture/SKILL.md", # illustrative `ADR-0007` example
|
|
82
|
+
"setup-workflow/domain.md", # illustrative `ADR-0007` example
|
|
83
|
+
"docs/agents/wave-anchor-template.md", # `Slice 1` / `Welle <N>` placeholders
|
|
84
|
+
"scripts/board-sync.py", # `Welle 7 — X` wave-prefix parser examples
|
|
85
|
+
)
|
|
86
|
+
# denied only OUTSIDE credit files (scrub should have neutralized these)
|
|
87
|
+
BARE_PRIVATE = [
|
|
88
|
+
("bare owner iKon85", re.compile(r"iKon85")),
|
|
89
|
+
("maintainer name", re.compile(r"\bNiko\b")),
|
|
90
|
+
]
|
|
91
|
+
PARENT_REF = re.compile(r"\.\./")
|
|
92
|
+
LONG_HEX = re.compile(r"\b[0-9a-f]{40,}\b")
|
|
93
|
+
GH_TOKEN = re.compile(r"\b(?:ghp|gho|ghu|ghs|github_pat)_[A-Za-z0-9_]{20,}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _parent_ref_scan_text(rel: str, text: str) -> str:
|
|
97
|
+
if rel != "scripts/loc_offender_core.py":
|
|
98
|
+
return text
|
|
99
|
+
# This is the portable path-traversal guard itself, not a cross-skill reach.
|
|
100
|
+
return "\n".join(
|
|
101
|
+
line for line in text.splitlines()
|
|
102
|
+
if 'p.startswith("../")' not in line and '"/../" in p' not in line
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _known_hashes(root: Path) -> set:
|
|
107
|
+
hashes = set(KNOWN_PIN_HASHES)
|
|
108
|
+
pkg = root / "agent-workflow-kit.package.json"
|
|
109
|
+
if pkg.exists():
|
|
110
|
+
data = json.loads(pkg.read_text(encoding="utf-8"))
|
|
111
|
+
for f in data.get("files", []):
|
|
112
|
+
if "sha256" in f:
|
|
113
|
+
hashes.add(f["sha256"])
|
|
114
|
+
return hashes
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _scan_file(rel: str, text: str, known_hashes: set) -> list:
|
|
118
|
+
out = []
|
|
119
|
+
base = Path(rel).name
|
|
120
|
+
credit = base in CREDIT_FILES
|
|
121
|
+
scan = text.replace(PUBLIC_SLUG, "") # allowlist the public slug
|
|
122
|
+
for label, pat in HARD_DENY:
|
|
123
|
+
if pat.search(scan):
|
|
124
|
+
out.append(f"{rel}: {label}")
|
|
125
|
+
if not rel.endswith(PROVENANCE_FIXTURE_SUFFIXES):
|
|
126
|
+
for label, pat in PROVENANCE_DENY:
|
|
127
|
+
if pat.search(scan):
|
|
128
|
+
out.append(f"{rel}: {label}")
|
|
129
|
+
if not credit:
|
|
130
|
+
for label, pat in BARE_PRIVATE:
|
|
131
|
+
if pat.search(scan):
|
|
132
|
+
out.append(f"{rel}: {label}")
|
|
133
|
+
if not rel.startswith("src/") and PARENT_REF.search(_parent_ref_scan_text(rel, text)):
|
|
134
|
+
out.append(f"{rel}: ../ parent reference")
|
|
135
|
+
for m in LONG_HEX.finditer(text):
|
|
136
|
+
if m.group(0) not in known_hashes:
|
|
137
|
+
out.append(f"{rel}: high-entropy hex (possible secret)")
|
|
138
|
+
break
|
|
139
|
+
if GH_TOKEN.search(text):
|
|
140
|
+
out.append(f"{rel}: GitHub-token-like string")
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def audit_dir(root) -> list:
|
|
145
|
+
"""Return a list of violation strings ([] = clean = publishable)."""
|
|
146
|
+
root = Path(root)
|
|
147
|
+
known = _known_hashes(root)
|
|
148
|
+
violations = []
|
|
149
|
+
for f in sorted(root.rglob("*")):
|
|
150
|
+
if not f.is_file():
|
|
151
|
+
continue
|
|
152
|
+
rel = f.relative_to(root).as_posix()
|
|
153
|
+
raw = f.read_bytes()
|
|
154
|
+
if b"\x00" in raw: # binary — skip
|
|
155
|
+
continue
|
|
156
|
+
violations += _scan_file(rel, raw.decode("utf-8", errors="replace"), known)
|
|
157
|
+
# project-private skill dirs must never appear in the kit
|
|
158
|
+
for surface in (".claude/skills", ".agents/skills"):
|
|
159
|
+
d = root / surface
|
|
160
|
+
if d.is_dir():
|
|
161
|
+
for child in d.iterdir():
|
|
162
|
+
if child.is_dir() and child.name in PRIVATE_SKILLS:
|
|
163
|
+
violations.append(f"{surface}/{child.name}: project-private skill shipped")
|
|
164
|
+
return violations
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# --------------------------------------------------------------------------- #
|
|
168
|
+
class AuditCatchesEachClass(unittest.TestCase):
|
|
169
|
+
"""Inject one violation per class into a clean fixture → it must be caught."""
|
|
170
|
+
|
|
171
|
+
def setUp(self):
|
|
172
|
+
self.dir = Path(tempfile.mkdtemp())
|
|
173
|
+
# a minimal CLEAN kit
|
|
174
|
+
(self.dir / "agent-workflow-kit.package.json").write_text(
|
|
175
|
+
json.dumps({"kitVersion": "0.1.0", "files": [
|
|
176
|
+
{"path": ".claude/skills/x/SKILL.md", "sha256": "a" * 64}]}),
|
|
177
|
+
encoding="utf-8")
|
|
178
|
+
self.skill = self.dir / ".claude/skills/x"
|
|
179
|
+
self.skill.mkdir(parents=True)
|
|
180
|
+
(self.skill / "SKILL.md").write_text("# clean skill\nuse `#<n>` and #0f172a\n",
|
|
181
|
+
encoding="utf-8")
|
|
182
|
+
|
|
183
|
+
def _body(self, content):
|
|
184
|
+
(self.skill / "SKILL.md").write_text(content, encoding="utf-8")
|
|
185
|
+
return audit_dir(self.dir)
|
|
186
|
+
|
|
187
|
+
def test_clean_passes(self):
|
|
188
|
+
self.assertEqual(audit_dir(self.dir), [])
|
|
189
|
+
|
|
190
|
+
def test_private_slug(self):
|
|
191
|
+
self.assertTrue(any("repo slug" in v for v in self._body("see iKon85/Testreporter")))
|
|
192
|
+
|
|
193
|
+
def test_board_id(self):
|
|
194
|
+
self.assertTrue(any("board" in v for v in self._body("id PVT_kwHOAuH31M4BVtcf")))
|
|
195
|
+
|
|
196
|
+
def test_domain(self):
|
|
197
|
+
self.assertTrue(any("domain" in v for v in self._body("curl testreporter.iverra.de")))
|
|
198
|
+
|
|
199
|
+
def test_email(self):
|
|
200
|
+
self.assertTrue(any("email" in v for v in self._body("ping a@b.com please")))
|
|
201
|
+
|
|
202
|
+
def test_home_path(self):
|
|
203
|
+
self.assertTrue(any("home path" in v for v in self._body("at /home/niko/x")))
|
|
204
|
+
|
|
205
|
+
def test_home_encoded_slug(self):
|
|
206
|
+
leak = '$HOME/.claude/projects/-home-niko-projects-x/memory'
|
|
207
|
+
self.assertTrue(any("home-encoded" in v for v in self._body(leak)))
|
|
208
|
+
|
|
209
|
+
def test_issue_ref(self):
|
|
210
|
+
self.assertTrue(any("issue ref" in v for v in self._body("fixed in #824 now")))
|
|
211
|
+
|
|
212
|
+
def test_hr_ref(self):
|
|
213
|
+
self.assertTrue(any("hard-rule" in v for v in self._body("per HR16 rule")))
|
|
214
|
+
|
|
215
|
+
def test_kit_issue_ref(self):
|
|
216
|
+
self.assertTrue(any("kit issue ref" in v for v in self._body("see kit#27 §2")))
|
|
217
|
+
|
|
218
|
+
def test_adr_ref(self):
|
|
219
|
+
self.assertTrue(any("ADR ref" in v for v in self._body("per ADR-0034 rule")))
|
|
220
|
+
|
|
221
|
+
def test_welle_ref(self):
|
|
222
|
+
self.assertTrue(any("Welle ref" in v for v in self._body("built in Welle 52")))
|
|
223
|
+
|
|
224
|
+
def test_slice_ref(self):
|
|
225
|
+
self.assertTrue(any("Slice ref" in v for v in self._body("done in Slice 1g")))
|
|
226
|
+
|
|
227
|
+
def test_parent_ref(self):
|
|
228
|
+
self.assertTrue(any("parent" in v for v in self._body("[x](../../other/SKILL.md)")))
|
|
229
|
+
|
|
230
|
+
def test_bare_owner_in_body(self):
|
|
231
|
+
self.assertTrue(any("iKon85" in v for v in self._body("authored by iKon85")))
|
|
232
|
+
|
|
233
|
+
def test_secret(self):
|
|
234
|
+
self.assertTrue(any("secret" in v for v in self._body("token " + "d" * 50)))
|
|
235
|
+
|
|
236
|
+
def test_gh_token(self):
|
|
237
|
+
self.assertTrue(any("token" in v for v in self._body("ghp_" + "A" * 30)))
|
|
238
|
+
|
|
239
|
+
def test_private_skill_dir_flagged(self):
|
|
240
|
+
(self.dir / ".claude/skills/drizzle").mkdir(parents=True)
|
|
241
|
+
self.assertTrue(any("project-private" in v for v in audit_dir(self.dir)))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class Exemptions(unittest.TestCase):
|
|
245
|
+
"""Public slug is allowlisted; credit files may legitimately name people."""
|
|
246
|
+
|
|
247
|
+
def setUp(self):
|
|
248
|
+
self.dir = Path(tempfile.mkdtemp())
|
|
249
|
+
(self.dir / "agent-workflow-kit.package.json").write_text(
|
|
250
|
+
'{"kitVersion":"0.1.0","files":[]}', encoding="utf-8")
|
|
251
|
+
|
|
252
|
+
def _write(self, name, content):
|
|
253
|
+
(self.dir / name).write_text(content, encoding="utf-8")
|
|
254
|
+
|
|
255
|
+
def test_public_slug_allowed(self):
|
|
256
|
+
self._write("README.md", "npx github:iKon85/agent-workflow-kit init")
|
|
257
|
+
self.assertEqual(audit_dir(self.dir), [])
|
|
258
|
+
|
|
259
|
+
def test_credit_file_may_name_maintainer(self):
|
|
260
|
+
self._write("LICENSE", "Copyright (c) 2026 Niko (iKon85)\nMatt Pocock\nChase AI")
|
|
261
|
+
self.assertEqual(audit_dir(self.dir), [])
|
|
262
|
+
|
|
263
|
+
def test_credit_file_still_denies_board_id(self):
|
|
264
|
+
self._write("LICENSE", "Niko (iKon85) PVT_leak")
|
|
265
|
+
self.assertTrue(any("board" in v for v in audit_dir(self.dir)))
|
|
266
|
+
|
|
267
|
+
def test_manifest_own_hashes_not_flagged_as_secret(self):
|
|
268
|
+
# the package manifest's 64-hex file hashes are exempt (they ARE the kit's hashes)
|
|
269
|
+
(self.dir / "agent-workflow-kit.package.json").write_text(
|
|
270
|
+
json.dumps({"kitVersion": "0.1.0", "files": [
|
|
271
|
+
{"path": "x", "sha256": "f" * 64}]}), encoding="utf-8")
|
|
272
|
+
self.assertEqual(audit_dir(self.dir), [])
|
|
273
|
+
|
|
274
|
+
def test_loc_offender_traversal_guard_allowed(self):
|
|
275
|
+
p = self.dir / "scripts"
|
|
276
|
+
p.mkdir()
|
|
277
|
+
(p / "loc_offender_core.py").write_text(
|
|
278
|
+
'if p == ".." or p.startswith("../") or "/../" in p or p.endswith("/.."):\n'
|
|
279
|
+
' return None\n',
|
|
280
|
+
encoding="utf-8")
|
|
281
|
+
self.assertEqual(audit_dir(self.dir), [])
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class RealDistKitIsClean(unittest.TestCase):
|
|
285
|
+
"""Build dist-kit/ from SSOT and assert the audit passes (the publish gate)."""
|
|
286
|
+
|
|
287
|
+
def test_built_dist_kit_passes_audit(self):
|
|
288
|
+
build = REPO / "scripts/build-kit.mjs"
|
|
289
|
+
try:
|
|
290
|
+
r = subprocess.run(["node", str(build)], cwd=REPO,
|
|
291
|
+
capture_output=True, text=True, timeout=120)
|
|
292
|
+
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
|
|
293
|
+
self.skipTest(f"node/build unavailable: {e}")
|
|
294
|
+
self.assertEqual(r.returncode, 0, r.stderr)
|
|
295
|
+
violations = audit_dir(REPO / "dist-kit")
|
|
296
|
+
self.assertEqual(violations, [], "\n".join(violations[:40]))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
unittest.main(verbosity=2)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Self-containment lint (Welle 26 / Slice 2 / #986): generic & vendored skills
|
|
3
|
+
must be portable.
|
|
4
|
+
|
|
5
|
+
Why: a published skill (npx/git-clone into a foreign repo, or a plugin cache)
|
|
6
|
+
cannot resolve `../../../docs/...` — the path does not exist there and `../`
|
|
7
|
+
escapes the plugin root. Project-specific content is found by *runtime
|
|
8
|
+
convention* (an inline project-root path like `docs/agents/board-sync.md` that
|
|
9
|
+
the model resolves from the project root), never a hardcoded `../` link.
|
|
10
|
+
project-private skills are exempt: they live in this repo forever and their
|
|
11
|
+
`../` refs are correct and clickable.
|
|
12
|
+
|
|
13
|
+
Scope: skills whose manifest class is `generic` or `vendored`. Every distributed
|
|
14
|
+
`*.md` file in the skill dir is scanned (SKILL.md + support docs), in both the
|
|
15
|
+
Claude source (`.claude/skills`) and the Codex mirror (`.agents/skills`).
|
|
16
|
+
|
|
17
|
+
A line carrying `self-containment-lint: ok` is exempt — line-scoped, for
|
|
18
|
+
deliberate "don't do this" doc examples.
|
|
19
|
+
|
|
20
|
+
Run: python3 scripts/test_skill_selfcontainment_lint.py
|
|
21
|
+
"""
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
import unittest
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
28
|
+
MANIFEST = REPO_ROOT / ".claude/skills/skill-manifest.json"
|
|
29
|
+
SKILL_DIRS = [".claude/skills", ".agents/skills"]
|
|
30
|
+
ENFORCED_CLASSES = {"generic", "vendored"}
|
|
31
|
+
PUBLISH_CLASSES = {"generic", "vendored", "adapter"}
|
|
32
|
+
VALID_CLASSES = {"generic", "vendored", "adapter", "project-private"}
|
|
33
|
+
VALID_SURFACES = {"claude", "codex"}
|
|
34
|
+
|
|
35
|
+
PARENT_REF = re.compile(r"\.\./")
|
|
36
|
+
EXEMPT = "self-containment-lint: ok"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_manifest() -> dict:
|
|
40
|
+
return json.loads(MANIFEST.read_text(encoding="utf-8"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def find_parent_refs(text: str) -> list[tuple[int, str]]:
|
|
44
|
+
"""Return (line_no, line) for every line with a `../` reach, minus exempt lines."""
|
|
45
|
+
out = []
|
|
46
|
+
for n, line in enumerate(text.splitlines(), 1):
|
|
47
|
+
if EXEMPT in line:
|
|
48
|
+
continue
|
|
49
|
+
if PARENT_REF.search(line):
|
|
50
|
+
out.append((n, line.strip()))
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def skill_dirs_on_disk(tree: str) -> set[str]:
|
|
55
|
+
base = REPO_ROOT / tree
|
|
56
|
+
if not base.is_dir():
|
|
57
|
+
return set()
|
|
58
|
+
# dot-dirs are tool caches (e.g. .impeccable/hook.cache.json, gitignored), not skills
|
|
59
|
+
return {p.name for p in base.iterdir() if p.is_dir() and not p.name.startswith(".")}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Detector(unittest.TestCase):
|
|
63
|
+
"""The `../` detector itself behaves."""
|
|
64
|
+
|
|
65
|
+
def test_flags_parent_link(self):
|
|
66
|
+
self.assertEqual(len(find_parent_refs("see [x](../../../docs/y.md)")), 1)
|
|
67
|
+
|
|
68
|
+
def test_flags_parent_in_code(self):
|
|
69
|
+
self.assertEqual(len(find_parent_refs("import x from '../lib/y'")), 1)
|
|
70
|
+
|
|
71
|
+
def test_ignores_same_dir_relative(self):
|
|
72
|
+
self.assertEqual(find_parent_refs("see [x](./sub/y.md)"), [])
|
|
73
|
+
|
|
74
|
+
def test_ignores_root_relative_convention(self):
|
|
75
|
+
# the portable form: an inline project-root path, no `../`
|
|
76
|
+
self.assertEqual(find_parent_refs("read `docs/agents/board-sync.md`"), [])
|
|
77
|
+
|
|
78
|
+
def test_exempt_marker_line_scoped(self):
|
|
79
|
+
self.assertEqual(
|
|
80
|
+
find_parent_refs("bad: [x](../y) <!-- self-containment-lint: ok -->"), [])
|
|
81
|
+
|
|
82
|
+
def test_exempt_is_line_scoped_not_file_wide(self):
|
|
83
|
+
text = "ok line <!-- self-containment-lint: ok -->\n[x](../../bad)"
|
|
84
|
+
self.assertEqual(len(find_parent_refs(text)), 1)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ManifestValid(unittest.TestCase):
|
|
88
|
+
def test_schema_and_classes(self):
|
|
89
|
+
m = load_manifest()
|
|
90
|
+
self.assertEqual(m.get("schema_version"), 1)
|
|
91
|
+
for name, entry in m["skills"].items():
|
|
92
|
+
self.assertIn(entry.get("class"), VALID_CLASSES, name)
|
|
93
|
+
self.assertTrue(entry.get("surfaces"), f"{name}: no surfaces")
|
|
94
|
+
for s in entry["surfaces"]:
|
|
95
|
+
self.assertIn(s, VALID_SURFACES, f"{name}:{s}")
|
|
96
|
+
# publish is a mandatory bool — build-kit + lints consume it, not class
|
|
97
|
+
# (Slice 6 / #990 / Codex R2#3+#5).
|
|
98
|
+
self.assertIsInstance(entry.get("publish"), bool,
|
|
99
|
+
f"{name}: `publish` must be present and boolean")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ManifestCompleteness(unittest.TestCase):
|
|
103
|
+
"""Manifest <-> filesystem, both trees (Codex R1#4 / R2#1)."""
|
|
104
|
+
|
|
105
|
+
def test_every_disk_skill_classified(self):
|
|
106
|
+
m = load_manifest()["skills"]
|
|
107
|
+
problems = [f"{tree}/{d} on disk but missing from manifest"
|
|
108
|
+
for tree in SKILL_DIRS
|
|
109
|
+
for d in sorted(skill_dirs_on_disk(tree)) if d not in m]
|
|
110
|
+
self.assertEqual(problems, [], "\n".join(problems))
|
|
111
|
+
|
|
112
|
+
def test_every_entry_matches_its_surfaces(self):
|
|
113
|
+
m = load_manifest()["skills"]
|
|
114
|
+
claude = skill_dirs_on_disk(".claude/skills")
|
|
115
|
+
agents = skill_dirs_on_disk(".agents/skills")
|
|
116
|
+
problems = []
|
|
117
|
+
for name, entry in m.items():
|
|
118
|
+
surfaces = set(entry.get("surfaces", []))
|
|
119
|
+
if "claude" in surfaces and name not in claude:
|
|
120
|
+
problems.append(f"{name}: surfaces claude but no .claude/skills/{name}")
|
|
121
|
+
if "codex" in surfaces and name not in agents:
|
|
122
|
+
problems.append(f"{name}: surfaces codex but no .agents/skills/{name}")
|
|
123
|
+
if name in claude and "claude" not in surfaces:
|
|
124
|
+
problems.append(f"{name}: in .claude/skills but surfaces lacks claude")
|
|
125
|
+
if name in agents and "codex" not in surfaces:
|
|
126
|
+
problems.append(f"{name}: in .agents/skills but surfaces lacks codex")
|
|
127
|
+
self.assertEqual(problems, [], "\n".join(problems))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class PublishExclusion(unittest.TestCase):
|
|
131
|
+
"""`publish` is authoritative; project-private skills never ship.
|
|
132
|
+
|
|
133
|
+
Adapters MAY ship — `adapter` is in PUBLISH_CLASSES (above). A published
|
|
134
|
+
skill can reference an adapter as a real workflow step (to-issues:59 +
|
|
135
|
+
orchestrate-wave -> codex-adapter-sync mirror), in which case build-kit's
|
|
136
|
+
shipped-deps-must-ship check (build-kit.mjs) *requires* the adapter to
|
|
137
|
+
publish — that dependency check is the authoritative consistency guardrail.
|
|
138
|
+
An adapter only ships when something published references it (spark-coordinator
|
|
139
|
+
stays publish:false — nothing published references it). Narrowed from the
|
|
140
|
+
original project-private+adapter rule (#990 R1#10/R2#3/#5) once a published
|
|
141
|
+
skill was found to depend on codex-adapter-sync; the blanket adapter ban
|
|
142
|
+
contradicted both PUBLISH_CLASSES and that dependency."""
|
|
143
|
+
|
|
144
|
+
def test_project_private_not_published(self):
|
|
145
|
+
m = load_manifest()["skills"]
|
|
146
|
+
leaks = sorted(n for n, e in m.items()
|
|
147
|
+
if e["class"] == "project-private" and e.get("publish"))
|
|
148
|
+
self.assertEqual(leaks, [], f"project-private marked publish:true: {leaks}")
|
|
149
|
+
|
|
150
|
+
def test_published_skill_dirs_exist_per_surface(self):
|
|
151
|
+
"""every publish:true skill exists on disk in each surface it claims."""
|
|
152
|
+
m = load_manifest()["skills"]
|
|
153
|
+
claude = skill_dirs_on_disk(".claude/skills")
|
|
154
|
+
agents = skill_dirs_on_disk(".agents/skills")
|
|
155
|
+
problems = []
|
|
156
|
+
for name, entry in m.items():
|
|
157
|
+
if not entry.get("publish"):
|
|
158
|
+
continue
|
|
159
|
+
if "claude" in entry["surfaces"] and name not in claude:
|
|
160
|
+
problems.append(f"{name}: publish:true, surfaces claude, but missing on disk")
|
|
161
|
+
if "codex" in entry["surfaces"] and name not in agents:
|
|
162
|
+
problems.append(f"{name}: publish:true, surfaces codex, but missing on disk")
|
|
163
|
+
self.assertEqual(problems, [], "\n".join(problems))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class EnforcedSkillsSelfContained(unittest.TestCase):
|
|
167
|
+
"""generic & vendored skills carry no `../` reach, all .md files, both trees."""
|
|
168
|
+
|
|
169
|
+
def test_no_parent_refs(self):
|
|
170
|
+
m = load_manifest()["skills"]
|
|
171
|
+
enforced = {n for n, e in m.items() if e["class"] in ENFORCED_CLASSES}
|
|
172
|
+
problems = []
|
|
173
|
+
for tree in SKILL_DIRS:
|
|
174
|
+
for name in sorted(enforced):
|
|
175
|
+
d = REPO_ROOT / tree / name
|
|
176
|
+
if not d.is_dir():
|
|
177
|
+
continue
|
|
178
|
+
for md in sorted(d.rglob("*.md")):
|
|
179
|
+
for ln, src in find_parent_refs(md.read_text(encoding="utf-8")):
|
|
180
|
+
problems.append(f"{md.relative_to(REPO_ROOT)}:{ln}: {src}")
|
|
181
|
+
self.assertEqual(
|
|
182
|
+
problems, [],
|
|
183
|
+
"`../` reach in a generic/vendored skill — replace with a project-root "
|
|
184
|
+
"convention path (e.g. `docs/agents/x.md`) + runtime fallback:\n"
|
|
185
|
+
+ "\n".join(problems))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class LookupContract(unittest.TestCase):
|
|
189
|
+
"""Skills migrated this slice reference project config by a project-root
|
|
190
|
+
convention path (no `../`) AND state a runtime fallback (Codex R2#2)."""
|
|
191
|
+
|
|
192
|
+
def test_migrated_skills_have_convention_path_and_fallback(self):
|
|
193
|
+
for name in ("to-prd", "to-issues", "spec-self-critique"):
|
|
194
|
+
txt = (REPO_ROOT / ".claude/skills" / name / "SKILL.md").read_text(encoding="utf-8")
|
|
195
|
+
self.assertRegex(txt, r"docs/(agents|conventions)/",
|
|
196
|
+
f"{name}: no project-root convention path")
|
|
197
|
+
self.assertIn("/setup-workflow", txt, f"{name}: no fallback")
|
|
198
|
+
# same scanner as the lint (honors the line-scoped exempt marker), not a
|
|
199
|
+
# raw substring — keeps LookupContract consistent with the real check (R2#4)
|
|
200
|
+
self.assertEqual(find_parent_refs(txt), [], f"{name}: still has a parent-dir reach")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class MockRepoLookup(unittest.TestCase):
|
|
204
|
+
"""Runtime-lookup contract against a mock consumer repo. The lookup itself is
|
|
205
|
+
a model convention (no resolver to unit-test), so we assert both branches:
|
|
206
|
+
project file present → resolves from the project root; absent → the skill's
|
|
207
|
+
stated fallback path applies."""
|
|
208
|
+
|
|
209
|
+
def test_present_resolves_absent_falls_back(self):
|
|
210
|
+
import tempfile
|
|
211
|
+
to_prd = (REPO_ROOT / ".claude/skills/to-prd/SKILL.md").read_text(encoding="utf-8")
|
|
212
|
+
with tempfile.TemporaryDirectory() as d:
|
|
213
|
+
target = Path(d) / "docs/agents/board-sync.md"
|
|
214
|
+
self.assertFalse(target.exists()) # absent → fallback branch
|
|
215
|
+
self.assertIn("/setup-workflow", to_prd) # skill states the fallback
|
|
216
|
+
target.parent.mkdir(parents=True)
|
|
217
|
+
target.write_text("x", encoding="utf-8")
|
|
218
|
+
self.assertTrue(target.exists()) # present → resolves from root
|
|
219
|
+
|
|
220
|
+
def test_spec_self_critique_layer_present_absent(self):
|
|
221
|
+
"""spec-self-critique runs with the project layer (full pass) and without
|
|
222
|
+
it (base pass + warning). Text/path contract — the skill names the layer
|
|
223
|
+
path + a fallback; behavior is verified manually in the mock-repo run."""
|
|
224
|
+
import tempfile
|
|
225
|
+
skill = (REPO_ROOT / ".claude/skills/spec-self-critique/SKILL.md").read_text(encoding="utf-8")
|
|
226
|
+
self.assertIn("docs/agents/skills/spec-self-critique.md", skill) # names the layer
|
|
227
|
+
self.assertIn("/setup-workflow", skill) # states the fallback
|
|
228
|
+
with tempfile.TemporaryDirectory() as d:
|
|
229
|
+
target = Path(d) / "docs/agents/skills/spec-self-critique.md"
|
|
230
|
+
self.assertFalse(target.exists()) # absent → base pass + warning
|
|
231
|
+
target.parent.mkdir(parents=True)
|
|
232
|
+
target.write_text("x", encoding="utf-8")
|
|
233
|
+
self.assertTrue(target.exists()) # present → full pass
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class NegativeFixture(unittest.TestCase):
|
|
237
|
+
"""A real `../` in a scanned .md file is caught end-to-end (regression guard)."""
|
|
238
|
+
|
|
239
|
+
def test_parent_ref_in_md_file_is_flagged(self):
|
|
240
|
+
import tempfile
|
|
241
|
+
with tempfile.TemporaryDirectory() as d:
|
|
242
|
+
f = Path(d) / "SKILL.md"
|
|
243
|
+
f.write_text("see [x](../../../docs/y.md)\n", encoding="utf-8")
|
|
244
|
+
self.assertEqual(len(find_parent_refs(f.read_text(encoding="utf-8"))), 1)
|
|
245
|
+
|
|
246
|
+
def test_exempt_line_in_md_file_is_not_flagged(self):
|
|
247
|
+
import tempfile
|
|
248
|
+
with tempfile.TemporaryDirectory() as d:
|
|
249
|
+
f = Path(d) / "SKILL.md"
|
|
250
|
+
f.write_text("anti: [x](../bad) <!-- self-containment-lint: ok -->\n", encoding="utf-8")
|
|
251
|
+
self.assertEqual(find_parent_refs(f.read_text(encoding="utf-8")), [])
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
if __name__ == "__main__":
|
|
255
|
+
unittest.main(verbosity=2)
|