Skip to content

Commit a141962

Browse files
cyrusagentclaudePaytonWebberConnoropolouscursoragent
authored
refactor: Extract 5 service modules from EdgeWorker.ts (CYPACK-822) (#874)
* feat: Add Slack event transport for @mention webhooks (CYPACK-807) Implement cyrus-slack-event-transport package to receive Slack app_mention webhooks forwarded from CYHOST. Adds SlackEventTransport (Fastify endpoint with Bearer token auth), SlackMessageTranslator (translates to unified SessionStartMessage/UserPromptMessage), thread-aware session keys, and Slack platform data types in cyrus-core. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: Handle Slack @mention webhooks as agent sessions with thread reply (CYPACK-807) Slack @mentions now trigger full agent sessions using the first configured repository, with results posted back as threaded replies in Slack. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: Slack sessions use empty working directories, not git worktrees (CYPACK-815) (#868) * feat: Slack sessions use empty working directories, not git worktrees (CYPACK-815) Decouple Slack agent sessions from repository configuration so they run in transient empty directories rather than git worktrees. Subsequent @mentions in the same Slack thread now share the same session context via resume or streaming input injection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: Update changelogs for CYPACK-815 (#868) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: Use plain systemPrompt for Slack sessions to avoid empty text block error The claude_code preset constructs context blocks (CLAUDE.md, git status) that are empty in Slack's non-git empty workspace directories, causing "cache_control cannot be set for empty text blocks" API errors. Using a direct systemPrompt string bypasses the preset entirely. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: Handle empty Slack @mention by providing default prompt Reverts 166d88b which switched from appendSystemPrompt to plain systemPrompt — we need the claude_code preset. The actual root cause was an empty user prompt: when a Slack message is just "@cyrus" with no other text, stripMention returns "", which the SDK sends as an empty text block with cache_control, rejected by the API. Now falls back to "Ask the user for more context" when the stripped text is empty. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: Skip procedure routing for Slack sessions on completion Slack sessions are created without a ProcedureAnalyzer since they are simple conversational sessions, not multi-subroutine workflows. When the session completed, completeSession called handleProcedureCompletion which threw "ProcedureAnalyzer not available". Now gracefully returns early when no ProcedureAnalyzer is configured. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: Add Slack thread context to agent sessions When a user @mentions Cyrus in a Slack thread, fetch the preceding conversation via conversations.replies and prepend it as XML context to the user prompt. This gives the agent visibility into what users were discussing before invoking it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: Extract Slack integration via ChatPlatformAdapter pattern Introduce ChatPlatformAdapter<TEvent> interface and ChatSessionHandler<TEvent> class to decouple chat platform session lifecycle from EdgeWorker. Slack becomes the first adapter implementation (SlackChatAdapter), removing ~450 lines and 9 private methods from EdgeWorker.ts. Adding a new chat platform now requires only an adapter (~80 lines) instead of duplicating session management logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: Add createChatSession for issue-free sessions, fix appendSystemPrompt Addresses PR review feedback: - Add AgentSessionManager.createChatSession() for chat platforms that don't have issue context, avoiding synthetic IssueMinimal objects - ChatSessionHandler now uses createChatSession instead of createLinearAgentSession with fake issue data - Fix ChatSessionHandler.buildRunnerConfig using appendSystemPrompt instead of systemPrompt (re-introduced the cache_control bug) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * revert: Use appendSystemPrompt for chat sessions (root cause was empty prompt) The cache_control error was caused by an empty prompt string after stripping the @mention (e.g. just "@cyrus" with no text), not by the claude_code preset. Revert back to appendSystemPrompt so chat sessions benefit from the full Claude Code system prompt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: Use streaming mode for chat sessions to enable follow-up message injection Chat sessions (Slack threads) were started with runner.start() (string mode), so follow-up messages arriving mid-session would crash with "Cannot add stream message when not in streaming mode". Switch to startStreaming() and add an isStreaming() guard before calling addStreamMessage(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Payton Webber <paytonwebber@gmail.com> * Merge main into cypack-807 branch (CYPACK-821) (#873) * Respect agent guidance for draft PRs and use --track for worktrees (#834) - Update gh-pr subroutine to conditionally gate `gh pr ready` based on agent guidance (respects --draft flag in workspace guidance) - Update changelog-update subroutine to include --base flag in PR creation targeting the configured base branch from context - Add --track flag to git worktree add commands to properly set upstream tracking branch Fixes issues where PRs were auto-converted from draft to ready and worktrees weren't tracking the correct upstream branch. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * Prepare release v0.2.20 (#835) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * Refactor TodoWrite formatting to support Task tools (#837) * Update @anthropic-ai/claude-agent-sdk to v0.2.34 and @anthropic-ai/sdk to v0.73.0 - Updated @anthropic-ai/claude-agent-sdk from ^0.2.7 to ^0.2.34 - Updated @anthropic-ai/sdk from ^0.71.2 to ^0.73.0 - Fixed type compatibility with new BetaUsage fields (inference_geo, iterations, stop_reason) - Updated gemini-runner adapters to handle new SDK types Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Update changelog for SDK updates Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Enable Claude Code experimental features in claude-runner - Set CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 - Set CLAUDE_CODE_ENABLE_TASKS=true - Set CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 - Updated test expectations to include new env configuration Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Refactor TodoWrite formatting to support Task tools - Added formatTaskParameter() method to IMessageFormatter interface - Implemented Task tool formatting (TaskCreate, TaskUpdate, TaskList, TaskGet) in ClaudeMessageFormatter and GeminiMessageFormatter - Updated AgentSessionManager to handle Task tools as thought activities - Added comprehensive tests for Task tool formatting (20 new tests) - Marked TodoWrite as deprecated while maintaining backward compatibility - Fixed linting warnings in switch case block scope Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Fix spawn node ENOENT by passing process.env to Claude SDK The env option only contained 3 hardcoded vars, causing PATH to be missing from the spawned Claude Code process. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Cherry-pick d5d9e5a: Fix single-turn subroutine silent failures (#843) * Cherry-pick d5d9e5a: Fix single-turn subroutine silent failures Cherry-picks commit d5d9e5a from refactor/improved-logging branch. Resolves merge conflicts adapting logging refactor patterns (sessionId/log.info) to current branch patterns (linearAgentActivitySessionId/console.log). When a single-turn subroutine exits with an error (e.g. error_max_turns), AgentSessionManager now recovers by using the last completed subroutine's result via ProcedureAnalyzer.getLastSubroutineResult(), allowing the procedure to continue to completion instead of failing silently. Also adds tools config pass-through to ClaudeRunner and EdgeWorker so disallowAllTools subroutines properly disable built-in tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update changelogs for CYPACK-792 (#843) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix ClaudeRunner test env assertions after process.env spread Tests expected env to contain only 3 explicit keys, but commit 6e8d873 added ...process.env to prevent spawn ENOENT errors. Updated assertions to use expect.objectContaining() so tests verify required keys without failing on additional process.env entries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * Fix TaskCreate/TaskGet formatting, add ToolSearch/TaskOutput formatting, include task subject in TaskUpdate/TaskGet (#846) * Fix TaskCreate/TaskGet formatting for parallel execution, add ToolSearch and TaskOutput formatting TaskCreate now renders as concise checklist items (⏳ **subject**) instead of verbose multi-line format, better suited for parallel async execution. TaskGet shows subject with ID when enriched. ToolSearch renders as a thought with descriptive formatting. TaskOutput shows task status with blocking/non-blocking context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add changelog entries for CYPACK-795 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Simplify TaskUpdate/TaskGet to use task number, show ToolSearch query directly TaskUpdate and TaskGet now always display the task number (e.g. "Task #123") instead of the subject description for consistency with parallel execution. ToolSearch now shows the query directly as the parameter value, matching the pattern of other tools like Bash (command) and Read (file_path). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Revert Gemini runner formatting changes (not applicable to these tools) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Include task subject in TaskUpdate and TaskGet formatting TaskUpdate and TaskGet now display the task's short description alongside the task number (e.g., "✅ Task #3 — Fix login bug") when a subject is available, falling back to number-only when not. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Enrich TaskUpdate/TaskGet activities with task subject (CYPACK-797) (#847) * Enrich TaskUpdate/TaskGet activities with task subject from cache and result parsing TaskUpdate and TaskGet tool inputs don't include the task subject, causing Linear activities to show only "Task #3" without context. This change defers their activity posting from tool_use time to tool_result time, where subject can be sourced from: (1) a cache populated by TaskCreate results, or (2) parsing the Subject field from TaskGet result content. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add changelog entries for CYPACK-797 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Delete packages/edge-worker/test/AgentSessionManager.task-subject-enrichment.test.ts --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Payton Webber <53197664+PaytonWebber@users.noreply.github.com> * Release v0.2.21 (#848) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * fix(orchestrator): ensure issues are created with 'To Do' status instead of 'Triage' (#815) * fix(orchestrator): ensure issues are created with 'To Do' status instead of 'Triage' Update orchestrator system prompts to explicitly require setting state to "To Do" when creating issues with mcp__linear__create_issue. Previously, issues were being created with default "Triage" status. Changes to both orchestrator.md (v2.5.0) and graphite-orchestrator.md (v1.3.0): - Added state requirement to Required Tools section - Added Status bullet point in Decompose section - Added status checklist item in Sub-Issue Creation Checklist - Fixed outdated tool names in orchestrator.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(changelog): add entry for CYPACK-761 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Connor Turland <1409121+Connoropolous@users.noreply.github.com> * Codex SDK migration + split agent/model selectors (#850) * chore: checkpoint codex sdk integration WIP * Switch codex runner to SDK and add agent/model selectors * chore: checkpoint codex sdk integration WIP * Switch codex runner to SDK and add agent/model selectors * Resolve rebase conflicts, migrate model defaults, and fix codex session init * Fix Codex/Gemini usage typing and add Codex formatter replay tests * Emit Codex tool lifecycle events for Linear activities * Format Codex todos as markdown checklists * Extract F1 test-drive workflow into shared skill * Add user-facing changelog entry for agent/model selectors * Add PR link to changelog entry for codex selector work * update sandbox settings * add .git folder for worktrees to allowed directories list * Cursor harness MCP enable + permission mapping hardening (#858) * fix(orchestrator): ensure issues are created with 'To Do' status instead of 'Triage' (#815) * fix(orchestrator): ensure issues are created with 'To Do' status instead of 'Triage' Update orchestrator system prompts to explicitly require setting state to "To Do" when creating issues with mcp__linear__create_issue. Previously, issues were being created with default "Triage" status. Changes to both orchestrator.md (v2.5.0) and graphite-orchestrator.md (v1.3.0): - Added state requirement to Required Tools section - Added Status bullet point in Decompose section - Added status checklist item in Sub-Issue Creation Checklist - Fixed outdated tool names in orchestrator.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(changelog): add entry for CYPACK-761 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Connor Turland <1409121+Connoropolous@users.noreply.github.com> * feat(runners): add codex/cursor harness support and F1 validation * fix(cursor-runner): support non-mock cursor F1 session output * Fix cursor harness verification regressions * fix(ci): resolve build type errors in config-updater and core session types * fix(edge-worker): wire cursor runner selection and prompt assets * docs(changelog): frame cursor harness as major feature * docs(changelog): use user-facing framing for cursor entry * docs(changelog): clarify cursor selector usage * fix(build): remove baseUrl/paths from tsconfig to fix dist output structure The baseUrl pointing to monorepo root caused TypeScript to expand rootDir, nesting compiled output under dist/edge-worker/src/ instead of dist/. This broke module resolution since package.json declares main as dist/index.js. Also adds missing cyrus-cursor-runner dependency to edge-worker. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(cursor/edge-worker): stop-session handling and cursor tool activity mapping * chore(gitignore): ignore nested node_modules directories * fix(edge-worker): resume cursor sessions for prompted continuation * fix(cursor-runner): sync project permissions for cursor CLI sessions * fix(cursor-runner): map MCP tool permissions to Cursor CLI config * docs(claude): add cursor permission translation gotcha * fix(codex): default runner model to gpt-5.3-codex * fix(cursor-runner): pre-enable MCP servers before cursor sessions * docs(cursor): clarify .cursor/mcp.json source-of-truth guidance * fix(cursor-runner): scope wildcard file permissions to workspace * fix(codex): post actual error message to Linear for usage limit errors When Codex hits usage limits or turn.failed errors, the full error message is now posted to Linear agent activity instead of a generic message. - AgentSessionManager: use errors[] for content when result is empty - CodexRunner: fallback to standalone error event when turn.failed has no message Co-authored-by: Cursor <cursoragent@cursor.com> * feat(cursor): enable sandbox by default, fix cli.json formatting - Default Cursor sandbox to enabled for tool execution isolation - Write .cursor/cli.json with tabs for Biome compatibility - Exclude .cursor from Biome checks (generated config) - Update CHANGELOG with sandbox default Co-authored-by: Cursor <cursoragent@cursor.com> * feat(cursor): validate cursor-agent version before run, post error to Linear on mismatch - Run cursor-agent --version before spawn and compare to tested version - Post error_during_execution to Linear via agent activity when version mismatches - Add cursorAgentVersion config and CYRUS_CURSOR_AGENT_VERSION env override - Add CursorRunner.version-check.test.ts with 3 tests - Update CHANGELOG with version validation behavior Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): render TODO_STATUS_COMPLETED todos as checked in Linear - Map Cursor API TODO_STATUS_COMPLETED to [x] marker in formatter and summarizeTodoList - Map TODO_STATUS_IN_PROGRESS for (in progress) suffix - Add formatter test for TODO_STATUS_* values - Update CHANGELOG Co-authored-by: Cursor <cursoragent@cursor.com> * feat(cursor): log assembled cursor-agent CLI args to console and session logs - Log full spawn command (path + args) before cursor-agent execution - Write to both console and session log file for debugging - Update CHANGELOG Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): remove --force and --api-key from CLI args - Remove --force option from cursor-agent invocations - Pass API key via CURSOR_API_KEY env only (no longer in args) - API key no longer appears in spawn logs or terminal output - Update CHANGELOG Co-authored-by: Cursor <cursoragent@cursor.com> * add --trust * move trust line * Delete .cursor/cli.json * fix(cursor-runner): backup and restore .cursor/cli.json instead of overwriting Temporarily rename existing .cursor/cli.json before writing Cyrus permissions, then restore original when session ends. Preserves user's config. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): add deny rules to scope Read/Write to workspace only - When broad Read or Write allowed, add deny rules for /etc, /usr, etc. - Add deny rules for workspace sibling directories - Prevents cursor-agent from accessing /etc/hosts and system paths - Add test: only mutates project .cursor/cli.json, leaves home config unchanged - Update wildcard permissions test to expect system-path denies Co-authored-by: Cursor <cursoragent@cursor.com> * docs(changelog): add Cursor .cursor/cli.json backup/restore entry for CYPACK-804 Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Connor Turland <1409121+Connoropolous@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: agentclear <agentops@ceedar.ai> Co-authored-by: Cyrus <208047790+cyrusagent@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> * Update README with agent capabilities and notes (#869) * Bump fastify from 5.6.1 to 5.7.3 (#828) Bumps [fastify](https://github.com/fastify/fastify) from 5.6.1 to 5.7.3. - [Release notes](https://github.com/fastify/fastify/releases) - [Commits](fastify/fastify@v5.6.1...v5.7.3) --- updated-dependencies: - dependency-name: fastify dependency-version: 5.7.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * docs: Update internal changelog for main merge (CYPACK-821) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Payton Webber <53197664+PaytonWebber@users.noreply.github.com> Co-authored-by: Connor Turland <1409121+Connoropolous@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * refactor: Extract 5 service modules from EdgeWorker.ts (CYPACK-822) Extract logically grouped functionality into dedicated modules to improve readability and maintainability of the 7,687-line EdgeWorker.ts file: - ActivityPoster: Linear activity posting (289 lines) - AttachmentService: Attachment download and manifest generation (488 lines) - ConfigManager: Config file watching, reload, and change detection (320 lines) - PromptBuilder: Prompt assembly, system prompts, and issue context (1,270 lines) - RunnerSelectionService: Runner/model selection and tool configuration (498 lines) EdgeWorker.ts reduced from 7,687 to 5,466 lines (29% reduction). All 522 tests passing, typecheck clean, lint clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Payton Webber <paytonwebber@gmail.com> Co-authored-by: Payton Webber <53197664+PaytonWebber@users.noreply.github.com> Co-authored-by: Connor Turland <1409121+Connoropolous@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 1117163 commit a141962

9 files changed

Lines changed: 3064 additions & 2368 deletions

CHANGELOG.internal.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ This changelog documents internal development changes, refactors, tooling update
55
## [Unreleased]
66

77
### Changed
8+
- Refactored `EdgeWorker.ts` by extracting 5 service modules: `ActivityPoster` (Linear activity posting), `AttachmentService` (attachment download/manifests), `ConfigManager` (config file watching/reload/change detection), `PromptBuilder` (prompt assembly/system prompts/issue context), and `RunnerSelectionService` (runner/model selection/tool configuration). Reduced EdgeWorker from 7,687 to 5,466 lines (29% reduction) while maintaining full test coverage (522 tests). ([CYPACK-822](https://linear.app/ceedar/issue/CYPACK-822), [#874](https://github.com/ceedaragents/cyrus/pull/874))
89
- Merged `main` into `cypack-807` branch, resolving 7 merge conflicts and fixing auto-merge issues across AgentSessionManager, EdgeWorker, GitService, ProcedureAnalyzer, gemini-runner, and changelogs. Updated 2 test files from `IIssueTrackerService` to `IActivitySink` interface. ([CYPACK-821](https://linear.app/ceedar/issue/CYPACK-821), [#873](https://github.com/ceedaragents/cyrus/pull/873))
910
- Decoupled Slack webhook handler from `RepositoryConfig`: introduced `NoopActivitySink` for non-repository sessions, dedicated `slackSessionManager` on `EdgeWorker`, and `slackThreadSessions` map for thread-based session reuse. `createSlackWorkspace` now creates plain directories under `~/.cyrus/slack-workspaces/` instead of git worktrees. Runner config is built inline (bypassing `buildAgentRunnerConfig` which requires a repository). Added `SlackReactionService` to `cyrus-slack-event-transport` package. ([CYPACK-815](https://linear.app/ceedar/issue/CYPACK-815), [#868](https://github.com/ceedaragents/cyrus/pull/868))
1011
- Refactored logging across all packages to use a dedicated `ILogger` interface and `Logger` implementation in `packages/core/src/logging/`. Replaced direct `console.log`/`console.error` calls in EdgeWorker, AgentSessionManager, ClaudeRunner, GitService, RepositoryRouter, SharedApplicationServer, SharedWebhookServer, WorktreeIncludeService, ProcedureAnalyzer, AskUserQuestionHandler, LinearEventTransport, and LinearIssueTrackerService with structured logger calls. Log level is configurable via the `CYRUS_LOG_LEVEL` environment variable (DEBUG, INFO, WARN, ERROR, SILENT).
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import type {
2+
AgentActivityCreateInput,
3+
IIssueTrackerService,
4+
ILogger,
5+
RepositoryConfig,
6+
} from "cyrus-core";
7+
8+
export class ActivityPoster {
9+
private issueTrackers: Map<string, IIssueTrackerService>;
10+
private repositories: Map<string, RepositoryConfig>;
11+
private logger: ILogger;
12+
13+
constructor(
14+
issueTrackers: Map<string, IIssueTrackerService>,
15+
repositories: Map<string, RepositoryConfig>,
16+
logger: ILogger,
17+
) {
18+
this.issueTrackers = issueTrackers;
19+
this.repositories = repositories;
20+
this.logger = logger;
21+
}
22+
23+
async postActivityDirect(
24+
issueTracker: IIssueTrackerService,
25+
input: AgentActivityCreateInput,
26+
label: string,
27+
): Promise<string | null> {
28+
try {
29+
const result = await issueTracker.createAgentActivity(input);
30+
if (result.success) {
31+
if (result.agentActivity) {
32+
const activity = await result.agentActivity;
33+
this.logger.debug(`Created ${label} activity ${activity.id}`);
34+
return activity.id;
35+
}
36+
this.logger.debug(`Created ${label}`);
37+
return null;
38+
}
39+
this.logger.error(`Failed to create ${label}:`, result);
40+
return null;
41+
} catch (error) {
42+
this.logger.error(`Error creating ${label}:`, error);
43+
return null;
44+
}
45+
}
46+
47+
async postInstantAcknowledgment(
48+
sessionId: string,
49+
repositoryId: string,
50+
): Promise<void> {
51+
const issueTracker = this.issueTrackers.get(repositoryId);
52+
if (!issueTracker) {
53+
this.logger.warn(`No issue tracker found for repository ${repositoryId}`);
54+
return;
55+
}
56+
57+
await this.postActivityDirect(
58+
issueTracker,
59+
{
60+
agentSessionId: sessionId,
61+
content: {
62+
type: "thought",
63+
body: "I've received your request and I'm starting to work on it. Let me analyze the issue and prepare my approach.",
64+
},
65+
},
66+
"instant acknowledgment",
67+
);
68+
}
69+
70+
async postParentResumeAcknowledgment(
71+
sessionId: string,
72+
repositoryId: string,
73+
): Promise<void> {
74+
const issueTracker = this.issueTrackers.get(repositoryId);
75+
if (!issueTracker) {
76+
this.logger.warn(`No issue tracker found for repository ${repositoryId}`);
77+
return;
78+
}
79+
80+
await this.postActivityDirect(
81+
issueTracker,
82+
{
83+
agentSessionId: sessionId,
84+
content: { type: "thought", body: "Resuming from child session" },
85+
},
86+
"parent resume acknowledgment",
87+
);
88+
}
89+
90+
async postRepositorySelectionActivity(
91+
sessionId: string,
92+
repositoryId: string,
93+
repositoryName: string,
94+
selectionMethod:
95+
| "description-tag"
96+
| "label-based"
97+
| "project-based"
98+
| "team-based"
99+
| "team-prefix"
100+
| "catch-all"
101+
| "workspace-fallback"
102+
| "user-selected",
103+
): Promise<void> {
104+
const issueTracker = this.issueTrackers.get(repositoryId);
105+
if (!issueTracker) {
106+
this.logger.warn(`No issue tracker found for repository ${repositoryId}`);
107+
return;
108+
}
109+
110+
let methodDisplay: string;
111+
if (selectionMethod === "user-selected") {
112+
methodDisplay = "selected by user";
113+
} else if (selectionMethod === "description-tag") {
114+
methodDisplay = "matched via [repo=...] tag in issue description";
115+
} else if (selectionMethod === "label-based") {
116+
methodDisplay = "matched via label-based routing";
117+
} else if (selectionMethod === "project-based") {
118+
methodDisplay = "matched via project-based routing";
119+
} else if (selectionMethod === "team-based") {
120+
methodDisplay = "matched via team-based routing";
121+
} else if (selectionMethod === "team-prefix") {
122+
methodDisplay = "matched via team prefix routing";
123+
} else if (selectionMethod === "catch-all") {
124+
methodDisplay = "matched via catch-all routing";
125+
} else {
126+
methodDisplay = "matched via workspace fallback";
127+
}
128+
129+
await this.postActivityDirect(
130+
issueTracker,
131+
{
132+
agentSessionId: sessionId,
133+
content: {
134+
type: "thought",
135+
body: `Repository "${repositoryName}" has been ${methodDisplay}.`,
136+
},
137+
},
138+
"repository selection",
139+
);
140+
}
141+
142+
async postSystemPromptSelectionThought(
143+
sessionId: string,
144+
labels: string[],
145+
repositoryId: string,
146+
): Promise<void> {
147+
const issueTracker = this.issueTrackers.get(repositoryId);
148+
if (!issueTracker) {
149+
this.logger.warn(`No issue tracker found for repository ${repositoryId}`);
150+
return;
151+
}
152+
153+
// Determine which prompt type was selected and which label triggered it
154+
let selectedPromptType: string | null = null;
155+
let triggerLabel: string | null = null;
156+
const repository = Array.from(this.repositories.values()).find(
157+
(r) => r.id === repositoryId,
158+
);
159+
160+
if (repository?.labelPrompts) {
161+
// Check debugger labels
162+
const debuggerConfig = repository.labelPrompts.debugger;
163+
const debuggerLabels = Array.isArray(debuggerConfig)
164+
? debuggerConfig
165+
: debuggerConfig?.labels;
166+
const debuggerLabel = debuggerLabels?.find((label) =>
167+
labels.includes(label),
168+
);
169+
if (debuggerLabel) {
170+
selectedPromptType = "debugger";
171+
triggerLabel = debuggerLabel;
172+
} else {
173+
// Check builder labels
174+
const builderConfig = repository.labelPrompts.builder;
175+
const builderLabels = Array.isArray(builderConfig)
176+
? builderConfig
177+
: builderConfig?.labels;
178+
const builderLabel = builderLabels?.find((label) =>
179+
labels.includes(label),
180+
);
181+
if (builderLabel) {
182+
selectedPromptType = "builder";
183+
triggerLabel = builderLabel;
184+
} else {
185+
// Check scoper labels
186+
const scoperConfig = repository.labelPrompts.scoper;
187+
const scoperLabels = Array.isArray(scoperConfig)
188+
? scoperConfig
189+
: scoperConfig?.labels;
190+
const scoperLabel = scoperLabels?.find((label) =>
191+
labels.includes(label),
192+
);
193+
if (scoperLabel) {
194+
selectedPromptType = "scoper";
195+
triggerLabel = scoperLabel;
196+
} else {
197+
// Check orchestrator labels
198+
const orchestratorConfig = repository.labelPrompts.orchestrator;
199+
const orchestratorLabels = Array.isArray(orchestratorConfig)
200+
? orchestratorConfig
201+
: (orchestratorConfig?.labels ?? ["orchestrator"]);
202+
const orchestratorLabel = orchestratorLabels?.find((label) =>
203+
labels.includes(label),
204+
);
205+
if (orchestratorLabel) {
206+
selectedPromptType = "orchestrator";
207+
triggerLabel = orchestratorLabel;
208+
}
209+
}
210+
}
211+
}
212+
}
213+
214+
// Only post if a role was actually triggered
215+
if (!selectedPromptType || !triggerLabel) {
216+
return;
217+
}
218+
219+
await this.postActivityDirect(
220+
issueTracker,
221+
{
222+
agentSessionId: sessionId,
223+
content: {
224+
type: "thought",
225+
body: `Entering '${selectedPromptType}' mode because of the '${triggerLabel}' label. I'll follow the ${selectedPromptType} process...`,
226+
},
227+
},
228+
"system prompt selection",
229+
);
230+
}
231+
232+
async postInstantPromptedAcknowledgment(
233+
sessionId: string,
234+
repositoryId: string,
235+
isStreaming: boolean,
236+
): Promise<void> {
237+
const issueTracker = this.issueTrackers.get(repositoryId);
238+
if (!issueTracker) {
239+
this.logger.warn(`No issue tracker found for repository ${repositoryId}`);
240+
return;
241+
}
242+
243+
const message = isStreaming
244+
? "I've queued up your message as guidance"
245+
: "Getting started on that...";
246+
247+
await this.postActivityDirect(
248+
issueTracker,
249+
{
250+
agentSessionId: sessionId,
251+
content: { type: "thought", body: message },
252+
},
253+
"prompted acknowledgment",
254+
);
255+
}
256+
257+
async postComment(
258+
issueId: string,
259+
body: string,
260+
repositoryId: string,
261+
parentId?: string,
262+
): Promise<void> {
263+
// Get the issue tracker for this repository
264+
const issueTracker = this.issueTrackers.get(repositoryId);
265+
if (!issueTracker) {
266+
throw new Error(`No issue tracker found for repository ${repositoryId}`);
267+
}
268+
const commentInput: { body: string; parentId?: string } = {
269+
body,
270+
};
271+
// Add parent ID if provided (for reply)
272+
if (parentId) {
273+
commentInput.parentId = parentId;
274+
}
275+
await issueTracker.createComment(issueId, commentInput);
276+
}
277+
}

0 commit comments

Comments
 (0)