@ikon85/agent-workflow-kit 0.43.0 → 0.44.1

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