@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,267 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Behavior tests for the pure Tier-2 anchor renderer."""
3
-
4
- from __future__ import annotations
5
-
6
- import importlib.util
7
- import os
8
- import subprocess
9
- import sys
10
- import tempfile
11
- import unittest
12
- from pathlib import Path
13
-
14
-
15
- SCRIPTS = Path(__file__).resolve().parent
16
- REPO = SCRIPTS.parent
17
- SKILLS = (
18
- REPO / ".claude/skills/to-issues/SKILL.md",
19
- REPO / ".agents/skills/to-issues/SKILL.md",
20
- )
21
- sys.path.insert(0, str(SCRIPTS))
22
- SPEC = importlib.util.spec_from_file_location(
23
- "render_anchor", SCRIPTS / "render-anchor.py"
24
- )
25
- render_anchor = importlib.util.module_from_spec(SPEC)
26
- assert SPEC.loader is not None
27
- SPEC.loader.exec_module(render_anchor)
28
-
29
-
30
- class RenderDocumentsGoldenTest(unittest.TestCase):
31
- def test_filled_template_and_prd_render_both_documents(self):
32
- template = "**Welle 12 — Safer publish.**\n\n## Slices\n| K1 |\n"
33
- prd = (
34
- "<!-- prd-source-id: safer-publish -->\n"
35
- "<!-- prd-content-fp: abc123 -->\n"
36
- "**plan_revision:** r4\n"
37
- "<!-- prd: awaiting-decomposition -->\n\n"
38
- "# Safer publish\n\nFull rationale.\n"
39
- )
40
-
41
- rendered = render_anchor.render_documents(template, prd)
42
-
43
- self.assertEqual(rendered.anchor_body, template)
44
- self.assertEqual(
45
- rendered.archive_body,
46
- "📄 Full PRD (archive, r4) — the body carries navigation/decisions only\n\n"
47
- "# Safer publish\n\nFull rationale.\n",
48
- )
49
-
50
- def test_only_canonical_markers_in_the_head_block_are_stripped(self):
51
- template = "Lean anchor\n"
52
- prd = (
53
- "<!-- wave-stub-source: safer-publish -->\n"
54
- "<!-- prd-source-id: safer-publish -->\n"
55
- "**plan_revision:** r5\n"
56
- "<!-- prd: awaiting-decomposition -->\n\n"
57
- "# PRD\n\n"
58
- "```md\n**plan_revision:** fake\n"
59
- "<!-- prd-source-id: quoted-example -->\n```\n\n"
60
- "> **plan_revision:** quoted-fake\n"
61
- )
62
-
63
- rendered = render_anchor.render_documents(template, prd)
64
-
65
- self.assertEqual(
66
- rendered.archive_body,
67
- "📄 Full PRD (archive, r5) — the body carries navigation/decisions only\n\n"
68
- "<!-- wave-stub-source: safer-publish -->\n\n# PRD\n\n"
69
- "```md\n**plan_revision:** fake\n"
70
- "<!-- prd-source-id: quoted-example -->\n```\n\n"
71
- "> **plan_revision:** quoted-fake\n",
72
- )
73
-
74
- def test_duplicate_plan_revisions_fail_closed(self):
75
- for revisions in (("r5", "r5"), ("r5", "r6")):
76
- with self.subTest(revisions=revisions):
77
- prd = (
78
- f"**plan_revision:** {revisions[0]}\n"
79
- f"**plan_revision:** {revisions[1]}\n\n# PRD\n"
80
- )
81
- with self.assertRaisesRegex(
82
- ValueError, "exactly one canonical plan_revision"
83
- ):
84
- render_anchor.render_documents("Lean anchor\n", prd)
85
-
86
- def test_plan_revision_requires_numeric_canonical_value(self):
87
- for lookalike in ("rfoo", "r5!"):
88
- with self.subTest(lookalike=lookalike, valid_revision=True):
89
- malformed = f"**plan_revision:** {lookalike}"
90
- prd = f"{malformed}\n**plan_revision:** r5\n\n# PRD\n"
91
- rendered = render_anchor.render_documents("Lean anchor\n", prd)
92
- self.assertIn(malformed, rendered.archive_body)
93
-
94
- with self.subTest(lookalike=lookalike, valid_revision=False):
95
- with self.assertRaisesRegex(
96
- ValueError, "exactly one canonical plan_revision"
97
- ):
98
- render_anchor.render_documents(
99
- "Lean anchor\n", f"**plan_revision:** {lookalike}\n# PRD\n"
100
- )
101
-
102
- def test_malformed_marker_lookalikes_are_preserved(self):
103
- lookalikes = (
104
- "<!-- prd-source-id: alpha --> trailing -->",
105
- "<!-- prd-content-fp: abc > trailing -->",
106
- "<!-- prd: program > trailing -->",
107
- )
108
- for lookalike in lookalikes:
109
- with self.subTest(lookalike=lookalike):
110
- prd = (
111
- f"{lookalike}\n"
112
- "<!-- prd-source-id: canonical -->\n"
113
- "<!-- prd-content-fp: abc123 -->\n"
114
- "**plan_revision:** r5\n"
115
- "<!-- prd: awaiting-decomposition -->\n\n# PRD\n"
116
- )
117
-
118
- rendered = render_anchor.render_documents("Lean anchor\n", prd)
119
-
120
- self.assertIn(lookalike, rendered.archive_body)
121
- self.assertNotIn(
122
- "<!-- prd-source-id: canonical -->", rendered.archive_body
123
- )
124
- self.assertNotIn(
125
- "<!-- prd-content-fp: abc123 -->", rendered.archive_body
126
- )
127
- self.assertNotIn(
128
- "<!-- prd: awaiting-decomposition -->", rendered.archive_body
129
- )
130
-
131
- def test_cli_emits_each_document_without_mutating_inputs(self):
132
- with tempfile.TemporaryDirectory() as temporary:
133
- root = Path(temporary)
134
- template = root / "anchor.md"
135
- prd = root / "prd.md"
136
- template.write_text("Lean anchor\n", encoding="utf-8")
137
- prd.write_text(
138
- "**plan_revision:** r2\n\n# Full PRD\n", encoding="utf-8"
139
- )
140
- original = (template.read_bytes(), prd.read_bytes())
141
-
142
- first = self._run_cli(template, prd, "archive")
143
- second = self._run_cli(template, prd, "archive")
144
- anchor = self._run_cli(template, prd, "anchor")
145
-
146
- self.assertEqual(first.stdout, second.stdout)
147
- self.assertEqual(anchor.stdout, b"Lean anchor\n")
148
- self.assertEqual((template.read_bytes(), prd.read_bytes()), original)
149
-
150
- def test_cli_archive_is_utf8_when_python_io_encoding_is_ascii(self):
151
- with tempfile.TemporaryDirectory() as temporary:
152
- root = Path(temporary)
153
- template = root / "anchor.md"
154
- prd = root / "prd.md"
155
- template.write_text("Lean anchor\n", encoding="utf-8")
156
- prd.write_text("**plan_revision:** r2\n\n# PRD\n", encoding="utf-8")
157
-
158
- result = self._run_cli(
159
- template,
160
- prd,
161
- "archive",
162
- env={**os.environ, "PYTHONIOENCODING": "ascii"},
163
- )
164
-
165
- self.assertTrue(result.stdout.startswith("📄".encode("utf-8")))
166
-
167
- def _run_cli(
168
- self, template: Path, prd: Path, document: str, env: dict | None = None
169
- ):
170
- return subprocess.run(
171
- [
172
- sys.executable,
173
- str(SCRIPTS / "render-anchor.py"),
174
- "--template",
175
- str(template),
176
- "--prd",
177
- str(prd),
178
- "--document",
179
- document,
180
- ],
181
- check=True,
182
- capture_output=True,
183
- env=env,
184
- )
185
-
186
-
187
- class PromotionStateTableTest(unittest.TestCase):
188
- EXPECTED_ROWS = {
189
- "initial": (
190
- "`S=yes`, `B=no`, `C=0`, `P=absent`",
191
- "render + write body",
192
- ),
193
- "body-written": (
194
- "`S=n/a`, `B=yes`, `C=0`, `P=absent`",
195
- "reconcile + write archive comment",
196
- ),
197
- "comment-written": (
198
- "`S=n/a`, `B=yes`, `C=exact-1`, `P=absent`",
199
- "promote board state",
200
- ),
201
- "promoted": (
202
- "`S=n/a`, `B=yes`, `C=exact-1`, `P=complete`",
203
- "no-op; continue publish audit",
204
- ),
205
- }
206
- EXPECTED_BOARD_OBSERVATIONS = {
207
- "ordinary-prestate": "absent",
208
- "stufe-1p-prestate": "absent",
209
- "promoted": "complete",
210
- "cluster-only": "partial",
211
- "wrong-wave": "partial",
212
- }
213
-
214
- def test_all_four_observable_states_have_one_resume_action(self):
215
- for path in SKILLS:
216
- text = path.read_text(encoding="utf-8")
217
- start = text.index("<!-- promotion-state-table:start -->")
218
- end = text.index("<!-- promotion-state-table:end -->", start)
219
- rows = {}
220
- for line in text[start:end].splitlines():
221
- if line.startswith("| `"):
222
- cells = [cell.strip() for cell in line.strip("|").split("|")]
223
- rows[cells[0].strip("`")] = (cells[1], cells[2])
224
- self.assertEqual(rows, self.EXPECTED_ROWS, str(path))
225
-
226
- def test_corrupt_comments_and_partial_board_writes_are_drift(self):
227
- for path in SKILLS:
228
- text = path.read_text(encoding="utf-8")
229
- self.assertIn("`C=wrong-1`", text, str(path))
230
- self.assertIn("`C=duplicates(<ids>)`", text, str(path))
231
- self.assertIn("`P=partial`", text, str(path))
232
- self.assertIn("drift and enters repair", text, str(path))
233
-
234
- def test_stufe_1p_prestamp_is_not_partial_promotion(self):
235
- for path in SKILLS:
236
- text = path.read_text(encoding="utf-8")
237
- start = text.index("<!-- promotion-board-observation-table:start -->")
238
- end = text.index("<!-- promotion-board-observation-table:end -->", start)
239
- rows = {}
240
- for line in text[start:end].splitlines():
241
- if line.startswith("| `"):
242
- cells = [cell.strip() for cell in line.strip("|").split("|")]
243
- rows[cells[0].strip("`")] = cells[2].strip("`")
244
- self.assertEqual(rows, self.EXPECTED_BOARD_OBSERVATIONS, str(path))
245
-
246
- def test_post_initial_body_drift_requires_an_operator_decision(self):
247
- for path in SKILLS:
248
- text = path.read_text(encoding="utf-8")
249
- self.assertIn("outside the valid `initial` tuple", text, str(path))
250
- self.assertIn("report the body diff", text, str(path))
251
- self.assertRegex(text, r"explicit\s+operator decision", str(path))
252
- self.assertNotIn(
253
- "wrong/missing `B` → rerender and rewrite the body", text, str(path)
254
- )
255
-
256
- def test_stale_source_snapshot_stops_before_body_write(self):
257
- for path in SKILLS:
258
- text = path.read_text(encoding="utf-8")
259
- self.assertRegex(text, r"Pre:\s+`S=yes`, `B=no`", str(path))
260
- self.assertRegex(text, r"Post:\s+`S=n/a`, `B=yes`", str(path))
261
- self.assertIn("`S=no`", text, str(path))
262
- self.assertIn("re-fetch the remote body", text, str(path))
263
- self.assertRegex(text, r"never\s+write the\s+stale render", str(path))
264
-
265
-
266
- if __name__ == "__main__":
267
- unittest.main(verbosity=2)
@@ -1,153 +0,0 @@
1
- """Contract tests for retro enforcement and safe wrapup workflow chaining."""
2
-
3
- from pathlib import Path
4
- import re
5
- import unittest
6
-
7
-
8
- ROOT = Path(__file__).resolve().parents[1]
9
- SURFACES = (".claude", ".agents")
10
-
11
-
12
- def skill(surface: str, name: str) -> str:
13
- return (ROOT / surface / "skills" / name / "SKILL.md").read_text(
14
- encoding="utf-8"
15
- )
16
-
17
-
18
- def contract_text(surface: str, name: str) -> str:
19
- """Return prose with Markdown emphasis removed for wording assertions."""
20
- return re.sub(r"\s+", " ", skill(surface, name).replace("**", ""))
21
-
22
-
23
- class RetroEnforcementContract(unittest.TestCase):
24
- def test_mechanical_check_precedes_target_and_weight_ladder(self):
25
- for surface in SURFACES:
26
- with self.subTest(surface=surface):
27
- text = contract_text(surface, "retro")
28
- mechanical = text.index("### 3a. Mechanical enforcement check")
29
- ladder = text.index("### 3b. Determine target + weight")
30
- self.assertLess(mechanical, ladder)
31
-
32
- def test_machine_checkable_findings_cannot_stop_at_prose(self):
33
- required = (
34
- "Could a machine decide this",
35
- "implement the enforcement",
36
- "tracked enforcement issue",
37
- "explicit trade-off",
38
- )
39
- for surface in SURFACES:
40
- with self.subTest(surface=surface):
41
- text = contract_text(surface, "retro")
42
- for phrase in required:
43
- self.assertIn(phrase, text)
44
-
45
- def test_capability_route_and_exact_sanitized_approval_survive(self):
46
- required = (
47
- "generic or project-specific",
48
- "recommend `own`",
49
- "contribute status <path> --surface=retro",
50
- "Missing, disabled, invalid, or unverifiable configuration",
51
- "never infer maintainer status",
52
- "sanitized exact preview",
53
- "separate explicit approval",
54
- "docs/agents/skills/<skill>.md",
55
- )
56
- for surface in SURFACES:
57
- with self.subTest(surface=surface):
58
- text = contract_text(surface, "retro")
59
- for phrase in required:
60
- self.assertIn(phrase, text)
61
-
62
-
63
- class WrapupChainingContract(unittest.TestCase):
64
- def test_prod_readiness_degrades_reporting_without_blocking_landing(self):
65
- required = (
66
- "readiness.mjs check --skill wrapup --json",
67
- "Prod readiness is pending or missing; deploy reporting omitted.",
68
- "landing continues normally",
69
- "never authorizes the agent to invent or configure a deploy target",
70
- "<!-- readiness:block deployReport -->",
71
- "<!-- readiness:end -->",
72
- )
73
- for surface in SURFACES:
74
- with self.subTest(surface=surface):
75
- text = skill(surface, "wrapup")
76
- for phrase in required:
77
- self.assertIn(phrase, text)
78
- self.assertEqual(text.count("<!-- readiness:block deployReport -->"), 1)
79
- self.assertEqual(text.count("<!-- readiness:end -->"), 1)
80
-
81
- def test_retro_is_model_invocable_on_both_surfaces(self):
82
- for surface in SURFACES:
83
- with self.subTest(surface=surface):
84
- frontmatter = skill(surface, "retro").split("---", 2)[1]
85
- self.assertIn("disable-model-invocation: false", frontmatter)
86
-
87
- def test_affirmative_retro_gate_chains_without_landing(self):
88
- required = (
89
- "invoke the `retro` skill immediately in this run",
90
- "land nothing in this run",
91
- "fresh explicit `$wrapup` or `/wrapup` invocation",
92
- )
93
- for surface in SURFACES:
94
- with self.subTest(surface=surface):
95
- text = contract_text(surface, "wrapup")
96
- for phrase in required:
97
- self.assertIn(phrase, text)
98
-
99
- def test_wrapup_does_not_forbid_its_affirmative_retro_chain(self):
100
- for surface in SURFACES:
101
- with self.subTest(surface=surface):
102
- text = contract_text(surface, "wrapup")
103
- self.assertNotIn("never run by this skill", text.lower())
104
-
105
- def test_only_model_invocable_non_deploying_workflows_chain(self):
106
- required = (
107
- "model-invocable",
108
- "non-deploying",
109
- "return control to the user",
110
- "state the reason",
111
- )
112
- for surface in SURFACES:
113
- with self.subTest(surface=surface):
114
- text = contract_text(surface, "wrapup")
115
- for phrase in required:
116
- self.assertIn(phrase, text)
117
-
118
- def test_wrapup_remains_manual_and_program_propagation_remains_present(self):
119
- for surface in SURFACES:
120
- with self.subTest(surface=surface):
121
- text = skill(surface, "wrapup")
122
- self.assertIn("disable-model-invocation: true", text)
123
- self.assertIn(
124
- "user's direct `$wrapup` or `/wrapup` input IS the explicit",
125
- text,
126
- )
127
- self.assertIn("program-sync", text)
128
- self.assertIn("Phasen-Gates", text)
129
-
130
- def test_wrapup_accepts_only_direct_user_dollar_or_slash_invocations(self):
131
- required = (
132
- "direct `$wrapup` or `/wrapup`",
133
- "Natural-language requests",
134
- "indirect skill chaining",
135
- "autonomous invocation",
136
- "fresh explicit `$wrapup` or `/wrapup` invocation",
137
- )
138
- for surface in SURFACES:
139
- with self.subTest(surface=surface):
140
- text = contract_text(surface, "wrapup")
141
- for phrase in required:
142
- self.assertIn(phrase, text)
143
-
144
- def test_new_contract_is_reference_free(self):
145
- for surface in SURFACES:
146
- with self.subTest(surface=surface):
147
- combined = skill(surface, "retro") + skill(surface, "wrapup")
148
- self.assertNotIn("testreporter", combined.lower())
149
- self.assertIsNone(re.search(r"#[0-9]{3,}", combined))
150
-
151
-
152
- if __name__ == "__main__":
153
- unittest.main(verbosity=2)
@@ -1,89 +0,0 @@
1
- #!/usr/bin/env python3
2
- """code-review skill + its setup-workflow project-layer seed (Welle 49).
3
-
4
- Companion to test_skill_setup_workflow_seeds.py (not touched here — a sibling
5
- slice edits it concurrently). Pins the two machine-checkable parts specific to
6
- this skill:
7
-
8
- 1. The seed template setup-workflow ships (`code-review.md`) is structurally
9
- valid for the skill that reads it — same "structured-but-empty crust"
10
- contract as the existing spec-self-critique / spec-completeness seeds.
11
- 2. The sentinel-based idempotency rule applies to it the same way (reference
12
- implementation duplicated locally so this file has no import-order
13
- dependency on the sibling-owned test module).
14
-
15
- Run: python3 scripts/test_skill_code_review_seed.py
16
- """
17
- import re
18
- import unittest
19
- from pathlib import Path
20
-
21
- REPO = Path(__file__).resolve().parent.parent
22
- SETUP_WORKFLOW = REPO / ".claude/skills/setup-workflow"
23
- CODE_REVIEW = REPO / ".claude/skills/code-review"
24
-
25
- SENTINEL_RE = re.compile(
26
- r"^<!--\s*setup-workflow:\s*state=(stub|filled|not-applicable)"
27
- r"(?:;\s*mode=(github-projects-v2|none))?\s*-->\s*$"
28
- )
29
-
30
-
31
- class SeedTemplateValid(unittest.TestCase):
32
- def test_code_review_seed_has_both_headings(self):
33
- """The two structured-but-empty headings the `code-review` skill's
34
- Standards axis expects to find once this file is filled."""
35
- t = (SETUP_WORKFLOW / "code-review.md").read_text(encoding="utf-8")
36
- self.assertIn("## Standards sources in this repo", t)
37
- self.assertIn("## Adjacent review tooling", t)
38
-
39
- def test_code_review_seed_is_generic(self):
40
- """No project-coupled tokens in a template shipped to every consumer."""
41
- t = (SETUP_WORKFLOW / "code-review.md").read_text(encoding="utf-8")
42
- for token in ("Testreporter", "iKon85", "/home/"):
43
- self.assertNotIn(token, t)
44
- self.assertNotRegex(t, r"#\d{3,5}\b")
45
- self.assertNotRegex(t, r"\bHR\d+\b")
46
-
47
- def test_setup_workflow_skill_registers_the_seed(self):
48
- """SKILL.md wires the target path + seed link into its Targets table
49
- and process, so a reader (or the manifest-completeness lint's sibling
50
- checks) can trace the seed from the skill body."""
51
- skill = (SETUP_WORKFLOW / "SKILL.md").read_text(encoding="utf-8")
52
- self.assertIn("docs/agents/code-review.md", skill)
53
- self.assertIn("[code-review.md](./code-review.md)", skill)
54
-
55
- def test_code_review_skill_names_its_project_layer_and_fallback(self):
56
- """The skill states the project-layer path + the /setup-workflow
57
- fallback (self-containment-lint's LookupContract pattern)."""
58
- body = (CODE_REVIEW / "SKILL.md").read_text(encoding="utf-8")
59
- self.assertIn("docs/agents/code-review.md", body)
60
- self.assertIn("/setup-workflow", body)
61
-
62
-
63
- class FreshFillProducesValidSentinel(unittest.TestCase):
64
- """Simulate the write step: prepend the documented sentinel to the seed
65
- body and assert the result is a well-formed project-layer file."""
66
-
67
- def test_prepended_sentinel_parses(self):
68
- body = (SETUP_WORKFLOW / "code-review.md").read_text(encoding="utf-8")
69
- written = "<!-- setup-workflow: state=filled -->\n" + body
70
- self.assertRegex(written.splitlines()[0], SENTINEL_RE)
71
-
72
-
73
- class ManifestEntryValid(unittest.TestCase):
74
- def test_code_review_is_own_generic_dual_surface_publish_true(self):
75
- import json
76
-
77
- manifest = json.loads(
78
- (REPO / ".claude/skills/skill-manifest.json").read_text(encoding="utf-8")
79
- )
80
- entry = manifest["skills"].get("code-review")
81
- self.assertIsNotNone(entry, "code-review missing from skill-manifest.json")
82
- self.assertEqual(entry["class"], "generic")
83
- self.assertIs(entry["publish"], True)
84
- self.assertEqual(entry["provenance"], "own")
85
- self.assertEqual(set(entry["surfaces"]), {"claude", "codex"})
86
-
87
-
88
- if __name__ == "__main__":
89
- unittest.main(verbosity=2)
@@ -1,123 +0,0 @@
1
- """Guard the Claude-only cross-model skills' thin codex-exec lifecycle."""
2
-
3
- import json
4
- import re
5
- import subprocess
6
- import unittest
7
- from pathlib import Path
8
-
9
-
10
- REPO = Path(__file__).resolve().parents[1]
11
- MANIFEST = REPO / ".claude/skills/skill-manifest.json"
12
- TARGETS = {
13
- "codex-review": ("review", "read-only"),
14
- "codex-build": ("build", "workspace-write"),
15
- "grill-me-codex": ("review", "read-only"),
16
- "grill-with-docs-codex": ("review", "read-only"),
17
- }
18
-
19
-
20
- class CodexExecSkillLifecycleTests(unittest.TestCase):
21
- @classmethod
22
- def setUpClass(cls):
23
- manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))["skills"]
24
- cls.skills = {}
25
- for name, expected in TARGETS.items():
26
- entry = manifest[name]
27
- if entry["surfaces"] != ["claude"]:
28
- raise AssertionError(f"{name} must remain Claude-only")
29
- path = REPO / ".claude/skills" / name / "SKILL.md"
30
- cls.skills[name] = (expected, path.read_text(encoding="utf-8"))
31
-
32
- def test_all_four_skills_use_the_thin_wrapper_lifecycle(self):
33
- self.assertEqual(len(self.skills), 4)
34
- for name, ((profile, mode), body) in self.skills.items():
35
- with self.subTest(skill=name):
36
- self.assertIn(
37
- f"scripts/codex-exec.sh new --profile {profile} --mode {mode}",
38
- body,
39
- )
40
- self.assertRegex(body, r"scripts/codex-exec\.sh resume [\"']?\$RUN_ID")
41
- self.assertRegex(body, r"scripts/codex-exec\.sh finalize [\"']?\$RUN_ID")
42
- self.assertEqual(body.count("scripts/codex-exec.sh handle-failure"), 2)
43
- self.assertNotRegex(
44
- body,
45
- r"scripts/codex-exec\.sh resume[^\n]*--mode",
46
- "resume must inherit the persisted mode",
47
- )
48
-
49
- def test_new_and_resume_are_thin_ordered_and_valid_bash(self):
50
- for name, (_, body) in self.skills.items():
51
- blocks = re.findall(
52
- r"```bash\n(if ROUND_RESULT=\$\(scripts/codex-exec\.sh (?:new|resume).*?\nfi)\n```",
53
- body,
54
- re.DOTALL,
55
- )
56
- self.assertEqual(len(blocks), 2, name)
57
- for block in blocks:
58
- first_line = block.splitlines()[0]
59
- is_resume = "scripts/codex-exec.sh resume " in first_line
60
- success, failure = block.split("\nelse\n", 1)
61
- self.assertIn("CODEX_REPORT=", success)
62
- self.assertNotIn("CODEX_REPORT=", failure)
63
- self.assertIn("scripts/codex-exec.sh handle-failure", failure)
64
- self.assertIn("|| :", failure)
65
- self.assertNotIn("json.load", failure)
66
- handler = failure.index("handle-failure")
67
- surfaced = failure.index('printf \'%s\\n\' "$FAILURE_RESULT" >&2')
68
- stopped = failure.index("exit 1")
69
- self.assertLess(handler, surfaced)
70
- self.assertLess(surfaced, stopped)
71
- if is_resume:
72
- self.assertIn('--run-id "$RUN_ID"', failure)
73
- self.assertNotIn("\n RUN_ID=", success)
74
- else:
75
- self.assertNotIn("--run-id", failure)
76
- self.assertLess(success.index("RUN_ID="), success.index("CODEX_REPORT="))
77
- parsed = subprocess.run(
78
- ["bash", "-n"],
79
- input=block,
80
- text=True,
81
- capture_output=True,
82
- check=False,
83
- )
84
- self.assertEqual(parsed.returncode, 0, parsed.stderr)
85
-
86
- def test_all_skills_abort_known_run_on_orchestration_cancellation(self):
87
- for name, (_, body) in self.skills.items():
88
- with self.subTest(skill=name):
89
- cancellation = re.search(
90
- r"[Cc]ancellation or a decision to stop (?:orchestration|delegation)"
91
- r".*?scripts/codex-exec\.sh abort \"\$RUN_ID\"",
92
- body,
93
- re.DOTALL,
94
- )
95
- self.assertIsNotNone(cancellation)
96
- self.assertRegex(body, r"scripts/codex-exec\.sh finalize [\"']?\$RUN_ID")
97
-
98
- build = self.skills["codex-build"][1]
99
- gate = build.split("## Step 5 — Human gate", 1)[1].split("## Hard rules", 1)[0]
100
- self.assertLess(
101
- gate.index("Rejected with another requested fix"),
102
- gate.index("Cancellation or a decision to stop delegation"),
103
- )
104
-
105
- def test_no_skill_reimplements_codex_process_or_failure_mechanics(self):
106
- forbidden = {
107
- "copied round function": re.compile(r"\brun_codex_round\b"),
108
- "copied status parser": re.compile(r"\bROUND_STATUS\b|\.get\(\"status\""),
109
- "copied run-id cleanup": re.compile(r"\bFAILED_RUN_ID\b"),
110
- "raw codex exec": re.compile(r"(?m)^\s*codex\s+exec\b"),
111
- "manual liveness sleep": re.compile(r"\bsleep\s+90\b"),
112
- "manual sandbox override": re.compile(r"\bsandbox_mode\s*="),
113
- "manual process probe": re.compile(r"\bkill\s+-0\b"),
114
- "manual codex pid": re.compile(r"\bCODEX_PID\b"),
115
- }
116
- for name, (_, body) in self.skills.items():
117
- for label, pattern in forbidden.items():
118
- with self.subTest(skill=name, forbidden=label):
119
- self.assertIsNone(pattern.search(body))
120
-
121
-
122
- if __name__ == "__main__":
123
- unittest.main()