@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,280 +0,0 @@
1
- #!/usr/bin/env python3
2
- """One repository-relative glob dialect backs every consumer-profile glob."""
3
-
4
- import importlib.util
5
- import json
6
- import subprocess
7
- import sys
8
- import tempfile
9
- import unittest
10
- from pathlib import Path
11
-
12
- REPO = Path(__file__).resolve().parent.parent
13
- DIALECT_MODULE = REPO / "scripts/profile_globs.py"
14
- ADVISORIES_CORE = REPO / "scripts/workflow-advisories/core.py"
15
- LIFECYCLE_CORE = REPO / "scripts/worktree-lifecycle/core.py"
16
-
17
-
18
- def load(name, path):
19
- spec = importlib.util.spec_from_file_location(name, path)
20
- module = importlib.util.module_from_spec(spec)
21
- sys.modules[name] = module
22
- spec.loader.exec_module(module)
23
- return module
24
-
25
-
26
- def load_lifecycle_core():
27
- module_dir = str(LIFECYCLE_CORE.parent)
28
- if module_dir not in sys.path:
29
- sys.path.insert(0, module_dir)
30
- return load("profile_globs_lifecycle_core", LIFECYCLE_CORE)
31
-
32
-
33
- dialect = load("profile_globs_under_test", DIALECT_MODULE)
34
-
35
-
36
- class MatcherContract(unittest.TestCase):
37
- """The documented dialect, exercised on the axes the contract names."""
38
-
39
- def test_star_stays_inside_one_segment(self):
40
- self.assertTrue(dialect.path_glob_matches("build.log", "*.log"))
41
- self.assertFalse(dialect.path_glob_matches("logs/build.log", "*.log"))
42
- self.assertTrue(dialect.path_glob_matches("logs/build.log", "logs/*.log"))
43
- self.assertFalse(dialect.path_glob_matches("logs/a/build.log", "logs/*.log"))
44
-
45
- def test_question_mark_matches_one_character_inside_one_segment(self):
46
- self.assertTrue(dialect.path_glob_matches("a1.tmp", "a?.tmp"))
47
- self.assertFalse(dialect.path_glob_matches("a/1.tmp", "a?1.tmp"))
48
-
49
- def test_character_classes_stay_per_segment(self):
50
- self.assertTrue(dialect.path_glob_matches("cache/7.tmp", "cache/[0-9].tmp"))
51
- self.assertFalse(dialect.path_glob_matches("cache/x.tmp", "cache/[0-9].tmp"))
52
- self.assertTrue(dialect.path_glob_matches("cache/x.tmp", "cache/[!0-9].tmp"))
53
-
54
- def test_leading_globstar_covers_the_repository_root_and_nested_paths(self):
55
- self.assertTrue(dialect.path_glob_matches("__pycache__/a.pyc", "**/__pycache__/**"))
56
- self.assertTrue(dialect.path_glob_matches("src/__pycache__/a.pyc", "**/__pycache__/**"))
57
- self.assertTrue(dialect.path_glob_matches("notes.md", "**/notes.md"))
58
- self.assertTrue(dialect.path_glob_matches("a/b/notes.md", "**/notes.md"))
59
-
60
- def test_directory_roots_match_every_depth_below_them(self):
61
- self.assertTrue(dialect.path_glob_matches("dist-kit/a", "dist-kit/**"))
62
- self.assertTrue(dialect.path_glob_matches("dist-kit/a/b", "dist-kit/**"))
63
- self.assertTrue(dialect.path_glob_matches("dist-kit", "dist-kit/**"))
64
- self.assertFalse(dialect.path_glob_matches("dist-kit/a/b", "dist-kit/*"))
65
- self.assertFalse(dialect.path_glob_matches("dist-kitten/a", "dist-kit/**"))
66
-
67
- def test_matching_is_case_sensitive_on_every_host(self):
68
- self.assertTrue(dialect.path_glob_matches("PLAN.md", "PLAN.md"))
69
- self.assertFalse(dialect.path_glob_matches("plan.md", "PLAN.md"))
70
- self.assertFalse(dialect.path_glob_matches("Logs/a.log", "logs/*.log"))
71
-
72
- def test_whole_path_must_match(self):
73
- self.assertFalse(dialect.path_glob_matches("src/index.mjs", "src"))
74
- self.assertFalse(dialect.path_glob_matches("src", "src/index.mjs"))
75
-
76
-
77
- class SharedMatcherContract(unittest.TestCase):
78
- """Both shipped cores resolve to the one shared matcher, not a copy."""
79
-
80
- def test_both_cores_share_one_matcher_object(self):
81
- advisories = load("profile_globs_advisories_core", ADVISORIES_CORE)
82
- lifecycle = load_lifecycle_core()
83
- self.assertIs(advisories.path_glob_matches, lifecycle.path_glob_matches)
84
-
85
- def test_the_shared_matcher_comes_from_the_shipped_dialect_module(self):
86
- advisories = load("profile_globs_advisories_core", ADVISORIES_CORE)
87
- shared = advisories.load_profile_globs()
88
- self.assertEqual(Path(shared.__file__).resolve(), DIALECT_MODULE.resolve())
89
- self.assertIs(advisories.path_glob_matches, shared.path_glob_matches)
90
-
91
- def test_both_cores_name_the_same_shared_module(self):
92
- advisories = load("profile_globs_advisories_core", ADVISORIES_CORE)
93
- lifecycle = load_lifecycle_core()
94
- self.assertEqual(
95
- advisories.PROFILE_GLOBS_MODULE, lifecycle.PROFILE_GLOBS_MODULE,
96
- )
97
-
98
- def test_neither_core_keeps_a_second_matcher(self):
99
- for core in (ADVISORIES_CORE, LIFECYCLE_CORE):
100
- body = core.read_text(encoding="utf-8")
101
- self.assertNotIn("fnmatch.fnmatch(", body)
102
- self.assertNotIn("def path_glob_matches", body)
103
-
104
-
105
- class AdvisorySurfaceContract(unittest.TestCase):
106
- """Advisory globs select the same paths the lifecycle globs would."""
107
-
108
- def decision_for(self, globs, changed):
109
- core = load("profile_globs_advisories_surface", ADVISORIES_CORE)
110
- profile = {
111
- "stopChecks": {
112
- "surfaces": [{"globs": globs, "command": ["python3", "-c", "pass"]}],
113
- "timeoutSeconds": 3,
114
- "outputBudget": 300,
115
- },
116
- }
117
- with tempfile.TemporaryDirectory() as root:
118
- return core.stop_check_decision(
119
- profile, {"changed_files": changed}, Path(root),
120
- )
121
-
122
- def test_single_segment_glob_no_longer_crosses_a_directory(self):
123
- self.assertIsNone(self.decision_for(["*.py"], ["pkg/mod.py"]).context)
124
- self.assertIsNotNone(self.decision_for(["*.py"], ["mod.py"]).context)
125
-
126
- def test_globstar_covers_root_and_nested_paths(self):
127
- self.assertIsNotNone(self.decision_for(["**/*.py"], ["pkg/mod.py"]).context)
128
- self.assertIsNotNone(self.decision_for(["**/*.py"], ["mod.py"]).context)
129
-
130
- def test_directory_root_glob_covers_every_depth(self):
131
- self.assertIsNotNone(self.decision_for(["src/**"], ["src/a/b.mjs"]).context)
132
- self.assertIsNone(self.decision_for(["src/*"], ["src/a/b.mjs"]).context)
133
-
134
- def test_advisory_matching_is_case_sensitive(self):
135
- self.assertIsNone(self.decision_for(["src/**"], ["SRC/a.mjs"]).context)
136
-
137
-
138
- class MigrationClassifier(unittest.TestCase):
139
- """Legacy patterns whose match set changes are named, never rewritten."""
140
-
141
- def test_literal_pattern_is_stable(self):
142
- self.assertEqual(dialect.classify_pattern("PLAN.md").effects, ())
143
-
144
- def test_single_segment_wildcard_narrows(self):
145
- migration = dialect.classify_pattern("*.log")
146
- self.assertIn(dialect.NARROWS, migration.effects)
147
- self.assertIn("/", migration.witnesses[dialect.NARROWS])
148
-
149
- def test_nested_wildcard_without_globstar_narrows(self):
150
- self.assertIn(
151
- dialect.NARROWS, dialect.classify_pattern("dist-kit/*").effects,
152
- )
153
-
154
- def test_leading_globstar_widens_at_the_repository_root(self):
155
- migration = dialect.classify_pattern("**/__pycache__/**")
156
- self.assertIn(dialect.WIDENS, migration.effects)
157
- self.assertEqual(migration.witnesses[dialect.WIDENS], "__pycache__")
158
-
159
- def test_directory_root_globstar_widens_onto_the_directory_itself(self):
160
- migration = dialect.classify_pattern("dist-kit/**")
161
- self.assertIn(dialect.WIDENS, migration.effects)
162
- self.assertEqual(migration.witnesses[dialect.WIDENS], "dist-kit")
163
-
164
- def test_bare_globstar_is_stable(self):
165
- self.assertEqual(dialect.classify_pattern("**").effects, ())
166
-
167
- def test_character_class_pattern_is_stable(self):
168
- self.assertEqual(dialect.classify_pattern("cache/[0-9].tmp").effects, ())
169
-
170
- def test_case_effect_only_applies_to_the_case_normalizing_legacy(self):
171
- insensitive = dialect.classify_pattern("PLAN.md", case_insensitive_legacy=True)
172
- self.assertIn(dialect.CASE_NARROWS, insensitive.effects)
173
- self.assertEqual(dialect.classify_pattern("PLAN.md").effects, ())
174
-
175
-
176
- class ProfileScan(unittest.TestCase):
177
- """Every shipped consumer-profile glob key is reachable from one scan."""
178
-
179
- document = {
180
- "worktreeLifecycle": {"scratchPatterns": ["PLAN.md", "*.log"]},
181
- "wrapup": {"landingGeneratedArtifactPatterns": ["dist-kit/**"]},
182
- "workflowAdvisories": {
183
- "baseline": {"sourceGlobs": ["src/**"]},
184
- "preRefactor": {"surfaces": [{"globs": ["frontend/*"]}]},
185
- "stopChecks": {"surfaces": [{"globs": ["backend/**"]}]},
186
- },
187
- }
188
-
189
- def locations(self):
190
- return [finding.location for finding in dialect.scan_profile(self.document)]
191
-
192
- def test_scan_reaches_every_shipped_glob_key(self):
193
- self.assertEqual(self.locations(), [
194
- "worktreeLifecycle.scratchPatterns[0]",
195
- "worktreeLifecycle.scratchPatterns[1]",
196
- "wrapup.landingGeneratedArtifactPatterns[0]",
197
- "workflowAdvisories.baseline.sourceGlobs[0]",
198
- "workflowAdvisories.preRefactor.surfaces[0].globs[0]",
199
- "workflowAdvisories.stopChecks.surfaces[0].globs[0]",
200
- ])
201
-
202
- def test_cleanup_keys_are_marked_as_deletion_authority(self):
203
- authority = {
204
- finding.location: finding.deletion_authority
205
- for finding in dialect.scan_profile(self.document)
206
- }
207
- self.assertTrue(authority["worktreeLifecycle.scratchPatterns[0]"])
208
- self.assertTrue(authority["wrapup.landingGeneratedArtifactPatterns[0]"])
209
- self.assertFalse(authority["workflowAdvisories.baseline.sourceGlobs[0]"])
210
-
211
- def test_advisory_globs_carry_the_case_normalizing_legacy(self):
212
- legacy = {
213
- finding.location: finding.migration.effects
214
- for finding in dialect.scan_profile(self.document)
215
- }
216
- self.assertIn(
217
- dialect.CASE_NARROWS,
218
- legacy["workflowAdvisories.baseline.sourceGlobs[0]"],
219
- )
220
- self.assertNotIn(
221
- dialect.CASE_NARROWS,
222
- legacy["worktreeLifecycle.scratchPatterns[0]"],
223
- )
224
-
225
- def test_malformed_sections_are_skipped_without_raising(self):
226
- self.assertEqual(dialect.scan_profile({"worktreeLifecycle": []}), ())
227
- self.assertEqual(dialect.scan_profile("not a profile"), ())
228
-
229
-
230
- class MigrationReport(unittest.TestCase):
231
- """The review command reports; it never edits the consumer profile."""
232
-
233
- def run_check(self, document, *args):
234
- with tempfile.TemporaryDirectory() as root:
235
- path = Path(root) / "workflow-capabilities.json"
236
- body = json.dumps(document, indent=2) + "\n"
237
- path.write_text(body, encoding="utf-8")
238
- result = subprocess.run(
239
- [sys.executable, str(DIALECT_MODULE), str(path), *args],
240
- capture_output=True, text=True,
241
- )
242
- self.assertEqual(path.read_text(encoding="utf-8"), body)
243
- return result
244
-
245
- def test_stable_profile_exits_zero(self):
246
- result = self.run_check({
247
- "worktreeLifecycle": {"scratchPatterns": ["PLAN.md"]},
248
- })
249
- self.assertEqual(result.returncode, 0, result.stderr)
250
-
251
- def test_changed_pattern_exits_one_and_names_witness_and_authority(self):
252
- result = self.run_check({
253
- "wrapup": {"landingGeneratedArtifactPatterns": ["**/__pycache__/**"]},
254
- })
255
- self.assertEqual(result.returncode, 1, result.stderr)
256
- self.assertIn("wrapup.landingGeneratedArtifactPatterns[0]", result.stdout)
257
- self.assertIn(dialect.WIDENS, result.stdout)
258
- self.assertIn("__pycache__", result.stdout)
259
- self.assertIn("deletion authority", result.stdout)
260
-
261
- def test_json_output_is_machine_readable(self):
262
- result = self.run_check(
263
- {"worktreeLifecycle": {"scratchPatterns": ["*.log"]}}, "--json",
264
- )
265
- report = json.loads(result.stdout)
266
- self.assertEqual(report["reviewed"], 1)
267
- self.assertEqual(report["changed"], 1)
268
- self.assertEqual(report["findings"][0]["effects"], [dialect.NARROWS])
269
-
270
- def test_unreadable_profile_exits_two(self):
271
- result = subprocess.run(
272
- [sys.executable, str(DIALECT_MODULE), "does/not/exist.json"],
273
- capture_output=True, text=True,
274
- )
275
- self.assertEqual(result.returncode, 2)
276
- self.assertNotEqual(result.stderr.strip(), "")
277
-
278
-
279
- if __name__ == "__main__":
280
- unittest.main()
@@ -1,133 +0,0 @@
1
- """Contract tests for the user-visible Program planning route."""
2
-
3
- from pathlib import Path
4
- import unittest
5
-
6
-
7
- ROOT = Path(__file__).resolve().parents[1]
8
-
9
-
10
- class ProgramPlanningContractTest(unittest.TestCase):
11
- def skill(self, name: str) -> str:
12
- return (ROOT / ".claude" / "skills" / name / "SKILL.md").read_text()
13
-
14
- def file(self, path: str) -> str:
15
- return (ROOT / path).read_text()
16
-
17
- def test_to_issues_is_the_public_planning_facade(self):
18
- text = self.skill("to-issues")
19
- self.assertIn("single public Planning facade", text)
20
- self.assertIn("planning-mode=feature", text)
21
- self.assertIn("planning-mode=program", text)
22
- self.assertIn("Missing or contradictory identity", text)
23
- self.assertIn("Size, prose, and model judgment never select the mode", text)
24
-
25
- def test_program_identity_delegates_to_the_internal_graph_engine(self):
26
- issues = self.skill("to-issues")
27
- waves = self.skill("to-waves")
28
- self.assertIn("delegate to the internal", issues)
29
- self.assertIn("`to-waves` graph engine", issues)
30
- self.assertIn("complete Program preview", issues)
31
- self.assertIn("Internal Program graph engine", waves)
32
- self.assertIn("compatibility entrypoint", waves)
33
- self.assertIn("carries both", waves)
34
- self.assertIn("never infer Program mode", waves)
35
-
36
- def test_feature_identity_keeps_the_existing_decomposition_path(self):
37
- text = self.skill("to-issues")
38
- self.assertIn("Feature identity", text)
39
- self.assertIn("PROMOTE", text)
40
- self.assertIn("ATOMIC", text)
41
-
42
- def test_normal_routes_name_only_the_planning_facade(self):
43
- route_files = (
44
- ".claude/skills/ask-matt/SKILL.md",
45
- ".claude/skills/scale-check/SKILL.md",
46
- ".claude/skills/setup-workflow/workflow-overview.md",
47
- ".claude/skills/board-to-waves/SKILL.md",
48
- "CLAUDE.md",
49
- "README.md",
50
- "docs/workflow.html",
51
- "docs/methodology.html",
52
- "docs/methodology.svg",
53
- )
54
- for path in route_files:
55
- with self.subTest(path=path):
56
- text = self.file(path)
57
- self.assertIn("to-issues", text)
58
- self.assertNotIn("→ `to-waves`", text)
59
- self.assertNotIn("→ to-waves", text)
60
-
61
- def test_dual_surface_planning_skills_are_coherent(self):
62
- skill_snippets = (
63
- ("to-issues", "single public Planning facade"),
64
- ("to-waves", "Internal Program graph engine"),
65
- ("ask-matt", "the user never chooses that engine"),
66
- ("scale-check", "explicit Program identity selects the internal graph path"),
67
- ("setup-workflow", "explicit Program identity"),
68
- ("board-to-waves", "not directly to a hidden engine"),
69
- )
70
- for skill, snippet in skill_snippets:
71
- with self.subTest(skill=skill):
72
- claude = self.file(f".claude/skills/{skill}/SKILL.md")
73
- codex = self.file(f".agents/skills/{skill}/SKILL.md")
74
- self.assertIn(snippet, claude)
75
- self.assertIn(snippet, codex)
76
-
77
- support_files = ("workflow-overview.md", "board-sync.md")
78
- for name in support_files:
79
- with self.subTest(support=name):
80
- claude = self.file(f".claude/skills/setup-workflow/{name}")
81
- codex = self.file(f".agents/skills/setup-workflow/{name}")
82
- self.assertEqual(claude, codex)
83
-
84
- def test_to_waves_finishes_with_every_wave_execute_ready_by_default(self):
85
- text = self.skill("to-waves")
86
- self.assertIn("## 7. Program completion contract", text)
87
- self.assertIn("every published wave", text)
88
- self.assertIn("execute-ready-check.py", text)
89
- self.assertIn("X von Y Wellen ausführungsreif", text)
90
-
91
- def test_program_preview_is_the_single_default_user_gate(self):
92
- text = self.skill("to-waves")
93
- self.assertIn("single user approval", text)
94
- self.assertIn("do not ask for another per-wave approval", text)
95
-
96
- def test_unresolved_work_is_modeled_as_an_explicit_gate_not_generic_late_binding(self):
97
- text = self.skill("to-waves")
98
- self.assertIn("Decision Gate", text)
99
- self.assertIn("Verify Spike", text)
100
- self.assertIn("Design-Grill", text)
101
- self.assertIn("Late Binding is not the default", text)
102
-
103
- def test_to_issues_supports_program_batch_and_keeps_human_actions_out_of_afk(self):
104
- text = self.skill("to-issues")
105
- self.assertIn("Program batch handoff", text)
106
- self.assertIn("A mandatory human or external setup action is never AFK", text)
107
-
108
- def test_atomic_program_wave_preserves_its_program_identity(self):
109
- text = self.skill("to-issues")
110
- self.assertIn("Program-batch atomic exception", text)
111
- self.assertIn("preserve its existing Wave and Phase", text)
112
-
113
- def test_atomic_program_wave_never_leaves_a_preliminary_child(self):
114
- waves = self.skill("to-waves")
115
- issues = self.skill("to-issues")
116
- self.assertIn("do not create a preliminary child", waves)
117
- self.assertIn("legacy or interrupted preliminary child", issues)
118
- self.assertIn("superseded by atomic wave leaf", issues)
119
- self.assertIn("Atomic supersession", waves)
120
- self.assertIn("matching source marker remains discoverable after unlink", issues)
121
-
122
- def test_program_completion_treats_audit_findings_as_blocking(self):
123
- text = self.skill("to-waves")
124
- self.assertIn("Program completion gate", text)
125
- self.assertIn("do not count that wave as matured", text)
126
-
127
- def test_public_program_route_promises_execute_ready_waves(self):
128
- text = self.skill("scale-check")
129
- self.assertIn("all waves execute-ready by default", text)
130
-
131
-
132
- if __name__ == "__main__":
133
- unittest.main()
@@ -1,101 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Release-authorization contract (#257).
3
-
4
- The confirmed Semver authorizes the whole release, through tag and publish.
5
- One human gate at version choice, not two — the second gate stalled every
6
- release at `awaiting-tag` until someone returned, which is how 0.34.2 was
7
- skipped and buried under 0.34.3 (#243).
8
-
9
- What this pins:
10
-
11
- - Both `kit-release` surfaces carry the carried-through authorization, and
12
- neither demands a separate confirmation before tagging.
13
- - The safety framing survives the change: irreversibility and the pre-tag
14
- gates stay named, so autonomy never reads as "tag whatever".
15
- - `CLAUDE.md`, `AGENTS.md` and ADR-0004 agree with the skill.
16
-
17
- Run: python3 scripts/test_release_authorization_contract.py
18
- """
19
- from __future__ import annotations
20
-
21
- import unittest
22
- from pathlib import Path
23
-
24
- ROOT = Path(__file__).resolve().parents[1]
25
- SURFACES = (".claude/skills/kit-release/SKILL.md", ".agents/skills/kit-release/SKILL.md")
26
- ADR = "docs/adr/0004-release-intent-is-a-version-tag.md"
27
- DOCTRINE_FILES = ("CLAUDE.md", "AGENTS.md")
28
- # Wording that would reinstate the second gate anywhere in the release surface.
29
- SECOND_GATE = (
30
- "separate explicit confirmation",
31
- "separately confirmed publication intent",
32
- "obtain a separate",
33
- "After that confirmation",
34
- )
35
-
36
-
37
- def read(relative: str) -> str:
38
- return (ROOT / relative).read_text(encoding="utf-8")
39
-
40
-
41
- def normalized(relative: str) -> str:
42
- return " ".join(read(relative).split())
43
-
44
-
45
- class SkillSurfaceContract(unittest.TestCase):
46
- def test_both_surfaces_carry_the_authorization_through_to_the_tag(self):
47
- for surface in SURFACES:
48
- with self.subTest(surface=surface):
49
- prose = normalized(surface)
50
- self.assertIn("confirmed Semver authorizes", prose)
51
- self.assertIn("tag and publish", prose)
52
- self.assertIn("without asking again", prose)
53
-
54
- def test_no_surface_reinstates_a_second_confirmation_gate(self):
55
- for surface in SURFACES:
56
- for phrase in SECOND_GATE:
57
- with self.subTest(surface=surface, phrase=phrase):
58
- self.assertNotIn(phrase, normalized(surface))
59
-
60
- def test_autonomy_never_drops_the_safety_framing(self):
61
- """Tagging stays irreversible and gated — the gates just move earlier."""
62
- for surface in SURFACES:
63
- with self.subTest(surface=surface):
64
- prose = normalized(surface)
65
- self.assertIn("irreversible", prose)
66
- self.assertIn("cannot be reused", prose)
67
- for gate in ("release:guard", "kit:staleness", "npm pack"):
68
- self.assertIn(gate, prose)
69
-
70
- def test_the_tag_remains_the_sole_publication_intent(self):
71
- for surface in SURFACES:
72
- with self.subTest(surface=surface):
73
- prose = normalized(surface)
74
- self.assertIn("Merging integrates", prose)
75
- self.assertIn("annotated", prose)
76
-
77
- def test_both_surfaces_stay_mirrored_below_the_frontmatter(self):
78
- bodies = [read(surface).split("\n---\n", 1)[1] for surface in SURFACES]
79
- self.assertEqual(bodies[0], bodies[1])
80
-
81
-
82
- class ProjectDoctrineContract(unittest.TestCase):
83
- def test_root_convention_files_agree_with_the_skill(self):
84
- for name in DOCTRINE_FILES:
85
- with self.subTest(document=name):
86
- prose = normalized(name)
87
- self.assertIn("confirmed Semver authorizes", prose)
88
- for phrase in SECOND_GATE:
89
- self.assertNotIn(phrase, prose)
90
-
91
- def test_the_adr_records_an_amendment_instead_of_a_silent_rewrite(self):
92
- prose = normalized(ADR)
93
- self.assertIn("Amended", prose)
94
- self.assertIn("2026-07-25", prose)
95
- self.assertIn("confirmed Semver authorizes", prose)
96
- # The original decision must remain readable, not be overwritten.
97
- self.assertIn("version tag", prose)
98
-
99
-
100
- if __name__ == "__main__":
101
- unittest.main()