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