@calltelemetry/openclaw-linear 0.9.11 → 0.9.12
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 +37 -31
- package/src/__test__/smoke-linear-api.test.ts +142 -0
- package/src/__test__/webhook-scenarios.test.ts +44 -0
- 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 +3 -1
- package/src/tools/linear-issues-tool.test.ts +1 -1
- package/src/tools/linear-issues-tool.ts +4 -2
package/README.md
CHANGED
|
@@ -847,6 +847,7 @@ Add settings under the plugin entry in `openclaw.json`:
|
|
|
847
847
|
| `codexBaseRepo` | string | `"/home/claw/ai-workspace"` | Git repo for worktrees |
|
|
848
848
|
| `worktreeBaseDir` | string | `"~/.openclaw/worktrees"` | Where worktrees are created |
|
|
849
849
|
| `repos` | object | — | Multi-repo map (see [Multi-Repo](#multi-repo)) |
|
|
850
|
+
| `projectName` | string | — | Human-readable project name (injected into agent prompts) |
|
|
850
851
|
| `dispatchStatePath` | string | `"~/.openclaw/linear-dispatch-state.json"` | Dispatch state file |
|
|
851
852
|
| `planningStatePath` | string | `"~/.openclaw/linear-planning-state.json"` | Planning session state file |
|
|
852
853
|
| `promptsPath` | string | — | Custom prompts file path |
|
|
@@ -1044,6 +1045,17 @@ Guidance is cached per-team (24h TTL) so comment webhooks (which don't carry gui
|
|
|
1044
1045
|
}
|
|
1045
1046
|
```
|
|
1046
1047
|
|
|
1048
|
+
### Project Context — CLAUDE.md & AGENTS.md
|
|
1049
|
+
|
|
1050
|
+
Agents are instructed to read two files from the repo root before starting work:
|
|
1051
|
+
|
|
1052
|
+
- **CLAUDE.md** — Project conventions, tech stack, build/test commands, architecture. This is the same convention used by Claude Code and other AI coding tools.
|
|
1053
|
+
- **AGENTS.md** — Behavioral guidelines, code style rules, workflow conventions (branch naming, commit format, etc.).
|
|
1054
|
+
|
|
1055
|
+
These files are the primary way agents learn about your project. Without them, agents will explore the codebase but may miss conventions.
|
|
1056
|
+
|
|
1057
|
+
Run `openclaw openclaw-linear doctor` to check if these files exist. The doctor output includes templates to get started.
|
|
1058
|
+
|
|
1047
1059
|
### Example Custom Prompts
|
|
1048
1060
|
|
|
1049
1061
|
```yaml
|
|
@@ -1083,6 +1095,7 @@ rework:
|
|
|
1083
1095
|
| `{{reviewModel}}` | Name of cross-model reviewer (planner review) |
|
|
1084
1096
|
| `{{crossModelFeedback}}` | Review recommendations (planner review) |
|
|
1085
1097
|
| `{{guidance}}` | Linear workspace/team guidance (if available, empty string otherwise) |
|
|
1098
|
+
| `{{projectContext}}` | Project context from config (project name, repo paths). Framework/build/test info belongs in CLAUDE.md. |
|
|
1086
1099
|
|
|
1087
1100
|
### CLI
|
|
1088
1101
|
|
package/package.json
CHANGED
package/prompts.yaml
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# prompts.yaml — Externalized phase prompts for the Linear dispatch pipeline.
|
|
2
2
|
#
|
|
3
3
|
# Template variables: {{identifier}}, {{title}}, {{description}},
|
|
4
|
-
# {{worktreePath}}, {{gaps}}, {{tier}}, {{attempt}}, {{guidance}}
|
|
4
|
+
# {{worktreePath}}, {{gaps}}, {{tier}}, {{attempt}}, {{guidance}},
|
|
5
|
+
# {{projectContext}}
|
|
5
6
|
#
|
|
6
7
|
# Edit these to customize worker/audit behavior without rebuilding the plugin.
|
|
7
8
|
# Override path via `promptsPath` in plugin config.
|
|
@@ -25,16 +26,24 @@ worker:
|
|
|
25
26
|
{{description}}
|
|
26
27
|
|
|
27
28
|
Worktree: {{worktreePath}}
|
|
29
|
+
{{projectContext}}
|
|
30
|
+
|
|
31
|
+
Before writing any code, read these files in the worktree root (if they exist):
|
|
32
|
+
- CLAUDE.md — project conventions, tech stack, build/test commands, architecture
|
|
33
|
+
- AGENTS.md — behavioral guidelines, code style, workflow rules
|
|
34
|
+
These files are your primary source of truth for how this project works.
|
|
35
|
+
If they don't exist, explore the codebase to understand conventions before coding.
|
|
28
36
|
|
|
29
37
|
Instructions:
|
|
30
38
|
1. Read the issue body carefully — it defines what needs to be done
|
|
31
39
|
2. If the description is vague or missing, implement a reasonable interpretation and note your assumptions
|
|
32
|
-
3.
|
|
33
|
-
4.
|
|
34
|
-
5.
|
|
35
|
-
6.
|
|
36
|
-
7.
|
|
37
|
-
8.
|
|
40
|
+
3. Read CLAUDE.md and AGENTS.md in the worktree, then explore the codebase to understand the existing architecture
|
|
41
|
+
4. Plan your approach
|
|
42
|
+
5. Implement the solution in the worktree
|
|
43
|
+
6. Run tests to verify your changes
|
|
44
|
+
7. If tests fail, diagnose and fix the failures before returning — do not return with failing tests unless you've exhausted your ability to fix them
|
|
45
|
+
8. Commit your work with a clear commit message
|
|
46
|
+
9. Return a text summary: what you changed, what tests passed, any assumptions you made, and any open questions
|
|
38
47
|
|
|
39
48
|
Your text output will be captured automatically. Do NOT use linear_issues or attempt to post comments.
|
|
40
49
|
{{guidance}}
|
|
@@ -60,6 +69,12 @@ audit:
|
|
|
60
69
|
{{description}}
|
|
61
70
|
|
|
62
71
|
Worktree: {{worktreePath}}
|
|
72
|
+
{{projectContext}}
|
|
73
|
+
|
|
74
|
+
Before auditing, read these files in the worktree root (if they exist):
|
|
75
|
+
- CLAUDE.md — project conventions, tech stack, build/test commands
|
|
76
|
+
- AGENTS.md — behavioral guidelines, code style, workflow rules
|
|
77
|
+
These define the project's standards. Use them to evaluate code quality.
|
|
63
78
|
|
|
64
79
|
Checklist:
|
|
65
80
|
1. Identify ALL acceptance criteria from the issue body
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Recorded API responses from sub-issue decomposition smoke test.
|
|
3
3
|
* Auto-generated — do not edit manually.
|
|
4
4
|
* Re-generate by running: npx vitest run src/__test__/smoke-linear-api.test.ts
|
|
5
|
-
* Last recorded: 2026-02-
|
|
5
|
+
* Last recorded: 2026-02-22T03:20:08.396Z
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export const RECORDED = {
|
|
@@ -44,20 +44,20 @@ export const RECORDED = {
|
|
|
44
44
|
}
|
|
45
45
|
],
|
|
46
46
|
"createParent": {
|
|
47
|
-
"id": "
|
|
48
|
-
"identifier": "UAT-
|
|
47
|
+
"id": "2becfd65-c313-4d4b-9362-b3ac431872c6",
|
|
48
|
+
"identifier": "UAT-354"
|
|
49
49
|
},
|
|
50
50
|
"createSubIssue1": {
|
|
51
|
-
"id": "
|
|
52
|
-
"identifier": "UAT-
|
|
51
|
+
"id": "b7433275-83a2-4e6e-a98c-5fe2b3aa1147",
|
|
52
|
+
"identifier": "UAT-355"
|
|
53
53
|
},
|
|
54
54
|
"createSubIssue2": {
|
|
55
|
-
"id": "
|
|
56
|
-
"identifier": "UAT-
|
|
55
|
+
"id": "3526cea7-2261-4eee-bd8a-5206e12c6124",
|
|
56
|
+
"identifier": "UAT-356"
|
|
57
57
|
},
|
|
58
58
|
"subIssue1Details": {
|
|
59
|
-
"id": "
|
|
60
|
-
"identifier": "UAT-
|
|
59
|
+
"id": "b7433275-83a2-4e6e-a98c-5fe2b3aa1147",
|
|
60
|
+
"identifier": "UAT-355",
|
|
61
61
|
"title": "[SMOKE TEST] Sub-Issue 1: Backend API",
|
|
62
62
|
"description": "Implement the backend search API endpoint.\n\nGiven a search query, when the API is called, then matching results are returned.",
|
|
63
63
|
"estimate": 2,
|
|
@@ -83,16 +83,16 @@ export const RECORDED = {
|
|
|
83
83
|
},
|
|
84
84
|
"project": null,
|
|
85
85
|
"parent": {
|
|
86
|
-
"id": "
|
|
87
|
-
"identifier": "UAT-
|
|
86
|
+
"id": "2becfd65-c313-4d4b-9362-b3ac431872c6",
|
|
87
|
+
"identifier": "UAT-354"
|
|
88
88
|
},
|
|
89
89
|
"relations": {
|
|
90
90
|
"nodes": []
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
93
|
"subIssue2Details": {
|
|
94
|
-
"id": "
|
|
95
|
-
"identifier": "UAT-
|
|
94
|
+
"id": "3526cea7-2261-4eee-bd8a-5206e12c6124",
|
|
95
|
+
"identifier": "UAT-356",
|
|
96
96
|
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
97
97
|
"description": "Build the frontend search UI component.\n\nGiven the search page loads, when the user types a query, then results display in real-time.",
|
|
98
98
|
"estimate": 3,
|
|
@@ -118,18 +118,18 @@ export const RECORDED = {
|
|
|
118
118
|
},
|
|
119
119
|
"project": null,
|
|
120
120
|
"parent": {
|
|
121
|
-
"id": "
|
|
122
|
-
"identifier": "UAT-
|
|
121
|
+
"id": "2becfd65-c313-4d4b-9362-b3ac431872c6",
|
|
122
|
+
"identifier": "UAT-354"
|
|
123
123
|
},
|
|
124
124
|
"relations": {
|
|
125
125
|
"nodes": []
|
|
126
126
|
}
|
|
127
127
|
},
|
|
128
128
|
"parentDetails": {
|
|
129
|
-
"id": "
|
|
130
|
-
"identifier": "UAT-
|
|
129
|
+
"id": "2becfd65-c313-4d4b-9362-b3ac431872c6",
|
|
130
|
+
"identifier": "UAT-354",
|
|
131
131
|
"title": "[SMOKE TEST] Sub-Issue Parent: Search Feature",
|
|
132
|
-
"description": "Auto-generated by smoke test to verify sub-issue decomposition.\n\nThis parent issue should have two sub-issues created under it.\n\nCreated: 2026-02-
|
|
132
|
+
"description": "Auto-generated by smoke test to verify sub-issue decomposition.\n\nThis parent issue should have two sub-issues created under it.\n\nCreated: 2026-02-22T03:20:04.703Z",
|
|
133
133
|
"estimate": null,
|
|
134
134
|
"state": {
|
|
135
135
|
"name": "Backlog",
|
|
@@ -158,11 +158,11 @@ export const RECORDED = {
|
|
|
158
158
|
}
|
|
159
159
|
},
|
|
160
160
|
"createRelation": {
|
|
161
|
-
"id": "
|
|
161
|
+
"id": "a9cac881-3d13-45ab-b000-855777698d71"
|
|
162
162
|
},
|
|
163
163
|
"subIssue1WithRelation": {
|
|
164
|
-
"id": "
|
|
165
|
-
"identifier": "UAT-
|
|
164
|
+
"id": "b7433275-83a2-4e6e-a98c-5fe2b3aa1147",
|
|
165
|
+
"identifier": "UAT-355",
|
|
166
166
|
"title": "[SMOKE TEST] Sub-Issue 1: Backend API",
|
|
167
167
|
"description": "Implement the backend search API endpoint.\n\nGiven a search query, when the API is called, then matching results are returned.",
|
|
168
168
|
"estimate": 2,
|
|
@@ -184,20 +184,26 @@ export const RECORDED = {
|
|
|
184
184
|
"issueEstimationType": "tShirt"
|
|
185
185
|
},
|
|
186
186
|
"comments": {
|
|
187
|
-
"nodes": [
|
|
187
|
+
"nodes": [
|
|
188
|
+
{
|
|
189
|
+
"body": "This thread is for an agent session with ctclaw.",
|
|
190
|
+
"user": null,
|
|
191
|
+
"createdAt": "2026-02-22T03:20:06.930Z"
|
|
192
|
+
}
|
|
193
|
+
]
|
|
188
194
|
},
|
|
189
195
|
"project": null,
|
|
190
196
|
"parent": {
|
|
191
|
-
"id": "
|
|
192
|
-
"identifier": "UAT-
|
|
197
|
+
"id": "2becfd65-c313-4d4b-9362-b3ac431872c6",
|
|
198
|
+
"identifier": "UAT-354"
|
|
193
199
|
},
|
|
194
200
|
"relations": {
|
|
195
201
|
"nodes": [
|
|
196
202
|
{
|
|
197
203
|
"type": "blocks",
|
|
198
204
|
"relatedIssue": {
|
|
199
|
-
"id": "
|
|
200
|
-
"identifier": "UAT-
|
|
205
|
+
"id": "3526cea7-2261-4eee-bd8a-5206e12c6124",
|
|
206
|
+
"identifier": "UAT-356",
|
|
201
207
|
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI"
|
|
202
208
|
}
|
|
203
209
|
}
|
|
@@ -205,8 +211,8 @@ export const RECORDED = {
|
|
|
205
211
|
}
|
|
206
212
|
},
|
|
207
213
|
"subIssue2WithRelation": {
|
|
208
|
-
"id": "
|
|
209
|
-
"identifier": "UAT-
|
|
214
|
+
"id": "3526cea7-2261-4eee-bd8a-5206e12c6124",
|
|
215
|
+
"identifier": "UAT-356",
|
|
210
216
|
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
211
217
|
"description": "Build the frontend search UI component.\n\nGiven the search page loads, when the user types a query, then results display in real-time.",
|
|
212
218
|
"estimate": 3,
|
|
@@ -232,14 +238,14 @@ export const RECORDED = {
|
|
|
232
238
|
{
|
|
233
239
|
"body": "This thread is for an agent session with ctclaw.",
|
|
234
240
|
"user": null,
|
|
235
|
-
"createdAt": "2026-02-
|
|
241
|
+
"createdAt": "2026-02-22T03:20:06.960Z"
|
|
236
242
|
}
|
|
237
243
|
]
|
|
238
244
|
},
|
|
239
245
|
"project": null,
|
|
240
246
|
"parent": {
|
|
241
|
-
"id": "
|
|
242
|
-
"identifier": "UAT-
|
|
247
|
+
"id": "2becfd65-c313-4d4b-9362-b3ac431872c6",
|
|
248
|
+
"identifier": "UAT-354"
|
|
243
249
|
},
|
|
244
250
|
"relations": {
|
|
245
251
|
"nodes": []
|
|
@@ -14,6 +14,7 @@ import { homedir } from "node:os";
|
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
16
16
|
import { LinearAgentApi } from "../api/linear-api.js";
|
|
17
|
+
import { createLinearIssuesTool } from "../tools/linear-issues-tool.js";
|
|
17
18
|
|
|
18
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
|
|
@@ -684,6 +685,147 @@ describe("Linear API smoke tests", () => {
|
|
|
684
685
|
});
|
|
685
686
|
});
|
|
686
687
|
|
|
688
|
+
// ---------------------------------------------------------------------------
|
|
689
|
+
// Tool-level sub-issue creation (linear_issues tool)
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
|
|
692
|
+
describe("tool-level sub-issue creation (linear_issues tool)", () => {
|
|
693
|
+
let toolParentIdentifier: string | null = null;
|
|
694
|
+
let toolSubIdentifier: string | null = null;
|
|
695
|
+
let toolParentId: string | null = null;
|
|
696
|
+
let toolSubId: string | null = null;
|
|
697
|
+
let tool: any;
|
|
698
|
+
|
|
699
|
+
function parseToolResult(result: any): any {
|
|
700
|
+
if (result?.content && Array.isArray(result.content)) {
|
|
701
|
+
const textBlock = result.content.find((r: any) => r.type === "text");
|
|
702
|
+
if (textBlock) return JSON.parse(textBlock.text);
|
|
703
|
+
}
|
|
704
|
+
if (result?.details) return result.details;
|
|
705
|
+
return typeof result === "string" ? JSON.parse(result) : result;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
it("instantiates linear_issues tool with real credentials", () => {
|
|
709
|
+
const apiKey = loadApiKey();
|
|
710
|
+
const pluginApi = {
|
|
711
|
+
logger: {
|
|
712
|
+
info: (...args: any[]) => console.log("[tool-smoke]", ...args),
|
|
713
|
+
warn: (...args: any[]) => console.warn("[tool-smoke]", ...args),
|
|
714
|
+
error: (...args: any[]) => console.error("[tool-smoke]", ...args),
|
|
715
|
+
debug: () => {},
|
|
716
|
+
},
|
|
717
|
+
pluginConfig: { accessToken: apiKey },
|
|
718
|
+
};
|
|
719
|
+
tool = createLinearIssuesTool(pluginApi as any);
|
|
720
|
+
expect(tool).toBeTruthy();
|
|
721
|
+
expect(tool.name).toBe("linear_issues");
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it("creates a parent issue via tool action=create", async () => {
|
|
725
|
+
const result = parseToolResult(
|
|
726
|
+
await tool.execute("smoke-call-1", {
|
|
727
|
+
action: "create",
|
|
728
|
+
title: "[SMOKE TEST] Tool Sub-Issue Parent",
|
|
729
|
+
description:
|
|
730
|
+
"Auto-generated by tool-level smoke test.\n" +
|
|
731
|
+
"Tests linear_issues tool can create parent + sub-issues.\n\n" +
|
|
732
|
+
`Created: ${new Date().toISOString()}`,
|
|
733
|
+
teamId: TEAM_ID,
|
|
734
|
+
priority: 4,
|
|
735
|
+
}),
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
expect(result.error).toBeUndefined();
|
|
739
|
+
expect(result.success).toBe(true);
|
|
740
|
+
expect(result.identifier).toBeTruthy();
|
|
741
|
+
expect(result.id).toBeTruthy();
|
|
742
|
+
|
|
743
|
+
toolParentIdentifier = result.identifier;
|
|
744
|
+
toolParentId = result.id;
|
|
745
|
+
console.log(`Tool created parent: ${result.identifier} (${result.id})`);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it("reads parent issue via tool action=read (by identifier)", async () => {
|
|
749
|
+
expect(toolParentIdentifier).toBeTruthy();
|
|
750
|
+
|
|
751
|
+
const result = parseToolResult(
|
|
752
|
+
await tool.execute("smoke-call-2", {
|
|
753
|
+
action: "read",
|
|
754
|
+
issueId: toolParentIdentifier!,
|
|
755
|
+
}),
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
expect(result.error).toBeUndefined();
|
|
759
|
+
expect(result.identifier).toBe(toolParentIdentifier);
|
|
760
|
+
expect(result.title).toContain("[SMOKE TEST]");
|
|
761
|
+
expect(result.team.id).toBe(TEAM_ID);
|
|
762
|
+
expect(result.parent).toBeNull();
|
|
763
|
+
console.log(`Tool read parent: ${result.identifier} (status=${result.status})`);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it("creates a sub-issue via tool action=create with parentIssueId (identifier)", async () => {
|
|
767
|
+
expect(toolParentIdentifier).toBeTruthy();
|
|
768
|
+
|
|
769
|
+
const result = parseToolResult(
|
|
770
|
+
await tool.execute("smoke-call-3", {
|
|
771
|
+
action: "create",
|
|
772
|
+
title: "[SMOKE TEST] Tool Sub-Issue: Backend work",
|
|
773
|
+
description:
|
|
774
|
+
"Sub-issue created via linear_issues tool with parentIssueId.\n" +
|
|
775
|
+
"Verifies identifier → UUID resolution and teamId inheritance.",
|
|
776
|
+
parentIssueId: toolParentIdentifier!,
|
|
777
|
+
priority: 3,
|
|
778
|
+
estimate: 2,
|
|
779
|
+
}),
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
expect(result.error).toBeUndefined();
|
|
783
|
+
expect(result.success).toBe(true);
|
|
784
|
+
expect(result.identifier).toBeTruthy();
|
|
785
|
+
expect(result.id).toBeTruthy();
|
|
786
|
+
expect(result.parentIssueId).toBe(toolParentIdentifier);
|
|
787
|
+
|
|
788
|
+
toolSubIdentifier = result.identifier;
|
|
789
|
+
toolSubId = result.id;
|
|
790
|
+
console.log(`Tool created sub-issue: ${result.identifier} (parent=${toolParentIdentifier})`);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it("verifies sub-issue has correct parent via tool action=read", async () => {
|
|
794
|
+
expect(toolSubIdentifier).toBeTruthy();
|
|
795
|
+
|
|
796
|
+
const result = parseToolResult(
|
|
797
|
+
await tool.execute("smoke-call-4", {
|
|
798
|
+
action: "read",
|
|
799
|
+
issueId: toolSubIdentifier!,
|
|
800
|
+
}),
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
expect(result.error).toBeUndefined();
|
|
804
|
+
expect(result.identifier).toBe(toolSubIdentifier);
|
|
805
|
+
expect(result.parent).not.toBeNull();
|
|
806
|
+
expect(result.parent.identifier).toBe(toolParentIdentifier);
|
|
807
|
+
// teamId was inherited from parent (not provided explicitly in create)
|
|
808
|
+
expect(result.team.id).toBe(TEAM_ID);
|
|
809
|
+
console.log(`Tool sub-issue parent confirmed: ${result.parent.identifier}`);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it("cleans up: cancels tool-created issues", async () => {
|
|
813
|
+
const states = await api.getTeamStates(TEAM_ID);
|
|
814
|
+
const canceledState = states.find(
|
|
815
|
+
(s) => s.type === "canceled" || s.name.toLowerCase().includes("cancel"),
|
|
816
|
+
);
|
|
817
|
+
|
|
818
|
+
for (const id of [toolSubId, toolParentId]) {
|
|
819
|
+
if (!id || !canceledState) continue;
|
|
820
|
+
try {
|
|
821
|
+
await api.updateIssue(id, { stateId: canceledState.id });
|
|
822
|
+
} catch {
|
|
823
|
+
// Best effort
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
});
|
|
828
|
+
|
|
687
829
|
describe("cleanup", () => {
|
|
688
830
|
it("cancels the smoke test issue", async () => {
|
|
689
831
|
if (!smokeIssueId) return;
|
|
@@ -82,6 +82,7 @@ vi.mock("../pipeline/pipeline.js", () => ({
|
|
|
82
82
|
runPlannerStage: vi.fn().mockResolvedValue("mock plan"),
|
|
83
83
|
runFullPipeline: vi.fn().mockResolvedValue(undefined),
|
|
84
84
|
resumePipeline: vi.fn().mockResolvedValue(undefined),
|
|
85
|
+
buildProjectContext: () => "",
|
|
85
86
|
}));
|
|
86
87
|
|
|
87
88
|
vi.mock("../pipeline/active-session.js", () => ({
|
|
@@ -787,4 +788,47 @@ describe("webhook scenario tests — full handler flows", () => {
|
|
|
787
788
|
expect(msg).toContain("Cached guidance from session event");
|
|
788
789
|
});
|
|
789
790
|
});
|
|
791
|
+
|
|
792
|
+
describe("Sub-issue guidance in agent prompt", () => {
|
|
793
|
+
it("created: triaged issue includes sub-issue guidance with parentIssueId", async () => {
|
|
794
|
+
// Issue is "In Progress" (type: "started") — triaged, so full tool access
|
|
795
|
+
mockGetIssueDetails.mockResolvedValue(makeIssueDetails({
|
|
796
|
+
state: { name: "In Progress", type: "started" },
|
|
797
|
+
}));
|
|
798
|
+
|
|
799
|
+
const api = createApi();
|
|
800
|
+
const payload = makeAgentSessionEventCreated();
|
|
801
|
+
await postWebhook(api, payload);
|
|
802
|
+
|
|
803
|
+
await waitForMock(mockClearActiveSession);
|
|
804
|
+
|
|
805
|
+
expect(mockRunAgent).toHaveBeenCalledOnce();
|
|
806
|
+
const msg = mockRunAgent.mock.calls[0][0].message;
|
|
807
|
+
|
|
808
|
+
// Verify sub-issue guidance text includes the correct parentIssueId
|
|
809
|
+
expect(msg).toContain("Sub-issue guidance");
|
|
810
|
+
expect(msg).toContain("break it into sub-issues");
|
|
811
|
+
expect(msg).toContain('parentIssueId="ENG-123"');
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it("created: backlog issue does NOT include sub-issue guidance", async () => {
|
|
815
|
+
// Issue is "Backlog" (type: "backlog") — untriaged, so read-only tool access
|
|
816
|
+
mockGetIssueDetails.mockResolvedValue(makeIssueDetails({
|
|
817
|
+
state: { name: "Backlog", type: "backlog" },
|
|
818
|
+
}));
|
|
819
|
+
|
|
820
|
+
const api = createApi();
|
|
821
|
+
const payload = makeAgentSessionEventCreated();
|
|
822
|
+
await postWebhook(api, payload);
|
|
823
|
+
|
|
824
|
+
await waitForMock(mockClearActiveSession);
|
|
825
|
+
|
|
826
|
+
expect(mockRunAgent).toHaveBeenCalledOnce();
|
|
827
|
+
const msg = mockRunAgent.mock.calls[0][0].message;
|
|
828
|
+
|
|
829
|
+
// Backlog issues get READ ONLY access — no sub-issue guidance
|
|
830
|
+
expect(msg).not.toContain("Sub-issue guidance");
|
|
831
|
+
expect(msg).toContain("READ ONLY");
|
|
832
|
+
});
|
|
833
|
+
});
|
|
790
834
|
});
|
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";
|
|
@@ -1236,6 +1236,7 @@ export async function handleLinearWebhook(
|
|
|
1236
1236
|
? formatGuidanceAppendix(triageGuidance)
|
|
1237
1237
|
: "";
|
|
1238
1238
|
|
|
1239
|
+
const projectCtx = buildProjectContext(pluginConfig);
|
|
1239
1240
|
const message = [
|
|
1240
1241
|
`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
1242
|
``,
|
|
@@ -1246,6 +1247,7 @@ export async function handleLinearWebhook(
|
|
|
1246
1247
|
`**Description:**`,
|
|
1247
1248
|
description,
|
|
1248
1249
|
``,
|
|
1250
|
+
...(projectCtx ? [projectCtx, ``] : []),
|
|
1249
1251
|
`## Your Triage Tasks`,
|
|
1250
1252
|
``,
|
|
1251
1253
|
`1. **Story Points** — Estimate complexity using ${estimationType} scale (1=trivial, 2=small, 3=medium, 5=large, 8=very large, 13=epic)`,
|
|
@@ -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;
|