@ikon85/agent-workflow-kit 0.42.1 → 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 (133) 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 +268 -7
  24. package/agent-workflow-kit.package.json +42 -42
  25. package/package.json +16 -2
  26. package/scripts/marker_lib.py +3 -0
  27. package/scripts/profile_globs.py +26 -45
  28. package/scripts/release-delta-guard.mjs +29 -4
  29. package/scripts/worktree-lifecycle/README.md +57 -154
  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 -1289
  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/domain.md +0 -7
  53. package/docs/agents/issue-tracker.md +0 -23
  54. package/docs/agents/skills/local-ci.md +0 -90
  55. package/docs/agents/skills/orchestrate-wave.md +0 -166
  56. package/docs/agents/skills/spec-self-critique.md +0 -18
  57. package/docs/agents/triage-labels.md +0 -10
  58. package/docs/agents/workflow-capabilities.json +0 -28
  59. package/docs/conventions/spec-completeness.md +0 -23
  60. package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
  61. package/docs/research/benchlm-routing-source.md +0 -198
  62. package/docs/research/consumer-owned-protocol-files.md +0 -238
  63. package/docs/research/frontend-agent-benchmarks.md +0 -282
  64. package/docs/research/model-effort-routing-benchmarks.md +0 -261
  65. package/docs/research/provider-neutral-agent-routing.md +0 -207
  66. package/docs/research/wave-152-consumer-acceptance.md +0 -98
  67. package/docs/research/wave-43-script-hook-census.md +0 -128
  68. package/scripts/build-kit.mjs +0 -117
  69. package/scripts/build-kit.test.mjs +0 -204
  70. package/scripts/census/census.test.mjs +0 -252
  71. package/scripts/census/state.test.mjs +0 -15
  72. package/scripts/census/transaction.test.mjs +0 -103
  73. package/scripts/census-contract.test.mjs +0 -227
  74. package/scripts/check-kit-staleness.mjs +0 -45
  75. package/scripts/check-kit-staleness.test.mjs +0 -19
  76. package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -152
  77. package/scripts/codex-exec.test.mjs +0 -871
  78. package/scripts/grill-census-wiring-guard.mjs +0 -87
  79. package/scripts/grill-census-wiring-guard.test.mjs +0 -107
  80. package/scripts/kit-release.test.mjs +0 -273
  81. package/scripts/kit-update-pr.test.mjs +0 -151
  82. package/scripts/lib/audit-refs.mjs +0 -146
  83. package/scripts/lib/scrub.mjs +0 -113
  84. package/scripts/lib/scrub.test.mjs +0 -187
  85. package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
  86. package/scripts/portability_profile_scan.py +0 -53
  87. package/scripts/release-delta-guard.test.mjs +0 -151
  88. package/scripts/release-parity.test.mjs +0 -90
  89. package/scripts/release-state.test.mjs +0 -360
  90. package/scripts/test_anchor_table.py +0 -69
  91. package/scripts/test_board_bootstrap.py +0 -348
  92. package/scripts/test_board_sync.py +0 -208
  93. package/scripts/test_board_sync_create_idempotency.py +0 -117
  94. package/scripts/test_board_sync_wave_title.py +0 -120
  95. package/scripts/test_census_backstop.py +0 -712
  96. package/scripts/test_census_forward_contract.py +0 -91
  97. package/scripts/test_census_update_contract.test.mjs +0 -454
  98. package/scripts/test_codex_adapter_sync_contract.py +0 -169
  99. package/scripts/test_dist_kit_smoke.py +0 -220
  100. package/scripts/test_drift_guard_diagnostics.py +0 -295
  101. package/scripts/test_issue_claim_contract.py +0 -169
  102. package/scripts/test_kit_docs_language_census.py +0 -178
  103. package/scripts/test_marker_lib.py +0 -162
  104. package/scripts/test_orchestrate_wave_contract.py +0 -381
  105. package/scripts/test_pr_body_check.py +0 -264
  106. package/scripts/test_profile_globs.py +0 -280
  107. package/scripts/test_program_planning_contract.py +0 -133
  108. package/scripts/test_release_authorization_contract.py +0 -101
  109. package/scripts/test_render_anchor.py +0 -267
  110. package/scripts/test_retro_wrapup_contract.py +0 -153
  111. package/scripts/test_skill_code_review_seed.py +0 -89
  112. package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
  113. package/scripts/test_skill_frontmatter_lint.py +0 -234
  114. package/scripts/test_skill_gh_lint.py +0 -124
  115. package/scripts/test_skill_language_census.py +0 -254
  116. package/scripts/test_skill_optional_readiness.py +0 -171
  117. package/scripts/test_skill_portability_lint.py +0 -866
  118. package/scripts/test_skill_precommit_template.py +0 -30
  119. package/scripts/test_skill_publish_audit.py +0 -358
  120. package/scripts/test_skill_readiness_contract.py +0 -264
  121. package/scripts/test_skill_readiness_preflight.py +0 -180
  122. package/scripts/test_skill_required_readiness.py +0 -233
  123. package/scripts/test_skill_selfcontainment_lint.py +0 -255
  124. package/scripts/test_skill_setup_workflow_seeds.py +0 -691
  125. package/scripts/test_skill_stale_name_lint.py +0 -126
  126. package/scripts/test_skill_surface_refs.py +0 -300
  127. package/scripts/test_skill_trailing_artifact_lint.py +0 -71
  128. package/scripts/test_tdd_contract.py +0 -78
  129. package/scripts/test_worktree_ignore_seed.py +0 -320
  130. package/scripts/test_worktree_setup_base_guard.py +0 -140
  131. package/scripts/test_worktree_wrapup_contract.py +0 -1648
  132. package/scripts/test_wrapup_land.py +0 -428
  133. 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()