@calltelemetry/openclaw-linear 0.9.19 → 0.9.20

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 CHANGED
@@ -41,7 +41,9 @@ Connect Linear to AI agents. Issues get triaged, implemented, and audited — au
41
41
  - [x] CI + coverage badges (1170+ tests, Codecov integration)
42
42
  - [x] Setup wizard (`openclaw openclaw-linear setup`) + `doctor --fix` auto-repair
43
43
  - [x] Project context auto-detection (repo, framework, build/test commands → worker/audit prompts)
44
- - [x] Per-backend CLI tools (`cli_codex`, `cli_claude`, `cli_gemini`) with Linear session activity streaming
44
+ - [x] Per-backend CLI tools (`cli_codex`, `cli_claude`, `cli_gemini`) with `[Codex]`/`[Claude]`/`[Gemini]`-prefixed Linear session activity streaming
45
+ - [x] Immediate thought emission on comment receipt and tool dispatch (visible before long-running tasks complete)
46
+ - [x] Proactive OAuth token refresh timer (runs on startup, then every 6h)
45
47
  - [ ] **Worktree → PR merge** — `createPullRequest()` exists but is not wired into the pipeline. After audit pass, commits sit on a `codex/{identifier}` branch. You create the PR manually.
46
48
  - [ ] **Sub-agent worktree sharing** — Sub-agents spawned via `spawn_agent`/`ask_agent` do not inherit the parent worktree. They run in their own session without code access.
47
49
  - [ ] **Parallel worktree conflict resolution** — DAG dispatch runs up to 3 issues concurrently in separate worktrees, but there's no merge conflict detection across them.
@@ -118,7 +120,7 @@ The end result: you work in Linear. You create issues, assign them, comment in p
118
120
 
119
121
  ### Multi-Backend & Multi-Repo
120
122
 
121
- - **Three coding backends** — Codex (OpenAI), Claude (Anthropic), Gemini (Google). Configurable globally or per-agent. Each backend registers as a dedicated tool (`cli_codex`, `cli_claude`, `cli_gemini`) so agents and Linear session UI show exactly which backend is running. Per-agent overrides let you assign different backends to different team members.
123
+ - **Three coding backends** — Codex (OpenAI), Claude (Anthropic), Gemini (Google). Configurable globally or per-agent. Each backend registers as a dedicated tool (`cli_codex`, `cli_claude`, `cli_gemini`) with `[Codex]`/`[Claude]`/`[Gemini]`-prefixed activity streaming — thoughts, actions, progress, and results all show the backend label in Linear's session UI so you always know which runner is active. Per-agent overrides let you assign different backends to different team members.
122
124
  - **Multi-repo dispatch** — Tag an issue with `<!-- repos: api, frontend -->` and the worker gets isolated worktrees for each repo. One issue, multiple codebases, one agent session.
123
125
 
124
126
  ### Operations
@@ -1303,7 +1305,7 @@ Every agent session gets these registered tools. They're available as native too
1303
1305
 
1304
1306
  ### `cli_codex` / `cli_claude` / `cli_gemini` — Coding backend tools
1305
1307
 
1306
- Three per-backend tools that send tasks to their respective coding CLIs. Each agent sees only the tool matching its configured backend (e.g., an agent configured for `codex` gets `cli_codex`). The tool name is visible in Linear's agent session UI, so you always know which backend is running. The agent writes the prompt; the plugin handles worktree setup, session activity streaming, and output capture.
1308
+ Three per-backend tools that send tasks to their respective coding CLIs. Each agent sees only the tool matching its configured backend (e.g., an agent configured for `codex` gets `cli_codex`). All activity emissions are prefixed with the backend label (`[Codex]`, `[Claude]`, `[Gemini]`) — thoughts, action starts, progress updates, results, and errors — so the Linear session UI always shows which runner is active. When a CLI tool is dispatched, an immediate thought is emitted with the prompt excerpt before the long-running task begins. The agent writes the prompt; the plugin handles worktree setup, session activity streaming, and output capture.
1307
1309
 
1308
1310
  ### `linear_issues` — Native Linear API
1309
1311
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calltelemetry/openclaw-linear",
3
- "version": "0.9.19",
3
+ "version": "0.9.20",
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",
@@ -293,6 +293,8 @@ async function runEmbedded(
293
293
 
294
294
  // Track last emitted tool to avoid duplicates
295
295
  let lastToolAction = "";
296
+ // Derive a friendly label from cli_ tool names: cli_codex→"Codex", cli_claude→"Claude"
297
+ const cliLabel = (name: string) => name.startsWith("cli_") ? name.slice(4).charAt(0).toUpperCase() + name.slice(5) : name;
296
298
 
297
299
  watchdog.start();
298
300
 
@@ -335,7 +337,8 @@ async function runEmbedded(
335
337
  if (text) {
336
338
  // Truncate tool results for activity display
337
339
  const truncated = text.length > 300 ? text.slice(0, 300) + "..." : text;
338
- emit({ type: "action", action: lastToolAction || "Tool result", parameter: truncated });
340
+ const prefix = lastToolAction.startsWith("cli_") ? `[${cliLabel(lastToolAction)}] ` : "";
341
+ emit({ type: "action", action: `${prefix}${lastToolAction || "Tool result"}`, parameter: truncated });
339
342
  }
340
343
  },
341
344
 
@@ -364,11 +367,14 @@ async function runEmbedded(
364
367
  if (phase === "start") {
365
368
  lastToolAction = toolName;
366
369
 
367
- // cli_codex / cli_claude / cli_gemini: show working dir and prompt excerpt
370
+ // cli_codex / cli_claude / cli_gemini: emit a thought + action so the
371
+ // user immediately sees what the agent is dispatching and why.
368
372
  if (toolName.startsWith("cli_") && inputObj) {
373
+ const tag = cliLabel(toolName);
369
374
  const prompt = String(inputObj.prompt ?? "").slice(0, 250);
370
375
  const workDir = inputObj.workingDir ? ` in ${inputObj.workingDir}` : "";
371
- emit({ type: "action", action: `Running ${toolName}${workDir}`, parameter: prompt });
376
+ emit({ type: "thought", body: `[${tag}] Starting${workDir}: "${prompt}"\n\n${toolName}\nin progress` });
377
+ emit({ type: "action", action: `[${tag}] Running${workDir}`, parameter: prompt });
372
378
  } else {
373
379
  const detail = input || meta || toolName;
374
380
  emit({ type: "action", action: `Running ${toolName}`, parameter: detail.slice(0, 300) });
@@ -378,18 +384,21 @@ async function runEmbedded(
378
384
  // Tool execution update — partial progress (keeps Linear UI alive for long tools)
379
385
  if (phase === "update") {
380
386
  const detail = meta || input || "in progress";
381
- emit({ type: "action", action: `${toolName}`, parameter: detail.slice(0, 300) });
387
+ const prefix = toolName.startsWith("cli_") ? `[${cliLabel(toolName)}] ` : "";
388
+ emit({ type: "action", action: `${prefix}${toolName}`, parameter: detail.slice(0, 300) });
382
389
  }
383
390
 
384
391
  // Tool execution completed successfully
385
392
  if (phase === "result" && !data.isError) {
386
393
  const detail = meta ? meta.slice(0, 300) : "completed";
387
- emit({ type: "action", action: `${toolName} done`, parameter: detail });
394
+ const prefix = toolName.startsWith("cli_") ? `[${cliLabel(toolName)}] ` : "";
395
+ emit({ type: "action", action: `${prefix}${toolName} done`, parameter: detail });
388
396
  }
389
397
 
390
398
  // Tool execution result with error
391
399
  if (phase === "result" && data.isError) {
392
- emit({ type: "action", action: `${toolName} failed`, parameter: (meta || "error").slice(0, 300) });
400
+ const prefix = toolName.startsWith("cli_") ? `[${cliLabel(toolName)}] ` : "";
401
+ emit({ type: "action", action: `${prefix}${toolName} failed`, parameter: (meta || "error").slice(0, 300) });
393
402
  }
394
403
  },
395
404
 
@@ -119,6 +119,7 @@ export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
119
119
  let killed = false;
120
120
  let killedByWatchdog = false;
121
121
  let resolved = false;
122
+ let completionEventReceived = false;
122
123
  const collectedMessages: string[] = [];
123
124
 
124
125
  const timer = setTimeout(() => {
@@ -175,8 +176,15 @@ export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
175
176
  output: `Agent timed out after ${Math.round(timeoutMs / 1000)}s. Partial output:\n${output}`,
176
177
  error: "timeout",
177
178
  });
179
+ } else if (reason === "unexpected_exit") {
180
+ logger.warn(`tmux session ${sessionName} exited without completion event`);
181
+ resolve({
182
+ success: false,
183
+ output: `Agent session exited unexpectedly (no completion event received). Partial output:\n${output}`,
184
+ error: "unexpected_exit",
185
+ });
178
186
  } else {
179
- // Normal completion
187
+ // Normal completion — only reached when completionEventReceived is true
180
188
  resolve({ success: true, output });
181
189
  }
182
190
  }
@@ -225,6 +233,7 @@ export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
225
233
 
226
234
  // Detect completion — Claude uses "result", Codex uses "session.completed"
227
235
  if (event.type === "result" || event.type === "session.completed") {
236
+ completionEventReceived = true;
228
237
  cleanup("done");
229
238
  rl.close();
230
239
  }
@@ -233,7 +242,12 @@ export async function runInTmux(opts: RunInTmuxOptions): Promise<CliResult> {
233
242
  // Handle tail process ending (tmux session exited)
234
243
  tail.on("close", () => {
235
244
  if (!resolved) {
236
- cleanup("done");
245
+ // If we never saw a completion event, the session died unexpectedly
246
+ if (completionEventReceived) {
247
+ cleanup("done");
248
+ } else {
249
+ cleanup("unexpected_exit");
250
+ }
237
251
  }
238
252
  rl.close();
239
253
  });
@@ -1731,11 +1731,12 @@ async function dispatchCommentToAgent(
1731
1731
  });
1732
1732
  }
1733
1733
 
1734
- // Emit thought
1734
+ // Emit thought — include comment excerpt so the user sees immediate context
1735
1735
  if (agentSessionId) {
1736
+ const excerpt = commentBody.length > 200 ? commentBody.slice(0, 200) + "..." : commentBody;
1736
1737
  await linearApi.emitActivity(agentSessionId, {
1737
1738
  type: "thought",
1738
- body: `${label} is processing comment on ${issueRef}...`,
1739
+ body: `${label} received comment on ${issueRef}: "${excerpt}" — working on it now...`,
1739
1740
  }).catch(() => {});
1740
1741
  }
1741
1742