@posthog/agent 1.29.0 → 2.0.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/dist/index.js CHANGED
@@ -46,16 +46,8 @@ var __callDispose = (stack, error, hasError) => {
46
46
 
47
47
  // src/acp-extensions.ts
48
48
  var POSTHOG_NOTIFICATIONS = {
49
- /** Artifact produced during task execution (research, plan, etc.) */
50
- ARTIFACT: "_posthog/artifact",
51
- /** Phase has started (research, plan, build, etc.) */
52
- PHASE_START: "_posthog/phase_start",
53
- /** Phase has completed */
54
- PHASE_COMPLETE: "_posthog/phase_complete",
55
49
  /** Git branch was created */
56
50
  BRANCH_CREATED: "_posthog/branch_created",
57
- /** Pull request was created */
58
- PR_CREATED: "_posthog/pr_created",
59
51
  /** Task run has started */
60
52
  RUN_STARTED: "_posthog/run_started",
61
53
  /** Task has completed */
@@ -68,19 +60,8 @@ var POSTHOG_NOTIFICATIONS = {
68
60
  SDK_SESSION: "_posthog/sdk_session"
69
61
  };
70
62
 
71
- // src/adapters/claude/claude.ts
72
- import * as fs from "fs";
73
- import * as os from "os";
74
- import * as path from "path";
75
- import {
76
- AgentSideConnection,
77
- ndJsonStream,
78
- RequestError
79
- } from "@agentclientprotocol/sdk";
80
- import {
81
- query
82
- } from "@anthropic-ai/claude-agent-sdk";
83
- import { v7 as uuidv7 } from "uuid";
63
+ // src/adapters/connection.ts
64
+ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
84
65
 
85
66
  // src/utils/logger.ts
86
67
  var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
@@ -161,7 +142,7 @@ var Logger = class _Logger {
161
142
 
162
143
  // src/utils/tapped-stream.ts
163
144
  function createTappedWritableStream(underlying, options) {
164
- const { onMessage, logger: logger2 } = options;
145
+ const { onMessage, logger } = options;
165
146
  const decoder = new TextDecoder();
166
147
  let buffer = "";
167
148
  let _messageCount = 0;
@@ -185,7 +166,7 @@ function createTappedWritableStream(underlying, options) {
185
166
  writer.releaseLock();
186
167
  },
187
168
  async abort(reason) {
188
- logger2?.warn("Tapped stream aborted", { reason });
169
+ logger?.warn("Tapped stream aborted", { reason });
189
170
  const writer = underlying.getWriter();
190
171
  await writer.abort(reason);
191
172
  writer.releaseLock();
@@ -193,10 +174,22 @@ function createTappedWritableStream(underlying, options) {
193
174
  });
194
175
  }
195
176
 
177
+ // src/adapters/claude/claude.ts
178
+ import * as fs from "fs";
179
+ import * as os from "os";
180
+ import * as path from "path";
181
+ import {
182
+ RequestError
183
+ } from "@agentclientprotocol/sdk";
184
+ import {
185
+ query
186
+ } from "@anthropic-ai/claude-agent-sdk";
187
+ import { v7 as uuidv7 } from "uuid";
188
+
196
189
  // package.json
197
190
  var package_default = {
198
191
  name: "@posthog/agent",
199
- version: "1.29.0",
192
+ version: "2.0.0",
200
193
  repository: "https://github.com/PostHog/array",
201
194
  description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
202
195
  main: "./dist/index.js",
@@ -309,14 +302,14 @@ var Pushable = class {
309
302
  };
310
303
  }
311
304
  };
312
- function unreachable(value, logger2) {
305
+ function unreachable(value, logger) {
313
306
  let valueAsString;
314
307
  try {
315
308
  valueAsString = JSON.stringify(value);
316
309
  } catch {
317
310
  valueAsString = value;
318
311
  }
319
- logger2.error(`Unexpected case: ${valueAsString}`);
312
+ logger.error(`Unexpected case: ${valueAsString}`);
320
313
  }
321
314
  function sleep(time) {
322
315
  return new Promise((resolve3) => setTimeout(resolve3, time));
@@ -1072,49 +1065,49 @@ No edits were applied.`
1072
1065
  }
1073
1066
 
1074
1067
  // src/adapters/claude/tools.ts
1075
- function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({ debug: false, prefix: "[ClaudeTools]" })) {
1068
+ function toolInfoFromToolUse(toolUse, cachedFileContent, logger = new Logger({ debug: false, prefix: "[ClaudeTools]" })) {
1076
1069
  const name = toolUse.name;
1077
1070
  const input = toolUse.input;
1078
1071
  switch (name) {
1079
1072
  case "Task":
1080
1073
  return {
1081
- title: input?.description ? input.description : "Task",
1074
+ title: input?.description ? String(input.description) : "Task",
1082
1075
  kind: "think",
1083
1076
  content: input?.prompt ? [
1084
1077
  {
1085
1078
  type: "content",
1086
- content: { type: "text", text: input.prompt }
1079
+ content: { type: "text", text: String(input.prompt) }
1087
1080
  }
1088
1081
  ] : []
1089
1082
  };
1090
1083
  case "NotebookRead":
1091
1084
  return {
1092
- title: input?.notebook_path ? `Read Notebook ${input.notebook_path}` : "Read Notebook",
1085
+ title: input?.notebook_path ? `Read Notebook ${String(input.notebook_path)}` : "Read Notebook",
1093
1086
  kind: "read",
1094
1087
  content: [],
1095
- locations: input?.notebook_path ? [{ path: input.notebook_path }] : []
1088
+ locations: input?.notebook_path ? [{ path: String(input.notebook_path) }] : []
1096
1089
  };
1097
1090
  case "NotebookEdit":
1098
1091
  return {
1099
- title: input?.notebook_path ? `Edit Notebook ${input.notebook_path}` : "Edit Notebook",
1092
+ title: input?.notebook_path ? `Edit Notebook ${String(input.notebook_path)}` : "Edit Notebook",
1100
1093
  kind: "edit",
1101
1094
  content: input?.new_source ? [
1102
1095
  {
1103
1096
  type: "content",
1104
- content: { type: "text", text: input.new_source }
1097
+ content: { type: "text", text: String(input.new_source) }
1105
1098
  }
1106
1099
  ] : [],
1107
- locations: input?.notebook_path ? [{ path: input.notebook_path }] : []
1100
+ locations: input?.notebook_path ? [{ path: String(input.notebook_path) }] : []
1108
1101
  };
1109
1102
  case "Bash":
1110
1103
  case toolNames.bash:
1111
1104
  return {
1112
- title: input?.command ? `\`${input.command.replaceAll("`", "\\`")}\`` : "Terminal",
1105
+ title: input?.command ? `\`${String(input.command).replaceAll("`", "\\`")}\`` : "Terminal",
1113
1106
  kind: "execute",
1114
1107
  content: input?.description ? [
1115
1108
  {
1116
1109
  type: "content",
1117
- content: { type: "text", text: input.description }
1110
+ content: { type: "text", text: String(input.description) }
1118
1111
  }
1119
1112
  ] : []
1120
1113
  };
@@ -1134,18 +1127,20 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({
1134
1127
  };
1135
1128
  case toolNames.read: {
1136
1129
  let limit = "";
1137
- if (input.limit) {
1138
- limit = " (" + ((input.offset ?? 0) + 1) + " - " + ((input.offset ?? 0) + input.limit) + ")";
1139
- } else if (input.offset) {
1140
- limit = ` (from line ${input.offset + 1})`;
1130
+ const inputLimit = input?.limit;
1131
+ const inputOffset = input?.offset ?? 0;
1132
+ if (inputLimit) {
1133
+ limit = ` (${inputOffset + 1} - ${inputOffset + inputLimit})`;
1134
+ } else if (inputOffset) {
1135
+ limit = ` (from line ${inputOffset + 1})`;
1141
1136
  }
1142
1137
  return {
1143
- title: `Read ${input.file_path ?? "File"}${limit}`,
1138
+ title: `Read ${input?.file_path ? String(input.file_path) : "File"}${limit}`,
1144
1139
  kind: "read",
1145
- locations: input.file_path ? [
1140
+ locations: input?.file_path ? [
1146
1141
  {
1147
- path: input.file_path,
1148
- line: input.offset ?? 0
1142
+ path: String(input.file_path),
1143
+ line: inputOffset
1149
1144
  }
1150
1145
  ] : [],
1151
1146
  content: []
@@ -1156,25 +1151,25 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({
1156
1151
  title: "Read File",
1157
1152
  kind: "read",
1158
1153
  content: [],
1159
- locations: input.file_path ? [
1154
+ locations: input?.file_path ? [
1160
1155
  {
1161
- path: input.file_path,
1162
- line: input.offset ?? 0
1156
+ path: String(input.file_path),
1157
+ line: input?.offset ?? 0
1163
1158
  }
1164
1159
  ] : []
1165
1160
  };
1166
1161
  case "LS":
1167
1162
  return {
1168
- title: `List the ${input?.path ? `\`${input.path}\`` : "current"} directory's contents`,
1163
+ title: `List the ${input?.path ? `\`${String(input.path)}\`` : "current"} directory's contents`,
1169
1164
  kind: "search",
1170
1165
  content: [],
1171
1166
  locations: []
1172
1167
  };
1173
1168
  case toolNames.edit:
1174
1169
  case "Edit": {
1175
- const path3 = input?.file_path ?? input?.file_path;
1176
- let oldText = input.old_string ?? null;
1177
- let newText = input.new_string ?? "";
1170
+ const path3 = input?.file_path ? String(input.file_path) : void 0;
1171
+ let oldText = input?.old_string ? String(input.old_string) : null;
1172
+ let newText = input?.new_string ? String(input.new_string) : "";
1178
1173
  let affectedLines = [];
1179
1174
  if (path3 && oldText) {
1180
1175
  try {
@@ -1190,7 +1185,7 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({
1190
1185
  newText = newContent.newContent;
1191
1186
  affectedLines = newContent.lineNumbers;
1192
1187
  } catch (e) {
1193
- logger2.error("Failed to edit file", e);
1188
+ logger.error("Failed to edit file", e);
1194
1189
  }
1195
1190
  }
1196
1191
  return {
@@ -1208,78 +1203,84 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({
1208
1203
  };
1209
1204
  }
1210
1205
  case toolNames.write: {
1211
- let content = [];
1212
- if (input?.file_path) {
1213
- content = [
1206
+ let contentResult = [];
1207
+ const filePath = input?.file_path ? String(input.file_path) : void 0;
1208
+ const contentStr = input?.content ? String(input.content) : void 0;
1209
+ if (filePath) {
1210
+ contentResult = [
1214
1211
  {
1215
1212
  type: "diff",
1216
- path: input.file_path,
1213
+ path: filePath,
1217
1214
  oldText: null,
1218
- newText: input.content
1215
+ newText: contentStr ?? ""
1219
1216
  }
1220
1217
  ];
1221
- } else if (input?.content) {
1222
- content = [
1218
+ } else if (contentStr) {
1219
+ contentResult = [
1223
1220
  {
1224
1221
  type: "content",
1225
- content: { type: "text", text: input.content }
1222
+ content: { type: "text", text: contentStr }
1226
1223
  }
1227
1224
  ];
1228
1225
  }
1229
1226
  return {
1230
- title: input?.file_path ? `Write ${input.file_path}` : "Write",
1227
+ title: filePath ? `Write ${filePath}` : "Write",
1231
1228
  kind: "edit",
1232
- content,
1233
- locations: input?.file_path ? [{ path: input.file_path }] : []
1229
+ content: contentResult,
1230
+ locations: filePath ? [{ path: filePath }] : []
1234
1231
  };
1235
1232
  }
1236
- case "Write":
1233
+ case "Write": {
1234
+ const filePath = input?.file_path ? String(input.file_path) : void 0;
1235
+ const contentStr = input?.content ? String(input.content) : "";
1237
1236
  return {
1238
- title: input?.file_path ? `Write ${input.file_path}` : "Write",
1237
+ title: filePath ? `Write ${filePath}` : "Write",
1239
1238
  kind: "edit",
1240
- content: input?.file_path ? [
1239
+ content: filePath ? [
1241
1240
  {
1242
1241
  type: "diff",
1243
- path: input.file_path,
1242
+ path: filePath,
1244
1243
  oldText: null,
1245
- newText: input.content
1244
+ newText: contentStr
1246
1245
  }
1247
1246
  ] : [],
1248
- locations: input?.file_path ? [{ path: input.file_path }] : []
1247
+ locations: filePath ? [{ path: filePath }] : []
1249
1248
  };
1249
+ }
1250
1250
  case "Glob": {
1251
1251
  let label = "Find";
1252
- if (input.path) {
1253
- label += ` \`${input.path}\``;
1252
+ const pathStr = input?.path ? String(input.path) : void 0;
1253
+ if (pathStr) {
1254
+ label += ` \`${pathStr}\``;
1254
1255
  }
1255
- if (input.pattern) {
1256
- label += ` \`${input.pattern}\``;
1256
+ if (input?.pattern) {
1257
+ label += ` \`${String(input.pattern)}\``;
1257
1258
  }
1258
1259
  return {
1259
1260
  title: label,
1260
1261
  kind: "search",
1261
1262
  content: [],
1262
- locations: input.path ? [{ path: input.path }] : []
1263
+ locations: pathStr ? [{ path: pathStr }] : []
1263
1264
  };
1264
1265
  }
1265
1266
  case "Grep": {
1266
1267
  let label = "grep";
1267
- if (input["-i"]) {
1268
+ if (input?.["-i"]) {
1268
1269
  label += " -i";
1269
1270
  }
1270
- if (input["-n"]) {
1271
+ if (input?.["-n"]) {
1271
1272
  label += " -n";
1272
1273
  }
1273
- if (input["-A"] !== void 0) {
1274
+ if (input?.["-A"] !== void 0) {
1274
1275
  label += ` -A ${input["-A"]}`;
1275
1276
  }
1276
- if (input["-B"] !== void 0) {
1277
+ if (input?.["-B"] !== void 0) {
1277
1278
  label += ` -B ${input["-B"]}`;
1278
1279
  }
1279
- if (input["-C"] !== void 0) {
1280
+ if (input?.["-C"] !== void 0) {
1280
1281
  label += ` -C ${input["-C"]}`;
1281
1282
  }
1282
- if (input.output_mode) {
1283
+ if (input?.output_mode) {
1283
1284
  switch (input.output_mode) {
1284
1285
  case "FilesWithMatches":
1285
1286
  label += " -l";
@@ -1291,21 +1292,21 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({
1291
1292
  break;
1292
1293
  }
1293
1294
  }
1294
- if (input.head_limit !== void 0) {
1295
+ if (input?.head_limit !== void 0) {
1295
1296
  label += ` | head -${input.head_limit}`;
1296
1297
  }
1297
- if (input.glob) {
1298
- label += ` --include="${input.glob}"`;
1298
+ if (input?.glob) {
1299
+ label += ` --include="${String(input.glob)}"`;
1299
1300
  }
1300
- if (input.type) {
1301
- label += ` --type=${input.type}`;
1301
+ if (input?.type) {
1302
+ label += ` --type=${String(input.type)}`;
1302
1303
  }
1303
- if (input.multiline) {
1304
+ if (input?.multiline) {
1304
1305
  label += " -P";
1305
1306
  }
1306
- label += ` "${input.pattern}"`;
1307
- if (input.path) {
1308
- label += ` ${input.path}`;
1307
+ label += ` "${input?.pattern ? String(input.pattern) : ""}"`;
1308
+ if (input?.path) {
1309
+ label += ` ${String(input.path)}`;
1309
1310
  }
1310
1311
  return {
1311
1312
  title: label,
@@ -1315,22 +1316,24 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({
1315
1316
  }
1316
1317
  case "WebFetch":
1317
1318
  return {
1318
- title: input?.url ? `Fetch ${input.url}` : "Fetch",
1319
+ title: input?.url ? `Fetch ${String(input.url)}` : "Fetch",
1319
1320
  kind: "fetch",
1320
1321
  content: input?.prompt ? [
1321
1322
  {
1322
1323
  type: "content",
1323
- content: { type: "text", text: input.prompt }
1324
+ content: { type: "text", text: String(input.prompt) }
1324
1325
  }
1325
1326
  ] : []
1326
1327
  };
1327
1328
  case "WebSearch": {
1328
- let label = `"${input.query}"`;
1329
- if (input.allowed_domains && input.allowed_domains.length > 0) {
1330
- label += ` (allowed: ${input.allowed_domains.join(", ")})`;
1329
+ let label = `"${input?.query ? String(input.query) : ""}"`;
1330
+ const allowedDomains = input?.allowed_domains;
1331
+ const blockedDomains = input?.blocked_domains;
1332
+ if (allowedDomains && allowedDomains.length > 0) {
1333
+ label += ` (allowed: ${allowedDomains.join(", ")})`;
1331
1334
  }
1332
- if (input.blocked_domains && input.blocked_domains.length > 0) {
1333
- label += ` (blocked: ${input.blocked_domains.join(", ")})`;
1335
+ if (blockedDomains && blockedDomains.length > 0) {
1336
+ label += ` (blocked: ${blockedDomains.join(", ")})`;
1334
1337
  }
1335
1338
  return {
1336
1339
  title: label,
@@ -1348,8 +1351,29 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({
1348
1351
  return {
1349
1352
  title: "Ready to code?",
1350
1353
  kind: "switch_mode",
1351
- content: input?.plan ? [{ type: "content", content: { type: "text", text: input.plan } }] : []
1354
+ content: input?.plan ? [
1355
+ {
1356
+ type: "content",
1357
+ content: { type: "text", text: String(input.plan) }
1358
+ }
1359
+ ] : []
1360
+ };
1361
+ case "AskUserQuestion": {
1362
+ const questions = input?.questions;
1363
+ return {
1364
+ title: questions?.[0]?.question || "Question",
1365
+ kind: "ask",
1366
+ content: questions ? [
1367
+ {
1368
+ type: "content",
1369
+ content: {
1370
+ type: "text",
1371
+ text: JSON.stringify(questions, null, 2)
1372
+ }
1373
+ }
1374
+ ] : []
1352
1375
  };
1376
+ }
1353
1377
  case "Other": {
1354
1378
  let output;
1355
1379
  try {
@@ -1386,15 +1410,24 @@ function toolUpdateFromToolResult(toolResult, toolUse) {
1386
1410
  case toolNames.read:
1387
1411
  if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
1388
1412
  return {
1389
- content: toolResult.content.map((content) => ({
1390
- type: "content",
1391
- content: content.type === "text" ? {
1392
- type: "text",
1393
- text: markdownEscape(
1394
- content.text.replace(SYSTEM_REMINDER, "")
1395
- )
1396
- } : content
1397
- }))
1413
+ content: toolResult.content.map((item) => {
1414
+ const itemObj = item;
1415
+ if (itemObj.type === "text") {
1416
+ return {
1417
+ type: "content",
1418
+ content: {
1419
+ type: "text",
1420
+ text: markdownEscape(
1421
+ (itemObj.text ?? "").replace(SYSTEM_REMINDER, "")
1422
+ )
1423
+ }
1424
+ };
1425
+ }
1426
+ return {
1427
+ type: "content",
1428
+ content: item
1429
+ };
1430
+ })
1398
1431
  };
1399
1432
  } else if (typeof toolResult.content === "string" && toolResult.content.length > 0) {
1400
1433
  return {
@@ -1426,6 +1459,24 @@ function toolUpdateFromToolResult(toolResult, toolUse) {
1426
1459
  case "ExitPlanMode": {
1427
1460
  return { title: "Exited Plan Mode" };
1428
1461
  }
1462
+ case "AskUserQuestion": {
1463
+ const content = toolResult.content;
1464
+ if (Array.isArray(content) && content.length > 0) {
1465
+ const firstItem = content[0];
1466
+ if (typeof firstItem === "object" && firstItem !== null && "text" in firstItem) {
1467
+ return {
1468
+ title: "Answer received",
1469
+ content: [
1470
+ {
1471
+ type: "content",
1472
+ content: { type: "text", text: String(firstItem.text) }
1473
+ }
1474
+ ]
1475
+ };
1476
+ }
1477
+ }
1478
+ return { title: "Question answered" };
1479
+ }
1429
1480
  default: {
1430
1481
  return toAcpContentUpdate(
1431
1482
  toolResult.content,
@@ -1437,15 +1488,24 @@ function toolUpdateFromToolResult(toolResult, toolUse) {
1437
1488
  function toAcpContentUpdate(content, isError = false) {
1438
1489
  if (Array.isArray(content) && content.length > 0) {
1439
1490
  return {
1440
- content: content.map((content2) => ({
1441
- type: "content",
1442
- content: isError && content2.type === "text" ? {
1443
- ...content2,
1444
- text: `\`\`\`
1445
- ${content2.text}
1491
+ content: content.map((item) => {
1492
+ const itemObj = item;
1493
+ if (isError && itemObj.type === "text") {
1494
+ return {
1495
+ type: "content",
1496
+ content: {
1497
+ type: "text",
1498
+ text: `\`\`\`
1499
+ ${itemObj.text ?? ""}
1446
1500
  \`\`\``
1447
- } : content2
1448
- }))
1501
+ }
1502
+ };
1503
+ }
1504
+ return {
1505
+ type: "content",
1506
+ content: item
1507
+ };
1508
+ })
1449
1509
  };
1450
1510
  } else if (typeof content === "string" && content.length > 0) {
1451
1511
  return {
@@ -1489,7 +1549,7 @@ var registerHookCallback = (toolUseID, {
1489
1549
  onPostToolUseHook
1490
1550
  };
1491
1551
  };
1492
- var createPostToolUseHook = (logger2 = new Logger({ prefix: "[createPostToolUseHook]" })) => async (input, toolUseID) => {
1552
+ var createPostToolUseHook = (logger = new Logger({ prefix: "[createPostToolUseHook]" })) => async (input, toolUseID) => {
1493
1553
  if (input.hook_event_name === "PostToolUse" && toolUseID) {
1494
1554
  const onPostToolUseHook = toolUseCallbacks[toolUseID]?.onPostToolUseHook;
1495
1555
  if (onPostToolUseHook) {
@@ -1500,7 +1560,7 @@ var createPostToolUseHook = (logger2 = new Logger({ prefix: "[createPostToolUseH
1500
1560
  );
1501
1561
  delete toolUseCallbacks[toolUseID];
1502
1562
  } else {
1503
- logger2.error(
1563
+ logger.error(
1504
1564
  `No onPostToolUseHook found for tool use ID: ${toolUseID}`
1505
1565
  );
1506
1566
  delete toolUseCallbacks[toolUseID];
@@ -1510,9 +1570,115 @@ var createPostToolUseHook = (logger2 = new Logger({ prefix: "[createPostToolUseH
1510
1570
  };
1511
1571
 
1512
1572
  // src/adapters/claude/claude.ts
1573
+ function getClaudeConfigDir() {
1574
+ return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
1575
+ }
1576
+ function getClaudePlansDir() {
1577
+ return path.join(getClaudeConfigDir(), "plans");
1578
+ }
1579
+ function isClaudePlanFilePath(filePath) {
1580
+ if (!filePath) return false;
1581
+ const resolved = path.resolve(filePath);
1582
+ const plansDir = path.resolve(getClaudePlansDir());
1583
+ return resolved === plansDir || resolved.startsWith(plansDir + path.sep);
1584
+ }
1585
+ var READ_ONLY_COMMAND_PREFIXES = [
1586
+ // File listing and info
1587
+ "ls",
1588
+ "find",
1589
+ "tree",
1590
+ "stat",
1591
+ "file",
1592
+ "wc",
1593
+ "du",
1594
+ "df",
1595
+ // File reading (non-modifying)
1596
+ "cat",
1597
+ "head",
1598
+ "tail",
1599
+ "less",
1600
+ "more",
1601
+ "bat",
1602
+ // Search
1603
+ "grep",
1604
+ "rg",
1605
+ "ag",
1606
+ "ack",
1607
+ "fzf",
1608
+ // Git read operations
1609
+ "git status",
1610
+ "git log",
1611
+ "git diff",
1612
+ "git show",
1613
+ "git branch",
1614
+ "git remote",
1615
+ "git fetch",
1616
+ "git rev-parse",
1617
+ "git ls-files",
1618
+ "git blame",
1619
+ "git shortlog",
1620
+ "git describe",
1621
+ "git tag -l",
1622
+ "git tag --list",
1623
+ // System info
1624
+ "pwd",
1625
+ "whoami",
1626
+ "which",
1627
+ "where",
1628
+ "type",
1629
+ "printenv",
1630
+ "env",
1631
+ "echo",
1632
+ "printf",
1633
+ "date",
1634
+ "uptime",
1635
+ "uname",
1636
+ "id",
1637
+ "groups",
1638
+ // Process info
1639
+ "ps",
1640
+ "top",
1641
+ "htop",
1642
+ "pgrep",
1643
+ "lsof",
1644
+ // Network read-only
1645
+ "curl",
1646
+ "wget",
1647
+ "ping",
1648
+ "host",
1649
+ "dig",
1650
+ "nslookup",
1651
+ // Package managers (info only)
1652
+ "npm list",
1653
+ "npm ls",
1654
+ "npm view",
1655
+ "npm info",
1656
+ "npm outdated",
1657
+ "pnpm list",
1658
+ "pnpm ls",
1659
+ "pnpm why",
1660
+ "yarn list",
1661
+ "yarn why",
1662
+ "yarn info",
1663
+ // Other read-only
1664
+ "jq",
1665
+ "yq",
1666
+ "xargs",
1667
+ "sort",
1668
+ "uniq",
1669
+ "tr",
1670
+ "cut",
1671
+ "awk",
1672
+ "sed -n"
1673
+ ];
1674
+ function isReadOnlyBashCommand(command) {
1675
+ const trimmed = command.trim();
1676
+ return READ_ONLY_COMMAND_PREFIXES.some(
1677
+ (prefix) => trimmed === prefix || trimmed.startsWith(`${prefix} `) || trimmed.startsWith(`${prefix} `)
1678
+ );
1679
+ }
1513
1680
  function clearStatsigCache() {
1514
- const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
1515
- const statsigPath = path.join(configDir, "statsig");
1681
+ const statsigPath = path.join(getClaudeConfigDir(), "statsig");
1516
1682
  try {
1517
1683
  if (fs.existsSync(statsigPath)) {
1518
1684
  fs.rmSync(statsigPath, { recursive: true, force: true });
@@ -1548,6 +1714,33 @@ var ClaudeAcpAgent = class {
1548
1714
  this.sessions[sessionId] = session;
1549
1715
  return session;
1550
1716
  }
1717
+ getLatestAssistantText(notifications) {
1718
+ const chunks = [];
1719
+ let started = false;
1720
+ for (let i = notifications.length - 1; i >= 0; i -= 1) {
1721
+ const update = notifications[i]?.update;
1722
+ if (!update) continue;
1723
+ if (update.sessionUpdate === "agent_message_chunk") {
1724
+ started = true;
1725
+ const content = update.content;
1726
+ if (content?.type === "text" && content.text) {
1727
+ chunks.push(content.text);
1728
+ }
1729
+ continue;
1730
+ }
1731
+ if (started) {
1732
+ break;
1733
+ }
1734
+ }
1735
+ if (chunks.length === 0) return null;
1736
+ return chunks.reverse().join("");
1737
+ }
1738
+ isPlanReady(plan) {
1739
+ if (!plan) return false;
1740
+ const trimmed = plan.trim();
1741
+ if (trimmed.length < 40) return false;
1742
+ return /(^|\n)#{1,6}\s+\S/.test(trimmed);
1743
+ }
1551
1744
  appendNotification(sessionId, notification) {
1552
1745
  this.sessions[sessionId]?.notificationHistory.push(notification);
1553
1746
  }
@@ -1629,7 +1822,9 @@ var ClaudeAcpAgent = class {
1629
1822
  systemPrompt.append = customPrompt.append;
1630
1823
  }
1631
1824
  }
1632
- const permissionMode = "default";
1825
+ const initialModeId = params._meta?.initialModeId;
1826
+ const ourPermissionMode = initialModeId ?? "default";
1827
+ const sdkPermissionMode = ourPermissionMode;
1633
1828
  const userProvidedOptions = params._meta?.claudeCode?.options;
1634
1829
  const options = {
1635
1830
  systemPrompt,
@@ -1643,7 +1838,8 @@ var ClaudeAcpAgent = class {
1643
1838
  // If we want bypassPermissions to be an option, we have to allow it here.
1644
1839
  // But it doesn't work in root mode, so we only activate it if it will work.
1645
1840
  allowDangerouslySkipPermissions: !IS_ROOT,
1646
- permissionMode,
1841
+ // Use the requested permission mode (including plan mode)
1842
+ permissionMode: sdkPermissionMode,
1647
1843
  canUseTool: this.canUseTool(sessionId),
1648
1844
  // Use "node" to resolve via PATH where a symlink to Electron exists.
1649
1845
  // This avoids launching the Electron binary directly from the app bundle,
@@ -1651,7 +1847,12 @@ var ClaudeAcpAgent = class {
1651
1847
  executable: "node",
1652
1848
  // Prevent spawned Electron processes from showing in dock/tray.
1653
1849
  // Must merge with process.env since SDK replaces rather than merges.
1654
- env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
1850
+ // Enable AskUserQuestion tool via environment variable (required by SDK feature flag)
1851
+ env: {
1852
+ ...process.env,
1853
+ ELECTRON_RUN_AS_NODE: "1",
1854
+ CLAUDE_CODE_ENABLE_ASK_USER_QUESTION_TOOL: "true"
1855
+ },
1655
1856
  ...process.env.CLAUDE_CODE_EXECUTABLE && {
1656
1857
  pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE
1657
1858
  },
@@ -1665,7 +1866,7 @@ var ClaudeAcpAgent = class {
1665
1866
  ]
1666
1867
  }
1667
1868
  };
1668
- const allowedTools = [];
1869
+ const allowedTools = ["AskUserQuestion"];
1669
1870
  const disallowedTools = [];
1670
1871
  const disableBuiltInTools = params._meta?.disableBuiltInTools === true;
1671
1872
  if (!disableBuiltInTools) {
@@ -1707,6 +1908,9 @@ var ClaudeAcpAgent = class {
1707
1908
  "NotebookEdit"
1708
1909
  );
1709
1910
  }
1911
+ if (ourPermissionMode !== "plan") {
1912
+ disallowedTools.push("ExitPlanMode");
1913
+ }
1710
1914
  if (allowedTools.length > 0) {
1711
1915
  options.allowedTools = allowedTools;
1712
1916
  }
@@ -1722,7 +1926,7 @@ var ClaudeAcpAgent = class {
1722
1926
  prompt: input,
1723
1927
  options
1724
1928
  });
1725
- this.createSession(sessionId, q, input, permissionMode);
1929
+ this.createSession(sessionId, q, input, ourPermissionMode);
1726
1930
  const persistence = params._meta?.persistence;
1727
1931
  if (persistence && this.sessionStore) {
1728
1932
  this.sessionStore.register(sessionId, persistence);
@@ -1778,7 +1982,7 @@ var ClaudeAcpAgent = class {
1778
1982
  sessionId,
1779
1983
  models,
1780
1984
  modes: {
1781
- currentModeId: permissionMode,
1985
+ currentModeId: ourPermissionMode,
1782
1986
  availableModes
1783
1987
  }
1784
1988
  };
@@ -1791,7 +1995,8 @@ var ClaudeAcpAgent = class {
1791
1995
  throw new Error("Session not found");
1792
1996
  }
1793
1997
  this.sessions[params.sessionId].cancelled = false;
1794
- const { query: query5, input } = this.sessions[params.sessionId];
1998
+ const session = this.sessions[params.sessionId];
1999
+ const { query: query2, input } = session;
1795
2000
  for (const chunk of params.prompt) {
1796
2001
  const userNotification = {
1797
2002
  sessionId: params.sessionId,
@@ -1803,9 +2008,9 @@ var ClaudeAcpAgent = class {
1803
2008
  await this.client.sessionUpdate(userNotification);
1804
2009
  this.appendNotification(params.sessionId, userNotification);
1805
2010
  }
1806
- input.push(promptToClaude(params));
2011
+ input.push(promptToClaude({ ...params, prompt: params.prompt }));
1807
2012
  while (true) {
1808
- const { value: message, done } = await query5.next();
2013
+ const { value: message, done } = await query2.next();
1809
2014
  if (done || !message) {
1810
2015
  if (this.sessions[params.sessionId].cancelled) {
1811
2016
  return { stopReason: "cancelled" };
@@ -1821,9 +2026,9 @@ var ClaudeAcpAgent = class {
1821
2026
  switch (message.subtype) {
1822
2027
  case "init":
1823
2028
  if (message.session_id) {
1824
- const session = this.sessions[params.sessionId];
1825
- if (session && !session.sdkSessionId) {
1826
- session.sdkSessionId = message.session_id;
2029
+ const session2 = this.sessions[params.sessionId];
2030
+ if (session2 && !session2.sdkSessionId) {
2031
+ session2.sdkSessionId = message.session_id;
1827
2032
  this.client.extNotification("_posthog/sdk_session", {
1828
2033
  sessionId: params.sessionId,
1829
2034
  sdkSessionId: message.session_id
@@ -1913,8 +2118,11 @@ var ClaudeAcpAgent = class {
1913
2118
  throw RequestError.authRequired();
1914
2119
  }
1915
2120
  const content = message.message.content;
2121
+ const contentToProcess = Array.isArray(content) ? content.filter(
2122
+ (block) => block.type !== "text" && block.type !== "thinking"
2123
+ ) : content;
1916
2124
  for (const notification of toAcpNotifications(
1917
- content,
2125
+ contentToProcess,
1918
2126
  message.message.role,
1919
2127
  params.sessionId,
1920
2128
  this.toolUseCache,
@@ -2003,7 +2211,64 @@ var ClaudeAcpAgent = class {
2003
2211
  interrupt: true
2004
2212
  };
2005
2213
  }
2214
+ const emitToolDenial = async (message) => {
2215
+ this.logger.info(`[canUseTool] Tool denied: ${toolName}`, { message });
2216
+ await this.client.sessionUpdate({
2217
+ sessionId,
2218
+ update: {
2219
+ sessionUpdate: "tool_call_update",
2220
+ toolCallId: toolUseID,
2221
+ status: "failed",
2222
+ content: [
2223
+ {
2224
+ type: "content",
2225
+ content: {
2226
+ type: "text",
2227
+ text: message
2228
+ }
2229
+ }
2230
+ ]
2231
+ }
2232
+ });
2233
+ };
2006
2234
  if (toolName === "ExitPlanMode") {
2235
+ if (session.permissionMode !== "plan") {
2236
+ return {
2237
+ behavior: "allow",
2238
+ updatedInput: toolInput
2239
+ };
2240
+ }
2241
+ let updatedInput = toolInput;
2242
+ const planFromFile = session.lastPlanContent || (session.lastPlanFilePath ? this.fileContentCache[session.lastPlanFilePath] : void 0);
2243
+ const hasPlan = typeof toolInput?.plan === "string";
2244
+ if (!hasPlan) {
2245
+ const fallbackPlan = planFromFile ? planFromFile : this.getLatestAssistantText(session.notificationHistory);
2246
+ if (fallbackPlan) {
2247
+ updatedInput = {
2248
+ ...toolInput,
2249
+ plan: fallbackPlan
2250
+ };
2251
+ }
2252
+ }
2253
+ const planText = typeof updatedInput?.plan === "string" ? String(updatedInput.plan) : void 0;
2254
+ if (!planText) {
2255
+ const message = `Plan not ready. Provide the full markdown plan in ExitPlanMode or write it to ${getClaudePlansDir()} before requesting approval.`;
2256
+ await emitToolDenial(message);
2257
+ return {
2258
+ behavior: "deny",
2259
+ message,
2260
+ interrupt: false
2261
+ };
2262
+ }
2263
+ if (!this.isPlanReady(planText)) {
2264
+ const message = "Plan not ready. Provide the full markdown plan in ExitPlanMode before requesting approval.";
2265
+ await emitToolDenial(message);
2266
+ return {
2267
+ behavior: "deny",
2268
+ message,
2269
+ interrupt: false
2270
+ };
2271
+ }
2007
2272
  const response2 = await this.client.requestPermission({
2008
2273
  options: [
2009
2274
  {
@@ -2025,9 +2290,9 @@ var ClaudeAcpAgent = class {
2025
2290
  sessionId,
2026
2291
  toolCall: {
2027
2292
  toolCallId: toolUseID,
2028
- rawInput: toolInput,
2293
+ rawInput: { ...updatedInput, toolName },
2029
2294
  title: toolInfoFromToolUse(
2030
- { name: toolName, input: toolInput },
2295
+ { name: toolName, input: updatedInput },
2031
2296
  this.fileContentCache,
2032
2297
  this.logger
2033
2298
  ).title
@@ -2044,7 +2309,7 @@ var ClaudeAcpAgent = class {
2044
2309
  });
2045
2310
  return {
2046
2311
  behavior: "allow",
2047
- updatedInput: toolInput,
2312
+ updatedInput,
2048
2313
  updatedPermissions: suggestions ?? [
2049
2314
  {
2050
2315
  type: "setMode",
@@ -2053,13 +2318,147 @@ var ClaudeAcpAgent = class {
2053
2318
  }
2054
2319
  ]
2055
2320
  };
2321
+ } else {
2322
+ const message = "User wants to continue planning. Please refine your plan based on any feedback provided, or ask clarifying questions if needed.";
2323
+ await emitToolDenial(message);
2324
+ return {
2325
+ behavior: "deny",
2326
+ message,
2327
+ interrupt: false
2328
+ };
2329
+ }
2330
+ }
2331
+ if (toolName === "AskUserQuestion") {
2332
+ const input = toolInput;
2333
+ let questions;
2334
+ if (input.questions && input.questions.length > 0) {
2335
+ questions = input.questions;
2336
+ } else if (input.question) {
2337
+ questions = [
2338
+ {
2339
+ question: input.question,
2340
+ header: input.header,
2341
+ options: input.options || [],
2342
+ multiSelect: input.multiSelect
2343
+ }
2344
+ ];
2056
2345
  } else {
2057
2346
  return {
2058
2347
  behavior: "deny",
2059
- message: "User rejected request to exit plan mode.",
2348
+ message: "No questions provided",
2060
2349
  interrupt: true
2061
2350
  };
2062
2351
  }
2352
+ const allAnswers = {};
2353
+ for (let i = 0; i < questions.length; i++) {
2354
+ const question = questions[i];
2355
+ const options = (question.options || []).map(
2356
+ (opt, idx) => ({
2357
+ kind: "allow_once",
2358
+ name: opt.label,
2359
+ optionId: `option_${idx}`,
2360
+ description: opt.description
2361
+ })
2362
+ );
2363
+ options.push({
2364
+ kind: "allow_once",
2365
+ name: "Other",
2366
+ optionId: "other",
2367
+ description: "Provide a custom response"
2368
+ });
2369
+ const response2 = await this.client.requestPermission({
2370
+ options,
2371
+ sessionId,
2372
+ toolCall: {
2373
+ toolCallId: toolUseID,
2374
+ rawInput: {
2375
+ ...toolInput,
2376
+ toolName,
2377
+ // Include full question data for UI rendering
2378
+ currentQuestion: question,
2379
+ questionIndex: i,
2380
+ totalQuestions: questions.length
2381
+ },
2382
+ // Use the full question text as title for the selection input
2383
+ title: question.question
2384
+ }
2385
+ });
2386
+ if (response2.outcome?.outcome === "selected") {
2387
+ const selectedOptionId = response2.outcome.optionId;
2388
+ const extendedOutcome = response2.outcome;
2389
+ if (selectedOptionId === "other" && extendedOutcome.customInput) {
2390
+ allAnswers[question.question] = extendedOutcome.customInput;
2391
+ } else if (selectedOptionId === "other") {
2392
+ allAnswers[question.question] = "other";
2393
+ } else if (question.multiSelect && extendedOutcome.selectedOptionIds) {
2394
+ const selectedLabels = extendedOutcome.selectedOptionIds.map((id) => {
2395
+ const idx = parseInt(id.replace("option_", ""), 10);
2396
+ return question.options?.[idx]?.label;
2397
+ }).filter(Boolean);
2398
+ allAnswers[question.question] = selectedLabels;
2399
+ } else {
2400
+ const selectedIdx = parseInt(
2401
+ selectedOptionId.replace("option_", ""),
2402
+ 10
2403
+ );
2404
+ const selectedOption = question.options?.[selectedIdx];
2405
+ allAnswers[question.question] = selectedOption?.label || selectedOptionId;
2406
+ }
2407
+ } else {
2408
+ return {
2409
+ behavior: "deny",
2410
+ message: "User did not complete all questions",
2411
+ interrupt: true
2412
+ };
2413
+ }
2414
+ }
2415
+ return {
2416
+ behavior: "allow",
2417
+ updatedInput: {
2418
+ ...toolInput,
2419
+ answers: allAnswers
2420
+ }
2421
+ };
2422
+ }
2423
+ const WRITE_TOOL_NAMES = [
2424
+ ...EDIT_TOOL_NAMES,
2425
+ "Edit",
2426
+ "Write",
2427
+ "NotebookEdit"
2428
+ ];
2429
+ if (session.permissionMode === "plan" && WRITE_TOOL_NAMES.includes(toolName)) {
2430
+ const filePath = toolInput?.file_path;
2431
+ const isPlanFile = isClaudePlanFilePath(filePath);
2432
+ if (isPlanFile) {
2433
+ session.lastPlanFilePath = filePath;
2434
+ const content = toolInput?.content;
2435
+ if (typeof content === "string") {
2436
+ session.lastPlanContent = content;
2437
+ }
2438
+ return {
2439
+ behavior: "allow",
2440
+ updatedInput: toolInput
2441
+ };
2442
+ }
2443
+ const message = "Cannot use write tools in plan mode. Use ExitPlanMode to request permission to make changes.";
2444
+ await emitToolDenial(message);
2445
+ return {
2446
+ behavior: "deny",
2447
+ message,
2448
+ interrupt: false
2449
+ };
2450
+ }
2451
+ if (session.permissionMode === "plan" && (toolName === "Bash" || toolName === toolNames.bash)) {
2452
+ const command = toolInput?.command ?? "";
2453
+ if (!isReadOnlyBashCommand(command)) {
2454
+ const message = "Cannot run write/modify bash commands in plan mode. Use ExitPlanMode to request permission to make changes.";
2455
+ await emitToolDenial(message);
2456
+ return {
2457
+ behavior: "deny",
2458
+ message,
2459
+ interrupt: false
2460
+ };
2461
+ }
2063
2462
  }
2064
2463
  if (session.permissionMode === "bypassPermissions" || session.permissionMode === "acceptEdits" && EDIT_TOOL_NAMES.includes(toolName)) {
2065
2464
  return {
@@ -2116,9 +2515,11 @@ var ClaudeAcpAgent = class {
2116
2515
  updatedInput: toolInput
2117
2516
  };
2118
2517
  } else {
2518
+ const message = "User refused permission to run tool";
2519
+ await emitToolDenial(message);
2119
2520
  return {
2120
2521
  behavior: "deny",
2121
- message: "User refused permission to run tool",
2522
+ message,
2122
2523
  interrupt: true
2123
2524
  };
2124
2525
  }
@@ -2138,6 +2539,11 @@ var ClaudeAcpAgent = class {
2138
2539
  await this.setSessionModel({ sessionId, modelId });
2139
2540
  return {};
2140
2541
  }
2542
+ if (method === "session/setMode") {
2543
+ const { sessionId, modeId } = params;
2544
+ await this.setSessionMode({ sessionId, modeId });
2545
+ return {};
2546
+ }
2141
2547
  throw RequestError.methodNotFound(method);
2142
2548
  }
2143
2549
  /**
@@ -2247,10 +2653,10 @@ var ClaudeAcpAgent = class {
2247
2653
  return {};
2248
2654
  }
2249
2655
  };
2250
- async function getAvailableModels(query5) {
2251
- const models = await query5.supportedModels();
2656
+ async function getAvailableModels(query2) {
2657
+ const models = await query2.supportedModels();
2252
2658
  const currentModel = models[0];
2253
- await query5.setModel(currentModel.value);
2659
+ await query2.setModel(currentModel.value);
2254
2660
  const availableModels = models.map((model) => ({
2255
2661
  modelId: model.value,
2256
2662
  name: model.displayName,
@@ -2261,7 +2667,7 @@ async function getAvailableModels(query5) {
2261
2667
  currentModelId: currentModel.value
2262
2668
  };
2263
2669
  }
2264
- async function getAvailableSlashCommands(query5) {
2670
+ async function getAvailableSlashCommands(query2) {
2265
2671
  const UNSUPPORTED_COMMANDS = [
2266
2672
  "context",
2267
2673
  "cost",
@@ -2271,7 +2677,7 @@ async function getAvailableSlashCommands(query5) {
2271
2677
  "release-notes",
2272
2678
  "todos"
2273
2679
  ];
2274
- const commands = await query5.supportedCommands();
2680
+ const commands = await query2.supportedCommands();
2275
2681
  return commands.map((command) => {
2276
2682
  const input = command.argumentHint ? { hint: command.argumentHint } : null;
2277
2683
  let name = command.name;
@@ -2379,7 +2785,7 @@ ${chunk.resource.text}
2379
2785
  parent_tool_use_id: null
2380
2786
  };
2381
2787
  }
2382
- function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentCache, client, logger2) {
2788
+ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentCache, client, logger) {
2383
2789
  if (typeof content === "string") {
2384
2790
  return [
2385
2791
  {
@@ -2460,7 +2866,7 @@ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentC
2460
2866
  update: update2
2461
2867
  });
2462
2868
  } else {
2463
- logger2.error(
2869
+ logger.error(
2464
2870
  `[claude-code-acp] Got a tool response for tool use that wasn't tracked: ${toolUseId}`
2465
2871
  );
2466
2872
  }
@@ -2481,7 +2887,7 @@ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentC
2481
2887
  sessionUpdate: "tool_call",
2482
2888
  rawInput,
2483
2889
  status: "pending",
2484
- ...toolInfoFromToolUse(chunk, fileContentCache, logger2)
2890
+ ...toolInfoFromToolUse(chunk, fileContentCache, logger)
2485
2891
  };
2486
2892
  }
2487
2893
  break;
@@ -2496,7 +2902,7 @@ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentC
2496
2902
  case "mcp_tool_result": {
2497
2903
  const toolUse = toolUseCache[chunk.tool_use_id];
2498
2904
  if (!toolUse) {
2499
- logger2.error(
2905
+ logger.error(
2500
2906
  `[claude-code-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`
2501
2907
  );
2502
2908
  break;
@@ -2525,7 +2931,7 @@ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentC
2525
2931
  case "container_upload":
2526
2932
  break;
2527
2933
  default:
2528
- unreachable(chunk, logger2);
2934
+ unreachable(chunk, logger);
2529
2935
  break;
2530
2936
  }
2531
2937
  if (update) {
@@ -2534,7 +2940,7 @@ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentC
2534
2940
  }
2535
2941
  return output;
2536
2942
  }
2537
- function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileContentCache, client, logger2) {
2943
+ function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileContentCache, client, logger) {
2538
2944
  const event = message.event;
2539
2945
  switch (event.type) {
2540
2946
  case "content_block_start":
@@ -2545,7 +2951,7 @@ function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileCon
2545
2951
  toolUseCache,
2546
2952
  fileContentCache,
2547
2953
  client,
2548
- logger2
2954
+ logger
2549
2955
  );
2550
2956
  case "content_block_delta":
2551
2957
  return toAcpNotifications(
@@ -2555,7 +2961,7 @@ function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileCon
2555
2961
  toolUseCache,
2556
2962
  fileContentCache,
2557
2963
  client,
2558
- logger2
2964
+ logger
2559
2965
  );
2560
2966
  // No content
2561
2967
  case "message_start":
@@ -2564,14 +2970,16 @@ function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileCon
2564
2970
  case "content_block_stop":
2565
2971
  return [];
2566
2972
  default:
2567
- unreachable(event, logger2);
2973
+ unreachable(event, logger);
2568
2974
  return [];
2569
2975
  }
2570
2976
  }
2977
+
2978
+ // src/adapters/connection.ts
2571
2979
  function createAcpConnection(config = {}) {
2572
- const logger2 = new Logger({ debug: true, prefix: "[AcpConnection]" });
2980
+ const logger = new Logger({ debug: true, prefix: "[AcpConnection]" });
2573
2981
  const streams = createBidirectionalStreams();
2574
- const { sessionStore } = config;
2982
+ const { sessionStore, framework = "claude" } = config;
2575
2983
  let agentWritable = streams.agent.writable;
2576
2984
  let clientWritable = streams.client.writable;
2577
2985
  if (config.sessionId && sessionStore) {
@@ -2587,25 +2995,25 @@ function createAcpConnection(config = {}) {
2587
2995
  onMessage: (line) => {
2588
2996
  sessionStore.appendRawLine(config.sessionId, line);
2589
2997
  },
2590
- logger: logger2
2998
+ logger
2591
2999
  });
2592
3000
  clientWritable = createTappedWritableStream(streams.client.writable, {
2593
3001
  onMessage: (line) => {
2594
3002
  sessionStore.appendRawLine(config.sessionId, line);
2595
3003
  },
2596
- logger: logger2
3004
+ logger
2597
3005
  });
2598
3006
  } else {
2599
- logger2.info("Tapped streams NOT enabled", {
3007
+ logger.info("Tapped streams NOT enabled", {
2600
3008
  hasSessionId: !!config.sessionId,
2601
3009
  hasSessionStore: !!sessionStore
2602
3010
  });
2603
3011
  }
2604
3012
  const agentStream = ndJsonStream(agentWritable, streams.agent.readable);
2605
- const agentConnection = new AgentSideConnection(
2606
- (client) => new ClaudeAcpAgent(client, sessionStore),
2607
- agentStream
2608
- );
3013
+ const agentConnection = new AgentSideConnection((client) => {
3014
+ logger.info("Creating Claude agent");
3015
+ return new ClaudeAcpAgent(client, sessionStore);
3016
+ }, agentStream);
2609
3017
  return {
2610
3018
  agentConnection,
2611
3019
  clientStreams: {
@@ -2629,9 +3037,9 @@ import z2 from "zod";
2629
3037
  var PostHogFileManager = class {
2630
3038
  repositoryPath;
2631
3039
  logger;
2632
- constructor(repositoryPath, logger2) {
3040
+ constructor(repositoryPath, logger) {
2633
3041
  this.repositoryPath = repositoryPath;
2634
- this.logger = logger2 || new Logger({ debug: false, prefix: "[FileManager]" });
3042
+ this.logger = logger || new Logger({ debug: false, prefix: "[FileManager]" });
2635
3043
  }
2636
3044
  getTaskDirectory(taskId) {
2637
3045
  return join2(this.repositoryPath, ".posthog", taskId);
@@ -2747,35 +3155,6 @@ var PostHogFileManager = class {
2747
3155
  async readRequirements(taskId) {
2748
3156
  return await this.readTaskFile(taskId, "requirements.md");
2749
3157
  }
2750
- async writeResearch(taskId, data) {
2751
- this.logger.debug("Writing research", {
2752
- taskId,
2753
- score: data.actionabilityScore,
2754
- hasQuestions: !!data.questions,
2755
- questionCount: data.questions?.length ?? 0,
2756
- answered: data.answered ?? false
2757
- });
2758
- await this.writeTaskFile(taskId, {
2759
- name: "research.json",
2760
- content: JSON.stringify(data, null, 2),
2761
- type: "artifact"
2762
- });
2763
- this.logger.info("Research file written", {
2764
- taskId,
2765
- score: data.actionabilityScore,
2766
- hasQuestions: !!data.questions,
2767
- answered: data.answered ?? false
2768
- });
2769
- }
2770
- async readResearch(taskId) {
2771
- try {
2772
- const content = await this.readTaskFile(taskId, "research.json");
2773
- return content ? JSON.parse(content) : null;
2774
- } catch (error) {
2775
- this.logger.debug("Failed to parse research.json", { error });
2776
- return null;
2777
- }
2778
- }
2779
3158
  async writeTodos(taskId, data) {
2780
3159
  const todos = z2.object({
2781
3160
  metadata: z2.object({
@@ -2877,13 +3256,9 @@ import { promisify } from "util";
2877
3256
  var execAsync = promisify(exec);
2878
3257
  var GitManager = class {
2879
3258
  repositoryPath;
2880
- authorName;
2881
- authorEmail;
2882
3259
  logger;
2883
3260
  constructor(config) {
2884
3261
  this.repositoryPath = config.repositoryPath;
2885
- this.authorName = config.authorName;
2886
- this.authorEmail = config.authorEmail;
2887
3262
  this.logger = config.logger || new Logger({ debug: false, prefix: "[GitManager]" });
2888
3263
  }
2889
3264
  escapeShellArg(str) {
@@ -3057,11 +3432,6 @@ ${error}`);
3057
3432
  if (options?.allowEmpty) {
3058
3433
  command += " --allow-empty";
3059
3434
  }
3060
- const authorName = options?.authorName || this.authorName;
3061
- const authorEmail = options?.authorEmail || this.authorEmail;
3062
- if (authorName && authorEmail) {
3063
- command += ` --author="${authorName} <${authorEmail}>"`;
3064
- }
3065
3435
  return command;
3066
3436
  }
3067
3437
  async getRemoteUrl() {
@@ -3181,7 +3551,6 @@ ${error}`);
3181
3551
  const message = `\u{1F4CB} Add plan for task: ${taskTitle}
3182
3552
 
3183
3553
  Task ID: ${taskId}
3184
- Generated by PostHog Agent
3185
3554
 
3186
3555
  This commit contains the implementation plan and supporting documentation
3187
3556
  for the task. Review the plan before proceeding with implementation.`;
@@ -3198,8 +3567,7 @@ for the task. Review the plan before proceeding with implementation.`;
3198
3567
  }
3199
3568
  let message = `\u2728 Implement task: ${taskTitle}
3200
3569
 
3201
- Task ID: ${taskId}
3202
- Generated by PostHog Agent`;
3570
+ Task ID: ${taskId}`;
3203
3571
  if (planSummary) {
3204
3572
  message += `
3205
3573
 
@@ -3303,6 +3671,18 @@ This commit implements the changes described in the task plan.`;
3303
3671
  }
3304
3672
  };
3305
3673
 
3674
+ // src/utils/gateway.ts
3675
+ function getLlmGatewayUrl(posthogHost) {
3676
+ const url = new URL(posthogHost);
3677
+ const hostname = url.hostname;
3678
+ if (hostname === "localhost" || hostname === "127.0.0.1") {
3679
+ return `${url.protocol}//localhost:3308`;
3680
+ }
3681
+ const regionMatch = hostname.match(/^(us|eu)\.posthog\.com$/);
3682
+ const region = regionMatch ? regionMatch[1] : "us";
3683
+ return `https://gateway.${region}.posthog.com`;
3684
+ }
3685
+
3306
3686
  // src/posthog-api.ts
3307
3687
  var PostHogAPIClient = class {
3308
3688
  config;
@@ -3315,7 +3695,7 @@ var PostHogAPIClient = class {
3315
3695
  }
3316
3696
  get headers() {
3317
3697
  return {
3318
- Authorization: `Bearer ${this.config.apiKey}`,
3698
+ Authorization: `Bearer ${this.config.getApiKey()}`,
3319
3699
  "Content-Type": "application/json"
3320
3700
  };
3321
3701
  }
@@ -3347,11 +3727,10 @@ var PostHogAPIClient = class {
3347
3727
  return this.baseUrl;
3348
3728
  }
3349
3729
  getApiKey() {
3350
- return this.config.apiKey;
3730
+ return this.config.getApiKey();
3351
3731
  }
3352
3732
  getLlmGatewayUrl() {
3353
- const teamId = this.getTeamId();
3354
- return `${this.baseUrl}/api/projects/${teamId}/llm_gateway`;
3733
+ return getLlmGatewayUrl(this.baseUrl);
3355
3734
  }
3356
3735
  async fetchTask(taskId) {
3357
3736
  const teamId = this.getTeamId();
@@ -3583,487 +3962,71 @@ ${errorData.stack_trace}
3583
3962
  }
3584
3963
  };
3585
3964
 
3586
- // src/prompt-builder.ts
3587
- import { promises as fs3 } from "fs";
3588
- import { join as join3 } from "path";
3589
- var PromptBuilder = class {
3590
- getTaskFiles;
3591
- generatePlanTemplate;
3592
- posthogClient;
3965
+ // src/session-store.ts
3966
+ var SessionStore = class {
3967
+ posthogAPI;
3968
+ pendingEntries = /* @__PURE__ */ new Map();
3969
+ flushTimeouts = /* @__PURE__ */ new Map();
3970
+ configs = /* @__PURE__ */ new Map();
3593
3971
  logger;
3594
- constructor(deps) {
3595
- this.getTaskFiles = deps.getTaskFiles;
3596
- this.generatePlanTemplate = deps.generatePlanTemplate;
3597
- this.posthogClient = deps.posthogClient;
3598
- this.logger = deps.logger || new Logger({ debug: false, prefix: "[PromptBuilder]" });
3972
+ constructor(posthogAPI, logger) {
3973
+ this.posthogAPI = posthogAPI;
3974
+ this.logger = logger ?? new Logger({ debug: false, prefix: "[SessionStore]" });
3975
+ const flushAllAndExit = async () => {
3976
+ const flushPromises = [];
3977
+ for (const sessionId of this.configs.keys()) {
3978
+ flushPromises.push(this.flush(sessionId));
3979
+ }
3980
+ await Promise.all(flushPromises);
3981
+ process.exit(0);
3982
+ };
3983
+ process.on("beforeExit", () => {
3984
+ flushAllAndExit().catch((e) => this.logger.error("Flush failed:", e));
3985
+ });
3986
+ process.on("SIGINT", () => {
3987
+ flushAllAndExit().catch((e) => this.logger.error("Flush failed:", e));
3988
+ });
3989
+ process.on("SIGTERM", () => {
3990
+ flushAllAndExit().catch((e) => this.logger.error("Flush failed:", e));
3991
+ });
3599
3992
  }
3600
- /**
3601
- * Extract file paths from XML tags in description
3602
- * Format: <file path="relative/path.ts" />
3603
- */
3604
- extractFilePaths(description) {
3605
- const fileTagRegex = /<file\s+path="([^"]+)"\s*\/>/g;
3606
- const paths = [];
3607
- let match;
3608
- match = fileTagRegex.exec(description);
3609
- while (match !== null) {
3610
- paths.push(match[1]);
3611
- match = fileTagRegex.exec(description);
3612
- }
3613
- return paths;
3993
+ /** Register a session for persistence */
3994
+ register(sessionId, config) {
3995
+ this.configs.set(sessionId, config);
3996
+ }
3997
+ /** Unregister and flush pending */
3998
+ async unregister(sessionId) {
3999
+ await this.flush(sessionId);
4000
+ this.configs.delete(sessionId);
4001
+ }
4002
+ /** Check if a session is registered for persistence */
4003
+ isRegistered(sessionId) {
4004
+ return this.configs.has(sessionId);
3614
4005
  }
3615
4006
  /**
3616
- * Read file contents from repository
4007
+ * Append a raw JSON-RPC line for persistence.
4008
+ * Parses and wraps as StoredNotification for the API.
3617
4009
  */
3618
- async readFileContent(repositoryPath, filePath) {
4010
+ appendRawLine(sessionId, line) {
4011
+ const config = this.configs.get(sessionId);
4012
+ if (!config) {
4013
+ return;
4014
+ }
3619
4015
  try {
3620
- const fullPath = join3(repositoryPath, filePath);
3621
- const content = await fs3.readFile(fullPath, "utf8");
3622
- return content;
3623
- } catch (error) {
3624
- this.logger.warn(`Failed to read referenced file: ${filePath}`, {
3625
- error
3626
- });
3627
- return null;
3628
- }
3629
- }
3630
- /**
3631
- * Extract URL mentions from XML tags in description
3632
- * Formats: <error id="..." />, <experiment id="..." />, <url href="..." />
3633
- */
3634
- extractUrlMentions(description) {
3635
- const mentions = [];
3636
- const resourceRegex = /<(error|experiment|insight|feature_flag)\s+id="([^"]+)"\s*\/>/g;
3637
- let match;
3638
- match = resourceRegex.exec(description);
3639
- while (match !== null) {
3640
- const [, type, id] = match;
3641
- mentions.push({
3642
- url: "",
3643
- // Will be reconstructed if needed
3644
- type,
3645
- id,
3646
- label: this.generateUrlLabel("", type)
3647
- });
3648
- match = resourceRegex.exec(description);
3649
- }
3650
- const urlRegex = /<url\s+href="([^"]+)"\s*\/>/g;
3651
- match = urlRegex.exec(description);
3652
- while (match !== null) {
3653
- const [, url] = match;
3654
- mentions.push({
3655
- url,
3656
- type: "generic",
3657
- label: this.generateUrlLabel(url, "generic")
3658
- });
3659
- match = urlRegex.exec(description);
3660
- }
3661
- return mentions;
3662
- }
3663
- /**
3664
- * Generate a display label for a URL mention
3665
- */
3666
- generateUrlLabel(url, type) {
3667
- try {
3668
- const urlObj = new URL(url);
3669
- switch (type) {
3670
- case "error": {
3671
- const errorMatch = url.match(/error_tracking\/([a-f0-9-]+)/);
3672
- return errorMatch ? `Error ${errorMatch[1].slice(0, 8)}...` : "Error";
3673
- }
3674
- case "experiment": {
3675
- const expMatch = url.match(/experiments\/(\d+)/);
3676
- return expMatch ? `Experiment #${expMatch[1]}` : "Experiment";
3677
- }
3678
- case "insight":
3679
- return "Insight";
3680
- case "feature_flag":
3681
- return "Feature Flag";
3682
- default:
3683
- return urlObj.hostname;
3684
- }
3685
- } catch {
3686
- return "URL";
3687
- }
3688
- }
3689
- /**
3690
- * Process URL references and fetch their content
3691
- */
3692
- async processUrlReferences(description) {
3693
- const urlMentions = this.extractUrlMentions(description);
3694
- const referencedResources = [];
3695
- if (urlMentions.length === 0 || !this.posthogClient) {
3696
- return { description, referencedResources };
3697
- }
3698
- for (const mention of urlMentions) {
3699
- try {
3700
- const resource = await this.posthogClient.fetchResourceByUrl(mention);
3701
- referencedResources.push(resource);
3702
- } catch (error) {
3703
- this.logger.warn(`Failed to fetch resource from URL: ${mention.url}`, {
3704
- error
3705
- });
3706
- referencedResources.push({
3707
- type: mention.type,
3708
- id: mention.id || "",
3709
- url: mention.url,
3710
- title: mention.label || "Unknown Resource",
3711
- content: `Failed to fetch resource from ${mention.url}: ${error}`,
3712
- metadata: {}
3713
- });
3714
- }
3715
- }
3716
- let processedDescription = description;
3717
- for (const mention of urlMentions) {
3718
- if (mention.type === "generic") {
3719
- const escapedUrl = mention.url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3720
- processedDescription = processedDescription.replace(
3721
- new RegExp(`<url\\s+href="${escapedUrl}"\\s*/>`, "g"),
3722
- `@${mention.label}`
3723
- );
3724
- } else {
3725
- const escapedType = mention.type.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3726
- const escapedId = mention.id ? mention.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : "";
3727
- processedDescription = processedDescription.replace(
3728
- new RegExp(`<${escapedType}\\s+id="${escapedId}"\\s*/>`, "g"),
3729
- `@${mention.label}`
3730
- );
3731
- }
3732
- }
3733
- return { description: processedDescription, referencedResources };
3734
- }
3735
- /**
3736
- * Process description to extract file tags and read contents
3737
- * Returns processed description and referenced file contents
3738
- */
3739
- async processFileReferences(description, repositoryPath) {
3740
- const filePaths = this.extractFilePaths(description);
3741
- const referencedFiles = [];
3742
- if (filePaths.length === 0 || !repositoryPath) {
3743
- return { description, referencedFiles };
3744
- }
3745
- for (const filePath of filePaths) {
3746
- const content = await this.readFileContent(repositoryPath, filePath);
3747
- if (content !== null) {
3748
- referencedFiles.push({ path: filePath, content });
3749
- }
3750
- }
3751
- let processedDescription = description;
3752
- for (const filePath of filePaths) {
3753
- const fileName = filePath.split("/").pop() || filePath;
3754
- processedDescription = processedDescription.replace(
3755
- new RegExp(
3756
- `<file\\s+path="${filePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"\\s*/>`,
3757
- "g"
3758
- ),
3759
- `@${fileName}`
3760
- );
3761
- }
3762
- return { description: processedDescription, referencedFiles };
3763
- }
3764
- async buildResearchPrompt(task, repositoryPath) {
3765
- const { description: descriptionAfterFiles, referencedFiles } = await this.processFileReferences(task.description, repositoryPath);
3766
- const { description: processedDescription, referencedResources } = await this.processUrlReferences(descriptionAfterFiles);
3767
- let prompt = "<task>\n";
3768
- prompt += `<title>${task.title}</title>
3769
- `;
3770
- prompt += `<description>${processedDescription}</description>
3771
- `;
3772
- if (task.repository) {
3773
- prompt += `<repository>${task.repository}</repository>
3774
- `;
3775
- }
3776
- prompt += "</task>\n";
3777
- if (referencedFiles.length > 0) {
3778
- prompt += "\n<referenced_files>\n";
3779
- for (const file of referencedFiles) {
3780
- prompt += `<file path="${file.path}">
3781
- \`\`\`
3782
- ${file.content}
3783
- \`\`\`
3784
- </file>
3785
- `;
3786
- }
3787
- prompt += "</referenced_files>\n";
3788
- }
3789
- if (referencedResources.length > 0) {
3790
- prompt += "\n<referenced_resources>\n";
3791
- for (const resource of referencedResources) {
3792
- prompt += `<resource type="${resource.type}" url="${resource.url}">
3793
- `;
3794
- prompt += `<title>${resource.title}</title>
3795
- `;
3796
- prompt += `<content>${resource.content}</content>
3797
- `;
3798
- prompt += "</resource>\n";
3799
- }
3800
- prompt += "</referenced_resources>\n";
3801
- }
3802
- try {
3803
- const taskFiles = await this.getTaskFiles(task.id);
3804
- const contextFiles = taskFiles.filter(
3805
- (f) => f.type === "context" || f.type === "reference"
3806
- );
3807
- if (contextFiles.length > 0) {
3808
- prompt += "\n<supporting_files>\n";
3809
- for (const file of contextFiles) {
3810
- prompt += `<file name="${file.name}" type="${file.type}">
3811
- ${file.content}
3812
- </file>
3813
- `;
3814
- }
3815
- prompt += "</supporting_files>\n";
3816
- }
3817
- } catch (_error) {
3818
- this.logger.debug("No existing task files found for research", {
3819
- taskId: task.id
3820
- });
3821
- }
3822
- return prompt;
3823
- }
3824
- async buildPlanningPrompt(task, repositoryPath) {
3825
- const { description: descriptionAfterFiles, referencedFiles } = await this.processFileReferences(task.description, repositoryPath);
3826
- const { description: processedDescription, referencedResources } = await this.processUrlReferences(descriptionAfterFiles);
3827
- let prompt = "<task>\n";
3828
- prompt += `<title>${task.title}</title>
3829
- `;
3830
- prompt += `<description>${processedDescription}</description>
3831
- `;
3832
- if (task.repository) {
3833
- prompt += `<repository>${task.repository}</repository>
3834
- `;
3835
- }
3836
- prompt += "</task>\n";
3837
- if (referencedFiles.length > 0) {
3838
- prompt += "\n<referenced_files>\n";
3839
- for (const file of referencedFiles) {
3840
- prompt += `<file path="${file.path}">
3841
- \`\`\`
3842
- ${file.content}
3843
- \`\`\`
3844
- </file>
3845
- `;
3846
- }
3847
- prompt += "</referenced_files>\n";
3848
- }
3849
- if (referencedResources.length > 0) {
3850
- prompt += "\n<referenced_resources>\n";
3851
- for (const resource of referencedResources) {
3852
- prompt += `<resource type="${resource.type}" url="${resource.url}">
3853
- `;
3854
- prompt += `<title>${resource.title}</title>
3855
- `;
3856
- prompt += `<content>${resource.content}</content>
3857
- `;
3858
- prompt += "</resource>\n";
3859
- }
3860
- prompt += "</referenced_resources>\n";
3861
- }
3862
- try {
3863
- const taskFiles = await this.getTaskFiles(task.id);
3864
- const contextFiles = taskFiles.filter(
3865
- (f) => f.type === "context" || f.type === "reference"
3866
- );
3867
- if (contextFiles.length > 0) {
3868
- prompt += "\n<supporting_files>\n";
3869
- for (const file of contextFiles) {
3870
- prompt += `<file name="${file.name}" type="${file.type}">
3871
- ${file.content}
3872
- </file>
3873
- `;
3874
- }
3875
- prompt += "</supporting_files>\n";
3876
- }
3877
- } catch (_error) {
3878
- this.logger.debug("No existing task files found for planning", {
3879
- taskId: task.id
3880
- });
3881
- }
3882
- const templateVariables = {
3883
- task_id: task.id,
3884
- task_title: task.title,
3885
- task_description: processedDescription,
3886
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
3887
- repository: task.repository || ""
3888
- };
3889
- const planTemplate = await this.generatePlanTemplate(templateVariables);
3890
- prompt += "\n<instructions>\n";
3891
- prompt += "Analyze the codebase and create a detailed implementation plan. Use the template structure below, filling each section with specific, actionable information.\n";
3892
- prompt += "</instructions>\n\n";
3893
- prompt += "<plan_template>\n";
3894
- prompt += planTemplate;
3895
- prompt += "\n</plan_template>";
3896
- return prompt;
3897
- }
3898
- async buildExecutionPrompt(task, repositoryPath) {
3899
- const { description: descriptionAfterFiles, referencedFiles } = await this.processFileReferences(task.description, repositoryPath);
3900
- const { description: processedDescription, referencedResources } = await this.processUrlReferences(descriptionAfterFiles);
3901
- let prompt = "<task>\n";
3902
- prompt += `<title>${task.title}</title>
3903
- `;
3904
- prompt += `<description>${processedDescription}</description>
3905
- `;
3906
- if (task.repository) {
3907
- prompt += `<repository>${task.repository}</repository>
3908
- `;
3909
- }
3910
- prompt += "</task>\n";
3911
- if (referencedFiles.length > 0) {
3912
- prompt += "\n<referenced_files>\n";
3913
- for (const file of referencedFiles) {
3914
- prompt += `<file path="${file.path}">
3915
- \`\`\`
3916
- ${file.content}
3917
- \`\`\`
3918
- </file>
3919
- `;
3920
- }
3921
- prompt += "</referenced_files>\n";
3922
- }
3923
- if (referencedResources.length > 0) {
3924
- prompt += "\n<referenced_resources>\n";
3925
- for (const resource of referencedResources) {
3926
- prompt += `<resource type="${resource.type}" url="${resource.url}">
3927
- `;
3928
- prompt += `<title>${resource.title}</title>
3929
- `;
3930
- prompt += `<content>${resource.content}</content>
3931
- `;
3932
- prompt += "</resource>\n";
3933
- }
3934
- prompt += "</referenced_resources>\n";
3935
- }
3936
- try {
3937
- const taskFiles = await this.getTaskFiles(task.id);
3938
- const hasPlan = taskFiles.some((f) => f.type === "plan");
3939
- const todosFile = taskFiles.find(
3940
- (f) => f.name === "todos.json"
3941
- );
3942
- if (taskFiles.length > 0) {
3943
- prompt += "\n<context>\n";
3944
- for (const file of taskFiles) {
3945
- if (file.type === "plan") {
3946
- prompt += `<plan>
3947
- ${file.content}
3948
- </plan>
3949
- `;
3950
- } else if (file.name === "todos.json") {
3951
- } else {
3952
- prompt += `<file name="${file.name}" type="${file.type}">
3953
- ${file.content}
3954
- </file>
3955
- `;
3956
- }
3957
- }
3958
- prompt += "</context>\n";
3959
- }
3960
- if (todosFile) {
3961
- try {
3962
- const todos = JSON.parse(todosFile.content);
3963
- if (todos.items && todos.items.length > 0) {
3964
- prompt += "\n<previous_todos>\n";
3965
- prompt += "You previously created the following todo list for this task:\n\n";
3966
- for (const item of todos.items) {
3967
- const statusIcon = item.status === "completed" ? "\u2713" : item.status === "in_progress" ? "\u25B6" : "\u25CB";
3968
- prompt += `${statusIcon} [${item.status}] ${item.content}
3969
- `;
3970
- }
3971
- prompt += `
3972
- Progress: ${todos.metadata.completed}/${todos.metadata.total} completed
3973
- `;
3974
- prompt += "\nYou can reference this list when resuming work or create an updated list as needed.\n";
3975
- prompt += "</previous_todos>\n";
3976
- }
3977
- } catch (error) {
3978
- this.logger.debug("Failed to parse todos.json for context", {
3979
- error
3980
- });
3981
- }
3982
- }
3983
- prompt += "\n<instructions>\n";
3984
- if (hasPlan) {
3985
- prompt += "Implement the changes described in the execution plan. Follow the plan step-by-step and make the necessary file modifications.\n";
3986
- } else {
3987
- prompt += "Implement the changes described in the task. Make the necessary file modifications to complete the task.\n";
3988
- }
3989
- prompt += "</instructions>";
3990
- } catch (_error) {
3991
- this.logger.debug("No supporting files found for execution", {
3992
- taskId: task.id
3993
- });
3994
- prompt += "\n<instructions>\n";
3995
- prompt += "Implement the changes described in the task.\n";
3996
- prompt += "</instructions>";
3997
- }
3998
- return prompt;
3999
- }
4000
- };
4001
-
4002
- // src/session-store.ts
4003
- var SessionStore = class {
4004
- posthogAPI;
4005
- pendingEntries = /* @__PURE__ */ new Map();
4006
- flushTimeouts = /* @__PURE__ */ new Map();
4007
- configs = /* @__PURE__ */ new Map();
4008
- logger;
4009
- constructor(posthogAPI, logger2) {
4010
- this.posthogAPI = posthogAPI;
4011
- this.logger = logger2 ?? new Logger({ debug: false, prefix: "[SessionStore]" });
4012
- const flushAllAndExit = async () => {
4013
- const flushPromises = [];
4014
- for (const sessionId of this.configs.keys()) {
4015
- flushPromises.push(this.flush(sessionId));
4016
- }
4017
- await Promise.all(flushPromises);
4018
- process.exit(0);
4019
- };
4020
- process.on("beforeExit", () => {
4021
- flushAllAndExit().catch((e) => this.logger.error("Flush failed:", e));
4022
- });
4023
- process.on("SIGINT", () => {
4024
- flushAllAndExit().catch((e) => this.logger.error("Flush failed:", e));
4025
- });
4026
- process.on("SIGTERM", () => {
4027
- flushAllAndExit().catch((e) => this.logger.error("Flush failed:", e));
4028
- });
4029
- }
4030
- /** Register a session for persistence */
4031
- register(sessionId, config) {
4032
- this.configs.set(sessionId, config);
4033
- }
4034
- /** Unregister and flush pending */
4035
- async unregister(sessionId) {
4036
- await this.flush(sessionId);
4037
- this.configs.delete(sessionId);
4038
- }
4039
- /** Check if a session is registered for persistence */
4040
- isRegistered(sessionId) {
4041
- return this.configs.has(sessionId);
4042
- }
4043
- /**
4044
- * Append a raw JSON-RPC line for persistence.
4045
- * Parses and wraps as StoredNotification for the API.
4046
- */
4047
- appendRawLine(sessionId, line) {
4048
- const config = this.configs.get(sessionId);
4049
- if (!config) {
4050
- return;
4051
- }
4052
- try {
4053
- const message = JSON.parse(line);
4054
- const entry = {
4055
- type: "notification",
4056
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4057
- notification: message
4058
- };
4059
- const pending = this.pendingEntries.get(sessionId) ?? [];
4060
- pending.push(entry);
4061
- this.pendingEntries.set(sessionId, pending);
4062
- this.scheduleFlush(sessionId);
4063
- } catch {
4064
- this.logger.warn("Failed to parse raw line for persistence", {
4065
- sessionId,
4066
- lineLength: line.length
4016
+ const message = JSON.parse(line);
4017
+ const entry = {
4018
+ type: "notification",
4019
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4020
+ notification: message
4021
+ };
4022
+ const pending = this.pendingEntries.get(sessionId) ?? [];
4023
+ pending.push(entry);
4024
+ this.pendingEntries.set(sessionId, pending);
4025
+ this.scheduleFlush(sessionId);
4026
+ } catch {
4027
+ this.logger.warn("Failed to parse raw line for persistence", {
4028
+ sessionId,
4029
+ lineLength: line.length
4067
4030
  });
4068
4031
  }
4069
4032
  }
@@ -4304,1329 +4267,30 @@ var TaskManager = class {
4304
4267
  if (!execution.result) {
4305
4268
  execution.result = {
4306
4269
  status: "timeout",
4307
- message: "Execution timed out"
4308
- };
4309
- }
4310
- }
4311
- }, timeout);
4312
- }
4313
- cleanup(olderThan = 60 * 60 * 1e3) {
4314
- const cutoff = Date.now() - olderThan;
4315
- for (const [executionId, execution] of this.executionStates) {
4316
- if (execution.completedAt && execution.completedAt < cutoff) {
4317
- this.executionStates.delete(executionId);
4318
- }
4319
- }
4320
- }
4321
- };
4322
-
4323
- // src/template-manager.ts
4324
- import { existsSync as existsSync2, promises as fs4 } from "fs";
4325
- import { dirname, join as join4 } from "path";
4326
- import { fileURLToPath } from "url";
4327
- var logger = new Logger({ prefix: "[TemplateManager]" });
4328
- var TemplateManager = class {
4329
- templatesDir;
4330
- constructor() {
4331
- const __filename = fileURLToPath(import.meta.url);
4332
- const __dirname = dirname(__filename);
4333
- const candidateDirs = [
4334
- // Standard build output (dist/src/template-manager.js -> dist/templates)
4335
- join4(__dirname, "..", "templates"),
4336
- // If preserveModules creates nested structure (dist/src/template-manager.js -> dist/src/templates)
4337
- join4(__dirname, "templates"),
4338
- // Development scenarios (src/template-manager.ts -> src/templates)
4339
- join4(__dirname, "..", "..", "src", "templates"),
4340
- // Package root templates directory
4341
- join4(__dirname, "..", "..", "templates"),
4342
- // When node_modules symlink or installed (node_modules/@posthog/agent/dist/src/... -> node_modules/@posthog/agent/dist/templates)
4343
- join4(__dirname, "..", "..", "dist", "templates"),
4344
- // When consumed from node_modules deep in tree
4345
- join4(__dirname, "..", "..", "..", "templates"),
4346
- join4(__dirname, "..", "..", "..", "dist", "templates"),
4347
- join4(__dirname, "..", "..", "..", "src", "templates"),
4348
- // When bundled by Vite/Webpack (e.g., .vite/build/index.js -> node_modules/@posthog/agent/dist/templates)
4349
- // Try to find node_modules from current location
4350
- join4(
4351
- __dirname,
4352
- "..",
4353
- "node_modules",
4354
- "@posthog",
4355
- "agent",
4356
- "dist",
4357
- "templates"
4358
- ),
4359
- join4(
4360
- __dirname,
4361
- "..",
4362
- "..",
4363
- "node_modules",
4364
- "@posthog",
4365
- "agent",
4366
- "dist",
4367
- "templates"
4368
- ),
4369
- join4(
4370
- __dirname,
4371
- "..",
4372
- "..",
4373
- "..",
4374
- "node_modules",
4375
- "@posthog",
4376
- "agent",
4377
- "dist",
4378
- "templates"
4379
- )
4380
- ];
4381
- const resolvedDir = candidateDirs.find((dir) => existsSync2(dir));
4382
- if (!resolvedDir) {
4383
- logger.error("Could not find templates directory.");
4384
- logger.error(`Current file: ${__filename}`);
4385
- logger.error(`Current dir: ${__dirname}`);
4386
- logger.error(
4387
- `Tried: ${candidateDirs.map((d) => `
4388
- - ${d} (exists: ${existsSync2(d)})`).join("")}`
4389
- );
4390
- }
4391
- this.templatesDir = resolvedDir ?? candidateDirs[0];
4392
- }
4393
- async loadTemplate(templateName) {
4394
- try {
4395
- const templatePath = join4(this.templatesDir, templateName);
4396
- return await fs4.readFile(templatePath, "utf8");
4397
- } catch (error) {
4398
- throw new Error(
4399
- `Failed to load template ${templateName} from ${this.templatesDir}: ${error}`
4400
- );
4401
- }
4402
- }
4403
- substituteVariables(template, variables) {
4404
- let result = template;
4405
- for (const [key, value] of Object.entries(variables)) {
4406
- if (value !== void 0) {
4407
- const placeholder = new RegExp(`{{${key}}}`, "g");
4408
- result = result.replace(placeholder, value);
4409
- }
4410
- }
4411
- result = result.replace(/{{[^}]+}}/g, "[PLACEHOLDER]");
4412
- return result;
4413
- }
4414
- async generatePlan(variables) {
4415
- const template = await this.loadTemplate("plan-template.md");
4416
- return this.substituteVariables(template, {
4417
- ...variables,
4418
- date: variables.date || (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
4419
- });
4420
- }
4421
- async generateCustomFile(templateName, variables) {
4422
- const template = await this.loadTemplate(templateName);
4423
- return this.substituteVariables(template, {
4424
- ...variables,
4425
- date: variables.date || (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
4426
- });
4427
- }
4428
- async createTaskStructure(taskId, taskTitle, options) {
4429
- const files = [];
4430
- const variables = {
4431
- task_id: taskId,
4432
- task_title: taskTitle,
4433
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
4434
- };
4435
- if (options?.includePlan !== false) {
4436
- const planContent = await this.generatePlan(variables);
4437
- files.push({
4438
- name: "plan.md",
4439
- content: planContent,
4440
- type: "plan"
4441
- });
4442
- }
4443
- if (options?.additionalFiles) {
4444
- for (const file of options.additionalFiles) {
4445
- let content;
4446
- if (file.template) {
4447
- content = await this.generateCustomFile(file.template, variables);
4448
- } else if (file.content) {
4449
- content = this.substituteVariables(file.content, variables);
4450
- } else {
4451
- content = `# ${file.name}
4452
-
4453
- Placeholder content for ${file.name}`;
4454
- }
4455
- files.push({
4456
- name: file.name,
4457
- content,
4458
- type: file.name.includes("context") ? "context" : "reference"
4459
- });
4460
- }
4461
- }
4462
- return files;
4463
- }
4464
- generatePostHogReadme() {
4465
- return `# PostHog Task Files
4466
-
4467
- This directory contains task-related files generated by the PostHog Agent.
4468
-
4469
- ## Structure
4470
-
4471
- Each task has its own subdirectory: \`.posthog/{task-id}/\`
4472
-
4473
- ### Common Files
4474
-
4475
- - **plan.md** - Implementation plan generated during planning phase
4476
- - **Supporting files** - Any additional files added for task context
4477
- - **artifacts/** - Generated files, outputs, and temporary artifacts
4478
-
4479
- ### Usage
4480
-
4481
- These files are:
4482
- - Version controlled alongside your code
4483
- - Used by the PostHog Agent for context
4484
- - Available for review in pull requests
4485
- - Organized by task ID for easy reference
4486
-
4487
- ### Gitignore
4488
-
4489
- Customize \`.posthog/.gitignore\` to control which files are committed:
4490
- - Include plans and documentation by default
4491
- - Exclude temporary files and sensitive data
4492
- - Customize based on your team's needs
4493
-
4494
- ---
4495
-
4496
- *Generated by PostHog Agent*
4497
- `;
4498
- }
4499
- };
4500
-
4501
- // src/workflow/steps/build.ts
4502
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
4503
-
4504
- // src/agents/execution.ts
4505
- var EXECUTION_SYSTEM_PROMPT = `<role>
4506
- PostHog AI Execution Agent \u2014 autonomously implement tasks as merge-ready code following project conventions.
4507
- </role>
4508
-
4509
- <context>
4510
- You have access to local repository files and PostHog MCP server. Work primarily with local files for implementation. Commit changes regularly.
4511
- </context>
4512
-
4513
- <constraints>
4514
- - Follow existing code style, patterns, and conventions found in the repository
4515
- - Minimize new external dependencies \u2014 only add when necessary
4516
- - Implement structured logging and error handling (never log secrets)
4517
- - Avoid destructive shell commands
4518
- - Create/update .gitignore to exclude build artifacts, dependencies, and temp files
4519
- </constraints>
4520
-
4521
- <approach>
4522
- 1. Review the implementation plan if provided, or create your own todo list
4523
- 2. Execute changes step by step
4524
- 3. Test thoroughly and verify functionality
4525
- 4. Commit changes with clear messages
4526
- </approach>
4527
-
4528
- <checklist>
4529
- Before completing the task, verify:
4530
- - .gitignore includes build artifacts, node_modules, __pycache__, etc.
4531
- - Dependency files (package.json, requirements.txt) use exact versions
4532
- - Code compiles and tests pass
4533
- - Added or updated relevant tests
4534
- - Captured meaningful events with PostHog SDK where appropriate
4535
- - Wrapped new logic in PostHog feature flags where appropriate
4536
- - Updated documentation, README, or type hints as needed
4537
- </checklist>
4538
-
4539
- <output_format>
4540
- Provide a concise summary of changes made when finished.
4541
- </output_format>`;
4542
-
4543
- // src/todo-manager.ts
4544
- var TodoManager = class {
4545
- fileManager;
4546
- logger;
4547
- constructor(fileManager, logger2) {
4548
- this.fileManager = fileManager;
4549
- this.logger = logger2 || new Logger({ debug: false, prefix: "[TodoManager]" });
4550
- }
4551
- async readTodos(taskId) {
4552
- try {
4553
- const content = await this.fileManager.readTaskFile(taskId, "todos.json");
4554
- if (!content) {
4555
- return null;
4556
- }
4557
- const parsed = JSON.parse(content);
4558
- this.logger.debug("Loaded todos", {
4559
- taskId,
4560
- total: parsed.metadata.total,
4561
- pending: parsed.metadata.pending,
4562
- in_progress: parsed.metadata.in_progress,
4563
- completed: parsed.metadata.completed
4564
- });
4565
- return parsed;
4566
- } catch (error) {
4567
- this.logger.debug("Failed to read todos.json", {
4568
- taskId,
4569
- error: error instanceof Error ? error.message : String(error)
4570
- });
4571
- return null;
4572
- }
4573
- }
4574
- async writeTodos(taskId, todos) {
4575
- this.logger.debug("Writing todos", {
4576
- taskId,
4577
- total: todos.metadata.total,
4578
- pending: todos.metadata.pending,
4579
- in_progress: todos.metadata.in_progress,
4580
- completed: todos.metadata.completed
4581
- });
4582
- await this.fileManager.writeTaskFile(taskId, {
4583
- name: "todos.json",
4584
- content: JSON.stringify(todos, null, 2),
4585
- type: "artifact"
4586
- });
4587
- this.logger.info("Todos saved", {
4588
- taskId,
4589
- total: todos.metadata.total,
4590
- completed: todos.metadata.completed
4591
- });
4592
- }
4593
- parseTodoWriteInput(toolInput) {
4594
- const items = [];
4595
- if (toolInput.todos && Array.isArray(toolInput.todos)) {
4596
- for (const todo of toolInput.todos) {
4597
- items.push({
4598
- content: todo.content || "",
4599
- status: todo.status || "pending",
4600
- activeForm: todo.activeForm || todo.content || ""
4601
- });
4602
- }
4603
- }
4604
- const metadata = this.calculateMetadata(items);
4605
- return { items, metadata };
4606
- }
4607
- calculateMetadata(items) {
4608
- const total = items.length;
4609
- const pending = items.filter((t) => t.status === "pending").length;
4610
- const in_progress = items.filter((t) => t.status === "in_progress").length;
4611
- const completed = items.filter((t) => t.status === "completed").length;
4612
- return {
4613
- total,
4614
- pending,
4615
- in_progress,
4616
- completed,
4617
- last_updated: (/* @__PURE__ */ new Date()).toISOString()
4618
- };
4619
- }
4620
- async getTodoContext(taskId) {
4621
- const todos = await this.readTodos(taskId);
4622
- if (!todos || todos.items.length === 0) {
4623
- return "";
4624
- }
4625
- const lines = ["## Previous Todo List\n"];
4626
- lines.push("You previously created the following todo list:\n");
4627
- for (const item of todos.items) {
4628
- const statusIcon = item.status === "completed" ? "\u2713" : item.status === "in_progress" ? "\u25B6" : "\u25CB";
4629
- lines.push(`${statusIcon} [${item.status}] ${item.content}`);
4630
- }
4631
- lines.push(
4632
- `
4633
- Progress: ${todos.metadata.completed}/${todos.metadata.total} completed
4634
- `
4635
- );
4636
- return lines.join("\n");
4637
- }
4638
- // check for TodoWrite tool call and persist if found
4639
- async checkAndPersistFromMessage(message, taskId) {
4640
- if (message.type !== "assistant" || typeof message.message !== "object" || !message.message || !("content" in message.message) || !Array.isArray(message.message.content)) {
4641
- return null;
4642
- }
4643
- for (const block of message.message.content) {
4644
- if (block.type === "tool_use" && block.name === "TodoWrite") {
4645
- try {
4646
- this.logger.info("TodoWrite detected, persisting todos", { taskId });
4647
- const todoList = this.parseTodoWriteInput(block.input);
4648
- await this.writeTodos(taskId, todoList);
4649
- this.logger.info("Persisted todos successfully", {
4650
- taskId,
4651
- total: todoList.metadata.total,
4652
- completed: todoList.metadata.completed
4653
- });
4654
- return todoList;
4655
- } catch (error) {
4656
- this.logger.error("Failed to persist todos", {
4657
- taskId,
4658
- error: error instanceof Error ? error.message : String(error)
4659
- });
4660
- return null;
4661
- }
4662
- }
4663
- }
4664
- return null;
4665
- }
4666
- };
4667
-
4668
- // src/types.ts
4669
- var PermissionMode = /* @__PURE__ */ ((PermissionMode2) => {
4670
- PermissionMode2["PLAN"] = "plan";
4671
- PermissionMode2["DEFAULT"] = "default";
4672
- PermissionMode2["ACCEPT_EDITS"] = "acceptEdits";
4673
- PermissionMode2["BYPASS"] = "bypassPermissions";
4674
- return PermissionMode2;
4675
- })(PermissionMode || {});
4676
-
4677
- // src/workflow/steps/build.ts
4678
- var buildStep = async ({ step, context }) => {
4679
- const {
4680
- task,
4681
- cwd,
4682
- options,
4683
- logger: logger2,
4684
- promptBuilder,
4685
- sessionId,
4686
- mcpServers,
4687
- gitManager,
4688
- sendNotification
4689
- } = context;
4690
- const stepLogger = logger2.child("BuildStep");
4691
- const latestRun = task.latest_run;
4692
- const prExists = latestRun?.output && typeof latestRun.output === "object" ? latestRun.output.pr_url : null;
4693
- if (prExists) {
4694
- stepLogger.info("PR already exists, skipping build phase", {
4695
- taskId: task.id
4696
- });
4697
- return { status: "skipped" };
4698
- }
4699
- stepLogger.info("Starting build phase", { taskId: task.id });
4700
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_START, {
4701
- sessionId,
4702
- phase: "build"
4703
- });
4704
- const executionPrompt = await promptBuilder.buildExecutionPrompt(task, cwd);
4705
- const fullPrompt = `${EXECUTION_SYSTEM_PROMPT}
4706
-
4707
- ${executionPrompt}`;
4708
- const configuredPermissionMode = options.permissionMode ?? (typeof step.permissionMode === "string" ? step.permissionMode : step.permissionMode) ?? "acceptEdits" /* ACCEPT_EDITS */;
4709
- const baseOptions = {
4710
- model: step.model,
4711
- cwd,
4712
- permissionMode: configuredPermissionMode,
4713
- settingSources: ["local"],
4714
- mcpServers,
4715
- // Allow all tools for build phase - full read/write access needed for implementation
4716
- allowedTools: [
4717
- "Task",
4718
- "Bash",
4719
- "BashOutput",
4720
- "KillBash",
4721
- "Edit",
4722
- "Read",
4723
- "Write",
4724
- "Glob",
4725
- "Grep",
4726
- "NotebookEdit",
4727
- "WebFetch",
4728
- "WebSearch",
4729
- "ListMcpResources",
4730
- "ReadMcpResource",
4731
- "TodoWrite"
4732
- ]
4733
- };
4734
- if (options.canUseTool) {
4735
- baseOptions.canUseTool = options.canUseTool;
4736
- }
4737
- const response = query2({
4738
- prompt: fullPrompt,
4739
- options: { ...baseOptions, ...options.queryOverrides || {} }
4740
- });
4741
- const commitTracker = await gitManager.trackCommitsDuring();
4742
- const todoManager = new TodoManager(context.fileManager, stepLogger);
4743
- try {
4744
- for await (const message of response) {
4745
- const todoList = await todoManager.checkAndPersistFromMessage(
4746
- message,
4747
- task.id
4748
- );
4749
- if (todoList) {
4750
- await sendNotification(POSTHOG_NOTIFICATIONS.ARTIFACT, {
4751
- sessionId,
4752
- kind: "todos",
4753
- content: todoList
4754
- });
4755
- }
4756
- }
4757
- } catch (error) {
4758
- stepLogger.error("Error during build step query", error);
4759
- throw error;
4760
- }
4761
- const { commitCreated, pushedBranch } = await commitTracker.finalize({
4762
- commitMessage: `Implementation for ${task.title}`,
4763
- push: step.push
4764
- });
4765
- context.stepResults[step.id] = { commitCreated };
4766
- if (!commitCreated) {
4767
- stepLogger.warn("No changes to commit in build phase", { taskId: task.id });
4768
- } else {
4769
- stepLogger.info("Build commits finalized", {
4770
- taskId: task.id,
4771
- pushedBranch
4772
- });
4773
- }
4774
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
4775
- sessionId,
4776
- phase: "build"
4777
- });
4778
- return { status: "completed" };
4779
- };
4780
-
4781
- // src/workflow/utils.ts
4782
- async function finalizeStepGitActions(context, step, options) {
4783
- if (!step.commit) {
4784
- return false;
4785
- }
4786
- const { gitManager, logger: logger2 } = context;
4787
- const hasStagedChanges = await gitManager.hasStagedChanges();
4788
- if (!hasStagedChanges && !options.allowEmptyCommit) {
4789
- logger2.debug("No staged changes to commit for step", { stepId: step.id });
4790
- return false;
4791
- }
4792
- try {
4793
- await gitManager.commitChanges(options.commitMessage);
4794
- logger2.info("Committed changes for step", {
4795
- stepId: step.id,
4796
- message: options.commitMessage
4797
- });
4798
- } catch (error) {
4799
- logger2.error("Failed to commit changes for step", {
4800
- stepId: step.id,
4801
- error: error instanceof Error ? error.message : String(error)
4802
- });
4803
- throw error;
4804
- }
4805
- if (step.push) {
4806
- const branchName = await gitManager.getCurrentBranch();
4807
- await gitManager.pushBranch(branchName);
4808
- logger2.info("Pushed branch after step", {
4809
- stepId: step.id,
4810
- branch: branchName
4811
- });
4812
- }
4813
- return true;
4814
- }
4815
-
4816
- // src/workflow/steps/finalize.ts
4817
- var MAX_SNIPPET_LENGTH = 1200;
4818
- var finalizeStep = async ({ step, context }) => {
4819
- const { task, logger: logger2, fileManager, gitManager, posthogAPI, runId } = context;
4820
- const stepLogger = logger2.child("FinalizeStep");
4821
- const artifacts = await fileManager.collectTaskArtifacts(task.id);
4822
- let uploadedArtifacts;
4823
- if (artifacts.length && posthogAPI && runId) {
4824
- try {
4825
- const payload = artifacts.map((artifact) => ({
4826
- name: artifact.name,
4827
- type: artifact.type,
4828
- content: artifact.content,
4829
- content_type: artifact.contentType
4830
- }));
4831
- uploadedArtifacts = await posthogAPI.uploadTaskArtifacts(
4832
- task.id,
4833
- runId,
4834
- payload
4835
- );
4836
- stepLogger.info("Uploaded task artifacts to PostHog", {
4837
- taskId: task.id,
4838
- uploadedCount: uploadedArtifacts.length
4839
- });
4840
- } catch (error) {
4841
- stepLogger.warn("Failed to upload task artifacts", {
4842
- taskId: task.id,
4843
- error: error instanceof Error ? error.message : String(error)
4844
- });
4845
- }
4846
- } else {
4847
- stepLogger.debug("Skipping artifact upload", {
4848
- hasArtifacts: artifacts.length > 0,
4849
- hasPostHogApi: Boolean(posthogAPI),
4850
- runId
4851
- });
4852
- }
4853
- const prBody = buildPullRequestBody(task, artifacts, uploadedArtifacts);
4854
- await fileManager.cleanupTaskDirectory(task.id);
4855
- await gitManager.addAllPostHogFiles();
4856
- await finalizeStepGitActions(context, step, {
4857
- commitMessage: `Cleanup task artifacts for ${task.title}`,
4858
- allowEmptyCommit: true
4859
- });
4860
- context.stepResults[step.id] = {
4861
- prBody,
4862
- uploadedArtifacts,
4863
- artifactCount: artifacts.length
4864
- };
4865
- return { status: "completed" };
4866
- };
4867
- function buildPullRequestBody(task, artifacts, uploaded) {
4868
- const lines = [];
4869
- const taskSlug = task.slug || task.id;
4870
- lines.push("## Task context");
4871
- lines.push(`- **Task**: ${taskSlug}`);
4872
- lines.push(`- **Title**: ${task.title}`);
4873
- lines.push(`- **Origin**: ${task.origin_product}`);
4874
- if (task.description) {
4875
- lines.push("");
4876
- lines.push(`> ${task.description.trim().split("\n").join("\n> ")}`);
4877
- }
4878
- const usedFiles = /* @__PURE__ */ new Set();
4879
- const contextArtifact = artifacts.find(
4880
- (artifact) => artifact.name === "context.md"
4881
- );
4882
- if (contextArtifact) {
4883
- lines.push("");
4884
- lines.push("### Task prompt");
4885
- lines.push(contextArtifact.content);
4886
- usedFiles.add(contextArtifact.name);
4887
- }
4888
- const researchArtifact = artifacts.find(
4889
- (artifact) => artifact.name === "research.json"
4890
- );
4891
- if (researchArtifact) {
4892
- usedFiles.add(researchArtifact.name);
4893
- const researchSection = formatResearchSection(researchArtifact.content);
4894
- if (researchSection) {
4895
- lines.push("");
4896
- lines.push(researchSection);
4897
- }
4898
- }
4899
- const planArtifact = artifacts.find(
4900
- (artifact) => artifact.name === "plan.md"
4901
- );
4902
- if (planArtifact) {
4903
- lines.push("");
4904
- lines.push("### Implementation plan");
4905
- lines.push(planArtifact.content);
4906
- usedFiles.add(planArtifact.name);
4907
- }
4908
- const todoArtifact = artifacts.find(
4909
- (artifact) => artifact.name === "todos.json"
4910
- );
4911
- if (todoArtifact) {
4912
- const summary = summarizeTodos(todoArtifact.content);
4913
- if (summary) {
4914
- lines.push("");
4915
- lines.push("### Todo list");
4916
- lines.push(summary);
4917
- }
4918
- usedFiles.add(todoArtifact.name);
4919
- }
4920
- const remainingArtifacts = artifacts.filter(
4921
- (artifact) => !usedFiles.has(artifact.name)
4922
- );
4923
- if (remainingArtifacts.length) {
4924
- lines.push("");
4925
- lines.push("### Additional artifacts");
4926
- for (const artifact of remainingArtifacts) {
4927
- lines.push(`#### ${artifact.name}`);
4928
- lines.push(renderCodeFence(artifact.content));
4929
- }
4930
- }
4931
- const artifactList = uploaded ?? artifacts.map((artifact) => ({
4932
- name: artifact.name,
4933
- type: artifact.type
4934
- }));
4935
- if (artifactList.length) {
4936
- lines.push("");
4937
- lines.push("### Uploaded artifacts");
4938
- for (const artifact of artifactList) {
4939
- const rawStoragePath = "storage_path" in artifact ? artifact.storage_path : void 0;
4940
- const storagePath = typeof rawStoragePath === "string" ? rawStoragePath : void 0;
4941
- const storage = storagePath && storagePath.trim().length > 0 ? ` \u2013 \`${storagePath.trim()}\`` : "";
4942
- lines.push(`- ${artifact.name} (${artifact.type})${storage}`);
4943
- }
4944
- }
4945
- return lines.join("\n\n");
4946
- }
4947
- function renderCodeFence(content) {
4948
- const snippet = truncate(content, MAX_SNIPPET_LENGTH);
4949
- return ["```", snippet, "```"].join("\n");
4950
- }
4951
- function truncate(value, maxLength) {
4952
- if (value.length <= maxLength) {
4953
- return value;
4954
- }
4955
- return `${value.slice(0, maxLength)}
4956
- \u2026`;
4957
- }
4958
- function formatResearchSection(content) {
4959
- try {
4960
- const parsed = JSON.parse(content);
4961
- const sections = [];
4962
- if (parsed.context) {
4963
- sections.push("### Research summary");
4964
- sections.push(parsed.context);
4965
- }
4966
- if (parsed.questions?.length) {
4967
- sections.push("");
4968
- sections.push("### Questions needing answers");
4969
- for (const question of parsed.questions) {
4970
- sections.push(`- ${question.question ?? question}`);
4971
- }
4972
- }
4973
- if (parsed.answers?.length) {
4974
- sections.push("");
4975
- sections.push("### Answers provided");
4976
- for (const answer of parsed.answers) {
4977
- const questionId = answer.questionId ? ` (Q: ${answer.questionId})` : "";
4978
- sections.push(
4979
- `- ${answer.selectedOption || answer.customInput || "answer"}${questionId}`
4980
- );
4981
- }
4982
- }
4983
- return sections.length ? sections.join("\n") : null;
4984
- } catch {
4985
- return null;
4986
- }
4987
- }
4988
- function summarizeTodos(content) {
4989
- try {
4990
- const data = JSON.parse(content);
4991
- const total = data?.metadata?.total ?? data?.items?.length;
4992
- const completed = data?.metadata?.completed ?? data?.items?.filter(
4993
- (item) => item.status === "completed"
4994
- ).length;
4995
- const lines = [`Progress: ${completed}/${total} completed`];
4996
- if (data?.items?.length) {
4997
- for (const item of data.items) {
4998
- lines.push(`- [${item.status}] ${item.content}`);
4999
- }
5000
- }
5001
- return lines.join("\n");
5002
- } catch {
5003
- return null;
5004
- }
5005
- }
5006
-
5007
- // src/workflow/steps/plan.ts
5008
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
5009
-
5010
- // src/agents/planning.ts
5011
- var PLANNING_SYSTEM_PROMPT = `<role>
5012
- PostHog AI Planning Agent \u2014 analyze codebases and create actionable implementation plans.
5013
- </role>
5014
-
5015
- <constraints>
5016
- - Read-only: analyze files, search code, explore structure
5017
- - No modifications or edits
5018
- - Output ONLY the plan markdown \u2014 no preamble, no acknowledgment, no meta-commentary
5019
- </constraints>
5020
-
5021
- <objective>
5022
- Create a detailed, actionable implementation plan that an execution agent can follow to complete the task successfully.
5023
- </objective>
5024
-
5025
- <process>
5026
- 1. Explore repository structure and identify relevant files/components
5027
- 2. Understand existing patterns, conventions, and dependencies
5028
- 3. Break down task requirements and identify technical constraints
5029
- 4. Define step-by-step implementation approach
5030
- 5. Specify files to modify/create with exact paths
5031
- 6. Identify testing requirements and potential risks
5032
- </process>
5033
-
5034
- <output_format>
5035
- Output the plan DIRECTLY as markdown with NO preamble text. Do NOT say "I'll create a plan" or "Here's the plan" \u2014 just output the plan content.
5036
-
5037
- Required sections (follow the template provided in the task prompt):
5038
- - Summary: Brief overview of approach
5039
- - Files to Create/Modify: Specific paths and purposes
5040
- - Implementation Steps: Ordered list of actions
5041
- - Testing Strategy: How to verify it works
5042
- - Considerations: Dependencies, risks, edge cases
5043
- </output_format>
5044
-
5045
- <examples>
5046
- <bad_example>
5047
- "Sure! I'll create a detailed implementation plan for you to add authentication. Here's what we'll do..."
5048
- Reason: No preamble \u2014 output the plan directly
5049
- </bad_example>
5050
-
5051
- <good_example>
5052
- "# Implementation Plan
5053
-
5054
- ## Summary
5055
- Add JWT-based authentication to API endpoints using existing middleware pattern...
5056
-
5057
- ## Files to Modify
5058
- - src/middleware/auth.ts: Add JWT verification
5059
- ..."
5060
- Reason: Direct plan output with no meta-commentary
5061
- </good_example>
5062
- </examples>
5063
-
5064
- <context_integration>
5065
- If research findings, context files, or reference materials are provided:
5066
- - Incorporate research findings into your analysis
5067
- - Follow patterns and approaches identified in research
5068
- - Build upon or refine any existing planning work
5069
- - Reference specific files and components mentioned in context
5070
- </context_integration>`;
5071
-
5072
- // src/workflow/steps/plan.ts
5073
- var planStep = async ({ step, context }) => {
5074
- const {
5075
- task,
5076
- cwd,
5077
- isCloudMode,
5078
- options,
5079
- logger: logger2,
5080
- fileManager,
5081
- gitManager,
5082
- promptBuilder,
5083
- sessionId,
5084
- mcpServers,
5085
- sendNotification
5086
- } = context;
5087
- const stepLogger = logger2.child("PlanStep");
5088
- const existingPlan = await fileManager.readPlan(task.id);
5089
- if (existingPlan) {
5090
- stepLogger.info("Plan already exists, skipping step", { taskId: task.id });
5091
- return { status: "skipped" };
5092
- }
5093
- const researchData = await fileManager.readResearch(task.id);
5094
- if (researchData?.questions && !researchData.answered) {
5095
- stepLogger.info("Waiting for answered research questions", {
5096
- taskId: task.id
5097
- });
5098
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
5099
- sessionId,
5100
- phase: "research_questions"
5101
- });
5102
- return { status: "skipped", halt: true };
5103
- }
5104
- stepLogger.info("Starting planning phase", { taskId: task.id });
5105
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_START, {
5106
- sessionId,
5107
- phase: "planning"
5108
- });
5109
- let researchContext = "";
5110
- if (researchData) {
5111
- researchContext += `## Research Context
5112
-
5113
- ${researchData.context}
5114
-
5115
- `;
5116
- if (researchData.keyFiles.length > 0) {
5117
- researchContext += `**Key Files:**
5118
- ${researchData.keyFiles.map((f) => `- ${f}`).join("\n")}
5119
-
5120
- `;
5121
- }
5122
- if (researchData.blockers && researchData.blockers.length > 0) {
5123
- researchContext += `**Considerations:**
5124
- ${researchData.blockers.map((b) => `- ${b}`).join("\n")}
5125
-
5126
- `;
5127
- }
5128
- if (researchData.questions && researchData.answers && researchData.answered) {
5129
- researchContext += `## Implementation Decisions
5130
-
5131
- `;
5132
- for (const question of researchData.questions) {
5133
- const answer = researchData.answers.find(
5134
- (a) => a.questionId === question.id
5135
- );
5136
- researchContext += `### ${question.question}
5137
-
5138
- `;
5139
- if (answer) {
5140
- researchContext += `**Selected:** ${answer.selectedOption}
5141
- `;
5142
- if (answer.customInput) {
5143
- researchContext += `**Details:** ${answer.customInput}
5144
- `;
5145
- }
5146
- } else {
5147
- researchContext += `**Selected:** Not answered
5148
- `;
5149
- }
5150
- researchContext += `
5151
- `;
5152
- }
5153
- }
5154
- }
5155
- const planningPrompt = await promptBuilder.buildPlanningPrompt(task, cwd);
5156
- const fullPrompt = `${PLANNING_SYSTEM_PROMPT}
5157
-
5158
- ${planningPrompt}
5159
-
5160
- ${researchContext}`;
5161
- const baseOptions = {
5162
- model: step.model,
5163
- cwd,
5164
- permissionMode: "plan",
5165
- settingSources: ["local"],
5166
- mcpServers,
5167
- // Allow research tools: read-only operations, web search, MCP resources, and ExitPlanMode
5168
- allowedTools: [
5169
- "Read",
5170
- "Glob",
5171
- "Grep",
5172
- "WebFetch",
5173
- "WebSearch",
5174
- "ListMcpResources",
5175
- "ReadMcpResource",
5176
- "ExitPlanMode",
5177
- "TodoWrite",
5178
- "BashOutput"
5179
- ]
5180
- };
5181
- const response = query3({
5182
- prompt: fullPrompt,
5183
- options: { ...baseOptions, ...options.queryOverrides || {} }
5184
- });
5185
- const todoManager = new TodoManager(fileManager, stepLogger);
5186
- let planContent = "";
5187
- try {
5188
- for await (const message of response) {
5189
- const todoList = await todoManager.checkAndPersistFromMessage(
5190
- message,
5191
- task.id
5192
- );
5193
- if (todoList) {
5194
- await sendNotification(POSTHOG_NOTIFICATIONS.ARTIFACT, {
5195
- sessionId,
5196
- kind: "todos",
5197
- content: todoList
5198
- });
5199
- }
5200
- if (message.type === "assistant" && message.message?.content) {
5201
- for (const block of message.message.content) {
5202
- if (block.type === "text" && block.text) {
5203
- planContent += `${block.text}
5204
- `;
5205
- }
5206
- }
5207
- }
5208
- }
5209
- } catch (error) {
5210
- stepLogger.error("Error during plan step query", error);
5211
- throw error;
5212
- }
5213
- if (planContent.trim()) {
5214
- await fileManager.writePlan(task.id, planContent.trim());
5215
- stepLogger.info("Plan completed", { taskId: task.id });
5216
- }
5217
- await gitManager.addAllPostHogFiles();
5218
- await finalizeStepGitActions(context, step, {
5219
- commitMessage: `Planning phase for ${task.title}`
5220
- });
5221
- if (!isCloudMode) {
5222
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
5223
- sessionId,
5224
- phase: "planning"
5225
- });
5226
- return { status: "completed", halt: true };
5227
- }
5228
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
5229
- sessionId,
5230
- phase: "planning"
5231
- });
5232
- return { status: "completed" };
5233
- };
5234
-
5235
- // src/workflow/steps/research.ts
5236
- import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
5237
-
5238
- // src/agents/research.ts
5239
- var RESEARCH_SYSTEM_PROMPT = `<role>
5240
- PostHog AI Research Agent \u2014 analyze codebases to evaluate task actionability and identify missing information.
5241
- </role>
5242
-
5243
- <constraints>
5244
- - Read-only: analyze files, search code, explore structure
5245
- - No modifications or code changes
5246
- - Output structured JSON only
5247
- </constraints>
5248
-
5249
- <objective>
5250
- Your PRIMARY goal is to evaluate whether a task is actionable and assign an actionability score.
5251
-
5252
- Calculate an actionabilityScore (0-1) based on:
5253
- - **Task clarity** (0.4 weight): Is the task description specific and unambiguous?
5254
- - **Codebase context** (0.3 weight): Can you locate the relevant code and patterns?
5255
- - **Architectural decisions** (0.2 weight): Are the implementation approaches clear?
5256
- - **Dependencies** (0.1 weight): Are required dependencies and constraints understood?
5257
-
5258
- If actionabilityScore < 0.7, generate specific clarifying questions to increase confidence.
5259
-
5260
- Questions must present complete implementation choices, NOT request information from the user:
5261
- options: array of strings
5262
- - GOOD: options: ["Use Redux Toolkit (matches pattern in src/store/)", "Zustand (lighter weight)"]
5263
- - BAD: "Tell me which state management library to use"
5264
- - GOOD: options: ["Place in Button.tsx (existing component)", "create NewButton.tsx (separate concerns)?"]
5265
- - BAD: "Where should I put this code?"
5266
-
5267
- DO NOT ask questions like "how should I fix this" or "tell me the pattern" \u2014 present concrete options that can be directly chosen and acted upon.
5268
- </objective>
5269
-
5270
- <process>
5271
- 1. Explore repository structure and identify relevant files/components
5272
- 2. Understand existing patterns, conventions, and dependencies
5273
- 3. Calculate actionabilityScore based on clarity, context, architecture, and dependencies
5274
- 4. Identify key files that will need modification
5275
- 5. If score < 0.7: generate 2-4 specific questions to resolve blockers
5276
- 6. Output JSON matching ResearchEvaluation schema
5277
- </process>
5278
-
5279
- <output_format>
5280
- Output ONLY valid JSON with no markdown wrappers, no preamble, no explanation:
5281
-
5282
- {
5283
- "actionabilityScore": 0.85,
5284
- "context": "Brief 2-3 sentence summary of the task and implementation approach",
5285
- "keyFiles": ["path/to/file1.ts", "path/to/file2.ts"],
5286
- "blockers": ["Optional: what's preventing full confidence"],
5287
- "questions": [
5288
- {
5289
- "id": "q1",
5290
- "question": "Specific architectural decision needed?",
5291
- "options": [
5292
- "First approach with concrete details",
5293
- "Alternative approach with concrete details",
5294
- "Third option if needed"
5295
- ]
5296
- }
5297
- ]
5298
- }
5299
-
5300
- Rules:
5301
- - actionabilityScore: number between 0 and 1
5302
- - context: concise summary for planning phase
5303
- - keyFiles: array of file paths that need modification
5304
- - blockers: optional array explaining confidence gaps
5305
- - questions: ONLY include if actionabilityScore < 0.7
5306
- - Each question must have 2-3 options (maximum 3)
5307
- - Max 3 questions total
5308
- - Options must be complete, actionable choices that require NO additional user input
5309
- - NEVER use options like "Tell me the pattern", "Show me examples", "Specify the approach"
5310
- - Each option must be a full implementation decision that can be directly acted upon
5311
- </output_format>
5312
-
5313
- <scoring_examples>
5314
- <example score="0.9">
5315
- Task: "Fix typo in login button text"
5316
- Reasoning: Completely clear task, found exact component, no architectural decisions
5317
- </example>
5318
-
5319
- <example score="0.75">
5320
- Task: "Add caching to API endpoints"
5321
- Reasoning: Clear goal, found endpoints, but multiple caching strategies possible
5322
- </example>
5323
-
5324
- <example score="0.55">
5325
- Task: "Improve performance"
5326
- Reasoning: Vague task, unclear scope, needs questions about which areas to optimize
5327
- Questions needed: Which features are slow? What metrics define success?
5328
- </example>
5329
-
5330
- <example score="0.3">
5331
- Task: "Add the new feature"
5332
- Reasoning: Extremely vague, no context, cannot locate relevant code
5333
- Questions needed: What feature? Which product area? What should it do?
5334
- </example>
5335
- </scoring_examples>
5336
-
5337
- <question_examples>
5338
- <good_example>
5339
- {
5340
- "id": "q1",
5341
- "question": "Which caching layer should we use for API responses?",
5342
- "options": [
5343
- "Redis with 1-hour TTL (existing infrastructure, requires Redis client setup)",
5344
- "In-memory LRU cache with 100MB limit (simpler, single-server only)",
5345
- "HTTP Cache-Control headers only (minimal backend changes, relies on browser/CDN)"
5346
- ]
5347
- }
5348
- Reason: Each option is a complete, actionable decision with concrete details
5349
- </good_example>
5350
-
5351
- <good_example>
5352
- {
5353
- "id": "q2",
5354
- "question": "Where should the new analytics tracking code be placed?",
5355
- "options": [
5356
- "In the existing UserAnalytics.ts module alongside page view tracking",
5357
- "Create a new EventTracking.ts module in src/analytics/ for all event tracking",
5358
- "Add directly to each component that needs tracking (no centralized module)"
5359
- ]
5360
- }
5361
- Reason: Specific file paths and architectural patterns, no user input needed
5362
- </good_example>
5363
-
5364
- <bad_example>
5365
- {
5366
- "id": "q1",
5367
- "question": "How should I implement this?",
5368
- "options": ["One way", "Another way"]
5369
- }
5370
- Reason: Too vague, doesn't explain the tradeoffs or provide concrete details
5371
- </bad_example>
5372
-
5373
- <bad_example>
5374
- {
5375
- "id": "q2",
5376
- "question": "Which pattern should we follow for state management?",
5377
- "options": [
5378
- "Tell me which pattern the codebase currently uses",
5379
- "Show me examples of state management",
5380
- "Whatever you think is best"
5381
- ]
5382
- }
5383
- Reason: Options request user input instead of being actionable choices. Should be concrete patterns like "Zustand stores (matching existing patterns in src/stores/)" or "React Context (simpler, no new dependencies)"
5384
- </bad_example>
5385
-
5386
- <bad_example>
5387
- {
5388
- "id": "q3",
5389
- "question": "What color scheme should the button use?",
5390
- "options": [
5391
- "Use the existing theme colors",
5392
- "Let me specify custom colors",
5393
- "Match the design system"
5394
- ]
5395
- }
5396
- Reason: "Let me specify" requires user input. Should be "Primary blue (#0066FF, existing theme)" or "Secondary gray (#6B7280, existing theme)"
5397
- </bad_example>
5398
- </question_examples>`;
5399
-
5400
- // src/workflow/steps/research.ts
5401
- var researchStep = async ({ step, context }) => {
5402
- const {
5403
- task,
5404
- cwd,
5405
- isCloudMode,
5406
- options,
5407
- logger: logger2,
5408
- fileManager,
5409
- gitManager,
5410
- promptBuilder,
5411
- sessionId,
5412
- mcpServers,
5413
- sendNotification
5414
- } = context;
5415
- const stepLogger = logger2.child("ResearchStep");
5416
- const existingResearch = await fileManager.readResearch(task.id);
5417
- if (existingResearch) {
5418
- stepLogger.info("Research already exists", {
5419
- taskId: task.id,
5420
- hasQuestions: !!existingResearch.questions,
5421
- answered: existingResearch.answered
5422
- });
5423
- if (existingResearch.questions && !existingResearch.answered) {
5424
- stepLogger.info("Re-emitting unanswered research questions", {
5425
- taskId: task.id,
5426
- questionCount: existingResearch.questions.length
5427
- });
5428
- await sendNotification(POSTHOG_NOTIFICATIONS.ARTIFACT, {
5429
- sessionId,
5430
- kind: "research_questions",
5431
- content: existingResearch.questions
5432
- });
5433
- if (!isCloudMode) {
5434
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
5435
- sessionId,
5436
- phase: "research"
5437
- });
5438
- return { status: "skipped", halt: true };
5439
- }
5440
- }
5441
- return { status: "skipped" };
5442
- }
5443
- stepLogger.info("Starting research phase", { taskId: task.id });
5444
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_START, {
5445
- sessionId,
5446
- phase: "research"
5447
- });
5448
- const researchPrompt = await promptBuilder.buildResearchPrompt(task, cwd);
5449
- const fullPrompt = `${RESEARCH_SYSTEM_PROMPT}
5450
-
5451
- ${researchPrompt}`;
5452
- const baseOptions = {
5453
- model: step.model,
5454
- cwd,
5455
- permissionMode: "plan",
5456
- settingSources: ["local"],
5457
- mcpServers,
5458
- // Allow research tools: read-only operations, web search, and MCP resources
5459
- allowedTools: [
5460
- "Read",
5461
- "Glob",
5462
- "Grep",
5463
- "WebFetch",
5464
- "WebSearch",
5465
- "ListMcpResources",
5466
- "ReadMcpResource",
5467
- "TodoWrite",
5468
- "BashOutput"
5469
- ]
5470
- };
5471
- const response = query4({
5472
- prompt: fullPrompt,
5473
- options: { ...baseOptions, ...options.queryOverrides || {} }
5474
- });
5475
- let jsonContent = "";
5476
- try {
5477
- for await (const message of response) {
5478
- if (message.type === "assistant" && message.message?.content) {
5479
- for (const c of message.message.content) {
5480
- if (c.type === "text" && c.text) {
5481
- jsonContent += c.text;
5482
- }
5483
- }
5484
- }
5485
- }
5486
- } catch (error) {
5487
- stepLogger.error("Error during research step query", error);
5488
- throw error;
5489
- }
5490
- if (!jsonContent.trim()) {
5491
- stepLogger.error("No JSON output from research agent", { taskId: task.id });
5492
- await sendNotification(POSTHOG_NOTIFICATIONS.ERROR, {
5493
- sessionId,
5494
- message: "Research agent returned no output"
5495
- });
5496
- return { status: "completed", halt: true };
5497
- }
5498
- let evaluation;
5499
- try {
5500
- const jsonMatch = jsonContent.match(/\{[\s\S]*\}/);
5501
- if (!jsonMatch) {
5502
- throw new Error("No JSON object found in response");
5503
- }
5504
- evaluation = JSON.parse(jsonMatch[0]);
5505
- stepLogger.info("Parsed research evaluation", {
5506
- taskId: task.id,
5507
- score: evaluation.actionabilityScore,
5508
- hasQuestions: !!evaluation.questions
5509
- });
5510
- } catch (error) {
5511
- stepLogger.error("Failed to parse research JSON", {
5512
- taskId: task.id,
5513
- error: error instanceof Error ? error.message : String(error),
5514
- content: jsonContent.substring(0, 500)
5515
- });
5516
- await sendNotification(POSTHOG_NOTIFICATIONS.ERROR, {
5517
- sessionId,
5518
- message: `Failed to parse research JSON: ${error instanceof Error ? error.message : String(error)}`
5519
- });
5520
- return { status: "completed", halt: true };
5521
- }
5522
- if (evaluation.questions && evaluation.questions.length > 0) {
5523
- evaluation.answered = false;
5524
- evaluation.answers = void 0;
5525
- }
5526
- await fileManager.writeResearch(task.id, evaluation);
5527
- stepLogger.info("Research evaluation written", {
5528
- taskId: task.id,
5529
- score: evaluation.actionabilityScore,
5530
- hasQuestions: !!evaluation.questions
5531
- });
5532
- await sendNotification(POSTHOG_NOTIFICATIONS.ARTIFACT, {
5533
- sessionId,
5534
- kind: "research_evaluation",
5535
- content: evaluation
5536
- });
5537
- await gitManager.addAllPostHogFiles();
5538
- await finalizeStepGitActions(context, step, {
5539
- commitMessage: `Research phase for ${task.title}`
5540
- });
5541
- if (evaluation.actionabilityScore < 0.7 && evaluation.questions && evaluation.questions.length > 0) {
5542
- stepLogger.info("Actionability score below threshold, questions needed", {
5543
- taskId: task.id,
5544
- score: evaluation.actionabilityScore,
5545
- questionCount: evaluation.questions.length
5546
- });
5547
- await sendNotification(POSTHOG_NOTIFICATIONS.ARTIFACT, {
5548
- sessionId,
5549
- kind: "research_questions",
5550
- content: evaluation.questions
5551
- });
5552
- } else {
5553
- stepLogger.info("Actionability score acceptable, proceeding to planning", {
5554
- taskId: task.id,
5555
- score: evaluation.actionabilityScore
5556
- });
5557
- }
5558
- if (!isCloudMode) {
5559
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
5560
- sessionId,
5561
- phase: "research"
5562
- });
5563
- return { status: "completed", halt: true };
4270
+ message: "Execution timed out"
4271
+ };
4272
+ }
4273
+ }
4274
+ }, timeout);
5564
4275
  }
5565
- const researchData = await fileManager.readResearch(task.id);
5566
- if (researchData?.questions && !researchData.answered) {
5567
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
5568
- sessionId,
5569
- phase: "research"
5570
- });
5571
- return { status: "completed", halt: true };
4276
+ cleanup(olderThan = 60 * 60 * 1e3) {
4277
+ const cutoff = Date.now() - olderThan;
4278
+ for (const [executionId, execution] of this.executionStates) {
4279
+ if (execution.completedAt && execution.completedAt < cutoff) {
4280
+ this.executionStates.delete(executionId);
4281
+ }
4282
+ }
5572
4283
  }
5573
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
5574
- sessionId,
5575
- phase: "research"
5576
- });
5577
- return { status: "completed" };
5578
4284
  };
5579
4285
 
5580
- // src/workflow/config.ts
5581
- var MODELS = {
5582
- SONNET: "claude-sonnet-4-5",
5583
- HAIKU: "claude-haiku-4-5"
5584
- };
5585
- var TASK_WORKFLOW = [
5586
- {
5587
- id: "research",
5588
- name: "Research",
5589
- agent: "research",
5590
- model: MODELS.HAIKU,
5591
- permissionMode: "plan",
5592
- commit: true,
5593
- push: true,
5594
- run: researchStep
5595
- },
5596
- {
5597
- id: "plan",
5598
- name: "Plan",
5599
- agent: "planning",
5600
- model: MODELS.SONNET,
5601
- permissionMode: "plan",
5602
- commit: true,
5603
- push: true,
5604
- run: planStep
5605
- },
5606
- {
5607
- id: "build",
5608
- name: "Build",
5609
- agent: "execution",
5610
- model: MODELS.SONNET,
5611
- permissionMode: "acceptEdits",
5612
- commit: true,
5613
- push: true,
5614
- run: buildStep
5615
- },
5616
- {
5617
- id: "finalize",
5618
- name: "Finalize",
5619
- agent: "system",
5620
- // not used
5621
- model: MODELS.HAIKU,
5622
- // not used
5623
- permissionMode: "plan",
5624
- // not used
5625
- commit: true,
5626
- push: true,
5627
- run: finalizeStep
5628
- }
5629
- ];
4286
+ // src/types.ts
4287
+ var PermissionMode = /* @__PURE__ */ ((PermissionMode2) => {
4288
+ PermissionMode2["PLAN"] = "plan";
4289
+ PermissionMode2["DEFAULT"] = "default";
4290
+ PermissionMode2["ACCEPT_EDITS"] = "acceptEdits";
4291
+ PermissionMode2["BYPASS"] = "bypassPermissions";
4292
+ return PermissionMode2;
4293
+ })(PermissionMode || {});
5630
4294
 
5631
4295
  // src/agent.ts
5632
4296
  var Agent = class {
@@ -5635,10 +4299,8 @@ var Agent = class {
5635
4299
  posthogAPI;
5636
4300
  fileManager;
5637
4301
  gitManager;
5638
- templateManager;
5639
4302
  logger;
5640
4303
  acpConnection;
5641
- promptBuilder;
5642
4304
  mcpServers;
5643
4305
  canUseTool;
5644
4306
  currentRunId;
@@ -5650,8 +4312,8 @@ var Agent = class {
5650
4312
  this.debug = config.debug || false;
5651
4313
  const posthogMcpUrl = config.posthogMcpUrl || process.env.POSTHOG_MCP_URL || "https://mcp.posthog.com/mcp";
5652
4314
  const headers = {};
5653
- if (config.posthogApiKey) {
5654
- headers.Authorization = `Bearer ${config.posthogApiKey}`;
4315
+ if (config.getPosthogApiKey) {
4316
+ headers.Authorization = `Bearer ${config.getPosthogApiKey()}`;
5655
4317
  }
5656
4318
  const defaultMcpServers = {
5657
4319
  posthog: {
@@ -5678,11 +4340,10 @@ var Agent = class {
5678
4340
  repositoryPath: this.workingDirectory,
5679
4341
  logger: this.logger.child("GitManager")
5680
4342
  });
5681
- this.templateManager = new TemplateManager();
5682
- if (config.posthogApiUrl && config.posthogApiKey && config.posthogProjectId) {
4343
+ if (config.posthogApiUrl && config.getPosthogApiKey && config.posthogProjectId) {
5683
4344
  this.posthogAPI = new PostHogAPIClient({
5684
4345
  apiUrl: config.posthogApiUrl,
5685
- apiKey: config.posthogApiKey,
4346
+ getApiKey: config.getPosthogApiKey,
5686
4347
  projectId: config.posthogProjectId
5687
4348
  });
5688
4349
  this.sessionStore = new SessionStore(
@@ -5690,12 +4351,6 @@ var Agent = class {
5690
4351
  this.logger.child("SessionStore")
5691
4352
  );
5692
4353
  }
5693
- this.promptBuilder = new PromptBuilder({
5694
- getTaskFiles: (taskId) => this.getTaskFiles(taskId),
5695
- generatePlanTemplate: (vars) => this.templateManager.generatePlan(vars),
5696
- posthogClient: this.posthogAPI,
5697
- logger: this.logger.child("PromptBuilder")
5698
- });
5699
4354
  }
5700
4355
  /**
5701
4356
  * Enable or disable debug logging
@@ -5705,7 +4360,7 @@ var Agent = class {
5705
4360
  this.logger.setDebug(enabled);
5706
4361
  }
5707
4362
  /**
5708
- * Configure LLM gateway environment variables for Claude Code CLI
4363
+ * Configure LLM gateway environment variables for Claude Code CLI.
5709
4364
  */
5710
4365
  async _configureLlmGateway() {
5711
4366
  if (!this.posthogAPI) {
@@ -5717,93 +4372,20 @@ var Agent = class {
5717
4372
  process.env.ANTHROPIC_BASE_URL = gatewayUrl;
5718
4373
  process.env.ANTHROPIC_AUTH_TOKEN = apiKey;
5719
4374
  this.ensureOpenAIGatewayEnv(gatewayUrl, apiKey);
4375
+ this.ensureGeminiGatewayEnv(gatewayUrl, apiKey);
5720
4376
  } catch (error) {
5721
4377
  this.logger.error("Failed to configure LLM gateway", error);
5722
4378
  throw error;
5723
4379
  }
5724
4380
  }
5725
- getOrCreateConnection() {
5726
- if (!this.acpConnection) {
5727
- this.acpConnection = createAcpConnection({
5728
- sessionStore: this.sessionStore
5729
- });
5730
- }
5731
- return this.acpConnection;
5732
- }
5733
- // Adaptive task execution orchestrated via workflow steps
5734
- async runTask(taskId, taskRunId, options = {}) {
5735
- const task = await this.fetchTask(taskId);
5736
- const cwd = options.repositoryPath || this.workingDirectory;
5737
- const isCloudMode = options.isCloudMode ?? false;
5738
- const taskSlug = task.slug || task.id;
5739
- this.currentRunId = taskRunId;
5740
- this.logger.info("Starting adaptive task execution", {
5741
- taskId: task.id,
5742
- taskSlug,
5743
- taskRunId,
5744
- isCloudMode
5745
- });
5746
- const connection = this.getOrCreateConnection();
5747
- const sendNotification = async (method, params) => {
5748
- this.logger.debug(`Notification: ${method}`, params);
5749
- await connection.agentConnection.extNotification?.(method, params);
5750
- };
5751
- await sendNotification(POSTHOG_NOTIFICATIONS.RUN_STARTED, {
5752
- sessionId: taskRunId,
5753
- runId: taskRunId
5754
- });
5755
- await this.prepareTaskBranch(taskSlug, isCloudMode, sendNotification);
5756
- let taskError;
5757
- try {
5758
- const workflowContext = {
5759
- task,
5760
- taskSlug,
5761
- runId: taskRunId,
5762
- cwd,
5763
- isCloudMode,
5764
- options,
5765
- logger: this.logger,
5766
- fileManager: this.fileManager,
5767
- gitManager: this.gitManager,
5768
- promptBuilder: this.promptBuilder,
5769
- connection: connection.agentConnection,
5770
- sessionId: taskRunId,
5771
- sendNotification,
5772
- mcpServers: this.mcpServers,
5773
- posthogAPI: this.posthogAPI,
5774
- stepResults: {}
5775
- };
5776
- for (const step of TASK_WORKFLOW) {
5777
- const result = await step.run({ step, context: workflowContext });
5778
- if (result.halt) {
5779
- return;
5780
- }
5781
- }
5782
- const shouldCreatePR = options.createPR ?? isCloudMode;
5783
- if (shouldCreatePR) {
5784
- await this.ensurePullRequest(
5785
- task,
5786
- workflowContext.stepResults,
5787
- sendNotification
5788
- );
5789
- }
5790
- this.logger.info("Task execution complete", { taskId: task.id });
5791
- await sendNotification(POSTHOG_NOTIFICATIONS.TASK_COMPLETE, {
5792
- sessionId: taskRunId,
5793
- taskId: task.id
5794
- });
5795
- } catch (error) {
5796
- taskError = error instanceof Error ? error : new Error(String(error));
5797
- this.logger.error("Task execution failed", {
5798
- taskId: task.id,
5799
- error: taskError.message
5800
- });
5801
- await sendNotification(POSTHOG_NOTIFICATIONS.ERROR, {
5802
- sessionId: taskRunId,
5803
- message: taskError.message
5804
- });
5805
- throw taskError;
5806
- }
4381
+ /**
4382
+ * @deprecated Use runTaskV2() for local execution or runTaskCloud() for cloud execution.
4383
+ * This method used the old workflow system which has been removed.
4384
+ */
4385
+ async runTask(_taskId, _taskRunId, _options = {}) {
4386
+ throw new Error(
4387
+ "runTask() is deprecated. Use runTaskV2() for local execution or runTaskCloud() for cloud execution."
4388
+ );
5807
4389
  }
5808
4390
  /**
5809
4391
  * Creates an in-process ACP connection for client communication.
@@ -5814,15 +4396,14 @@ var Agent = class {
5814
4396
  */
5815
4397
  async runTaskV2(taskId, taskRunId, options = {}) {
5816
4398
  await this._configureLlmGateway();
5817
- const task = await this.fetchTask(taskId);
5818
- const taskSlug = task.slug || task.id;
5819
4399
  const isCloudMode = options.isCloudMode ?? false;
5820
4400
  const _cwd = options.repositoryPath || this.workingDirectory;
5821
4401
  this.currentRunId = taskRunId;
5822
4402
  this.acpConnection = createAcpConnection({
4403
+ framework: options.framework,
5823
4404
  sessionStore: this.sessionStore,
5824
4405
  sessionId: taskRunId,
5825
- taskId: task.id
4406
+ taskId
5826
4407
  });
5827
4408
  const sendNotification = async (method, params) => {
5828
4409
  this.logger.debug(`Notification: ${method}`, params);
@@ -5831,11 +4412,15 @@ var Agent = class {
5831
4412
  params
5832
4413
  );
5833
4414
  };
5834
- await sendNotification(POSTHOG_NOTIFICATIONS.RUN_STARTED, {
5835
- sessionId: taskRunId,
5836
- runId: taskRunId
5837
- });
4415
+ if (!options.isReconnect) {
4416
+ await sendNotification(POSTHOG_NOTIFICATIONS.RUN_STARTED, {
4417
+ sessionId: taskRunId,
4418
+ runId: taskRunId
4419
+ });
4420
+ }
5838
4421
  if (!options.skipGitBranch) {
4422
+ const task = options.task ?? await this.fetchTask(taskId);
4423
+ const taskSlug = task.slug || task.id;
5839
4424
  try {
5840
4425
  await this.prepareTaskBranch(taskSlug, isCloudMode, sendNotification);
5841
4426
  } catch (error) {
@@ -5900,9 +4485,7 @@ var Agent = class {
5900
4485
  **Description**: ${taskDescription}
5901
4486
 
5902
4487
  ## Changes
5903
- This PR implements the changes described in the task.
5904
-
5905
- Generated by PostHog Agent`;
4488
+ This PR implements the changes described in the task.`;
5906
4489
  const prBody = customBody || defaultBody;
5907
4490
  const prUrl = await this.gitManager.createPullRequest(
5908
4491
  branchName,
@@ -6021,6 +4604,16 @@ Generated by PostHog Agent`;
6021
4604
  process.env.OPENAI_API_KEY = resolvedToken;
6022
4605
  }
6023
4606
  }
4607
+ ensureGeminiGatewayEnv(gatewayUrl, token) {
4608
+ const resolvedGatewayUrl = gatewayUrl || process.env.ANTHROPIC_BASE_URL;
4609
+ const resolvedToken = token || process.env.ANTHROPIC_AUTH_TOKEN;
4610
+ if (resolvedGatewayUrl) {
4611
+ process.env.GEMINI_BASE_URL = resolvedGatewayUrl;
4612
+ }
4613
+ if (resolvedToken) {
4614
+ process.env.GEMINI_API_KEY = resolvedToken;
4615
+ }
4616
+ }
6024
4617
  async runTaskCloud(taskId, taskRunId, options = {}) {
6025
4618
  await this._configureLlmGateway();
6026
4619
  const task = await this.fetchTask(taskId);
@@ -6181,47 +4774,6 @@ ${task.description}`
6181
4774
  throw error;
6182
4775
  }
6183
4776
  }
6184
- async ensurePullRequest(task, stepResults, sendNotification) {
6185
- const latestRun = task.latest_run;
6186
- const existingPr = latestRun?.output && typeof latestRun.output === "object" ? latestRun.output.pr_url : null;
6187
- if (existingPr) {
6188
- this.logger.info("PR already exists, skipping creation", {
6189
- taskId: task.id,
6190
- prUrl: existingPr
6191
- });
6192
- return;
6193
- }
6194
- const buildResult = stepResults.build;
6195
- if (!buildResult?.commitCreated) {
6196
- this.logger.warn(
6197
- "Build step did not produce a commit; skipping PR creation",
6198
- { taskId: task.id }
6199
- );
6200
- return;
6201
- }
6202
- const branchName = await this.gitManager.getCurrentBranch();
6203
- const finalizeResult = stepResults.finalize;
6204
- const prBody = finalizeResult?.prBody;
6205
- const prUrl = await this.createPullRequest(
6206
- task.id,
6207
- branchName,
6208
- task.title,
6209
- task.description ?? "",
6210
- prBody
6211
- );
6212
- await sendNotification(POSTHOG_NOTIFICATIONS.PR_CREATED, { prUrl });
6213
- try {
6214
- await this.attachPullRequestToTask(task.id, prUrl, branchName);
6215
- this.logger.info("PR attached to task successfully", {
6216
- taskId: task.id,
6217
- prUrl
6218
- });
6219
- } catch (error) {
6220
- this.logger.warn("Could not attach PR to task", {
6221
- error: error instanceof Error ? error.message : String(error)
6222
- });
6223
- }
6224
- }
6225
4777
  };
6226
4778
 
6227
4779
  // src/schemas.ts
@@ -6396,6 +4948,131 @@ function parseAgentEvents(inputs) {
6396
4948
  return inputs.map((input) => parseAgentEvent(input)).filter((event) => event !== null);
6397
4949
  }
6398
4950
 
4951
+ // src/todo-manager.ts
4952
+ var TodoManager = class {
4953
+ fileManager;
4954
+ logger;
4955
+ constructor(fileManager, logger) {
4956
+ this.fileManager = fileManager;
4957
+ this.logger = logger || new Logger({ debug: false, prefix: "[TodoManager]" });
4958
+ }
4959
+ async readTodos(taskId) {
4960
+ try {
4961
+ const content = await this.fileManager.readTaskFile(taskId, "todos.json");
4962
+ if (!content) {
4963
+ return null;
4964
+ }
4965
+ const parsed = JSON.parse(content);
4966
+ this.logger.debug("Loaded todos", {
4967
+ taskId,
4968
+ total: parsed.metadata.total,
4969
+ pending: parsed.metadata.pending,
4970
+ in_progress: parsed.metadata.in_progress,
4971
+ completed: parsed.metadata.completed
4972
+ });
4973
+ return parsed;
4974
+ } catch (error) {
4975
+ this.logger.debug("Failed to read todos.json", {
4976
+ taskId,
4977
+ error: error instanceof Error ? error.message : String(error)
4978
+ });
4979
+ return null;
4980
+ }
4981
+ }
4982
+ async writeTodos(taskId, todos) {
4983
+ this.logger.debug("Writing todos", {
4984
+ taskId,
4985
+ total: todos.metadata.total,
4986
+ pending: todos.metadata.pending,
4987
+ in_progress: todos.metadata.in_progress,
4988
+ completed: todos.metadata.completed
4989
+ });
4990
+ await this.fileManager.writeTaskFile(taskId, {
4991
+ name: "todos.json",
4992
+ content: JSON.stringify(todos, null, 2),
4993
+ type: "artifact"
4994
+ });
4995
+ this.logger.info("Todos saved", {
4996
+ taskId,
4997
+ total: todos.metadata.total,
4998
+ completed: todos.metadata.completed
4999
+ });
5000
+ }
5001
+ parseTodoWriteInput(toolInput) {
5002
+ const items = [];
5003
+ if (toolInput.todos && Array.isArray(toolInput.todos)) {
5004
+ for (const todo of toolInput.todos) {
5005
+ items.push({
5006
+ content: todo.content || "",
5007
+ status: todo.status || "pending",
5008
+ activeForm: todo.activeForm || todo.content || ""
5009
+ });
5010
+ }
5011
+ }
5012
+ const metadata = this.calculateMetadata(items);
5013
+ return { items, metadata };
5014
+ }
5015
+ calculateMetadata(items) {
5016
+ const total = items.length;
5017
+ const pending = items.filter((t) => t.status === "pending").length;
5018
+ const in_progress = items.filter((t) => t.status === "in_progress").length;
5019
+ const completed = items.filter((t) => t.status === "completed").length;
5020
+ return {
5021
+ total,
5022
+ pending,
5023
+ in_progress,
5024
+ completed,
5025
+ last_updated: (/* @__PURE__ */ new Date()).toISOString()
5026
+ };
5027
+ }
5028
+ async getTodoContext(taskId) {
5029
+ const todos = await this.readTodos(taskId);
5030
+ if (!todos || todos.items.length === 0) {
5031
+ return "";
5032
+ }
5033
+ const lines = ["## Previous Todo List\n"];
5034
+ lines.push("You previously created the following todo list:\n");
5035
+ for (const item of todos.items) {
5036
+ const statusIcon = item.status === "completed" ? "\u2713" : item.status === "in_progress" ? "\u25B6" : "\u25CB";
5037
+ lines.push(`${statusIcon} [${item.status}] ${item.content}`);
5038
+ }
5039
+ lines.push(
5040
+ `
5041
+ Progress: ${todos.metadata.completed}/${todos.metadata.total} completed
5042
+ `
5043
+ );
5044
+ return lines.join("\n");
5045
+ }
5046
+ // check for TodoWrite tool call and persist if found
5047
+ async checkAndPersistFromMessage(message, taskId) {
5048
+ if (message.type !== "assistant" || typeof message.message !== "object" || !message.message || !("content" in message.message) || !Array.isArray(message.message.content)) {
5049
+ return null;
5050
+ }
5051
+ for (const block of message.message.content) {
5052
+ if (block.type === "tool_use" && block.name === "TodoWrite") {
5053
+ try {
5054
+ this.logger.info("TodoWrite detected, persisting todos", { taskId });
5055
+ const todoList = this.parseTodoWriteInput(block.input);
5056
+ await this.writeTodos(taskId, todoList);
5057
+ this.logger.info("Persisted todos successfully", {
5058
+ taskId,
5059
+ total: todoList.metadata.total,
5060
+ completed: todoList.metadata.completed
5061
+ });
5062
+ return todoList;
5063
+ } catch (error) {
5064
+ this.logger.error("Failed to persist todos", {
5065
+ taskId,
5066
+ error: error instanceof Error ? error.message : String(error)
5067
+ });
5068
+ return null;
5069
+ }
5070
+ }
5071
+ }
5072
+ return null;
5073
+ }
5074
+ };
5075
+
6399
5076
  // src/tools/registry.ts
6400
5077
  var TOOL_DEFINITIONS = {
6401
5078
  // Filesystem tools
@@ -6473,6 +5150,11 @@ var TOOL_DEFINITIONS = {
6473
5150
  category: "assistant",
6474
5151
  description: "Exit plan mode and present plan to user"
6475
5152
  },
5153
+ AskUserQuestion: {
5154
+ name: "AskUserQuestion",
5155
+ category: "assistant",
5156
+ description: "Ask the user a clarifying question with options"
5157
+ },
6476
5158
  SlashCommand: {
6477
5159
  name: "SlashCommand",
6478
5160
  category: "assistant",
@@ -6510,12 +5192,11 @@ var ToolRegistry = class {
6510
5192
  };
6511
5193
 
6512
5194
  // src/worktree-manager.ts
6513
- import { exec as exec2, execFile } from "child_process";
5195
+ import { execFile } from "child_process";
6514
5196
  import * as crypto from "crypto";
6515
- import * as fs5 from "fs/promises";
5197
+ import * as fs3 from "fs/promises";
6516
5198
  import * as path2 from "path";
6517
5199
  import { promisify as promisify2 } from "util";
6518
- var execAsync2 = promisify2(exec2);
6519
5200
  var execFileAsync = promisify2(execFile);
6520
5201
  var ADJECTIVES = [
6521
5202
  "swift",
@@ -7005,14 +5686,14 @@ var WorktreeManager = class {
7005
5686
  usesExternalPath() {
7006
5687
  return this.worktreeBasePath !== null;
7007
5688
  }
7008
- async runGitCommand(command) {
5689
+ async runGitCommand(args) {
7009
5690
  try {
7010
- const { stdout } = await execAsync2(`git ${command}`, {
5691
+ const { stdout } = await execFileAsync("git", args, {
7011
5692
  cwd: this.mainRepoPath
7012
5693
  });
7013
5694
  return stdout.trim();
7014
5695
  } catch (error) {
7015
- throw new Error(`Git command failed: ${command}
5696
+ throw new Error(`Git command failed: git ${args.join(" ")}
7016
5697
  ${error}`);
7017
5698
  }
7018
5699
  }
@@ -7037,7 +5718,7 @@ ${error}`);
7037
5718
  async worktreeExists(name) {
7038
5719
  const worktreePath = this.getWorktreePath(name);
7039
5720
  try {
7040
- await fs5.access(worktreePath);
5721
+ await fs3.access(worktreePath);
7041
5722
  return true;
7042
5723
  } catch {
7043
5724
  return false;
@@ -7048,7 +5729,7 @@ ${error}`);
7048
5729
  const ignorePattern = `/${WORKTREE_FOLDER_NAME}/`;
7049
5730
  let content = "";
7050
5731
  try {
7051
- content = await fs5.readFile(excludePath, "utf-8");
5732
+ content = await fs3.readFile(excludePath, "utf-8");
7052
5733
  } catch {
7053
5734
  }
7054
5735
  if (content.includes(`/${WORKTREE_FOLDER_NAME}/`) || content.includes(`/${WORKTREE_FOLDER_NAME}`)) {
@@ -7056,13 +5737,13 @@ ${error}`);
7056
5737
  return;
7057
5738
  }
7058
5739
  const infoDir = path2.join(this.mainRepoPath, ".git", "info");
7059
- await fs5.mkdir(infoDir, { recursive: true });
5740
+ await fs3.mkdir(infoDir, { recursive: true });
7060
5741
  const newContent = `${content.trimEnd()}
7061
5742
 
7062
5743
  # Array worktrees
7063
5744
  ${ignorePattern}
7064
5745
  `;
7065
- await fs5.writeFile(excludePath, newContent);
5746
+ await fs3.writeFile(excludePath, newContent);
7066
5747
  this.logger.info("Added .array folder to .git/info/exclude");
7067
5748
  }
7068
5749
  async generateUniqueWorktreeName() {
@@ -7079,61 +5760,83 @@ ${ignorePattern}
7079
5760
  return name;
7080
5761
  }
7081
5762
  async getDefaultBranch() {
7082
- try {
7083
- const remoteBranch = await this.runGitCommand(
7084
- "symbolic-ref refs/remotes/origin/HEAD"
7085
- );
7086
- return remoteBranch.replace("refs/remotes/origin/", "");
7087
- } catch {
7088
- try {
7089
- await this.runGitCommand("rev-parse --verify main");
7090
- return "main";
7091
- } catch {
7092
- try {
7093
- await this.runGitCommand("rev-parse --verify master");
7094
- return "master";
7095
- } catch {
7096
- throw new Error(
7097
- "Cannot determine default branch. No main or master branch found."
7098
- );
7099
- }
7100
- }
7101
- }
5763
+ const [symbolicRef, mainExists, masterExists] = await Promise.allSettled([
5764
+ this.runGitCommand(["symbolic-ref", "refs/remotes/origin/HEAD"]),
5765
+ this.runGitCommand(["rev-parse", "--verify", "main"]),
5766
+ this.runGitCommand(["rev-parse", "--verify", "master"])
5767
+ ]);
5768
+ if (symbolicRef.status === "fulfilled") {
5769
+ return symbolicRef.value.replace("refs/remotes/origin/", "");
5770
+ }
5771
+ if (mainExists.status === "fulfilled") {
5772
+ return "main";
5773
+ }
5774
+ if (masterExists.status === "fulfilled") {
5775
+ return "master";
5776
+ }
5777
+ throw new Error(
5778
+ "Cannot determine default branch. No main or master branch found."
5779
+ );
7102
5780
  }
7103
5781
  async createWorktree(options) {
5782
+ const totalStart = Date.now();
5783
+ const setupPromises = [];
7104
5784
  if (!this.usesExternalPath()) {
7105
- await this.ensureArrayDirIgnored();
7106
- }
7107
- if (this.usesExternalPath()) {
5785
+ setupPromises.push(this.ensureArrayDirIgnored());
5786
+ } else {
7108
5787
  const folderPath = this.getWorktreeFolderPath();
7109
- await fs5.mkdir(folderPath, { recursive: true });
7110
- }
7111
- const worktreeName = await this.generateUniqueWorktreeName();
5788
+ setupPromises.push(fs3.mkdir(folderPath, { recursive: true }));
5789
+ }
5790
+ const worktreeNamePromise = this.generateUniqueWorktreeName();
5791
+ setupPromises.push(worktreeNamePromise);
5792
+ const baseBranchPromise = options?.baseBranch ? Promise.resolve(options.baseBranch) : this.getDefaultBranch();
5793
+ setupPromises.push(baseBranchPromise);
5794
+ await Promise.all(setupPromises);
5795
+ const setupTime = Date.now() - totalStart;
5796
+ const worktreeName = await worktreeNamePromise;
5797
+ const baseBranch = await baseBranchPromise;
7112
5798
  const worktreePath = this.getWorktreePath(worktreeName);
7113
5799
  const branchName = `array/${worktreeName}`;
7114
- const baseBranch = options?.baseBranch ?? await this.getDefaultBranch();
7115
5800
  this.logger.info("Creating worktree", {
7116
5801
  worktreeName,
7117
5802
  worktreePath,
7118
5803
  branchName,
7119
5804
  baseBranch,
7120
- external: this.usesExternalPath()
5805
+ external: this.usesExternalPath(),
5806
+ setupTimeMs: setupTime
7121
5807
  });
5808
+ const gitStart = Date.now();
7122
5809
  if (this.usesExternalPath()) {
7123
- await this.runGitCommand(
7124
- `worktree add -b "${branchName}" "${worktreePath}" "${baseBranch}"`
7125
- );
5810
+ await this.runGitCommand([
5811
+ "worktree",
5812
+ "add",
5813
+ "--quiet",
5814
+ "-b",
5815
+ branchName,
5816
+ worktreePath,
5817
+ baseBranch
5818
+ ]);
7126
5819
  } else {
7127
- const relativePath = `${WORKTREE_FOLDER_NAME}/${worktreeName}`;
7128
- await this.runGitCommand(
7129
- `worktree add -b "${branchName}" "./${relativePath}" "${baseBranch}"`
7130
- );
7131
- }
5820
+ const relativePath = `./${WORKTREE_FOLDER_NAME}/${worktreeName}`;
5821
+ await this.runGitCommand([
5822
+ "worktree",
5823
+ "add",
5824
+ "--quiet",
5825
+ "-b",
5826
+ branchName,
5827
+ relativePath,
5828
+ baseBranch
5829
+ ]);
5830
+ }
5831
+ const gitTime = Date.now() - gitStart;
7132
5832
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
7133
5833
  this.logger.info("Worktree created successfully", {
7134
5834
  worktreeName,
7135
5835
  worktreePath,
7136
- branchName
5836
+ branchName,
5837
+ setupTimeMs: setupTime,
5838
+ gitWorktreeAddMs: gitTime,
5839
+ totalMs: Date.now() - totalStart
7137
5840
  });
7138
5841
  return {
7139
5842
  worktreePath,
@@ -7162,7 +5865,7 @@ ${ignorePattern}
7162
5865
  }
7163
5866
  try {
7164
5867
  const gitPath = path2.join(resolvedWorktreePath, ".git");
7165
- const stat2 = await fs5.stat(gitPath);
5868
+ const stat2 = await fs3.stat(gitPath);
7166
5869
  if (stat2.isDirectory()) {
7167
5870
  const error = new Error(
7168
5871
  "Cannot delete worktree: path appears to be a main repository (contains .git directory)"
@@ -7194,8 +5897,8 @@ ${ignorePattern}
7194
5897
  }
7195
5898
  );
7196
5899
  try {
7197
- await fs5.rm(worktreePath, { recursive: true, force: true });
7198
- await this.runGitCommand("worktree prune");
5900
+ await fs3.rm(worktreePath, { recursive: true, force: true });
5901
+ await this.runGitCommand(["worktree", "prune"]);
7199
5902
  this.logger.info("Worktree cleaned up manually", { worktreePath });
7200
5903
  } catch (cleanupError) {
7201
5904
  this.logger.error("Failed to cleanup worktree", {
@@ -7208,7 +5911,11 @@ ${ignorePattern}
7208
5911
  }
7209
5912
  async getWorktreeInfo(worktreePath) {
7210
5913
  try {
7211
- const output = await this.runGitCommand("worktree list --porcelain");
5914
+ const output = await this.runGitCommand([
5915
+ "worktree",
5916
+ "list",
5917
+ "--porcelain"
5918
+ ]);
7212
5919
  const worktrees = this.parseWorktreeList(output);
7213
5920
  const worktree = worktrees.find((w) => w.worktreePath === worktreePath);
7214
5921
  return worktree || null;
@@ -7219,7 +5926,11 @@ ${ignorePattern}
7219
5926
  }
7220
5927
  async listWorktrees() {
7221
5928
  try {
7222
- const output = await this.runGitCommand("worktree list --porcelain");
5929
+ const output = await this.runGitCommand([
5930
+ "worktree",
5931
+ "list",
5932
+ "--porcelain"
5933
+ ]);
7223
5934
  return this.parseWorktreeList(output);
7224
5935
  } catch (error) {
7225
5936
  this.logger.debug("Failed to list worktrees", { error });
@@ -7258,15 +5969,16 @@ ${ignorePattern}
7258
5969
  }
7259
5970
  async isWorktree(repoPath) {
7260
5971
  try {
7261
- const { stdout } = await execAsync2(
7262
- "git rev-parse --is-inside-work-tree",
5972
+ const { stdout } = await execFileAsync(
5973
+ "git",
5974
+ ["rev-parse", "--is-inside-work-tree"],
7263
5975
  { cwd: repoPath }
7264
5976
  );
7265
5977
  if (stdout.trim() !== "true") {
7266
5978
  return false;
7267
5979
  }
7268
5980
  const gitPath = path2.join(repoPath, ".git");
7269
- const stat2 = await fs5.stat(gitPath);
5981
+ const stat2 = await fs3.stat(gitPath);
7270
5982
  return stat2.isFile();
7271
5983
  } catch {
7272
5984
  return false;
@@ -7275,7 +5987,7 @@ ${ignorePattern}
7275
5987
  async getMainRepoPathFromWorktree(worktreePath) {
7276
5988
  try {
7277
5989
  const gitFilePath = path2.join(worktreePath, ".git");
7278
- const content = await fs5.readFile(gitFilePath, "utf-8");
5990
+ const content = await fs3.readFile(gitFilePath, "utf-8");
7279
5991
  const match = content.match(/gitdir:\s*(.+)/);
7280
5992
  if (match) {
7281
5993
  const gitDir = match[1].trim();
@@ -7338,6 +6050,7 @@ export {
7338
6050
  ToolRegistry,
7339
6051
  WorktreeManager,
7340
6052
  createAcpConnection,
6053
+ getLlmGatewayUrl,
7341
6054
  parseAgentEvent,
7342
6055
  parseAgentEvents
7343
6056
  };