cyrus-edge-worker 0.2.51 → 0.2.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ChatSessionHandler.d.ts +32 -1
- package/dist/ChatSessionHandler.d.ts.map +1 -1
- package/dist/ChatSessionHandler.js +30 -0
- package/dist/ChatSessionHandler.js.map +1 -1
- package/dist/ConfigManager.d.ts.map +1 -1
- package/dist/ConfigManager.js +12 -2
- package/dist/ConfigManager.js.map +1 -1
- package/dist/EdgeWorker.d.ts +82 -0
- package/dist/EdgeWorker.d.ts.map +1 -1
- package/dist/EdgeWorker.js +298 -28
- package/dist/EdgeWorker.js.map +1 -1
- package/dist/McpConfigService.d.ts +8 -4
- package/dist/McpConfigService.d.ts.map +1 -1
- package/dist/McpConfigService.js +12 -5
- package/dist/McpConfigService.js.map +1 -1
- package/dist/RunnerConfigBuilder.d.ts +61 -17
- package/dist/RunnerConfigBuilder.d.ts.map +1 -1
- package/dist/RunnerConfigBuilder.js +109 -42
- package/dist/RunnerConfigBuilder.js.map +1 -1
- package/dist/SkillsPluginResolver.d.ts +52 -5
- package/dist/SkillsPluginResolver.d.ts.map +1 -1
- package/dist/SkillsPluginResolver.js +130 -17
- package/dist/SkillsPluginResolver.js.map +1 -1
- package/dist/SlackChatAdapter.d.ts.map +1 -1
- package/dist/SlackChatAdapter.js +1 -0
- package/dist/SlackChatAdapter.js.map +1 -1
- package/dist/ToolPermissionResolver.d.ts +41 -18
- package/dist/ToolPermissionResolver.d.ts.map +1 -1
- package/dist/ToolPermissionResolver.js +77 -47
- package/dist/ToolPermissionResolver.js.map +1 -1
- package/dist/hooks/IntentToAddHook.d.ts +56 -0
- package/dist/hooks/IntentToAddHook.d.ts.map +1 -0
- package/dist/hooks/IntentToAddHook.js +150 -0
- package/dist/hooks/IntentToAddHook.js.map +1 -0
- package/dist/prompts/failureModePromptAddendum.d.ts +26 -0
- package/dist/prompts/failureModePromptAddendum.d.ts.map +1 -0
- package/dist/prompts/failureModePromptAddendum.js +67 -0
- package/dist/prompts/failureModePromptAddendum.js.map +1 -0
- package/package.json +14 -14
package/dist/EdgeWorker.js
CHANGED
|
@@ -14,7 +14,7 @@ import { GeminiRunner } from "cyrus-gemini-runner";
|
|
|
14
14
|
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";
|
|
15
15
|
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";
|
|
16
16
|
import { LinearEventTransport, LinearIssueTrackerService, } from "cyrus-linear-event-transport";
|
|
17
|
-
import { createCyrusToolsServer, } from "cyrus-mcp-tools";
|
|
17
|
+
import { createCyrusToolsServer, createFetchFailureModesClient, } from "cyrus-mcp-tools";
|
|
18
18
|
import { SlackEventTransport, } from "cyrus-slack-event-transport";
|
|
19
19
|
import { Sessions, streamableHttp } from "fastify-mcp";
|
|
20
20
|
import { ActivityPoster } from "./ActivityPoster.js";
|
|
@@ -34,7 +34,7 @@ import { RepositoryRouter, } from "./RepositoryRouter.js";
|
|
|
34
34
|
import { RunnerConfigBuilder } from "./RunnerConfigBuilder.js";
|
|
35
35
|
import { RunnerSelectionService } from "./RunnerSelectionService.js";
|
|
36
36
|
import { SharedApplicationServer } from "./SharedApplicationServer.js";
|
|
37
|
-
import { SkillsPluginResolver } from "./SkillsPluginResolver.js";
|
|
37
|
+
import { SkillsPluginResolver, } from "./SkillsPluginResolver.js";
|
|
38
38
|
import { SlackChatAdapter } from "./SlackChatAdapter.js";
|
|
39
39
|
import { LinearActivitySink } from "./sinks/LinearActivitySink.js";
|
|
40
40
|
import { ToolPermissionResolver } from "./ToolPermissionResolver.js";
|
|
@@ -158,8 +158,22 @@ export class EdgeWorker extends EventEmitter {
|
|
|
158
158
|
}
|
|
159
159
|
// Initialize GitHub comment service for posting replies to GitHub PRs
|
|
160
160
|
this.gitHubCommentService = new GitHubCommentService();
|
|
161
|
-
// Initialize GitLab comment service for posting replies to GitLab MRs
|
|
162
|
-
|
|
161
|
+
// Initialize GitLab comment service for posting replies to GitLab MRs.
|
|
162
|
+
// For Self-Managed GitLab the API base URL must be derived from the
|
|
163
|
+
// configured repos' gitlabUrl host; otherwise the service falls back to
|
|
164
|
+
// gitlab.com and 404s on every reply. Picks the first configured
|
|
165
|
+
// GitLab repo's host (single GitLab host per Cyrus instance).
|
|
166
|
+
const firstGitlabRepo = config.repositories.find((r) => r.gitlabUrl);
|
|
167
|
+
let gitlabApiBaseUrl;
|
|
168
|
+
if (firstGitlabRepo?.gitlabUrl) {
|
|
169
|
+
try {
|
|
170
|
+
gitlabApiBaseUrl = new URL(firstGitlabRepo.gitlabUrl).origin;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// malformed gitlabUrl — leave undefined and fall through to default
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
this.gitLabCommentService = new GitLabCommentService(gitlabApiBaseUrl ? { apiBaseUrl: gitlabApiBaseUrl } : undefined);
|
|
163
177
|
// Initialize global session registry (centralized session storage)
|
|
164
178
|
this.globalSessionRegistry = new GlobalSessionRegistry();
|
|
165
179
|
// Initialize repository router with dependencies
|
|
@@ -234,7 +248,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
234
248
|
this.logger.error(`No repository found for child session ${childSessionId}`);
|
|
235
249
|
return;
|
|
236
250
|
}
|
|
237
|
-
await this.handleResumeParentSession(parentSessionId, prompt, childSessionId
|
|
251
|
+
await this.handleResumeParentSession(parentSessionId, prompt, childSessionId);
|
|
238
252
|
});
|
|
239
253
|
// Initialize repositories with path resolution
|
|
240
254
|
for (const repo of config.repositories) {
|
|
@@ -632,6 +646,9 @@ export class EdgeWorker extends EventEmitter {
|
|
|
632
646
|
fallbackModel: this.getDefaultFallbackModelForRunner(runnerType),
|
|
633
647
|
});
|
|
634
648
|
},
|
|
649
|
+
// Live read so hot-reloaded config (`setConfig`) picks up new
|
|
650
|
+
// per-platform MCP paths without rebuilding the handler.
|
|
651
|
+
getPlatformMcpConfigOverrides: () => this.config.slackMcpConfigs,
|
|
635
652
|
onWebhookStart: () => {
|
|
636
653
|
this.activeWebhookCount++;
|
|
637
654
|
},
|
|
@@ -868,9 +885,11 @@ export class EdgeWorker extends EventEmitter {
|
|
|
868
885
|
const systemPrompt = isPullRequestReview
|
|
869
886
|
? this.buildGitHubChangeRequestSystemPrompt(event, branchRef, taskInstructions)
|
|
870
887
|
: this.buildGitHubSystemPrompt(event, branchRef, taskInstructions);
|
|
871
|
-
// Build allowed tools
|
|
872
|
-
//
|
|
873
|
-
|
|
888
|
+
// Build allowed tools using the GitHub platform resolver, which honors
|
|
889
|
+
// `githubAllowedTools` on the workspace config and falls back to
|
|
890
|
+
// `GITHUB_DEFAULT_ALLOWED_TOOLS` (which intentionally omits
|
|
891
|
+
// `mcp__slack` — no subtractive filtering needed).
|
|
892
|
+
const allowedTools = this.toolPermissionResolver.buildGithubAllowedTools(repository);
|
|
874
893
|
const disallowedTools = this.buildDisallowedTools(repository);
|
|
875
894
|
const allowedDirectories = [repository.repositoryPath];
|
|
876
895
|
// Create agent runner using the standard config builder
|
|
@@ -878,7 +897,9 @@ export class EdgeWorker extends EventEmitter {
|
|
|
878
897
|
undefined, // labels
|
|
879
898
|
undefined, // issueDescription
|
|
880
899
|
200, // maxTurns
|
|
881
|
-
|
|
900
|
+
undefined, // linearWorkspaceId
|
|
901
|
+
undefined, // skillContext
|
|
902
|
+
"github");
|
|
882
903
|
const runner = this.createRunnerForType(runnerType, runnerConfig);
|
|
883
904
|
// Store the runner in the session manager
|
|
884
905
|
agentSessionManager.addAgentRunner(githubSessionId, runner);
|
|
@@ -1341,9 +1362,10 @@ ${taskSection}`;
|
|
|
1341
1362
|
const systemPrompt = isMergeRequestEvent
|
|
1342
1363
|
? this.buildGitLabChangeRequestSystemPrompt(event, branchRef, taskInstructions)
|
|
1343
1364
|
: this.buildGitLabSystemPrompt(event, branchRef, taskInstructions);
|
|
1344
|
-
// Build allowed tools and
|
|
1345
|
-
//
|
|
1346
|
-
|
|
1365
|
+
// Build allowed tools using the GitHub platform resolver — GitLab and
|
|
1366
|
+
// GitHub share the same PR-targeted, single-repo intent, so they use
|
|
1367
|
+
// the same `githubAllowedTools` knob and the same `GITHUB_*` default.
|
|
1368
|
+
const allowedTools = this.toolPermissionResolver.buildGithubAllowedTools(repository);
|
|
1347
1369
|
const disallowedTools = this.buildDisallowedTools(repository);
|
|
1348
1370
|
const allowedDirectories = [repository.repositoryPath];
|
|
1349
1371
|
// Create agent runner using the standard config builder
|
|
@@ -1351,7 +1373,9 @@ ${taskSection}`;
|
|
|
1351
1373
|
undefined, // labels
|
|
1352
1374
|
undefined, // issueDescription
|
|
1353
1375
|
200, // maxTurns
|
|
1354
|
-
|
|
1376
|
+
undefined, // linearWorkspaceId
|
|
1377
|
+
undefined, // skillContext
|
|
1378
|
+
"gitlab");
|
|
1355
1379
|
const runner = this.createRunnerForType(runnerType, runnerConfig);
|
|
1356
1380
|
// Store the runner in the session manager
|
|
1357
1381
|
agentSessionManager.addAgentRunner(gitlabSessionId, runner);
|
|
@@ -1588,6 +1612,62 @@ ${taskSection}`;
|
|
|
1588
1612
|
}
|
|
1589
1613
|
return "idle";
|
|
1590
1614
|
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Test-only: dispatch a synthetic Slack webhook event through the chat
|
|
1617
|
+
* session handler. Used by the F1 test harness to exercise the Slack →
|
|
1618
|
+
* ClaudeRunner code path end-to-end without a real Slack signature.
|
|
1619
|
+
*/
|
|
1620
|
+
async dispatchChatTestEvent(event) {
|
|
1621
|
+
if (!this.chatSessionHandler) {
|
|
1622
|
+
throw new Error("chatSessionHandler not initialized");
|
|
1623
|
+
}
|
|
1624
|
+
await this.chatSessionHandler.handleEvent(event);
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Public accessor for the shared Fastify-based application server.
|
|
1628
|
+
* Used by F1 to register test-only routes alongside production webhook routes.
|
|
1629
|
+
*/
|
|
1630
|
+
getSharedApplicationServer() {
|
|
1631
|
+
return this.sharedApplicationServer;
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Test-only: list active chat threads (threadKey → sessionId).
|
|
1635
|
+
*/
|
|
1636
|
+
listChatThreads() {
|
|
1637
|
+
if (!this.chatSessionHandler)
|
|
1638
|
+
return [];
|
|
1639
|
+
return this.chatSessionHandler.listThreads();
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Test-only: fetch the last assistant text reply for a chat thread.
|
|
1643
|
+
* Returns null when the thread or runner is unknown, or no assistant
|
|
1644
|
+
* message has been produced yet.
|
|
1645
|
+
*/
|
|
1646
|
+
getChatThreadLastReply(threadKey) {
|
|
1647
|
+
if (!this.chatSessionHandler)
|
|
1648
|
+
return null;
|
|
1649
|
+
const runner = this.chatSessionHandler.getRunnerForThread(threadKey);
|
|
1650
|
+
if (!runner)
|
|
1651
|
+
return null;
|
|
1652
|
+
const messages = runner.getMessages();
|
|
1653
|
+
const lastAssistant = [...messages]
|
|
1654
|
+
.reverse()
|
|
1655
|
+
.find((m) => m.type === "assistant");
|
|
1656
|
+
let text = "";
|
|
1657
|
+
if (lastAssistant &&
|
|
1658
|
+
lastAssistant.type === "assistant" &&
|
|
1659
|
+
"message" in lastAssistant) {
|
|
1660
|
+
const msg = lastAssistant;
|
|
1661
|
+
const block = msg.message.content?.find((b) => b.type === "text" && b.text);
|
|
1662
|
+
if (block?.text)
|
|
1663
|
+
text = block.text;
|
|
1664
|
+
}
|
|
1665
|
+
return {
|
|
1666
|
+
text,
|
|
1667
|
+
isRunning: runner.isRunning(),
|
|
1668
|
+
messageCount: messages.length,
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1591
1671
|
/**
|
|
1592
1672
|
* Stop the edge worker
|
|
1593
1673
|
*/
|
|
@@ -1752,7 +1832,7 @@ ${taskSection}`;
|
|
|
1752
1832
|
* This is the core logic used by the resume parent session callback
|
|
1753
1833
|
* Extracted to reduce duplication between constructor and addNewRepositories
|
|
1754
1834
|
*/
|
|
1755
|
-
async handleResumeParentSession(parentSessionId, prompt, childSessionId
|
|
1835
|
+
async handleResumeParentSession(parentSessionId, prompt, childSessionId) {
|
|
1756
1836
|
const log = this.logger.withContext({ sessionId: parentSessionId });
|
|
1757
1837
|
log.info(`Child session completed, resuming parent session ${parentSessionId}`);
|
|
1758
1838
|
// Find parent session from the single session manager
|
|
@@ -1771,8 +1851,7 @@ ${taskSection}`;
|
|
|
1771
1851
|
const parentWorkspaceId = requireLinearWorkspaceId(parentRepo);
|
|
1772
1852
|
log.debug(`Found parent session - Issue: ${parentSession.issueId}, Workspace: ${parentSession.workspace.path}`);
|
|
1773
1853
|
// Get the child session to access its workspace path
|
|
1774
|
-
|
|
1775
|
-
const childSession = childAgentSessionManager.getSession(childSessionId);
|
|
1854
|
+
const childSession = this.agentSessionManager.getSession(childSessionId);
|
|
1776
1855
|
const childWorkspaceDirs = [];
|
|
1777
1856
|
if (childSession) {
|
|
1778
1857
|
childWorkspaceDirs.push(childSession.workspace.path);
|
|
@@ -2928,8 +3007,7 @@ ${taskSection}`;
|
|
|
2928
3007
|
labels, // Pass labels for runner selection and model override
|
|
2929
3008
|
fullIssue.description || undefined, // Description tags can override label selectors
|
|
2930
3009
|
undefined, // maxTurns
|
|
2931
|
-
|
|
2932
|
-
linearWorkspaceId);
|
|
3010
|
+
linearWorkspaceId, this.buildSkillSessionContext(primaryRepo, fullIssue));
|
|
2933
3011
|
log.debug(`Label-based runner selection for new session: ${runnerType} (session ${sessionId})`);
|
|
2934
3012
|
const runner = this.createRunnerForType(runnerType, runnerConfig);
|
|
2935
3013
|
// Store runner by comment ID
|
|
@@ -3375,6 +3453,28 @@ ${taskSection}`;
|
|
|
3375
3453
|
async fetchIssueLabels(issue) {
|
|
3376
3454
|
return this.promptBuilder.fetchIssueLabels(issue);
|
|
3377
3455
|
}
|
|
3456
|
+
/**
|
|
3457
|
+
* Build the session context used to evaluate per-skill scope restrictions.
|
|
3458
|
+
*
|
|
3459
|
+
* Skill scopes (persisted in `scope.json` sidecars by the config-updater)
|
|
3460
|
+
* match against:
|
|
3461
|
+
* - the active repository's Cyrus config ID,
|
|
3462
|
+
* - the Linear team that owns the issue, and
|
|
3463
|
+
* - the Linear label IDs attached to the issue.
|
|
3464
|
+
*/
|
|
3465
|
+
buildSkillSessionContext(repository, fullIssue) {
|
|
3466
|
+
const context = {
|
|
3467
|
+
repositoryId: repository.id,
|
|
3468
|
+
};
|
|
3469
|
+
if (fullIssue?.teamId) {
|
|
3470
|
+
context.linearTeamId = fullIssue.teamId;
|
|
3471
|
+
}
|
|
3472
|
+
if (Array.isArray(fullIssue?.labelIds) &&
|
|
3473
|
+
(fullIssue?.labelIds?.length ?? 0) > 0) {
|
|
3474
|
+
context.linearLabelIds = [...(fullIssue?.labelIds ?? [])];
|
|
3475
|
+
}
|
|
3476
|
+
return context;
|
|
3477
|
+
}
|
|
3378
3478
|
/**
|
|
3379
3479
|
* Resolve default model for a given runner from config with sensible built-in defaults.
|
|
3380
3480
|
* Supports legacy config keys for backwards compatibility.
|
|
@@ -3654,8 +3754,132 @@ ${taskSection}`;
|
|
|
3654
3754
|
this.cyrusToolsMcpRegistered = true;
|
|
3655
3755
|
console.log(`✅ Cyrus tools MCP endpoint registered at ${this.cyrusToolsMcpEndpoint}`);
|
|
3656
3756
|
}
|
|
3657
|
-
|
|
3757
|
+
failureModesClient = null;
|
|
3758
|
+
/**
|
|
3759
|
+
* Lazily build the HTTP client used by `log_failure_mode` to POST to
|
|
3760
|
+
* cyrus-hosted. Uses `CYRUS_APP_URL` (the same env var the remote
|
|
3761
|
+
* session-store client reads, see top of this file) so preview
|
|
3762
|
+
* environments and prod share a single way to point at a control
|
|
3763
|
+
* plane. Returns null when either the URL or the `CYRUS_API_KEY` are
|
|
3764
|
+
* missing — in that mode the tool is simply not registered, so
|
|
3765
|
+
* customer-mode CLI users without a control plane don't see a broken
|
|
3766
|
+
* tool.
|
|
3767
|
+
*/
|
|
3768
|
+
getFailureModesClient() {
|
|
3769
|
+
if (this.failureModesClient)
|
|
3770
|
+
return this.failureModesClient;
|
|
3771
|
+
const apiKey = process.env.CYRUS_API_KEY?.trim();
|
|
3772
|
+
const baseUrl = process.env.CYRUS_APP_URL?.trim();
|
|
3773
|
+
if (!apiKey || !baseUrl)
|
|
3774
|
+
return null;
|
|
3775
|
+
this.failureModesClient = createFetchFailureModesClient({
|
|
3776
|
+
baseUrl,
|
|
3777
|
+
apiKey,
|
|
3778
|
+
});
|
|
3779
|
+
return this.failureModesClient;
|
|
3780
|
+
}
|
|
3781
|
+
/**
|
|
3782
|
+
* Resolve a working-directory string to the agent session id that owns
|
|
3783
|
+
* that workspace. The `log_failure_mode` MCP tool calls this with the
|
|
3784
|
+
* agent's reported `cwd`. We normalize and compare against each known
|
|
3785
|
+
* session's `workspace.path` (and any sub-repo paths the session opens).
|
|
3786
|
+
*/
|
|
3787
|
+
/**
|
|
3788
|
+
* Resolve a working-directory string to the rich session bundle a
|
|
3789
|
+
* Cyrus team member needs to triage a failure-mode report: the
|
|
3790
|
+
* internal session id (for dedup), the runner session id + runner
|
|
3791
|
+
* type (so triage can pull the Claude/Gemini/Codex/Cursor transcript),
|
|
3792
|
+
* the Linear AgentSession + source-issue identifiers (so triage can
|
|
3793
|
+
* jump to the customer thread), and the workspace path (for repro).
|
|
3794
|
+
*
|
|
3795
|
+
* Returns null only when no session matches. We prefer an exact
|
|
3796
|
+
* workspace-path or sub-repo-path match; if neither hits, we fall
|
|
3797
|
+
* back to a prefix match for nested cwds (e.g. shells in a subdir).
|
|
3798
|
+
*/
|
|
3799
|
+
/**
|
|
3800
|
+
* Aggregator over every place active sessions live in this process.
|
|
3801
|
+
* Today: the primary AgentSessionManager (issue sessions) and the
|
|
3802
|
+
* ChatSessionHandler's private one (Slack / GitHub-PR-chat / future
|
|
3803
|
+
* chat platforms). New session origins should be added here so
|
|
3804
|
+
* downstream consumers (currently just resolveSessionFromCwd) keep
|
|
3805
|
+
* working without modification — single open extension point (OCP),
|
|
3806
|
+
* single responsibility (SRP: this method's only job is "where do
|
|
3807
|
+
* sessions live?", separate from "how do we match one by cwd?").
|
|
3808
|
+
*/
|
|
3809
|
+
getAllKnownSessions() {
|
|
3810
|
+
return [
|
|
3811
|
+
...this.agentSessionManager.getAllSessions(),
|
|
3812
|
+
...(this.chatSessionHandler?.getAllChatSessions() ?? []),
|
|
3813
|
+
];
|
|
3814
|
+
}
|
|
3815
|
+
resolveSessionFromCwd(cwd) {
|
|
3816
|
+
if (!cwd)
|
|
3817
|
+
return null;
|
|
3818
|
+
const normalize = (p) => p.replace(/\/+$/, "");
|
|
3819
|
+
const target = normalize(cwd);
|
|
3820
|
+
const sessions = this.getAllKnownSessions();
|
|
3821
|
+
const exact = sessions.find((session) => {
|
|
3822
|
+
if (normalize(session.workspace?.path ?? "") === target)
|
|
3823
|
+
return true;
|
|
3824
|
+
const repoPaths = session.workspace?.repoPaths;
|
|
3825
|
+
if (repoPaths) {
|
|
3826
|
+
for (const p of Object.values(repoPaths)) {
|
|
3827
|
+
if (typeof p === "string" && normalize(p) === target)
|
|
3828
|
+
return true;
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
return false;
|
|
3832
|
+
});
|
|
3833
|
+
const prefix = exact
|
|
3834
|
+
? undefined
|
|
3835
|
+
: sessions.find((session) => {
|
|
3836
|
+
const root = normalize(session.workspace?.path ?? "");
|
|
3837
|
+
return root && target.startsWith(`${root}/`);
|
|
3838
|
+
});
|
|
3839
|
+
const session = exact ?? prefix;
|
|
3840
|
+
if (!session)
|
|
3841
|
+
return null;
|
|
3842
|
+
const runnerType = session.claudeSessionId
|
|
3843
|
+
? "claude"
|
|
3844
|
+
: session.geminiSessionId
|
|
3845
|
+
? "gemini"
|
|
3846
|
+
: session.codexSessionId
|
|
3847
|
+
? "codex"
|
|
3848
|
+
: session.cursorSessionId
|
|
3849
|
+
? "cursor"
|
|
3850
|
+
: null;
|
|
3851
|
+
const runnerSessionId = session.claudeSessionId ??
|
|
3852
|
+
session.geminiSessionId ??
|
|
3853
|
+
session.codexSessionId ??
|
|
3854
|
+
session.cursorSessionId ??
|
|
3855
|
+
null;
|
|
3856
|
+
const sessionSource = session.id.startsWith("github-")
|
|
3857
|
+
? "github"
|
|
3858
|
+
: session.id.startsWith("gitlab-")
|
|
3859
|
+
? "gitlab"
|
|
3860
|
+
: session.id.startsWith("slack-")
|
|
3861
|
+
? "slack"
|
|
3862
|
+
: (session.issueContext?.trackerId ?? "linear");
|
|
3863
|
+
// For Linear-source sessions, `session.id` is already the Linear
|
|
3864
|
+
// AgentSession id (they're literally the same UUID — the v3 rename
|
|
3865
|
+
// from `linearAgentActivitySessionId` to `id` kept the value). So we
|
|
3866
|
+
// don't surface a separate `linearAgentSessionId` — the server keys
|
|
3867
|
+
// dedup on `session_id` and that *is* the Linear AgentSession id when
|
|
3868
|
+
// `session_source === 'linear'`.
|
|
3658
3869
|
return {
|
|
3870
|
+
sessionId: session.id,
|
|
3871
|
+
runnerSessionId,
|
|
3872
|
+
runnerType,
|
|
3873
|
+
sourceIssueIdentifier: session.issueContext?.issueIdentifier ??
|
|
3874
|
+
session.issue?.identifier ??
|
|
3875
|
+
null,
|
|
3876
|
+
workspacePath: session.workspace?.path ?? null,
|
|
3877
|
+
sessionSource,
|
|
3878
|
+
};
|
|
3879
|
+
}
|
|
3880
|
+
createCyrusToolsOptions(parentSessionId) {
|
|
3881
|
+
const failureModesClient = this.getFailureModesClient();
|
|
3882
|
+
const options = {
|
|
3659
3883
|
parentSessionId,
|
|
3660
3884
|
onSessionCreated: (childSessionId, parentId) => {
|
|
3661
3885
|
this.handleChildSessionMapping(childSessionId, parentId);
|
|
@@ -3664,6 +3888,13 @@ ${taskSection}`;
|
|
|
3664
3888
|
return this.handleFeedbackDeliveryToChildSession(childSessionId, message);
|
|
3665
3889
|
},
|
|
3666
3890
|
};
|
|
3891
|
+
if (failureModesClient) {
|
|
3892
|
+
options.failureModes = {
|
|
3893
|
+
resolveSessionFromCwd: (cwd) => this.resolveSessionFromCwd(cwd),
|
|
3894
|
+
httpClient: failureModesClient,
|
|
3895
|
+
};
|
|
3896
|
+
}
|
|
3897
|
+
return options;
|
|
3667
3898
|
}
|
|
3668
3899
|
handleChildSessionMapping(childSessionId, parentSessionId) {
|
|
3669
3900
|
console.log(`[EdgeWorker] Agent session created: ${childSessionId}, mapping to parent ${parentSessionId}`);
|
|
@@ -3846,8 +4077,12 @@ ${taskSection}`;
|
|
|
3846
4077
|
const sharedInstructions = await this.loadSharedInstructions();
|
|
3847
4078
|
systemPrompt = sharedInstructions;
|
|
3848
4079
|
}
|
|
3849
|
-
// 3. Append skills guidance — instruct the agent to use skills based on context
|
|
3850
|
-
|
|
4080
|
+
// 3. Append skills guidance — instruct the agent to use skills based on context.
|
|
4081
|
+
// Skills hidden by per-skill scope (repo / Linear team / Linear label) are
|
|
4082
|
+
// omitted from the guidance so the model doesn't reference skills it
|
|
4083
|
+
// cannot invoke.
|
|
4084
|
+
const skillsContext = this.buildSkillSessionContext(repositories[0], input.fullIssue);
|
|
4085
|
+
systemPrompt += await this.skillsPluginResolver.buildSkillsGuidance(undefined, skillsContext);
|
|
3851
4086
|
// 4. Append agent context — dynamic values for skills to reference
|
|
3852
4087
|
systemPrompt += this.buildAgentContextBlock();
|
|
3853
4088
|
// 5. Build issue context using appropriate builder
|
|
@@ -4004,12 +4239,25 @@ ${input.userComment}
|
|
|
4004
4239
|
* Delegates to RunnerConfigBuilder for shared config assembly.
|
|
4005
4240
|
* @returns Object containing the runner config and runner type to use
|
|
4006
4241
|
*/
|
|
4007
|
-
async buildAgentRunnerConfig(session, repository, sessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels, issueDescription, maxTurns,
|
|
4242
|
+
async buildAgentRunnerConfig(session, repository, sessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels, issueDescription, maxTurns, linearWorkspaceId, skillContext,
|
|
4243
|
+
/**
|
|
4244
|
+
* Which platform initiated the session — drives which
|
|
4245
|
+
* `EdgeWorkerConfig.<platform>McpConfigs` override list applies.
|
|
4246
|
+
* Defaults to `"linear"` (the pre-platform-aware behavior).
|
|
4247
|
+
*/
|
|
4248
|
+
sessionPlatform = "linear") {
|
|
4008
4249
|
const log = this.logger.withContext({
|
|
4009
4250
|
sessionId,
|
|
4010
4251
|
platform: session.issueContext?.trackerId,
|
|
4011
4252
|
issueIdentifier: session.issueContext?.issueIdentifier,
|
|
4012
4253
|
});
|
|
4254
|
+
// Resolve plugins once so we can also derive the per-session scoped
|
|
4255
|
+
// skill allow-list from the same filesystem snapshot.
|
|
4256
|
+
const plugins = await this.skillsPluginResolver.resolve();
|
|
4257
|
+
const resolvedSkillContext = skillContext ?? {
|
|
4258
|
+
repositoryId: repository.id,
|
|
4259
|
+
};
|
|
4260
|
+
const allowedSkillNames = await this.skillsPluginResolver.discoverSkillNames(plugins, resolvedSkillContext);
|
|
4013
4261
|
const result = this.runnerConfigBuilder.buildIssueConfig({
|
|
4014
4262
|
session,
|
|
4015
4263
|
repository,
|
|
@@ -4022,11 +4270,21 @@ ${input.userComment}
|
|
|
4022
4270
|
labels,
|
|
4023
4271
|
issueDescription,
|
|
4024
4272
|
maxTurns,
|
|
4025
|
-
|
|
4273
|
+
// Per-platform MCP config paths — GitHub + GitLab share the
|
|
4274
|
+
// `githubMcpConfigs` knob (single-repo PR contexts both); Linear
|
|
4275
|
+
// gets `linearMcpConfigs`. Not a blanket override: the builder
|
|
4276
|
+
// uses `repository.mcpConfigPath` when this repo has its own
|
|
4277
|
+
// `allowedTools` override (so the repo's permission rules and
|
|
4278
|
+
// MCP server set travel as a unit), and only falls through to
|
|
4279
|
+
// this list when the repo inherits the platform allow-list.
|
|
4280
|
+
platformMcpConfigOverrides: sessionPlatform === "linear"
|
|
4281
|
+
? this.config.linearMcpConfigs
|
|
4282
|
+
: this.config.githubMcpConfigs,
|
|
4026
4283
|
linearWorkspaceId,
|
|
4027
4284
|
cyrusHome: this.cyrusHome,
|
|
4028
4285
|
logger: log,
|
|
4029
|
-
plugins
|
|
4286
|
+
plugins,
|
|
4287
|
+
skills: allowedSkillNames,
|
|
4030
4288
|
sandboxSettings: this.sdkSandboxSettings ?? undefined,
|
|
4031
4289
|
egressCaCertPath: this.egressCaCertPath ?? undefined,
|
|
4032
4290
|
onMessage: (message) => {
|
|
@@ -4220,8 +4478,21 @@ ${input.userComment}
|
|
|
4220
4478
|
// Build MCP config for this session (same as the live runner would use)
|
|
4221
4479
|
const linearWorkspaceId = requireLinearWorkspaceId(repo);
|
|
4222
4480
|
const mcpConfig = this.mcpConfigService.buildMcpConfig(repo.id, linearWorkspaceId, session.id);
|
|
4223
|
-
// Merge any file-based MCP configs (reuses shared normalization)
|
|
4224
|
-
|
|
4481
|
+
// Merge any file-based MCP configs (reuses shared normalization).
|
|
4482
|
+
// Warmup paths reconstruct Linear-triggered issue sessions:
|
|
4483
|
+
// if the repo has its own `allowedTools` override its
|
|
4484
|
+
// mcpConfigPath stays scoped to that repo, otherwise the
|
|
4485
|
+
// team-level `linearMcpConfigs` list applies. Same coupling
|
|
4486
|
+
// the live `buildIssueConfig` path uses.
|
|
4487
|
+
const repoHasAllowedToolsOverride = Array.isArray(repo.allowedTools) && repo.allowedTools.length > 0;
|
|
4488
|
+
const mcpConfigPath = repoHasAllowedToolsOverride
|
|
4489
|
+
? this.mcpConfigService.buildMergedMcpConfigPath(repo)
|
|
4490
|
+
: this.config.linearMcpConfigs &&
|
|
4491
|
+
this.config.linearMcpConfigs.length > 0
|
|
4492
|
+
? this.config.linearMcpConfigs.length === 1
|
|
4493
|
+
? this.config.linearMcpConfigs[0]
|
|
4494
|
+
: [...this.config.linearMcpConfigs]
|
|
4495
|
+
: undefined;
|
|
4225
4496
|
let mcpServers = { ...mcpConfig };
|
|
4226
4497
|
if (mcpConfigPath) {
|
|
4227
4498
|
const paths = Array.isArray(mcpConfigPath)
|
|
@@ -4514,8 +4785,7 @@ ${input.userComment}
|
|
|
4514
4785
|
const { config: runnerConfig, runnerType } = await this.buildAgentRunnerConfig(session, repository, sessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels, // Always pass labels to preserve model override
|
|
4515
4786
|
fullIssue.description || undefined, // Description tags can override label selectors
|
|
4516
4787
|
maxTurns, // Pass maxTurns if specified
|
|
4517
|
-
|
|
4518
|
-
resolvedWorkspaceId);
|
|
4788
|
+
resolvedWorkspaceId, this.buildSkillSessionContext(repository, fullIssue));
|
|
4519
4789
|
// Create the appropriate runner based on session state
|
|
4520
4790
|
const runner = this.createRunnerForType(runnerType, runnerConfig);
|
|
4521
4791
|
// Store runner
|