@calltelemetry/openclaw-linear 0.4.1 → 0.5.1
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 +263 -249
- package/index.ts +115 -6
- package/openclaw.plugin.json +4 -1
- package/package.json +5 -1
- package/prompts.yaml +79 -0
- package/src/active-session.ts +1 -1
- package/src/agent.ts +41 -7
- package/src/claude-tool.ts +1 -1
- package/src/cli.ts +103 -0
- package/src/dispatch-service.ts +50 -2
- package/src/dispatch-state.ts +240 -8
- package/src/notify.ts +91 -0
- package/src/pipeline.ts +561 -406
- package/src/tier-assess.ts +1 -1
- package/src/webhook.ts +90 -45
package/src/tier-assess.ts
CHANGED
|
@@ -133,7 +133,7 @@ function resolveDefaultAgent(api: OpenClawPluginApi): string {
|
|
|
133
133
|
if (defaultAgent) return defaultAgent[0];
|
|
134
134
|
} catch { /* fall through */ }
|
|
135
135
|
|
|
136
|
-
return "
|
|
136
|
+
return "default";
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
function parseAssessment(raw: string): TierAssessment | null {
|
package/src/webhook.ts
CHANGED
|
@@ -3,9 +3,10 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
5
5
|
import { LinearAgentApi, resolveLinearToken } from "./linear-api.js";
|
|
6
|
-
import {
|
|
6
|
+
import { spawnWorker, type HookContext } from "./pipeline.js";
|
|
7
7
|
import { setActiveSession, clearActiveSession } from "./active-session.js";
|
|
8
8
|
import { readDispatchState, getActiveDispatch, registerDispatch, updateDispatchStatus, completeDispatch, removeActiveDispatch } from "./dispatch-state.js";
|
|
9
|
+
import { createDiscordNotifier, createNoopNotifier, type NotifyFn } from "./notify.js";
|
|
9
10
|
import { assessTier } from "./tier-assess.js";
|
|
10
11
|
import { createWorktree, prepareWorkspace } from "./codex-worktree.js";
|
|
11
12
|
|
|
@@ -186,12 +187,18 @@ export async function handleLinearWebhook(
|
|
|
186
187
|
.map((c: any) => ` - **${c.user?.name ?? "Unknown"}**: ${c.body?.slice(0, 200)}`)
|
|
187
188
|
.join("\n");
|
|
188
189
|
|
|
190
|
+
const notifIssueRef = enrichedIssue?.identifier ?? issue.id;
|
|
189
191
|
const message = [
|
|
190
|
-
`
|
|
192
|
+
`You are an orchestrator responding to a Linear issue notification. Your text output will be automatically posted as a comment on the issue (do NOT post a comment yourself — the handler does it).`,
|
|
191
193
|
``,
|
|
192
|
-
|
|
194
|
+
`**Tool access:**`,
|
|
195
|
+
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${notifIssueRef}\`), list, and search. Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
196
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
197
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
193
198
|
``,
|
|
194
|
-
|
|
199
|
+
`**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`,
|
|
200
|
+
``,
|
|
201
|
+
`## Issue: ${notifIssueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
195
202
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
196
203
|
``,
|
|
197
204
|
`**Description:**`,
|
|
@@ -199,7 +206,7 @@ export async function handleLinearWebhook(
|
|
|
199
206
|
commentSummary ? `\n**Recent comments:**\n${commentSummary}` : "",
|
|
200
207
|
comment?.body ? `\n**Triggering comment:**\n> ${comment.body}` : "",
|
|
201
208
|
``,
|
|
202
|
-
`Respond concisely.
|
|
209
|
+
`Respond concisely. For work requests, dispatch via \`code_run\` and summarize the result.`,
|
|
203
210
|
].filter(Boolean).join("\n");
|
|
204
211
|
|
|
205
212
|
// Dispatch agent with session lifecycle (non-blocking)
|
|
@@ -362,11 +369,19 @@ export async function handleLinearWebhook(
|
|
|
362
369
|
.map((c: any) => `**${c.user?.name ?? c.actorName ?? "User"}**: ${(c.body ?? "").slice(0, 300)}`)
|
|
363
370
|
.join("\n\n");
|
|
364
371
|
|
|
372
|
+
const issueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
|
|
365
373
|
const message = [
|
|
366
|
-
`You are an
|
|
367
|
-
|
|
374
|
+
`You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
|
|
375
|
+
``,
|
|
376
|
+
`**Tool access:**`,
|
|
377
|
+
`- \`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.`,
|
|
378
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
379
|
+
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
380
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
368
381
|
``,
|
|
369
|
-
|
|
382
|
+
`**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.`,
|
|
383
|
+
``,
|
|
384
|
+
`## Issue: ${issueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
370
385
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
371
386
|
``,
|
|
372
387
|
`**Description:**`,
|
|
@@ -374,7 +389,7 @@ export async function handleLinearWebhook(
|
|
|
374
389
|
commentContext ? `\n**Conversation:**\n${commentContext}` : "",
|
|
375
390
|
userMessage ? `\n**Latest message:**\n> ${userMessage}` : "",
|
|
376
391
|
``,
|
|
377
|
-
`Respond to the user's request.
|
|
392
|
+
`Respond to the user's request. For work requests, dispatch via \`code_run\` and summarize the result. Be concise and action-oriented.`,
|
|
378
393
|
].filter(Boolean).join("\n");
|
|
379
394
|
|
|
380
395
|
// Run agent directly (non-blocking)
|
|
@@ -542,11 +557,19 @@ export async function handleLinearWebhook(
|
|
|
542
557
|
.map((c: any) => `**${c.user?.name ?? "User"}**: ${(c.body ?? "").slice(0, 300)}`)
|
|
543
558
|
.join("\n\n");
|
|
544
559
|
|
|
560
|
+
const followUpIssueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
|
|
545
561
|
const message = [
|
|
546
|
-
`You are an
|
|
547
|
-
|
|
562
|
+
`You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
|
|
563
|
+
``,
|
|
564
|
+
`**Tool access:**`,
|
|
565
|
+
`- \`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.`,
|
|
566
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
567
|
+
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
568
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
548
569
|
``,
|
|
549
|
-
|
|
570
|
+
`**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`,
|
|
571
|
+
``,
|
|
572
|
+
`## Issue: ${followUpIssueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
550
573
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
551
574
|
``,
|
|
552
575
|
`**Description:**`,
|
|
@@ -554,7 +577,7 @@ export async function handleLinearWebhook(
|
|
|
554
577
|
commentContext ? `\n**Recent conversation:**\n${commentContext}` : "",
|
|
555
578
|
`\n**User's follow-up message:**\n> ${userMessage}`,
|
|
556
579
|
``,
|
|
557
|
-
`Respond to the user's follow-up. Be concise and action-oriented.`,
|
|
580
|
+
`Respond to the user's follow-up. For work requests, dispatch via \`code_run\`. Be concise and action-oriented.`,
|
|
558
581
|
].filter(Boolean).join("\n");
|
|
559
582
|
|
|
560
583
|
setActiveSession({
|
|
@@ -682,6 +705,14 @@ export async function handleLinearWebhook(
|
|
|
682
705
|
|
|
683
706
|
api.logger.info(`Comment mention: @${mentionedAgent} on ${issue.identifier ?? issue.id} by ${commentor}`);
|
|
684
707
|
|
|
708
|
+
// Guard: skip if an agent run is already active for this issue
|
|
709
|
+
// (prevents dual-dispatch when both Comment.create and AgentSessionEvent fire)
|
|
710
|
+
if (activeRuns.has(issue.id)) {
|
|
711
|
+
api.logger.info(`Comment mention: agent already running for ${issue.identifier ?? issue.id} — skipping`);
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
activeRuns.add(issue.id);
|
|
715
|
+
|
|
685
716
|
// React with eyes to acknowledge the comment
|
|
686
717
|
if (comment?.id) {
|
|
687
718
|
linearApi.createReaction(comment.id, "eyes").catch(() => {});
|
|
@@ -711,9 +742,14 @@ export async function handleLinearWebhook(
|
|
|
711
742
|
const assignee = enrichedIssue.assignee?.name ?? "Unassigned";
|
|
712
743
|
|
|
713
744
|
const taskMessage = [
|
|
714
|
-
`
|
|
745
|
+
`You are an orchestrator responding to a Linear issue comment. Your text output will be automatically posted as a comment on the issue (do NOT post a comment yourself — the handler does it).`,
|
|
715
746
|
``,
|
|
716
|
-
|
|
747
|
+
`**Tool access:**`,
|
|
748
|
+
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${enrichedIssue.identifier ?? "API-XXX"}\`), list issues (\`linearis issues list\`), and search (\`linearis issues search "..."\`). Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
749
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
750
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
751
|
+
``,
|
|
752
|
+
`**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.`,
|
|
717
753
|
``,
|
|
718
754
|
`**Issue:** ${enrichedIssue.identifier ?? enrichedIssue.id} — ${enrichedIssue.title ?? "(untitled)"}`,
|
|
719
755
|
`**Status:** ${state} | **Priority:** ${priority} | **Assignee:** ${assignee} | **Labels:** ${labels}`,
|
|
@@ -724,7 +760,7 @@ export async function handleLinearWebhook(
|
|
|
724
760
|
`**${commentor} wrote:**`,
|
|
725
761
|
`> ${commentBody}`,
|
|
726
762
|
``,
|
|
727
|
-
`Respond to their message. Be concise and direct.
|
|
763
|
+
`Respond to their message. Be concise and direct. For work requests, dispatch via \`code_run\` and summarize the result.`,
|
|
728
764
|
].filter(Boolean).join("\n");
|
|
729
765
|
|
|
730
766
|
// Dispatch to agent with full session lifecycle (non-blocking)
|
|
@@ -813,6 +849,7 @@ export async function handleLinearWebhook(
|
|
|
813
849
|
}
|
|
814
850
|
} finally {
|
|
815
851
|
clearActiveSession(issue.id);
|
|
852
|
+
activeRuns.delete(issue.id);
|
|
816
853
|
}
|
|
817
854
|
})();
|
|
818
855
|
|
|
@@ -1225,6 +1262,7 @@ async function handleDispatch(
|
|
|
1225
1262
|
status: "dispatched",
|
|
1226
1263
|
dispatchedAt: now,
|
|
1227
1264
|
agentSessionId,
|
|
1265
|
+
attempt: 0,
|
|
1228
1266
|
}, statePath);
|
|
1229
1267
|
|
|
1230
1268
|
// 8. Register active session for tool resolution
|
|
@@ -1274,41 +1312,48 @@ async function handleDispatch(
|
|
|
1274
1312
|
api.logger.warn(`@dispatch: could not apply tier label: ${err}`);
|
|
1275
1313
|
}
|
|
1276
1314
|
|
|
1277
|
-
// 11. Run pipeline (non-blocking)
|
|
1278
|
-
|
|
1279
|
-
|
|
1315
|
+
// 11. Run v2 pipeline: worker → audit → verdict (non-blocking)
|
|
1316
|
+
activeRuns.add(issue.id);
|
|
1317
|
+
|
|
1318
|
+
// Instantiate notifier
|
|
1319
|
+
const discordBotToken = (() => {
|
|
1320
|
+
try {
|
|
1321
|
+
const config = JSON.parse(
|
|
1322
|
+
require("node:fs").readFileSync(
|
|
1323
|
+
require("node:path").join(process.env.HOME ?? "/home/claw", ".openclaw", "openclaw.json"),
|
|
1324
|
+
"utf8",
|
|
1325
|
+
),
|
|
1326
|
+
);
|
|
1327
|
+
return config?.channels?.discord?.token as string | undefined;
|
|
1328
|
+
} catch { return undefined; }
|
|
1329
|
+
})();
|
|
1330
|
+
const flowDiscordChannel = pluginConfig?.flowDiscordChannel as string | undefined;
|
|
1331
|
+
const notify: NotifyFn = (discordBotToken && flowDiscordChannel)
|
|
1332
|
+
? createDiscordNotifier(discordBotToken, flowDiscordChannel)
|
|
1333
|
+
: createNoopNotifier();
|
|
1334
|
+
|
|
1335
|
+
const hookCtx: HookContext = {
|
|
1280
1336
|
api,
|
|
1281
1337
|
linearApi,
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
id: issue.id,
|
|
1286
|
-
identifier,
|
|
1287
|
-
title: enrichedIssue.title ?? "(untitled)",
|
|
1288
|
-
description: enrichedIssue.description,
|
|
1289
|
-
},
|
|
1290
|
-
worktreePath: worktree.path,
|
|
1291
|
-
codexBranch: worktree.branch,
|
|
1292
|
-
tier: assessment.tier,
|
|
1293
|
-
model: assessment.model,
|
|
1338
|
+
notify,
|
|
1339
|
+
pluginConfig,
|
|
1340
|
+
configPath: statePath,
|
|
1294
1341
|
};
|
|
1295
1342
|
|
|
1296
|
-
|
|
1343
|
+
// Re-read dispatch to get fresh state after registration
|
|
1344
|
+
const freshState = await readDispatchState(statePath);
|
|
1345
|
+
const dispatch = getActiveDispatch(freshState, identifier)!;
|
|
1297
1346
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
completedAt: new Date().toISOString(),
|
|
1307
|
-
}, statePath);
|
|
1308
|
-
api.logger.info(`@dispatch: pipeline completed for ${identifier}`);
|
|
1309
|
-
})
|
|
1347
|
+
await notify("dispatch", {
|
|
1348
|
+
identifier,
|
|
1349
|
+
title: enrichedIssue.title ?? "(untitled)",
|
|
1350
|
+
status: "dispatched",
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
// spawnWorker handles: dispatched→working→auditing→done/rework/stuck
|
|
1354
|
+
spawnWorker(hookCtx, dispatch)
|
|
1310
1355
|
.catch(async (err) => {
|
|
1311
|
-
api.logger.error(`@dispatch: pipeline failed for ${identifier}: ${err}`);
|
|
1356
|
+
api.logger.error(`@dispatch: pipeline v2 failed for ${identifier}: ${err}`);
|
|
1312
1357
|
await updateDispatchStatus(identifier, "failed", statePath);
|
|
1313
1358
|
})
|
|
1314
1359
|
.finally(() => {
|