@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 +114 -16
- package/lib/git-remote-utils.js +90 -7
- 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/semantic-release-notes.js +44 -0
- package/scripts/setup-merge-queue.sh +104 -118
- 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/isolation-manager.js +164 -74
- package/src/orchestrator.js +83 -34
- package/task-lib/attachable-watcher.js +3 -9
- package/task-lib/commands/run.js +17 -2
- package/task-lib/runner.js +21 -3
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()}`);
|
|
@@ -1796,18 +1799,30 @@ class Orchestrator {
|
|
|
1796
1799
|
const workDir = options.cwd || process.cwd();
|
|
1797
1800
|
|
|
1798
1801
|
isolationManager = new IsolationManager({});
|
|
1799
|
-
|
|
1800
|
-
|
|
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.
|
|
1801
1810
|
const worktreeOptions = {};
|
|
1802
1811
|
if (options.prBase) {
|
|
1803
|
-
|
|
1804
|
-
|
|
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}`);
|
|
1805
1819
|
}
|
|
1806
1820
|
worktreeInfo = isolationManager.createWorktreeIsolation(clusterId, workDir, worktreeOptions);
|
|
1807
1821
|
|
|
1808
1822
|
this._log(`[Orchestrator] Starting cluster in worktree isolation mode`);
|
|
1809
1823
|
this._log(`[Orchestrator] Worktree: ${worktreeInfo.path}`);
|
|
1810
1824
|
this._log(`[Orchestrator] Branch: ${worktreeInfo.branch}`);
|
|
1825
|
+
this._log(`[Orchestrator] Worktree base commit: ${worktreeInfo.baseSha}`);
|
|
1811
1826
|
}
|
|
1812
1827
|
|
|
1813
1828
|
return { isolationManager, containerId, worktreeInfo, image: isolationImage };
|
|
@@ -1870,16 +1885,13 @@ class Orchestrator {
|
|
|
1870
1885
|
closeIssue: options.closeIssue,
|
|
1871
1886
|
requiredQualityGates: options.requiredQualityGates,
|
|
1872
1887
|
autoMerge: resolveRunPlan(options).autoMerge,
|
|
1888
|
+
gitRemote: gitContext?.remote || options.gitRemote,
|
|
1889
|
+
issueNumber: inputData.number,
|
|
1890
|
+
issueTitle: inputData.title,
|
|
1891
|
+
includeIssueReference: !skipCloseIssue,
|
|
1873
1892
|
cwd: options.cwd,
|
|
1874
1893
|
});
|
|
1875
1894
|
|
|
1876
|
-
// Template replacement for issue context
|
|
1877
|
-
const issueRef = skipCloseIssue ? '' : `Closes #${inputData.number || 'unknown'}`;
|
|
1878
|
-
gitPusherConfig.prompt = gitPusherConfig.prompt
|
|
1879
|
-
.replace(/\{\{issue_number\}\}/g, inputData.number || 'unknown')
|
|
1880
|
-
.replace(/\{\{issue_title\}\}/g, inputData.title || 'Implementation')
|
|
1881
|
-
.replace(/Closes #\{\{issue_number\}\}/g, issueRef);
|
|
1882
|
-
|
|
1883
1895
|
config.agents.push(gitPusherConfig);
|
|
1884
1896
|
this._log(
|
|
1885
1897
|
`[Orchestrator] Injected ${platform}-git-pusher agent (issue: ${inputData.number || 'N/A'}, PR platform: ${platform.toUpperCase()})`
|
|
@@ -2649,31 +2661,68 @@ class Orchestrator {
|
|
|
2649
2661
|
}
|
|
2650
2662
|
|
|
2651
2663
|
_resolveFailureInfo(cluster, clusterId) {
|
|
2652
|
-
|
|
2653
|
-
|
|
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
|
+
);
|
|
2654
2672
|
}
|
|
2655
2673
|
|
|
2656
2674
|
const errors = cluster.messageBus.query({
|
|
2657
2675
|
cluster_id: clusterId,
|
|
2658
2676
|
topic: 'AGENT_ERROR',
|
|
2659
|
-
limit: 10,
|
|
2660
2677
|
order: 'desc',
|
|
2661
2678
|
});
|
|
2662
2679
|
|
|
2663
|
-
|
|
2664
|
-
|
|
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;
|
|
2665
2697
|
}
|
|
2666
2698
|
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
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
|
+
});
|
|
2677
2726
|
}
|
|
2678
2727
|
|
|
2679
2728
|
_requireFailedAgent(clusterId, cluster, failureInfo) {
|
|
@@ -3996,7 +4045,6 @@ Continue from where you left off. Review your previous output to understand what
|
|
|
3996
4045
|
}
|
|
3997
4046
|
}
|
|
3998
4047
|
|
|
3999
|
-
// Generate platform-specific git-pusher agent from template
|
|
4000
4048
|
const {
|
|
4001
4049
|
generateGitPusherAgent,
|
|
4002
4050
|
isPlatformSupported,
|
|
@@ -4008,18 +4056,19 @@ Continue from where you left off. Review your previous output to understand what
|
|
|
4008
4056
|
);
|
|
4009
4057
|
}
|
|
4010
4058
|
|
|
4011
|
-
// Use persisted PR options from cluster state (or empty for repo settings fallback)
|
|
4012
|
-
const gitPusherConfig = generateGitPusherAgent(platform, cluster.prOptions || {});
|
|
4013
|
-
|
|
4014
4059
|
// Get issue context from ledger
|
|
4015
4060
|
const issueMsg = cluster.messageBus.ledger.findLast({ topic: 'ISSUE_OPENED' });
|
|
4016
4061
|
const issueNumber = issueMsg?.content?.data?.number || 'unknown';
|
|
4017
4062
|
const issueTitle = issueMsg?.content?.data?.title || 'Implementation';
|
|
4018
4063
|
|
|
4019
|
-
//
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
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
|
+
});
|
|
4023
4072
|
|
|
4024
4073
|
await this._opAddAgents(cluster, { agents: [gitPusherConfig] }, context);
|
|
4025
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);
|
package/task-lib/commands/run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { spawnTask } from '../runner.js';
|
|
2
|
+
import { shouldUseAttachableWatcher, spawnTask } from '../runner.js';
|
|
3
3
|
|
|
4
4
|
export async function runTask(prompt, options = {}) {
|
|
5
5
|
if (!prompt || prompt.trim().length === 0) {
|
|
@@ -46,8 +46,23 @@ export async function runTask(prompt, options = {}) {
|
|
|
46
46
|
console.log(chalk.dim(` Log: ${task.logFile}`));
|
|
47
47
|
console.log(chalk.dim(` CWD: ${task.cwd}`));
|
|
48
48
|
|
|
49
|
+
const attachSupported = shouldUseAttachableWatcher(
|
|
50
|
+
{
|
|
51
|
+
jsonSchema: outputFormat === 'json' ? jsonSchema : null,
|
|
52
|
+
},
|
|
53
|
+
task.provider
|
|
54
|
+
);
|
|
55
|
+
|
|
49
56
|
console.log(chalk.dim('\nCommands:'));
|
|
50
|
-
|
|
57
|
+
if (attachSupported) {
|
|
58
|
+
console.log(chalk.dim(` zeroshot attach ${task.id} # Attach to task (Ctrl+B d to detach)`));
|
|
59
|
+
} else {
|
|
60
|
+
console.log(
|
|
61
|
+
chalk.dim(
|
|
62
|
+
` Attach unavailable: ${task.provider} strict structured output uses a non-PTY watcher`
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
51
66
|
console.log(chalk.dim(` zeroshot logs ${task.id} # View output`));
|
|
52
67
|
console.log(chalk.dim(` zeroshot logs -f ${task.id} # Follow output`));
|
|
53
68
|
console.log(chalk.dim(` zeroshot status ${task.id} # Check status`));
|
package/task-lib/runner.js
CHANGED
|
@@ -53,7 +53,13 @@ export function spawnTask(prompt, options = {}) {
|
|
|
53
53
|
providerName,
|
|
54
54
|
commandSpec
|
|
55
55
|
);
|
|
56
|
-
const watcherScript = resolveWatcherScript(
|
|
56
|
+
const watcherScript = resolveWatcherScript(
|
|
57
|
+
{
|
|
58
|
+
attachable: options.attachable,
|
|
59
|
+
jsonSchema,
|
|
60
|
+
},
|
|
61
|
+
providerName
|
|
62
|
+
);
|
|
57
63
|
spawnWatcher({
|
|
58
64
|
watcherScript,
|
|
59
65
|
id,
|
|
@@ -165,8 +171,20 @@ function buildWatcherCommandSpec(commandSpec) {
|
|
|
165
171
|
return watcherCommandSpec;
|
|
166
172
|
}
|
|
167
173
|
|
|
168
|
-
function
|
|
169
|
-
|
|
174
|
+
export function shouldUseAttachableWatcher(options, providerName) {
|
|
175
|
+
if (options.attachable === false) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Claude strict structured output still needs the non-PTY watcher. Claude
|
|
180
|
+
// can treat PTY notifications as streaming commands and reject the run.
|
|
181
|
+
// Other providers, including Codex, support their structured-output mode in
|
|
182
|
+
// the attachable PTY watcher and must not lose the advertised attach socket.
|
|
183
|
+
return !(providerName === 'claude' && options.jsonSchema);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function resolveWatcherScript(options, providerName) {
|
|
187
|
+
const useAttachable = shouldUseAttachableWatcher(options, providerName);
|
|
170
188
|
return useAttachable ? join(__dirname, 'attachable-watcher.js') : join(__dirname, 'watcher.js');
|
|
171
189
|
}
|
|
172
190
|
|