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