@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.
@@ -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 "zoe";
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 { runFullPipeline, type PipelineContext } from "./pipeline.js";
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
- `IMPORTANT: You are responding to a Linear issue notification. Your ENTIRE text output will be automatically posted as a comment on the issue. Do NOT attempt to post to Linear yourself — no tools, no CLI, no API calls. Just write your response as plain text/markdown.`,
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
- `You were mentioned/assigned in a Linear issue. Respond naturally and helpfully.`,
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
- `## Issue: ${enrichedIssue?.identifier ?? issue.id}${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
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. If there's a task, explain what you'll do and do it.`,
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 AI agent responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
367
- `You have access to tools including \`code_run\` for coding tasks, \`spawn_agent\`/\`ask_agent\` for delegation, and standard tools (exec, read, edit, write, web_search, etc.). To manage Linear issues (update status, close, assign, comment, etc.) use the \`linearis\` CLI via exec — e.g. \`linearis issues update API-123 --status done\` to close an issue, \`linearis issues update API-123 --status "In Progress"\` to change status, \`linearis issues list\` to list issues. Run \`linearis usage\` for full command reference.`,
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
- `## Issue: ${enrichedIssue?.identifier ?? issue.identifier ?? issue.id} ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
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. If they ask you to write code or make changes, use the \`code_run\` tool. Be concise and action-oriented.`,
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 AI agent responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
547
- `You have access to tools including \`code_run\` for coding tasks, \`spawn_agent\`/\`ask_agent\` for delegation, and standard tools (exec, read, edit, write, web_search, etc.). To manage Linear issues (update status, close, assign, comment, etc.) use the \`linearis\` CLI via exec — e.g. \`linearis issues update API-123 --status done\` to close an issue, \`linearis issues update API-123 --status "In Progress"\` to change status, \`linearis issues list\` to list issues. Run \`linearis usage\` for full command reference.`,
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
- `## Issue: ${enrichedIssue?.identifier ?? issue.identifier ?? issue.id}${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
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
- `IMPORTANT: You are responding to a Linear issue comment. Your ENTIRE text output will be automatically posted as a comment on the issue. Do NOT attempt to post to Linear yourself — no tools, no CLI, no API calls. Just write your response as plain text/markdown.`,
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
- `You were mentioned by name. Respond naturally and helpfully as a team member. Be concise, markdown-friendly. Do NOT use JSON or structured output.`,
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. If they're asking you to do work, explain what you'll do and do it.`,
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
- const agentId = resolveAgentId(api);
1279
- const pipelineCtx: PipelineContext = {
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
- agentSessionId: agentSessionId ?? `dispatch-${identifier}-${Date.now()}`,
1283
- agentId,
1284
- issue: {
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
- activeRuns.add(issue.id);
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
- // Update status to running
1299
- await updateDispatchStatus(identifier, "running", statePath);
1300
-
1301
- runFullPipeline(pipelineCtx)
1302
- .then(async () => {
1303
- await completeDispatch(identifier, {
1304
- tier: assessment.tier,
1305
- status: "done",
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(() => {