@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.
Files changed (126) hide show
  1. package/.agents/skills/grill-me/SKILL.md +3 -1
  2. package/.agents/skills/grill-with-docs/SKILL.md +3 -1
  3. package/.agents/skills/kit-update/SKILL.md +9 -11
  4. package/.agents/skills/orchestrate-wave/SKILL.md +8 -8
  5. package/.agents/skills/setup-workflow/SKILL.md +20 -18
  6. package/.agents/skills/setup-workflow/board-sync.md +4 -1
  7. package/.agents/skills/setup-workflow/workflow-advisories.md +3 -2
  8. package/.agents/skills/setup-workflow/worktree-lifecycle.md +36 -31
  9. package/.agents/skills/to-issues/SKILL.md +43 -0
  10. package/.agents/skills/wrapup/SKILL.md +98 -54
  11. package/.claude/skills/grill-me/SKILL.md +3 -1
  12. package/.claude/skills/grill-me-codex/SKILL.md +3 -1
  13. package/.claude/skills/grill-with-docs/SKILL.md +3 -1
  14. package/.claude/skills/grill-with-docs-codex/SKILL.md +3 -1
  15. package/.claude/skills/kit-update/SKILL.md +9 -11
  16. package/.claude/skills/orchestrate-wave/SKILL.md +8 -8
  17. package/.claude/skills/setup-workflow/SKILL.md +20 -18
  18. package/.claude/skills/setup-workflow/board-sync.md +4 -1
  19. package/.claude/skills/setup-workflow/workflow-advisories.md +3 -2
  20. package/.claude/skills/setup-workflow/worktree-lifecycle.md +36 -31
  21. package/.claude/skills/to-issues/SKILL.md +43 -0
  22. package/.claude/skills/wrapup/SKILL.md +98 -54
  23. package/README.md +249 -7
  24. package/agent-workflow-kit.package.json +42 -42
  25. package/package.json +16 -12
  26. package/scripts/marker_lib.py +3 -0
  27. package/scripts/profile_globs.py +26 -45
  28. package/scripts/release-delta-guard.mjs +26 -9
  29. package/scripts/worktree-lifecycle/README.md +57 -160
  30. package/scripts/worktree-lifecycle/capabilities.json +2 -3
  31. package/scripts/worktree-lifecycle/classify.py +480 -0
  32. package/scripts/worktree-lifecycle/cleanup.py +9 -29
  33. package/scripts/worktree-lifecycle/core.py +68 -1429
  34. package/scripts/worktree-lifecycle/ignore_seed.py +1 -1
  35. package/scripts/worktree-lifecycle/profile.py +46 -29
  36. package/scripts/worktree-lifecycle/setup.py +7 -19
  37. package/scripts/wrapup-land.py +1292 -545
  38. package/src/consumer-migrations.json +1 -16
  39. package/src/lib/bundle.mjs +31 -1
  40. package/docs/adr/0001-consumer-divergence-policy.md +0 -53
  41. package/docs/adr/0002-capability-gated-orchestration.md +0 -70
  42. package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +0 -63
  43. package/docs/adr/0004-release-intent-is-a-version-tag.md +0 -64
  44. package/docs/adr/0005-to-issues-is-the-planning-facade.md +0 -42
  45. package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +0 -95
  46. package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +0 -88
  47. package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +0 -84
  48. package/docs/adr/0009-teardown-authority-is-stateless-repository-classification.md +0 -101
  49. package/docs/adr/0010-model-roster-replaces-the-optimization-dial.md +0 -87
  50. package/docs/agents/board-sync.md +0 -74
  51. package/docs/agents/code-review.md +0 -14
  52. package/docs/agents/workflow-capabilities.json +0 -28
  53. package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
  54. package/docs/research/benchlm-routing-source.md +0 -198
  55. package/docs/research/consumer-owned-protocol-files.md +0 -238
  56. package/docs/research/frontend-agent-benchmarks.md +0 -282
  57. package/docs/research/model-effort-routing-benchmarks.md +0 -261
  58. package/docs/research/provider-neutral-agent-routing.md +0 -207
  59. package/docs/research/wave-152-consumer-acceptance.md +0 -98
  60. package/docs/research/wave-43-script-hook-census.md +0 -128
  61. package/scripts/build-kit.mjs +0 -117
  62. package/scripts/build-kit.test.mjs +0 -207
  63. package/scripts/census/census.test.mjs +0 -252
  64. package/scripts/census/state.test.mjs +0 -15
  65. package/scripts/census/transaction.test.mjs +0 -103
  66. package/scripts/census-contract.test.mjs +0 -227
  67. package/scripts/check-kit-staleness.mjs +0 -45
  68. package/scripts/check-kit-staleness.test.mjs +0 -19
  69. package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -165
  70. package/scripts/codex-exec.test.mjs +0 -871
  71. package/scripts/grill-census-wiring-guard.mjs +0 -87
  72. package/scripts/grill-census-wiring-guard.test.mjs +0 -107
  73. package/scripts/kit-release.test.mjs +0 -273
  74. package/scripts/kit-update-pr.test.mjs +0 -151
  75. package/scripts/lib/audit-refs.mjs +0 -146
  76. package/scripts/lib/scrub.mjs +0 -113
  77. package/scripts/lib/scrub.test.mjs +0 -187
  78. package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
  79. package/scripts/portability_profile_scan.py +0 -53
  80. package/scripts/release-delta-guard.test.mjs +0 -174
  81. package/scripts/release-parity.test.mjs +0 -90
  82. package/scripts/release-state.test.mjs +0 -360
  83. package/scripts/test_anchor_table.py +0 -69
  84. package/scripts/test_board_bootstrap.py +0 -348
  85. package/scripts/test_board_sync.py +0 -208
  86. package/scripts/test_board_sync_create_idempotency.py +0 -117
  87. package/scripts/test_board_sync_wave_title.py +0 -120
  88. package/scripts/test_census_backstop.py +0 -712
  89. package/scripts/test_census_forward_contract.py +0 -91
  90. package/scripts/test_census_update_contract.test.mjs +0 -454
  91. package/scripts/test_codex_adapter_sync_contract.py +0 -169
  92. package/scripts/test_dist_kit_smoke.py +0 -220
  93. package/scripts/test_drift_guard_diagnostics.py +0 -295
  94. package/scripts/test_issue_claim_contract.py +0 -169
  95. package/scripts/test_kit_docs_language_census.py +0 -178
  96. package/scripts/test_marker_lib.py +0 -162
  97. package/scripts/test_orchestrate_wave_contract.py +0 -381
  98. package/scripts/test_pr_body_check.py +0 -264
  99. package/scripts/test_profile_globs.py +0 -280
  100. package/scripts/test_program_planning_contract.py +0 -133
  101. package/scripts/test_release_authorization_contract.py +0 -101
  102. package/scripts/test_render_anchor.py +0 -267
  103. package/scripts/test_retro_wrapup_contract.py +0 -153
  104. package/scripts/test_skill_code_review_seed.py +0 -89
  105. package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
  106. package/scripts/test_skill_frontmatter_lint.py +0 -234
  107. package/scripts/test_skill_gh_lint.py +0 -124
  108. package/scripts/test_skill_language_census.py +0 -254
  109. package/scripts/test_skill_optional_readiness.py +0 -171
  110. package/scripts/test_skill_portability_lint.py +0 -866
  111. package/scripts/test_skill_precommit_template.py +0 -30
  112. package/scripts/test_skill_publish_audit.py +0 -358
  113. package/scripts/test_skill_readiness_contract.py +0 -264
  114. package/scripts/test_skill_readiness_preflight.py +0 -180
  115. package/scripts/test_skill_required_readiness.py +0 -233
  116. package/scripts/test_skill_selfcontainment_lint.py +0 -255
  117. package/scripts/test_skill_setup_workflow_seeds.py +0 -691
  118. package/scripts/test_skill_stale_name_lint.py +0 -126
  119. package/scripts/test_skill_surface_refs.py +0 -300
  120. package/scripts/test_skill_trailing_artifact_lint.py +0 -71
  121. package/scripts/test_tdd_contract.py +0 -78
  122. package/scripts/test_worktree_ignore_seed.py +0 -320
  123. package/scripts/test_worktree_setup_base_guard.py +0 -140
  124. package/scripts/test_worktree_wrapup_contract.py +0 -1737
  125. package/scripts/test_wrapup_land.py +0 -428
  126. 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()