@peakflo/agent-harness 0.0.0-stage → 0.2.2-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -2
  3. package/dist/adapters/acp-adapter.d.ts +177 -0
  4. package/dist/adapters/acp-adapter.js +1732 -0
  5. package/dist/adapters/claude-code-adapter.d.ts +171 -0
  6. package/dist/adapters/claude-code-adapter.js +1918 -0
  7. package/dist/adapters/codex-adapter.d.ts +67 -0
  8. package/dist/adapters/codex-adapter.js +681 -0
  9. package/dist/adapters/codex-app-server-adapter.d.ts +105 -0
  10. package/dist/adapters/codex-app-server-adapter.js +1309 -0
  11. package/dist/adapters/coding-agent-adapter.d.ts +289 -0
  12. package/dist/adapters/coding-agent-adapter.js +31 -0
  13. package/dist/adapters/index.d.ts +17 -0
  14. package/dist/adapters/index.js +35 -0
  15. package/dist/adapters/model-gateway.d.ts +84 -0
  16. package/dist/adapters/model-gateway.js +270 -0
  17. package/dist/adapters/opencode-adapter.d.ts +403 -0
  18. package/dist/adapters/opencode-adapter.js +2710 -0
  19. package/dist/adapters/opencode-config.d.ts +41 -0
  20. package/dist/adapters/opencode-config.js +147 -0
  21. package/dist/adapters/opencode-server-policy.d.ts +82 -0
  22. package/dist/adapters/opencode-server-policy.js +148 -0
  23. package/dist/adapters/pi-adapter.d.ts +168 -0
  24. package/dist/adapters/pi-adapter.js +1468 -0
  25. package/dist/capabilities.d.ts +58 -0
  26. package/dist/capabilities.js +161 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +231 -0
  29. package/dist/contract.d.ts +273 -0
  30. package/dist/contract.js +357 -0
  31. package/dist/control-plane.d.ts +178 -0
  32. package/dist/control-plane.js +431 -0
  33. package/dist/credential-store.d.ts +110 -0
  34. package/dist/credential-store.js +181 -0
  35. package/dist/enrol/browser-enrol.d.ts +85 -0
  36. package/dist/enrol/browser-enrol.js +387 -0
  37. package/dist/github-command-guards.d.ts +20 -0
  38. package/dist/github-command-guards.js +330 -0
  39. package/dist/github-proxy-certificates.d.ts +25 -0
  40. package/dist/github-proxy-certificates.js +145 -0
  41. package/dist/github-proxy.d.ts +37 -0
  42. package/dist/github-proxy.js +476 -0
  43. package/dist/github-workspace.d.ts +55 -0
  44. package/dist/github-workspace.js +537 -0
  45. package/dist/harness.d.ts +333 -0
  46. package/dist/harness.js +1414 -0
  47. package/dist/health.d.ts +45 -0
  48. package/dist/health.js +116 -0
  49. package/dist/index.d.ts +53 -0
  50. package/dist/index.js +123 -0
  51. package/dist/model-probe.d.ts +96 -0
  52. package/dist/model-probe.js +195 -0
  53. package/dist/options.d.ts +15 -0
  54. package/dist/options.js +212 -0
  55. package/dist/ports-default.d.ts +212 -0
  56. package/dist/ports-default.js +333 -0
  57. package/dist/ports.d.ts +436 -0
  58. package/dist/ports.js +27 -0
  59. package/dist/runtime/attachments.d.ts +92 -0
  60. package/dist/runtime/attachments.js +377 -0
  61. package/dist/runtime/child-stream-guards.d.ts +45 -0
  62. package/dist/runtime/child-stream-guards.js +111 -0
  63. package/dist/runtime/conversation.d.ts +87 -0
  64. package/dist/runtime/conversation.js +156 -0
  65. package/dist/runtime/lease-renewal.d.ts +169 -0
  66. package/dist/runtime/lease-renewal.js +433 -0
  67. package/dist/runtime/mcp-auth-proxy.d.ts +82 -0
  68. package/dist/runtime/mcp-auth-proxy.js +300 -0
  69. package/dist/runtime/permission-mode.d.ts +116 -0
  70. package/dist/runtime/permission-mode.js +146 -0
  71. package/dist/runtime/projection.d.ts +71 -0
  72. package/dist/runtime/projection.js +211 -0
  73. package/dist/runtime/session-runner.d.ts +520 -0
  74. package/dist/runtime/session-runner.js +2122 -0
  75. package/dist/shared/reasoning-effort.d.ts +11 -0
  76. package/dist/shared/reasoning-effort.js +20 -0
  77. package/dist/store/conversation-store.d.ts +100 -0
  78. package/dist/store/conversation-store.js +305 -0
  79. package/dist/store/sqlite-store.d.ts +120 -0
  80. package/dist/store/sqlite-store.js +346 -0
  81. package/dist/types.d.ts +952 -0
  82. package/dist/types.js +55 -0
  83. package/dist/version.d.ts +32 -0
  84. package/dist/version.js +77 -0
  85. package/package.json +53 -4
@@ -0,0 +1,537 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GitHubWorkspaceProvider = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_crypto_1 = require("node:crypto");
6
+ const node_fs_1 = require("node:fs");
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_http_1 = require("node:http");
9
+ const node_path_1 = require("node:path");
10
+ const node_util_1 = require("node:util");
11
+ const github_command_guards_1 = require("./github-command-guards");
12
+ const github_proxy_1 = require("./github-proxy");
13
+ const github_proxy_certificates_1 = require("./github-proxy-certificates");
14
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
15
+ const DEFAULT_PROXY_PORT = 7780;
16
+ const LINK_SECRET_FILE = '.github-proxy-link-secret';
17
+ const WIP_PUSH_FAILED_FILE = '.worktree-wip-push-failed';
18
+ const WIP_COMMIT_MESSAGE = 'chore(agent): wip — uncommitted at turn end';
19
+ /** Bare clone plus one worktree per session, adapted from 20x. */
20
+ class GitHubWorkspaceProvider {
21
+ options;
22
+ sessions = new Map();
23
+ gitEnvironment;
24
+ server = null;
25
+ port = null;
26
+ serverStart = null;
27
+ linkSecret = null;
28
+ caCertificatePath = null;
29
+ restartNotedSessions = new Set();
30
+ restoredWorktreeNotes = new Map();
31
+ preparedWorktrees = new Map();
32
+ directHosts = new Set();
33
+ constructor(options) {
34
+ this.options = options;
35
+ // Capture PATH before harness.start prepends the agent-facing guard bin.
36
+ // These control-plane-owned clone/worktree commands use the original Git;
37
+ // only backend shells should traverse the defense-in-depth launcher.
38
+ this.gitEnvironment = { ...process.env };
39
+ for (const key of Object.keys(this.gitEnvironment)) {
40
+ if (key.startsWith('GIT_') || key.startsWith('GH_')) {
41
+ delete this.gitEnvironment[key];
42
+ }
43
+ }
44
+ this.gitEnvironment.GIT_TERMINAL_PROMPT = '0';
45
+ this.rememberDirectUrls(options.directUrls ?? []);
46
+ }
47
+ rememberDirectUrls(urls) {
48
+ for (const value of urls) {
49
+ if (!value)
50
+ continue;
51
+ try {
52
+ this.directHosts.add(new URL(value).hostname.toLowerCase());
53
+ }
54
+ catch {
55
+ /* The component that owns an invalid URL reports it. */
56
+ }
57
+ }
58
+ }
59
+ runGit(args, options = {}) {
60
+ const { environment, ...execOptions } = options;
61
+ return execFileAsync('git', args, {
62
+ ...execOptions,
63
+ env: { ...this.gitEnvironment, ...environment }
64
+ });
65
+ }
66
+ /**
67
+ * A harness crash must not erase or recreate a session checkout. Accept an
68
+ * existing path only when Git proves that it is the exact worktree this
69
+ * provider would have created: same canonical root, same bare repository,
70
+ * and same per-session branch. Anything else fails closed.
71
+ */
72
+ async canReuseWorktree(bare, worktree, branch) {
73
+ let entry;
74
+ try {
75
+ entry = await (0, promises_1.lstat)(worktree);
76
+ }
77
+ catch (error) {
78
+ if (error.code === 'ENOENT')
79
+ return false;
80
+ throw error;
81
+ }
82
+ if (!entry.isDirectory() || entry.isSymbolicLink()) {
83
+ throw new Error(`Existing repository workspace is not a directory: ${worktree}`);
84
+ }
85
+ try {
86
+ const [topLevel, commonDir, currentBranch] = await Promise.all([
87
+ this.runGit(['rev-parse', '--show-toplevel'], { cwd: worktree }),
88
+ this.runGit(['rev-parse', '--git-common-dir'], { cwd: worktree }),
89
+ this.runGit(['symbolic-ref', '--quiet', '--short', 'HEAD'], {
90
+ cwd: worktree
91
+ })
92
+ ]);
93
+ const [expectedWorktree, actualWorktree, expectedBare, actualBare] = await Promise.all([
94
+ (0, promises_1.realpath)(worktree),
95
+ (0, promises_1.realpath)((0, node_path_1.resolve)(worktree, topLevel.stdout.trim())),
96
+ (0, promises_1.realpath)(bare),
97
+ (0, promises_1.realpath)((0, node_path_1.resolve)(worktree, commonDir.stdout.trim()))
98
+ ]);
99
+ if (actualWorktree !== expectedWorktree ||
100
+ actualBare !== expectedBare ||
101
+ currentBranch.stdout.trim() !== branch) {
102
+ throw new Error('repository or branch does not match');
103
+ }
104
+ return true;
105
+ }
106
+ catch (error) {
107
+ throw new Error(`Existing repository workspace cannot be reused safely: ${worktree}: ${error instanceof Error ? error.message : String(error)}`);
108
+ }
109
+ }
110
+ async dirtyFileCount(worktree) {
111
+ const status = await this.runGit(['status', '--porcelain=v1', '-z'], {
112
+ cwd: worktree,
113
+ maxBuffer: 10 * 1024 * 1024
114
+ });
115
+ return status.stdout.split('\0').filter(Boolean).length;
116
+ }
117
+ async worktreesForSession(sessionId) {
118
+ const prepared = this.preparedWorktrees.get(sessionId);
119
+ if (prepared)
120
+ return prepared;
121
+ const root = this.sessionPath(sessionId);
122
+ let entries;
123
+ try {
124
+ entries = await (0, promises_1.readdir)(root, { withFileTypes: true });
125
+ }
126
+ catch (error) {
127
+ if (error.code === 'ENOENT')
128
+ return [];
129
+ throw error;
130
+ }
131
+ const worktrees = [];
132
+ try {
133
+ await this.runGit(['rev-parse', '--is-inside-work-tree'], { cwd: root });
134
+ worktrees.push(root);
135
+ }
136
+ catch {
137
+ /* Repository-mode session roots contain child worktrees instead. */
138
+ }
139
+ for (const entry of entries) {
140
+ if (!entry.isDirectory() || entry.isSymbolicLink())
141
+ continue;
142
+ const candidate = (0, node_path_1.join)(root, entry.name);
143
+ try {
144
+ await this.runGit(['rev-parse', '--is-inside-work-tree'], {
145
+ cwd: candidate
146
+ });
147
+ worktrees.push(candidate);
148
+ }
149
+ catch {
150
+ /* An ordinary session child directory is not a Git worktree. */
151
+ }
152
+ }
153
+ return worktrees;
154
+ }
155
+ failedPushMarker(sessionId) {
156
+ return (0, node_path_1.join)(this.sessionPath(sessionId), WIP_PUSH_FAILED_FILE);
157
+ }
158
+ async readFailedPushes(sessionId) {
159
+ try {
160
+ const value = JSON.parse(await (0, promises_1.readFile)(this.failedPushMarker(sessionId), 'utf8'));
161
+ return new Set(Array.isArray(value)
162
+ ? value.filter((entry) => typeof entry === 'string')
163
+ : []);
164
+ }
165
+ catch (error) {
166
+ if (error.code === 'ENOENT')
167
+ return new Set();
168
+ throw error;
169
+ }
170
+ }
171
+ async writeFailedPushes(sessionId, failures) {
172
+ const marker = this.failedPushMarker(sessionId);
173
+ if (failures.size === 0) {
174
+ await (0, promises_1.unlink)(marker).catch((error) => {
175
+ if (error.code !== 'ENOENT')
176
+ throw error;
177
+ });
178
+ return;
179
+ }
180
+ await (0, promises_1.writeFile)(marker, `${JSON.stringify([...failures].sort())}\n`, {
181
+ mode: 0o600
182
+ });
183
+ await (0, promises_1.chmod)(marker, 0o600);
184
+ }
185
+ setCredential(sessionId, credential) {
186
+ const existing = this.sessions.get(sessionId);
187
+ this.sessions.set(sessionId, {
188
+ sessionId,
189
+ sentinel: existing?.sentinel ?? `pfgh_${this.sentinelFor(sessionId)}`,
190
+ credential
191
+ });
192
+ }
193
+ loadOrCreateLinkSecret() {
194
+ (0, node_fs_1.mkdirSync)(this.options.root, { recursive: true, mode: 0o700 });
195
+ const path = (0, node_path_1.join)((0, node_path_1.resolve)(this.options.root), LINK_SECRET_FILE);
196
+ let descriptor = null;
197
+ try {
198
+ descriptor = (0, node_fs_1.openSync)(path, 'wx', 0o600);
199
+ (0, node_fs_1.writeFileSync)(descriptor, (0, node_crypto_1.randomBytes)(32));
200
+ }
201
+ catch (error) {
202
+ if (error.code !== 'EEXIST')
203
+ throw error;
204
+ }
205
+ finally {
206
+ if (descriptor !== null)
207
+ (0, node_fs_1.closeSync)(descriptor);
208
+ }
209
+ (0, node_fs_1.chmodSync)(path, 0o600);
210
+ const secret = (0, node_fs_1.readFileSync)(path);
211
+ if (secret.length !== 32) {
212
+ throw new Error(`Invalid GitHub proxy link secret at ${path}.`);
213
+ }
214
+ return secret;
215
+ }
216
+ sentinelFor(sessionId) {
217
+ this.linkSecret ??= this.loadOrCreateLinkSecret();
218
+ return (0, node_crypto_1.createHmac)('sha256', this.linkSecret)
219
+ .update(sessionId)
220
+ .digest('hex');
221
+ }
222
+ clearCredential(sessionId) {
223
+ this.sessions.delete(sessionId);
224
+ }
225
+ sessionPath(sessionId) {
226
+ if (!/^[A-Za-z0-9._-]+$/.test(sessionId) ||
227
+ ['.', '..'].includes(sessionId)) {
228
+ throw new Error(`Unusable session id for a workspace path: ${JSON.stringify(sessionId)}`);
229
+ }
230
+ const root = (0, node_path_1.resolve)(this.options.root);
231
+ const path = (0, node_path_1.resolve)((0, node_path_1.join)(root, sessionId));
232
+ if (!path.startsWith(root + '/'))
233
+ throw new Error('Workspace path escaped the root.');
234
+ return path;
235
+ }
236
+ async ensureServer() {
237
+ if (this.port !== null)
238
+ return this.port;
239
+ if (this.serverStart)
240
+ return this.serverStart;
241
+ this.serverStart = this.startServer();
242
+ try {
243
+ return await this.serverStart;
244
+ }
245
+ finally {
246
+ this.serverStart = null;
247
+ }
248
+ }
249
+ async startServer() {
250
+ const server = (this.options.createServer ?? node_http_1.createServer)((_request, response) => {
251
+ response.writeHead(400, { 'content-type': 'text/plain' });
252
+ response.end('This endpoint is an HTTPS CONNECT proxy.\n');
253
+ });
254
+ const certificates = await (0, github_proxy_certificates_1.createGitHubProxyCertificates)(this.options.root);
255
+ this.caCertificatePath = certificates.caCertificatePath;
256
+ new github_proxy_1.GitHubTlsProxy(server, {
257
+ certificates,
258
+ logger: this.options.logger,
259
+ isDirectHostAllowed: (hostname) => this.directHosts.has(hostname),
260
+ findSession: (sentinel) => [...this.sessions.values()].find((entry) => entry.sentinel === sentinel)
261
+ });
262
+ const listen = (port) => new Promise((resolvePromise, reject) => {
263
+ const onError = (error) => reject(error);
264
+ server.once('error', onError);
265
+ server.listen(port, '127.0.0.1', () => {
266
+ server.off('error', onError);
267
+ resolvePromise();
268
+ });
269
+ });
270
+ const preferredPort = this.options.port ?? DEFAULT_PROXY_PORT;
271
+ try {
272
+ await listen(preferredPort);
273
+ }
274
+ catch (error) {
275
+ if (preferredPort === 0 ||
276
+ error.code !== 'EADDRINUSE') {
277
+ throw error;
278
+ }
279
+ (this.options.logger ?? console).warn(`[GitHubWorkspace] GitHub proxy port ${preferredPort} is in use; ` +
280
+ `using a kernel-assigned loopback port instead: ${error instanceof Error ? error.message : String(error)}`);
281
+ await listen(0);
282
+ }
283
+ const address = server.address();
284
+ if (!address || typeof address === 'string') {
285
+ server.close();
286
+ throw new Error('GitHub proxy did not bind.');
287
+ }
288
+ this.server = server;
289
+ this.port = address.port;
290
+ return this.port;
291
+ }
292
+ async prepare(request) {
293
+ const sessionRoot = this.sessionPath(request.sessionId);
294
+ await (0, promises_1.mkdir)(sessionRoot, { recursive: true, mode: 0o700 });
295
+ await (0, promises_1.chmod)(sessionRoot, 0o700);
296
+ if (request.mode === 'scratch')
297
+ return sessionRoot;
298
+ if (request.refusalReason)
299
+ throw new Error(request.refusalReason);
300
+ const session = this.sessions.get(request.sessionId);
301
+ if (!session || !request.repositories?.length) {
302
+ throw new Error('GitHub repository workspace has no in-memory credential.');
303
+ }
304
+ await this.ensureServer();
305
+ const helper = '!gh auth git-credential';
306
+ const ghConfig = (0, node_path_1.join)(sessionRoot, '.gh');
307
+ await (0, promises_1.mkdir)(ghConfig, { recursive: true, mode: 0o700 });
308
+ await (0, promises_1.chmod)(ghConfig, 0o700);
309
+ const gitNetworkEnvironment = this.getEnvironment(request.sessionId);
310
+ const repositoriesRoot = (0, node_path_1.join)((0, node_path_1.resolve)(this.options.root), '.repositories');
311
+ await (0, promises_1.mkdir)(repositoriesRoot, { recursive: true, mode: 0o700 });
312
+ const worktrees = [];
313
+ let reusedWorktree = false;
314
+ for (const repository of request.repositories) {
315
+ const [owner, name] = repository.fullName.split('/');
316
+ const bare = (0, node_path_1.join)(repositoriesRoot, `${owner}--${name}.git`);
317
+ const worktree = (0, node_path_1.join)(sessionRoot, name);
318
+ // The empty value resets every inherited/global helper. Without it, Git
319
+ // can pass the installation token to a desktop keychain helper on the
320
+ // approval phase even though this helper itself never writes a token.
321
+ const gitArgs = [
322
+ '-c',
323
+ 'credential.helper=',
324
+ '-c',
325
+ `credential.helper=${helper}`
326
+ ];
327
+ try {
328
+ await this.runGit(['rev-parse', '--is-bare-repository'], {
329
+ cwd: bare
330
+ });
331
+ }
332
+ catch {
333
+ await this.runGit([...gitArgs, 'clone', '--bare', repository.cloneUrl, bare], {
334
+ timeout: 10 * 60_000,
335
+ maxBuffer: 10 * 1024 * 1024,
336
+ environment: gitNetworkEnvironment
337
+ });
338
+ await this.runGit([
339
+ 'config',
340
+ 'remote.origin.fetch',
341
+ '+refs/heads/*:refs/remotes/origin/*'
342
+ ], { cwd: bare });
343
+ }
344
+ await this.runGit([...gitArgs, 'fetch', 'origin', '+refs/heads/*:refs/remotes/origin/*'], {
345
+ cwd: bare,
346
+ timeout: 10 * 60_000,
347
+ maxBuffer: 10 * 1024 * 1024,
348
+ environment: gitNetworkEnvironment
349
+ });
350
+ await this.runGit(['worktree', 'prune'], { cwd: bare });
351
+ const branch = `agent/${request.sessionId}`;
352
+ const reuse = await this.canReuseWorktree(bare, worktree, branch);
353
+ if (!reuse) {
354
+ try {
355
+ await this.runGit(['worktree', 'add', worktree, branch], {
356
+ cwd: bare
357
+ });
358
+ }
359
+ catch {
360
+ await this.runGit([
361
+ 'worktree',
362
+ 'add',
363
+ '-b',
364
+ branch,
365
+ worktree,
366
+ `origin/${repository.defaultBranch}`
367
+ ], { cwd: bare });
368
+ }
369
+ }
370
+ else {
371
+ reusedWorktree = true;
372
+ }
373
+ worktrees.push(worktree);
374
+ await this.runGit(['config', '--unset-all', 'credential.helper'], {
375
+ cwd: worktree
376
+ }).catch(() => undefined);
377
+ await this.runGit(['config', '--add', 'credential.helper', ''], {
378
+ cwd: worktree
379
+ });
380
+ await this.runGit(['config', '--add', 'credential.helper', helper], {
381
+ cwd: worktree
382
+ });
383
+ // Hooks must be per worktree; a shared bare-repo hook would overwrite
384
+ // another session's allowed destination when sessions overlap.
385
+ await this.runGit(['config', 'extensions.worktreeConfig', 'true'], {
386
+ cwd: bare
387
+ });
388
+ await this.runGit(['config', '--worktree', 'core.bare', 'false'], {
389
+ cwd: worktree
390
+ });
391
+ const hooks = (0, node_path_1.join)(sessionRoot, '.github-hooks', name);
392
+ await (0, github_command_guards_1.installPushGuard)(hooks, request.sessionId, repository.defaultBranch, repository.cloneUrl);
393
+ await this.runGit(['config', '--worktree', 'core.hooksPath', hooks], {
394
+ cwd: worktree
395
+ });
396
+ request.onRepositoryStatus?.({
397
+ fullName: repository.fullName,
398
+ status: 'checkedOut'
399
+ });
400
+ }
401
+ this.preparedWorktrees.set(request.sessionId, worktrees);
402
+ if (reusedWorktree) {
403
+ let dirtyFiles = 0;
404
+ for (const worktree of worktrees) {
405
+ dirtyFiles += await this.dirtyFileCount(worktree);
406
+ }
407
+ this.restoredWorktreeNotes.set(request.sessionId, dirtyFiles === 0
408
+ ? 'Workspace restored; the repository worktree is clean.'
409
+ : `Workspace restored; ${dirtyFiles} ${dirtyFiles === 1 ? 'file' : 'files'} with uncommitted changes ${dirtyFiles === 1 ? 'is' : 'are'} still here.`);
410
+ }
411
+ const instructions = [
412
+ '# GitHub repository access',
413
+ '',
414
+ 'Each repository is a child directory. Commit and push only the agent/<session-id> branch.',
415
+ 'GitHub is reachable through a local proxy; use gh and git as usual. The token in GH_TOKEN is a session sentinel that works only inside this sandbox. Merges and pushes outside agent/<session-id> are refused.',
416
+ ''
417
+ ].join('\n');
418
+ await (0, promises_1.writeFile)((0, node_path_1.join)(sessionRoot, 'AGENTS.md'), instructions, {
419
+ mode: 0o600
420
+ });
421
+ return sessionRoot;
422
+ }
423
+ getEnvironment(sessionId, directUrls = []) {
424
+ const session = this.sessions.get(sessionId);
425
+ if (!session || this.port === null || !this.caCertificatePath)
426
+ return {};
427
+ this.rememberDirectUrls(directUrls);
428
+ const noProxy = new Set(['127.0.0.1', 'localhost', ...this.directHosts]);
429
+ const proxy = `http://127.0.0.1:${this.port}`;
430
+ const bypass = [...noProxy].join(',');
431
+ return {
432
+ // Git's libcurl on the Debian sandbox reads the lowercase form. With
433
+ // uppercase only, public clones appeared to work anonymously while a
434
+ // private clone sent the sentinel directly to GitHub and failed with
435
+ // "Invalid username or token". Keep both forms: Go-based gh uses the
436
+ // uppercase convention and native Git uses the lowercase convention.
437
+ HTTPS_PROXY: proxy,
438
+ https_proxy: proxy,
439
+ NO_PROXY: bypass,
440
+ no_proxy: bypass,
441
+ SSL_CERT_FILE: this.caCertificatePath,
442
+ GIT_SSL_CAINFO: this.caCertificatePath,
443
+ CURL_CA_BUNDLE: this.caCertificatePath,
444
+ NODE_EXTRA_CA_CERTS: this.caCertificatePath,
445
+ GH_TOKEN: session.sentinel,
446
+ GH_CONFIG_DIR: (0, node_path_1.join)(this.sessionPath(sessionId), '.gh')
447
+ };
448
+ }
449
+ takeTurnNote(sessionId) {
450
+ if (this.restartNotedSessions.has(sessionId))
451
+ return null;
452
+ if (!this.sessions.has(sessionId) || this.port === null)
453
+ return null;
454
+ this.restartNotedSessions.add(sessionId);
455
+ const restored = this.restoredWorktreeNotes.get(sessionId);
456
+ return [
457
+ 'Harness restarted; GitHub access is through the local proxy — use gh/git as usual.',
458
+ restored
459
+ ]
460
+ .filter(Boolean)
461
+ .join('\n');
462
+ }
463
+ async preserveAtTurnEnd(sessionId) {
464
+ const worktrees = await this.worktreesForSession(sessionId);
465
+ const failures = await this.readFailedPushes(sessionId);
466
+ const branch = `agent/${sessionId}`;
467
+ for (const worktree of worktrees) {
468
+ const name = worktree.slice(worktree.lastIndexOf('/') + 1);
469
+ try {
470
+ const dirtyFiles = await this.dirtyFileCount(worktree);
471
+ if (dirtyFiles > 0) {
472
+ await this.runGit(['add', '--all'], { cwd: worktree });
473
+ await this.runGit([
474
+ '-c',
475
+ 'user.name=Peakflo Agent',
476
+ '-c',
477
+ 'user.email=agent@peakflo.bot',
478
+ 'commit',
479
+ '-m',
480
+ WIP_COMMIT_MESSAGE
481
+ ], { cwd: worktree, maxBuffer: 10 * 1024 * 1024 });
482
+ }
483
+ if (dirtyFiles > 0 || failures.has(name)) {
484
+ await this.runGit(['push', 'origin', `HEAD:refs/heads/${branch}`], {
485
+ cwd: worktree,
486
+ timeout: 10 * 60_000,
487
+ maxBuffer: 10 * 1024 * 1024,
488
+ environment: this.getEnvironment(sessionId)
489
+ });
490
+ failures.delete(name);
491
+ }
492
+ }
493
+ catch (error) {
494
+ failures.add(name);
495
+ (this.options.logger ?? console).warn(`[GitHubWorkspace] worktree-wip-push-failed sessionId=${sessionId} ` +
496
+ `repository=${name} error=${error instanceof Error ? error.message : String(error)}`);
497
+ }
498
+ }
499
+ await this.writeFailedPushes(sessionId, failures);
500
+ return failures.size === 0;
501
+ }
502
+ async release(sessionId, options = {}) {
503
+ const failedPushes = await this.readFailedPushes(sessionId);
504
+ if (failedPushes.size > 0) {
505
+ (this.options.logger ?? console).warn(`[GitHubWorkspace] worktree-release-refused-unpushed-wip ` +
506
+ `sessionId=${sessionId} repositories=${[...failedPushes].join(',')}`);
507
+ return false;
508
+ }
509
+ const worktrees = await this.worktreesForSession(sessionId);
510
+ let dirtyFiles = 0;
511
+ for (const worktree of worktrees) {
512
+ dirtyFiles += await this.dirtyFileCount(worktree);
513
+ }
514
+ if (dirtyFiles > 0 && !options.force) {
515
+ (this.options.logger ?? console).warn(`[GitHubWorkspace] worktree-release-refused-dirty ` +
516
+ `sessionId=${sessionId} dirtyFiles=${dirtyFiles}`);
517
+ return false;
518
+ }
519
+ await (0, promises_1.rm)(this.sessionPath(sessionId), { recursive: true, force: true });
520
+ this.preparedWorktrees.delete(sessionId);
521
+ this.restoredWorktreeNotes.delete(sessionId);
522
+ this.restartNotedSessions.delete(sessionId);
523
+ this.clearCredential(sessionId);
524
+ return true;
525
+ }
526
+ async stop() {
527
+ if (!this.server)
528
+ return;
529
+ await new Promise((resolvePromise) => this.server?.close(() => resolvePromise()));
530
+ this.server = null;
531
+ this.port = null;
532
+ this.serverStart = null;
533
+ this.sessions.clear();
534
+ this.caCertificatePath = null;
535
+ }
536
+ }
537
+ exports.GitHubWorkspaceProvider = GitHubWorkspaceProvider;