cyrus-edge-worker 0.0.39 → 0.2.0-rc
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/AgentSessionManager.d.ts.map +1 -1
- package/dist/AgentSessionManager.js +2 -4
- package/dist/AgentSessionManager.js.map +1 -1
- package/dist/EdgeWorker.d.ts +83 -32
- package/dist/EdgeWorker.d.ts.map +1 -1
- package/dist/EdgeWorker.js +618 -457
- package/dist/EdgeWorker.js.map +1 -1
- package/dist/SharedApplicationServer.d.ts +21 -63
- package/dist/SharedApplicationServer.d.ts.map +1 -1
- package/dist/SharedApplicationServer.js +93 -764
- package/dist/SharedApplicationServer.js.map +1 -1
- package/dist/procedures/ProcedureRouter.d.ts.map +1 -1
- package/dist/procedures/ProcedureRouter.js +11 -2
- package/dist/procedures/ProcedureRouter.js.map +1 -1
- package/dist/procedures/registry.d.ts +29 -0
- package/dist/procedures/registry.d.ts.map +1 -1
- package/dist/procedures/registry.js +45 -8
- package/dist/procedures/registry.js.map +1 -1
- package/dist/procedures/types.d.ts +1 -1
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/prompt-assembly/types.d.ts +81 -0
- package/dist/prompt-assembly/types.d.ts.map +1 -0
- package/dist/prompt-assembly/types.js +8 -0
- package/dist/prompt-assembly/types.js.map +1 -0
- package/dist/prompts/subroutines/coding-activity.md +10 -0
- package/dist/prompts/subroutines/concise-summary.md +16 -2
- package/dist/prompts/subroutines/debugger-fix.md +8 -25
- package/dist/prompts/subroutines/debugger-reproduction.md +11 -44
- package/dist/prompts/subroutines/git-gh.md +9 -6
- package/dist/prompts/subroutines/plan-summary.md +21 -0
- package/dist/prompts/subroutines/preparation.md +16 -0
- package/dist/prompts/subroutines/question-answer.md +8 -0
- package/dist/prompts/subroutines/question-investigation.md +8 -0
- package/dist/prompts/subroutines/verifications.md +9 -6
- package/package.json +8 -6
- package/prompts/orchestrator.md +9 -1
- package/prompts/standard-issue-assigned-user-prompt.md +33 -0
- package/prompts/todolist-system-prompt-extension.md +15 -0
- package/prompt-template-v2.md +0 -89
package/dist/EdgeWorker.js
CHANGED
|
@@ -5,9 +5,9 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { LinearClient, } from "@linear/sdk";
|
|
6
6
|
import { watch as chokidarWatch } from "chokidar";
|
|
7
7
|
import { ClaudeRunner, createCyrusToolsServer, createImageToolsServer, createSoraToolsServer, getAllTools, getCoordinatorTools, getReadOnlyTools, getSafeTools, } from "cyrus-claude-runner";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { ConfigUpdater } from "cyrus-config-updater";
|
|
9
|
+
import { DEFAULT_PROXY_URL, isAgentSessionCreatedWebhook, isAgentSessionPromptedWebhook, isIssueAssignedWebhook, isIssueCommentMentionWebhook, isIssueNewCommentWebhook, isIssueUnassignedWebhook, PersistenceManager, resolvePath, } from "cyrus-core";
|
|
10
|
+
import { LinearEventTransport } from "cyrus-linear-event-transport";
|
|
11
11
|
import { fileTypeFromBuffer } from "file-type";
|
|
12
12
|
import { AgentSessionManager } from "./AgentSessionManager.js";
|
|
13
13
|
import { ProcedureRouter, } from "./procedures/index.js";
|
|
@@ -23,7 +23,8 @@ export class EdgeWorker extends EventEmitter {
|
|
|
23
23
|
repositories = new Map(); // repository 'id' (internal, stored in config.json) mapped to the full repo config
|
|
24
24
|
agentSessionManagers = new Map(); // Maps repository ID to AgentSessionManager, which manages ClaudeRunners for a repo
|
|
25
25
|
linearClients = new Map(); // one linear client per 'repository'
|
|
26
|
-
|
|
26
|
+
linearEventTransport = null; // Single event transport for webhook delivery
|
|
27
|
+
configUpdater = null; // Single config updater for configuration updates
|
|
27
28
|
persistenceManager;
|
|
28
29
|
sharedApplicationServer;
|
|
29
30
|
cyrusHome;
|
|
@@ -31,7 +32,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
31
32
|
procedureRouter; // Intelligent workflow routing
|
|
32
33
|
configWatcher; // File watcher for config.json
|
|
33
34
|
configPath; // Path to config.json file
|
|
34
|
-
tokenToRepoIds = new Map(); // Maps Linear token to repository IDs using that token
|
|
35
35
|
constructor(config) {
|
|
36
36
|
super();
|
|
37
37
|
this.config = config;
|
|
@@ -48,15 +48,28 @@ export class EdgeWorker extends EventEmitter {
|
|
|
48
48
|
// Initialize shared application server
|
|
49
49
|
const serverPort = config.serverPort || config.webhookPort || 3456;
|
|
50
50
|
const serverHost = config.serverHost || "localhost";
|
|
51
|
-
this.sharedApplicationServer = new SharedApplicationServer(serverPort, serverHost
|
|
52
|
-
//
|
|
53
|
-
if (config.handlers?.onOAuthCallback) {
|
|
54
|
-
this.sharedApplicationServer.registerOAuthCallbackHandler(config.handlers.onOAuthCallback);
|
|
55
|
-
}
|
|
56
|
-
// Initialize repositories
|
|
51
|
+
this.sharedApplicationServer = new SharedApplicationServer(serverPort, serverHost);
|
|
52
|
+
// Initialize repositories with path resolution
|
|
57
53
|
for (const repo of config.repositories) {
|
|
58
54
|
if (repo.isActive !== false) {
|
|
59
|
-
|
|
55
|
+
// Resolve paths that may contain tilde (~) prefix
|
|
56
|
+
const resolvedRepo = {
|
|
57
|
+
...repo,
|
|
58
|
+
repositoryPath: resolvePath(repo.repositoryPath),
|
|
59
|
+
workspaceBaseDir: resolvePath(repo.workspaceBaseDir),
|
|
60
|
+
mcpConfigPath: Array.isArray(repo.mcpConfigPath)
|
|
61
|
+
? repo.mcpConfigPath.map(resolvePath)
|
|
62
|
+
: repo.mcpConfigPath
|
|
63
|
+
? resolvePath(repo.mcpConfigPath)
|
|
64
|
+
: undefined,
|
|
65
|
+
promptTemplatePath: repo.promptTemplatePath
|
|
66
|
+
? resolvePath(repo.promptTemplatePath)
|
|
67
|
+
: undefined,
|
|
68
|
+
openaiOutputDirectory: repo.openaiOutputDirectory
|
|
69
|
+
? resolvePath(repo.openaiOutputDirectory)
|
|
70
|
+
: undefined,
|
|
71
|
+
};
|
|
72
|
+
this.repositories.set(repo.id, resolvedRepo);
|
|
60
73
|
// Create Linear client for this repository's workspace
|
|
61
74
|
const linearClient = new LinearClient({
|
|
62
75
|
accessToken: repo.linearToken,
|
|
@@ -123,77 +136,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
123
136
|
this.agentSessionManagers.set(repo.id, agentSessionManager);
|
|
124
137
|
}
|
|
125
138
|
}
|
|
126
|
-
//
|
|
127
|
-
const tokenToRepos = new Map();
|
|
128
|
-
for (const repo of this.repositories.values()) {
|
|
129
|
-
const repos = tokenToRepos.get(repo.linearToken) || [];
|
|
130
|
-
repos.push(repo);
|
|
131
|
-
tokenToRepos.set(repo.linearToken, repos);
|
|
132
|
-
// Track token-to-repo-id mapping for dynamic config updates
|
|
133
|
-
const repoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
|
|
134
|
-
if (!repoIds.includes(repo.id)) {
|
|
135
|
-
repoIds.push(repo.id);
|
|
136
|
-
}
|
|
137
|
-
this.tokenToRepoIds.set(repo.linearToken, repoIds);
|
|
138
|
-
}
|
|
139
|
-
// Create one NDJSON client per unique token using shared application server
|
|
140
|
-
for (const [token, repos] of tokenToRepos) {
|
|
141
|
-
if (!repos || repos.length === 0)
|
|
142
|
-
continue;
|
|
143
|
-
const firstRepo = repos[0];
|
|
144
|
-
if (!firstRepo)
|
|
145
|
-
continue;
|
|
146
|
-
const primaryRepoId = firstRepo.id;
|
|
147
|
-
// Determine which client to use based on environment variable
|
|
148
|
-
const useLinearDirectWebhooks = process.env.LINEAR_DIRECT_WEBHOOKS?.toLowerCase().trim() === "true";
|
|
149
|
-
const clientConfig = {
|
|
150
|
-
proxyUrl: config.proxyUrl,
|
|
151
|
-
token: token,
|
|
152
|
-
name: repos.map((r) => r.name).join(", "), // Pass repository names
|
|
153
|
-
transport: "webhook",
|
|
154
|
-
// Use shared application server instead of individual servers
|
|
155
|
-
useExternalWebhookServer: true,
|
|
156
|
-
externalWebhookServer: this.sharedApplicationServer,
|
|
157
|
-
webhookPort: serverPort, // All clients use same port
|
|
158
|
-
webhookPath: "/webhook",
|
|
159
|
-
webhookHost: serverHost,
|
|
160
|
-
...(config.baseUrl && { webhookBaseUrl: config.baseUrl }),
|
|
161
|
-
// Legacy fallback support
|
|
162
|
-
...(!config.baseUrl &&
|
|
163
|
-
config.webhookBaseUrl && { webhookBaseUrl: config.webhookBaseUrl }),
|
|
164
|
-
onConnect: () => this.handleConnect(primaryRepoId, repos),
|
|
165
|
-
onDisconnect: (reason) => this.handleDisconnect(primaryRepoId, repos, reason),
|
|
166
|
-
onError: (error) => this.handleError(error),
|
|
167
|
-
};
|
|
168
|
-
// Create the appropriate client based on configuration
|
|
169
|
-
const ndjsonClient = useLinearDirectWebhooks
|
|
170
|
-
? new LinearWebhookClient({
|
|
171
|
-
...clientConfig,
|
|
172
|
-
onWebhook: (payload) => {
|
|
173
|
-
// Get fresh repositories for this token to avoid stale closures
|
|
174
|
-
const freshRepos = this.getRepositoriesForToken(token);
|
|
175
|
-
this.handleWebhook(payload, freshRepos);
|
|
176
|
-
},
|
|
177
|
-
})
|
|
178
|
-
: new NdjsonClient(clientConfig);
|
|
179
|
-
// Set up webhook handler for NdjsonClient (LinearWebhookClient uses onWebhook in constructor)
|
|
180
|
-
if (!useLinearDirectWebhooks) {
|
|
181
|
-
ndjsonClient.on("webhook", (data) => {
|
|
182
|
-
// Get fresh repositories for this token to avoid stale closures
|
|
183
|
-
const freshRepos = this.getRepositoriesForToken(token);
|
|
184
|
-
this.handleWebhook(data, freshRepos);
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
// Optional heartbeat logging (only for NdjsonClient)
|
|
188
|
-
if (process.env.DEBUG_EDGE === "true" && !useLinearDirectWebhooks) {
|
|
189
|
-
ndjsonClient.on("heartbeat", () => {
|
|
190
|
-
console.log(`❤️ Heartbeat received for token ending in ...${token.slice(-4)}`);
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
// Store with the first repo's ID as the key (for error messages)
|
|
194
|
-
// But also store the token mapping for lookup
|
|
195
|
-
this.ndjsonClients.set(primaryRepoId, ndjsonClient);
|
|
196
|
-
}
|
|
139
|
+
// Components will be initialized and registered in start() method before server starts
|
|
197
140
|
}
|
|
198
141
|
/**
|
|
199
142
|
* Start the edge worker
|
|
@@ -205,52 +148,53 @@ export class EdgeWorker extends EventEmitter {
|
|
|
205
148
|
if (this.configPath) {
|
|
206
149
|
this.startConfigWatcher();
|
|
207
150
|
}
|
|
208
|
-
//
|
|
151
|
+
// Initialize and register components BEFORE starting server (routes must be registered before listen())
|
|
152
|
+
await this.initializeComponents();
|
|
153
|
+
// Start shared application server (this also starts Cloudflare tunnel if CLOUDFLARE_TOKEN is set)
|
|
209
154
|
await this.sharedApplicationServer.start();
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// For other errors, still log but with less guidance
|
|
232
|
-
console.error(`\n❌ Failed to connect repository: ${repoName}`);
|
|
233
|
-
console.error(` Error: ${error.message}\n`);
|
|
234
|
-
return { repoId, success: false, error };
|
|
235
|
-
}
|
|
236
|
-
return { repoId, success: true };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Initialize and register components (routes) before server starts
|
|
158
|
+
*/
|
|
159
|
+
async initializeComponents() {
|
|
160
|
+
// Get the first active repository for configuration
|
|
161
|
+
const firstRepo = Array.from(this.repositories.values())[0];
|
|
162
|
+
if (!firstRepo) {
|
|
163
|
+
throw new Error("No active repositories configured");
|
|
164
|
+
}
|
|
165
|
+
// 1. Create and register LinearEventTransport
|
|
166
|
+
const useDirectWebhooks = process.env.LINEAR_DIRECT_WEBHOOKS?.toLowerCase() === "true";
|
|
167
|
+
const verificationMode = useDirectWebhooks ? "direct" : "proxy";
|
|
168
|
+
// Get appropriate secret based on mode
|
|
169
|
+
const secret = useDirectWebhooks
|
|
170
|
+
? process.env.LINEAR_WEBHOOK_SECRET || ""
|
|
171
|
+
: process.env.CYRUS_API_KEY || "";
|
|
172
|
+
this.linearEventTransport = new LinearEventTransport({
|
|
173
|
+
fastifyServer: this.sharedApplicationServer.getFastifyInstance(),
|
|
174
|
+
verificationMode,
|
|
175
|
+
secret,
|
|
237
176
|
});
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
177
|
+
// Listen for webhook events
|
|
178
|
+
this.linearEventTransport.on("webhook", (payload) => {
|
|
179
|
+
// Get all active repositories for webhook handling
|
|
180
|
+
const repos = Array.from(this.repositories.values());
|
|
181
|
+
this.handleWebhook(payload, repos);
|
|
182
|
+
});
|
|
183
|
+
// Listen for errors
|
|
184
|
+
this.linearEventTransport.on("error", (error) => {
|
|
185
|
+
this.handleError(error);
|
|
186
|
+
});
|
|
187
|
+
// Register the /webhook endpoint
|
|
188
|
+
this.linearEventTransport.register();
|
|
189
|
+
console.log(`✅ Linear event transport registered (${verificationMode} mode)`);
|
|
190
|
+
console.log(` Webhook endpoint: ${this.sharedApplicationServer.getWebhookUrl()}`);
|
|
191
|
+
// 2. Create and register ConfigUpdater
|
|
192
|
+
this.configUpdater = new ConfigUpdater(this.sharedApplicationServer.getFastifyInstance(), this.cyrusHome, process.env.CYRUS_API_KEY || "");
|
|
193
|
+
// Register config update routes
|
|
194
|
+
this.configUpdater.register();
|
|
195
|
+
console.log("✅ Config updater registered");
|
|
196
|
+
console.log(" Routes: /api/update/cyrus-config, /api/update/cyrus-env,");
|
|
197
|
+
console.log(" /api/update/repository, /api/test-mcp, /api/configure-mcp");
|
|
254
198
|
}
|
|
255
199
|
/**
|
|
256
200
|
* Stop the edge worker
|
|
@@ -285,11 +229,10 @@ export class EdgeWorker extends EventEmitter {
|
|
|
285
229
|
}
|
|
286
230
|
}
|
|
287
231
|
}
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// Stop shared application server
|
|
232
|
+
// Clear event transport (no explicit cleanup needed, routes are removed when server stops)
|
|
233
|
+
this.linearEventTransport = null;
|
|
234
|
+
this.configUpdater = null;
|
|
235
|
+
// Stop shared application server (this also stops Cloudflare tunnel if running)
|
|
293
236
|
await this.sharedApplicationServer.stop();
|
|
294
237
|
}
|
|
295
238
|
/**
|
|
@@ -298,21 +241,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
298
241
|
setConfigPath(configPath) {
|
|
299
242
|
this.configPath = configPath;
|
|
300
243
|
}
|
|
301
|
-
/**
|
|
302
|
-
* Get fresh list of repositories for a given Linear token
|
|
303
|
-
* This ensures webhook handlers always work with current repository state
|
|
304
|
-
*/
|
|
305
|
-
getRepositoriesForToken(token) {
|
|
306
|
-
const repoIds = this.tokenToRepoIds.get(token) || [];
|
|
307
|
-
const repos = [];
|
|
308
|
-
for (const repoId of repoIds) {
|
|
309
|
-
const repo = this.repositories.get(repoId);
|
|
310
|
-
if (repo) {
|
|
311
|
-
repos.push(repo);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return repos;
|
|
315
|
-
}
|
|
316
244
|
/**
|
|
317
245
|
* Handle resuming a parent session when a child session completes
|
|
318
246
|
* This is the core logic used by the resume parent session callback
|
|
@@ -339,13 +267,38 @@ export class EdgeWorker extends EventEmitter {
|
|
|
339
267
|
console.warn(`[Parent Session Resume] Could not find child session ${childSessionId} to add workspace to parent allowed directories`);
|
|
340
268
|
}
|
|
341
269
|
await this.postParentResumeAcknowledgment(parentSessionId, repo.id);
|
|
342
|
-
//
|
|
343
|
-
|
|
270
|
+
// Post thought to Linear showing child result receipt
|
|
271
|
+
const linearClient = this.linearClients.get(repo.id);
|
|
272
|
+
if (linearClient && childSession) {
|
|
273
|
+
const childIssueIdentifier = childSession.issue?.identifier || childSession.issueId;
|
|
274
|
+
const resultThought = `Received result from sub-issue ${childIssueIdentifier}:\n\n---\n\n${prompt}\n\n---`;
|
|
275
|
+
try {
|
|
276
|
+
const result = await linearClient.createAgentActivity({
|
|
277
|
+
agentSessionId: parentSessionId,
|
|
278
|
+
content: {
|
|
279
|
+
type: "thought",
|
|
280
|
+
body: resultThought,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
if (result.success) {
|
|
284
|
+
console.log(`[Parent Session Resume] Posted child result receipt thought for parent session ${parentSessionId}`);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
console.error(`[Parent Session Resume] Failed to post child result receipt thought:`, result);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
console.error(`[Parent Session Resume] Error posting child result receipt thought:`, error);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Use centralized streaming check and routing logic
|
|
295
|
+
console.log(`[Parent Session Resume] Handling child result for parent session ${parentSessionId}`);
|
|
344
296
|
try {
|
|
345
|
-
await this.
|
|
297
|
+
await this.handlePromptWithStreamingCheck(parentSession, repo, parentSessionId, agentSessionManager, prompt, "", // No attachment manifest for child results
|
|
346
298
|
false, // Not a new session
|
|
347
|
-
childWorkspaceDirs
|
|
348
|
-
|
|
299
|
+
childWorkspaceDirs, // Add child workspace directories to parent's allowed directories
|
|
300
|
+
"parent resume from child");
|
|
301
|
+
console.log(`[Parent Session Resume] Successfully handled child result for parent session ${parentSessionId}`);
|
|
349
302
|
}
|
|
350
303
|
catch (error) {
|
|
351
304
|
console.error(`[Parent Session Resume] Failed to resume parent session ${parentSessionId}:`, error);
|
|
@@ -496,8 +449,25 @@ export class EdgeWorker extends EventEmitter {
|
|
|
496
449
|
}
|
|
497
450
|
try {
|
|
498
451
|
console.log(`➕ Adding repository: ${repo.name} (${repo.id})`);
|
|
452
|
+
// Resolve paths that may contain tilde (~) prefix
|
|
453
|
+
const resolvedRepo = {
|
|
454
|
+
...repo,
|
|
455
|
+
repositoryPath: resolvePath(repo.repositoryPath),
|
|
456
|
+
workspaceBaseDir: resolvePath(repo.workspaceBaseDir),
|
|
457
|
+
mcpConfigPath: Array.isArray(repo.mcpConfigPath)
|
|
458
|
+
? repo.mcpConfigPath.map(resolvePath)
|
|
459
|
+
: repo.mcpConfigPath
|
|
460
|
+
? resolvePath(repo.mcpConfigPath)
|
|
461
|
+
: undefined,
|
|
462
|
+
promptTemplatePath: repo.promptTemplatePath
|
|
463
|
+
? resolvePath(repo.promptTemplatePath)
|
|
464
|
+
: undefined,
|
|
465
|
+
openaiOutputDirectory: repo.openaiOutputDirectory
|
|
466
|
+
? resolvePath(repo.openaiOutputDirectory)
|
|
467
|
+
: undefined,
|
|
468
|
+
};
|
|
499
469
|
// Add to internal map
|
|
500
|
-
this.repositories.set(repo.id,
|
|
470
|
+
this.repositories.set(repo.id, resolvedRepo);
|
|
501
471
|
// Create Linear client
|
|
502
472
|
const linearClient = new LinearClient({
|
|
503
473
|
accessToken: repo.linearToken,
|
|
@@ -511,14 +481,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
511
481
|
}, undefined, // No resumeNextSubroutine callback for dynamically added repos
|
|
512
482
|
this.procedureRouter, this.sharedApplicationServer);
|
|
513
483
|
this.agentSessionManagers.set(repo.id, agentSessionManager);
|
|
514
|
-
// Update token-to-repo mapping
|
|
515
|
-
const repoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
|
|
516
|
-
if (!repoIds.includes(repo.id)) {
|
|
517
|
-
repoIds.push(repo.id);
|
|
518
|
-
}
|
|
519
|
-
this.tokenToRepoIds.set(repo.linearToken, repoIds);
|
|
520
|
-
// Set up webhook listener
|
|
521
|
-
await this.setupWebhookListener(repo);
|
|
522
484
|
console.log(`✅ Repository added successfully: ${repo.name}`);
|
|
523
485
|
}
|
|
524
486
|
catch (error) {
|
|
@@ -538,8 +500,25 @@ export class EdgeWorker extends EventEmitter {
|
|
|
538
500
|
continue;
|
|
539
501
|
}
|
|
540
502
|
console.log(`🔄 Updating repository: ${repo.name} (${repo.id})`);
|
|
503
|
+
// Resolve paths that may contain tilde (~) prefix
|
|
504
|
+
const resolvedRepo = {
|
|
505
|
+
...repo,
|
|
506
|
+
repositoryPath: resolvePath(repo.repositoryPath),
|
|
507
|
+
workspaceBaseDir: resolvePath(repo.workspaceBaseDir),
|
|
508
|
+
mcpConfigPath: Array.isArray(repo.mcpConfigPath)
|
|
509
|
+
? repo.mcpConfigPath.map(resolvePath)
|
|
510
|
+
: repo.mcpConfigPath
|
|
511
|
+
? resolvePath(repo.mcpConfigPath)
|
|
512
|
+
: undefined,
|
|
513
|
+
promptTemplatePath: repo.promptTemplatePath
|
|
514
|
+
? resolvePath(repo.promptTemplatePath)
|
|
515
|
+
: undefined,
|
|
516
|
+
openaiOutputDirectory: repo.openaiOutputDirectory
|
|
517
|
+
? resolvePath(repo.openaiOutputDirectory)
|
|
518
|
+
: undefined,
|
|
519
|
+
};
|
|
541
520
|
// Update stored config
|
|
542
|
-
this.repositories.set(repo.id,
|
|
521
|
+
this.repositories.set(repo.id, resolvedRepo);
|
|
543
522
|
// If token changed, recreate Linear client
|
|
544
523
|
if (oldRepo.linearToken !== repo.linearToken) {
|
|
545
524
|
console.log(` 🔑 Token changed, recreating Linear client`);
|
|
@@ -547,22 +526,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
547
526
|
accessToken: repo.linearToken,
|
|
548
527
|
});
|
|
549
528
|
this.linearClients.set(repo.id, linearClient);
|
|
550
|
-
// Update token mapping
|
|
551
|
-
const oldRepoIds = this.tokenToRepoIds.get(oldRepo.linearToken) || [];
|
|
552
|
-
const filteredOldIds = oldRepoIds.filter((id) => id !== repo.id);
|
|
553
|
-
if (filteredOldIds.length > 0) {
|
|
554
|
-
this.tokenToRepoIds.set(oldRepo.linearToken, filteredOldIds);
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
this.tokenToRepoIds.delete(oldRepo.linearToken);
|
|
558
|
-
}
|
|
559
|
-
const newRepoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
|
|
560
|
-
if (!newRepoIds.includes(repo.id)) {
|
|
561
|
-
newRepoIds.push(repo.id);
|
|
562
|
-
}
|
|
563
|
-
this.tokenToRepoIds.set(repo.linearToken, newRepoIds);
|
|
564
|
-
// Reconnect webhook if needed
|
|
565
|
-
await this.reconnectWebhook(oldRepo, repo);
|
|
566
529
|
}
|
|
567
530
|
// If active status changed
|
|
568
531
|
if (oldRepo.isActive !== repo.isActive) {
|
|
@@ -571,7 +534,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
571
534
|
}
|
|
572
535
|
else {
|
|
573
536
|
console.log(` ▶️ Repository reactivated`);
|
|
574
|
-
await this.setupWebhookListener(repo);
|
|
575
537
|
}
|
|
576
538
|
}
|
|
577
539
|
console.log(`✅ Repository updated successfully: ${repo.name}`);
|
|
@@ -626,17 +588,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
626
588
|
this.repositories.delete(repo.id);
|
|
627
589
|
this.linearClients.delete(repo.id);
|
|
628
590
|
this.agentSessionManagers.delete(repo.id);
|
|
629
|
-
// Update token mapping
|
|
630
|
-
const repoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
|
|
631
|
-
const filteredIds = repoIds.filter((id) => id !== repo.id);
|
|
632
|
-
if (filteredIds.length > 0) {
|
|
633
|
-
this.tokenToRepoIds.set(repo.linearToken, filteredIds);
|
|
634
|
-
}
|
|
635
|
-
else {
|
|
636
|
-
this.tokenToRepoIds.delete(repo.linearToken);
|
|
637
|
-
}
|
|
638
|
-
// Clean up webhook listener if no other repos use the same token
|
|
639
|
-
await this.cleanupWebhookIfUnused(repo);
|
|
640
591
|
console.log(`✅ Repository removed successfully: ${repo.name}`);
|
|
641
592
|
}
|
|
642
593
|
catch (error) {
|
|
@@ -644,115 +595,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
644
595
|
}
|
|
645
596
|
}
|
|
646
597
|
}
|
|
647
|
-
/**
|
|
648
|
-
* Set up webhook listener for a repository
|
|
649
|
-
*/
|
|
650
|
-
async setupWebhookListener(repo) {
|
|
651
|
-
// Check if we already have a client for this token
|
|
652
|
-
const existingRepoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
|
|
653
|
-
const existingClient = existingRepoIds.length > 0
|
|
654
|
-
? this.ndjsonClients.get(existingRepoIds[0] || "")
|
|
655
|
-
: null;
|
|
656
|
-
if (existingClient) {
|
|
657
|
-
console.log(` ℹ️ Reusing existing webhook connection for token ...${repo.linearToken.slice(-4)}`);
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
// Create new NDJSON client for this token
|
|
661
|
-
const serverPort = this.config.serverPort || this.config.webhookPort || 3456;
|
|
662
|
-
const serverHost = this.config.serverHost || "localhost";
|
|
663
|
-
const useLinearDirectWebhooks = process.env.LINEAR_DIRECT_WEBHOOKS?.toLowerCase().trim() === "true";
|
|
664
|
-
const clientConfig = {
|
|
665
|
-
proxyUrl: this.config.proxyUrl,
|
|
666
|
-
token: repo.linearToken,
|
|
667
|
-
name: repo.name,
|
|
668
|
-
transport: "webhook",
|
|
669
|
-
useExternalWebhookServer: true,
|
|
670
|
-
externalWebhookServer: this.sharedApplicationServer,
|
|
671
|
-
webhookPort: serverPort,
|
|
672
|
-
webhookPath: "/webhook",
|
|
673
|
-
webhookHost: serverHost,
|
|
674
|
-
...(this.config.baseUrl && { webhookBaseUrl: this.config.baseUrl }),
|
|
675
|
-
...(!this.config.baseUrl &&
|
|
676
|
-
this.config.webhookBaseUrl && {
|
|
677
|
-
webhookBaseUrl: this.config.webhookBaseUrl,
|
|
678
|
-
}),
|
|
679
|
-
onConnect: () => this.handleConnect(repo.id, [repo]),
|
|
680
|
-
onDisconnect: (reason) => this.handleDisconnect(repo.id, [repo], reason),
|
|
681
|
-
onError: (error) => this.handleError(error),
|
|
682
|
-
};
|
|
683
|
-
const ndjsonClient = useLinearDirectWebhooks
|
|
684
|
-
? new LinearWebhookClient({
|
|
685
|
-
...clientConfig,
|
|
686
|
-
onWebhook: (payload) => {
|
|
687
|
-
// Get fresh repositories for this token to avoid stale closures
|
|
688
|
-
const freshRepos = this.getRepositoriesForToken(repo.linearToken);
|
|
689
|
-
this.handleWebhook(payload, freshRepos);
|
|
690
|
-
},
|
|
691
|
-
})
|
|
692
|
-
: new NdjsonClient(clientConfig);
|
|
693
|
-
if (!useLinearDirectWebhooks) {
|
|
694
|
-
ndjsonClient.on("webhook", (data) => {
|
|
695
|
-
// Get fresh repositories for this token to avoid stale closures
|
|
696
|
-
const freshRepos = this.getRepositoriesForToken(repo.linearToken);
|
|
697
|
-
this.handleWebhook(data, freshRepos);
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
this.ndjsonClients.set(repo.id, ndjsonClient);
|
|
701
|
-
// Connect the client
|
|
702
|
-
try {
|
|
703
|
-
await ndjsonClient.connect();
|
|
704
|
-
console.log(` ✅ Webhook listener connected for ${repo.name}`);
|
|
705
|
-
}
|
|
706
|
-
catch (error) {
|
|
707
|
-
console.error(` ❌ Failed to connect webhook listener:`, error);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
/**
|
|
711
|
-
* Reconnect webhook when token changes
|
|
712
|
-
*/
|
|
713
|
-
async reconnectWebhook(oldRepo, newRepo) {
|
|
714
|
-
console.log(` 🔌 Reconnecting webhook due to token change`);
|
|
715
|
-
// Disconnect old client if no other repos use it
|
|
716
|
-
await this.cleanupWebhookIfUnused(oldRepo);
|
|
717
|
-
// Set up new connection
|
|
718
|
-
await this.setupWebhookListener(newRepo);
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
|
-
* Clean up webhook listener if no other repositories use the token
|
|
722
|
-
*/
|
|
723
|
-
async cleanupWebhookIfUnused(repo) {
|
|
724
|
-
const repoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
|
|
725
|
-
const otherRepos = repoIds.filter((id) => id !== repo.id);
|
|
726
|
-
if (otherRepos.length === 0) {
|
|
727
|
-
// No other repos use this token, safe to disconnect
|
|
728
|
-
const client = this.ndjsonClients.get(repo.id);
|
|
729
|
-
if (client) {
|
|
730
|
-
console.log(` 🔌 Disconnecting webhook for token ...${repo.linearToken.slice(-4)}`);
|
|
731
|
-
client.disconnect();
|
|
732
|
-
this.ndjsonClients.delete(repo.id);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
else {
|
|
736
|
-
console.log(` ℹ️ Token still used by ${otherRepos.length} other repository(ies), keeping connection`);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
/**
|
|
740
|
-
* Handle connection established
|
|
741
|
-
*/
|
|
742
|
-
handleConnect(clientId, repos) {
|
|
743
|
-
// Get the token for backward compatibility with events
|
|
744
|
-
const token = repos[0]?.linearToken || clientId;
|
|
745
|
-
this.emit("connected", token);
|
|
746
|
-
// Connection logged by CLI app event handler
|
|
747
|
-
}
|
|
748
|
-
/**
|
|
749
|
-
* Handle disconnection
|
|
750
|
-
*/
|
|
751
|
-
handleDisconnect(clientId, repos, reason) {
|
|
752
|
-
// Get the token for backward compatibility with events
|
|
753
|
-
const token = repos[0]?.linearToken || clientId;
|
|
754
|
-
this.emit("disconnected", token, reason);
|
|
755
|
-
}
|
|
756
598
|
/**
|
|
757
599
|
* Handle errors
|
|
758
600
|
*/
|
|
@@ -1106,71 +948,66 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1106
948
|
this.procedureRouter.initializeProcedureMetadata(session, finalProcedure);
|
|
1107
949
|
// Post single procedure selection result (replaces ephemeral routing thought)
|
|
1108
950
|
await agentSessionManager.postProcedureSelectionThought(linearAgentActivitySessionId, finalProcedure.name, finalClassification);
|
|
1109
|
-
// Only determine system prompt for delegation (not mentions) or when /label-based-prompt is requested
|
|
1110
|
-
let systemPrompt;
|
|
1111
|
-
let systemPromptVersion;
|
|
1112
|
-
let promptType;
|
|
1113
|
-
if (!isMentionTriggered || isLabelBasedPromptRequested) {
|
|
1114
|
-
// Determine system prompt based on labels (delegation case or /label-based-prompt command)
|
|
1115
|
-
const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
|
|
1116
|
-
systemPrompt = systemPromptResult?.prompt;
|
|
1117
|
-
systemPromptVersion = systemPromptResult?.version;
|
|
1118
|
-
promptType = systemPromptResult?.type;
|
|
1119
|
-
// Post thought about system prompt selection
|
|
1120
|
-
if (systemPrompt) {
|
|
1121
|
-
await this.postSystemPromptSelectionThought(linearAgentActivitySessionId, labels, repository.id);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
else {
|
|
1125
|
-
console.log(`[EdgeWorker] Skipping system prompt for mention-triggered session ${linearAgentActivitySessionId}`);
|
|
1126
|
-
}
|
|
1127
|
-
// Build allowed tools list with Linear MCP tools (now with prompt type context)
|
|
1128
|
-
const allowedTools = this.buildAllowedTools(repository, promptType);
|
|
1129
|
-
const disallowedTools = this.buildDisallowedTools(repository, promptType);
|
|
1130
|
-
console.log(`[EdgeWorker] Configured allowed tools for ${fullIssue.identifier}:`, allowedTools);
|
|
1131
|
-
if (disallowedTools.length > 0) {
|
|
1132
|
-
console.log(`[EdgeWorker] Configured disallowed tools for ${fullIssue.identifier}:`, disallowedTools);
|
|
1133
|
-
}
|
|
1134
|
-
// Create Claude runner with attachment directory access and optional system prompt
|
|
1135
|
-
const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
|
|
1136
|
-
labels);
|
|
1137
|
-
const runner = new ClaudeRunner(runnerConfig);
|
|
1138
|
-
// Store runner by comment ID
|
|
1139
|
-
agentSessionManager.addClaudeRunner(linearAgentActivitySessionId, runner);
|
|
1140
|
-
// Save state after mapping changes
|
|
1141
|
-
await this.savePersistedState();
|
|
1142
|
-
// Emit events using full Linear issue
|
|
1143
|
-
this.emit("session:started", fullIssue.id, fullIssue, repository.id);
|
|
1144
|
-
this.config.handlers?.onSessionStart?.(fullIssue.id, fullIssue, repository.id);
|
|
1145
951
|
// Build and start Claude with initial prompt using full issue (streaming mode)
|
|
1146
952
|
console.log(`[EdgeWorker] Building initial prompt for issue ${fullIssue.identifier}`);
|
|
1147
953
|
try {
|
|
1148
|
-
//
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
954
|
+
// Create input for unified prompt assembly
|
|
955
|
+
const input = {
|
|
956
|
+
session,
|
|
957
|
+
fullIssue,
|
|
958
|
+
repository,
|
|
959
|
+
userComment: commentBody || "", // Empty for delegation, present for mentions
|
|
960
|
+
attachmentManifest: attachmentResult.manifest,
|
|
961
|
+
guidance,
|
|
962
|
+
agentSession,
|
|
963
|
+
labels,
|
|
964
|
+
isNewSession: true,
|
|
965
|
+
isStreaming: false, // Not yet streaming
|
|
966
|
+
isMentionTriggered: isMentionTriggered || false,
|
|
967
|
+
isLabelBasedPromptRequested: isLabelBasedPromptRequested || false,
|
|
968
|
+
};
|
|
969
|
+
// Use unified prompt assembly
|
|
970
|
+
const assembly = await this.assemblePrompt(input);
|
|
971
|
+
// Get systemPromptVersion for tracking (TODO: add to PromptAssembly metadata)
|
|
972
|
+
let systemPromptVersion;
|
|
973
|
+
let promptType;
|
|
974
|
+
if (!isMentionTriggered || isLabelBasedPromptRequested) {
|
|
975
|
+
const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
|
|
976
|
+
systemPromptVersion = systemPromptResult?.version;
|
|
977
|
+
promptType = systemPromptResult?.type;
|
|
978
|
+
// Post thought about system prompt selection
|
|
979
|
+
if (assembly.systemPrompt) {
|
|
980
|
+
await this.postSystemPromptSelectionThought(linearAgentActivitySessionId, labels, repository.id);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
// Build allowed tools list with Linear MCP tools (now with prompt type context)
|
|
984
|
+
const allowedTools = this.buildAllowedTools(repository, promptType);
|
|
985
|
+
const disallowedTools = this.buildDisallowedTools(repository, promptType);
|
|
986
|
+
console.log(`[EdgeWorker] Configured allowed tools for ${fullIssue.identifier}:`, allowedTools);
|
|
987
|
+
if (disallowedTools.length > 0) {
|
|
988
|
+
console.log(`[EdgeWorker] Configured disallowed tools for ${fullIssue.identifier}:`, disallowedTools);
|
|
989
|
+
}
|
|
990
|
+
// Create Claude runner with system prompt from assembly
|
|
991
|
+
const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, assembly.systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
|
|
992
|
+
labels);
|
|
993
|
+
const runner = new ClaudeRunner(runnerConfig);
|
|
994
|
+
// Store runner by comment ID
|
|
995
|
+
agentSessionManager.addClaudeRunner(linearAgentActivitySessionId, runner);
|
|
996
|
+
// Save state after mapping changes
|
|
997
|
+
await this.savePersistedState();
|
|
998
|
+
// Emit events using full Linear issue
|
|
999
|
+
this.emit("session:started", fullIssue.id, fullIssue, repository.id);
|
|
1000
|
+
this.config.handlers?.onSessionStart?.(fullIssue.id, fullIssue, repository.id);
|
|
1001
|
+
// Update runner with version information (if available)
|
|
1002
|
+
if (systemPromptVersion) {
|
|
1159
1003
|
runner.updatePromptVersions({
|
|
1160
|
-
userPromptVersion,
|
|
1161
1004
|
systemPromptVersion,
|
|
1162
1005
|
});
|
|
1163
1006
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
: isMentionTriggered
|
|
1167
|
-
? "mention"
|
|
1168
|
-
: systemPrompt
|
|
1169
|
-
? "label-based"
|
|
1170
|
-
: "fallback";
|
|
1171
|
-
console.log(`[EdgeWorker] Initial prompt built successfully using ${promptType} workflow, length: ${prompt.length} characters`);
|
|
1007
|
+
// Log metadata for debugging
|
|
1008
|
+
console.log(`[EdgeWorker] Initial prompt built successfully - components: ${assembly.metadata.components.join(", ")}, type: ${assembly.metadata.promptType}, length: ${assembly.userPrompt.length} characters`);
|
|
1172
1009
|
console.log(`[EdgeWorker] Starting Claude streaming session`);
|
|
1173
|
-
const sessionInfo = await runner.startStreaming(
|
|
1010
|
+
const sessionInfo = await runner.startStreaming(assembly.userPrompt);
|
|
1174
1011
|
console.log(`[EdgeWorker] Claude streaming session started: ${sessionInfo.sessionId}`);
|
|
1175
1012
|
// Note: AgentSessionManager will be initialized automatically when the first system message
|
|
1176
1013
|
// is received via handleClaudeMessage() callback
|
|
@@ -1235,38 +1072,8 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1235
1072
|
}
|
|
1236
1073
|
}
|
|
1237
1074
|
}
|
|
1238
|
-
//
|
|
1239
|
-
|
|
1240
|
-
const isStreaming = existingRunner?.isStreaming() || false;
|
|
1241
|
-
// Always route procedure for new comments, UNLESS actively streaming
|
|
1242
|
-
if (!isStreaming) {
|
|
1243
|
-
// Initialize procedure metadata using intelligent routing
|
|
1244
|
-
if (!session.metadata) {
|
|
1245
|
-
session.metadata = {};
|
|
1246
|
-
}
|
|
1247
|
-
// Post ephemeral "Routing..." thought
|
|
1248
|
-
await agentSessionManager.postRoutingThought(linearAgentActivitySessionId);
|
|
1249
|
-
// For prompted events, use the actual prompt content from the user
|
|
1250
|
-
// Combine with issue context for better routing
|
|
1251
|
-
if (!fullIssue) {
|
|
1252
|
-
console.warn(`[EdgeWorker] Routing without full issue details for ${linearAgentActivitySessionId}`);
|
|
1253
|
-
}
|
|
1254
|
-
const promptBody = webhook.agentActivity.content.body;
|
|
1255
|
-
const routingDecision = await this.procedureRouter.determineRoutine(promptBody.trim());
|
|
1256
|
-
const selectedProcedure = routingDecision.procedure;
|
|
1257
|
-
// Initialize procedure metadata in session (resets for each new comment)
|
|
1258
|
-
this.procedureRouter.initializeProcedureMetadata(session, selectedProcedure);
|
|
1259
|
-
// Post procedure selection result (replaces ephemeral routing thought)
|
|
1260
|
-
await agentSessionManager.postProcedureSelectionThought(linearAgentActivitySessionId, selectedProcedure.name, routingDecision.classification);
|
|
1261
|
-
// Log routing decision
|
|
1262
|
-
console.log(`[EdgeWorker] Routing decision for ${linearAgentActivitySessionId} (prompted webhook, ${isNewSession ? "new" : "existing"} session):`);
|
|
1263
|
-
console.log(` Classification: ${routingDecision.classification}`);
|
|
1264
|
-
console.log(` Procedure: ${selectedProcedure.name}`);
|
|
1265
|
-
console.log(` Reasoning: ${routingDecision.reasoning}`);
|
|
1266
|
-
}
|
|
1267
|
-
else {
|
|
1268
|
-
console.log(`[EdgeWorker] Skipping routing for ${linearAgentActivitySessionId} - runner is actively streaming`);
|
|
1269
|
-
}
|
|
1075
|
+
// Note: Routing and streaming check happens later in handlePromptWithStreamingCheck
|
|
1076
|
+
// after attachments are processed
|
|
1270
1077
|
// Ensure session is not null after creation/retrieval
|
|
1271
1078
|
if (!session) {
|
|
1272
1079
|
throw new Error(`Failed to get or create session for agent activity session ${linearAgentActivitySessionId}`);
|
|
@@ -1285,6 +1092,8 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1285
1092
|
// Ensure directory exists
|
|
1286
1093
|
await mkdir(attachmentsDir, { recursive: true });
|
|
1287
1094
|
let attachmentManifest = "";
|
|
1095
|
+
let commentAuthor;
|
|
1096
|
+
let commentTimestamp;
|
|
1288
1097
|
try {
|
|
1289
1098
|
const result = await linearClient.client.rawRequest(`
|
|
1290
1099
|
query GetComment($id: String!) {
|
|
@@ -1295,16 +1104,27 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1295
1104
|
updatedAt
|
|
1296
1105
|
user {
|
|
1297
1106
|
name
|
|
1107
|
+
displayName
|
|
1108
|
+
email
|
|
1298
1109
|
id
|
|
1299
1110
|
}
|
|
1300
1111
|
}
|
|
1301
1112
|
}
|
|
1302
1113
|
`, { id: commentId });
|
|
1114
|
+
// Extract comment data
|
|
1115
|
+
const comment = result.data.comment;
|
|
1116
|
+
// Extract comment metadata for multi-player context
|
|
1117
|
+
if (comment) {
|
|
1118
|
+
const user = comment.user;
|
|
1119
|
+
commentAuthor =
|
|
1120
|
+
user?.displayName || user?.name || user?.email || "Unknown";
|
|
1121
|
+
commentTimestamp = comment.createdAt || new Date().toISOString();
|
|
1122
|
+
}
|
|
1303
1123
|
// Count existing attachments
|
|
1304
1124
|
const existingFiles = await readdir(attachmentsDir).catch(() => []);
|
|
1305
1125
|
const existingAttachmentCount = existingFiles.filter((file) => file.startsWith("attachment_") || file.startsWith("image_")).length;
|
|
1306
1126
|
// Download new attachments from the comment
|
|
1307
|
-
const downloadResult = await this.downloadCommentAttachments(
|
|
1127
|
+
const downloadResult = await this.downloadCommentAttachments(comment.body, attachmentsDir, repository.linearToken, existingAttachmentCount);
|
|
1308
1128
|
if (downloadResult.totalNewAttachments > 0) {
|
|
1309
1129
|
attachmentManifest = this.generateNewAttachmentManifest(downloadResult);
|
|
1310
1130
|
}
|
|
@@ -1318,6 +1138,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1318
1138
|
if (stopSignal) {
|
|
1319
1139
|
console.log(`[EdgeWorker] Received stop signal for agent activity session ${linearAgentActivitySessionId}`);
|
|
1320
1140
|
// Stop the existing runner if it's active
|
|
1141
|
+
const existingRunner = session.claudeRunner;
|
|
1321
1142
|
if (existingRunner) {
|
|
1322
1143
|
existingRunner.stop();
|
|
1323
1144
|
console.log(`[EdgeWorker] Stopped Claude session for agent activity session ${linearAgentActivitySessionId}`);
|
|
@@ -1327,34 +1148,13 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1327
1148
|
await agentSessionManager.createResponseActivity(linearAgentActivitySessionId, stopConfirmation);
|
|
1328
1149
|
return; // Exit early - stop signal handled
|
|
1329
1150
|
}
|
|
1330
|
-
//
|
|
1331
|
-
if (existingRunner?.isStreaming()) {
|
|
1332
|
-
// Add comment with attachment manifest to existing stream
|
|
1333
|
-
console.log(`[EdgeWorker] Adding comment to existing stream for agent activity session ${linearAgentActivitySessionId}`);
|
|
1334
|
-
// Append attachment manifest to the prompt if we have one
|
|
1335
|
-
let fullPrompt = promptBody;
|
|
1336
|
-
if (attachmentManifest) {
|
|
1337
|
-
fullPrompt = `${promptBody}\n\n${attachmentManifest}`;
|
|
1338
|
-
}
|
|
1339
|
-
existingRunner.addStreamMessage(fullPrompt);
|
|
1340
|
-
return; // Exit early - comment has been added to stream
|
|
1341
|
-
}
|
|
1342
|
-
// Use the new resumeClaudeSession function
|
|
1151
|
+
// Use centralized streaming check and routing logic
|
|
1343
1152
|
try {
|
|
1344
|
-
await this.
|
|
1153
|
+
await this.handlePromptWithStreamingCheck(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, [], // No additional allowed directories for regular continuation
|
|
1154
|
+
`prompted webhook (${isNewSession ? "new" : "existing"} session)`, commentAuthor, commentTimestamp);
|
|
1345
1155
|
}
|
|
1346
1156
|
catch (error) {
|
|
1347
|
-
console.error("Failed to
|
|
1348
|
-
// Remove any partially created session
|
|
1349
|
-
// this.sessionManager.removeSession(threadRootCommentId)
|
|
1350
|
-
// this.commentToRepo.delete(threadRootCommentId)
|
|
1351
|
-
// this.commentToIssue.delete(threadRootCommentId)
|
|
1352
|
-
// // Start fresh for root comments, or fall back to issue assignment
|
|
1353
|
-
// if (isRootComment) {
|
|
1354
|
-
// await this.handleNewRootComment(issue, comment, repository)
|
|
1355
|
-
// } else {
|
|
1356
|
-
// await this.handleIssueAssigned(issue, repository)
|
|
1357
|
-
// }
|
|
1157
|
+
console.error("Failed to handle prompted webhook:", error);
|
|
1358
1158
|
}
|
|
1359
1159
|
}
|
|
1360
1160
|
/**
|
|
@@ -1585,10 +1385,12 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1585
1385
|
async buildMentionPrompt(issue, agentSession, attachmentManifest = "", guidance) {
|
|
1586
1386
|
try {
|
|
1587
1387
|
console.log(`[EdgeWorker] Building mention prompt for issue ${issue.identifier}`);
|
|
1588
|
-
// Get the mention comment
|
|
1388
|
+
// Get the mention comment metadata
|
|
1589
1389
|
const mentionContent = agentSession.comment?.body || "";
|
|
1590
|
-
|
|
1591
|
-
|
|
1390
|
+
const authorName = agentSession.creator?.name || agentSession.creator?.id || "Unknown";
|
|
1391
|
+
const timestamp = agentSession.createdAt || new Date().toISOString();
|
|
1392
|
+
// Build a focused prompt with comment metadata
|
|
1393
|
+
let prompt = `You were mentioned in a Linear comment on this issue:
|
|
1592
1394
|
|
|
1593
1395
|
<linear_issue>
|
|
1594
1396
|
<id>${issue.id}</id>
|
|
@@ -1597,11 +1399,15 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1597
1399
|
<url>${issue.url}</url>
|
|
1598
1400
|
</linear_issue>
|
|
1599
1401
|
|
|
1600
|
-
<
|
|
1402
|
+
<mention_comment>
|
|
1403
|
+
<author>${authorName}</author>
|
|
1404
|
+
<timestamp>${timestamp}</timestamp>
|
|
1405
|
+
<content>
|
|
1601
1406
|
${mentionContent}
|
|
1602
|
-
</
|
|
1407
|
+
</content>
|
|
1408
|
+
</mention_comment>
|
|
1603
1409
|
|
|
1604
|
-
|
|
1410
|
+
Focus on addressing the specific request in the mention. You can use the Linear MCP tools to fetch additional context if needed.`;
|
|
1605
1411
|
// Append agent guidance if present
|
|
1606
1412
|
prompt += this.formatAgentGuidance(guidance);
|
|
1607
1413
|
// Append attachment manifest if any
|
|
@@ -1817,17 +1623,17 @@ ${reply.body}
|
|
|
1817
1623
|
* @param guidance Optional agent guidance rules from Linear
|
|
1818
1624
|
* @returns Formatted prompt string
|
|
1819
1625
|
*/
|
|
1820
|
-
async
|
|
1821
|
-
console.log(`[EdgeWorker]
|
|
1626
|
+
async buildIssueContextPrompt(issue, repository, newComment, attachmentManifest = "", guidance) {
|
|
1627
|
+
console.log(`[EdgeWorker] buildIssueContextPrompt called for issue ${issue.identifier}${newComment ? " with new comment" : ""}`);
|
|
1822
1628
|
try {
|
|
1823
1629
|
// Use custom template if provided (repository-specific takes precedence)
|
|
1824
1630
|
let templatePath = repository.promptTemplatePath ||
|
|
1825
1631
|
this.config.features?.promptTemplatePath;
|
|
1826
|
-
// If no custom template, use the
|
|
1632
|
+
// If no custom template, use the standard issue assigned user prompt template
|
|
1827
1633
|
if (!templatePath) {
|
|
1828
1634
|
const __filename = fileURLToPath(import.meta.url);
|
|
1829
1635
|
const __dirname = dirname(__filename);
|
|
1830
|
-
templatePath = resolve(__dirname, "../
|
|
1636
|
+
templatePath = resolve(__dirname, "../prompts/standard-issue-assigned-user-prompt.md");
|
|
1831
1637
|
}
|
|
1832
1638
|
// Load the template
|
|
1833
1639
|
console.log(`[EdgeWorker] Loading prompt template from: ${templatePath}`);
|
|
@@ -1913,8 +1719,8 @@ IMPORTANT: Focus specifically on addressing the new comment above. This is a new
|
|
|
1913
1719
|
.replace(/{{new_comment_content}}/g, newComment.body || "");
|
|
1914
1720
|
}
|
|
1915
1721
|
else {
|
|
1916
|
-
// Remove the new comment section entirely
|
|
1917
|
-
prompt = prompt.replace(
|
|
1722
|
+
// Remove the new comment section entirely (including preceding newlines)
|
|
1723
|
+
prompt = prompt.replace(/\n*{{#if new_comment}}[\s\S]*?{{\/if}}/g, "");
|
|
1918
1724
|
}
|
|
1919
1725
|
// Append agent guidance if present
|
|
1920
1726
|
prompt += this.formatAgentGuidance(guidance);
|
|
@@ -1960,29 +1766,28 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1960
1766
|
*/
|
|
1961
1767
|
getConnectionStatus() {
|
|
1962
1768
|
const status = new Map();
|
|
1963
|
-
|
|
1964
|
-
|
|
1769
|
+
// Single event transport is "connected" if it exists
|
|
1770
|
+
if (this.linearEventTransport) {
|
|
1771
|
+
// Mark all repositories as connected since they share the single transport
|
|
1772
|
+
for (const repoId of this.repositories.keys()) {
|
|
1773
|
+
status.set(repoId, true);
|
|
1774
|
+
}
|
|
1965
1775
|
}
|
|
1966
1776
|
return status;
|
|
1967
1777
|
}
|
|
1968
1778
|
/**
|
|
1969
|
-
* Get
|
|
1779
|
+
* Get event transport (for testing purposes)
|
|
1970
1780
|
* @internal
|
|
1971
1781
|
*/
|
|
1972
|
-
_getClientByToken(
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
if (repo?.linearToken === token) {
|
|
1976
|
-
return client;
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
return undefined;
|
|
1782
|
+
_getClientByToken(_token) {
|
|
1783
|
+
// Return the single shared event transport
|
|
1784
|
+
return this.linearEventTransport;
|
|
1980
1785
|
}
|
|
1981
1786
|
/**
|
|
1982
1787
|
* Start OAuth flow using the shared application server
|
|
1983
1788
|
*/
|
|
1984
1789
|
async startOAuthFlow(proxyUrl) {
|
|
1985
|
-
const oauthProxyUrl = proxyUrl || this.config.proxyUrl;
|
|
1790
|
+
const oauthProxyUrl = proxyUrl || this.config.proxyUrl || DEFAULT_PROXY_URL;
|
|
1986
1791
|
return this.sharedApplicationServer.startOAuthFlow(oauthProxyUrl);
|
|
1987
1792
|
}
|
|
1988
1793
|
/**
|
|
@@ -2481,6 +2286,8 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
2481
2286
|
},
|
|
2482
2287
|
onFeedbackDelivery: async (childSessionId, message) => {
|
|
2483
2288
|
console.log(`[EdgeWorker] Processing feedback delivery to child session ${childSessionId}`);
|
|
2289
|
+
// Find the parent session ID for context
|
|
2290
|
+
const parentSessionId = this.childToParentAgentSession.get(childSessionId);
|
|
2484
2291
|
// Find the repository containing the child session
|
|
2485
2292
|
// We need to search all repositories for this child session
|
|
2486
2293
|
let childRepo;
|
|
@@ -2503,22 +2310,62 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
2503
2310
|
return false;
|
|
2504
2311
|
}
|
|
2505
2312
|
console.log(`[EdgeWorker] Found child session - Issue: ${childSession.issueId}`);
|
|
2313
|
+
// Get parent session info for better context in the thought
|
|
2314
|
+
let parentIssueId;
|
|
2315
|
+
if (parentSessionId) {
|
|
2316
|
+
// Find parent session across all repositories
|
|
2317
|
+
for (const manager of this.agentSessionManagers.values()) {
|
|
2318
|
+
const parentSession = manager.getSession(parentSessionId);
|
|
2319
|
+
if (parentSession) {
|
|
2320
|
+
parentIssueId =
|
|
2321
|
+
parentSession.issue?.identifier || parentSession.issueId;
|
|
2322
|
+
break;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
// Post thought to Linear showing feedback receipt
|
|
2327
|
+
const linearClient = this.linearClients.get(childRepo.id);
|
|
2328
|
+
if (linearClient) {
|
|
2329
|
+
const feedbackThought = parentIssueId
|
|
2330
|
+
? `Received feedback from orchestrator (${parentIssueId}):\n\n---\n\n${message}\n\n---`
|
|
2331
|
+
: `Received feedback from orchestrator:\n\n---\n\n${message}\n\n---`;
|
|
2332
|
+
try {
|
|
2333
|
+
const result = await linearClient.createAgentActivity({
|
|
2334
|
+
agentSessionId: childSessionId,
|
|
2335
|
+
content: {
|
|
2336
|
+
type: "thought",
|
|
2337
|
+
body: feedbackThought,
|
|
2338
|
+
},
|
|
2339
|
+
});
|
|
2340
|
+
if (result.success) {
|
|
2341
|
+
console.log(`[EdgeWorker] Posted feedback receipt thought for child session ${childSessionId}`);
|
|
2342
|
+
}
|
|
2343
|
+
else {
|
|
2344
|
+
console.error(`[EdgeWorker] Failed to post feedback receipt thought:`, result);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
catch (error) {
|
|
2348
|
+
console.error(`[EdgeWorker] Error posting feedback receipt thought:`, error);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2506
2351
|
// Format the feedback as a prompt for the child session with enhanced markdown formatting
|
|
2507
2352
|
const feedbackPrompt = `## Received feedback from orchestrator\n\n---\n\n${message}\n\n---`;
|
|
2508
|
-
//
|
|
2353
|
+
// Use centralized streaming check and routing logic
|
|
2509
2354
|
// Important: We don't await the full session completion to avoid timeouts.
|
|
2510
2355
|
// The feedback is delivered immediately when the session starts, so we can
|
|
2511
2356
|
// return success right away while the session continues in the background.
|
|
2512
|
-
|
|
2357
|
+
console.log(`[EdgeWorker] Handling feedback delivery to child session ${childSessionId}`);
|
|
2358
|
+
this.handlePromptWithStreamingCheck(childSession, childRepo, childSessionId, childAgentSessionManager, feedbackPrompt, "", // No attachment manifest for feedback
|
|
2513
2359
|
false, // Not a new session
|
|
2514
|
-
[]
|
|
2360
|
+
[], // No additional allowed directories for feedback
|
|
2361
|
+
"give feedback to child")
|
|
2515
2362
|
.then(() => {
|
|
2516
2363
|
console.log(`[EdgeWorker] Child session ${childSessionId} completed processing feedback`);
|
|
2517
2364
|
})
|
|
2518
2365
|
.catch((error) => {
|
|
2519
|
-
console.error(`[EdgeWorker] Failed to
|
|
2366
|
+
console.error(`[EdgeWorker] Failed to process feedback in child session:`, error);
|
|
2520
2367
|
});
|
|
2521
|
-
// Return success immediately after initiating the
|
|
2368
|
+
// Return success immediately after initiating the handling
|
|
2522
2369
|
console.log(`[EdgeWorker] Feedback delivered successfully to child session ${childSessionId}`);
|
|
2523
2370
|
return true;
|
|
2524
2371
|
},
|
|
@@ -2562,22 +2409,264 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
2562
2409
|
}
|
|
2563
2410
|
}
|
|
2564
2411
|
/**
|
|
2565
|
-
* Build prompt for a session -
|
|
2412
|
+
* Build the complete prompt for a session - shows full prompt assembly in one place
|
|
2413
|
+
*
|
|
2414
|
+
* New session prompt structure:
|
|
2415
|
+
* 1. Issue context (from buildIssueContextPrompt)
|
|
2416
|
+
* 2. Initial subroutine prompt (if procedure initialized)
|
|
2417
|
+
* 3. User comment
|
|
2418
|
+
*
|
|
2419
|
+
* Existing session prompt structure:
|
|
2420
|
+
* 1. User comment
|
|
2421
|
+
* 2. Attachment manifest (if present)
|
|
2566
2422
|
*/
|
|
2567
|
-
async buildSessionPrompt(isNewSession, fullIssue, repository, promptBody, attachmentManifest) {
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2423
|
+
async buildSessionPrompt(isNewSession, session, fullIssue, repository, promptBody, attachmentManifest, commentAuthor, commentTimestamp) {
|
|
2424
|
+
// Fetch labels for system prompt determination
|
|
2425
|
+
const labels = await this.fetchIssueLabels(fullIssue);
|
|
2426
|
+
// Create input for unified prompt assembly
|
|
2427
|
+
const input = {
|
|
2428
|
+
session,
|
|
2429
|
+
fullIssue,
|
|
2430
|
+
repository,
|
|
2431
|
+
userComment: promptBody,
|
|
2432
|
+
commentAuthor,
|
|
2433
|
+
commentTimestamp,
|
|
2434
|
+
attachmentManifest,
|
|
2435
|
+
isNewSession,
|
|
2436
|
+
isStreaming: false, // This path is only for non-streaming prompts
|
|
2437
|
+
labels,
|
|
2438
|
+
};
|
|
2439
|
+
// Use unified prompt assembly
|
|
2440
|
+
const assembly = await this.assemblePrompt(input);
|
|
2441
|
+
// Log metadata for debugging
|
|
2442
|
+
console.log(`[EdgeWorker] Built prompt - components: ${assembly.metadata.components.join(", ")}, type: ${assembly.metadata.promptType}`);
|
|
2443
|
+
return assembly.userPrompt;
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* Assemble a complete prompt - unified entry point for all prompt building
|
|
2447
|
+
* This method contains all prompt assembly logic in one place
|
|
2448
|
+
*/
|
|
2449
|
+
async assemblePrompt(input) {
|
|
2450
|
+
// If actively streaming, just pass through the comment
|
|
2451
|
+
if (input.isStreaming) {
|
|
2452
|
+
return this.buildStreamingPrompt(input);
|
|
2453
|
+
}
|
|
2454
|
+
// If new session, build full prompt with all components
|
|
2455
|
+
if (input.isNewSession) {
|
|
2456
|
+
return this.buildNewSessionPrompt(input);
|
|
2457
|
+
}
|
|
2458
|
+
// Existing session continuation - just user comment + attachments
|
|
2459
|
+
return this.buildContinuationPrompt(input);
|
|
2460
|
+
}
|
|
2461
|
+
/**
|
|
2462
|
+
* Build prompt for actively streaming session - pass through user comment as-is
|
|
2463
|
+
*/
|
|
2464
|
+
buildStreamingPrompt(input) {
|
|
2465
|
+
const components = ["user-comment"];
|
|
2466
|
+
if (input.attachmentManifest) {
|
|
2467
|
+
components.push("attachment-manifest");
|
|
2468
|
+
}
|
|
2469
|
+
const parts = [input.userComment];
|
|
2470
|
+
if (input.attachmentManifest) {
|
|
2471
|
+
parts.push(input.attachmentManifest);
|
|
2472
|
+
}
|
|
2473
|
+
return {
|
|
2474
|
+
systemPrompt: undefined,
|
|
2475
|
+
userPrompt: parts.join("\n\n"),
|
|
2476
|
+
metadata: {
|
|
2477
|
+
components,
|
|
2478
|
+
promptType: "continuation",
|
|
2479
|
+
isNewSession: false,
|
|
2480
|
+
isStreaming: true,
|
|
2481
|
+
},
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* Build prompt for new session - includes issue context, subroutine prompt, and user comment
|
|
2486
|
+
*/
|
|
2487
|
+
async buildNewSessionPrompt(input) {
|
|
2488
|
+
const components = [];
|
|
2489
|
+
const parts = [];
|
|
2490
|
+
// 1. Determine system prompt from labels
|
|
2491
|
+
// Only for delegation (not mentions) or when /label-based-prompt is requested
|
|
2492
|
+
let labelBasedSystemPrompt;
|
|
2493
|
+
if (!input.isMentionTriggered || input.isLabelBasedPromptRequested) {
|
|
2494
|
+
labelBasedSystemPrompt = await this.determineSystemPromptForAssembly(input.labels || [], input.repository);
|
|
2495
|
+
}
|
|
2496
|
+
// 2. Determine system prompt based on prompt type
|
|
2497
|
+
// Label-based: Use only the label-based system prompt
|
|
2498
|
+
// Fallback: Use scenarios system prompt (shared instructions)
|
|
2499
|
+
let systemPrompt;
|
|
2500
|
+
if (labelBasedSystemPrompt) {
|
|
2501
|
+
// Use label-based system prompt as-is (no shared instructions)
|
|
2502
|
+
systemPrompt = labelBasedSystemPrompt;
|
|
2573
2503
|
}
|
|
2574
2504
|
else {
|
|
2575
|
-
//
|
|
2576
|
-
const
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2505
|
+
// Use scenarios system prompt for fallback cases
|
|
2506
|
+
const sharedInstructions = await this.loadSharedInstructions();
|
|
2507
|
+
systemPrompt = sharedInstructions;
|
|
2508
|
+
}
|
|
2509
|
+
// 3. Build issue context using appropriate builder
|
|
2510
|
+
// Use label-based prompt ONLY if we have a label-based system prompt
|
|
2511
|
+
const promptType = this.determinePromptType(input, !!labelBasedSystemPrompt);
|
|
2512
|
+
const issueContext = await this.buildIssueContextForPromptAssembly(input.fullIssue, input.repository, promptType, input.attachmentManifest, input.guidance, input.agentSession);
|
|
2513
|
+
parts.push(issueContext.prompt);
|
|
2514
|
+
components.push("issue-context");
|
|
2515
|
+
// 4. Load and append initial subroutine prompt
|
|
2516
|
+
const currentSubroutine = this.procedureRouter.getCurrentSubroutine(input.session);
|
|
2517
|
+
let subroutineName;
|
|
2518
|
+
if (currentSubroutine) {
|
|
2519
|
+
const subroutinePrompt = await this.loadSubroutinePrompt(currentSubroutine);
|
|
2520
|
+
if (subroutinePrompt) {
|
|
2521
|
+
parts.push(subroutinePrompt);
|
|
2522
|
+
components.push("subroutine-prompt");
|
|
2523
|
+
subroutineName = currentSubroutine.name;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
// 5. Add user comment (if present)
|
|
2527
|
+
// Skip for mention-triggered prompts since the comment is already in the mention block
|
|
2528
|
+
if (input.userComment.trim() && !input.isMentionTriggered) {
|
|
2529
|
+
// If we have author/timestamp metadata, include it for multi-player context
|
|
2530
|
+
if (input.commentAuthor || input.commentTimestamp) {
|
|
2531
|
+
const author = input.commentAuthor || "Unknown";
|
|
2532
|
+
const timestamp = input.commentTimestamp || new Date().toISOString();
|
|
2533
|
+
parts.push(`<user_comment>
|
|
2534
|
+
<author>${author}</author>
|
|
2535
|
+
<timestamp>${timestamp}</timestamp>
|
|
2536
|
+
<content>
|
|
2537
|
+
${input.userComment}
|
|
2538
|
+
</content>
|
|
2539
|
+
</user_comment>`);
|
|
2540
|
+
}
|
|
2541
|
+
else {
|
|
2542
|
+
// Legacy format without metadata
|
|
2543
|
+
parts.push(`<user_comment>\n${input.userComment}\n</user_comment>`);
|
|
2544
|
+
}
|
|
2545
|
+
components.push("user-comment");
|
|
2546
|
+
}
|
|
2547
|
+
// 6. Add guidance rules (if present)
|
|
2548
|
+
if (input.guidance && input.guidance.length > 0) {
|
|
2549
|
+
components.push("guidance-rules");
|
|
2550
|
+
}
|
|
2551
|
+
return {
|
|
2552
|
+
systemPrompt,
|
|
2553
|
+
userPrompt: parts.join("\n\n"),
|
|
2554
|
+
metadata: {
|
|
2555
|
+
components,
|
|
2556
|
+
subroutineName,
|
|
2557
|
+
promptType,
|
|
2558
|
+
isNewSession: true,
|
|
2559
|
+
isStreaming: false,
|
|
2560
|
+
},
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
/**
|
|
2564
|
+
* Build prompt for existing session continuation - user comment and attachments only
|
|
2565
|
+
*/
|
|
2566
|
+
buildContinuationPrompt(input) {
|
|
2567
|
+
const components = ["user-comment"];
|
|
2568
|
+
if (input.attachmentManifest) {
|
|
2569
|
+
components.push("attachment-manifest");
|
|
2570
|
+
}
|
|
2571
|
+
// Wrap comment in XML with author and timestamp for multi-player context
|
|
2572
|
+
const author = input.commentAuthor || "Unknown";
|
|
2573
|
+
const timestamp = input.commentTimestamp || new Date().toISOString();
|
|
2574
|
+
const commentXml = `<new_comment>
|
|
2575
|
+
<author>${author}</author>
|
|
2576
|
+
<timestamp>${timestamp}</timestamp>
|
|
2577
|
+
<content>
|
|
2578
|
+
${input.userComment}
|
|
2579
|
+
</content>
|
|
2580
|
+
</new_comment>`;
|
|
2581
|
+
const parts = [commentXml];
|
|
2582
|
+
if (input.attachmentManifest) {
|
|
2583
|
+
parts.push(input.attachmentManifest);
|
|
2580
2584
|
}
|
|
2585
|
+
return {
|
|
2586
|
+
systemPrompt: undefined,
|
|
2587
|
+
userPrompt: parts.join("\n\n"),
|
|
2588
|
+
metadata: {
|
|
2589
|
+
components,
|
|
2590
|
+
promptType: "continuation",
|
|
2591
|
+
isNewSession: false,
|
|
2592
|
+
isStreaming: false,
|
|
2593
|
+
},
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
/**
|
|
2597
|
+
* Determine the prompt type based on input flags and system prompt availability
|
|
2598
|
+
*/
|
|
2599
|
+
determinePromptType(input, hasSystemPrompt) {
|
|
2600
|
+
if (input.isMentionTriggered && input.isLabelBasedPromptRequested) {
|
|
2601
|
+
return "label-based-prompt-command";
|
|
2602
|
+
}
|
|
2603
|
+
if (input.isMentionTriggered) {
|
|
2604
|
+
return "mention";
|
|
2605
|
+
}
|
|
2606
|
+
if (hasSystemPrompt) {
|
|
2607
|
+
return "label-based";
|
|
2608
|
+
}
|
|
2609
|
+
return "fallback";
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Load a subroutine prompt file
|
|
2613
|
+
* Extracted helper to make prompt assembly more readable
|
|
2614
|
+
*/
|
|
2615
|
+
async loadSubroutinePrompt(subroutine) {
|
|
2616
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
2617
|
+
const __dirname = dirname(__filename);
|
|
2618
|
+
const subroutinePromptPath = join(__dirname, "prompts", subroutine.promptPath);
|
|
2619
|
+
try {
|
|
2620
|
+
const prompt = await readFile(subroutinePromptPath, "utf-8");
|
|
2621
|
+
console.log(`[EdgeWorker] Loaded ${subroutine.name} subroutine prompt (${prompt.length} characters)`);
|
|
2622
|
+
return prompt;
|
|
2623
|
+
}
|
|
2624
|
+
catch (error) {
|
|
2625
|
+
console.warn(`[EdgeWorker] Failed to load subroutine prompt from ${subroutinePromptPath}:`, error);
|
|
2626
|
+
return null;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
/**
|
|
2630
|
+
* Load shared instructions that get appended to all system prompts
|
|
2631
|
+
*/
|
|
2632
|
+
async loadSharedInstructions() {
|
|
2633
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
2634
|
+
const __dirname = dirname(__filename);
|
|
2635
|
+
const instructionsPath = join(__dirname, "..", "prompts", "todolist-system-prompt-extension.md");
|
|
2636
|
+
try {
|
|
2637
|
+
const instructions = await readFile(instructionsPath, "utf-8");
|
|
2638
|
+
return instructions;
|
|
2639
|
+
}
|
|
2640
|
+
catch (error) {
|
|
2641
|
+
console.error(`[EdgeWorker] Failed to load shared instructions from ${instructionsPath}:`, error);
|
|
2642
|
+
return ""; // Return empty string if file can't be loaded
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
/**
|
|
2646
|
+
* Adapter method for prompt assembly - extracts just the prompt string
|
|
2647
|
+
*/
|
|
2648
|
+
async determineSystemPromptForAssembly(labels, repository) {
|
|
2649
|
+
const result = await this.determineSystemPromptFromLabels(labels, repository);
|
|
2650
|
+
return result?.prompt;
|
|
2651
|
+
}
|
|
2652
|
+
/**
|
|
2653
|
+
* Adapter method for prompt assembly - routes to appropriate issue context builder
|
|
2654
|
+
*/
|
|
2655
|
+
async buildIssueContextForPromptAssembly(issue, repository, promptType, attachmentManifest, guidance, agentSession) {
|
|
2656
|
+
// Delegate to appropriate builder based on promptType
|
|
2657
|
+
if (promptType === "mention") {
|
|
2658
|
+
if (!agentSession) {
|
|
2659
|
+
throw new Error("agentSession is required for mention-triggered prompts");
|
|
2660
|
+
}
|
|
2661
|
+
return this.buildMentionPrompt(issue, agentSession, attachmentManifest, guidance);
|
|
2662
|
+
}
|
|
2663
|
+
if (promptType === "label-based" ||
|
|
2664
|
+
promptType === "label-based-prompt-command") {
|
|
2665
|
+
return this.buildLabelBasedPrompt(issue, repository, attachmentManifest, guidance);
|
|
2666
|
+
}
|
|
2667
|
+
// Fallback to standard issue context
|
|
2668
|
+
return this.buildIssueContextPrompt(issue, repository, undefined, // No new comment for initial prompt assembly
|
|
2669
|
+
attachmentManifest, guidance);
|
|
2581
2670
|
}
|
|
2582
2671
|
/**
|
|
2583
2672
|
* Build Claude runner configuration with common settings
|
|
@@ -2879,6 +2968,78 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
2879
2968
|
console.error(`[EdgeWorker] Error posting parent resumption acknowledgment:`, error);
|
|
2880
2969
|
}
|
|
2881
2970
|
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Re-route procedure for a session (used when resuming from child or give feedback)
|
|
2973
|
+
* This ensures the currentSubroutine is reset to avoid suppression issues
|
|
2974
|
+
*/
|
|
2975
|
+
async rerouteProcedureForSession(session, linearAgentActivitySessionId, agentSessionManager, promptBody) {
|
|
2976
|
+
// Initialize procedure metadata using intelligent routing
|
|
2977
|
+
if (!session.metadata) {
|
|
2978
|
+
session.metadata = {};
|
|
2979
|
+
}
|
|
2980
|
+
// Post ephemeral "Routing..." thought
|
|
2981
|
+
await agentSessionManager.postRoutingThought(linearAgentActivitySessionId);
|
|
2982
|
+
// Route based on the prompt content
|
|
2983
|
+
const routingDecision = await this.procedureRouter.determineRoutine(promptBody.trim());
|
|
2984
|
+
const selectedProcedure = routingDecision.procedure;
|
|
2985
|
+
// Initialize procedure metadata in session (resets currentSubroutine)
|
|
2986
|
+
this.procedureRouter.initializeProcedureMetadata(session, selectedProcedure);
|
|
2987
|
+
// Post procedure selection result (replaces ephemeral routing thought)
|
|
2988
|
+
await agentSessionManager.postProcedureSelectionThought(linearAgentActivitySessionId, selectedProcedure.name, routingDecision.classification);
|
|
2989
|
+
// Log routing decision
|
|
2990
|
+
console.log(`[EdgeWorker] Routing decision for ${linearAgentActivitySessionId}:`);
|
|
2991
|
+
console.log(` Classification: ${routingDecision.classification}`);
|
|
2992
|
+
console.log(` Procedure: ${selectedProcedure.name}`);
|
|
2993
|
+
console.log(` Reasoning: ${routingDecision.reasoning}`);
|
|
2994
|
+
}
|
|
2995
|
+
/**
|
|
2996
|
+
* Handle prompt with streaming check - centralized logic for all input types
|
|
2997
|
+
*
|
|
2998
|
+
* This method implements the unified pattern for handling prompts:
|
|
2999
|
+
* 1. Check if runner is actively streaming
|
|
3000
|
+
* 2. Route procedure if NOT streaming (resets currentSubroutine)
|
|
3001
|
+
* 3. Add to stream if streaming, OR resume session if not
|
|
3002
|
+
*
|
|
3003
|
+
* @param session The Cyrus agent session
|
|
3004
|
+
* @param repository Repository configuration
|
|
3005
|
+
* @param linearAgentActivitySessionId Linear agent activity session ID
|
|
3006
|
+
* @param agentSessionManager Agent session manager instance
|
|
3007
|
+
* @param promptBody The prompt text to send
|
|
3008
|
+
* @param attachmentManifest Optional attachment manifest to append
|
|
3009
|
+
* @param isNewSession Whether this is a new session
|
|
3010
|
+
* @param additionalAllowedDirs Additional directories to allow access to
|
|
3011
|
+
* @param logContext Context string for logging (e.g., "prompted webhook", "parent resume")
|
|
3012
|
+
* @returns true if message was added to stream, false if session was resumed
|
|
3013
|
+
*/
|
|
3014
|
+
async handlePromptWithStreamingCheck(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, additionalAllowedDirs, logContext, commentAuthor, commentTimestamp) {
|
|
3015
|
+
// Check if runner is actively streaming before routing
|
|
3016
|
+
const existingRunner = session.claudeRunner;
|
|
3017
|
+
const isStreaming = existingRunner?.isStreaming() || false;
|
|
3018
|
+
// Always route procedure for new input, UNLESS actively streaming
|
|
3019
|
+
if (!isStreaming) {
|
|
3020
|
+
await this.rerouteProcedureForSession(session, linearAgentActivitySessionId, agentSessionManager, promptBody);
|
|
3021
|
+
console.log(`[EdgeWorker] Routed procedure for ${logContext}`);
|
|
3022
|
+
}
|
|
3023
|
+
else {
|
|
3024
|
+
console.log(`[EdgeWorker] Skipping routing for ${linearAgentActivitySessionId} (${logContext}) - runner is actively streaming`);
|
|
3025
|
+
}
|
|
3026
|
+
// Handle streaming case - add message to existing stream
|
|
3027
|
+
if (existingRunner?.isStreaming()) {
|
|
3028
|
+
console.log(`[EdgeWorker] Adding prompt to existing stream for ${linearAgentActivitySessionId} (${logContext})`);
|
|
3029
|
+
// Append attachment manifest to the prompt if we have one
|
|
3030
|
+
let fullPrompt = promptBody;
|
|
3031
|
+
if (attachmentManifest) {
|
|
3032
|
+
fullPrompt = `${promptBody}\n\n${attachmentManifest}`;
|
|
3033
|
+
}
|
|
3034
|
+
existingRunner.addStreamMessage(fullPrompt);
|
|
3035
|
+
return true; // Message added to stream
|
|
3036
|
+
}
|
|
3037
|
+
// Not streaming - resume/start session
|
|
3038
|
+
console.log(`[EdgeWorker] Resuming Claude session for ${linearAgentActivitySessionId} (${logContext})`);
|
|
3039
|
+
await this.resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, additionalAllowedDirs, undefined, // maxTurns
|
|
3040
|
+
commentAuthor, commentTimestamp);
|
|
3041
|
+
return false; // Session was resumed
|
|
3042
|
+
}
|
|
2882
3043
|
/**
|
|
2883
3044
|
* Post thought about system prompt selection based on labels
|
|
2884
3045
|
*/
|
|
@@ -2975,7 +3136,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
2975
3136
|
* @param attachmentManifest Optional attachment manifest
|
|
2976
3137
|
* @param isNewSession Whether this is a new session
|
|
2977
3138
|
*/
|
|
2978
|
-
async resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest = "", isNewSession = false, additionalAllowedDirectories = [], maxTurns) {
|
|
3139
|
+
async resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest = "", isNewSession = false, additionalAllowedDirectories = [], maxTurns, commentAuthor, commentTimestamp) {
|
|
2979
3140
|
// Check for existing runner
|
|
2980
3141
|
const existingRunner = session.claudeRunner;
|
|
2981
3142
|
// If there's an existing streaming runner, add to it
|
|
@@ -3027,7 +3188,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
3027
3188
|
// Save state
|
|
3028
3189
|
await this.savePersistedState();
|
|
3029
3190
|
// Prepare the full prompt
|
|
3030
|
-
const fullPrompt = await this.buildSessionPrompt(isNewSession, fullIssue, repository, promptBody, attachmentManifest);
|
|
3191
|
+
const fullPrompt = await this.buildSessionPrompt(isNewSession, session, fullIssue, repository, promptBody, attachmentManifest, commentAuthor, commentTimestamp);
|
|
3031
3192
|
// Start streaming session
|
|
3032
3193
|
try {
|
|
3033
3194
|
await runner.startStreaming(fullPrompt);
|