@posthog/agent 2.3.351 → 2.3.354

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.
@@ -8688,6 +8688,32 @@ async function getCurrentBranch(baseDir, options) {
8688
8688
  return branch === "HEAD" ? null : branch;
8689
8689
  }, { signal: options?.abortSignal });
8690
8690
  }
8691
+ async function listWorktrees(baseDir, options) {
8692
+ const manager = getGitOperationManager();
8693
+ return manager.executeRead(baseDir, async (git) => {
8694
+ const output = await git.raw(["worktree", "list", "--porcelain"]);
8695
+ const worktrees = [];
8696
+ let current2 = {};
8697
+ for (const line of output.split("\n")) {
8698
+ if (line.startsWith("worktree ")) {
8699
+ if (current2.path) {
8700
+ worktrees.push(current2);
8701
+ }
8702
+ current2 = { path: line.slice(9), branch: null };
8703
+ } else if (line.startsWith("HEAD ")) {
8704
+ current2.head = line.slice(5);
8705
+ } else if (line.startsWith("branch ")) {
8706
+ current2.branch = line.slice(7).replace("refs/heads/", "");
8707
+ } else if (line === "detached") {
8708
+ current2.branch = null;
8709
+ }
8710
+ }
8711
+ if (current2.path) {
8712
+ worktrees.push(current2);
8713
+ }
8714
+ return worktrees;
8715
+ }, { signal: options?.abortSignal });
8716
+ }
8691
8717
  async function getHeadSha(baseDir, options) {
8692
8718
  const manager = getGitOperationManager();
8693
8719
  return manager.executeRead(baseDir, (git) => git.revparse(["HEAD"]), {
@@ -8697,11 +8723,12 @@ async function getHeadSha(baseDir, options) {
8697
8723
 
8698
8724
  // src/server/agent-server.ts
8699
8725
  var import_hono = require("hono");
8726
+ var import_zod3 = require("zod");
8700
8727
 
8701
8728
  // package.json
8702
8729
  var package_default = {
8703
8730
  name: "@posthog/agent",
8704
- version: "2.3.351",
8731
+ version: "2.3.354",
8705
8732
  repository: "https://github.com/PostHog/code",
8706
8733
  description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
8707
8734
  exports: {
@@ -13857,6 +13884,17 @@ async function handleSystemMessage(message, context) {
13857
13884
  break;
13858
13885
  }
13859
13886
  }
13887
+ function classifyAgentError(result) {
13888
+ if (!result) return "agent_error";
13889
+ const text2 = result.trim();
13890
+ if (/API Error:\s*terminated\b/i.test(text2)) {
13891
+ return "upstream_stream_terminated";
13892
+ }
13893
+ if (/API Error:\s*Connection error\b/i.test(text2)) {
13894
+ return "upstream_connection_error";
13895
+ }
13896
+ return "agent_error";
13897
+ }
13860
13898
  function handleResultMessage(message) {
13861
13899
  const usage = extractUsageFromResult(message);
13862
13900
  switch (message.subtype) {
@@ -13872,9 +13910,13 @@ function handleResultMessage(message) {
13872
13910
  return { shouldStop: true, stopReason: "max_tokens", usage };
13873
13911
  }
13874
13912
  if (message.is_error) {
13913
+ const classification = classifyAgentError(message.result);
13875
13914
  return {
13876
13915
  shouldStop: true,
13877
- error: import_sdk.RequestError.internalError(void 0, message.result),
13916
+ error: import_sdk.RequestError.internalError(
13917
+ { classification, result: message.result },
13918
+ message.result
13919
+ ),
13878
13920
  usage
13879
13921
  };
13880
13922
  }
@@ -14231,16 +14273,16 @@ function permissionOptions(allowAlwaysLabel) {
14231
14273
  }
14232
14274
  ];
14233
14275
  }
14234
- function buildPermissionOptions(toolName, toolInput, cwd, suggestions) {
14276
+ function buildPermissionOptions(toolName, toolInput, repoRoot, suggestions) {
14235
14277
  if (BASH_TOOLS.has(toolName)) {
14236
14278
  const rawRuleContent = suggestions?.flatMap((s) => "rules" in s ? s.rules : []).find((r) => r.toolName === "Bash" && r.ruleContent)?.ruleContent;
14237
14279
  const ruleContent = rawRuleContent?.replace(/:?\*$/, "");
14238
14280
  const command = toolInput?.command;
14239
14281
  const cmdName = command?.split(/\s+/)[0] ?? "this command";
14240
- const cwdLabel = cwd ? ` in ${cwd}` : "";
14282
+ const scopeLabel = repoRoot ? ` in ${repoRoot}` : "";
14241
14283
  const label = ruleContent ?? `\`${cmdName}\` commands`;
14242
14284
  return permissionOptions(
14243
- `Yes, and don't ask again for ${label}${cwdLabel}`
14285
+ `Yes, and don't ask again for ${label}${scopeLabel}`
14244
14286
  );
14245
14287
  }
14246
14288
  if (toolName === "BashOutput") {
@@ -14526,7 +14568,7 @@ async function handleDefaultPermissionFlow(context) {
14526
14568
  const options = buildPermissionOptions(
14527
14569
  toolName,
14528
14570
  toolInput,
14529
- session?.cwd,
14571
+ session.settingsManager.getRepoRoot(),
14530
14572
  suggestions
14531
14573
  );
14532
14574
  const response = await client.requestPermission({
@@ -14546,17 +14588,19 @@ async function handleDefaultPermissionFlow(context) {
14546
14588
  }
14547
14589
  if (response.outcome?.outcome === "selected" && (response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")) {
14548
14590
  if (response.outcome.optionId === "allow_always") {
14591
+ const rules = extractAllowRules(suggestions, toolName);
14592
+ try {
14593
+ await session.settingsManager.addAllowRules(rules);
14594
+ } catch (error) {
14595
+ context.logger.warn(
14596
+ "[canUseTool] Failed to persist allow rules to repository settings",
14597
+ { error: error instanceof Error ? error.message : String(error) }
14598
+ );
14599
+ }
14549
14600
  return {
14550
14601
  behavior: "allow",
14551
14602
  updatedInput: toolInput,
14552
- updatedPermissions: suggestions ?? [
14553
- {
14554
- type: "addRules",
14555
- rules: [{ toolName }],
14556
- behavior: "allow",
14557
- destination: "localSettings"
14558
- }
14559
- ]
14603
+ updatedPermissions: buildSessionPermissions(suggestions, rules)
14560
14604
  };
14561
14605
  }
14562
14606
  return {
@@ -14589,6 +14633,26 @@ function handlePlanFileException(context) {
14589
14633
  updatedInput: toolInput
14590
14634
  };
14591
14635
  }
14636
+ function extractAllowRules(suggestions, toolName) {
14637
+ if (!suggestions || suggestions.length === 0) {
14638
+ return [{ toolName }];
14639
+ }
14640
+ return suggestions.filter(
14641
+ (update) => update.type === "addRules" && update.behavior === "allow"
14642
+ ).flatMap((update) => "rules" in update ? update.rules : []);
14643
+ }
14644
+ function buildSessionPermissions(suggestions, rules) {
14645
+ const passthrough = (suggestions ?? []).filter(
14646
+ (update) => !(update.type === "addRules" && update.behavior === "allow")
14647
+ ).map((update) => ({ ...update, destination: "session" }));
14648
+ if (rules.length === 0) {
14649
+ return passthrough;
14650
+ }
14651
+ return [
14652
+ { type: "addRules", rules, behavior: "allow", destination: "session" },
14653
+ ...passthrough
14654
+ ];
14655
+ }
14592
14656
  function extractDomainFromUrl(url) {
14593
14657
  try {
14594
14658
  return new URL(url).hostname;
@@ -14991,6 +15055,47 @@ var fs7 = __toESM(require("fs"), 1);
14991
15055
  var os3 = __toESM(require("os"), 1);
14992
15056
  var path9 = __toESM(require("path"), 1);
14993
15057
  var import_minimatch = require("minimatch");
15058
+
15059
+ // src/utils/async-mutex.ts
15060
+ var AsyncMutex = class {
15061
+ locked = false;
15062
+ queue = [];
15063
+ async acquire() {
15064
+ if (!this.locked) {
15065
+ this.locked = true;
15066
+ return;
15067
+ }
15068
+ return new Promise((resolve4) => {
15069
+ this.queue.push(resolve4);
15070
+ });
15071
+ }
15072
+ release() {
15073
+ const next = this.queue.shift();
15074
+ if (next) {
15075
+ next();
15076
+ } else {
15077
+ this.locked = false;
15078
+ }
15079
+ }
15080
+ isLocked() {
15081
+ return this.locked;
15082
+ }
15083
+ get queueLength() {
15084
+ return this.queue.length;
15085
+ }
15086
+ };
15087
+
15088
+ // src/adapters/claude/session/repo-path.ts
15089
+ async function resolveMainRepoPath(cwd) {
15090
+ try {
15091
+ const worktrees = await listWorktrees(cwd);
15092
+ return worktrees[0]?.path ?? cwd;
15093
+ } catch {
15094
+ return cwd;
15095
+ }
15096
+ }
15097
+
15098
+ // src/adapters/claude/session/settings.ts
14994
15099
  var ACP_TOOL_NAME_PREFIX = "mcp__acp__";
14995
15100
  var acpToolNames = {
14996
15101
  read: `${ACP_TOOL_NAME_PREFIX}Read`,
@@ -15047,7 +15152,7 @@ function matchesGlob(pattern, filePath, cwd) {
15047
15152
  });
15048
15153
  }
15049
15154
  function matchesRule(rule, toolName, toolInput, cwd) {
15050
- const ruleAppliesToTool = rule.toolName === "Bash" && toolName === acpToolNames.bash || rule.toolName === "Edit" && FILE_EDITING_TOOLS.includes(toolName) || rule.toolName === "Read" && FILE_READING_TOOLS.includes(toolName);
15155
+ const ruleAppliesToTool = rule.toolName === "Bash" && toolName === acpToolNames.bash || rule.toolName === "Edit" && FILE_EDITING_TOOLS.includes(toolName) || rule.toolName === "Read" && FILE_READING_TOOLS.includes(toolName) || rule.toolName === toolName && !rule.argument;
15051
15156
  if (!ruleAppliesToTool) {
15052
15157
  return false;
15053
15158
  }
@@ -15077,6 +15182,19 @@ function matchesRule(rule, toolName, toolInput, cwd) {
15077
15182
  }
15078
15183
  return matchesGlob(rule.argument, actualArg, cwd);
15079
15184
  }
15185
+ function formatRule(rule) {
15186
+ return rule.ruleContent ? `${rule.toolName}(${rule.ruleContent})` : rule.toolName;
15187
+ }
15188
+ async function writeFileAtomic(filePath, data) {
15189
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
15190
+ await fs7.promises.writeFile(tmpPath, data);
15191
+ try {
15192
+ await fs7.promises.rename(tmpPath, filePath);
15193
+ } catch (error) {
15194
+ await fs7.promises.rm(tmpPath, { force: true });
15195
+ throw error;
15196
+ }
15197
+ }
15080
15198
  async function loadSettingsFile(filePath) {
15081
15199
  if (!filePath) {
15082
15200
  return {};
@@ -15095,6 +15213,17 @@ async function loadSettingsFile(filePath) {
15095
15213
  return {};
15096
15214
  }
15097
15215
  }
15216
+ async function readSettingsFileForUpdate(filePath) {
15217
+ try {
15218
+ const content = await fs7.promises.readFile(filePath, "utf-8");
15219
+ return JSON.parse(content);
15220
+ } catch (error) {
15221
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
15222
+ return {};
15223
+ }
15224
+ throw error;
15225
+ }
15226
+ }
15098
15227
  function getManagedSettingsPath() {
15099
15228
  switch (process.platform) {
15100
15229
  case "darwin":
@@ -15109,6 +15238,7 @@ function getManagedSettingsPath() {
15109
15238
  }
15110
15239
  var SettingsManager = class {
15111
15240
  cwd;
15241
+ repoRoot;
15112
15242
  userSettings = {};
15113
15243
  projectSettings = {};
15114
15244
  localSettings = {};
@@ -15116,8 +15246,10 @@ var SettingsManager = class {
15116
15246
  mergedSettings = {};
15117
15247
  initialized = false;
15118
15248
  initPromise = null;
15249
+ writeMutex = new AsyncMutex();
15119
15250
  constructor(cwd) {
15120
15251
  this.cwd = cwd;
15252
+ this.repoRoot = cwd;
15121
15253
  }
15122
15254
  async initialize() {
15123
15255
  if (this.initialized) return;
@@ -15135,10 +15267,16 @@ var SettingsManager = class {
15135
15267
  getProjectSettingsPath() {
15136
15268
  return path9.join(this.cwd, ".claude", "settings.json");
15137
15269
  }
15270
+ /**
15271
+ * Local settings are anchored to the primary worktree so every worktree of
15272
+ * the same repository shares a single `.claude/settings.local.json`. This
15273
+ * avoids re-prompting for the same permission in every worktree.
15274
+ */
15138
15275
  getLocalSettingsPath() {
15139
- return path9.join(this.cwd, ".claude", "settings.local.json");
15276
+ return path9.join(this.repoRoot, ".claude", "settings.local.json");
15140
15277
  }
15141
15278
  async loadAllSettings() {
15279
+ this.repoRoot = await resolveMainRepoPath(this.cwd);
15142
15280
  const [userSettings, projectSettings, localSettings, enterpriseSettings] = await Promise.all([
15143
15281
  loadSettingsFile(this.getUserSettingsPath()),
15144
15282
  loadSettingsFile(this.getProjectSettingsPath()),
@@ -15195,9 +15333,6 @@ var SettingsManager = class {
15195
15333
  this.mergedSettings = merged;
15196
15334
  }
15197
15335
  checkPermission(toolName, toolInput) {
15198
- if (!toolName.startsWith(ACP_TOOL_NAME_PREFIX)) {
15199
- return { decision: "ask" };
15200
- }
15201
15336
  const permissions = this.mergedSettings.permissions;
15202
15337
  if (!permissions) {
15203
15338
  return { decision: "ask" };
@@ -15228,6 +15363,43 @@ var SettingsManager = class {
15228
15363
  getCwd() {
15229
15364
  return this.cwd;
15230
15365
  }
15366
+ getRepoRoot() {
15367
+ return this.repoRoot;
15368
+ }
15369
+ /**
15370
+ * Persists allow rules to `<primary-worktree>/.claude/settings.local.json`.
15371
+ * Because local settings are resolved against the primary worktree, every
15372
+ * worktree of the same repository picks up the new rule on next load.
15373
+ *
15374
+ * Writes are serialised via `writeMutex` to prevent concurrent callers from
15375
+ * clobbering each other, and use a temp-file + rename to keep the file
15376
+ * consistent if the process dies mid-write.
15377
+ */
15378
+ async addAllowRules(rules) {
15379
+ if (rules.length === 0) return;
15380
+ if (!this.initialized) await this.initialize();
15381
+ await this.writeMutex.acquire();
15382
+ try {
15383
+ const filePath = this.getLocalSettingsPath();
15384
+ const existing = await readSettingsFileForUpdate(filePath);
15385
+ const permissions = {
15386
+ ...existing.permissions ?? {}
15387
+ };
15388
+ const current2 = new Set(permissions.allow ?? []);
15389
+ for (const rule of rules) {
15390
+ current2.add(formatRule(rule));
15391
+ }
15392
+ permissions.allow = Array.from(current2);
15393
+ const next = { ...existing, permissions };
15394
+ await fs7.promises.mkdir(path9.dirname(filePath), { recursive: true });
15395
+ await writeFileAtomic(filePath, `${JSON.stringify(next, null, 2)}
15396
+ `);
15397
+ this.localSettings = next;
15398
+ this.mergeAllSettings();
15399
+ } finally {
15400
+ this.writeMutex.release();
15401
+ }
15402
+ }
15231
15403
  async setCwd(cwd) {
15232
15404
  if (this.cwd === cwd) return;
15233
15405
  if (this.initPromise) await this.initPromise;
@@ -18813,35 +18985,6 @@ var SessionLogWriter = class _SessionLogWriter {
18813
18985
  }
18814
18986
  };
18815
18987
 
18816
- // src/utils/async-mutex.ts
18817
- var AsyncMutex = class {
18818
- locked = false;
18819
- queue = [];
18820
- async acquire() {
18821
- if (!this.locked) {
18822
- this.locked = true;
18823
- return;
18824
- }
18825
- return new Promise((resolve4) => {
18826
- this.queue.push(resolve4);
18827
- });
18828
- }
18829
- release() {
18830
- const next = this.queue.shift();
18831
- if (next) {
18832
- next();
18833
- } else {
18834
- this.locked = false;
18835
- }
18836
- }
18837
- isLocked() {
18838
- return this.locked;
18839
- }
18840
- get queueLength() {
18841
- return this.queue.length;
18842
- }
18843
- };
18844
-
18845
18988
  // src/server/cloud-prompt.ts
18846
18989
  function normalizeCloudPromptContent(content) {
18847
18990
  if (typeof content === "string") {
@@ -18987,6 +19130,14 @@ function validateCommandParams(method, params) {
18987
19130
  }
18988
19131
 
18989
19132
  // src/server/agent-server.ts
19133
+ var agentErrorClassificationSchema = import_zod3.z.enum([
19134
+ "upstream_stream_terminated",
19135
+ "upstream_connection_error",
19136
+ "agent_error"
19137
+ ]);
19138
+ var errorWithClassificationSchema = import_zod3.z.object({
19139
+ data: import_zod3.z.object({ classification: agentErrorClassificationSchema })
19140
+ });
18990
19141
  var NdJsonTap = class {
18991
19142
  constructor(onMessage) {
18992
19143
  this.onMessage = onMessage;
@@ -19689,6 +19840,23 @@ var AgentServer = class _AgentServer {
19689
19840
  );
19690
19841
  await this.sendInitialTaskMessage(payload, preTaskRun);
19691
19842
  }
19843
+ extractErrorClassification(error) {
19844
+ const message = error instanceof Error ? error.message : String(error ?? "");
19845
+ const parsed = errorWithClassificationSchema.safeParse(error);
19846
+ if (parsed.success) {
19847
+ return { classification: parsed.data.data.classification, message };
19848
+ }
19849
+ return { classification: classifyAgentError(message), message };
19850
+ }
19851
+ classifyAndSignalFailure(payload, phase, error) {
19852
+ const { classification, message } = this.extractErrorClassification(error);
19853
+ const errorMessage = classification === "upstream_stream_terminated" ? "Upstream LLM stream terminated" : classification === "upstream_connection_error" ? "Upstream LLM connection error" : message || "Agent error";
19854
+ this.logger.error(`send_${phase}_task_message_failed`, {
19855
+ classification,
19856
+ message
19857
+ });
19858
+ return this.signalTaskComplete(payload, "error", errorMessage);
19859
+ }
19692
19860
  async sendInitialTaskMessage(payload, prefetchedRun) {
19693
19861
  if (!this.session) return;
19694
19862
  let taskRun = prefetchedRun ?? null;
@@ -19781,7 +19949,7 @@ var AgentServer = class _AgentServer {
19781
19949
  if (this.session) {
19782
19950
  await this.session.logWriter.flushAll();
19783
19951
  }
19784
- await this.signalTaskComplete(payload, "error");
19952
+ await this.classifyAndSignalFailure(payload, "initial", error);
19785
19953
  }
19786
19954
  }
19787
19955
  async sendResumeMessage(payload, taskRun) {
@@ -19855,7 +20023,7 @@ Continue from where you left off. The user is waiting for your response.`
19855
20023
  if (this.session) {
19856
20024
  await this.session.logWriter.flushAll();
19857
20025
  }
19858
- await this.signalTaskComplete(payload, "error");
20026
+ await this.classifyAndSignalFailure(payload, "resume", error);
19859
20027
  }
19860
20028
  }
19861
20029
  static RESUME_HISTORY_TOKEN_BUDGET = 5e4;
@@ -20213,7 +20381,7 @@ ${attributionInstructions}
20213
20381
  });
20214
20382
  }
20215
20383
  }
20216
- async signalTaskComplete(payload, stopReason) {
20384
+ async signalTaskComplete(payload, stopReason, errorMessage) {
20217
20385
  if (this.session?.payload.run_id === payload.run_id) {
20218
20386
  try {
20219
20387
  await this.session.logWriter.flush(payload.run_id, {
@@ -20237,7 +20405,7 @@ ${attributionInstructions}
20237
20405
  try {
20238
20406
  await this.posthogAPI.updateTaskRun(payload.task_id, payload.run_id, {
20239
20407
  status,
20240
- error_message: stopReason === "error" ? "Agent error" : void 0
20408
+ error_message: errorMessage ?? "Agent error"
20241
20409
  });
20242
20410
  this.logger.info("Task completion signaled", { status, stopReason });
20243
20411
  } catch (error) {