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