@calltelemetry/openclaw-linear 0.8.5 → 0.8.7

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.
@@ -16,6 +16,7 @@ import { initiatePlanningSession, handlePlannerTurn, runPlanAudit } from "./plan
16
16
  import { startProjectDispatch } from "./dag-dispatch.js";
17
17
  import { emitDiagnostic } from "../infra/observability.js";
18
18
  import { classifyIntent } from "./intent-classify.js";
19
+ import { extractGuidance, formatGuidanceAppendix, cacheGuidanceForTeam, getCachedGuidanceForTeam, isGuidanceEnabled, _resetGuidanceCacheForTesting } from "./guidance.js";
19
20
 
20
21
  // ── Agent profiles (loaded from config, no hardcoded names) ───────
21
22
  interface AgentProfile {
@@ -101,6 +102,7 @@ export function _resetForTesting(): void {
101
102
  profilesCache = null;
102
103
  linearApiCache = null;
103
104
  lastSweep = Date.now();
105
+ _resetGuidanceCacheForTesting();
104
106
  }
105
107
 
106
108
  /** @internal — test-only; add an issue ID to the activeRuns set. */
@@ -236,6 +238,8 @@ export async function handleLinearWebhook(
236
238
  }
237
239
 
238
240
  const payload = body.value;
241
+ const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
242
+
239
243
  // Debug: log full payload structure for diagnosing webhook types
240
244
  const payloadKeys = Object.keys(payload).join(", ");
241
245
  api.logger.info(`Linear webhook received: type=${payload.type} action=${payload.action} keys=[${payloadKeys}]`);
@@ -295,18 +299,33 @@ export async function handleLinearWebhook(
295
299
  return true;
296
300
  }
297
301
 
298
- const agentId = resolveAgentId(api);
299
302
  const previousComments = payload.previousComments ?? [];
300
- const guidance = payload.guidance;
303
+ const guidanceCtx = extractGuidance(payload);
301
304
 
302
- api.logger.info(`AgentSession created: ${session.id} for issue ${issue?.identifier ?? issue?.id} (comments: ${previousComments.length}, guidance: ${guidance ? "yes" : "no"})`)
303
-
304
- // Extract the user's latest message from previousComments
305
- // The last comment is the most recent user message
305
+ // Extract the user's latest message from previousComments (NOT from guidance)
306
306
  const lastComment = previousComments.length > 0
307
307
  ? previousComments[previousComments.length - 1]
308
308
  : null;
309
- const userMessage = lastComment?.body ?? guidance ?? "";
309
+ const userMessage = lastComment?.body ?? "";
310
+
311
+ // Route to the mentioned agent if the user's message contains an @mention.
312
+ // AgentSessionEvent doesn't carry mention routing — we must check manually.
313
+ const profiles = loadAgentProfiles();
314
+ const mentionPattern = buildMentionPattern(profiles);
315
+ let agentId = resolveAgentId(api);
316
+ if (mentionPattern && userMessage) {
317
+ const mentionMatch = userMessage.match(mentionPattern);
318
+ if (mentionMatch) {
319
+ const alias = mentionMatch[1];
320
+ const resolved = resolveAgentFromAlias(alias, profiles);
321
+ if (resolved) {
322
+ api.logger.info(`AgentSession routed to ${resolved.agentId} via @${alias} mention`);
323
+ agentId = resolved.agentId;
324
+ }
325
+ }
326
+ }
327
+
328
+ api.logger.info(`AgentSession created: ${session.id} for issue ${issue?.identifier ?? issue?.id} agent=${agentId} (comments: ${previousComments.length}, guidance: ${guidanceCtx.guidance ? "yes" : "no"})`);
310
329
 
311
330
  // Fetch full issue details
312
331
  let enrichedIssue: any = issue;
@@ -318,6 +337,13 @@ export async function handleLinearWebhook(
318
337
 
319
338
  const description = enrichedIssue?.description ?? issue?.description ?? "(no description)";
320
339
 
340
+ // Cache guidance for this team (enables Comment webhook paths)
341
+ const teamId = enrichedIssue?.team?.id;
342
+ if (guidanceCtx.guidance && teamId) cacheGuidanceForTeam(teamId, guidanceCtx.guidance);
343
+ const guidanceAppendix = isGuidanceEnabled(pluginConfig as Record<string, unknown> | undefined, teamId)
344
+ ? formatGuidanceAppendix(guidanceCtx.guidance)
345
+ : "";
346
+
321
347
  // Build conversation context from previous comments
322
348
  const commentContext = previousComments
323
349
  .slice(-5)
@@ -331,22 +357,24 @@ export async function handleLinearWebhook(
331
357
  const toolAccessLines = isTriaged
332
358
  ? [
333
359
  `**Tool access:**`,
334
- `- \`linearis\` CLI: Full access. You can read, update, close, and comment on issues. Use \`linearis issues update ${issueRef} --state <state>\` to change status, \`linearis issues update ${issueRef} --priority <1-4>\` to set priority, etc.`,
335
- `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
360
+ `- \`linear_issues\` tool: Full access. Use action="read" with issueId="${issueRef}" to get details, action="create" to create issues (with parentIssueId to create sub-issues for granular work breakdown), action="update" with status/priority/labels/estimate to modify issues, action="comment" to post comments, action="list_states" to see available workflow states.`,
361
+ `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linear_issues.`,
336
362
  `- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
337
363
  `- Standard tools: exec, read, edit, write, web_search, etc.`,
364
+ ``,
365
+ `**Sub-issue guidance:** When a task is too large or has multiple distinct parts, break it into sub-issues using action="create" with parentIssueId="${issueRef}". Each sub-issue should be an atomic, independently testable unit of work with its own acceptance criteria. This enables parallel dispatch and clearer progress tracking.`,
338
366
  ]
339
367
  : [
340
368
  `**Tool access:**`,
341
- `- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${issueRef}\`), list issues (\`linearis issues list\`), and search (\`linearis issues search "..."\`). Do NOT use linearis to update, close, comment, or modify issues.`,
342
- `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
369
+ `- \`linear_issues\` tool: READ ONLY. Use action="read" with issueId="${issueRef}" to get details, action="list_states"/"list_labels" for metadata. Do NOT use action="update", action="create", or action="comment".`,
370
+ `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linear_issues.`,
343
371
  `- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
344
372
  `- Standard tools: exec, read, edit, write, web_search, etc.`,
345
373
  ];
346
374
 
347
375
  const roleLines = isTriaged
348
376
  ? [`**Your role:** Orchestrator with full Linear access. You can update issue fields, change status, and dispatch work via \`code_run\`. Do NOT post comments yourself — the handler posts your text output.`]
349
- : [`**Your role:** You are the dispatcher. For any coding or implementation work, use \`code_run\` to dispatch it. Workers return text output. You summarize results. You do NOT update issue status or post linearis comments — the audit system handles lifecycle transitions.`];
377
+ : [`**Your role:** You are the dispatcher. For any coding or implementation work, use \`code_run\` to dispatch it. Workers return text output. You summarize results. You do NOT update issue status or post comments via linear_issues — the audit system handles lifecycle transitions.`];
350
378
 
351
379
  const message = [
352
380
  `You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
@@ -364,6 +392,7 @@ export async function handleLinearWebhook(
364
392
  userMessage ? `\n**Latest message:**\n> ${userMessage}` : "",
365
393
  ``,
366
394
  `Respond to the user's request. For work requests, dispatch via \`code_run\` and summarize the result. Be concise and action-oriented.`,
395
+ guidanceAppendix,
367
396
  ].filter(Boolean).join("\n");
368
397
 
369
398
  // Run agent directly (non-blocking)
@@ -473,13 +502,11 @@ export async function handleLinearWebhook(
473
502
  return true;
474
503
  }
475
504
 
476
- // Extract user message from the activity or prompt context
477
- const promptContext = payload.promptContext;
505
+ // Extract user message from the activity (not from promptContext which contains issue data + guidance)
506
+ const guidanceCtxPrompted = extractGuidance(payload);
478
507
  const userMessage =
479
508
  activity?.content?.body ??
480
509
  activity?.body ??
481
- promptContext?.message ??
482
- promptContext ??
483
510
  "";
484
511
 
485
512
  if (!userMessage || typeof userMessage !== "string" || userMessage.trim().length === 0) {
@@ -493,9 +520,23 @@ export async function handleLinearWebhook(
493
520
  return true;
494
521
  }
495
522
 
496
- api.logger.info(`AgentSession prompted (follow-up): ${session.id} issue=${issue?.identifier ?? issue?.id} message="${userMessage.slice(0, 80)}..."`);
523
+ // Route to mentioned agent if user's message contains an @mention (one-time detour)
524
+ const promptedProfiles = loadAgentProfiles();
525
+ const promptedMentionPattern = buildMentionPattern(promptedProfiles);
526
+ let agentId = resolveAgentId(api);
527
+ if (promptedMentionPattern && userMessage) {
528
+ const mentionMatch = userMessage.match(promptedMentionPattern);
529
+ if (mentionMatch) {
530
+ const alias = mentionMatch[1];
531
+ const resolved = resolveAgentFromAlias(alias, promptedProfiles);
532
+ if (resolved) {
533
+ api.logger.info(`AgentSession prompted: routed to ${resolved.agentId} via @${alias} mention`);
534
+ agentId = resolved.agentId;
535
+ }
536
+ }
537
+ }
497
538
 
498
- const agentId = resolveAgentId(api);
539
+ api.logger.info(`AgentSession prompted (follow-up): ${session.id} issue=${issue?.identifier ?? issue?.id} agent=${agentId} message="${userMessage.slice(0, 80)}..."`);
499
540
 
500
541
  // Run agent for follow-up (non-blocking)
501
542
  activeRuns.add(issue.id);
@@ -514,6 +555,13 @@ export async function handleLinearWebhook(
514
555
 
515
556
  const description = enrichedIssue?.description ?? issue?.description ?? "(no description)";
516
557
 
558
+ // Resolve guidance for follow-up
559
+ const followUpTeamId = enrichedIssue?.team?.id;
560
+ if (guidanceCtxPrompted.guidance && followUpTeamId) cacheGuidanceForTeam(followUpTeamId, guidanceCtxPrompted.guidance);
561
+ const followUpGuidanceAppendix = isGuidanceEnabled(pluginConfig as Record<string, unknown> | undefined, followUpTeamId)
562
+ ? formatGuidanceAppendix(guidanceCtxPrompted.guidance ?? (followUpTeamId ? getCachedGuidanceForTeam(followUpTeamId) : null))
563
+ : "";
564
+
517
565
  // Build context from recent comments
518
566
  const recentComments = enrichedIssue?.comments?.nodes ?? [];
519
567
  const commentContext = recentComments
@@ -528,15 +576,17 @@ export async function handleLinearWebhook(
528
576
  const followUpToolAccessLines = followUpIsTriaged
529
577
  ? [
530
578
  `**Tool access:**`,
531
- `- \`linearis\` CLI: Full access. You can read, update, close, and comment on issues. Use \`linearis issues update ${followUpIssueRef} --state <state>\` to change status, \`linearis issues update ${followUpIssueRef} --priority <1-4>\` to set priority, etc.`,
532
- `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
579
+ `- \`linear_issues\` tool: Full access. Use action="read" with issueId="${followUpIssueRef}" to get details, action="create" to create issues (with parentIssueId to create sub-issues for granular work breakdown), action="update" with status/priority/labels/estimate to modify issues, action="comment" to post comments, action="list_states" to see available workflow states.`,
580
+ `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linear_issues.`,
533
581
  `- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
534
582
  `- Standard tools: exec, read, edit, write, web_search, etc.`,
583
+ ``,
584
+ `**Sub-issue guidance:** When a task is too large or has multiple distinct parts, break it into sub-issues using action="create" with parentIssueId="${followUpIssueRef}". Each sub-issue should be an atomic, independently testable unit of work with its own acceptance criteria. This enables parallel dispatch and clearer progress tracking.`,
535
585
  ]
536
586
  : [
537
587
  `**Tool access:**`,
538
- `- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${followUpIssueRef}\`), list, and search. Do NOT use linearis to update, close, comment, or modify issues.`,
539
- `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
588
+ `- \`linear_issues\` tool: READ ONLY. Use action="read" with issueId="${followUpIssueRef}" to get details, action="list_states"/"list_labels" for metadata. Do NOT use action="update", action="create", or action="comment".`,
589
+ `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linear_issues.`,
540
590
  `- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
541
591
  `- Standard tools: exec, read, edit, write, web_search, etc.`,
542
592
  ];
@@ -561,6 +611,7 @@ export async function handleLinearWebhook(
561
611
  `\n**User's follow-up message:**\n> ${userMessage}`,
562
612
  ``,
563
613
  `Respond to the user's follow-up. For work requests, dispatch via \`code_run\`. Be concise and action-oriented.`,
614
+ followUpGuidanceAppendix,
564
615
  ].filter(Boolean).join("\n");
565
616
 
566
617
  setActiveSession({
@@ -635,7 +686,6 @@ export async function handleLinearWebhook(
635
686
  const commentBody = comment?.body ?? "";
636
687
  const commentor = comment?.user?.name ?? "Unknown";
637
688
  const issue = comment?.issue ?? payload.issue;
638
- const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
639
689
 
640
690
  if (!issue?.id) {
641
691
  api.logger.error("Comment webhook: missing issue data");
@@ -1050,6 +1100,13 @@ export async function handleLinearWebhook(
1050
1100
  ? `**Created by:** ${creatorName} (${creatorEmail})`
1051
1101
  : `**Created by:** ${creatorName}`;
1052
1102
 
1103
+ // Look up cached guidance for triage
1104
+ const triageTeamId = enrichedIssue?.team?.id ?? issue?.team?.id;
1105
+ const triageGuidance = triageTeamId ? getCachedGuidanceForTeam(triageTeamId) : null;
1106
+ const triageGuidanceAppendix = isGuidanceEnabled(pluginConfig as Record<string, unknown> | undefined, triageTeamId)
1107
+ ? formatGuidanceAppendix(triageGuidance)
1108
+ : "";
1109
+
1053
1110
  const message = [
1054
1111
  `IMPORTANT: You are triaging a new Linear issue. You MUST respond with a JSON block containing your triage decisions, followed by your assessment as plain text.`,
1055
1112
  ``,
@@ -1086,6 +1143,7 @@ export async function handleLinearWebhook(
1086
1143
  `IMPORTANT: Only reference real users from the issue data above. Do NOT fabricate or guess user names, emails, or identities. The issue creator is shown in the "Created by" field.`,
1087
1144
  ``,
1088
1145
  `Then write your full assessment as markdown below the JSON block.`,
1146
+ triageGuidanceAppendix,
1089
1147
  ].filter(Boolean).join("\n");
1090
1148
 
1091
1149
  const sessionId = `linear-triage-${issue.id}-${Date.now()}`;
@@ -1237,6 +1295,13 @@ async function dispatchCommentToAgent(
1237
1295
  .map((c: any) => `**${c.user?.name ?? "Unknown"}**: ${(c.body ?? "").slice(0, 200)}`)
1238
1296
  .join("\n");
1239
1297
 
1298
+ // Look up cached guidance for this team (Comment webhooks don't carry guidance)
1299
+ const commentTeamId = enrichedIssue?.team?.id;
1300
+ const cachedGuidance = commentTeamId ? getCachedGuidanceForTeam(commentTeamId) : null;
1301
+ const commentGuidanceAppendix = isGuidanceEnabled(pluginConfig, commentTeamId)
1302
+ ? formatGuidanceAppendix(cachedGuidance)
1303
+ : "";
1304
+
1240
1305
  const issueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
1241
1306
  const stateType = enrichedIssue?.state?.type ?? "";
1242
1307
  const isTriaged = stateType === "started" || stateType === "completed" || stateType === "canceled";
@@ -1244,14 +1309,16 @@ async function dispatchCommentToAgent(
1244
1309
  const toolAccessLines = isTriaged
1245
1310
  ? [
1246
1311
  `**Tool access:**`,
1247
- `- \`linearis\` CLI: Full access. You can read, update, close, and comment on issues. Use \`linearis issues update ${issueRef} --state <state>\` to change status, \`linearis issues update ${issueRef} --priority <1-4>\` to set priority, etc.`,
1248
- `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
1312
+ `- \`linear_issues\` tool: Full access. Use action="read" with issueId="${issueRef}" to get details, action="create" to create issues (with parentIssueId to create sub-issues for granular work breakdown), action="update" with status/priority/labels/estimate to modify issues, action="comment" to post comments, action="list_states" to see available workflow states.`,
1313
+ `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linear_issues.`,
1249
1314
  `- Standard tools: exec, read, edit, write, web_search, etc.`,
1315
+ ``,
1316
+ `**Sub-issue guidance:** When a task is too large or has multiple distinct parts, break it into sub-issues using action="create" with parentIssueId="${issueRef}". Each sub-issue should be an atomic, independently testable unit of work with its own acceptance criteria. This enables parallel dispatch and clearer progress tracking.`,
1250
1317
  ]
1251
1318
  : [
1252
1319
  `**Tool access:**`,
1253
- `- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${issueRef}\`), list, and search. Do NOT use linearis to update, close, comment, or modify issues.`,
1254
- `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
1320
+ `- \`linear_issues\` tool: READ ONLY. Use action="read" with issueId="${issueRef}" to get details, action="list_states"/"list_labels" for metadata. Do NOT use action="update", action="create", or action="comment".`,
1321
+ `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linear_issues.`,
1255
1322
  `- Standard tools: exec, read, edit, write, web_search, etc.`,
1256
1323
  ];
1257
1324
 
@@ -1278,6 +1345,7 @@ async function dispatchCommentToAgent(
1278
1345
  `IMPORTANT: Only reference real users from the issue data above. Do NOT fabricate or guess user names, emails, or identities.`,
1279
1346
  ``,
1280
1347
  `Respond concisely. For work requests, dispatch via \`code_run\` and summarize the result.`,
1348
+ commentGuidanceAppendix,
1281
1349
  ].filter(Boolean).join("\n");
1282
1350
 
1283
1351
  // Dispatch with session lifecycle
@@ -1416,6 +1484,12 @@ async function handleCloseIssue(
1416
1484
  .map((c: any) => `**${c.user?.name ?? "Unknown"}**: ${(c.body ?? "").slice(0, 300)}`)
1417
1485
  .join("\n");
1418
1486
 
1487
+ // Look up cached guidance
1488
+ const closeGuidance = teamId ? getCachedGuidanceForTeam(teamId) : null;
1489
+ const closeGuidanceAppendix = isGuidanceEnabled(pluginConfig, teamId)
1490
+ ? formatGuidanceAppendix(closeGuidance)
1491
+ : "";
1492
+
1419
1493
  const message = [
1420
1494
  `You are writing a closure report for a Linear issue that is being marked as done.`,
1421
1495
  `Your text output will be posted as the closing comment on the issue.`,
@@ -1437,6 +1511,7 @@ async function handleCloseIssue(
1437
1511
  `- **Notes**: Any follow-up items or caveats (if applicable)`,
1438
1512
  ``,
1439
1513
  `Keep it brief and factual. Use markdown formatting.`,
1514
+ closeGuidanceAppendix,
1440
1515
  ].filter(Boolean).join("\n");
1441
1516
 
1442
1517
  // Execute with session lifecycle