@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.
@@ -1,15 +1,16 @@
1
1
  ---
2
2
  name: to-waves
3
3
  disable-model-invocation: true
4
- description: "Unfold a Program-PRD's wave plan onto the board parse + graph-validate it, show the complete Wellenplan as a chat preview before ANY board write, then after approval publish named wave stubs (native parent = the PRD) + slice leaves (native parent = their stub) with Wave and Phase stamped in one batch, and report a counted completion. Idempotent + crash-recoverable on re-run; adopts referenced existing issues instead of duplicating them. Use on a Program-PRD, the native anchor over several waves. NOT for a single-wave feature (to-issues) and NOT for clustering a backlog (board-to-waves)."
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
- **named wave stubs + slice leaves** on the board. Pipeline position:
12
- `scale-check → grill → to-prd (program mode) → to-waves per wave: to-issues`.
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
- Then ask for approval. Only on an explicit "yes" proceed to §4/§5. On "no", stop —
75
- no writes happened.
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). Not yet Wave/Phase-stamped (that is step 4).
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. **Slice leaves.** One per slice under each wave, native parent = its stub. Created
91
- with the spec-role status and **no** `ready-for-agent` (a leaf is not buildable until its
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, sharpened only at promotion). **Title carries the
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. **Counted completion report + program view link.** Report `X von Y` for each part
135
- (stubs created, leaves created, issues adopted, field stamps set) and link the
136
- program board view. Completeness is **counted, never claimed** the numbers come
137
- from the create outputs + the `stamp-batch` response, not from memory.
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-grill agenda + the per-wave-start content pass
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
- **The per-wave-start content pass** is where slice content is sharpened — deliberately
215
- **just-in-time (Late Binding)**, never ex ante on spec. When a wave is promoted:
216
- `to-prd` (Mode B, into the wave stub) + `spec-self-critique` are **mandatory**, and
217
- the depth ladder is **raised for a high-stakes wave** (a design-grill or a
218
- `decision-gate` before slicing). This is what folds in any drift propagated since the
219
- plan was first written.
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.10.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": "e24be892d52e29bf1add2a417afb34d17e9e5fa46ba1014ba81b4ad7069f905e",
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": "aa5f96503278e831c0229cdec4ba057d245c6dfa202296cbe0f236328bc67bf4",
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": "87e5ab578f1c17905b39b9cb39b515e4661f3437d96de919f1b23a5775760c4f",
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": "3cc1f667c0b961f3682bf71e345be9b27b96ab62edb71aa080912418b9344728",
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": "1bbffd3858c9a7fb6873f448579a8d888959ff336572a25eb9780ce55b31a2d7",
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": "e24be892d52e29bf1add2a417afb34d17e9e5fa46ba1014ba81b4ad7069f905e",
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": "aa5f96503278e831c0229cdec4ba057d245c6dfa202296cbe0f236328bc67bf4",
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": "87e5ab578f1c17905b39b9cb39b515e4661f3437d96de919f1b23a5775760c4f",
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": "4a8b4badb99f459d88789ba47f8378602f66be3b7ad1bd0871737e41520dfbb1",
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": "1bbffd3858c9a7fb6873f448579a8d888959ff336572a25eb9780ce55b31a2d7",
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.10.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
+ }