@the-open-engine/zeroshot 6.3.0 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/index.js CHANGED
@@ -189,7 +189,9 @@ function normalizeRunOptions(options) {
189
189
  if (options.docker) {
190
190
  options.worktree = false;
191
191
  }
192
- options.autoMerge = Boolean(options.ship);
192
+ // autoMerge is NOT stored here — it is derived from the run plan (delivery ===
193
+ // 'ship') at every consumer. Writing it here unconditionally would clobber an
194
+ // explicit autoMerge intent (e.g. a future `--auto-merge` flag) back to false.
193
195
  }
194
196
 
195
197
  async function runClusterPreflight({ input, options, providerOverride, settings, forceProvider }) {
@@ -2610,6 +2612,7 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps), -N (Line
2610
2612
 
2611
2613
  if (!process.env.ZEROSHOT_DAEMON) {
2612
2614
  await streamClusterInForeground(cluster, orchestrator, clusterId, options);
2615
+ orchestrator.close();
2613
2616
  }
2614
2617
 
2615
2618
  setupDaemonCleanup(orchestrator, clusterId);
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
4
  const lockfile = require('proper-lockfile');
5
+ const { resolveRunPlan } = require('./run-plan');
5
6
 
6
7
  const DEFAULT_WAIT_TIMEOUT_SECONDS = 180;
7
8
  const DEFAULT_WAIT_POLL_MS = 1000;
@@ -116,6 +117,7 @@ async function registerDetachedSetupCluster({
116
117
  runOptions = {},
117
118
  cwd,
118
119
  }) {
120
+ const plan = resolveRunPlan(runOptions);
119
121
  await updateClustersFile(storageDir, (clusters) => {
120
122
  clusters[clusterId] = {
121
123
  id: clusterId,
@@ -125,13 +127,13 @@ async function registerDetachedSetupCluster({
125
127
  setupLogPath: logPath || null,
126
128
  setupStartedAt: Date.now(),
127
129
  setupStage: 'starting',
128
- autoPr: Boolean(runOptions.pr || runOptions.ship),
130
+ autoPr: plan.delivery !== 'none',
129
131
  prOptions: runOptions.prBase
130
132
  ? {
131
133
  prBase: runOptions.prBase,
132
134
  mergeQueue: runOptions.mergeQueue || false,
133
135
  closeIssue: runOptions.closeIssue || null,
134
- autoMerge: Boolean(runOptions.ship),
136
+ autoMerge: plan.autoMerge,
135
137
  cwd: cwd || null,
136
138
  }
137
139
  : null,
package/lib/run-mode.js CHANGED
@@ -1,11 +1,23 @@
1
- function resolveRunMode(options) {
2
- if (options.ship) return options.docker ? 'ship+docker' : 'ship';
3
- if (options.pr) return options.docker ? 'pr+docker' : 'pr';
4
- if (options.docker) return 'docker';
5
- if (options.worktree) return 'worktree';
1
+ const { resolveRunPlan } = require('./run-plan');
2
+
3
+ // The run-mode label is a VIEW of the canonical plan, never an independent
4
+ // cascade. Deriving it from the plan is what keeps the user-facing label and the
5
+ // actual isolation/delivery/autoMerge behavior from drifting apart. Callers that
6
+ // already hold a plan (e.g. the effective plan with env/settings folded in) use
7
+ // runModeFromPlan directly so the label reflects the SAME plan as behavior.
8
+ function runModeFromPlan({ isolation, delivery }) {
9
+ const dockerSuffix = isolation === 'docker' ? '+docker' : '';
10
+ if (delivery === 'ship') return `ship${dockerSuffix}`;
11
+ if (delivery === 'pr') return `pr${dockerSuffix}`;
12
+ if (isolation === 'docker') return 'docker';
13
+ if (isolation === 'worktree') return 'worktree';
6
14
  return null;
7
15
  }
8
16
 
17
+ function resolveRunMode(options) {
18
+ return runModeFromPlan(resolveRunPlan(options));
19
+ }
20
+
9
21
  const RUN_MODE_LABELS = {
10
22
  ship: 'ship (worktree + PR + auto-merge)',
11
23
  'ship+docker': 'ship (docker + PR + auto-merge)',
@@ -19,4 +31,4 @@ function describeRunMode(mode) {
19
31
  return RUN_MODE_LABELS[mode] || 'local (no isolation)';
20
32
  }
21
33
 
22
- module.exports = { resolveRunMode, describeRunMode };
34
+ module.exports = { resolveRunMode, runModeFromPlan, describeRunMode };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The single canonical run-mode resolver.
3
+ *
4
+ * Every consumer (orchestrator, daemon startup, CLI label) derives isolation,
5
+ * delivery, and autoMerge from THIS function and nowhere else. The raw
6
+ * worktree/docker/pr/ship/autoMerge booleans are inputs; the frozen plan is the
7
+ * one truth. Deriving autoMerge separately (the old `Boolean(ship)` scatter) is
8
+ * what let `--pr` merge and let the run-mode label drift from behavior.
9
+ */
10
+
11
+ function resolveIsolation(options) {
12
+ if (options.docker) return 'docker';
13
+ if (options.worktree || options.pr || options.ship) return 'worktree';
14
+ return 'none';
15
+ }
16
+
17
+ function resolveDelivery(options) {
18
+ // Explicit autoMerge (e.g. a future `--auto-merge` flag) is ship-equivalent:
19
+ // "merge it" implies the ship delivery. This is the ONLY place that intent is
20
+ // interpreted, so it can never be silently overwritten downstream.
21
+ if (options.ship || options.autoMerge === true) return 'ship';
22
+ if (options.pr) return 'pr';
23
+ return 'none';
24
+ }
25
+
26
+ function resolveRunPlan(options = {}) {
27
+ const isolation = resolveIsolation(options);
28
+ const delivery = resolveDelivery(options);
29
+ return Object.freeze({ isolation, delivery, autoMerge: delivery === 'ship' });
30
+ }
31
+
32
+ module.exports = { resolveRunPlan };
@@ -5,7 +5,8 @@ const { normalizeProviderName } = require('./provider-names');
5
5
  const { getProvider } = require('../src/providers');
6
6
  const { detectProvider } = require('../src/issue-providers');
7
7
  const TemplateResolver = require('../src/template-resolver');
8
- const { resolveRunMode } = require('./run-mode');
8
+ const { runModeFromPlan } = require('./run-mode');
9
+ const { resolveRunPlan } = require('./run-plan');
9
10
 
10
11
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
11
12
 
@@ -198,8 +199,20 @@ function mergeRunOptions(options) {
198
199
  return envRunOptions ? { ...envRunOptions, ...options } : options;
199
200
  }
200
201
 
201
- function resolveIsolation(mergedOptions, settings) {
202
- return mergedOptions.docker || process.env.ZEROSHOT_DOCKER === '1' || settings.defaultDocker;
202
+ // The single producer of the run plan for a cluster start: fold env + settings
203
+ // into the flags, then resolve the canonical isolation/delivery/autoMerge plan.
204
+ // buildStartOptions reads EVERY mode field off this one plan — no field
205
+ // (isolation, worktree, autoPr, autoMerge, runMode) is derived independently.
206
+ function resolveEffectiveRunPlan(mergedOptions, settings) {
207
+ return resolveRunPlan({
208
+ ...mergedOptions,
209
+ docker: anyTruthy(
210
+ mergedOptions.docker,
211
+ process.env.ZEROSHOT_DOCKER === '1',
212
+ settings.defaultDocker
213
+ ),
214
+ worktree: anyTruthy(mergedOptions.worktree, process.env.ZEROSHOT_WORKTREE === '1'),
215
+ });
203
216
  }
204
217
 
205
218
  function resolveMergeQueue(mergedOptions) {
@@ -238,14 +251,15 @@ function buildStartOptions({
238
251
  forceProvider,
239
252
  }) {
240
253
  const mergedOptions = mergeRunOptions(options);
254
+ const plan = resolveEffectiveRunPlan(mergedOptions, settings);
241
255
  return {
242
256
  clusterId,
243
257
  cwd: resolveTargetCwd(),
244
- isolation: resolveIsolation(mergedOptions, settings),
258
+ isolation: plan.isolation === 'docker',
245
259
  isolationImage: firstTruthy(mergedOptions.dockerImage, process.env.ZEROSHOT_DOCKER_IMAGE),
246
- worktree: anyTruthy(mergedOptions.worktree, process.env.ZEROSHOT_WORKTREE === '1'),
247
- autoPr: anyTruthy(mergedOptions.pr, process.env.ZEROSHOT_PR === '1'),
248
- autoMerge: Boolean(mergedOptions.ship),
260
+ worktree: plan.isolation === 'worktree',
261
+ autoPr: plan.delivery !== 'none',
262
+ autoMerge: plan.autoMerge,
249
263
  autoPush: process.env.ZEROSHOT_PUSH === '1',
250
264
  modelOverride: optionalValue(modelOverride),
251
265
  providerOverride: optionalValue(providerOverride),
@@ -257,7 +271,7 @@ function buildStartOptions({
257
271
  mergeQueue: resolveMergeQueue(mergedOptions),
258
272
  closeIssue: resolveCloseIssue(mergedOptions),
259
273
  ship: mergedOptions.ship,
260
- runMode: resolveRunMode(mergedOptions) || null,
274
+ runMode: runModeFromPlan(plan),
261
275
  requiredQualityGates: mergedOptions.requiredQualityGates,
262
276
  settings,
263
277
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-open-engine/zeroshot",
3
- "version": "6.3.0",
3
+ "version": "6.4.0",
4
4
  "description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
5
5
  "main": "src/orchestrator.js",
6
6
  "bin": {
@@ -15,8 +15,10 @@
15
15
  "pretest": "npm run build:agent-cli-provider",
16
16
  "test": "node tests/run-tests.js",
17
17
  "test:unit": "node tests/run-tests.js",
18
- "test:slow": "mocha 'tests/integration/**/*.test.js' --timeout 180000",
19
- "test:all": "npm run test && npm run test:slow",
18
+ "test:e2e": "mocha 'tests/e2e/**/*.test.js' --timeout 120000",
19
+ "test:e2e:docker": "bash tests/integration/e2e-isolation-and-auto.test.sh",
20
+ "test:slow": "mocha 'tests/integration/**/*.test.js' 'tests/providers/**/*.test.js' --timeout 180000",
21
+ "test:all": "npm run test && npm run test:e2e && npm run test:slow",
20
22
  "test:coverage": "c8 npm run test:unit",
21
23
  "test:coverage:report": "c8 --reporter=html npm run test:unit && echo 'Coverage report generated at coverage/index.html'",
22
24
  "postinstall": "node scripts/fix-node-pty-permissions.js && node scripts/check-path.js",
@@ -200,6 +200,8 @@ function start(agent) {
200
200
  * @returns {Promise<void>}
201
201
  */
202
202
  async function stop(agent) {
203
+ stopLivenessCheck(agent);
204
+
203
205
  if (!agent.running) {
204
206
  return;
205
207
  }
package/src/ledger.js CHANGED
@@ -715,6 +715,9 @@ class Ledger extends EventEmitter {
715
715
  * Close the database connection
716
716
  */
717
717
  close() {
718
+ if (this._closed) {
719
+ return;
720
+ }
718
721
  this._closed = true; // Set flag BEFORE closing to prevent race conditions
719
722
  this.db.close();
720
723
  }
@@ -24,12 +24,14 @@ class MessageBus extends EventEmitter {
24
24
  this.setMaxListeners(MAX_LISTENERS);
25
25
  this.ledger = ledger || new Ledger();
26
26
  this.wsClients = new Set();
27
+ this._closed = false;
27
28
 
28
29
  // Forward ledger events
29
- this.ledger.on('message', (message) => {
30
+ this._forwardLedgerMessage = (message) => {
30
31
  this.emit('message', message);
31
32
  this._broadcastToWebSocket(message);
32
- });
33
+ };
34
+ this.ledger.on('message', this._forwardLedgerMessage);
33
35
  }
34
36
 
35
37
  /**
@@ -241,6 +243,14 @@ class MessageBus extends EventEmitter {
241
243
  * Close the message bus
242
244
  */
243
245
  close() {
246
+ if (this._closed) {
247
+ return;
248
+ }
249
+ this._closed = true;
250
+
251
+ this.ledger.off('message', this._forwardLedgerMessage);
252
+ this.removeAllListeners();
253
+
244
254
  // Close all WebSocket connections
245
255
  for (const ws of this.wsClients) {
246
256
  try {
@@ -47,6 +47,7 @@ const configValidator = require('./config-validator');
47
47
  const TemplateResolver = require('./template-resolver');
48
48
  const { loadSettings } = require('../lib/settings');
49
49
  const { normalizeProviderName } = require('../lib/provider-names');
50
+ const { resolveRunPlan } = require('../lib/run-plan');
50
51
  const { getProvider } = require('./providers');
51
52
  const StateSnapshotter = require('./state-snapshotter');
52
53
  const { resolveClusterRequiredQualityGates } = require('./quality-gates');
@@ -212,14 +213,11 @@ function applyPushBlockedRepairTriggers(config) {
212
213
  }
213
214
  }
214
215
 
215
- function resolveAutoMerge(options) {
216
- return Boolean(options.ship) || Boolean(options.autoMerge);
217
- }
218
-
219
216
  function buildPrOptions(options, requiredQualityGates) {
220
217
  // autoMerge must always be persisted (even when no other PR fields are set) so that
221
218
  // `zeroshot run --pr` (autoMerge=false) vs `--ship` (autoMerge=true) survives resume.
222
- const autoMerge = resolveAutoMerge(options);
219
+ // Derived from the canonical run plan — never recomputed from ship/pr here.
220
+ const autoMerge = resolveRunPlan(options).autoMerge;
223
221
 
224
222
  return {
225
223
  prBase: options.prBase || null,
@@ -577,6 +575,12 @@ class Orchestrator {
577
575
  // missing - a ledger write. Read-only orchestrators must never write, so they
578
576
  // skip it; they only need to read existing snapshots, not produce new ones.
579
577
  if (!this.readonly) {
578
+ this._registerClusterSubscriptions({
579
+ messageBus,
580
+ clusterId,
581
+ isolationManager,
582
+ containerId: isolation?.containerId || null,
583
+ });
580
584
  this._startSnapshotter(clusterContext);
581
585
  }
582
586
  this._log(`[Orchestrator] Loaded cluster: ${clusterId} with ${agents.length} agents`);
@@ -1825,7 +1829,7 @@ class Orchestrator {
1825
1829
  mergeQueue: options.mergeQueue,
1826
1830
  closeIssue: options.closeIssue,
1827
1831
  requiredQualityGates: options.requiredQualityGates,
1828
- autoMerge: resolveAutoMerge(options),
1832
+ autoMerge: resolveRunPlan(options).autoMerge,
1829
1833
  cwd: options.cwd,
1830
1834
  });
1831
1835
 
@@ -2316,6 +2320,9 @@ class Orchestrator {
2316
2320
  * Call before deleting storageDir to prevent ENOENT race conditions during cleanup
2317
2321
  */
2318
2322
  close() {
2323
+ if (this.closed) {
2324
+ return;
2325
+ }
2319
2326
  this.closed = true;
2320
2327
  }
2321
2328
 
@@ -4103,7 +4110,7 @@ Continue from where you left off. Review your previous output to understand what
4103
4110
 
4104
4111
  // Exported for testing (PR options persistence, e.g. autoMerge for --pr vs --ship).
4105
4112
  Orchestrator.buildPrOptions = buildPrOptions;
4106
- Orchestrator.resolveAutoMerge = resolveAutoMerge;
4113
+ Orchestrator.resolveRunPlan = resolveRunPlan;
4107
4114
 
4108
4115
  module.exports = Orchestrator;
4109
4116
  module.exports.DuplicateClusterError = DuplicateClusterError;