@calltelemetry/openclaw-linear 0.9.8 → 0.9.9
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/package.json +1 -1
- package/src/__test__/fixtures/recorded-sub-issue-flow.ts +51 -33
- package/src/__test__/webhook-scenarios.test.ts +11 -10
- package/src/infra/cli.ts +260 -0
- package/src/infra/doctor.ts +40 -10
- package/src/infra/shared-profiles.ts +37 -5
- package/src/pipeline/guidance.test.ts +1 -1
- package/src/pipeline/guidance.ts +3 -1
- package/src/pipeline/pipeline.test.ts +4 -4
- package/src/pipeline/pipeline.ts +2 -2
- package/src/pipeline/webhook.ts +10 -2
package/package.json
CHANGED
|
@@ -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-22T02:
|
|
5
|
+
* Last recorded: 2026-02-22T02:27:42.743Z
|
|
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": "fa2dd666-d1bc-4d53-bc18-db2d02caef96",
|
|
48
|
+
"identifier": "UAT-305"
|
|
49
49
|
},
|
|
50
50
|
"createSubIssue1": {
|
|
51
|
-
"id": "
|
|
52
|
-
"identifier": "UAT-
|
|
51
|
+
"id": "f342ec90-c5f1-483d-987b-9de406d65fac",
|
|
52
|
+
"identifier": "UAT-306"
|
|
53
53
|
},
|
|
54
54
|
"createSubIssue2": {
|
|
55
|
-
"id": "
|
|
56
|
-
"identifier": "UAT-
|
|
55
|
+
"id": "210a5f52-a2bd-41a9-871f-b92563446d06",
|
|
56
|
+
"identifier": "UAT-307"
|
|
57
57
|
},
|
|
58
58
|
"subIssue1Details": {
|
|
59
|
-
"id": "
|
|
60
|
-
"identifier": "UAT-
|
|
59
|
+
"id": "f342ec90-c5f1-483d-987b-9de406d65fac",
|
|
60
|
+
"identifier": "UAT-306",
|
|
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": "fa2dd666-d1bc-4d53-bc18-db2d02caef96",
|
|
87
|
+
"identifier": "UAT-305"
|
|
88
88
|
},
|
|
89
89
|
"relations": {
|
|
90
90
|
"nodes": []
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
93
|
"subIssue2Details": {
|
|
94
|
-
"id": "
|
|
95
|
-
"identifier": "UAT-
|
|
94
|
+
"id": "210a5f52-a2bd-41a9-871f-b92563446d06",
|
|
95
|
+
"identifier": "UAT-307",
|
|
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,
|
|
@@ -114,22 +114,28 @@ export const RECORDED = {
|
|
|
114
114
|
"issueEstimationType": "tShirt"
|
|
115
115
|
},
|
|
116
116
|
"comments": {
|
|
117
|
-
"nodes": [
|
|
117
|
+
"nodes": [
|
|
118
|
+
{
|
|
119
|
+
"body": "This thread is for an agent session with ctclaw.",
|
|
120
|
+
"user": null,
|
|
121
|
+
"createdAt": "2026-02-22T02:27:41.198Z"
|
|
122
|
+
}
|
|
123
|
+
]
|
|
118
124
|
},
|
|
119
125
|
"project": null,
|
|
120
126
|
"parent": {
|
|
121
|
-
"id": "
|
|
122
|
-
"identifier": "UAT-
|
|
127
|
+
"id": "fa2dd666-d1bc-4d53-bc18-db2d02caef96",
|
|
128
|
+
"identifier": "UAT-305"
|
|
123
129
|
},
|
|
124
130
|
"relations": {
|
|
125
131
|
"nodes": []
|
|
126
132
|
}
|
|
127
133
|
},
|
|
128
134
|
"parentDetails": {
|
|
129
|
-
"id": "
|
|
130
|
-
"identifier": "UAT-
|
|
135
|
+
"id": "fa2dd666-d1bc-4d53-bc18-db2d02caef96",
|
|
136
|
+
"identifier": "UAT-305",
|
|
131
137
|
"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-22T02:
|
|
138
|
+
"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-22T02:27:39.194Z",
|
|
133
139
|
"estimate": null,
|
|
134
140
|
"state": {
|
|
135
141
|
"name": "Backlog",
|
|
@@ -149,7 +155,13 @@ export const RECORDED = {
|
|
|
149
155
|
"issueEstimationType": "tShirt"
|
|
150
156
|
},
|
|
151
157
|
"comments": {
|
|
152
|
-
"nodes": [
|
|
158
|
+
"nodes": [
|
|
159
|
+
{
|
|
160
|
+
"body": "This thread is for an agent session with ctclaw.",
|
|
161
|
+
"user": null,
|
|
162
|
+
"createdAt": "2026-02-22T02:27:39.821Z"
|
|
163
|
+
}
|
|
164
|
+
]
|
|
153
165
|
},
|
|
154
166
|
"project": null,
|
|
155
167
|
"parent": null,
|
|
@@ -158,11 +170,11 @@ export const RECORDED = {
|
|
|
158
170
|
}
|
|
159
171
|
},
|
|
160
172
|
"createRelation": {
|
|
161
|
-
"id": "
|
|
173
|
+
"id": "b7040537-aa53-4e7d-a76d-fd220df3e527"
|
|
162
174
|
},
|
|
163
175
|
"subIssue1WithRelation": {
|
|
164
|
-
"id": "
|
|
165
|
-
"identifier": "UAT-
|
|
176
|
+
"id": "f342ec90-c5f1-483d-987b-9de406d65fac",
|
|
177
|
+
"identifier": "UAT-306",
|
|
166
178
|
"title": "[SMOKE TEST] Sub-Issue 1: Backend API",
|
|
167
179
|
"description": "Implement the backend search API endpoint.\n\nGiven a search query, when the API is called, then matching results are returned.",
|
|
168
180
|
"estimate": 2,
|
|
@@ -184,20 +196,26 @@ export const RECORDED = {
|
|
|
184
196
|
"issueEstimationType": "tShirt"
|
|
185
197
|
},
|
|
186
198
|
"comments": {
|
|
187
|
-
"nodes": [
|
|
199
|
+
"nodes": [
|
|
200
|
+
{
|
|
201
|
+
"body": "This thread is for an agent session with ctclaw.",
|
|
202
|
+
"user": null,
|
|
203
|
+
"createdAt": "2026-02-22T02:27:40.662Z"
|
|
204
|
+
}
|
|
205
|
+
]
|
|
188
206
|
},
|
|
189
207
|
"project": null,
|
|
190
208
|
"parent": {
|
|
191
|
-
"id": "
|
|
192
|
-
"identifier": "UAT-
|
|
209
|
+
"id": "fa2dd666-d1bc-4d53-bc18-db2d02caef96",
|
|
210
|
+
"identifier": "UAT-305"
|
|
193
211
|
},
|
|
194
212
|
"relations": {
|
|
195
213
|
"nodes": [
|
|
196
214
|
{
|
|
197
215
|
"type": "blocks",
|
|
198
216
|
"relatedIssue": {
|
|
199
|
-
"id": "
|
|
200
|
-
"identifier": "UAT-
|
|
217
|
+
"id": "210a5f52-a2bd-41a9-871f-b92563446d06",
|
|
218
|
+
"identifier": "UAT-307",
|
|
201
219
|
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI"
|
|
202
220
|
}
|
|
203
221
|
}
|
|
@@ -205,8 +223,8 @@ export const RECORDED = {
|
|
|
205
223
|
}
|
|
206
224
|
},
|
|
207
225
|
"subIssue2WithRelation": {
|
|
208
|
-
"id": "
|
|
209
|
-
"identifier": "UAT-
|
|
226
|
+
"id": "210a5f52-a2bd-41a9-871f-b92563446d06",
|
|
227
|
+
"identifier": "UAT-307",
|
|
210
228
|
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
211
229
|
"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
230
|
"estimate": 3,
|
|
@@ -232,14 +250,14 @@ export const RECORDED = {
|
|
|
232
250
|
{
|
|
233
251
|
"body": "This thread is for an agent session with ctclaw.",
|
|
234
252
|
"user": null,
|
|
235
|
-
"createdAt": "2026-02-22T02:
|
|
253
|
+
"createdAt": "2026-02-22T02:27:41.198Z"
|
|
236
254
|
}
|
|
237
255
|
]
|
|
238
256
|
},
|
|
239
257
|
"project": null,
|
|
240
258
|
"parent": {
|
|
241
|
-
"id": "
|
|
242
|
-
"identifier": "UAT-
|
|
259
|
+
"id": "fa2dd666-d1bc-4d53-bc18-db2d02caef96",
|
|
260
|
+
"identifier": "UAT-305"
|
|
243
261
|
},
|
|
244
262
|
"relations": {
|
|
245
263
|
"nodes": []
|
|
@@ -649,7 +649,7 @@ describe("webhook scenario tests — full handler flows", () => {
|
|
|
649
649
|
|
|
650
650
|
expect(mockRunAgent).toHaveBeenCalledOnce();
|
|
651
651
|
const runArgs = mockRunAgent.mock.calls[0][0];
|
|
652
|
-
expect(runArgs.message).toContain("
|
|
652
|
+
expect(runArgs.message).toContain("Workspace Guidance");
|
|
653
653
|
expect(runArgs.message).toContain("Always use the main branch");
|
|
654
654
|
});
|
|
655
655
|
|
|
@@ -668,11 +668,12 @@ describe("webhook scenario tests — full handler flows", () => {
|
|
|
668
668
|
expect(mockRunAgent).toHaveBeenCalledOnce();
|
|
669
669
|
const msg = mockRunAgent.mock.calls[0][0].message;
|
|
670
670
|
|
|
671
|
-
// Guidance text should appear in the
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
expect(
|
|
671
|
+
// Guidance text should appear in the guidance section, not as the user's latest message
|
|
672
|
+
expect(msg).toContain("Please fix the routing bug");
|
|
673
|
+
// The guidance text should be within the guidance section, not in the "Latest message" block
|
|
674
|
+
const latestMsgSection = msg.split("**Latest message:**")[1] ?? "";
|
|
675
|
+
expect(latestMsgSection).toContain("Please fix the routing bug");
|
|
676
|
+
expect(latestMsgSection).not.toContain("Always use the main branch");
|
|
676
677
|
});
|
|
677
678
|
|
|
678
679
|
it("prompted: includes guidance from promptContext", async () => {
|
|
@@ -688,7 +689,7 @@ describe("webhook scenario tests — full handler flows", () => {
|
|
|
688
689
|
expect(mockRunAgent).toHaveBeenCalledOnce();
|
|
689
690
|
const msg = mockRunAgent.mock.calls[0][0].message;
|
|
690
691
|
expect(msg).toContain("Can you also fix the tests?");
|
|
691
|
-
expect(msg).toContain("
|
|
692
|
+
expect(msg).toContain("Workspace Guidance");
|
|
692
693
|
expect(msg).toContain("Use TypeScript strict mode");
|
|
693
694
|
});
|
|
694
695
|
|
|
@@ -704,7 +705,7 @@ describe("webhook scenario tests — full handler flows", () => {
|
|
|
704
705
|
|
|
705
706
|
expect(mockRunAgent).toHaveBeenCalledOnce();
|
|
706
707
|
const msg = mockRunAgent.mock.calls[0][0].message;
|
|
707
|
-
expect(msg).not.toContain("
|
|
708
|
+
expect(msg).not.toContain("Workspace Guidance");
|
|
708
709
|
expect(msg).not.toContain("Should not appear in prompt");
|
|
709
710
|
});
|
|
710
711
|
|
|
@@ -724,7 +725,7 @@ describe("webhook scenario tests — full handler flows", () => {
|
|
|
724
725
|
|
|
725
726
|
expect(mockRunAgent).toHaveBeenCalledOnce();
|
|
726
727
|
const msg = mockRunAgent.mock.calls[0][0].message;
|
|
727
|
-
expect(msg).not.toContain("
|
|
728
|
+
expect(msg).not.toContain("Workspace Guidance");
|
|
728
729
|
expect(msg).not.toContain("Should be suppressed");
|
|
729
730
|
});
|
|
730
731
|
|
|
@@ -782,7 +783,7 @@ describe("webhook scenario tests — full handler flows", () => {
|
|
|
782
783
|
|
|
783
784
|
expect(mockRunAgent).toHaveBeenCalledOnce();
|
|
784
785
|
const msg = mockRunAgent.mock.calls[0][0].message;
|
|
785
|
-
expect(msg).toContain("
|
|
786
|
+
expect(msg).toContain("Workspace Guidance");
|
|
786
787
|
expect(msg).toContain("Cached guidance from session event");
|
|
787
788
|
});
|
|
788
789
|
});
|
package/src/infra/cli.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { validateRepoPath } from "./multi-repo.js";
|
|
|
13
13
|
import { LINEAR_OAUTH_AUTH_URL, LINEAR_OAUTH_TOKEN_URL, LINEAR_AGENT_SCOPES } from "../api/auth.js";
|
|
14
14
|
import { listWorktrees } from "./codex-worktree.js";
|
|
15
15
|
import { loadPrompts, clearPromptCache } from "../pipeline/pipeline.js";
|
|
16
|
+
import { PROFILES_PATH, createAgentProfilesFile, loadAgentProfiles } from "./shared-profiles.js";
|
|
16
17
|
import {
|
|
17
18
|
formatMessage,
|
|
18
19
|
parseNotificationsConfig,
|
|
@@ -213,6 +214,265 @@ export function registerCli(program: Command, api: OpenClawPluginApi): void {
|
|
|
213
214
|
console.log();
|
|
214
215
|
});
|
|
215
216
|
|
|
217
|
+
// --- interactive profile creation helper ---
|
|
218
|
+
async function createProfileInteractive(): Promise<void> {
|
|
219
|
+
const name = await prompt(" Agent name (lowercase, no spaces — e.g. bobbin): ");
|
|
220
|
+
if (!name) {
|
|
221
|
+
console.log(" Skipped.\n");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const agentId = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
226
|
+
if (!agentId) {
|
|
227
|
+
console.log(" Invalid name. Skipped.\n");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const labelDefault = agentId.charAt(0).toUpperCase() + agentId.slice(1);
|
|
232
|
+
const labelInput = await prompt(` Display label [${labelDefault}]: `);
|
|
233
|
+
const label = labelInput || labelDefault;
|
|
234
|
+
|
|
235
|
+
const aliasInput = await prompt(` Additional @mention aliases (comma-separated, or blank): `);
|
|
236
|
+
const extraAliases = aliasInput
|
|
237
|
+
? aliasInput.split(",").map(a => a.trim().toLowerCase()).filter(Boolean)
|
|
238
|
+
: [];
|
|
239
|
+
const mentionAliases = [agentId, ...extraAliases.filter(a => a !== agentId)];
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
createAgentProfilesFile({ agentId, label, mentionAliases });
|
|
243
|
+
console.log(`\n ✓ Created ${PROFILES_PATH}`);
|
|
244
|
+
console.log(` Agent: ${agentId} (${label})`);
|
|
245
|
+
console.log(` Aliases: ${mentionAliases.map(a => "@" + a).join(", ")}`);
|
|
246
|
+
console.log(` Default: yes`);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
console.log(`\n ✗ Failed to create agent-profiles.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// --- openclaw openclaw-linear setup ---
|
|
253
|
+
linear
|
|
254
|
+
.command("setup")
|
|
255
|
+
.description("Guided first-time setup — creates agent profile, checks auth, provisions webhook")
|
|
256
|
+
.action(async () => {
|
|
257
|
+
const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
|
|
258
|
+
|
|
259
|
+
console.log("\nLinear Plugin Setup");
|
|
260
|
+
console.log("═".repeat(50));
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------
|
|
263
|
+
// Step 1: Agent Profile
|
|
264
|
+
// ---------------------------------------------------------------
|
|
265
|
+
console.log("\nStep 1: Agent Profile");
|
|
266
|
+
console.log("─".repeat(50));
|
|
267
|
+
|
|
268
|
+
if (existsSync(PROFILES_PATH)) {
|
|
269
|
+
const profiles = loadAgentProfiles();
|
|
270
|
+
const count = Object.keys(profiles).length;
|
|
271
|
+
if (count > 0) {
|
|
272
|
+
const names = Object.entries(profiles)
|
|
273
|
+
.map(([id, p]) => `${id}${p.isDefault ? " (default)" : ""}`)
|
|
274
|
+
.join(", ");
|
|
275
|
+
console.log(` ✓ agent-profiles.json found (${count} agent${count > 1 ? "s" : ""}: ${names})`);
|
|
276
|
+
} else {
|
|
277
|
+
console.log(" ⚠ agent-profiles.json exists but has no agents.");
|
|
278
|
+
const fix = await prompt(" Overwrite with a new profile? [Y/n]: ");
|
|
279
|
+
if (fix.toLowerCase() === "n") {
|
|
280
|
+
console.log(" Skipped. Fix the file manually and re-run setup.\n");
|
|
281
|
+
} else {
|
|
282
|
+
await createProfileInteractive();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
console.log(" No agent-profiles.json found — let's create one!\n");
|
|
287
|
+
console.log(" Your agent needs a name so Linear users can @mention it.");
|
|
288
|
+
console.log(" Example: if your agent is called \"bobbin\", users type @bobbin in comments.\n");
|
|
289
|
+
await createProfileInteractive();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------
|
|
293
|
+
// Step 2: Authentication
|
|
294
|
+
// ---------------------------------------------------------------
|
|
295
|
+
console.log("\nStep 2: Authentication");
|
|
296
|
+
console.log("─".repeat(50));
|
|
297
|
+
|
|
298
|
+
const tokenInfo = resolveLinearToken(pluginConfig);
|
|
299
|
+
if (tokenInfo.accessToken) {
|
|
300
|
+
// Verify the token works
|
|
301
|
+
try {
|
|
302
|
+
const authHeader = tokenInfo.refreshToken
|
|
303
|
+
? `Bearer ${tokenInfo.accessToken}`
|
|
304
|
+
: tokenInfo.accessToken;
|
|
305
|
+
const res = await fetch(LINEAR_GRAPHQL_URL, {
|
|
306
|
+
method: "POST",
|
|
307
|
+
headers: { "Content-Type": "application/json", Authorization: authHeader },
|
|
308
|
+
body: JSON.stringify({ query: `{ viewer { name } organization { name } }` }),
|
|
309
|
+
});
|
|
310
|
+
if (res.ok) {
|
|
311
|
+
const payload = await res.json() as any;
|
|
312
|
+
if (payload.data?.viewer) {
|
|
313
|
+
console.log(` ✓ Authenticated as ${payload.data.viewer.name} (${payload.data.organization.name})`);
|
|
314
|
+
} else {
|
|
315
|
+
console.log(` ⚠ Token found but API returned errors. Run: openclaw openclaw-linear auth`);
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
console.log(` ⚠ Token found but API returned ${res.status}. Run: openclaw openclaw-linear auth`);
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
console.log(` ⚠ Token found but API unreachable. Check network and retry.`);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
console.log(" No Linear token found.\n");
|
|
325
|
+
const doAuth = await prompt(" Run OAuth authorization now? [Y/n]: ");
|
|
326
|
+
if (doAuth.toLowerCase() !== "n") {
|
|
327
|
+
const clientId = (pluginConfig?.clientId as string) ?? process.env.LINEAR_CLIENT_ID;
|
|
328
|
+
const clientSecret = (pluginConfig?.clientSecret as string) ?? process.env.LINEAR_CLIENT_SECRET;
|
|
329
|
+
|
|
330
|
+
if (!clientId || !clientSecret) {
|
|
331
|
+
console.log("\n ✗ OAuth client ID and secret not configured.");
|
|
332
|
+
console.log(" Set LINEAR_CLIENT_ID and LINEAR_CLIENT_SECRET env vars,");
|
|
333
|
+
console.log(" or add clientId/clientSecret to the plugin config in openclaw.json.");
|
|
334
|
+
console.log(" Then re-run: openclaw openclaw-linear setup\n");
|
|
335
|
+
} else {
|
|
336
|
+
const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT ?? "18789";
|
|
337
|
+
const redirectUri = (pluginConfig?.redirectUri as string)
|
|
338
|
+
?? process.env.LINEAR_REDIRECT_URI
|
|
339
|
+
?? `http://localhost:${gatewayPort}/linear/oauth/callback`;
|
|
340
|
+
|
|
341
|
+
const state = Math.random().toString(36).substring(7);
|
|
342
|
+
const authUrl = `${LINEAR_OAUTH_AUTH_URL}?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(LINEAR_AGENT_SCOPES)}&state=${state}&actor=app`;
|
|
343
|
+
|
|
344
|
+
console.log("\n Opening Linear OAuth page...\n");
|
|
345
|
+
console.log(` ${authUrl}\n`);
|
|
346
|
+
openBrowser(authUrl);
|
|
347
|
+
|
|
348
|
+
const code = await prompt(" Paste the authorization code from Linear: ");
|
|
349
|
+
if (code) {
|
|
350
|
+
try {
|
|
351
|
+
const response = await fetch(LINEAR_OAUTH_TOKEN_URL, {
|
|
352
|
+
method: "POST",
|
|
353
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
354
|
+
body: new URLSearchParams({
|
|
355
|
+
grant_type: "authorization_code",
|
|
356
|
+
code,
|
|
357
|
+
client_id: clientId,
|
|
358
|
+
client_secret: clientSecret,
|
|
359
|
+
redirect_uri: redirectUri,
|
|
360
|
+
}),
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
if (response.ok) {
|
|
364
|
+
const tokens = await response.json() as any;
|
|
365
|
+
let store: any = { version: 1, profiles: {} };
|
|
366
|
+
try {
|
|
367
|
+
const raw = readFileSync(AUTH_PROFILES_PATH, "utf8");
|
|
368
|
+
store = JSON.parse(raw);
|
|
369
|
+
} catch { /* fresh store */ }
|
|
370
|
+
|
|
371
|
+
store.profiles = store.profiles ?? {};
|
|
372
|
+
store.profiles["linear:default"] = {
|
|
373
|
+
type: "oauth",
|
|
374
|
+
provider: "linear",
|
|
375
|
+
accessToken: tokens.access_token,
|
|
376
|
+
access: tokens.access_token,
|
|
377
|
+
refreshToken: tokens.refresh_token ?? null,
|
|
378
|
+
refresh: tokens.refresh_token ?? null,
|
|
379
|
+
expiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : null,
|
|
380
|
+
expires: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : null,
|
|
381
|
+
scope: tokens.scope,
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
writeFileSync(AUTH_PROFILES_PATH, JSON.stringify(store, null, 2), "utf8");
|
|
385
|
+
console.log(" ✓ Token saved to auth-profiles.json");
|
|
386
|
+
} else {
|
|
387
|
+
const error = await response.text();
|
|
388
|
+
console.log(` ✗ Token exchange failed (${response.status}): ${error}`);
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.log(` ✗ Token exchange error: ${err instanceof Error ? err.message : String(err)}`);
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
console.log(" Skipped. Run: openclaw openclaw-linear auth later.");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
console.log(" Skipped. Run: openclaw openclaw-linear auth when ready.");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ---------------------------------------------------------------
|
|
403
|
+
// Step 3: Webhook
|
|
404
|
+
// ---------------------------------------------------------------
|
|
405
|
+
console.log("\nStep 3: Webhook");
|
|
406
|
+
console.log("─".repeat(50));
|
|
407
|
+
|
|
408
|
+
const freshToken = resolveLinearToken(pluginConfig);
|
|
409
|
+
if (!freshToken.accessToken) {
|
|
410
|
+
console.log(" ⚠ Skipped — no auth token available. Complete Step 2 first.");
|
|
411
|
+
} else {
|
|
412
|
+
const { provisionWebhook, getWebhookStatus } = await import("./webhook-provision.js");
|
|
413
|
+
const linearApi = new LinearAgentApi(freshToken.accessToken, {
|
|
414
|
+
refreshToken: freshToken.refreshToken,
|
|
415
|
+
expiresAt: freshToken.expiresAt,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const webhookUrl = (pluginConfig?.webhookUrl as string)
|
|
419
|
+
?? "https://linear.calltelemetry.com/linear/webhook";
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const status = await getWebhookStatus(linearApi, webhookUrl);
|
|
423
|
+
if (status && status.issues.length === 0) {
|
|
424
|
+
console.log(` ✓ Webhook already configured (${webhookUrl})`);
|
|
425
|
+
} else {
|
|
426
|
+
const result = await provisionWebhook(linearApi, webhookUrl, { allPublicTeams: true });
|
|
427
|
+
if (result.action === "created") {
|
|
428
|
+
console.log(` ✓ Webhook created (${result.webhookId})`);
|
|
429
|
+
} else if (result.action === "updated") {
|
|
430
|
+
console.log(` ✓ Webhook updated (${result.webhookId})`);
|
|
431
|
+
} else {
|
|
432
|
+
console.log(` ✓ Webhook OK (${result.webhookId})`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.log(` ⚠ Webhook check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
437
|
+
console.log(" Run: openclaw openclaw-linear webhooks setup");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ---------------------------------------------------------------
|
|
442
|
+
// Step 4: Verify
|
|
443
|
+
// ---------------------------------------------------------------
|
|
444
|
+
console.log("\nStep 4: Verification");
|
|
445
|
+
console.log("─".repeat(50));
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
const { runDoctor, formatReport } = await import("./doctor.js");
|
|
449
|
+
const report = await runDoctor({
|
|
450
|
+
fix: false,
|
|
451
|
+
json: false,
|
|
452
|
+
pluginConfig,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
console.log(formatReport(report));
|
|
456
|
+
|
|
457
|
+
if (report.summary.errors === 0) {
|
|
458
|
+
const profiles = loadAgentProfiles();
|
|
459
|
+
const defaultAgent = Object.entries(profiles).find(([, p]) => p.isDefault);
|
|
460
|
+
const agentName = defaultAgent?.[1]?.label ?? defaultAgent?.[0] ?? "your agent";
|
|
461
|
+
console.log("─".repeat(50));
|
|
462
|
+
console.log(`Setup complete! ${agentName} is ready to receive Linear issues.`);
|
|
463
|
+
console.log(`\nRestart the gateway to apply changes:`);
|
|
464
|
+
console.log(` systemctl --user restart openclaw-gateway\n`);
|
|
465
|
+
} else {
|
|
466
|
+
console.log("─".repeat(50));
|
|
467
|
+
console.log("Some issues remain. Fix them and run: openclaw openclaw-linear doctor --fix\n");
|
|
468
|
+
process.exitCode = 1;
|
|
469
|
+
}
|
|
470
|
+
} catch (err) {
|
|
471
|
+
console.log(` ⚠ Doctor failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
472
|
+
console.log(" Run: openclaw openclaw-linear doctor for details.\n");
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
216
476
|
// --- openclaw openclaw-linear worktrees ---
|
|
217
477
|
linear
|
|
218
478
|
.command("worktrees")
|
package/src/infra/doctor.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { loadPrompts, clearPromptCache } from "../pipeline/pipeline.js";
|
|
|
14
14
|
import { listWorktrees } from "./codex-worktree.js";
|
|
15
15
|
import { loadCodingConfig, resolveCodingBackend, type CodingBackend } from "../tools/code-tool.js";
|
|
16
16
|
import { getWebhookStatus, provisionWebhook, REQUIRED_RESOURCE_TYPES } from "./webhook-provision.js";
|
|
17
|
+
import { createAgentProfilesFile } from "./shared-profiles.js";
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Types
|
|
@@ -227,22 +228,51 @@ export async function checkAuth(pluginConfig?: Record<string, unknown>): Promise
|
|
|
227
228
|
// Section 2: Agent Configuration
|
|
228
229
|
// ---------------------------------------------------------------------------
|
|
229
230
|
|
|
230
|
-
export function checkAgentConfig(pluginConfig?: Record<string, unknown
|
|
231
|
+
export function checkAgentConfig(pluginConfig?: Record<string, unknown>, fix = false): CheckResult[] {
|
|
231
232
|
const checks: CheckResult[] = [];
|
|
232
233
|
|
|
233
234
|
// Load profiles
|
|
234
235
|
let profiles: Record<string, AgentProfile>;
|
|
235
236
|
try {
|
|
236
237
|
if (!existsSync(AGENT_PROFILES_PATH)) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
if (fix) {
|
|
239
|
+
try {
|
|
240
|
+
createAgentProfilesFile({
|
|
241
|
+
agentId: "my-agent",
|
|
242
|
+
label: "My Agent",
|
|
243
|
+
mentionAliases: ["my-agent"],
|
|
244
|
+
});
|
|
245
|
+
checks.push(pass('agent-profiles.json created with default "my-agent" profile (--fix)'));
|
|
246
|
+
checks.push(warn(
|
|
247
|
+
'Customize your agent profile',
|
|
248
|
+
undefined,
|
|
249
|
+
{ fix: `Edit ${AGENT_PROFILES_PATH} to set your agent's name and aliases, or run: openclaw openclaw-linear setup` },
|
|
250
|
+
));
|
|
251
|
+
// Reload after creation
|
|
252
|
+
const raw = readFileSync(AGENT_PROFILES_PATH, "utf8");
|
|
253
|
+
profiles = JSON.parse(raw).agents ?? {};
|
|
254
|
+
// Continue to remaining checks below
|
|
255
|
+
} catch (err) {
|
|
256
|
+
checks.push(fail(
|
|
257
|
+
"Failed to create agent-profiles.json",
|
|
258
|
+
err instanceof Error ? err.message : String(err),
|
|
259
|
+
"Run: openclaw openclaw-linear setup",
|
|
260
|
+
));
|
|
261
|
+
return checks;
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
checks.push(fail(
|
|
265
|
+
"agent-profiles.json not found",
|
|
266
|
+
`Expected at: ${AGENT_PROFILES_PATH}`,
|
|
267
|
+
"Run: openclaw openclaw-linear setup",
|
|
268
|
+
));
|
|
269
|
+
return checks;
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
const raw = readFileSync(AGENT_PROFILES_PATH, "utf8");
|
|
273
|
+
const parsed = JSON.parse(raw);
|
|
274
|
+
profiles = parsed.agents ?? {};
|
|
242
275
|
}
|
|
243
|
-
const raw = readFileSync(AGENT_PROFILES_PATH, "utf8");
|
|
244
|
-
const parsed = JSON.parse(raw);
|
|
245
|
-
profiles = parsed.agents ?? {};
|
|
246
276
|
} catch (err) {
|
|
247
277
|
checks.push(fail(
|
|
248
278
|
"agent-profiles.json invalid JSON",
|
|
@@ -730,7 +760,7 @@ export async function runDoctor(opts: DoctorOptions): Promise<DoctorReport> {
|
|
|
730
760
|
sections.push({ name: "Authentication & Tokens", checks: auth.checks });
|
|
731
761
|
|
|
732
762
|
// 2. Agent config
|
|
733
|
-
sections.push({ name: "Agent Configuration", checks: checkAgentConfig(opts.pluginConfig) });
|
|
763
|
+
sections.push({ name: "Agent Configuration", checks: checkAgentConfig(opts.pluginConfig, opts.fix) });
|
|
734
764
|
|
|
735
765
|
// 3. Coding tools
|
|
736
766
|
sections.push({ name: "Coding Tools", checks: checkCodingTools(opts.pluginConfig) });
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* resolveAgentFromAlias() implementations that were previously in
|
|
6
6
|
* webhook.ts, intent-classify.ts, and tier-assess.ts.
|
|
7
7
|
*/
|
|
8
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
9
|
+
import { join, dirname } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -26,7 +26,7 @@ export interface AgentProfile {
|
|
|
26
26
|
// Cached profile loader (5s TTL)
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
|
|
29
|
-
const PROFILES_PATH = join(homedir(), ".openclaw", "agent-profiles.json");
|
|
29
|
+
export const PROFILES_PATH = join(homedir(), ".openclaw", "agent-profiles.json");
|
|
30
30
|
|
|
31
31
|
let profilesCache: { data: Record<string, AgentProfile>; loadedAt: number } | null = null;
|
|
32
32
|
const PROFILES_CACHE_TTL_MS = 5_000;
|
|
@@ -137,7 +137,7 @@ export function validateProfiles(): string | null {
|
|
|
137
137
|
`EOF\n` +
|
|
138
138
|
"```\n\n" +
|
|
139
139
|
`Then restart the gateway: \`systemctl --user restart openclaw-gateway\`\n\n` +
|
|
140
|
-
`
|
|
140
|
+
`Or run the guided setup: \`openclaw openclaw-linear setup\``
|
|
141
141
|
);
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -158,13 +158,45 @@ export function validateProfiles(): string | null {
|
|
|
158
158
|
return (
|
|
159
159
|
`**Critical setup error:** \`agent-profiles.json\` has no agents configured.\n\n` +
|
|
160
160
|
`Add at least one agent entry to the \`"agents"\` object in \`${PROFILES_PATH}\`.\n` +
|
|
161
|
-
`Run \`openclaw openclaw-linear
|
|
161
|
+
`Run \`openclaw openclaw-linear setup\` for guided setup.`
|
|
162
162
|
);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
return null;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Profile creation (setup wizard / doctor --fix)
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
export interface CreateProfileOpts {
|
|
173
|
+
agentId: string;
|
|
174
|
+
label: string;
|
|
175
|
+
mentionAliases: string[];
|
|
176
|
+
mission?: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Create `agent-profiles.json` with a single default agent.
|
|
181
|
+
* Creates the parent directory if needed. Throws on write failure.
|
|
182
|
+
*/
|
|
183
|
+
export function createAgentProfilesFile(opts: CreateProfileOpts): void {
|
|
184
|
+
const data = {
|
|
185
|
+
agents: {
|
|
186
|
+
[opts.agentId]: {
|
|
187
|
+
label: opts.label,
|
|
188
|
+
mission: opts.mission ?? "AI assistant for Linear issues",
|
|
189
|
+
isDefault: true,
|
|
190
|
+
mentionAliases: opts.mentionAliases,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
mkdirSync(dirname(PROFILES_PATH), { recursive: true });
|
|
195
|
+
writeFileSync(PROFILES_PATH, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
196
|
+
// Bust the cache so the next load picks up the new file
|
|
197
|
+
profilesCache = null;
|
|
198
|
+
}
|
|
199
|
+
|
|
168
200
|
// ---------------------------------------------------------------------------
|
|
169
201
|
// Test-only: reset cache
|
|
170
202
|
// ---------------------------------------------------------------------------
|
|
@@ -93,7 +93,7 @@ describe("extractGuidanceFromPromptContext", () => {
|
|
|
93
93
|
describe("formatGuidanceAppendix", () => {
|
|
94
94
|
it("formats guidance as appendix block", () => {
|
|
95
95
|
const result = formatGuidanceAppendix("Use main branch.");
|
|
96
|
-
expect(result).toContain("##
|
|
96
|
+
expect(result).toContain("## IMPORTANT — Workspace Guidance");
|
|
97
97
|
expect(result).toContain("Use main branch.");
|
|
98
98
|
expect(result).toMatch(/^---\n/);
|
|
99
99
|
expect(result).toMatch(/\n---$/);
|
package/src/pipeline/guidance.ts
CHANGED
|
@@ -119,7 +119,9 @@ export function formatGuidanceAppendix(guidance: string | null): string {
|
|
|
119
119
|
const trimmed = guidance.trim().slice(0, MAX_GUIDANCE_CHARS);
|
|
120
120
|
return [
|
|
121
121
|
`---`,
|
|
122
|
-
`##
|
|
122
|
+
`## IMPORTANT — Workspace Guidance (MUST follow)`,
|
|
123
|
+
`The workspace owner has set the following mandatory instructions. You MUST incorporate these into your response:`,
|
|
124
|
+
``,
|
|
123
125
|
trimmed,
|
|
124
126
|
`---`,
|
|
125
127
|
].join("\n");
|
|
@@ -405,7 +405,7 @@ describe("buildWorkerTask (additional branches)", () => {
|
|
|
405
405
|
const { task } = buildWorkerTask(issue, "/wt/API-42", {
|
|
406
406
|
guidance: "Always use TypeScript strict mode",
|
|
407
407
|
});
|
|
408
|
-
expect(task).toContain("
|
|
408
|
+
expect(task).toContain("Workspace Guidance");
|
|
409
409
|
expect(task).toContain("Always use TypeScript strict mode");
|
|
410
410
|
});
|
|
411
411
|
|
|
@@ -413,7 +413,7 @@ describe("buildWorkerTask (additional branches)", () => {
|
|
|
413
413
|
const { task } = buildWorkerTask(issue, "/wt/API-42", {
|
|
414
414
|
guidance: undefined,
|
|
415
415
|
});
|
|
416
|
-
expect(task).not.toContain("
|
|
416
|
+
expect(task).not.toContain("Workspace Guidance");
|
|
417
417
|
});
|
|
418
418
|
|
|
419
419
|
it("uses undefined description as (no description)", () => {
|
|
@@ -443,7 +443,7 @@ describe("buildAuditTask (additional branches)", () => {
|
|
|
443
443
|
const { task } = buildAuditTask(issue, "/wt/API-99", undefined, {
|
|
444
444
|
guidance: "Focus on security",
|
|
445
445
|
});
|
|
446
|
-
expect(task).toContain("
|
|
446
|
+
expect(task).toContain("Workspace Guidance");
|
|
447
447
|
expect(task).toContain("Focus on security");
|
|
448
448
|
});
|
|
449
449
|
|
|
@@ -451,7 +451,7 @@ describe("buildAuditTask (additional branches)", () => {
|
|
|
451
451
|
const { task } = buildAuditTask(issue, "/wt/API-99", undefined, {
|
|
452
452
|
guidance: undefined,
|
|
453
453
|
});
|
|
454
|
-
expect(task).not.toContain("
|
|
454
|
+
expect(task).not.toContain("Workspace Guidance");
|
|
455
455
|
});
|
|
456
456
|
|
|
457
457
|
it("handles undefined description", () => {
|
package/src/pipeline/pipeline.ts
CHANGED
|
@@ -197,7 +197,7 @@ export function buildWorkerTask(
|
|
|
197
197
|
attempt: String(opts?.attempt ?? 0),
|
|
198
198
|
gaps: opts?.gaps?.length ? "- " + opts.gaps.join("\n- ") : "",
|
|
199
199
|
guidance: opts?.guidance
|
|
200
|
-
? `\n---\n##
|
|
200
|
+
? `\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
201
|
: "",
|
|
202
202
|
};
|
|
203
203
|
|
|
@@ -231,7 +231,7 @@ export function buildAuditTask(
|
|
|
231
231
|
attempt: "0",
|
|
232
232
|
gaps: "",
|
|
233
233
|
guidance: opts?.guidance
|
|
234
|
-
? `\n---\n##
|
|
234
|
+
? `\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
235
|
: "",
|
|
236
236
|
};
|
|
237
237
|
|
package/src/pipeline/webhook.ts
CHANGED
|
@@ -457,12 +457,17 @@ export async function handleLinearWebhook(
|
|
|
457
457
|
? [`**Your role:** Orchestrator with full Linear access. You can update issue fields, change status, and dispatch work via \`code_run\`. Do NOT post comments yourself — the handler posts your text output.`]
|
|
458
458
|
: [`**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 comments via linear_issues — the audit system handles lifecycle transitions.`];
|
|
459
459
|
|
|
460
|
+
if (guidanceAppendix) {
|
|
461
|
+
api.logger.info(`Guidance injected (${guidanceCtx.source}): ${guidanceCtx.guidance?.slice(0, 120)}...`);
|
|
462
|
+
}
|
|
463
|
+
|
|
460
464
|
const message = [
|
|
461
465
|
`You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
|
|
462
466
|
``,
|
|
463
467
|
...toolAccessLines,
|
|
464
468
|
``,
|
|
465
469
|
...roleLines,
|
|
470
|
+
guidanceAppendix ? `\n${guidanceAppendix}` : "",
|
|
466
471
|
``,
|
|
467
472
|
`## Issue: ${issueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
468
473
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
@@ -473,7 +478,6 @@ export async function handleLinearWebhook(
|
|
|
473
478
|
userMessage ? `\n**Latest message:**\n> ${userMessage}` : "",
|
|
474
479
|
``,
|
|
475
480
|
`Respond to the user's request. For work requests, dispatch via \`code_run\` and summarize the result. Be concise and action-oriented.`,
|
|
476
|
-
guidanceAppendix,
|
|
477
481
|
].filter(Boolean).join("\n");
|
|
478
482
|
|
|
479
483
|
// Run agent directly (non-blocking)
|
|
@@ -697,12 +701,17 @@ export async function handleLinearWebhook(
|
|
|
697
701
|
? [`**Your role:** Orchestrator with full Linear access. You can update issue fields, change status, and dispatch work via \`code_run\`. Do NOT post comments yourself — the handler posts your text output.`]
|
|
698
702
|
: [`**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`];
|
|
699
703
|
|
|
704
|
+
if (followUpGuidanceAppendix) {
|
|
705
|
+
api.logger.info(`Follow-up guidance injected: ${(guidanceCtxPrompted.guidance ?? "cached").slice(0, 120)}...`);
|
|
706
|
+
}
|
|
707
|
+
|
|
700
708
|
const message = [
|
|
701
709
|
`You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
|
|
702
710
|
``,
|
|
703
711
|
...followUpToolAccessLines,
|
|
704
712
|
``,
|
|
705
713
|
...followUpRoleLines,
|
|
714
|
+
followUpGuidanceAppendix ? `\n${followUpGuidanceAppendix}` : "",
|
|
706
715
|
``,
|
|
707
716
|
`## Issue: ${followUpIssueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
708
717
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
@@ -713,7 +722,6 @@ export async function handleLinearWebhook(
|
|
|
713
722
|
`\n**User's follow-up message:**\n> ${userMessage}`,
|
|
714
723
|
``,
|
|
715
724
|
`Respond to the user's follow-up. For work requests, dispatch via \`code_run\`. Be concise and action-oriented.`,
|
|
716
|
-
followUpGuidanceAppendix,
|
|
717
725
|
].filter(Boolean).join("\n");
|
|
718
726
|
|
|
719
727
|
setActiveSession({
|