@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.
- package/README.md +276 -1
- package/openclaw.plugin.json +3 -1
- package/package.json +1 -1
- package/prompts.yaml +24 -12
- package/src/__test__/fixtures/webhook-payloads.ts +9 -2
- package/src/__test__/webhook-scenarios.test.ts +150 -0
- package/src/infra/doctor.test.ts +3 -3
- package/src/pipeline/guidance.test.ts +222 -0
- package/src/pipeline/guidance.ts +156 -0
- package/src/pipeline/pipeline.ts +23 -2
- package/src/pipeline/webhook.ts +102 -27
- package/src/tools/linear-issues-tool.test.ts +453 -0
- package/src/tools/linear-issues-tool.ts +338 -0
- package/src/tools/tools.test.ts +36 -7
- package/src/tools/tools.ts +9 -2
package/src/pipeline/webhook.ts
CHANGED
|
@@ -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
|
|
303
|
+
const guidanceCtx = extractGuidance(payload);
|
|
301
304
|
|
|
302
|
-
|
|
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 ??
|
|
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
|
-
`- \`
|
|
335
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access
|
|
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
|
-
`- \`
|
|
342
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access
|
|
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
|
|
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
|
|
477
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`- \`
|
|
532
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access
|
|
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
|
-
`- \`
|
|
539
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access
|
|
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
|
-
`- \`
|
|
1248
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access
|
|
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
|
-
`- \`
|
|
1254
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access
|
|
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
|