@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,320 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Consumer ignore-gap contract (#255).
|
|
3
|
-
|
|
4
|
-
Shipped skills write `PLAN.md`, `PLAN-REVIEW-LOG.md`, and `ANNAHMEN.md` into a
|
|
5
|
-
session worktree, but `.gitignore` is a consumer file the kit does not own, so
|
|
6
|
-
`init`/`update` never seed the matching rules. ADR 0008 resolves the gap two
|
|
7
|
-
ways, and both are pinned here:
|
|
8
|
-
|
|
9
|
-
1. `/setup-workflow` may **offer** the rules through
|
|
10
|
-
`scripts/worktree-lifecycle/ignore_seed.py`. The helper is append-only
|
|
11
|
-
inside one idempotent marker block: it never rewrites, reorders, or removes
|
|
12
|
-
an existing line, it is a no-op on re-run, and it is unreachable from
|
|
13
|
-
`init`/`update` reconciliation.
|
|
14
|
-
2. Shipped skill prose states the assumption instead of asserting the ignore
|
|
15
|
-
state as a fact the kit cannot guarantee.
|
|
16
|
-
|
|
17
|
-
Run: python3 scripts/test_worktree_ignore_seed.py
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
from __future__ import annotations
|
|
21
|
-
|
|
22
|
-
import importlib.util
|
|
23
|
-
import json
|
|
24
|
-
import re
|
|
25
|
-
import subprocess
|
|
26
|
-
import sys
|
|
27
|
-
import tempfile
|
|
28
|
-
import unittest
|
|
29
|
-
from pathlib import Path
|
|
30
|
-
|
|
31
|
-
REPO = Path(__file__).resolve().parent.parent
|
|
32
|
-
LIFECYCLE = REPO / "scripts/worktree-lifecycle"
|
|
33
|
-
HELPER = LIFECYCLE / "ignore_seed.py"
|
|
34
|
-
ARTIFACT_MANIFEST = LIFECYCLE / "plan-artifacts.json"
|
|
35
|
-
SKILL_TREES = (REPO / ".claude/skills", REPO / ".agents/skills")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def load_helper():
|
|
39
|
-
spec = importlib.util.spec_from_file_location("wl_ignore_seed", HELPER)
|
|
40
|
-
module = importlib.util.module_from_spec(spec)
|
|
41
|
-
sys.modules[spec.name] = module
|
|
42
|
-
spec.loader.exec_module(module)
|
|
43
|
-
return module
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def git(repo: Path, *args: str) -> subprocess.CompletedProcess:
|
|
47
|
-
return subprocess.run(
|
|
48
|
-
["git", *args], cwd=repo, capture_output=True, text=True, check=True,
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def make_repo(stack, gitignore: str | None = None) -> Path:
|
|
53
|
-
repo = Path(stack.enter_context(tempfile.TemporaryDirectory()))
|
|
54
|
-
git(repo, "init", "-q")
|
|
55
|
-
git(repo, "config", "user.email", "test@example.com")
|
|
56
|
-
git(repo, "config", "user.name", "test")
|
|
57
|
-
if gitignore is not None:
|
|
58
|
-
(repo / ".gitignore").write_text(gitignore, encoding="utf-8")
|
|
59
|
-
return repo
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class ArtifactManifestTest(unittest.TestCase):
|
|
63
|
-
"""The kit declares which planning artifacts its own skills write."""
|
|
64
|
-
|
|
65
|
-
def test_manifest_declares_the_three_planning_artifacts(self):
|
|
66
|
-
document = json.loads(ARTIFACT_MANIFEST.read_text(encoding="utf-8"))
|
|
67
|
-
paths = [entry["path"] for entry in document["artifacts"]]
|
|
68
|
-
self.assertEqual(
|
|
69
|
-
paths, ["PLAN.md", "PLAN-REVIEW-LOG.md", "ANNAHMEN.md"],
|
|
70
|
-
)
|
|
71
|
-
for entry in document["artifacts"]:
|
|
72
|
-
self.assertTrue(entry["writtenBy"], entry["path"])
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class PlanTest(unittest.TestCase):
|
|
76
|
-
"""`plan()` reports the exact append, never a rewrite."""
|
|
77
|
-
|
|
78
|
-
def setUp(self):
|
|
79
|
-
self.helper = load_helper()
|
|
80
|
-
self.stack = __import__("contextlib").ExitStack()
|
|
81
|
-
self.addCleanup(self.stack.close)
|
|
82
|
-
|
|
83
|
-
def test_fresh_repo_lists_every_artifact_as_pending(self):
|
|
84
|
-
repo = make_repo(self.stack, "node_modules/\n")
|
|
85
|
-
plan = self.helper.plan(repo)
|
|
86
|
-
self.assertEqual(
|
|
87
|
-
plan.pending, ("PLAN.md", "PLAN-REVIEW-LOG.md", "ANNAHMEN.md"),
|
|
88
|
-
)
|
|
89
|
-
self.assertEqual(plan.already_ignored, ())
|
|
90
|
-
self.assertEqual(plan.status, "append")
|
|
91
|
-
for path in plan.pending:
|
|
92
|
-
self.assertIn(f"\n{path}\n", plan.block)
|
|
93
|
-
self.assertTrue(plan.block.startswith(self.helper.BLOCK_START))
|
|
94
|
-
self.assertTrue(plan.block.rstrip("\n").endswith(self.helper.BLOCK_END))
|
|
95
|
-
|
|
96
|
-
def test_missing_gitignore_is_reported_as_a_create(self):
|
|
97
|
-
repo = make_repo(self.stack)
|
|
98
|
-
plan = self.helper.plan(repo)
|
|
99
|
-
self.assertEqual(plan.status, "append")
|
|
100
|
-
self.assertFalse(plan.gitignore_exists)
|
|
101
|
-
|
|
102
|
-
def test_consumer_rules_already_covering_everything_are_nothing_to_do(self):
|
|
103
|
-
repo = make_repo(
|
|
104
|
-
self.stack, "PLAN.md\nPLAN-REVIEW-LOG.md\nANNAHMEN.md\n",
|
|
105
|
-
)
|
|
106
|
-
plan = self.helper.plan(repo)
|
|
107
|
-
self.assertEqual(plan.pending, ())
|
|
108
|
-
self.assertEqual(plan.status, "nothing-to-do")
|
|
109
|
-
self.assertIsNone(plan.block)
|
|
110
|
-
|
|
111
|
-
def test_partial_coverage_pends_only_the_missing_rules(self):
|
|
112
|
-
repo = make_repo(self.stack, "PLAN.md\n")
|
|
113
|
-
plan = self.helper.plan(repo)
|
|
114
|
-
self.assertEqual(plan.pending, ("PLAN-REVIEW-LOG.md", "ANNAHMEN.md"))
|
|
115
|
-
self.assertEqual(plan.already_ignored, ("PLAN.md",))
|
|
116
|
-
self.assertNotIn("\nPLAN.md\n", plan.block)
|
|
117
|
-
|
|
118
|
-
def test_a_wildcard_consumer_rule_counts_as_covered(self):
|
|
119
|
-
repo = make_repo(self.stack, "*.md\n")
|
|
120
|
-
plan = self.helper.plan(repo)
|
|
121
|
-
self.assertEqual(plan.pending, ())
|
|
122
|
-
|
|
123
|
-
def test_a_tracked_artifact_is_named_because_a_rule_cannot_untrack_it(self):
|
|
124
|
-
repo = make_repo(self.stack, "node_modules/\n")
|
|
125
|
-
(repo / "PLAN.md").write_text("plan\n", encoding="utf-8")
|
|
126
|
-
git(repo, "add", "PLAN.md")
|
|
127
|
-
git(repo, "commit", "-qm", "add plan")
|
|
128
|
-
plan = self.helper.plan(repo)
|
|
129
|
-
self.assertEqual(plan.tracked, ("PLAN.md",))
|
|
130
|
-
self.assertIn("PLAN.md", plan.pending)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
class ApplyTest(unittest.TestCase):
|
|
134
|
-
"""`apply()` is append-only, idempotent, and never rewrites."""
|
|
135
|
-
|
|
136
|
-
def setUp(self):
|
|
137
|
-
self.helper = load_helper()
|
|
138
|
-
self.stack = __import__("contextlib").ExitStack()
|
|
139
|
-
self.addCleanup(self.stack.close)
|
|
140
|
-
|
|
141
|
-
def test_apply_appends_and_preserves_the_existing_bytes_verbatim(self):
|
|
142
|
-
original = "node_modules/\n\n# my rules\ndist/\n"
|
|
143
|
-
repo = make_repo(self.stack, original)
|
|
144
|
-
result = self.helper.apply(repo)
|
|
145
|
-
self.assertEqual(result.status, "appended")
|
|
146
|
-
text = (repo / ".gitignore").read_text(encoding="utf-8")
|
|
147
|
-
self.assertTrue(text.startswith(original))
|
|
148
|
-
self.assertIn(self.helper.BLOCK_START, text)
|
|
149
|
-
for path in ("PLAN.md", "PLAN-REVIEW-LOG.md", "ANNAHMEN.md"):
|
|
150
|
-
self.assertIn(f"\n{path}\n", text)
|
|
151
|
-
|
|
152
|
-
def test_rerun_is_a_byte_identical_no_op(self):
|
|
153
|
-
repo = make_repo(self.stack, "node_modules/\n")
|
|
154
|
-
self.helper.apply(repo)
|
|
155
|
-
first = (repo / ".gitignore").read_bytes()
|
|
156
|
-
second_result = self.helper.apply(repo)
|
|
157
|
-
self.assertEqual(second_result.status, "nothing-to-do")
|
|
158
|
-
self.assertEqual((repo / ".gitignore").read_bytes(), first)
|
|
159
|
-
self.assertEqual(first.decode().count(self.helper.BLOCK_START), 1)
|
|
160
|
-
|
|
161
|
-
def test_decline_path_writes_nothing(self):
|
|
162
|
-
repo = make_repo(self.stack, "node_modules/\n")
|
|
163
|
-
before = (repo / ".gitignore").read_bytes()
|
|
164
|
-
self.helper.plan(repo)
|
|
165
|
-
self.assertEqual((repo / ".gitignore").read_bytes(), before)
|
|
166
|
-
|
|
167
|
-
def test_already_covered_repo_is_left_untouched(self):
|
|
168
|
-
original = "PLAN.md\nPLAN-REVIEW-LOG.md\nANNAHMEN.md\n"
|
|
169
|
-
repo = make_repo(self.stack, original)
|
|
170
|
-
result = self.helper.apply(repo)
|
|
171
|
-
self.assertEqual(result.status, "nothing-to-do")
|
|
172
|
-
self.assertEqual(
|
|
173
|
-
(repo / ".gitignore").read_text(encoding="utf-8"), original,
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
def test_missing_gitignore_is_created_with_only_the_block(self):
|
|
177
|
-
repo = make_repo(self.stack)
|
|
178
|
-
self.helper.apply(repo)
|
|
179
|
-
text = (repo / ".gitignore").read_text(encoding="utf-8")
|
|
180
|
-
self.assertTrue(text.startswith(self.helper.BLOCK_START))
|
|
181
|
-
|
|
182
|
-
def test_an_edited_marker_block_blocks_instead_of_rewriting(self):
|
|
183
|
-
repo = make_repo(self.stack)
|
|
184
|
-
self.helper.apply(repo)
|
|
185
|
-
text = (repo / ".gitignore").read_text(encoding="utf-8")
|
|
186
|
-
edited = text.replace("ANNAHMEN.md\n", "")
|
|
187
|
-
(repo / ".gitignore").write_text(edited, encoding="utf-8")
|
|
188
|
-
result = self.helper.apply(repo)
|
|
189
|
-
self.assertEqual(result.status, "blocked")
|
|
190
|
-
self.assertEqual(
|
|
191
|
-
(repo / ".gitignore").read_text(encoding="utf-8"), edited,
|
|
192
|
-
)
|
|
193
|
-
self.assertEqual(edited.count(self.helper.BLOCK_START), 1)
|
|
194
|
-
|
|
195
|
-
def test_cli_preview_writes_nothing_and_reports_json(self):
|
|
196
|
-
repo = make_repo(self.stack, "node_modules/\n")
|
|
197
|
-
before = (repo / ".gitignore").read_bytes()
|
|
198
|
-
result = subprocess.run(
|
|
199
|
-
[sys.executable, str(HELPER), "preview", "--repo", str(repo), "--json"],
|
|
200
|
-
capture_output=True, text=True,
|
|
201
|
-
)
|
|
202
|
-
self.assertEqual(result.returncode, 0, result.stderr)
|
|
203
|
-
payload = json.loads(result.stdout)
|
|
204
|
-
self.assertEqual(payload["status"], "append")
|
|
205
|
-
self.assertEqual((repo / ".gitignore").read_bytes(), before)
|
|
206
|
-
|
|
207
|
-
def test_cli_apply_exits_zero_and_is_idempotent(self):
|
|
208
|
-
repo = make_repo(self.stack, "node_modules/\n")
|
|
209
|
-
for _ in range(2):
|
|
210
|
-
result = subprocess.run(
|
|
211
|
-
[sys.executable, str(HELPER), "apply", "--repo", str(repo)],
|
|
212
|
-
capture_output=True, text=True,
|
|
213
|
-
)
|
|
214
|
-
self.assertEqual(result.returncode, 0, result.stderr)
|
|
215
|
-
text = (repo / ".gitignore").read_text(encoding="utf-8")
|
|
216
|
-
self.assertEqual(text.count(self.helper.BLOCK_START), 1)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
class ReconciliationBoundaryTest(unittest.TestCase):
|
|
220
|
-
"""Only setup-workflow may reach the seeder; never init/update."""
|
|
221
|
-
|
|
222
|
-
def test_no_installer_command_invokes_the_seeder(self):
|
|
223
|
-
# bundle.mjs only DECLARES the file as shipped; every other installer
|
|
224
|
-
# module must not know the seeder exists.
|
|
225
|
-
declaration = REPO / "src/lib/bundle.mjs"
|
|
226
|
-
offenders = []
|
|
227
|
-
for path in sorted((REPO / "src").rglob("*.mjs")):
|
|
228
|
-
if path == declaration:
|
|
229
|
-
continue
|
|
230
|
-
if "ignore_seed" in path.read_text(encoding="utf-8"):
|
|
231
|
-
offenders.append(str(path.relative_to(REPO)))
|
|
232
|
-
self.assertEqual(offenders, [])
|
|
233
|
-
|
|
234
|
-
def test_the_seeder_ships_with_the_kit(self):
|
|
235
|
-
bundle = (REPO / "src/lib/bundle.mjs").read_text(encoding="utf-8")
|
|
236
|
-
self.assertIn("scripts/worktree-lifecycle/ignore_seed.py", bundle)
|
|
237
|
-
self.assertIn("scripts/worktree-lifecycle/plan-artifacts.json", bundle)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
class SetupWorkflowOfferTest(unittest.TestCase):
|
|
241
|
-
"""The offer is an explicit, previewed, declinable setup step."""
|
|
242
|
-
|
|
243
|
-
def _skill(self, tree: Path) -> str:
|
|
244
|
-
return (tree / "setup-workflow/SKILL.md").read_text(encoding="utf-8")
|
|
245
|
-
|
|
246
|
-
def _seed(self, tree: Path) -> str:
|
|
247
|
-
return (tree / "setup-workflow/worktree-lifecycle.md").read_text(
|
|
248
|
-
encoding="utf-8",
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
def test_both_surfaces_document_the_previewed_offer(self):
|
|
252
|
-
for tree in SKILL_TREES:
|
|
253
|
-
body = self._skill(tree)
|
|
254
|
-
self.assertIn(
|
|
255
|
-
"python3 scripts/worktree-lifecycle/ignore_seed.py preview", body,
|
|
256
|
-
)
|
|
257
|
-
self.assertIn(
|
|
258
|
-
"python3 scripts/worktree-lifecycle/ignore_seed.py apply", body,
|
|
259
|
-
)
|
|
260
|
-
flat = re.sub(r"\s+", " ", body)
|
|
261
|
-
self.assertIn("Add the rules", flat)
|
|
262
|
-
self.assertIn("Not now", flat)
|
|
263
|
-
|
|
264
|
-
def test_the_seed_contract_carries_the_ignore_offer_matrix(self):
|
|
265
|
-
for tree in SKILL_TREES:
|
|
266
|
-
flat = re.sub(r"\s+", " ", self._seed(tree))
|
|
267
|
-
self.assertIn("ignore_seed.py", flat)
|
|
268
|
-
for row in ("approve", "decline", "already ignored", "re-run"):
|
|
269
|
-
self.assertIn(row, flat)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
class ProseClaimCensusTest(unittest.TestCase):
|
|
273
|
-
"""No shipped skill asserts an ignore state the kit cannot guarantee."""
|
|
274
|
-
|
|
275
|
-
ARTIFACT = re.compile(
|
|
276
|
-
r"PLAN\.md|PLAN-REVIEW-LOG\.md|ANNAHMEN\.md|plan doc|assumptions log",
|
|
277
|
-
re.IGNORECASE,
|
|
278
|
-
)
|
|
279
|
-
# Unconditional assertions of the ignore state. Conditional or hedged
|
|
280
|
-
# wording ("a project may gitignore these files", "when `PLAN.md` is
|
|
281
|
-
# ignored") is deliberately allowed.
|
|
282
|
-
FORBIDDEN = (
|
|
283
|
-
re.compile(r"\(gitignored\b", re.IGNORECASE),
|
|
284
|
-
re.compile(r"\b(?:is|are|stays?|remains?)\s+gitignored\b", re.IGNORECASE),
|
|
285
|
-
re.compile(r"\bgitignored\s+(?:at|in|since)\b", re.IGNORECASE),
|
|
286
|
-
re.compile(r"\ba\s+gitignored\s+plan\s+doc\b", re.IGNORECASE),
|
|
287
|
-
re.compile(r"\bgitignored,\s*on-disk only\b", re.IGNORECASE),
|
|
288
|
-
)
|
|
289
|
-
WINDOW = 160
|
|
290
|
-
|
|
291
|
-
def _claim_offenders(self):
|
|
292
|
-
offenders = []
|
|
293
|
-
for tree in SKILL_TREES:
|
|
294
|
-
for path in sorted(tree.rglob("*.md")):
|
|
295
|
-
flat = re.sub(r"\s+", " ", path.read_text(encoding="utf-8"))
|
|
296
|
-
for pattern in self.FORBIDDEN:
|
|
297
|
-
for match in pattern.finditer(flat):
|
|
298
|
-
window = flat[
|
|
299
|
-
max(0, match.start() - self.WINDOW):
|
|
300
|
-
match.end() + self.WINDOW
|
|
301
|
-
]
|
|
302
|
-
if self.ARTIFACT.search(window):
|
|
303
|
-
offenders.append(
|
|
304
|
-
f"{path.relative_to(REPO)}: {match.group(0)}"
|
|
305
|
-
)
|
|
306
|
-
return sorted(set(offenders))
|
|
307
|
-
|
|
308
|
-
def test_no_skill_states_the_ignore_rule_as_an_installed_fact(self):
|
|
309
|
-
self.assertEqual(self._claim_offenders(), [])
|
|
310
|
-
|
|
311
|
-
def test_the_replacement_wording_names_who_can_make_it_true(self):
|
|
312
|
-
for name in ("grill-me", "grill-with-docs", "orchestrate-wave"):
|
|
313
|
-
for tree in SKILL_TREES:
|
|
314
|
-
body = (tree / name / "SKILL.md").read_text(encoding="utf-8")
|
|
315
|
-
flat = re.sub(r"\s+", " ", body)
|
|
316
|
-
self.assertIn("setup-workflow", flat, f"{tree}/{name}")
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if __name__ == "__main__":
|
|
320
|
-
unittest.main()
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Worktree reuse must refuse a stale base on BOTH reuse paths.
|
|
3
|
-
|
|
4
|
-
`setup.py create()` can reach an existing worktree two ways: the target is
|
|
5
|
-
already a registered worktree, or only its branch exists locally and
|
|
6
|
-
`git worktree add <target> <branch>` silently ignores `--base`. Reuse is safe
|
|
7
|
-
only when the reused HEAD is AT the base or cleanly behind it (fast-forwardable);
|
|
8
|
-
anything ahead or diverged builds the slice on a stale base and must STOP.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import importlib.util
|
|
12
|
-
import os
|
|
13
|
-
import subprocess
|
|
14
|
-
import sys
|
|
15
|
-
import tempfile
|
|
16
|
-
import unittest
|
|
17
|
-
from argparse import Namespace
|
|
18
|
-
from contextlib import contextmanager
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
|
-
REPO = Path(__file__).resolve().parent.parent
|
|
22
|
-
LIFECYCLE = REPO / "scripts/worktree-lifecycle"
|
|
23
|
-
|
|
24
|
-
PROFILE = """{
|
|
25
|
-
"worktreeLifecycle": {
|
|
26
|
-
"enabled": true,
|
|
27
|
-
"worktreeRoot": ".worktrees",
|
|
28
|
-
"branchTemplate": "{type}/{issue}-{slug}",
|
|
29
|
-
"pathTemplate": "{issue}-{slug}",
|
|
30
|
-
"setupSteps": []
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def load_setup():
|
|
37
|
-
sys.path.insert(0, str(LIFECYCLE))
|
|
38
|
-
try:
|
|
39
|
-
spec = importlib.util.spec_from_file_location(
|
|
40
|
-
"worktree_setup_base_guard", LIFECYCLE / "setup.py"
|
|
41
|
-
)
|
|
42
|
-
module = importlib.util.module_from_spec(spec)
|
|
43
|
-
spec.loader.exec_module(module)
|
|
44
|
-
return module
|
|
45
|
-
finally:
|
|
46
|
-
sys.path.remove(str(LIFECYCLE))
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def git(repo: Path, *args: str) -> str:
|
|
50
|
-
result = subprocess.run(
|
|
51
|
-
["git", *args], cwd=repo, capture_output=True, text=True, check=True
|
|
52
|
-
)
|
|
53
|
-
return result.stdout.strip()
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
@contextmanager
|
|
57
|
-
def fixture():
|
|
58
|
-
"""A repo whose `main` carries two commits: base_first, then head."""
|
|
59
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
60
|
-
repo = Path(tmp).resolve() / "repo"
|
|
61
|
-
repo.mkdir()
|
|
62
|
-
git(repo, "init", "--initial-branch=main")
|
|
63
|
-
git(repo, "config", "user.email", "test@example.invalid")
|
|
64
|
-
git(repo, "config", "user.name", "Test User")
|
|
65
|
-
profile = repo / "docs/agents/workflow-capabilities.json"
|
|
66
|
-
profile.parent.mkdir(parents=True)
|
|
67
|
-
profile.write_text(PROFILE, encoding="utf-8")
|
|
68
|
-
(repo / "README.md").write_text("# fixture\n", encoding="utf-8")
|
|
69
|
-
git(repo, "add", "-A")
|
|
70
|
-
git(repo, "commit", "-m", "first")
|
|
71
|
-
first = git(repo, "rev-parse", "HEAD")
|
|
72
|
-
(repo / "README.md").write_text("# fixture 2\n", encoding="utf-8")
|
|
73
|
-
git(repo, "commit", "-am", "second")
|
|
74
|
-
second = git(repo, "rev-parse", "HEAD")
|
|
75
|
-
yield repo, first, second
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@contextmanager
|
|
79
|
-
def chdir(path: Path):
|
|
80
|
-
previous = Path.cwd()
|
|
81
|
-
os.chdir(path)
|
|
82
|
-
try:
|
|
83
|
-
yield
|
|
84
|
-
finally:
|
|
85
|
-
os.chdir(previous)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def args_for(base: str) -> Namespace:
|
|
89
|
-
return Namespace(
|
|
90
|
-
profile="docs/agents/workflow-capabilities.json",
|
|
91
|
-
base=base,
|
|
92
|
-
issue="173",
|
|
93
|
-
slug="wave-safety",
|
|
94
|
-
branch_type="feat",
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
class WorktreeReuseBaseGuard(unittest.TestCase):
|
|
99
|
-
def test_registered_worktree_reuse_stops_when_head_is_not_at_base(self):
|
|
100
|
-
setup = load_setup()
|
|
101
|
-
with fixture() as (repo, first, second):
|
|
102
|
-
target = repo / ".worktrees/173-wave-safety"
|
|
103
|
-
git(repo, "worktree", "add", "-b", "feat/173-wave-safety", str(target), second)
|
|
104
|
-
# base is the OLDER commit -> the registered worktree is ahead of it.
|
|
105
|
-
with chdir(repo):
|
|
106
|
-
with self.assertRaises(setup.LifecycleError) as stopped:
|
|
107
|
-
setup.create(args_for(first))
|
|
108
|
-
self.assertIn("stale", str(stopped.exception).lower())
|
|
109
|
-
|
|
110
|
-
def test_existing_branch_reuse_stops_when_branch_is_not_at_base(self):
|
|
111
|
-
setup = load_setup()
|
|
112
|
-
with fixture() as (repo, first, second):
|
|
113
|
-
git(repo, "branch", "feat/173-wave-safety", second)
|
|
114
|
-
target = repo / ".worktrees/173-wave-safety"
|
|
115
|
-
with chdir(repo):
|
|
116
|
-
with self.assertRaises(setup.LifecycleError) as stopped:
|
|
117
|
-
setup.create(args_for(first))
|
|
118
|
-
self.assertIn("stale", str(stopped.exception).lower())
|
|
119
|
-
self.assertFalse(target.exists(), "no worktree may be created on a stale base")
|
|
120
|
-
|
|
121
|
-
def test_registered_worktree_reuse_allows_head_at_base(self):
|
|
122
|
-
setup = load_setup()
|
|
123
|
-
with fixture() as (repo, _first, second):
|
|
124
|
-
target = repo / ".worktrees/173-wave-safety"
|
|
125
|
-
git(repo, "worktree", "add", "-b", "feat/173-wave-safety", str(target), second)
|
|
126
|
-
with chdir(repo):
|
|
127
|
-
self.assertEqual(setup.create(args_for(second)).resolve(), target.resolve())
|
|
128
|
-
|
|
129
|
-
def test_existing_branch_reuse_allows_a_branch_cleanly_behind_base(self):
|
|
130
|
-
setup = load_setup()
|
|
131
|
-
with fixture() as (repo, first, second):
|
|
132
|
-
git(repo, "branch", "feat/173-wave-safety", first)
|
|
133
|
-
target = repo / ".worktrees/173-wave-safety"
|
|
134
|
-
with chdir(repo):
|
|
135
|
-
self.assertEqual(setup.create(args_for(second)).resolve(), target.resolve())
|
|
136
|
-
self.assertTrue(target.is_dir())
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if __name__ == "__main__":
|
|
140
|
-
unittest.main()
|