@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.
- package/.agents/skills/diagnose/SKILL.md +35 -0
- package/.agents/skills/implement/SKILL.md +33 -0
- package/.agents/skills/kit-release/SKILL.md +43 -8
- package/.agents/skills/orchestrate-wave/SKILL.md +25 -2
- package/.agents/skills/setup-workflow/issue-tracker-github.md +11 -0
- package/.agents/skills/setup-workflow/issue-tracker-gitlab.md +11 -0
- package/.agents/skills/setup-workflow/issue-tracker-local.md +11 -0
- package/.claude/skills/diagnose/SKILL.md +35 -0
- package/.claude/skills/implement/SKILL.md +33 -0
- package/.claude/skills/kit-release/SKILL.md +43 -8
- package/.claude/skills/orchestrate-wave/SKILL.md +25 -2
- package/.claude/skills/setup-workflow/issue-tracker-github.md +11 -0
- package/.claude/skills/setup-workflow/issue-tracker-gitlab.md +11 -0
- package/.claude/skills/setup-workflow/issue-tracker-local.md +11 -0
- package/README.md +32 -2
- package/agent-workflow-kit.package.json +16 -16
- package/docs/adr/0004-release-intent-is-a-version-tag.md +51 -0
- package/docs/agents/issue-tracker.md +11 -0
- package/package.json +1 -1
- package/scripts/kit-release.test.mjs +190 -0
- package/scripts/release-state.mjs +35 -5
- package/scripts/release-state.test.mjs +122 -5
- package/scripts/test_issue_claim_contract.py +169 -0
- package/scripts/test_orchestrate_wave_contract.py +13 -1
|
@@ -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
|
-
|
|
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())
|