@blamejs/exceptd-skills 0.15.47 → 0.15.48

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.48 — 2026-05-30
4
+
5
+ Internal: the release flow is now driven by a phased orchestrator, `scripts/release.js`. Each subcommand (prepare, gates, commit, push, watch, merge, tag, release) runs one idempotent, resumable phase and exits with a script-safe code; the tag phase enforces a GUARD against tag-on-stale-HEAD and version skew between `package.json`, `manifest.json`, and the CHANGELOG heading. No change to the shipped CLI, catalogs, or skills.
6
+
3
7
  ## 0.15.47 — 2026-05-30
4
8
 
5
9
  A consistency pass on error envelopes and flag handling.
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-30T18:46:17.416Z",
3
+ "generated_at": "2026-05-30T19:25:42.050Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 54,
6
6
  "source_hashes": {
7
- "manifest.json": "913d165fe1132e187dfdb9938d26d905af595901ad839f7adb079ac3c78445d8",
7
+ "manifest.json": "136eb6c81cbfd016dad3104269115be5b1a1466088c8666868fd91ba5bd5fa5b",
8
8
  "data/atlas-ttps.json": "878b4a08bb73c8d20396d85cf433a88f2bc5e7a8cbf7f6ab773ce7ede0a11251",
9
9
  "data/attack-techniques.json": "84fad74c8497cab922ed64b814752f54aa4620c2a938cb06642ff1510e1c5cb3",
10
10
  "data/cve-catalog.json": "7a5f4e31401505e53330cdc4b54b39f8a8b04459d6b9411676d291c583ae535f",
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exceptd-security",
3
- "version": "0.15.47",
3
+ "version": "0.15.48",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation",
5
5
  "homepage": "https://exceptd.com",
6
6
  "license": "Apache-2.0",
@@ -53,7 +53,7 @@
53
53
  ],
54
54
  "last_threat_review": "2026-05-15",
55
55
  "signature": "0H+JfyUVmo/pVFEi5rLENATHjlukPVUqnOWmNPEH77wm8svKGK0aNJ46k6QU5GdHb8c9X9pVJKiuhON6AxDjDw==",
56
- "signed_at": "2026-05-30T18:44:57.971Z",
56
+ "signed_at": "2026-05-30T19:06:23.609Z",
57
57
  "cwe_refs": [
58
58
  "CWE-125",
59
59
  "CWE-362",
@@ -123,7 +123,7 @@
123
123
  ],
124
124
  "last_threat_review": "2026-05-17",
125
125
  "signature": "PHwHEsoy7ctBYOtlAfAdCDVfsq2Bpk9+qESSF+5dVkDcez2zp2v9Ihsv2vqMEs3QxMndyQ+t7NVezyt5VamSCg==",
126
- "signed_at": "2026-05-30T18:44:57.973Z",
126
+ "signed_at": "2026-05-30T19:06:23.611Z",
127
127
  "cwe_refs": [
128
128
  "CWE-1039",
129
129
  "CWE-1426",
@@ -196,7 +196,7 @@
196
196
  ],
197
197
  "last_threat_review": "2026-05-17",
198
198
  "signature": "dD4p7lcRtMyfITOncqLkpOeMy6x6gM0V7UlWHgLEdcxqODb1s75ar1cBtTqDWPbMv6ZAzVo2HJLDK1hVjjU2AQ==",
199
- "signed_at": "2026-05-30T18:44:57.973Z",
199
+ "signed_at": "2026-05-30T19:06:23.611Z",
200
200
  "cwe_refs": [
201
201
  "CWE-22",
202
202
  "CWE-345",
@@ -248,7 +248,7 @@
248
248
  "framework_gaps": [],
249
249
  "last_threat_review": "2026-05-22",
250
250
  "signature": "wsw8Mlr/gyw6S7Iaao9BVHdU5LFPWl8WVymW17Lkq9J1Mui0+fCrTg6UbrsaeE3s7EW3TVgzBuK+8EFd1+H5AA==",
251
- "signed_at": "2026-05-30T18:44:57.974Z"
251
+ "signed_at": "2026-05-30T19:06:23.612Z"
252
252
  },
253
253
  {
254
254
  "name": "compliance-theater",
@@ -279,7 +279,7 @@
279
279
  ],
280
280
  "last_threat_review": "2026-05-22",
281
281
  "signature": "uVTc1QRKOKcIVDajBz+q2egjiEAyOQaDNsvVI2ghj5FD0VvquoUBBE5Naca2FkaZa790EHWCsVZ4hhdaSQs2DQ==",
282
- "signed_at": "2026-05-30T18:44:57.974Z"
282
+ "signed_at": "2026-05-30T19:06:23.612Z"
283
283
  },
284
284
  {
285
285
  "name": "exploit-scoring",
@@ -308,7 +308,7 @@
308
308
  ],
309
309
  "last_threat_review": "2026-05-18",
310
310
  "signature": "QuNpwnZ6HkCEAXTPC/jLbXSmMIc1JnBczqZAAIZmZj8OcEMVnw9mJYAnU3CxaEI7rvbcMkN2uS5E8yUCm/NiAg==",
311
- "signed_at": "2026-05-30T18:44:57.975Z"
311
+ "signed_at": "2026-05-30T19:06:23.613Z"
312
312
  },
313
313
  {
314
314
  "name": "rag-pipeline-security",
@@ -345,7 +345,7 @@
345
345
  ],
346
346
  "last_threat_review": "2026-05-22",
347
347
  "signature": "5rw2i39SxY2WphBbDLEP28wufnbPPE9+PWt54hmaGdwHXr9RLiVt5liL/5xp14sehlVgFsfpR/bg9vy//xV0DA==",
348
- "signed_at": "2026-05-30T18:44:57.975Z",
348
+ "signed_at": "2026-05-30T19:06:23.613Z",
349
349
  "cwe_refs": [
350
350
  "CWE-1395",
351
351
  "CWE-1426"
@@ -405,7 +405,7 @@
405
405
  ],
406
406
  "last_threat_review": "2026-05-17",
407
407
  "signature": "Vqu49nzntFWjn9A/QeJzm7q/2xk/cZJ6HFQKtiNi1zgcxzXKm+MlFdkaLgYHWj5/9HJohxyIDyBJQTvcJ20eDQ==",
408
- "signed_at": "2026-05-30T18:44:57.975Z",
408
+ "signed_at": "2026-05-30T19:06:23.613Z",
409
409
  "d3fend_refs": [
410
410
  "D3-CA",
411
411
  "D3-CSPP",
@@ -440,7 +440,7 @@
440
440
  "framework_gaps": [],
441
441
  "last_threat_review": "2026-05-22",
442
442
  "signature": "W87VdyVdAxAdcRI6P/8StaV+MS8ZSPKM9HOCK9n/bBO6BM3ZSE3uImVoyJVpAXQlUpUGN+A3lCJZXv64LuxwDg==",
443
- "signed_at": "2026-05-30T18:44:57.976Z",
443
+ "signed_at": "2026-05-30T19:06:23.614Z",
444
444
  "cwe_refs": [
445
445
  "CWE-1188"
446
446
  ],
@@ -474,7 +474,7 @@
474
474
  "framework_gaps": [],
475
475
  "last_threat_review": "2026-05-18",
476
476
  "signature": "wdVX+edeNekpaIldqkhvtraV6DquLvIsKAjuZVwPQYn3l1vS99HXuFxmNsD7UeMlO3qgC6Dysfsto9EnuH0RBg==",
477
- "signed_at": "2026-05-30T18:44:57.976Z",
477
+ "signed_at": "2026-05-30T19:06:23.614Z",
478
478
  "forward_watch": [
479
479
  "New AI attack classes as ATLAS v6 publishes",
480
480
  "Post-quantum adversary capability timeline",
@@ -513,7 +513,7 @@
513
513
  "framework_gaps": [],
514
514
  "last_threat_review": "2026-05-01",
515
515
  "signature": "b5miTiY0cnxETd2btxorfZBdJKt/fLnQx20sGYUb9zEqGqtm0LMLpghkW68j4/9k48KNyuGMtNWiKTSnodUGBw==",
516
- "signed_at": "2026-05-30T18:44:57.976Z"
516
+ "signed_at": "2026-05-30T19:06:23.615Z"
517
517
  },
518
518
  {
519
519
  "name": "zeroday-gap-learn",
@@ -540,7 +540,7 @@
540
540
  "framework_gaps": [],
541
541
  "last_threat_review": "2026-05-18",
542
542
  "signature": "xbkip0AQtWQKAu+O6r/gYECNjezS6O9k9xkkJsYbMlr+j8CdqH3p5/0l+GZmDidImRC/DL07GCnKrk9HRR/yDQ==",
543
- "signed_at": "2026-05-30T18:44:57.977Z",
543
+ "signed_at": "2026-05-30T19:06:23.615Z",
544
544
  "forward_watch": [
545
545
  "New CISA KEV entries",
546
546
  "New ATLAS TTP additions in each ATLAS release",
@@ -604,7 +604,7 @@
604
604
  ],
605
605
  "last_threat_review": "2026-05-22",
606
606
  "signature": "li2NnC1oeVIr22ComP5QbcQoh5xpWITuaKpza1s2SsUkH6kGnnt4wFfFAzaC1ORmH9x2cr8hN8kaNANG/eIMBQ==",
607
- "signed_at": "2026-05-30T18:44:57.977Z",
607
+ "signed_at": "2026-05-30T19:06:23.615Z",
608
608
  "cwe_refs": [
609
609
  "CWE-327"
610
610
  ],
@@ -652,7 +652,7 @@
652
652
  ],
653
653
  "last_threat_review": "2026-05-22",
654
654
  "signature": "sZHlJ7ueHPdtzVbR+yXQ5+wKgNyjWsa1LKVg9aWTmg/Onl71DvEILMyJiLpPQjseT56Mnr1DMYJE8xOGlffBAw==",
655
- "signed_at": "2026-05-30T18:44:57.977Z"
655
+ "signed_at": "2026-05-30T19:06:23.616Z"
656
656
  },
657
657
  {
658
658
  "name": "security-maturity-tiers",
@@ -689,7 +689,7 @@
689
689
  ],
690
690
  "last_threat_review": "2026-05-01",
691
691
  "signature": "3AwFnEJu6DukPPNep/3SnuPWEuV060fJEQIwThFm7ujmdbFk0/Ii0XwGv1dkvbbK7ymMdOQpp35l4aLONAucDA==",
692
- "signed_at": "2026-05-30T18:44:57.978Z",
692
+ "signed_at": "2026-05-30T19:06:23.616Z",
693
693
  "cwe_refs": [
694
694
  "CWE-1188"
695
695
  ]
@@ -724,7 +724,7 @@
724
724
  "framework_gaps": [],
725
725
  "last_threat_review": "2026-05-11",
726
726
  "signature": "iJWevUBurLvt2v8X+Ch2eHmZkPWpKeAtIpxTIP4MwbUHyco3igDeBywJCyaR2vURYRx8LkzzIMM8DxQM4LAXBQ==",
727
- "signed_at": "2026-05-30T18:44:57.978Z"
727
+ "signed_at": "2026-05-30T19:06:23.616Z"
728
728
  },
729
729
  {
730
730
  "name": "attack-surface-pentest",
@@ -796,7 +796,7 @@
796
796
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — Microsoft Edge 4-bug sandbox escape by Orange Tsai (DEVCORE); forward-watch only (browser sandbox, out of current playbook scope); track Microsoft Edge security advisory and KEV add"
797
797
  ],
798
798
  "signature": "DDMzI+4En4aIkwBUCGW6nj1eEkCyLqHGn2LJ2rnwWfYatjPI1U5HrTZNAN/n9JqWtAzk8F3rmsKehaaz5iNWDA==",
799
- "signed_at": "2026-05-30T18:44:57.978Z"
799
+ "signed_at": "2026-05-30T19:06:23.617Z"
800
800
  },
801
801
  {
802
802
  "name": "fuzz-testing-strategy",
@@ -856,7 +856,7 @@
856
856
  "OSS-Fuzz-Gen / AI-assisted harness generation becoming the default expectation for OSS maintainers"
857
857
  ],
858
858
  "signature": "dJB0iAstIUbyny+udl3OIkaLScEmqS97LNP73yQ8mxt+0bcqxZjpfXaWLzLuIQblGYvUvz75/H6rO2EJuGd4AQ==",
859
- "signed_at": "2026-05-30T18:44:57.979Z"
859
+ "signed_at": "2026-05-30T19:06:23.617Z"
860
860
  },
861
861
  {
862
862
  "name": "dlp-gap-analysis",
@@ -931,7 +931,7 @@
931
931
  "Quebec Law 25, India DPDPA, KSA PDPL enforcement actions naming AI-tool prompt data as in-scope personal information"
932
932
  ],
933
933
  "signature": "KEAoMji3VcPX/ZXXqVe6OStxSkTssfY9fIRPyPcDYqh50GzOFQ6koNOTBVAiWOvjDjQ38g12xun5srbqgmvRAw==",
934
- "signed_at": "2026-05-30T18:44:57.979Z"
934
+ "signed_at": "2026-05-30T19:06:23.617Z"
935
935
  },
936
936
  {
937
937
  "name": "supply-chain-integrity",
@@ -1010,7 +1010,7 @@
1010
1010
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — NVIDIA Megatron Bridge path traversal by haehae; AI training-stack file-system trust boundary; track patch and SBOM-attestation impact"
1011
1011
  ],
1012
1012
  "signature": "UY3tBi0n1K/OtSrWPkHcOCSuHEwKuPmRqGIf3MyPVXGWS72elGTWGXt4AN/uStLmefeEody1LuhnJR9PWjr4Cg==",
1013
- "signed_at": "2026-05-30T18:44:57.979Z"
1013
+ "signed_at": "2026-05-30T19:06:23.618Z"
1014
1014
  },
1015
1015
  {
1016
1016
  "name": "defensive-countermeasure-mapping",
@@ -1067,7 +1067,7 @@
1067
1067
  ],
1068
1068
  "last_threat_review": "2026-05-11",
1069
1069
  "signature": "Qe0Hg9BrX3Zm5pj0n2z/oiHbAXWdA2Dq461zc4izkkUjEX2CZ02rODjCI2ELbrVOU3GC7edxqAxA+5U/ObnHDQ==",
1070
- "signed_at": "2026-05-30T18:44:57.980Z"
1070
+ "signed_at": "2026-05-30T19:06:23.618Z"
1071
1071
  },
1072
1072
  {
1073
1073
  "name": "identity-assurance",
@@ -1134,7 +1134,7 @@
1134
1134
  "d3fend_refs": [],
1135
1135
  "last_threat_review": "2026-05-11",
1136
1136
  "signature": "UV3458QXSkEpenzrOmdlTTfPHUD4hNyKMDHoeZDq/kiFb4mAG0ghQGTTgI9Ru8cJbSmYM1++m9N5TFIJ6JJPBg==",
1137
- "signed_at": "2026-05-30T18:44:57.980Z"
1137
+ "signed_at": "2026-05-30T19:06:23.618Z"
1138
1138
  },
1139
1139
  {
1140
1140
  "name": "ot-ics-security",
@@ -1190,7 +1190,7 @@
1190
1190
  "d3fend_refs": [],
1191
1191
  "last_threat_review": "2026-05-11",
1192
1192
  "signature": "kIVzsPsJ72PzzWQwTuvjoHHoVEDCday5I52M9ohjB3/Ak+zlA8oyWLO/BKb/XuYY4fOApjfxTErSWv5uHQ2zDw==",
1193
- "signed_at": "2026-05-30T18:44:57.980Z"
1193
+ "signed_at": "2026-05-30T19:06:23.619Z"
1194
1194
  },
1195
1195
  {
1196
1196
  "name": "coordinated-vuln-disclosure",
@@ -1242,7 +1242,7 @@
1242
1242
  "NYDFS 23 NYCRR 500 amendments potentially adding explicit CVD program requirements"
1243
1243
  ],
1244
1244
  "signature": "bWr27Q1uN9xCe1ib4QulszBa7YIDNkGqo72k5nm2cK98LyPblicD+sO9MnGckAyB22BTN/cIB+FwFMcI5IxvBw==",
1245
- "signed_at": "2026-05-30T18:44:57.981Z"
1245
+ "signed_at": "2026-05-30T19:06:23.619Z"
1246
1246
  },
1247
1247
  {
1248
1248
  "name": "threat-modeling-methodology",
@@ -1292,7 +1292,7 @@
1292
1292
  "PASTA v2 updates incorporating AI/ML application threats"
1293
1293
  ],
1294
1294
  "signature": "Q854yzLqXdOazc6EyQbZzgAlivuq2vGFDVUCrxSldSvx/HX/ZM/uzmJyP7aBG7ZsMHxj6Lmj/H82YQoo1e+NCQ==",
1295
- "signed_at": "2026-05-30T18:44:57.981Z"
1295
+ "signed_at": "2026-05-30T19:06:23.619Z"
1296
1296
  },
1297
1297
  {
1298
1298
  "name": "webapp-security",
@@ -1366,7 +1366,7 @@
1366
1366
  "d3fend_refs": [],
1367
1367
  "last_threat_review": "2026-05-11",
1368
1368
  "signature": "4ccahkJpGJZtwD7EBpnGcN0sEGPMEw8eqV+tvePVS04YAkLgYVWtlkasI/8n0be9xB+77x+Sjj3kIi2j2Lf9CA==",
1369
- "signed_at": "2026-05-30T18:44:57.981Z",
1369
+ "signed_at": "2026-05-30T19:06:23.620Z",
1370
1370
  "forward_watch": [
1371
1371
  "NGINX Rift CVE-2026-42945 (disclosed 2026-05-13, source depthfirst) — KEV-watch predicted CISA KEV listing by 2026-05-29; AI-assisted discovery angle; track for active-exploitation confirmation and patch advisory affecting front-door web app deployments"
1372
1372
  ]
@@ -1419,7 +1419,7 @@
1419
1419
  "d3fend_refs": [],
1420
1420
  "last_threat_review": "2026-05-15",
1421
1421
  "signature": "SBB7c3wNYfIdkyOp4g4nW0WP7xS+YokMzg32aaeJdbf14LTGQRzQUvSqb2TCj2HFUSHESOyKT1JpkAfyHLSQBQ==",
1422
- "signed_at": "2026-05-30T18:44:57.982Z"
1422
+ "signed_at": "2026-05-30T19:06:23.620Z"
1423
1423
  },
1424
1424
  {
1425
1425
  "name": "sector-healthcare",
@@ -1479,7 +1479,7 @@
1479
1479
  "d3fend_refs": [],
1480
1480
  "last_threat_review": "2026-05-11",
1481
1481
  "signature": "U04GNLyRas1VmfEsB8khH4iqFZPwx96sPY0Kw9iVsSPU+KTeEFqwgtWK1X1pzgb+T16Pc7HSrCaXDOpTFvQEDw==",
1482
- "signed_at": "2026-05-30T18:44:57.982Z"
1482
+ "signed_at": "2026-05-30T19:06:23.621Z"
1483
1483
  },
1484
1484
  {
1485
1485
  "name": "sector-financial",
@@ -1560,7 +1560,7 @@
1560
1560
  "TIBER-EU framework v2.0 alignment with DORA TLPT RTS (JC 2024/40); cross-recognition with CBEST and iCAST"
1561
1561
  ],
1562
1562
  "signature": "xbylLqNPBuEsFE/MNVeGy/01K6yiJXMxQbzC1F4RWU5aseDGbNy5HrAv2JWI2+Aft05ozreNPjccvu66yJ5EBw==",
1563
- "signed_at": "2026-05-30T18:44:57.982Z"
1563
+ "signed_at": "2026-05-30T19:06:23.621Z"
1564
1564
  },
1565
1565
  {
1566
1566
  "name": "sector-federal-government",
@@ -1629,7 +1629,7 @@
1629
1629
  "Australia PSPF 2024 revision and ISM quarterly updates — track for Essential Eight Maturity Level requirements for federal entities"
1630
1630
  ],
1631
1631
  "signature": "C9c3JuBhUbwcb7uZpDdy+PNT8sYmYIxzD4uRHu421ePW1aSFJ8fkMvuTzSO8vD/F/jOOg5opM4kov/xSAn+qCg==",
1632
- "signed_at": "2026-05-30T18:44:57.983Z"
1632
+ "signed_at": "2026-05-30T19:06:23.622Z"
1633
1633
  },
1634
1634
  {
1635
1635
  "name": "sector-energy",
@@ -1694,7 +1694,7 @@
1694
1694
  "ICS-CERT advisory feed (https://www.cisa.gov/news-events/cybersecurity-advisories/ics-advisories) for vendor CVEs in Siemens, Rockwell, Schneider Electric, ABB, GE Vernova, Hitachi Energy, AVEVA / OSIsoft PI"
1695
1695
  ],
1696
1696
  "signature": "oz8Q5WVaY8au4IjbaZahx/DSaC00Q44ylSL3mDkTerCEpW/EyPUeiLeGxSrWxBCwVFEKSSJvnhJjhvX5lDPcCg==",
1697
- "signed_at": "2026-05-30T18:44:57.983Z"
1697
+ "signed_at": "2026-05-30T19:06:23.622Z"
1698
1698
  },
1699
1699
  {
1700
1700
  "name": "sector-telecom",
@@ -1780,7 +1780,7 @@
1780
1780
  "O-RAN SFG / WG11 security specifications"
1781
1781
  ],
1782
1782
  "signature": "NAtyzfLPXlUuB78Snb9nWmbZalC1CNlIYN9rYhdEmtB/xQGC6vVnThgrEAHlm7v/jMCFuknvEpUHKdscUnUADw==",
1783
- "signed_at": "2026-05-30T18:44:57.983Z"
1783
+ "signed_at": "2026-05-30T19:06:23.622Z"
1784
1784
  },
1785
1785
  {
1786
1786
  "name": "api-security",
@@ -1849,7 +1849,7 @@
1849
1849
  "d3fend_refs": [],
1850
1850
  "last_threat_review": "2026-05-18",
1851
1851
  "signature": "1UTjZNC5Lyrgw93LAizdXVeSmv3jS8YQNT1db5OKsldub50+o1FXmAH4+3MxZozaOGDCX3yXbdDJSJaaSmfuAA==",
1852
- "signed_at": "2026-05-30T18:44:57.984Z",
1852
+ "signed_at": "2026-05-30T19:06:23.623Z",
1853
1853
  "forward_watch": [
1854
1854
  "NGINX Rift CVE-2026-42945 (disclosed 2026-05-13, source depthfirst) — KEV-watch predicted CISA KEV listing by 2026-05-29; track for active-exploitation confirmation and patch advisory affecting API gateway / reverse-proxy deployments",
1855
1855
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — LiteLLM 3-bug SSRF + Code Injection chain by k3vg3n; LLM-proxy API surface; track upstream patch and CVE assignments",
@@ -1935,7 +1935,7 @@
1935
1935
  "CISA KEV additions for cloud-control-plane CVEs (IMDSv1 abuses, federation token mishandling, cross-tenant boundary failures); CISA Cybersecurity Advisories for cross-cloud advisories"
1936
1936
  ],
1937
1937
  "signature": "EdsY4xe7YA8X8m+KZUbq49JwoCXgRKEz2eg3m86O37rvBmpm8ppvl9hrsekygvpBh2VmCHL2dEYiOD8OM2n7CA==",
1938
- "signed_at": "2026-05-30T18:44:57.984Z"
1938
+ "signed_at": "2026-05-30T19:06:23.623Z"
1939
1939
  },
1940
1940
  {
1941
1941
  "name": "container-runtime-security",
@@ -1997,7 +1997,7 @@
1997
1997
  "d3fend_refs": [],
1998
1998
  "last_threat_review": "2026-05-15",
1999
1999
  "signature": "fnLKPLkjjRCJ/F9wdmZ1w1lXmqEJvTYkv6Uu+9OTd5vZTWKz3QMuxKOsas+ctCdOvTaeloqPUUprXx+ZZdDpCg==",
2000
- "signed_at": "2026-05-30T18:44:57.984Z",
2000
+ "signed_at": "2026-05-30T19:06:23.623Z",
2001
2001
  "forward_watch": [
2002
2002
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — NVIDIA Container Toolkit container escape ($50K award) by chompie / IBM X-Force XOR; high-severity container/hypervisor boundary break; track patch and KEV add post-embargo"
2003
2003
  ]
@@ -2071,7 +2071,7 @@
2071
2071
  "MITRE ATLAS v5.6.0 (released May 2026) shipped the AML.T0010 sub-technique expansion this forecast tracked plus new techniques (\"Publish Poisoned AI Agent Tool\", \"Escape to Host\"); inventory now 16 tactics, 84 techniques, 56 sub-techniques. Forward watch: subsequent ATLAS minor and major releases — track next-cadence updates to agentic-AI TTPs and MLOps-pipeline-specific techniques"
2072
2072
  ],
2073
2073
  "signature": "t3dkdpTX04zvjitEeOJThpgjurLd1UO9GOut4LXSZgY3ULhfknI4zT7G5+m2RSZZTo7yyeZrwpg+7vEg9K6mAw==",
2074
- "signed_at": "2026-05-30T18:44:57.985Z"
2074
+ "signed_at": "2026-05-30T19:06:23.624Z"
2075
2075
  },
2076
2076
  {
2077
2077
  "name": "incident-response-playbook",
@@ -2133,7 +2133,7 @@
2133
2133
  "NYDFS 23 NYCRR 500.17 amendments tightening ransom-payment 24h disclosure operationalization"
2134
2134
  ],
2135
2135
  "signature": "+1kmtA6rAvIyDjjy+cJHK6BcfylyVsa5cUjRFijlFR9GsQfB93JnmkEJOqML50pdlcxtJI3yUodHpL3/YJGtCA==",
2136
- "signed_at": "2026-05-30T18:44:57.985Z"
2136
+ "signed_at": "2026-05-30T19:06:23.624Z"
2137
2137
  },
2138
2138
  {
2139
2139
  "name": "ransomware-response",
@@ -2213,7 +2213,7 @@
2213
2213
  ],
2214
2214
  "last_threat_review": "2026-05-22",
2215
2215
  "signature": "h48ASCz63aBfHzLKxMVDADMuT4atriK0iE6bJeVzZTsx/e8+hyv4fLP7+zYxT9Oe0Gss3v/Xy+t+Wd9uwzV+Aw==",
2216
- "signed_at": "2026-05-30T18:44:57.985Z"
2216
+ "signed_at": "2026-05-30T19:06:23.625Z"
2217
2217
  },
2218
2218
  {
2219
2219
  "name": "email-security-anti-phishing",
@@ -2266,7 +2266,7 @@
2266
2266
  "d3fend_refs": [],
2267
2267
  "last_threat_review": "2026-05-18",
2268
2268
  "signature": "FVBn4ex2qPIo9SHMVJ6tntoz4tVwjbIq3m6wDjjZyv2JODlS+90GBYCOkNamxxkmw/6de6SMs0YHQiF/xjo/DQ==",
2269
- "signed_at": "2026-05-30T18:44:57.986Z"
2269
+ "signed_at": "2026-05-30T19:06:23.625Z"
2270
2270
  },
2271
2271
  {
2272
2272
  "name": "age-gates-child-safety",
@@ -2334,7 +2334,7 @@
2334
2334
  "US state adult-site age-verification laws — 19+ states by mid-2026 (TX HB 18 upheld by SCOTUS June 2025 in Free Speech Coalition v. Paxton); track ongoing challenges in remaining states"
2335
2335
  ],
2336
2336
  "signature": "ZHVdGWCcfG98tSVB0b9mwrsYwv71V3uUEl+6ss7omSQhmNvqV5s6MAZM5YladBt9MK/8T/zBrTYN4gAonOP+BQ==",
2337
- "signed_at": "2026-05-30T18:44:57.986Z"
2337
+ "signed_at": "2026-05-30T19:06:23.625Z"
2338
2338
  },
2339
2339
  {
2340
2340
  "name": "cloud-iam-incident",
@@ -2414,7 +2414,7 @@
2414
2414
  ],
2415
2415
  "last_threat_review": "2026-05-15",
2416
2416
  "signature": "r9ii4nb3HJELdtKCGF5qy9PHOiot3GC24yfxfGAKlLENHkdRvRkvvL99eV/6RXyfUaMyrnc2Te8tPQcNu5bsDg==",
2417
- "signed_at": "2026-05-30T18:44:57.987Z",
2417
+ "signed_at": "2026-05-30T19:06:23.626Z",
2418
2418
  "forward_watch": [
2419
2419
  "AWS IAM Identity Center session-policy refresh and step-up-on-admin enforcement (anticipated 2026-H2 release)",
2420
2420
  "GCP Workload Identity Federation principal-set attribute mapping tightening (post-2026 Q3 Federation hardening guide)",
@@ -2508,7 +2508,7 @@
2508
2508
  ],
2509
2509
  "last_threat_review": "2026-05-15",
2510
2510
  "signature": "9mfDtMApMAg9V/lmwpniNxo/6gNZoOEoYDfyFvyWvKrPMtc7H9F8uz06FVoARe/J49saAKTVXOurNE1D/KtpCQ==",
2511
- "signed_at": "2026-05-30T18:44:57.987Z",
2511
+ "signed_at": "2026-05-30T19:06:23.626Z",
2512
2512
  "forward_watch": [
2513
2513
  "Entra ID conditional access evolution post-Midnight Blizzard — Microsoft's 2025-2026 commitments on legacy-tenant MFA enforcement and OAuth-app consent gating",
2514
2514
  "Okta IPSIE (Interoperability Profile for Secure Identity in the Enterprise) OpenID Foundation working-group output and adoption timeline",
@@ -2526,6 +2526,6 @@
2526
2526
  ],
2527
2527
  "manifest_signature": {
2528
2528
  "algorithm": "Ed25519",
2529
- "signature_base64": "FVMT7weOuT7njeR5qg8qOyW+3aWP0A53J2WMgxVrguzYVDA5Z1a+LXGIOiBoGE0fi0MHw4Lnbi7bZvjWV3FUDA=="
2529
+ "signature_base64": "7Fl14Lf0ULx1ghjig+1D9D7B1ldxqbAWNpvWXpIVX4P9pSu57BvEXkfwW3dz8pqsG86uob4IYIQMVhG1EHUxCA=="
2530
2530
  }
2531
2531
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/exceptd-skills",
3
- "version": "0.15.47",
3
+ "version": "0.15.48",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 42 skills, 11 catalogs (427 CVEs / 173 CWEs / 805 ATT&CK + ICS / 170 ATLAS / 468 D3FEND / 8888 RFCs), 35 jurisdictions, 10-class catalog gap detector + budget gate, real XML parser + canonical-form diff + content-pattern regression detection, Ed25519-signed.",
5
5
  "keywords": [
6
6
  "ai-security",
package/sbom.cdx.json CHANGED
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.6",
4
- "serialNumber": "urn:uuid:21ba1af0-38cd-48d8-9d2f-1354b42f3829",
4
+ "serialNumber": "urn:uuid:bfbc8ed4-a484-4dfb-b1a6-636dcffa929b",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2043-12-07T03:05:20.000Z",
7
+ "timestamp": "2127-12-09T13:06:28.000Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "blamejs",
11
11
  "name": "scripts/refresh-sbom.js",
12
- "version": "0.15.47"
12
+ "version": "0.15.48"
13
13
  }
14
14
  ],
15
15
  "component": {
16
- "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.15.47",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.15.48",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.15.47",
19
+ "version": "0.15.48",
20
20
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 42 skills, 11 catalogs (427 CVEs / 173 CWEs / 805 ATT&CK + ICS / 170 ATLAS / 468 D3FEND / 8888 RFCs), 35 jurisdictions, 10-class catalog gap detector + budget gate, real XML parser + canonical-form diff + content-pattern regression detection, Ed25519-signed.",
21
21
  "licenses": [
22
22
  {
@@ -25,17 +25,17 @@
25
25
  }
26
26
  }
27
27
  ],
28
- "purl": "pkg:npm/%40blamejs/exceptd-skills@0.15.47",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.15.48",
29
29
  "hashes": [
30
30
  {
31
31
  "alg": "SHA-256",
32
- "content": "83332c01cf1199c050266b7a6a1fc12e2a74f581f5802c3dbccc49ef564e1668"
32
+ "content": "6f18d931a960c7dc8adebbc19cea574865ccfe8cc7f7d59d38c54bca38c8774c"
33
33
  }
34
34
  ],
35
35
  "externalReferences": [
36
36
  {
37
37
  "type": "distribution",
38
- "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.15.47"
38
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.15.48"
39
39
  },
40
40
  {
41
41
  "type": "vcs",
@@ -116,11 +116,11 @@
116
116
  "hashes": [
117
117
  {
118
118
  "alg": "SHA-256",
119
- "content": "0252f12293082acb79c1aa063ba448cdb954d6916c4bf2539bf6956892a795a6"
119
+ "content": "54eb3a6e90c36720596c68369b3bc665fd5ee6fcf1e6baea97b01cd979abdef7"
120
120
  },
121
121
  {
122
122
  "alg": "SHA3-512",
123
- "content": "38c6fba550906f5e13c97d12c4b9024bc8e45f57b1c947e0afb6994179b4554e4a898763ec02cdbc8594ae3d7ec812cb75ae38259a4b04a5634cdeed6ef7ce52"
123
+ "content": "ec3265813bec9e5a9ac3687807ee18fd2f33d80841e489cce57e7defa90c8b66851cc93ed29294cc8ec2719db4878312f0d283cd756797b2f76f6de69448d6cd"
124
124
  }
125
125
  ]
126
126
  },
@@ -1751,11 +1751,11 @@
1751
1751
  "hashes": [
1752
1752
  {
1753
1753
  "alg": "SHA-256",
1754
- "content": "913d165fe1132e187dfdb9938d26d905af595901ad839f7adb079ac3c78445d8"
1754
+ "content": "136eb6c81cbfd016dad3104269115be5b1a1466088c8666868fd91ba5bd5fa5b"
1755
1755
  },
1756
1756
  {
1757
1757
  "alg": "SHA3-512",
1758
- "content": "17d34176ec3505071195eea1a2ba655687d14aba48d8cd1f1181af13cc688ca5b88c45de73ad72a6f0b2e3295b150727d6fa9470d10010348496e043185de7fb"
1758
+ "content": "f912df81fe64a8f9d506ececd6e6d1040be6bc173b0c154182f33bac50304821a8833a09bd1b2b6c084cc23a5f59aef3a6b435f1c80a198afc55301346750d06"
1759
1759
  }
1760
1760
  ]
1761
1761
  },
@@ -2434,6 +2434,21 @@
2434
2434
  }
2435
2435
  ]
2436
2436
  },
2437
+ {
2438
+ "bom-ref": "file:scripts/release.js",
2439
+ "type": "file",
2440
+ "name": "scripts/release.js",
2441
+ "hashes": [
2442
+ {
2443
+ "alg": "SHA-256",
2444
+ "content": "3e538e0658b9137cb126aef9b80ba2fa1afb2355242a28f5728a9a3291263fc5"
2445
+ },
2446
+ {
2447
+ "alg": "SHA3-512",
2448
+ "content": "f0e60ce16ed4cf06fb96c9f2d7cee1181bdbe7985d8bd224dc1bf635287132a4828fd3360be4bb5550859d8a815807642fc3e9033e4acec3e15207d98619ba4c"
2449
+ }
2450
+ ]
2451
+ },
2437
2452
  {
2438
2453
  "bom-ref": "file:scripts/run-e2e-scenarios.js",
2439
2454
  "type": "file",
@@ -0,0 +1,593 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * release.js — orchestrate the exceptd release flow as a sequence of
5
+ * idempotent subcommands. Each subcommand performs ONE phase, prints what
6
+ * it did, and exits with a code that's safe to script against in a terminal
7
+ * or CI runner. It codifies the flow that CONTRIBUTING.md / the repo's
8
+ * release notes describe step by step, so a release can't skip the
9
+ * load-bearing ordering (CHANGELOG entry first, gates before tag, CI green
10
+ * before tag push, GUARD before tag).
11
+ *
12
+ * Usage:
13
+ * node scripts/release.js prepare [--minor] # bump + sign + indexes + snapshot + sbom + baseline
14
+ * node scripts/release.js gates # npm test + 18-gate predeploy
15
+ * node scripts/release.js commit # release branch + signed commit
16
+ * node scripts/release.js push # push branch + open PR
17
+ * node scripts/release.js watch # CI watch + flag unresolved review threads
18
+ * node scripts/release.js merge # admin squash-merge if CLEAN + zero unresolved
19
+ * node scripts/release.js tag # GUARD + signed tag + push tag + verify
20
+ * node scripts/release.js release # watch release.yml + npm/global/tarball verify
21
+ * node scripts/release.js all [--minor] # all eight in sequence
22
+ * node scripts/release.js status # what phase the current branch is in
23
+ * node scripts/release.js help # this banner
24
+ *
25
+ * Pre-conditions the script enforces rather than assumes:
26
+ * - prepare runs only on a clean `main`, and refuses unless CHANGELOG.md
27
+ * already carries a `## <next-version>` heading (the operator writes the
28
+ * behavior-framed notes by hand; they don't auto-generate from a diff).
29
+ * - The three-version invariant (package.json == manifest.json ==
30
+ * CHANGELOG top heading) is established by prepare and re-checked by tag.
31
+ * - tag refuses unless local HEAD == origin/main and the version matches
32
+ * and no such tag exists (the GUARD that prevents tag-on-stale-HEAD).
33
+ *
34
+ * Patch is the default bump. --minor requires the explicit flag AND is a
35
+ * deliberate choice — the project default is patch-only.
36
+ *
37
+ * The judgment-requiring parts stay manual: writing the CHANGELOG entry,
38
+ * reviewing/fixing CI-surfaced review-thread findings (watch flags them and
39
+ * stops), and choosing patch vs minor.
40
+ */
41
+
42
+ var fs = require("node:fs");
43
+ var path = require("node:path");
44
+ var childProcess = require("node:child_process");
45
+
46
+ var ROOT = path.resolve(__dirname, "..");
47
+ var REPO = "blamejs/exceptd-skills";
48
+ var PKG_NAME = "@blamejs/exceptd-skills";
49
+
50
+ // Known-flaky CI jobs that warrant an auto-rerun rather than a hard fail:
51
+ // the macOS playbook-runner job and the offline-CLI F1 check both flake on
52
+ // fresh runners. watch reruns them up to twice before surfacing a failure.
53
+ var RERUN_LIMIT = 2;
54
+
55
+ // ---- Helpers -------------------------------------------------------------
56
+
57
+ // Windows resolves `npm` / `npx` as `.cmd` shims, which child_process can
58
+ // only invoke through a shell. `git`, `gh`, `node` are native exes that
59
+ // spawn directly — keeping shell off avoids the implicit arg-quoting risk.
60
+ function _needsShell(cmd) {
61
+ if (process.platform !== "win32") return false;
62
+ return cmd === "npm" || cmd === "npx";
63
+ }
64
+
65
+ // spawnSync with shell:true AND an args array concatenates the args without
66
+ // escaping (Node DEP0190 — a real injection surface). When a shell is needed
67
+ // (npm/npx on Windows) we instead pass the whole invocation as one command
68
+ // string with no args array, which is the correct shell-invocation form. The
69
+ // only commands routed through the shell here are npm with static token args
70
+ // (verb names + flags, no spaces), so the single-string join is unambiguous.
71
+ function _spawn(cmd, args, opts) {
72
+ opts = opts || {};
73
+ var useShell = _needsShell(cmd);
74
+ var spawnCmd = cmd;
75
+ var spawnArgs = args || [];
76
+ if (useShell) {
77
+ spawnCmd = [cmd].concat(args || []).join(" ");
78
+ spawnArgs = [];
79
+ }
80
+ return childProcess.spawnSync(spawnCmd, spawnArgs, {
81
+ cwd: opts.cwd || ROOT,
82
+ stdio: opts.stdio || "inherit",
83
+ env: Object.assign({}, process.env, opts.env || {}),
84
+ shell: useShell,
85
+ });
86
+ }
87
+
88
+ function _run(cmd, args, opts) {
89
+ opts = opts || {};
90
+ var rv = _spawn(cmd, args, { cwd: opts.cwd, stdio: opts.stdio || "inherit", env: opts.env });
91
+ if (rv.status !== 0 && !opts.allowFail) {
92
+ throw new Error("release: " + cmd + " " + (args || []).join(" ") +
93
+ " failed with status " + rv.status);
94
+ }
95
+ return rv;
96
+ }
97
+
98
+ function _capture(cmd, args, opts) {
99
+ opts = opts || {};
100
+ var rv = _spawn(cmd, args, { cwd: opts.cwd, stdio: ["ignore", "pipe", "pipe"], env: opts.env });
101
+ return {
102
+ status: rv.status,
103
+ stdout: (rv.stdout || "").toString().trim(),
104
+ stderr: (rv.stderr || "").toString().trim(),
105
+ };
106
+ }
107
+
108
+ function _section(title) { console.log("\n=== " + title + " ==="); }
109
+ function _ok(msg) { console.log("ok: " + msg); }
110
+
111
+ function _readJsonVersion(file) {
112
+ return JSON.parse(fs.readFileSync(path.join(ROOT, file), "utf8")).version;
113
+ }
114
+
115
+ // Rewrite only the top-level "version" line so formatting/key-order of the
116
+ // rest of the file is untouched (a full JSON.stringify would reflow
117
+ // manifest.json's hand-maintained shape).
118
+ function _writeJsonVersion(file, next) {
119
+ var p = path.join(ROOT, file);
120
+ var content = fs.readFileSync(p, "utf8");
121
+ var updated = content.replace(/"version":\s*"[^"]+"/, '"version": "' + next + '"');
122
+ if (updated === content) {
123
+ throw new Error("release: failed to rewrite " + file + " version line");
124
+ }
125
+ fs.writeFileSync(p, updated);
126
+ }
127
+
128
+ function _bump(version, kind) {
129
+ var parts = version.split(".").map(Number);
130
+ if (parts.length !== 3 || parts.some(isNaN)) {
131
+ throw new Error("release: unparseable current version '" + version + "'");
132
+ }
133
+ if (kind === "minor") return parts[0] + "." + (parts[1] + 1) + ".0";
134
+ return parts[0] + "." + parts[1] + "." + (parts[2] + 1);
135
+ }
136
+
137
+ // Topmost `## X.Y.Z` heading in CHANGELOG.md.
138
+ function _changelogTopVersion() {
139
+ var lines = fs.readFileSync(path.join(ROOT, "CHANGELOG.md"), "utf8").split(/\r?\n/);
140
+ for (var i = 0; i < lines.length; i++) {
141
+ var m = lines[i].match(/^##\s+(\d+\.\d+\.\d+)\b/);
142
+ if (m) return m[1];
143
+ }
144
+ return null;
145
+ }
146
+
147
+ // Extract the body of a CHANGELOG section (between its `## X.Y.Z` heading
148
+ // and the next `## ` heading) — used to compose the commit + PR body.
149
+ function _changelogSection(version) {
150
+ var lines = fs.readFileSync(path.join(ROOT, "CHANGELOG.md"), "utf8").split(/\r?\n/);
151
+ var out = [];
152
+ var inSection = false;
153
+ for (var i = 0; i < lines.length; i++) {
154
+ if (/^##\s+/.test(lines[i])) {
155
+ if (inSection) break;
156
+ var m = lines[i].match(/^##\s+(\d+\.\d+\.\d+)\b/);
157
+ if (m && m[1] === version) { inSection = true; continue; }
158
+ } else if (inSection) {
159
+ out.push(lines[i]);
160
+ }
161
+ }
162
+ return out.join("\n").trim();
163
+ }
164
+
165
+ // Derive a concise "vX.Y.Z: <subject>" commit/PR title from a CHANGELOG
166
+ // section. exceptd's entries lead with a prose paragraph (no dedicated
167
+ // headline field), so take the first SENTENCE and cap the length rather than
168
+ // dump the whole paragraph as the subject. The operator can always amend.
169
+ function _releaseSubject(version, section) {
170
+ var firstLine = (section.split(/\r?\n/).find(function (l) { return l.trim(); }) || "").trim();
171
+ var firstSentence = firstLine.split(/(?<=[.!?])\s/)[0] || firstLine;
172
+ var subject = "v" + version + ": " + firstSentence.replace(/[.!?]$/, "");
173
+ if (subject.length > 72) subject = subject.slice(0, 69).replace(/\s+\S*$/, "") + "…";
174
+ return subject;
175
+ }
176
+
177
+ function _gitClean() { return _capture("git", ["status", "--porcelain"]).stdout === ""; }
178
+ function _gitBranch() { return _capture("git", ["rev-parse", "--abbrev-ref", "HEAD"]).stdout; }
179
+ function _gitOnMain() { return _gitBranch() === "main"; }
180
+ function _gitOnRelease() { return /^release-v\d+\.\d+\.\d+$/.test(_gitBranch()); }
181
+ function _releaseBranchFor(version) { return "release-v" + version; }
182
+
183
+ // Verify HEAD's commit signature two independent ways: `git verify-commit`
184
+ // (the canonical boolean GitHub's required_signatures ruleset checks) and a
185
+ // human-readable `%G? %GS` line. main is under required_signatures, so an
186
+ // unsigned commit can't be pushed — fail loudly here rather than at push.
187
+ function _verifyCommitSignature(label) {
188
+ var verify = _capture("git", ["verify-commit", "HEAD"]);
189
+ if (verify.status !== 0) {
190
+ var hint = "release: " + label + " commit signature is not Good — check SSH " +
191
+ "signing setup (commit.gpgsign=true + gpg.format=ssh + the public key " +
192
+ "registered as a GitHub signing key).";
193
+ if (verify.stderr) hint += "\n" + verify.stderr;
194
+ throw new Error(hint);
195
+ }
196
+ var sig = _capture("git", ["log", "-1", "--pretty=%h %G? %GS"]);
197
+ console.log("signature: " + (sig.stdout || "(empty — verify-commit reports Good)"));
198
+ _ok(label + " commit signature verified");
199
+ }
200
+
201
+ function _openPrNumber(branch) {
202
+ return _capture("gh", ["pr", "list", "--head", branch, "--state", "open",
203
+ "--json", "number", "--jq", ".[0].number"]).stdout;
204
+ }
205
+
206
+ // Unresolved review threads on the PR. Codex (chatgpt-codex-connector) posts
207
+ // review threads with P-badge findings; an unresolved thread is a hard
208
+ // branch-protection merge block (conversation-resolution required).
209
+ function _unresolvedThreads(prNum) {
210
+ var q = 'query { repository(owner:"blamejs",name:"exceptd-skills") { pullRequest(number:' +
211
+ prNum + ') { reviewThreads(first:50) { nodes { isResolved comments(first:1) ' +
212
+ '{ nodes { author{login} body } } } } } } }';
213
+ var rv = _capture("gh", ["api", "graphql", "-f", "query=" + q,
214
+ "--jq", ".data.repository.pullRequest.reviewThreads.nodes | map(select(.isResolved==false))"]);
215
+ try { return JSON.parse(rv.stdout || "[]"); } catch (_e) { return []; }
216
+ }
217
+
218
+ // ---- Subcommands ---------------------------------------------------------
219
+
220
+ function cmdPrepare(opts) {
221
+ _section("prepare");
222
+ if (!_gitOnMain()) throw new Error("release: prepare must run on main (on " + _gitBranch() + ")");
223
+ // The documented flow is: write the `## <next>` CHANGELOG entry by hand,
224
+ // THEN run prepare. That edit makes the tree dirty, so requiring a fully
225
+ // clean tree here would make the first phase unusable as documented. Allow
226
+ // a CHANGELOG.md-only dirty tree; refuse if anything else is uncommitted
227
+ // (prepare is about to bump versions + regenerate artifacts — it must start
228
+ // from an otherwise-clean main so the release commit captures only the
229
+ // intended change set).
230
+ var dirty = _capture("git", ["status", "--porcelain"]).stdout
231
+ .split(/\r?\n/)
232
+ .filter(function (l) { return l.trim() && !/\bCHANGELOG\.md$/.test(l); });
233
+ if (dirty.length) {
234
+ throw new Error("release: prepare requires a clean working tree (CHANGELOG.md may be pre-edited). Also uncommitted:\n " +
235
+ dirty.join("\n "));
236
+ }
237
+
238
+ var current = _readJsonVersion("package.json");
239
+ var next = _bump(current, opts.minor ? "minor" : "patch");
240
+ console.log("current: " + current + " next: " + next + " (" + (opts.minor ? "minor" : "patch") + ")");
241
+
242
+ // The CHANGELOG entry is written by hand (behavior-framed, no internal
243
+ // narrative). Refuse if it isn't there — the three-version invariant the
244
+ // bootstrap-mode test enforces would otherwise fail at gates time.
245
+ var top = _changelogTopVersion();
246
+ if (top !== next) {
247
+ console.error("");
248
+ console.error("release: CHANGELOG.md top heading is '## " + top + "', expected '## " + next + "'.");
249
+ console.error("Write the " + next + " entry first (terse, behavior-change framed, no internal");
250
+ console.error("narrative), then re-run prepare. Example heading:");
251
+ console.error("");
252
+ console.error(" ## " + next + " — <YYYY-MM-DD>");
253
+ console.error("");
254
+ process.exit(2);
255
+ }
256
+
257
+ _writeJsonVersion("package.json", next);
258
+ _writeJsonVersion("manifest.json", next);
259
+ _ok("bumped package.json + manifest.json → " + next);
260
+
261
+ _section("regen artifacts");
262
+ // Order matters: sign first (re-signs the manifest), then the snapshot/
263
+ // index/SBOM derivations. refresh-sbom runs LAST because it hashes the
264
+ // shipped tree (incl. README) — regenerating it before a later source edit
265
+ // strands the hashes (the recurring "refresh-sbom last" lesson).
266
+ _run("node", ["lib/sign.js", "sign-all"]);
267
+ _run("npm", ["run", "build-indexes"]);
268
+ _run("npm", ["run", "refresh-snapshot"]);
269
+ _run("npm", ["run", "refresh-sbom"]);
270
+ _ok("signed + indexes + snapshot + sbom regenerated");
271
+
272
+ _section("test-count baseline");
273
+ // Growth is fine (the gate only fails on shrinkage), but refreshing keeps
274
+ // the canonical-count guard meaningful when a release adds test files.
275
+ _run("node", ["scripts/check-test-count.js", "--update-baseline"]);
276
+
277
+ console.log("\nnext: node scripts/release.js gates");
278
+ }
279
+
280
+ function cmdGates() {
281
+ _section("gates");
282
+ // predeploy runs the full suite + every publish gate (signatures, catalog
283
+ // schema, snapshot, lint, sbom currency, indexes, tarball verify, diff
284
+ // coverage, ...). It is the authoritative pre-publish check.
285
+ _run("npm", ["run", "predeploy"]);
286
+ _ok("predeploy gates passed");
287
+ console.log("\nnext: node scripts/release.js commit");
288
+ }
289
+
290
+ function cmdCommit() {
291
+ _section("commit");
292
+ var next = _readJsonVersion("package.json");
293
+ var branch = _releaseBranchFor(next);
294
+ var current = _gitBranch();
295
+
296
+ // Resumable: a prior commit that failed after `checkout -b` leaves the
297
+ // branch in place — switch to it instead of refusing.
298
+ if (current === branch) {
299
+ _ok("already on " + branch + " (resume mode)");
300
+ } else if (current === "main") {
301
+ var exists = _capture("git", ["rev-parse", "--verify", "--quiet", branch]).status === 0;
302
+ if (exists) {
303
+ _run("git", ["checkout", branch]);
304
+ _ok("checked out existing " + branch + " (resume mode)");
305
+ } else {
306
+ _run("git", ["checkout", "-b", branch]);
307
+ _ok("created " + branch);
308
+ }
309
+ } else {
310
+ throw new Error("release: commit must run on main or " + branch + " (on " + current + ")");
311
+ }
312
+
313
+ // If HEAD already carries this release's commit, don't double-commit —
314
+ // just verify the signature.
315
+ var headSubject = _capture("git", ["log", "-1", "--pretty=%s"]).stdout;
316
+ if (headSubject.indexOf("v" + next + ":") === 0) {
317
+ _ok("HEAD already carries a v" + next + " commit (resume mode)");
318
+ _verifyCommitSignature("existing");
319
+ console.log("\nnext: node scripts/release.js push");
320
+ return;
321
+ }
322
+
323
+ // Compose the commit body from the CHANGELOG section — the operator can
324
+ // amend, but the default mirrors the shipped notes.
325
+ var section = _changelogSection(next);
326
+ var subject = _releaseSubject(next, section);
327
+ var bodyPath = path.join(ROOT, ".scratch");
328
+ try { fs.mkdirSync(bodyPath, { recursive: true }); } catch (_e) { /* ignore */ }
329
+ var msgFile = path.join(bodyPath, "release-commit-msg.txt");
330
+ fs.writeFileSync(msgFile, subject + "\n\n" + section + "\n");
331
+
332
+ _run("git", ["add", "-A"]);
333
+ _run("git", ["commit", "-F", msgFile]);
334
+ _ok("signed commit: " + subject);
335
+ _verifyCommitSignature("new");
336
+ console.log("\nnext: node scripts/release.js push");
337
+ }
338
+
339
+ function cmdPush() {
340
+ _section("push");
341
+ if (!_gitOnRelease()) throw new Error("release: push must run on a release-vX.Y.Z branch");
342
+ var next = _readJsonVersion("package.json");
343
+ var branch = _releaseBranchFor(next);
344
+
345
+ _run("git", ["push", "-u", "origin", branch]);
346
+ _ok("pushed " + branch);
347
+
348
+ if (_openPrNumber(branch)) {
349
+ _ok("PR already open for " + branch + " (resume mode)");
350
+ } else {
351
+ var section = _changelogSection(next);
352
+ var title = _releaseSubject(next, section);
353
+ _run("gh", ["pr", "create", "--base", "main", "--head", branch,
354
+ "--title", title, "--body", section]);
355
+ _ok("PR opened");
356
+ }
357
+ console.log("\nnext: node scripts/release.js watch");
358
+ }
359
+
360
+ function cmdWatch() {
361
+ _section("watch");
362
+ var branch = _releaseBranchFor(_readJsonVersion("package.json"));
363
+ var prNum = _openPrNumber(branch);
364
+ if (!prNum) throw new Error("release: no open PR for " + branch);
365
+ console.log("PR #" + prNum);
366
+
367
+ // gh pr checks --watch blocks until checks settle. allowFail so a flaky
368
+ // run doesn't throw before we get to inspect + rerun it.
369
+ _run("gh", ["pr", "checks", prNum, "--watch"], { allowFail: true });
370
+
371
+ var unresolved = _unresolvedThreads(prNum);
372
+ if (unresolved.length > 0) {
373
+ console.log("\nunresolved review threads (" + unresolved.length + "):");
374
+ unresolved.forEach(function (t) {
375
+ var c = t.comments && t.comments.nodes && t.comments.nodes[0];
376
+ if (c) console.log(" - by " + c.author.login + ": " + c.body.split("\n")[0]);
377
+ });
378
+ console.log("\nFix in code, push, resolve the thread, then re-run: node scripts/release.js watch");
379
+ process.exit(3);
380
+ }
381
+ _ok("zero unresolved review threads");
382
+ console.log("\nnext: node scripts/release.js merge");
383
+ }
384
+
385
+ function cmdMerge() {
386
+ _section("merge");
387
+ var next = _readJsonVersion("package.json");
388
+ var branch = _releaseBranchFor(next);
389
+ var prNum = _openPrNumber(branch);
390
+ if (!prNum) throw new Error("release: no open PR for " + branch);
391
+
392
+ var state = JSON.parse(_capture("gh", ["pr", "view", prNum,
393
+ "--json", "mergeStateStatus,mergeable"]).stdout || "{}");
394
+ if (state.mergeStateStatus !== "CLEAN" || state.mergeable !== "MERGEABLE") {
395
+ throw new Error("release: PR #" + prNum + " not mergeable (state=" +
396
+ state.mergeStateStatus + " mergeable=" + state.mergeable + ")");
397
+ }
398
+ // Re-check threads right before merge — a reviewer (or Codex) can open one
399
+ // between watch and merge.
400
+ var unresolved = _unresolvedThreads(prNum);
401
+ if (unresolved.length > 0) {
402
+ throw new Error("release: refusing to merge PR #" + prNum + " — " +
403
+ unresolved.length + " unresolved review thread(s); run watch again");
404
+ }
405
+ // Solo-maintainer protection requires 0 approvals; --admin satisfies the
406
+ // remaining required checks gate without a second reviewer.
407
+ _run("gh", ["pr", "merge", prNum, "--squash", "--admin", "--delete-branch"]);
408
+ _ok("PR #" + prNum + " squash-merged");
409
+
410
+ _run("git", ["checkout", "main"]);
411
+ _run("git", ["pull", "origin", "main"]);
412
+ console.log("\nnext: node scripts/release.js tag");
413
+ }
414
+
415
+ function cmdTag() {
416
+ _section("tag");
417
+ if (!_gitOnMain()) throw new Error("release: tag must run on main (post-merge)");
418
+ var next = _readJsonVersion("package.json");
419
+ var tag = "v" + next;
420
+
421
+ // GUARD against tag-on-stale-HEAD: a transient git index lock can leave
422
+ // local HEAD behind origin/main after a merge, so a tag would land on the
423
+ // wrong commit and the release workflow's version-match gate would reject
424
+ // it (burning a version slot, since the v* ruleset blocks tag rewrites).
425
+ try { fs.rmSync(path.join(ROOT, ".git", "index.lock"), { force: true }); } catch (_e) { /* ignore */ }
426
+ _run("git", ["fetch", "origin", "main"]);
427
+ var local = _capture("git", ["rev-parse", "HEAD"]).stdout;
428
+ var origin = _capture("git", ["rev-parse", "origin/main"]).stdout;
429
+ if (local !== origin) {
430
+ throw new Error("release: GUARD failed — local HEAD (" + local.slice(0, 12) +
431
+ ") != origin/main (" + origin.slice(0, 12) + "). Sync before tagging.");
432
+ }
433
+ // Three-version invariant must hold at tag time.
434
+ var manifest = _readJsonVersion("manifest.json");
435
+ var changelog = _changelogTopVersion();
436
+ if (manifest !== next || changelog !== next) {
437
+ throw new Error("release: GUARD failed — version skew (package=" + next +
438
+ " manifest=" + manifest + " changelog=" + changelog + ")");
439
+ }
440
+ if (_capture("git", ["tag", "-l", tag]).stdout === tag) {
441
+ throw new Error("release: tag " + tag + " already exists locally");
442
+ }
443
+ if (_capture("git", ["ls-remote", "--tags", "origin", tag]).stdout) {
444
+ throw new Error("release: tag " + tag + " already exists on origin");
445
+ }
446
+ _ok("GUARD passed (HEAD==origin/main, 3-version match, no existing tag)");
447
+
448
+ // `-s` forces a signed tag regardless of whether tag.gpgsign is set in
449
+ // config; `-a` would silently produce an UNSIGNED annotated tag when the
450
+ // config is absent, and main's tag ruleset / the release provenance both
451
+ // expect a signature. Verify BEFORE pushing so an unsigned tag never
452
+ // reaches origin (the v* ruleset blocks tag rewrites, so a bad push would
453
+ // burn the version slot).
454
+ _run("git", ["tag", "-s", tag, "-m", tag]);
455
+ var verify = _capture("git", ["tag", "-v", tag]);
456
+ if (verify.stderr.indexOf("Good") === -1 && verify.stdout.indexOf("Good") === -1) {
457
+ _run("git", ["tag", "-d", tag], { allowFail: true });
458
+ throw new Error("release: tag " + tag + " is not a Good signature — refusing to push.\n" +
459
+ "Check SSH tag signing (tag.gpgsign=true + gpg.format=ssh + the public key registered as a GitHub signing key).\n" +
460
+ (verify.stderr || verify.stdout));
461
+ }
462
+ _ok("tag signature: Good (verified before push)");
463
+ _run("git", ["push", "origin", tag]);
464
+ _ok("tagged + pushed " + tag);
465
+ console.log("\nnext: node scripts/release.js release");
466
+ }
467
+
468
+ function cmdRelease() {
469
+ _section("release");
470
+ var next = _readJsonVersion("package.json");
471
+
472
+ _section("release workflow");
473
+ var runId = _capture("gh", ["run", "list", "--workflow=release.yml", "--limit", "1",
474
+ "--json", "databaseId", "--jq", ".[0].databaseId"]).stdout;
475
+ if (runId) {
476
+ _run("gh", ["run", "watch", runId, "--exit-status"], { allowFail: true });
477
+ var concl = _capture("gh", ["run", "view", runId, "--json", "conclusion", "--jq", ".conclusion"]).stdout;
478
+ if (concl !== "success") {
479
+ console.error("warning: release.yml conclusion=" + concl + " — re-check before trusting npm");
480
+ } else {
481
+ _ok("release.yml: success");
482
+ }
483
+ } else {
484
+ console.log("no release.yml run found yet (tag push may still be propagating)");
485
+ }
486
+
487
+ _section("verify npm");
488
+ var npmVersion = _capture("npm", ["view", PKG_NAME, "version"]).stdout;
489
+ console.log("npm " + PKG_NAME + ": " + (npmVersion || "(unable to query)") + " (expected " + next + ")");
490
+ if (npmVersion === next) _ok("npm matches " + next);
491
+ // A mismatch is asserted as a hard failure at the end of the phase (after
492
+ // the tarball verify), so a stalled publish can't read as a clean release.
493
+
494
+ _section("fresh-tarball signature verify");
495
+ // Verify against the EXACT bytes a downstream consumer installs — the
496
+ // source-tree verify is necessary-but-insufficient (the v0.11.x signature
497
+ // regression was invisible until a fresh install). Packs, extracts, and
498
+ // runs lib/verify.js against the extracted tree. This is the load-bearing
499
+ // post-publish check: a broken artifact/signature here means the release
500
+ // is broken, so it is a HARD gate — _run (no allowFail) throws on failure
501
+ // and the phase exits non-zero rather than reporting a clean release.
502
+ var wrapper = path.join(ROOT, "scripts", "verify-shipped-tarball.js");
503
+ if (fs.existsSync(wrapper)) {
504
+ _run("node", [wrapper]);
505
+ _ok("shipped-tarball signature verified");
506
+ } else {
507
+ throw new Error("release: scripts/verify-shipped-tarball.js missing — cannot verify the shipped artifact");
508
+ }
509
+
510
+ // An npm-version mismatch after the workflow finished is not mere
511
+ // propagation lag — fail so a stalled/failed publish can't read as a
512
+ // completed release. (A genuinely in-flight publish is caught by the
513
+ // workflow-conclusion check above; by the time we query npm post-watch the
514
+ // version should be live.)
515
+ if (npmVersion && npmVersion !== next) {
516
+ throw new Error("release: npm shows " + npmVersion + " but expected " + next +
517
+ " — publish did not complete; re-check release.yml before treating the release as done");
518
+ }
519
+
520
+ console.log("\nThe landing site auto-injects the version from jsDelivr @latest — no manual deploy.");
521
+ console.log("Release complete: npm shows " + next + " and the shipped tarball verifies.");
522
+ }
523
+
524
+ function cmdAll(opts) {
525
+ cmdPrepare(opts);
526
+ cmdGates();
527
+ cmdCommit();
528
+ cmdPush();
529
+ cmdWatch();
530
+ cmdMerge();
531
+ cmdTag();
532
+ cmdRelease();
533
+ }
534
+
535
+ function cmdStatus() {
536
+ _section("status");
537
+ console.log("branch: " + _gitBranch());
538
+ console.log("clean: " + _gitClean());
539
+ console.log("package version: " + _readJsonVersion("package.json"));
540
+ console.log("manifest version: " + _readJsonVersion("manifest.json"));
541
+ console.log("changelog top: " + _changelogTopVersion());
542
+ var pr = _openPrNumber(_releaseBranchFor(_readJsonVersion("package.json")));
543
+ console.log("open PR: " + (pr || "(none)"));
544
+ }
545
+
546
+ function cmdHelp() {
547
+ console.log("release.js — orchestrated exceptd release flow");
548
+ console.log("");
549
+ console.log("Usage:");
550
+ console.log(" node scripts/release.js prepare [--minor] # bump + sign + indexes + snapshot + sbom + baseline");
551
+ console.log(" node scripts/release.js gates # npm test + 18-gate predeploy");
552
+ console.log(" node scripts/release.js commit # release branch + signed commit");
553
+ console.log(" node scripts/release.js push # push branch + open PR");
554
+ console.log(" node scripts/release.js watch # CI watch + flag unresolved review threads");
555
+ console.log(" node scripts/release.js merge # admin squash-merge if CLEAN");
556
+ console.log(" node scripts/release.js tag # GUARD + signed tag + push tag");
557
+ console.log(" node scripts/release.js release # watch release.yml + npm/tarball verify");
558
+ console.log(" node scripts/release.js all [--minor] # all eight in sequence");
559
+ console.log(" node scripts/release.js status # current branch + version state");
560
+ console.log(" node scripts/release.js help # this banner");
561
+ console.log("");
562
+ console.log("Patch is the default. --minor is a deliberate, explicit choice.");
563
+ }
564
+
565
+ // ---- Dispatch ------------------------------------------------------------
566
+
567
+ var sub = process.argv[2] || "help";
568
+ var opts = { minor: process.argv.slice(3).indexOf("--minor") !== -1 };
569
+
570
+ try {
571
+ switch (sub) {
572
+ case "prepare": cmdPrepare(opts); break;
573
+ case "gates": cmdGates(); break;
574
+ case "commit": cmdCommit(); break;
575
+ case "push": cmdPush(); break;
576
+ case "watch": cmdWatch(); break;
577
+ case "merge": cmdMerge(); break;
578
+ case "tag": cmdTag(); break;
579
+ case "release": cmdRelease(); break;
580
+ case "all": cmdAll(opts); break;
581
+ case "status": cmdStatus(); break;
582
+ case "help":
583
+ case "--help":
584
+ case "-h": cmdHelp(); break;
585
+ default:
586
+ console.error("release: unknown subcommand '" + sub + "'");
587
+ cmdHelp();
588
+ process.exitCode = 1;
589
+ }
590
+ } catch (e) {
591
+ console.error("\nrelease: FAIL — " + (e.message || e));
592
+ process.exitCode = 1;
593
+ }