@ikon85/agent-workflow-kit 0.34.0 → 0.34.3

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.
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env python3
2
+ """Issue-claim-on-pickup contract (#231).
3
+
4
+ A skill that ACCEPTS a tracked issue for building must leave a claim on the
5
+ issue itself before it builds — the local worktree/branch/PR guard only sees
6
+ the same machine, and only until someone pushes. The claim is what a second
7
+ session, a second machine, or a cloud agent can actually see.
8
+
9
+ Enforced here:
10
+
11
+ - `implement` and `diagnose` carry one byte-identical claim block on both
12
+ surfaces, and it sits before their first build instruction.
13
+ - The block covers all three legs: check for a foreign claim (STOP), plant a
14
+ claim naming branch + worktree, release it.
15
+ - `orchestrate-wave` claims each slice issue at builder-launch time and
16
+ releases only its own slice claims.
17
+ - Every tracker seed and this repo's own tracker layer document the concrete
18
+ claim / check / release operations, so the tracker-neutral skill prose
19
+ resolves to real commands.
20
+
21
+ Run: python3 scripts/test_issue_claim_contract.py
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import unittest
26
+ from pathlib import Path
27
+
28
+ ROOT = Path(__file__).resolve().parents[1]
29
+ SURFACES = (".claude/skills", ".agents/skills")
30
+ PICKUP_SKILLS = ("implement", "diagnose")
31
+ START = "<!-- issue-claim:start -->"
32
+ END = "<!-- issue-claim:end -->"
33
+ MARKER = "<!-- agent-claim:"
34
+ TRACKER_DOC = "docs/agents/issue-tracker.md"
35
+ TRACKER_SEEDS = (
36
+ "setup-workflow/issue-tracker-github.md",
37
+ "setup-workflow/issue-tracker-gitlab.md",
38
+ "setup-workflow/issue-tracker-local.md",
39
+ )
40
+ # The first instruction that already assumes the build has started.
41
+ FIRST_BUILD_ANCHOR = {
42
+ "implement": "Use /tdd where possible",
43
+ "diagnose": "## Phase 1 — Build a feedback loop",
44
+ }
45
+
46
+
47
+ def skill_body(surface: str, skill: str) -> str:
48
+ return (ROOT / surface / skill / "SKILL.md").read_text(encoding="utf-8")
49
+
50
+
51
+ def claim_block(body: str) -> str:
52
+ return body.split(START, 1)[1].split(END, 1)[0]
53
+
54
+
55
+ def section(body: str, heading: str) -> str:
56
+ """Return one second-level Markdown section, excluding the next one."""
57
+ start = body.index(heading) + len(heading)
58
+ end = body.find("\n## ", start)
59
+ return body[start:] if end == -1 else body[start:end]
60
+
61
+
62
+ class ClaimBlockContract(unittest.TestCase):
63
+ def test_pickup_skills_carry_exactly_one_claim_block_on_both_surfaces(self):
64
+ for skill in PICKUP_SKILLS:
65
+ for surface in SURFACES:
66
+ with self.subTest(skill=skill, surface=surface):
67
+ body = skill_body(surface, skill)
68
+ self.assertEqual(body.count(START), 1)
69
+ self.assertEqual(body.count(END), 1)
70
+ self.assertLess(body.index(START), body.index(END))
71
+
72
+ def test_claim_block_is_byte_identical_across_skills_and_surfaces(self):
73
+ blocks = {
74
+ (skill, surface): claim_block(skill_body(surface, skill))
75
+ for skill in PICKUP_SKILLS
76
+ for surface in SURFACES
77
+ }
78
+ first = blocks[(PICKUP_SKILLS[0], SURFACES[0])]
79
+ for key, block in blocks.items():
80
+ with self.subTest(key=key):
81
+ self.assertEqual(block, first)
82
+
83
+ def test_claim_block_covers_check_plant_and_release(self):
84
+ block = claim_block(skill_body(SURFACES[0], PICKUP_SKILLS[0]))
85
+ # Check leg — a foreign claim stops the pickup, and is never removed.
86
+ self.assertIn("foreign claim", block)
87
+ self.assertIn("STOP", block)
88
+ self.assertIn("never delete a foreign claim", block)
89
+ # Plant leg — the marker must carry branch AND worktree, or a colliding
90
+ # session cannot find the work in progress.
91
+ self.assertIn(MARKER, block)
92
+ self.assertIn("branch=", block)
93
+ self.assertIn("worktree=", block)
94
+ # Release leg — the PR supersedes it; an abandoned claim is removed.
95
+ self.assertIn("supersedes", block)
96
+ self.assertIn("abandon", block)
97
+
98
+ def test_claim_block_resolves_through_the_tracker_layer_with_a_fallback(self):
99
+ block = claim_block(skill_body(SURFACES[0], PICKUP_SKILLS[0]))
100
+ self.assertIn(TRACKER_DOC, block)
101
+ self.assertIn("fall back", block)
102
+
103
+ def test_claim_is_planted_before_the_first_build_instruction(self):
104
+ for skill in PICKUP_SKILLS:
105
+ for surface in SURFACES:
106
+ with self.subTest(skill=skill, surface=surface):
107
+ body = skill_body(surface, skill)
108
+ self.assertLess(
109
+ body.index(END), body.index(FIRST_BUILD_ANCHOR[skill])
110
+ )
111
+
112
+
113
+ class OrchestrateWaveSliceClaimContract(unittest.TestCase):
114
+ def test_dispatch_claims_each_slice_issue_at_builder_launch(self):
115
+ for surface in SURFACES:
116
+ with self.subTest(surface=surface):
117
+ dispatch = section(
118
+ skill_body(surface, "orchestrate-wave"),
119
+ "## Phase 2 — Dispatch one wave in parallel",
120
+ )
121
+ self.assertIn(MARKER, dispatch)
122
+ self.assertIn("slice issue", dispatch)
123
+ self.assertIn("branch=", dispatch)
124
+ self.assertIn("worktree=", dispatch)
125
+
126
+ def test_wave_claim_is_not_treated_as_a_slice_claim(self):
127
+ for surface in SURFACES:
128
+ with self.subTest(surface=surface):
129
+ dispatch = section(
130
+ skill_body(surface, "orchestrate-wave"),
131
+ "## Phase 2 — Dispatch one wave in parallel",
132
+ )
133
+ self.assertIn("wave claim", dispatch)
134
+
135
+ def test_cleanup_releases_only_this_runs_slice_claims(self):
136
+ for surface in SURFACES:
137
+ with self.subTest(surface=surface):
138
+ cleanup = section(
139
+ skill_body(surface, "orchestrate-wave"),
140
+ "## Phase 6 — Cleanup + close",
141
+ )
142
+ self.assertIn("slice claim", cleanup)
143
+ self.assertIn("this run", cleanup)
144
+
145
+
146
+ class TrackerLayerClaimOperations(unittest.TestCase):
147
+ def documents(self) -> dict[str, str]:
148
+ docs = {TRACKER_DOC: (ROOT / TRACKER_DOC).read_text(encoding="utf-8")}
149
+ for surface in SURFACES:
150
+ for seed in TRACKER_SEEDS:
151
+ path = ROOT / surface / seed
152
+ docs[str(path.relative_to(ROOT))] = path.read_text(encoding="utf-8")
153
+ return docs
154
+
155
+ def test_every_tracker_layer_defines_claim_check_and_release(self):
156
+ for name, body in self.documents().items():
157
+ with self.subTest(document=name):
158
+ self.assertIn("## Pickup claim", body)
159
+ claim = section(body, "## Pickup claim")
160
+ self.assertIn(MARKER, claim)
161
+ self.assertIn("branch=", claim)
162
+ self.assertIn("worktree=", claim)
163
+ # a read side (detect a foreign claim) and a release side
164
+ self.assertIn("Check", claim)
165
+ self.assertIn("Release", claim)
166
+
167
+
168
+ if __name__ == "__main__":
169
+ unittest.main()
@@ -95,6 +95,14 @@ BEHAVIORAL_PARITY = {
95
95
  "Program-PRD",
96
96
  "program sync",
97
97
  ),
98
+ "program authorization continuity": (
99
+ "explicit whole-Program mandate",
100
+ "all planned Waves",
101
+ "Do not re-ask at each Wave boundary",
102
+ "gated action",
103
+ "meaningful authorized safe work",
104
+ "must not be marked `blocked`",
105
+ ),
98
106
  }
99
107
 
100
108
 
@@ -175,7 +183,11 @@ class OrchestrateWaveContract(unittest.TestCase):
175
183
  ):
176
184
  self.assertIn(fragment, self.skill)
177
185
 
178
- self.assertLessEqual(len(self.skill.splitlines()), 345)
186
+ # Skeleton-size ratchet: detail belongs in references/, not here. 345 ->
187
+ # 360 carried the sticky Program-authorization rule; 360 -> 368 adds the
188
+ # per-slice pickup claim (#231). Both are phase-level responsibilities
189
+ # the reference files cannot carry — they gate dispatch itself.
190
+ self.assertLessEqual(len(self.skill.splitlines()), 368)
179
191
 
180
192
  def test_registry_ownership_distinguishes_safe_from_eager_registries(self):
181
193
  prose = " ".join(self.skill.split())