@posthog/agent 2.0.0 → 2.0.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.
Files changed (131) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +221 -219
  3. package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +21 -0
  4. package/dist/adapters/claude/conversion/tool-use-to-acp.js +547 -0
  5. package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -0
  6. package/dist/adapters/claude/permissions/permission-options.d.ts +13 -0
  7. package/dist/adapters/claude/permissions/permission-options.js +117 -0
  8. package/dist/adapters/claude/permissions/permission-options.js.map +1 -0
  9. package/dist/adapters/claude/questions/utils.d.ts +132 -0
  10. package/dist/adapters/claude/questions/utils.js +63 -0
  11. package/dist/adapters/claude/questions/utils.js.map +1 -0
  12. package/dist/adapters/claude/tools.d.ts +18 -0
  13. package/dist/adapters/claude/tools.js +95 -0
  14. package/dist/adapters/claude/tools.js.map +1 -0
  15. package/dist/agent-DBQY1BfC.d.ts +123 -0
  16. package/dist/agent.d.ts +5 -0
  17. package/dist/agent.js +3656 -0
  18. package/dist/agent.js.map +1 -0
  19. package/dist/claude-cli/cli.js +3695 -2746
  20. package/dist/claude-cli/vendor/ripgrep/COPYING +3 -0
  21. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
  22. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/ripgrep.node +0 -0
  23. package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
  24. package/dist/claude-cli/vendor/ripgrep/arm64-linux/ripgrep.node +0 -0
  25. package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
  26. package/dist/claude-cli/vendor/ripgrep/x64-darwin/ripgrep.node +0 -0
  27. package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
  28. package/dist/claude-cli/vendor/ripgrep/x64-linux/ripgrep.node +0 -0
  29. package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
  30. package/dist/claude-cli/vendor/ripgrep/x64-win32/ripgrep.node +0 -0
  31. package/dist/gateway-models.d.ts +24 -0
  32. package/dist/gateway-models.js +93 -0
  33. package/dist/gateway-models.js.map +1 -0
  34. package/dist/index.d.ts +170 -1157
  35. package/dist/index.js +9373 -5135
  36. package/dist/index.js.map +1 -1
  37. package/dist/logger-DDBiMOOD.d.ts +24 -0
  38. package/dist/posthog-api.d.ts +40 -0
  39. package/dist/posthog-api.js +175 -0
  40. package/dist/posthog-api.js.map +1 -0
  41. package/dist/server/agent-server.d.ts +41 -0
  42. package/dist/server/agent-server.js +10503 -0
  43. package/dist/server/agent-server.js.map +1 -0
  44. package/dist/server/bin.d.ts +1 -0
  45. package/dist/server/bin.js +10558 -0
  46. package/dist/server/bin.js.map +1 -0
  47. package/dist/types.d.ts +129 -0
  48. package/dist/types.js +1 -0
  49. package/dist/types.js.map +1 -0
  50. package/package.json +65 -13
  51. package/src/acp-extensions.ts +98 -16
  52. package/src/adapters/acp-connection.ts +494 -0
  53. package/src/adapters/base-acp-agent.ts +150 -0
  54. package/src/adapters/claude/claude-agent.ts +596 -0
  55. package/src/adapters/claude/conversion/acp-to-sdk.ts +102 -0
  56. package/src/adapters/claude/conversion/sdk-to-acp.ts +571 -0
  57. package/src/adapters/claude/conversion/tool-use-to-acp.ts +618 -0
  58. package/src/adapters/claude/hooks.ts +64 -0
  59. package/src/adapters/claude/mcp/tool-metadata.ts +102 -0
  60. package/src/adapters/claude/permissions/permission-handlers.ts +433 -0
  61. package/src/adapters/claude/permissions/permission-options.ts +103 -0
  62. package/src/adapters/claude/plan/utils.ts +56 -0
  63. package/src/adapters/claude/questions/utils.ts +92 -0
  64. package/src/adapters/claude/session/commands.ts +38 -0
  65. package/src/adapters/claude/session/mcp-config.ts +37 -0
  66. package/src/adapters/claude/session/models.ts +12 -0
  67. package/src/adapters/claude/session/options.ts +236 -0
  68. package/src/adapters/claude/tool-meta.ts +143 -0
  69. package/src/adapters/claude/tools.ts +53 -688
  70. package/src/adapters/claude/types.ts +61 -0
  71. package/src/adapters/codex/spawn.ts +130 -0
  72. package/src/agent.ts +96 -587
  73. package/src/execution-mode.ts +43 -0
  74. package/src/gateway-models.ts +135 -0
  75. package/src/index.ts +79 -0
  76. package/src/otel-log-writer.test.ts +105 -0
  77. package/src/otel-log-writer.ts +94 -0
  78. package/src/posthog-api.ts +75 -235
  79. package/src/resume.ts +115 -0
  80. package/src/sagas/apply-snapshot-saga.test.ts +690 -0
  81. package/src/sagas/apply-snapshot-saga.ts +88 -0
  82. package/src/sagas/capture-tree-saga.test.ts +892 -0
  83. package/src/sagas/capture-tree-saga.ts +141 -0
  84. package/src/sagas/resume-saga.test.ts +558 -0
  85. package/src/sagas/resume-saga.ts +332 -0
  86. package/src/sagas/test-fixtures.ts +250 -0
  87. package/src/server/agent-server.test.ts +220 -0
  88. package/src/server/agent-server.ts +748 -0
  89. package/src/server/bin.ts +88 -0
  90. package/src/server/jwt.ts +65 -0
  91. package/src/server/schemas.ts +47 -0
  92. package/src/server/types.ts +13 -0
  93. package/src/server/utils/retry.test.ts +122 -0
  94. package/src/server/utils/retry.ts +61 -0
  95. package/src/server/utils/sse-parser.test.ts +93 -0
  96. package/src/server/utils/sse-parser.ts +46 -0
  97. package/src/session-log-writer.test.ts +140 -0
  98. package/src/session-log-writer.ts +137 -0
  99. package/src/test/assertions.ts +114 -0
  100. package/src/test/controllers/sse-controller.ts +107 -0
  101. package/src/test/fixtures/api.ts +111 -0
  102. package/src/test/fixtures/config.ts +33 -0
  103. package/src/test/fixtures/notifications.ts +92 -0
  104. package/src/test/mocks/claude-sdk.ts +251 -0
  105. package/src/test/mocks/msw-handlers.ts +48 -0
  106. package/src/test/setup.ts +114 -0
  107. package/src/test/wait.ts +41 -0
  108. package/src/tree-tracker.ts +173 -0
  109. package/src/types.ts +54 -137
  110. package/src/utils/acp-content.ts +58 -0
  111. package/src/utils/async-mutex.test.ts +104 -0
  112. package/src/utils/async-mutex.ts +31 -0
  113. package/src/utils/common.ts +15 -0
  114. package/src/utils/gateway.ts +9 -6
  115. package/src/utils/logger.ts +0 -30
  116. package/src/utils/streams.ts +220 -0
  117. package/CLAUDE.md +0 -331
  118. package/src/adapters/claude/claude.ts +0 -1947
  119. package/src/adapters/claude/mcp-server.ts +0 -810
  120. package/src/adapters/claude/utils.ts +0 -267
  121. package/src/adapters/connection.ts +0 -95
  122. package/src/file-manager.ts +0 -273
  123. package/src/git-manager.ts +0 -577
  124. package/src/schemas.ts +0 -241
  125. package/src/session-store.ts +0 -259
  126. package/src/task-manager.ts +0 -163
  127. package/src/todo-manager.ts +0 -180
  128. package/src/tools/registry.ts +0 -134
  129. package/src/tools/types.ts +0 -133
  130. package/src/utils/tapped-stream.ts +0 -60
  131. package/src/worktree-manager.ts +0 -974
@@ -1,577 +0,0 @@
1
- import { exec } from "node:child_process";
2
- import { promisify } from "node:util";
3
- import { Logger } from "./utils/logger.js";
4
-
5
- const execAsync = promisify(exec);
6
-
7
- export interface GitConfig {
8
- repositoryPath: string;
9
- logger?: Logger;
10
- }
11
-
12
- export interface BranchInfo {
13
- name: string;
14
- exists: boolean;
15
- isCurrentBranch: boolean;
16
- }
17
-
18
- export class GitManager {
19
- private repositoryPath: string;
20
- private logger: Logger;
21
-
22
- constructor(config: GitConfig) {
23
- this.repositoryPath = config.repositoryPath;
24
- this.logger =
25
- config.logger || new Logger({ debug: false, prefix: "[GitManager]" });
26
- }
27
-
28
- private escapeShellArg(str: string): string {
29
- return str
30
- .replace(/\\/g, "\\\\")
31
- .replace(/"/g, '\\"')
32
- .replace(/`/g, "\\`")
33
- .replace(/\$/g, "\\$");
34
- }
35
-
36
- private async runGitCommand(command: string): Promise<string> {
37
- try {
38
- const { stdout } = await execAsync(
39
- `cd "${this.repositoryPath}" && git ${command}`,
40
- );
41
- return stdout.trim();
42
- } catch (error) {
43
- throw new Error(`Git command failed: ${command}\n${error}`);
44
- }
45
- }
46
-
47
- private async runCommand(command: string): Promise<string> {
48
- try {
49
- const { stdout } = await execAsync(
50
- `cd "${this.repositoryPath}" && ${command}`,
51
- );
52
- return stdout.trim();
53
- } catch (error) {
54
- throw new Error(`Command failed: ${command}\n${error}`);
55
- }
56
- }
57
-
58
- async isGitRepository(): Promise<boolean> {
59
- try {
60
- await this.runGitCommand("rev-parse --git-dir");
61
- return true;
62
- } catch {
63
- return false;
64
- }
65
- }
66
-
67
- async getCurrentBranch(): Promise<string> {
68
- return await this.runGitCommand("branch --show-current");
69
- }
70
-
71
- async getDefaultBranch(): Promise<string> {
72
- try {
73
- // Try to get the default branch from remote
74
- const remoteBranch = await this.runGitCommand(
75
- "symbolic-ref refs/remotes/origin/HEAD",
76
- );
77
- return remoteBranch.replace("refs/remotes/origin/", "");
78
- } catch {
79
- // Fallback: check if main exists, otherwise use master
80
- if (await this.branchExists("main")) {
81
- return "main";
82
- } else if (await this.branchExists("master")) {
83
- return "master";
84
- } else {
85
- throw new Error(
86
- "Cannot determine default branch. No main or master branch found.",
87
- );
88
- }
89
- }
90
- }
91
-
92
- async branchExists(branchName: string): Promise<boolean> {
93
- try {
94
- await this.runGitCommand(`rev-parse --verify ${branchName}`);
95
- return true;
96
- } catch {
97
- return false;
98
- }
99
- }
100
-
101
- async createBranch(branchName: string, baseBranch?: string): Promise<void> {
102
- const base = baseBranch || (await this.getCurrentBranch());
103
- await this.runGitCommand(`checkout -b ${branchName} ${base}`);
104
- }
105
-
106
- async switchToBranch(branchName: string): Promise<void> {
107
- await this.runGitCommand(`checkout ${branchName}`);
108
- }
109
-
110
- async resetToDefaultBranchIfNeeded(): Promise<boolean> {
111
- const currentBranch = await this.getCurrentBranch();
112
- const defaultBranch = await this.getDefaultBranch();
113
-
114
- if (currentBranch === defaultBranch) {
115
- this.logger.debug("Already on default branch", { branch: defaultBranch });
116
- return true;
117
- }
118
-
119
- if (await this.hasChanges()) {
120
- this.logger.warn("Skipping branch reset - uncommitted changes present", {
121
- currentBranch,
122
- defaultBranch,
123
- });
124
- return false;
125
- }
126
-
127
- await this.switchToBranch(defaultBranch);
128
- this.logger.info("Reset to default branch", {
129
- from: currentBranch,
130
- to: defaultBranch,
131
- });
132
- return true;
133
- }
134
-
135
- async createOrSwitchToBranch(
136
- branchName: string,
137
- baseBranch?: string,
138
- ): Promise<void> {
139
- await this.ensureCleanWorkingDirectory("switching branches");
140
-
141
- const exists = await this.branchExists(branchName);
142
- if (exists) {
143
- await this.switchToBranch(branchName);
144
- } else {
145
- await this.createBranch(branchName, baseBranch);
146
- }
147
- }
148
-
149
- async addFiles(paths: string[]): Promise<void> {
150
- const pathList = paths.map((p) => `"${this.escapeShellArg(p)}"`).join(" ");
151
- await this.runGitCommand(`add ${pathList}`);
152
- }
153
-
154
- async addAllPostHogFiles(): Promise<void> {
155
- try {
156
- // Use -A flag to add all changes (including new files) and ignore errors if directory is empty
157
- await this.runGitCommand("add -A .posthog/");
158
- } catch (error) {
159
- // If the directory doesn't exist or has no files, that's fine - just log and continue
160
- this.logger.debug("No PostHog files to add", { error });
161
- }
162
- }
163
-
164
- async commitChanges(
165
- message: string,
166
- options?: {
167
- allowEmpty?: boolean;
168
- },
169
- ): Promise<string> {
170
- const command = this.buildCommitCommand(message, options);
171
- return await this.runGitCommand(command);
172
- }
173
-
174
- async hasChanges(): Promise<boolean> {
175
- try {
176
- const status = await this.runGitCommand("status --porcelain");
177
- if (!status || status.trim().length === 0) {
178
- return false;
179
- }
180
-
181
- const lines = status.split("\n").filter((line) => {
182
- const trimmed = line.trim();
183
- return trimmed.length > 0 && !trimmed.includes(".posthog/");
184
- });
185
-
186
- return lines.length > 0;
187
- } catch {
188
- return false;
189
- }
190
- }
191
-
192
- async hasStagedChanges(): Promise<boolean> {
193
- try {
194
- const status = await this.runGitCommand("diff --cached --name-only");
195
- return status.length > 0;
196
- } catch {
197
- return false;
198
- }
199
- }
200
-
201
- // Helper: Centralized safety check for uncommitted changes
202
- private async ensureCleanWorkingDirectory(operation: string): Promise<void> {
203
- if (await this.hasChanges()) {
204
- throw new Error(
205
- `Uncommitted changes detected. Please commit or stash changes before ${operation}.`,
206
- );
207
- }
208
- }
209
-
210
- private async generateUniqueBranchName(baseName: string): Promise<string> {
211
- if (!(await this.branchExists(baseName))) {
212
- return baseName;
213
- }
214
-
215
- let counter = 1;
216
- let uniqueName = `${baseName}-${counter}`;
217
- while (await this.branchExists(uniqueName)) {
218
- counter++;
219
- uniqueName = `${baseName}-${counter}`;
220
- }
221
- return uniqueName;
222
- }
223
-
224
- private async ensureOnDefaultBranch(): Promise<string> {
225
- const defaultBranch = await this.getDefaultBranch();
226
- const currentBranch = await this.getCurrentBranch();
227
-
228
- if (currentBranch !== defaultBranch) {
229
- await this.ensureCleanWorkingDirectory("switching to default branch");
230
- await this.switchToBranch(defaultBranch);
231
- }
232
-
233
- return defaultBranch;
234
- }
235
-
236
- private buildCommitCommand(
237
- message: string,
238
- options?: {
239
- allowEmpty?: boolean;
240
- },
241
- ): string {
242
- let command = `commit -m "${this.escapeShellArg(message)}"`;
243
-
244
- if (options?.allowEmpty) {
245
- command += " --allow-empty";
246
- }
247
-
248
- return command;
249
- }
250
-
251
- async getRemoteUrl(): Promise<string | null> {
252
- try {
253
- return await this.runGitCommand("remote get-url origin");
254
- } catch {
255
- return null;
256
- }
257
- }
258
-
259
- async pushBranch(branchName: string, force: boolean = false): Promise<void> {
260
- const forceFlag = force ? "--force" : "";
261
- await this.runGitCommand(`push ${forceFlag} -u origin ${branchName}`);
262
- }
263
-
264
- /**
265
- * Tracks whether commits were made during an operation by comparing HEAD SHA
266
- * before and after. Returns an object with methods to finalize the operation.
267
- *
268
- * Usage:
269
- * const tracker = await gitManager.trackCommitsDuring();
270
- * // ... do work that might create commits ...
271
- * const result = await tracker.finalize({ commitMessage: 'fallback message', push: true });
272
- */
273
- async trackCommitsDuring(): Promise<{
274
- finalize: (options: {
275
- commitMessage: string;
276
- push?: boolean;
277
- }) => Promise<{ commitCreated: boolean; pushedBranch: boolean }>;
278
- }> {
279
- const initialSha = await this.getCommitSha("HEAD");
280
-
281
- return {
282
- finalize: async (options) => {
283
- const currentSha = await this.getCommitSha("HEAD");
284
- const externalCommitsCreated = initialSha !== currentSha;
285
- const hasUncommittedChanges = await this.hasChanges();
286
-
287
- // If no commits and no changes, nothing to do
288
- if (!externalCommitsCreated && !hasUncommittedChanges) {
289
- return { commitCreated: false, pushedBranch: false };
290
- }
291
-
292
- let commitCreated = externalCommitsCreated;
293
-
294
- // Commit any remaining uncommitted changes
295
- if (hasUncommittedChanges) {
296
- await this.runGitCommand("add .");
297
- const hasStagedChanges = await this.hasStagedChanges();
298
-
299
- if (hasStagedChanges) {
300
- await this.commitChanges(options.commitMessage);
301
- commitCreated = true;
302
- }
303
- }
304
-
305
- // Push if requested and commits were made
306
- let pushedBranch = false;
307
- if (options.push && commitCreated) {
308
- const currentBranch = await this.getCurrentBranch();
309
- await this.pushBranch(currentBranch);
310
- pushedBranch = true;
311
- this.logger.info("Pushed branch after operation", {
312
- branch: currentBranch,
313
- });
314
- }
315
-
316
- return { commitCreated, pushedBranch };
317
- },
318
- };
319
- }
320
-
321
- async createTaskBranch(taskSlug: string): Promise<string> {
322
- const branchName = `posthog/task-${taskSlug}`;
323
-
324
- // Ensure we're on default branch before creating task branch
325
- const defaultBranch = await this.ensureOnDefaultBranch();
326
-
327
- this.logger.info("Creating task branch from default branch", {
328
- branchName,
329
- taskSlug,
330
- baseBranch: defaultBranch,
331
- });
332
-
333
- await this.createOrSwitchToBranch(branchName, defaultBranch);
334
-
335
- return branchName;
336
- }
337
-
338
- async createTaskPlanningBranch(
339
- taskId: string,
340
- baseBranch?: string,
341
- ): Promise<string> {
342
- const baseName = `posthog/task-${taskId}-planning`;
343
- const branchName = await this.generateUniqueBranchName(baseName);
344
-
345
- this.logger.debug("Creating unique planning branch", {
346
- branchName,
347
- taskId,
348
- });
349
-
350
- const base = baseBranch || (await this.ensureOnDefaultBranch());
351
- await this.createBranch(branchName, base);
352
-
353
- return branchName;
354
- }
355
-
356
- async createTaskImplementationBranch(
357
- taskId: string,
358
- planningBranchName?: string,
359
- ): Promise<string> {
360
- const baseName = `posthog/task-${taskId}-implementation`;
361
- const branchName = await this.generateUniqueBranchName(baseName);
362
-
363
- this.logger.debug("Creating unique implementation branch", {
364
- branchName,
365
- taskId,
366
- currentBranch: await this.getCurrentBranch(),
367
- });
368
-
369
- // Determine base branch: explicit param > current planning branch > default
370
- let baseBranch = planningBranchName;
371
-
372
- if (!baseBranch) {
373
- const currentBranch = await this.getCurrentBranch();
374
- if (currentBranch.includes("-planning")) {
375
- baseBranch = currentBranch;
376
- this.logger.debug("Using current planning branch", { baseBranch });
377
- } else {
378
- baseBranch = await this.ensureOnDefaultBranch();
379
- this.logger.debug("Using default branch", { baseBranch });
380
- }
381
- }
382
-
383
- this.logger.debug("Creating implementation branch from base", {
384
- baseBranch,
385
- branchName,
386
- });
387
- await this.createBranch(branchName, baseBranch);
388
-
389
- this.logger.info("Implementation branch created", {
390
- branchName,
391
- currentBranch: await this.getCurrentBranch(),
392
- });
393
-
394
- return branchName;
395
- }
396
-
397
- async commitPlan(taskId: string, taskTitle: string): Promise<string> {
398
- const currentBranch = await this.getCurrentBranch();
399
- this.logger.debug("Committing plan", { taskId, currentBranch });
400
-
401
- await this.addAllPostHogFiles();
402
-
403
- const hasChanges = await this.hasStagedChanges();
404
- this.logger.debug("Checking for staged changes", { hasChanges });
405
-
406
- if (!hasChanges) {
407
- this.logger.info("No plan changes to commit", { taskId });
408
- return "No changes to commit";
409
- }
410
-
411
- const message = `📋 Add plan for task: ${taskTitle}
412
-
413
- Task ID: ${taskId}
414
-
415
- This commit contains the implementation plan and supporting documentation
416
- for the task. Review the plan before proceeding with implementation.`;
417
-
418
- const result = await this.commitChanges(message);
419
- this.logger.info("Plan committed", { taskId, taskTitle });
420
- return result;
421
- }
422
-
423
- async commitImplementation(
424
- taskId: string,
425
- taskTitle: string,
426
- planSummary?: string,
427
- ): Promise<string> {
428
- await this.runGitCommand("add .");
429
-
430
- const hasChanges = await this.hasStagedChanges();
431
- if (!hasChanges) {
432
- this.logger.warn("No implementation changes to commit", { taskId });
433
- return "No changes to commit";
434
- }
435
-
436
- let message = `✨ Implement task: ${taskTitle}
437
-
438
- Task ID: ${taskId}`;
439
-
440
- if (planSummary) {
441
- message += `\n\nPlan Summary:\n${planSummary}`;
442
- }
443
-
444
- message += `\n\nThis commit implements the changes described in the task plan.`;
445
-
446
- const result = await this.commitChanges(message);
447
- this.logger.info("Implementation committed", { taskId, taskTitle });
448
- return result;
449
- }
450
-
451
- async deleteBranch(
452
- branchName: string,
453
- force: boolean = false,
454
- ): Promise<void> {
455
- const forceFlag = force ? "-D" : "-d";
456
- await this.runGitCommand(`branch ${forceFlag} ${branchName}`);
457
- }
458
-
459
- async deleteRemoteBranch(branchName: string): Promise<void> {
460
- await this.runGitCommand(`push origin --delete ${branchName}`);
461
- }
462
-
463
- async getBranchInfo(branchName: string): Promise<BranchInfo> {
464
- const exists = await this.branchExists(branchName);
465
- const currentBranch = await this.getCurrentBranch();
466
-
467
- return {
468
- name: branchName,
469
- exists,
470
- isCurrentBranch: branchName === currentBranch,
471
- };
472
- }
473
-
474
- async getCommitSha(ref: string = "HEAD"): Promise<string> {
475
- return await this.runGitCommand(`rev-parse ${ref}`);
476
- }
477
-
478
- async getCommitMessage(ref: string = "HEAD"): Promise<string> {
479
- return await this.runGitCommand(`log -1 --pretty=%B ${ref}`);
480
- }
481
-
482
- async createPullRequest(
483
- branchName: string,
484
- title: string,
485
- body: string,
486
- baseBranch?: string,
487
- ): Promise<string> {
488
- const currentBranch = await this.getCurrentBranch();
489
- if (currentBranch !== branchName) {
490
- await this.ensureCleanWorkingDirectory("creating PR");
491
- await this.switchToBranch(branchName);
492
- }
493
-
494
- await this.pushBranch(branchName);
495
-
496
- let command = `gh pr create --title "${this.escapeShellArg(title)}" --body "${this.escapeShellArg(body)}"`;
497
-
498
- if (baseBranch) {
499
- command += ` --base ${baseBranch}`;
500
- }
501
-
502
- try {
503
- const prUrl = await this.runCommand(command);
504
- return prUrl.trim();
505
- } catch (error) {
506
- throw new Error(`Failed to create PR: ${error}`);
507
- }
508
- }
509
-
510
- async getTaskBranch(taskSlug: string): Promise<string | null> {
511
- try {
512
- // Get all branches matching the task slug pattern
513
- const branches = await this.runGitCommand("branch --list --all");
514
- const branchPattern = `posthog/task-${taskSlug}`;
515
-
516
- // Look for exact match or with counter suffix
517
- const lines = branches
518
- .split("\n")
519
- .map((l) => l.trim().replace(/^\*\s+/, ""));
520
- for (const line of lines) {
521
- const cleanBranch = line.replace("remotes/origin/", "");
522
- if (cleanBranch.startsWith(branchPattern)) {
523
- return cleanBranch;
524
- }
525
- }
526
-
527
- return null;
528
- } catch (error) {
529
- this.logger.debug("Failed to get task branch", { taskSlug, error });
530
- return null;
531
- }
532
- }
533
-
534
- async commitAndPush(
535
- message: string,
536
- options?: { allowEmpty?: boolean },
537
- ): Promise<void> {
538
- const hasChanges = await this.hasStagedChanges();
539
-
540
- if (!hasChanges && !options?.allowEmpty) {
541
- this.logger.debug("No changes to commit, skipping");
542
- return;
543
- }
544
-
545
- const command = this.buildCommitCommand(message, options);
546
- await this.runGitCommand(command);
547
-
548
- // Push to origin
549
- const currentBranch = await this.getCurrentBranch();
550
- await this.pushBranch(currentBranch);
551
-
552
- this.logger.info("Committed and pushed changes", {
553
- branch: currentBranch,
554
- message,
555
- });
556
- }
557
-
558
- async isWorktree(): Promise<boolean> {
559
- try {
560
- // In a worktree, .git is a file pointing to the main repo's .git/worktrees/{name}
561
- // In a normal repo, .git is a directory
562
- const result = await this.runGitCommand(
563
- "rev-parse --git-common-dir --git-dir",
564
- );
565
- const lines = result.split("\n");
566
- if (lines.length >= 2) {
567
- const commonDir = lines[0].trim();
568
- const gitDir = lines[1].trim();
569
- // If they're different, we're in a worktree
570
- return commonDir !== gitDir;
571
- }
572
- return false;
573
- } catch {
574
- return false;
575
- }
576
- }
577
- }