@calltelemetry/openclaw-linear 0.8.5 → 0.8.6
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 +68 -23
- 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}]`);
|
|
@@ -297,16 +301,15 @@ export async function handleLinearWebhook(
|
|
|
297
301
|
|
|
298
302
|
const agentId = resolveAgentId(api);
|
|
299
303
|
const previousComments = payload.previousComments ?? [];
|
|
300
|
-
const
|
|
304
|
+
const guidanceCtx = extractGuidance(payload);
|
|
301
305
|
|
|
302
|
-
api.logger.info(`AgentSession created: ${session.id} for issue ${issue?.identifier ?? issue?.id} (comments: ${previousComments.length}, guidance: ${guidance ? "yes" : "no"})`)
|
|
306
|
+
api.logger.info(`AgentSession created: ${session.id} for issue ${issue?.identifier ?? issue?.id} (comments: ${previousComments.length}, guidance: ${guidanceCtx.guidance ? "yes" : "no"})`)
|
|
303
307
|
|
|
304
|
-
// Extract the user's latest message from previousComments
|
|
305
|
-
// The last comment is the most recent user message
|
|
308
|
+
// Extract the user's latest message from previousComments (NOT from guidance)
|
|
306
309
|
const lastComment = previousComments.length > 0
|
|
307
310
|
? previousComments[previousComments.length - 1]
|
|
308
311
|
: null;
|
|
309
|
-
const userMessage = lastComment?.body ??
|
|
312
|
+
const userMessage = lastComment?.body ?? "";
|
|
310
313
|
|
|
311
314
|
// Fetch full issue details
|
|
312
315
|
let enrichedIssue: any = issue;
|
|
@@ -318,6 +321,13 @@ export async function handleLinearWebhook(
|
|
|
318
321
|
|
|
319
322
|
const description = enrichedIssue?.description ?? issue?.description ?? "(no description)";
|
|
320
323
|
|
|
324
|
+
// Cache guidance for this team (enables Comment webhook paths)
|
|
325
|
+
const teamId = enrichedIssue?.team?.id;
|
|
326
|
+
if (guidanceCtx.guidance && teamId) cacheGuidanceForTeam(teamId, guidanceCtx.guidance);
|
|
327
|
+
const guidanceAppendix = isGuidanceEnabled(pluginConfig as Record<string, unknown> | undefined, teamId)
|
|
328
|
+
? formatGuidanceAppendix(guidanceCtx.guidance)
|
|
329
|
+
: "";
|
|
330
|
+
|
|
321
331
|
// Build conversation context from previous comments
|
|
322
332
|
const commentContext = previousComments
|
|
323
333
|
.slice(-5)
|
|
@@ -331,22 +341,24 @@ export async function handleLinearWebhook(
|
|
|
331
341
|
const toolAccessLines = isTriaged
|
|
332
342
|
? [
|
|
333
343
|
`**Tool access:**`,
|
|
334
|
-
`- \`
|
|
335
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access
|
|
344
|
+
`- \`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.`,
|
|
345
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linear_issues.`,
|
|
336
346
|
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
337
347
|
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
348
|
+
``,
|
|
349
|
+
`**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
350
|
]
|
|
339
351
|
: [
|
|
340
352
|
`**Tool access:**`,
|
|
341
|
-
`- \`
|
|
342
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access
|
|
353
|
+
`- \`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".`,
|
|
354
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linear_issues.`,
|
|
343
355
|
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
344
356
|
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
345
357
|
];
|
|
346
358
|
|
|
347
359
|
const roleLines = isTriaged
|
|
348
360
|
? [`**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
|
|
361
|
+
: [`**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
362
|
|
|
351
363
|
const message = [
|
|
352
364
|
`You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
|
|
@@ -364,6 +376,7 @@ export async function handleLinearWebhook(
|
|
|
364
376
|
userMessage ? `\n**Latest message:**\n> ${userMessage}` : "",
|
|
365
377
|
``,
|
|
366
378
|
`Respond to the user's request. For work requests, dispatch via \`code_run\` and summarize the result. Be concise and action-oriented.`,
|
|
379
|
+
guidanceAppendix,
|
|
367
380
|
].filter(Boolean).join("\n");
|
|
368
381
|
|
|
369
382
|
// Run agent directly (non-blocking)
|
|
@@ -473,13 +486,11 @@ export async function handleLinearWebhook(
|
|
|
473
486
|
return true;
|
|
474
487
|
}
|
|
475
488
|
|
|
476
|
-
// Extract user message from the activity
|
|
477
|
-
const
|
|
489
|
+
// Extract user message from the activity (not from promptContext which contains issue data + guidance)
|
|
490
|
+
const guidanceCtxPrompted = extractGuidance(payload);
|
|
478
491
|
const userMessage =
|
|
479
492
|
activity?.content?.body ??
|
|
480
493
|
activity?.body ??
|
|
481
|
-
promptContext?.message ??
|
|
482
|
-
promptContext ??
|
|
483
494
|
"";
|
|
484
495
|
|
|
485
496
|
if (!userMessage || typeof userMessage !== "string" || userMessage.trim().length === 0) {
|
|
@@ -514,6 +525,13 @@ export async function handleLinearWebhook(
|
|
|
514
525
|
|
|
515
526
|
const description = enrichedIssue?.description ?? issue?.description ?? "(no description)";
|
|
516
527
|
|
|
528
|
+
// Resolve guidance for follow-up
|
|
529
|
+
const followUpTeamId = enrichedIssue?.team?.id;
|
|
530
|
+
if (guidanceCtxPrompted.guidance && followUpTeamId) cacheGuidanceForTeam(followUpTeamId, guidanceCtxPrompted.guidance);
|
|
531
|
+
const followUpGuidanceAppendix = isGuidanceEnabled(pluginConfig as Record<string, unknown> | undefined, followUpTeamId)
|
|
532
|
+
? formatGuidanceAppendix(guidanceCtxPrompted.guidance ?? (followUpTeamId ? getCachedGuidanceForTeam(followUpTeamId) : null))
|
|
533
|
+
: "";
|
|
534
|
+
|
|
517
535
|
// Build context from recent comments
|
|
518
536
|
const recentComments = enrichedIssue?.comments?.nodes ?? [];
|
|
519
537
|
const commentContext = recentComments
|
|
@@ -528,15 +546,17 @@ export async function handleLinearWebhook(
|
|
|
528
546
|
const followUpToolAccessLines = followUpIsTriaged
|
|
529
547
|
? [
|
|
530
548
|
`**Tool access:**`,
|
|
531
|
-
`- \`
|
|
532
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access
|
|
549
|
+
`- \`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.`,
|
|
550
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linear_issues.`,
|
|
533
551
|
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
534
552
|
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
553
|
+
``,
|
|
554
|
+
`**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
555
|
]
|
|
536
556
|
: [
|
|
537
557
|
`**Tool access:**`,
|
|
538
|
-
`- \`
|
|
539
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access
|
|
558
|
+
`- \`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".`,
|
|
559
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linear_issues.`,
|
|
540
560
|
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
541
561
|
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
542
562
|
];
|
|
@@ -561,6 +581,7 @@ export async function handleLinearWebhook(
|
|
|
561
581
|
`\n**User's follow-up message:**\n> ${userMessage}`,
|
|
562
582
|
``,
|
|
563
583
|
`Respond to the user's follow-up. For work requests, dispatch via \`code_run\`. Be concise and action-oriented.`,
|
|
584
|
+
followUpGuidanceAppendix,
|
|
564
585
|
].filter(Boolean).join("\n");
|
|
565
586
|
|
|
566
587
|
setActiveSession({
|
|
@@ -635,7 +656,6 @@ export async function handleLinearWebhook(
|
|
|
635
656
|
const commentBody = comment?.body ?? "";
|
|
636
657
|
const commentor = comment?.user?.name ?? "Unknown";
|
|
637
658
|
const issue = comment?.issue ?? payload.issue;
|
|
638
|
-
const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
|
|
639
659
|
|
|
640
660
|
if (!issue?.id) {
|
|
641
661
|
api.logger.error("Comment webhook: missing issue data");
|
|
@@ -1050,6 +1070,13 @@ export async function handleLinearWebhook(
|
|
|
1050
1070
|
? `**Created by:** ${creatorName} (${creatorEmail})`
|
|
1051
1071
|
: `**Created by:** ${creatorName}`;
|
|
1052
1072
|
|
|
1073
|
+
// Look up cached guidance for triage
|
|
1074
|
+
const triageTeamId = enrichedIssue?.team?.id ?? issue?.team?.id;
|
|
1075
|
+
const triageGuidance = triageTeamId ? getCachedGuidanceForTeam(triageTeamId) : null;
|
|
1076
|
+
const triageGuidanceAppendix = isGuidanceEnabled(pluginConfig as Record<string, unknown> | undefined, triageTeamId)
|
|
1077
|
+
? formatGuidanceAppendix(triageGuidance)
|
|
1078
|
+
: "";
|
|
1079
|
+
|
|
1053
1080
|
const message = [
|
|
1054
1081
|
`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
1082
|
``,
|
|
@@ -1086,6 +1113,7 @@ export async function handleLinearWebhook(
|
|
|
1086
1113
|
`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
1114
|
``,
|
|
1088
1115
|
`Then write your full assessment as markdown below the JSON block.`,
|
|
1116
|
+
triageGuidanceAppendix,
|
|
1089
1117
|
].filter(Boolean).join("\n");
|
|
1090
1118
|
|
|
1091
1119
|
const sessionId = `linear-triage-${issue.id}-${Date.now()}`;
|
|
@@ -1237,6 +1265,13 @@ async function dispatchCommentToAgent(
|
|
|
1237
1265
|
.map((c: any) => `**${c.user?.name ?? "Unknown"}**: ${(c.body ?? "").slice(0, 200)}`)
|
|
1238
1266
|
.join("\n");
|
|
1239
1267
|
|
|
1268
|
+
// Look up cached guidance for this team (Comment webhooks don't carry guidance)
|
|
1269
|
+
const commentTeamId = enrichedIssue?.team?.id;
|
|
1270
|
+
const cachedGuidance = commentTeamId ? getCachedGuidanceForTeam(commentTeamId) : null;
|
|
1271
|
+
const commentGuidanceAppendix = isGuidanceEnabled(pluginConfig, commentTeamId)
|
|
1272
|
+
? formatGuidanceAppendix(cachedGuidance)
|
|
1273
|
+
: "";
|
|
1274
|
+
|
|
1240
1275
|
const issueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
|
|
1241
1276
|
const stateType = enrichedIssue?.state?.type ?? "";
|
|
1242
1277
|
const isTriaged = stateType === "started" || stateType === "completed" || stateType === "canceled";
|
|
@@ -1244,14 +1279,16 @@ async function dispatchCommentToAgent(
|
|
|
1244
1279
|
const toolAccessLines = isTriaged
|
|
1245
1280
|
? [
|
|
1246
1281
|
`**Tool access:**`,
|
|
1247
|
-
`- \`
|
|
1248
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access
|
|
1282
|
+
`- \`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.`,
|
|
1283
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linear_issues.`,
|
|
1249
1284
|
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
1285
|
+
``,
|
|
1286
|
+
`**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
1287
|
]
|
|
1251
1288
|
: [
|
|
1252
1289
|
`**Tool access:**`,
|
|
1253
|
-
`- \`
|
|
1254
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access
|
|
1290
|
+
`- \`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".`,
|
|
1291
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linear_issues.`,
|
|
1255
1292
|
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
1256
1293
|
];
|
|
1257
1294
|
|
|
@@ -1278,6 +1315,7 @@ async function dispatchCommentToAgent(
|
|
|
1278
1315
|
`IMPORTANT: Only reference real users from the issue data above. Do NOT fabricate or guess user names, emails, or identities.`,
|
|
1279
1316
|
``,
|
|
1280
1317
|
`Respond concisely. For work requests, dispatch via \`code_run\` and summarize the result.`,
|
|
1318
|
+
commentGuidanceAppendix,
|
|
1281
1319
|
].filter(Boolean).join("\n");
|
|
1282
1320
|
|
|
1283
1321
|
// Dispatch with session lifecycle
|
|
@@ -1416,6 +1454,12 @@ async function handleCloseIssue(
|
|
|
1416
1454
|
.map((c: any) => `**${c.user?.name ?? "Unknown"}**: ${(c.body ?? "").slice(0, 300)}`)
|
|
1417
1455
|
.join("\n");
|
|
1418
1456
|
|
|
1457
|
+
// Look up cached guidance
|
|
1458
|
+
const closeGuidance = teamId ? getCachedGuidanceForTeam(teamId) : null;
|
|
1459
|
+
const closeGuidanceAppendix = isGuidanceEnabled(pluginConfig, teamId)
|
|
1460
|
+
? formatGuidanceAppendix(closeGuidance)
|
|
1461
|
+
: "";
|
|
1462
|
+
|
|
1419
1463
|
const message = [
|
|
1420
1464
|
`You are writing a closure report for a Linear issue that is being marked as done.`,
|
|
1421
1465
|
`Your text output will be posted as the closing comment on the issue.`,
|
|
@@ -1437,6 +1481,7 @@ async function handleCloseIssue(
|
|
|
1437
1481
|
`- **Notes**: Any follow-up items or caveats (if applicable)`,
|
|
1438
1482
|
``,
|
|
1439
1483
|
`Keep it brief and factual. Use markdown formatting.`,
|
|
1484
|
+
closeGuidanceAppendix,
|
|
1440
1485
|
].filter(Boolean).join("\n");
|
|
1441
1486
|
|
|
1442
1487
|
// Execute with session lifecycle
|