@calltelemetry/openclaw-linear 0.3.0 → 0.4.0
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 +339 -365
- package/index.ts +50 -2
- package/openclaw.plugin.json +7 -1
- package/package.json +3 -2
- package/src/active-session.ts +66 -0
- package/src/agent.ts +173 -1
- package/src/auth.ts +6 -2
- package/src/claude-tool.ts +280 -0
- package/src/cli-shared.ts +75 -0
- package/src/cli.ts +39 -0
- package/src/client.ts +1 -0
- package/src/code-tool.ts +202 -0
- package/src/codex-tool.ts +240 -0
- package/src/codex-worktree.ts +264 -0
- package/src/gemini-tool.ts +238 -0
- package/src/orchestration-tools.ts +134 -0
- package/src/pipeline.ts +68 -10
- package/src/tools.ts +29 -79
- package/src/webhook.ts +321 -90
package/src/pipeline.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import type { LinearAgentApi, ActivityContent } from "./linear-api.js";
|
|
3
3
|
import { runAgent } from "./agent.js";
|
|
4
|
+
import { setActiveSession, clearActiveSession } from "./active-session.js";
|
|
4
5
|
|
|
5
6
|
export interface PipelineContext {
|
|
6
7
|
api: OpenClawPluginApi;
|
|
@@ -14,6 +15,10 @@ export interface PipelineContext {
|
|
|
14
15
|
description?: string | null;
|
|
15
16
|
};
|
|
16
17
|
promptContext?: unknown;
|
|
18
|
+
/** Populated by implementor stage if Codex creates a worktree */
|
|
19
|
+
worktreePath?: string | null;
|
|
20
|
+
/** Codex branch name, e.g. codex/UAT-123 */
|
|
21
|
+
codexBranch?: string | null;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
function emit(ctx: PipelineContext, content: ActivityContent): Promise<void> {
|
|
@@ -22,6 +27,16 @@ function emit(ctx: PipelineContext, content: ActivityContent): Promise<void> {
|
|
|
22
27
|
});
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
function toolContext(ctx: PipelineContext): string {
|
|
31
|
+
return [
|
|
32
|
+
`\n## Tool Context`,
|
|
33
|
+
`When calling \`code_run\`, you MUST pass these parameters:`,
|
|
34
|
+
`- \`agentSessionId\`: \`"${ctx.agentSessionId}"\``,
|
|
35
|
+
`- \`issueIdentifier\`: \`"${ctx.issue.identifier}"\``,
|
|
36
|
+
`This enables real-time progress streaming to Linear and isolated worktree creation.`,
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
// ── Stage 1: Planner ───────────────────────────────────────────────
|
|
26
41
|
|
|
27
42
|
export async function runPlannerStage(ctx: PipelineContext): Promise<string | null> {
|
|
@@ -54,6 +69,8 @@ ${ctx.promptContext ? `**Additional context:**\n${JSON.stringify(ctx.promptConte
|
|
|
54
69
|
4. Note any risks or dependencies
|
|
55
70
|
5. Output your plan in markdown format
|
|
56
71
|
|
|
72
|
+
IMPORTANT: Do NOT call code_run or any coding tools. Your job is ONLY to analyze and write a plan. The implementor stage will execute the plan using code_run after you're done.
|
|
73
|
+
|
|
57
74
|
Output ONLY the plan, nothing else.`;
|
|
58
75
|
|
|
59
76
|
await emit(ctx, { type: "action", action: "Planning", parameter: ctx.issue.identifier });
|
|
@@ -64,6 +81,7 @@ Output ONLY the plan, nothing else.`;
|
|
|
64
81
|
sessionId: `linear-plan-${ctx.agentSessionId}`,
|
|
65
82
|
message,
|
|
66
83
|
timeoutMs: 5 * 60_000,
|
|
84
|
+
|
|
67
85
|
});
|
|
68
86
|
|
|
69
87
|
if (!result.success) {
|
|
@@ -104,14 +122,16 @@ ${plan}
|
|
|
104
122
|
|
|
105
123
|
## Instructions
|
|
106
124
|
1. Follow the plan step by step
|
|
107
|
-
2.
|
|
108
|
-
3.
|
|
109
|
-
4.
|
|
110
|
-
5.
|
|
125
|
+
2. Use \`code_run\` to write code, create files, run tests, and refactor — it works in an isolated git worktree
|
|
126
|
+
3. Use \`spawn_agent\` / \`ask_agent\` to delegate to other crew agents if needed
|
|
127
|
+
4. Create commits for each logical change
|
|
128
|
+
5. If the plan involves creating a PR, do so
|
|
129
|
+
6. Report what you did, any files changed, and the worktree/branch path
|
|
130
|
+
${toolContext(ctx)}
|
|
111
131
|
|
|
112
132
|
Be thorough but stay within scope of the plan.`;
|
|
113
133
|
|
|
114
|
-
await emit(ctx, { type: "action", action: "
|
|
134
|
+
await emit(ctx, { type: "action", action: "Calling coding provider", parameter: "Codex" });
|
|
115
135
|
|
|
116
136
|
const result = await runAgent({
|
|
117
137
|
api: ctx.api,
|
|
@@ -119,6 +139,7 @@ Be thorough but stay within scope of the plan.`;
|
|
|
119
139
|
sessionId: `linear-impl-${ctx.agentSessionId}`,
|
|
120
140
|
message,
|
|
121
141
|
timeoutMs: 10 * 60_000,
|
|
142
|
+
|
|
122
143
|
});
|
|
123
144
|
|
|
124
145
|
if (!result.success) {
|
|
@@ -126,7 +147,13 @@ Be thorough but stay within scope of the plan.`;
|
|
|
126
147
|
return null;
|
|
127
148
|
}
|
|
128
149
|
|
|
129
|
-
|
|
150
|
+
// Try to extract worktree info from agent output (Codex results include it)
|
|
151
|
+
const worktreeMatch = result.output.match(/worktreePath["\s:]+([/\w.-]+)/);
|
|
152
|
+
const branchMatch = result.output.match(/branch["\s:]+([/\w.-]+)/);
|
|
153
|
+
if (worktreeMatch) ctx.worktreePath = worktreeMatch[1];
|
|
154
|
+
if (branchMatch) ctx.codexBranch = branchMatch[1];
|
|
155
|
+
|
|
156
|
+
await emit(ctx, { type: "action", action: "Implementation complete", parameter: ctx.issue.identifier });
|
|
130
157
|
return result.output;
|
|
131
158
|
}
|
|
132
159
|
|
|
@@ -139,6 +166,10 @@ export async function runAuditorStage(
|
|
|
139
166
|
): Promise<void> {
|
|
140
167
|
await emit(ctx, { type: "thought", body: "Auditing implementation against the plan..." });
|
|
141
168
|
|
|
169
|
+
const worktreeInfo = ctx.worktreePath
|
|
170
|
+
? `\n## Worktree\nCode changes are at: \`${ctx.worktreePath}\` (branch: \`${ctx.codexBranch ?? "unknown"}\`)\n`
|
|
171
|
+
: "";
|
|
172
|
+
|
|
142
173
|
const message = `You are an auditor. Review this implementation against the original plan.
|
|
143
174
|
|
|
144
175
|
## Issue: ${ctx.issue.identifier} — ${ctx.issue.title}
|
|
@@ -148,13 +179,14 @@ ${plan}
|
|
|
148
179
|
|
|
149
180
|
## Implementation Result:
|
|
150
181
|
${implResult}
|
|
151
|
-
|
|
182
|
+
${worktreeInfo}
|
|
152
183
|
## Instructions
|
|
153
184
|
1. Verify each plan step was completed
|
|
154
|
-
2. Check for any missed items
|
|
185
|
+
2. Check for any missed items — use \`ask_agent\` / \`spawn_agent\` for specialized review if needed
|
|
155
186
|
3. Note any concerns or improvements needed
|
|
156
187
|
4. Provide a pass/fail verdict with reasoning
|
|
157
188
|
5. Output a concise audit summary in markdown
|
|
189
|
+
${toolContext(ctx)}
|
|
158
190
|
|
|
159
191
|
Output ONLY the audit summary.`;
|
|
160
192
|
|
|
@@ -166,6 +198,7 @@ Output ONLY the audit summary.`;
|
|
|
166
198
|
sessionId: `linear-audit-${ctx.agentSessionId}`,
|
|
167
199
|
message,
|
|
168
200
|
timeoutMs: 5 * 60_000,
|
|
201
|
+
|
|
169
202
|
});
|
|
170
203
|
|
|
171
204
|
const auditSummary = result.success ? result.output : `Audit failed: ${result.output.slice(0, 500)}`;
|
|
@@ -184,20 +217,43 @@ Output ONLY the audit summary.`;
|
|
|
184
217
|
// ── Full Pipeline ─────────────────────────────────────────────────
|
|
185
218
|
|
|
186
219
|
export async function runFullPipeline(ctx: PipelineContext): Promise<void> {
|
|
220
|
+
// Register active session so tools (code_run) can resolve it
|
|
221
|
+
setActiveSession({
|
|
222
|
+
agentSessionId: ctx.agentSessionId,
|
|
223
|
+
issueIdentifier: ctx.issue.identifier,
|
|
224
|
+
issueId: ctx.issue.id,
|
|
225
|
+
agentId: ctx.agentId,
|
|
226
|
+
startedAt: Date.now(),
|
|
227
|
+
});
|
|
228
|
+
|
|
187
229
|
try {
|
|
188
230
|
// Stage 1: Plan
|
|
189
231
|
const plan = await runPlannerStage(ctx);
|
|
190
|
-
if (!plan)
|
|
232
|
+
if (!plan) {
|
|
233
|
+
clearActiveSession(ctx.issue.id);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
191
236
|
|
|
192
237
|
// Pipeline pauses here — user must reply to approve.
|
|
193
|
-
// The "prompted" webhook will call resumePipeline().
|
|
238
|
+
// The "prompted" / "created" webhook will call resumePipeline().
|
|
239
|
+
// Active session stays registered until resume completes.
|
|
194
240
|
} catch (err) {
|
|
241
|
+
clearActiveSession(ctx.issue.id);
|
|
195
242
|
ctx.api.logger.error(`Pipeline error: ${err}`);
|
|
196
243
|
await emit(ctx, { type: "error", body: `Pipeline failed: ${String(err).slice(0, 500)}` });
|
|
197
244
|
}
|
|
198
245
|
}
|
|
199
246
|
|
|
200
247
|
export async function resumePipeline(ctx: PipelineContext, plan: string): Promise<void> {
|
|
248
|
+
// Register (or update) active session for tool resolution
|
|
249
|
+
setActiveSession({
|
|
250
|
+
agentSessionId: ctx.agentSessionId,
|
|
251
|
+
issueIdentifier: ctx.issue.identifier,
|
|
252
|
+
issueId: ctx.issue.id,
|
|
253
|
+
agentId: ctx.agentId,
|
|
254
|
+
startedAt: Date.now(),
|
|
255
|
+
});
|
|
256
|
+
|
|
201
257
|
try {
|
|
202
258
|
// Stage 2: Implement
|
|
203
259
|
const implResult = await runImplementorStage(ctx, plan);
|
|
@@ -208,5 +264,7 @@ export async function resumePipeline(ctx: PipelineContext, plan: string): Promis
|
|
|
208
264
|
} catch (err) {
|
|
209
265
|
ctx.api.logger.error(`Pipeline error: ${err}`);
|
|
210
266
|
await emit(ctx, { type: "error", body: `Pipeline failed: ${String(err).slice(0, 500)}` });
|
|
267
|
+
} finally {
|
|
268
|
+
clearActiveSession(ctx.issue.id);
|
|
211
269
|
}
|
|
212
270
|
}
|
package/src/tools.ts
CHANGED
|
@@ -1,84 +1,34 @@
|
|
|
1
|
-
import type { AnyAgentTool, OpenClawPluginApi
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { createCodeTool } from "./code-tool.js";
|
|
3
|
+
import { createOrchestrationTools } from "./orchestration-tools.js";
|
|
4
4
|
|
|
5
|
-
export function createLinearTools(api: OpenClawPluginApi, ctx:
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
export function createLinearTools(api: OpenClawPluginApi, ctx: Record<string, unknown>): any[] {
|
|
6
|
+
const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
|
|
7
|
+
|
|
8
|
+
// Unified code_run tool — dispatches to configured backend (claude/codex/gemini)
|
|
9
|
+
const codeTools: AnyAgentTool[] = [];
|
|
10
|
+
try {
|
|
11
|
+
codeTools.push(createCodeTool(api, ctx));
|
|
12
|
+
} catch (err) {
|
|
13
|
+
api.logger.warn(`code_run tool not available: ${err}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Orchestration tools (conditional on config — defaults to enabled)
|
|
17
|
+
const orchestrationTools: AnyAgentTool[] = [];
|
|
18
|
+
const enableOrchestration = pluginConfig?.enableOrchestration !== false;
|
|
19
|
+
if (enableOrchestration) {
|
|
20
|
+
try {
|
|
21
|
+
orchestrationTools.push(...createOrchestrationTools(api, ctx));
|
|
22
|
+
} catch (err) {
|
|
23
|
+
api.logger.warn(`Orchestration tools not available: ${err}`);
|
|
12
24
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Linear issue management (list, create, update, close, comment, etc.)
|
|
28
|
+
// is handled by the `linearis` skill — no custom tools needed here.
|
|
29
|
+
|
|
16
30
|
return [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
description: "List issues from a Linear workspace",
|
|
20
|
-
parameters: {
|
|
21
|
-
type: "object",
|
|
22
|
-
properties: {
|
|
23
|
-
limit: { type: "number", description: "Max issues to return", default: 10 },
|
|
24
|
-
teamId: { type: "string", description: "Filter by team ID" }
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
execute: async ({ limit, teamId }) => {
|
|
28
|
-
const client = getClient();
|
|
29
|
-
const data = await client.listIssues({ limit, teamId });
|
|
30
|
-
return jsonResult({
|
|
31
|
-
message: `Found ${data.issues.nodes.length} issues`,
|
|
32
|
-
issues: data.issues.nodes
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
name: "linear_create_issue",
|
|
38
|
-
description: "Create a new issue in Linear",
|
|
39
|
-
parameters: {
|
|
40
|
-
type: "object",
|
|
41
|
-
properties: {
|
|
42
|
-
title: { type: "string", description: "Issue title" },
|
|
43
|
-
description: { type: "string", description: "Issue description" },
|
|
44
|
-
teamId: { type: "string", description: "Team ID" }
|
|
45
|
-
},
|
|
46
|
-
required: ["title", "teamId"]
|
|
47
|
-
},
|
|
48
|
-
execute: async ({ title, description, teamId }) => {
|
|
49
|
-
const client = getClient();
|
|
50
|
-
const data = await client.createIssue({ title, description, teamId });
|
|
51
|
-
if (data.issueCreate.success) {
|
|
52
|
-
return jsonResult({
|
|
53
|
-
message: "Created issue successfully",
|
|
54
|
-
issue: data.issueCreate.issue
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return jsonResult({ message: "Failed to create issue" });
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: "linear_add_comment",
|
|
62
|
-
description: "Add a comment to a Linear issue",
|
|
63
|
-
parameters: {
|
|
64
|
-
type: "object",
|
|
65
|
-
properties: {
|
|
66
|
-
issueId: { type: "string", description: "Issue ID" },
|
|
67
|
-
body: { type: "string", description: "Comment body" }
|
|
68
|
-
},
|
|
69
|
-
required: ["issueId", "body"]
|
|
70
|
-
},
|
|
71
|
-
execute: async ({ issueId, body }) => {
|
|
72
|
-
const client = getClient();
|
|
73
|
-
const data = await client.addComment({ issueId, body });
|
|
74
|
-
if (data.commentCreate.success) {
|
|
75
|
-
return jsonResult({
|
|
76
|
-
message: "Added comment successfully",
|
|
77
|
-
commentId: data.commentCreate.comment.id
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
return jsonResult({ message: "Failed to add comment" });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
31
|
+
...codeTools,
|
|
32
|
+
...orchestrationTools,
|
|
83
33
|
];
|
|
84
34
|
}
|