@ikon85/agent-workflow-kit 0.10.0 → 0.12.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/kit-release/SKILL.md +26 -5
- package/.agents/skills/kit-update/SKILL.md +52 -0
- package/.agents/skills/scale-check/SKILL.md +5 -3
- package/.agents/skills/setup-workflow/workflow-overview.md +1 -0
- package/.agents/skills/to-issues/SKILL.md +29 -0
- package/.agents/skills/to-waves/SKILL.md +60 -21
- package/.claude/skills/kit-release/SKILL.md +26 -5
- package/.claude/skills/kit-update/SKILL.md +52 -0
- package/.claude/skills/scale-check/SKILL.md +5 -3
- package/.claude/skills/setup-workflow/workflow-overview.md +1 -0
- package/.claude/skills/skill-manifest.json +10 -0
- package/.claude/skills/to-issues/SKILL.md +29 -0
- package/.claude/skills/to-waves/SKILL.md +60 -21
- package/README.md +20 -0
- package/agent-workflow-kit.package.json +43 -11
- package/package.json +3 -2
- package/scripts/build-kit.test.mjs +1 -0
- package/scripts/kit-release.test.mjs +10 -0
- package/scripts/release-parity.mjs +52 -0
- package/scripts/release-parity.test.mjs +53 -0
- package/scripts/release-state.mjs +172 -0
- package/scripts/release-state.test.mjs +106 -0
- package/scripts/test_program_planning_contract.py +63 -0
- package/src/cli.mjs +29 -4
- package/src/commands/update.mjs +110 -76
- package/src/lib/bundle.mjs +5 -0
- package/src/lib/updateCandidate.mjs +101 -0
- package/src/lib/updateReconcile.mjs +89 -0
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: to-waves
|
|
3
3
|
disable-model-invocation: true
|
|
4
|
-
description: "
|
|
4
|
+
description: "Turn a Program-PRD's wave plan into fully planned, execute-ready wave anchors and slice leaves after one complete chat preview. Validates the graph, publishes every wave, runs the to-prd/spec-self-critique/to-issues maturity contract for each wave, models unresolved work as explicit gates, and audits counted readiness. Idempotent and crash-recoverable. Use on a Program-PRD over several waves. NOT for a single-wave feature (to-issues) or backlog clustering (board-to-waves)."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# to-waves — Unfold a Program-PRD's wave plan onto the board
|
|
8
8
|
|
|
9
9
|
Takes a **Program-PRD** — the native Sub-Issue anchor over a multi-wave program
|
|
10
10
|
(Programm → Phase → Welle → Slice) — and turns its `## Wellenplan` chapter into
|
|
11
|
-
**
|
|
12
|
-
`scale-check → grill → to-prd (program mode) → to-waves
|
|
11
|
+
**fully planned wave anchors + slice leaves** on the board. Pipeline position:
|
|
12
|
+
`scale-check → grill → to-prd (program mode) → to-waves`, with the per-wave
|
|
13
|
+
`to-prd → spec-self-critique → to-issues` maturity pass owned inside this run.
|
|
13
14
|
The **grill and to-prd sit upstream**; to-waves runs once the Program-PRD exists.
|
|
14
15
|
It **never invents structure** — it only unfolds what the plan already decided.
|
|
15
16
|
|
|
@@ -70,28 +71,39 @@ Show:
|
|
|
70
71
|
- The **publish plan**: how many stubs + leaves will be created, which referenced
|
|
71
72
|
existing issues will be **adopted** (§4) rather than created, and which
|
|
72
73
|
Wave/Phase stamps will be applied.
|
|
74
|
+
- The complete per-wave issue contracts that will be materialized: outcome,
|
|
75
|
+
Blast-Radius, acceptance criteria, AFK/HITL bucket, native dependencies, and
|
|
76
|
+
self-contained handoff. A placeholder is not preview-complete.
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
This complete Program preview is the **single user approval** for the default
|
|
79
|
+
planning run. Only on an explicit "yes" proceed to §4/§5/§7; when a later
|
|
80
|
+
per-wave maturity pass reproduces the approved cut, **do not ask for another per-wave approval**.
|
|
81
|
+
Ask again only when new evidence changes structure or exposes
|
|
82
|
+
a genuinely undecided gate. On "no", stop — no writes happened.
|
|
76
83
|
|
|
77
84
|
## 4. Publish — the fixed order
|
|
78
85
|
|
|
79
86
|
After approval, publish in **exactly this order** (issue CREATES stay sequential so
|
|
80
87
|
the numbering is deterministic; only the field stamps are batched):
|
|
81
88
|
|
|
82
|
-
1. **Wave stubs — Stufe 1p.** One per Wellenplan row, native parent = the PRD.
|
|
89
|
+
1. **Wave stubs — transient Stufe 1p.** One per Wellenplan row, native parent = the PRD.
|
|
83
90
|
Title `Welle <N> — <Name>`; created with the `wave-stub` label and Status Spec.
|
|
84
91
|
Body = the Stufe-1p stub template (named header, the program idempotency marker,
|
|
85
|
-
the revision marker — see §6).
|
|
92
|
+
the revision marker — see §6). This is a crash-recovery checkpoint inside the
|
|
93
|
+
run, never its successful terminal state. Not yet Wave/Phase-stamped (step 4).
|
|
86
94
|
```bash
|
|
87
95
|
python3 scripts/board-sync.py create --title "Welle <N> — <Name>" \
|
|
88
96
|
--body-file <stub.md> --wave-stub --status-role spec
|
|
89
97
|
```
|
|
90
|
-
2. **
|
|
91
|
-
|
|
98
|
+
2. **Preliminary slice leaves for multi-slice waves.** Create one per slice under
|
|
99
|
+
each wave whose approved cut contains ≥2 slices, native parent = its stub. For
|
|
100
|
+
an exactly-one-slice wave, **do not create a preliminary child**: keep that
|
|
101
|
+
slice's metadata on the stub so step 6 can turn the stub itself into the atomic
|
|
102
|
+
executable wave leaf without a duplicate identity. Preliminary children use
|
|
103
|
+
the spec-role status and **no** `ready-for-agent` (a leaf is not buildable until its
|
|
92
104
|
wave is promoted — the ordering guard stays unambiguous). Body = the slice's
|
|
93
105
|
`## Slices` section carried forward per `SLICE-METADATA-FORMAT.md` (metadata block +
|
|
94
|
-
the outcome/placeholder skeleton,
|
|
106
|
+
the outcome/placeholder skeleton, finalized in step 6). **Title carries the
|
|
95
107
|
navigation prefix** `Welle <N> / Slice <local-id> — <title>` — the local-id
|
|
96
108
|
(`1a`, `1b`, …) encodes the build order, so the sub-issue LIST is navigable by title
|
|
97
109
|
alone (same convention as `to-issues`' promoted children, `Welle N / Slice X — <outcome>`).
|
|
@@ -131,10 +143,22 @@ the numbering is deterministic; only the field stamps are batched):
|
|
|
131
143
|
```bash
|
|
132
144
|
python3 scripts/board-sync.py program-sync <prd#>
|
|
133
145
|
```
|
|
134
|
-
6. **
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
146
|
+
6. **Mature every wave.** For each stub in dependency order, run the approved
|
|
147
|
+
Program batch handoff: `to-prd` Mode B writes the per-wave Draft-PRD,
|
|
148
|
+
`spec-self-critique` hardens it, and `to-issues` promotes/atomizes it while
|
|
149
|
+
reusing the preliminary leaves. The pass writes complete issue bodies, final
|
|
150
|
+
AFK/HITL buckets, handoffs, native dependencies, `plan_revision`, and removes
|
|
151
|
+
`wave-stub`. It inherits the §3 approval when the cut is unchanged.
|
|
152
|
+
7. **Audit every wave.** Run `execute-ready-check.py --mode audit` on every promoted
|
|
153
|
+
anchor or atomic wave leaf. Any incomplete placeholder, incoherent revision,
|
|
154
|
+
missing bucket, or child-set mismatch blocks successful completion. **Program completion gate:**
|
|
155
|
+
Although `to-issues` uses audit mode as an informational handoff check, **do not count that wave as matured** or report
|
|
156
|
+
Program success until its audit findings are clean.
|
|
157
|
+
8. **Counted completion report + program view link.** Report `X von Y` for each part
|
|
158
|
+
(stubs created, leaves adopted/created, field stamps set, waves matured, waves
|
|
159
|
+
execute-ready) and link the program board view. Completeness is **counted, never
|
|
160
|
+
claimed** — the numbers come from the commands, including the exact line
|
|
161
|
+
`X von Y Wellen ausführungsreif`.
|
|
138
162
|
|
|
139
163
|
## 5. Adopt path — referenced existing issues
|
|
140
164
|
|
|
@@ -176,6 +200,10 @@ A re-run of to-waves on the same program is **delta-apply** and doubles as
|
|
|
176
200
|
report as **orphaned** (do not auto-close — closing is the abort convention, §8).
|
|
177
201
|
Cross-check the native children (`children-of <prd#>` and each stub) against the
|
|
178
202
|
plan so both origins are covered.
|
|
203
|
+
- **Atomic supersession exception.** A marked preliminary leaf whose local-id maps
|
|
204
|
+
to the one slice now carried by its atomic stub is not a generic orphan. Route it
|
|
205
|
+
to `to-issues` §5b cleanup whether it is still linked or was already unlinked by
|
|
206
|
+
an interrupted run. Search-by-marker is the durable recovery identity.
|
|
179
207
|
- **Crash recovery.** Because create is search-before-create, `link` is idempotent,
|
|
180
208
|
and `stamp-batch` field-sets are idempotent, re-running the whole publish after an
|
|
181
209
|
abort resumes cleanly — already-created issues match, missing ones are created,
|
|
@@ -191,7 +219,7 @@ A re-run of to-waves on the same program is **delta-apply** and doubles as
|
|
|
191
219
|
(the same sanctioned body-write `to-prd` Mode B uses). The mechanism that renews
|
|
192
220
|
coherence is the same one the revision broke — it self-repairs.
|
|
193
221
|
|
|
194
|
-
## 7. Program
|
|
222
|
+
## 7. Program completion contract
|
|
195
223
|
|
|
196
224
|
to-waves also carries the **program-grill agenda** as a reference chapter — the
|
|
197
225
|
checklist a program grill (upstream) should cover so the wave plan does not rest on
|
|
@@ -211,12 +239,22 @@ open switches:
|
|
|
211
239
|
single bounded choice to `decision-gate` — not deferred into the plan as an open
|
|
212
240
|
task.
|
|
213
241
|
|
|
214
|
-
**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
242
|
+
**Default postcondition:** every published wave is fully planned and passes the
|
|
243
|
+
shared execute-ready audit before this run reports success. Stufe 1p is an
|
|
244
|
+
internal recovery state, not the result presented to the user. **Late Binding is not the default**
|
|
245
|
+
for unfinished issue prose or missing buckets.
|
|
246
|
+
|
|
247
|
+
Uncertainty is planned, not hidden. A bounded trade-off becomes an explicit
|
|
248
|
+
**Decision Gate**, a factual unknown becomes a **Verify Spike**, and an unresolved
|
|
249
|
+
structure seam becomes a HITL **Design-Grill** slice (or an explicitly named
|
|
250
|
+
planning wave). Place that gate before its dependent build slice and write the
|
|
251
|
+
native blocking edge. The gate issue itself still receives a complete contract and
|
|
252
|
+
handoff; the dependent work is blocked, not vaguely under-planned.
|
|
253
|
+
|
|
254
|
+
A mandatory human or external setup action follows the same rule: model it as a
|
|
255
|
+
HITL predecessor, never bury it inside a `ready-for-agent` build leaf. If later
|
|
256
|
+
drift invalidates an already planned wave, use the revision/escalation path in §8;
|
|
257
|
+
do not pre-emptively leave every future wave incomplete.
|
|
220
258
|
|
|
221
259
|
## 8. Close protection, abort convention, escalation path
|
|
222
260
|
|
|
@@ -255,6 +293,7 @@ python3 scripts/board-sync.py program-sync <prd#>
|
|
|
255
293
|
to-waves: prd=#<n> preview=<approved|rejected>
|
|
256
294
|
created=<stubs X von Y, leaves X von Y> adopted=<#a #b … | none>
|
|
257
295
|
stamped=<N von M Wave/Phase> phase=<stamped | skipped (profile lacks fields.phase)>
|
|
296
|
+
matured=<X von Y> execute-ready=<X von Y Wellen ausführungsreif>
|
|
258
297
|
revision=r<N> renewed-markers=<count | none> orphaned=<#… | none>
|
|
259
298
|
graph=<ok | blocking: …> program-view=<url>
|
|
260
299
|
```
|
package/README.md
CHANGED
|
@@ -332,6 +332,26 @@ still reference. Flags: `--force` (overwrite pre-existing files on `init`),
|
|
|
332
332
|
|
|
333
333
|
## Release notes
|
|
334
334
|
|
|
335
|
+
### 0.12.0
|
|
336
|
+
|
|
337
|
+
- added: `.agents/skills/kit-update/SKILL.md`
|
|
338
|
+
- added: `.claude/skills/kit-update/SKILL.md`
|
|
339
|
+
- changed: `.agents/skills/setup-workflow/workflow-overview.md`
|
|
340
|
+
- changed: `.claude/skills/setup-workflow/workflow-overview.md`
|
|
341
|
+
|
|
342
|
+
### 0.11.0
|
|
343
|
+
|
|
344
|
+
- added: `scripts/release-parity.mjs`
|
|
345
|
+
- added: `scripts/release-state.mjs`
|
|
346
|
+
- changed: `.agents/skills/kit-release/SKILL.md`
|
|
347
|
+
- changed: `.agents/skills/scale-check/SKILL.md`
|
|
348
|
+
- changed: `.agents/skills/to-issues/SKILL.md`
|
|
349
|
+
- changed: `.agents/skills/to-waves/SKILL.md`
|
|
350
|
+
- changed: `.claude/skills/kit-release/SKILL.md`
|
|
351
|
+
- changed: `.claude/skills/scale-check/SKILL.md`
|
|
352
|
+
- changed: `.claude/skills/to-issues/SKILL.md`
|
|
353
|
+
- changed: `.claude/skills/to-waves/SKILL.md`
|
|
354
|
+
|
|
335
355
|
### 0.10.0
|
|
336
356
|
|
|
337
357
|
- added: `.agents/skills/kit-release/SKILL.md`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kitVersion": "0.
|
|
2
|
+
"kitVersion": "0.12.0",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"path": ".agents/skills/ask-matt/SKILL.md",
|
|
@@ -321,7 +321,16 @@
|
|
|
321
321
|
"kind": "skill",
|
|
322
322
|
"ownerSkill": "kit-release",
|
|
323
323
|
"surface": "codex",
|
|
324
|
-
"sha256": "
|
|
324
|
+
"sha256": "d05a4059a19816ff302c119f797f1ea2b71554ecffac7a8f0b919cfe5517d940",
|
|
325
|
+
"mode": 420,
|
|
326
|
+
"origin": "kit"
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
"path": ".agents/skills/kit-update/SKILL.md",
|
|
330
|
+
"kind": "skill",
|
|
331
|
+
"ownerSkill": "kit-update",
|
|
332
|
+
"surface": "codex",
|
|
333
|
+
"sha256": "26737ea49ac104260e56fd1604f60249ca1d59f1a2c1f22db55649833d01ebc3",
|
|
325
334
|
"mode": 420,
|
|
326
335
|
"origin": "kit"
|
|
327
336
|
},
|
|
@@ -438,7 +447,7 @@
|
|
|
438
447
|
"kind": "skill",
|
|
439
448
|
"ownerSkill": "scale-check",
|
|
440
449
|
"surface": "codex",
|
|
441
|
-
"sha256": "
|
|
450
|
+
"sha256": "80f8d5b4b811afd0bce4546d18170eed8a79ae2ee872b40563814374ea2ad1c2",
|
|
442
451
|
"mode": 420,
|
|
443
452
|
"origin": "kit"
|
|
444
453
|
},
|
|
@@ -564,7 +573,7 @@
|
|
|
564
573
|
"kind": "skill",
|
|
565
574
|
"ownerSkill": "setup-workflow",
|
|
566
575
|
"surface": "codex",
|
|
567
|
-
"sha256": "
|
|
576
|
+
"sha256": "6ce3954e4f94d112801cba0d145e4d3b6f101aca5e50e6b1f9794aa0288881a9",
|
|
568
577
|
"mode": 420,
|
|
569
578
|
"origin": "kit"
|
|
570
579
|
},
|
|
@@ -654,7 +663,7 @@
|
|
|
654
663
|
"kind": "skill",
|
|
655
664
|
"ownerSkill": "to-issues",
|
|
656
665
|
"surface": "codex",
|
|
657
|
-
"sha256": "
|
|
666
|
+
"sha256": "3fdd05150f428cf8098e4dd12f67cbc7ebe17fa9ab315e456fd0df70e5c9925d",
|
|
658
667
|
"mode": 420,
|
|
659
668
|
"origin": "kit"
|
|
660
669
|
},
|
|
@@ -699,7 +708,7 @@
|
|
|
699
708
|
"kind": "skill",
|
|
700
709
|
"ownerSkill": "to-waves",
|
|
701
710
|
"surface": "codex",
|
|
702
|
-
"sha256": "
|
|
711
|
+
"sha256": "5ac4fd5d26d6ea2327fc50ad4bcc8f3d8c80186afb976410d8c3e8f3c21d896e",
|
|
703
712
|
"mode": 420,
|
|
704
713
|
"origin": "kit"
|
|
705
714
|
},
|
|
@@ -1267,7 +1276,16 @@
|
|
|
1267
1276
|
"kind": "skill",
|
|
1268
1277
|
"ownerSkill": "kit-release",
|
|
1269
1278
|
"surface": "claude",
|
|
1270
|
-
"sha256": "
|
|
1279
|
+
"sha256": "d05a4059a19816ff302c119f797f1ea2b71554ecffac7a8f0b919cfe5517d940",
|
|
1280
|
+
"mode": 420,
|
|
1281
|
+
"origin": "kit"
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
"path": ".claude/skills/kit-update/SKILL.md",
|
|
1285
|
+
"kind": "skill",
|
|
1286
|
+
"ownerSkill": "kit-update",
|
|
1287
|
+
"surface": "claude",
|
|
1288
|
+
"sha256": "26737ea49ac104260e56fd1604f60249ca1d59f1a2c1f22db55649833d01ebc3",
|
|
1271
1289
|
"mode": 420,
|
|
1272
1290
|
"origin": "kit"
|
|
1273
1291
|
},
|
|
@@ -1384,7 +1402,7 @@
|
|
|
1384
1402
|
"kind": "skill",
|
|
1385
1403
|
"ownerSkill": "scale-check",
|
|
1386
1404
|
"surface": "claude",
|
|
1387
|
-
"sha256": "
|
|
1405
|
+
"sha256": "80f8d5b4b811afd0bce4546d18170eed8a79ae2ee872b40563814374ea2ad1c2",
|
|
1388
1406
|
"mode": 420,
|
|
1389
1407
|
"origin": "kit"
|
|
1390
1408
|
},
|
|
@@ -1528,7 +1546,7 @@
|
|
|
1528
1546
|
"kind": "skill",
|
|
1529
1547
|
"ownerSkill": "setup-workflow",
|
|
1530
1548
|
"surface": "claude",
|
|
1531
|
-
"sha256": "
|
|
1549
|
+
"sha256": "6ce3954e4f94d112801cba0d145e4d3b6f101aca5e50e6b1f9794aa0288881a9",
|
|
1532
1550
|
"mode": 420,
|
|
1533
1551
|
"origin": "kit"
|
|
1534
1552
|
},
|
|
@@ -1618,7 +1636,7 @@
|
|
|
1618
1636
|
"kind": "skill",
|
|
1619
1637
|
"ownerSkill": "to-issues",
|
|
1620
1638
|
"surface": "claude",
|
|
1621
|
-
"sha256": "
|
|
1639
|
+
"sha256": "5874d4b2d4999bd32a0a90c955467fc4e58e230295cd6ee21936fa30456bda6e",
|
|
1622
1640
|
"mode": 420,
|
|
1623
1641
|
"origin": "kit"
|
|
1624
1642
|
},
|
|
@@ -1663,7 +1681,7 @@
|
|
|
1663
1681
|
"kind": "skill",
|
|
1664
1682
|
"ownerSkill": "to-waves",
|
|
1665
1683
|
"surface": "claude",
|
|
1666
|
-
"sha256": "
|
|
1684
|
+
"sha256": "5ac4fd5d26d6ea2327fc50ad4bcc8f3d8c80186afb976410d8c3e8f3c21d896e",
|
|
1667
1685
|
"mode": 420,
|
|
1668
1686
|
"origin": "kit"
|
|
1669
1687
|
},
|
|
@@ -1928,6 +1946,20 @@
|
|
|
1928
1946
|
"mode": 420,
|
|
1929
1947
|
"origin": "kit"
|
|
1930
1948
|
},
|
|
1949
|
+
{
|
|
1950
|
+
"path": "scripts/release-parity.mjs",
|
|
1951
|
+
"kind": "script",
|
|
1952
|
+
"sha256": "faa4a1d1116d58f69c8541a586ebd6076ebc3f7af7809e9e4a53928f49298365",
|
|
1953
|
+
"mode": 420,
|
|
1954
|
+
"origin": "kit"
|
|
1955
|
+
},
|
|
1956
|
+
{
|
|
1957
|
+
"path": "scripts/release-state.mjs",
|
|
1958
|
+
"kind": "script",
|
|
1959
|
+
"sha256": "2aae056a8e0e945dbf68298236719c78e22f13dbd8f0c3466b969355d3809058",
|
|
1960
|
+
"mode": 420,
|
|
1961
|
+
"origin": "kit"
|
|
1962
|
+
},
|
|
1931
1963
|
{
|
|
1932
1964
|
"path": "scripts/wrapup-land.py",
|
|
1933
1965
|
"kind": "script",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikon85/agent-workflow-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Portable AI-agent workflow skills (plan → execute → land → learn) for Claude Code & Codex — grilling, TDD, diagnosis, two-axis code review, cross-model Codex review, design & domain-modeling, plus a skill router (ask-matt). npx init/update/diff/uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"kit:build": "node scripts/build-kit.mjs",
|
|
14
14
|
"kit:staleness": "node scripts/check-kit-staleness.mjs",
|
|
15
15
|
"release:prepare": "node scripts/kit-release.mjs",
|
|
16
|
-
"release:guard": "node scripts/release-delta-guard.mjs"
|
|
16
|
+
"release:guard": "node scripts/release-delta-guard.mjs",
|
|
17
|
+
"release:status": "node scripts/release-state.mjs --status"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
20
|
".claude/",
|
|
@@ -35,6 +35,7 @@ test('current build contains post-tag public files and repository metadata', asy
|
|
|
35
35
|
await readFile(join(dist, '.agents/skills/codex-adapter-sync/SKILL.md'));
|
|
36
36
|
await readFile(join(dist, 'scripts/program_graph.py'));
|
|
37
37
|
const pkg = JSON.parse(await readFile(join(dist, 'package.json'), 'utf8'));
|
|
38
|
+
assert.equal(pkg.name, '@ikon85/agent-workflow-kit');
|
|
38
39
|
assert.equal(pkg.repository.url, 'git+https://github.com/iKon85/agent-workflow-kit.git');
|
|
39
40
|
});
|
|
40
41
|
});
|
|
@@ -13,6 +13,16 @@ test('the install manifest ships both release primitives named by the skill', as
|
|
|
13
13
|
const paths = manifest.files.map((entry) => entry.path);
|
|
14
14
|
assert.ok(paths.includes('scripts/kit-release.mjs'));
|
|
15
15
|
assert.ok(paths.includes('scripts/release-delta-guard.mjs'));
|
|
16
|
+
assert.ok(paths.includes('scripts/release-parity.mjs'));
|
|
17
|
+
assert.ok(paths.includes('scripts/release-state.mjs'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('both release skill surfaces name only the owned scoped npm package', async () => {
|
|
21
|
+
const claude = await readFile(join(REPO, '.claude/skills/kit-release/SKILL.md'), 'utf8');
|
|
22
|
+
const codex = await readFile(join(REPO, '.agents/skills/kit-release/SKILL.md'), 'utf8');
|
|
23
|
+
for (const body of [claude, codex]) {
|
|
24
|
+
assert.match(body, /`@ikon85\/agent-workflow-kit`/);
|
|
25
|
+
}
|
|
16
26
|
});
|
|
17
27
|
|
|
18
28
|
test('patch, minor, and major confirmations select exactly one target version', () => {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
|
|
6
|
+
const exec = promisify(execFile);
|
|
7
|
+
const FIELDS = ['name', 'version', 'tarballIntegrity', 'manifestSha256'];
|
|
8
|
+
|
|
9
|
+
const digest = (algorithm, content, encoding = 'hex') =>
|
|
10
|
+
createHash(algorithm).update(content).digest(encoding);
|
|
11
|
+
|
|
12
|
+
async function readTarEntry(tarball, entry) {
|
|
13
|
+
const { stdout } = await exec('tar', ['-xOf', tarball, entry], { encoding: 'buffer' });
|
|
14
|
+
return stdout;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function releaseIdentityFromTarball(tarball) {
|
|
18
|
+
const bytes = await readFile(tarball);
|
|
19
|
+
const packageJson = JSON.parse(await readTarEntry(tarball, 'package/package.json'));
|
|
20
|
+
const manifest = await readTarEntry(tarball, 'package/agent-workflow-kit.package.json');
|
|
21
|
+
if (packageJson.version !== JSON.parse(manifest).kitVersion) {
|
|
22
|
+
throw new Error('tarball package and manifest versions mismatch');
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
name: packageJson.name,
|
|
26
|
+
version: packageJson.version,
|
|
27
|
+
tarballIntegrity: `sha512-${digest('sha512', bytes, 'base64')}`,
|
|
28
|
+
manifestSha256: digest('sha256', manifest),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validate(label, identity) {
|
|
33
|
+
if (!identity || typeof identity !== 'object') throw new Error(`${label} release identity missing`);
|
|
34
|
+
for (const field of FIELDS) {
|
|
35
|
+
if (typeof identity[field] !== 'string' || !identity[field]) {
|
|
36
|
+
throw new Error(`${label} ${field} missing`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function assertReleaseParity(identities) {
|
|
42
|
+
validate('local', identities.local);
|
|
43
|
+
for (const label of ['npm', 'github']) {
|
|
44
|
+
validate(label, identities[label]);
|
|
45
|
+
for (const field of FIELDS) {
|
|
46
|
+
if (identities[label][field] !== identities.local[field]) {
|
|
47
|
+
throw new Error(`${label} ${field} mismatch`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return identities.local;
|
|
52
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { assertReleaseParity, releaseIdentityFromTarball } from './release-parity.mjs';
|
|
9
|
+
|
|
10
|
+
const exec = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
const identity = {
|
|
13
|
+
name: '@ikon85/agent-workflow-kit',
|
|
14
|
+
version: '1.2.3',
|
|
15
|
+
tarballIntegrity: 'sha512-example',
|
|
16
|
+
manifestSha256: 'abc123',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
test('identical local, npm, and GitHub release identities have parity', () => {
|
|
20
|
+
assert.deepEqual(assertReleaseParity({ local: identity, npm: { ...identity }, github: { ...identity } }), identity);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('a registry or GitHub content mismatch fails closed', () => {
|
|
24
|
+
assert.throws(() => assertReleaseParity({
|
|
25
|
+
local: identity,
|
|
26
|
+
npm: { ...identity, manifestSha256: 'different' },
|
|
27
|
+
github: { ...identity },
|
|
28
|
+
}), /npm manifestSha256 mismatch/);
|
|
29
|
+
assert.throws(() => assertReleaseParity({
|
|
30
|
+
local: identity,
|
|
31
|
+
npm: { ...identity },
|
|
32
|
+
github: { ...identity, tarballIntegrity: 'different' },
|
|
33
|
+
}), /github tarballIntegrity mismatch/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('a package tarball produces a deterministic content identity', async () => {
|
|
37
|
+
const root = await mkdtemp(join(tmpdir(), 'release-parity-'));
|
|
38
|
+
const packageDir = join(root, 'package');
|
|
39
|
+
const tarball = join(root, 'kit.tgz');
|
|
40
|
+
try {
|
|
41
|
+
await mkdir(packageDir);
|
|
42
|
+
await writeFile(join(packageDir, 'package.json'), '{"name":"kit","version":"2.0.0"}\n');
|
|
43
|
+
await writeFile(join(packageDir, 'agent-workflow-kit.package.json'), '{"kitVersion":"2.0.0","files":[]}\n');
|
|
44
|
+
await exec('tar', ['-czf', tarball, '-C', root, 'package']);
|
|
45
|
+
const first = await releaseIdentityFromTarball(tarball);
|
|
46
|
+
const second = await releaseIdentityFromTarball(tarball);
|
|
47
|
+
assert.equal(first.name, 'kit');
|
|
48
|
+
assert.equal(first.version, '2.0.0');
|
|
49
|
+
assert.match(first.tarballIntegrity, /^sha512-/);
|
|
50
|
+
assert.match(first.manifestSha256, /^[a-f0-9]{64}$/);
|
|
51
|
+
assert.deepEqual(first, second);
|
|
52
|
+
} finally { await rm(root, { recursive: true, force: true }); }
|
|
53
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { assertReleaseParity, releaseIdentityFromTarball } from './release-parity.mjs';
|
|
8
|
+
|
|
9
|
+
const exec = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
const assertMatches = (local, remote, label) => assertReleaseParity({
|
|
12
|
+
local,
|
|
13
|
+
npm: label === 'npm' ? remote : local,
|
|
14
|
+
github: label === 'github' ? remote : local,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export async function inspectRelease(adapter) {
|
|
18
|
+
const { identity } = await adapter.local();
|
|
19
|
+
const npm = await adapter.npm(identity);
|
|
20
|
+
const github = await adapter.github(identity);
|
|
21
|
+
if (!npm) {
|
|
22
|
+
if (github) throw new Error('GitHub release exists before npm package');
|
|
23
|
+
return { status: 'awaiting-npm', identity };
|
|
24
|
+
}
|
|
25
|
+
assertMatches(identity, npm, 'npm');
|
|
26
|
+
if (!github) return { status: 'awaiting-github', identity };
|
|
27
|
+
assertReleaseParity({ local: identity, npm, github });
|
|
28
|
+
return { status: 'released', identity };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function reconcileRelease(adapter) {
|
|
32
|
+
const { identity, tarball } = await adapter.local();
|
|
33
|
+
let npm = await adapter.npm(identity);
|
|
34
|
+
let github = await adapter.github(identity);
|
|
35
|
+
|
|
36
|
+
if (!npm) {
|
|
37
|
+
if (github) throw new Error('GitHub release exists before npm package');
|
|
38
|
+
await adapter.publishNpm({ identity, tarball });
|
|
39
|
+
npm = await adapter.npm(identity);
|
|
40
|
+
if (!npm) throw new Error('npm package missing after publish');
|
|
41
|
+
}
|
|
42
|
+
assertMatches(identity, npm, 'npm');
|
|
43
|
+
|
|
44
|
+
if (!github) {
|
|
45
|
+
await adapter.createGithub({ identity, tarball });
|
|
46
|
+
github = await adapter.github(identity);
|
|
47
|
+
if (!github) throw new Error('GitHub release missing after create');
|
|
48
|
+
}
|
|
49
|
+
assertReleaseParity({ local: identity, npm, github });
|
|
50
|
+
return { status: 'released', identity };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isMissingRelease(error, service) {
|
|
54
|
+
const detail = `${error.stderr ?? ''}\n${error.message ?? ''}`;
|
|
55
|
+
return service === 'npm'
|
|
56
|
+
? /E404|404 Not Found|ETARGET|No matching version found/i.test(detail)
|
|
57
|
+
: /release not found|HTTP 404/i.test(detail);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function githubReleaseArgs({ exists, tag, tarball, target }) {
|
|
61
|
+
if (exists) return ['release', 'upload', tag, tarball, '--clobber'];
|
|
62
|
+
return [
|
|
63
|
+
'release', 'create', tag, tarball, '--target', target,
|
|
64
|
+
'--title', tag, '--generate-notes',
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function npmTarballFilename(name, version) {
|
|
69
|
+
return `${name.replace(/^@/, '').replaceAll('/', '-')}-${version}.tgz`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function packedTarball(run, spec, directory, repoRoot) {
|
|
73
|
+
const { stdout } = await run(
|
|
74
|
+
'npm', ['pack', spec, '--json', '--pack-destination', directory], { cwd: repoRoot },
|
|
75
|
+
);
|
|
76
|
+
const result = JSON.parse(stdout);
|
|
77
|
+
if (!Array.isArray(result) || result.length !== 1 || !result[0].filename) {
|
|
78
|
+
throw new Error(`unexpected npm pack result for ${spec}`);
|
|
79
|
+
}
|
|
80
|
+
return join(directory, result[0].filename);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function releaseReaders(context) {
|
|
84
|
+
const { run, repoRoot, env, scratch, state } = context;
|
|
85
|
+
async function local() {
|
|
86
|
+
const tarball = await packedTarball(run, '.', scratch, repoRoot);
|
|
87
|
+
return { tarball, identity: await releaseIdentityFromTarball(tarball) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function npm(identity) {
|
|
91
|
+
try {
|
|
92
|
+
state.npmTarball = await packedTarball(
|
|
93
|
+
run, `${identity.name}@${identity.version}`, scratch, repoRoot,
|
|
94
|
+
);
|
|
95
|
+
return await releaseIdentityFromTarball(state.npmTarball);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (isMissingRelease(error, 'npm')) return null;
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function github(identity) {
|
|
103
|
+
const tag = `v${identity.version}`;
|
|
104
|
+
try {
|
|
105
|
+
await run('gh', ['release', 'view', tag, '--json', 'tagName'], { cwd: repoRoot, env });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (isMissingRelease(error, 'github')) return null;
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const asset = npmTarballFilename(identity.name, identity.version);
|
|
112
|
+
await run('gh', ['release', 'download', tag, '--pattern', asset, '--dir', scratch, '--clobber'], {
|
|
113
|
+
cwd: repoRoot, env,
|
|
114
|
+
});
|
|
115
|
+
return await releaseIdentityFromTarball(join(scratch, asset));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (/no assets match|not found/i.test(`${error.stderr ?? ''}\n${error.message ?? ''}`)) return null;
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { local, npm, github };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function releaseWriters(context) {
|
|
126
|
+
const { run, repoRoot, env, state } = context;
|
|
127
|
+
async function publishNpm({ tarball }) {
|
|
128
|
+
await run('npm', ['publish', tarball, '--access', 'public', '--provenance'], { cwd: repoRoot, env });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function createGithub({ identity }) {
|
|
132
|
+
if (!state.npmTarball) throw new Error('verified npm tarball unavailable for GitHub release');
|
|
133
|
+
const tag = `v${identity.version}`;
|
|
134
|
+
let exists = true;
|
|
135
|
+
try {
|
|
136
|
+
await run('gh', ['release', 'view', tag, '--json', 'tagName'], { cwd: repoRoot, env });
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (!isMissingRelease(error, 'github')) throw error;
|
|
139
|
+
exists = false;
|
|
140
|
+
}
|
|
141
|
+
await run('gh', githubReleaseArgs({
|
|
142
|
+
exists, tag, tarball: state.npmTarball, target: env.GITHUB_SHA,
|
|
143
|
+
}), { cwd: repoRoot, env });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { publishNpm, createGithub };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function createCommandAdapter({ repoRoot, run = exec, env = process.env } = {}) {
|
|
150
|
+
const scratch = await mkdtemp(join(tmpdir(), 'awkit-publish-'));
|
|
151
|
+
const context = { run, repoRoot, env, scratch, state: {} };
|
|
152
|
+
return {
|
|
153
|
+
...releaseReaders(context),
|
|
154
|
+
...releaseWriters(context),
|
|
155
|
+
dispose: () => rm(scratch, { recursive: true, force: true }),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function main() {
|
|
160
|
+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
161
|
+
const adapter = await createCommandAdapter({ repoRoot });
|
|
162
|
+
try {
|
|
163
|
+
const result = process.argv.includes('--status')
|
|
164
|
+
? await inspectRelease(adapter)
|
|
165
|
+
: await reconcileRelease(adapter);
|
|
166
|
+
console.log(`release ${result.identity.version}: ${result.status}`);
|
|
167
|
+
} finally { await adapter.dispose(); }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
171
|
+
main().catch((error) => { console.error(`release-state: ${error.message}`); process.exitCode = 1; });
|
|
172
|
+
}
|