@peakflo/agent-harness 0.2.17-rc.2 → 0.2.17-rc.3
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/dist/github-workspace.d.ts +3 -0
- package/dist/github-workspace.js +94 -22
- package/package.json +1 -1
|
@@ -41,6 +41,9 @@ export declare class GitHubWorkspaceProvider implements WorkspaceProvider {
|
|
|
41
41
|
/** Never fetch a different repository into a shared durable bare clone. */
|
|
42
42
|
private bareRepositoryPath;
|
|
43
43
|
private isUnusableCheckout;
|
|
44
|
+
private checkoutPath;
|
|
45
|
+
/** Locate only this checkout's registration; never prune sibling records. */
|
|
46
|
+
private worktreeRegistration;
|
|
44
47
|
/**
|
|
45
48
|
* A session owns its checkout, including branch changes and detached HEAD.
|
|
46
49
|
* Check its root and repository identity, never its branch or commit. Keep
|
package/dist/github-workspace.js
CHANGED
|
@@ -87,6 +87,8 @@ class GitHubWorkspaceProvider {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
this.gitEnvironment.GIT_TERMINAL_PROMPT = '0';
|
|
90
|
+
// Recovery classifies Git's stable diagnostics, never localized text.
|
|
91
|
+
this.gitEnvironment.LC_ALL = 'C';
|
|
90
92
|
this.rememberDirectUrls(options.directUrls ?? []);
|
|
91
93
|
}
|
|
92
94
|
rememberDirectUrls(urls) {
|
|
@@ -152,6 +154,41 @@ class GitHubWorkspaceProvider {
|
|
|
152
154
|
return (gitError.code === 128 &&
|
|
153
155
|
/not a git repository|invalid gitfile format|bad config line/i.test(gitError.stderr ?? ''));
|
|
154
156
|
}
|
|
157
|
+
async checkoutPath(path) {
|
|
158
|
+
// Resolve the parent, not the checkout entry: it may now be a symlink or
|
|
159
|
+
// be missing. Match the registered location without following its target.
|
|
160
|
+
return (0, node_path_1.join)(await (0, promises_1.realpath)((0, node_path_1.dirname)(path)), (0, node_path_1.basename)(path));
|
|
161
|
+
}
|
|
162
|
+
/** Locate only this checkout's registration; never prune sibling records. */
|
|
163
|
+
async worktreeRegistration(bare, worktree) {
|
|
164
|
+
const canonical = await this.checkoutPath(worktree);
|
|
165
|
+
const root = (0, node_path_1.join)(bare, 'worktrees');
|
|
166
|
+
const entries = await (0, promises_1.readdir)(root, { withFileTypes: true }).catch((error) => {
|
|
167
|
+
if (error.code === 'ENOENT')
|
|
168
|
+
return [];
|
|
169
|
+
throw error;
|
|
170
|
+
});
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
173
|
+
continue;
|
|
174
|
+
const admin = (0, node_path_1.join)(root, entry.name);
|
|
175
|
+
const target = await (0, promises_1.readFile)((0, node_path_1.join)(admin, 'gitdir'), 'utf8').catch((error) => {
|
|
176
|
+
if (error.code === 'ENOENT')
|
|
177
|
+
return '';
|
|
178
|
+
throw error;
|
|
179
|
+
});
|
|
180
|
+
if ((0, node_path_1.basename)(target.trim()) !== '.git')
|
|
181
|
+
continue;
|
|
182
|
+
const parent = await this.checkoutPath((0, node_path_1.dirname)(target.trim())).catch((error) => {
|
|
183
|
+
if (error.code === 'ENOENT')
|
|
184
|
+
return null;
|
|
185
|
+
throw error;
|
|
186
|
+
});
|
|
187
|
+
if (parent === canonical)
|
|
188
|
+
return admin;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
155
192
|
/**
|
|
156
193
|
* A session owns its checkout, including branch changes and detached HEAD.
|
|
157
194
|
* Check its root and repository identity, never its branch or commit. Keep
|
|
@@ -163,11 +200,10 @@ class GitHubWorkspaceProvider {
|
|
|
163
200
|
entry = await (0, promises_1.lstat)(worktree);
|
|
164
201
|
}
|
|
165
202
|
catch (error) {
|
|
166
|
-
if (error.code
|
|
167
|
-
|
|
168
|
-
throw error;
|
|
203
|
+
if (error.code !== 'ENOENT')
|
|
204
|
+
throw error;
|
|
169
205
|
}
|
|
170
|
-
if (entry
|
|
206
|
+
if (entry?.isDirectory() && !entry.isSymbolicLink()) {
|
|
171
207
|
try {
|
|
172
208
|
const [topLevel, origin, commonDir] = await Promise.all([
|
|
173
209
|
this.runGit(['rev-parse', '--show-toplevel'], { cwd: worktree }),
|
|
@@ -203,27 +239,56 @@ class GitHubWorkspaceProvider {
|
|
|
203
239
|
}
|
|
204
240
|
}
|
|
205
241
|
}
|
|
242
|
+
const registration = await this.worktreeRegistration(bare, worktree);
|
|
243
|
+
if (!entry && !registration)
|
|
244
|
+
return false;
|
|
206
245
|
const backupRoot = await (0, promises_1.mkdtemp)((0, node_path_1.join)(this.sessionPath(sessionId), `.preserved-${Date.now()}-`));
|
|
207
246
|
const backup = (0, node_path_1.join)(backupRoot, 'repository');
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
247
|
+
const repairRegistration = async () => {
|
|
248
|
+
try {
|
|
249
|
+
if (registration) {
|
|
250
|
+
// Keep the index, HEAD and reflog. Retain the original .git bytes too.
|
|
251
|
+
const gitFile = (0, node_path_1.join)(backup, '.git');
|
|
252
|
+
await (0, promises_1.rename)(gitFile, (0, node_path_1.join)(backupRoot, 'original-git-file')).catch((error) => {
|
|
253
|
+
if (error.code !== 'ENOENT')
|
|
254
|
+
throw error;
|
|
255
|
+
});
|
|
256
|
+
await (0, promises_1.writeFile)(gitFile, `gitdir: ${registration}\n`, {
|
|
257
|
+
mode: 0o600
|
|
258
|
+
});
|
|
259
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(registration, 'gitdir'), `${gitFile}\n`, {
|
|
260
|
+
mode: 0o600
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
await this.runGit(['worktree', 'repair', backup], { cwd: bare }).catch(() => undefined);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
throw new Error(`Repository files were kept at ${backup}. Git state repair failed. ` +
|
|
267
|
+
'Keep the saved folder and retry after the filesystem is available.');
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
if (!entry) {
|
|
271
|
+
// A removed checkout can still have staged work in its Git index.
|
|
272
|
+
await (0, promises_1.mkdir)(backup, { mode: 0o700 });
|
|
273
|
+
await repairRegistration();
|
|
274
|
+
}
|
|
275
|
+
else if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
211
276
|
await this.runGit(['worktree', 'move', worktree, backup], {
|
|
212
277
|
cwd: worktree
|
|
213
278
|
}).catch(async () => {
|
|
214
279
|
await (0, promises_1.rename)(worktree, backup);
|
|
215
|
-
await
|
|
280
|
+
await repairRegistration();
|
|
216
281
|
});
|
|
217
282
|
}
|
|
218
283
|
else {
|
|
219
|
-
await (0, promises_1.
|
|
284
|
+
await (0, promises_1.mkdir)(backup, { mode: 0o700 });
|
|
285
|
+
await (0, promises_1.rename)(worktree, (0, node_path_1.join)(backup, 'original-entry'));
|
|
286
|
+
await repairRegistration();
|
|
220
287
|
}
|
|
221
|
-
// A broken .git file cannot be repaired. Remove only obsolete Git
|
|
222
|
-
// registrations now, before reusing the vacated path; keep all saved files.
|
|
223
|
-
await this.runGit(['worktree', 'prune', '--expire=now'], { cwd: bare });
|
|
224
288
|
const notes = this.preparationNotes.get(sessionId) ?? [];
|
|
225
|
-
notes.push(`The previous repository folder could not be reused.
|
|
226
|
-
`were kept
|
|
289
|
+
notes.push(`The previous repository folder could not be reused. Its files and Git state ` +
|
|
290
|
+
`were kept under ${backupRoot}. Saved checkout: ${backup}. ` +
|
|
291
|
+
'A fresh checkout was prepared. ' +
|
|
227
292
|
'Review the saved folder before you remove it.');
|
|
228
293
|
this.preparationNotes.set(sessionId, notes);
|
|
229
294
|
return false;
|
|
@@ -439,6 +504,12 @@ class GitHubWorkspaceProvider {
|
|
|
439
504
|
let reusedWorktree = false;
|
|
440
505
|
for (const repository of request.repositories) {
|
|
441
506
|
const [owner, name] = repository.fullName.split('/');
|
|
507
|
+
const githubIdentity = githubRepositoryIdentity(repository.cloneUrl);
|
|
508
|
+
// GitHub's canonical clone endpoint is HTTPS owner/repo.git. Strip URL
|
|
509
|
+
// credentials and old loopback ports; the credential proxy supplies auth.
|
|
510
|
+
const transportUrl = githubIdentity
|
|
511
|
+
? `https://${githubIdentity}.git`
|
|
512
|
+
: repository.cloneUrl;
|
|
442
513
|
const bare = await this.bareRepositoryPath((0, node_path_1.join)(repositoriesRoot, `${owner}--${name}.git`), repository.cloneUrl);
|
|
443
514
|
const worktree = (0, node_path_1.join)(sessionRoot, name);
|
|
444
515
|
// The empty value resets every inherited/global helper. Without it, Git
|
|
@@ -456,7 +527,7 @@ class GitHubWorkspaceProvider {
|
|
|
456
527
|
});
|
|
457
528
|
}
|
|
458
529
|
catch {
|
|
459
|
-
await this.runGit([...gitArgs, 'clone', '--bare',
|
|
530
|
+
await this.runGit([...gitArgs, 'clone', '--bare', transportUrl, bare], {
|
|
460
531
|
timeout: 10 * 60_000,
|
|
461
532
|
maxBuffer: 10 * 1024 * 1024,
|
|
462
533
|
environment: gitNetworkEnvironment
|
|
@@ -469,16 +540,17 @@ class GitHubWorkspaceProvider {
|
|
|
469
540
|
}
|
|
470
541
|
// The identity check above permits old proxy/sentinel URL forms. Fetch
|
|
471
542
|
// through the current lease URL so a stale loopback port cannot fail first.
|
|
472
|
-
await this.runGit([
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
543
|
+
await this.runGit([
|
|
544
|
+
...gitArgs,
|
|
545
|
+
'fetch',
|
|
546
|
+
transportUrl,
|
|
547
|
+
'+refs/heads/*:refs/remotes/origin/*'
|
|
548
|
+
], {
|
|
476
549
|
cwd: bare,
|
|
477
550
|
timeout: 10 * 60_000,
|
|
478
551
|
maxBuffer: 10 * 1024 * 1024,
|
|
479
552
|
environment: gitNetworkEnvironment
|
|
480
553
|
});
|
|
481
|
-
await this.runGit(['worktree', 'prune'], { cwd: bare });
|
|
482
554
|
const branch = `agent/${request.sessionId}`;
|
|
483
555
|
const reuse = await this.canReuseWorktree(bare, worktree, repository.cloneUrl, request.sessionId);
|
|
484
556
|
if (!reuse) {
|
|
@@ -524,9 +596,9 @@ class GitHubWorkspaceProvider {
|
|
|
524
596
|
await this.runGit(['config', '--worktree', 'core.bare', 'false'], {
|
|
525
597
|
cwd: worktree
|
|
526
598
|
});
|
|
527
|
-
await this.runGit(['config', '--worktree', 'remote.origin.url',
|
|
599
|
+
await this.runGit(['config', '--worktree', 'remote.origin.url', transportUrl], { cwd: worktree });
|
|
528
600
|
const hooks = (0, node_path_1.join)(sessionRoot, '.github-hooks', name);
|
|
529
|
-
await (0, github_command_guards_1.installPushGuard)(hooks, request.sessionId, repository.defaultBranch,
|
|
601
|
+
await (0, github_command_guards_1.installPushGuard)(hooks, request.sessionId, repository.defaultBranch, transportUrl);
|
|
530
602
|
await this.runGit(['config', '--worktree', 'core.hooksPath', hooks], {
|
|
531
603
|
cwd: worktree
|
|
532
604
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peakflo/agent-harness",
|
|
3
|
-
"version": "0.2.17-rc.
|
|
3
|
+
"version": "0.2.17-rc.3",
|
|
4
4
|
"description": "Headless Peakflo coding-agent harness. One codebase, three hosting modes: a Daytona cloud sandbox, a customer VM, and the 20x desktop.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|