@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 +5 -3
- package/package.json +1 -1
- package/src/agent/agent.ts +15 -6
- package/src/infra/tmux-runner.ts +16 -2
- package/src/pipeline/webhook.ts +3 -2
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`)
|
|
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`).
|
|
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
package/src/agent/agent.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/infra/tmux-runner.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
});
|
package/src/pipeline/webhook.ts
CHANGED
|
@@ -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}
|
|
1739
|
+
body: `${label} received comment on ${issueRef}: "${excerpt}" — working on it now...`,
|
|
1739
1740
|
}).catch(() => {});
|
|
1740
1741
|
}
|
|
1741
1742
|
|