@the-open-engine/zeroshot 6.7.1 → 6.8.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 +114 -16
- package/lib/agent-cli-provider/adapters/common.js +1 -1
- package/lib/agent-cli-provider/adapters/common.js.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode-models.d.ts +7 -0
- package/lib/agent-cli-provider/adapters/opencode-models.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/opencode-models.js +87 -0
- package/lib/agent-cli-provider/adapters/opencode-models.js.map +1 -0
- package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.js +6 -58
- package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +27 -13
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +1 -0
- 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/package.json +6 -15
- package/protocol/openengine-cluster/v1/worker.schema.json +47 -0
- package/scripts/assert-release-published.js +161 -2
- 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/agent/agent-task-executor.js +18 -9
- package/src/agent-cli-provider/adapters/common.ts +1 -1
- package/src/agent-cli-provider/adapters/opencode-models.ts +100 -0
- package/src/agent-cli-provider/adapters/opencode.ts +8 -65
- package/src/agent-cli-provider/single-agent-runtime.ts +51 -33
- package/src/agent-cli-provider/types.ts +1 -0
- package/src/agent-wrapper.js +14 -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/claude-task-runner.js +121 -37
- package/src/config-validator.js +2 -2
- package/src/isolation-manager.js +164 -74
- package/src/orchestrator.js +83 -34
- package/src/providers/index.js +5 -0
- package/src/task-run-model-args.js +155 -0
- package/task-lib/attachable-watcher.js +3 -9
- package/task-lib/commands/run.js +17 -2
- package/task-lib/runner.js +87 -29
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic attach socket paths.
|
|
3
|
+
*
|
|
4
|
+
* Unix-domain sockets have a small path budget (104 bytes on macOS). Keep the
|
|
5
|
+
* live socket namespace independent from HOME while retaining one namespace
|
|
6
|
+
* per OS user and Zeroshot home.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const SOCKET_ROOT = process.platform === 'win32' ? null : '/tmp';
|
|
15
|
+
const SOCKET_DIR_MODE = 0o700;
|
|
16
|
+
|
|
17
|
+
function shortHash(value) {
|
|
18
|
+
return crypto.createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function userNamespace() {
|
|
22
|
+
if (typeof process.getuid === 'function') {
|
|
23
|
+
return String(process.getuid());
|
|
24
|
+
}
|
|
25
|
+
return shortHash(os.userInfo().username);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveHomeDir(env = process.env) {
|
|
29
|
+
return env.ZEROSHOT_HOME || env.HOME || env.USERPROFILE || os.homedir();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getSocketDir(homeDir = resolveHomeDir()) {
|
|
33
|
+
if (process.platform === 'win32') {
|
|
34
|
+
return path.join(homeDir, '.zeroshot', 'sockets');
|
|
35
|
+
}
|
|
36
|
+
return path.join(SOCKET_ROOT, `zeroshot-${userNamespace()}-${shortHash(homeDir)}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function assertSafeSocketDir(socketDir) {
|
|
40
|
+
const stat = fs.lstatSync(socketDir);
|
|
41
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
42
|
+
throw new Error(`Attach socket path is not a directory: ${socketDir}`);
|
|
43
|
+
}
|
|
44
|
+
if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
|
|
45
|
+
throw new Error(`Attach socket directory is not owned by the current user: ${socketDir}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ensureOwnedDirectory(socketDir) {
|
|
50
|
+
fs.mkdirSync(socketDir, { recursive: true, mode: SOCKET_DIR_MODE });
|
|
51
|
+
assertSafeSocketDir(socketDir);
|
|
52
|
+
if (process.platform !== 'win32') {
|
|
53
|
+
fs.chmodSync(socketDir, SOCKET_DIR_MODE);
|
|
54
|
+
}
|
|
55
|
+
return socketDir;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ensureSocketDir(homeDir = resolveHomeDir()) {
|
|
59
|
+
const socketDir = getSocketDir(homeDir);
|
|
60
|
+
return ensureOwnedDirectory(socketDir);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getTaskSocketPath(taskId, homeDir = resolveHomeDir()) {
|
|
64
|
+
return path.join(ensureSocketDir(homeDir), `${taskId}.sock`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getAgentSocketPath(clusterId, agentId, homeDir = resolveHomeDir()) {
|
|
68
|
+
const clusterDir = path.join(ensureSocketDir(homeDir), clusterId);
|
|
69
|
+
ensureOwnedDirectory(clusterDir);
|
|
70
|
+
return path.join(clusterDir, `${agentId}.sock`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getClusterSocketPath(clusterId, homeDir = resolveHomeDir()) {
|
|
74
|
+
return path.join(ensureSocketDir(homeDir), `${clusterId}.sock`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
SOCKET_DIR_MODE,
|
|
79
|
+
resolveHomeDir,
|
|
80
|
+
getSocketDir,
|
|
81
|
+
ensureSocketDir,
|
|
82
|
+
getTaskSocketPath,
|
|
83
|
+
getAgentSocketPath,
|
|
84
|
+
getClusterSocketPath,
|
|
85
|
+
};
|
|
@@ -13,6 +13,18 @@ const { normalizeProviderName } = require('../lib/provider-names');
|
|
|
13
13
|
const { getProvider } = require('./providers');
|
|
14
14
|
const { prependWorktreeToolBinToEnv } = require('./worktree-tooling-env');
|
|
15
15
|
const { prepareClaudeConfigDir } = require('./worktree-claude-config');
|
|
16
|
+
const {
|
|
17
|
+
appendTaskRunModelArgs,
|
|
18
|
+
wrapTaskRunWithIsolatedSettings,
|
|
19
|
+
} = require('./task-run-model-args');
|
|
20
|
+
|
|
21
|
+
function rejectCallerSuppliedModelProvenance(options) {
|
|
22
|
+
if (Object.prototype.hasOwnProperty.call(options, 'modelSpecSource')) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
'modelSpecSource is derived from model versus modelLevel/default and cannot be supplied.'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
16
28
|
|
|
17
29
|
function runCommand(command, args, options = {}, callback = null) {
|
|
18
30
|
const timeout = options.timeout ?? 30000;
|
|
@@ -112,10 +124,11 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
112
124
|
* Execute a task via zeroshot CLI
|
|
113
125
|
*
|
|
114
126
|
* @param {string} context - Full prompt/context
|
|
115
|
-
* @param {{agentId?: string, model?: string, outputFormat?: string, jsonSchema?: any, strictSchema?: boolean, cwd?: string, worktreePath?: string|null, isolation?: any}} options - Execution options
|
|
127
|
+
* @param {{agentId?: string, model?: string, modelLevel?: string, modelSpec?: Object|null, reasoningEffort?: string, outputFormat?: string, jsonSchema?: any, strictSchema?: boolean, cwd?: string, worktreePath?: string|null, isolation?: any}} options - Execution options
|
|
116
128
|
* @returns {Promise<{success: boolean, output: string, error: string|null, taskId?: string}>}
|
|
117
129
|
*/
|
|
118
130
|
async run(context, options = {}) {
|
|
131
|
+
rejectCallerSuppliedModelProvenance(options);
|
|
119
132
|
const {
|
|
120
133
|
agentId = 'unknown',
|
|
121
134
|
provider,
|
|
@@ -137,7 +150,7 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
137
150
|
providerName,
|
|
138
151
|
settings
|
|
139
152
|
);
|
|
140
|
-
const
|
|
153
|
+
const modelSelection = this._resolveModelSelection({
|
|
141
154
|
explicitModelSpec,
|
|
142
155
|
model,
|
|
143
156
|
reasoningEffort,
|
|
@@ -146,13 +159,13 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
146
159
|
providerSettings,
|
|
147
160
|
levelOverrides,
|
|
148
161
|
});
|
|
162
|
+
const resolvedModelSpec = modelSelection.modelSpec;
|
|
149
163
|
|
|
150
164
|
// Isolation mode delegates to separate method
|
|
151
165
|
if (isolation?.enabled) {
|
|
152
166
|
return this._runIsolated(context, {
|
|
153
167
|
...options,
|
|
154
168
|
provider: providerName,
|
|
155
|
-
modelSpec: resolvedModelSpec,
|
|
156
169
|
});
|
|
157
170
|
}
|
|
158
171
|
|
|
@@ -168,6 +181,7 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
168
181
|
providerName,
|
|
169
182
|
runOutputFormat,
|
|
170
183
|
resolvedModelSpec,
|
|
184
|
+
modelSpecSource: modelSelection.source,
|
|
171
185
|
jsonSchema,
|
|
172
186
|
});
|
|
173
187
|
|
|
@@ -204,25 +218,74 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
204
218
|
providerSettings,
|
|
205
219
|
levelOverrides,
|
|
206
220
|
}) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
221
|
+
return this._resolveModelSelection({
|
|
222
|
+
explicitModelSpec,
|
|
223
|
+
model,
|
|
224
|
+
reasoningEffort,
|
|
225
|
+
modelLevel,
|
|
226
|
+
providerModule,
|
|
227
|
+
providerSettings,
|
|
228
|
+
levelOverrides,
|
|
229
|
+
}).modelSpec;
|
|
230
|
+
}
|
|
213
231
|
|
|
232
|
+
_resolveModelSelection({
|
|
233
|
+
explicitModelSpec,
|
|
234
|
+
model,
|
|
235
|
+
reasoningEffort,
|
|
236
|
+
modelLevel,
|
|
237
|
+
providerModule,
|
|
238
|
+
providerSettings,
|
|
239
|
+
levelOverrides,
|
|
240
|
+
}) {
|
|
214
241
|
if (model) {
|
|
215
242
|
providerModule.validateModelId(model);
|
|
216
|
-
return {
|
|
243
|
+
return {
|
|
244
|
+
source: 'direct',
|
|
245
|
+
modelSpec: { model, reasoningEffort },
|
|
246
|
+
};
|
|
217
247
|
}
|
|
218
248
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
249
|
+
if (explicitModelSpec?.model !== undefined) {
|
|
250
|
+
providerModule.validateModelId(explicitModelSpec.model);
|
|
251
|
+
return {
|
|
252
|
+
source: 'direct',
|
|
253
|
+
modelSpec: explicitModelSpec,
|
|
254
|
+
};
|
|
223
255
|
}
|
|
224
256
|
|
|
225
|
-
return
|
|
257
|
+
return {
|
|
258
|
+
source: 'provider-level',
|
|
259
|
+
modelSpec: this._resolveProviderLevelModelSpec({
|
|
260
|
+
explicitModelSpec,
|
|
261
|
+
modelLevel,
|
|
262
|
+
reasoningEffort,
|
|
263
|
+
providerModule,
|
|
264
|
+
providerSettings,
|
|
265
|
+
levelOverrides,
|
|
266
|
+
}),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
_resolveProviderLevelModelSpec({
|
|
271
|
+
explicitModelSpec,
|
|
272
|
+
modelLevel,
|
|
273
|
+
reasoningEffort,
|
|
274
|
+
providerModule,
|
|
275
|
+
providerSettings,
|
|
276
|
+
levelOverrides,
|
|
277
|
+
}) {
|
|
278
|
+
const level =
|
|
279
|
+
explicitModelSpec?.level ||
|
|
280
|
+
modelLevel ||
|
|
281
|
+
providerSettings.defaultLevel ||
|
|
282
|
+
providerModule.getDefaultLevel();
|
|
283
|
+
const resolvedModelSpec = providerModule.resolveModelSpec(level, levelOverrides);
|
|
284
|
+
const resolvedReasoningEffort = explicitModelSpec?.reasoningEffort || reasoningEffort;
|
|
285
|
+
|
|
286
|
+
return resolvedReasoningEffort
|
|
287
|
+
? { ...resolvedModelSpec, reasoningEffort: resolvedReasoningEffort }
|
|
288
|
+
: resolvedModelSpec;
|
|
226
289
|
}
|
|
227
290
|
|
|
228
291
|
_resolveOutputFormat({ outputFormat, jsonSchema, strictSchema }) {
|
|
@@ -232,16 +295,16 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
232
295
|
return jsonSchema && outputFormat === 'json' && !strictSchema ? 'stream-json' : outputFormat;
|
|
233
296
|
}
|
|
234
297
|
|
|
235
|
-
_buildRunArgs({
|
|
298
|
+
_buildRunArgs({
|
|
299
|
+
context,
|
|
300
|
+
providerName,
|
|
301
|
+
runOutputFormat,
|
|
302
|
+
resolvedModelSpec,
|
|
303
|
+
modelSpecSource = 'direct',
|
|
304
|
+
jsonSchema,
|
|
305
|
+
}) {
|
|
236
306
|
const args = ['task', 'run', '--output-format', runOutputFormat, '--provider', providerName];
|
|
237
|
-
|
|
238
|
-
if (resolvedModelSpec?.model) {
|
|
239
|
-
args.push('--model', resolvedModelSpec.model);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (resolvedModelSpec?.reasoningEffort) {
|
|
243
|
-
args.push('--reasoning-effort', resolvedModelSpec.reasoningEffort);
|
|
244
|
-
}
|
|
307
|
+
appendTaskRunModelArgs(args, resolvedModelSpec, modelSpecSource);
|
|
245
308
|
|
|
246
309
|
// Pass schema to CLI only when using json output (strictSchema=true or no conflict)
|
|
247
310
|
if (jsonSchema && runOutputFormat === 'json') {
|
|
@@ -542,20 +605,40 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
542
605
|
/**
|
|
543
606
|
* Run task in isolated Docker container
|
|
544
607
|
* @param {string} context
|
|
545
|
-
* @param {{agentId?: string, model?: string, outputFormat?: string, jsonSchema?: any, strictSchema?: boolean, isolation?: any}} options
|
|
608
|
+
* @param {{agentId?: string, provider?: string, model?: string, modelLevel?: string, modelSpec?: Object|null, reasoningEffort?: string, outputFormat?: string, jsonSchema?: any, strictSchema?: boolean, isolation?: any}} options
|
|
546
609
|
* @returns {Promise<{success: boolean, output: string, error: string|null}>}
|
|
547
610
|
*/
|
|
548
611
|
_runIsolated(context, options) {
|
|
612
|
+
rejectCallerSuppliedModelProvenance(options);
|
|
549
613
|
const {
|
|
550
614
|
agentId = 'unknown',
|
|
551
615
|
provider = 'claude',
|
|
552
|
-
|
|
616
|
+
model = null,
|
|
617
|
+
modelLevel = null,
|
|
618
|
+
modelSpec: explicitModelSpec = null,
|
|
619
|
+
reasoningEffort = null,
|
|
553
620
|
outputFormat = 'stream-json',
|
|
554
621
|
jsonSchema = null,
|
|
555
622
|
strictSchema = false,
|
|
556
623
|
isolation,
|
|
557
624
|
} = options;
|
|
558
625
|
const { manager, clusterId } = isolation;
|
|
626
|
+
const settings = loadSettings();
|
|
627
|
+
const { providerModule, providerSettings, levelOverrides } = this._getProviderContext(
|
|
628
|
+
provider,
|
|
629
|
+
settings
|
|
630
|
+
);
|
|
631
|
+
const modelSelection = this._resolveModelSelection({
|
|
632
|
+
explicitModelSpec,
|
|
633
|
+
model,
|
|
634
|
+
reasoningEffort,
|
|
635
|
+
modelLevel,
|
|
636
|
+
providerModule,
|
|
637
|
+
providerSettings,
|
|
638
|
+
levelOverrides,
|
|
639
|
+
});
|
|
640
|
+
const modelSpec = modelSelection.modelSpec;
|
|
641
|
+
const modelSpecSource = modelSelection.source;
|
|
559
642
|
|
|
560
643
|
this._log(`📦 [${agentId}]: Running task in isolated container...`);
|
|
561
644
|
|
|
@@ -565,7 +648,7 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
565
648
|
? 'stream-json'
|
|
566
649
|
: desiredOutputFormat;
|
|
567
650
|
|
|
568
|
-
|
|
651
|
+
let command = [
|
|
569
652
|
'zeroshot',
|
|
570
653
|
'task',
|
|
571
654
|
'run',
|
|
@@ -575,13 +658,7 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
575
658
|
provider,
|
|
576
659
|
];
|
|
577
660
|
|
|
578
|
-
|
|
579
|
-
command.push('--model', modelSpec.model);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
if (modelSpec?.reasoningEffort) {
|
|
583
|
-
command.push('--reasoning-effort', modelSpec.reasoningEffort);
|
|
584
|
-
}
|
|
661
|
+
appendTaskRunModelArgs(command, modelSpec, modelSpecSource);
|
|
585
662
|
|
|
586
663
|
if (jsonSchema && runOutputFormat === 'json') {
|
|
587
664
|
command.push('--json-schema', JSON.stringify(jsonSchema));
|
|
@@ -597,16 +674,23 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
597
674
|
}
|
|
598
675
|
|
|
599
676
|
command.push(finalContext);
|
|
677
|
+
command = wrapTaskRunWithIsolatedSettings(command, {
|
|
678
|
+
providerName: provider,
|
|
679
|
+
settings,
|
|
680
|
+
modelSpecSource,
|
|
681
|
+
modelSpec,
|
|
682
|
+
});
|
|
600
683
|
|
|
601
684
|
return new Promise((resolve, reject) => {
|
|
602
685
|
let output = '';
|
|
603
686
|
let resolved = false;
|
|
604
687
|
|
|
605
688
|
const proc = manager.spawnInContainer(clusterId, command, {
|
|
606
|
-
env:
|
|
607
|
-
provider === 'claude' && modelSpec?.model
|
|
689
|
+
env: {
|
|
690
|
+
...(provider === 'claude' && modelSpec?.model
|
|
608
691
|
? { ANTHROPIC_MODEL: modelSpec.model, ZEROSHOT_BLOCK_ASK_USER: '1' }
|
|
609
|
-
: {},
|
|
692
|
+
: {}),
|
|
693
|
+
},
|
|
610
694
|
});
|
|
611
695
|
|
|
612
696
|
proc.stdout.on('data', (/** @type {Buffer} */ data) => {
|
package/src/config-validator.js
CHANGED
|
@@ -2053,8 +2053,8 @@ function validateProviderSettings(provider, providerSettings) {
|
|
|
2053
2053
|
`Invalid model override (must be non-empty string) for provider "${provider}"`
|
|
2054
2054
|
);
|
|
2055
2055
|
}
|
|
2056
|
-
if (override?.model) {
|
|
2057
|
-
providerModule.
|
|
2056
|
+
if (override?.model || (provider === 'opencode' && override?.model === '')) {
|
|
2057
|
+
providerModule.resolveModelSpec(level, { [level]: override });
|
|
2058
2058
|
}
|
|
2059
2059
|
if (override?.reasoningEffort && !providerSupportsCapability(provider, 'reasoningEffort')) {
|
|
2060
2060
|
throw new Error(
|
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
|
|