@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,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()