@calltelemetry/openclaw-linear 0.9.12 → 0.9.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +50 -0
- package/package.json +1 -1
- package/src/__test__/fixtures/recorded-sub-issue-flow.ts +38 -32
- package/src/agent/agent.ts +18 -4
- package/src/infra/doctor.test.ts +77 -578
- package/src/pipeline/webhook.test.ts +7 -7
- package/src/pipeline/webhook.ts +158 -8
- package/src/tools/code-tool.ts +12 -0
package/index.ts
CHANGED
|
@@ -229,6 +229,56 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
229
229
|
}
|
|
230
230
|
});
|
|
231
231
|
|
|
232
|
+
// Hard gate: prepend planning-only constraints to code_run when issue is not "started".
|
|
233
|
+
// Even if the orchestrator LLM ignores scope rules, the coding agent receives hard constraints.
|
|
234
|
+
api.on("before_tool_call", async (event: any, _ctx: any) => {
|
|
235
|
+
if (event.toolName !== "code_run") return;
|
|
236
|
+
|
|
237
|
+
const { getCurrentSession } = await import("./src/pipeline/active-session.js");
|
|
238
|
+
const session = getCurrentSession();
|
|
239
|
+
if (!session?.issueId) return; // Non-Linear context, allow
|
|
240
|
+
|
|
241
|
+
// Check issue state
|
|
242
|
+
const hookTokenInfo = resolveLinearToken(pluginConfig);
|
|
243
|
+
if (!hookTokenInfo.accessToken) return;
|
|
244
|
+
const hookLinearApi = new LinearAgentApi(hookTokenInfo.accessToken, {
|
|
245
|
+
refreshToken: hookTokenInfo.refreshToken,
|
|
246
|
+
expiresAt: hookTokenInfo.expiresAt,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const issue = await hookLinearApi.getIssueDetails(session.issueId);
|
|
251
|
+
const stateType = issue?.state?.type ?? "";
|
|
252
|
+
const isStarted = stateType === "started";
|
|
253
|
+
|
|
254
|
+
if (!isStarted) {
|
|
255
|
+
const constraint = [
|
|
256
|
+
"CRITICAL CONSTRAINT — PLANNING MODE ONLY:",
|
|
257
|
+
`This issue (${session.issueIdentifier}) is in "${issue?.state?.name ?? stateType}" state — NOT In Progress.`,
|
|
258
|
+
"You may ONLY:",
|
|
259
|
+
"- Read and explore files to understand the codebase",
|
|
260
|
+
"- Write plan files (PLAN.md, notes, design outlines)",
|
|
261
|
+
"- Search code to inform planning",
|
|
262
|
+
"You MUST NOT:",
|
|
263
|
+
"- Create, modify, or delete source code, config, or infrastructure files",
|
|
264
|
+
"- Run system commands that change state (deploys, installs, migrations)",
|
|
265
|
+
"- Make external API requests that modify data",
|
|
266
|
+
"- Build, implement, or scaffold any application code",
|
|
267
|
+
"Plan and explore ONLY. Do not implement anything.",
|
|
268
|
+
"---",
|
|
269
|
+
].join("\n");
|
|
270
|
+
|
|
271
|
+
const originalPrompt = event.params?.prompt ?? "";
|
|
272
|
+
return {
|
|
273
|
+
params: { ...event.params, prompt: `${constraint}\n${originalPrompt}` },
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
} catch (err) {
|
|
277
|
+
api.logger.warn(`before_tool_call: issue state check failed: ${err}`);
|
|
278
|
+
// Don't block on failure — fall through to allow
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
232
282
|
// Narration Guard: catch short "Let me explore..." responses that narrate intent
|
|
233
283
|
// without actually calling tools, and append a warning for the user.
|
|
234
284
|
const NARRATION_PATTERNS = [
|
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-22T03:
|
|
5
|
+
* Last recorded: 2026-02-22T03:40:54.418Z
|
|
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": "933fa3c0-981a-4d9e-8b2c-7c5c4d5b4349",
|
|
48
|
+
"identifier": "UAT-438"
|
|
49
49
|
},
|
|
50
50
|
"createSubIssue1": {
|
|
51
|
-
"id": "
|
|
52
|
-
"identifier": "UAT-
|
|
51
|
+
"id": "4caa7593-5a51-4795-b98c-29e532589dfe",
|
|
52
|
+
"identifier": "UAT-439"
|
|
53
53
|
},
|
|
54
54
|
"createSubIssue2": {
|
|
55
|
-
"id": "
|
|
56
|
-
"identifier": "UAT-
|
|
55
|
+
"id": "0519065b-95eb-4752-966b-2099bbc5f3d1",
|
|
56
|
+
"identifier": "UAT-440"
|
|
57
57
|
},
|
|
58
58
|
"subIssue1Details": {
|
|
59
|
-
"id": "
|
|
60
|
-
"identifier": "UAT-
|
|
59
|
+
"id": "4caa7593-5a51-4795-b98c-29e532589dfe",
|
|
60
|
+
"identifier": "UAT-439",
|
|
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": "933fa3c0-981a-4d9e-8b2c-7c5c4d5b4349",
|
|
87
|
+
"identifier": "UAT-438"
|
|
88
88
|
},
|
|
89
89
|
"relations": {
|
|
90
90
|
"nodes": []
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
93
|
"subIssue2Details": {
|
|
94
|
-
"id": "
|
|
95
|
-
"identifier": "UAT-
|
|
94
|
+
"id": "0519065b-95eb-4752-966b-2099bbc5f3d1",
|
|
95
|
+
"identifier": "UAT-440",
|
|
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": "933fa3c0-981a-4d9e-8b2c-7c5c4d5b4349",
|
|
122
|
+
"identifier": "UAT-438"
|
|
123
123
|
},
|
|
124
124
|
"relations": {
|
|
125
125
|
"nodes": []
|
|
126
126
|
}
|
|
127
127
|
},
|
|
128
128
|
"parentDetails": {
|
|
129
|
-
"id": "
|
|
130
|
-
"identifier": "UAT-
|
|
129
|
+
"id": "933fa3c0-981a-4d9e-8b2c-7c5c4d5b4349",
|
|
130
|
+
"identifier": "UAT-438",
|
|
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-22T03:
|
|
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:40:52.229Z",
|
|
133
133
|
"estimate": null,
|
|
134
134
|
"state": {
|
|
135
135
|
"name": "Backlog",
|
|
@@ -149,7 +149,13 @@ export const RECORDED = {
|
|
|
149
149
|
"issueEstimationType": "tShirt"
|
|
150
150
|
},
|
|
151
151
|
"comments": {
|
|
152
|
-
"nodes": [
|
|
152
|
+
"nodes": [
|
|
153
|
+
{
|
|
154
|
+
"body": "This thread is for an agent session with ctclaw.",
|
|
155
|
+
"user": null,
|
|
156
|
+
"createdAt": "2026-02-22T03:40:53.165Z"
|
|
157
|
+
}
|
|
158
|
+
]
|
|
153
159
|
},
|
|
154
160
|
"project": null,
|
|
155
161
|
"parent": null,
|
|
@@ -158,11 +164,11 @@ export const RECORDED = {
|
|
|
158
164
|
}
|
|
159
165
|
},
|
|
160
166
|
"createRelation": {
|
|
161
|
-
"id": "
|
|
167
|
+
"id": "185dfd6c-362e-48a4-b717-e900407ced84"
|
|
162
168
|
},
|
|
163
169
|
"subIssue1WithRelation": {
|
|
164
|
-
"id": "
|
|
165
|
-
"identifier": "UAT-
|
|
170
|
+
"id": "4caa7593-5a51-4795-b98c-29e532589dfe",
|
|
171
|
+
"identifier": "UAT-439",
|
|
166
172
|
"title": "[SMOKE TEST] Sub-Issue 1: Backend API",
|
|
167
173
|
"description": "Implement the backend search API endpoint.\n\nGiven a search query, when the API is called, then matching results are returned.",
|
|
168
174
|
"estimate": 2,
|
|
@@ -188,22 +194,22 @@ export const RECORDED = {
|
|
|
188
194
|
{
|
|
189
195
|
"body": "This thread is for an agent session with ctclaw.",
|
|
190
196
|
"user": null,
|
|
191
|
-
"createdAt": "2026-02-22T03:
|
|
197
|
+
"createdAt": "2026-02-22T03:40:53.603Z"
|
|
192
198
|
}
|
|
193
199
|
]
|
|
194
200
|
},
|
|
195
201
|
"project": null,
|
|
196
202
|
"parent": {
|
|
197
|
-
"id": "
|
|
198
|
-
"identifier": "UAT-
|
|
203
|
+
"id": "933fa3c0-981a-4d9e-8b2c-7c5c4d5b4349",
|
|
204
|
+
"identifier": "UAT-438"
|
|
199
205
|
},
|
|
200
206
|
"relations": {
|
|
201
207
|
"nodes": [
|
|
202
208
|
{
|
|
203
209
|
"type": "blocks",
|
|
204
210
|
"relatedIssue": {
|
|
205
|
-
"id": "
|
|
206
|
-
"identifier": "UAT-
|
|
211
|
+
"id": "0519065b-95eb-4752-966b-2099bbc5f3d1",
|
|
212
|
+
"identifier": "UAT-440",
|
|
207
213
|
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI"
|
|
208
214
|
}
|
|
209
215
|
}
|
|
@@ -211,8 +217,8 @@ export const RECORDED = {
|
|
|
211
217
|
}
|
|
212
218
|
},
|
|
213
219
|
"subIssue2WithRelation": {
|
|
214
|
-
"id": "
|
|
215
|
-
"identifier": "UAT-
|
|
220
|
+
"id": "0519065b-95eb-4752-966b-2099bbc5f3d1",
|
|
221
|
+
"identifier": "UAT-440",
|
|
216
222
|
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
217
223
|
"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.",
|
|
218
224
|
"estimate": 3,
|
|
@@ -238,14 +244,14 @@ export const RECORDED = {
|
|
|
238
244
|
{
|
|
239
245
|
"body": "This thread is for an agent session with ctclaw.",
|
|
240
246
|
"user": null,
|
|
241
|
-
"createdAt": "2026-02-22T03:
|
|
247
|
+
"createdAt": "2026-02-22T03:40:53.840Z"
|
|
242
248
|
}
|
|
243
249
|
]
|
|
244
250
|
},
|
|
245
251
|
"project": null,
|
|
246
252
|
"parent": {
|
|
247
|
-
"id": "
|
|
248
|
-
"identifier": "UAT-
|
|
253
|
+
"id": "933fa3c0-981a-4d9e-8b2c-7c5c4d5b4349",
|
|
254
|
+
"identifier": "UAT-438"
|
|
249
255
|
},
|
|
250
256
|
"relations": {
|
|
251
257
|
"nodes": []
|
package/src/agent/agent.ts
CHANGED
|
@@ -323,7 +323,7 @@ async function runEmbedded(
|
|
|
323
323
|
}
|
|
324
324
|
},
|
|
325
325
|
|
|
326
|
-
// Raw agent events — capture tool starts/ends
|
|
326
|
+
// Raw agent events — capture tool starts/ends/updates
|
|
327
327
|
onAgentEvent: (evt) => {
|
|
328
328
|
watchdog.tick();
|
|
329
329
|
const { stream, data } = evt;
|
|
@@ -333,16 +333,30 @@ async function runEmbedded(
|
|
|
333
333
|
const phase = String(data.phase ?? "");
|
|
334
334
|
const toolName = String(data.name ?? "tool");
|
|
335
335
|
const meta = typeof data.meta === "string" ? data.meta : "";
|
|
336
|
+
const input = typeof data.input === "string" ? data.input : "";
|
|
336
337
|
|
|
337
|
-
// Tool execution start — emit action with tool name +
|
|
338
|
+
// Tool execution start — emit action with tool name + available context
|
|
338
339
|
if (phase === "start") {
|
|
339
340
|
lastToolAction = toolName;
|
|
340
|
-
|
|
341
|
+
const detail = input || meta || toolName;
|
|
342
|
+
emit({ type: "action", action: `Running ${toolName}`, parameter: detail.slice(0, 300) });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Tool execution update — partial progress (keeps Linear UI alive for long tools)
|
|
346
|
+
if (phase === "update") {
|
|
347
|
+
const detail = meta || input || "in progress";
|
|
348
|
+
emit({ type: "action", action: `${toolName}`, parameter: detail.slice(0, 300) });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Tool execution completed successfully
|
|
352
|
+
if (phase === "result" && !data.isError) {
|
|
353
|
+
const detail = meta ? meta.slice(0, 300) : "completed";
|
|
354
|
+
emit({ type: "action", action: `${toolName} done`, parameter: detail });
|
|
341
355
|
}
|
|
342
356
|
|
|
343
357
|
// Tool execution result with error
|
|
344
358
|
if (phase === "result" && data.isError) {
|
|
345
|
-
emit({ type: "action", action: `${toolName} failed`, parameter: meta.slice(0,
|
|
359
|
+
emit({ type: "action", action: `${toolName} failed`, parameter: (meta || "error").slice(0, 300) });
|
|
346
360
|
}
|
|
347
361
|
},
|
|
348
362
|
|