@ikon85/agent-workflow-kit 0.42.1 → 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 (133) 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 +268 -7
  24. package/agent-workflow-kit.package.json +42 -42
  25. package/package.json +16 -2
  26. package/scripts/marker_lib.py +3 -0
  27. package/scripts/profile_globs.py +26 -45
  28. package/scripts/release-delta-guard.mjs +29 -4
  29. package/scripts/worktree-lifecycle/README.md +57 -154
  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 -1289
  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/domain.md +0 -7
  53. package/docs/agents/issue-tracker.md +0 -23
  54. package/docs/agents/skills/local-ci.md +0 -90
  55. package/docs/agents/skills/orchestrate-wave.md +0 -166
  56. package/docs/agents/skills/spec-self-critique.md +0 -18
  57. package/docs/agents/triage-labels.md +0 -10
  58. package/docs/agents/workflow-capabilities.json +0 -28
  59. package/docs/conventions/spec-completeness.md +0 -23
  60. package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
  61. package/docs/research/benchlm-routing-source.md +0 -198
  62. package/docs/research/consumer-owned-protocol-files.md +0 -238
  63. package/docs/research/frontend-agent-benchmarks.md +0 -282
  64. package/docs/research/model-effort-routing-benchmarks.md +0 -261
  65. package/docs/research/provider-neutral-agent-routing.md +0 -207
  66. package/docs/research/wave-152-consumer-acceptance.md +0 -98
  67. package/docs/research/wave-43-script-hook-census.md +0 -128
  68. package/scripts/build-kit.mjs +0 -117
  69. package/scripts/build-kit.test.mjs +0 -204
  70. package/scripts/census/census.test.mjs +0 -252
  71. package/scripts/census/state.test.mjs +0 -15
  72. package/scripts/census/transaction.test.mjs +0 -103
  73. package/scripts/census-contract.test.mjs +0 -227
  74. package/scripts/check-kit-staleness.mjs +0 -45
  75. package/scripts/check-kit-staleness.test.mjs +0 -19
  76. package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -152
  77. package/scripts/codex-exec.test.mjs +0 -871
  78. package/scripts/grill-census-wiring-guard.mjs +0 -87
  79. package/scripts/grill-census-wiring-guard.test.mjs +0 -107
  80. package/scripts/kit-release.test.mjs +0 -273
  81. package/scripts/kit-update-pr.test.mjs +0 -151
  82. package/scripts/lib/audit-refs.mjs +0 -146
  83. package/scripts/lib/scrub.mjs +0 -113
  84. package/scripts/lib/scrub.test.mjs +0 -187
  85. package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
  86. package/scripts/portability_profile_scan.py +0 -53
  87. package/scripts/release-delta-guard.test.mjs +0 -151
  88. package/scripts/release-parity.test.mjs +0 -90
  89. package/scripts/release-state.test.mjs +0 -360
  90. package/scripts/test_anchor_table.py +0 -69
  91. package/scripts/test_board_bootstrap.py +0 -348
  92. package/scripts/test_board_sync.py +0 -208
  93. package/scripts/test_board_sync_create_idempotency.py +0 -117
  94. package/scripts/test_board_sync_wave_title.py +0 -120
  95. package/scripts/test_census_backstop.py +0 -712
  96. package/scripts/test_census_forward_contract.py +0 -91
  97. package/scripts/test_census_update_contract.test.mjs +0 -454
  98. package/scripts/test_codex_adapter_sync_contract.py +0 -169
  99. package/scripts/test_dist_kit_smoke.py +0 -220
  100. package/scripts/test_drift_guard_diagnostics.py +0 -295
  101. package/scripts/test_issue_claim_contract.py +0 -169
  102. package/scripts/test_kit_docs_language_census.py +0 -178
  103. package/scripts/test_marker_lib.py +0 -162
  104. package/scripts/test_orchestrate_wave_contract.py +0 -381
  105. package/scripts/test_pr_body_check.py +0 -264
  106. package/scripts/test_profile_globs.py +0 -280
  107. package/scripts/test_program_planning_contract.py +0 -133
  108. package/scripts/test_release_authorization_contract.py +0 -101
  109. package/scripts/test_render_anchor.py +0 -267
  110. package/scripts/test_retro_wrapup_contract.py +0 -153
  111. package/scripts/test_skill_code_review_seed.py +0 -89
  112. package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
  113. package/scripts/test_skill_frontmatter_lint.py +0 -234
  114. package/scripts/test_skill_gh_lint.py +0 -124
  115. package/scripts/test_skill_language_census.py +0 -254
  116. package/scripts/test_skill_optional_readiness.py +0 -171
  117. package/scripts/test_skill_portability_lint.py +0 -866
  118. package/scripts/test_skill_precommit_template.py +0 -30
  119. package/scripts/test_skill_publish_audit.py +0 -358
  120. package/scripts/test_skill_readiness_contract.py +0 -264
  121. package/scripts/test_skill_readiness_preflight.py +0 -180
  122. package/scripts/test_skill_required_readiness.py +0 -233
  123. package/scripts/test_skill_selfcontainment_lint.py +0 -255
  124. package/scripts/test_skill_setup_workflow_seeds.py +0 -691
  125. package/scripts/test_skill_stale_name_lint.py +0 -126
  126. package/scripts/test_skill_surface_refs.py +0 -300
  127. package/scripts/test_skill_trailing_artifact_lint.py +0 -71
  128. package/scripts/test_tdd_contract.py +0 -78
  129. package/scripts/test_worktree_ignore_seed.py +0 -320
  130. package/scripts/test_worktree_setup_base_guard.py +0 -140
  131. package/scripts/test_worktree_wrapup_contract.py +0 -1648
  132. package/scripts/test_wrapup_land.py +0 -428
  133. package/scripts/worktree-lifecycle/session.py +0 -1857
@@ -1,381 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Reference-free behavioral contract for the portable orchestrate-wave skill.
3
-
4
- The parity table names outcomes, not the consumer that first proved them. That
5
- keeps the regression test useful in every repository that installs the kit.
6
-
7
- Run: python3 scripts/test_orchestrate_wave_contract.py
8
- """
9
-
10
- import re
11
- import unittest
12
- from pathlib import Path
13
-
14
-
15
- REPO = Path(__file__).resolve().parent.parent
16
- CLAUDE_SKILL = REPO / ".claude/skills/orchestrate-wave/SKILL.md"
17
- CODEX_SKILL = REPO / ".agents/skills/orchestrate-wave/SKILL.md"
18
- CLAUDE_BUILDER = (
19
- REPO / ".claude/skills/orchestrate-wave/references/builder-contract.md"
20
- )
21
- CODEX_BUILDER = (
22
- REPO / ".agents/skills/orchestrate-wave/references/builder-contract.md"
23
- )
24
- CLAUDE_WORKFLOW = (
25
- REPO / ".claude/skills/orchestrate-wave/references/dispatch-workflow.md"
26
- )
27
- CODEX_WORKFLOW = (
28
- REPO / ".agents/skills/orchestrate-wave/references/dispatch-workflow.md"
29
- )
30
- CLAUDE_SUBAGENTS = (
31
- REPO / ".claude/skills/orchestrate-wave/references/dispatch-subagents.md"
32
- )
33
- CODEX_SUBAGENTS = (
34
- REPO / ".agents/skills/orchestrate-wave/references/dispatch-subagents.md"
35
- )
36
- CODEX_SURFACE = REPO / ".agents/skills/orchestrate-wave"
37
- EXPECTED_DIRECT_SPAWN_CONTRACTS = (
38
- "audit-skills/SKILL.md",
39
- "code-review/SKILL.md",
40
- "codebase-design/DESIGN-IT-TWICE.md",
41
- "improve-codebase-architecture/INTERFACE-DESIGN.md",
42
- "improve-codebase-architecture/SKILL.md",
43
- "research/SKILL.md",
44
- )
45
- DIRECT_SPAWN_PATTERN = re.compile(
46
- r"Run \*\*one read-only research subagent|"
47
- r"Run both axes as \*\*parallel sub-agents|"
48
- r"Spawn 3\+ sub-agents|"
49
- r"spawn 3\+ sub-agents|"
50
- r"make 3\+ parallel\s+`spawn_agent` calls|"
51
- r"Then use the Agent tool|"
52
- r"use `spawn_agent` with `agent_type: explorer`|"
53
- r"use `spawn_agent` with only|"
54
- r"Spin up a \*\*background agent",
55
- )
56
-
57
-
58
- # Outcome -> fragments whose conjunction proves that portable behavior.
59
- BEHAVIORAL_PARITY = {
60
- "collision-safe claim": (
61
- "wave-active/<anchor>",
62
- "ahead",
63
- "uncommitted changes",
64
- "LOCAL annotated tag",
65
- "never push",
66
- ),
67
- "compare-and-set claim protocol": (
68
- "src/lib/waveClaim.mjs",
69
- "claimWave",
70
- "compare-and-set",
71
- "acquired",
72
- ),
73
- "owner-safe abort cleanup": (
74
- "this run planted",
75
- "On ANY wave STOP/abort",
76
- "Never delete a claim marker observed during a preflight collision",
77
- "releaseWaveClaim",
78
- ),
79
- "dependency-aware retirement": (
80
- "topological",
81
- "internal import graph",
82
- "ONE atomic slice",
83
- "cycle",
84
- ),
85
- "safe stacked landing": (
86
- "Do NOT rely on auto-retarget",
87
- "FRESH PR",
88
- "merge order",
89
- "manual gate",
90
- ),
91
- "completion propagation": (
92
- "Closing Conditions",
93
- "completion status",
94
- "native parent",
95
- "Program-PRD",
96
- "program sync",
97
- ),
98
- "session-owned teardown": (
99
- "session.py begin",
100
- "session.py create",
101
- "refuses reused/pre-existing targets",
102
- "seal the receipt",
103
- "compare-deleted",
104
- "foreign targets",
105
- "recovery-OID evidence",
106
- ),
107
- "program authorization continuity": (
108
- "explicit whole-Program mandate",
109
- "all planned Waves",
110
- "Do not re-ask at each Wave boundary",
111
- "gated action",
112
- "meaningful authorized safe work",
113
- "must not be marked `blocked`",
114
- ),
115
- }
116
-
117
-
118
- def markdown_body(text: str) -> str:
119
- """Ignore frontmatter representation differences between adapters."""
120
- if not text.startswith("---\n"):
121
- return text
122
- return text.split("\n---\n", 1)[1]
123
-
124
-
125
- def direct_spawn_contracts(surface: str) -> tuple[str, ...]:
126
- root = REPO / surface
127
- return tuple(sorted(
128
- str(path.relative_to(root))
129
- for path in root.rglob("*.md")
130
- if DIRECT_SPAWN_PATTERN.search(path.read_text(encoding="utf-8"))
131
- ))
132
-
133
-
134
- class OrchestrateWaveContract(unittest.TestCase):
135
- @classmethod
136
- def setUpClass(cls):
137
- cls.skill = CLAUDE_SKILL.read_text(encoding="utf-8")
138
- cls.builder = CLAUDE_BUILDER.read_text(encoding="utf-8")
139
-
140
- def test_reference_free_behavioral_parity_table(self):
141
- prose = " ".join(self.skill.split())
142
- for outcome, fragments in BEHAVIORAL_PARITY.items():
143
- with self.subTest(outcome=outcome):
144
- for fragment in fragments:
145
- self.assertIn(" ".join(fragment.split()), prose)
146
-
147
- self.assertNotRegex(self.skill, re.compile(r"/home/|#[0-9]{3,}"))
148
-
149
- def test_builder_commands_finish_in_the_foreground(self):
150
- prose = " ".join(self.builder.split())
151
- for fragment in (
152
- "IN THE FOREGROUND",
153
- "never background a test/gate command",
154
- "completed command results",
155
- "exit status",
156
- ):
157
- self.assertIn(" ".join(fragment.split()), prose)
158
-
159
- def test_builder_report_contract_is_identical_on_every_path(self):
160
- """One report contract, whichever mechanic dispatched the builder."""
161
- prose = " ".join(self.builder.split())
162
- for fragment in (
163
- "identical on every orchestration path",
164
- "references/report-contracts.md",
165
- "exactly ONE JSON object",
166
- "src/lib/reportValidator.mjs",
167
- "semanticVerify",
168
- ):
169
- self.assertIn(" ".join(fragment.split()), prose)
170
-
171
- def test_current_portable_contracts_survive_the_port(self):
172
- for fragment in (
173
- "project layer",
174
- "Native blocking edges are the frontier authority",
175
- "AFK heartbeat",
176
- "Re-run your project's full CI/verify gate CENTRALLY yourself",
177
- "already authenticated",
178
- ):
179
- self.assertIn(fragment, self.skill)
180
-
181
- def test_capability_selector_routes_exactly_one_orchestration_mechanic(self):
182
- for fragment in (
183
- "## Orchestration mechanics",
184
- "literal `Workflow`",
185
- "do not emulate",
186
- "returns exactly one",
187
- "references/dispatch-workflow.md",
188
- "references/dispatch-subagents.md",
189
- "Path C",
190
- "Phase 1 uses the selected orchestration mechanics",
191
- "Phase 2 uses the selected orchestration mechanics",
192
- ):
193
- self.assertIn(fragment, self.skill)
194
-
195
- # Skeleton-size ratchet: detail belongs in references/, not here. 345 ->
196
- # 360 carried the sticky Program-authorization rule; 360 -> 368 adds the
197
- # per-slice pickup claim (#231). Both are phase-level responsibilities
198
- # the reference files cannot carry — they gate dispatch itself.
199
- self.assertLessEqual(len(self.skill.splitlines()), 368)
200
-
201
- def test_registry_ownership_distinguishes_safe_from_eager_registries(self):
202
- prose = " ".join(self.skill.split())
203
- for fragment in (
204
- "declaration-only registries",
205
- "may be predeclared by one hub",
206
- "eager/validated registries",
207
- "each appending only its own existing artifact after creation",
208
- "dependency edges",
209
- ):
210
- self.assertIn(fragment, prose)
211
-
212
- phase_one = prose.split("## Phase 1", 1)[1].split("## Phase 2", 1)[0]
213
- done = phase_one.split("**Done when:**", 1)[1]
214
- for fragment in (
215
- "either one declaration-only owner",
216
- "verbatim consume-only dependents",
217
- "or an explicit serialized owner sequence",
218
- "each owner appends only its own existing artifact",
219
- ):
220
- self.assertIn(fragment, done)
221
-
222
- def test_phase_one_orders_producers_before_surface_measurers(self):
223
- prose = " ".join(self.skill.split())
224
- phase_one = prose.split("## Phase 1", 1)[1].split("## Phase 2", 1)[0]
225
- for fragment in (
226
- "Producer/measurer coupling",
227
- "semantic dependency",
228
- "measurer strictly after the producer",
229
- "baseline reconciliation",
230
- "drop producer-resolved entries",
231
- ):
232
- self.assertIn(fragment, phase_one)
233
-
234
- done = phase_one.split("**Done when:**", 1)[1]
235
- for fragment in (
236
- "producer/measurer pair",
237
- "explicit edge/order",
238
- "reconciliation owner",
239
- ):
240
- self.assertIn(fragment, done)
241
-
242
- def test_phase_four_centrally_runs_builder_browser_specs_before_ci(self):
243
- prose = " ".join(self.skill.split())
244
- phase_four = prose.split("## Phase 4", 1)[1].split("## Phase 5", 1)[0]
245
- browser_contract = "builders authored or changed browser specs"
246
- central_gate = "Re-run your project's full CI/verify gate CENTRALLY yourself"
247
- for fragment in (
248
- browser_contract,
249
- "unverified artifact",
250
- "ONE central green run",
251
- "if `§Verify Recipe` names a local e2e runner",
252
- "Never invent a command under generic fallback",
253
- ):
254
- self.assertIn(fragment, phase_four)
255
- self.assertLess(phase_four.index(browser_contract), phase_four.index(central_gate))
256
-
257
- def test_phase_two_keeps_per_slice_routing_decisions(self):
258
- prose = " ".join(self.skill.split())
259
- self.assertIn("(a) inline vs delegate", prose)
260
- self.assertIn("(b) tier + effort", prose)
261
- self.assertIn("Standing rules", prose)
262
-
263
- def test_every_surface_dispatch_crosses_resolver_and_spawn_guard(self):
264
- prose = " ".join(self.skill.split())
265
- for fragment in (
266
- "provider-neutral Routing intent",
267
- "shared resolver",
268
- "spawn guard",
269
- "Claude or Codex adapter",
270
- "routingAdapters/codex.mjs",
271
- "Dispatch receipt",
272
- "requested and applied route",
273
- "environment precedence",
274
- "detected transport is not authorization",
275
- "AFK",
276
- ):
277
- self.assertIn(fragment, prose)
278
-
279
- def test_every_direct_spawn_contract_routes_through_the_shared_guard(self):
280
- required = (
281
- "provider-neutral Routing intent",
282
- "routeDispatcher.mjs",
283
- "shared spawn guard",
284
- "Dispatch receipt",
285
- )
286
- claude_actual = direct_spawn_contracts(".claude/skills")
287
- codex_actual = direct_spawn_contracts(".agents/skills")
288
- self.assertEqual(claude_actual, EXPECTED_DIRECT_SPAWN_CONTRACTS)
289
- self.assertEqual(codex_actual, EXPECTED_DIRECT_SPAWN_CONTRACTS)
290
- for relative in claude_actual:
291
- claude = " ".join(
292
- (REPO / ".claude/skills" / relative).read_text(encoding="utf-8").split()
293
- )
294
- codex = " ".join(
295
- (REPO / ".agents/skills" / relative).read_text(encoding="utf-8").split()
296
- )
297
- with self.subTest(contract=relative):
298
- for fragment in required:
299
- self.assertIn(fragment, claude)
300
- self.assertIn(fragment, codex)
301
- for unsupported in ("Agent tool", "subagent_type", "agent_type"):
302
- self.assertNotIn(unsupported, codex)
303
-
304
- def test_path_a_reference_locks_the_two_run_dispatch_contract(self):
305
- workflow = CLAUDE_WORKFLOW.read_text(encoding="utf-8")
306
- for fragment in (
307
- "meta.phases",
308
- "one `agent()` call per slice",
309
- "model`, `effort`, and `phase`",
310
- "inline schema literal",
311
- "Recon run",
312
- "reconcileReconReports",
313
- "Build run",
314
- "resumeFromRunId",
315
- "exactly once",
316
- "journal.jsonl",
317
- "timestamps through `args`",
318
- "Date.now()",
319
- "Math.random()",
320
- "Every orchestration path",
321
- ):
322
- self.assertIn(fragment, workflow)
323
-
324
- self.assertEqual(workflow, CODEX_WORKFLOW.read_text(encoding="utf-8"))
325
-
326
- def test_path_b_reference_locks_the_native_subagent_contract(self):
327
- subagents = CLAUDE_SUBAGENTS.read_text(encoding="utf-8")
328
- prose = " ".join(subagents.split())
329
- for fragment in (
330
- "one read-only explorer per slice",
331
- "one builder per slice",
332
- "explicit wait",
333
- "exactly ONE JSON object",
334
- "reportValidator.mjs",
335
- "reconcileReconReports",
336
- "main thread",
337
- "is not a PASS",
338
- "waveClaim",
339
- "spawn_agents_on_csv",
340
- "output_schema",
341
- "dormant",
342
- "task_name`, `message`, and `fork_turns",
343
- "no model or effort selector",
344
- "blocks differentiated AFK",
345
- "routingAdapters/codex.mjs",
346
- ):
347
- self.assertIn(" ".join(fragment.split()), prose)
348
-
349
- self.assertEqual(subagents, CODEX_SUBAGENTS.read_text(encoding="utf-8"))
350
-
351
- def test_codex_surface_carries_no_path_a_primitive_outside_its_pointer_target(self):
352
- """B-surface prose must not require Workflow-only primitives."""
353
- offenders = {}
354
- for path in sorted(CODEX_SURFACE.rglob("*.md")):
355
- if path.name == "dispatch-workflow.md":
356
- continue
357
- body = path.read_text(encoding="utf-8")
358
- hits = [term for term in ("journal.jsonl", "resumeFromRunId") if term in body]
359
- # The literal tool name is admissible ONLY inside the capability gate.
360
- if "`Workflow`" in body:
361
- gate = body.split("## Orchestration mechanics", 1)
362
- remainder = gate[0] + (gate[1].split("\n## ", 1)[1] if len(gate) > 1 and "\n## " in gate[1] else "")
363
- if "`Workflow`" in remainder:
364
- hits.append("`Workflow`")
365
- if hits:
366
- offenders[str(path.relative_to(REPO))] = hits
367
- self.assertEqual(offenders, {})
368
-
369
- def test_claude_and_codex_surfaces_match(self):
370
- self.assertEqual(
371
- markdown_body(self.skill),
372
- markdown_body(CODEX_SKILL.read_text(encoding="utf-8")),
373
- )
374
- self.assertEqual(
375
- self.builder,
376
- CODEX_BUILDER.read_text(encoding="utf-8"),
377
- )
378
-
379
-
380
- if __name__ == "__main__":
381
- unittest.main()
@@ -1,264 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Behavior tests for the PR-body convention guard."""
3
- import importlib.util
4
- import os
5
- import subprocess
6
- import sys
7
- import tempfile
8
- import unittest
9
- from unittest import mock
10
- from pathlib import Path
11
- from types import SimpleNamespace
12
-
13
- PROFILE_PATH = Path(__file__).parent.parent / "docs/agents/board-sync.md"
14
- TEST_ENV = os.environ.copy()
15
- TEST_ENV["BOARD_SYNC_PROFILE"] = str(PROFILE_PATH)
16
- _PREVIOUS_PROFILE = os.environ.get("BOARD_SYNC_PROFILE")
17
- os.environ["BOARD_SYNC_PROFILE"] = str(PROFILE_PATH)
18
-
19
- _SPEC = importlib.util.spec_from_file_location(
20
- "pr_body_check", Path(__file__).parent / "pr-body-check.py")
21
- pbc = importlib.util.module_from_spec(_SPEC)
22
- _SPEC.loader.exec_module(pbc)
23
- if _PREVIOUS_PROFILE is None:
24
- os.environ.pop("BOARD_SYNC_PROFILE", None)
25
- else:
26
- os.environ["BOARD_SYNC_PROFILE"] = _PREVIOUS_PROFILE
27
- import pr_body_e2e as e2e # noqa: E402
28
-
29
- SCRIPT_PATH = Path(__file__).parent / "pr-body-check.py"
30
-
31
- RETRO = "**Retro:** skipped — focused guard change"
32
- VALID_LEAF_BODY = f"closes #149\n{RETRO}"
33
-
34
-
35
- class E2eNaBodyEvidence(unittest.TestCase):
36
- def test_no_trailer_and_no_evidence_is_green(self):
37
- self.assertEqual(pbc.check_pr_body(VALID_LEAF_BODY, 149, None), [])
38
-
39
- def test_no_trailer_with_evidence_is_green(self):
40
- body = VALID_LEAF_BODY + "\nE2E: n/a — harmless extra context"
41
- self.assertEqual(pbc.check_pr_body(body, 149, None), [])
42
-
43
- def test_valid_trailer_without_body_evidence_is_actionable(self):
44
- violations = pbc.check_pr_body(
45
- VALID_LEAF_BODY,
46
- 149,
47
- None,
48
- has_e2e_na_trailer=True,
49
- )
50
- self.assertTrue(any("E2E: n/a" in violation for violation in violations))
51
-
52
- def test_valid_trailer_with_non_empty_evidence_is_green(self):
53
- body = VALID_LEAF_BODY + "\nE2E: n/a — backend-only change"
54
- self.assertEqual(
55
- pbc.check_pr_body(body, 149, None, has_e2e_na_trailer=True), []
56
- )
57
-
58
- def test_valid_trailer_with_empty_evidence_is_actionable(self):
59
- body = VALID_LEAF_BODY + "\nE2E: n/a — "
60
- violations = pbc.check_pr_body(
61
- body, 149, None, has_e2e_na_trailer=True
62
- )
63
- self.assertTrue(any("E2E: n/a" in violation for violation in violations))
64
-
65
-
66
- class ExistingBodyRules(unittest.TestCase):
67
- def test_anchor_slice_still_accepts_part_of_and_leaf_close(self):
68
- body = f"Part of #130\ncloses #149\n{RETRO}"
69
- self.assertEqual(pbc.check_pr_body(body, 149, 130), [])
70
-
71
- def test_anchor_slice_still_rejects_close_on_anchor(self):
72
- body = f"Part of #130\ncloses #130\n{RETRO}"
73
- self.assertTrue(any("130" in item for item in pbc.check_pr_body(body, 149, 130)))
74
-
75
- def test_leaf_still_requires_active_close(self):
76
- body = f"`closes #149`\n{RETRO}"
77
- self.assertTrue(any("closes #149" in item for item in pbc.check_pr_body(body, 149, None)))
78
-
79
- def test_retro_line_still_required(self):
80
- self.assertTrue(any("Retro" in item for item in pbc.check_pr_body("closes #149", 149, None)))
81
-
82
- def test_active_close_targets_extracts_all_forms(self):
83
- body = ("closes #12\nFixes: #13\n"
84
- "resolves https://github.com/iKon85/agent-workflow-kit/issues/14\n"
85
- "`closes #99` and Part of #320 and closes #12")
86
- self.assertEqual(pbc.active_close_targets(body), ["12", "13", "14"])
87
-
88
- def test_active_close_targets_empty_for_part_of_only(self):
89
- self.assertEqual(pbc.active_close_targets("Part of #320\n**Retro:** skipped — x"), [])
90
-
91
- def test_program_prd_label_counts_as_anchor(self):
92
- with mock.patch.object(pbc, "_run",
93
- return_value=(0, "type:program\npriority:medium")):
94
- self.assertTrue(pbc.fetch_is_anchor(320))
95
-
96
- def test_cluster_label_still_counts_as_anchor(self):
97
- with mock.patch.object(pbc, "_run",
98
- return_value=(0, "type:cluster")):
99
- self.assertTrue(pbc.fetch_is_anchor(41))
100
-
101
- def test_wave_pr_still_requires_part_of_without_closing_anchor(self):
102
- body = f"Part of #130\ncloses #149\n{RETRO}"
103
- self.assertEqual(pbc.check_pr_body(body, 130, None, is_anchor=True), [])
104
-
105
-
106
- class ImmutableRangeTrailer(unittest.TestCase):
107
- def test_two_commit_range_finds_one_valid_trailer(self):
108
- with tempfile.TemporaryDirectory() as temp_dir:
109
- repo = Path(temp_dir)
110
- subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
111
- subprocess.run(
112
- ["git", "config", "user.email", "tests@example.invalid"],
113
- cwd=repo,
114
- check=True,
115
- )
116
- subprocess.run(
117
- ["git", "config", "user.name", "Test User"], cwd=repo, check=True
118
- )
119
- (repo / "change.txt").write_text("base\n", encoding="utf-8")
120
- subprocess.run(["git", "add", "change.txt"], cwd=repo, check=True)
121
- subprocess.run(["git", "commit", "-qm", "base"], cwd=repo, check=True)
122
- base = subprocess.check_output(
123
- ["git", "rev-parse", "HEAD"], cwd=repo, text=True
124
- ).strip()
125
- (repo / "change.txt").write_text("head\n", encoding="utf-8")
126
- subprocess.run(["git", "add", "change.txt"], cwd=repo, check=True)
127
- subprocess.run(
128
- [
129
- "git",
130
- "commit",
131
- "-qm",
132
- "head\n\nE2E-NA: backend-only change",
133
- ],
134
- cwd=repo,
135
- check=True,
136
- )
137
- head = subprocess.check_output(
138
- ["git", "rev-parse", "HEAD"], cwd=repo, text=True
139
- ).strip()
140
-
141
- self.assertTrue(e2e.fetch_has_e2e_na_trailer(base, head, cwd=repo))
142
-
143
- def test_unreadable_range_fails_open(self):
144
- self.assertFalse(e2e.fetch_has_e2e_na_trailer("missing-base", "missing-head"))
145
-
146
- def test_empty_or_multiple_trailers_are_not_a_single_valid_trailer(self):
147
- with mock.patch.object(
148
- e2e, "_collect_e2e_na_trailers", side_effect=[[""], ["one", "two"]]
149
- ):
150
- self.assertFalse(e2e.fetch_has_e2e_na_trailer("base", "head"))
151
- self.assertFalse(e2e.fetch_has_e2e_na_trailer("base", "head"))
152
-
153
- def test_pr_range_reads_immutable_base_and_head_oids(self):
154
- payload = '{"baseRefOid":"base-sha","headRefOid":"head-sha"}'
155
- with mock.patch.object(e2e, "_run", return_value=(0, payload)):
156
- self.assertEqual(e2e.fetch_pr_range("feat/149-guard"), ("base-sha", "head-sha"))
157
-
158
- def test_checker_defaults_to_pr_range_when_no_overrides_are_given(self):
159
- args = SimpleNamespace(base_sha=None, head_sha=None)
160
- with (
161
- mock.patch.object(pbc, "fetch_pr_range", return_value=("base", "head")) as get_range,
162
- mock.patch.object(pbc, "fetch_has_e2e_na_trailer", return_value=True) as has_trailer,
163
- ):
164
- self.assertTrue(pbc.resolve_has_e2e_na(args, "feat/149-guard"))
165
- get_range.assert_called_once_with("feat/149-guard")
166
- has_trailer.assert_called_once_with("base", "head")
167
-
168
- def test_unavailable_or_invalid_pr_range_fails_open(self):
169
- with mock.patch.object(e2e, "_run", side_effect=[(1, ""), (0, "not-json")]):
170
- self.assertEqual(e2e.fetch_pr_range("missing"), (None, None))
171
- self.assertEqual(e2e.fetch_pr_range("invalid"), (None, None))
172
-
173
- def test_explicit_range_requires_then_accepts_body_evidence(self):
174
- with tempfile.TemporaryDirectory() as temp_dir:
175
- repo = Path(temp_dir)
176
- subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
177
- subprocess.run(
178
- ["git", "config", "user.email", "tests@example.invalid"],
179
- cwd=repo,
180
- check=True,
181
- )
182
- subprocess.run(
183
- ["git", "config", "user.name", "Test User"], cwd=repo, check=True
184
- )
185
- (repo / "change.txt").write_text("base\n", encoding="utf-8")
186
- subprocess.run(["git", "add", "change.txt"], cwd=repo, check=True)
187
- subprocess.run(["git", "commit", "-qm", "base"], cwd=repo, check=True)
188
- base = subprocess.check_output(
189
- ["git", "rev-parse", "HEAD"], cwd=repo, text=True
190
- ).strip()
191
- (repo / "change.txt").write_text("head\n", encoding="utf-8")
192
- subprocess.run(["git", "add", "change.txt"], cwd=repo, check=True)
193
- subprocess.run(
194
- ["git", "commit", "-qm", "head\n\nE2E-NA: backend-only change"],
195
- cwd=repo,
196
- check=True,
197
- )
198
- head = subprocess.check_output(
199
- ["git", "rev-parse", "HEAD"], cwd=repo, text=True
200
- ).strip()
201
- body_path = repo / "body.md"
202
- base_body = f"Part of #130\ncloses #149\n{RETRO}"
203
- command = [
204
- sys.executable,
205
- str(SCRIPT_PATH),
206
- "--issue",
207
- "149",
208
- "--parent",
209
- "130",
210
- "--body-file",
211
- str(body_path),
212
- "--base-sha",
213
- base,
214
- "--head-sha",
215
- head,
216
- ]
217
-
218
- body_path.write_text(base_body, encoding="utf-8")
219
- missing = subprocess.run(
220
- command, cwd=repo, capture_output=True, text=True, env=TEST_ENV
221
- )
222
- self.assertEqual(missing.returncode, 1, missing.stdout + missing.stderr)
223
- self.assertIn("E2E: n/a", missing.stdout)
224
-
225
- body_path.write_text(
226
- base_body + "\nE2E: n/a — backend-only change\n", encoding="utf-8"
227
- )
228
- matching = subprocess.run(
229
- command, cwd=repo, capture_output=True, text=True, env=TEST_ENV
230
- )
231
- self.assertEqual(matching.returncode, 0, matching.stdout + matching.stderr)
232
-
233
-
234
- class ExistingExitCodes(unittest.TestCase):
235
- def test_no_issue_is_still_exit_two(self):
236
- result = subprocess.run(
237
- [sys.executable, str(SCRIPT_PATH), "--branch", "main"],
238
- capture_output=True,
239
- text=True,
240
- env=TEST_ENV,
241
- )
242
- self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
243
-
244
- def test_unreadable_body_file_is_still_exit_two(self):
245
- result = subprocess.run(
246
- [
247
- sys.executable,
248
- str(SCRIPT_PATH),
249
- "--issue",
250
- "149",
251
- "--parent",
252
- "130",
253
- "--body-file",
254
- "/definitely/missing/body.md",
255
- ],
256
- capture_output=True,
257
- text=True,
258
- env=TEST_ENV,
259
- )
260
- self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
261
-
262
-
263
- if __name__ == "__main__":
264
- unittest.main()