@calltelemetry/openclaw-linear 0.9.2 → 0.9.4

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
@@ -20,22 +20,23 @@ Connect Linear to AI agents. Issues get triaged, implemented, and audited — au
20
20
 
21
21
  - [x] Cloudflare tunnel setup (webhook ingress, no inbound ports)
22
22
  - [x] Linear webhook sync (Comment + Issue events)
23
+ - [x] Linear OAuth app webhook (AgentSessionEvent created/prompted)
23
24
  - [x] Linear API integration (issues, comments, labels, state transitions)
24
25
  - [x] Agent routing (`@mentions`, natural language intent classifier)
25
- - [ ] Linear OAuth app webhook (AgentSessionEvent created/prompted)
26
26
  - [x] Auto-triage (story points, labels, priority — read-only)
27
27
  - [x] Complexity-tier dispatch (small → Haiku, medium → Sonnet, high → Opus)
28
28
  - [x] Isolated git worktrees per dispatch
29
29
  - [x] Worker → Auditor pipeline (hard-enforced, not LLM-mediated)
30
- - [ ] Audit rework loop (gaps fed back, automatic retry)
31
- - [ ] Watchdog timeout + escalation
30
+ - [x] Audit rework loop (gaps fed back, automatic retry)
31
+ - [x] Watchdog timeout + escalation
32
32
  - [x] Webhook deduplication (60s sliding window across session/comment/assignment)
33
33
  - [ ] Multi-repo worktree support
34
34
  - [ ] Project planner (interview → user stories → sub-issues → DAG dispatch)
35
35
  - [ ] Cross-model plan review (Claude ↔ Codex ↔ Gemini)
36
- - [ ] Issue closure with summary report
36
+ - [x] Issue closure with summary report
37
37
  - [ ] Sub-issue decomposition (orchestrator-level only)
38
38
  - [x] `spawn_agent` / `ask_agent` sub-agent tools
39
+ - [x] CI + coverage badges (1000+ tests, Codecov integration)
39
40
  - [ ] **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.
40
41
  - [ ] **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.
41
42
  - [ ] **Parallel worktree conflict resolution** — DAG dispatch runs up to 3 issues concurrently in separate worktrees, but there's no merge conflict detection across them.
@@ -548,11 +549,97 @@ flowchart LR
548
549
  >
549
550
  > **Summary:** The search API endpoint was implemented with pagination, input validation, and error handling. All 14 tests pass. The frontend search page renders results correctly.
550
551
 
551
- ### Timeout recovery
552
+ ### Watchdog & timeout recovery
552
553
 
553
- If an agent produces no output for 2 minutes (configurable), the watchdog kills it and retries once. If the retry also times out, the issue is escalated.
554
+ Every running agent has an inactivity watchdog. If the agent goes silent no text, no tool calls, no thinking the watchdog kills it.
554
555
 
555
- **Notification:** `⚡ ENG-100 timed out (no activity for 120s). Will retry.`
556
+ ```
557
+ Agent runs ─────────── output ──→ timer resets (120s default)
558
+ output ──→ timer resets
559
+ ...
560
+ silence ─→ 120s passes ─→ KILL
561
+
562
+ ┌────────┴────────┐
563
+ ▼ ▼
564
+ Retry (auto) Already retried?
565
+ │ │
566
+ ▼ ▼
567
+ Agent runs again STUCK → you're notified
568
+ ```
569
+
570
+ **What resets the timer:** any agent output — partial text, tool call start/result, reasoning stream, or error.
571
+
572
+ **What triggers a kill:** LLM hangs, API timeouts, CLI lockups, rate limiting — anything that causes the agent to stop producing output.
573
+
574
+ **After a kill:**
575
+ 1. First timeout → automatic retry (new attempt, same worktree)
576
+ 2. Second timeout → dispatch transitions to `stuck`, Linear comment posted with remediation steps, you get a notification
577
+
578
+ **The "Agent Timed Out" comment includes:**
579
+ - `/dispatch retry ENG-100` command to try again
580
+ - Suggestion to break the issue into smaller pieces
581
+ - How to increase `inactivitySec` in agent profiles
582
+ - Path to `.claw/log.jsonl` for debugging
583
+
584
+ **Configure per agent** in `~/.openclaw/agent-profiles.json`:
585
+ ```json
586
+ { "agents": { "mal": { "watchdog": { "inactivitySec": 180 } } } }
587
+ ```
588
+
589
+ ### Audit rework loop
590
+
591
+ When the auditor finds problems, it doesn't just fail — it tells the worker exactly what's wrong, and the worker tries again automatically.
592
+
593
+ ```
594
+ Worker implements ──→ Auditor reviews
595
+
596
+ ┌────┴────┐
597
+ ▼ ▼
598
+ PASS FAIL
599
+ │ │
600
+ ▼ ▼
601
+ Done Gaps extracted
602
+
603
+
604
+ Worker gets gaps as context ──→ "PREVIOUS AUDIT FAILED:
605
+ │ - Missing input validation
606
+ │ - No test for empty query"
607
+
608
+ Rework attempt (same worktree)
609
+
610
+ ┌────┴────┐
611
+ ▼ ▼
612
+ PASS FAIL again?
613
+ │ │
614
+ ▼ ▼
615
+ Done Retries left?
616
+
617
+ ┌────┴────┐
618
+ ▼ ▼
619
+ Retry STUCK → you're notified
620
+ ```
621
+
622
+ **How gaps flow back:**
623
+ 1. Auditor returns a structured verdict: `{ pass: false, gaps: ["missing validation", "no empty query test"], criteria: [...] }`
624
+ 2. Pipeline extracts the `gaps` array
625
+ 3. Next worker prompt gets a "PREVIOUS AUDIT FAILED" addendum with the gap list
626
+ 4. Worker sees exactly what to fix — no guessing
627
+
628
+ **What you control:**
629
+ - `maxReworkAttempts` (default: `2`) — how many audit failures before escalation
630
+ - After max attempts, issue goes to `stuck` with reason `audit_failed_Nx`
631
+ - You get a Linear comment with what went wrong and a notification
632
+
633
+ **What the worker sees on rework:**
634
+ ```
635
+ PREVIOUS AUDIT FAILED — fix these gaps before proceeding:
636
+ 1. Missing input validation on the search endpoint
637
+ 2. No test for empty query string
638
+
639
+ Your previous work is still in the worktree. Fix the issues above and run tests again.
640
+ ```
641
+
642
+ **Artifacts per attempt:** Each rework cycle writes `worker-{N}.md` and `audit-{N}.json` to `.claw/`, so you can see what happened at every attempt.
556
643
 
557
644
  ### Project-level progress
558
645
 
@@ -1488,7 +1575,7 @@ This is separate from the main `doctor` because each live test spawns a real CLI
1488
1575
 
1489
1576
  ### Unit tests
1490
1577
 
1491
- 551 tests covering the full pipeline — triage, dispatch, audit, planning, intent classification, native issue tools, cross-model review, notifications, and infrastructure:
1578
+ 1000+ tests covering the full pipeline — triage, dispatch, audit, planning, intent classification, native issue tools, cross-model review, notifications, watchdog, and infrastructure:
1492
1579
 
1493
1580
  ```bash
1494
1581
  cd ~/claw-extensions/linear
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calltelemetry/openclaw-linear",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
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",
@@ -10,13 +10,14 @@
10
10
  */
11
11
  import { readFileSync } from "node:fs";
12
12
  import { join } from "node:path";
13
+ import { homedir } from "node:os";
13
14
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
14
15
  import { LinearAgentApi } from "../api/linear-api.js";
15
16
 
16
17
  // ── Setup ──────────────────────────────────────────────────────────
17
18
 
18
19
  const AUTH_PROFILES_PATH = join(
19
- process.env.HOME ?? "/home/claw",
20
+ homedir(),
20
21
  ".openclaw",
21
22
  "auth-profiles.json",
22
23
  );
@@ -87,6 +87,9 @@ vi.mock("../pipeline/pipeline.js", () => ({
87
87
  vi.mock("../pipeline/active-session.js", () => ({
88
88
  setActiveSession: mockSetActiveSession,
89
89
  clearActiveSession: mockClearActiveSession,
90
+ getIssueAffinity: vi.fn().mockReturnValue(null),
91
+ _configureAffinityTtl: vi.fn(),
92
+ _resetAffinityForTesting: vi.fn(),
90
93
  }));
91
94
 
92
95
  vi.mock("../infra/observability.js", () => ({
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { join } from "node:path";
2
+ import { join, dirname } from "node:path";
3
+ import { homedir } from "node:os";
3
4
  import { mkdirSync, readFileSync } from "node:fs";
5
+ import { createRequire } from "node:module";
4
6
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
5
7
  import type { LinearAgentApi, ActivityContent } from "../api/linear-api.js";
6
8
  import { InactivityWatchdog, resolveWatchdogConfig } from "./watchdog.js";
@@ -15,7 +17,7 @@ interface AgentDirs {
15
17
  }
16
18
 
17
19
  function resolveAgentDirs(agentId: string, config: Record<string, any>): AgentDirs {
18
- const home = process.env.HOME ?? "/home/claw";
20
+ const home = homedir();
19
21
  const agentList = config?.agents?.list as Array<Record<string, any>> | undefined;
20
22
  const agentEntry = agentList?.find((a) => a.id === agentId);
21
23
 
@@ -33,13 +35,13 @@ function resolveAgentDirs(agentId: string, config: Record<string, any>): AgentDi
33
35
  }
34
36
 
35
37
  // Import extensionAPI for embedded agent runner (internal, not in public SDK)
36
- let _extensionAPI: typeof import("/home/claw/.npm-global/lib/node_modules/openclaw/dist/extensionAPI.js") | null = null;
38
+ let _extensionAPI: any | null = null;
37
39
  async function getExtensionAPI() {
38
40
  if (!_extensionAPI) {
39
- // Dynamic import to avoid blocking module load if unavailable
40
- _extensionAPI = await import(
41
- "/home/claw/.npm-global/lib/node_modules/openclaw/dist/extensionAPI.js"
42
- );
41
+ // Resolve the openclaw package location dynamically, then import extensionAPI
42
+ const _require = createRequire(import.meta.url);
43
+ const openclawDir = dirname(_require.resolve("openclaw/package.json"));
44
+ _extensionAPI = await import(join(openclawDir, "dist", "extensionAPI.js"));
43
45
  }
44
46
  return _extensionAPI;
45
47
  }
@@ -129,7 +129,7 @@ interface AgentProfileWatchdog {
129
129
  toolTimeoutSec?: number;
130
130
  }
131
131
 
132
- const PROFILES_PATH = join(process.env.HOME ?? "/home/claw", ".openclaw", "agent-profiles.json");
132
+ const PROFILES_PATH = join(homedir(), ".openclaw", "agent-profiles.json");
133
133
 
134
134
  function loadProfileWatchdog(agentId: string): AgentProfileWatchdog | null {
135
135
  try {
@@ -1,11 +1,12 @@
1
1
  import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { homedir } from "node:os";
3
4
  import { refreshLinearToken } from "./auth.js";
4
5
  import { withResilience } from "../infra/resilience.js";
5
6
 
6
7
  export const LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql";
7
8
  export const AUTH_PROFILES_PATH = join(
8
- process.env.HOME ?? "/home/claw",
9
+ homedir(),
9
10
  ".openclaw",
10
11
  "auth-profiles.json",
11
12
  );
@@ -2,10 +2,11 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
3
  import { writeFileSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
+ import { homedir } from "node:os";
5
6
 
6
7
  const LINEAR_OAUTH_TOKEN_URL = "https://api.linear.app/oauth/token";
7
8
  const AUTH_PROFILES_PATH = join(
8
- process.env.HOME ?? "/home/claw",
9
+ homedir(),
9
10
  ".openclaw",
10
11
  "auth-profiles.json",
11
12
  );
package/src/infra/cli.ts CHANGED
@@ -846,8 +846,8 @@ async function reposAction(
846
846
  console.log(`\n No "repos" configured in plugin config.`);
847
847
  console.log(` Add a repos map to openclaw.json → plugins.entries.openclaw-linear.config:`);
848
848
  console.log(`\n "repos": {`);
849
- console.log(` "api": "/home/claw/repos/api",`);
850
- console.log(` "frontend": "/home/claw/repos/frontend"`);
849
+ console.log(` "api": "~/repos/api",`);
850
+ console.log(` "frontend": "~/repos/frontend"`);
851
851
  console.log(` }\n`);
852
852
  return;
853
853
  }
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import { ensureGitignore } from "../pipeline/artifacts.js";
6
6
  import type { RepoConfig } from "./multi-repo.js";
7
7
 
8
- const DEFAULT_BASE_REPO = "/home/claw/ai-workspace";
8
+ const DEFAULT_BASE_REPO = path.join(homedir(), "ai-workspace");
9
9
  const DEFAULT_WORKTREE_BASE_DIR = path.join(homedir(), ".openclaw", "worktrees");
10
10
 
11
11
  export interface WorktreeInfo {
@@ -22,7 +22,7 @@ export interface WorktreeStatus {
22
22
  }
23
23
 
24
24
  export interface WorktreeOptions {
25
- /** Base git repo to create worktrees from. Default: /home/claw/ai-workspace */
25
+ /** Base git repo to create worktrees from. Default: ~/ai-workspace */
26
26
  baseRepo?: string;
27
27
  /** Directory under which worktrees are created. Default: ~/.openclaw/worktrees */
28
28
  baseDir?: string;
@@ -33,6 +33,9 @@ vi.mock("../api/linear-api.js", () => ({
33
33
  vi.mock("../pipeline/active-session.js", () => ({
34
34
  setActiveSession: vi.fn(),
35
35
  clearActiveSession: vi.fn(),
36
+ getIssueAffinity: vi.fn().mockReturnValue(null),
37
+ _configureAffinityTtl: vi.fn(),
38
+ _resetAffinityForTesting: vi.fn(),
36
39
  }));
37
40
 
38
41
  vi.mock("../infra/observability.js", () => ({