@calltelemetry/openclaw-linear 0.9.5 → 0.9.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/package.json +1 -1
- package/src/__test__/fixtures/recorded-sub-issue-flow.ts +232 -156
- package/src/__test__/smoke-linear-api.test.ts +208 -3
- package/src/__test__/webhook-scenarios.test.ts +5 -0
- package/src/infra/shared-profiles.ts +59 -1
- package/src/pipeline/sub-issue-decomposition.test.ts +388 -0
- package/src/pipeline/webhook.test.ts +16 -3
- package/src/pipeline/webhook.ts +94 -11
package/src/pipeline/webhook.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { startProjectDispatch } from "./dag-dispatch.js";
|
|
|
17
17
|
import { emitDiagnostic } from "../infra/observability.js";
|
|
18
18
|
import { classifyIntent } from "./intent-classify.js";
|
|
19
19
|
import { extractGuidance, formatGuidanceAppendix, cacheGuidanceForTeam, getCachedGuidanceForTeam, isGuidanceEnabled, _resetGuidanceCacheForTesting } from "./guidance.js";
|
|
20
|
-
import { loadAgentProfiles, buildMentionPattern, resolveAgentFromAlias, _resetProfilesCacheForTesting, type AgentProfile } from "../infra/shared-profiles.js";
|
|
20
|
+
import { loadAgentProfiles, buildMentionPattern, resolveAgentFromAlias, validateProfiles, _resetProfilesCacheForTesting, type AgentProfile } from "../infra/shared-profiles.js";
|
|
21
21
|
|
|
22
22
|
// ── Prompt input sanitization ─────────────────────────────────────
|
|
23
23
|
|
|
@@ -322,6 +322,21 @@ export async function handleLinearWebhook(
|
|
|
322
322
|
return true;
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
// Validate agent profiles before doing any work
|
|
326
|
+
const profilesError = validateProfiles();
|
|
327
|
+
if (profilesError) {
|
|
328
|
+
api.logger.error("Agent profiles validation failed — posting setup error to Linear");
|
|
329
|
+
await linearApi.emitActivity(session.id, {
|
|
330
|
+
type: "error",
|
|
331
|
+
body: profilesError,
|
|
332
|
+
}).catch(() => {});
|
|
333
|
+
// Also try posting as a comment in case emitActivity doesn't render markdown
|
|
334
|
+
try {
|
|
335
|
+
await createCommentWithDedup(linearApi, issue.id, profilesError);
|
|
336
|
+
} catch {}
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
325
340
|
const previousComments = payload.previousComments ?? [];
|
|
326
341
|
const guidanceCtx = extractGuidance(payload);
|
|
327
342
|
|
|
@@ -331,24 +346,59 @@ export async function handleLinearWebhook(
|
|
|
331
346
|
: null;
|
|
332
347
|
const userMessage = lastComment?.body ?? "";
|
|
333
348
|
|
|
349
|
+
// Also extract the session prompt from promptContext — on initial session
|
|
350
|
+
// creation, previousComments is empty but the user's message lives here.
|
|
351
|
+
const promptContext = typeof payload.promptContext === "string" ? payload.promptContext : "";
|
|
352
|
+
|
|
334
353
|
// Route to the mentioned agent if the user's message contains an @mention.
|
|
335
354
|
// AgentSessionEvent doesn't carry mention routing — we must check manually.
|
|
355
|
+
// Check the last comment body, promptContext, AND agentSession prompt for @mentions.
|
|
336
356
|
const profiles = loadAgentProfiles();
|
|
337
357
|
const mentionPattern = buildMentionPattern(profiles);
|
|
338
358
|
let agentId = resolveAgentId(api);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
359
|
+
let mentionOverride = false;
|
|
360
|
+
const sessionPrompt = typeof (payload.agentSession as any)?.prompt === "string"
|
|
361
|
+
? (payload.agentSession as any).prompt : "";
|
|
362
|
+
const textsToScan = [userMessage, sessionPrompt, promptContext].filter(Boolean);
|
|
363
|
+
for (const text of textsToScan) {
|
|
364
|
+
if (mentionOverride) break;
|
|
365
|
+
if (mentionPattern) {
|
|
366
|
+
mentionPattern.lastIndex = 0;
|
|
367
|
+
const mentionMatch = text.match(mentionPattern);
|
|
368
|
+
if (mentionMatch) {
|
|
369
|
+
const alias = mentionMatch[1];
|
|
370
|
+
const resolved = resolveAgentFromAlias(alias, profiles);
|
|
371
|
+
if (resolved) {
|
|
372
|
+
api.logger.info(`AgentSession routed to ${resolved.agentId} via @${alias} mention in ${text === userMessage ? "comment" : text === sessionPrompt ? "session prompt" : "promptContext"}`);
|
|
373
|
+
agentId = resolved.agentId;
|
|
374
|
+
mentionOverride = true;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Also try bare-name matching (e.g. "hey mal" without @) in the user's text
|
|
380
|
+
if (!mentionOverride) {
|
|
381
|
+
for (const text of textsToScan) {
|
|
382
|
+
if (mentionOverride) break;
|
|
383
|
+
const lowerText = text.toLowerCase();
|
|
384
|
+
for (const [id, profile] of Object.entries(profiles)) {
|
|
385
|
+
const allNames = [...profile.mentionAliases, ...(profile.appAliases ?? [])];
|
|
386
|
+
for (const name of allNames) {
|
|
387
|
+
// Match bare name at word boundary (e.g. "hey mal" but not "malware")
|
|
388
|
+
const nameRe = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
|
|
389
|
+
if (nameRe.test(lowerText)) {
|
|
390
|
+
api.logger.info(`AgentSession routed to ${id} via bare name "${name}" in ${text === userMessage ? "comment" : text === sessionPrompt ? "session prompt" : "promptContext"}`);
|
|
391
|
+
agentId = id;
|
|
392
|
+
mentionOverride = true;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (mentionOverride) break;
|
|
347
397
|
}
|
|
348
398
|
}
|
|
349
399
|
}
|
|
350
400
|
// Session affinity: if no @mention override, prefer the agent that last handled this issue
|
|
351
|
-
if (
|
|
401
|
+
if (!mentionOverride && issue?.id) {
|
|
352
402
|
const affinityAgent = getIssueAffinity(issue.id);
|
|
353
403
|
if (affinityAgent) {
|
|
354
404
|
api.logger.info(`AgentSession routed to ${affinityAgent} via session affinity for ${issue.identifier ?? issue.id}`);
|
|
@@ -551,10 +601,22 @@ export async function handleLinearWebhook(
|
|
|
551
601
|
return true;
|
|
552
602
|
}
|
|
553
603
|
|
|
604
|
+
// Validate agent profiles before doing any work
|
|
605
|
+
const profilesError = validateProfiles();
|
|
606
|
+
if (profilesError) {
|
|
607
|
+
api.logger.error("Agent profiles validation failed — posting setup error to Linear");
|
|
608
|
+
await linearApi.emitActivity(session.id, {
|
|
609
|
+
type: "error",
|
|
610
|
+
body: profilesError,
|
|
611
|
+
}).catch(() => {});
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
|
|
554
615
|
// Route to mentioned agent if user's message contains an @mention (one-time detour)
|
|
555
616
|
const promptedProfiles = loadAgentProfiles();
|
|
556
617
|
const promptedMentionPattern = buildMentionPattern(promptedProfiles);
|
|
557
618
|
let agentId = resolveAgentId(api);
|
|
619
|
+
let mentionOverride = false;
|
|
558
620
|
if (promptedMentionPattern && userMessage) {
|
|
559
621
|
const mentionMatch = userMessage.match(promptedMentionPattern);
|
|
560
622
|
if (mentionMatch) {
|
|
@@ -563,11 +625,12 @@ export async function handleLinearWebhook(
|
|
|
563
625
|
if (resolved) {
|
|
564
626
|
api.logger.info(`AgentSession prompted: routed to ${resolved.agentId} via @${alias} mention`);
|
|
565
627
|
agentId = resolved.agentId;
|
|
628
|
+
mentionOverride = true;
|
|
566
629
|
}
|
|
567
630
|
}
|
|
568
631
|
}
|
|
569
632
|
// Session affinity: if no @mention override, prefer the agent that last handled this issue
|
|
570
|
-
if (
|
|
633
|
+
if (!mentionOverride && issue?.id) {
|
|
571
634
|
const affinityAgent = getIssueAffinity(issue.id);
|
|
572
635
|
if (affinityAgent) {
|
|
573
636
|
api.logger.info(`AgentSession prompted: routed to ${affinityAgent} via session affinity for ${issue.identifier ?? issue.id}`);
|
|
@@ -760,6 +823,16 @@ export async function handleLinearWebhook(
|
|
|
760
823
|
return true;
|
|
761
824
|
}
|
|
762
825
|
|
|
826
|
+
// Validate agent profiles before doing any work
|
|
827
|
+
const profilesError = validateProfiles();
|
|
828
|
+
if (profilesError) {
|
|
829
|
+
api.logger.error("Agent profiles validation failed — posting setup error to Linear");
|
|
830
|
+
try {
|
|
831
|
+
await createCommentWithDedup(linearApi, issue.id, profilesError);
|
|
832
|
+
} catch {}
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
|
|
763
836
|
// Load agent profiles
|
|
764
837
|
const profiles = loadAgentProfiles();
|
|
765
838
|
const agentNames = Object.keys(profiles);
|
|
@@ -1046,6 +1119,16 @@ export async function handleLinearWebhook(
|
|
|
1046
1119
|
return true;
|
|
1047
1120
|
}
|
|
1048
1121
|
|
|
1122
|
+
// Validate agent profiles
|
|
1123
|
+
const profilesError = validateProfiles();
|
|
1124
|
+
if (profilesError) {
|
|
1125
|
+
api.logger.error("Agent profiles validation failed — cannot triage new issue");
|
|
1126
|
+
try {
|
|
1127
|
+
await createCommentWithDedup(linearApi, issue.id, profilesError);
|
|
1128
|
+
} catch {}
|
|
1129
|
+
return true;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1049
1132
|
const agentId = resolveAgentId(api);
|
|
1050
1133
|
|
|
1051
1134
|
// Guard: prevent duplicate runs on same issue (also blocks AgentSessionEvent
|