@calltelemetry/openclaw-linear 0.5.0 → 0.5.1
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/index.ts +11 -5
- package/package.json +1 -1
- package/prompts.yaml +27 -9
- package/src/agent.ts +41 -7
- package/src/claude-tool.ts +1 -1
- package/src/webhook.ts +51 -15
package/index.ts
CHANGED
|
@@ -196,15 +196,21 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
196
196
|
try {
|
|
197
197
|
const raw = execFileSync(bin, ["--version"], {
|
|
198
198
|
encoding: "utf8",
|
|
199
|
-
timeout:
|
|
199
|
+
timeout: 15_000,
|
|
200
200
|
env: { ...process.env, CLAUDECODE: undefined } as any,
|
|
201
201
|
}).trim();
|
|
202
202
|
cliChecks[name] = raw || "unknown";
|
|
203
203
|
} catch {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
// Fallback: check if the file exists (execFileSync can fail in worker contexts)
|
|
205
|
+
try {
|
|
206
|
+
require("node:fs").accessSync(bin, require("node:fs").constants.X_OK);
|
|
207
|
+
cliChecks[name] = "installed (version check skipped)";
|
|
208
|
+
} catch {
|
|
209
|
+
cliChecks[name] = "not found";
|
|
210
|
+
api.logger.warn(
|
|
211
|
+
`${name} CLI not found at ${bin}. The ${name}_run tool will fail. Install with: ${installCmd}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
208
214
|
}
|
|
209
215
|
}
|
|
210
216
|
|
package/package.json
CHANGED
package/prompts.yaml
CHANGED
|
@@ -5,13 +5,19 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Edit these to customize worker/audit behavior without rebuilding the plugin.
|
|
7
7
|
# Override path via `promptsPath` in plugin config.
|
|
8
|
+
#
|
|
9
|
+
# Access model:
|
|
10
|
+
# Zoe (orchestrator) — linearis READ ONLY (issues read/list/search)
|
|
11
|
+
# Worker — NO linearis access. Return text output only.
|
|
12
|
+
# Auditor — linearis READ + WRITE (can update status, close, comment)
|
|
8
13
|
|
|
9
14
|
worker:
|
|
10
15
|
system: |
|
|
11
|
-
You are implementing a Linear issue. Your job is to
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
You are a coding worker implementing a Linear issue. Your ONLY job is to write code and return a text summary.
|
|
17
|
+
You do NOT have access to linearis or any Linear issue management tools.
|
|
18
|
+
Do NOT attempt to update, close, comment on, or modify the Linear issue in any way.
|
|
19
|
+
Do NOT mark the issue as Done — the audit system handles all issue lifecycle.
|
|
20
|
+
Just write code and return your implementation summary as text.
|
|
15
21
|
task: |
|
|
16
22
|
Implement issue {{identifier}}: {{title}}
|
|
17
23
|
|
|
@@ -25,15 +31,22 @@ worker:
|
|
|
25
31
|
2. Plan your approach
|
|
26
32
|
3. Implement the solution in the worktree
|
|
27
33
|
4. Run tests to verify your changes
|
|
28
|
-
5.
|
|
29
|
-
|
|
34
|
+
5. Return a text summary of what you changed, what tests you ran, and any notes
|
|
35
|
+
|
|
36
|
+
Your text output will be captured automatically. Do NOT use linearis or attempt to post comments.
|
|
30
37
|
|
|
31
38
|
audit:
|
|
32
39
|
system: |
|
|
33
|
-
You are an independent auditor. Your job is to verify that work was completed correctly
|
|
40
|
+
You are an independent auditor. Your job is to verify that work was completed correctly
|
|
41
|
+
and then update the Linear issue accordingly.
|
|
34
42
|
The Linear issue body is the SOURCE OF TRUTH for what "done" means.
|
|
35
|
-
Worker
|
|
43
|
+
Worker output is secondary evidence of what was attempted.
|
|
36
44
|
You must be thorough and objective. Do not rubber-stamp.
|
|
45
|
+
|
|
46
|
+
You have WRITE access to linearis. After auditing, you are responsible for:
|
|
47
|
+
- Posting an audit summary comment on the issue
|
|
48
|
+
- Updating the issue status if the audit passes
|
|
49
|
+
Use `linearis` CLI via exec for these operations.
|
|
37
50
|
task: |
|
|
38
51
|
Audit issue {{identifier}}: {{title}}
|
|
39
52
|
|
|
@@ -44,12 +57,16 @@ audit:
|
|
|
44
57
|
|
|
45
58
|
Checklist:
|
|
46
59
|
1. Identify ALL acceptance criteria from the issue body
|
|
47
|
-
2. Read worker comments
|
|
60
|
+
2. Read worker comments: `linearis issues read {{identifier}}`
|
|
48
61
|
3. Verify each acceptance criterion is addressed in the code
|
|
49
62
|
4. Run tests in the worktree — verify they pass
|
|
50
63
|
5. Check test coverage if expectations are stated in the issue
|
|
51
64
|
6. Review the code diff for quality and correctness
|
|
52
65
|
|
|
66
|
+
After auditing:
|
|
67
|
+
- Post your audit findings as a comment: `linearis comments create {{identifier}} --body "..."`
|
|
68
|
+
- If PASS: update status: `linearis issues update {{identifier}} --status "Done"`
|
|
69
|
+
|
|
53
70
|
You MUST return a JSON verdict as the last line of your response:
|
|
54
71
|
{"pass": true/false, "criteria": ["list of criteria found"], "gaps": ["list of unmet criteria"], "testResults": "summary of test output"}
|
|
55
72
|
|
|
@@ -59,3 +76,4 @@ rework:
|
|
|
59
76
|
{{gaps}}
|
|
60
77
|
|
|
61
78
|
Address these specific issues in your rework. Focus on the gaps listed above.
|
|
79
|
+
Remember: you do NOT have linearis access. Just fix the code and return a text summary.
|
package/src/agent.ts
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
2
4
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
5
|
import type { LinearAgentApi, ActivityContent } from "./linear-api.js";
|
|
4
6
|
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Agent directory resolution (config-based, not ext API which ignores agentId)
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
interface AgentDirs {
|
|
12
|
+
workspaceDir: string;
|
|
13
|
+
agentDir: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveAgentDirs(agentId: string, config: Record<string, any>): AgentDirs {
|
|
17
|
+
const home = process.env.HOME ?? "/home/claw";
|
|
18
|
+
const agentList = config?.agents?.list as Array<Record<string, any>> | undefined;
|
|
19
|
+
const agentEntry = agentList?.find((a) => a.id === agentId);
|
|
20
|
+
|
|
21
|
+
// Workspace: agent-specific override → agents.defaults.workspace → fallback
|
|
22
|
+
const workspaceDir = agentEntry?.workspace
|
|
23
|
+
?? config?.agents?.defaults?.workspace
|
|
24
|
+
?? join(home, ".openclaw", "workspace");
|
|
25
|
+
|
|
26
|
+
// Agent runtime dir: always ~/.openclaw/agents/{agentId}/agent
|
|
27
|
+
// (matches OpenClaw's internal structure)
|
|
28
|
+
const agentDir = join(home, ".openclaw", "agents", agentId, "agent");
|
|
29
|
+
mkdirSync(agentDir, { recursive: true });
|
|
30
|
+
|
|
31
|
+
return { workspaceDir, agentDir };
|
|
32
|
+
}
|
|
33
|
+
|
|
5
34
|
// Import extensionAPI for embedded agent runner (internal, not in public SDK)
|
|
6
35
|
let _extensionAPI: typeof import("/home/claw/.npm-global/lib/node_modules/openclaw/dist/extensionAPI.js") | null = null;
|
|
7
36
|
async function getExtensionAPI() {
|
|
@@ -66,17 +95,22 @@ async function runEmbedded(
|
|
|
66
95
|
): Promise<AgentRunResult> {
|
|
67
96
|
const ext = await getExtensionAPI();
|
|
68
97
|
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const
|
|
98
|
+
// Load config so we can resolve agent dirs and providers correctly.
|
|
99
|
+
const config = await api.runtime.config.loadConfig();
|
|
100
|
+
const configAny = config as Record<string, any>;
|
|
101
|
+
|
|
102
|
+
// Resolve workspace and agent dirs from config (ext API ignores agentId).
|
|
103
|
+
const dirs = resolveAgentDirs(agentId, configAny);
|
|
104
|
+
const { workspaceDir, agentDir } = dirs;
|
|
72
105
|
const runId = randomUUID();
|
|
73
106
|
|
|
74
|
-
//
|
|
75
|
-
const
|
|
107
|
+
// Build session file path under the correct agent's sessions directory.
|
|
108
|
+
const sessionsDir = join(agentDir, "sessions");
|
|
109
|
+
try { mkdirSync(sessionsDir, { recursive: true }); } catch {}
|
|
110
|
+
const sessionFile = join(sessionsDir, `${sessionId}.jsonl`);
|
|
76
111
|
|
|
77
112
|
// Resolve model/provider from config — default is anthropic which requires
|
|
78
113
|
// a separate API key. Our agents use openrouter.
|
|
79
|
-
const configAny = config as Record<string, any>;
|
|
80
114
|
const agentList = configAny?.agents?.list as Array<Record<string, any>> | undefined;
|
|
81
115
|
const agentEntry = agentList?.find((a) => a.id === agentId);
|
|
82
116
|
const modelRef: string =
|
|
@@ -89,7 +123,7 @@ async function runEmbedded(
|
|
|
89
123
|
const provider = slashIdx > 0 ? modelRef.slice(0, slashIdx) : ext.DEFAULT_PROVIDER;
|
|
90
124
|
const model = slashIdx > 0 ? modelRef.slice(slashIdx + 1) : modelRef;
|
|
91
125
|
|
|
92
|
-
api.logger.info(`Embedded agent run: agent=${agentId} session=${sessionId} runId=${runId} provider=${provider} model=${model}`);
|
|
126
|
+
api.logger.info(`Embedded agent run: agent=${agentId} session=${sessionId} runId=${runId} provider=${provider} model=${model} workspaceDir=${workspaceDir} agentDir=${agentDir}`);
|
|
93
127
|
|
|
94
128
|
const emit = (content: ActivityContent) => {
|
|
95
129
|
streaming.linearApi.emitActivity(streaming.agentSessionId, content).catch((err) => {
|
package/src/claude-tool.ts
CHANGED
|
@@ -138,7 +138,6 @@ export async function runClaude(
|
|
|
138
138
|
if (model ?? pluginConfig?.claudeModel) {
|
|
139
139
|
args.push("--model", (model ?? pluginConfig?.claudeModel) as string);
|
|
140
140
|
}
|
|
141
|
-
args.push("-C", workingDir);
|
|
142
141
|
args.push("-p", prompt);
|
|
143
142
|
|
|
144
143
|
api.logger.info(`Claude exec: ${CLAUDE_BIN} ${args.join(" ").slice(0, 200)}...`);
|
|
@@ -150,6 +149,7 @@ export async function runClaude(
|
|
|
150
149
|
|
|
151
150
|
const child = spawn(CLAUDE_BIN, args, {
|
|
152
151
|
stdio: ["ignore", "pipe", "pipe"],
|
|
152
|
+
cwd: workingDir,
|
|
153
153
|
env,
|
|
154
154
|
timeout: 0,
|
|
155
155
|
});
|
package/src/webhook.ts
CHANGED
|
@@ -187,12 +187,18 @@ export async function handleLinearWebhook(
|
|
|
187
187
|
.map((c: any) => ` - **${c.user?.name ?? "Unknown"}**: ${c.body?.slice(0, 200)}`)
|
|
188
188
|
.join("\n");
|
|
189
189
|
|
|
190
|
+
const notifIssueRef = enrichedIssue?.identifier ?? issue.id;
|
|
190
191
|
const message = [
|
|
191
|
-
`
|
|
192
|
+
`You are an orchestrator responding to a Linear issue notification. Your text output will be automatically posted as a comment on the issue (do NOT post a comment yourself — the handler does it).`,
|
|
192
193
|
``,
|
|
193
|
-
|
|
194
|
+
`**Tool access:**`,
|
|
195
|
+
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${notifIssueRef}\`), list, and search. Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
196
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
197
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
194
198
|
``,
|
|
195
|
-
|
|
199
|
+
`**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`,
|
|
200
|
+
``,
|
|
201
|
+
`## Issue: ${notifIssueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
196
202
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
197
203
|
``,
|
|
198
204
|
`**Description:**`,
|
|
@@ -200,7 +206,7 @@ export async function handleLinearWebhook(
|
|
|
200
206
|
commentSummary ? `\n**Recent comments:**\n${commentSummary}` : "",
|
|
201
207
|
comment?.body ? `\n**Triggering comment:**\n> ${comment.body}` : "",
|
|
202
208
|
``,
|
|
203
|
-
`Respond concisely.
|
|
209
|
+
`Respond concisely. For work requests, dispatch via \`code_run\` and summarize the result.`,
|
|
204
210
|
].filter(Boolean).join("\n");
|
|
205
211
|
|
|
206
212
|
// Dispatch agent with session lifecycle (non-blocking)
|
|
@@ -363,11 +369,19 @@ export async function handleLinearWebhook(
|
|
|
363
369
|
.map((c: any) => `**${c.user?.name ?? c.actorName ?? "User"}**: ${(c.body ?? "").slice(0, 300)}`)
|
|
364
370
|
.join("\n\n");
|
|
365
371
|
|
|
372
|
+
const issueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
|
|
366
373
|
const message = [
|
|
367
|
-
`You are an
|
|
368
|
-
|
|
374
|
+
`You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
|
|
375
|
+
``,
|
|
376
|
+
`**Tool access:**`,
|
|
377
|
+
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${issueRef}\`), list issues (\`linearis issues list\`), and search (\`linearis issues search "..."\`). Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
378
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
379
|
+
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
380
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
369
381
|
``,
|
|
370
|
-
|
|
382
|
+
`**Your role:** You are the dispatcher. For any coding or implementation work, use \`code_run\` to dispatch it. Workers return text output. You summarize results. You do NOT update issue status or post linearis comments — the audit system handles lifecycle transitions.`,
|
|
383
|
+
``,
|
|
384
|
+
`## Issue: ${issueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
371
385
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
372
386
|
``,
|
|
373
387
|
`**Description:**`,
|
|
@@ -375,7 +389,7 @@ export async function handleLinearWebhook(
|
|
|
375
389
|
commentContext ? `\n**Conversation:**\n${commentContext}` : "",
|
|
376
390
|
userMessage ? `\n**Latest message:**\n> ${userMessage}` : "",
|
|
377
391
|
``,
|
|
378
|
-
`Respond to the user's request.
|
|
392
|
+
`Respond to the user's request. For work requests, dispatch via \`code_run\` and summarize the result. Be concise and action-oriented.`,
|
|
379
393
|
].filter(Boolean).join("\n");
|
|
380
394
|
|
|
381
395
|
// Run agent directly (non-blocking)
|
|
@@ -543,11 +557,19 @@ export async function handleLinearWebhook(
|
|
|
543
557
|
.map((c: any) => `**${c.user?.name ?? "User"}**: ${(c.body ?? "").slice(0, 300)}`)
|
|
544
558
|
.join("\n\n");
|
|
545
559
|
|
|
560
|
+
const followUpIssueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
|
|
546
561
|
const message = [
|
|
547
|
-
`You are an
|
|
548
|
-
|
|
562
|
+
`You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
|
|
563
|
+
``,
|
|
564
|
+
`**Tool access:**`,
|
|
565
|
+
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${followUpIssueRef}\`), list, and search. Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
566
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
567
|
+
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
568
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
549
569
|
``,
|
|
550
|
-
|
|
570
|
+
`**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`,
|
|
571
|
+
``,
|
|
572
|
+
`## Issue: ${followUpIssueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
551
573
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
552
574
|
``,
|
|
553
575
|
`**Description:**`,
|
|
@@ -555,7 +577,7 @@ export async function handleLinearWebhook(
|
|
|
555
577
|
commentContext ? `\n**Recent conversation:**\n${commentContext}` : "",
|
|
556
578
|
`\n**User's follow-up message:**\n> ${userMessage}`,
|
|
557
579
|
``,
|
|
558
|
-
`Respond to the user's follow-up. Be concise and action-oriented.`,
|
|
580
|
+
`Respond to the user's follow-up. For work requests, dispatch via \`code_run\`. Be concise and action-oriented.`,
|
|
559
581
|
].filter(Boolean).join("\n");
|
|
560
582
|
|
|
561
583
|
setActiveSession({
|
|
@@ -683,6 +705,14 @@ export async function handleLinearWebhook(
|
|
|
683
705
|
|
|
684
706
|
api.logger.info(`Comment mention: @${mentionedAgent} on ${issue.identifier ?? issue.id} by ${commentor}`);
|
|
685
707
|
|
|
708
|
+
// Guard: skip if an agent run is already active for this issue
|
|
709
|
+
// (prevents dual-dispatch when both Comment.create and AgentSessionEvent fire)
|
|
710
|
+
if (activeRuns.has(issue.id)) {
|
|
711
|
+
api.logger.info(`Comment mention: agent already running for ${issue.identifier ?? issue.id} — skipping`);
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
activeRuns.add(issue.id);
|
|
715
|
+
|
|
686
716
|
// React with eyes to acknowledge the comment
|
|
687
717
|
if (comment?.id) {
|
|
688
718
|
linearApi.createReaction(comment.id, "eyes").catch(() => {});
|
|
@@ -712,9 +742,14 @@ export async function handleLinearWebhook(
|
|
|
712
742
|
const assignee = enrichedIssue.assignee?.name ?? "Unassigned";
|
|
713
743
|
|
|
714
744
|
const taskMessage = [
|
|
715
|
-
`
|
|
745
|
+
`You are an orchestrator responding to a Linear issue comment. Your text output will be automatically posted as a comment on the issue (do NOT post a comment yourself — the handler does it).`,
|
|
746
|
+
``,
|
|
747
|
+
`**Tool access:**`,
|
|
748
|
+
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${enrichedIssue.identifier ?? "API-XXX"}\`), list issues (\`linearis issues list\`), and search (\`linearis issues search "..."\`). Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
749
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
750
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
716
751
|
``,
|
|
717
|
-
|
|
752
|
+
`**Your role:** You are the dispatcher. For any coding or implementation work, use \`code_run\` to dispatch it. Workers return text output. You summarize results. You do NOT update issue status or post linearis comments — the audit system handles lifecycle transitions.`,
|
|
718
753
|
``,
|
|
719
754
|
`**Issue:** ${enrichedIssue.identifier ?? enrichedIssue.id} — ${enrichedIssue.title ?? "(untitled)"}`,
|
|
720
755
|
`**Status:** ${state} | **Priority:** ${priority} | **Assignee:** ${assignee} | **Labels:** ${labels}`,
|
|
@@ -725,7 +760,7 @@ export async function handleLinearWebhook(
|
|
|
725
760
|
`**${commentor} wrote:**`,
|
|
726
761
|
`> ${commentBody}`,
|
|
727
762
|
``,
|
|
728
|
-
`Respond to their message. Be concise and direct.
|
|
763
|
+
`Respond to their message. Be concise and direct. For work requests, dispatch via \`code_run\` and summarize the result.`,
|
|
729
764
|
].filter(Boolean).join("\n");
|
|
730
765
|
|
|
731
766
|
// Dispatch to agent with full session lifecycle (non-blocking)
|
|
@@ -814,6 +849,7 @@ export async function handleLinearWebhook(
|
|
|
814
849
|
}
|
|
815
850
|
} finally {
|
|
816
851
|
clearActiveSession(issue.id);
|
|
852
|
+
activeRuns.delete(issue.id);
|
|
817
853
|
}
|
|
818
854
|
})();
|
|
819
855
|
|