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.
Files changed (101) hide show
  1. package/cyrus-skills-plugin/.claude-plugin/plugin.json +4 -0
  2. package/dist/AgentSessionManager.d.ts +4 -58
  3. package/dist/AgentSessionManager.d.ts.map +1 -1
  4. package/dist/AgentSessionManager.js +11 -304
  5. package/dist/AgentSessionManager.js.map +1 -1
  6. package/dist/ChatSessionHandler.d.ts +2 -2
  7. package/dist/ChatSessionHandler.d.ts.map +1 -1
  8. package/dist/ChatSessionHandler.js +2 -4
  9. package/dist/ChatSessionHandler.js.map +1 -1
  10. package/dist/DefaultSkillsDeployer.d.ts +32 -0
  11. package/dist/DefaultSkillsDeployer.d.ts.map +1 -0
  12. package/dist/DefaultSkillsDeployer.js +82 -0
  13. package/dist/DefaultSkillsDeployer.js.map +1 -0
  14. package/dist/EdgeWorker.d.ts +20 -46
  15. package/dist/EdgeWorker.d.ts.map +1 -1
  16. package/dist/EdgeWorker.js +98 -450
  17. package/dist/EdgeWorker.js.map +1 -1
  18. package/dist/PromptBuilder.d.ts +1 -7
  19. package/dist/PromptBuilder.d.ts.map +1 -1
  20. package/dist/PromptBuilder.js +2 -33
  21. package/dist/PromptBuilder.js.map +1 -1
  22. package/dist/RunnerConfigBuilder.d.ts +13 -6
  23. package/dist/RunnerConfigBuilder.d.ts.map +1 -1
  24. package/dist/RunnerConfigBuilder.js +50 -27
  25. package/dist/RunnerConfigBuilder.js.map +1 -1
  26. package/dist/SkillsPluginResolver.d.ts +66 -0
  27. package/dist/SkillsPluginResolver.d.ts.map +1 -0
  28. package/dist/SkillsPluginResolver.js +180 -0
  29. package/dist/SkillsPluginResolver.js.map +1 -0
  30. package/dist/ToolPermissionResolver.d.ts +1 -12
  31. package/dist/ToolPermissionResolver.d.ts.map +1 -1
  32. package/dist/ToolPermissionResolver.js +0 -23
  33. package/dist/ToolPermissionResolver.js.map +1 -1
  34. package/dist/cyrus-skills-plugin/.claude-plugin/plugin.json +4 -0
  35. package/dist/cyrus-skills-plugin/cyrus-skills-plugin/.claude-plugin/plugin.json +4 -0
  36. package/dist/cyrus-skills-plugin/cyrus-skills-plugin/skills/debug/SKILL.md +29 -0
  37. package/dist/cyrus-skills-plugin/cyrus-skills-plugin/skills/implementation/SKILL.md +17 -0
  38. package/dist/cyrus-skills-plugin/cyrus-skills-plugin/skills/investigate/SKILL.md +23 -0
  39. package/dist/cyrus-skills-plugin/cyrus-skills-plugin/skills/summarize/SKILL.md +47 -0
  40. package/dist/cyrus-skills-plugin/cyrus-skills-plugin/skills/verify-and-ship/SKILL.md +74 -0
  41. package/dist/cyrus-skills-plugin/skills/debug/SKILL.md +29 -0
  42. package/dist/cyrus-skills-plugin/skills/implementation/SKILL.md +17 -0
  43. package/dist/cyrus-skills-plugin/skills/investigate/SKILL.md +23 -0
  44. package/dist/cyrus-skills-plugin/skills/summarize/SKILL.md +47 -0
  45. package/dist/cyrus-skills-plugin/skills/verify-and-ship/SKILL.md +74 -0
  46. package/dist/index.d.ts +2 -1
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +2 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/prompt-assembly/types.d.ts +1 -3
  51. package/dist/prompt-assembly/types.d.ts.map +1 -1
  52. package/package.json +17 -16
  53. package/dist/procedures/ProcedureAnalyzer.d.ts +0 -69
  54. package/dist/procedures/ProcedureAnalyzer.d.ts.map +0 -1
  55. package/dist/procedures/ProcedureAnalyzer.js +0 -274
  56. package/dist/procedures/ProcedureAnalyzer.js.map +0 -1
  57. package/dist/procedures/index.d.ts +0 -7
  58. package/dist/procedures/index.d.ts.map +0 -1
  59. package/dist/procedures/index.js +0 -7
  60. package/dist/procedures/index.js.map +0 -1
  61. package/dist/procedures/registry.d.ts +0 -173
  62. package/dist/procedures/registry.d.ts.map +0 -1
  63. package/dist/procedures/registry.js +0 -264
  64. package/dist/procedures/registry.js.map +0 -1
  65. package/dist/procedures/types.d.ts +0 -101
  66. package/dist/procedures/types.d.ts.map +0 -1
  67. package/dist/procedures/types.js +0 -5
  68. package/dist/procedures/types.js.map +0 -1
  69. package/dist/prompts/subroutines/changelog-update-gitlab.md +0 -79
  70. package/dist/prompts/subroutines/changelog-update.md +0 -79
  71. package/dist/prompts/subroutines/coding-activity.md +0 -12
  72. package/dist/prompts/subroutines/concise-summary.md +0 -67
  73. package/dist/prompts/subroutines/debugger-fix.md +0 -92
  74. package/dist/prompts/subroutines/debugger-reproduction.md +0 -74
  75. package/dist/prompts/subroutines/get-approval.md +0 -175
  76. package/dist/prompts/subroutines/gh-pr.md +0 -110
  77. package/dist/prompts/subroutines/git-commit.md +0 -37
  78. package/dist/prompts/subroutines/glab-mr.md +0 -106
  79. package/dist/prompts/subroutines/plan-summary.md +0 -21
  80. package/dist/prompts/subroutines/preparation.md +0 -16
  81. package/dist/prompts/subroutines/question-answer.md +0 -8
  82. package/dist/prompts/subroutines/question-investigation.md +0 -8
  83. package/dist/prompts/subroutines/release-execution.md +0 -81
  84. package/dist/prompts/subroutines/release-summary.md +0 -60
  85. package/dist/prompts/subroutines/user-testing-summary.md +0 -87
  86. package/dist/prompts/subroutines/user-testing.md +0 -48
  87. package/dist/prompts/subroutines/validation-fixer.md +0 -56
  88. package/dist/prompts/subroutines/verbose-summary.md +0 -46
  89. package/dist/prompts/subroutines/verifications.md +0 -77
  90. package/dist/validation/ValidationLoopController.d.ts +0 -54
  91. package/dist/validation/ValidationLoopController.d.ts.map +0 -1
  92. package/dist/validation/ValidationLoopController.js +0 -242
  93. package/dist/validation/ValidationLoopController.js.map +0 -1
  94. package/dist/validation/index.d.ts +0 -7
  95. package/dist/validation/index.d.ts.map +0 -1
  96. package/dist/validation/index.js +0 -7
  97. package/dist/validation/index.js.map +0 -1
  98. package/dist/validation/types.d.ts +0 -82
  99. package/dist/validation/types.d.ts.map +0 -1
  100. package/dist/validation/types.js +0 -29
  101. package/dist/validation/types.js.map +0 -1
@@ -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 MCP config from first available repo (all repos share the same MCPs today)
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
- mcpConfig,
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 = event.installationToken || process.env.GITHUB_TOKEN;
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 procedure metadata
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
- // Prefer forwarded installation token, fall back to GITHUB_TOKEN
848
- const token = event.installationToken || process.env.GITHUB_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
- // Prefer the forwarded installation token from CYHOST (1-hour expiry)
1011
- // Fall back to process.env.GITHUB_TOKEN if not provided
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 (may be running or paused between subroutines)
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
- // Initialize procedure metadata using intelligent routing
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
- // Lowercase labels for case-insensitive comparison
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
- // If subroutine has disallowAllTools: true, use empty array to disable all tools
2663
- const allowedTools = currentSubroutine?.disallowAllTools
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: Routing and streaming check happens later in handlePromptWithStreamingCheck
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. Initial subroutine prompt (if procedure initialized)
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, subroutine prompt, and user comment
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. Build issue context using appropriate builder
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. Load and append initial subroutine prompt
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, singleTurn, disallowAllTools, mcpOptions, linearWorkspaceId) {
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. Route procedure if NOT streaming (resets currentSubroutine)
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
- // Get current subroutine to check for singleTurn mode and disallowAllTools
4181
- const currentSubroutine = this.procedureAnalyzer.getCurrentSubroutine(session);
4182
- // Build allowed tools list
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