@calltelemetry/openclaw-linear 0.8.4 → 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.
@@ -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 guidance = payload.guidance;
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 ?? guidance ?? "";
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)
@@ -325,16 +335,37 @@ export async function handleLinearWebhook(
325
335
  .join("\n\n");
326
336
 
327
337
  const issueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
338
+ const stateType = enrichedIssue?.state?.type ?? "";
339
+ const isTriaged = stateType === "started" || stateType === "completed" || stateType === "canceled";
340
+
341
+ const toolAccessLines = isTriaged
342
+ ? [
343
+ `**Tool 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.`,
346
+ `- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
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.`,
350
+ ]
351
+ : [
352
+ `**Tool 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.`,
355
+ `- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
356
+ `- Standard tools: exec, read, edit, write, web_search, etc.`,
357
+ ];
358
+
359
+ const roleLines = isTriaged
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.`]
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.`];
362
+
328
363
  const message = [
329
364
  `You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
330
365
  ``,
331
- `**Tool access:**`,
332
- `- \`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.`,
333
- `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
334
- `- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
335
- `- Standard tools: exec, read, edit, write, web_search, etc.`,
366
+ ...toolAccessLines,
336
367
  ``,
337
- `**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.`,
368
+ ...roleLines,
338
369
  ``,
339
370
  `## Issue: ${issueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
340
371
  `**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
@@ -345,6 +376,7 @@ export async function handleLinearWebhook(
345
376
  userMessage ? `\n**Latest message:**\n> ${userMessage}` : "",
346
377
  ``,
347
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,
348
380
  ].filter(Boolean).join("\n");
349
381
 
350
382
  // Run agent directly (non-blocking)
@@ -367,7 +399,7 @@ export async function handleLinearWebhook(
367
399
  // Emit initial thought
368
400
  await linearApi.emitActivity(session.id, {
369
401
  type: "thought",
370
- body: `Processing request for ${enrichedIssue?.identifier ?? issue.id}...`,
402
+ body: `${label} is processing request for ${enrichedIssue?.identifier ?? issue.id}...`,
371
403
  }).catch(() => {});
372
404
 
373
405
  // Run agent with streaming to Linear
@@ -454,13 +486,11 @@ export async function handleLinearWebhook(
454
486
  return true;
455
487
  }
456
488
 
457
- // Extract user message from the activity or prompt context
458
- const promptContext = payload.promptContext;
489
+ // Extract user message from the activity (not from promptContext which contains issue data + guidance)
490
+ const guidanceCtxPrompted = extractGuidance(payload);
459
491
  const userMessage =
460
492
  activity?.content?.body ??
461
493
  activity?.body ??
462
- promptContext?.message ??
463
- promptContext ??
464
494
  "";
465
495
 
466
496
  if (!userMessage || typeof userMessage !== "string" || userMessage.trim().length === 0) {
@@ -495,6 +525,13 @@ export async function handleLinearWebhook(
495
525
 
496
526
  const description = enrichedIssue?.description ?? issue?.description ?? "(no description)";
497
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
+
498
535
  // Build context from recent comments
499
536
  const recentComments = enrichedIssue?.comments?.nodes ?? [];
500
537
  const commentContext = recentComments
@@ -503,16 +540,37 @@ export async function handleLinearWebhook(
503
540
  .join("\n\n");
504
541
 
505
542
  const followUpIssueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
543
+ const followUpStateType = enrichedIssue?.state?.type ?? "";
544
+ const followUpIsTriaged = followUpStateType === "started" || followUpStateType === "completed" || followUpStateType === "canceled";
545
+
546
+ const followUpToolAccessLines = followUpIsTriaged
547
+ ? [
548
+ `**Tool 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.`,
551
+ `- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
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.`,
555
+ ]
556
+ : [
557
+ `**Tool 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.`,
560
+ `- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
561
+ `- Standard tools: exec, read, edit, write, web_search, etc.`,
562
+ ];
563
+
564
+ const followUpRoleLines = followUpIsTriaged
565
+ ? [`**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.`]
566
+ : [`**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`];
567
+
506
568
  const message = [
507
569
  `You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
508
570
  ``,
509
- `**Tool access:**`,
510
- `- \`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.`,
511
- `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
512
- `- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
513
- `- Standard tools: exec, read, edit, write, web_search, etc.`,
571
+ ...followUpToolAccessLines,
514
572
  ``,
515
- `**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`,
573
+ ...followUpRoleLines,
516
574
  ``,
517
575
  `## Issue: ${followUpIssueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
518
576
  `**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
@@ -523,6 +581,7 @@ export async function handleLinearWebhook(
523
581
  `\n**User's follow-up message:**\n> ${userMessage}`,
524
582
  ``,
525
583
  `Respond to the user's follow-up. For work requests, dispatch via \`code_run\`. Be concise and action-oriented.`,
584
+ followUpGuidanceAppendix,
526
585
  ].filter(Boolean).join("\n");
527
586
 
528
587
  setActiveSession({
@@ -536,7 +595,7 @@ export async function handleLinearWebhook(
536
595
  try {
537
596
  await linearApi.emitActivity(session.id, {
538
597
  type: "thought",
539
- body: `Processing follow-up for ${enrichedIssue?.identifier ?? issue.id}...`,
598
+ body: `${label} is processing follow-up for ${enrichedIssue?.identifier ?? issue.id}...`,
540
599
  }).catch(() => {});
541
600
 
542
601
  const sessionId = `linear-session-${session.id}`;
@@ -597,7 +656,6 @@ export async function handleLinearWebhook(
597
656
  const commentBody = comment?.body ?? "";
598
657
  const commentor = comment?.user?.name ?? "Unknown";
599
658
  const issue = comment?.issue ?? payload.issue;
600
- const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
601
659
 
602
660
  if (!issue?.id) {
603
661
  api.logger.error("Comment webhook: missing issue data");
@@ -1006,11 +1064,25 @@ export async function handleLinearWebhook(
1006
1064
  }).catch(() => {});
1007
1065
  }
1008
1066
 
1067
+ const creatorName = enrichedIssue?.creator?.name ?? "Unknown";
1068
+ const creatorEmail = enrichedIssue?.creator?.email ?? null;
1069
+ const creatorLine = creatorEmail
1070
+ ? `**Created by:** ${creatorName} (${creatorEmail})`
1071
+ : `**Created by:** ${creatorName}`;
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
+
1009
1080
  const message = [
1010
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.`,
1011
1082
  ``,
1012
1083
  `## Issue: ${enrichedIssue?.identifier ?? issue.identifier ?? issue.id} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
1013
1084
  `**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Current Estimate:** ${enrichedIssue?.estimate ?? "None"} | **Current Labels:** ${currentLabelNames}`,
1085
+ creatorLine,
1014
1086
  ``,
1015
1087
  `**Description:**`,
1016
1088
  description,
@@ -1038,7 +1110,10 @@ export async function handleLinearWebhook(
1038
1110
  `}`,
1039
1111
  '```',
1040
1112
  ``,
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.`,
1114
+ ``,
1041
1115
  `Then write your full assessment as markdown below the JSON block.`,
1116
+ triageGuidanceAppendix,
1042
1117
  ].filter(Boolean).join("\n");
1043
1118
 
1044
1119
  const sessionId = `linear-triage-${issue.id}-${Date.now()}`;
@@ -1190,26 +1265,57 @@ async function dispatchCommentToAgent(
1190
1265
  .map((c: any) => `**${c.user?.name ?? "Unknown"}**: ${(c.body ?? "").slice(0, 200)}`)
1191
1266
  .join("\n");
1192
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
+
1193
1275
  const issueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
1276
+ const stateType = enrichedIssue?.state?.type ?? "";
1277
+ const isTriaged = stateType === "started" || stateType === "completed" || stateType === "canceled";
1278
+
1279
+ const toolAccessLines = isTriaged
1280
+ ? [
1281
+ `**Tool 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.`,
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.`,
1287
+ ]
1288
+ : [
1289
+ `**Tool 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.`,
1292
+ `- Standard tools: exec, read, edit, write, web_search, etc.`,
1293
+ ];
1294
+
1295
+ const roleLines = isTriaged
1296
+ ? [`**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.`]
1297
+ : [`**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`];
1298
+
1194
1299
  const message = [
1195
1300
  `You are an orchestrator responding to a Linear comment. Your text output will be automatically posted as a comment on the issue (do NOT post a comment yourself — the handler does it).`,
1196
1301
  ``,
1197
- `**Tool access:**`,
1198
- `- \`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.`,
1199
- `- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
1200
- `- Standard tools: exec, read, edit, write, web_search, etc.`,
1302
+ ...toolAccessLines,
1201
1303
  ``,
1202
- `**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`,
1304
+ ...roleLines,
1203
1305
  ``,
1204
1306
  `## Issue: ${issueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
1205
1307
  `**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
1308
+ enrichedIssue?.creator ? `**Created by:** ${enrichedIssue.creator.name}${enrichedIssue.creator.email ? ` (${enrichedIssue.creator.email})` : ""}` : "",
1206
1309
  ``,
1207
1310
  `**Description:**`,
1208
1311
  description,
1209
1312
  commentSummary ? `\n**Recent comments:**\n${commentSummary}` : "",
1210
1313
  `\n**${commentor} says:**\n> ${commentBody}`,
1211
1314
  ``,
1315
+ `IMPORTANT: Only reference real users from the issue data above. Do NOT fabricate or guess user names, emails, or identities.`,
1316
+ ``,
1212
1317
  `Respond concisely. For work requests, dispatch via \`code_run\` and summarize the result.`,
1318
+ commentGuidanceAppendix,
1213
1319
  ].filter(Boolean).join("\n");
1214
1320
 
1215
1321
  // Dispatch with session lifecycle
@@ -1348,24 +1454,34 @@ async function handleCloseIssue(
1348
1454
  .map((c: any) => `**${c.user?.name ?? "Unknown"}**: ${(c.body ?? "").slice(0, 300)}`)
1349
1455
  .join("\n");
1350
1456
 
1457
+ // Look up cached guidance
1458
+ const closeGuidance = teamId ? getCachedGuidanceForTeam(teamId) : null;
1459
+ const closeGuidanceAppendix = isGuidanceEnabled(pluginConfig, teamId)
1460
+ ? formatGuidanceAppendix(closeGuidance)
1461
+ : "";
1462
+
1351
1463
  const message = [
1352
1464
  `You are writing a closure report for a Linear issue that is being marked as done.`,
1353
1465
  `Your text output will be posted as the closing comment on the issue.`,
1354
1466
  ``,
1355
1467
  `## Issue: ${issueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
1356
1468
  `**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
1469
+ enrichedIssue?.creator ? `**Created by:** ${enrichedIssue.creator.name}${enrichedIssue.creator.email ? ` (${enrichedIssue.creator.email})` : ""}` : "",
1357
1470
  ``,
1358
1471
  `**Description:**`,
1359
1472
  description,
1360
1473
  commentSummary ? `\n**Comment history:**\n${commentSummary}` : "",
1361
1474
  `\n**${commentor} says (closure request):**\n> ${commentBody}`,
1362
1475
  ``,
1476
+ `IMPORTANT: Only reference real users from the issue data above. Do NOT fabricate or guess user names, emails, or identities.`,
1477
+ ``,
1363
1478
  `Write a concise closure report with:`,
1364
1479
  `- **Summary**: What was done (1-2 sentences)`,
1365
1480
  `- **Resolution**: How it was resolved`,
1366
1481
  `- **Notes**: Any follow-up items or caveats (if applicable)`,
1367
1482
  ``,
1368
1483
  `Keep it brief and factual. Use markdown formatting.`,
1484
+ closeGuidanceAppendix,
1369
1485
  ].filter(Boolean).join("\n");
1370
1486
 
1371
1487
  // Execute with session lifecycle
@@ -1404,9 +1520,13 @@ async function handleCloseIssue(
1404
1520
  readOnly: true,
1405
1521
  });
1406
1522
 
1523
+ if (!result.success) {
1524
+ api.logger.error(`Closure report agent failed for ${issueRef}: ${(result.output ?? "no output").slice(0, 500)}`);
1525
+ }
1526
+
1407
1527
  const closureReport = result.success
1408
1528
  ? result.output
1409
- : "Issue closed. (Closure report generation failed.)";
1529
+ : `Issue closed by ${commentor}.\n\n> ${commentBody}\n\n*Closure report generation failed — agent returned: ${(result.output ?? "no output").slice(0, 200)}*`;
1410
1530
 
1411
1531
  const fullReport = `## Closure Report\n\n${closureReport}`;
1412
1532