@calltelemetry/openclaw-linear 0.5.0 → 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/index.ts CHANGED
@@ -196,15 +196,21 @@ export default function register(api: OpenClawPluginApi) {
196
196
  try {
197
197
  const raw = execFileSync(bin, ["--version"], {
198
198
  encoding: "utf8",
199
- timeout: 5_000,
199
+ timeout: 15_000,
200
200
  env: { ...process.env, CLAUDECODE: undefined } as any,
201
201
  }).trim();
202
202
  cliChecks[name] = raw || "unknown";
203
203
  } catch {
204
- cliChecks[name] = "not found";
205
- api.logger.warn(
206
- `${name} CLI not found at ${bin}. The ${name}_run tool will fail. Install with: ${installCmd}`,
207
- );
204
+ // Fallback: check if the file exists (execFileSync can fail in worker contexts)
205
+ try {
206
+ require("node:fs").accessSync(bin, require("node:fs").constants.X_OK);
207
+ cliChecks[name] = "installed (version check skipped)";
208
+ } catch {
209
+ cliChecks[name] = "not found";
210
+ api.logger.warn(
211
+ `${name} CLI not found at ${bin}. The ${name}_run tool will fail. Install with: ${installCmd}`,
212
+ );
213
+ }
208
214
  }
209
215
  }
210
216
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calltelemetry/openclaw-linear",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Linear Agent plugin for OpenClaw — webhook-driven AI pipeline with OAuth, multi-agent routing, and issue triage",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/prompts.yaml CHANGED
@@ -5,13 +5,19 @@
5
5
  #
6
6
  # Edit these to customize worker/audit behavior without rebuilding the plugin.
7
7
  # Override path via `promptsPath` in plugin config.
8
+ #
9
+ # Access model:
10
+ # Zoe (orchestrator) — linearis READ ONLY (issues read/list/search)
11
+ # Worker — NO linearis access. Return text output only.
12
+ # Auditor — linearis READ + WRITE (can update status, close, comment)
8
13
 
9
14
  worker:
10
15
  system: |
11
- You are implementing a Linear issue. Your job is to plan and code the solution.
12
- Post an implementation summary as a Linear comment when done.
13
- DO NOT mark the issue as Done that is handled by the audit system.
14
- DO NOT attempt to change the issue status or labels.
16
+ You are a coding worker implementing a Linear issue. Your ONLY job is to write code and return a text summary.
17
+ You do NOT have access to linearis or any Linear issue management tools.
18
+ Do NOT attempt to update, close, comment on, or modify the Linear issue in any way.
19
+ Do NOT mark the issue as Done — the audit system handles all issue lifecycle.
20
+ Just write code and return your implementation summary as text.
15
21
  task: |
16
22
  Implement issue {{identifier}}: {{title}}
17
23
 
@@ -25,15 +31,22 @@ worker:
25
31
  2. Plan your approach
26
32
  3. Implement the solution in the worktree
27
33
  4. Run tests to verify your changes
28
- 5. Post an implementation summary as a comment on the Linear issue
29
- 6. Include what was changed, what tests were run, and any notes
34
+ 5. Return a text summary of what you changed, what tests you ran, and any notes
35
+
36
+ Your text output will be captured automatically. Do NOT use linearis or attempt to post comments.
30
37
 
31
38
  audit:
32
39
  system: |
33
- You are an independent auditor. Your job is to verify that work was completed correctly.
40
+ You are an independent auditor. Your job is to verify that work was completed correctly
41
+ and then update the Linear issue accordingly.
34
42
  The Linear issue body is the SOURCE OF TRUTH for what "done" means.
35
- Worker comments are secondary evidence of what was attempted.
43
+ Worker output is secondary evidence of what was attempted.
36
44
  You must be thorough and objective. Do not rubber-stamp.
45
+
46
+ You have WRITE access to linearis. After auditing, you are responsible for:
47
+ - Posting an audit summary comment on the issue
48
+ - Updating the issue status if the audit passes
49
+ Use `linearis` CLI via exec for these operations.
37
50
  task: |
38
51
  Audit issue {{identifier}}: {{title}}
39
52
 
@@ -44,12 +57,16 @@ audit:
44
57
 
45
58
  Checklist:
46
59
  1. Identify ALL acceptance criteria from the issue body
47
- 2. Read worker comments on the issue (use linearis)
60
+ 2. Read worker comments: `linearis issues read {{identifier}}`
48
61
  3. Verify each acceptance criterion is addressed in the code
49
62
  4. Run tests in the worktree — verify they pass
50
63
  5. Check test coverage if expectations are stated in the issue
51
64
  6. Review the code diff for quality and correctness
52
65
 
66
+ After auditing:
67
+ - Post your audit findings as a comment: `linearis comments create {{identifier}} --body "..."`
68
+ - If PASS: update status: `linearis issues update {{identifier}} --status "Done"`
69
+
53
70
  You MUST return a JSON verdict as the last line of your response:
54
71
  {"pass": true/false, "criteria": ["list of criteria found"], "gaps": ["list of unmet criteria"], "testResults": "summary of test output"}
55
72
 
@@ -59,3 +76,4 @@ rework:
59
76
  {{gaps}}
60
77
 
61
78
  Address these specific issues in your rework. Focus on the gaps listed above.
79
+ Remember: you do NOT have linearis access. Just fix the code and return a text summary.
package/src/agent.ts CHANGED
@@ -1,7 +1,36 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import { mkdirSync, readFileSync } from "node:fs";
2
4
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
5
  import type { LinearAgentApi, ActivityContent } from "./linear-api.js";
4
6
 
7
+ // ---------------------------------------------------------------------------
8
+ // Agent directory resolution (config-based, not ext API which ignores agentId)
9
+ // ---------------------------------------------------------------------------
10
+
11
+ interface AgentDirs {
12
+ workspaceDir: string;
13
+ agentDir: string;
14
+ }
15
+
16
+ function resolveAgentDirs(agentId: string, config: Record<string, any>): AgentDirs {
17
+ const home = process.env.HOME ?? "/home/claw";
18
+ const agentList = config?.agents?.list as Array<Record<string, any>> | undefined;
19
+ const agentEntry = agentList?.find((a) => a.id === agentId);
20
+
21
+ // Workspace: agent-specific override → agents.defaults.workspace → fallback
22
+ const workspaceDir = agentEntry?.workspace
23
+ ?? config?.agents?.defaults?.workspace
24
+ ?? join(home, ".openclaw", "workspace");
25
+
26
+ // Agent runtime dir: always ~/.openclaw/agents/{agentId}/agent
27
+ // (matches OpenClaw's internal structure)
28
+ const agentDir = join(home, ".openclaw", "agents", agentId, "agent");
29
+ mkdirSync(agentDir, { recursive: true });
30
+
31
+ return { workspaceDir, agentDir };
32
+ }
33
+
5
34
  // Import extensionAPI for embedded agent runner (internal, not in public SDK)
6
35
  let _extensionAPI: typeof import("/home/claw/.npm-global/lib/node_modules/openclaw/dist/extensionAPI.js") | null = null;
7
36
  async function getExtensionAPI() {
@@ -66,17 +95,22 @@ async function runEmbedded(
66
95
  ): Promise<AgentRunResult> {
67
96
  const ext = await getExtensionAPI();
68
97
 
69
- const workspaceDir = ext.resolveAgentWorkspaceDir({ agentId });
70
- const sessionFile = ext.resolveSessionFilePath(sessionId);
71
- const agentDir = ext.resolveAgentDir({ agentId });
98
+ // Load config so we can resolve agent dirs and providers correctly.
99
+ const config = await api.runtime.config.loadConfig();
100
+ const configAny = config as Record<string, any>;
101
+
102
+ // Resolve workspace and agent dirs from config (ext API ignores agentId).
103
+ const dirs = resolveAgentDirs(agentId, configAny);
104
+ const { workspaceDir, agentDir } = dirs;
72
105
  const runId = randomUUID();
73
106
 
74
- // Load config so embedded runner can resolve providers, API keys, etc.
75
- const config = await api.runtime.config.loadConfig();
107
+ // Build session file path under the correct agent's sessions directory.
108
+ const sessionsDir = join(agentDir, "sessions");
109
+ try { mkdirSync(sessionsDir, { recursive: true }); } catch {}
110
+ const sessionFile = join(sessionsDir, `${sessionId}.jsonl`);
76
111
 
77
112
  // Resolve model/provider from config — default is anthropic which requires
78
113
  // a separate API key. Our agents use openrouter.
79
- const configAny = config as Record<string, any>;
80
114
  const agentList = configAny?.agents?.list as Array<Record<string, any>> | undefined;
81
115
  const agentEntry = agentList?.find((a) => a.id === agentId);
82
116
  const modelRef: string =
@@ -89,7 +123,7 @@ async function runEmbedded(
89
123
  const provider = slashIdx > 0 ? modelRef.slice(0, slashIdx) : ext.DEFAULT_PROVIDER;
90
124
  const model = slashIdx > 0 ? modelRef.slice(slashIdx + 1) : modelRef;
91
125
 
92
- api.logger.info(`Embedded agent run: agent=${agentId} session=${sessionId} runId=${runId} provider=${provider} model=${model}`);
126
+ api.logger.info(`Embedded agent run: agent=${agentId} session=${sessionId} runId=${runId} provider=${provider} model=${model} workspaceDir=${workspaceDir} agentDir=${agentDir}`);
93
127
 
94
128
  const emit = (content: ActivityContent) => {
95
129
  streaming.linearApi.emitActivity(streaming.agentSessionId, content).catch((err) => {
@@ -138,7 +138,6 @@ export async function runClaude(
138
138
  if (model ?? pluginConfig?.claudeModel) {
139
139
  args.push("--model", (model ?? pluginConfig?.claudeModel) as string);
140
140
  }
141
- args.push("-C", workingDir);
142
141
  args.push("-p", prompt);
143
142
 
144
143
  api.logger.info(`Claude exec: ${CLAUDE_BIN} ${args.join(" ").slice(0, 200)}...`);
@@ -150,6 +149,7 @@ export async function runClaude(
150
149
 
151
150
  const child = spawn(CLAUDE_BIN, args, {
152
151
  stdio: ["ignore", "pipe", "pipe"],
152
+ cwd: workingDir,
153
153
  env,
154
154
  timeout: 0,
155
155
  });
package/src/webhook.ts CHANGED
@@ -187,12 +187,18 @@ export async function handleLinearWebhook(
187
187
  .map((c: any) => ` - **${c.user?.name ?? "Unknown"}**: ${c.body?.slice(0, 200)}`)
188
188
  .join("\n");
189
189
 
190
+ const notifIssueRef = enrichedIssue?.identifier ?? issue.id;
190
191
  const message = [
191
- `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).`,
192
193
  ``,
193
- `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.`,
194
198
  ``,
195
- `## 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)"}`,
196
202
  `**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
197
203
  ``,
198
204
  `**Description:**`,
@@ -200,7 +206,7 @@ export async function handleLinearWebhook(
200
206
  commentSummary ? `\n**Recent comments:**\n${commentSummary}` : "",
201
207
  comment?.body ? `\n**Triggering comment:**\n> ${comment.body}` : "",
202
208
  ``,
203
- `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.`,
204
210
  ].filter(Boolean).join("\n");
205
211
 
206
212
  // Dispatch agent with session lifecycle (non-blocking)
@@ -363,11 +369,19 @@ export async function handleLinearWebhook(
363
369
  .map((c: any) => `**${c.user?.name ?? c.actorName ?? "User"}**: ${(c.body ?? "").slice(0, 300)}`)
364
370
  .join("\n\n");
365
371
 
372
+ const issueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
366
373
  const message = [
367
- `You are an AI agent responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
368
- `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.`,
369
381
  ``,
370
- `## 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)"}`,
371
385
  `**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
372
386
  ``,
373
387
  `**Description:**`,
@@ -375,7 +389,7 @@ export async function handleLinearWebhook(
375
389
  commentContext ? `\n**Conversation:**\n${commentContext}` : "",
376
390
  userMessage ? `\n**Latest message:**\n> ${userMessage}` : "",
377
391
  ``,
378
- `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.`,
379
393
  ].filter(Boolean).join("\n");
380
394
 
381
395
  // Run agent directly (non-blocking)
@@ -543,11 +557,19 @@ export async function handleLinearWebhook(
543
557
  .map((c: any) => `**${c.user?.name ?? "User"}**: ${(c.body ?? "").slice(0, 300)}`)
544
558
  .join("\n\n");
545
559
 
560
+ const followUpIssueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
546
561
  const message = [
547
- `You are an AI agent responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
548
- `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.`,
549
569
  ``,
550
- `## 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)"}`,
551
573
  `**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
552
574
  ``,
553
575
  `**Description:**`,
@@ -555,7 +577,7 @@ export async function handleLinearWebhook(
555
577
  commentContext ? `\n**Recent conversation:**\n${commentContext}` : "",
556
578
  `\n**User's follow-up message:**\n> ${userMessage}`,
557
579
  ``,
558
- `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.`,
559
581
  ].filter(Boolean).join("\n");
560
582
 
561
583
  setActiveSession({
@@ -683,6 +705,14 @@ export async function handleLinearWebhook(
683
705
 
684
706
  api.logger.info(`Comment mention: @${mentionedAgent} on ${issue.identifier ?? issue.id} by ${commentor}`);
685
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
+
686
716
  // React with eyes to acknowledge the comment
687
717
  if (comment?.id) {
688
718
  linearApi.createReaction(comment.id, "eyes").catch(() => {});
@@ -712,9 +742,14 @@ export async function handleLinearWebhook(
712
742
  const assignee = enrichedIssue.assignee?.name ?? "Unassigned";
713
743
 
714
744
  const taskMessage = [
715
- `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).`,
746
+ ``,
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.`,
716
751
  ``,
717
- `You were mentioned by name. Respond naturally and helpfully as a team member. Be concise, markdown-friendly. Do NOT use JSON or structured output.`,
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.`,
718
753
  ``,
719
754
  `**Issue:** ${enrichedIssue.identifier ?? enrichedIssue.id} — ${enrichedIssue.title ?? "(untitled)"}`,
720
755
  `**Status:** ${state} | **Priority:** ${priority} | **Assignee:** ${assignee} | **Labels:** ${labels}`,
@@ -725,7 +760,7 @@ export async function handleLinearWebhook(
725
760
  `**${commentor} wrote:**`,
726
761
  `> ${commentBody}`,
727
762
  ``,
728
- `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.`,
729
764
  ].filter(Boolean).join("\n");
730
765
 
731
766
  // Dispatch to agent with full session lifecycle (non-blocking)
@@ -814,6 +849,7 @@ export async function handleLinearWebhook(
814
849
  }
815
850
  } finally {
816
851
  clearActiveSession(issue.id);
852
+ activeRuns.delete(issue.id);
817
853
  }
818
854
  })();
819
855