@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 +95 -8
- package/package.json +1 -1
- package/src/__test__/smoke-linear-api.test.ts +2 -1
- package/src/__test__/webhook-scenarios.test.ts +3 -0
- package/src/agent/agent.ts +9 -7
- package/src/agent/watchdog.ts +1 -1
- package/src/api/linear-api.ts +2 -1
- package/src/api/oauth-callback.ts +2 -1
- package/src/infra/cli.ts +2 -2
- package/src/infra/codex-worktree.ts +2 -2
- package/src/infra/config-paths.test.ts +3 -0
- package/src/infra/doctor.test.ts +621 -1
- package/src/infra/multi-repo.test.ts +11 -9
- package/src/infra/multi-repo.ts +3 -2
- package/src/infra/shared-profiles.ts +2 -1
- package/src/infra/template.test.ts +2 -2
- package/src/pipeline/active-session.test.ts +96 -1
- package/src/pipeline/active-session.ts +60 -0
- package/src/pipeline/artifacts.ts +1 -1
- package/src/pipeline/e2e-dispatch.test.ts +135 -0
- package/src/pipeline/pipeline.test.ts +82 -0
- package/src/pipeline/webhook-dedup.test.ts +3 -0
- package/src/pipeline/webhook.test.ts +2088 -2
- package/src/pipeline/webhook.ts +24 -6
- package/src/tools/claude-tool.ts +1 -1
- package/src/tools/cli-shared.test.ts +3 -0
- package/src/tools/cli-shared.ts +3 -1
- package/src/tools/code-tool.test.ts +3 -0
- package/src/tools/code-tool.ts +1 -1
- package/src/tools/codex-tool.ts +1 -1
- package/src/tools/gemini-tool.ts +1 -1
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
|
-
- [
|
|
31
|
-
- [
|
|
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
|
-
- [
|
|
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
|
-
###
|
|
552
|
+
### Watchdog & timeout recovery
|
|
552
553
|
|
|
553
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
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", () => ({
|
package/src/agent/agent.ts
CHANGED
|
@@ -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 =
|
|
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:
|
|
38
|
+
let _extensionAPI: any | null = null;
|
|
37
39
|
async function getExtensionAPI() {
|
|
38
40
|
if (!_extensionAPI) {
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
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
|
}
|
package/src/agent/watchdog.ts
CHANGED
|
@@ -129,7 +129,7 @@ interface AgentProfileWatchdog {
|
|
|
129
129
|
toolTimeoutSec?: number;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
const PROFILES_PATH = join(
|
|
132
|
+
const PROFILES_PATH = join(homedir(), ".openclaw", "agent-profiles.json");
|
|
133
133
|
|
|
134
134
|
function loadProfileWatchdog(agentId: string): AgentProfileWatchdog | null {
|
|
135
135
|
try {
|
package/src/api/linear-api.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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": "
|
|
850
|
-
console.log(` "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 = "
|
|
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:
|
|
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", () => ({
|