@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.
- package/.agents/skills/grill-me/SKILL.md +3 -1
- package/.agents/skills/grill-with-docs/SKILL.md +3 -1
- package/.agents/skills/kit-update/SKILL.md +9 -11
- package/.agents/skills/orchestrate-wave/SKILL.md +8 -8
- package/.agents/skills/setup-workflow/SKILL.md +20 -18
- package/.agents/skills/setup-workflow/board-sync.md +4 -1
- package/.agents/skills/setup-workflow/workflow-advisories.md +3 -2
- package/.agents/skills/setup-workflow/worktree-lifecycle.md +36 -31
- package/.agents/skills/to-issues/SKILL.md +43 -0
- package/.agents/skills/wrapup/SKILL.md +98 -54
- package/.claude/skills/grill-me/SKILL.md +3 -1
- package/.claude/skills/grill-me-codex/SKILL.md +3 -1
- package/.claude/skills/grill-with-docs/SKILL.md +3 -1
- package/.claude/skills/grill-with-docs-codex/SKILL.md +3 -1
- package/.claude/skills/kit-update/SKILL.md +9 -11
- package/.claude/skills/orchestrate-wave/SKILL.md +8 -8
- package/.claude/skills/setup-workflow/SKILL.md +20 -18
- package/.claude/skills/setup-workflow/board-sync.md +4 -1
- package/.claude/skills/setup-workflow/workflow-advisories.md +3 -2
- package/.claude/skills/setup-workflow/worktree-lifecycle.md +36 -31
- package/.claude/skills/to-issues/SKILL.md +43 -0
- package/.claude/skills/wrapup/SKILL.md +98 -54
- package/README.md +249 -7
- package/agent-workflow-kit.package.json +42 -42
- package/package.json +16 -12
- package/scripts/marker_lib.py +3 -0
- package/scripts/profile_globs.py +26 -45
- package/scripts/release-delta-guard.mjs +26 -9
- package/scripts/worktree-lifecycle/README.md +57 -160
- package/scripts/worktree-lifecycle/capabilities.json +2 -3
- package/scripts/worktree-lifecycle/classify.py +480 -0
- package/scripts/worktree-lifecycle/cleanup.py +9 -29
- package/scripts/worktree-lifecycle/core.py +68 -1429
- package/scripts/worktree-lifecycle/ignore_seed.py +1 -1
- package/scripts/worktree-lifecycle/profile.py +46 -29
- package/scripts/worktree-lifecycle/setup.py +7 -19
- package/scripts/wrapup-land.py +1292 -545
- package/src/consumer-migrations.json +1 -16
- package/src/lib/bundle.mjs +31 -1
- package/docs/adr/0001-consumer-divergence-policy.md +0 -53
- package/docs/adr/0002-capability-gated-orchestration.md +0 -70
- package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +0 -63
- package/docs/adr/0004-release-intent-is-a-version-tag.md +0 -64
- package/docs/adr/0005-to-issues-is-the-planning-facade.md +0 -42
- package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +0 -95
- package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +0 -88
- package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +0 -84
- package/docs/adr/0009-teardown-authority-is-stateless-repository-classification.md +0 -101
- package/docs/adr/0010-model-roster-replaces-the-optimization-dial.md +0 -87
- package/docs/agents/board-sync.md +0 -74
- package/docs/agents/code-review.md +0 -14
- package/docs/agents/workflow-capabilities.json +0 -28
- package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
- package/docs/research/benchlm-routing-source.md +0 -198
- package/docs/research/consumer-owned-protocol-files.md +0 -238
- package/docs/research/frontend-agent-benchmarks.md +0 -282
- package/docs/research/model-effort-routing-benchmarks.md +0 -261
- package/docs/research/provider-neutral-agent-routing.md +0 -207
- package/docs/research/wave-152-consumer-acceptance.md +0 -98
- package/docs/research/wave-43-script-hook-census.md +0 -128
- package/scripts/build-kit.mjs +0 -117
- package/scripts/build-kit.test.mjs +0 -207
- package/scripts/census/census.test.mjs +0 -252
- package/scripts/census/state.test.mjs +0 -15
- package/scripts/census/transaction.test.mjs +0 -103
- package/scripts/census-contract.test.mjs +0 -227
- package/scripts/check-kit-staleness.mjs +0 -45
- package/scripts/check-kit-staleness.test.mjs +0 -19
- package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -165
- package/scripts/codex-exec.test.mjs +0 -871
- package/scripts/grill-census-wiring-guard.mjs +0 -87
- package/scripts/grill-census-wiring-guard.test.mjs +0 -107
- package/scripts/kit-release.test.mjs +0 -273
- package/scripts/kit-update-pr.test.mjs +0 -151
- package/scripts/lib/audit-refs.mjs +0 -146
- package/scripts/lib/scrub.mjs +0 -113
- package/scripts/lib/scrub.test.mjs +0 -187
- package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
- package/scripts/portability_profile_scan.py +0 -53
- package/scripts/release-delta-guard.test.mjs +0 -174
- package/scripts/release-parity.test.mjs +0 -90
- package/scripts/release-state.test.mjs +0 -360
- package/scripts/test_anchor_table.py +0 -69
- package/scripts/test_board_bootstrap.py +0 -348
- package/scripts/test_board_sync.py +0 -208
- package/scripts/test_board_sync_create_idempotency.py +0 -117
- package/scripts/test_board_sync_wave_title.py +0 -120
- package/scripts/test_census_backstop.py +0 -712
- package/scripts/test_census_forward_contract.py +0 -91
- package/scripts/test_census_update_contract.test.mjs +0 -454
- package/scripts/test_codex_adapter_sync_contract.py +0 -169
- package/scripts/test_dist_kit_smoke.py +0 -220
- package/scripts/test_drift_guard_diagnostics.py +0 -295
- package/scripts/test_issue_claim_contract.py +0 -169
- package/scripts/test_kit_docs_language_census.py +0 -178
- package/scripts/test_marker_lib.py +0 -162
- package/scripts/test_orchestrate_wave_contract.py +0 -381
- package/scripts/test_pr_body_check.py +0 -264
- package/scripts/test_profile_globs.py +0 -280
- package/scripts/test_program_planning_contract.py +0 -133
- package/scripts/test_release_authorization_contract.py +0 -101
- package/scripts/test_render_anchor.py +0 -267
- package/scripts/test_retro_wrapup_contract.py +0 -153
- package/scripts/test_skill_code_review_seed.py +0 -89
- package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
- package/scripts/test_skill_frontmatter_lint.py +0 -234
- package/scripts/test_skill_gh_lint.py +0 -124
- package/scripts/test_skill_language_census.py +0 -254
- package/scripts/test_skill_optional_readiness.py +0 -171
- package/scripts/test_skill_portability_lint.py +0 -866
- package/scripts/test_skill_precommit_template.py +0 -30
- package/scripts/test_skill_publish_audit.py +0 -358
- package/scripts/test_skill_readiness_contract.py +0 -264
- package/scripts/test_skill_readiness_preflight.py +0 -180
- package/scripts/test_skill_required_readiness.py +0 -233
- package/scripts/test_skill_selfcontainment_lint.py +0 -255
- package/scripts/test_skill_setup_workflow_seeds.py +0 -691
- package/scripts/test_skill_stale_name_lint.py +0 -126
- package/scripts/test_skill_surface_refs.py +0 -300
- package/scripts/test_skill_trailing_artifact_lint.py +0 -71
- package/scripts/test_tdd_contract.py +0 -78
- package/scripts/test_worktree_ignore_seed.py +0 -320
- package/scripts/test_worktree_setup_base_guard.py +0 -140
- package/scripts/test_worktree_wrapup_contract.py +0 -1737
- package/scripts/test_wrapup_land.py +0 -428
- 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()
|