@calltelemetry/openclaw-linear 0.9.11 → 0.9.14

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.
@@ -484,6 +484,112 @@ export async function checkFilesAndDirs(pluginConfig?: Record<string, unknown>,
484
484
  checks.push(fail(`Base repo does not exist: ${baseRepo}`, undefined, "Set codexBaseRepo in plugin config to your git repository path"));
485
485
  }
486
486
 
487
+ // CLAUDE.md in base repo
488
+ if (existsSync(baseRepo)) {
489
+ const claudeMdPath = join(baseRepo, "CLAUDE.md");
490
+ if (existsSync(claudeMdPath)) {
491
+ try {
492
+ const stat = statSync(claudeMdPath);
493
+ const sizeKb = Math.round(stat.size / 1024);
494
+ checks.push(pass(`CLAUDE.md found in base repo (${sizeKb}KB)`));
495
+ } catch {
496
+ checks.push(pass("CLAUDE.md found in base repo"));
497
+ }
498
+ } else {
499
+ checks.push(warn(
500
+ "No CLAUDE.md in base repo",
501
+ `Expected at: ${claudeMdPath}`,
502
+ {
503
+ fix: [
504
+ `Create ${claudeMdPath} — this is how agents learn your project.`,
505
+ "",
506
+ "Template:",
507
+ " # Project Name",
508
+ "",
509
+ " ## Tech Stack",
510
+ " - Language/framework here",
511
+ "",
512
+ " ## Build",
513
+ " ```bash",
514
+ " your build command here",
515
+ " ```",
516
+ "",
517
+ " ## Test",
518
+ " ```bash",
519
+ " your test command here",
520
+ " ```",
521
+ "",
522
+ " ## Architecture",
523
+ " Brief description of directory structure and key patterns.",
524
+ ].join("\n"),
525
+ },
526
+ ));
527
+ }
528
+ }
529
+
530
+ // AGENTS.md in base repo
531
+ if (existsSync(baseRepo)) {
532
+ const agentsMdPath = join(baseRepo, "AGENTS.md");
533
+ if (existsSync(agentsMdPath)) {
534
+ try {
535
+ const stat = statSync(agentsMdPath);
536
+ const sizeKb = Math.round(stat.size / 1024);
537
+ checks.push(pass(`AGENTS.md found in base repo (${sizeKb}KB)`));
538
+ } catch {
539
+ checks.push(pass("AGENTS.md found in base repo"));
540
+ }
541
+ } else {
542
+ checks.push(warn(
543
+ "No AGENTS.md in base repo",
544
+ `Expected at: ${agentsMdPath}`,
545
+ {
546
+ fix: [
547
+ `Create ${agentsMdPath} — this tells agents how to work on your project.`,
548
+ "",
549
+ "Template:",
550
+ " # Agent Guidelines",
551
+ "",
552
+ " ## Code Style",
553
+ " - Patterns and conventions to follow",
554
+ "",
555
+ " ## Workflow",
556
+ " - Branch naming, commit messages, PR process",
557
+ "",
558
+ " ## Do / Don't",
559
+ " - Rules agents must follow in this codebase",
560
+ ].join("\n"),
561
+ },
562
+ ));
563
+ }
564
+ }
565
+
566
+ // Multi-repo path validation
567
+ const repos = pluginConfig?.repos as Record<string, string> | undefined;
568
+ if (repos && typeof repos === "object") {
569
+ for (const [name, repoPath] of Object.entries(repos)) {
570
+ if (typeof repoPath !== "string") continue;
571
+ const resolved = repoPath.startsWith("~/") ? repoPath.replace("~", homedir()) : repoPath;
572
+ if (!existsSync(resolved)) {
573
+ checks.push(fail(
574
+ `Repo "${name}": path does not exist: ${resolved}`,
575
+ undefined,
576
+ `Verify the path in plugin config repos.${name}, or create the directory and run: git init ${resolved}`,
577
+ ));
578
+ } else {
579
+ try {
580
+ execFileSync("git", ["rev-parse", "--git-dir"], { cwd: resolved, encoding: "utf8", timeout: 5_000 });
581
+ checks.push(pass(`Repo "${name}": valid git repo`));
582
+ } catch {
583
+ checks.push(fail(
584
+ `Repo "${name}": not a git repo at ${resolved}`,
585
+ undefined,
586
+ `Run: git init ${resolved}`,
587
+ ));
588
+ }
589
+ }
590
+ }
591
+ }
592
+
487
593
  // Prompts
488
594
  try {
489
595
  clearPromptCache();
@@ -52,6 +52,7 @@ import {
52
52
  parseVerdict,
53
53
  buildWorkerTask,
54
54
  buildAuditTask,
55
+ buildProjectContext,
55
56
  loadPrompts,
56
57
  loadRawPromptYaml,
57
58
  clearPromptCache,
@@ -238,6 +239,100 @@ describe("buildAuditTask", () => {
238
239
  });
239
240
  });
240
241
 
242
+ // ---------------------------------------------------------------------------
243
+ // buildProjectContext
244
+ // ---------------------------------------------------------------------------
245
+
246
+ describe("buildProjectContext", () => {
247
+ it("returns empty string when no config", () => {
248
+ expect(buildProjectContext()).toBe("");
249
+ expect(buildProjectContext(undefined)).toBe("");
250
+ });
251
+
252
+ it("returns empty string when config has no relevant keys", () => {
253
+ expect(buildProjectContext({ someOtherKey: "value" })).toBe("");
254
+ });
255
+
256
+ it("includes project name", () => {
257
+ const ctx = buildProjectContext({ projectName: "CallTelemetry" });
258
+ expect(ctx).toContain("Project: CallTelemetry");
259
+ });
260
+
261
+ it("includes single repo from codexBaseRepo", () => {
262
+ const ctx = buildProjectContext({ codexBaseRepo: "/home/claw/ai-workspace" });
263
+ expect(ctx).toContain("Repo: /home/claw/ai-workspace");
264
+ });
265
+
266
+ it("includes multi-repo map", () => {
267
+ const ctx = buildProjectContext({
268
+ repos: { api: "/home/repos/api", frontend: "/home/repos/frontend" },
269
+ });
270
+ expect(ctx).toContain("Repos:");
271
+ expect(ctx).toContain("api (/home/repos/api)");
272
+ expect(ctx).toContain("frontend (/home/repos/frontend)");
273
+ });
274
+
275
+ it("prefers repos over codexBaseRepo when both present", () => {
276
+ const ctx = buildProjectContext({
277
+ codexBaseRepo: "/home/claw/fallback",
278
+ repos: { main: "/home/claw/main" },
279
+ });
280
+ expect(ctx).toContain("Repos:");
281
+ expect(ctx).not.toContain("Repo: /home/claw/fallback");
282
+ });
283
+
284
+ it("ignores framework, buildCommand, testCommand (belong in CLAUDE.md)", () => {
285
+ const ctx = buildProjectContext({
286
+ framework: "Phoenix/Elixir",
287
+ buildCommand: "mix compile",
288
+ testCommand: "mix test",
289
+ });
290
+ expect(ctx).toBe("");
291
+ });
292
+
293
+ it("includes projectName + repo together", () => {
294
+ const ctx = buildProjectContext({
295
+ projectName: "MyApp",
296
+ codexBaseRepo: "/repo",
297
+ });
298
+ expect(ctx).toContain("## Project Context");
299
+ expect(ctx).toContain("Project: MyApp");
300
+ expect(ctx).toContain("Repo: /repo");
301
+ });
302
+
303
+ it("injects projectContext into worker prompt", () => {
304
+ clearPromptCache();
305
+ const issue: IssueContext = { id: "1", identifier: "X-1", title: "test", description: "desc" };
306
+ const { task } = buildWorkerTask(issue, "/wt", {
307
+ pluginConfig: { projectName: "TestProject" },
308
+ });
309
+ expect(task).toContain("Project: TestProject");
310
+ });
311
+
312
+ it("injects projectContext into audit prompt", () => {
313
+ clearPromptCache();
314
+ const issue: IssueContext = { id: "1", identifier: "X-1", title: "test", description: "desc" };
315
+ const { task } = buildAuditTask(issue, "/wt", { projectName: "TestProject" });
316
+ expect(task).toContain("Project: TestProject");
317
+ });
318
+
319
+ it("worker prompt instructs reading CLAUDE.md and AGENTS.md", () => {
320
+ clearPromptCache();
321
+ const issue: IssueContext = { id: "1", identifier: "X-1", title: "test", description: "desc" };
322
+ const { task } = buildWorkerTask(issue, "/wt");
323
+ expect(task).toContain("CLAUDE.md");
324
+ expect(task).toContain("AGENTS.md");
325
+ });
326
+
327
+ it("audit prompt instructs reading CLAUDE.md and AGENTS.md", () => {
328
+ clearPromptCache();
329
+ const issue: IssueContext = { id: "1", identifier: "X-1", title: "test", description: "desc" };
330
+ const { task } = buildAuditTask(issue, "/wt");
331
+ expect(task).toContain("CLAUDE.md");
332
+ expect(task).toContain("AGENTS.md");
333
+ });
334
+ });
335
+
241
336
  // ---------------------------------------------------------------------------
242
337
  // loadPrompts / clearPromptCache
243
338
  // ---------------------------------------------------------------------------
@@ -63,11 +63,11 @@ interface PromptTemplates {
63
63
  const DEFAULT_PROMPTS: PromptTemplates = {
64
64
  worker: {
65
65
  system: "You are a coding worker implementing a Linear issue. Your ONLY job is to write code and return a text summary. Do NOT attempt to update, close, comment on, or modify the Linear issue. Do NOT mark the issue as Done.",
66
- task: "Implement issue {{identifier}}: {{title}}\n\nIssue body:\n{{description}}\n\nWorktree: {{worktreePath}}\n\nImplement the solution, run tests, commit your work, and return a text summary.",
66
+ task: "Implement issue {{identifier}}: {{title}}\n\nIssue body:\n{{description}}\n\nWorktree: {{worktreePath}}\n{{projectContext}}\n\nBefore coding, read CLAUDE.md and AGENTS.md in the worktree root for project conventions and guidelines. If they don't exist, explore the codebase first.\n\nImplement the solution, run tests, commit your work, and return a text summary.",
67
67
  },
68
68
  audit: {
69
69
  system: "You are an independent auditor. The Linear issue body is the SOURCE OF TRUTH. Worker comments are secondary evidence.",
70
- task: 'Audit issue {{identifier}}: {{title}}\n\nIssue body:\n{{description}}\n\nWorktree: {{worktreePath}}\n\nReturn JSON verdict: {"pass": true/false, "criteria": [...], "gaps": [...], "testResults": "..."}',
70
+ task: 'Audit issue {{identifier}}: {{title}}\n\nIssue body:\n{{description}}\n\nWorktree: {{worktreePath}}\n{{projectContext}}\n\nRead CLAUDE.md and AGENTS.md in the worktree root for project standards.\n\nReturn JSON verdict: {"pass": true/false, "criteria": [...], "gaps": [...], "testResults": "..."}',
71
71
  },
72
72
  rework: {
73
73
  addendum: "PREVIOUS AUDIT FAILED (attempt {{attempt}}). Gaps:\n{{gaps}}\n\nAddress these specific issues. Preserve correct code from prior attempts.",
@@ -167,6 +167,39 @@ export function clearPromptCache(): void {
167
167
  _projectPromptCache.clear();
168
168
  }
169
169
 
170
+ // ---------------------------------------------------------------------------
171
+ // Project context builder
172
+ // ---------------------------------------------------------------------------
173
+
174
+ /**
175
+ * Build a project context block from plugin config.
176
+ *
177
+ * Provides structural info only: project name and repo paths.
178
+ * Framework, build/test commands, and conventions belong in CLAUDE.md
179
+ * and AGENTS.md in the repo root — agents are instructed to read those.
180
+ */
181
+ export function buildProjectContext(pluginConfig?: Record<string, unknown>): string {
182
+ if (!pluginConfig) return "";
183
+
184
+ const lines: string[] = [];
185
+
186
+ // Project name
187
+ const projectName = pluginConfig.projectName as string | undefined;
188
+ if (projectName) lines.push(`Project: ${projectName}`);
189
+
190
+ // Repo(s)
191
+ const repos = pluginConfig.repos as Record<string, string> | undefined;
192
+ const baseRepo = pluginConfig.codexBaseRepo as string | undefined;
193
+ if (repos && Object.keys(repos).length > 0) {
194
+ lines.push(`Repos: ${Object.entries(repos).map(([k, v]) => `${k} (${v})`).join(", ")}`);
195
+ } else if (baseRepo) {
196
+ lines.push(`Repo: ${baseRepo}`);
197
+ }
198
+
199
+ if (lines.length === 0) return "";
200
+ return "## Project Context\n" + lines.join("\n");
201
+ }
202
+
170
203
  // ---------------------------------------------------------------------------
171
204
  // Task builders
172
205
  // ---------------------------------------------------------------------------
@@ -199,6 +232,7 @@ export function buildWorkerTask(
199
232
  guidance: opts?.guidance
200
233
  ? `\n---\n## IMPORTANT — Workspace Guidance (MUST follow)\nThe workspace owner has set the following mandatory instructions. You MUST incorporate these into your response:\n\n${opts.guidance.slice(0, 2000)}\n---`
201
234
  : "",
235
+ projectContext: buildProjectContext(opts?.pluginConfig),
202
236
  };
203
237
 
204
238
  let task = renderTemplate(prompts.worker.task, vars);
@@ -233,6 +267,7 @@ export function buildAuditTask(
233
267
  guidance: opts?.guidance
234
268
  ? `\n---\n## IMPORTANT — Workspace Guidance (MUST follow)\nThe workspace owner has set the following mandatory instructions. You MUST incorporate these into your response:\n\n${opts.guidance.slice(0, 2000)}\n---`
235
269
  : "",
270
+ projectContext: buildProjectContext(pluginConfig),
236
271
  };
237
272
 
238
273
  return {
@@ -141,6 +141,7 @@ vi.mock("./pipeline.js", () => ({
141
141
  runFullPipeline: runFullPipelineMock,
142
142
  resumePipeline: resumePipelineMock,
143
143
  spawnWorker: spawnWorkerMock,
144
+ buildProjectContext: () => "",
144
145
  }));
145
146
 
146
147
  vi.mock("../api/linear-api.js", () => ({
@@ -3,7 +3,7 @@ import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
5
5
  import { LinearAgentApi, resolveLinearToken } from "../api/linear-api.js";
6
- import { spawnWorker, type HookContext } from "./pipeline.js";
6
+ import { spawnWorker, buildProjectContext, type HookContext } from "./pipeline.js";
7
7
  import { setActiveSession, clearActiveSession, getIssueAffinity, _configureAffinityTtl, _resetAffinityForTesting } from "./active-session.js";
8
8
  import { readDispatchState, getActiveDispatch, registerDispatch, updateDispatchStatus, completeDispatch, removeActiveDispatch } from "./dispatch-state.js";
9
9
  import { createNotifierFromConfig, type NotifyFn } from "../infra/notify.js";
@@ -477,7 +477,14 @@ export async function handleLinearWebhook(
477
477
  commentContext ? `\n**Conversation:**\n${commentContext}` : "",
478
478
  userMessage ? `\n**Latest message:**\n> ${userMessage}` : "",
479
479
  ``,
480
- `Respond to the user's request. For work requests, dispatch via \`code_run\` and summarize the result. Be concise and action-oriented.`,
480
+ `## Scope Rules`,
481
+ `1. **Read the issue first.** The issue title + description define your scope. Everything you do must serve the issue as written.`,
482
+ `2. **\`code_run\` is ONLY for issue-body work.** Only dispatch \`code_run\` when the issue description contains implementation requirements. A greeting, question, or conversational issue gets a conversational response — NOT code_run.`,
483
+ `3. **Comments explore, issue body builds.** User comments may explore scope or ask questions but NEVER trigger \`code_run\` alone. If a comment requests new implementation, update the issue description first, then build from the issue text.`,
484
+ `4. **Plan before building.** For non-trivial work, respond with a plan first. Only dispatch \`code_run\` after the plan is clear and grounded in the issue body.`,
485
+ `5. **Match response to request.** Greeting → greet. Question → answer. No implementation requirements → no code_run.`,
486
+ ``,
487
+ `Respond within the scope defined above. Be concise and action-oriented.`,
481
488
  ].filter(Boolean).join("\n");
482
489
 
483
490
  // Run agent directly (non-blocking)
@@ -720,7 +727,12 @@ export async function handleLinearWebhook(
720
727
  commentContext ? `\n**Recent conversation:**\n${commentContext}` : "",
721
728
  `\n**User's follow-up message:**\n> ${userMessage}`,
722
729
  ``,
723
- `Respond to the user's follow-up. For work requests, dispatch via \`code_run\`. Be concise and action-oriented.`,
730
+ `## Scope Rules`,
731
+ `1. **The issue body is your scope.** Re-read the description above before acting.`,
732
+ `2. **Comments explore, issue body builds.** The follow-up may refine understanding or ask questions — NEVER dispatch \`code_run\` from a comment alone. If the user requests implementation, suggest updating the issue description first.`,
733
+ `3. **Match response to request.** Answer questions with answers. Do NOT escalate conversational messages into builds.`,
734
+ ``,
735
+ `Respond to the follow-up within the scope defined above. Be concise and action-oriented.`,
724
736
  ].filter(Boolean).join("\n");
725
737
 
726
738
  setActiveSession({
@@ -1236,6 +1248,7 @@ export async function handleLinearWebhook(
1236
1248
  ? formatGuidanceAppendix(triageGuidance)
1237
1249
  : "";
1238
1250
 
1251
+ const projectCtx = buildProjectContext(pluginConfig);
1239
1252
  const message = [
1240
1253
  `IMPORTANT: You are triaging a new Linear issue. You MUST respond with a JSON block containing your triage decisions, followed by your assessment as plain text.`,
1241
1254
  ``,
@@ -1246,6 +1259,7 @@ export async function handleLinearWebhook(
1246
1259
  `**Description:**`,
1247
1260
  description,
1248
1261
  ``,
1262
+ ...(projectCtx ? [projectCtx, ``] : []),
1249
1263
  `## Your Triage Tasks`,
1250
1264
  ``,
1251
1265
  `1. **Story Points** — Estimate complexity using ${estimationType} scale (1=trivial, 2=small, 3=medium, 5=large, 8=very large, 13=epic)`,
@@ -178,6 +178,11 @@ export function createCodeTool(
178
178
  required: ["prompt"],
179
179
  },
180
180
  execute: async (toolCallId: string, params: CliToolParams & { backend?: string }, ...rest: unknown[]) => {
181
+ // Extract onUpdate callback for progress reporting to Linear
182
+ const onUpdate = typeof rest[1] === "function"
183
+ ? rest[1] as (update: Record<string, unknown>) => void
184
+ : undefined;
185
+
181
186
  // Resolve backend: explicit alias → per-agent config → global default
182
187
  const currentSession = getCurrentSession();
183
188
  const agentId = currentSession?.agentId;
@@ -189,6 +194,13 @@ export function createCodeTool(
189
194
 
190
195
  api.logger.info(`code_run: backend=${backend} agent=${agentId ?? "unknown"}`);
191
196
 
197
+ // Emit prompt summary so Linear users see what's being built
198
+ const promptSummary = (params.prompt ?? "").slice(0, 200);
199
+ api.logger.info(`code_run prompt: [${backend}] ${promptSummary}`);
200
+ if (onUpdate) {
201
+ try { onUpdate({ status: "running", summary: `[${backend}] ${promptSummary}` }); } catch {}
202
+ }
203
+
192
204
  const result = await runner(api, params, pluginConfig);
193
205
 
194
206
  return jsonResult({
@@ -342,7 +342,7 @@ describe("linear_issues tool", () => {
342
342
  projectId: "proj-1",
343
343
  title: "Sub-task: handle edge case",
344
344
  description: "Fix the edge case for empty input",
345
- parentId: "ENG-123",
345
+ parentId: "issue-1", // Resolved UUID from getIssueDetails, not the identifier "ENG-123"
346
346
  });
347
347
  expect(result.success).toBe(true);
348
348
  expect(result.identifier).toBe("ENG-201");
@@ -86,12 +86,14 @@ async function handleCreate(api: LinearAgentApi, params: ToolParams) {
86
86
  // Resolve teamId: explicit param, or derive from parent issue
87
87
  let teamId = params.teamId;
88
88
  let projectId = params.projectId;
89
+ let resolvedParentId: string | undefined;
89
90
 
90
91
  if (params.parentIssueId) {
91
- // Fetch parent to get teamId and projectId
92
+ // Fetch parent to get teamId, projectId, and resolved UUID
92
93
  const parent = await api.getIssueDetails(params.parentIssueId);
93
94
  teamId = teamId ?? parent.team.id;
94
95
  projectId = projectId ?? parent.project?.id ?? undefined;
96
+ resolvedParentId = parent.id;
95
97
  }
96
98
 
97
99
  if (!teamId) {
@@ -106,7 +108,7 @@ async function handleCreate(api: LinearAgentApi, params: ToolParams) {
106
108
  };
107
109
 
108
110
  if (params.description) input.description = params.description;
109
- if (params.parentIssueId) input.parentId = params.parentIssueId;
111
+ if (resolvedParentId) input.parentId = resolvedParentId;
110
112
  if (projectId) input.projectId = projectId;
111
113
  if (params.priority != null) input.priority = params.priority;
112
114
  if (params.estimate != null) input.estimate = params.estimate;