@the-open-engine/zeroshot 6.7.0 → 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.
Files changed (61) hide show
  1. package/cli/commands/providers.js +4 -1
  2. package/cli/index.js +131 -27
  3. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  4. package/lib/agent-cli-provider/adapters/claude.js +23 -10
  5. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  6. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  7. package/lib/agent-cli-provider/adapters/codex.js +4 -0
  8. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  9. package/lib/agent-cli-provider/adapters/common.d.ts.map +1 -1
  10. package/lib/agent-cli-provider/adapters/common.js +2 -1
  11. package/lib/agent-cli-provider/adapters/common.js.map +1 -1
  12. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  13. package/lib/agent-cli-provider/contract-options.js +2 -1
  14. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  15. package/lib/agent-cli-provider/index.d.ts +1 -1
  16. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  17. package/lib/agent-cli-provider/index.js.map +1 -1
  18. package/lib/agent-cli-provider/provider-registry.d.ts +1 -1
  19. package/lib/agent-cli-provider/provider-registry.js +1 -1
  20. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  21. package/lib/agent-cli-provider/single-agent-runtime.js +6 -2
  22. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  23. package/lib/agent-cli-provider/types.d.ts +3 -1
  24. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  25. package/lib/agent-cli-provider/types.js.map +1 -1
  26. package/lib/git-remote-utils.js +90 -7
  27. package/lib/settings.js +1 -0
  28. package/package.json +6 -15
  29. package/protocol/openengine-cluster/v1/worker.schema.json +47 -0
  30. package/scripts/assert-release-published.js +128 -0
  31. package/scripts/release-dry-run.js +83 -0
  32. package/scripts/release-preflight.js +24 -12
  33. package/scripts/release-recovery.js +149 -0
  34. package/scripts/run-lint-staged-no-stash.js +68 -0
  35. package/scripts/semantic-release-notes.js +44 -0
  36. package/scripts/setup-merge-queue.sh +104 -118
  37. package/src/agent/agent-lifecycle.js +368 -91
  38. package/src/agent/agent-task-executor.js +384 -101
  39. package/src/agent-cli-provider/adapters/claude.ts +29 -11
  40. package/src/agent-cli-provider/adapters/codex.ts +4 -0
  41. package/src/agent-cli-provider/adapters/common.ts +2 -1
  42. package/src/agent-cli-provider/contract-options.ts +2 -1
  43. package/src/agent-cli-provider/index.ts +1 -0
  44. package/src/agent-cli-provider/provider-registry.ts +1 -1
  45. package/src/agent-cli-provider/single-agent-runtime.ts +8 -2
  46. package/src/agent-cli-provider/types.ts +3 -1
  47. package/src/agent-wrapper.js +9 -2
  48. package/src/agents/git-pusher-template.js +94 -19
  49. package/src/attach/socket-discovery.js +9 -19
  50. package/src/attach/socket-paths.js +85 -0
  51. package/src/config-validator.js +10 -11
  52. package/src/isolation-manager.js +164 -74
  53. package/src/orchestrator.js +101 -40
  54. package/task-lib/attachable-watcher.js +13 -10
  55. package/task-lib/commands/kill.js +34 -9
  56. package/task-lib/commands/run.js +17 -2
  57. package/task-lib/commands/status.js +13 -3
  58. package/task-lib/process-termination.js +202 -0
  59. package/task-lib/runner.js +29 -23
  60. package/task-lib/store.js +28 -6
  61. package/task-lib/watcher.js +14 -2
@@ -28,6 +28,7 @@ const { readRepoSettings } = require('../lib/repo-settings');
28
28
  const { provisionClaudeCredentials } = require('./claude-credentials');
29
29
 
30
30
  const DEFAULT_WORKTREE_SETUP_TIMEOUT_MS = 15 * 60 * 1000;
31
+ const FRESH_BASE_REF_PREFIX = 'refs/zeroshot/base-fetch';
31
32
 
32
33
  function runSync(command, args, options = {}) {
33
34
  const timeout = options.timeout ?? 30000;
@@ -48,6 +49,61 @@ function runShellSync(command, options = {}) {
48
49
  return runSync('/bin/bash', ['-lc', command], options);
49
50
  }
50
51
 
52
+ function resolveCommit(repoRoot, ref) {
53
+ return runSync('git', ['rev-parse', '--verify', `${ref}^{commit}`], {
54
+ cwd: repoRoot,
55
+ encoding: 'utf8',
56
+ stdio: 'pipe',
57
+ }).trim();
58
+ }
59
+
60
+ function deleteTemporaryBaseRef(repoRoot, temporaryRef) {
61
+ try {
62
+ runSync('git', ['update-ref', '-d', temporaryRef], {
63
+ cwd: repoRoot,
64
+ encoding: 'utf8',
65
+ stdio: 'pipe',
66
+ });
67
+ } catch (err) {
68
+ console.warn(
69
+ `[IsolationManager] Warning: failed to remove temporary base ref ${temporaryRef}: ${err.message}`
70
+ );
71
+ }
72
+ }
73
+
74
+ function fetchFreshRemoteBase(repoRoot, remoteName, branch) {
75
+ const temporaryRef = `${FRESH_BASE_REF_PREFIX}/${crypto.randomBytes(16).toString('hex')}`;
76
+
77
+ try {
78
+ runSync(
79
+ 'git',
80
+ [
81
+ 'fetch',
82
+ '--atomic',
83
+ '--no-tags',
84
+ '--no-write-fetch-head',
85
+ '--refmap=',
86
+ '--',
87
+ remoteName,
88
+ `+refs/heads/${branch}:${temporaryRef}`,
89
+ ],
90
+ {
91
+ cwd: repoRoot,
92
+ encoding: 'utf8',
93
+ stdio: 'pipe',
94
+ }
95
+ );
96
+
97
+ return {
98
+ baseSha: resolveCommit(repoRoot, temporaryRef),
99
+ temporaryRef,
100
+ };
101
+ } catch (err) {
102
+ deleteTemporaryBaseRef(repoRoot, temporaryRef);
103
+ throw err;
104
+ }
105
+ }
106
+
51
107
  function expandHomePath(value) {
52
108
  if (!value) return value;
53
109
  if (value === '~') return os.homedir();
@@ -127,7 +183,7 @@ class IsolationManager {
127
183
  this.containers = new Map(); // clusterId -> containerId
128
184
  this.isolatedDirs = new Map(); // clusterId -> { path, originalDir }
129
185
  this.clusterConfigDirs = new Map(); // clusterId -> configDirPath
130
- this.worktrees = new Map(); // clusterId -> { path, branch, repoRoot }
186
+ this.worktrees = new Map(); // clusterId -> { path, branch, repoRoot, baseRef, baseSha }
131
187
  this._exitWatchers = new Map(); // clusterId -> ChildProcess
132
188
  }
133
189
 
@@ -1443,7 +1499,7 @@ class IsolationManager {
1443
1499
  * Creates a git worktree at ~/.zeroshot/worktrees/{clusterId}
1444
1500
  * @param {string} clusterId - Cluster ID
1445
1501
  * @param {string} workDir - Original working directory (must be a git repo)
1446
- * @returns {{ path: string, branch: string, repoRoot: string }}
1502
+ * @returns {{ path: string, branch: string, repoRoot: string, baseRef: string, baseSha: string }}
1447
1503
  */
1448
1504
  createWorktreeIsolation(clusterId, workDir, options = {}) {
1449
1505
  if (!this._isGitRepo(workDir)) {
@@ -1485,8 +1541,10 @@ class IsolationManager {
1485
1541
  * @param {string} workDir - Original working directory
1486
1542
  * @param {object} [options] - Worktree creation options
1487
1543
  * @param {string} [options.baseRef] - Git ref to base the worktree branch on
1544
+ * @param {string} [options.remoteName=origin] - Remote used by a remote base ref
1545
+ * @param {boolean} [options.requireFreshBase=false] - Require a freshly fetched remote base
1488
1546
  * @param {number} [options.worktreeSetupTimeoutMs] - Setup command timeout in milliseconds
1489
- * @returns {{ path: string, branch: string, repoRoot: string }}
1547
+ * @returns {{ path: string, branch: string, repoRoot: string, baseRef: string, baseSha: string }}
1490
1548
  */
1491
1549
  createWorktree(clusterId, workDir, options = {}) {
1492
1550
  const repoRoot = this._getGitRoot(workDir);
@@ -1560,20 +1618,42 @@ class IsolationManager {
1560
1618
  }
1561
1619
  const worktreeSetupTimeoutMs = resolveWorktreeSetupTimeoutMs(repoSettings, options);
1562
1620
 
1563
- // Best-effort ensure origin/<branch> exists locally if requested.
1564
- if (worktreeBaseRef && worktreeBaseRef.startsWith('origin/')) {
1565
- const branch = worktreeBaseRef.slice('origin/'.length);
1566
- try {
1567
- runSync('git', ['fetch', 'origin', branch], {
1568
- cwd: repoRoot,
1569
- encoding: 'utf8',
1570
- stdio: 'pipe',
1571
- });
1572
- } catch {
1573
- // ignore
1621
+ const baseRef = worktreeBaseRef || 'HEAD';
1622
+ const remoteName = options.remoteName || 'origin';
1623
+ let baseSha;
1624
+ let temporaryBaseRef = null;
1625
+
1626
+ if (worktreeBaseRef && worktreeBaseRef.startsWith(`${remoteName}/`)) {
1627
+ const branch = worktreeBaseRef.slice(remoteName.length + 1);
1628
+ const remoteTrackingRef = `refs/remotes/${remoteName}/${branch}`;
1629
+
1630
+ if (options.requireFreshBase === true) {
1631
+ const freshBase = fetchFreshRemoteBase(repoRoot, remoteName, branch);
1632
+ baseSha = freshBase.baseSha;
1633
+ temporaryBaseRef = freshBase.temporaryRef;
1634
+ } else {
1635
+ try {
1636
+ runSync(
1637
+ 'git',
1638
+ ['fetch', '--no-tags', '--', remoteName, `+refs/heads/${branch}:${remoteTrackingRef}`],
1639
+ {
1640
+ cwd: repoRoot,
1641
+ encoding: 'utf8',
1642
+ stdio: 'pipe',
1643
+ }
1644
+ );
1645
+ } catch {
1646
+ // Repository worktree settings historically allowed an offline cached remote ref.
1647
+ }
1648
+ baseSha = resolveCommit(repoRoot, remoteTrackingRef);
1574
1649
  }
1650
+ } else {
1651
+ baseSha = resolveCommit(repoRoot, baseRef);
1575
1652
  }
1576
1653
 
1654
+ console.log(`[IsolationManager] Worktree base ref: ${baseRef}`);
1655
+ console.log(`[IsolationManager] Worktree base commit: ${baseSha}`);
1656
+
1577
1657
  // Create branch name from cluster ID (e.g., cluster-cosmic-meteor-87 -> zeroshot/cosmic-meteor-87)
1578
1658
  const baseBranchName = `zeroshot/${clusterId.replace(/^cluster-/, '')}`;
1579
1659
  let branchName = baseBranchName;
@@ -1581,44 +1661,18 @@ class IsolationManager {
1581
1661
  // Worktree path in persistent location (survives reboots)
1582
1662
  const worktreePath = path.join(os.homedir(), '.zeroshot', 'worktrees', clusterId);
1583
1663
 
1584
- // Ensure parent directory exists
1585
- const parentDir = path.dirname(worktreePath);
1586
- if (!fs.existsSync(parentDir)) {
1587
- fs.mkdirSync(parentDir, { recursive: true });
1588
- }
1589
-
1590
- // Best-effort cleanup of stale worktree metadata and directory.
1591
- // IMPORTANT: If a previous run deleted the directory without deregistering the worktree,
1592
- // git may keep the branch "checked out" and block deletion/reuse.
1593
1664
  try {
1594
- runSync('git', ['worktree', 'remove', '--force', worktreePath], {
1595
- cwd: repoRoot,
1596
- encoding: 'utf8',
1597
- stdio: 'pipe',
1598
- });
1599
- } catch {
1600
- // ignore
1601
- }
1602
- try {
1603
- runSync('git', ['worktree', 'prune'], { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' });
1604
- } catch {
1605
- // ignore
1606
- }
1607
- try {
1608
- fs.rmSync(worktreePath, { recursive: true, force: true });
1609
- } catch {
1610
- // ignore
1611
- }
1612
-
1613
- const baseRef = worktreeBaseRef || 'HEAD';
1614
- console.log(`[IsolationManager] Worktree base ref: ${baseRef}`);
1615
- console.log(`[IsolationManager] Worktree path: ${worktreePath}`);
1665
+ // Ensure parent directory exists
1666
+ const parentDir = path.dirname(worktreePath);
1667
+ if (!fs.existsSync(parentDir)) {
1668
+ fs.mkdirSync(parentDir, { recursive: true });
1669
+ }
1616
1670
 
1617
- // Create worktree with new branch based on baseRef (retry on branch collision/in-use)
1618
- for (let attempt = 0; attempt < 10; attempt++) {
1619
- // Best-effort delete if branch exists and is not in use by another worktree.
1671
+ // Best-effort cleanup of stale worktree metadata and directory.
1672
+ // IMPORTANT: If a previous run deleted the directory without deregistering the worktree,
1673
+ // git may keep the branch "checked out" and block deletion/reuse.
1620
1674
  try {
1621
- runSync('git', ['branch', '-D', branchName], {
1675
+ runSync('git', ['worktree', 'remove', '--force', worktreePath], {
1622
1676
  cwd: repoRoot,
1623
1677
  encoding: 'utf8',
1624
1678
  stdio: 'pipe',
@@ -1626,38 +1680,72 @@ class IsolationManager {
1626
1680
  } catch {
1627
1681
  // ignore
1628
1682
  }
1629
-
1630
1683
  try {
1631
- runSync('git', ['worktree', 'add', '-b', branchName, worktreePath, baseRef], {
1684
+ runSync('git', ['worktree', 'prune'], {
1632
1685
  cwd: repoRoot,
1633
1686
  encoding: 'utf8',
1634
1687
  stdio: 'pipe',
1635
1688
  });
1636
- console.log(`[IsolationManager] Worktree setup phase: created branch ${branchName}`);
1637
- break;
1638
- } catch (err) {
1639
- const stderr = (
1640
- err && (err.stderr || err.message) ? String(err.stderr || err.message) : ''
1641
- ).toLowerCase();
1642
- const isBranchCollision =
1643
- stderr.includes('already exists') ||
1644
- stderr.includes('cannot delete branch') ||
1645
- stderr.includes('checked out');
1646
-
1647
- if (attempt < 9 && isBranchCollision) {
1648
- branchName = `${baseBranchName}-${crypto.randomBytes(3).toString('hex')}`;
1649
- try {
1650
- runSync('git', ['worktree', 'prune'], {
1651
- cwd: repoRoot,
1652
- encoding: 'utf8',
1653
- stdio: 'pipe',
1654
- });
1655
- } catch {
1656
- // ignore
1689
+ } catch {
1690
+ // ignore
1691
+ }
1692
+ try {
1693
+ fs.rmSync(worktreePath, { recursive: true, force: true });
1694
+ } catch {
1695
+ // ignore
1696
+ }
1697
+
1698
+ console.log(`[IsolationManager] Worktree path: ${worktreePath}`);
1699
+
1700
+ // Create worktree with new branch based on baseRef (retry on branch collision/in-use)
1701
+ for (let attempt = 0; attempt < 10; attempt++) {
1702
+ // Best-effort delete if branch exists and is not in use by another worktree.
1703
+ try {
1704
+ runSync('git', ['branch', '-D', branchName], {
1705
+ cwd: repoRoot,
1706
+ encoding: 'utf8',
1707
+ stdio: 'pipe',
1708
+ });
1709
+ } catch {
1710
+ // ignore
1711
+ }
1712
+
1713
+ try {
1714
+ runSync('git', ['worktree', 'add', '-b', branchName, worktreePath, baseSha], {
1715
+ cwd: repoRoot,
1716
+ encoding: 'utf8',
1717
+ stdio: 'pipe',
1718
+ });
1719
+ console.log(`[IsolationManager] Worktree setup phase: created branch ${branchName}`);
1720
+ break;
1721
+ } catch (err) {
1722
+ const stderr = (
1723
+ err && (err.stderr || err.message) ? String(err.stderr || err.message) : ''
1724
+ ).toLowerCase();
1725
+ const isBranchCollision =
1726
+ stderr.includes('already exists') ||
1727
+ stderr.includes('cannot delete branch') ||
1728
+ stderr.includes('checked out');
1729
+
1730
+ if (attempt < 9 && isBranchCollision) {
1731
+ branchName = `${baseBranchName}-${crypto.randomBytes(3).toString('hex')}`;
1732
+ try {
1733
+ runSync('git', ['worktree', 'prune'], {
1734
+ cwd: repoRoot,
1735
+ encoding: 'utf8',
1736
+ stdio: 'pipe',
1737
+ });
1738
+ } catch {
1739
+ // ignore
1740
+ }
1741
+ continue;
1657
1742
  }
1658
- continue;
1743
+ throw err;
1659
1744
  }
1660
- throw err;
1745
+ }
1746
+ } finally {
1747
+ if (temporaryBaseRef) {
1748
+ deleteTemporaryBaseRef(repoRoot, temporaryBaseRef);
1661
1749
  }
1662
1750
  }
1663
1751
 
@@ -1684,6 +1772,8 @@ class IsolationManager {
1684
1772
  path: worktreePath,
1685
1773
  branch: branchName,
1686
1774
  repoRoot,
1775
+ baseRef,
1776
+ baseSha,
1687
1777
  };
1688
1778
  }
1689
1779
 
@@ -224,6 +224,7 @@ function buildPrOptions(options, requiredQualityGates) {
224
224
  prBase: options.prBase || null,
225
225
  mergeQueue: options.mergeQueue || false,
226
226
  closeIssue: options.closeIssue || null,
227
+ gitRemote: options.gitRemote || null,
227
228
  autoMerge,
228
229
  ...(requiredQualityGates.length > 0 ? { requiredQualityGates } : {}),
229
230
  cwd: options.cwd || process.cwd(),
@@ -1266,6 +1267,8 @@ class Orchestrator {
1266
1267
  const { detectGitContext } = require('../lib/git-remote-utils');
1267
1268
  const gitContext = detectGitContext(options.cwd);
1268
1269
  cluster.gitPlatform = gitContext?.provider || null;
1270
+ options.gitRemote = gitContext?.remote || options.gitRemote || null;
1271
+ cluster.prOptions = buildPrOptions(options, requiredQualityGates);
1269
1272
 
1270
1273
  if (cluster.gitPlatform) {
1271
1274
  this._log(`[Orchestrator] Git platform detected: ${cluster.gitPlatform.toUpperCase()}`);
@@ -1535,6 +1538,7 @@ class Orchestrator {
1535
1538
  const agentRole = message.content?.data?.role;
1536
1539
  const attempts = message.content?.data?.attempts || 1;
1537
1540
  const hookFailure = message.content?.data?.hookFailure === true;
1541
+ const restartExhausted = message.content?.data?.restartExhausted === true;
1538
1542
 
1539
1543
  await this._saveClusters();
1540
1544
 
@@ -1542,7 +1546,7 @@ class Orchestrator {
1542
1546
  agentRole === 'implementation' ||
1543
1547
  agentRole === 'coordinator' ||
1544
1548
  message.sender === 'consensus-coordinator';
1545
- const shouldStop = shouldStopForRole && (hookFailure || attempts >= 3);
1549
+ const shouldStop = shouldStopForRole && (hookFailure || restartExhausted || attempts >= 3);
1546
1550
 
1547
1551
  if (shouldStop) {
1548
1552
  this._log(`\n${'='.repeat(80)}`);
@@ -1551,6 +1555,7 @@ class Orchestrator {
1551
1555
  this._log(
1552
1556
  `${message.sender} (${agentRole || 'unknown role'}) failed` +
1553
1557
  (hookFailure ? ` (hookFailure=true)` : ``) +
1558
+ (restartExhausted ? ` (restartExhausted=true)` : ``) +
1554
1559
  ` after ${attempts} attempts`
1555
1560
  );
1556
1561
  this._log(`Error: ${message.content?.data?.error || 'unknown'}`);
@@ -1573,7 +1578,7 @@ class Orchestrator {
1573
1578
  });
1574
1579
  }
1575
1580
 
1576
- _registerAgentLifecycleHandlers(messageBus, clusterId) {
1581
+ _registerAgentLifecycleHandlers(messageBus, _clusterId) {
1577
1582
  messageBus.on('topic:AGENT_LIFECYCLE', async (message) => {
1578
1583
  const event = message.content?.data?.event;
1579
1584
  if (
@@ -1596,13 +1601,14 @@ class Orchestrator {
1596
1601
  const timeSinceLastOutput = message.content?.data?.timeSinceLastOutput;
1597
1602
  const analysis = message.content?.data?.analysis || 'No analysis available';
1598
1603
 
1604
+ const consecutiveWarnings = message.content?.data?.consecutiveWarnings;
1605
+ const warningsBeforeKill = message.content?.data?.warningsBeforeKill;
1599
1606
  this._log(
1600
- `⚠️ Orchestrator: Agent ${agentId} appears stale (${Math.round(timeSinceLastOutput / 1000)}s no output) but will NOT be killed`
1607
+ `⚠️ Orchestrator: Agent ${agentId} has produced no output for ${Math.round(timeSinceLastOutput / 1000)}s ` +
1608
+ `(warning ${consecutiveWarnings}/${warningsBeforeKill})`
1601
1609
  );
1602
1610
  this._log(` Analysis: ${analysis}`);
1603
- this._log(
1604
- ` Manual intervention may be needed - use 'zeroshot resume ${clusterId}' if stuck`
1605
- );
1611
+ this._log(` Zeroshot will terminate and restart the task if inactivity persists`);
1606
1612
  });
1607
1613
  }
1608
1614
 
@@ -1793,18 +1799,30 @@ class Orchestrator {
1793
1799
  const workDir = options.cwd || process.cwd();
1794
1800
 
1795
1801
  isolationManager = new IsolationManager({});
1796
- // `prBase` is the PR target branch. Use that same local ref as the
1797
- // worktree base so repo-local integration commits are not skipped.
1802
+ const { detectGitContext } = require('../lib/git-remote-utils');
1803
+ const gitContext = detectGitContext(workDir);
1804
+ if (gitContext?.remote) {
1805
+ options.gitRemote = gitContext.remote;
1806
+ }
1807
+
1808
+ // `prBase` is the PR target branch. Base isolated work on its refreshed
1809
+ // remote-tracking ref so a stale local branch cannot change the plan.
1798
1810
  const worktreeOptions = {};
1799
1811
  if (options.prBase) {
1800
- worktreeOptions.baseRef = options.prBase;
1801
- this._log(`[Orchestrator] Using worktree base ref: ${options.prBase}`);
1812
+ const remoteName = options.gitRemote || 'origin';
1813
+ worktreeOptions.baseRef = `${remoteName}/${options.prBase}`;
1814
+ if (remoteName !== 'origin') {
1815
+ worktreeOptions.remoteName = remoteName;
1816
+ }
1817
+ worktreeOptions.requireFreshBase = true;
1818
+ this._log(`[Orchestrator] Using worktree base ref: ${worktreeOptions.baseRef}`);
1802
1819
  }
1803
1820
  worktreeInfo = isolationManager.createWorktreeIsolation(clusterId, workDir, worktreeOptions);
1804
1821
 
1805
1822
  this._log(`[Orchestrator] Starting cluster in worktree isolation mode`);
1806
1823
  this._log(`[Orchestrator] Worktree: ${worktreeInfo.path}`);
1807
1824
  this._log(`[Orchestrator] Branch: ${worktreeInfo.branch}`);
1825
+ this._log(`[Orchestrator] Worktree base commit: ${worktreeInfo.baseSha}`);
1808
1826
  }
1809
1827
 
1810
1828
  return { isolationManager, containerId, worktreeInfo, image: isolationImage };
@@ -1867,16 +1885,13 @@ class Orchestrator {
1867
1885
  closeIssue: options.closeIssue,
1868
1886
  requiredQualityGates: options.requiredQualityGates,
1869
1887
  autoMerge: resolveRunPlan(options).autoMerge,
1888
+ gitRemote: gitContext?.remote || options.gitRemote,
1889
+ issueNumber: inputData.number,
1890
+ issueTitle: inputData.title,
1891
+ includeIssueReference: !skipCloseIssue,
1870
1892
  cwd: options.cwd,
1871
1893
  });
1872
1894
 
1873
- // Template replacement for issue context
1874
- const issueRef = skipCloseIssue ? '' : `Closes #${inputData.number || 'unknown'}`;
1875
- gitPusherConfig.prompt = gitPusherConfig.prompt
1876
- .replace(/\{\{issue_number\}\}/g, inputData.number || 'unknown')
1877
- .replace(/\{\{issue_title\}\}/g, inputData.title || 'Implementation')
1878
- .replace(/Closes #\{\{issue_number\}\}/g, issueRef);
1879
-
1880
1895
  config.agents.push(gitPusherConfig);
1881
1896
  this._log(
1882
1897
  `[Orchestrator] Injected ${platform}-git-pusher agent (issue: ${inputData.number || 'N/A'}, PR platform: ${platform.toUpperCase()})`
@@ -2361,6 +2376,15 @@ class Orchestrator {
2361
2376
  return;
2362
2377
  }
2363
2378
  this.closed = true;
2379
+
2380
+ for (const cluster of this.clusters.values()) {
2381
+ if (typeof cluster.snapshotter?.stop === 'function') {
2382
+ cluster.snapshotter.stop();
2383
+ }
2384
+ if (typeof cluster.messageBus?.close === 'function') {
2385
+ cluster.messageBus.close();
2386
+ }
2387
+ }
2364
2388
  }
2365
2389
 
2366
2390
  /**
@@ -2637,31 +2661,68 @@ class Orchestrator {
2637
2661
  }
2638
2662
 
2639
2663
  _resolveFailureInfo(cluster, clusterId) {
2640
- if (cluster.failureInfo) {
2641
- return cluster.failureInfo.agentId ? cluster.failureInfo : null;
2664
+ const persistedFailure = cluster.failureInfo?.agentId ? cluster.failureInfo : null;
2665
+ if (persistedFailure) {
2666
+ if (!this._hasDurableProgressAfterFailure(cluster, clusterId, persistedFailure)) {
2667
+ return persistedFailure;
2668
+ }
2669
+ this._log(
2670
+ `[Orchestrator] Ignoring recovered registry failure from ${persistedFailure.agentId}; durable task progress followed it`
2671
+ );
2642
2672
  }
2643
2673
 
2644
2674
  const errors = cluster.messageBus.query({
2645
2675
  cluster_id: clusterId,
2646
2676
  topic: 'AGENT_ERROR',
2647
- limit: 10,
2648
2677
  order: 'desc',
2649
2678
  });
2650
2679
 
2651
- if (errors.length === 0) {
2652
- return null;
2680
+ for (const error of errors) {
2681
+ const failureInfo = {
2682
+ agentId: error.sender,
2683
+ taskId: error.content?.data?.taskId || null,
2684
+ iteration: error.content?.data?.iteration || 0,
2685
+ error: error.content?.data?.error || error.content?.text,
2686
+ timestamp: error.timestamp,
2687
+ };
2688
+ if (this._hasDurableProgressAfterFailure(cluster, clusterId, failureInfo)) {
2689
+ this._log(
2690
+ `[Orchestrator] Ignoring recovered ledger failure from ${error.sender}; durable task progress followed it`
2691
+ );
2692
+ continue;
2693
+ }
2694
+
2695
+ this._log(`[Orchestrator] Found failure from ledger: ${failureInfo.agentId}`);
2696
+ return failureInfo;
2653
2697
  }
2654
2698
 
2655
- const firstError = errors[0];
2656
- const failureInfo = {
2657
- agentId: firstError.sender,
2658
- taskId: firstError.content?.data?.taskId || null,
2659
- iteration: firstError.content?.data?.iteration || 0,
2660
- error: firstError.content?.data?.error || firstError.content?.text,
2661
- timestamp: firstError.timestamp,
2662
- };
2663
- this._log(`[Orchestrator] Found failure from ledger: ${failureInfo.agentId}`);
2664
- return failureInfo;
2699
+ return null;
2700
+ }
2701
+
2702
+ _hasDurableProgressAfterFailure(cluster, clusterId, failureInfo) {
2703
+ const failureIteration = failureInfo.iteration;
2704
+ const failureTimestamp = failureInfo.timestamp;
2705
+ return cluster.messageBus
2706
+ .query({
2707
+ cluster_id: clusterId,
2708
+ topic: 'AGENT_LIFECYCLE',
2709
+ sender: failureInfo.agentId,
2710
+ since: failureTimestamp,
2711
+ })
2712
+ .some((message) => {
2713
+ const event = message.content?.data?.event;
2714
+ if (!['TASK_STARTED', 'TASK_COMPLETED'].includes(event)) {
2715
+ return false;
2716
+ }
2717
+
2718
+ const iteration = message.content?.data?.iteration;
2719
+ return (
2720
+ (Number.isFinite(failureTimestamp) && message.timestamp > failureTimestamp) ||
2721
+ (Number.isInteger(failureIteration) &&
2722
+ Number.isInteger(iteration) &&
2723
+ iteration > failureIteration)
2724
+ );
2725
+ });
2665
2726
  }
2666
2727
 
2667
2728
  _requireFailedAgent(clusterId, cluster, failureInfo) {
@@ -3984,7 +4045,6 @@ Continue from where you left off. Review your previous output to understand what
3984
4045
  }
3985
4046
  }
3986
4047
 
3987
- // Generate platform-specific git-pusher agent from template
3988
4048
  const {
3989
4049
  generateGitPusherAgent,
3990
4050
  isPlatformSupported,
@@ -3996,18 +4056,19 @@ Continue from where you left off. Review your previous output to understand what
3996
4056
  );
3997
4057
  }
3998
4058
 
3999
- // Use persisted PR options from cluster state (or empty for repo settings fallback)
4000
- const gitPusherConfig = generateGitPusherAgent(platform, cluster.prOptions || {});
4001
-
4002
4059
  // Get issue context from ledger
4003
4060
  const issueMsg = cluster.messageBus.ledger.findLast({ topic: 'ISSUE_OPENED' });
4004
4061
  const issueNumber = issueMsg?.content?.data?.number || 'unknown';
4005
4062
  const issueTitle = issueMsg?.content?.data?.title || 'Implementation';
4006
4063
 
4007
- // Inject issue context into prompt
4008
- gitPusherConfig.prompt = gitPusherConfig.prompt
4009
- .replace(/\{\{issue_number\}\}/g, issueNumber)
4010
- .replace(/\{\{issue_title\}\}/g, issueTitle);
4064
+ // Generate the final prompt in one typed assembly pass. Issue values are
4065
+ // resolved before the shell-quoted remote is inserted, so placeholder-like
4066
+ // remote names cannot be rewritten by issue context.
4067
+ const gitPusherConfig = generateGitPusherAgent(platform, {
4068
+ ...(cluster.prOptions || {}),
4069
+ issueNumber,
4070
+ issueTitle,
4071
+ });
4011
4072
 
4012
4073
  await this._opAddAgents(cluster, { agents: [gitPusherConfig] }, context);
4013
4074
  this._log(` [--pr mode] Injected ${platform}-git-pusher agent`);
@@ -5,10 +5,8 @@
5
5
  * Runs detached from parent, provides Unix socket for attach clients.
6
6
  */
7
7
 
8
- import { appendFileSync, existsSync, mkdirSync, unlinkSync } from 'fs';
8
+ import { appendFileSync, unlinkSync } from 'fs';
9
9
  import { unlink } from 'fs/promises';
10
- import { join } from 'path';
11
- import { homedir } from 'os';
12
10
  import { updateTask } from './store.js';
13
11
  import {
14
12
  detectProviderFatalError,
@@ -72,6 +70,7 @@ process.on('unhandledRejection', (reason) => {
72
70
 
73
71
  const require = createRequire(import.meta.url);
74
72
  const { AttachServer } = require('../src/attach');
73
+ const { getTaskSocketPath } = require('../src/attach/socket-paths');
75
74
  const { normalizeProviderName } = require('../lib/provider-names');
76
75
 
77
76
  const taskId = taskIdArg;
@@ -88,12 +87,7 @@ const commandSpec = config.commandSpec || {
88
87
  commandSpecCleanup = commandSpec.cleanup || [];
89
88
  let server = null;
90
89
 
91
- const SOCKET_DIR = join(homedir(), '.zeroshot', 'sockets');
92
- const socketPath = join(SOCKET_DIR, `${taskId}.sock`);
93
-
94
- if (!existsSync(SOCKET_DIR)) {
95
- mkdirSync(SOCKET_DIR, { recursive: true });
96
- }
90
+ const socketPath = getTaskSocketPath(taskId);
97
91
 
98
92
  function log(msg) {
99
93
  appendFileSync(logFile, msg);
@@ -319,6 +313,8 @@ server.on('exit', async ({ exitCode, signal }) => {
319
313
  try {
320
314
  await updateTask(taskId, {
321
315
  status,
316
+ pid: null,
317
+ processGroupId: null,
322
318
  exitCode: resolvedCode,
323
319
  error: fatalError || (resolvedCode !== 0 && signal ? `Killed by ${signal}` : null),
324
320
  socketPath: null,
@@ -336,7 +332,12 @@ server.on('error', async (err) => {
336
332
  log(`\nError: ${err.message}\n`);
337
333
  await cleanupCommandSpec();
338
334
  try {
339
- await updateTask(taskId, { status: 'failed', error: err.message });
335
+ await updateTask(taskId, {
336
+ status: 'failed',
337
+ pid: null,
338
+ processGroupId: null,
339
+ error: err.message,
340
+ });
340
341
  } catch (updateError) {
341
342
  log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
342
343
  }
@@ -358,6 +359,8 @@ try {
358
359
  pid: server.pid,
359
360
  socketPath,
360
361
  attachable: true,
362
+ processGroupId: process.platform === 'win32' ? null : server.pid,
363
+ terminationStrategy: process.platform === 'win32' ? 'process-tree' : 'process-group',
361
364
  });
362
365
 
363
366
  log(`[${Date.now()}][SYSTEM] Started with PTY (attachable)\n`);