cyrus-edge-worker 0.2.39 → 0.2.41
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/cyrus-skills-plugin/.claude-plugin/plugin.json +4 -0
- package/dist/AgentSessionManager.d.ts +4 -58
- package/dist/AgentSessionManager.d.ts.map +1 -1
- package/dist/AgentSessionManager.js +11 -304
- package/dist/AgentSessionManager.js.map +1 -1
- package/dist/ChatSessionHandler.d.ts +2 -2
- package/dist/ChatSessionHandler.d.ts.map +1 -1
- package/dist/ChatSessionHandler.js +2 -4
- package/dist/ChatSessionHandler.js.map +1 -1
- package/dist/DefaultSkillsDeployer.d.ts +32 -0
- package/dist/DefaultSkillsDeployer.d.ts.map +1 -0
- package/dist/DefaultSkillsDeployer.js +82 -0
- package/dist/DefaultSkillsDeployer.js.map +1 -0
- package/dist/EdgeWorker.d.ts +20 -46
- package/dist/EdgeWorker.d.ts.map +1 -1
- package/dist/EdgeWorker.js +98 -450
- package/dist/EdgeWorker.js.map +1 -1
- package/dist/PromptBuilder.d.ts +1 -7
- package/dist/PromptBuilder.d.ts.map +1 -1
- package/dist/PromptBuilder.js +2 -33
- package/dist/PromptBuilder.js.map +1 -1
- package/dist/RunnerConfigBuilder.d.ts +13 -6
- package/dist/RunnerConfigBuilder.d.ts.map +1 -1
- package/dist/RunnerConfigBuilder.js +50 -27
- package/dist/RunnerConfigBuilder.js.map +1 -1
- package/dist/SkillsPluginResolver.d.ts +66 -0
- package/dist/SkillsPluginResolver.d.ts.map +1 -0
- package/dist/SkillsPluginResolver.js +180 -0
- package/dist/SkillsPluginResolver.js.map +1 -0
- package/dist/ToolPermissionResolver.d.ts +1 -12
- package/dist/ToolPermissionResolver.d.ts.map +1 -1
- package/dist/ToolPermissionResolver.js +0 -23
- package/dist/ToolPermissionResolver.js.map +1 -1
- package/dist/cyrus-skills-plugin/.claude-plugin/plugin.json +4 -0
- package/dist/cyrus-skills-plugin/cyrus-skills-plugin/.claude-plugin/plugin.json +4 -0
- package/dist/cyrus-skills-plugin/cyrus-skills-plugin/skills/debug/SKILL.md +29 -0
- package/dist/cyrus-skills-plugin/cyrus-skills-plugin/skills/implementation/SKILL.md +17 -0
- package/dist/cyrus-skills-plugin/cyrus-skills-plugin/skills/investigate/SKILL.md +23 -0
- package/dist/cyrus-skills-plugin/cyrus-skills-plugin/skills/summarize/SKILL.md +47 -0
- package/dist/cyrus-skills-plugin/cyrus-skills-plugin/skills/verify-and-ship/SKILL.md +74 -0
- package/dist/cyrus-skills-plugin/skills/debug/SKILL.md +29 -0
- package/dist/cyrus-skills-plugin/skills/implementation/SKILL.md +17 -0
- package/dist/cyrus-skills-plugin/skills/investigate/SKILL.md +23 -0
- package/dist/cyrus-skills-plugin/skills/summarize/SKILL.md +47 -0
- package/dist/cyrus-skills-plugin/skills/verify-and-ship/SKILL.md +74 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/prompt-assembly/types.d.ts +1 -3
- package/dist/prompt-assembly/types.d.ts.map +1 -1
- package/package.json +17 -16
- package/dist/procedures/ProcedureAnalyzer.d.ts +0 -69
- package/dist/procedures/ProcedureAnalyzer.d.ts.map +0 -1
- package/dist/procedures/ProcedureAnalyzer.js +0 -274
- package/dist/procedures/ProcedureAnalyzer.js.map +0 -1
- package/dist/procedures/index.d.ts +0 -7
- package/dist/procedures/index.d.ts.map +0 -1
- package/dist/procedures/index.js +0 -7
- package/dist/procedures/index.js.map +0 -1
- package/dist/procedures/registry.d.ts +0 -173
- package/dist/procedures/registry.d.ts.map +0 -1
- package/dist/procedures/registry.js +0 -264
- package/dist/procedures/registry.js.map +0 -1
- package/dist/procedures/types.d.ts +0 -101
- package/dist/procedures/types.d.ts.map +0 -1
- package/dist/procedures/types.js +0 -5
- package/dist/procedures/types.js.map +0 -1
- package/dist/prompts/subroutines/changelog-update-gitlab.md +0 -79
- package/dist/prompts/subroutines/changelog-update.md +0 -79
- package/dist/prompts/subroutines/coding-activity.md +0 -12
- package/dist/prompts/subroutines/concise-summary.md +0 -67
- package/dist/prompts/subroutines/debugger-fix.md +0 -92
- package/dist/prompts/subroutines/debugger-reproduction.md +0 -74
- package/dist/prompts/subroutines/get-approval.md +0 -175
- package/dist/prompts/subroutines/gh-pr.md +0 -110
- package/dist/prompts/subroutines/git-commit.md +0 -37
- package/dist/prompts/subroutines/glab-mr.md +0 -106
- package/dist/prompts/subroutines/plan-summary.md +0 -21
- package/dist/prompts/subroutines/preparation.md +0 -16
- package/dist/prompts/subroutines/question-answer.md +0 -8
- package/dist/prompts/subroutines/question-investigation.md +0 -8
- package/dist/prompts/subroutines/release-execution.md +0 -81
- package/dist/prompts/subroutines/release-summary.md +0 -60
- package/dist/prompts/subroutines/user-testing-summary.md +0 -87
- package/dist/prompts/subroutines/user-testing.md +0 -48
- package/dist/prompts/subroutines/validation-fixer.md +0 -56
- package/dist/prompts/subroutines/verbose-summary.md +0 -46
- package/dist/prompts/subroutines/verifications.md +0 -77
- package/dist/validation/ValidationLoopController.d.ts +0 -54
- package/dist/validation/ValidationLoopController.d.ts.map +0 -1
- package/dist/validation/ValidationLoopController.js +0 -242
- package/dist/validation/ValidationLoopController.js.map +0 -1
- package/dist/validation/index.d.ts +0 -7
- package/dist/validation/index.d.ts.map +0 -1
- package/dist/validation/index.js +0 -7
- package/dist/validation/index.js.map +0 -1
- package/dist/validation/types.d.ts +0 -82
- package/dist/validation/types.d.ts.map +0 -1
- package/dist/validation/types.js +0 -29
- package/dist/validation/types.js.map +0 -1
package/dist/EdgeWorker.js
CHANGED
|
@@ -9,7 +9,7 @@ import { ConfigUpdater } from "cyrus-config-updater";
|
|
|
9
9
|
import { CLIIssueTrackerService, CLIRPCServer, createLogger, DEFAULT_PROXY_URL, isAgentSessionCreatedWebhook, isAgentSessionPromptedWebhook, isContentUpdateMessage, isIssueAssignedWebhook, isIssueCommentMentionWebhook, isIssueDeletedWebhook, isIssueNewCommentWebhook, isIssueStateChangeMessage, isIssueStateChangeWebhook, isIssueTitleOrDescriptionUpdateWebhook, isIssueUnassignedWebhook, isSessionStartMessage, isStopSignalMessage, isUnassignMessage, isUserPromptMessage, PersistenceManager, requireLinearWorkspaceId, resolvePath, } from "cyrus-core";
|
|
10
10
|
import { CursorRunner } from "cyrus-cursor-runner";
|
|
11
11
|
import { GeminiRunner } from "cyrus-gemini-runner";
|
|
12
|
-
import { extractCommentAuthor, extractCommentBody, extractCommentId, extractCommentUrl, extractPRBaseBranchRef, extractPRBranchRef, extractPRNumber, extractPRTitle, extractRepoFullName, extractRepoName, extractRepoOwner, extractSessionKey, GitHubCommentService, GitHubEventTransport, isCommentOnPullRequest, isIssueCommentPayload, isPullRequestReviewCommentPayload, isPullRequestReviewPayload, stripMention, } from "cyrus-github-event-transport";
|
|
12
|
+
import { extractCommentAuthor, extractCommentBody, extractCommentId, extractCommentUrl, extractPRBaseBranchRef, extractPRBranchRef, extractPRNumber, extractPRTitle, extractRepoFullName, extractRepoName, extractRepoOwner, extractSessionKey, GitHubAppTokenProvider, GitHubCommentService, GitHubEventTransport, isCommentOnPullRequest, isIssueCommentPayload, isPullRequestReviewCommentPayload, isPullRequestReviewPayload, stripMention, } from "cyrus-github-event-transport";
|
|
13
13
|
import { extractDiscussionId, extractSessionKey as extractGitLabSessionKey, extractMRBaseBranchRef, extractMRBranchRef, extractMRIid, extractMRTitle, extractNoteAuthor, extractNoteBody, extractNoteId, extractNoteUrl, extractProjectId, extractProjectPath, GitLabCommentService, GitLabEventTransport, isNoteOnMergeRequest, stripMention as stripGitLabMention, } from "cyrus-gitlab-event-transport";
|
|
14
14
|
import { LinearEventTransport, LinearIssueTrackerService, } from "cyrus-linear-event-transport";
|
|
15
15
|
import { createCyrusToolsServer, } from "cyrus-mcp-tools";
|
|
@@ -21,15 +21,16 @@ import { AskUserQuestionHandler } from "./AskUserQuestionHandler.js";
|
|
|
21
21
|
import { AttachmentService } from "./AttachmentService.js";
|
|
22
22
|
import { ChatSessionHandler } from "./ChatSessionHandler.js";
|
|
23
23
|
import { ConfigManager } from "./ConfigManager.js";
|
|
24
|
+
import { DefaultSkillsDeployer } from "./DefaultSkillsDeployer.js";
|
|
24
25
|
import { GitService } from "./GitService.js";
|
|
25
26
|
import { GlobalSessionRegistry } from "./GlobalSessionRegistry.js";
|
|
26
27
|
import { McpConfigService } from "./McpConfigService.js";
|
|
27
28
|
import { PromptBuilder } from "./PromptBuilder.js";
|
|
28
|
-
import { applyPlatformSubroutines, ProcedureAnalyzer, } from "./procedures/index.js";
|
|
29
29
|
import { RepositoryRouter, } from "./RepositoryRouter.js";
|
|
30
30
|
import { RunnerConfigBuilder } from "./RunnerConfigBuilder.js";
|
|
31
31
|
import { RunnerSelectionService } from "./RunnerSelectionService.js";
|
|
32
32
|
import { SharedApplicationServer } from "./SharedApplicationServer.js";
|
|
33
|
+
import { SkillsPluginResolver } from "./SkillsPluginResolver.js";
|
|
33
34
|
import { SlackChatAdapter } from "./SlackChatAdapter.js";
|
|
34
35
|
import { LinearActivitySink } from "./sinks/LinearActivitySink.js";
|
|
35
36
|
import { ToolPermissionResolver } from "./ToolPermissionResolver.js";
|
|
@@ -49,6 +50,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
49
50
|
issueTrackers = new Map(); // one issue tracker per Linear workspace (keyed by linearWorkspaceId)
|
|
50
51
|
linearEventTransport = null; // Single event transport for webhook delivery
|
|
51
52
|
gitHubEventTransport = null; // GitHub event transport for forwarded GitHub webhooks
|
|
53
|
+
gitHubAppTokenProvider = null; // Self-hosted GitHub App token minting
|
|
52
54
|
gitLabEventTransport = null; // GitLab event transport for forwarded GitLab webhooks
|
|
53
55
|
slackEventTransport = null;
|
|
54
56
|
chatSessionHandler = null;
|
|
@@ -60,7 +62,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
60
62
|
sharedApplicationServer;
|
|
61
63
|
cyrusHome;
|
|
62
64
|
globalSessionRegistry; // Centralized session storage across all repositories
|
|
63
|
-
procedureAnalyzer; // Intelligent workflow routing
|
|
64
65
|
configPath; // Path to config.json file
|
|
65
66
|
/** @internal - Exposed for testing only */
|
|
66
67
|
repositoryRouter; // Repository routing and selection
|
|
@@ -80,6 +81,8 @@ export class EdgeWorker extends EventEmitter {
|
|
|
80
81
|
activityPoster;
|
|
81
82
|
configManager;
|
|
82
83
|
promptBuilder;
|
|
84
|
+
defaultSkillsDeployer;
|
|
85
|
+
skillsPluginResolver;
|
|
83
86
|
cyrusToolsMcpEndpoint = "/mcp/cyrus-tools";
|
|
84
87
|
cyrusToolsMcpRegistered = false;
|
|
85
88
|
cyrusToolsMcpRequestContext = new AsyncLocalStorage();
|
|
@@ -102,20 +105,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
102
105
|
this.gitLabCommentService = new GitLabCommentService();
|
|
103
106
|
// Initialize global session registry (centralized session storage)
|
|
104
107
|
this.globalSessionRegistry = new GlobalSessionRegistry();
|
|
105
|
-
// Initialize procedure router for fast classification
|
|
106
|
-
// Use the configured default runner (or auto-detect from API keys)
|
|
107
|
-
const simpleRunnerType = this.resolveDefaultSimpleRunnerType();
|
|
108
|
-
const simpleRunnerModel = simpleRunnerType === "claude"
|
|
109
|
-
? "haiku"
|
|
110
|
-
: simpleRunnerType === "gemini"
|
|
111
|
-
? "gemini-2.5-flash-lite"
|
|
112
|
-
: "gpt-5";
|
|
113
|
-
this.procedureAnalyzer = new ProcedureAnalyzer({
|
|
114
|
-
cyrusHome: this.cyrusHome,
|
|
115
|
-
model: simpleRunnerModel,
|
|
116
|
-
timeoutMs: 100000,
|
|
117
|
-
runnerType: simpleRunnerType,
|
|
118
|
-
});
|
|
119
108
|
// Initialize repository router with dependencies
|
|
120
109
|
const repositoryRouterDeps = {
|
|
121
110
|
fetchIssueLabels: async (issueId, linearWorkspaceId) => {
|
|
@@ -176,36 +165,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
176
165
|
return;
|
|
177
166
|
}
|
|
178
167
|
await this.handleResumeParentSession(parentSessionId, prompt, childSessionId, repo, this.agentSessionManager);
|
|
179
|
-
}, this.procedureAnalyzer, this.sharedApplicationServer);
|
|
180
|
-
// Subscribe to session events once on the single ASM
|
|
181
|
-
this.agentSessionManager.on("subroutineComplete", async ({ sessionId, session }) => {
|
|
182
|
-
const repoId = this.sessionRepositories.get(sessionId);
|
|
183
|
-
const repo = repoId ? this.repositories.get(repoId) : undefined;
|
|
184
|
-
if (!repo) {
|
|
185
|
-
this.logger.error(`No repository found for session ${sessionId} during subroutine transition`);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
await this.handleSubroutineTransition(sessionId, session, repo, this.agentSessionManager);
|
|
189
|
-
});
|
|
190
|
-
this.agentSessionManager.on("validationLoopIteration", async ({ sessionId, session, fixerPrompt, iteration, maxIterations }) => {
|
|
191
|
-
const repoId = this.sessionRepositories.get(sessionId);
|
|
192
|
-
const repo = repoId ? this.repositories.get(repoId) : undefined;
|
|
193
|
-
if (!repo) {
|
|
194
|
-
this.logger.error(`No repository found for session ${sessionId} during validation loop`);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
this.logger.info(`Validation loop iteration ${iteration}/${maxIterations}, running fixer`);
|
|
198
|
-
await this.handleValidationLoopFixer(sessionId, session, repo, this.agentSessionManager, fixerPrompt, iteration);
|
|
199
|
-
});
|
|
200
|
-
this.agentSessionManager.on("validationLoopRerun", async ({ sessionId, session, iteration }) => {
|
|
201
|
-
const repoId = this.sessionRepositories.get(sessionId);
|
|
202
|
-
const repo = repoId ? this.repositories.get(repoId) : undefined;
|
|
203
|
-
if (!repo) {
|
|
204
|
-
this.logger.error(`No repository found for session ${sessionId} during validation rerun`);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
this.logger.info(`Validation loop re-running verifications (iteration ${iteration})`);
|
|
208
|
-
await this.handleValidationLoopRerun(sessionId, session, repo, this.agentSessionManager);
|
|
209
168
|
});
|
|
210
169
|
// Initialize repositories with path resolution
|
|
211
170
|
for (const repo of config.repositories) {
|
|
@@ -280,12 +239,18 @@ export class EdgeWorker extends EventEmitter {
|
|
|
280
239
|
gitService: this.gitService,
|
|
281
240
|
config: this.config,
|
|
282
241
|
});
|
|
242
|
+
this.defaultSkillsDeployer = new DefaultSkillsDeployer(this.cyrusHome, this.logger);
|
|
243
|
+
this.skillsPluginResolver = new SkillsPluginResolver(this.cyrusHome, this.logger);
|
|
283
244
|
// Components will be initialized and registered in start() method before server starts
|
|
284
245
|
}
|
|
285
246
|
/**
|
|
286
247
|
* Start the edge worker
|
|
287
248
|
*/
|
|
288
249
|
async start() {
|
|
250
|
+
// Deploy default skills to cyrusHome if not already present (one-time setup)
|
|
251
|
+
await this.defaultSkillsDeployer.ensureDeployed();
|
|
252
|
+
// Scaffold user skills plugin manifest if needed (one-time setup)
|
|
253
|
+
await this.skillsPluginResolver.ensureUserPluginScaffolded();
|
|
289
254
|
// Load persisted state for each repository
|
|
290
255
|
await this.loadPersistedState();
|
|
291
256
|
// Start config file watcher via ConfigManager
|
|
@@ -295,28 +260,10 @@ export class EdgeWorker extends EventEmitter {
|
|
|
295
260
|
await this.addNewRepositories(changes.added);
|
|
296
261
|
// Detect and apply workspace token changes before overwriting config
|
|
297
262
|
this.updateLinearWorkspaceTokens(changes.newConfig);
|
|
298
|
-
const prevDefaultRunner = this.config.defaultRunner;
|
|
299
263
|
this.config = changes.newConfig;
|
|
300
264
|
this.configManager.setConfig(changes.newConfig);
|
|
301
265
|
this.runnerSelectionService.setConfig(changes.newConfig);
|
|
302
266
|
this.toolPermissionResolver.setConfig(changes.newConfig);
|
|
303
|
-
// Reconstruct ProcedureAnalyzer if the default runner changed,
|
|
304
|
-
// since its internal SimpleRunner is baked in at construction time.
|
|
305
|
-
if (changes.newConfig.defaultRunner !== prevDefaultRunner) {
|
|
306
|
-
const simpleRunnerType = this.resolveDefaultSimpleRunnerType();
|
|
307
|
-
const simpleRunnerModel = simpleRunnerType === "claude"
|
|
308
|
-
? "haiku"
|
|
309
|
-
: simpleRunnerType === "gemini"
|
|
310
|
-
? "gemini-2.5-flash-lite"
|
|
311
|
-
: "gpt-5";
|
|
312
|
-
this.procedureAnalyzer = new ProcedureAnalyzer({
|
|
313
|
-
cyrusHome: this.cyrusHome,
|
|
314
|
-
model: simpleRunnerModel,
|
|
315
|
-
timeoutMs: 100000,
|
|
316
|
-
runnerType: simpleRunnerType,
|
|
317
|
-
});
|
|
318
|
-
this.logger.info(`🔄 ProcedureAnalyzer reconstructed with runner type: ${simpleRunnerType}`);
|
|
319
|
-
}
|
|
320
267
|
});
|
|
321
268
|
this.configManager.startConfigWatcher();
|
|
322
269
|
// Initialize and register components BEFORE starting server (routes must be registered before listen())
|
|
@@ -479,6 +426,18 @@ export class EdgeWorker extends EventEmitter {
|
|
|
479
426
|
});
|
|
480
427
|
// Register the /github-webhook endpoint
|
|
481
428
|
this.gitHubEventTransport.register();
|
|
429
|
+
// Initialize GitHub App token provider for self-hosted users
|
|
430
|
+
const appId = process.env.GITHUB_APP_ID;
|
|
431
|
+
const installationId = process.env.GITHUB_APP_INSTALLATION_ID;
|
|
432
|
+
if (appId && installationId) {
|
|
433
|
+
const pemPath = join(this.cyrusHome, "github-app.pem");
|
|
434
|
+
this.gitHubAppTokenProvider = new GitHubAppTokenProvider({
|
|
435
|
+
appId,
|
|
436
|
+
installationId,
|
|
437
|
+
privateKeyPath: pemPath,
|
|
438
|
+
});
|
|
439
|
+
this.logger.info("GitHub App token provider initialized (self-hosted mode)");
|
|
440
|
+
}
|
|
482
441
|
this.logger.info(`GitHub event transport registered (${verificationMode} mode)`);
|
|
483
442
|
this.logger.info("Webhook endpoint: POST /github-webhook");
|
|
484
443
|
}
|
|
@@ -528,19 +487,16 @@ export class EdgeWorker extends EventEmitter {
|
|
|
528
487
|
const chatRepositoryPaths = allRepos.map((repo) => repo.repositoryPath);
|
|
529
488
|
const routingContext = this.promptBuilder.generateRoutingContextForAllWorkspaces();
|
|
530
489
|
const slackAdapter = new SlackChatAdapter(chatRepositoryPaths, this.logger, { repositoryRoutingContext: routingContext });
|
|
531
|
-
// V1: Source
|
|
490
|
+
// V1: Source workspace/repo from first available (all repos share the same MCPs today)
|
|
532
491
|
const firstLinearWorkspaceId = Object.keys(this.config.linearWorkspaces || {})[0];
|
|
533
492
|
const firstRepo = allRepos[0];
|
|
534
|
-
const mcpConfig = firstLinearWorkspaceId && firstRepo
|
|
535
|
-
? this.buildMcpConfig(firstRepo.id, firstLinearWorkspaceId)
|
|
536
|
-
: undefined;
|
|
537
493
|
if (!firstLinearWorkspaceId || !firstRepo) {
|
|
538
494
|
this.logger.warn("No repositories or workspaces configured — Slack sessions will not have access to MCP tools");
|
|
539
495
|
}
|
|
540
496
|
this.chatSessionHandler = new ChatSessionHandler(slackAdapter, {
|
|
541
497
|
cyrusHome: this.cyrusHome,
|
|
542
498
|
chatRepositoryPaths,
|
|
543
|
-
|
|
499
|
+
linearWorkspaceId: firstLinearWorkspaceId,
|
|
544
500
|
repository: firstRepo,
|
|
545
501
|
runnerConfigBuilder: this.runnerConfigBuilder,
|
|
546
502
|
createRunner: (config) => {
|
|
@@ -598,6 +554,25 @@ export class EdgeWorker extends EventEmitter {
|
|
|
598
554
|
* This creates a new session for the GitHub PR comment, checks out the PR branch
|
|
599
555
|
* via git worktree, and processes the comment as a task prompt.
|
|
600
556
|
*/
|
|
557
|
+
/**
|
|
558
|
+
* Resolve a GitHub API token from (in priority order):
|
|
559
|
+
* 1. Forwarded installation token from CYHOST (cloud/proxy mode)
|
|
560
|
+
* 2. Self-minted installation token from GitHub App credentials (self-hosted)
|
|
561
|
+
* 3. Personal access token from GITHUB_TOKEN env var (fallback)
|
|
562
|
+
*/
|
|
563
|
+
async resolveGitHubToken(event) {
|
|
564
|
+
if (event.installationToken)
|
|
565
|
+
return event.installationToken;
|
|
566
|
+
if (this.gitHubAppTokenProvider) {
|
|
567
|
+
try {
|
|
568
|
+
return await this.gitHubAppTokenProvider.getToken();
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
this.logger.warn("Failed to mint GitHub App installation token, falling back to GITHUB_TOKEN", error instanceof Error ? error : new Error(String(error)));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return process.env.GITHUB_TOKEN;
|
|
575
|
+
}
|
|
601
576
|
async handleGitHubWebhook(event) {
|
|
602
577
|
this.activeWebhookCount++;
|
|
603
578
|
try {
|
|
@@ -637,7 +612,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
637
612
|
}
|
|
638
613
|
this.logger.info(`Processing GitHub webhook: ${repoFullName}#${prNumber} by @${commentAuthor}${isPullRequestReview ? " (pull_request_review)" : ""}`);
|
|
639
614
|
// Add "eyes" reaction to acknowledge receipt (not for pull_request_review — we post a comment instead)
|
|
640
|
-
const reactionToken =
|
|
615
|
+
const reactionToken = await this.resolveGitHubToken(event);
|
|
641
616
|
if (reactionToken && !isPullRequestReview) {
|
|
642
617
|
const commentId = extractCommentId(event);
|
|
643
618
|
if (commentId) {
|
|
@@ -758,7 +733,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
758
733
|
this.logger.error(`Failed to create session for GitHub webhook ${event.deliveryId}`);
|
|
759
734
|
return;
|
|
760
735
|
}
|
|
761
|
-
// Initialize
|
|
736
|
+
// Initialize session metadata
|
|
762
737
|
if (!session.metadata) {
|
|
763
738
|
session.metadata = {};
|
|
764
739
|
}
|
|
@@ -774,12 +749,10 @@ export class EdgeWorker extends EventEmitter {
|
|
|
774
749
|
const disallowedTools = this.buildDisallowedTools(repository);
|
|
775
750
|
const allowedDirectories = [repository.repositoryPath];
|
|
776
751
|
// Create agent runner using the standard config builder
|
|
777
|
-
const { config: runnerConfig, runnerType } = this.buildAgentRunnerConfig(session, repository, githubSessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
|
|
752
|
+
const { config: runnerConfig, runnerType } = await this.buildAgentRunnerConfig(session, repository, githubSessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
|
|
778
753
|
undefined, // labels
|
|
779
754
|
undefined, // issueDescription
|
|
780
755
|
200, // maxTurns
|
|
781
|
-
false, // singleTurn
|
|
782
|
-
undefined, // disallowAllTools
|
|
783
756
|
{ excludeSlackMcp: true });
|
|
784
757
|
const runner = this.createRunnerForType(runnerType, runnerConfig);
|
|
785
758
|
// Store the runner in the session manager
|
|
@@ -844,8 +817,8 @@ export class EdgeWorker extends EventEmitter {
|
|
|
844
817
|
Accept: "application/vnd.github+json",
|
|
845
818
|
"X-GitHub-Api-Version": "2022-11-28",
|
|
846
819
|
};
|
|
847
|
-
//
|
|
848
|
-
const token =
|
|
820
|
+
// Resolve GitHub token (installation token > App token > PAT)
|
|
821
|
+
const token = await this.resolveGitHubToken(event);
|
|
849
822
|
if (token) {
|
|
850
823
|
headers.Authorization = `Bearer ${token}`;
|
|
851
824
|
}
|
|
@@ -1007,9 +980,8 @@ ${taskSection}`;
|
|
|
1007
980
|
this.logger.warn("Cannot post GitHub reply: no PR number");
|
|
1008
981
|
return;
|
|
1009
982
|
}
|
|
1010
|
-
//
|
|
1011
|
-
|
|
1012
|
-
const token = event.installationToken || process.env.GITHUB_TOKEN;
|
|
983
|
+
// Resolve GitHub token (installation token > App token > PAT)
|
|
984
|
+
const token = await this.resolveGitHubToken(event);
|
|
1013
985
|
if (!token) {
|
|
1014
986
|
this.logger.warn("Cannot post GitHub reply: no installation token or GITHUB_TOKEN configured");
|
|
1015
987
|
this.logger.debug(`Would have posted reply to ${owner}/${repo}#${prNumber} (comment ${commentId}): ${summary}`);
|
|
@@ -1188,12 +1160,10 @@ ${taskSection}`;
|
|
|
1188
1160
|
const disallowedTools = this.buildDisallowedTools(repository);
|
|
1189
1161
|
const allowedDirectories = [repository.repositoryPath];
|
|
1190
1162
|
// Create agent runner using the standard config builder
|
|
1191
|
-
const { config: runnerConfig, runnerType } = this.buildAgentRunnerConfig(session, repository, gitlabSessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
|
|
1163
|
+
const { config: runnerConfig, runnerType } = await this.buildAgentRunnerConfig(session, repository, gitlabSessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
|
|
1192
1164
|
undefined, // labels
|
|
1193
1165
|
undefined, // issueDescription
|
|
1194
1166
|
200, // maxTurns
|
|
1195
|
-
false, // singleTurn
|
|
1196
|
-
undefined, // disallowAllTools
|
|
1197
1167
|
{ excludeSlackMcp: true });
|
|
1198
1168
|
const runner = this.createRunnerForType(runnerType, runnerConfig);
|
|
1199
1169
|
// Store the runner in the session manager
|
|
@@ -1538,96 +1508,6 @@ ${taskSection}`;
|
|
|
1538
1508
|
log.error(`Error context - Parent issue: ${parentSession.issueId}, Repository: ${parentRepo.name}`);
|
|
1539
1509
|
}
|
|
1540
1510
|
}
|
|
1541
|
-
/**
|
|
1542
|
-
* Handle subroutine transition when a subroutine completes
|
|
1543
|
-
* This is triggered by the AgentSessionManager's 'subroutineComplete' event
|
|
1544
|
-
*/
|
|
1545
|
-
async handleSubroutineTransition(sessionId, session, repo, agentSessionManager) {
|
|
1546
|
-
const log = this.logger.withContext({ sessionId });
|
|
1547
|
-
log.info(`Handling subroutine completion for session ${sessionId}`);
|
|
1548
|
-
// Get next subroutine (advancement already handled by AgentSessionManager)
|
|
1549
|
-
const nextSubroutine = this.procedureAnalyzer.getCurrentSubroutine(session);
|
|
1550
|
-
if (!nextSubroutine) {
|
|
1551
|
-
log.info(`Procedure complete for session ${sessionId}`);
|
|
1552
|
-
return;
|
|
1553
|
-
}
|
|
1554
|
-
log.info(`Next subroutine: ${nextSubroutine.name}`);
|
|
1555
|
-
// Post a visually distinct status update to Linear so the user knows what's happening next
|
|
1556
|
-
await agentSessionManager.createThoughtActivity(sessionId, `---\n**${nextSubroutine.description}...**`);
|
|
1557
|
-
// Load subroutine prompt
|
|
1558
|
-
let subroutinePrompt;
|
|
1559
|
-
try {
|
|
1560
|
-
subroutinePrompt = await this.loadSubroutinePrompt(nextSubroutine, this.getWorkspaceSlug(requireLinearWorkspaceId(repo)));
|
|
1561
|
-
if (!subroutinePrompt) {
|
|
1562
|
-
// Fallback if loadSubroutinePrompt returns null
|
|
1563
|
-
subroutinePrompt = `Continue with: ${nextSubroutine.description}`;
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
catch (error) {
|
|
1567
|
-
log.error(`Failed to load subroutine prompt:`, error);
|
|
1568
|
-
// Fallback to simple prompt
|
|
1569
|
-
subroutinePrompt = `Continue with: ${nextSubroutine.description}`;
|
|
1570
|
-
}
|
|
1571
|
-
// Resume Claude session with subroutine prompt
|
|
1572
|
-
try {
|
|
1573
|
-
await this.resumeAgentSession(session, repo, sessionId, agentSessionManager, subroutinePrompt, "", // No attachment manifest
|
|
1574
|
-
false, // Not a new session
|
|
1575
|
-
[], // No additional allowed directories
|
|
1576
|
-
undefined, // linearWorkspaceId — will fall back to repo.linearWorkspaceId
|
|
1577
|
-
nextSubroutine?.singleTurn ? 1 : undefined);
|
|
1578
|
-
log.info(`Successfully resumed session for ${nextSubroutine.name} subroutine${nextSubroutine.singleTurn ? " (singleTurn)" : ""}`);
|
|
1579
|
-
}
|
|
1580
|
-
catch (error) {
|
|
1581
|
-
log.error(`Failed to resume session for ${nextSubroutine.name} subroutine:`, error);
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
/**
|
|
1585
|
-
* Handle validation loop fixer - run the fixer prompt
|
|
1586
|
-
*/
|
|
1587
|
-
async handleValidationLoopFixer(sessionId, session, repo, agentSessionManager, fixerPrompt, iteration) {
|
|
1588
|
-
this.logger.info(`Running fixer for session ${sessionId}, iteration ${iteration}`);
|
|
1589
|
-
try {
|
|
1590
|
-
await this.resumeAgentSession(session, repo, sessionId, agentSessionManager, fixerPrompt, "", // No attachment manifest
|
|
1591
|
-
false, // Not a new session
|
|
1592
|
-
[], // No additional allowed directories
|
|
1593
|
-
undefined, // linearWorkspaceId — will fall back to repo.linearWorkspaceId
|
|
1594
|
-
undefined);
|
|
1595
|
-
this.logger.info(`Successfully started fixer for iteration ${iteration}`);
|
|
1596
|
-
}
|
|
1597
|
-
catch (error) {
|
|
1598
|
-
this.logger.error(`Failed to run fixer for iteration ${iteration}:`, error);
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
/**
|
|
1602
|
-
* Handle validation loop rerun - re-run the verifications subroutine
|
|
1603
|
-
*/
|
|
1604
|
-
async handleValidationLoopRerun(sessionId, session, repo, agentSessionManager) {
|
|
1605
|
-
this.logger.info(`Re-running verifications for session ${sessionId}`);
|
|
1606
|
-
// Get the verifications subroutine definition
|
|
1607
|
-
const verificationsSubroutine = this.procedureAnalyzer.getCurrentSubroutine(session);
|
|
1608
|
-
if (!verificationsSubroutine ||
|
|
1609
|
-
verificationsSubroutine.name !== "verifications") {
|
|
1610
|
-
this.logger.error(`Expected verifications subroutine, got: ${verificationsSubroutine?.name}`);
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
try {
|
|
1614
|
-
// Load the verifications prompt
|
|
1615
|
-
const subroutinePrompt = await this.loadSubroutinePrompt(verificationsSubroutine, this.getWorkspaceSlug(requireLinearWorkspaceId(repo)));
|
|
1616
|
-
if (!subroutinePrompt) {
|
|
1617
|
-
this.logger.error(`Failed to load verifications prompt`);
|
|
1618
|
-
return;
|
|
1619
|
-
}
|
|
1620
|
-
await this.resumeAgentSession(session, repo, sessionId, agentSessionManager, subroutinePrompt, "", // No attachment manifest
|
|
1621
|
-
false, // Not a new session
|
|
1622
|
-
[], // No additional allowed directories
|
|
1623
|
-
undefined, // linearWorkspaceId — will fall back to repo.linearWorkspaceId
|
|
1624
|
-
undefined);
|
|
1625
|
-
this.logger.info(`Successfully re-started verifications`);
|
|
1626
|
-
}
|
|
1627
|
-
catch (error) {
|
|
1628
|
-
this.logger.error(`Failed to re-run verifications:`, error);
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
1511
|
/**
|
|
1632
1512
|
* Detect workspace token changes and update all dependent services.
|
|
1633
1513
|
*
|
|
@@ -2183,7 +2063,7 @@ ${taskSection}`;
|
|
|
2183
2063
|
if ("attachments" in updatedFrom)
|
|
2184
2064
|
changedFields.push("attachments");
|
|
2185
2065
|
this.logger.info(`Handling issue content update: ${issueIdentifier} (changed: ${changedFields.join(", ")})`);
|
|
2186
|
-
// Find session(s) for this issue
|
|
2066
|
+
// Find session(s) for this issue
|
|
2187
2067
|
const sessions = this.agentSessionManager.getSessionsByIssueId(issueId);
|
|
2188
2068
|
if (sessions.length === 0) {
|
|
2189
2069
|
if (process.env.CYRUS_WEBHOOK_DEBUG === "true") {
|
|
@@ -2284,13 +2164,6 @@ ${taskSection}`;
|
|
|
2284
2164
|
}
|
|
2285
2165
|
return workspaceConfig.linearToken;
|
|
2286
2166
|
}
|
|
2287
|
-
/**
|
|
2288
|
-
* Get the Linear workspace slug for a workspace from workspace-level config.
|
|
2289
|
-
*/
|
|
2290
|
-
getWorkspaceSlug(linearWorkspaceId) {
|
|
2291
|
-
return this.config.linearWorkspaces?.[linearWorkspaceId]
|
|
2292
|
-
?.linearWorkspaceSlug;
|
|
2293
|
-
}
|
|
2294
2167
|
/**
|
|
2295
2168
|
* Create a new Cyrus agent session with all necessary setup
|
|
2296
2169
|
* @param sessionId The Linear agent activity session ID
|
|
@@ -2535,92 +2408,9 @@ ${taskSection}`;
|
|
|
2535
2408
|
const sessionData = await this.createCyrusAgentSession(sessionId, issue, repositories, agentSessionManager, linearWorkspaceId, baseBranchOverrides, routingMethod);
|
|
2536
2409
|
// Destructure the session data (excluding allowedTools which we'll build with promptType)
|
|
2537
2410
|
const { session, fullIssue, workspace: _workspace, attachmentResult, attachmentsDir: _attachmentsDir, allowedDirectories, } = sessionData;
|
|
2538
|
-
//
|
|
2539
|
-
if (!session.metadata) {
|
|
2540
|
-
session.metadata = {};
|
|
2541
|
-
}
|
|
2542
|
-
// Post ephemeral "Routing..." thought
|
|
2543
|
-
await agentSessionManager.postAnalyzingThought(sessionId);
|
|
2544
|
-
// Fetch labels early (needed for label override check)
|
|
2411
|
+
// Fetch labels early (needed for system prompt and runner selection)
|
|
2545
2412
|
const labels = await this.fetchIssueLabels(fullIssue);
|
|
2546
|
-
|
|
2547
|
-
const lowercaseLabels = labels.map((label) => label.toLowerCase());
|
|
2548
|
-
// Check for label overrides BEFORE AI routing (use primary repo for label config)
|
|
2549
|
-
const debuggerConfig = primaryRepo.labelPrompts?.debugger;
|
|
2550
|
-
const debuggerLabels = Array.isArray(debuggerConfig)
|
|
2551
|
-
? debuggerConfig
|
|
2552
|
-
: debuggerConfig?.labels;
|
|
2553
|
-
const hasDebuggerLabel = debuggerLabels?.some((label) => lowercaseLabels.includes(label.toLowerCase()));
|
|
2554
|
-
// ALWAYS check for 'orchestrator' label (case-insensitive) regardless of EdgeConfig
|
|
2555
|
-
// This is a hardcoded rule: any issue with 'orchestrator'/'Orchestrator' label
|
|
2556
|
-
// goes to orchestrator procedure
|
|
2557
|
-
const hasHardcodedOrchestratorLabel = lowercaseLabels.includes("orchestrator");
|
|
2558
|
-
// Also check any additional orchestrator labels from config
|
|
2559
|
-
const orchestratorConfig = primaryRepo.labelPrompts?.orchestrator;
|
|
2560
|
-
const orchestratorLabels = Array.isArray(orchestratorConfig)
|
|
2561
|
-
? orchestratorConfig
|
|
2562
|
-
: orchestratorConfig?.labels;
|
|
2563
|
-
const hasConfiguredOrchestratorLabel = orchestratorLabels?.some((label) => lowercaseLabels.includes(label.toLowerCase())) ?? false;
|
|
2564
|
-
const hasOrchestratorLabel = hasHardcodedOrchestratorLabel || hasConfiguredOrchestratorLabel;
|
|
2565
|
-
// Check for graphite label (for graphite-orchestrator combination)
|
|
2566
|
-
const graphiteConfig = primaryRepo.labelPrompts?.graphite;
|
|
2567
|
-
const graphiteLabels = Array.isArray(graphiteConfig)
|
|
2568
|
-
? graphiteConfig
|
|
2569
|
-
: (graphiteConfig?.labels ?? ["graphite"]);
|
|
2570
|
-
const hasGraphiteLabel = graphiteLabels?.some((label) => lowercaseLabels.includes(label.toLowerCase()));
|
|
2571
|
-
// Graphite-orchestrator requires BOTH graphite AND orchestrator labels
|
|
2572
|
-
const hasGraphiteOrchestratorLabels = hasGraphiteLabel && hasOrchestratorLabel;
|
|
2573
|
-
let finalProcedure;
|
|
2574
|
-
let finalClassification;
|
|
2575
|
-
// If labels indicate a specific procedure, use that instead of AI routing
|
|
2576
|
-
if (hasDebuggerLabel) {
|
|
2577
|
-
const debuggerProcedure = this.procedureAnalyzer.getProcedure("debugger-full");
|
|
2578
|
-
if (!debuggerProcedure) {
|
|
2579
|
-
throw new Error("debugger-full procedure not found in registry");
|
|
2580
|
-
}
|
|
2581
|
-
finalProcedure = debuggerProcedure;
|
|
2582
|
-
finalClassification = "debugger";
|
|
2583
|
-
log.info(`Using debugger-full procedure due to debugger label (skipping AI routing)`);
|
|
2584
|
-
}
|
|
2585
|
-
else if (hasGraphiteOrchestratorLabels) {
|
|
2586
|
-
// Graphite-orchestrator takes precedence over regular orchestrator when both labels present
|
|
2587
|
-
const orchestratorProcedure = this.procedureAnalyzer.getProcedure("orchestrator-full");
|
|
2588
|
-
if (!orchestratorProcedure) {
|
|
2589
|
-
throw new Error("orchestrator-full procedure not found in registry");
|
|
2590
|
-
}
|
|
2591
|
-
finalProcedure = orchestratorProcedure;
|
|
2592
|
-
// Use orchestrator classification but the system prompt will be graphite-orchestrator
|
|
2593
|
-
finalClassification = "orchestrator";
|
|
2594
|
-
log.info(`Using orchestrator-full procedure with graphite-orchestrator prompt (graphite + orchestrator labels)`);
|
|
2595
|
-
}
|
|
2596
|
-
else if (hasOrchestratorLabel) {
|
|
2597
|
-
const orchestratorProcedure = this.procedureAnalyzer.getProcedure("orchestrator-full");
|
|
2598
|
-
if (!orchestratorProcedure) {
|
|
2599
|
-
throw new Error("orchestrator-full procedure not found in registry");
|
|
2600
|
-
}
|
|
2601
|
-
finalProcedure = orchestratorProcedure;
|
|
2602
|
-
finalClassification = "orchestrator";
|
|
2603
|
-
log.info(`Using orchestrator-full procedure due to orchestrator label (skipping AI routing)`);
|
|
2604
|
-
}
|
|
2605
|
-
else {
|
|
2606
|
-
// No label override - use AI routing
|
|
2607
|
-
const issueDescription = `${issue.title}\n\n${fullIssue.description || ""}`.trim();
|
|
2608
|
-
const routingDecision = await this.procedureAnalyzer.determineRoutine(issueDescription);
|
|
2609
|
-
finalProcedure = routingDecision.procedure;
|
|
2610
|
-
finalClassification = routingDecision.classification;
|
|
2611
|
-
log.info(`AI routing: ${routingDecision.classification} → ${finalProcedure.name}`);
|
|
2612
|
-
}
|
|
2613
|
-
// Apply platform-specific subroutine substitution (e.g., gh-pr → glab-mr for GitLab repos)
|
|
2614
|
-
const hostingPlatform = primaryRepo.gitlabUrl
|
|
2615
|
-
? "gitlab"
|
|
2616
|
-
: primaryRepo.githubUrl
|
|
2617
|
-
? "github"
|
|
2618
|
-
: undefined;
|
|
2619
|
-
finalProcedure = applyPlatformSubroutines(finalProcedure, hostingPlatform);
|
|
2620
|
-
// Initialize procedure metadata in session with final decision
|
|
2621
|
-
this.procedureAnalyzer.initializeProcedureMetadata(session, finalProcedure);
|
|
2622
|
-
// Post single procedure selection result (replaces ephemeral routing thought)
|
|
2623
|
-
await agentSessionManager.postProcedureSelectionThought(sessionId, finalProcedure.name, finalClassification);
|
|
2413
|
+
log.info(`Starting agent session for issue ${fullIssue.identifier}`);
|
|
2624
2414
|
// Build and start Claude with initial prompt using full issue (streaming mode)
|
|
2625
2415
|
log.info(`Building initial prompt for issue ${fullIssue.identifier}`);
|
|
2626
2416
|
try {
|
|
@@ -2656,33 +2446,19 @@ ${taskSection}`;
|
|
|
2656
2446
|
await this.postSystemPromptSelectionThought(sessionId, labels, linearWorkspaceId, primaryRepo.id);
|
|
2657
2447
|
}
|
|
2658
2448
|
}
|
|
2659
|
-
// Get current subroutine to check for singleTurn mode and disallowAllTools
|
|
2660
|
-
const currentSubroutine = this.procedureAnalyzer.getCurrentSubroutine(session);
|
|
2661
2449
|
// Build allowed tools list with Linear MCP tools (now with prompt type context)
|
|
2662
|
-
|
|
2663
|
-
const
|
|
2664
|
-
|
|
2665
|
-
: this.buildAllowedTools(repositories, promptType);
|
|
2666
|
-
const baseDisallowedTools = this.buildDisallowedTools(repositories, promptType);
|
|
2667
|
-
// Merge subroutine-level disallowedTools if applicable
|
|
2668
|
-
const disallowedTools = this.mergeSubroutineDisallowedTools(session, baseDisallowedTools, "EdgeWorker");
|
|
2669
|
-
if (currentSubroutine?.disallowAllTools) {
|
|
2670
|
-
log.debug(`All tools disabled for ${fullIssue.identifier} (subroutine: ${currentSubroutine.name})`);
|
|
2671
|
-
}
|
|
2672
|
-
else {
|
|
2673
|
-
log.debug(`Configured allowed tools for ${fullIssue.identifier}:`, allowedTools);
|
|
2674
|
-
}
|
|
2450
|
+
const allowedTools = this.buildAllowedTools(repositories, promptType);
|
|
2451
|
+
const disallowedTools = this.buildDisallowedTools(repositories, promptType);
|
|
2452
|
+
log.debug(`Configured allowed tools for ${fullIssue.identifier}:`, allowedTools);
|
|
2675
2453
|
if (disallowedTools.length > 0) {
|
|
2676
2454
|
log.debug(`Configured disallowed tools for ${fullIssue.identifier}:`, disallowedTools);
|
|
2677
2455
|
}
|
|
2678
2456
|
// Create agent runner with system prompt from assembly
|
|
2679
2457
|
// buildAgentRunnerConfig now determines runner type from labels internally
|
|
2680
|
-
const { config: runnerConfig, runnerType } = this.buildAgentRunnerConfig(session, primaryRepo, sessionId, assembly.systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
|
|
2458
|
+
const { config: runnerConfig, runnerType } = await this.buildAgentRunnerConfig(session, primaryRepo, sessionId, assembly.systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
|
|
2681
2459
|
labels, // Pass labels for runner selection and model override
|
|
2682
2460
|
fullIssue.description || undefined, // Description tags can override label selectors
|
|
2683
2461
|
undefined, // maxTurns
|
|
2684
|
-
currentSubroutine?.singleTurn, // singleTurn flag
|
|
2685
|
-
currentSubroutine?.disallowAllTools, // disallowAllTools flag - also disables MCP tools
|
|
2686
2462
|
undefined, // mcpOptions
|
|
2687
2463
|
linearWorkspaceId);
|
|
2688
2464
|
log.debug(`Label-based runner selection for new session: ${runnerType} (session ${sessionId})`);
|
|
@@ -2885,7 +2661,7 @@ ${taskSection}`;
|
|
|
2885
2661
|
}
|
|
2886
2662
|
}
|
|
2887
2663
|
}
|
|
2888
|
-
// Note:
|
|
2664
|
+
// Note: Streaming check happens later in handlePromptWithStreamingCheck
|
|
2889
2665
|
// after attachments are processed
|
|
2890
2666
|
// Ensure session is not null after creation/retrieval
|
|
2891
2667
|
if (!session) {
|
|
@@ -3464,19 +3240,12 @@ ${taskSection}`;
|
|
|
3464
3240
|
: this.config.serverPort || this.config.webhookPort || 3456;
|
|
3465
3241
|
return `http://127.0.0.1:${port}${this.cyrusToolsMcpEndpoint}`;
|
|
3466
3242
|
}
|
|
3467
|
-
/**
|
|
3468
|
-
* Build MCP configuration — delegates to McpConfigService.
|
|
3469
|
-
*/
|
|
3470
|
-
buildMcpConfig(repoId, linearWorkspaceId, parentSessionId, options) {
|
|
3471
|
-
return this.mcpConfigService.buildMcpConfig(repoId, linearWorkspaceId, parentSessionId, options);
|
|
3472
|
-
}
|
|
3473
3243
|
/**
|
|
3474
3244
|
* Build the complete prompt for a session - shows full prompt assembly in one place
|
|
3475
3245
|
*
|
|
3476
3246
|
* New session prompt structure:
|
|
3477
3247
|
* 1. Issue context (from buildIssueContextPrompt)
|
|
3478
|
-
* 2.
|
|
3479
|
-
* 3. User comment
|
|
3248
|
+
* 2. User comment
|
|
3480
3249
|
*
|
|
3481
3250
|
* Existing session prompt structure:
|
|
3482
3251
|
* 1. User comment
|
|
@@ -3545,7 +3314,7 @@ ${taskSection}`;
|
|
|
3545
3314
|
};
|
|
3546
3315
|
}
|
|
3547
3316
|
/**
|
|
3548
|
-
* Build prompt for new session - includes issue context
|
|
3317
|
+
* Build prompt for new session - includes issue context and user comment
|
|
3549
3318
|
*/
|
|
3550
3319
|
async buildNewSessionPrompt(input) {
|
|
3551
3320
|
const components = [];
|
|
@@ -3571,25 +3340,17 @@ ${taskSection}`;
|
|
|
3571
3340
|
const sharedInstructions = await this.loadSharedInstructions();
|
|
3572
3341
|
systemPrompt = sharedInstructions;
|
|
3573
3342
|
}
|
|
3574
|
-
// 3.
|
|
3343
|
+
// 3. Append skills guidance — instruct the agent to use skills based on context
|
|
3344
|
+
systemPrompt += await this.skillsPluginResolver.buildSkillsGuidance();
|
|
3345
|
+
// 4. Append agent context — dynamic values for skills to reference
|
|
3346
|
+
systemPrompt += this.buildAgentContextBlock();
|
|
3347
|
+
// 5. Build issue context using appropriate builder
|
|
3575
3348
|
// Use label-based prompt ONLY if we have a label-based system prompt
|
|
3576
3349
|
const promptType = this.determinePromptType(input, !!labelBasedSystemPrompt);
|
|
3577
3350
|
const issueContext = await this.buildIssueContextForPromptAssembly(input.fullIssue, repositories, promptType, input.attachmentManifest, input.guidance, input.agentSession, input.resolvedBaseBranches);
|
|
3578
3351
|
parts.push(issueContext.prompt);
|
|
3579
3352
|
components.push("issue-context");
|
|
3580
|
-
// 4.
|
|
3581
|
-
const currentSubroutine = this.procedureAnalyzer.getCurrentSubroutine(input.session);
|
|
3582
|
-
let subroutineName;
|
|
3583
|
-
if (currentSubroutine) {
|
|
3584
|
-
const resolvedWsId = input.linearWorkspaceId ?? requireLinearWorkspaceId(input.repository);
|
|
3585
|
-
const subroutinePrompt = await this.loadSubroutinePrompt(currentSubroutine, this.getWorkspaceSlug(resolvedWsId));
|
|
3586
|
-
if (subroutinePrompt) {
|
|
3587
|
-
parts.push(subroutinePrompt);
|
|
3588
|
-
components.push("subroutine-prompt");
|
|
3589
|
-
subroutineName = currentSubroutine.name;
|
|
3590
|
-
}
|
|
3591
|
-
}
|
|
3592
|
-
// 5. Add user comment (if present)
|
|
3353
|
+
// 4. Add user comment (if present)
|
|
3593
3354
|
// Skip for mention-triggered prompts since the comment is already in the mention block
|
|
3594
3355
|
if (input.userComment.trim() && !input.isMentionTriggered) {
|
|
3595
3356
|
// If we have author/timestamp metadata, include it for multi-player context
|
|
@@ -3619,13 +3380,34 @@ ${input.userComment}
|
|
|
3619
3380
|
userPrompt: parts.join("\n\n"),
|
|
3620
3381
|
metadata: {
|
|
3621
3382
|
components,
|
|
3622
|
-
subroutineName,
|
|
3623
3383
|
promptType,
|
|
3624
3384
|
isNewSession: true,
|
|
3625
3385
|
isStreaming: false,
|
|
3626
3386
|
},
|
|
3627
3387
|
};
|
|
3628
3388
|
}
|
|
3389
|
+
/**
|
|
3390
|
+
* Build an <agent_context> block with dynamic values that skills can reference.
|
|
3391
|
+
*
|
|
3392
|
+
* Provides bot usernames so skills (e.g. verify-and-ship) can refer to the
|
|
3393
|
+
* correct bot account without hardcoding.
|
|
3394
|
+
*/
|
|
3395
|
+
buildAgentContextBlock() {
|
|
3396
|
+
const githubBot = process.env.GITHUB_BOT_USERNAME || "";
|
|
3397
|
+
const gitlabBot = process.env.GITLAB_BOT_USERNAME || "";
|
|
3398
|
+
if (!githubBot && !gitlabBot) {
|
|
3399
|
+
return "";
|
|
3400
|
+
}
|
|
3401
|
+
const lines = ["\n\n<agent_context>"];
|
|
3402
|
+
if (githubBot) {
|
|
3403
|
+
lines.push(` <github_bot_username>${githubBot}</github_bot_username>`);
|
|
3404
|
+
}
|
|
3405
|
+
if (gitlabBot) {
|
|
3406
|
+
lines.push(` <gitlab_bot_username>${gitlabBot}</gitlab_bot_username>`);
|
|
3407
|
+
}
|
|
3408
|
+
lines.push("</agent_context>");
|
|
3409
|
+
return lines.join("\n");
|
|
3410
|
+
}
|
|
3629
3411
|
/**
|
|
3630
3412
|
* Build prompt for existing session continuation - user comment and attachments only
|
|
3631
3413
|
*/
|
|
@@ -3674,13 +3456,6 @@ ${input.userComment}
|
|
|
3674
3456
|
}
|
|
3675
3457
|
return "fallback";
|
|
3676
3458
|
}
|
|
3677
|
-
/**
|
|
3678
|
-
* Load a subroutine prompt file
|
|
3679
|
-
* Extracted helper to make prompt assembly more readable
|
|
3680
|
-
*/
|
|
3681
|
-
async loadSubroutinePrompt(subroutine, workspaceSlug) {
|
|
3682
|
-
return this.promptBuilder.loadSubroutinePrompt(subroutine, workspaceSlug);
|
|
3683
|
-
}
|
|
3684
3459
|
/**
|
|
3685
3460
|
* Load shared instructions that get appended to all system prompts
|
|
3686
3461
|
*/
|
|
@@ -3711,35 +3486,12 @@ ${input.userComment}
|
|
|
3711
3486
|
* Uses config.defaultRunner if set, otherwise auto-detects from API keys,
|
|
3712
3487
|
* falling back to "claude".
|
|
3713
3488
|
*/
|
|
3714
|
-
resolveDefaultSimpleRunnerType() {
|
|
3715
|
-
if (this.config.defaultRunner) {
|
|
3716
|
-
this.logger.info(`🏃 SimpleRunner type resolved from config.defaultRunner: ${this.config.defaultRunner}`);
|
|
3717
|
-
return this.config.defaultRunner;
|
|
3718
|
-
}
|
|
3719
|
-
// Auto-detect: if exactly one runner has API keys set, use it
|
|
3720
|
-
const available = [];
|
|
3721
|
-
if (process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY) {
|
|
3722
|
-
available.push("claude");
|
|
3723
|
-
}
|
|
3724
|
-
if (process.env.GEMINI_API_KEY) {
|
|
3725
|
-
available.push("gemini");
|
|
3726
|
-
}
|
|
3727
|
-
if (process.env.OPENAI_API_KEY) {
|
|
3728
|
-
available.push("codex");
|
|
3729
|
-
}
|
|
3730
|
-
if (process.env.CURSOR_API_KEY) {
|
|
3731
|
-
available.push("cursor");
|
|
3732
|
-
}
|
|
3733
|
-
const result = available.length === 1 && available[0] ? available[0] : "claude";
|
|
3734
|
-
this.logger.info(`🏃 SimpleRunner type auto-detected: ${result} (available: ${available.join(", ") || "none"}, config.defaultRunner not set)`);
|
|
3735
|
-
return result;
|
|
3736
|
-
}
|
|
3737
3489
|
/**
|
|
3738
3490
|
* Build agent runner configuration with common settings.
|
|
3739
3491
|
* Delegates to RunnerConfigBuilder for shared config assembly.
|
|
3740
3492
|
* @returns Object containing the runner config and runner type to use
|
|
3741
3493
|
*/
|
|
3742
|
-
buildAgentRunnerConfig(session, repository, sessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels, issueDescription, maxTurns,
|
|
3494
|
+
async buildAgentRunnerConfig(session, repository, sessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels, issueDescription, maxTurns, mcpOptions, linearWorkspaceId) {
|
|
3743
3495
|
const log = this.logger.withContext({
|
|
3744
3496
|
sessionId,
|
|
3745
3497
|
platform: session.issueContext?.trackerId,
|
|
@@ -3757,12 +3509,11 @@ ${input.userComment}
|
|
|
3757
3509
|
labels,
|
|
3758
3510
|
issueDescription,
|
|
3759
3511
|
maxTurns,
|
|
3760
|
-
singleTurn,
|
|
3761
|
-
disallowAllTools,
|
|
3762
3512
|
mcpOptions,
|
|
3763
3513
|
linearWorkspaceId,
|
|
3764
3514
|
cyrusHome: this.cyrusHome,
|
|
3765
3515
|
logger: log,
|
|
3516
|
+
plugins: await this.skillsPluginResolver.resolve(),
|
|
3766
3517
|
onMessage: (message) => {
|
|
3767
3518
|
this.handleClaudeMessage(sessionId, message, repository.id);
|
|
3768
3519
|
},
|
|
@@ -3793,16 +3544,6 @@ ${input.userComment}
|
|
|
3793
3544
|
buildDisallowedTools(repositories, promptType) {
|
|
3794
3545
|
return this.toolPermissionResolver.buildDisallowedTools(repositories, promptType);
|
|
3795
3546
|
}
|
|
3796
|
-
/**
|
|
3797
|
-
* Merge subroutine-level disallowedTools with base disallowedTools
|
|
3798
|
-
* @param session Current agent session
|
|
3799
|
-
* @param baseDisallowedTools Base disallowed tools from repository/global config
|
|
3800
|
-
* @param logContext Context string for logging (e.g., "EdgeWorker", "resumeClaudeSession")
|
|
3801
|
-
* @returns Merged disallowed tools list
|
|
3802
|
-
*/
|
|
3803
|
-
mergeSubroutineDisallowedTools(session, baseDisallowedTools, logContext) {
|
|
3804
|
-
return this.toolPermissionResolver.mergeSubroutineDisallowedTools(session, baseDisallowedTools, logContext, this.procedureAnalyzer);
|
|
3805
|
-
}
|
|
3806
3547
|
/**
|
|
3807
3548
|
* Build allowed tools list with Linear MCP tools automatically included.
|
|
3808
3549
|
* Accepts single or multiple repositories (union for multi-repo).
|
|
@@ -3991,83 +3732,12 @@ ${input.userComment}
|
|
|
3991
3732
|
async postRoutingActivity(sessionId, linearWorkspaceId, repoLines, routingMethod) {
|
|
3992
3733
|
return this.activityPoster.postRoutingActivity(sessionId, linearWorkspaceId, repoLines, routingMethod);
|
|
3993
3734
|
}
|
|
3994
|
-
/**
|
|
3995
|
-
* Re-route procedure for a session (used when resuming from child or give feedback)
|
|
3996
|
-
* This ensures the currentSubroutine is reset to avoid suppression issues
|
|
3997
|
-
*/
|
|
3998
|
-
async rerouteProcedureForSession(session, sessionId, agentSessionManager, promptBody, repository, linearWorkspaceId) {
|
|
3999
|
-
// Initialize procedure metadata using intelligent routing
|
|
4000
|
-
if (!session.metadata) {
|
|
4001
|
-
session.metadata = {};
|
|
4002
|
-
}
|
|
4003
|
-
// Post ephemeral "Routing..." thought
|
|
4004
|
-
await agentSessionManager.postAnalyzingThought(sessionId);
|
|
4005
|
-
// Fetch full issue and labels to check for Orchestrator label override
|
|
4006
|
-
const issueTracker = this.issueTrackers.get(linearWorkspaceId);
|
|
4007
|
-
let hasOrchestratorLabel = false;
|
|
4008
|
-
// Get issueId from issueContext (preferred) or deprecated issueId field
|
|
4009
|
-
const issueId = session.issueContext?.issueId ?? session.issueId;
|
|
4010
|
-
if (issueTracker && issueId) {
|
|
4011
|
-
try {
|
|
4012
|
-
const fullIssue = await issueTracker.fetchIssue(issueId);
|
|
4013
|
-
const labels = await this.fetchIssueLabels(fullIssue);
|
|
4014
|
-
// ALWAYS check for 'orchestrator' label (case-insensitive) regardless of EdgeConfig
|
|
4015
|
-
// This is a hardcoded rule: any issue with 'orchestrator'/'Orchestrator' label
|
|
4016
|
-
// goes to orchestrator procedure
|
|
4017
|
-
const lowercaseLabels = labels.map((label) => label.toLowerCase());
|
|
4018
|
-
const hasHardcodedOrchestratorLabel = lowercaseLabels.includes("orchestrator");
|
|
4019
|
-
// Also check any additional orchestrator labels from config
|
|
4020
|
-
const orchestratorConfig = repository.labelPrompts?.orchestrator;
|
|
4021
|
-
const orchestratorLabels = Array.isArray(orchestratorConfig)
|
|
4022
|
-
? orchestratorConfig
|
|
4023
|
-
: orchestratorConfig?.labels;
|
|
4024
|
-
const hasConfiguredOrchestratorLabel = orchestratorLabels?.some((label) => lowercaseLabels.includes(label.toLowerCase())) ?? false;
|
|
4025
|
-
hasOrchestratorLabel =
|
|
4026
|
-
hasHardcodedOrchestratorLabel || hasConfiguredOrchestratorLabel;
|
|
4027
|
-
}
|
|
4028
|
-
catch (error) {
|
|
4029
|
-
this.logger.error(`Failed to fetch issue labels for routing:`, error);
|
|
4030
|
-
// Continue with AI routing if label fetch fails
|
|
4031
|
-
}
|
|
4032
|
-
}
|
|
4033
|
-
let selectedProcedure;
|
|
4034
|
-
let finalClassification;
|
|
4035
|
-
// If Orchestrator label is present, ALWAYS use orchestrator-full procedure
|
|
4036
|
-
if (hasOrchestratorLabel) {
|
|
4037
|
-
const orchestratorProcedure = this.procedureAnalyzer.getProcedure("orchestrator-full");
|
|
4038
|
-
if (!orchestratorProcedure) {
|
|
4039
|
-
throw new Error("orchestrator-full procedure not found in registry");
|
|
4040
|
-
}
|
|
4041
|
-
selectedProcedure = orchestratorProcedure;
|
|
4042
|
-
finalClassification = "orchestrator";
|
|
4043
|
-
this.logger.info(`Using orchestrator-full procedure due to Orchestrator label (skipping AI routing)`);
|
|
4044
|
-
}
|
|
4045
|
-
else {
|
|
4046
|
-
// No Orchestrator label - use AI routing based on prompt content
|
|
4047
|
-
const routingDecision = await this.procedureAnalyzer.determineRoutine(promptBody.trim());
|
|
4048
|
-
selectedProcedure = routingDecision.procedure;
|
|
4049
|
-
finalClassification = routingDecision.classification;
|
|
4050
|
-
this.logger.info(`AI routing: ${routingDecision.classification} → ${selectedProcedure.name}`);
|
|
4051
|
-
}
|
|
4052
|
-
// Apply platform-specific subroutine substitution (e.g., gh-pr → glab-mr for GitLab repos)
|
|
4053
|
-
const hostingPlatform = repository.gitlabUrl
|
|
4054
|
-
? "gitlab"
|
|
4055
|
-
: repository.githubUrl
|
|
4056
|
-
? "github"
|
|
4057
|
-
: undefined;
|
|
4058
|
-
selectedProcedure = applyPlatformSubroutines(selectedProcedure, hostingPlatform);
|
|
4059
|
-
// Initialize procedure metadata in session (resets currentSubroutine)
|
|
4060
|
-
this.procedureAnalyzer.initializeProcedureMetadata(session, selectedProcedure);
|
|
4061
|
-
// Post procedure selection result (replaces ephemeral routing thought)
|
|
4062
|
-
await agentSessionManager.postProcedureSelectionThought(sessionId, selectedProcedure.name, finalClassification);
|
|
4063
|
-
}
|
|
4064
3735
|
/**
|
|
4065
3736
|
* Handle prompt with streaming check - centralized logic for all input types
|
|
4066
3737
|
*
|
|
4067
3738
|
* This method implements the unified pattern for handling prompts:
|
|
4068
3739
|
* 1. Check if runner is actively streaming
|
|
4069
|
-
* 2.
|
|
4070
|
-
* 3. Add to stream if streaming, OR resume session if not
|
|
3740
|
+
* 2. Add to stream if streaming, OR resume session if not
|
|
4071
3741
|
*
|
|
4072
3742
|
* @param session The Cyrus agent session
|
|
4073
3743
|
* @param repository Repository configuration
|
|
@@ -4082,17 +3752,7 @@ ${input.userComment}
|
|
|
4082
3752
|
*/
|
|
4083
3753
|
async handlePromptWithStreamingCheck(session, repository, sessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, additionalAllowedDirs, logContext, linearWorkspaceId, commentAuthor, commentTimestamp) {
|
|
4084
3754
|
const log = this.logger.withContext({ sessionId });
|
|
4085
|
-
// Check if runner is actively running before routing
|
|
4086
3755
|
const existingRunner = session.agentRunner;
|
|
4087
|
-
const isRunning = existingRunner?.isRunning() || false;
|
|
4088
|
-
// Always route procedure for new input, UNLESS actively running
|
|
4089
|
-
if (!isRunning) {
|
|
4090
|
-
await this.rerouteProcedureForSession(session, sessionId, agentSessionManager, promptBody, repository, linearWorkspaceId);
|
|
4091
|
-
log.debug(`Routed procedure for ${logContext}`);
|
|
4092
|
-
}
|
|
4093
|
-
else {
|
|
4094
|
-
log.debug(`Skipping routing for ${sessionId} (${logContext}) - runner is actively running`);
|
|
4095
|
-
}
|
|
4096
3756
|
// Handle running case - add message to existing stream (if supported)
|
|
4097
3757
|
if (existingRunner?.isRunning() &&
|
|
4098
3758
|
existingRunner.supportsStreamingInput &&
|
|
@@ -4177,19 +3837,9 @@ ${input.userComment}
|
|
|
4177
3837
|
const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
|
|
4178
3838
|
const systemPrompt = systemPromptResult?.prompt;
|
|
4179
3839
|
const promptType = systemPromptResult?.type;
|
|
4180
|
-
//
|
|
4181
|
-
const
|
|
4182
|
-
|
|
4183
|
-
// If subroutine has disallowAllTools: true, use empty array to disable all tools
|
|
4184
|
-
const allowedTools = currentSubroutine?.disallowAllTools
|
|
4185
|
-
? []
|
|
4186
|
-
: this.buildAllowedTools(repository, promptType);
|
|
4187
|
-
const baseDisallowedTools = this.buildDisallowedTools(repository, promptType);
|
|
4188
|
-
// Merge subroutine-level disallowedTools if applicable
|
|
4189
|
-
const disallowedTools = this.mergeSubroutineDisallowedTools(session, baseDisallowedTools, "resumeClaudeSession");
|
|
4190
|
-
if (currentSubroutine?.disallowAllTools) {
|
|
4191
|
-
log.debug(`All tools disabled for subroutine: ${currentSubroutine.name}`);
|
|
4192
|
-
}
|
|
3840
|
+
// Build allowed and disallowed tools lists
|
|
3841
|
+
const allowedTools = this.buildAllowedTools(repository, promptType);
|
|
3842
|
+
const disallowedTools = this.buildDisallowedTools(repository, promptType);
|
|
4193
3843
|
// Set up attachments directory
|
|
4194
3844
|
const workspaceFolderName = basename(session.workspace.path);
|
|
4195
3845
|
const attachmentsDir = join(this.cyrusHome, workspaceFolderName, "attachments");
|
|
@@ -4215,11 +3865,9 @@ ${input.userComment}
|
|
|
4215
3865
|
// Create runner configuration
|
|
4216
3866
|
// buildAgentRunnerConfig determines runner type from labels for new sessions
|
|
4217
3867
|
// For existing sessions, we still need labels for model override but ignore runner type
|
|
4218
|
-
const { config: runnerConfig, runnerType } = this.buildAgentRunnerConfig(session, repository, sessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels, // Always pass labels to preserve model override
|
|
3868
|
+
const { config: runnerConfig, runnerType } = await this.buildAgentRunnerConfig(session, repository, sessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels, // Always pass labels to preserve model override
|
|
4219
3869
|
fullIssue.description || undefined, // Description tags can override label selectors
|
|
4220
3870
|
maxTurns, // Pass maxTurns if specified
|
|
4221
|
-
currentSubroutine?.singleTurn, // singleTurn flag
|
|
4222
|
-
currentSubroutine?.disallowAllTools, // disallowAllTools flag - also disables MCP tools
|
|
4223
3871
|
undefined, // mcpOptions
|
|
4224
3872
|
resolvedWorkspaceId);
|
|
4225
3873
|
// Create the appropriate runner based on session state
|