@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.
- package/cli/commands/providers.js +4 -1
- package/cli/index.js +131 -27
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +23 -10
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +4 -0
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/adapters/common.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/common.js +2 -1
- package/lib/agent-cli-provider/adapters/common.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +2 -1
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/index.d.ts +1 -1
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +1 -1
- package/lib/agent-cli-provider/provider-registry.js +1 -1
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +6 -2
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +3 -1
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/git-remote-utils.js +90 -7
- package/lib/settings.js +1 -0
- package/package.json +6 -15
- package/protocol/openengine-cluster/v1/worker.schema.json +47 -0
- package/scripts/assert-release-published.js +128 -0
- package/scripts/release-dry-run.js +83 -0
- package/scripts/release-preflight.js +24 -12
- package/scripts/release-recovery.js +149 -0
- package/scripts/run-lint-staged-no-stash.js +68 -0
- package/scripts/semantic-release-notes.js +44 -0
- package/scripts/setup-merge-queue.sh +104 -118
- package/src/agent/agent-lifecycle.js +368 -91
- package/src/agent/agent-task-executor.js +384 -101
- package/src/agent-cli-provider/adapters/claude.ts +29 -11
- package/src/agent-cli-provider/adapters/codex.ts +4 -0
- package/src/agent-cli-provider/adapters/common.ts +2 -1
- package/src/agent-cli-provider/contract-options.ts +2 -1
- package/src/agent-cli-provider/index.ts +1 -0
- package/src/agent-cli-provider/provider-registry.ts +1 -1
- package/src/agent-cli-provider/single-agent-runtime.ts +8 -2
- package/src/agent-cli-provider/types.ts +3 -1
- package/src/agent-wrapper.js +9 -2
- package/src/agents/git-pusher-template.js +94 -19
- package/src/attach/socket-discovery.js +9 -19
- package/src/attach/socket-paths.js +85 -0
- package/src/config-validator.js +10 -11
- package/src/isolation-manager.js +164 -74
- package/src/orchestrator.js +101 -40
- package/task-lib/attachable-watcher.js +13 -10
- package/task-lib/commands/kill.js +34 -9
- package/task-lib/commands/run.js +17 -2
- package/task-lib/commands/status.js +13 -3
- package/task-lib/process-termination.js +202 -0
- package/task-lib/runner.js +29 -23
- package/task-lib/store.js +28 -6
- package/task-lib/watcher.js +14 -2
package/src/isolation-manager.js
CHANGED
|
@@ -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
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
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
|
-
|
|
1618
|
-
|
|
1619
|
-
//
|
|
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', ['
|
|
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', '
|
|
1684
|
+
runSync('git', ['worktree', 'prune'], {
|
|
1632
1685
|
cwd: repoRoot,
|
|
1633
1686
|
encoding: 'utf8',
|
|
1634
1687
|
stdio: 'pipe',
|
|
1635
1688
|
});
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
}
|
|
1656
|
-
|
|
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
|
-
|
|
1743
|
+
throw err;
|
|
1659
1744
|
}
|
|
1660
|
-
|
|
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
|
|
package/src/orchestrator.js
CHANGED
|
@@ -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,
|
|
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}
|
|
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
|
-
|
|
1797
|
-
|
|
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
|
-
|
|
1801
|
-
|
|
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
|
-
|
|
2641
|
-
|
|
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
|
-
|
|
2652
|
-
|
|
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
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
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
|
-
//
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
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,
|
|
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
|
|
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, {
|
|
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`);
|