@ikon85/agent-workflow-kit 0.43.0 → 0.44.0

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