cyrus-edge-worker 0.0.38 → 0.0.40

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 (35) hide show
  1. package/dist/AgentSessionManager.d.ts.map +1 -1
  2. package/dist/AgentSessionManager.js +2 -4
  3. package/dist/AgentSessionManager.js.map +1 -1
  4. package/dist/EdgeWorker.d.ts +75 -3
  5. package/dist/EdgeWorker.d.ts.map +1 -1
  6. package/dist/EdgeWorker.js +496 -151
  7. package/dist/EdgeWorker.js.map +1 -1
  8. package/dist/procedures/ProcedureRouter.d.ts.map +1 -1
  9. package/dist/procedures/ProcedureRouter.js +11 -2
  10. package/dist/procedures/ProcedureRouter.js.map +1 -1
  11. package/dist/procedures/registry.d.ts +29 -0
  12. package/dist/procedures/registry.d.ts.map +1 -1
  13. package/dist/procedures/registry.js +45 -8
  14. package/dist/procedures/registry.js.map +1 -1
  15. package/dist/procedures/types.d.ts +1 -1
  16. package/dist/procedures/types.d.ts.map +1 -1
  17. package/dist/prompt-assembly/types.d.ts +81 -0
  18. package/dist/prompt-assembly/types.d.ts.map +1 -0
  19. package/dist/prompt-assembly/types.js +8 -0
  20. package/dist/prompt-assembly/types.js.map +1 -0
  21. package/dist/prompts/subroutines/coding-activity.md +10 -0
  22. package/dist/prompts/subroutines/concise-summary.md +16 -2
  23. package/dist/prompts/subroutines/debugger-fix.md +8 -25
  24. package/dist/prompts/subroutines/debugger-reproduction.md +11 -44
  25. package/dist/prompts/subroutines/git-gh.md +9 -6
  26. package/dist/prompts/subroutines/plan-summary.md +21 -0
  27. package/dist/prompts/subroutines/preparation.md +16 -0
  28. package/dist/prompts/subroutines/question-answer.md +8 -0
  29. package/dist/prompts/subroutines/question-investigation.md +8 -0
  30. package/dist/prompts/subroutines/verifications.md +9 -6
  31. package/package.json +4 -4
  32. package/prompts/orchestrator.md +9 -1
  33. package/prompts/standard-issue-assigned-user-prompt.md +33 -0
  34. package/prompts/todolist-system-prompt-extension.md +15 -0
  35. package/prompt-template-v2.md +0 -89
@@ -339,13 +339,38 @@ export class EdgeWorker extends EventEmitter {
339
339
  console.warn(`[Parent Session Resume] Could not find child session ${childSessionId} to add workspace to parent allowed directories`);
340
340
  }
341
341
  await this.postParentResumeAcknowledgment(parentSessionId, repo.id);
342
- // Resume the parent session with the child's result
343
- console.log(`[Parent Session Resume] Resuming parent Claude session with child results`);
342
+ // Post thought to Linear showing child result receipt
343
+ const linearClient = this.linearClients.get(repo.id);
344
+ if (linearClient && childSession) {
345
+ const childIssueIdentifier = childSession.issue?.identifier || childSession.issueId;
346
+ const resultThought = `Received result from sub-issue ${childIssueIdentifier}:\n\n---\n\n${prompt}\n\n---`;
347
+ try {
348
+ const result = await linearClient.createAgentActivity({
349
+ agentSessionId: parentSessionId,
350
+ content: {
351
+ type: "thought",
352
+ body: resultThought,
353
+ },
354
+ });
355
+ if (result.success) {
356
+ console.log(`[Parent Session Resume] Posted child result receipt thought for parent session ${parentSessionId}`);
357
+ }
358
+ else {
359
+ console.error(`[Parent Session Resume] Failed to post child result receipt thought:`, result);
360
+ }
361
+ }
362
+ catch (error) {
363
+ console.error(`[Parent Session Resume] Error posting child result receipt thought:`, error);
364
+ }
365
+ }
366
+ // Use centralized streaming check and routing logic
367
+ console.log(`[Parent Session Resume] Handling child result for parent session ${parentSessionId}`);
344
368
  try {
345
- await this.resumeClaudeSession(parentSession, repo, parentSessionId, agentSessionManager, prompt, "", // No attachment manifest for child results
369
+ await this.handlePromptWithStreamingCheck(parentSession, repo, parentSessionId, agentSessionManager, prompt, "", // No attachment manifest for child results
346
370
  false, // Not a new session
347
- childWorkspaceDirs);
348
- console.log(`[Parent Session Resume] Successfully resumed parent session ${parentSessionId} with child results`);
371
+ childWorkspaceDirs, // Add child workspace directories to parent's allowed directories
372
+ "parent resume from child");
373
+ console.log(`[Parent Session Resume] Successfully handled child result for parent session ${parentSessionId}`);
349
374
  }
350
375
  catch (error) {
351
376
  console.error(`[Parent Session Resume] Failed to resume parent session ${parentSessionId}:`, error);
@@ -1106,71 +1131,66 @@ export class EdgeWorker extends EventEmitter {
1106
1131
  this.procedureRouter.initializeProcedureMetadata(session, finalProcedure);
1107
1132
  // Post single procedure selection result (replaces ephemeral routing thought)
1108
1133
  await agentSessionManager.postProcedureSelectionThought(linearAgentActivitySessionId, finalProcedure.name, finalClassification);
1109
- // Only determine system prompt for delegation (not mentions) or when /label-based-prompt is requested
1110
- let systemPrompt;
1111
- let systemPromptVersion;
1112
- let promptType;
1113
- if (!isMentionTriggered || isLabelBasedPromptRequested) {
1114
- // Determine system prompt based on labels (delegation case or /label-based-prompt command)
1115
- const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
1116
- systemPrompt = systemPromptResult?.prompt;
1117
- systemPromptVersion = systemPromptResult?.version;
1118
- promptType = systemPromptResult?.type;
1119
- // Post thought about system prompt selection
1120
- if (systemPrompt) {
1121
- await this.postSystemPromptSelectionThought(linearAgentActivitySessionId, labels, repository.id);
1122
- }
1123
- }
1124
- else {
1125
- console.log(`[EdgeWorker] Skipping system prompt for mention-triggered session ${linearAgentActivitySessionId}`);
1126
- }
1127
- // Build allowed tools list with Linear MCP tools (now with prompt type context)
1128
- const allowedTools = this.buildAllowedTools(repository, promptType);
1129
- const disallowedTools = this.buildDisallowedTools(repository, promptType);
1130
- console.log(`[EdgeWorker] Configured allowed tools for ${fullIssue.identifier}:`, allowedTools);
1131
- if (disallowedTools.length > 0) {
1132
- console.log(`[EdgeWorker] Configured disallowed tools for ${fullIssue.identifier}:`, disallowedTools);
1133
- }
1134
- // Create Claude runner with attachment directory access and optional system prompt
1135
- const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
1136
- labels);
1137
- const runner = new ClaudeRunner(runnerConfig);
1138
- // Store runner by comment ID
1139
- agentSessionManager.addClaudeRunner(linearAgentActivitySessionId, runner);
1140
- // Save state after mapping changes
1141
- await this.savePersistedState();
1142
- // Emit events using full Linear issue
1143
- this.emit("session:started", fullIssue.id, fullIssue, repository.id);
1144
- this.config.handlers?.onSessionStart?.(fullIssue.id, fullIssue, repository.id);
1145
1134
  // Build and start Claude with initial prompt using full issue (streaming mode)
1146
1135
  console.log(`[EdgeWorker] Building initial prompt for issue ${fullIssue.identifier}`);
1147
1136
  try {
1148
- // Choose the appropriate prompt builder based on trigger type and system prompt
1149
- const promptResult = isMentionTriggered && isLabelBasedPromptRequested
1150
- ? await this.buildLabelBasedPrompt(fullIssue, repository, attachmentResult.manifest, guidance)
1151
- : isMentionTriggered
1152
- ? await this.buildMentionPrompt(fullIssue, agentSession, attachmentResult.manifest, guidance)
1153
- : systemPrompt
1154
- ? await this.buildLabelBasedPrompt(fullIssue, repository, attachmentResult.manifest, guidance)
1155
- : await this.buildPromptV2(fullIssue, repository, undefined, attachmentResult.manifest, guidance);
1156
- const { prompt, version: userPromptVersion } = promptResult;
1157
- // Update runner with version information
1158
- if (userPromptVersion || systemPromptVersion) {
1137
+ // Create input for unified prompt assembly
1138
+ const input = {
1139
+ session,
1140
+ fullIssue,
1141
+ repository,
1142
+ userComment: commentBody || "", // Empty for delegation, present for mentions
1143
+ attachmentManifest: attachmentResult.manifest,
1144
+ guidance,
1145
+ agentSession,
1146
+ labels,
1147
+ isNewSession: true,
1148
+ isStreaming: false, // Not yet streaming
1149
+ isMentionTriggered: isMentionTriggered || false,
1150
+ isLabelBasedPromptRequested: isLabelBasedPromptRequested || false,
1151
+ };
1152
+ // Use unified prompt assembly
1153
+ const assembly = await this.assemblePrompt(input);
1154
+ // Get systemPromptVersion for tracking (TODO: add to PromptAssembly metadata)
1155
+ let systemPromptVersion;
1156
+ let promptType;
1157
+ if (!isMentionTriggered || isLabelBasedPromptRequested) {
1158
+ const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
1159
+ systemPromptVersion = systemPromptResult?.version;
1160
+ promptType = systemPromptResult?.type;
1161
+ // Post thought about system prompt selection
1162
+ if (assembly.systemPrompt) {
1163
+ await this.postSystemPromptSelectionThought(linearAgentActivitySessionId, labels, repository.id);
1164
+ }
1165
+ }
1166
+ // Build allowed tools list with Linear MCP tools (now with prompt type context)
1167
+ const allowedTools = this.buildAllowedTools(repository, promptType);
1168
+ const disallowedTools = this.buildDisallowedTools(repository, promptType);
1169
+ console.log(`[EdgeWorker] Configured allowed tools for ${fullIssue.identifier}:`, allowedTools);
1170
+ if (disallowedTools.length > 0) {
1171
+ console.log(`[EdgeWorker] Configured disallowed tools for ${fullIssue.identifier}:`, disallowedTools);
1172
+ }
1173
+ // Create Claude runner with system prompt from assembly
1174
+ const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, assembly.systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
1175
+ labels);
1176
+ const runner = new ClaudeRunner(runnerConfig);
1177
+ // Store runner by comment ID
1178
+ agentSessionManager.addClaudeRunner(linearAgentActivitySessionId, runner);
1179
+ // Save state after mapping changes
1180
+ await this.savePersistedState();
1181
+ // Emit events using full Linear issue
1182
+ this.emit("session:started", fullIssue.id, fullIssue, repository.id);
1183
+ this.config.handlers?.onSessionStart?.(fullIssue.id, fullIssue, repository.id);
1184
+ // Update runner with version information (if available)
1185
+ if (systemPromptVersion) {
1159
1186
  runner.updatePromptVersions({
1160
- userPromptVersion,
1161
1187
  systemPromptVersion,
1162
1188
  });
1163
1189
  }
1164
- const promptType = isMentionTriggered && isLabelBasedPromptRequested
1165
- ? "label-based-prompt-command"
1166
- : isMentionTriggered
1167
- ? "mention"
1168
- : systemPrompt
1169
- ? "label-based"
1170
- : "fallback";
1171
- console.log(`[EdgeWorker] Initial prompt built successfully using ${promptType} workflow, length: ${prompt.length} characters`);
1190
+ // Log metadata for debugging
1191
+ console.log(`[EdgeWorker] Initial prompt built successfully - components: ${assembly.metadata.components.join(", ")}, type: ${assembly.metadata.promptType}, length: ${assembly.userPrompt.length} characters`);
1172
1192
  console.log(`[EdgeWorker] Starting Claude streaming session`);
1173
- const sessionInfo = await runner.startStreaming(prompt);
1193
+ const sessionInfo = await runner.startStreaming(assembly.userPrompt);
1174
1194
  console.log(`[EdgeWorker] Claude streaming session started: ${sessionInfo.sessionId}`);
1175
1195
  // Note: AgentSessionManager will be initialized automatically when the first system message
1176
1196
  // is received via handleClaudeMessage() callback
@@ -1235,38 +1255,8 @@ export class EdgeWorker extends EventEmitter {
1235
1255
  }
1236
1256
  }
1237
1257
  }
1238
- // Check if runner is actively streaming before routing
1239
- const existingRunner = session?.claudeRunner;
1240
- const isStreaming = existingRunner?.isStreaming() || false;
1241
- // Always route procedure for new comments, UNLESS actively streaming
1242
- if (!isStreaming) {
1243
- // Initialize procedure metadata using intelligent routing
1244
- if (!session.metadata) {
1245
- session.metadata = {};
1246
- }
1247
- // Post ephemeral "Routing..." thought
1248
- await agentSessionManager.postRoutingThought(linearAgentActivitySessionId);
1249
- // For prompted events, use the actual prompt content from the user
1250
- // Combine with issue context for better routing
1251
- if (!fullIssue) {
1252
- console.warn(`[EdgeWorker] Routing without full issue details for ${linearAgentActivitySessionId}`);
1253
- }
1254
- const promptBody = webhook.agentActivity.content.body;
1255
- const routingDecision = await this.procedureRouter.determineRoutine(promptBody.trim());
1256
- const selectedProcedure = routingDecision.procedure;
1257
- // Initialize procedure metadata in session (resets for each new comment)
1258
- this.procedureRouter.initializeProcedureMetadata(session, selectedProcedure);
1259
- // Post procedure selection result (replaces ephemeral routing thought)
1260
- await agentSessionManager.postProcedureSelectionThought(linearAgentActivitySessionId, selectedProcedure.name, routingDecision.classification);
1261
- // Log routing decision
1262
- console.log(`[EdgeWorker] Routing decision for ${linearAgentActivitySessionId} (prompted webhook, ${isNewSession ? "new" : "existing"} session):`);
1263
- console.log(` Classification: ${routingDecision.classification}`);
1264
- console.log(` Procedure: ${selectedProcedure.name}`);
1265
- console.log(` Reasoning: ${routingDecision.reasoning}`);
1266
- }
1267
- else {
1268
- console.log(`[EdgeWorker] Skipping routing for ${linearAgentActivitySessionId} - runner is actively streaming`);
1269
- }
1258
+ // Note: Routing and streaming check happens later in handlePromptWithStreamingCheck
1259
+ // after attachments are processed
1270
1260
  // Ensure session is not null after creation/retrieval
1271
1261
  if (!session) {
1272
1262
  throw new Error(`Failed to get or create session for agent activity session ${linearAgentActivitySessionId}`);
@@ -1285,6 +1275,8 @@ export class EdgeWorker extends EventEmitter {
1285
1275
  // Ensure directory exists
1286
1276
  await mkdir(attachmentsDir, { recursive: true });
1287
1277
  let attachmentManifest = "";
1278
+ let commentAuthor;
1279
+ let commentTimestamp;
1288
1280
  try {
1289
1281
  const result = await linearClient.client.rawRequest(`
1290
1282
  query GetComment($id: String!) {
@@ -1295,16 +1287,27 @@ export class EdgeWorker extends EventEmitter {
1295
1287
  updatedAt
1296
1288
  user {
1297
1289
  name
1290
+ displayName
1291
+ email
1298
1292
  id
1299
1293
  }
1300
1294
  }
1301
1295
  }
1302
1296
  `, { id: commentId });
1297
+ // Extract comment data
1298
+ const comment = result.data.comment;
1299
+ // Extract comment metadata for multi-player context
1300
+ if (comment) {
1301
+ const user = comment.user;
1302
+ commentAuthor =
1303
+ user?.displayName || user?.name || user?.email || "Unknown";
1304
+ commentTimestamp = comment.createdAt || new Date().toISOString();
1305
+ }
1303
1306
  // Count existing attachments
1304
1307
  const existingFiles = await readdir(attachmentsDir).catch(() => []);
1305
1308
  const existingAttachmentCount = existingFiles.filter((file) => file.startsWith("attachment_") || file.startsWith("image_")).length;
1306
1309
  // Download new attachments from the comment
1307
- const downloadResult = await this.downloadCommentAttachments(result.data.comment.body, attachmentsDir, repository.linearToken, existingAttachmentCount);
1310
+ const downloadResult = await this.downloadCommentAttachments(comment.body, attachmentsDir, repository.linearToken, existingAttachmentCount);
1308
1311
  if (downloadResult.totalNewAttachments > 0) {
1309
1312
  attachmentManifest = this.generateNewAttachmentManifest(downloadResult);
1310
1313
  }
@@ -1318,6 +1321,7 @@ export class EdgeWorker extends EventEmitter {
1318
1321
  if (stopSignal) {
1319
1322
  console.log(`[EdgeWorker] Received stop signal for agent activity session ${linearAgentActivitySessionId}`);
1320
1323
  // Stop the existing runner if it's active
1324
+ const existingRunner = session.claudeRunner;
1321
1325
  if (existingRunner) {
1322
1326
  existingRunner.stop();
1323
1327
  console.log(`[EdgeWorker] Stopped Claude session for agent activity session ${linearAgentActivitySessionId}`);
@@ -1327,34 +1331,13 @@ export class EdgeWorker extends EventEmitter {
1327
1331
  await agentSessionManager.createResponseActivity(linearAgentActivitySessionId, stopConfirmation);
1328
1332
  return; // Exit early - stop signal handled
1329
1333
  }
1330
- // Check if there's an existing runner for this comment thread
1331
- if (existingRunner?.isStreaming()) {
1332
- // Add comment with attachment manifest to existing stream
1333
- console.log(`[EdgeWorker] Adding comment to existing stream for agent activity session ${linearAgentActivitySessionId}`);
1334
- // Append attachment manifest to the prompt if we have one
1335
- let fullPrompt = promptBody;
1336
- if (attachmentManifest) {
1337
- fullPrompt = `${promptBody}\n\n${attachmentManifest}`;
1338
- }
1339
- existingRunner.addStreamMessage(fullPrompt);
1340
- return; // Exit early - comment has been added to stream
1341
- }
1342
- // Use the new resumeClaudeSession function
1334
+ // Use centralized streaming check and routing logic
1343
1335
  try {
1344
- await this.resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, []);
1336
+ await this.handlePromptWithStreamingCheck(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, [], // No additional allowed directories for regular continuation
1337
+ `prompted webhook (${isNewSession ? "new" : "existing"} session)`, commentAuthor, commentTimestamp);
1345
1338
  }
1346
1339
  catch (error) {
1347
- console.error("Failed to continue conversation:", error);
1348
- // Remove any partially created session
1349
- // this.sessionManager.removeSession(threadRootCommentId)
1350
- // this.commentToRepo.delete(threadRootCommentId)
1351
- // this.commentToIssue.delete(threadRootCommentId)
1352
- // // Start fresh for root comments, or fall back to issue assignment
1353
- // if (isRootComment) {
1354
- // await this.handleNewRootComment(issue, comment, repository)
1355
- // } else {
1356
- // await this.handleIssueAssigned(issue, repository)
1357
- // }
1340
+ console.error("Failed to handle prompted webhook:", error);
1358
1341
  }
1359
1342
  }
1360
1343
  /**
@@ -1585,10 +1568,12 @@ export class EdgeWorker extends EventEmitter {
1585
1568
  async buildMentionPrompt(issue, agentSession, attachmentManifest = "", guidance) {
1586
1569
  try {
1587
1570
  console.log(`[EdgeWorker] Building mention prompt for issue ${issue.identifier}`);
1588
- // Get the mention comment body
1571
+ // Get the mention comment metadata
1589
1572
  const mentionContent = agentSession.comment?.body || "";
1590
- // Build a simple prompt focused on the mention
1591
- let prompt = `You were mentioned in a Linear comment. Please help with the following request.
1573
+ const authorName = agentSession.creator?.name || agentSession.creator?.id || "Unknown";
1574
+ const timestamp = agentSession.createdAt || new Date().toISOString();
1575
+ // Build a focused prompt with comment metadata
1576
+ let prompt = `You were mentioned in a Linear comment on this issue:
1592
1577
 
1593
1578
  <linear_issue>
1594
1579
  <id>${issue.id}</id>
@@ -1597,11 +1582,15 @@ export class EdgeWorker extends EventEmitter {
1597
1582
  <url>${issue.url}</url>
1598
1583
  </linear_issue>
1599
1584
 
1600
- <mention_request>
1585
+ <mention_comment>
1586
+ <author>${authorName}</author>
1587
+ <timestamp>${timestamp}</timestamp>
1588
+ <content>
1601
1589
  ${mentionContent}
1602
- </mention_request>
1590
+ </content>
1591
+ </mention_comment>
1603
1592
 
1604
- IMPORTANT: You were specifically mentioned in the comment above. Focus on addressing the specific question or request in the mention. You can use the Linear MCP tools to fetch additional context about the issue if needed.`;
1593
+ Focus on addressing the specific request in the mention. You can use the Linear MCP tools to fetch additional context if needed.`;
1605
1594
  // Append agent guidance if present
1606
1595
  prompt += this.formatAgentGuidance(guidance);
1607
1596
  // Append attachment manifest if any
@@ -1817,17 +1806,17 @@ ${reply.body}
1817
1806
  * @param guidance Optional agent guidance rules from Linear
1818
1807
  * @returns Formatted prompt string
1819
1808
  */
1820
- async buildPromptV2(issue, repository, newComment, attachmentManifest = "", guidance) {
1821
- console.log(`[EdgeWorker] buildPromptV2 called for issue ${issue.identifier}${newComment ? " with new comment" : ""}`);
1809
+ async buildIssueContextPrompt(issue, repository, newComment, attachmentManifest = "", guidance) {
1810
+ console.log(`[EdgeWorker] buildIssueContextPrompt called for issue ${issue.identifier}${newComment ? " with new comment" : ""}`);
1822
1811
  try {
1823
1812
  // Use custom template if provided (repository-specific takes precedence)
1824
1813
  let templatePath = repository.promptTemplatePath ||
1825
1814
  this.config.features?.promptTemplatePath;
1826
- // If no custom template, use the v2 template
1815
+ // If no custom template, use the standard issue assigned user prompt template
1827
1816
  if (!templatePath) {
1828
1817
  const __filename = fileURLToPath(import.meta.url);
1829
1818
  const __dirname = dirname(__filename);
1830
- templatePath = resolve(__dirname, "../prompt-template-v2.md");
1819
+ templatePath = resolve(__dirname, "../prompts/standard-issue-assigned-user-prompt.md");
1831
1820
  }
1832
1821
  // Load the template
1833
1822
  console.log(`[EdgeWorker] Loading prompt template from: ${templatePath}`);
@@ -1913,8 +1902,8 @@ IMPORTANT: Focus specifically on addressing the new comment above. This is a new
1913
1902
  .replace(/{{new_comment_content}}/g, newComment.body || "");
1914
1903
  }
1915
1904
  else {
1916
- // Remove the new comment section entirely
1917
- prompt = prompt.replace(/{{#if new_comment}}[\s\S]*?{{\/if}}/g, "");
1905
+ // Remove the new comment section entirely (including preceding newlines)
1906
+ prompt = prompt.replace(/\n*{{#if new_comment}}[\s\S]*?{{\/if}}/g, "");
1918
1907
  }
1919
1908
  // Append agent guidance if present
1920
1909
  prompt += this.formatAgentGuidance(guidance);
@@ -2481,6 +2470,8 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2481
2470
  },
2482
2471
  onFeedbackDelivery: async (childSessionId, message) => {
2483
2472
  console.log(`[EdgeWorker] Processing feedback delivery to child session ${childSessionId}`);
2473
+ // Find the parent session ID for context
2474
+ const parentSessionId = this.childToParentAgentSession.get(childSessionId);
2484
2475
  // Find the repository containing the child session
2485
2476
  // We need to search all repositories for this child session
2486
2477
  let childRepo;
@@ -2503,22 +2494,62 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2503
2494
  return false;
2504
2495
  }
2505
2496
  console.log(`[EdgeWorker] Found child session - Issue: ${childSession.issueId}`);
2497
+ // Get parent session info for better context in the thought
2498
+ let parentIssueId;
2499
+ if (parentSessionId) {
2500
+ // Find parent session across all repositories
2501
+ for (const manager of this.agentSessionManagers.values()) {
2502
+ const parentSession = manager.getSession(parentSessionId);
2503
+ if (parentSession) {
2504
+ parentIssueId =
2505
+ parentSession.issue?.identifier || parentSession.issueId;
2506
+ break;
2507
+ }
2508
+ }
2509
+ }
2510
+ // Post thought to Linear showing feedback receipt
2511
+ const linearClient = this.linearClients.get(childRepo.id);
2512
+ if (linearClient) {
2513
+ const feedbackThought = parentIssueId
2514
+ ? `Received feedback from orchestrator (${parentIssueId}):\n\n---\n\n${message}\n\n---`
2515
+ : `Received feedback from orchestrator:\n\n---\n\n${message}\n\n---`;
2516
+ try {
2517
+ const result = await linearClient.createAgentActivity({
2518
+ agentSessionId: childSessionId,
2519
+ content: {
2520
+ type: "thought",
2521
+ body: feedbackThought,
2522
+ },
2523
+ });
2524
+ if (result.success) {
2525
+ console.log(`[EdgeWorker] Posted feedback receipt thought for child session ${childSessionId}`);
2526
+ }
2527
+ else {
2528
+ console.error(`[EdgeWorker] Failed to post feedback receipt thought:`, result);
2529
+ }
2530
+ }
2531
+ catch (error) {
2532
+ console.error(`[EdgeWorker] Error posting feedback receipt thought:`, error);
2533
+ }
2534
+ }
2506
2535
  // Format the feedback as a prompt for the child session with enhanced markdown formatting
2507
2536
  const feedbackPrompt = `## Received feedback from orchestrator\n\n---\n\n${message}\n\n---`;
2508
- // Resume the CHILD session with the feedback from the parent
2537
+ // Use centralized streaming check and routing logic
2509
2538
  // Important: We don't await the full session completion to avoid timeouts.
2510
2539
  // The feedback is delivered immediately when the session starts, so we can
2511
2540
  // return success right away while the session continues in the background.
2512
- this.resumeClaudeSession(childSession, childRepo, childSessionId, childAgentSessionManager, feedbackPrompt, "", // No attachment manifest for feedback
2541
+ console.log(`[EdgeWorker] Handling feedback delivery to child session ${childSessionId}`);
2542
+ this.handlePromptWithStreamingCheck(childSession, childRepo, childSessionId, childAgentSessionManager, feedbackPrompt, "", // No attachment manifest for feedback
2513
2543
  false, // Not a new session
2514
- [])
2544
+ [], // No additional allowed directories for feedback
2545
+ "give feedback to child")
2515
2546
  .then(() => {
2516
2547
  console.log(`[EdgeWorker] Child session ${childSessionId} completed processing feedback`);
2517
2548
  })
2518
2549
  .catch((error) => {
2519
- console.error(`[EdgeWorker] Failed to complete child session with feedback:`, error);
2550
+ console.error(`[EdgeWorker] Failed to process feedback in child session:`, error);
2520
2551
  });
2521
- // Return success immediately after initiating the session
2552
+ // Return success immediately after initiating the handling
2522
2553
  console.log(`[EdgeWorker] Feedback delivered successfully to child session ${childSessionId}`);
2523
2554
  return true;
2524
2555
  },
@@ -2562,22 +2593,264 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2562
2593
  }
2563
2594
  }
2564
2595
  /**
2565
- * Build prompt for a session - handles both new and existing sessions
2596
+ * Build the complete prompt for a session - shows full prompt assembly in one place
2597
+ *
2598
+ * New session prompt structure:
2599
+ * 1. Issue context (from buildIssueContextPrompt)
2600
+ * 2. Initial subroutine prompt (if procedure initialized)
2601
+ * 3. User comment
2602
+ *
2603
+ * Existing session prompt structure:
2604
+ * 1. User comment
2605
+ * 2. Attachment manifest (if present)
2566
2606
  */
2567
- async buildSessionPrompt(isNewSession, fullIssue, repository, promptBody, attachmentManifest) {
2568
- if (isNewSession) {
2569
- // For completely new sessions, create a complete initial prompt
2570
- const promptResult = await this.buildPromptV2(fullIssue, repository, undefined, attachmentManifest);
2571
- // Add the user's comment to the initial prompt
2572
- return `${promptResult.prompt}\n\nUser comment: ${promptBody}`;
2607
+ async buildSessionPrompt(isNewSession, session, fullIssue, repository, promptBody, attachmentManifest, commentAuthor, commentTimestamp) {
2608
+ // Fetch labels for system prompt determination
2609
+ const labels = await this.fetchIssueLabels(fullIssue);
2610
+ // Create input for unified prompt assembly
2611
+ const input = {
2612
+ session,
2613
+ fullIssue,
2614
+ repository,
2615
+ userComment: promptBody,
2616
+ commentAuthor,
2617
+ commentTimestamp,
2618
+ attachmentManifest,
2619
+ isNewSession,
2620
+ isStreaming: false, // This path is only for non-streaming prompts
2621
+ labels,
2622
+ };
2623
+ // Use unified prompt assembly
2624
+ const assembly = await this.assemblePrompt(input);
2625
+ // Log metadata for debugging
2626
+ console.log(`[EdgeWorker] Built prompt - components: ${assembly.metadata.components.join(", ")}, type: ${assembly.metadata.promptType}`);
2627
+ return assembly.userPrompt;
2628
+ }
2629
+ /**
2630
+ * Assemble a complete prompt - unified entry point for all prompt building
2631
+ * This method contains all prompt assembly logic in one place
2632
+ */
2633
+ async assemblePrompt(input) {
2634
+ // If actively streaming, just pass through the comment
2635
+ if (input.isStreaming) {
2636
+ return this.buildStreamingPrompt(input);
2637
+ }
2638
+ // If new session, build full prompt with all components
2639
+ if (input.isNewSession) {
2640
+ return this.buildNewSessionPrompt(input);
2641
+ }
2642
+ // Existing session continuation - just user comment + attachments
2643
+ return this.buildContinuationPrompt(input);
2644
+ }
2645
+ /**
2646
+ * Build prompt for actively streaming session - pass through user comment as-is
2647
+ */
2648
+ buildStreamingPrompt(input) {
2649
+ const components = ["user-comment"];
2650
+ if (input.attachmentManifest) {
2651
+ components.push("attachment-manifest");
2652
+ }
2653
+ const parts = [input.userComment];
2654
+ if (input.attachmentManifest) {
2655
+ parts.push(input.attachmentManifest);
2656
+ }
2657
+ return {
2658
+ systemPrompt: undefined,
2659
+ userPrompt: parts.join("\n\n"),
2660
+ metadata: {
2661
+ components,
2662
+ promptType: "continuation",
2663
+ isNewSession: false,
2664
+ isStreaming: true,
2665
+ },
2666
+ };
2667
+ }
2668
+ /**
2669
+ * Build prompt for new session - includes issue context, subroutine prompt, and user comment
2670
+ */
2671
+ async buildNewSessionPrompt(input) {
2672
+ const components = [];
2673
+ const parts = [];
2674
+ // 1. Determine system prompt from labels
2675
+ // Only for delegation (not mentions) or when /label-based-prompt is requested
2676
+ let labelBasedSystemPrompt;
2677
+ if (!input.isMentionTriggered || input.isLabelBasedPromptRequested) {
2678
+ labelBasedSystemPrompt = await this.determineSystemPromptForAssembly(input.labels || [], input.repository);
2679
+ }
2680
+ // 2. Determine system prompt based on prompt type
2681
+ // Label-based: Use only the label-based system prompt
2682
+ // Fallback: Use scenarios system prompt (shared instructions)
2683
+ let systemPrompt;
2684
+ if (labelBasedSystemPrompt) {
2685
+ // Use label-based system prompt as-is (no shared instructions)
2686
+ systemPrompt = labelBasedSystemPrompt;
2573
2687
  }
2574
2688
  else {
2575
- // For existing sessions, just use the comment with attachment manifest
2576
- const manifestSuffix = attachmentManifest
2577
- ? `\n\n${attachmentManifest}`
2578
- : "";
2579
- return `${promptBody}${manifestSuffix}`;
2689
+ // Use scenarios system prompt for fallback cases
2690
+ const sharedInstructions = await this.loadSharedInstructions();
2691
+ systemPrompt = sharedInstructions;
2692
+ }
2693
+ // 3. Build issue context using appropriate builder
2694
+ // Use label-based prompt ONLY if we have a label-based system prompt
2695
+ const promptType = this.determinePromptType(input, !!labelBasedSystemPrompt);
2696
+ const issueContext = await this.buildIssueContextForPromptAssembly(input.fullIssue, input.repository, promptType, input.attachmentManifest, input.guidance, input.agentSession);
2697
+ parts.push(issueContext.prompt);
2698
+ components.push("issue-context");
2699
+ // 4. Load and append initial subroutine prompt
2700
+ const currentSubroutine = this.procedureRouter.getCurrentSubroutine(input.session);
2701
+ let subroutineName;
2702
+ if (currentSubroutine) {
2703
+ const subroutinePrompt = await this.loadSubroutinePrompt(currentSubroutine);
2704
+ if (subroutinePrompt) {
2705
+ parts.push(subroutinePrompt);
2706
+ components.push("subroutine-prompt");
2707
+ subroutineName = currentSubroutine.name;
2708
+ }
2709
+ }
2710
+ // 5. Add user comment (if present)
2711
+ // Skip for mention-triggered prompts since the comment is already in the mention block
2712
+ if (input.userComment.trim() && !input.isMentionTriggered) {
2713
+ // If we have author/timestamp metadata, include it for multi-player context
2714
+ if (input.commentAuthor || input.commentTimestamp) {
2715
+ const author = input.commentAuthor || "Unknown";
2716
+ const timestamp = input.commentTimestamp || new Date().toISOString();
2717
+ parts.push(`<user_comment>
2718
+ <author>${author}</author>
2719
+ <timestamp>${timestamp}</timestamp>
2720
+ <content>
2721
+ ${input.userComment}
2722
+ </content>
2723
+ </user_comment>`);
2724
+ }
2725
+ else {
2726
+ // Legacy format without metadata
2727
+ parts.push(`<user_comment>\n${input.userComment}\n</user_comment>`);
2728
+ }
2729
+ components.push("user-comment");
2730
+ }
2731
+ // 6. Add guidance rules (if present)
2732
+ if (input.guidance && input.guidance.length > 0) {
2733
+ components.push("guidance-rules");
2734
+ }
2735
+ return {
2736
+ systemPrompt,
2737
+ userPrompt: parts.join("\n\n"),
2738
+ metadata: {
2739
+ components,
2740
+ subroutineName,
2741
+ promptType,
2742
+ isNewSession: true,
2743
+ isStreaming: false,
2744
+ },
2745
+ };
2746
+ }
2747
+ /**
2748
+ * Build prompt for existing session continuation - user comment and attachments only
2749
+ */
2750
+ buildContinuationPrompt(input) {
2751
+ const components = ["user-comment"];
2752
+ if (input.attachmentManifest) {
2753
+ components.push("attachment-manifest");
2754
+ }
2755
+ // Wrap comment in XML with author and timestamp for multi-player context
2756
+ const author = input.commentAuthor || "Unknown";
2757
+ const timestamp = input.commentTimestamp || new Date().toISOString();
2758
+ const commentXml = `<new_comment>
2759
+ <author>${author}</author>
2760
+ <timestamp>${timestamp}</timestamp>
2761
+ <content>
2762
+ ${input.userComment}
2763
+ </content>
2764
+ </new_comment>`;
2765
+ const parts = [commentXml];
2766
+ if (input.attachmentManifest) {
2767
+ parts.push(input.attachmentManifest);
2580
2768
  }
2769
+ return {
2770
+ systemPrompt: undefined,
2771
+ userPrompt: parts.join("\n\n"),
2772
+ metadata: {
2773
+ components,
2774
+ promptType: "continuation",
2775
+ isNewSession: false,
2776
+ isStreaming: false,
2777
+ },
2778
+ };
2779
+ }
2780
+ /**
2781
+ * Determine the prompt type based on input flags and system prompt availability
2782
+ */
2783
+ determinePromptType(input, hasSystemPrompt) {
2784
+ if (input.isMentionTriggered && input.isLabelBasedPromptRequested) {
2785
+ return "label-based-prompt-command";
2786
+ }
2787
+ if (input.isMentionTriggered) {
2788
+ return "mention";
2789
+ }
2790
+ if (hasSystemPrompt) {
2791
+ return "label-based";
2792
+ }
2793
+ return "fallback";
2794
+ }
2795
+ /**
2796
+ * Load a subroutine prompt file
2797
+ * Extracted helper to make prompt assembly more readable
2798
+ */
2799
+ async loadSubroutinePrompt(subroutine) {
2800
+ const __filename = fileURLToPath(import.meta.url);
2801
+ const __dirname = dirname(__filename);
2802
+ const subroutinePromptPath = join(__dirname, "prompts", subroutine.promptPath);
2803
+ try {
2804
+ const prompt = await readFile(subroutinePromptPath, "utf-8");
2805
+ console.log(`[EdgeWorker] Loaded ${subroutine.name} subroutine prompt (${prompt.length} characters)`);
2806
+ return prompt;
2807
+ }
2808
+ catch (error) {
2809
+ console.warn(`[EdgeWorker] Failed to load subroutine prompt from ${subroutinePromptPath}:`, error);
2810
+ return null;
2811
+ }
2812
+ }
2813
+ /**
2814
+ * Load shared instructions that get appended to all system prompts
2815
+ */
2816
+ async loadSharedInstructions() {
2817
+ const __filename = fileURLToPath(import.meta.url);
2818
+ const __dirname = dirname(__filename);
2819
+ const instructionsPath = join(__dirname, "..", "prompts", "todolist-system-prompt-extension.md");
2820
+ try {
2821
+ const instructions = await readFile(instructionsPath, "utf-8");
2822
+ return instructions;
2823
+ }
2824
+ catch (error) {
2825
+ console.error(`[EdgeWorker] Failed to load shared instructions from ${instructionsPath}:`, error);
2826
+ return ""; // Return empty string if file can't be loaded
2827
+ }
2828
+ }
2829
+ /**
2830
+ * Adapter method for prompt assembly - extracts just the prompt string
2831
+ */
2832
+ async determineSystemPromptForAssembly(labels, repository) {
2833
+ const result = await this.determineSystemPromptFromLabels(labels, repository);
2834
+ return result?.prompt;
2835
+ }
2836
+ /**
2837
+ * Adapter method for prompt assembly - routes to appropriate issue context builder
2838
+ */
2839
+ async buildIssueContextForPromptAssembly(issue, repository, promptType, attachmentManifest, guidance, agentSession) {
2840
+ // Delegate to appropriate builder based on promptType
2841
+ if (promptType === "mention") {
2842
+ if (!agentSession) {
2843
+ throw new Error("agentSession is required for mention-triggered prompts");
2844
+ }
2845
+ return this.buildMentionPrompt(issue, agentSession, attachmentManifest, guidance);
2846
+ }
2847
+ if (promptType === "label-based" ||
2848
+ promptType === "label-based-prompt-command") {
2849
+ return this.buildLabelBasedPrompt(issue, repository, attachmentManifest, guidance);
2850
+ }
2851
+ // Fallback to standard issue context
2852
+ return this.buildIssueContextPrompt(issue, repository, undefined, // No new comment for initial prompt assembly
2853
+ attachmentManifest, guidance);
2581
2854
  }
2582
2855
  /**
2583
2856
  * Build Claude runner configuration with common settings
@@ -2879,6 +3152,78 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2879
3152
  console.error(`[EdgeWorker] Error posting parent resumption acknowledgment:`, error);
2880
3153
  }
2881
3154
  }
3155
+ /**
3156
+ * Re-route procedure for a session (used when resuming from child or give feedback)
3157
+ * This ensures the currentSubroutine is reset to avoid suppression issues
3158
+ */
3159
+ async rerouteProcedureForSession(session, linearAgentActivitySessionId, agentSessionManager, promptBody) {
3160
+ // Initialize procedure metadata using intelligent routing
3161
+ if (!session.metadata) {
3162
+ session.metadata = {};
3163
+ }
3164
+ // Post ephemeral "Routing..." thought
3165
+ await agentSessionManager.postRoutingThought(linearAgentActivitySessionId);
3166
+ // Route based on the prompt content
3167
+ const routingDecision = await this.procedureRouter.determineRoutine(promptBody.trim());
3168
+ const selectedProcedure = routingDecision.procedure;
3169
+ // Initialize procedure metadata in session (resets currentSubroutine)
3170
+ this.procedureRouter.initializeProcedureMetadata(session, selectedProcedure);
3171
+ // Post procedure selection result (replaces ephemeral routing thought)
3172
+ await agentSessionManager.postProcedureSelectionThought(linearAgentActivitySessionId, selectedProcedure.name, routingDecision.classification);
3173
+ // Log routing decision
3174
+ console.log(`[EdgeWorker] Routing decision for ${linearAgentActivitySessionId}:`);
3175
+ console.log(` Classification: ${routingDecision.classification}`);
3176
+ console.log(` Procedure: ${selectedProcedure.name}`);
3177
+ console.log(` Reasoning: ${routingDecision.reasoning}`);
3178
+ }
3179
+ /**
3180
+ * Handle prompt with streaming check - centralized logic for all input types
3181
+ *
3182
+ * This method implements the unified pattern for handling prompts:
3183
+ * 1. Check if runner is actively streaming
3184
+ * 2. Route procedure if NOT streaming (resets currentSubroutine)
3185
+ * 3. Add to stream if streaming, OR resume session if not
3186
+ *
3187
+ * @param session The Cyrus agent session
3188
+ * @param repository Repository configuration
3189
+ * @param linearAgentActivitySessionId Linear agent activity session ID
3190
+ * @param agentSessionManager Agent session manager instance
3191
+ * @param promptBody The prompt text to send
3192
+ * @param attachmentManifest Optional attachment manifest to append
3193
+ * @param isNewSession Whether this is a new session
3194
+ * @param additionalAllowedDirs Additional directories to allow access to
3195
+ * @param logContext Context string for logging (e.g., "prompted webhook", "parent resume")
3196
+ * @returns true if message was added to stream, false if session was resumed
3197
+ */
3198
+ async handlePromptWithStreamingCheck(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, additionalAllowedDirs, logContext, commentAuthor, commentTimestamp) {
3199
+ // Check if runner is actively streaming before routing
3200
+ const existingRunner = session.claudeRunner;
3201
+ const isStreaming = existingRunner?.isStreaming() || false;
3202
+ // Always route procedure for new input, UNLESS actively streaming
3203
+ if (!isStreaming) {
3204
+ await this.rerouteProcedureForSession(session, linearAgentActivitySessionId, agentSessionManager, promptBody);
3205
+ console.log(`[EdgeWorker] Routed procedure for ${logContext}`);
3206
+ }
3207
+ else {
3208
+ console.log(`[EdgeWorker] Skipping routing for ${linearAgentActivitySessionId} (${logContext}) - runner is actively streaming`);
3209
+ }
3210
+ // Handle streaming case - add message to existing stream
3211
+ if (existingRunner?.isStreaming()) {
3212
+ console.log(`[EdgeWorker] Adding prompt to existing stream for ${linearAgentActivitySessionId} (${logContext})`);
3213
+ // Append attachment manifest to the prompt if we have one
3214
+ let fullPrompt = promptBody;
3215
+ if (attachmentManifest) {
3216
+ fullPrompt = `${promptBody}\n\n${attachmentManifest}`;
3217
+ }
3218
+ existingRunner.addStreamMessage(fullPrompt);
3219
+ return true; // Message added to stream
3220
+ }
3221
+ // Not streaming - resume/start session
3222
+ console.log(`[EdgeWorker] Resuming Claude session for ${linearAgentActivitySessionId} (${logContext})`);
3223
+ await this.resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, additionalAllowedDirs, undefined, // maxTurns
3224
+ commentAuthor, commentTimestamp);
3225
+ return false; // Session was resumed
3226
+ }
2882
3227
  /**
2883
3228
  * Post thought about system prompt selection based on labels
2884
3229
  */
@@ -2975,7 +3320,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2975
3320
  * @param attachmentManifest Optional attachment manifest
2976
3321
  * @param isNewSession Whether this is a new session
2977
3322
  */
2978
- async resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest = "", isNewSession = false, additionalAllowedDirectories = [], maxTurns) {
3323
+ async resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest = "", isNewSession = false, additionalAllowedDirectories = [], maxTurns, commentAuthor, commentTimestamp) {
2979
3324
  // Check for existing runner
2980
3325
  const existingRunner = session.claudeRunner;
2981
3326
  // If there's an existing streaming runner, add to it
@@ -3027,7 +3372,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
3027
3372
  // Save state
3028
3373
  await this.savePersistedState();
3029
3374
  // Prepare the full prompt
3030
- const fullPrompt = await this.buildSessionPrompt(isNewSession, fullIssue, repository, promptBody, attachmentManifest);
3375
+ const fullPrompt = await this.buildSessionPrompt(isNewSession, session, fullIssue, repository, promptBody, attachmentManifest, commentAuthor, commentTimestamp);
3031
3376
  // Start streaming session
3032
3377
  try {
3033
3378
  await runner.startStreaming(fullPrompt);