@calltelemetry/openclaw-linear 0.9.3 → 0.9.5
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/package.json +1 -1
- package/src/__test__/fixtures/recorded-sub-issue-flow.ts +172 -0
- 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 +2 -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 +931 -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/pipeline.test.ts +3 -0
- package/src/pipeline/webhook-dedup.test.ts +3 -0
- package/src/pipeline/webhook.test.ts +2268 -0
- 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/package.json
CHANGED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recorded API responses from sub-issue decomposition smoke test.
|
|
3
|
+
*
|
|
4
|
+
* Initially seeded with placeholder data. Overwritten with real API responses
|
|
5
|
+
* when the smoke test runs: npx vitest run src/__test__/smoke-linear-api.test.ts
|
|
6
|
+
*
|
|
7
|
+
* Last recorded: seed (placeholder)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const RECORDED = {
|
|
11
|
+
teamStates: [
|
|
12
|
+
{ id: "state-backlog", name: "Backlog", type: "backlog" },
|
|
13
|
+
{ id: "state-started", name: "In Progress", type: "started" },
|
|
14
|
+
{ id: "state-done", name: "Done", type: "completed" },
|
|
15
|
+
{ id: "state-canceled", name: "Canceled", type: "canceled" },
|
|
16
|
+
],
|
|
17
|
+
createParent: { id: "parent-001", identifier: "UAT-100" },
|
|
18
|
+
createSubIssue1: { id: "sub1-001", identifier: "UAT-101" },
|
|
19
|
+
createSubIssue2: { id: "sub2-001", identifier: "UAT-102" },
|
|
20
|
+
parentDetails: {
|
|
21
|
+
id: "parent-001",
|
|
22
|
+
identifier: "UAT-100",
|
|
23
|
+
title: "[SMOKE TEST] Sub-Issue Parent: Search Feature",
|
|
24
|
+
description:
|
|
25
|
+
"Auto-generated by smoke test to verify sub-issue decomposition.\n\n" +
|
|
26
|
+
"This parent issue should have two sub-issues created under it.",
|
|
27
|
+
estimate: null,
|
|
28
|
+
state: { name: "Backlog", type: "backlog" },
|
|
29
|
+
creator: { name: "Test User", email: null as string | null },
|
|
30
|
+
assignee: null as { name: string } | null,
|
|
31
|
+
labels: { nodes: [] as Array<{ id: string; name: string }> },
|
|
32
|
+
team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
|
|
33
|
+
comments: {
|
|
34
|
+
nodes: [] as Array<{
|
|
35
|
+
body: string;
|
|
36
|
+
user: { name: string } | null;
|
|
37
|
+
createdAt: string;
|
|
38
|
+
}>,
|
|
39
|
+
},
|
|
40
|
+
project: null as { id: string; name: string } | null,
|
|
41
|
+
parent: null as { id: string; identifier: string } | null,
|
|
42
|
+
relations: {
|
|
43
|
+
nodes: [] as Array<{
|
|
44
|
+
type: string;
|
|
45
|
+
relatedIssue: { id: string; identifier: string; title: string };
|
|
46
|
+
}>,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
subIssue1Details: {
|
|
50
|
+
id: "sub1-001",
|
|
51
|
+
identifier: "UAT-101",
|
|
52
|
+
title: "[SMOKE TEST] Sub-Issue 1: Backend API",
|
|
53
|
+
description:
|
|
54
|
+
"Implement the backend search API endpoint.\n\n" +
|
|
55
|
+
"Given a search query, when the API is called, then matching results are returned.",
|
|
56
|
+
estimate: 2,
|
|
57
|
+
state: { name: "Backlog", type: "backlog" },
|
|
58
|
+
creator: { name: "Test User", email: null as string | null },
|
|
59
|
+
assignee: null as { name: string } | null,
|
|
60
|
+
labels: { nodes: [] as Array<{ id: string; name: string }> },
|
|
61
|
+
team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
|
|
62
|
+
comments: {
|
|
63
|
+
nodes: [] as Array<{
|
|
64
|
+
body: string;
|
|
65
|
+
user: { name: string } | null;
|
|
66
|
+
createdAt: string;
|
|
67
|
+
}>,
|
|
68
|
+
},
|
|
69
|
+
project: null as { id: string; name: string } | null,
|
|
70
|
+
parent: { id: "parent-001", identifier: "UAT-100" },
|
|
71
|
+
relations: {
|
|
72
|
+
nodes: [] as Array<{
|
|
73
|
+
type: string;
|
|
74
|
+
relatedIssue: { id: string; identifier: string; title: string };
|
|
75
|
+
}>,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
subIssue2Details: {
|
|
79
|
+
id: "sub2-001",
|
|
80
|
+
identifier: "UAT-102",
|
|
81
|
+
title: "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
82
|
+
description:
|
|
83
|
+
"Build the frontend search UI component.\n\n" +
|
|
84
|
+
"Given the search page loads, when the user types a query, then results display in real-time.",
|
|
85
|
+
estimate: 3,
|
|
86
|
+
state: { name: "Backlog", type: "backlog" },
|
|
87
|
+
creator: { name: "Test User", email: null as string | null },
|
|
88
|
+
assignee: null as { name: string } | null,
|
|
89
|
+
labels: { nodes: [] as Array<{ id: string; name: string }> },
|
|
90
|
+
team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
|
|
91
|
+
comments: {
|
|
92
|
+
nodes: [] as Array<{
|
|
93
|
+
body: string;
|
|
94
|
+
user: { name: string } | null;
|
|
95
|
+
createdAt: string;
|
|
96
|
+
}>,
|
|
97
|
+
},
|
|
98
|
+
project: null as { id: string; name: string } | null,
|
|
99
|
+
parent: { id: "parent-001", identifier: "UAT-100" },
|
|
100
|
+
relations: {
|
|
101
|
+
nodes: [] as Array<{
|
|
102
|
+
type: string;
|
|
103
|
+
relatedIssue: { id: string; identifier: string; title: string };
|
|
104
|
+
}>,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
createRelation: { id: "rel-001" },
|
|
108
|
+
subIssue1WithRelation: {
|
|
109
|
+
id: "sub1-001",
|
|
110
|
+
identifier: "UAT-101",
|
|
111
|
+
title: "[SMOKE TEST] Sub-Issue 1: Backend API",
|
|
112
|
+
description:
|
|
113
|
+
"Implement the backend search API endpoint.\n\n" +
|
|
114
|
+
"Given a search query, when the API is called, then matching results are returned.",
|
|
115
|
+
estimate: 2,
|
|
116
|
+
state: { name: "Backlog", type: "backlog" },
|
|
117
|
+
creator: { name: "Test User", email: null as string | null },
|
|
118
|
+
assignee: null as { name: string } | null,
|
|
119
|
+
labels: { nodes: [] as Array<{ id: string; name: string }> },
|
|
120
|
+
team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
|
|
121
|
+
comments: {
|
|
122
|
+
nodes: [] as Array<{
|
|
123
|
+
body: string;
|
|
124
|
+
user: { name: string } | null;
|
|
125
|
+
createdAt: string;
|
|
126
|
+
}>,
|
|
127
|
+
},
|
|
128
|
+
project: null as { id: string; name: string } | null,
|
|
129
|
+
parent: { id: "parent-001", identifier: "UAT-100" },
|
|
130
|
+
relations: {
|
|
131
|
+
nodes: [
|
|
132
|
+
{
|
|
133
|
+
type: "blocks",
|
|
134
|
+
relatedIssue: {
|
|
135
|
+
id: "sub2-001",
|
|
136
|
+
identifier: "UAT-102",
|
|
137
|
+
title: "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
subIssue2WithRelation: {
|
|
144
|
+
id: "sub2-001",
|
|
145
|
+
identifier: "UAT-102",
|
|
146
|
+
title: "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
147
|
+
description:
|
|
148
|
+
"Build the frontend search UI component.\n\n" +
|
|
149
|
+
"Given the search page loads, when the user types a query, then results display in real-time.",
|
|
150
|
+
estimate: 3,
|
|
151
|
+
state: { name: "Backlog", type: "backlog" },
|
|
152
|
+
creator: { name: "Test User", email: null as string | null },
|
|
153
|
+
assignee: null as { name: string } | null,
|
|
154
|
+
labels: { nodes: [] as Array<{ id: string; name: string }> },
|
|
155
|
+
team: { id: "team-uat", name: "UAT", issueEstimationType: "notUsed" },
|
|
156
|
+
comments: {
|
|
157
|
+
nodes: [] as Array<{
|
|
158
|
+
body: string;
|
|
159
|
+
user: { name: string } | null;
|
|
160
|
+
createdAt: string;
|
|
161
|
+
}>,
|
|
162
|
+
},
|
|
163
|
+
project: null as { id: string; name: string } | null,
|
|
164
|
+
parent: { id: "parent-001", identifier: "UAT-100" },
|
|
165
|
+
relations: {
|
|
166
|
+
nodes: [] as Array<{
|
|
167
|
+
type: string;
|
|
168
|
+
relatedIssue: { id: string; identifier: string; title: string };
|
|
169
|
+
}>,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
@@ -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
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
10
11
|
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Defaults (seconds — matches config units)
|
|
@@ -129,7 +130,7 @@ interface AgentProfileWatchdog {
|
|
|
129
130
|
toolTimeoutSec?: number;
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
const PROFILES_PATH = join(
|
|
133
|
+
const PROFILES_PATH = join(homedir(), ".openclaw", "agent-profiles.json");
|
|
133
134
|
|
|
134
135
|
function loadProfileWatchdog(agentId: string): AgentProfileWatchdog | null {
|
|
135
136
|
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", () => ({
|