@the-open-engine/zeroshot 6.7.1 → 6.7.2

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/cli/index.js CHANGED
@@ -1597,21 +1597,87 @@ function reportNoActiveAgents(status, id) {
1597
1597
  }
1598
1598
  }
1599
1599
 
1600
- function printAttachableAgentList(id, activeAgents) {
1601
- console.log(chalk.yellow(`\nCluster ${id} - attachable agents:\n`));
1602
- for (const agent of activeAgents) {
1600
+ async function inspectAgentAttachment(agent, socketDiscovery, readTask = readTaskFromDisk) {
1601
+ if (!agent.currentTaskId) {
1602
+ return { agent, state: 'starting', task: null, socketPath: null };
1603
+ }
1604
+
1605
+ const task = await readTask(agent.currentTaskId);
1606
+ if (task?.pid && !task.attachable) {
1607
+ return { agent, state: 'non_attachable', task, socketPath: null };
1608
+ }
1609
+
1610
+ const socketPath = task?.socketPath || socketDiscovery.getTaskSocketPath(agent.currentTaskId);
1611
+ if (await socketDiscovery.isSocketAlive(socketPath)) {
1612
+ return { agent, state: 'attachable', task, socketPath };
1613
+ }
1614
+ return { agent, state: 'unavailable', task, socketPath };
1615
+ }
1616
+
1617
+ function printLiveAttachableAgents(attachments) {
1618
+ for (const { agent } of attachments) {
1603
1619
  const modelLabel = agent.model ? chalk.dim(` [${agent.model}]`) : '';
1604
- if (agent.currentTaskId) {
1605
- console.log(
1606
- ` ${chalk.cyan(agent.id)}${modelLabel} → task ${chalk.green(agent.currentTaskId)}`
1607
- );
1608
- console.log(chalk.dim(` zeroshot attach ${agent.currentTaskId}`));
1609
- } else {
1610
- console.log(` ${chalk.cyan(agent.id)}${modelLabel} → ${chalk.yellow('starting...')}`);
1611
- console.log(chalk.dim(` (task ID not yet assigned, try again in a moment)`));
1612
- }
1620
+ console.log(
1621
+ ` ${chalk.cyan(agent.id)}${modelLabel} → task ${chalk.green(agent.currentTaskId)}`
1622
+ );
1623
+ console.log(chalk.dim(` zeroshot attach ${agent.currentTaskId}`));
1624
+ }
1625
+ }
1626
+
1627
+ function printNonAttachableAgents(id, attachments) {
1628
+ if (attachments.length === 0) {
1629
+ return;
1630
+ }
1631
+
1632
+ console.log(chalk.yellow('\nRunning without PTY attach support:'));
1633
+ for (const { agent, task } of attachments) {
1634
+ const modelLabel = agent.model ? chalk.dim(` [${agent.model}]`) : '';
1635
+ const providerLabel = task?.provider ? chalk.dim(` [provider: ${task.provider}]`) : '';
1636
+ console.log(
1637
+ ` ${chalk.cyan(agent.id)}${modelLabel} → task ${chalk.green(agent.currentTaskId)}${providerLabel}`
1638
+ );
1639
+ }
1640
+ console.log(chalk.dim(` Follow output instead: zeroshot logs ${id} -f`));
1641
+ }
1642
+
1643
+ function printUnavailableAgents(attachments) {
1644
+ if (attachments.length === 0) {
1645
+ return;
1646
+ }
1647
+
1648
+ console.log(chalk.yellow('\nActive agents without a live attach socket:'));
1649
+ for (const { agent, state } of attachments) {
1650
+ const modelLabel = agent.model ? chalk.dim(` [${agent.model}]`) : '';
1651
+ const detail =
1652
+ state === 'starting'
1653
+ ? 'task ID not yet assigned'
1654
+ : `task ${agent.currentTaskId} socket not ready`;
1655
+ console.log(` ${chalk.cyan(agent.id)}${modelLabel} → ${chalk.yellow(detail)}`);
1656
+ }
1657
+ }
1658
+
1659
+ async function printAttachableAgentList(
1660
+ id,
1661
+ activeAgents,
1662
+ socketDiscovery,
1663
+ readTask = readTaskFromDisk
1664
+ ) {
1665
+ const inspected = await Promise.all(
1666
+ activeAgents.map((agent) => inspectAgentAttachment(agent, socketDiscovery, readTask))
1667
+ );
1668
+ const attachable = inspected.filter(({ state }) => state === 'attachable');
1669
+ const nonAttachable = inspected.filter(({ state }) => state === 'non_attachable');
1670
+ const unavailable = inspected.filter(({ state }) => ['starting', 'unavailable'].includes(state));
1671
+
1672
+ console.log(chalk.yellow(`\nCluster ${id} - attachable agents:\n`));
1673
+ if (attachable.length === 0) {
1674
+ console.log(chalk.dim(' No live attach sockets.'));
1675
+ } else {
1676
+ printLiveAttachableAgents(attachable);
1677
+ console.log(chalk.dim('\nAttach to an agent by running: zeroshot attach <taskId>'));
1613
1678
  }
1614
- console.log(chalk.dim('\nAttach to an agent by running: zeroshot attach <taskId>'));
1679
+ printNonAttachableAgents(id, nonAttachable);
1680
+ printUnavailableAgents(unavailable);
1615
1681
  }
1616
1682
 
1617
1683
  function reportMissingAgent(agentName, status) {
@@ -1648,6 +1714,27 @@ async function reportClusterStatusUnavailable(err, socketDiscovery) {
1648
1714
  }
1649
1715
  }
1650
1716
 
1717
+ async function readTaskFromDisk(taskId) {
1718
+ try {
1719
+ const { getTask } = await import('../task-lib/store.js');
1720
+ return getTask(taskId);
1721
+ } catch {
1722
+ return null;
1723
+ }
1724
+ }
1725
+
1726
+ function reportAgentTaskNotAttachable(id, agent, task) {
1727
+ console.error(
1728
+ chalk.yellow(
1729
+ `Agent '${agent.id}' is running task ${agent.currentTaskId} without PTY attach support`
1730
+ )
1731
+ );
1732
+ if (task.provider) {
1733
+ console.error(chalk.dim(`Provider: ${task.provider}`));
1734
+ }
1735
+ console.error(chalk.dim(`Follow output instead: zeroshot logs ${id} -f`));
1736
+ }
1737
+
1651
1738
  async function resolveClusterSocketPath(id, options, socketDiscovery) {
1652
1739
  const cluster = readClusterFromDisk(id);
1653
1740
  ensureClusterRunning(cluster, id);
@@ -1662,7 +1749,7 @@ async function resolveClusterSocketPath(id, options, socketDiscovery) {
1662
1749
  }
1663
1750
 
1664
1751
  if (!options.agent) {
1665
- printAttachableAgentList(id, activeAgents);
1752
+ await printAttachableAgentList(id, activeAgents, socketDiscovery);
1666
1753
  return null;
1667
1754
  }
1668
1755
 
@@ -1680,7 +1767,12 @@ async function resolveClusterSocketPath(id, options, socketDiscovery) {
1680
1767
  console.log(
1681
1768
  chalk.dim(`Attaching to agent ${options.agent} via task ${agent.currentTaskId}...`)
1682
1769
  );
1683
- return socketDiscovery.getTaskSocketPath(agent.currentTaskId);
1770
+ const task = await readTaskFromDisk(agent.currentTaskId);
1771
+ if (task?.pid && !task.attachable) {
1772
+ reportAgentTaskNotAttachable(id, agent, task);
1773
+ return null;
1774
+ }
1775
+ return task?.socketPath || socketDiscovery.getTaskSocketPath(agent.currentTaskId);
1684
1776
  } catch (err) {
1685
1777
  await reportClusterStatusUnavailable(err, socketDiscovery);
1686
1778
  return null;
@@ -5860,4 +5952,10 @@ if (require.main === module) {
5860
5952
  });
5861
5953
  }
5862
5954
 
5863
- module.exports = { applyModelOverrideToConfig, renderRecentMessagesToTerminal, resolveRunMode };
5955
+ module.exports = {
5956
+ applyModelOverrideToConfig,
5957
+ inspectAgentAttachment,
5958
+ printAttachableAgentList,
5959
+ renderRecentMessagesToTerminal,
5960
+ resolveRunMode,
5961
+ };
@@ -5,6 +5,63 @@
5
5
 
6
6
  const { execSync } = require('../src/lib/safe-exec');
7
7
 
8
+ function hasInvalidGitRefCharacter(value) {
9
+ for (const character of value) {
10
+ const codePoint = character.codePointAt(0);
11
+ if (codePoint <= 0x20 || codePoint === 0x7f || '~^:?*[\\'.includes(character)) {
12
+ return true;
13
+ }
14
+ }
15
+ return false;
16
+ }
17
+
18
+ /**
19
+ * Normalize a Git remote name using the same ref-format rules Git applies to
20
+ * refs/remotes/<name>. Keeping this next to detection prevents a discovered
21
+ * remote from being rejected later by a narrower consumer-specific allowlist.
22
+ *
23
+ * @param {unknown} value - Candidate remote name
24
+ * @returns {string|null} Normalized remote name, or null when invalid
25
+ */
26
+ function normalizeGitRemoteName(value) {
27
+ if (typeof value !== 'string') {
28
+ return null;
29
+ }
30
+
31
+ const name = value.trim();
32
+ if (
33
+ name.length === 0 ||
34
+ name.endsWith('.') ||
35
+ name.includes('..') ||
36
+ name.includes('@{') ||
37
+ hasInvalidGitRefCharacter(name)
38
+ ) {
39
+ return null;
40
+ }
41
+
42
+ const components = name.split('/');
43
+ if (
44
+ components.some(
45
+ (component) =>
46
+ component.length === 0 || component.startsWith('.') || component.endsWith('.lock')
47
+ )
48
+ ) {
49
+ return null;
50
+ }
51
+
52
+ return name;
53
+ }
54
+
55
+ /**
56
+ * Quote one argument for the POSIX shell snippets embedded in agent prompts.
57
+ *
58
+ * @param {string} value - Argument to quote
59
+ * @returns {string} Single-quoted shell argument
60
+ */
61
+ function quoteShellArgument(value) {
62
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
63
+ }
64
+
8
65
  /**
9
66
  * Parse git remote URL into structured provider context.
10
67
  * Supports GitHub, GitLab, and Azure DevOps (cloud + self-hosted).
@@ -140,19 +197,43 @@ function detectGitContext(cwd = process.cwd()) {
140
197
  }
141
198
 
142
199
  try {
143
- // Try to get remote URL (origin by default)
144
- const remoteUrl = execSync('git remote get-url origin', {
200
+ const remoteOutput = execSync('git remote -v', {
145
201
  cwd,
146
202
  stdio: 'pipe',
147
203
  encoding: 'utf8',
148
- }).trim();
204
+ });
149
205
 
150
- if (!remoteUrl) {
151
- return null;
206
+ const supportedRemotes = new Map();
207
+ for (const line of remoteOutput.split(/\r?\n/)) {
208
+ const match = line.match(/^(\S+)\s+(.+)\s+\(fetch\)$/);
209
+ if (!match) {
210
+ continue;
211
+ }
212
+
213
+ const [, remoteCandidate, remoteUrl] = match;
214
+ const remote = normalizeGitRemoteName(remoteCandidate);
215
+ if (!remote) {
216
+ continue;
217
+ }
218
+ const context = parseGitRemoteUrl(remoteUrl);
219
+ if (context && !supportedRemotes.has(remote)) {
220
+ supportedRemotes.set(remote, { ...context, remote });
221
+ }
222
+ }
223
+
224
+ // Preserve existing behavior whenever origin is usable. Without origin,
225
+ // select a remote only when there is exactly one supported target. Guessing
226
+ // between multiple repositories could push code or create a PR in the wrong
227
+ // place, so ambiguous configurations deliberately fail closed.
228
+ if (supportedRemotes.has('origin')) {
229
+ return supportedRemotes.get('origin');
152
230
  }
153
231
 
154
- // Parse the remote URL to extract provider context
155
- return parseGitRemoteUrl(remoteUrl);
232
+ if (supportedRemotes.size === 1) {
233
+ return supportedRemotes.values().next().value;
234
+ }
235
+
236
+ return null;
156
237
  } catch {
157
238
  // No remote configured or command failed
158
239
  return null;
@@ -160,6 +241,8 @@ function detectGitContext(cwd = process.cwd()) {
160
241
  }
161
242
 
162
243
  module.exports = {
244
+ normalizeGitRemoteName,
245
+ quoteShellArgument,
163
246
  parseGitRemoteUrl,
164
247
  detectGitContext,
165
248
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-open-engine/zeroshot",
3
- "version": "6.7.1",
3
+ "version": "6.7.2",
4
4
  "description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
5
5
  "main": "src/orchestrator.js",
6
6
  "bin": {
@@ -17,18 +17,8 @@
17
17
  "main"
18
18
  ],
19
19
  "plugins": [
20
- [
21
- "@semantic-release/commit-analyzer",
22
- {
23
- "releaseRules": [
24
- {
25
- "type": "release",
26
- "release": "minor"
27
- }
28
- ]
29
- }
30
- ],
31
- "@semantic-release/release-notes-generator",
20
+ "@semantic-release/commit-analyzer",
21
+ "./scripts/semantic-release-notes.js",
32
22
  [
33
23
  "@semantic-release/npm",
34
24
  {
@@ -75,6 +65,7 @@
75
65
  "check:all": "npm run check && npm run deadcode:all",
76
66
  "release": "semantic-release",
77
67
  "release:preflight": "node scripts/release-preflight.js",
68
+ "release:recover": "node scripts/release-recovery.js",
78
69
  "release:assert-published": "node scripts/assert-release-published.js",
79
70
  "prepublishOnly": "npm run lint && npm run typecheck && npm run check:agent-cli-provider:ci",
80
71
  "prepare": "npm run build:agent-cli-provider && husky"
@@ -151,8 +142,8 @@
151
142
  "proper-lockfile": "^4.1.2"
152
143
  },
153
144
  "devDependencies": {
154
- "@semantic-release/changelog": "^6.0.3",
155
- "@semantic-release/git": "^10.0.1",
145
+ "@commitlint/cli": "^19.8.1",
146
+ "@commitlint/config-conventional": "^19.8.1",
156
147
  "@semantic-release/github": "^11.0.6",
157
148
  "@types/better-sqlite3": "^7.6.13",
158
149
  "@types/node": "^25.0.3",
@@ -821,6 +821,26 @@
821
821
  "profile"
822
822
  ],
823
823
  "type": "object"
824
+ },
825
+ {
826
+ "additionalProperties": false,
827
+ "properties": {
828
+ "profile": {
829
+ "const": "openengine.worker.builtin/v1"
830
+ },
831
+ "protocol": {
832
+ "const": "builtin"
833
+ },
834
+ "version": {
835
+ "const": "1"
836
+ }
837
+ },
838
+ "required": [
839
+ "protocol",
840
+ "version",
841
+ "profile"
842
+ ],
843
+ "type": "object"
824
844
  }
825
845
  ]
826
846
  }
@@ -1166,6 +1186,33 @@
1166
1186
  "worker",
1167
1187
  "binding"
1168
1188
  ]
1189
+ },
1190
+ {
1191
+ "properties": {
1192
+ "binding": {
1193
+ "properties": {
1194
+ "protocol": {
1195
+ "const": "builtin"
1196
+ }
1197
+ },
1198
+ "required": [
1199
+ "protocol"
1200
+ ]
1201
+ },
1202
+ "credentialRequirements": {
1203
+ "maxItems": 0
1204
+ },
1205
+ "worker": {
1206
+ "not": {
1207
+ "const": "legacy.zeroshot.ship@1"
1208
+ }
1209
+ }
1210
+ },
1211
+ "required": [
1212
+ "worker",
1213
+ "binding",
1214
+ "credentialRequirements"
1215
+ ]
1169
1216
  }
1170
1217
  ]
1171
1218
  }
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const fs = require('fs');
4
+ const https = require('https');
5
+ const path = require('path');
3
6
  const { execFileSync } = require('child_process');
7
+ const { releaseTypeForMessages } = require('./release-preflight');
4
8
 
5
9
  const DEFAULT_ATTEMPTS = 24;
6
10
  const DEFAULT_DELAY_MS = 5000;
@@ -17,6 +21,87 @@ function npmLatest(name) {
17
21
  return JSON.parse(run('npm', ['view', name, 'dist-tags.latest', '--json']));
18
22
  }
19
23
 
24
+ function npmReleaseMetadata(name, version) {
25
+ return JSON.parse(
26
+ run('npm', ['view', `${name}@${version}`, 'version', 'gitHead', 'dist.attestations', '--json'])
27
+ );
28
+ }
29
+
30
+ function httpsJson(url) {
31
+ return new Promise((resolve, reject) => {
32
+ const request = https.get(url, { headers: { Accept: 'application/json' } }, (response) => {
33
+ let body = '';
34
+ response.setEncoding('utf8');
35
+ response.on('data', (chunk) => {
36
+ body += chunk;
37
+ });
38
+ response.on('end', () => {
39
+ if (response.statusCode < 200 || response.statusCode >= 300) {
40
+ reject(new Error(`HTTP ${response.statusCode} from ${url}: ${body}`));
41
+ return;
42
+ }
43
+ resolve(JSON.parse(body));
44
+ });
45
+ });
46
+ request.on('error', reject);
47
+ });
48
+ }
49
+
50
+ function provenanceStatement(attestations) {
51
+ const provenance = attestations.attestations?.find(
52
+ (attestation) => attestation.predicateType === 'https://slsa.dev/provenance/v1'
53
+ );
54
+ const payload = provenance?.bundle?.dsseEnvelope?.payload;
55
+ if (!payload) throw new Error('npm provenance statement is missing');
56
+ return JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
57
+ }
58
+
59
+ function verifyProvenance(statement, expectedCommit) {
60
+ const workflow = statement.predicate?.buildDefinition?.externalParameters?.workflow;
61
+ if (workflow?.repository !== 'https://github.com/the-open-engine/zeroshot') {
62
+ throw new Error(`unexpected provenance repository: ${workflow?.repository || '(missing)'}`);
63
+ }
64
+ if (workflow?.path !== '.github/workflows/release.yml') {
65
+ throw new Error(`unexpected provenance workflow: ${workflow?.path || '(missing)'}`);
66
+ }
67
+
68
+ const resolved = statement.predicate?.buildDefinition?.resolvedDependencies || [];
69
+ if (!resolved.some((dependency) => dependency.digest?.gitCommit === expectedCommit)) {
70
+ throw new Error(`provenance does not resolve to release commit ${expectedCommit}`);
71
+ }
72
+ }
73
+
74
+ function githubRelease(tag) {
75
+ return JSON.parse(run('gh', ['release', 'view', tag, '--json', 'tagName,body,url,publishedAt']));
76
+ }
77
+
78
+ function verifyCuratedNotes(tag, release) {
79
+ const notesPath = path.join(process.cwd(), 'docs', 'releases', `${tag}.md`);
80
+ if (!fs.existsSync(notesPath)) return;
81
+ const expected = fs.readFileSync(notesPath, 'utf8').trim();
82
+ const actual = String(release.body || '').trim();
83
+ if (actual !== expected) {
84
+ throw new Error(`GitHub Release ${tag} does not match ${notesPath}`);
85
+ }
86
+ }
87
+
88
+ function verifyInstalledCli(name, version) {
89
+ const packageSpec = `${name}@${version}`;
90
+ const reported = run('npm', [
91
+ 'exec',
92
+ '--yes',
93
+ `--package=${packageSpec}`,
94
+ '--',
95
+ 'zeroshot',
96
+ '--version',
97
+ ]);
98
+ if (!reported.split(/\s+/).includes(version)) {
99
+ throw new Error(`installed CLI reported ${reported}; expected ${version}`);
100
+ }
101
+ run('npm', ['exec', '--yes', `--package=${packageSpec}`, '--', 'zeroshot', '--help']);
102
+ run('npm', ['exec', '--yes', `--package=${packageSpec}`, '--', 'zeroshot', 'list']);
103
+ }
104
+
20
105
  function tagsPointingAtHead() {
21
106
  run('git', ['fetch', '--tags', '--force']);
22
107
  return run('git', ['tag', '--points-at', 'HEAD', '--list', 'v[0-9]*'])
@@ -25,6 +110,18 @@ function tagsPointingAtHead() {
25
110
  .filter(Boolean);
26
111
  }
27
112
 
113
+ function latestReachableReleaseTag() {
114
+ return run('git', ['describe', '--tags', '--abbrev=0', '--match', 'v[0-9]*']);
115
+ }
116
+
117
+ function commitMessagesSince(tag) {
118
+ const output = run('git', ['log', '--format=%B%x1e', `${tag}..HEAD`]);
119
+ return output
120
+ .split('\x1e')
121
+ .map((message) => message.trim())
122
+ .filter(Boolean);
123
+ }
124
+
28
125
  function releaseTagParts(tag) {
29
126
  const match = tag.match(/^v(\d+)\.(\d+)\.(\d+)$/);
30
127
  if (!match) return null;
@@ -80,6 +177,12 @@ async function main() {
80
177
  const expectedTag = latestReleaseTag(headTags);
81
178
 
82
179
  if (!expectedTag) {
180
+ const previousTag = latestReachableReleaseTag();
181
+ const releaseType = releaseTypeForMessages(commitMessagesSince(previousTag));
182
+ if (!releaseType) {
183
+ console.log('No release-worthy commits since the latest tag; no publication expected');
184
+ return;
185
+ }
83
186
  throw new Error('expected a vX.Y.Z release tag to point at HEAD after release');
84
187
  }
85
188
 
@@ -89,6 +192,27 @@ async function main() {
89
192
 
90
193
  console.log(`npm latest for ${name}: ${latest}`);
91
194
 
195
+ const expectedCommit = run('git', ['rev-parse', 'HEAD']);
196
+ const metadata = npmReleaseMetadata(name, expectedVersion);
197
+ if (metadata.version !== expectedVersion) {
198
+ throw new Error(`npm metadata returned ${metadata.version}; expected ${expectedVersion}`);
199
+ }
200
+ if (metadata.gitHead !== expectedCommit) {
201
+ throw new Error(`npm gitHead ${metadata.gitHead || '(missing)'} does not match HEAD`);
202
+ }
203
+
204
+ const attestationUrl = metadata['dist.attestations']?.url;
205
+ if (!attestationUrl) throw new Error('npm attestation URL is missing');
206
+ const attestations = await httpsJson(attestationUrl);
207
+ verifyProvenance(provenanceStatement(attestations), expectedCommit);
208
+
209
+ const release = githubRelease(expectedTag);
210
+ if (release.tagName !== expectedTag) {
211
+ throw new Error(`GitHub Release tag ${release.tagName} does not match ${expectedTag}`);
212
+ }
213
+ verifyCuratedNotes(expectedTag, release);
214
+ verifyInstalledCli(name, expectedVersion);
215
+
92
216
  console.log(`Release publication verified: ${name}@${latest}`);
93
217
  }
94
218
 
@@ -101,7 +225,11 @@ if (require.main === module) {
101
225
 
102
226
  module.exports = {
103
227
  latestReleaseTag,
228
+ latestReachableReleaseTag,
229
+ npmReleaseMetadata,
104
230
  npmLatest,
231
+ provenanceStatement,
105
232
  tagsPointingAtHead,
233
+ verifyProvenance,
106
234
  waitForNpmLatest,
107
235
  };
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { execFileSync } = require('child_process');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+
9
+ const projectRoot = path.resolve(__dirname, '..');
10
+
11
+ function runGit(args) {
12
+ execFileSync('git', args, {
13
+ cwd: projectRoot,
14
+ encoding: 'utf8',
15
+ stdio: ['ignore', 'pipe', 'pipe'],
16
+ });
17
+ }
18
+
19
+ function pluginName(plugin) {
20
+ return Array.isArray(plugin) ? plugin[0] : plugin;
21
+ }
22
+
23
+ function validationPlugins(releaseConfig) {
24
+ const allowed = new Set([
25
+ '@semantic-release/commit-analyzer',
26
+ './scripts/semantic-release-notes.js',
27
+ ]);
28
+ return releaseConfig.plugins.filter((plugin) => allowed.has(pluginName(plugin)));
29
+ }
30
+
31
+ async function main() {
32
+ const branch = process.env.GITHUB_REF_NAME?.trim();
33
+ if (!branch) throw new Error('GITHUB_REF_NAME is required for a release dry run');
34
+ runGit(['check-ref-format', `refs/heads/${branch}`]);
35
+
36
+ const packageJson = require('../package.json');
37
+ const releaseConfig = packageJson.release;
38
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'zeroshot-release-dry-run-'));
39
+ const mirrorPath = path.join(tempRoot, 'candidate.git');
40
+
41
+ try {
42
+ runGit(['init', '--bare', mirrorPath]);
43
+ runGit([
44
+ '--git-dir',
45
+ mirrorPath,
46
+ 'fetch',
47
+ projectRoot,
48
+ `+HEAD:refs/heads/${branch}`,
49
+ '+refs/tags/*:refs/tags/*',
50
+ ]);
51
+ runGit(['--git-dir', mirrorPath, 'symbolic-ref', 'HEAD', `refs/heads/${branch}`]);
52
+
53
+ const semanticRelease = (await import('semantic-release')).default;
54
+ const result = await semanticRelease(
55
+ {
56
+ ...releaseConfig,
57
+ branches: [branch],
58
+ repositoryUrl: mirrorPath,
59
+ plugins: validationPlugins(releaseConfig),
60
+ dryRun: true,
61
+ ci: false,
62
+ },
63
+ {
64
+ cwd: projectRoot,
65
+ env: process.env,
66
+ }
67
+ );
68
+
69
+ if (!result) {
70
+ console.log('RELEASE_DRY_RUN_RESULT=no-release');
71
+ return;
72
+ }
73
+
74
+ console.log(`RELEASE_DRY_RUN_RESULT=${result.nextRelease.version}`);
75
+ } finally {
76
+ fs.rmSync(tempRoot, { recursive: true, force: true });
77
+ }
78
+ }
79
+
80
+ main().catch((error) => {
81
+ console.error(error);
82
+ process.exitCode = 1;
83
+ });