@ikon85/agent-workflow-kit 0.43.0 → 0.44.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/grill-me/SKILL.md +3 -1
- package/.agents/skills/grill-with-docs/SKILL.md +3 -1
- package/.agents/skills/kit-update/SKILL.md +9 -11
- package/.agents/skills/orchestrate-wave/SKILL.md +8 -8
- package/.agents/skills/setup-workflow/SKILL.md +20 -18
- package/.agents/skills/setup-workflow/board-sync.md +4 -1
- package/.agents/skills/setup-workflow/workflow-advisories.md +3 -2
- package/.agents/skills/setup-workflow/worktree-lifecycle.md +36 -31
- package/.agents/skills/to-issues/SKILL.md +43 -0
- package/.agents/skills/wrapup/SKILL.md +98 -54
- package/.claude/skills/grill-me/SKILL.md +3 -1
- package/.claude/skills/grill-me-codex/SKILL.md +3 -1
- package/.claude/skills/grill-with-docs/SKILL.md +3 -1
- package/.claude/skills/grill-with-docs-codex/SKILL.md +3 -1
- package/.claude/skills/kit-update/SKILL.md +9 -11
- package/.claude/skills/orchestrate-wave/SKILL.md +8 -8
- package/.claude/skills/setup-workflow/SKILL.md +20 -18
- package/.claude/skills/setup-workflow/board-sync.md +4 -1
- package/.claude/skills/setup-workflow/workflow-advisories.md +3 -2
- package/.claude/skills/setup-workflow/worktree-lifecycle.md +36 -31
- package/.claude/skills/to-issues/SKILL.md +43 -0
- package/.claude/skills/wrapup/SKILL.md +98 -54
- package/README.md +249 -7
- package/agent-workflow-kit.package.json +42 -42
- package/package.json +16 -12
- package/scripts/marker_lib.py +3 -0
- package/scripts/profile_globs.py +26 -45
- package/scripts/release-delta-guard.mjs +26 -9
- package/scripts/worktree-lifecycle/README.md +57 -160
- package/scripts/worktree-lifecycle/capabilities.json +2 -3
- package/scripts/worktree-lifecycle/classify.py +480 -0
- package/scripts/worktree-lifecycle/cleanup.py +9 -29
- package/scripts/worktree-lifecycle/core.py +68 -1429
- package/scripts/worktree-lifecycle/ignore_seed.py +1 -1
- package/scripts/worktree-lifecycle/profile.py +46 -29
- package/scripts/worktree-lifecycle/setup.py +7 -19
- package/scripts/wrapup-land.py +1292 -545
- package/src/consumer-migrations.json +1 -16
- package/src/lib/bundle.mjs +31 -1
- package/docs/adr/0001-consumer-divergence-policy.md +0 -53
- package/docs/adr/0002-capability-gated-orchestration.md +0 -70
- package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +0 -63
- package/docs/adr/0004-release-intent-is-a-version-tag.md +0 -64
- package/docs/adr/0005-to-issues-is-the-planning-facade.md +0 -42
- package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +0 -95
- package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +0 -88
- package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +0 -84
- package/docs/adr/0009-teardown-authority-is-stateless-repository-classification.md +0 -101
- package/docs/adr/0010-model-roster-replaces-the-optimization-dial.md +0 -87
- package/docs/agents/board-sync.md +0 -74
- package/docs/agents/code-review.md +0 -14
- package/docs/agents/workflow-capabilities.json +0 -28
- package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
- package/docs/research/benchlm-routing-source.md +0 -198
- package/docs/research/consumer-owned-protocol-files.md +0 -238
- package/docs/research/frontend-agent-benchmarks.md +0 -282
- package/docs/research/model-effort-routing-benchmarks.md +0 -261
- package/docs/research/provider-neutral-agent-routing.md +0 -207
- package/docs/research/wave-152-consumer-acceptance.md +0 -98
- package/docs/research/wave-43-script-hook-census.md +0 -128
- package/scripts/build-kit.mjs +0 -117
- package/scripts/build-kit.test.mjs +0 -207
- package/scripts/census/census.test.mjs +0 -252
- package/scripts/census/state.test.mjs +0 -15
- package/scripts/census/transaction.test.mjs +0 -103
- package/scripts/census-contract.test.mjs +0 -227
- package/scripts/check-kit-staleness.mjs +0 -45
- package/scripts/check-kit-staleness.test.mjs +0 -19
- package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -165
- package/scripts/codex-exec.test.mjs +0 -871
- package/scripts/grill-census-wiring-guard.mjs +0 -87
- package/scripts/grill-census-wiring-guard.test.mjs +0 -107
- package/scripts/kit-release.test.mjs +0 -273
- package/scripts/kit-update-pr.test.mjs +0 -151
- package/scripts/lib/audit-refs.mjs +0 -146
- package/scripts/lib/scrub.mjs +0 -113
- package/scripts/lib/scrub.test.mjs +0 -187
- package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
- package/scripts/portability_profile_scan.py +0 -53
- package/scripts/release-delta-guard.test.mjs +0 -174
- package/scripts/release-parity.test.mjs +0 -90
- package/scripts/release-state.test.mjs +0 -360
- package/scripts/test_anchor_table.py +0 -69
- package/scripts/test_board_bootstrap.py +0 -348
- package/scripts/test_board_sync.py +0 -208
- package/scripts/test_board_sync_create_idempotency.py +0 -117
- package/scripts/test_board_sync_wave_title.py +0 -120
- package/scripts/test_census_backstop.py +0 -712
- package/scripts/test_census_forward_contract.py +0 -91
- package/scripts/test_census_update_contract.test.mjs +0 -454
- package/scripts/test_codex_adapter_sync_contract.py +0 -169
- package/scripts/test_dist_kit_smoke.py +0 -220
- package/scripts/test_drift_guard_diagnostics.py +0 -295
- package/scripts/test_issue_claim_contract.py +0 -169
- package/scripts/test_kit_docs_language_census.py +0 -178
- package/scripts/test_marker_lib.py +0 -162
- package/scripts/test_orchestrate_wave_contract.py +0 -381
- package/scripts/test_pr_body_check.py +0 -264
- package/scripts/test_profile_globs.py +0 -280
- package/scripts/test_program_planning_contract.py +0 -133
- package/scripts/test_release_authorization_contract.py +0 -101
- package/scripts/test_render_anchor.py +0 -267
- package/scripts/test_retro_wrapup_contract.py +0 -153
- package/scripts/test_skill_code_review_seed.py +0 -89
- package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
- package/scripts/test_skill_frontmatter_lint.py +0 -234
- package/scripts/test_skill_gh_lint.py +0 -124
- package/scripts/test_skill_language_census.py +0 -254
- package/scripts/test_skill_optional_readiness.py +0 -171
- package/scripts/test_skill_portability_lint.py +0 -866
- package/scripts/test_skill_precommit_template.py +0 -30
- package/scripts/test_skill_publish_audit.py +0 -358
- package/scripts/test_skill_readiness_contract.py +0 -264
- package/scripts/test_skill_readiness_preflight.py +0 -180
- package/scripts/test_skill_required_readiness.py +0 -233
- package/scripts/test_skill_selfcontainment_lint.py +0 -255
- package/scripts/test_skill_setup_workflow_seeds.py +0 -691
- package/scripts/test_skill_stale_name_lint.py +0 -126
- package/scripts/test_skill_surface_refs.py +0 -300
- package/scripts/test_skill_trailing_artifact_lint.py +0 -71
- package/scripts/test_tdd_contract.py +0 -78
- package/scripts/test_worktree_ignore_seed.py +0 -320
- package/scripts/test_worktree_setup_base_guard.py +0 -140
- package/scripts/test_worktree_wrapup_contract.py +0 -1737
- package/scripts/test_wrapup_land.py +0 -428
- package/scripts/worktree-lifecycle/session.py +0 -1857
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Stale-skill-name lint (Welle 26 / Slice 5 / #989).
|
|
3
|
-
|
|
4
|
-
When a skill is renamed (here: `setup-matt-pocock-skills` -> `setup-workflow`),
|
|
5
|
-
every ACTIVE reference must move with it, or consumer skills point at a command
|
|
6
|
-
that no longer exists (Codex R1 #1: `to-issues/SKILL.md:10` still invoked the
|
|
7
|
-
deleted name). This lint fails on any active reference to a retired skill name.
|
|
8
|
-
|
|
9
|
-
ACTIVE reference = a form that would actually misroute: a slash invocation
|
|
10
|
-
(`/<name>`), a frontmatter `name:` value, a `.../skills/<name>` dir/path reference,
|
|
11
|
-
or a manifest JSON key (`"<name>":`).
|
|
12
|
-
|
|
13
|
-
EXPLICITLY NOT a violation (Codex R2 #1): legitimate *attribution prose* that names
|
|
14
|
-
the upstream skill — the provenance upstream-path `engineering/<name>/`, a manifest
|
|
15
|
-
`note`, or a SKILL.md description "adapted from <name>". Those credit the upstream,
|
|
16
|
-
they don't route anywhere. The active-form patterns below match none of them. A
|
|
17
|
-
per-line `stale-name-lint: ok` marker also exempts a single line.
|
|
18
|
-
|
|
19
|
-
Run: python3 scripts/test_skill_stale_name_lint.py
|
|
20
|
-
"""
|
|
21
|
-
import re
|
|
22
|
-
import unittest
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
|
|
25
|
-
REPO = Path(__file__).resolve().parent.parent
|
|
26
|
-
|
|
27
|
-
# Retired skill names that must have zero active references anywhere we scan.
|
|
28
|
-
RETIRED_NAMES = ["setup-matt-pocock-skills"]
|
|
29
|
-
|
|
30
|
-
LINT_OK = "stale-name-lint: ok"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _active_patterns(name):
|
|
34
|
-
"""Forms that would actually misroute to a non-existent skill.
|
|
35
|
-
|
|
36
|
-
Boundary-guarded so neither the provenance path `engineering/<name>/` nor
|
|
37
|
-
attribution prose (`adapted from <name>`, a manifest note) matches.
|
|
38
|
-
"""
|
|
39
|
-
n = re.escape(name)
|
|
40
|
-
return [
|
|
41
|
-
re.compile(rf"(?<![\w/])/{n}(?![\w/-])"), # slash invocation /<name>
|
|
42
|
-
re.compile(rf"name:\s*{n}\b"), # frontmatter name:
|
|
43
|
-
re.compile(rf"skills/{n}\b"), # .claude|.agents/skills/<name>
|
|
44
|
-
re.compile(rf'"{n}"\s*:'), # manifest JSON key
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _scan_files():
|
|
49
|
-
"""Files where an active reference would actually mislead a consumer."""
|
|
50
|
-
files = []
|
|
51
|
-
for root in ("CLAUDE.md", "AGENTS.md"):
|
|
52
|
-
p = REPO / root
|
|
53
|
-
if p.is_file():
|
|
54
|
-
files.append(p)
|
|
55
|
-
for d in (".claude/skills", ".agents/skills", "docs"):
|
|
56
|
-
base = REPO / d
|
|
57
|
-
if base.is_dir():
|
|
58
|
-
files.extend(base.rglob("*.md"))
|
|
59
|
-
manifest = REPO / ".claude/skills/skill-manifest.json"
|
|
60
|
-
if manifest.is_file():
|
|
61
|
-
files.append(manifest)
|
|
62
|
-
return files
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _violations_for(name):
|
|
66
|
-
"""Return list of (file, lineno, line) with an active reference to `name`."""
|
|
67
|
-
patterns = _active_patterns(name)
|
|
68
|
-
out = []
|
|
69
|
-
for f in _scan_files():
|
|
70
|
-
try:
|
|
71
|
-
text = f.read_text(encoding="utf-8")
|
|
72
|
-
except (UnicodeDecodeError, OSError):
|
|
73
|
-
continue
|
|
74
|
-
for i, line in enumerate(text.splitlines(), start=1):
|
|
75
|
-
if name not in line or LINT_OK in line:
|
|
76
|
-
continue
|
|
77
|
-
if any(p.search(line) for p in patterns):
|
|
78
|
-
out.append((f.relative_to(REPO), i, line.strip()))
|
|
79
|
-
return out
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
class StaleSkillName(unittest.TestCase):
|
|
83
|
-
def test_no_active_references_to_retired_skill_names(self):
|
|
84
|
-
all_v = []
|
|
85
|
-
for name in RETIRED_NAMES:
|
|
86
|
-
all_v.extend(_violations_for(name))
|
|
87
|
-
msg = "\n".join(f" {f}:{n}: {ln}" for f, n, ln in all_v)
|
|
88
|
-
self.assertEqual(
|
|
89
|
-
all_v,
|
|
90
|
-
[],
|
|
91
|
-
f"\nActive references to retired skill name(s) found "
|
|
92
|
-
f"(rename them or add `{LINT_OK}`):\n{msg}",
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
def test_attribution_prose_is_not_flagged(self):
|
|
96
|
-
"""Credit forms that name the upstream must NOT be treated as violations."""
|
|
97
|
-
for name in RETIRED_NAMES:
|
|
98
|
-
patterns = _active_patterns(name)
|
|
99
|
-
for benign in (
|
|
100
|
-
f"| `setup-workflow` | generic | `engineering/{name}/` |",
|
|
101
|
-
f"adapted from Matt Pocock's `{name}` (MIT)",
|
|
102
|
-
f'"note": "evolved from {name} (Welle 26)"',
|
|
103
|
-
):
|
|
104
|
-
self.assertFalse(
|
|
105
|
-
any(p.search(benign) for p in patterns),
|
|
106
|
-
f"attribution prose should be exempt: {benign!r}",
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
def test_active_forms_are_flagged(self):
|
|
110
|
-
"""The misrouting forms must be caught."""
|
|
111
|
-
for name in RETIRED_NAMES:
|
|
112
|
-
patterns = _active_patterns(name)
|
|
113
|
-
for active in (
|
|
114
|
-
f"run `/{name}` if not",
|
|
115
|
-
f"name: {name}",
|
|
116
|
-
f".claude/skills/{name}/SKILL.md",
|
|
117
|
-
f'"{name}": {{ "class": "vendored" }}',
|
|
118
|
-
):
|
|
119
|
-
self.assertTrue(
|
|
120
|
-
any(p.search(active) for p in patterns),
|
|
121
|
-
f"active form should be flagged: {active!r}",
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if __name__ == "__main__":
|
|
126
|
-
unittest.main(verbosity=2)
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Skill surface-reference lint (Welle 49 / Slice 2 / #1873).
|
|
3
|
-
|
|
4
|
-
Every `/skill-name` or backtick `` `skill-name` `` a SKILL.md names must
|
|
5
|
-
resolve to a skill that exists as a directory on the SAME surface the
|
|
6
|
-
referencing file lives on. `skill-manifest.json`'s `surfaces` array is the
|
|
7
|
-
SSOT for which trees (`.claude/skills`, `.agents/skills`) host a skill.
|
|
8
|
-
|
|
9
|
-
Two independent checks:
|
|
10
|
-
|
|
11
|
-
MISMATCH — an exact backtick reference to a REAL manifest skill NOT hosted
|
|
12
|
-
on the referencing file's surface (a Claude-only skill named from a
|
|
13
|
-
Codex-surface file, or vice-versa). This is the class behind the
|
|
14
|
-
Codex-escalation dangling-target finding and the `/code-review` dangling:
|
|
15
|
-
a PUBLISHED skill can pass the existing publish audit
|
|
16
|
-
(`auditSkillNameRefs` in `scripts/lib/audit-refs.mjs`, which only checks
|
|
17
|
-
*publish status*) while still dangling on ONE surface — that audit has no
|
|
18
|
-
concept of `surfaces` at all.
|
|
19
|
-
|
|
20
|
-
NONEXISTENT — a `/name` slash-token styled as a catalog entry (a list item
|
|
21
|
-
opening with an optionally-bold backtick slash-token, the convention every
|
|
22
|
-
genuine skill listing in this repo uses, e.g. `- **`/tdd`** — ...`) that
|
|
23
|
-
names NO manifest skill anywhere — a typo'd or dead name in a router list.
|
|
24
|
-
|
|
25
|
-
`class: "adapter"` skills (e.g. `codex-adapter-sync`) are exempt from
|
|
26
|
-
MISMATCH: their purpose is to be invoked FROM the opposite surface —
|
|
27
|
-
Claude-side prose runs the Codex-mirror-sync tool that itself lives only
|
|
28
|
-
under `.agents/skills` (manifest note: "published skills reference it as the
|
|
29
|
-
Claude→Codex mirror step"). That's the intended direction, not a dangler.
|
|
30
|
-
|
|
31
|
-
Deliberate exceptions: an inline marker on the line (`Claude Code only`, or
|
|
32
|
-
the generic `skill-surface-lint: ok`), or `PROSE_ALLOWLIST` below for
|
|
33
|
-
skill-system PROSE in files this slice doesn't touch (each entry carries its
|
|
34
|
-
own reason — an explicit, auditable allowlist, not a blanket exemption).
|
|
35
|
-
|
|
36
|
-
Run: python3 scripts/test_skill_surface_refs.py
|
|
37
|
-
"""
|
|
38
|
-
import json
|
|
39
|
-
import re
|
|
40
|
-
import unittest
|
|
41
|
-
from pathlib import Path
|
|
42
|
-
|
|
43
|
-
from test_skill_portability_lint import load_manifest, SKILL_DIRS
|
|
44
|
-
|
|
45
|
-
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
46
|
-
TREE_SURFACE = {".claude/skills": "claude", ".agents/skills": "codex"}
|
|
47
|
-
|
|
48
|
-
EXEMPT = "skill-surface-lint: ok"
|
|
49
|
-
CLAUDE_ONLY_MARKER = "Claude Code only"
|
|
50
|
-
|
|
51
|
-
# Slash-tokens a catalog bullet can legitimately name without matching any
|
|
52
|
-
# manifest skill: either a real command OUTSIDE this repo's manifest (a
|
|
53
|
-
# Claude Code CLI built-in, or — like `handoff` — a skill living in the
|
|
54
|
-
# user's global `~/.claude/skills`, never a repo skill directory; ask-matt
|
|
55
|
-
# names both, formatted exactly like a real catalog entry, `/compact` even
|
|
56
|
-
# self-annotates "(built-in)"), or a documented TEMPLATE PLACEHOLDER in
|
|
57
|
-
# third-party (vendored, not ours to rewrite) skill prose — `impeccable`'s
|
|
58
|
-
# recommended-actions template repeats `/command-name` as a fill-in-the-blank,
|
|
59
|
-
# not a specific skill.
|
|
60
|
-
EXTERNAL_COMMANDS = {"compact", "handoff", "command-name"}
|
|
61
|
-
|
|
62
|
-
# Deliberate skill-system PROSE that NAMES a skill without inviting an
|
|
63
|
-
# operator on THIS surface to invoke it — not a routing reference. Each is a
|
|
64
|
-
# file this slice does not edit (see #1873 scope); (relpath, distinctive
|
|
65
|
-
# line substring, reason).
|
|
66
|
-
PROSE_ALLOWLIST = [
|
|
67
|
-
(".agents/skills/ask-matt/SKILL.md",
|
|
68
|
-
"Folder↔upstream-name note:",
|
|
69
|
-
"identity blockquote names its own upstream-name mapping (Matt "
|
|
70
|
-
"Pocock's write-a-skill == upstream writing-great-skills), not a "
|
|
71
|
-
"routing invocation"),
|
|
72
|
-
(".agents/skills/sync-upstream-skills/SKILL.md",
|
|
73
|
-
"write-a-skill` = upstream `writing-great-skills`",
|
|
74
|
-
"folder↔upstream naming-convention example, not an invocation"),
|
|
75
|
-
(".agents/skills/codex-adapter-sync/SKILL.md",
|
|
76
|
-
"grill-with-docs-codex`, `grill-me-codex`, Chase AI's cross-model",
|
|
77
|
-
"rule-doc prose naming the escalation-target-rewrite pattern by "
|
|
78
|
-
"example, describing the very class this lint catches"),
|
|
79
|
-
]
|
|
80
|
-
|
|
81
|
-
# A markdown list item that OPENS with an (optionally bold) backtick
|
|
82
|
-
# slash-token — the sole convention every genuine skill-catalog entry in
|
|
83
|
-
# this repo uses (`- **`/tdd`** — ...`). Deliberately excludes inline/prose
|
|
84
|
-
# slash mentions (route examples, `/tmp`, upstream-repo slugs) which never
|
|
85
|
-
# open a bullet this way.
|
|
86
|
-
BULLET_ENTRY = re.compile(r"^\s*(?:[-*]|\d+\.)\s+\*{0,2}`([^`\n]+)`")
|
|
87
|
-
NAME_SHAPE = re.compile(r"^(/?)([a-z][a-z0-9]*(?:-[a-z0-9]+)*)$")
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def escape_name(name: str) -> str:
|
|
91
|
-
return re.escape(name)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def name_pattern(name: str) -> re.Pattern:
|
|
95
|
-
"""A backtick-wrapped exact reference (`` `/name` `` or `` `name` ``) OR a
|
|
96
|
-
boundary-guarded bare slash-token — mirrors the proven construction in
|
|
97
|
-
`scripts/lib/audit-refs.mjs`'s `auditSkillNameRefs` (same repo, same
|
|
98
|
-
problem shape: a name match that ignores doc paths / repo slugs, since
|
|
99
|
-
`owner/name` and `skills/name` never satisfy either alternative — the
|
|
100
|
-
char immediately before the match is excluded by the boundary guard)."""
|
|
101
|
-
e = escape_name(name)
|
|
102
|
-
return re.compile(rf"`/?{e}`|(?<![\w/-])/{e}(?![\w-])")
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def is_allowlisted(relpath: str, line: str) -> bool:
|
|
106
|
-
return any(relpath == path and substr in line
|
|
107
|
-
for path, substr, _reason in PROSE_ALLOWLIST)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def find_mismatches(surface: str, relpath: str, text: str, manifest: dict) -> list[str]:
|
|
111
|
-
"""Lines on `surface` naming a real manifest skill not hosted there."""
|
|
112
|
-
problems = []
|
|
113
|
-
for lineno, line in enumerate(text.splitlines(), 1):
|
|
114
|
-
if EXEMPT in line or CLAUDE_ONLY_MARKER in line or is_allowlisted(relpath, line):
|
|
115
|
-
continue
|
|
116
|
-
for name, entry in manifest.items():
|
|
117
|
-
if entry.get("class") == "adapter":
|
|
118
|
-
continue
|
|
119
|
-
if surface in entry.get("surfaces", []):
|
|
120
|
-
continue
|
|
121
|
-
if name_pattern(name).search(line):
|
|
122
|
-
problems.append(
|
|
123
|
-
f"{relpath}:{lineno}: `{name}` not hosted on '{surface}' "
|
|
124
|
-
f"(hosted={entry.get('surfaces', [])}): {line.strip()}")
|
|
125
|
-
return problems
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def find_nonexistent(relpath: str, text: str, manifest: dict) -> list[str]:
|
|
129
|
-
"""Catalog-style bullets naming a slash-token that matches NO manifest
|
|
130
|
-
skill anywhere and is not a known external command."""
|
|
131
|
-
problems = []
|
|
132
|
-
for lineno, line in enumerate(text.splitlines(), 1):
|
|
133
|
-
if EXEMPT in line:
|
|
134
|
-
continue
|
|
135
|
-
m = BULLET_ENTRY.match(line)
|
|
136
|
-
if not m:
|
|
137
|
-
continue
|
|
138
|
-
shape = NAME_SHAPE.match(m.group(1))
|
|
139
|
-
if not shape or not shape.group(1):
|
|
140
|
-
continue
|
|
141
|
-
name = shape.group(2)
|
|
142
|
-
if name in manifest or name in EXTERNAL_COMMANDS:
|
|
143
|
-
continue
|
|
144
|
-
problems.append(f"{relpath}:{lineno}: `/{name}` names no skill anywhere: {line.strip()}")
|
|
145
|
-
return problems
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def iter_skill_markdown():
|
|
149
|
-
"""Yield (surface, relpath-as-posix-string, text) for every distributed
|
|
150
|
-
*.md under every skill directory on both trees."""
|
|
151
|
-
for tree in SKILL_DIRS:
|
|
152
|
-
surface = TREE_SURFACE[tree]
|
|
153
|
-
base = REPO_ROOT / tree
|
|
154
|
-
if not base.is_dir():
|
|
155
|
-
continue
|
|
156
|
-
for skill_dir in sorted(p for p in base.iterdir() if p.is_dir()):
|
|
157
|
-
for md in sorted(skill_dir.rglob("*.md")):
|
|
158
|
-
relpath = md.relative_to(REPO_ROOT).as_posix()
|
|
159
|
-
yield surface, relpath, md.read_text(encoding="utf-8")
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
class PatternDetectorFixtures(unittest.TestCase):
|
|
163
|
-
"""Fixture (a)+(c): a manufactured Codex-surface skill naming a
|
|
164
|
-
Claude-only manifest skill is red unmarked, green once marked."""
|
|
165
|
-
|
|
166
|
-
MANIFEST = {
|
|
167
|
-
"claude-only-skill": {"class": "vendored", "surfaces": ["claude"]},
|
|
168
|
-
"dual-skill": {"class": "generic", "surfaces": ["claude", "codex"]},
|
|
169
|
-
"the-adapter": {"class": "adapter", "surfaces": ["codex"]},
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
def test_unmarked_claude_only_ref_on_codex_surface_is_red(self):
|
|
173
|
-
text = "- **`/claude-only-skill`** — does a thing."
|
|
174
|
-
problems = find_mismatches("codex", "fixture/SKILL.md", text, self.MANIFEST)
|
|
175
|
-
self.assertEqual(len(problems), 1)
|
|
176
|
-
self.assertIn("claude-only-skill", problems[0])
|
|
177
|
-
|
|
178
|
-
def test_marked_claude_only_ref_is_green(self):
|
|
179
|
-
text = "- **`/claude-only-skill`** (Claude Code only) — does a thing."
|
|
180
|
-
self.assertEqual(find_mismatches("codex", "fixture/SKILL.md", text, self.MANIFEST), [])
|
|
181
|
-
|
|
182
|
-
def test_dual_surface_ref_is_always_green(self):
|
|
183
|
-
text = "- **`/dual-skill`** — does a thing."
|
|
184
|
-
self.assertEqual(find_mismatches("codex", "fixture/SKILL.md", text, self.MANIFEST), [])
|
|
185
|
-
|
|
186
|
-
def test_adapter_class_target_is_exempt_both_directions(self):
|
|
187
|
-
text = "run **`/the-adapter`** after editing a skill."
|
|
188
|
-
self.assertEqual(find_mismatches("claude", "fixture/SKILL.md", text, self.MANIFEST), [])
|
|
189
|
-
|
|
190
|
-
def test_doc_path_and_repo_slug_are_not_matched(self):
|
|
191
|
-
text = ("see `docs/agents/skills/claude-only-skill.md` or the upstream "
|
|
192
|
-
"repo `someorg/claude-only-skill`")
|
|
193
|
-
self.assertEqual(find_mismatches("codex", "fixture/SKILL.md", text, self.MANIFEST), [])
|
|
194
|
-
|
|
195
|
-
def test_generic_exempt_marker_is_line_scoped(self):
|
|
196
|
-
text = "- **`/claude-only-skill`** <!-- skill-surface-lint: ok -->"
|
|
197
|
-
self.assertEqual(find_mismatches("codex", "fixture/SKILL.md", text, self.MANIFEST), [])
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
class NonexistentFixtures(unittest.TestCase):
|
|
201
|
-
"""Fixture (b): a catalog bullet naming a skill that exists nowhere."""
|
|
202
|
-
|
|
203
|
-
MANIFEST = {"tdd": {"class": "vendored", "surfaces": ["claude", "codex"]}}
|
|
204
|
-
|
|
205
|
-
def test_wholly_nonexistent_skill_bullet_is_red(self):
|
|
206
|
-
text = "- **`/frobnicate-widget`** — does a thing that never existed."
|
|
207
|
-
problems = find_nonexistent("fixture/SKILL.md", text, self.MANIFEST)
|
|
208
|
-
self.assertEqual(len(problems), 1)
|
|
209
|
-
self.assertIn("frobnicate-widget", problems[0])
|
|
210
|
-
|
|
211
|
-
def test_known_skill_bullet_is_green(self):
|
|
212
|
-
text = "- **`/tdd`** — test-driven development."
|
|
213
|
-
self.assertEqual(find_nonexistent("fixture/SKILL.md", text, self.MANIFEST), [])
|
|
214
|
-
|
|
215
|
-
def test_external_builtin_bullet_is_green(self):
|
|
216
|
-
text = "- **`/handoff`** — compacts the conversation."
|
|
217
|
-
self.assertEqual(find_nonexistent("fixture/SKILL.md", text, self.MANIFEST), [])
|
|
218
|
-
|
|
219
|
-
def test_inline_prose_slash_mention_is_not_a_catalog_entry(self):
|
|
220
|
-
text = "falling back to `/tmp` when $TMPDIR is unset."
|
|
221
|
-
self.assertEqual(find_nonexistent("fixture/SKILL.md", text, self.MANIFEST), [])
|
|
222
|
-
|
|
223
|
-
def test_route_example_list_is_not_flagged(self):
|
|
224
|
-
text = "- Product signals: `/app/*`, `/dashboard`, `/settings`, forms."
|
|
225
|
-
self.assertEqual(find_nonexistent("fixture/SKILL.md", text, self.MANIFEST), [])
|
|
226
|
-
|
|
227
|
-
def test_exempt_marker_line_scoped(self):
|
|
228
|
-
text = "- **`/frobnicate-widget`** <!-- skill-surface-lint: ok -->"
|
|
229
|
-
self.assertEqual(find_nonexistent("fixture/SKILL.md", text, self.MANIFEST), [])
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
class AllowlistFixtures(unittest.TestCase):
|
|
233
|
-
"""Fixture (d): the deliberate skill-system-prose exceptions stay green."""
|
|
234
|
-
|
|
235
|
-
MANIFEST = {"write-a-skill": {"class": "vendored", "surfaces": ["claude"]}}
|
|
236
|
-
|
|
237
|
-
def test_ask_matt_identity_blockquote_is_allowlisted(self):
|
|
238
|
-
text = ("> Homage. Folder↔upstream-name note: `/diagnose` = upstream "
|
|
239
|
-
"`diagnosing-bugs`, `/write-a-skill` = upstream `writing-great-skills`.")
|
|
240
|
-
problems = find_mismatches(
|
|
241
|
-
"codex", ".agents/skills/ask-matt/SKILL.md", text, self.MANIFEST)
|
|
242
|
-
self.assertEqual(problems, [])
|
|
243
|
-
|
|
244
|
-
def test_sync_upstream_skills_naming_note_is_allowlisted(self):
|
|
245
|
-
text = ("e.g. `diagnose` = upstream `diagnosing-bugs`, "
|
|
246
|
-
"`write-a-skill` = upstream `writing-great-skills`")
|
|
247
|
-
problems = find_mismatches(
|
|
248
|
-
"codex", ".agents/skills/sync-upstream-skills/SKILL.md", text, self.MANIFEST)
|
|
249
|
-
self.assertEqual(problems, [])
|
|
250
|
-
|
|
251
|
-
def test_codex_adapter_sync_rule_doc_example_is_allowlisted(self):
|
|
252
|
-
text = ("has no `.agents` mirror. Any Codex-side reference to that skill "
|
|
253
|
-
"name is a dangling target (e.g. `grill-with-docs-codex`, "
|
|
254
|
-
"`grill-me-codex`, Chase AI's cross-model Act-2 variants).")
|
|
255
|
-
manifest = {"grill-with-docs-codex": {"class": "vendored", "surfaces": ["claude"]},
|
|
256
|
-
"grill-me-codex": {"class": "vendored", "surfaces": ["claude"]}}
|
|
257
|
-
problems = find_mismatches(
|
|
258
|
-
"codex", ".agents/skills/codex-adapter-sync/SKILL.md", text, manifest)
|
|
259
|
-
self.assertEqual(problems, [])
|
|
260
|
-
|
|
261
|
-
def test_allowlist_does_not_leak_to_other_files(self):
|
|
262
|
-
"""The allowlist is (path, substring)-scoped — the SAME prose in a
|
|
263
|
-
DIFFERENT file must still be caught (regression guard)."""
|
|
264
|
-
text = ("e.g. `diagnose` = upstream `diagnosing-bugs`, "
|
|
265
|
-
"`write-a-skill` = upstream `writing-great-skills`")
|
|
266
|
-
problems = find_mismatches("codex", "fixture/other-skill/SKILL.md", text, self.MANIFEST)
|
|
267
|
-
self.assertEqual(len(problems), 1)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
class RealRepoSurfaceRefs(unittest.TestCase):
|
|
271
|
-
"""The actual repo, end to end: every skill-name reference in every
|
|
272
|
-
SKILL.md resolves on its own surface (AC: green on HEAD)."""
|
|
273
|
-
|
|
274
|
-
def test_no_cross_surface_dangling_references(self):
|
|
275
|
-
manifest = load_manifest()["skills"]
|
|
276
|
-
problems = []
|
|
277
|
-
for surface, relpath, text in iter_skill_markdown():
|
|
278
|
-
problems.extend(find_mismatches(surface, relpath, text, manifest))
|
|
279
|
-
self.assertEqual(
|
|
280
|
-
problems, [],
|
|
281
|
-
"skill reference dangles on its own surface — either the target "
|
|
282
|
-
"doesn't ship there (mark `(Claude Code only)` / port a Codex "
|
|
283
|
-
"mirror) or this is deliberate prose (add a PROSE_ALLOWLIST "
|
|
284
|
-
"entry with a reason):\n" + "\n".join(problems))
|
|
285
|
-
|
|
286
|
-
def test_no_wholly_nonexistent_catalog_entries(self):
|
|
287
|
-
manifest = load_manifest()["skills"]
|
|
288
|
-
problems = []
|
|
289
|
-
for _surface, relpath, text in iter_skill_markdown():
|
|
290
|
-
problems.extend(find_nonexistent(relpath, text, manifest))
|
|
291
|
-
self.assertEqual(
|
|
292
|
-
problems, [],
|
|
293
|
-
"catalog bullet names a skill that exists on no surface — fix "
|
|
294
|
-
"the name, remove the stale entry, or add it to "
|
|
295
|
-
"EXTERNAL_COMMANDS if it's a real command outside this repo's "
|
|
296
|
-
"manifest:\n" + "\n".join(problems))
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if __name__ == "__main__":
|
|
300
|
-
unittest.main(verbosity=2)
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Trailing paste-artifact lint (#1991 retro).
|
|
3
|
-
|
|
4
|
-
Skill bodies and agent docs are often assembled by pasting rendered tool output.
|
|
5
|
-
That paste can drag a literal harness closing tag along — a stray `</content>`
|
|
6
|
-
or `</invoke>` line at the end of the file. It is invisible in most renders but
|
|
7
|
-
ships verbatim (one such line sat in a publish:true skill and would have shipped
|
|
8
|
-
in the kit). The 2026-07-09 census found 8 of them across both skill surfaces
|
|
9
|
-
and docs/agents.
|
|
10
|
-
|
|
11
|
-
This lint fails when the LAST non-empty line of a scanned file is a known paste
|
|
12
|
-
artifact. Deliberately trailing-only: the same token inside a fenced code block
|
|
13
|
-
mid-file can be a legitimate example, but as the file's final line it is always
|
|
14
|
-
garbage. Extend ARTIFACTS only with verified occurrences, not speculation.
|
|
15
|
-
|
|
16
|
-
Run: python3 scripts/test_skill_trailing_artifact_lint.py
|
|
17
|
-
"""
|
|
18
|
-
import unittest
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
22
|
-
|
|
23
|
-
SCAN_DIRS = [".claude/skills", ".agents/skills", "docs/agents"]
|
|
24
|
-
|
|
25
|
-
# Verified paste artifacts (both seen in the 2026-07-09 census). Extend on sight.
|
|
26
|
-
ARTIFACTS = {"</content>", "</invoke>"}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _last_nonempty_line(path):
|
|
30
|
-
last = None
|
|
31
|
-
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
32
|
-
if line.strip():
|
|
33
|
-
last = line.strip()
|
|
34
|
-
return last
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _scan():
|
|
38
|
-
hits = []
|
|
39
|
-
for d in SCAN_DIRS:
|
|
40
|
-
base = ROOT / d
|
|
41
|
-
if not base.is_dir():
|
|
42
|
-
continue
|
|
43
|
-
for p in sorted(base.rglob("*.md")):
|
|
44
|
-
if _last_nonempty_line(p) in ARTIFACTS:
|
|
45
|
-
hits.append(str(p.relative_to(ROOT)))
|
|
46
|
-
return hits
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class TrailingPasteArtifact(unittest.TestCase):
|
|
50
|
-
def test_no_file_ends_on_a_paste_artifact(self):
|
|
51
|
-
hits = _scan()
|
|
52
|
-
self.assertEqual(
|
|
53
|
-
hits, [],
|
|
54
|
-
"file(s) end on a literal harness closing tag — a paste artifact "
|
|
55
|
-
f"from assembling the body out of rendered tool output; delete the "
|
|
56
|
-
f"trailing line(s): {hits}",
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
def test_detector_fires_on_synthetic_artifact(self):
|
|
60
|
-
"""Negative proof: the detector actually recognizes an artifact tail."""
|
|
61
|
-
import tempfile
|
|
62
|
-
|
|
63
|
-
with tempfile.TemporaryDirectory() as td:
|
|
64
|
-
p = Path(td) / "x.md"
|
|
65
|
-
p.write_text("# Title\n\nbody\n</content>\n\n")
|
|
66
|
-
self.assertEqual(_last_nonempty_line(p), "</content>")
|
|
67
|
-
self.assertIn(_last_nonempty_line(p), ARTIFACTS)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if __name__ == "__main__":
|
|
71
|
-
unittest.main(verbosity=2)
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Contract tests for existing-test-first TDD without weakening RED-first."""
|
|
3
|
-
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
import re
|
|
6
|
-
import unittest
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
ROOT = Path(__file__).resolve().parents[1]
|
|
10
|
-
SURFACES = (
|
|
11
|
-
ROOT / ".claude" / "skills" / "tdd" / "SKILL.md",
|
|
12
|
-
ROOT / ".agents" / "skills" / "tdd" / "SKILL.md",
|
|
13
|
-
)
|
|
14
|
-
DECISIONS = ("REUSE", "EXTEND", "REPLACE", "NEW", "RETIRE", "NO-NEW-TEST")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class TddContractTest(unittest.TestCase):
|
|
18
|
-
def texts(self):
|
|
19
|
-
return {path: path.read_text(encoding="utf-8") for path in SURFACES}
|
|
20
|
-
|
|
21
|
-
def test_each_behavior_gets_exactly_one_existing_test_first_decision(self):
|
|
22
|
-
for path, text in self.texts().items():
|
|
23
|
-
with self.subTest(surface=path.relative_to(ROOT)):
|
|
24
|
-
self.assertIn("### 2. Existing-Test-First Decision", text)
|
|
25
|
-
self.assertIn("For each planned behavior, record exactly one", text)
|
|
26
|
-
for decision in DECISIONS:
|
|
27
|
-
self.assertEqual(
|
|
28
|
-
len(re.findall(rf"^- \*\*{re.escape(decision)}\*\*", text, re.MULTILINE)),
|
|
29
|
-
1,
|
|
30
|
-
f"{decision} must have exactly one canonical definition",
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
def test_new_files_require_rejecting_the_nearest_owner_with_a_reason(self):
|
|
34
|
-
for path, text in self.texts().items():
|
|
35
|
-
with self.subTest(surface=path.relative_to(ROOT)):
|
|
36
|
-
self.assertIn("nearest existing owner", text)
|
|
37
|
-
self.assertIn("rejected with a reason", text)
|
|
38
|
-
|
|
39
|
-
def test_red_first_is_unconditional_for_executable_behavior_and_bug_fixes(self):
|
|
40
|
-
for path, text in self.texts().items():
|
|
41
|
-
with self.subTest(surface=path.relative_to(ROOT)):
|
|
42
|
-
self.assertIn(
|
|
43
|
-
"Every executable new behavior and bug fix must begin with a failing assertion",
|
|
44
|
-
text,
|
|
45
|
-
)
|
|
46
|
-
self.assertIn("REUSE and NO-NEW-TEST never bypass this RED-first invariant", text)
|
|
47
|
-
self.assertRegex(
|
|
48
|
-
text,
|
|
49
|
-
r"an already-green\s+assertion does not prove the requested change",
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
def test_retirement_keeps_negative_tests_only_for_durable_absence(self):
|
|
53
|
-
for path, text in self.texts().items():
|
|
54
|
-
with self.subTest(surface=path.relative_to(ROOT)):
|
|
55
|
-
self.assertIn("delete the tests that specified the retired behavior", text)
|
|
56
|
-
self.assertIn("only when absence itself is a durable", text)
|
|
57
|
-
|
|
58
|
-
def test_worked_matrix_and_counted_handoff_cover_all_decisions(self):
|
|
59
|
-
for path, text in self.texts().items():
|
|
60
|
-
with self.subTest(surface=path.relative_to(ROOT)):
|
|
61
|
-
matrix = re.search(
|
|
62
|
-
r"### Worked decision matrix\n(?P<body>.*?)(?=\n### |\n## )",
|
|
63
|
-
text,
|
|
64
|
-
re.DOTALL,
|
|
65
|
-
)
|
|
66
|
-
self.assertIsNotNone(matrix)
|
|
67
|
-
body = matrix.group("body")
|
|
68
|
-
for decision in DECISIONS:
|
|
69
|
-
self.assertRegex(body, rf"\|\s*{re.escape(decision)}\s*\|")
|
|
70
|
-
self.assertIn("list every behavior and its one decision", text)
|
|
71
|
-
self.assertIn(
|
|
72
|
-
"Reused X · Extended Y · New Z · Replaced/retired W",
|
|
73
|
-
text,
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if __name__ == "__main__":
|
|
78
|
-
unittest.main()
|