@posthog/agent 1.30.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 */
@@ -65,24 +57,11 @@ var POSTHOG_NOTIFICATIONS = {
65
57
  /** Console/log output */
66
58
  CONSOLE: "_posthog/console",
67
59
  /** SDK session ID notification (for resumption) */
68
- SDK_SESSION: "_posthog/sdk_session",
69
- /** Sandbox execution output (stdout/stderr from Modal or Docker) */
70
- SANDBOX_OUTPUT: "_posthog/sandbox_output"
60
+ SDK_SESSION: "_posthog/sdk_session"
71
61
  };
72
62
 
73
- // src/adapters/claude/claude.ts
74
- import * as fs from "fs";
75
- import * as os from "os";
76
- import * as path from "path";
77
- import {
78
- AgentSideConnection,
79
- ndJsonStream,
80
- RequestError
81
- } from "@agentclientprotocol/sdk";
82
- import {
83
- query
84
- } from "@anthropic-ai/claude-agent-sdk";
85
- import { v7 as uuidv7 } from "uuid";
63
+ // src/adapters/connection.ts
64
+ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
86
65
 
87
66
  // src/utils/logger.ts
88
67
  var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
@@ -163,7 +142,7 @@ var Logger = class _Logger {
163
142
 
164
143
  // src/utils/tapped-stream.ts
165
144
  function createTappedWritableStream(underlying, options) {
166
- const { onMessage, logger: logger2 } = options;
145
+ const { onMessage, logger } = options;
167
146
  const decoder = new TextDecoder();
168
147
  let buffer = "";
169
148
  let _messageCount = 0;
@@ -187,7 +166,7 @@ function createTappedWritableStream(underlying, options) {
187
166
  writer.releaseLock();
188
167
  },
189
168
  async abort(reason) {
190
- logger2?.warn("Tapped stream aborted", { reason });
169
+ logger?.warn("Tapped stream aborted", { reason });
191
170
  const writer = underlying.getWriter();
192
171
  await writer.abort(reason);
193
172
  writer.releaseLock();
@@ -195,10 +174,22 @@ function createTappedWritableStream(underlying, options) {
195
174
  });
196
175
  }
197
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
+
198
189
  // package.json
199
190
  var package_default = {
200
191
  name: "@posthog/agent",
201
- version: "1.30.0",
192
+ version: "2.0.0",
202
193
  repository: "https://github.com/PostHog/array",
203
194
  description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
204
195
  main: "./dist/index.js",
@@ -311,14 +302,14 @@ var Pushable = class {
311
302
  };
312
303
  }
313
304
  };
314
- function unreachable(value, logger2) {
305
+ function unreachable(value, logger) {
315
306
  let valueAsString;
316
307
  try {
317
308
  valueAsString = JSON.stringify(value);
318
309
  } catch {
319
310
  valueAsString = value;
320
311
  }
321
- logger2.error(`Unexpected case: ${valueAsString}`);
312
+ logger.error(`Unexpected case: ${valueAsString}`);
322
313
  }
323
314
  function sleep(time) {
324
315
  return new Promise((resolve3) => setTimeout(resolve3, time));
@@ -1074,49 +1065,49 @@ No edits were applied.`
1074
1065
  }
1075
1066
 
1076
1067
  // src/adapters/claude/tools.ts
1077
- function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({ debug: false, prefix: "[ClaudeTools]" })) {
1068
+ function toolInfoFromToolUse(toolUse, cachedFileContent, logger = new Logger({ debug: false, prefix: "[ClaudeTools]" })) {
1078
1069
  const name = toolUse.name;
1079
1070
  const input = toolUse.input;
1080
1071
  switch (name) {
1081
1072
  case "Task":
1082
1073
  return {
1083
- title: input?.description ? input.description : "Task",
1074
+ title: input?.description ? String(input.description) : "Task",
1084
1075
  kind: "think",
1085
1076
  content: input?.prompt ? [
1086
1077
  {
1087
1078
  type: "content",
1088
- content: { type: "text", text: input.prompt }
1079
+ content: { type: "text", text: String(input.prompt) }
1089
1080
  }
1090
1081
  ] : []
1091
1082
  };
1092
1083
  case "NotebookRead":
1093
1084
  return {
1094
- title: input?.notebook_path ? `Read Notebook ${input.notebook_path}` : "Read Notebook",
1085
+ title: input?.notebook_path ? `Read Notebook ${String(input.notebook_path)}` : "Read Notebook",
1095
1086
  kind: "read",
1096
1087
  content: [],
1097
- locations: input?.notebook_path ? [{ path: input.notebook_path }] : []
1088
+ locations: input?.notebook_path ? [{ path: String(input.notebook_path) }] : []
1098
1089
  };
1099
1090
  case "NotebookEdit":
1100
1091
  return {
1101
- title: input?.notebook_path ? `Edit Notebook ${input.notebook_path}` : "Edit Notebook",
1092
+ title: input?.notebook_path ? `Edit Notebook ${String(input.notebook_path)}` : "Edit Notebook",
1102
1093
  kind: "edit",
1103
1094
  content: input?.new_source ? [
1104
1095
  {
1105
1096
  type: "content",
1106
- content: { type: "text", text: input.new_source }
1097
+ content: { type: "text", text: String(input.new_source) }
1107
1098
  }
1108
1099
  ] : [],
1109
- locations: input?.notebook_path ? [{ path: input.notebook_path }] : []
1100
+ locations: input?.notebook_path ? [{ path: String(input.notebook_path) }] : []
1110
1101
  };
1111
1102
  case "Bash":
1112
1103
  case toolNames.bash:
1113
1104
  return {
1114
- title: input?.command ? `\`${input.command.replaceAll("`", "\\`")}\`` : "Terminal",
1105
+ title: input?.command ? `\`${String(input.command).replaceAll("`", "\\`")}\`` : "Terminal",
1115
1106
  kind: "execute",
1116
1107
  content: input?.description ? [
1117
1108
  {
1118
1109
  type: "content",
1119
- content: { type: "text", text: input.description }
1110
+ content: { type: "text", text: String(input.description) }
1120
1111
  }
1121
1112
  ] : []
1122
1113
  };
@@ -1136,18 +1127,20 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({
1136
1127
  };
1137
1128
  case toolNames.read: {
1138
1129
  let limit = "";
1139
- if (input.limit) {
1140
- limit = " (" + ((input.offset ?? 0) + 1) + " - " + ((input.offset ?? 0) + input.limit) + ")";
1141
- } else if (input.offset) {
1142
- 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})`;
1143
1136
  }
1144
1137
  return {
1145
- title: `Read ${input.file_path ?? "File"}${limit}`,
1138
+ title: `Read ${input?.file_path ? String(input.file_path) : "File"}${limit}`,
1146
1139
  kind: "read",
1147
- locations: input.file_path ? [
1140
+ locations: input?.file_path ? [
1148
1141
  {
1149
- path: input.file_path,
1150
- line: input.offset ?? 0
1142
+ path: String(input.file_path),
1143
+ line: inputOffset
1151
1144
  }
1152
1145
  ] : [],
1153
1146
  content: []
@@ -1158,25 +1151,25 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({
1158
1151
  title: "Read File",
1159
1152
  kind: "read",
1160
1153
  content: [],
1161
- locations: input.file_path ? [
1154
+ locations: input?.file_path ? [
1162
1155
  {
1163
- path: input.file_path,
1164
- line: input.offset ?? 0
1156
+ path: String(input.file_path),
1157
+ line: input?.offset ?? 0
1165
1158
  }
1166
1159
  ] : []
1167
1160
  };
1168
1161
  case "LS":
1169
1162
  return {
1170
- title: `List the ${input?.path ? `\`${input.path}\`` : "current"} directory's contents`,
1163
+ title: `List the ${input?.path ? `\`${String(input.path)}\`` : "current"} directory's contents`,
1171
1164
  kind: "search",
1172
1165
  content: [],
1173
1166
  locations: []
1174
1167
  };
1175
1168
  case toolNames.edit:
1176
1169
  case "Edit": {
1177
- const path3 = input?.file_path ?? input?.file_path;
1178
- let oldText = input.old_string ?? null;
1179
- 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) : "";
1180
1173
  let affectedLines = [];
1181
1174
  if (path3 && oldText) {
1182
1175
  try {
@@ -1192,7 +1185,7 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({
1192
1185
  newText = newContent.newContent;
1193
1186
  affectedLines = newContent.lineNumbers;
1194
1187
  } catch (e) {
1195
- logger2.error("Failed to edit file", e);
1188
+ logger.error("Failed to edit file", e);
1196
1189
  }
1197
1190
  }
1198
1191
  return {
@@ -1210,78 +1203,84 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({
1210
1203
  };
1211
1204
  }
1212
1205
  case toolNames.write: {
1213
- let content = [];
1214
- if (input?.file_path) {
1215
- 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 = [
1216
1211
  {
1217
1212
  type: "diff",
1218
- path: input.file_path,
1213
+ path: filePath,
1219
1214
  oldText: null,
1220
- newText: input.content
1215
+ newText: contentStr ?? ""
1221
1216
  }
1222
1217
  ];
1223
- } else if (input?.content) {
1224
- content = [
1218
+ } else if (contentStr) {
1219
+ contentResult = [
1225
1220
  {
1226
1221
  type: "content",
1227
- content: { type: "text", text: input.content }
1222
+ content: { type: "text", text: contentStr }
1228
1223
  }
1229
1224
  ];
1230
1225
  }
1231
1226
  return {
1232
- title: input?.file_path ? `Write ${input.file_path}` : "Write",
1227
+ title: filePath ? `Write ${filePath}` : "Write",
1233
1228
  kind: "edit",
1234
- content,
1235
- locations: input?.file_path ? [{ path: input.file_path }] : []
1229
+ content: contentResult,
1230
+ locations: filePath ? [{ path: filePath }] : []
1236
1231
  };
1237
1232
  }
1238
- 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) : "";
1239
1236
  return {
1240
- title: input?.file_path ? `Write ${input.file_path}` : "Write",
1237
+ title: filePath ? `Write ${filePath}` : "Write",
1241
1238
  kind: "edit",
1242
- content: input?.file_path ? [
1239
+ content: filePath ? [
1243
1240
  {
1244
1241
  type: "diff",
1245
- path: input.file_path,
1242
+ path: filePath,
1246
1243
  oldText: null,
1247
- newText: input.content
1244
+ newText: contentStr
1248
1245
  }
1249
1246
  ] : [],
1250
- locations: input?.file_path ? [{ path: input.file_path }] : []
1247
+ locations: filePath ? [{ path: filePath }] : []
1251
1248
  };
1249
+ }
1252
1250
  case "Glob": {
1253
1251
  let label = "Find";
1254
- if (input.path) {
1255
- label += ` \`${input.path}\``;
1252
+ const pathStr = input?.path ? String(input.path) : void 0;
1253
+ if (pathStr) {
1254
+ label += ` \`${pathStr}\``;
1256
1255
  }
1257
- if (input.pattern) {
1258
- label += ` \`${input.pattern}\``;
1256
+ if (input?.pattern) {
1257
+ label += ` \`${String(input.pattern)}\``;
1259
1258
  }
1260
1259
  return {
1261
1260
  title: label,
1262
1261
  kind: "search",
1263
1262
  content: [],
1264
- locations: input.path ? [{ path: input.path }] : []
1263
+ locations: pathStr ? [{ path: pathStr }] : []
1265
1264
  };
1266
1265
  }
1267
1266
  case "Grep": {
1268
1267
  let label = "grep";
1269
- if (input["-i"]) {
1268
+ if (input?.["-i"]) {
1270
1269
  label += " -i";
1271
1270
  }
1272
- if (input["-n"]) {
1271
+ if (input?.["-n"]) {
1273
1272
  label += " -n";
1274
1273
  }
1275
- if (input["-A"] !== void 0) {
1274
+ if (input?.["-A"] !== void 0) {
1276
1275
  label += ` -A ${input["-A"]}`;
1277
1276
  }
1278
- if (input["-B"] !== void 0) {
1277
+ if (input?.["-B"] !== void 0) {
1279
1278
  label += ` -B ${input["-B"]}`;
1280
1279
  }
1281
- if (input["-C"] !== void 0) {
1280
+ if (input?.["-C"] !== void 0) {
1282
1281
  label += ` -C ${input["-C"]}`;
1283
1282
  }
1284
- if (input.output_mode) {
1283
+ if (input?.output_mode) {
1285
1284
  switch (input.output_mode) {
1286
1285
  case "FilesWithMatches":
1287
1286
  label += " -l";
@@ -1293,21 +1292,21 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({
1293
1292
  break;
1294
1293
  }
1295
1294
  }
1296
- if (input.head_limit !== void 0) {
1295
+ if (input?.head_limit !== void 0) {
1297
1296
  label += ` | head -${input.head_limit}`;
1298
1297
  }
1299
- if (input.glob) {
1300
- label += ` --include="${input.glob}"`;
1298
+ if (input?.glob) {
1299
+ label += ` --include="${String(input.glob)}"`;
1301
1300
  }
1302
- if (input.type) {
1303
- label += ` --type=${input.type}`;
1301
+ if (input?.type) {
1302
+ label += ` --type=${String(input.type)}`;
1304
1303
  }
1305
- if (input.multiline) {
1304
+ if (input?.multiline) {
1306
1305
  label += " -P";
1307
1306
  }
1308
- label += ` "${input.pattern}"`;
1309
- if (input.path) {
1310
- label += ` ${input.path}`;
1307
+ label += ` "${input?.pattern ? String(input.pattern) : ""}"`;
1308
+ if (input?.path) {
1309
+ label += ` ${String(input.path)}`;
1311
1310
  }
1312
1311
  return {
1313
1312
  title: label,
@@ -1317,22 +1316,24 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({
1317
1316
  }
1318
1317
  case "WebFetch":
1319
1318
  return {
1320
- title: input?.url ? `Fetch ${input.url}` : "Fetch",
1319
+ title: input?.url ? `Fetch ${String(input.url)}` : "Fetch",
1321
1320
  kind: "fetch",
1322
1321
  content: input?.prompt ? [
1323
1322
  {
1324
1323
  type: "content",
1325
- content: { type: "text", text: input.prompt }
1324
+ content: { type: "text", text: String(input.prompt) }
1326
1325
  }
1327
1326
  ] : []
1328
1327
  };
1329
1328
  case "WebSearch": {
1330
- let label = `"${input.query}"`;
1331
- if (input.allowed_domains && input.allowed_domains.length > 0) {
1332
- 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(", ")})`;
1333
1334
  }
1334
- if (input.blocked_domains && input.blocked_domains.length > 0) {
1335
- label += ` (blocked: ${input.blocked_domains.join(", ")})`;
1335
+ if (blockedDomains && blockedDomains.length > 0) {
1336
+ label += ` (blocked: ${blockedDomains.join(", ")})`;
1336
1337
  }
1337
1338
  return {
1338
1339
  title: label,
@@ -1350,8 +1351,29 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger2 = new Logger({
1350
1351
  return {
1351
1352
  title: "Ready to code?",
1352
1353
  kind: "switch_mode",
1353
- 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
+ ] : []
1354
1375
  };
1376
+ }
1355
1377
  case "Other": {
1356
1378
  let output;
1357
1379
  try {
@@ -1388,15 +1410,24 @@ function toolUpdateFromToolResult(toolResult, toolUse) {
1388
1410
  case toolNames.read:
1389
1411
  if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
1390
1412
  return {
1391
- content: toolResult.content.map((content) => ({
1392
- type: "content",
1393
- content: content.type === "text" ? {
1394
- type: "text",
1395
- text: markdownEscape(
1396
- content.text.replace(SYSTEM_REMINDER, "")
1397
- )
1398
- } : content
1399
- }))
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
+ })
1400
1431
  };
1401
1432
  } else if (typeof toolResult.content === "string" && toolResult.content.length > 0) {
1402
1433
  return {
@@ -1428,6 +1459,24 @@ function toolUpdateFromToolResult(toolResult, toolUse) {
1428
1459
  case "ExitPlanMode": {
1429
1460
  return { title: "Exited Plan Mode" };
1430
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
+ }
1431
1480
  default: {
1432
1481
  return toAcpContentUpdate(
1433
1482
  toolResult.content,
@@ -1439,15 +1488,24 @@ function toolUpdateFromToolResult(toolResult, toolUse) {
1439
1488
  function toAcpContentUpdate(content, isError = false) {
1440
1489
  if (Array.isArray(content) && content.length > 0) {
1441
1490
  return {
1442
- content: content.map((content2) => ({
1443
- type: "content",
1444
- content: isError && content2.type === "text" ? {
1445
- ...content2,
1446
- text: `\`\`\`
1447
- ${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 ?? ""}
1448
1500
  \`\`\``
1449
- } : content2
1450
- }))
1501
+ }
1502
+ };
1503
+ }
1504
+ return {
1505
+ type: "content",
1506
+ content: item
1507
+ };
1508
+ })
1451
1509
  };
1452
1510
  } else if (typeof content === "string" && content.length > 0) {
1453
1511
  return {
@@ -1491,7 +1549,7 @@ var registerHookCallback = (toolUseID, {
1491
1549
  onPostToolUseHook
1492
1550
  };
1493
1551
  };
1494
- var createPostToolUseHook = (logger2 = new Logger({ prefix: "[createPostToolUseHook]" })) => async (input, toolUseID) => {
1552
+ var createPostToolUseHook = (logger = new Logger({ prefix: "[createPostToolUseHook]" })) => async (input, toolUseID) => {
1495
1553
  if (input.hook_event_name === "PostToolUse" && toolUseID) {
1496
1554
  const onPostToolUseHook = toolUseCallbacks[toolUseID]?.onPostToolUseHook;
1497
1555
  if (onPostToolUseHook) {
@@ -1502,7 +1560,7 @@ var createPostToolUseHook = (logger2 = new Logger({ prefix: "[createPostToolUseH
1502
1560
  );
1503
1561
  delete toolUseCallbacks[toolUseID];
1504
1562
  } else {
1505
- logger2.error(
1563
+ logger.error(
1506
1564
  `No onPostToolUseHook found for tool use ID: ${toolUseID}`
1507
1565
  );
1508
1566
  delete toolUseCallbacks[toolUseID];
@@ -1512,9 +1570,115 @@ var createPostToolUseHook = (logger2 = new Logger({ prefix: "[createPostToolUseH
1512
1570
  };
1513
1571
 
1514
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
+ }
1515
1680
  function clearStatsigCache() {
1516
- const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
1517
- const statsigPath = path.join(configDir, "statsig");
1681
+ const statsigPath = path.join(getClaudeConfigDir(), "statsig");
1518
1682
  try {
1519
1683
  if (fs.existsSync(statsigPath)) {
1520
1684
  fs.rmSync(statsigPath, { recursive: true, force: true });
@@ -1550,6 +1714,33 @@ var ClaudeAcpAgent = class {
1550
1714
  this.sessions[sessionId] = session;
1551
1715
  return session;
1552
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
+ }
1553
1744
  appendNotification(sessionId, notification) {
1554
1745
  this.sessions[sessionId]?.notificationHistory.push(notification);
1555
1746
  }
@@ -1631,7 +1822,9 @@ var ClaudeAcpAgent = class {
1631
1822
  systemPrompt.append = customPrompt.append;
1632
1823
  }
1633
1824
  }
1634
- const permissionMode = "default";
1825
+ const initialModeId = params._meta?.initialModeId;
1826
+ const ourPermissionMode = initialModeId ?? "default";
1827
+ const sdkPermissionMode = ourPermissionMode;
1635
1828
  const userProvidedOptions = params._meta?.claudeCode?.options;
1636
1829
  const options = {
1637
1830
  systemPrompt,
@@ -1645,7 +1838,8 @@ var ClaudeAcpAgent = class {
1645
1838
  // If we want bypassPermissions to be an option, we have to allow it here.
1646
1839
  // But it doesn't work in root mode, so we only activate it if it will work.
1647
1840
  allowDangerouslySkipPermissions: !IS_ROOT,
1648
- permissionMode,
1841
+ // Use the requested permission mode (including plan mode)
1842
+ permissionMode: sdkPermissionMode,
1649
1843
  canUseTool: this.canUseTool(sessionId),
1650
1844
  // Use "node" to resolve via PATH where a symlink to Electron exists.
1651
1845
  // This avoids launching the Electron binary directly from the app bundle,
@@ -1653,7 +1847,12 @@ var ClaudeAcpAgent = class {
1653
1847
  executable: "node",
1654
1848
  // Prevent spawned Electron processes from showing in dock/tray.
1655
1849
  // Must merge with process.env since SDK replaces rather than merges.
1656
- 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
+ },
1657
1856
  ...process.env.CLAUDE_CODE_EXECUTABLE && {
1658
1857
  pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE
1659
1858
  },
@@ -1667,7 +1866,7 @@ var ClaudeAcpAgent = class {
1667
1866
  ]
1668
1867
  }
1669
1868
  };
1670
- const allowedTools = [];
1869
+ const allowedTools = ["AskUserQuestion"];
1671
1870
  const disallowedTools = [];
1672
1871
  const disableBuiltInTools = params._meta?.disableBuiltInTools === true;
1673
1872
  if (!disableBuiltInTools) {
@@ -1709,6 +1908,9 @@ var ClaudeAcpAgent = class {
1709
1908
  "NotebookEdit"
1710
1909
  );
1711
1910
  }
1911
+ if (ourPermissionMode !== "plan") {
1912
+ disallowedTools.push("ExitPlanMode");
1913
+ }
1712
1914
  if (allowedTools.length > 0) {
1713
1915
  options.allowedTools = allowedTools;
1714
1916
  }
@@ -1724,7 +1926,7 @@ var ClaudeAcpAgent = class {
1724
1926
  prompt: input,
1725
1927
  options
1726
1928
  });
1727
- this.createSession(sessionId, q, input, permissionMode);
1929
+ this.createSession(sessionId, q, input, ourPermissionMode);
1728
1930
  const persistence = params._meta?.persistence;
1729
1931
  if (persistence && this.sessionStore) {
1730
1932
  this.sessionStore.register(sessionId, persistence);
@@ -1780,7 +1982,7 @@ var ClaudeAcpAgent = class {
1780
1982
  sessionId,
1781
1983
  models,
1782
1984
  modes: {
1783
- currentModeId: permissionMode,
1985
+ currentModeId: ourPermissionMode,
1784
1986
  availableModes
1785
1987
  }
1786
1988
  };
@@ -1793,7 +1995,8 @@ var ClaudeAcpAgent = class {
1793
1995
  throw new Error("Session not found");
1794
1996
  }
1795
1997
  this.sessions[params.sessionId].cancelled = false;
1796
- const { query: query5, input } = this.sessions[params.sessionId];
1998
+ const session = this.sessions[params.sessionId];
1999
+ const { query: query2, input } = session;
1797
2000
  for (const chunk of params.prompt) {
1798
2001
  const userNotification = {
1799
2002
  sessionId: params.sessionId,
@@ -1805,9 +2008,9 @@ var ClaudeAcpAgent = class {
1805
2008
  await this.client.sessionUpdate(userNotification);
1806
2009
  this.appendNotification(params.sessionId, userNotification);
1807
2010
  }
1808
- input.push(promptToClaude(params));
2011
+ input.push(promptToClaude({ ...params, prompt: params.prompt }));
1809
2012
  while (true) {
1810
- const { value: message, done } = await query5.next();
2013
+ const { value: message, done } = await query2.next();
1811
2014
  if (done || !message) {
1812
2015
  if (this.sessions[params.sessionId].cancelled) {
1813
2016
  return { stopReason: "cancelled" };
@@ -1823,9 +2026,9 @@ var ClaudeAcpAgent = class {
1823
2026
  switch (message.subtype) {
1824
2027
  case "init":
1825
2028
  if (message.session_id) {
1826
- const session = this.sessions[params.sessionId];
1827
- if (session && !session.sdkSessionId) {
1828
- session.sdkSessionId = message.session_id;
2029
+ const session2 = this.sessions[params.sessionId];
2030
+ if (session2 && !session2.sdkSessionId) {
2031
+ session2.sdkSessionId = message.session_id;
1829
2032
  this.client.extNotification("_posthog/sdk_session", {
1830
2033
  sessionId: params.sessionId,
1831
2034
  sdkSessionId: message.session_id
@@ -2008,7 +2211,64 @@ var ClaudeAcpAgent = class {
2008
2211
  interrupt: true
2009
2212
  };
2010
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
+ };
2011
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
+ }
2012
2272
  const response2 = await this.client.requestPermission({
2013
2273
  options: [
2014
2274
  {
@@ -2030,9 +2290,9 @@ var ClaudeAcpAgent = class {
2030
2290
  sessionId,
2031
2291
  toolCall: {
2032
2292
  toolCallId: toolUseID,
2033
- rawInput: toolInput,
2293
+ rawInput: { ...updatedInput, toolName },
2034
2294
  title: toolInfoFromToolUse(
2035
- { name: toolName, input: toolInput },
2295
+ { name: toolName, input: updatedInput },
2036
2296
  this.fileContentCache,
2037
2297
  this.logger
2038
2298
  ).title
@@ -2049,7 +2309,7 @@ var ClaudeAcpAgent = class {
2049
2309
  });
2050
2310
  return {
2051
2311
  behavior: "allow",
2052
- updatedInput: toolInput,
2312
+ updatedInput,
2053
2313
  updatedPermissions: suggestions ?? [
2054
2314
  {
2055
2315
  type: "setMode",
@@ -2058,13 +2318,147 @@ var ClaudeAcpAgent = class {
2058
2318
  }
2059
2319
  ]
2060
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
+ ];
2061
2345
  } else {
2062
2346
  return {
2063
2347
  behavior: "deny",
2064
- message: "User rejected request to exit plan mode.",
2348
+ message: "No questions provided",
2065
2349
  interrupt: true
2066
2350
  };
2067
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
+ }
2068
2462
  }
2069
2463
  if (session.permissionMode === "bypassPermissions" || session.permissionMode === "acceptEdits" && EDIT_TOOL_NAMES.includes(toolName)) {
2070
2464
  return {
@@ -2121,9 +2515,11 @@ var ClaudeAcpAgent = class {
2121
2515
  updatedInput: toolInput
2122
2516
  };
2123
2517
  } else {
2518
+ const message = "User refused permission to run tool";
2519
+ await emitToolDenial(message);
2124
2520
  return {
2125
2521
  behavior: "deny",
2126
- message: "User refused permission to run tool",
2522
+ message,
2127
2523
  interrupt: true
2128
2524
  };
2129
2525
  }
@@ -2143,6 +2539,11 @@ var ClaudeAcpAgent = class {
2143
2539
  await this.setSessionModel({ sessionId, modelId });
2144
2540
  return {};
2145
2541
  }
2542
+ if (method === "session/setMode") {
2543
+ const { sessionId, modeId } = params;
2544
+ await this.setSessionMode({ sessionId, modeId });
2545
+ return {};
2546
+ }
2146
2547
  throw RequestError.methodNotFound(method);
2147
2548
  }
2148
2549
  /**
@@ -2252,10 +2653,10 @@ var ClaudeAcpAgent = class {
2252
2653
  return {};
2253
2654
  }
2254
2655
  };
2255
- async function getAvailableModels(query5) {
2256
- const models = await query5.supportedModels();
2656
+ async function getAvailableModels(query2) {
2657
+ const models = await query2.supportedModels();
2257
2658
  const currentModel = models[0];
2258
- await query5.setModel(currentModel.value);
2659
+ await query2.setModel(currentModel.value);
2259
2660
  const availableModels = models.map((model) => ({
2260
2661
  modelId: model.value,
2261
2662
  name: model.displayName,
@@ -2266,7 +2667,7 @@ async function getAvailableModels(query5) {
2266
2667
  currentModelId: currentModel.value
2267
2668
  };
2268
2669
  }
2269
- async function getAvailableSlashCommands(query5) {
2670
+ async function getAvailableSlashCommands(query2) {
2270
2671
  const UNSUPPORTED_COMMANDS = [
2271
2672
  "context",
2272
2673
  "cost",
@@ -2276,7 +2677,7 @@ async function getAvailableSlashCommands(query5) {
2276
2677
  "release-notes",
2277
2678
  "todos"
2278
2679
  ];
2279
- const commands = await query5.supportedCommands();
2680
+ const commands = await query2.supportedCommands();
2280
2681
  return commands.map((command) => {
2281
2682
  const input = command.argumentHint ? { hint: command.argumentHint } : null;
2282
2683
  let name = command.name;
@@ -2384,7 +2785,7 @@ ${chunk.resource.text}
2384
2785
  parent_tool_use_id: null
2385
2786
  };
2386
2787
  }
2387
- function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentCache, client, logger2) {
2788
+ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentCache, client, logger) {
2388
2789
  if (typeof content === "string") {
2389
2790
  return [
2390
2791
  {
@@ -2465,7 +2866,7 @@ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentC
2465
2866
  update: update2
2466
2867
  });
2467
2868
  } else {
2468
- logger2.error(
2869
+ logger.error(
2469
2870
  `[claude-code-acp] Got a tool response for tool use that wasn't tracked: ${toolUseId}`
2470
2871
  );
2471
2872
  }
@@ -2486,7 +2887,7 @@ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentC
2486
2887
  sessionUpdate: "tool_call",
2487
2888
  rawInput,
2488
2889
  status: "pending",
2489
- ...toolInfoFromToolUse(chunk, fileContentCache, logger2)
2890
+ ...toolInfoFromToolUse(chunk, fileContentCache, logger)
2490
2891
  };
2491
2892
  }
2492
2893
  break;
@@ -2501,7 +2902,7 @@ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentC
2501
2902
  case "mcp_tool_result": {
2502
2903
  const toolUse = toolUseCache[chunk.tool_use_id];
2503
2904
  if (!toolUse) {
2504
- logger2.error(
2905
+ logger.error(
2505
2906
  `[claude-code-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`
2506
2907
  );
2507
2908
  break;
@@ -2530,7 +2931,7 @@ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentC
2530
2931
  case "container_upload":
2531
2932
  break;
2532
2933
  default:
2533
- unreachable(chunk, logger2);
2934
+ unreachable(chunk, logger);
2534
2935
  break;
2535
2936
  }
2536
2937
  if (update) {
@@ -2539,7 +2940,7 @@ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentC
2539
2940
  }
2540
2941
  return output;
2541
2942
  }
2542
- function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileContentCache, client, logger2) {
2943
+ function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileContentCache, client, logger) {
2543
2944
  const event = message.event;
2544
2945
  switch (event.type) {
2545
2946
  case "content_block_start":
@@ -2550,7 +2951,7 @@ function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileCon
2550
2951
  toolUseCache,
2551
2952
  fileContentCache,
2552
2953
  client,
2553
- logger2
2954
+ logger
2554
2955
  );
2555
2956
  case "content_block_delta":
2556
2957
  return toAcpNotifications(
@@ -2560,7 +2961,7 @@ function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileCon
2560
2961
  toolUseCache,
2561
2962
  fileContentCache,
2562
2963
  client,
2563
- logger2
2964
+ logger
2564
2965
  );
2565
2966
  // No content
2566
2967
  case "message_start":
@@ -2569,14 +2970,16 @@ function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileCon
2569
2970
  case "content_block_stop":
2570
2971
  return [];
2571
2972
  default:
2572
- unreachable(event, logger2);
2973
+ unreachable(event, logger);
2573
2974
  return [];
2574
2975
  }
2575
2976
  }
2977
+
2978
+ // src/adapters/connection.ts
2576
2979
  function createAcpConnection(config = {}) {
2577
- const logger2 = new Logger({ debug: true, prefix: "[AcpConnection]" });
2980
+ const logger = new Logger({ debug: true, prefix: "[AcpConnection]" });
2578
2981
  const streams = createBidirectionalStreams();
2579
- const { sessionStore } = config;
2982
+ const { sessionStore, framework = "claude" } = config;
2580
2983
  let agentWritable = streams.agent.writable;
2581
2984
  let clientWritable = streams.client.writable;
2582
2985
  if (config.sessionId && sessionStore) {
@@ -2592,25 +2995,25 @@ function createAcpConnection(config = {}) {
2592
2995
  onMessage: (line) => {
2593
2996
  sessionStore.appendRawLine(config.sessionId, line);
2594
2997
  },
2595
- logger: logger2
2998
+ logger
2596
2999
  });
2597
3000
  clientWritable = createTappedWritableStream(streams.client.writable, {
2598
3001
  onMessage: (line) => {
2599
3002
  sessionStore.appendRawLine(config.sessionId, line);
2600
3003
  },
2601
- logger: logger2
3004
+ logger
2602
3005
  });
2603
3006
  } else {
2604
- logger2.info("Tapped streams NOT enabled", {
3007
+ logger.info("Tapped streams NOT enabled", {
2605
3008
  hasSessionId: !!config.sessionId,
2606
3009
  hasSessionStore: !!sessionStore
2607
3010
  });
2608
3011
  }
2609
3012
  const agentStream = ndJsonStream(agentWritable, streams.agent.readable);
2610
- const agentConnection = new AgentSideConnection(
2611
- (client) => new ClaudeAcpAgent(client, sessionStore),
2612
- agentStream
2613
- );
3013
+ const agentConnection = new AgentSideConnection((client) => {
3014
+ logger.info("Creating Claude agent");
3015
+ return new ClaudeAcpAgent(client, sessionStore);
3016
+ }, agentStream);
2614
3017
  return {
2615
3018
  agentConnection,
2616
3019
  clientStreams: {
@@ -2634,9 +3037,9 @@ import z2 from "zod";
2634
3037
  var PostHogFileManager = class {
2635
3038
  repositoryPath;
2636
3039
  logger;
2637
- constructor(repositoryPath, logger2) {
3040
+ constructor(repositoryPath, logger) {
2638
3041
  this.repositoryPath = repositoryPath;
2639
- this.logger = logger2 || new Logger({ debug: false, prefix: "[FileManager]" });
3042
+ this.logger = logger || new Logger({ debug: false, prefix: "[FileManager]" });
2640
3043
  }
2641
3044
  getTaskDirectory(taskId) {
2642
3045
  return join2(this.repositoryPath, ".posthog", taskId);
@@ -2752,35 +3155,6 @@ var PostHogFileManager = class {
2752
3155
  async readRequirements(taskId) {
2753
3156
  return await this.readTaskFile(taskId, "requirements.md");
2754
3157
  }
2755
- async writeResearch(taskId, data) {
2756
- this.logger.debug("Writing research", {
2757
- taskId,
2758
- score: data.actionabilityScore,
2759
- hasQuestions: !!data.questions,
2760
- questionCount: data.questions?.length ?? 0,
2761
- answered: data.answered ?? false
2762
- });
2763
- await this.writeTaskFile(taskId, {
2764
- name: "research.json",
2765
- content: JSON.stringify(data, null, 2),
2766
- type: "artifact"
2767
- });
2768
- this.logger.info("Research file written", {
2769
- taskId,
2770
- score: data.actionabilityScore,
2771
- hasQuestions: !!data.questions,
2772
- answered: data.answered ?? false
2773
- });
2774
- }
2775
- async readResearch(taskId) {
2776
- try {
2777
- const content = await this.readTaskFile(taskId, "research.json");
2778
- return content ? JSON.parse(content) : null;
2779
- } catch (error) {
2780
- this.logger.debug("Failed to parse research.json", { error });
2781
- return null;
2782
- }
2783
- }
2784
3158
  async writeTodos(taskId, data) {
2785
3159
  const todos = z2.object({
2786
3160
  metadata: z2.object({
@@ -3588,489 +3962,71 @@ ${errorData.stack_trace}
3588
3962
  }
3589
3963
  };
3590
3964
 
3591
- // src/prompt-builder.ts
3592
- import { promises as fs3 } from "fs";
3593
- import { join as join3 } from "path";
3594
- var PromptBuilder = class {
3595
- getTaskFiles;
3596
- generatePlanTemplate;
3597
- 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();
3598
3971
  logger;
3599
- constructor(deps) {
3600
- this.getTaskFiles = deps.getTaskFiles;
3601
- this.generatePlanTemplate = deps.generatePlanTemplate;
3602
- this.posthogClient = deps.posthogClient;
3603
- 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
+ });
3604
3992
  }
3605
- /**
3606
- * Extract file paths from XML tags in description
3607
- * Format: <file path="relative/path.ts" />
3608
- */
3609
- extractFilePaths(description) {
3610
- const fileTagRegex = /<file\s+path="([^"]+)"\s*\/>/g;
3611
- const paths = [];
3612
- let match;
3613
- match = fileTagRegex.exec(description);
3614
- while (match !== null) {
3615
- paths.push(match[1]);
3616
- match = fileTagRegex.exec(description);
3617
- }
3618
- return paths;
3993
+ /** Register a session for persistence */
3994
+ register(sessionId, config) {
3995
+ this.configs.set(sessionId, config);
3619
3996
  }
3620
- /**
3621
- * Read file contents from repository
3622
- */
3623
- async readFileContent(repositoryPath, filePath) {
3624
- try {
3625
- const fullPath = join3(repositoryPath, filePath);
3626
- const content = await fs3.readFile(fullPath, "utf8");
3627
- return content;
3628
- } catch (error) {
3629
- this.logger.warn(`Failed to read referenced file: ${filePath}`, {
3630
- error
3631
- });
3632
- return null;
3633
- }
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);
3634
4005
  }
3635
4006
  /**
3636
- * Extract URL mentions from XML tags in description
3637
- * Formats: <error id="..." />, <experiment id="..." />, <url href="..." />
4007
+ * Append a raw JSON-RPC line for persistence.
4008
+ * Parses and wraps as StoredNotification for the API.
3638
4009
  */
3639
- extractUrlMentions(description) {
3640
- const mentions = [];
3641
- const resourceRegex = /<(error|experiment|insight|feature_flag)\s+id="([^"]+)"\s*\/>/g;
3642
- let match;
3643
- match = resourceRegex.exec(description);
3644
- while (match !== null) {
3645
- const [, type, id] = match;
3646
- mentions.push({
3647
- url: "",
3648
- // Will be reconstructed if needed
3649
- type,
3650
- id,
3651
- label: this.generateUrlLabel("", type)
3652
- });
3653
- match = resourceRegex.exec(description);
3654
- }
3655
- const urlRegex = /<url\s+href="([^"]+)"\s*\/>/g;
3656
- match = urlRegex.exec(description);
3657
- while (match !== null) {
3658
- const [, url] = match;
3659
- mentions.push({
3660
- url,
3661
- type: "generic",
3662
- label: this.generateUrlLabel(url, "generic")
3663
- });
3664
- match = urlRegex.exec(description);
3665
- }
3666
- return mentions;
3667
- }
3668
- /**
3669
- * Generate a display label for a URL mention
3670
- */
3671
- generateUrlLabel(url, type) {
3672
- try {
3673
- const urlObj = new URL(url);
3674
- switch (type) {
3675
- case "error": {
3676
- const errorMatch = url.match(/error_tracking\/([a-f0-9-]+)/);
3677
- return errorMatch ? `Error ${errorMatch[1].slice(0, 8)}...` : "Error";
3678
- }
3679
- case "experiment": {
3680
- const expMatch = url.match(/experiments\/(\d+)/);
3681
- return expMatch ? `Experiment #${expMatch[1]}` : "Experiment";
3682
- }
3683
- case "insight":
3684
- return "Insight";
3685
- case "feature_flag":
3686
- return "Feature Flag";
3687
- default:
3688
- return urlObj.hostname;
3689
- }
3690
- } catch {
3691
- return "URL";
3692
- }
3693
- }
3694
- /**
3695
- * Process URL references and fetch their content
3696
- */
3697
- async processUrlReferences(description) {
3698
- const urlMentions = this.extractUrlMentions(description);
3699
- const referencedResources = [];
3700
- if (urlMentions.length === 0 || !this.posthogClient) {
3701
- return { description, referencedResources };
3702
- }
3703
- for (const mention of urlMentions) {
3704
- try {
3705
- const resource = await this.posthogClient.fetchResourceByUrl(mention);
3706
- referencedResources.push(resource);
3707
- } catch (error) {
3708
- this.logger.warn(`Failed to fetch resource from URL: ${mention.url}`, {
3709
- error
3710
- });
3711
- referencedResources.push({
3712
- type: mention.type,
3713
- id: mention.id || "",
3714
- url: mention.url,
3715
- title: mention.label || "Unknown Resource",
3716
- content: `Failed to fetch resource from ${mention.url}: ${error}`,
3717
- metadata: {}
3718
- });
3719
- }
3720
- }
3721
- let processedDescription = description;
3722
- for (const mention of urlMentions) {
3723
- if (mention.type === "generic") {
3724
- const escapedUrl = mention.url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3725
- processedDescription = processedDescription.replace(
3726
- new RegExp(`<url\\s+href="${escapedUrl}"\\s*/>`, "g"),
3727
- `@${mention.label}`
3728
- );
3729
- } else {
3730
- const escapedType = mention.type.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3731
- const escapedId = mention.id ? mention.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : "";
3732
- processedDescription = processedDescription.replace(
3733
- new RegExp(`<${escapedType}\\s+id="${escapedId}"\\s*/>`, "g"),
3734
- `@${mention.label}`
3735
- );
3736
- }
3737
- }
3738
- return { description: processedDescription, referencedResources };
3739
- }
3740
- /**
3741
- * Process description to extract file tags and read contents
3742
- * Returns processed description and referenced file contents
3743
- */
3744
- async processFileReferences(description, repositoryPath) {
3745
- const filePaths = this.extractFilePaths(description);
3746
- const referencedFiles = [];
3747
- if (filePaths.length === 0 || !repositoryPath) {
3748
- return { description, referencedFiles };
3749
- }
3750
- const successfulPaths = /* @__PURE__ */ new Set();
3751
- for (const filePath of filePaths) {
3752
- const content = await this.readFileContent(repositoryPath, filePath);
3753
- if (content !== null) {
3754
- referencedFiles.push({ path: filePath, content });
3755
- successfulPaths.add(filePath);
3756
- }
3757
- }
3758
- let processedDescription = description;
3759
- for (const filePath of successfulPaths) {
3760
- const fileName = filePath.split("/").pop() || filePath;
3761
- processedDescription = processedDescription.replace(
3762
- new RegExp(
3763
- `<file\\s+path="${filePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"\\s*/>`,
3764
- "g"
3765
- ),
3766
- `@${fileName}`
3767
- );
3768
- }
3769
- return { description: processedDescription, referencedFiles };
3770
- }
3771
- async buildResearchPrompt(task, repositoryPath) {
3772
- const { description: descriptionAfterFiles, referencedFiles } = await this.processFileReferences(task.description, repositoryPath);
3773
- const { description: processedDescription, referencedResources } = await this.processUrlReferences(descriptionAfterFiles);
3774
- let prompt = "<task>\n";
3775
- prompt += `<title>${task.title}</title>
3776
- `;
3777
- prompt += `<description>${processedDescription}</description>
3778
- `;
3779
- if (task.repository) {
3780
- prompt += `<repository>${task.repository}</repository>
3781
- `;
3782
- }
3783
- prompt += "</task>\n";
3784
- if (referencedFiles.length > 0) {
3785
- prompt += "\n<referenced_files>\n";
3786
- for (const file of referencedFiles) {
3787
- prompt += `<file path="${file.path}">
3788
- \`\`\`
3789
- ${file.content}
3790
- \`\`\`
3791
- </file>
3792
- `;
3793
- }
3794
- prompt += "</referenced_files>\n";
3795
- }
3796
- if (referencedResources.length > 0) {
3797
- prompt += "\n<referenced_resources>\n";
3798
- for (const resource of referencedResources) {
3799
- prompt += `<resource type="${resource.type}" url="${resource.url}">
3800
- `;
3801
- prompt += `<title>${resource.title}</title>
3802
- `;
3803
- prompt += `<content>${resource.content}</content>
3804
- `;
3805
- prompt += "</resource>\n";
3806
- }
3807
- prompt += "</referenced_resources>\n";
3808
- }
3809
- try {
3810
- const taskFiles = await this.getTaskFiles(task.id);
3811
- const contextFiles = taskFiles.filter(
3812
- (f) => f.type === "context" || f.type === "reference"
3813
- );
3814
- if (contextFiles.length > 0) {
3815
- prompt += "\n<supporting_files>\n";
3816
- for (const file of contextFiles) {
3817
- prompt += `<file name="${file.name}" type="${file.type}">
3818
- ${file.content}
3819
- </file>
3820
- `;
3821
- }
3822
- prompt += "</supporting_files>\n";
3823
- }
3824
- } catch (_error) {
3825
- this.logger.debug("No existing task files found for research", {
3826
- taskId: task.id
3827
- });
3828
- }
3829
- return prompt;
3830
- }
3831
- async buildPlanningPrompt(task, repositoryPath) {
3832
- const { description: descriptionAfterFiles, referencedFiles } = await this.processFileReferences(task.description, repositoryPath);
3833
- const { description: processedDescription, referencedResources } = await this.processUrlReferences(descriptionAfterFiles);
3834
- let prompt = "<task>\n";
3835
- prompt += `<title>${task.title}</title>
3836
- `;
3837
- prompt += `<description>${processedDescription}</description>
3838
- `;
3839
- if (task.repository) {
3840
- prompt += `<repository>${task.repository}</repository>
3841
- `;
3842
- }
3843
- prompt += "</task>\n";
3844
- if (referencedFiles.length > 0) {
3845
- prompt += "\n<referenced_files>\n";
3846
- for (const file of referencedFiles) {
3847
- prompt += `<file path="${file.path}">
3848
- \`\`\`
3849
- ${file.content}
3850
- \`\`\`
3851
- </file>
3852
- `;
3853
- }
3854
- prompt += "</referenced_files>\n";
3855
- }
3856
- if (referencedResources.length > 0) {
3857
- prompt += "\n<referenced_resources>\n";
3858
- for (const resource of referencedResources) {
3859
- prompt += `<resource type="${resource.type}" url="${resource.url}">
3860
- `;
3861
- prompt += `<title>${resource.title}</title>
3862
- `;
3863
- prompt += `<content>${resource.content}</content>
3864
- `;
3865
- prompt += "</resource>\n";
3866
- }
3867
- prompt += "</referenced_resources>\n";
3868
- }
3869
- try {
3870
- const taskFiles = await this.getTaskFiles(task.id);
3871
- const contextFiles = taskFiles.filter(
3872
- (f) => f.type === "context" || f.type === "reference"
3873
- );
3874
- if (contextFiles.length > 0) {
3875
- prompt += "\n<supporting_files>\n";
3876
- for (const file of contextFiles) {
3877
- prompt += `<file name="${file.name}" type="${file.type}">
3878
- ${file.content}
3879
- </file>
3880
- `;
3881
- }
3882
- prompt += "</supporting_files>\n";
3883
- }
3884
- } catch (_error) {
3885
- this.logger.debug("No existing task files found for planning", {
3886
- taskId: task.id
3887
- });
3888
- }
3889
- const templateVariables = {
3890
- task_id: task.id,
3891
- task_title: task.title,
3892
- task_description: processedDescription,
3893
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
3894
- repository: task.repository || ""
3895
- };
3896
- const planTemplate = await this.generatePlanTemplate(templateVariables);
3897
- prompt += "\n<instructions>\n";
3898
- prompt += "Analyze the codebase and create a detailed implementation plan. Use the template structure below, filling each section with specific, actionable information.\n";
3899
- prompt += "</instructions>\n\n";
3900
- prompt += "<plan_template>\n";
3901
- prompt += planTemplate;
3902
- prompt += "\n</plan_template>";
3903
- return prompt;
3904
- }
3905
- async buildExecutionPrompt(task, repositoryPath) {
3906
- const { description: descriptionAfterFiles, referencedFiles } = await this.processFileReferences(task.description, repositoryPath);
3907
- const { description: processedDescription, referencedResources } = await this.processUrlReferences(descriptionAfterFiles);
3908
- let prompt = "<task>\n";
3909
- prompt += `<title>${task.title}</title>
3910
- `;
3911
- prompt += `<description>${processedDescription}</description>
3912
- `;
3913
- if (task.repository) {
3914
- prompt += `<repository>${task.repository}</repository>
3915
- `;
3916
- }
3917
- prompt += "</task>\n";
3918
- if (referencedFiles.length > 0) {
3919
- prompt += "\n<referenced_files>\n";
3920
- for (const file of referencedFiles) {
3921
- prompt += `<file path="${file.path}">
3922
- \`\`\`
3923
- ${file.content}
3924
- \`\`\`
3925
- </file>
3926
- `;
3927
- }
3928
- prompt += "</referenced_files>\n";
3929
- }
3930
- if (referencedResources.length > 0) {
3931
- prompt += "\n<referenced_resources>\n";
3932
- for (const resource of referencedResources) {
3933
- prompt += `<resource type="${resource.type}" url="${resource.url}">
3934
- `;
3935
- prompt += `<title>${resource.title}</title>
3936
- `;
3937
- prompt += `<content>${resource.content}</content>
3938
- `;
3939
- prompt += "</resource>\n";
3940
- }
3941
- prompt += "</referenced_resources>\n";
3942
- }
3943
- try {
3944
- const taskFiles = await this.getTaskFiles(task.id);
3945
- const hasPlan = taskFiles.some((f) => f.type === "plan");
3946
- const todosFile = taskFiles.find(
3947
- (f) => f.name === "todos.json"
3948
- );
3949
- if (taskFiles.length > 0) {
3950
- prompt += "\n<context>\n";
3951
- for (const file of taskFiles) {
3952
- if (file.type === "plan") {
3953
- prompt += `<plan>
3954
- ${file.content}
3955
- </plan>
3956
- `;
3957
- } else if (file.name === "todos.json") {
3958
- } else {
3959
- prompt += `<file name="${file.name}" type="${file.type}">
3960
- ${file.content}
3961
- </file>
3962
- `;
3963
- }
3964
- }
3965
- prompt += "</context>\n";
3966
- }
3967
- if (todosFile) {
3968
- try {
3969
- const todos = JSON.parse(todosFile.content);
3970
- if (todos.items && todos.items.length > 0) {
3971
- prompt += "\n<previous_todos>\n";
3972
- prompt += "You previously created the following todo list for this task:\n\n";
3973
- for (const item of todos.items) {
3974
- const statusIcon = item.status === "completed" ? "\u2713" : item.status === "in_progress" ? "\u25B6" : "\u25CB";
3975
- prompt += `${statusIcon} [${item.status}] ${item.content}
3976
- `;
3977
- }
3978
- prompt += `
3979
- Progress: ${todos.metadata.completed}/${todos.metadata.total} completed
3980
- `;
3981
- prompt += "\nYou can reference this list when resuming work or create an updated list as needed.\n";
3982
- prompt += "</previous_todos>\n";
3983
- }
3984
- } catch (error) {
3985
- this.logger.debug("Failed to parse todos.json for context", {
3986
- error
3987
- });
3988
- }
3989
- }
3990
- prompt += "\n<instructions>\n";
3991
- if (hasPlan) {
3992
- prompt += "Implement the changes described in the execution plan. Follow the plan step-by-step and make the necessary file modifications.\n";
3993
- } else {
3994
- prompt += "Implement the changes described in the task. Make the necessary file modifications to complete the task.\n";
3995
- }
3996
- prompt += "</instructions>";
3997
- } catch (_error) {
3998
- this.logger.debug("No supporting files found for execution", {
3999
- taskId: task.id
4000
- });
4001
- prompt += "\n<instructions>\n";
4002
- prompt += "Implement the changes described in the task.\n";
4003
- prompt += "</instructions>";
4004
- }
4005
- return prompt;
4006
- }
4007
- };
4008
-
4009
- // src/session-store.ts
4010
- var SessionStore = class {
4011
- posthogAPI;
4012
- pendingEntries = /* @__PURE__ */ new Map();
4013
- flushTimeouts = /* @__PURE__ */ new Map();
4014
- configs = /* @__PURE__ */ new Map();
4015
- logger;
4016
- constructor(posthogAPI, logger2) {
4017
- this.posthogAPI = posthogAPI;
4018
- this.logger = logger2 ?? new Logger({ debug: false, prefix: "[SessionStore]" });
4019
- const flushAllAndExit = async () => {
4020
- const flushPromises = [];
4021
- for (const sessionId of this.configs.keys()) {
4022
- flushPromises.push(this.flush(sessionId));
4023
- }
4024
- await Promise.all(flushPromises);
4025
- process.exit(0);
4026
- };
4027
- process.on("beforeExit", () => {
4028
- flushAllAndExit().catch((e) => this.logger.error("Flush failed:", e));
4029
- });
4030
- process.on("SIGINT", () => {
4031
- flushAllAndExit().catch((e) => this.logger.error("Flush failed:", e));
4032
- });
4033
- process.on("SIGTERM", () => {
4034
- flushAllAndExit().catch((e) => this.logger.error("Flush failed:", e));
4035
- });
4036
- }
4037
- /** Register a session for persistence */
4038
- register(sessionId, config) {
4039
- this.configs.set(sessionId, config);
4040
- }
4041
- /** Unregister and flush pending */
4042
- async unregister(sessionId) {
4043
- await this.flush(sessionId);
4044
- this.configs.delete(sessionId);
4045
- }
4046
- /** Check if a session is registered for persistence */
4047
- isRegistered(sessionId) {
4048
- return this.configs.has(sessionId);
4049
- }
4050
- /**
4051
- * Append a raw JSON-RPC line for persistence.
4052
- * Parses and wraps as StoredNotification for the API.
4053
- */
4054
- appendRawLine(sessionId, line) {
4055
- const config = this.configs.get(sessionId);
4056
- if (!config) {
4057
- return;
4058
- }
4059
- try {
4060
- const message = JSON.parse(line);
4061
- const entry = {
4062
- type: "notification",
4063
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4064
- notification: message
4065
- };
4066
- const pending = this.pendingEntries.get(sessionId) ?? [];
4067
- pending.push(entry);
4068
- this.pendingEntries.set(sessionId, pending);
4069
- this.scheduleFlush(sessionId);
4070
- } catch {
4071
- this.logger.warn("Failed to parse raw line for persistence", {
4072
- sessionId,
4073
- lineLength: line.length
4010
+ appendRawLine(sessionId, line) {
4011
+ const config = this.configs.get(sessionId);
4012
+ if (!config) {
4013
+ return;
4014
+ }
4015
+ try {
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
4074
4030
  });
4075
4031
  }
4076
4032
  }
@@ -4303,1333 +4259,38 @@ var TaskManager = class {
4303
4259
  }
4304
4260
  scheduleTimeout(executionId, timeout = this.defaultTimeout) {
4305
4261
  setTimeout(() => {
4306
- const execution = this.executionStates.get(executionId);
4307
- if (execution && execution.status === "running") {
4308
- execution.status = "timeout";
4309
- execution.completedAt = Date.now();
4310
- execution.abortController?.abort();
4311
- if (!execution.result) {
4312
- execution.result = {
4313
- status: "timeout",
4314
- message: "Execution timed out"
4315
- };
4316
- }
4317
- }
4318
- }, timeout);
4319
- }
4320
- cleanup(olderThan = 60 * 60 * 1e3) {
4321
- const cutoff = Date.now() - olderThan;
4322
- for (const [executionId, execution] of this.executionStates) {
4323
- if (execution.completedAt && execution.completedAt < cutoff) {
4324
- this.executionStates.delete(executionId);
4325
- }
4326
- }
4327
- }
4328
- };
4329
-
4330
- // src/template-manager.ts
4331
- import { existsSync as existsSync2, promises as fs4 } from "fs";
4332
- import { dirname, join as join4 } from "path";
4333
- import { fileURLToPath } from "url";
4334
- var logger = new Logger({ prefix: "[TemplateManager]" });
4335
- var TemplateManager = class {
4336
- templatesDir;
4337
- constructor() {
4338
- const __filename = fileURLToPath(import.meta.url);
4339
- const __dirname = dirname(__filename);
4340
- const candidateDirs = [
4341
- // Standard build output (dist/src/template-manager.js -> dist/templates)
4342
- join4(__dirname, "..", "templates"),
4343
- // If preserveModules creates nested structure (dist/src/template-manager.js -> dist/src/templates)
4344
- join4(__dirname, "templates"),
4345
- // Development scenarios (src/template-manager.ts -> src/templates)
4346
- join4(__dirname, "..", "..", "src", "templates"),
4347
- // Package root templates directory
4348
- join4(__dirname, "..", "..", "templates"),
4349
- // When node_modules symlink or installed (node_modules/@posthog/agent/dist/src/... -> node_modules/@posthog/agent/dist/templates)
4350
- join4(__dirname, "..", "..", "dist", "templates"),
4351
- // When consumed from node_modules deep in tree
4352
- join4(__dirname, "..", "..", "..", "templates"),
4353
- join4(__dirname, "..", "..", "..", "dist", "templates"),
4354
- join4(__dirname, "..", "..", "..", "src", "templates"),
4355
- // When bundled by Vite/Webpack (e.g., .vite/build/index.js -> node_modules/@posthog/agent/dist/templates)
4356
- // Try to find node_modules from current location
4357
- join4(
4358
- __dirname,
4359
- "..",
4360
- "node_modules",
4361
- "@posthog",
4362
- "agent",
4363
- "dist",
4364
- "templates"
4365
- ),
4366
- join4(
4367
- __dirname,
4368
- "..",
4369
- "..",
4370
- "node_modules",
4371
- "@posthog",
4372
- "agent",
4373
- "dist",
4374
- "templates"
4375
- ),
4376
- join4(
4377
- __dirname,
4378
- "..",
4379
- "..",
4380
- "..",
4381
- "node_modules",
4382
- "@posthog",
4383
- "agent",
4384
- "dist",
4385
- "templates"
4386
- )
4387
- ];
4388
- const resolvedDir = candidateDirs.find((dir) => existsSync2(dir));
4389
- if (!resolvedDir) {
4390
- logger.error("Could not find templates directory.");
4391
- logger.error(`Current file: ${__filename}`);
4392
- logger.error(`Current dir: ${__dirname}`);
4393
- logger.error(
4394
- `Tried: ${candidateDirs.map((d) => `
4395
- - ${d} (exists: ${existsSync2(d)})`).join("")}`
4396
- );
4397
- }
4398
- this.templatesDir = resolvedDir ?? candidateDirs[0];
4399
- }
4400
- async loadTemplate(templateName) {
4401
- try {
4402
- const templatePath = join4(this.templatesDir, templateName);
4403
- return await fs4.readFile(templatePath, "utf8");
4404
- } catch (error) {
4405
- throw new Error(
4406
- `Failed to load template ${templateName} from ${this.templatesDir}: ${error}`
4407
- );
4408
- }
4409
- }
4410
- substituteVariables(template, variables) {
4411
- let result = template;
4412
- for (const [key, value] of Object.entries(variables)) {
4413
- if (value !== void 0) {
4414
- const placeholder = new RegExp(`{{${key}}}`, "g");
4415
- result = result.replace(placeholder, value);
4416
- }
4417
- }
4418
- result = result.replace(/{{[^}]+}}/g, "[PLACEHOLDER]");
4419
- return result;
4420
- }
4421
- async generatePlan(variables) {
4422
- const template = await this.loadTemplate("plan-template.md");
4423
- return this.substituteVariables(template, {
4424
- ...variables,
4425
- date: variables.date || (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
4426
- });
4427
- }
4428
- async generateCustomFile(templateName, variables) {
4429
- const template = await this.loadTemplate(templateName);
4430
- return this.substituteVariables(template, {
4431
- ...variables,
4432
- date: variables.date || (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
4433
- });
4434
- }
4435
- async createTaskStructure(taskId, taskTitle, options) {
4436
- const files = [];
4437
- const variables = {
4438
- task_id: taskId,
4439
- task_title: taskTitle,
4440
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
4441
- };
4442
- if (options?.includePlan !== false) {
4443
- const planContent = await this.generatePlan(variables);
4444
- files.push({
4445
- name: "plan.md",
4446
- content: planContent,
4447
- type: "plan"
4448
- });
4449
- }
4450
- if (options?.additionalFiles) {
4451
- for (const file of options.additionalFiles) {
4452
- let content;
4453
- if (file.template) {
4454
- content = await this.generateCustomFile(file.template, variables);
4455
- } else if (file.content) {
4456
- content = this.substituteVariables(file.content, variables);
4457
- } else {
4458
- content = `# ${file.name}
4459
-
4460
- Placeholder content for ${file.name}`;
4461
- }
4462
- files.push({
4463
- name: file.name,
4464
- content,
4465
- type: file.name.includes("context") ? "context" : "reference"
4466
- });
4467
- }
4468
- }
4469
- return files;
4470
- }
4471
- generatePostHogReadme() {
4472
- return `# PostHog Task Files
4473
-
4474
- This directory contains task-related files.
4475
-
4476
- ## Structure
4477
-
4478
- Each task has its own subdirectory: \`.posthog/{task-id}/\`
4479
-
4480
- ### Common Files
4481
-
4482
- - **plan.md** - Implementation plan generated during planning phase
4483
- - **Supporting files** - Any additional files added for task context
4484
- - **artifacts/** - Generated files, outputs, and temporary artifacts
4485
-
4486
- ### Usage
4487
-
4488
- These files are:
4489
- - Version controlled alongside your code
4490
- - Used for task context and planning
4491
- - Available for review in pull requests
4492
- - Organized by task ID for easy reference
4493
-
4494
- ### Gitignore
4495
-
4496
- Customize \`.posthog/.gitignore\` to control which files are committed:
4497
- - Include plans and documentation by default
4498
- - Exclude temporary files and sensitive data
4499
- - Customize based on your team's needs
4500
- `;
4501
- }
4502
- };
4503
-
4504
- // src/workflow/steps/build.ts
4505
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
4506
-
4507
- // src/agents/execution.ts
4508
- var EXECUTION_SYSTEM_PROMPT = `<role>
4509
- PostHog AI Execution Agent \u2014 autonomously implement tasks as merge-ready code following project conventions.
4510
- </role>
4511
-
4512
- <context>
4513
- You have access to local repository files and PostHog MCP server. Work primarily with local files for implementation. Commit changes regularly.
4514
- </context>
4515
-
4516
- <constraints>
4517
- - Follow existing code style, patterns, and conventions found in the repository
4518
- - Minimize new external dependencies \u2014 only add when necessary
4519
- - Implement structured logging and error handling (never log secrets)
4520
- - Avoid destructive shell commands
4521
- - Create/update .gitignore to exclude build artifacts, dependencies, and temp files
4522
- </constraints>
4523
-
4524
- <approach>
4525
- 1. Review the implementation plan if provided, or create your own todo list
4526
- 2. Execute changes step by step
4527
- 3. Test thoroughly and verify functionality
4528
- 4. Commit changes with clear messages
4529
- </approach>
4530
-
4531
- <checklist>
4532
- Before completing the task, verify:
4533
- - .gitignore includes build artifacts, node_modules, __pycache__, etc.
4534
- - Dependency files (package.json, requirements.txt) use exact versions
4535
- - Code compiles and tests pass
4536
- - Added or updated relevant tests
4537
- - Captured meaningful events with PostHog SDK where appropriate
4538
- - Wrapped new logic in PostHog feature flags where appropriate
4539
- - Updated documentation, README, or type hints as needed
4540
- </checklist>
4541
-
4542
- <output_format>
4543
- Provide a concise summary of changes made when finished.
4544
- </output_format>`;
4545
-
4546
- // src/todo-manager.ts
4547
- var TodoManager = class {
4548
- fileManager;
4549
- logger;
4550
- constructor(fileManager, logger2) {
4551
- this.fileManager = fileManager;
4552
- this.logger = logger2 || new Logger({ debug: false, prefix: "[TodoManager]" });
4553
- }
4554
- async readTodos(taskId) {
4555
- try {
4556
- const content = await this.fileManager.readTaskFile(taskId, "todos.json");
4557
- if (!content) {
4558
- return null;
4559
- }
4560
- const parsed = JSON.parse(content);
4561
- this.logger.debug("Loaded todos", {
4562
- taskId,
4563
- total: parsed.metadata.total,
4564
- pending: parsed.metadata.pending,
4565
- in_progress: parsed.metadata.in_progress,
4566
- completed: parsed.metadata.completed
4567
- });
4568
- return parsed;
4569
- } catch (error) {
4570
- this.logger.debug("Failed to read todos.json", {
4571
- taskId,
4572
- error: error instanceof Error ? error.message : String(error)
4573
- });
4574
- return null;
4575
- }
4576
- }
4577
- async writeTodos(taskId, todos) {
4578
- this.logger.debug("Writing todos", {
4579
- taskId,
4580
- total: todos.metadata.total,
4581
- pending: todos.metadata.pending,
4582
- in_progress: todos.metadata.in_progress,
4583
- completed: todos.metadata.completed
4584
- });
4585
- await this.fileManager.writeTaskFile(taskId, {
4586
- name: "todos.json",
4587
- content: JSON.stringify(todos, null, 2),
4588
- type: "artifact"
4589
- });
4590
- this.logger.info("Todos saved", {
4591
- taskId,
4592
- total: todos.metadata.total,
4593
- completed: todos.metadata.completed
4594
- });
4595
- }
4596
- parseTodoWriteInput(toolInput) {
4597
- const items = [];
4598
- if (toolInput.todos && Array.isArray(toolInput.todos)) {
4599
- for (const todo of toolInput.todos) {
4600
- items.push({
4601
- content: todo.content || "",
4602
- status: todo.status || "pending",
4603
- activeForm: todo.activeForm || todo.content || ""
4604
- });
4605
- }
4606
- }
4607
- const metadata = this.calculateMetadata(items);
4608
- return { items, metadata };
4609
- }
4610
- calculateMetadata(items) {
4611
- const total = items.length;
4612
- const pending = items.filter((t) => t.status === "pending").length;
4613
- const in_progress = items.filter((t) => t.status === "in_progress").length;
4614
- const completed = items.filter((t) => t.status === "completed").length;
4615
- return {
4616
- total,
4617
- pending,
4618
- in_progress,
4619
- completed,
4620
- last_updated: (/* @__PURE__ */ new Date()).toISOString()
4621
- };
4622
- }
4623
- async getTodoContext(taskId) {
4624
- const todos = await this.readTodos(taskId);
4625
- if (!todos || todos.items.length === 0) {
4626
- return "";
4627
- }
4628
- const lines = ["## Previous Todo List\n"];
4629
- lines.push("You previously created the following todo list:\n");
4630
- for (const item of todos.items) {
4631
- const statusIcon = item.status === "completed" ? "\u2713" : item.status === "in_progress" ? "\u25B6" : "\u25CB";
4632
- lines.push(`${statusIcon} [${item.status}] ${item.content}`);
4633
- }
4634
- lines.push(
4635
- `
4636
- Progress: ${todos.metadata.completed}/${todos.metadata.total} completed
4637
- `
4638
- );
4639
- return lines.join("\n");
4640
- }
4641
- // check for TodoWrite tool call and persist if found
4642
- async checkAndPersistFromMessage(message, taskId) {
4643
- if (message.type !== "assistant" || typeof message.message !== "object" || !message.message || !("content" in message.message) || !Array.isArray(message.message.content)) {
4644
- return null;
4645
- }
4646
- for (const block of message.message.content) {
4647
- if (block.type === "tool_use" && block.name === "TodoWrite") {
4648
- try {
4649
- this.logger.info("TodoWrite detected, persisting todos", { taskId });
4650
- const todoList = this.parseTodoWriteInput(block.input);
4651
- await this.writeTodos(taskId, todoList);
4652
- this.logger.info("Persisted todos successfully", {
4653
- taskId,
4654
- total: todoList.metadata.total,
4655
- completed: todoList.metadata.completed
4656
- });
4657
- return todoList;
4658
- } catch (error) {
4659
- this.logger.error("Failed to persist todos", {
4660
- taskId,
4661
- error: error instanceof Error ? error.message : String(error)
4662
- });
4663
- return null;
4664
- }
4665
- }
4666
- }
4667
- return null;
4668
- }
4669
- };
4670
-
4671
- // src/types.ts
4672
- var PermissionMode = /* @__PURE__ */ ((PermissionMode2) => {
4673
- PermissionMode2["PLAN"] = "plan";
4674
- PermissionMode2["DEFAULT"] = "default";
4675
- PermissionMode2["ACCEPT_EDITS"] = "acceptEdits";
4676
- PermissionMode2["BYPASS"] = "bypassPermissions";
4677
- return PermissionMode2;
4678
- })(PermissionMode || {});
4679
-
4680
- // src/workflow/steps/build.ts
4681
- var buildStep = async ({ step, context }) => {
4682
- const {
4683
- task,
4684
- cwd,
4685
- options,
4686
- logger: logger2,
4687
- promptBuilder,
4688
- sessionId,
4689
- mcpServers,
4690
- gitManager,
4691
- sendNotification
4692
- } = context;
4693
- const stepLogger = logger2.child("BuildStep");
4694
- const latestRun = task.latest_run;
4695
- const prExists = latestRun?.output && typeof latestRun.output === "object" ? latestRun.output.pr_url : null;
4696
- if (prExists) {
4697
- stepLogger.info("PR already exists, skipping build phase", {
4698
- taskId: task.id
4699
- });
4700
- return { status: "skipped" };
4701
- }
4702
- stepLogger.info("Starting build phase", { taskId: task.id });
4703
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_START, {
4704
- sessionId,
4705
- phase: "build"
4706
- });
4707
- const executionPrompt = await promptBuilder.buildExecutionPrompt(task, cwd);
4708
- const fullPrompt = `${EXECUTION_SYSTEM_PROMPT}
4709
-
4710
- ${executionPrompt}`;
4711
- const configuredPermissionMode = options.permissionMode ?? (typeof step.permissionMode === "string" ? step.permissionMode : step.permissionMode) ?? "acceptEdits" /* ACCEPT_EDITS */;
4712
- const baseOptions = {
4713
- model: step.model,
4714
- cwd,
4715
- permissionMode: configuredPermissionMode,
4716
- settingSources: ["local"],
4717
- mcpServers,
4718
- // Allow all tools for build phase - full read/write access needed for implementation
4719
- allowedTools: [
4720
- "Task",
4721
- "Bash",
4722
- "BashOutput",
4723
- "KillBash",
4724
- "Edit",
4725
- "Read",
4726
- "Write",
4727
- "Glob",
4728
- "Grep",
4729
- "NotebookEdit",
4730
- "WebFetch",
4731
- "WebSearch",
4732
- "ListMcpResources",
4733
- "ReadMcpResource",
4734
- "TodoWrite"
4735
- ]
4736
- };
4737
- if (options.canUseTool) {
4738
- baseOptions.canUseTool = options.canUseTool;
4739
- }
4740
- const response = query2({
4741
- prompt: fullPrompt,
4742
- options: { ...baseOptions, ...options.queryOverrides || {} }
4743
- });
4744
- const commitTracker = await gitManager.trackCommitsDuring();
4745
- const todoManager = new TodoManager(context.fileManager, stepLogger);
4746
- try {
4747
- for await (const message of response) {
4748
- const todoList = await todoManager.checkAndPersistFromMessage(
4749
- message,
4750
- task.id
4751
- );
4752
- if (todoList) {
4753
- await sendNotification(POSTHOG_NOTIFICATIONS.ARTIFACT, {
4754
- sessionId,
4755
- kind: "todos",
4756
- content: todoList
4757
- });
4758
- }
4759
- }
4760
- } catch (error) {
4761
- stepLogger.error("Error during build step query", error);
4762
- throw error;
4763
- }
4764
- const { commitCreated, pushedBranch } = await commitTracker.finalize({
4765
- commitMessage: `Implementation for ${task.title}`,
4766
- push: step.push
4767
- });
4768
- context.stepResults[step.id] = { commitCreated };
4769
- if (!commitCreated) {
4770
- stepLogger.warn("No changes to commit in build phase", { taskId: task.id });
4771
- } else {
4772
- stepLogger.info("Build commits finalized", {
4773
- taskId: task.id,
4774
- pushedBranch
4775
- });
4776
- }
4777
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
4778
- sessionId,
4779
- phase: "build"
4780
- });
4781
- return { status: "completed" };
4782
- };
4783
-
4784
- // src/workflow/utils.ts
4785
- async function finalizeStepGitActions(context, step, options) {
4786
- if (!step.commit) {
4787
- return false;
4788
- }
4789
- const { gitManager, logger: logger2 } = context;
4790
- const hasStagedChanges = await gitManager.hasStagedChanges();
4791
- if (!hasStagedChanges && !options.allowEmptyCommit) {
4792
- logger2.debug("No staged changes to commit for step", { stepId: step.id });
4793
- return false;
4794
- }
4795
- try {
4796
- await gitManager.commitChanges(options.commitMessage);
4797
- logger2.info("Committed changes for step", {
4798
- stepId: step.id,
4799
- message: options.commitMessage
4800
- });
4801
- } catch (error) {
4802
- logger2.error("Failed to commit changes for step", {
4803
- stepId: step.id,
4804
- error: error instanceof Error ? error.message : String(error)
4805
- });
4806
- throw error;
4807
- }
4808
- if (step.push) {
4809
- const branchName = await gitManager.getCurrentBranch();
4810
- await gitManager.pushBranch(branchName);
4811
- logger2.info("Pushed branch after step", {
4812
- stepId: step.id,
4813
- branch: branchName
4814
- });
4815
- }
4816
- return true;
4817
- }
4818
-
4819
- // src/workflow/steps/finalize.ts
4820
- var MAX_SNIPPET_LENGTH = 1200;
4821
- var finalizeStep = async ({ step, context }) => {
4822
- const { task, logger: logger2, fileManager, gitManager, posthogAPI, runId } = context;
4823
- const stepLogger = logger2.child("FinalizeStep");
4824
- const artifacts = await fileManager.collectTaskArtifacts(task.id);
4825
- let uploadedArtifacts;
4826
- if (artifacts.length && posthogAPI && runId) {
4827
- try {
4828
- const payload = artifacts.map((artifact) => ({
4829
- name: artifact.name,
4830
- type: artifact.type,
4831
- content: artifact.content,
4832
- content_type: artifact.contentType
4833
- }));
4834
- uploadedArtifacts = await posthogAPI.uploadTaskArtifacts(
4835
- task.id,
4836
- runId,
4837
- payload
4838
- );
4839
- stepLogger.info("Uploaded task artifacts to PostHog", {
4840
- taskId: task.id,
4841
- uploadedCount: uploadedArtifacts.length
4842
- });
4843
- } catch (error) {
4844
- stepLogger.warn("Failed to upload task artifacts", {
4845
- taskId: task.id,
4846
- error: error instanceof Error ? error.message : String(error)
4847
- });
4848
- }
4849
- } else {
4850
- stepLogger.debug("Skipping artifact upload", {
4851
- hasArtifacts: artifacts.length > 0,
4852
- hasPostHogApi: Boolean(posthogAPI),
4853
- runId
4854
- });
4855
- }
4856
- const prBody = buildPullRequestBody(task, artifacts, uploadedArtifacts);
4857
- await fileManager.cleanupTaskDirectory(task.id);
4858
- await gitManager.addAllPostHogFiles();
4859
- await finalizeStepGitActions(context, step, {
4860
- commitMessage: `Cleanup task artifacts for ${task.title}`,
4861
- allowEmptyCommit: true
4862
- });
4863
- context.stepResults[step.id] = {
4864
- prBody,
4865
- uploadedArtifacts,
4866
- artifactCount: artifacts.length
4867
- };
4868
- return { status: "completed" };
4869
- };
4870
- function buildPullRequestBody(task, artifacts, uploaded) {
4871
- const lines = [];
4872
- const taskSlug = task.slug || task.id;
4873
- lines.push("## Task context");
4874
- lines.push(`- **Task**: ${taskSlug}`);
4875
- lines.push(`- **Title**: ${task.title}`);
4876
- lines.push(`- **Origin**: ${task.origin_product}`);
4877
- if (task.description) {
4878
- lines.push("");
4879
- lines.push(`> ${task.description.trim().split("\n").join("\n> ")}`);
4880
- }
4881
- const usedFiles = /* @__PURE__ */ new Set();
4882
- const contextArtifact = artifacts.find(
4883
- (artifact) => artifact.name === "context.md"
4884
- );
4885
- if (contextArtifact) {
4886
- lines.push("");
4887
- lines.push("### Task prompt");
4888
- lines.push(contextArtifact.content);
4889
- usedFiles.add(contextArtifact.name);
4890
- }
4891
- const researchArtifact = artifacts.find(
4892
- (artifact) => artifact.name === "research.json"
4893
- );
4894
- if (researchArtifact) {
4895
- usedFiles.add(researchArtifact.name);
4896
- const researchSection = formatResearchSection(researchArtifact.content);
4897
- if (researchSection) {
4898
- lines.push("");
4899
- lines.push(researchSection);
4900
- }
4901
- }
4902
- const planArtifact = artifacts.find(
4903
- (artifact) => artifact.name === "plan.md"
4904
- );
4905
- if (planArtifact) {
4906
- lines.push("");
4907
- lines.push("### Implementation plan");
4908
- lines.push(planArtifact.content);
4909
- usedFiles.add(planArtifact.name);
4910
- }
4911
- const todoArtifact = artifacts.find(
4912
- (artifact) => artifact.name === "todos.json"
4913
- );
4914
- if (todoArtifact) {
4915
- const summary = summarizeTodos(todoArtifact.content);
4916
- if (summary) {
4917
- lines.push("");
4918
- lines.push("### Todo list");
4919
- lines.push(summary);
4920
- }
4921
- usedFiles.add(todoArtifact.name);
4922
- }
4923
- const remainingArtifacts = artifacts.filter(
4924
- (artifact) => !usedFiles.has(artifact.name)
4925
- );
4926
- if (remainingArtifacts.length) {
4927
- lines.push("");
4928
- lines.push("### Additional artifacts");
4929
- for (const artifact of remainingArtifacts) {
4930
- lines.push(`#### ${artifact.name}`);
4931
- lines.push(renderCodeFence(artifact.content));
4932
- }
4933
- }
4934
- const artifactList = uploaded ?? artifacts.map((artifact) => ({
4935
- name: artifact.name,
4936
- type: artifact.type
4937
- }));
4938
- if (artifactList.length) {
4939
- lines.push("");
4940
- lines.push("### Uploaded artifacts");
4941
- for (const artifact of artifactList) {
4942
- const rawStoragePath = "storage_path" in artifact ? artifact.storage_path : void 0;
4943
- const storagePath = typeof rawStoragePath === "string" ? rawStoragePath : void 0;
4944
- const storage = storagePath && storagePath.trim().length > 0 ? ` \u2013 \`${storagePath.trim()}\`` : "";
4945
- lines.push(`- ${artifact.name} (${artifact.type})${storage}`);
4946
- }
4947
- }
4948
- return lines.join("\n\n");
4949
- }
4950
- function renderCodeFence(content) {
4951
- const snippet = truncate(content, MAX_SNIPPET_LENGTH);
4952
- return ["```", snippet, "```"].join("\n");
4953
- }
4954
- function truncate(value, maxLength) {
4955
- if (value.length <= maxLength) {
4956
- return value;
4957
- }
4958
- return `${value.slice(0, maxLength)}
4959
- \u2026`;
4960
- }
4961
- function formatResearchSection(content) {
4962
- try {
4963
- const parsed = JSON.parse(content);
4964
- const sections = [];
4965
- if (parsed.context) {
4966
- sections.push("### Research summary");
4967
- sections.push(parsed.context);
4968
- }
4969
- if (parsed.questions?.length) {
4970
- sections.push("");
4971
- sections.push("### Questions needing answers");
4972
- for (const question of parsed.questions) {
4973
- sections.push(`- ${question.question ?? question}`);
4974
- }
4975
- }
4976
- if (parsed.answers?.length) {
4977
- sections.push("");
4978
- sections.push("### Answers provided");
4979
- for (const answer of parsed.answers) {
4980
- const questionId = answer.questionId ? ` (Q: ${answer.questionId})` : "";
4981
- sections.push(
4982
- `- ${answer.selectedOption || answer.customInput || "answer"}${questionId}`
4983
- );
4984
- }
4985
- }
4986
- return sections.length ? sections.join("\n") : null;
4987
- } catch {
4988
- return null;
4989
- }
4990
- }
4991
- function summarizeTodos(content) {
4992
- try {
4993
- const data = JSON.parse(content);
4994
- const total = data?.metadata?.total ?? data?.items?.length;
4995
- const completed = data?.metadata?.completed ?? data?.items?.filter(
4996
- (item) => item.status === "completed"
4997
- ).length;
4998
- const lines = [`Progress: ${completed}/${total} completed`];
4999
- if (data?.items?.length) {
5000
- for (const item of data.items) {
5001
- lines.push(`- [${item.status}] ${item.content}`);
5002
- }
5003
- }
5004
- return lines.join("\n");
5005
- } catch {
5006
- return null;
5007
- }
5008
- }
5009
-
5010
- // src/workflow/steps/plan.ts
5011
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
5012
-
5013
- // src/agents/planning.ts
5014
- var PLANNING_SYSTEM_PROMPT = `<role>
5015
- PostHog AI Planning Agent \u2014 analyze codebases and create actionable implementation plans.
5016
- </role>
5017
-
5018
- <constraints>
5019
- - Read-only: analyze files, search code, explore structure
5020
- - No modifications or edits
5021
- - Output ONLY the plan markdown \u2014 no preamble, no acknowledgment, no meta-commentary
5022
- </constraints>
5023
-
5024
- <objective>
5025
- Create a detailed, actionable implementation plan that an execution agent can follow to complete the task successfully.
5026
- </objective>
5027
-
5028
- <process>
5029
- 1. Explore repository structure and identify relevant files/components
5030
- 2. Understand existing patterns, conventions, and dependencies
5031
- 3. Break down task requirements and identify technical constraints
5032
- 4. Define step-by-step implementation approach
5033
- 5. Specify files to modify/create with exact paths
5034
- 6. Identify testing requirements and potential risks
5035
- </process>
5036
-
5037
- <output_format>
5038
- 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.
5039
-
5040
- Required sections (follow the template provided in the task prompt):
5041
- - Summary: Brief overview of approach
5042
- - Files to Create/Modify: Specific paths and purposes
5043
- - Implementation Steps: Ordered list of actions
5044
- - Testing Strategy: How to verify it works
5045
- - Considerations: Dependencies, risks, edge cases
5046
- </output_format>
5047
-
5048
- <examples>
5049
- <bad_example>
5050
- "Sure! I'll create a detailed implementation plan for you to add authentication. Here's what we'll do..."
5051
- Reason: No preamble \u2014 output the plan directly
5052
- </bad_example>
5053
-
5054
- <good_example>
5055
- "# Implementation Plan
5056
-
5057
- ## Summary
5058
- Add JWT-based authentication to API endpoints using existing middleware pattern...
5059
-
5060
- ## Files to Modify
5061
- - src/middleware/auth.ts: Add JWT verification
5062
- ..."
5063
- Reason: Direct plan output with no meta-commentary
5064
- </good_example>
5065
- </examples>
5066
-
5067
- <context_integration>
5068
- If research findings, context files, or reference materials are provided:
5069
- - Incorporate research findings into your analysis
5070
- - Follow patterns and approaches identified in research
5071
- - Build upon or refine any existing planning work
5072
- - Reference specific files and components mentioned in context
5073
- </context_integration>`;
5074
-
5075
- // src/workflow/steps/plan.ts
5076
- var planStep = async ({ step, context }) => {
5077
- const {
5078
- task,
5079
- cwd,
5080
- isCloudMode,
5081
- options,
5082
- logger: logger2,
5083
- fileManager,
5084
- gitManager,
5085
- promptBuilder,
5086
- sessionId,
5087
- mcpServers,
5088
- sendNotification
5089
- } = context;
5090
- const stepLogger = logger2.child("PlanStep");
5091
- const existingPlan = await fileManager.readPlan(task.id);
5092
- if (existingPlan) {
5093
- stepLogger.info("Plan already exists, skipping step", { taskId: task.id });
5094
- return { status: "skipped" };
5095
- }
5096
- const researchData = await fileManager.readResearch(task.id);
5097
- if (researchData?.questions && !researchData.answered) {
5098
- stepLogger.info("Waiting for answered research questions", {
5099
- taskId: task.id
5100
- });
5101
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
5102
- sessionId,
5103
- phase: "research_questions"
5104
- });
5105
- return { status: "skipped", halt: true };
5106
- }
5107
- stepLogger.info("Starting planning phase", { taskId: task.id });
5108
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_START, {
5109
- sessionId,
5110
- phase: "planning"
5111
- });
5112
- let researchContext = "";
5113
- if (researchData) {
5114
- researchContext += `## Research Context
5115
-
5116
- ${researchData.context}
5117
-
5118
- `;
5119
- if (researchData.keyFiles.length > 0) {
5120
- researchContext += `**Key Files:**
5121
- ${researchData.keyFiles.map((f) => `- ${f}`).join("\n")}
5122
-
5123
- `;
5124
- }
5125
- if (researchData.blockers && researchData.blockers.length > 0) {
5126
- researchContext += `**Considerations:**
5127
- ${researchData.blockers.map((b) => `- ${b}`).join("\n")}
5128
-
5129
- `;
5130
- }
5131
- if (researchData.questions && researchData.answers && researchData.answered) {
5132
- researchContext += `## Implementation Decisions
5133
-
5134
- `;
5135
- for (const question of researchData.questions) {
5136
- const answer = researchData.answers.find(
5137
- (a) => a.questionId === question.id
5138
- );
5139
- researchContext += `### ${question.question}
5140
-
5141
- `;
5142
- if (answer) {
5143
- researchContext += `**Selected:** ${answer.selectedOption}
5144
- `;
5145
- if (answer.customInput) {
5146
- researchContext += `**Details:** ${answer.customInput}
5147
- `;
5148
- }
5149
- } else {
5150
- researchContext += `**Selected:** Not answered
5151
- `;
5152
- }
5153
- researchContext += `
5154
- `;
5155
- }
5156
- }
5157
- }
5158
- const planningPrompt = await promptBuilder.buildPlanningPrompt(task, cwd);
5159
- const fullPrompt = `${PLANNING_SYSTEM_PROMPT}
5160
-
5161
- ${planningPrompt}
5162
-
5163
- ${researchContext}`;
5164
- const baseOptions = {
5165
- model: step.model,
5166
- cwd,
5167
- permissionMode: "plan",
5168
- settingSources: ["local"],
5169
- mcpServers,
5170
- // Allow research tools: read-only operations, web search, MCP resources, and ExitPlanMode
5171
- allowedTools: [
5172
- "Read",
5173
- "Glob",
5174
- "Grep",
5175
- "WebFetch",
5176
- "WebSearch",
5177
- "ListMcpResources",
5178
- "ReadMcpResource",
5179
- "ExitPlanMode",
5180
- "TodoWrite",
5181
- "BashOutput"
5182
- ]
5183
- };
5184
- const response = query3({
5185
- prompt: fullPrompt,
5186
- options: { ...baseOptions, ...options.queryOverrides || {} }
5187
- });
5188
- const todoManager = new TodoManager(fileManager, stepLogger);
5189
- let planContent = "";
5190
- try {
5191
- for await (const message of response) {
5192
- const todoList = await todoManager.checkAndPersistFromMessage(
5193
- message,
5194
- task.id
5195
- );
5196
- if (todoList) {
5197
- await sendNotification(POSTHOG_NOTIFICATIONS.ARTIFACT, {
5198
- sessionId,
5199
- kind: "todos",
5200
- content: todoList
5201
- });
5202
- }
5203
- if (message.type === "assistant" && message.message?.content) {
5204
- for (const block of message.message.content) {
5205
- if (block.type === "text" && block.text) {
5206
- planContent += `${block.text}
5207
- `;
5208
- }
5209
- }
5210
- }
5211
- }
5212
- } catch (error) {
5213
- stepLogger.error("Error during plan step query", error);
5214
- throw error;
5215
- }
5216
- if (planContent.trim()) {
5217
- await fileManager.writePlan(task.id, planContent.trim());
5218
- stepLogger.info("Plan completed", { taskId: task.id });
5219
- }
5220
- await gitManager.addAllPostHogFiles();
5221
- await finalizeStepGitActions(context, step, {
5222
- commitMessage: `Planning phase for ${task.title}`
5223
- });
5224
- if (!isCloudMode) {
5225
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
5226
- sessionId,
5227
- phase: "planning"
5228
- });
5229
- return { status: "completed", halt: true };
5230
- }
5231
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
5232
- sessionId,
5233
- phase: "planning"
5234
- });
5235
- return { status: "completed" };
5236
- };
5237
-
5238
- // src/workflow/steps/research.ts
5239
- import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
5240
-
5241
- // src/agents/research.ts
5242
- var RESEARCH_SYSTEM_PROMPT = `<role>
5243
- PostHog AI Research Agent \u2014 analyze codebases to evaluate task actionability and identify missing information.
5244
- </role>
5245
-
5246
- <constraints>
5247
- - Read-only: analyze files, search code, explore structure
5248
- - No modifications or code changes
5249
- - Output structured JSON only
5250
- </constraints>
5251
-
5252
- <objective>
5253
- Your PRIMARY goal is to evaluate whether a task is actionable and assign an actionability score.
5254
-
5255
- Calculate an actionabilityScore (0-1) based on:
5256
- - **Task clarity** (0.4 weight): Is the task description specific and unambiguous?
5257
- - **Codebase context** (0.3 weight): Can you locate the relevant code and patterns?
5258
- - **Architectural decisions** (0.2 weight): Are the implementation approaches clear?
5259
- - **Dependencies** (0.1 weight): Are required dependencies and constraints understood?
5260
-
5261
- If actionabilityScore < 0.7, generate specific clarifying questions to increase confidence.
5262
-
5263
- Questions must present complete implementation choices, NOT request information from the user:
5264
- options: array of strings
5265
- - GOOD: options: ["Use Redux Toolkit (matches pattern in src/store/)", "Zustand (lighter weight)"]
5266
- - BAD: "Tell me which state management library to use"
5267
- - GOOD: options: ["Place in Button.tsx (existing component)", "create NewButton.tsx (separate concerns)?"]
5268
- - BAD: "Where should I put this code?"
5269
-
5270
- 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.
5271
- </objective>
5272
-
5273
- <process>
5274
- 1. Explore repository structure and identify relevant files/components
5275
- 2. Understand existing patterns, conventions, and dependencies
5276
- 3. Calculate actionabilityScore based on clarity, context, architecture, and dependencies
5277
- 4. Identify key files that will need modification
5278
- 5. If score < 0.7: generate 2-4 specific questions to resolve blockers
5279
- 6. Output JSON matching ResearchEvaluation schema
5280
- </process>
5281
-
5282
- <output_format>
5283
- Output ONLY valid JSON with no markdown wrappers, no preamble, no explanation:
5284
-
5285
- {
5286
- "actionabilityScore": 0.85,
5287
- "context": "Brief 2-3 sentence summary of the task and implementation approach",
5288
- "keyFiles": ["path/to/file1.ts", "path/to/file2.ts"],
5289
- "blockers": ["Optional: what's preventing full confidence"],
5290
- "questions": [
5291
- {
5292
- "id": "q1",
5293
- "question": "Specific architectural decision needed?",
5294
- "options": [
5295
- "First approach with concrete details",
5296
- "Alternative approach with concrete details",
5297
- "Third option if needed"
5298
- ]
5299
- }
5300
- ]
5301
- }
5302
-
5303
- Rules:
5304
- - actionabilityScore: number between 0 and 1
5305
- - context: concise summary for planning phase
5306
- - keyFiles: array of file paths that need modification
5307
- - blockers: optional array explaining confidence gaps
5308
- - questions: ONLY include if actionabilityScore < 0.7
5309
- - Each question must have 2-3 options (maximum 3)
5310
- - Max 3 questions total
5311
- - Options must be complete, actionable choices that require NO additional user input
5312
- - NEVER use options like "Tell me the pattern", "Show me examples", "Specify the approach"
5313
- - Each option must be a full implementation decision that can be directly acted upon
5314
- </output_format>
5315
-
5316
- <scoring_examples>
5317
- <example score="0.9">
5318
- Task: "Fix typo in login button text"
5319
- Reasoning: Completely clear task, found exact component, no architectural decisions
5320
- </example>
5321
-
5322
- <example score="0.75">
5323
- Task: "Add caching to API endpoints"
5324
- Reasoning: Clear goal, found endpoints, but multiple caching strategies possible
5325
- </example>
5326
-
5327
- <example score="0.55">
5328
- Task: "Improve performance"
5329
- Reasoning: Vague task, unclear scope, needs questions about which areas to optimize
5330
- Questions needed: Which features are slow? What metrics define success?
5331
- </example>
5332
-
5333
- <example score="0.3">
5334
- Task: "Add the new feature"
5335
- Reasoning: Extremely vague, no context, cannot locate relevant code
5336
- Questions needed: What feature? Which product area? What should it do?
5337
- </example>
5338
- </scoring_examples>
5339
-
5340
- <question_examples>
5341
- <good_example>
5342
- {
5343
- "id": "q1",
5344
- "question": "Which caching layer should we use for API responses?",
5345
- "options": [
5346
- "Redis with 1-hour TTL (existing infrastructure, requires Redis client setup)",
5347
- "In-memory LRU cache with 100MB limit (simpler, single-server only)",
5348
- "HTTP Cache-Control headers only (minimal backend changes, relies on browser/CDN)"
5349
- ]
5350
- }
5351
- Reason: Each option is a complete, actionable decision with concrete details
5352
- </good_example>
5353
-
5354
- <good_example>
5355
- {
5356
- "id": "q2",
5357
- "question": "Where should the new analytics tracking code be placed?",
5358
- "options": [
5359
- "In the existing UserAnalytics.ts module alongside page view tracking",
5360
- "Create a new EventTracking.ts module in src/analytics/ for all event tracking",
5361
- "Add directly to each component that needs tracking (no centralized module)"
5362
- ]
5363
- }
5364
- Reason: Specific file paths and architectural patterns, no user input needed
5365
- </good_example>
5366
-
5367
- <bad_example>
5368
- {
5369
- "id": "q1",
5370
- "question": "How should I implement this?",
5371
- "options": ["One way", "Another way"]
5372
- }
5373
- Reason: Too vague, doesn't explain the tradeoffs or provide concrete details
5374
- </bad_example>
5375
-
5376
- <bad_example>
5377
- {
5378
- "id": "q2",
5379
- "question": "Which pattern should we follow for state management?",
5380
- "options": [
5381
- "Tell me which pattern the codebase currently uses",
5382
- "Show me examples of state management",
5383
- "Whatever you think is best"
5384
- ]
5385
- }
5386
- 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)"
5387
- </bad_example>
5388
-
5389
- <bad_example>
5390
- {
5391
- "id": "q3",
5392
- "question": "What color scheme should the button use?",
5393
- "options": [
5394
- "Use the existing theme colors",
5395
- "Let me specify custom colors",
5396
- "Match the design system"
5397
- ]
5398
- }
5399
- Reason: "Let me specify" requires user input. Should be "Primary blue (#0066FF, existing theme)" or "Secondary gray (#6B7280, existing theme)"
5400
- </bad_example>
5401
- </question_examples>`;
5402
-
5403
- // src/workflow/steps/research.ts
5404
- var researchStep = async ({ step, context }) => {
5405
- const {
5406
- task,
5407
- cwd,
5408
- isCloudMode,
5409
- options,
5410
- logger: logger2,
5411
- fileManager,
5412
- gitManager,
5413
- promptBuilder,
5414
- sessionId,
5415
- mcpServers,
5416
- sendNotification
5417
- } = context;
5418
- const stepLogger = logger2.child("ResearchStep");
5419
- const existingResearch = await fileManager.readResearch(task.id);
5420
- if (existingResearch) {
5421
- stepLogger.info("Research already exists", {
5422
- taskId: task.id,
5423
- hasQuestions: !!existingResearch.questions,
5424
- answered: existingResearch.answered
5425
- });
5426
- if (existingResearch.questions && !existingResearch.answered) {
5427
- stepLogger.info("Re-emitting unanswered research questions", {
5428
- taskId: task.id,
5429
- questionCount: existingResearch.questions.length
5430
- });
5431
- await sendNotification(POSTHOG_NOTIFICATIONS.ARTIFACT, {
5432
- sessionId,
5433
- kind: "research_questions",
5434
- content: existingResearch.questions
5435
- });
5436
- if (!isCloudMode) {
5437
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
5438
- sessionId,
5439
- phase: "research"
5440
- });
5441
- return { status: "skipped", halt: true };
5442
- }
5443
- }
5444
- return { status: "skipped" };
5445
- }
5446
- stepLogger.info("Starting research phase", { taskId: task.id });
5447
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_START, {
5448
- sessionId,
5449
- phase: "research"
5450
- });
5451
- const researchPrompt = await promptBuilder.buildResearchPrompt(task, cwd);
5452
- const fullPrompt = `${RESEARCH_SYSTEM_PROMPT}
5453
-
5454
- ${researchPrompt}`;
5455
- const baseOptions = {
5456
- model: step.model,
5457
- cwd,
5458
- permissionMode: "plan",
5459
- settingSources: ["local"],
5460
- mcpServers,
5461
- // Allow research tools: read-only operations, web search, and MCP resources
5462
- allowedTools: [
5463
- "Read",
5464
- "Glob",
5465
- "Grep",
5466
- "WebFetch",
5467
- "WebSearch",
5468
- "ListMcpResources",
5469
- "ReadMcpResource",
5470
- "TodoWrite",
5471
- "BashOutput"
5472
- ]
5473
- };
5474
- const response = query4({
5475
- prompt: fullPrompt,
5476
- options: { ...baseOptions, ...options.queryOverrides || {} }
5477
- });
5478
- let jsonContent = "";
5479
- try {
5480
- for await (const message of response) {
5481
- if (message.type === "assistant" && message.message?.content) {
5482
- for (const c of message.message.content) {
5483
- if (c.type === "text" && c.text) {
5484
- jsonContent += c.text;
5485
- }
5486
- }
5487
- }
5488
- }
5489
- } catch (error) {
5490
- stepLogger.error("Error during research step query", error);
5491
- throw error;
5492
- }
5493
- if (!jsonContent.trim()) {
5494
- stepLogger.error("No JSON output from research agent", { taskId: task.id });
5495
- await sendNotification(POSTHOG_NOTIFICATIONS.ERROR, {
5496
- sessionId,
5497
- message: "Research agent returned no output"
5498
- });
5499
- return { status: "completed", halt: true };
5500
- }
5501
- let evaluation;
5502
- try {
5503
- const jsonMatch = jsonContent.match(/\{[\s\S]*\}/);
5504
- if (!jsonMatch) {
5505
- throw new Error("No JSON object found in response");
5506
- }
5507
- evaluation = JSON.parse(jsonMatch[0]);
5508
- stepLogger.info("Parsed research evaluation", {
5509
- taskId: task.id,
5510
- score: evaluation.actionabilityScore,
5511
- hasQuestions: !!evaluation.questions
5512
- });
5513
- } catch (error) {
5514
- stepLogger.error("Failed to parse research JSON", {
5515
- taskId: task.id,
5516
- error: error instanceof Error ? error.message : String(error),
5517
- content: jsonContent.substring(0, 500)
5518
- });
5519
- await sendNotification(POSTHOG_NOTIFICATIONS.ERROR, {
5520
- sessionId,
5521
- message: `Failed to parse research JSON: ${error instanceof Error ? error.message : String(error)}`
5522
- });
5523
- return { status: "completed", halt: true };
5524
- }
5525
- if (evaluation.questions && evaluation.questions.length > 0) {
5526
- evaluation.answered = false;
5527
- evaluation.answers = void 0;
5528
- }
5529
- await fileManager.writeResearch(task.id, evaluation);
5530
- stepLogger.info("Research evaluation written", {
5531
- taskId: task.id,
5532
- score: evaluation.actionabilityScore,
5533
- hasQuestions: !!evaluation.questions
5534
- });
5535
- await sendNotification(POSTHOG_NOTIFICATIONS.ARTIFACT, {
5536
- sessionId,
5537
- kind: "research_evaluation",
5538
- content: evaluation
5539
- });
5540
- await gitManager.addAllPostHogFiles();
5541
- await finalizeStepGitActions(context, step, {
5542
- commitMessage: `Research phase for ${task.title}`
5543
- });
5544
- if (evaluation.actionabilityScore < 0.7 && evaluation.questions && evaluation.questions.length > 0) {
5545
- stepLogger.info("Actionability score below threshold, questions needed", {
5546
- taskId: task.id,
5547
- score: evaluation.actionabilityScore,
5548
- questionCount: evaluation.questions.length
5549
- });
5550
- await sendNotification(POSTHOG_NOTIFICATIONS.ARTIFACT, {
5551
- sessionId,
5552
- kind: "research_questions",
5553
- content: evaluation.questions
5554
- });
5555
- } else {
5556
- stepLogger.info("Actionability score acceptable, proceeding to planning", {
5557
- taskId: task.id,
5558
- score: evaluation.actionabilityScore
5559
- });
5560
- }
5561
- if (!isCloudMode) {
5562
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
5563
- sessionId,
5564
- phase: "research"
5565
- });
5566
- return { status: "completed", halt: true };
4262
+ const execution = this.executionStates.get(executionId);
4263
+ if (execution && execution.status === "running") {
4264
+ execution.status = "timeout";
4265
+ execution.completedAt = Date.now();
4266
+ execution.abortController?.abort();
4267
+ if (!execution.result) {
4268
+ execution.result = {
4269
+ status: "timeout",
4270
+ message: "Execution timed out"
4271
+ };
4272
+ }
4273
+ }
4274
+ }, timeout);
5567
4275
  }
5568
- const researchData = await fileManager.readResearch(task.id);
5569
- if (researchData?.questions && !researchData.answered) {
5570
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
5571
- sessionId,
5572
- phase: "research"
5573
- });
5574
- 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
+ }
5575
4283
  }
5576
- await sendNotification(POSTHOG_NOTIFICATIONS.PHASE_COMPLETE, {
5577
- sessionId,
5578
- phase: "research"
5579
- });
5580
- return { status: "completed" };
5581
4284
  };
5582
4285
 
5583
- // src/workflow/config.ts
5584
- var MODELS = {
5585
- SONNET: "claude-sonnet-4-5",
5586
- HAIKU: "claude-haiku-4-5"
5587
- };
5588
- var TASK_WORKFLOW = [
5589
- {
5590
- id: "research",
5591
- name: "Research",
5592
- agent: "research",
5593
- model: MODELS.HAIKU,
5594
- permissionMode: "plan",
5595
- commit: true,
5596
- push: true,
5597
- run: researchStep
5598
- },
5599
- {
5600
- id: "plan",
5601
- name: "Plan",
5602
- agent: "planning",
5603
- model: MODELS.SONNET,
5604
- permissionMode: "plan",
5605
- commit: true,
5606
- push: true,
5607
- run: planStep
5608
- },
5609
- {
5610
- id: "build",
5611
- name: "Build",
5612
- agent: "execution",
5613
- model: MODELS.SONNET,
5614
- permissionMode: "acceptEdits",
5615
- commit: true,
5616
- push: true,
5617
- run: buildStep
5618
- },
5619
- {
5620
- id: "finalize",
5621
- name: "Finalize",
5622
- agent: "system",
5623
- // not used
5624
- model: MODELS.HAIKU,
5625
- // not used
5626
- permissionMode: "plan",
5627
- // not used
5628
- commit: true,
5629
- push: true,
5630
- run: finalizeStep
5631
- }
5632
- ];
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 || {});
5633
4294
 
5634
4295
  // src/agent.ts
5635
4296
  var Agent = class {
@@ -5638,10 +4299,8 @@ var Agent = class {
5638
4299
  posthogAPI;
5639
4300
  fileManager;
5640
4301
  gitManager;
5641
- templateManager;
5642
4302
  logger;
5643
4303
  acpConnection;
5644
- promptBuilder;
5645
4304
  mcpServers;
5646
4305
  canUseTool;
5647
4306
  currentRunId;
@@ -5681,7 +4340,6 @@ var Agent = class {
5681
4340
  repositoryPath: this.workingDirectory,
5682
4341
  logger: this.logger.child("GitManager")
5683
4342
  });
5684
- this.templateManager = new TemplateManager();
5685
4343
  if (config.posthogApiUrl && config.getPosthogApiKey && config.posthogProjectId) {
5686
4344
  this.posthogAPI = new PostHogAPIClient({
5687
4345
  apiUrl: config.posthogApiUrl,
@@ -5693,12 +4351,6 @@ var Agent = class {
5693
4351
  this.logger.child("SessionStore")
5694
4352
  );
5695
4353
  }
5696
- this.promptBuilder = new PromptBuilder({
5697
- getTaskFiles: (taskId) => this.getTaskFiles(taskId),
5698
- generatePlanTemplate: (vars) => this.templateManager.generatePlan(vars),
5699
- posthogClient: this.posthogAPI,
5700
- logger: this.logger.child("PromptBuilder")
5701
- });
5702
4354
  }
5703
4355
  /**
5704
4356
  * Enable or disable debug logging
@@ -5726,88 +4378,14 @@ var Agent = class {
5726
4378
  throw error;
5727
4379
  }
5728
4380
  }
5729
- getOrCreateConnection() {
5730
- if (!this.acpConnection) {
5731
- this.acpConnection = createAcpConnection({
5732
- sessionStore: this.sessionStore
5733
- });
5734
- }
5735
- return this.acpConnection;
5736
- }
5737
- // Adaptive task execution orchestrated via workflow steps
5738
- async runTask(taskId, taskRunId, options = {}) {
5739
- const task = await this.fetchTask(taskId);
5740
- const cwd = options.repositoryPath || this.workingDirectory;
5741
- const isCloudMode = options.isCloudMode ?? false;
5742
- const taskSlug = task.slug || task.id;
5743
- this.currentRunId = taskRunId;
5744
- this.logger.info("Starting adaptive task execution", {
5745
- taskId: task.id,
5746
- taskSlug,
5747
- taskRunId,
5748
- isCloudMode
5749
- });
5750
- const connection = this.getOrCreateConnection();
5751
- const sendNotification = async (method, params) => {
5752
- this.logger.debug(`Notification: ${method}`, params);
5753
- await connection.agentConnection.extNotification?.(method, params);
5754
- };
5755
- await sendNotification(POSTHOG_NOTIFICATIONS.RUN_STARTED, {
5756
- sessionId: taskRunId,
5757
- runId: taskRunId
5758
- });
5759
- await this.prepareTaskBranch(taskSlug, isCloudMode, sendNotification);
5760
- let taskError;
5761
- try {
5762
- const workflowContext = {
5763
- task,
5764
- taskSlug,
5765
- runId: taskRunId,
5766
- cwd,
5767
- isCloudMode,
5768
- options,
5769
- logger: this.logger,
5770
- fileManager: this.fileManager,
5771
- gitManager: this.gitManager,
5772
- promptBuilder: this.promptBuilder,
5773
- connection: connection.agentConnection,
5774
- sessionId: taskRunId,
5775
- sendNotification,
5776
- mcpServers: this.mcpServers,
5777
- posthogAPI: this.posthogAPI,
5778
- stepResults: {}
5779
- };
5780
- for (const step of TASK_WORKFLOW) {
5781
- const result = await step.run({ step, context: workflowContext });
5782
- if (result.halt) {
5783
- return;
5784
- }
5785
- }
5786
- const shouldCreatePR = options.createPR ?? isCloudMode;
5787
- if (shouldCreatePR) {
5788
- await this.ensurePullRequest(
5789
- task,
5790
- workflowContext.stepResults,
5791
- sendNotification
5792
- );
5793
- }
5794
- this.logger.info("Task execution complete", { taskId: task.id });
5795
- await sendNotification(POSTHOG_NOTIFICATIONS.TASK_COMPLETE, {
5796
- sessionId: taskRunId,
5797
- taskId: task.id
5798
- });
5799
- } catch (error) {
5800
- taskError = error instanceof Error ? error : new Error(String(error));
5801
- this.logger.error("Task execution failed", {
5802
- taskId: task.id,
5803
- error: taskError.message
5804
- });
5805
- await sendNotification(POSTHOG_NOTIFICATIONS.ERROR, {
5806
- sessionId: taskRunId,
5807
- message: taskError.message
5808
- });
5809
- throw taskError;
5810
- }
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
+ );
5811
4389
  }
5812
4390
  /**
5813
4391
  * Creates an in-process ACP connection for client communication.
@@ -5818,15 +4396,14 @@ var Agent = class {
5818
4396
  */
5819
4397
  async runTaskV2(taskId, taskRunId, options = {}) {
5820
4398
  await this._configureLlmGateway();
5821
- const task = await this.fetchTask(taskId);
5822
- const taskSlug = task.slug || task.id;
5823
4399
  const isCloudMode = options.isCloudMode ?? false;
5824
4400
  const _cwd = options.repositoryPath || this.workingDirectory;
5825
4401
  this.currentRunId = taskRunId;
5826
4402
  this.acpConnection = createAcpConnection({
4403
+ framework: options.framework,
5827
4404
  sessionStore: this.sessionStore,
5828
4405
  sessionId: taskRunId,
5829
- taskId: task.id
4406
+ taskId
5830
4407
  });
5831
4408
  const sendNotification = async (method, params) => {
5832
4409
  this.logger.debug(`Notification: ${method}`, params);
@@ -5835,11 +4412,15 @@ var Agent = class {
5835
4412
  params
5836
4413
  );
5837
4414
  };
5838
- await sendNotification(POSTHOG_NOTIFICATIONS.RUN_STARTED, {
5839
- sessionId: taskRunId,
5840
- runId: taskRunId
5841
- });
4415
+ if (!options.isReconnect) {
4416
+ await sendNotification(POSTHOG_NOTIFICATIONS.RUN_STARTED, {
4417
+ sessionId: taskRunId,
4418
+ runId: taskRunId
4419
+ });
4420
+ }
5842
4421
  if (!options.skipGitBranch) {
4422
+ const task = options.task ?? await this.fetchTask(taskId);
4423
+ const taskSlug = task.slug || task.id;
5843
4424
  try {
5844
4425
  await this.prepareTaskBranch(taskSlug, isCloudMode, sendNotification);
5845
4426
  } catch (error) {
@@ -6193,47 +4774,6 @@ ${task.description}`
6193
4774
  throw error;
6194
4775
  }
6195
4776
  }
6196
- async ensurePullRequest(task, stepResults, sendNotification) {
6197
- const latestRun = task.latest_run;
6198
- const existingPr = latestRun?.output && typeof latestRun.output === "object" ? latestRun.output.pr_url : null;
6199
- if (existingPr) {
6200
- this.logger.info("PR already exists, skipping creation", {
6201
- taskId: task.id,
6202
- prUrl: existingPr
6203
- });
6204
- return;
6205
- }
6206
- const buildResult = stepResults.build;
6207
- if (!buildResult?.commitCreated) {
6208
- this.logger.warn(
6209
- "Build step did not produce a commit; skipping PR creation",
6210
- { taskId: task.id }
6211
- );
6212
- return;
6213
- }
6214
- const branchName = await this.gitManager.getCurrentBranch();
6215
- const finalizeResult = stepResults.finalize;
6216
- const prBody = finalizeResult?.prBody;
6217
- const prUrl = await this.createPullRequest(
6218
- task.id,
6219
- branchName,
6220
- task.title,
6221
- task.description ?? "",
6222
- prBody
6223
- );
6224
- await sendNotification(POSTHOG_NOTIFICATIONS.PR_CREATED, { prUrl });
6225
- try {
6226
- await this.attachPullRequestToTask(task.id, prUrl, branchName);
6227
- this.logger.info("PR attached to task successfully", {
6228
- taskId: task.id,
6229
- prUrl
6230
- });
6231
- } catch (error) {
6232
- this.logger.warn("Could not attach PR to task", {
6233
- error: error instanceof Error ? error.message : String(error)
6234
- });
6235
- }
6236
- }
6237
4777
  };
6238
4778
 
6239
4779
  // src/schemas.ts
@@ -6408,6 +4948,131 @@ function parseAgentEvents(inputs) {
6408
4948
  return inputs.map((input) => parseAgentEvent(input)).filter((event) => event !== null);
6409
4949
  }
6410
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
+
6411
5076
  // src/tools/registry.ts
6412
5077
  var TOOL_DEFINITIONS = {
6413
5078
  // Filesystem tools
@@ -6485,6 +5150,11 @@ var TOOL_DEFINITIONS = {
6485
5150
  category: "assistant",
6486
5151
  description: "Exit plan mode and present plan to user"
6487
5152
  },
5153
+ AskUserQuestion: {
5154
+ name: "AskUserQuestion",
5155
+ category: "assistant",
5156
+ description: "Ask the user a clarifying question with options"
5157
+ },
6488
5158
  SlashCommand: {
6489
5159
  name: "SlashCommand",
6490
5160
  category: "assistant",
@@ -6522,12 +5192,11 @@ var ToolRegistry = class {
6522
5192
  };
6523
5193
 
6524
5194
  // src/worktree-manager.ts
6525
- import { exec as exec2, execFile } from "child_process";
5195
+ import { execFile } from "child_process";
6526
5196
  import * as crypto from "crypto";
6527
- import * as fs5 from "fs/promises";
5197
+ import * as fs3 from "fs/promises";
6528
5198
  import * as path2 from "path";
6529
5199
  import { promisify as promisify2 } from "util";
6530
- var execAsync2 = promisify2(exec2);
6531
5200
  var execFileAsync = promisify2(execFile);
6532
5201
  var ADJECTIVES = [
6533
5202
  "swift",
@@ -7017,14 +5686,14 @@ var WorktreeManager = class {
7017
5686
  usesExternalPath() {
7018
5687
  return this.worktreeBasePath !== null;
7019
5688
  }
7020
- async runGitCommand(command) {
5689
+ async runGitCommand(args) {
7021
5690
  try {
7022
- const { stdout } = await execAsync2(`git ${command}`, {
5691
+ const { stdout } = await execFileAsync("git", args, {
7023
5692
  cwd: this.mainRepoPath
7024
5693
  });
7025
5694
  return stdout.trim();
7026
5695
  } catch (error) {
7027
- throw new Error(`Git command failed: ${command}
5696
+ throw new Error(`Git command failed: git ${args.join(" ")}
7028
5697
  ${error}`);
7029
5698
  }
7030
5699
  }
@@ -7049,7 +5718,7 @@ ${error}`);
7049
5718
  async worktreeExists(name) {
7050
5719
  const worktreePath = this.getWorktreePath(name);
7051
5720
  try {
7052
- await fs5.access(worktreePath);
5721
+ await fs3.access(worktreePath);
7053
5722
  return true;
7054
5723
  } catch {
7055
5724
  return false;
@@ -7060,7 +5729,7 @@ ${error}`);
7060
5729
  const ignorePattern = `/${WORKTREE_FOLDER_NAME}/`;
7061
5730
  let content = "";
7062
5731
  try {
7063
- content = await fs5.readFile(excludePath, "utf-8");
5732
+ content = await fs3.readFile(excludePath, "utf-8");
7064
5733
  } catch {
7065
5734
  }
7066
5735
  if (content.includes(`/${WORKTREE_FOLDER_NAME}/`) || content.includes(`/${WORKTREE_FOLDER_NAME}`)) {
@@ -7068,13 +5737,13 @@ ${error}`);
7068
5737
  return;
7069
5738
  }
7070
5739
  const infoDir = path2.join(this.mainRepoPath, ".git", "info");
7071
- await fs5.mkdir(infoDir, { recursive: true });
5740
+ await fs3.mkdir(infoDir, { recursive: true });
7072
5741
  const newContent = `${content.trimEnd()}
7073
5742
 
7074
5743
  # Array worktrees
7075
5744
  ${ignorePattern}
7076
5745
  `;
7077
- await fs5.writeFile(excludePath, newContent);
5746
+ await fs3.writeFile(excludePath, newContent);
7078
5747
  this.logger.info("Added .array folder to .git/info/exclude");
7079
5748
  }
7080
5749
  async generateUniqueWorktreeName() {
@@ -7091,61 +5760,83 @@ ${ignorePattern}
7091
5760
  return name;
7092
5761
  }
7093
5762
  async getDefaultBranch() {
7094
- try {
7095
- const remoteBranch = await this.runGitCommand(
7096
- "symbolic-ref refs/remotes/origin/HEAD"
7097
- );
7098
- return remoteBranch.replace("refs/remotes/origin/", "");
7099
- } catch {
7100
- try {
7101
- await this.runGitCommand("rev-parse --verify main");
7102
- return "main";
7103
- } catch {
7104
- try {
7105
- await this.runGitCommand("rev-parse --verify master");
7106
- return "master";
7107
- } catch {
7108
- throw new Error(
7109
- "Cannot determine default branch. No main or master branch found."
7110
- );
7111
- }
7112
- }
7113
- }
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
+ );
7114
5780
  }
7115
5781
  async createWorktree(options) {
5782
+ const totalStart = Date.now();
5783
+ const setupPromises = [];
7116
5784
  if (!this.usesExternalPath()) {
7117
- await this.ensureArrayDirIgnored();
7118
- }
7119
- if (this.usesExternalPath()) {
5785
+ setupPromises.push(this.ensureArrayDirIgnored());
5786
+ } else {
7120
5787
  const folderPath = this.getWorktreeFolderPath();
7121
- await fs5.mkdir(folderPath, { recursive: true });
7122
- }
7123
- 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;
7124
5798
  const worktreePath = this.getWorktreePath(worktreeName);
7125
5799
  const branchName = `array/${worktreeName}`;
7126
- const baseBranch = options?.baseBranch ?? await this.getDefaultBranch();
7127
5800
  this.logger.info("Creating worktree", {
7128
5801
  worktreeName,
7129
5802
  worktreePath,
7130
5803
  branchName,
7131
5804
  baseBranch,
7132
- external: this.usesExternalPath()
5805
+ external: this.usesExternalPath(),
5806
+ setupTimeMs: setupTime
7133
5807
  });
5808
+ const gitStart = Date.now();
7134
5809
  if (this.usesExternalPath()) {
7135
- await this.runGitCommand(
7136
- `worktree add -b "${branchName}" "${worktreePath}" "${baseBranch}"`
7137
- );
5810
+ await this.runGitCommand([
5811
+ "worktree",
5812
+ "add",
5813
+ "--quiet",
5814
+ "-b",
5815
+ branchName,
5816
+ worktreePath,
5817
+ baseBranch
5818
+ ]);
7138
5819
  } else {
7139
- const relativePath = `${WORKTREE_FOLDER_NAME}/${worktreeName}`;
7140
- await this.runGitCommand(
7141
- `worktree add -b "${branchName}" "./${relativePath}" "${baseBranch}"`
7142
- );
7143
- }
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;
7144
5832
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
7145
5833
  this.logger.info("Worktree created successfully", {
7146
5834
  worktreeName,
7147
5835
  worktreePath,
7148
- branchName
5836
+ branchName,
5837
+ setupTimeMs: setupTime,
5838
+ gitWorktreeAddMs: gitTime,
5839
+ totalMs: Date.now() - totalStart
7149
5840
  });
7150
5841
  return {
7151
5842
  worktreePath,
@@ -7174,7 +5865,7 @@ ${ignorePattern}
7174
5865
  }
7175
5866
  try {
7176
5867
  const gitPath = path2.join(resolvedWorktreePath, ".git");
7177
- const stat2 = await fs5.stat(gitPath);
5868
+ const stat2 = await fs3.stat(gitPath);
7178
5869
  if (stat2.isDirectory()) {
7179
5870
  const error = new Error(
7180
5871
  "Cannot delete worktree: path appears to be a main repository (contains .git directory)"
@@ -7206,8 +5897,8 @@ ${ignorePattern}
7206
5897
  }
7207
5898
  );
7208
5899
  try {
7209
- await fs5.rm(worktreePath, { recursive: true, force: true });
7210
- await this.runGitCommand("worktree prune");
5900
+ await fs3.rm(worktreePath, { recursive: true, force: true });
5901
+ await this.runGitCommand(["worktree", "prune"]);
7211
5902
  this.logger.info("Worktree cleaned up manually", { worktreePath });
7212
5903
  } catch (cleanupError) {
7213
5904
  this.logger.error("Failed to cleanup worktree", {
@@ -7220,7 +5911,11 @@ ${ignorePattern}
7220
5911
  }
7221
5912
  async getWorktreeInfo(worktreePath) {
7222
5913
  try {
7223
- const output = await this.runGitCommand("worktree list --porcelain");
5914
+ const output = await this.runGitCommand([
5915
+ "worktree",
5916
+ "list",
5917
+ "--porcelain"
5918
+ ]);
7224
5919
  const worktrees = this.parseWorktreeList(output);
7225
5920
  const worktree = worktrees.find((w) => w.worktreePath === worktreePath);
7226
5921
  return worktree || null;
@@ -7231,7 +5926,11 @@ ${ignorePattern}
7231
5926
  }
7232
5927
  async listWorktrees() {
7233
5928
  try {
7234
- const output = await this.runGitCommand("worktree list --porcelain");
5929
+ const output = await this.runGitCommand([
5930
+ "worktree",
5931
+ "list",
5932
+ "--porcelain"
5933
+ ]);
7235
5934
  return this.parseWorktreeList(output);
7236
5935
  } catch (error) {
7237
5936
  this.logger.debug("Failed to list worktrees", { error });
@@ -7270,15 +5969,16 @@ ${ignorePattern}
7270
5969
  }
7271
5970
  async isWorktree(repoPath) {
7272
5971
  try {
7273
- const { stdout } = await execAsync2(
7274
- "git rev-parse --is-inside-work-tree",
5972
+ const { stdout } = await execFileAsync(
5973
+ "git",
5974
+ ["rev-parse", "--is-inside-work-tree"],
7275
5975
  { cwd: repoPath }
7276
5976
  );
7277
5977
  if (stdout.trim() !== "true") {
7278
5978
  return false;
7279
5979
  }
7280
5980
  const gitPath = path2.join(repoPath, ".git");
7281
- const stat2 = await fs5.stat(gitPath);
5981
+ const stat2 = await fs3.stat(gitPath);
7282
5982
  return stat2.isFile();
7283
5983
  } catch {
7284
5984
  return false;
@@ -7287,7 +5987,7 @@ ${ignorePattern}
7287
5987
  async getMainRepoPathFromWorktree(worktreePath) {
7288
5988
  try {
7289
5989
  const gitFilePath = path2.join(worktreePath, ".git");
7290
- const content = await fs5.readFile(gitFilePath, "utf-8");
5990
+ const content = await fs3.readFile(gitFilePath, "utf-8");
7291
5991
  const match = content.match(/gitdir:\s*(.+)/);
7292
5992
  if (match) {
7293
5993
  const gitDir = match[1].trim();