@djordje-stojanovic/sigmaskills 0.2.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +21 -0
  3. package/README.md +342 -0
  4. package/bin/sigmaskills.js +7 -0
  5. package/manifest.json +28 -0
  6. package/package.json +42 -0
  7. package/registry/agent-hosts.json +2404 -0
  8. package/registry/schema.json +110 -0
  9. package/registry/skill-baselines.json +4 -0
  10. package/registry/source.json +6 -0
  11. package/sigmabrief/SKILL.md +56 -0
  12. package/sigmabrief/agents/openai.yaml +12 -0
  13. package/sigmabrief/references/brief-method.md +73 -0
  14. package/sigmabrief/references/prompt-contract.md +174 -0
  15. package/sigmaperformance/SKILL.md +118 -0
  16. package/sigmaperformance/agents/openai.yaml +12 -0
  17. package/sigmaperformance/references/audit-method.md +112 -0
  18. package/sigmaperformance/references/calibration.md +45 -0
  19. package/sigmaperformance/references/report-contract.md +103 -0
  20. package/sigmareview/SKILL.md +133 -0
  21. package/sigmareview/agents/openai.yaml +12 -0
  22. package/sigmareview/references/report-contract.md +217 -0
  23. package/sigmareview/references/review-method.md +233 -0
  24. package/sigmawrite/SKILL.md +45 -0
  25. package/sigmawrite/agents/openai.yaml +12 -0
  26. package/src/adoption.js +370 -0
  27. package/src/backup.js +398 -0
  28. package/src/catalog.js +211 -0
  29. package/src/cli.js +657 -0
  30. package/src/customization.js +344 -0
  31. package/src/destinations.js +491 -0
  32. package/src/interactive.js +959 -0
  33. package/src/links.js +157 -0
  34. package/src/plan.js +429 -0
  35. package/src/prepack.js +10 -0
  36. package/src/project-lock.js +169 -0
  37. package/src/purge.js +477 -0
  38. package/src/registry/automation-ci.js +411 -0
  39. package/src/registry/automation.js +554 -0
  40. package/src/registry/diff.js +149 -0
  41. package/src/registry/normalize.js +67 -0
  42. package/src/registry/parse.js +184 -0
  43. package/src/registry/sync.js +230 -0
  44. package/src/registry/validate.js +223 -0
  45. package/src/release-ci.js +12 -0
  46. package/src/release.js +837 -0
  47. package/src/restore.js +518 -0
  48. package/src/revision.js +82 -0
  49. package/src/state.js +480 -0
  50. package/src/status.js +469 -0
  51. package/src/transaction.js +636 -0
  52. package/src/uninstall.js +647 -0
  53. package/src/update.js +815 -0
package/src/release.js ADDED
@@ -0,0 +1,837 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { getCatalog } from './catalog.js';
7
+
8
+ export const RELEASE_SCHEMA_VERSION = 1;
9
+ export const RELEASE_WORKFLOW_FILE = 'release.yml';
10
+ export const RELEASE_ENVIRONMENT = 'release';
11
+ export const RELEASE_DIST_TAG = 'latest';
12
+ export const RELEASE_PACKAGE_NAME = '@djordje-stojanovic/sigmaskills';
13
+ export const CHECKOUT_ACTION_PIN = '11bd71901bbe5b1630ceea73d27597364c9af683';
14
+ export const SETUP_NODE_ACTION_PIN = '49933ea5288caeca8642d1e84afbd3f7d6820020';
15
+
16
+ function codedError(message, code) {
17
+ const err = new Error(message);
18
+ err.code = code;
19
+ return err;
20
+ }
21
+
22
+ function readJson(file) {
23
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
24
+ }
25
+
26
+ function persistJson(file, payload) {
27
+ const tmp = `${file}.${process.pid}.tmp`;
28
+ fs.writeFileSync(tmp, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
29
+ fs.renameSync(tmp, file);
30
+ }
31
+
32
+ export function bumpSemver(version, kind) {
33
+ const match = String(version || '').match(/^(\d+)\.(\d+)\.(\d+)$/);
34
+ if (!match) throw codedError(`invalid semantic version '${version}'`, 'invalid-version');
35
+ let major = Number(match[1]);
36
+ let minor = Number(match[2]);
37
+ let patch = Number(match[3]);
38
+ if (kind === 'major') {
39
+ major += 1;
40
+ minor = 0;
41
+ patch = 0;
42
+ } else if (kind === 'minor') {
43
+ minor += 1;
44
+ patch = 0;
45
+ } else {
46
+ patch += 1;
47
+ }
48
+ return `${major}.${minor}.${patch}`;
49
+ }
50
+
51
+ function sectionBody(changelog, headingTest) {
52
+ const lines = String(changelog || '').split(/\r?\n/);
53
+ const start = lines.findIndex((line) => headingTest.test(line));
54
+ if (start < 0) return '';
55
+ let end = lines.length;
56
+ for (let i = start + 1; i < lines.length; i += 1) {
57
+ if (/^## \[/.test(lines[i])) {
58
+ end = i;
59
+ break;
60
+ }
61
+ }
62
+ return lines.slice(start + 1, end).join('\n');
63
+ }
64
+
65
+ export function extractUnreleased(changelog) {
66
+ return sectionBody(changelog, /^## \[Unreleased\]\s*$/);
67
+ }
68
+
69
+ export function extractVersionSection(changelog, version) {
70
+ const escaped = String(version).replace(/\./g, '\\.');
71
+ return sectionBody(changelog, new RegExp(`^## \\[${escaped}\\](?:\\s|$)`));
72
+ }
73
+
74
+ export function lastReleasedVersion(changelog) {
75
+ const matches = [...String(changelog || '').matchAll(/## \[(\d+\.\d+\.\d+)\]/g)];
76
+ return matches[0] ? matches[0][1] : null;
77
+ }
78
+
79
+ export function parseChangeSet(section) {
80
+ const changeSet = [];
81
+ let current = null;
82
+ for (const line of String(section || '').split(/\r?\n/)) {
83
+ const heading = line.match(/^###\s+(.+?)\s*$/);
84
+ if (heading) {
85
+ current = { heading: heading[1], items: [] };
86
+ changeSet.push(current);
87
+ continue;
88
+ }
89
+ const item = line.match(/^-\s+(.+?)\s*$/);
90
+ if (item && current) current.items.push(item[1]);
91
+ }
92
+ return changeSet.filter((group) => group.items.length > 0);
93
+ }
94
+
95
+ export function classifyChangelogBump(unreleased) {
96
+ const text = String(unreleased || '');
97
+ if (!parseChangeSet(text).length && !/###\s+/.test(text.trim())) return null;
98
+ if (/BREAKING CHANGE/i.test(text) || /###\s+Removed/i.test(text)) return 'major';
99
+ if (/###\s+Added/i.test(text)) return 'minor';
100
+ if (/###\s+Fixed/i.test(text) || /###\s+Changed/i.test(text)) return 'patch';
101
+ return parseChangeSet(text).length ? 'patch' : null;
102
+ }
103
+
104
+ export function calculateReleasePlan({ packageVersion, manifestVersion, changelog, sourceCommit }) {
105
+ if (packageVersion !== manifestVersion) {
106
+ throw codedError('package.json and manifest.json versions disagree', 'identity-mismatch');
107
+ }
108
+ const last = lastReleasedVersion(changelog);
109
+ const unreleased = extractUnreleased(changelog);
110
+ const bump = classifyChangelogBump(unreleased);
111
+ const changelogHasPackage = new RegExp(`## \\[${String(packageVersion).replace(/\./g, '\\.')}\\]`).test(changelog || '');
112
+
113
+ let version;
114
+ let changeSet;
115
+ let identitiesCommitted;
116
+
117
+ if (changelogHasPackage && !(bump && last === packageVersion)) {
118
+ version = packageVersion;
119
+ changeSet = parseChangeSet(extractVersionSection(changelog, version));
120
+ identitiesCommitted = true;
121
+ } else if (bump && last === packageVersion) {
122
+ version = bumpSemver(packageVersion, bump);
123
+ changeSet = parseChangeSet(unreleased);
124
+ identitiesCommitted = false;
125
+ } else if (!bump) {
126
+ throw codedError('CHANGELOG.md has no Unreleased change set to publish', 'nothing-to-release');
127
+ } else {
128
+ throw codedError(
129
+ `Release identities are inconsistent: package ${packageVersion}, last changelog ${last || 'none'}`,
130
+ 'identity-mismatch',
131
+ );
132
+ }
133
+
134
+ return {
135
+ sourceCommit,
136
+ version,
137
+ bump: bump || null,
138
+ changeSet,
139
+ identitiesCommitted,
140
+ tag: `v${version}`,
141
+ githubRelease: `v${version}`,
142
+ npmPackage: `${RELEASE_PACKAGE_NAME}@${version}`,
143
+ distTag: RELEASE_DIST_TAG,
144
+ tests: { command: 'npm test', required: true },
145
+ };
146
+ }
147
+
148
+ export function applyReleaseIdentities({ packageJson, manifest, changelog, version, date }) {
149
+ const unreleased = extractUnreleased(changelog).replace(/^\s+|\s+$/g, '');
150
+ const nextChangelog = String(changelog).replace(
151
+ /## \[Unreleased\]\s*\n[\s\S]*?(?=\n## \[)/,
152
+ `## [Unreleased]\n\n## [${version}] — ${date}\n\n${unreleased}\n\n`,
153
+ );
154
+ return {
155
+ packageJson: { ...packageJson, version },
156
+ manifest: { ...manifest, version },
157
+ changelog: nextChangelog,
158
+ };
159
+ }
160
+
161
+ export function applyRegistryPatchIdentities({ packageJson, manifest, changelog, version, date, note }) {
162
+ if (new RegExp(`^## \\[${String(version).replace(/\./g, '\\.')}\\]`, 'm').test(String(changelog || ''))) {
163
+ return {
164
+ packageJson: { ...packageJson, version },
165
+ manifest: { ...manifest, version },
166
+ changelog,
167
+ };
168
+ }
169
+ const insertion = `## [Unreleased]\n\n## [${version}] — ${date}\n\n### Changed\n\n- ${note}\n`;
170
+ const nextChangelog = String(changelog).replace(/## \[Unreleased\]\s*\n/, `${insertion}\n`);
171
+ return {
172
+ packageJson: { ...packageJson, version },
173
+ manifest: { ...manifest, version },
174
+ changelog: nextChangelog,
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Patch-only trusted publication. Reuses evaluatePublicationGate and
180
+ * planIdempotentPublish; never overwrites an existing npm version.
181
+ */
182
+ export async function executeTrustedPatchRelease(options = {}) {
183
+ const tarball = options.tarball || (options.rootDir ? defaultPack(options.rootDir) : null);
184
+ const expected = {
185
+ version: options.version,
186
+ commit: options.commit,
187
+ digest: tarball?.digest,
188
+ integrity: tarball?.integrity,
189
+ tag: `v${options.version}`,
190
+ };
191
+ const gate = evaluatePublicationGate({
192
+ expected,
193
+ workflow: options.workflow || { ok: true },
194
+ npmPackage: options.npmPackage,
195
+ githubRelease: options.githubRelease,
196
+ gitTag: options.gitTag,
197
+ environment: options.environment,
198
+ trustedPublisher: options.trustedPublisher,
199
+ });
200
+ const recovery = planIdempotentPublish({
201
+ expected,
202
+ npmPackage: options.npmPackage,
203
+ githubRelease: options.githubRelease,
204
+ gitTag: options.gitTag,
205
+ });
206
+ const preview = {
207
+ ok: gate.ok && recovery.ok,
208
+ errors: [...(gate.ok ? [] : gate.errors || []), ...(recovery.ok ? [] : recovery.errors || [])],
209
+ version: options.version,
210
+ commit: options.commit,
211
+ tag: expected.tag,
212
+ bump: 'patch',
213
+ tarball,
214
+ gate,
215
+ recovery,
216
+ };
217
+ if (!preview.ok || options.dryRun) return preview;
218
+
219
+ const publishers = options.publishers || {};
220
+ if (recovery.createTag && publishers.createTag) publishers.createTag(preview);
221
+ if (recovery.createGithubRelease && publishers.createGithubRelease) publishers.createGithubRelease(preview);
222
+ if (recovery.publishNpm && publishers.publishNpm) publishers.publishNpm(preview);
223
+ return preview;
224
+ }
225
+
226
+ export function inspectReleaseWorkflow(yaml) {
227
+ const text = String(yaml || '');
228
+ const errors = [];
229
+ const onBlock = text.match(/\non:\s*\n([\s\S]*?)(?=\n(?:permissions|concurrency|env|jobs):|\njobs:)/);
230
+ const onText = onBlock ? onBlock[1] : '';
231
+ const triggers = [];
232
+ if (/^\s{2}workflow_dispatch:/m.test(onText) || /^\s+workflow_dispatch:/m.test(text)) triggers.push('workflow_dispatch');
233
+ if (/^\s{2}push:/m.test(onText)) {
234
+ errors.push('release workflow must not publish on push');
235
+ triggers.push('push');
236
+ }
237
+ if (/^\s{2}pull_request:/m.test(onText)) {
238
+ errors.push('release workflow must not publish on pull_request');
239
+ triggers.push('pull_request');
240
+ }
241
+ if (!triggers.includes('workflow_dispatch')) errors.push('release workflow must use workflow_dispatch');
242
+
243
+ const usesFloatingTags = /uses:\s+actions\/(?:checkout|setup-node)@v\d+/m.test(text);
244
+ if (usesFloatingTags) errors.push('release workflow must pin actions by commit SHA');
245
+
246
+ const checkoutPin = (text.match(/uses:\s+actions\/checkout@([a-f0-9]{40})/) || [])[1] || null;
247
+ const setupNodePin = (text.match(/uses:\s+actions\/setup-node@([a-f0-9]{40})/) || [])[1] || null;
248
+ if (checkoutPin !== CHECKOUT_ACTION_PIN) errors.push('actions/checkout pin is missing or drifted');
249
+ if (setupNodePin !== SETUP_NODE_ACTION_PIN) errors.push('actions/setup-node pin is missing or drifted');
250
+
251
+ const validateMatch = text.match(/validate:[\s\S]*?permissions:\s*\n((?:\s{6,}[^\n]+\n)+)/);
252
+ const publishMatch = text.match(/publish:[\s\S]*?permissions:\s*\n((?:\s{6,}[^\n]+\n)+)/);
253
+ const parsePerms = (block) => {
254
+ const perms = {};
255
+ for (const line of String(block || '').split(/\n/)) {
256
+ const match = line.match(/^\s+([a-z-]+):\s+(\w+)\s*$/);
257
+ if (match) perms[match[1]] = match[2];
258
+ }
259
+ return perms;
260
+ };
261
+ const validate = { permissions: parsePerms(validateMatch ? validateMatch[1] : '') };
262
+ const publish = {
263
+ permissions: parsePerms(publishMatch ? publishMatch[1] : ''),
264
+ environment: /publish:[\s\S]*?environment:\s+(\S+)/.test(text)
265
+ ? text.match(/publish:[\s\S]*?environment:\s+(\S+)/)[1]
266
+ : null,
267
+ };
268
+
269
+ if (validate.permissions.contents !== 'read') errors.push('validate job must use contents: read');
270
+ if (validate.permissions['id-token']) errors.push('validate job must not receive id-token');
271
+ if (publish.permissions['id-token'] !== 'write') errors.push('publish job must use id-token: write');
272
+ if (publish.permissions.contents !== 'write') errors.push('publish job must use contents: write');
273
+ if (publish.environment !== RELEASE_ENVIRONMENT) errors.push(`publish job must use environment ${RELEASE_ENVIRONMENT}`);
274
+ if (!/node \.\/src\/release-ci\.js validate/.test(text)) errors.push('validate job must rebuild through release-ci.js');
275
+ if (!/node \.\/src\/release-ci\.js publish/.test(text)) errors.push('publish job must publish through release-ci.js');
276
+
277
+ return {
278
+ ok: errors.length === 0,
279
+ errors,
280
+ triggers: [...new Set(triggers)],
281
+ validate,
282
+ publish,
283
+ checkoutPin,
284
+ setupNodePin,
285
+ usesFloatingTags,
286
+ trustedPublisherShape: publish.permissions['id-token'] === 'write' && publish.environment === RELEASE_ENVIRONMENT,
287
+ };
288
+ }
289
+
290
+ export function evaluatePublicationGate({
291
+ expected,
292
+ workflow,
293
+ npmPackage,
294
+ githubRelease,
295
+ gitTag,
296
+ environment,
297
+ trustedPublisher,
298
+ }) {
299
+ const errors = [];
300
+ if (workflow && workflow.ok === false) errors.push(...(workflow.errors || ['release workflow failed inspection']));
301
+ if (!npmPackage?.exists) {
302
+ errors.push(
303
+ 'Reserve the npm name `sigmaskills` at https://www.npmjs.com/package/sigmaskills, then configure a trusted publisher for workflow `release.yml` and GitHub Environment `release`.',
304
+ );
305
+ }
306
+ if (trustedPublisher === false) {
307
+ errors.push(
308
+ 'Configure an npm trusted publisher for this GitHub repository, workflow filename `release.yml`, and environment `release`. Publication cannot start until that one-time setup exists.',
309
+ );
310
+ }
311
+ if (!environment || environment.name !== RELEASE_ENVIRONMENT) {
312
+ errors.push('Create a GitHub Environment named `release` with required reviewers, then retry publication.');
313
+ } else if (!environment.protected) {
314
+ errors.push('Add protection rules (required reviewers) to the GitHub Environment named `release`.');
315
+ }
316
+ const tagName = `v${expected.version}`;
317
+ if (gitTag && gitTag.commit && gitTag.commit !== expected.commit) {
318
+ errors.push(`git tag ${tagName} already points at ${gitTag.commit}; refusing to overwrite.`);
319
+ }
320
+ if (githubRelease && githubRelease.targetCommit && githubRelease.targetCommit !== expected.commit && !releasePointsAtCommit(githubRelease, gitTag, expected)) {
321
+ errors.push(`GitHub Release ${tagName} already targets ${githubRelease.targetCommit}; refusing to duplicate or overwrite.`);
322
+ }
323
+ const published = npmPackage?.versions?.[expected.version];
324
+ if (published && published.integrity && expected.integrity && published.integrity !== expected.integrity) {
325
+ errors.push(`npm already has ${RELEASE_PACKAGE_NAME}@${expected.version} with a different digest; versions are immutable.`);
326
+ }
327
+ return { ok: errors.length === 0, errors };
328
+ }
329
+
330
+ function releasePointsAtCommit(githubRelease, gitTag, expected) {
331
+ if (!githubRelease) return false;
332
+ const tagged = gitTag && gitTag.commit === expected.commit;
333
+ return githubRelease.targetCommit === expected.commit || tagged;
334
+ }
335
+
336
+ export function planIdempotentPublish({ expected, npmPackage, githubRelease, gitTag }) {
337
+ const errors = [];
338
+ const published = npmPackage?.versions?.[expected.version];
339
+ if (published && published.integrity && expected.integrity && published.integrity !== expected.integrity) {
340
+ errors.push(`npm already has ${RELEASE_PACKAGE_NAME}@${expected.version} with a different digest; versions are immutable.`);
341
+ }
342
+ const tagName = `v${expected.version}`;
343
+ if (gitTag && gitTag.commit && gitTag.commit !== expected.commit) {
344
+ errors.push(`git tag ${tagName} already points at ${gitTag.commit}; refusing to overwrite.`);
345
+ }
346
+ if (githubRelease && githubRelease.targetCommit && githubRelease.targetCommit !== expected.commit && !releasePointsAtCommit(githubRelease, gitTag, expected)) {
347
+ errors.push(`GitHub Release ${tagName} already targets ${githubRelease.targetCommit}; refusing to duplicate or overwrite.`);
348
+ }
349
+ if (errors.length) return { ok: false, errors, publishNpm: false, createGithubRelease: false, createTag: false };
350
+ const sameNpm = Boolean(published && (!expected.integrity || published.integrity === expected.integrity));
351
+ const sameRelease = releasePointsAtCommit(githubRelease, gitTag, expected);
352
+ const sameTag = Boolean(gitTag && gitTag.commit === expected.commit);
353
+ return {
354
+ ok: true,
355
+ errors,
356
+ publishNpm: !sameNpm,
357
+ createGithubRelease: !sameRelease,
358
+ createTag: !sameTag,
359
+ };
360
+ }
361
+
362
+ function defaultGit(rootDir) {
363
+ const run = (args) => execFileSync('git', args, { cwd: rootDir, encoding: 'utf8' }).trim();
364
+ return {
365
+ revParse: (ref = 'HEAD') => run(['rev-parse', ref]),
366
+ show: (spec) => run(['show', spec]),
367
+ statusPorcelain: () => run(['status', '--porcelain']),
368
+ tagCommit: (tag) => {
369
+ try {
370
+ return run(['rev-list', '-n', '1', tag]);
371
+ } catch {
372
+ return null;
373
+ }
374
+ },
375
+ };
376
+ }
377
+
378
+ function npmCommand() {
379
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm';
380
+ }
381
+
382
+ function defaultPack(rootDir) {
383
+ const dest = fs.mkdtempSync(path.join(os.tmpdir(), 'sigma-release-pack-'));
384
+ const output = execFileSync(npmCommand(), ['pack', '--json', `--pack-destination=${dest}`], {
385
+ cwd: rootDir,
386
+ encoding: 'utf8',
387
+ });
388
+ const parsed = JSON.parse(output);
389
+ const info = Array.isArray(parsed) ? parsed[0] : parsed;
390
+ const tarballPath = path.join(dest, info.filename);
391
+ const bytes = fs.readFileSync(tarballPath);
392
+ const digest = crypto.createHash('sha256').update(bytes).digest('hex');
393
+ const contents = execFileSync('tar', ['-tf', tarballPath], { encoding: 'utf8' })
394
+ .split(/\r?\n/)
395
+ .map((line) => line.trim().replace(/\\/g, '/'))
396
+ .filter(Boolean);
397
+ return {
398
+ filename: info.filename,
399
+ digest,
400
+ integrity: info.integrity,
401
+ contents,
402
+ path: tarballPath,
403
+ };
404
+ }
405
+
406
+ function loadIdentities(rootDir, git, commit) {
407
+ if (git?.show && commit) {
408
+ try {
409
+ return {
410
+ packageJson: JSON.parse(git.show(`${commit}:package.json`)),
411
+ manifest: JSON.parse(git.show(`${commit}:manifest.json`)),
412
+ changelog: git.show(`${commit}:CHANGELOG.md`),
413
+ };
414
+ } catch {
415
+ // Use the working tree when this commit does not contain identity files.
416
+ }
417
+ }
418
+ return {
419
+ packageJson: readJson(path.join(rootDir, 'package.json')),
420
+ manifest: readJson(path.join(rootDir, 'manifest.json')),
421
+ changelog: fs.readFileSync(path.join(rootDir, 'CHANGELOG.md'), 'utf8'),
422
+ };
423
+ }
424
+
425
+ function utcDate(now) {
426
+ const date = now ? new Date(now) : new Date();
427
+ return date.toISOString().slice(0, 10);
428
+ }
429
+
430
+ export function writeReleaseIdentities(rootDir, { now } = {}) {
431
+ const identities = loadIdentities(rootDir);
432
+ const plan = calculateReleasePlan({
433
+ packageVersion: identities.packageJson.version,
434
+ manifestVersion: identities.manifest.version,
435
+ changelog: identities.changelog,
436
+ sourceCommit: null,
437
+ });
438
+ if (plan.identitiesCommitted) return plan;
439
+ const applied = applyReleaseIdentities({
440
+ ...identities,
441
+ version: plan.version,
442
+ date: utcDate(now),
443
+ });
444
+ persistJson(path.join(rootDir, 'package.json'), applied.packageJson);
445
+ persistJson(path.join(rootDir, 'manifest.json'), applied.manifest);
446
+ fs.writeFileSync(path.join(rootDir, 'CHANGELOG.md'), applied.changelog, 'utf8');
447
+ return { ...plan, identitiesCommitted: true, wroteIdentities: true };
448
+ }
449
+
450
+ function defaultProbes({ git, expected, rootDir }) {
451
+ let npmPackage = { exists: false, versions: {} };
452
+ try {
453
+ const name = execFileSync(npmCommand(), ['view', RELEASE_PACKAGE_NAME, 'name'], {
454
+ encoding: 'utf8',
455
+ }).trim();
456
+ npmPackage.exists = name === RELEASE_PACKAGE_NAME;
457
+ if (npmPackage.exists) {
458
+ try {
459
+ const integrity = execFileSync(
460
+ npmCommand(),
461
+ ['view', `${RELEASE_PACKAGE_NAME}@${expected.version}`, 'dist.integrity'],
462
+ { encoding: 'utf8' },
463
+ ).trim();
464
+ if (integrity) npmPackage.versions[expected.version] = { integrity };
465
+ } catch {
466
+ // Version is unpublished; reservation still counts.
467
+ }
468
+ }
469
+ } catch {
470
+ npmPackage = { exists: false, versions: {} };
471
+ }
472
+
473
+ let githubRelease = null;
474
+ try {
475
+ const raw = execFileSync(
476
+ 'gh',
477
+ ['release', 'view', expected.tag, '--json', 'tagName,targetCommitish'],
478
+ { cwd: rootDir, encoding: 'utf8' },
479
+ );
480
+ const parsed = JSON.parse(raw);
481
+ let targetCommit = parsed.targetCommitish;
482
+ try {
483
+ targetCommit = git.revParse ? git.revParse(`${parsed.targetCommitish}^{commit}`) : targetCommit;
484
+ } catch {
485
+ try {
486
+ targetCommit = git.tagCommit?.(parsed.tagName) || targetCommit;
487
+ } catch {
488
+ targetCommit = parsed.targetCommitish;
489
+ }
490
+ }
491
+ githubRelease = { tag: parsed.tagName, targetCommit };
492
+ } catch {
493
+ githubRelease = null;
494
+ }
495
+
496
+ let environment = null;
497
+ try {
498
+ const repo = process.env.GITHUB_REPOSITORY || 'Djordje-Stojanovic/Sigmaskills';
499
+ const raw = execFileSync(
500
+ 'gh',
501
+ ['api', `repos/${repo}/environments/${RELEASE_ENVIRONMENT}`],
502
+ { encoding: 'utf8' },
503
+ );
504
+ const parsed = JSON.parse(raw);
505
+ environment = {
506
+ name: parsed.name,
507
+ protected: Array.isArray(parsed.protection_rules) && parsed.protection_rules.length > 0,
508
+ };
509
+ } catch {
510
+ environment = null;
511
+ }
512
+
513
+ return {
514
+ npmPackage,
515
+ githubRelease,
516
+ environment,
517
+ gitTag: git.tagCommit ? (git.tagCommit(expected.tag) ? { name: expected.tag, commit: git.tagCommit(expected.tag) } : null) : null,
518
+ };
519
+ }
520
+
521
+ function defaultDispatch({ expectedCommit, expectedVersion, expectedDigest, rootDir }) {
522
+ execFileSync(
523
+ 'gh',
524
+ [
525
+ 'workflow',
526
+ 'run',
527
+ RELEASE_WORKFLOW_FILE,
528
+ '-f',
529
+ `expected_commit=${expectedCommit}`,
530
+ '-f',
531
+ `expected_version=${expectedVersion}`,
532
+ '-f',
533
+ `expected_digest=${expectedDigest}`,
534
+ ],
535
+ { cwd: rootDir, encoding: 'utf8' },
536
+ );
537
+ return { ok: true };
538
+ }
539
+
540
+ function loadWorkflow(rootDir) {
541
+ const file = path.join(rootDir, '.github', 'workflows', RELEASE_WORKFLOW_FILE);
542
+ if (!fs.existsSync(file)) {
543
+ return inspectReleaseWorkflow('');
544
+ }
545
+ return inspectReleaseWorkflow(fs.readFileSync(file, 'utf8'));
546
+ }
547
+
548
+ /**
549
+ * Prepare or dispatch an owner-triggered Release.
550
+ *
551
+ * @param {object} options
552
+ * @returns {object}
553
+ */
554
+ export function executeRelease(options = {}) {
555
+ const rootDir = options.rootDir;
556
+ if (!rootDir) throw codedError('release requires a package root', 'missing-root');
557
+ const hasExpected = Boolean(options.expectedCommit && options.expectedVersion && options.expectedDigest);
558
+ const dryRun = Boolean(options.dryRun) || (Boolean(options.writeIdentities) && !hasExpected);
559
+ const git = options.git || defaultGit(rootDir);
560
+ const sourceCommit = git.revParse();
561
+ const before = loadIdentities(rootDir);
562
+
563
+ if (options.writeIdentities) {
564
+ writeReleaseIdentities(rootDir, { now: options.now });
565
+ }
566
+
567
+ const current = options.writeIdentities
568
+ ? loadIdentities(rootDir)
569
+ : loadIdentities(rootDir, git, sourceCommit);
570
+ const plan = calculateReleasePlan({
571
+ packageVersion: current.packageJson.version,
572
+ manifestVersion: current.manifest.version,
573
+ changelog: current.changelog,
574
+ sourceCommit,
575
+ });
576
+
577
+ const catalog = options.catalog || getCatalog(rootDir);
578
+ const pack = options.pack || (() => defaultPack(rootDir));
579
+ const tarball = pack();
580
+
581
+ const preview = {
582
+ schemaVersion: RELEASE_SCHEMA_VERSION,
583
+ command: 'release',
584
+ dryRun,
585
+ commit: sourceCommit,
586
+ version: plan.version,
587
+ bump: plan.bump,
588
+ changeSet: plan.changeSet,
589
+ identitiesCommitted: plan.identitiesCommitted,
590
+ tests: plan.tests,
591
+ tarball,
592
+ skills: (catalog.skills || []).map((skill) => ({
593
+ id: skill.id,
594
+ title: skill.title,
595
+ revision: skill.revision,
596
+ })),
597
+ tag: plan.tag,
598
+ githubRelease: plan.githubRelease,
599
+ npmPackage: plan.npmPackage,
600
+ distTag: plan.distTag,
601
+ dispatched: false,
602
+ };
603
+
604
+ if (before.packageJson.version !== current.packageJson.version) {
605
+ preview.wroteIdentities = true;
606
+ }
607
+
608
+ const expected = {
609
+ version: plan.version,
610
+ commit: sourceCommit,
611
+ digest: tarball.digest,
612
+ integrity: tarball.integrity,
613
+ tag: plan.tag,
614
+ };
615
+ const workflow = options.workflow || loadWorkflow(rootDir);
616
+ const probes = options.probes || defaultProbes({ git, expected, rootDir });
617
+ const gitTag = probes.gitTag !== undefined
618
+ ? probes.gitTag
619
+ : (git.tagCommit?.(plan.tag) ? { name: plan.tag, commit: git.tagCommit(plan.tag) } : null);
620
+ const gate = evaluatePublicationGate({
621
+ expected,
622
+ workflow,
623
+ npmPackage: probes.npmPackage,
624
+ githubRelease: probes.githubRelease,
625
+ gitTag,
626
+ environment: probes.environment,
627
+ trustedPublisher: probes.trustedPublisher,
628
+ });
629
+ const recovery = planIdempotentPublish({
630
+ expected,
631
+ npmPackage: probes.npmPackage,
632
+ githubRelease: probes.githubRelease,
633
+ gitTag,
634
+ });
635
+ preview.gate = gate;
636
+ preview.recovery = recovery;
637
+
638
+ if (dryRun) return preview;
639
+
640
+ if (!hasExpected) {
641
+ throw codedError(
642
+ 'release requires immutable expected commit, version, and digest; --yes, CI, and informal text are not authority',
643
+ 'confirmation-required',
644
+ );
645
+ }
646
+ if (git.statusPorcelain && git.statusPorcelain().trim()) {
647
+ throw codedError('release dispatch requires a clean working tree at the approved commit', 'dirty-tree');
648
+ }
649
+ if (!plan.identitiesCommitted) {
650
+ throw codedError(
651
+ `commit Release identities for ${plan.version} (package.json, manifest.json, CHANGELOG.md) before dispatch`,
652
+ 'identities-pending',
653
+ );
654
+ }
655
+ if (options.expectedCommit !== sourceCommit || options.expectedVersion !== plan.version || options.expectedDigest !== tarball.digest) {
656
+ throw codedError(
657
+ `approved commit/version/digest does not match prepared ${sourceCommit} ${plan.version} sha256:${tarball.digest}`,
658
+ 'expectation-mismatch',
659
+ );
660
+ }
661
+ if (!gate.ok) {
662
+ throw codedError(gate.errors[0], 'setup-incomplete');
663
+ }
664
+ if (!recovery.ok) {
665
+ throw codedError(recovery.errors[0], 'conflict');
666
+ }
667
+
668
+ const dispatch = options.dispatch || ((payload) => defaultDispatch({ ...payload, rootDir }));
669
+ dispatch({
670
+ workflow: RELEASE_WORKFLOW_FILE,
671
+ expectedCommit: options.expectedCommit,
672
+ expectedVersion: options.expectedVersion,
673
+ expectedDigest: options.expectedDigest,
674
+ });
675
+ preview.dispatched = true;
676
+ preview.dryRun = false;
677
+ return preview;
678
+ }
679
+
680
+ export function formatReleaseHuman(result) {
681
+ const lines = [
682
+ `SigmaSkills Release ${result.dryRun ? 'preview' : result.dispatched ? 'dispatch' : 'plan'}`,
683
+ ` Commit: ${result.commit}`,
684
+ ` Version: ${result.version}`,
685
+ ` Tests: ${result.tests?.command || 'npm test'} (trusted workflow re-runs before publish)`,
686
+ ` Tarball digest: ${result.tarball?.digest ? `sha256:${result.tarball.digest}` : 'pending identity write'}`,
687
+ ` Tarball integrity: ${result.tarball?.integrity || 'pending'}`,
688
+ ` Tag: ${result.tag}`,
689
+ ` GitHub Release: ${result.githubRelease}`,
690
+ ` npm package: ${result.npmPackage}`,
691
+ ` dist-tag: ${result.distTag}`,
692
+ ];
693
+ if (result.tarball?.contents?.length) {
694
+ lines.push(' Tarball contents:');
695
+ for (const file of result.tarball.contents.slice(0, 40)) lines.push(` - ${file}`);
696
+ if (result.tarball.contents.length > 40) lines.push(` - … ${result.tarball.contents.length - 40} more`);
697
+ }
698
+ lines.push(' Skill Revisions:');
699
+ for (const skill of result.skills || []) {
700
+ lines.push(` - ${skill.id}: ${skill.revision}`);
701
+ }
702
+ if (result.changeSet?.length) {
703
+ lines.push(' Change set:');
704
+ for (const group of result.changeSet) {
705
+ lines.push(` ${group.heading}`);
706
+ for (const item of group.items) lines.push(` - ${item}`);
707
+ }
708
+ }
709
+ if (result.gate?.errors?.length) {
710
+ lines.push(' Setup blockers:');
711
+ for (const error of result.gate.errors) lines.push(` - ${error}`);
712
+ }
713
+ if (!result.identitiesCommitted) {
714
+ lines.push(' Next: run `sigmaskills release --write-identities`, commit the identity files, then re-run --dry-run.');
715
+ } else if (result.dryRun) {
716
+ lines.push(' Next: dispatch with --expected-commit, --expected-version, and --expected-digest matching this preview.');
717
+ }
718
+ return lines.join('\n');
719
+ }
720
+
721
+ export function formatReleaseJson(result) {
722
+ return `${JSON.stringify({
723
+ schemaVersion: RELEASE_SCHEMA_VERSION,
724
+ command: 'release',
725
+ dryRun: Boolean(result.dryRun),
726
+ dispatched: Boolean(result.dispatched),
727
+ commit: result.commit,
728
+ version: result.version,
729
+ bump: result.bump || null,
730
+ changeSet: result.changeSet || [],
731
+ identitiesCommitted: Boolean(result.identitiesCommitted),
732
+ tests: result.tests,
733
+ tarball: result.tarball,
734
+ skills: result.skills || [],
735
+ tag: result.tag,
736
+ githubRelease: result.githubRelease,
737
+ npmPackage: result.npmPackage,
738
+ distTag: result.distTag,
739
+ gate: result.gate || null,
740
+ recovery: result.recovery || null,
741
+ }, null, 2)}\n`;
742
+ }
743
+
744
+ export function verifyApprovedCommit({ git, expectedCommit, expectedVersion, rootDir }) {
745
+ const head = git.revParse();
746
+ if (head !== expectedCommit) {
747
+ throw codedError(`checked-out commit ${head} is not the approved commit ${expectedCommit}`, 'commit-mismatch');
748
+ }
749
+ const identities = loadIdentities(rootDir);
750
+ if (identities.packageJson.version !== expectedVersion || identities.manifest.version !== expectedVersion) {
751
+ throw codedError('package.json, manifest.json, and approved version disagree', 'identity-mismatch');
752
+ }
753
+ if (!new RegExp(`## \\[${String(expectedVersion).replace(/\./g, '\\.')}\\]`).test(identities.changelog)) {
754
+ throw codedError(`CHANGELOG.md has no ${expectedVersion} heading`, 'identity-mismatch');
755
+ }
756
+ }
757
+
758
+ export async function runTrustedValidate(env, options = {}) {
759
+ const rootDir = options.rootDir || process.cwd();
760
+ const expectedCommit = env.EXPECTED_COMMIT;
761
+ const expectedVersion = env.EXPECTED_VERSION;
762
+ const expectedDigest = env.EXPECTED_DIGEST;
763
+ const git = options.git || defaultGit(rootDir);
764
+ verifyApprovedCommit({ git, expectedCommit, expectedVersion, rootDir });
765
+ if (options.runTests) await options.runTests();
766
+ else execFileSync(npmCommand(), ['test'], { cwd: rootDir, stdio: 'inherit' });
767
+ const tarball = options.pack ? options.pack() : defaultPack(rootDir);
768
+ if (tarball.digest !== expectedDigest) {
769
+ throw codedError(`rebuilt tarball sha256:${tarball.digest} does not match approved digest sha256:${expectedDigest}`, 'digest-mismatch');
770
+ }
771
+ const catalog = options.catalog || getCatalog(rootDir);
772
+ const result = executeRelease({
773
+ rootDir,
774
+ catalog,
775
+ dryRun: true,
776
+ git,
777
+ pack: () => tarball,
778
+ probes: options.probes,
779
+ workflow: options.workflow || loadWorkflow(rootDir),
780
+ });
781
+ if (result.version !== expectedVersion) {
782
+ throw codedError(`prepared version ${result.version} is not the approved version ${expectedVersion}`, 'version-mismatch');
783
+ }
784
+ if (!result.gate?.ok) throw codedError(result.gate.errors[0], 'setup-incomplete');
785
+ return result;
786
+ }
787
+
788
+ export async function runTrustedPublish(env, options = {}) {
789
+ const preview = await runTrustedValidate(env, options);
790
+ const recovery = preview.recovery;
791
+ if (!recovery.ok) throw codedError(recovery.errors[0], 'conflict');
792
+ const publishers = options.publishers || {};
793
+ if (recovery.createTag && publishers.createTag) publishers.createTag(preview);
794
+ if (recovery.createGithubRelease && publishers.createGithubRelease) publishers.createGithubRelease(preview);
795
+ if (recovery.publishNpm && publishers.publishNpm) publishers.publishNpm(preview);
796
+ if (!options.publishers) {
797
+ const rootDir = options.rootDir || process.cwd();
798
+ if (recovery.createTag) {
799
+ try {
800
+ execFileSync('git', ['tag', preview.tag, preview.commit], { cwd: rootDir, encoding: 'utf8' });
801
+ } catch {
802
+ // Tag may already exist at this commit.
803
+ }
804
+ execFileSync('git', ['push', 'origin', preview.tag], { cwd: rootDir, encoding: 'utf8' });
805
+ }
806
+ if (recovery.createGithubRelease) {
807
+ execFileSync(
808
+ 'gh',
809
+ ['release', 'create', preview.tag, '--target', preview.commit, '--title', preview.tag, '--notes', formatReleaseHuman(preview)],
810
+ { cwd: rootDir, encoding: 'utf8' },
811
+ );
812
+ }
813
+ if (recovery.publishNpm) {
814
+ const artifact = preview.tarball?.path;
815
+ const publishArgs = artifact
816
+ ? ['publish', artifact, '--access', 'public', '--tag', RELEASE_DIST_TAG, '--provenance']
817
+ : ['publish', '--access', 'public', '--tag', RELEASE_DIST_TAG, '--provenance'];
818
+ try {
819
+ execFileSync(npmCommand(), publishArgs, {
820
+ cwd: rootDir,
821
+ encoding: 'utf8',
822
+ stdio: 'pipe',
823
+ });
824
+ } catch (err) {
825
+ const detail = `${err.message || ''}\n${err.stderr || ''}\n${err.stdout || ''}`;
826
+ if (/ENEEDAUTH|unable to authenticate|not authorized/i.test(detail)) {
827
+ throw codedError(
828
+ 'Configure an npm trusted publisher for this GitHub repository, workflow filename `release.yml`, and environment `release`. Publication cannot start until that one-time setup exists.',
829
+ 'trusted-publisher',
830
+ );
831
+ }
832
+ throw err;
833
+ }
834
+ }
835
+ }
836
+ return { ...preview, dryRun: false, published: recovery };
837
+ }