cyrus-edge-worker 0.0.28 → 0.0.30
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 +9 -1
- package/dist/AgentSessionManager.d.ts.map +1 -1
- package/dist/AgentSessionManager.js +15 -1
- package/dist/AgentSessionManager.js.map +1 -1
- package/dist/EdgeWorker.d.ts +6 -6
- package/dist/EdgeWorker.d.ts.map +1 -1
- package/dist/EdgeWorker.js +242 -140
- package/dist/EdgeWorker.js.map +1 -1
- package/dist/SharedApplicationServer.d.ts +6 -1
- package/dist/SharedApplicationServer.d.ts.map +1 -1
- package/dist/SharedApplicationServer.js +52 -6
- package/dist/SharedApplicationServer.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/label-prompt-template.md +14 -1
- package/package.json +5 -4
- package/prompts/orchestrator.md +134 -37
package/dist/EdgeWorker.js
CHANGED
|
@@ -3,8 +3,9 @@ import { mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { LinearClient, } from "@linear/sdk";
|
|
6
|
-
import { ClaudeRunner, getAllTools, getCoordinatorTools, getReadOnlyTools, getSafeTools, } from "cyrus-claude-runner";
|
|
6
|
+
import { ClaudeRunner, createCyrusToolsServer, getAllTools, getCoordinatorTools, getReadOnlyTools, getSafeTools, } from "cyrus-claude-runner";
|
|
7
7
|
import { isAgentSessionCreatedWebhook, isAgentSessionPromptedWebhook, isIssueAssignedWebhook, isIssueCommentMentionWebhook, isIssueNewCommentWebhook, isIssueUnassignedWebhook, PersistenceManager, } from "cyrus-core";
|
|
8
|
+
import { LinearWebhookClient } from "cyrus-linear-webhook-client";
|
|
8
9
|
import { NdjsonClient } from "cyrus-ndjson-client";
|
|
9
10
|
import { fileTypeFromBuffer } from "file-type";
|
|
10
11
|
import { AgentSessionManager } from "./AgentSessionManager.js";
|
|
@@ -65,7 +66,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
65
66
|
const parentId = this.childToParentAgentSession.get(childSessionId);
|
|
66
67
|
console.log(`[Parent-Child Lookup] Child ${childSessionId} -> Parent ${parentId || "not found"}`);
|
|
67
68
|
return parentId;
|
|
68
|
-
}, async (parentSessionId, prompt) => {
|
|
69
|
+
}, async (parentSessionId, prompt, childSessionId) => {
|
|
69
70
|
console.log(`[Parent Session Resume] Child session completed, resuming parent session ${parentSessionId}`);
|
|
70
71
|
// Get the parent session and repository
|
|
71
72
|
// This works because by the time this callback runs, agentSessionManager is fully initialized
|
|
@@ -76,12 +77,23 @@ export class EdgeWorker extends EventEmitter {
|
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
79
|
console.log(`[Parent Session Resume] Found parent session - Issue: ${parentSession.issueId}, Workspace: ${parentSession.workspace.path}`);
|
|
80
|
+
// Get the child session to access its workspace path
|
|
81
|
+
const childSession = agentSessionManager.getSession(childSessionId);
|
|
82
|
+
const childWorkspaceDirs = [];
|
|
83
|
+
if (childSession) {
|
|
84
|
+
childWorkspaceDirs.push(childSession.workspace.path);
|
|
85
|
+
console.log(`[Parent Session Resume] Adding child workspace to parent allowed directories: ${childSession.workspace.path}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.warn(`[Parent Session Resume] Could not find child session ${childSessionId} to add workspace to parent allowed directories`);
|
|
89
|
+
}
|
|
79
90
|
await this.postParentResumeAcknowledgment(parentSessionId, repo.id);
|
|
80
91
|
// Resume the parent session with the child's result
|
|
81
92
|
console.log(`[Parent Session Resume] Resuming parent Claude session with child results`);
|
|
82
93
|
try {
|
|
83
94
|
await this.resumeClaudeSession(parentSession, repo, parentSessionId, agentSessionManager, prompt, "", // No attachment manifest for child results
|
|
84
|
-
false
|
|
95
|
+
false, // Not a new session
|
|
96
|
+
childWorkspaceDirs);
|
|
85
97
|
console.log(`[Parent Session Resume] Successfully resumed parent session ${parentSessionId} with child results`);
|
|
86
98
|
}
|
|
87
99
|
catch (error) {
|
|
@@ -107,7 +119,9 @@ export class EdgeWorker extends EventEmitter {
|
|
|
107
119
|
if (!firstRepo)
|
|
108
120
|
continue;
|
|
109
121
|
const primaryRepoId = firstRepo.id;
|
|
110
|
-
|
|
122
|
+
// Determine which client to use based on environment variable
|
|
123
|
+
const useLinearDirectWebhooks = process.env.LINEAR_DIRECT_WEBHOOKS === "true";
|
|
124
|
+
const clientConfig = {
|
|
111
125
|
proxyUrl: config.proxyUrl,
|
|
112
126
|
token: token,
|
|
113
127
|
name: repos.map((r) => r.name).join(", "), // Pass repository names
|
|
@@ -125,11 +139,20 @@ export class EdgeWorker extends EventEmitter {
|
|
|
125
139
|
onConnect: () => this.handleConnect(primaryRepoId, repos),
|
|
126
140
|
onDisconnect: (reason) => this.handleDisconnect(primaryRepoId, repos, reason),
|
|
127
141
|
onError: (error) => this.handleError(error),
|
|
128
|
-
}
|
|
129
|
-
//
|
|
130
|
-
ndjsonClient
|
|
131
|
-
|
|
132
|
-
|
|
142
|
+
};
|
|
143
|
+
// Create the appropriate client based on configuration
|
|
144
|
+
const ndjsonClient = useLinearDirectWebhooks
|
|
145
|
+
? new LinearWebhookClient({
|
|
146
|
+
...clientConfig,
|
|
147
|
+
onWebhook: (payload) => this.handleWebhook(payload, repos),
|
|
148
|
+
})
|
|
149
|
+
: new NdjsonClient(clientConfig);
|
|
150
|
+
// Set up webhook handler for NdjsonClient (LinearWebhookClient uses onWebhook in constructor)
|
|
151
|
+
if (!useLinearDirectWebhooks) {
|
|
152
|
+
ndjsonClient.on("webhook", (data) => this.handleWebhook(data, repos));
|
|
153
|
+
}
|
|
154
|
+
// Optional heartbeat logging (only for NdjsonClient)
|
|
155
|
+
if (process.env.DEBUG_EDGE === "true" && !useLinearDirectWebhooks) {
|
|
133
156
|
ndjsonClient.on("heartbeat", () => {
|
|
134
157
|
console.log(`❤️ Heartbeat received for token ending in ...${token.slice(-4)}`);
|
|
135
158
|
});
|
|
@@ -483,6 +506,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
483
506
|
console.log(`[EdgeWorker] Configured allowed directories for ${fullIssue.identifier}:`, allowedDirectories);
|
|
484
507
|
// Build allowed tools list with Linear MCP tools
|
|
485
508
|
const allowedTools = this.buildAllowedTools(repository);
|
|
509
|
+
const disallowedTools = this.buildDisallowedTools(repository);
|
|
486
510
|
return {
|
|
487
511
|
session,
|
|
488
512
|
fullIssue,
|
|
@@ -491,6 +515,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
491
515
|
attachmentsDir,
|
|
492
516
|
allowedDirectories,
|
|
493
517
|
allowedTools,
|
|
518
|
+
disallowedTools,
|
|
494
519
|
};
|
|
495
520
|
}
|
|
496
521
|
/**
|
|
@@ -508,6 +533,8 @@ export class EdgeWorker extends EventEmitter {
|
|
|
508
533
|
// HACK: This is required since the comment body is always populated, thus there is no other way to differentiate between the two trigger events
|
|
509
534
|
const AGENT_SESSION_MARKER = "This thread is for an agent session";
|
|
510
535
|
const isMentionTriggered = commentBody && !commentBody.includes(AGENT_SESSION_MARKER);
|
|
536
|
+
// Check if the comment contains the /label-based-prompt command
|
|
537
|
+
const isLabelBasedPromptRequested = commentBody?.includes("/label-based-prompt");
|
|
511
538
|
// Initialize the agent session in AgentSessionManager
|
|
512
539
|
const agentSessionManager = this.agentSessionManagers.get(repository.id);
|
|
513
540
|
if (!agentSessionManager) {
|
|
@@ -522,12 +549,12 @@ export class EdgeWorker extends EventEmitter {
|
|
|
522
549
|
const { session, fullIssue, workspace: _workspace, attachmentResult, attachmentsDir: _attachmentsDir, allowedDirectories, } = sessionData;
|
|
523
550
|
// Fetch labels (needed for both model selection and system prompt determination)
|
|
524
551
|
const labels = await this.fetchIssueLabels(fullIssue);
|
|
525
|
-
// Only determine system prompt for delegation (not mentions)
|
|
552
|
+
// Only determine system prompt for delegation (not mentions) or when /label-based-prompt is requested
|
|
526
553
|
let systemPrompt;
|
|
527
554
|
let systemPromptVersion;
|
|
528
555
|
let promptType;
|
|
529
|
-
if (!isMentionTriggered) {
|
|
530
|
-
// Determine system prompt based on labels (delegation case)
|
|
556
|
+
if (!isMentionTriggered || isLabelBasedPromptRequested) {
|
|
557
|
+
// Determine system prompt based on labels (delegation case or /label-based-prompt command)
|
|
531
558
|
const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
|
|
532
559
|
systemPrompt = systemPromptResult?.prompt;
|
|
533
560
|
systemPromptVersion = systemPromptResult?.version;
|
|
@@ -542,10 +569,13 @@ export class EdgeWorker extends EventEmitter {
|
|
|
542
569
|
}
|
|
543
570
|
// Build allowed tools list with Linear MCP tools (now with prompt type context)
|
|
544
571
|
const allowedTools = this.buildAllowedTools(repository, promptType);
|
|
572
|
+
const disallowedTools = this.buildDisallowedTools(repository, promptType);
|
|
545
573
|
console.log(`[EdgeWorker] Configured allowed tools for ${fullIssue.identifier}:`, allowedTools);
|
|
574
|
+
if (disallowedTools.length > 0) {
|
|
575
|
+
console.log(`[EdgeWorker] Configured disallowed tools for ${fullIssue.identifier}:`, disallowedTools);
|
|
576
|
+
}
|
|
546
577
|
// Create Claude runner with attachment directory access and optional system prompt
|
|
547
|
-
const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId,
|
|
548
|
-
linearAgentActivitySessionId, // Pass current session ID as parent context
|
|
578
|
+
const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
|
|
549
579
|
labels);
|
|
550
580
|
const runner = new ClaudeRunner(runnerConfig);
|
|
551
581
|
// Store runner by comment ID
|
|
@@ -559,11 +589,13 @@ export class EdgeWorker extends EventEmitter {
|
|
|
559
589
|
console.log(`[EdgeWorker] Building initial prompt for issue ${fullIssue.identifier}`);
|
|
560
590
|
try {
|
|
561
591
|
// Choose the appropriate prompt builder based on trigger type and system prompt
|
|
562
|
-
const promptResult = isMentionTriggered
|
|
563
|
-
? await this.
|
|
564
|
-
:
|
|
565
|
-
? await this.
|
|
566
|
-
:
|
|
592
|
+
const promptResult = isMentionTriggered && isLabelBasedPromptRequested
|
|
593
|
+
? await this.buildLabelBasedPrompt(fullIssue, repository, attachmentResult.manifest)
|
|
594
|
+
: isMentionTriggered
|
|
595
|
+
? await this.buildMentionPrompt(fullIssue, agentSession, attachmentResult.manifest)
|
|
596
|
+
: systemPrompt
|
|
597
|
+
? await this.buildLabelBasedPrompt(fullIssue, repository, attachmentResult.manifest)
|
|
598
|
+
: await this.buildPromptV2(fullIssue, repository, undefined, attachmentResult.manifest);
|
|
567
599
|
const { prompt, version: userPromptVersion } = promptResult;
|
|
568
600
|
// Update runner with version information
|
|
569
601
|
if (userPromptVersion || systemPromptVersion) {
|
|
@@ -572,11 +604,13 @@ export class EdgeWorker extends EventEmitter {
|
|
|
572
604
|
systemPromptVersion,
|
|
573
605
|
});
|
|
574
606
|
}
|
|
575
|
-
const promptType = isMentionTriggered
|
|
576
|
-
? "
|
|
577
|
-
:
|
|
578
|
-
? "
|
|
579
|
-
:
|
|
607
|
+
const promptType = isMentionTriggered && isLabelBasedPromptRequested
|
|
608
|
+
? "label-based-prompt-command"
|
|
609
|
+
: isMentionTriggered
|
|
610
|
+
? "mention"
|
|
611
|
+
: systemPrompt
|
|
612
|
+
? "label-based"
|
|
613
|
+
: "fallback";
|
|
580
614
|
console.log(`[EdgeWorker] Initial prompt built successfully using ${promptType} workflow, length: ${prompt.length} characters`);
|
|
581
615
|
console.log(`[EdgeWorker] Starting Claude streaming session`);
|
|
582
616
|
const sessionInfo = await runner.startStreaming(prompt);
|
|
@@ -703,7 +737,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
703
737
|
}
|
|
704
738
|
// Use the new resumeClaudeSession function
|
|
705
739
|
try {
|
|
706
|
-
await this.resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession);
|
|
740
|
+
await this.resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, []);
|
|
707
741
|
}
|
|
708
742
|
catch (error) {
|
|
709
743
|
console.error("Failed to continue conversation:", error);
|
|
@@ -845,6 +879,67 @@ export class EdgeWorker extends EventEmitter {
|
|
|
845
879
|
}
|
|
846
880
|
// Determine the base branch considering parent issues
|
|
847
881
|
const baseBranch = await this.determineBaseBranch(issue, repository);
|
|
882
|
+
// Fetch assignee information
|
|
883
|
+
let assigneeId = "";
|
|
884
|
+
let assigneeName = "";
|
|
885
|
+
try {
|
|
886
|
+
if (issue.assigneeId) {
|
|
887
|
+
assigneeId = issue.assigneeId;
|
|
888
|
+
// Fetch the full assignee object to get the name
|
|
889
|
+
const assignee = await issue.assignee;
|
|
890
|
+
if (assignee) {
|
|
891
|
+
assigneeName = assignee.displayName || assignee.name || "";
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
console.warn(`[EdgeWorker] Failed to fetch assignee details:`, error);
|
|
897
|
+
}
|
|
898
|
+
// Get LinearClient for this repository
|
|
899
|
+
const linearClient = this.linearClients.get(repository.id);
|
|
900
|
+
if (!linearClient) {
|
|
901
|
+
console.error(`No LinearClient found for repository ${repository.id}`);
|
|
902
|
+
throw new Error(`No LinearClient found for repository ${repository.id}`);
|
|
903
|
+
}
|
|
904
|
+
// Fetch workspace teams and labels
|
|
905
|
+
let workspaceTeams = "";
|
|
906
|
+
let workspaceLabels = "";
|
|
907
|
+
try {
|
|
908
|
+
console.log(`[EdgeWorker] Fetching workspace teams and labels for repository ${repository.id}`);
|
|
909
|
+
// Fetch teams
|
|
910
|
+
const teamsConnection = await linearClient.teams();
|
|
911
|
+
const teamsArray = [];
|
|
912
|
+
for (const team of teamsConnection.nodes) {
|
|
913
|
+
teamsArray.push({
|
|
914
|
+
id: team.id,
|
|
915
|
+
name: team.name,
|
|
916
|
+
key: team.key,
|
|
917
|
+
description: team.description || "",
|
|
918
|
+
color: team.color,
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
workspaceTeams = teamsArray
|
|
922
|
+
.map((team) => `- ${team.name} (${team.key}): ${team.id}${team.description ? ` - ${team.description}` : ""}`)
|
|
923
|
+
.join("\n");
|
|
924
|
+
// Fetch labels
|
|
925
|
+
const labelsConnection = await linearClient.issueLabels();
|
|
926
|
+
const labelsArray = [];
|
|
927
|
+
for (const label of labelsConnection.nodes) {
|
|
928
|
+
labelsArray.push({
|
|
929
|
+
id: label.id,
|
|
930
|
+
name: label.name,
|
|
931
|
+
description: label.description || "",
|
|
932
|
+
color: label.color,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
workspaceLabels = labelsArray
|
|
936
|
+
.map((label) => `- ${label.name}: ${label.id}${label.description ? ` - ${label.description}` : ""}`)
|
|
937
|
+
.join("\n");
|
|
938
|
+
console.log(`[EdgeWorker] Fetched ${teamsArray.length} teams and ${labelsArray.length} labels`);
|
|
939
|
+
}
|
|
940
|
+
catch (error) {
|
|
941
|
+
console.warn(`[EdgeWorker] Failed to fetch workspace teams and labels:`, error);
|
|
942
|
+
}
|
|
848
943
|
// Build the simplified prompt with only essential variables
|
|
849
944
|
let prompt = template
|
|
850
945
|
.replace(/{{repository_name}}/g, repository.name)
|
|
@@ -853,7 +948,11 @@ export class EdgeWorker extends EventEmitter {
|
|
|
853
948
|
.replace(/{{issue_identifier}}/g, issue.identifier || "")
|
|
854
949
|
.replace(/{{issue_title}}/g, issue.title || "")
|
|
855
950
|
.replace(/{{issue_description}}/g, issue.description || "No description provided")
|
|
856
|
-
.replace(/{{issue_url}}/g, issue.url || "")
|
|
951
|
+
.replace(/{{issue_url}}/g, issue.url || "")
|
|
952
|
+
.replace(/{{assignee_id}}/g, assigneeId)
|
|
953
|
+
.replace(/{{assignee_name}}/g, assigneeName)
|
|
954
|
+
.replace(/{{workspace_teams}}/g, workspaceTeams)
|
|
955
|
+
.replace(/{{workspace_labels}}/g, workspaceLabels);
|
|
857
956
|
if (attachmentManifest) {
|
|
858
957
|
console.log(`[EdgeWorker] Adding attachment manifest to label-based prompt, length: ${attachmentManifest.length} characters`);
|
|
859
958
|
prompt = `${prompt}\n\n${attachmentManifest}`;
|
|
@@ -1267,26 +1366,6 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1267
1366
|
* @param issue Full Linear issue object from Linear SDK
|
|
1268
1367
|
* @param repositoryId Repository ID for Linear client lookup
|
|
1269
1368
|
*/
|
|
1270
|
-
/**
|
|
1271
|
-
* Get the repository configuration for a given session
|
|
1272
|
-
*/
|
|
1273
|
-
getRepositoryForSession(session) {
|
|
1274
|
-
// Find the repository that matches the session's workspace path
|
|
1275
|
-
for (const repo of this.config.repositories) {
|
|
1276
|
-
if (session.workspace.path.includes(repo.workspaceBaseDir)) {
|
|
1277
|
-
return repo;
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
// Fallback: try to find by issue ID in Linear client
|
|
1281
|
-
for (const [repoId, _] of this.linearClients) {
|
|
1282
|
-
const repo = this.config.repositories.find((r) => r.id === repoId);
|
|
1283
|
-
if (repo) {
|
|
1284
|
-
// Additional checks could be added here if needed
|
|
1285
|
-
return repo;
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
return undefined;
|
|
1289
|
-
}
|
|
1290
1369
|
async moveIssueToStartedState(issue, repositoryId) {
|
|
1291
1370
|
try {
|
|
1292
1371
|
const linearClient = this.linearClients.get(repositoryId);
|
|
@@ -1387,7 +1466,8 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1387
1466
|
if (!text)
|
|
1388
1467
|
return [];
|
|
1389
1468
|
// Match URLs that start with https://uploads.linear.app
|
|
1390
|
-
|
|
1469
|
+
// Exclude brackets and parentheses to avoid capturing malformed markdown link syntax
|
|
1470
|
+
const regex = /https:\/\/uploads\.linear\.app\/[a-zA-Z0-9/_.-]+/gi;
|
|
1391
1471
|
const matches = text.match(regex) || [];
|
|
1392
1472
|
// Remove duplicates
|
|
1393
1473
|
return [...new Set(matches)];
|
|
@@ -1409,7 +1489,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1409
1489
|
let imageCount = 0;
|
|
1410
1490
|
let skippedCount = 0;
|
|
1411
1491
|
let failedCount = 0;
|
|
1412
|
-
const maxAttachments =
|
|
1492
|
+
const maxAttachments = 20;
|
|
1413
1493
|
// Ensure directory exists
|
|
1414
1494
|
await mkdir(attachmentsDir, { recursive: true });
|
|
1415
1495
|
// Extract URLs from issue description
|
|
@@ -1574,7 +1654,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1574
1654
|
let newAttachmentCount = 0;
|
|
1575
1655
|
let newImageCount = 0;
|
|
1576
1656
|
let failedCount = 0;
|
|
1577
|
-
const maxAttachments =
|
|
1657
|
+
const maxAttachments = 20;
|
|
1578
1658
|
// Extract URLs from the comment
|
|
1579
1659
|
const urls = this.extractAttachmentUrls(commentBody);
|
|
1580
1660
|
if (urls.length === 0) {
|
|
@@ -1742,9 +1822,9 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1742
1822
|
return manifest;
|
|
1743
1823
|
}
|
|
1744
1824
|
/**
|
|
1745
|
-
* Build MCP configuration with automatic Linear server injection
|
|
1825
|
+
* Build MCP configuration with automatic Linear server injection and inline cyrus tools
|
|
1746
1826
|
*/
|
|
1747
|
-
buildMcpConfig(repository) {
|
|
1827
|
+
buildMcpConfig(repository, parentSessionId) {
|
|
1748
1828
|
// Always inject the Linear MCP servers with the repository's token
|
|
1749
1829
|
const mcpConfig = {
|
|
1750
1830
|
linear: {
|
|
@@ -1755,14 +1835,58 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1755
1835
|
LINEAR_API_TOKEN: repository.linearToken,
|
|
1756
1836
|
},
|
|
1757
1837
|
},
|
|
1758
|
-
"cyrus-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1838
|
+
"cyrus-tools": createCyrusToolsServer(repository.linearToken, {
|
|
1839
|
+
parentSessionId,
|
|
1840
|
+
onSessionCreated: (childSessionId, parentId) => {
|
|
1841
|
+
console.log(`[EdgeWorker] Agent session created: ${childSessionId}, mapping to parent ${parentId}`);
|
|
1842
|
+
// Map child to parent session
|
|
1843
|
+
this.childToParentAgentSession.set(childSessionId, parentId);
|
|
1844
|
+
console.log(`[EdgeWorker] Parent-child mapping updated: ${this.childToParentAgentSession.size} mappings`);
|
|
1764
1845
|
},
|
|
1765
|
-
|
|
1846
|
+
onFeedbackDelivery: async (childSessionId, message) => {
|
|
1847
|
+
console.log(`[EdgeWorker] Processing feedback delivery to child session ${childSessionId}`);
|
|
1848
|
+
// Find the repository containing the child session
|
|
1849
|
+
// We need to search all repositories for this child session
|
|
1850
|
+
let childRepo;
|
|
1851
|
+
let childAgentSessionManager;
|
|
1852
|
+
for (const [repoId, manager] of this.agentSessionManagers) {
|
|
1853
|
+
if (manager.hasClaudeRunner(childSessionId)) {
|
|
1854
|
+
childRepo = this.repositories.get(repoId);
|
|
1855
|
+
childAgentSessionManager = manager;
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
if (!childRepo || !childAgentSessionManager) {
|
|
1860
|
+
console.error(`[EdgeWorker] Child session ${childSessionId} not found in any repository`);
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
// Get the child session
|
|
1864
|
+
const childSession = childAgentSessionManager.getSession(childSessionId);
|
|
1865
|
+
if (!childSession) {
|
|
1866
|
+
console.error(`[EdgeWorker] Child session ${childSessionId} not found`);
|
|
1867
|
+
return false;
|
|
1868
|
+
}
|
|
1869
|
+
console.log(`[EdgeWorker] Found child session - Issue: ${childSession.issueId}`);
|
|
1870
|
+
// Format the feedback as a prompt for the child session with enhanced markdown formatting
|
|
1871
|
+
const feedbackPrompt = `## Received feedback from orchestrator\n\n---\n\n${message}\n\n---`;
|
|
1872
|
+
// Resume the CHILD session with the feedback from the parent
|
|
1873
|
+
// Important: We don't await the full session completion to avoid timeouts.
|
|
1874
|
+
// The feedback is delivered immediately when the session starts, so we can
|
|
1875
|
+
// return success right away while the session continues in the background.
|
|
1876
|
+
this.resumeClaudeSession(childSession, childRepo, childSessionId, childAgentSessionManager, feedbackPrompt, "", // No attachment manifest for feedback
|
|
1877
|
+
false, // Not a new session
|
|
1878
|
+
[])
|
|
1879
|
+
.then(() => {
|
|
1880
|
+
console.log(`[EdgeWorker] Child session ${childSessionId} completed processing feedback`);
|
|
1881
|
+
})
|
|
1882
|
+
.catch((error) => {
|
|
1883
|
+
console.error(`[EdgeWorker] Failed to complete child session with feedback:`, error);
|
|
1884
|
+
});
|
|
1885
|
+
// Return success immediately after initiating the session
|
|
1886
|
+
console.log(`[EdgeWorker] Feedback delivered successfully to child session ${childSessionId}`);
|
|
1887
|
+
return true;
|
|
1888
|
+
},
|
|
1889
|
+
}),
|
|
1766
1890
|
};
|
|
1767
1891
|
return mcpConfig;
|
|
1768
1892
|
}
|
|
@@ -1808,85 +1932,20 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1808
1932
|
/**
|
|
1809
1933
|
* Build Claude runner configuration with common settings
|
|
1810
1934
|
*/
|
|
1811
|
-
buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId,
|
|
1812
|
-
//
|
|
1935
|
+
buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels) {
|
|
1936
|
+
// Configure PostToolUse hook for playwright screenshots
|
|
1813
1937
|
const hooks = {
|
|
1814
1938
|
PostToolUse: [
|
|
1815
1939
|
{
|
|
1816
|
-
matcher: "
|
|
1940
|
+
matcher: "playwright_screenshot",
|
|
1817
1941
|
hooks: [
|
|
1818
|
-
async (input, _toolUseID,
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
if (Array.isArray(toolResponse) &&
|
|
1826
|
-
toolResponse.length > 0 &&
|
|
1827
|
-
toolResponse[0].type === "text" &&
|
|
1828
|
-
toolResponse[0].text) {
|
|
1829
|
-
try {
|
|
1830
|
-
const responseData = JSON.parse(toolResponse[0].text);
|
|
1831
|
-
const childAgentSessionId = responseData.agentSessionId;
|
|
1832
|
-
// If there's a parent session, create the mapping
|
|
1833
|
-
if (parentAgentSessionId && childAgentSessionId) {
|
|
1834
|
-
console.log(`[Parent-Child Mapping] Creating: child ${childAgentSessionId} -> parent ${parentAgentSessionId}`);
|
|
1835
|
-
this.childToParentAgentSession.set(childAgentSessionId, parentAgentSessionId);
|
|
1836
|
-
console.log(`[Parent-Child Mapping] Successfully created. Total mappings: ${this.childToParentAgentSession.size}`);
|
|
1837
|
-
// Save state after adding new mapping
|
|
1838
|
-
this.savePersistedState().catch((error) => {
|
|
1839
|
-
console.error(`[Parent-Child Mapping] Failed to save state after creating mapping:`, error);
|
|
1840
|
-
});
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
catch (error) {
|
|
1844
|
-
console.error(`[Parent-Child Mapping] Failed to parse agentSessionId from tool response:`, error);
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
return { continue: true };
|
|
1849
|
-
},
|
|
1850
|
-
],
|
|
1851
|
-
},
|
|
1852
|
-
{
|
|
1853
|
-
matcher: "mcp__cyrus-mcp-tools__linear_agent_give_feedback",
|
|
1854
|
-
hooks: [
|
|
1855
|
-
async (input, _toolUseID, _options) => {
|
|
1856
|
-
// Check if this is the give_feedback tool
|
|
1857
|
-
if (input.tool_name ===
|
|
1858
|
-
"mcp__cyrus-mcp-tools__linear_agent_give_feedback") {
|
|
1859
|
-
const toolInput = input.tool_input;
|
|
1860
|
-
const childAgentSessionId = toolInput?.agentSessionId;
|
|
1861
|
-
const feedbackMessage = toolInput?.message;
|
|
1862
|
-
if (childAgentSessionId && feedbackMessage) {
|
|
1863
|
-
console.log(`[Give Feedback] Triggering child session resumption: ${childAgentSessionId}`);
|
|
1864
|
-
// Get the child session
|
|
1865
|
-
const childSession = agentSessionManager.getSession(childAgentSessionId);
|
|
1866
|
-
if (!childSession) {
|
|
1867
|
-
console.error(`[Give Feedback] Child session not found: ${childAgentSessionId}`);
|
|
1868
|
-
return { continue: true };
|
|
1869
|
-
}
|
|
1870
|
-
// Find the repository for this session
|
|
1871
|
-
const repo = this.getRepositoryForSession(childSession);
|
|
1872
|
-
if (!repo) {
|
|
1873
|
-
console.error(`[Give Feedback] Repository not found for child session: ${childAgentSessionId}`);
|
|
1874
|
-
return { continue: true };
|
|
1875
|
-
}
|
|
1876
|
-
// Prepare the prompt with the feedback message and child session ID
|
|
1877
|
-
const prompt = `Feedback from parent session regarding child agent session ${childAgentSessionId}:\n\n${feedbackMessage}`;
|
|
1878
|
-
// Resume the child session with the feedback
|
|
1879
|
-
try {
|
|
1880
|
-
await this.resumeClaudeSession(childSession, repo, childAgentSessionId, agentSessionManager, prompt, "", // No attachment manifest for feedback
|
|
1881
|
-
false);
|
|
1882
|
-
console.log(`[Give Feedback] Successfully resumed child session ${childAgentSessionId} with feedback`);
|
|
1883
|
-
}
|
|
1884
|
-
catch (error) {
|
|
1885
|
-
console.error(`[Give Feedback] Failed to resume child session ${childAgentSessionId}:`, error);
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
return { continue: true };
|
|
1942
|
+
async (input, _toolUseID, { signal: _signal }) => {
|
|
1943
|
+
const postToolUseInput = input;
|
|
1944
|
+
console.log(`Tool ${postToolUseInput.tool_name} completed with response:`, postToolUseInput.tool_response);
|
|
1945
|
+
return {
|
|
1946
|
+
continue: true,
|
|
1947
|
+
additionalContext: "Screenshot taken successfully. You should use the Read tool to view the screenshot file to analyze the visual content.",
|
|
1948
|
+
};
|
|
1890
1949
|
},
|
|
1891
1950
|
],
|
|
1892
1951
|
},
|
|
@@ -1912,7 +1971,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1912
1971
|
}
|
|
1913
1972
|
// If a model override is found, also set a reasonable fallback
|
|
1914
1973
|
if (modelOverride) {
|
|
1915
|
-
// Set fallback to the next lower tier: opus->sonnet, sonnet->haiku, haiku->
|
|
1974
|
+
// Set fallback to the next lower tier: opus->sonnet, sonnet->haiku, haiku->sonnet
|
|
1916
1975
|
if (modelOverride === "opus") {
|
|
1917
1976
|
fallbackModelOverride = "sonnet";
|
|
1918
1977
|
}
|
|
@@ -1920,18 +1979,19 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1920
1979
|
fallbackModelOverride = "haiku";
|
|
1921
1980
|
}
|
|
1922
1981
|
else {
|
|
1923
|
-
fallbackModelOverride = "
|
|
1982
|
+
fallbackModelOverride = "sonnet"; // haiku falls back to sonnet since same model retry doesn't help
|
|
1924
1983
|
}
|
|
1925
1984
|
}
|
|
1926
1985
|
}
|
|
1927
1986
|
const config = {
|
|
1928
1987
|
workingDirectory: session.workspace.path,
|
|
1929
1988
|
allowedTools,
|
|
1989
|
+
disallowedTools,
|
|
1930
1990
|
allowedDirectories,
|
|
1931
1991
|
workspaceName: session.issue?.identifier || session.issueId,
|
|
1932
1992
|
cyrusHome: this.cyrusHome,
|
|
1933
1993
|
mcpConfigPath: repository.mcpConfigPath,
|
|
1934
|
-
mcpConfig: this.buildMcpConfig(repository),
|
|
1994
|
+
mcpConfig: this.buildMcpConfig(repository, linearAgentActivitySessionId),
|
|
1935
1995
|
appendSystemPrompt: (systemPrompt || "") + LAST_MESSAGE_MARKER,
|
|
1936
1996
|
// Priority order: label override > repository config > global default
|
|
1937
1997
|
model: modelOverride || repository.model || this.config.defaultModel,
|
|
@@ -1949,6 +2009,44 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1949
2009
|
}
|
|
1950
2010
|
return config;
|
|
1951
2011
|
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Build disallowed tools list following the same hierarchy as allowed tools
|
|
2014
|
+
*/
|
|
2015
|
+
buildDisallowedTools(repository, promptType) {
|
|
2016
|
+
let disallowedTools = [];
|
|
2017
|
+
let toolSource = "";
|
|
2018
|
+
// Priority order (same as allowedTools):
|
|
2019
|
+
// 1. Repository-specific prompt type configuration
|
|
2020
|
+
if (promptType && repository.labelPrompts?.[promptType]?.disallowedTools) {
|
|
2021
|
+
disallowedTools = repository.labelPrompts[promptType].disallowedTools;
|
|
2022
|
+
toolSource = `repository label prompt (${promptType})`;
|
|
2023
|
+
}
|
|
2024
|
+
// 2. Global prompt type defaults
|
|
2025
|
+
else if (promptType &&
|
|
2026
|
+
this.config.promptDefaults?.[promptType]?.disallowedTools) {
|
|
2027
|
+
disallowedTools = this.config.promptDefaults[promptType].disallowedTools;
|
|
2028
|
+
toolSource = `global prompt defaults (${promptType})`;
|
|
2029
|
+
}
|
|
2030
|
+
// 3. Repository-level disallowed tools
|
|
2031
|
+
else if (repository.disallowedTools) {
|
|
2032
|
+
disallowedTools = repository.disallowedTools;
|
|
2033
|
+
toolSource = "repository configuration";
|
|
2034
|
+
}
|
|
2035
|
+
// 4. Global default disallowed tools
|
|
2036
|
+
else if (this.config.defaultDisallowedTools) {
|
|
2037
|
+
disallowedTools = this.config.defaultDisallowedTools;
|
|
2038
|
+
toolSource = "global defaults";
|
|
2039
|
+
}
|
|
2040
|
+
// 5. No defaults for disallowedTools (as per requirements)
|
|
2041
|
+
else {
|
|
2042
|
+
disallowedTools = [];
|
|
2043
|
+
toolSource = "none (no defaults)";
|
|
2044
|
+
}
|
|
2045
|
+
if (disallowedTools.length > 0) {
|
|
2046
|
+
console.log(`[EdgeWorker] Disallowed tools for ${repository.name}: ${disallowedTools.length} tools from ${toolSource}`);
|
|
2047
|
+
}
|
|
2048
|
+
return disallowedTools;
|
|
2049
|
+
}
|
|
1952
2050
|
/**
|
|
1953
2051
|
* Build allowed tools list with Linear MCP tools automatically included
|
|
1954
2052
|
*/
|
|
@@ -1984,7 +2082,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1984
2082
|
}
|
|
1985
2083
|
// Linear MCP tools that should always be available
|
|
1986
2084
|
// See: https://docs.anthropic.com/en/docs/claude-code/iam#tool-specific-permission-rules
|
|
1987
|
-
const linearMcpTools = ["mcp__linear", "mcp__cyrus-
|
|
2085
|
+
const linearMcpTools = ["mcp__linear", "mcp__cyrus-tools"];
|
|
1988
2086
|
// Combine and deduplicate
|
|
1989
2087
|
const allTools = [...new Set([...baseTools, ...linearMcpTools])];
|
|
1990
2088
|
console.log(`[EdgeWorker] Tool selection for ${repository.name}: ${allTools.length} tools from ${toolSource}`);
|
|
@@ -2224,7 +2322,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
2224
2322
|
* @param attachmentManifest Optional attachment manifest
|
|
2225
2323
|
* @param isNewSession Whether this is a new session
|
|
2226
2324
|
*/
|
|
2227
|
-
async resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest = "", isNewSession = false) {
|
|
2325
|
+
async resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest = "", isNewSession = false, additionalAllowedDirectories = []) {
|
|
2228
2326
|
// Check for existing runner
|
|
2229
2327
|
const existingRunner = session.claudeRunner;
|
|
2230
2328
|
// If there's an existing streaming runner, add to it
|
|
@@ -2256,13 +2354,17 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
2256
2354
|
const promptType = systemPromptResult?.type;
|
|
2257
2355
|
// Build allowed tools list
|
|
2258
2356
|
const allowedTools = this.buildAllowedTools(repository, promptType);
|
|
2357
|
+
const disallowedTools = this.buildDisallowedTools(repository, promptType);
|
|
2259
2358
|
// Set up attachments directory
|
|
2260
2359
|
const workspaceFolderName = basename(session.workspace.path);
|
|
2261
2360
|
const attachmentsDir = join(this.cyrusHome, workspaceFolderName, "attachments");
|
|
2262
2361
|
await mkdir(attachmentsDir, { recursive: true });
|
|
2263
|
-
const allowedDirectories = [
|
|
2362
|
+
const allowedDirectories = [
|
|
2363
|
+
attachmentsDir,
|
|
2364
|
+
...additionalAllowedDirectories,
|
|
2365
|
+
];
|
|
2264
2366
|
// Create runner configuration
|
|
2265
|
-
const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId,
|
|
2367
|
+
const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, needsNewClaudeSession ? undefined : session.claudeSessionId, labels);
|
|
2266
2368
|
const runner = new ClaudeRunner(runnerConfig);
|
|
2267
2369
|
// Store runner
|
|
2268
2370
|
agentSessionManager.addClaudeRunner(linearAgentActivitySessionId, runner);
|