@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.
- package/README.md +13 -0
- package/package.json +1 -1
- package/prompts.yaml +22 -7
- package/src/__test__/fixtures/recorded-sub-issue-flow.ts +44 -32
- package/src/__test__/smoke-linear-api.test.ts +142 -0
- package/src/__test__/webhook-scenarios.test.ts +44 -0
- package/src/agent/agent.ts +18 -4
- package/src/infra/doctor.test.ts +77 -578
- package/src/infra/doctor.ts +106 -0
- package/src/pipeline/pipeline.test.ts +95 -0
- package/src/pipeline/pipeline.ts +37 -2
- package/src/pipeline/webhook.test.ts +1 -0
- package/src/pipeline/webhook.ts +17 -3
- package/src/tools/code-tool.ts +12 -0
- package/src/tools/linear-issues-tool.test.ts +1 -1
- package/src/tools/linear-issues-tool.ts +4 -2
package/src/infra/doctor.ts
CHANGED
|
@@ -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
|
// ---------------------------------------------------------------------------
|
package/src/pipeline/pipeline.ts
CHANGED
|
@@ -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 {
|
package/src/pipeline/webhook.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)`,
|
package/src/tools/code-tool.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
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;
|