@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calltelemetry/openclaw-linear",
3
- "version": "0.9.11",
3
+ "version": "0.9.14",
4
4
  "description": "Linear Agent plugin for OpenClaw — webhook-driven AI pipeline with OAuth, multi-agent routing, and issue triage",
5
5
  "type": "module",
6
6
  "license": "MIT",
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. Plan your approach
33
- 4. Implement the solution in the worktree
34
- 5. Run tests to verify your changes
35
- 6. 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
36
- 7. Commit your work with a clear commit message
37
- 8. Return a text summary: what you changed, what tests passed, any assumptions you made, and any open questions
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-22T02:38:53.901Z
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": "953324f8-878f-40ba-b773-8d8815ee7987",
48
- "identifier": "UAT-320"
47
+ "id": "933fa3c0-981a-4d9e-8b2c-7c5c4d5b4349",
48
+ "identifier": "UAT-438"
49
49
  },
50
50
  "createSubIssue1": {
51
- "id": "69ce9a01-3bdb-4f4a-a882-e28e23d1b628",
52
- "identifier": "UAT-321"
51
+ "id": "4caa7593-5a51-4795-b98c-29e532589dfe",
52
+ "identifier": "UAT-439"
53
53
  },
54
54
  "createSubIssue2": {
55
- "id": "b39cc5eb-ec05-44aa-917b-562a290ecab6",
56
- "identifier": "UAT-322"
55
+ "id": "0519065b-95eb-4752-966b-2099bbc5f3d1",
56
+ "identifier": "UAT-440"
57
57
  },
58
58
  "subIssue1Details": {
59
- "id": "69ce9a01-3bdb-4f4a-a882-e28e23d1b628",
60
- "identifier": "UAT-321",
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": "953324f8-878f-40ba-b773-8d8815ee7987",
87
- "identifier": "UAT-320"
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": "b39cc5eb-ec05-44aa-917b-562a290ecab6",
95
- "identifier": "UAT-322",
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": "953324f8-878f-40ba-b773-8d8815ee7987",
122
- "identifier": "UAT-320"
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": "953324f8-878f-40ba-b773-8d8815ee7987",
130
- "identifier": "UAT-320",
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-22T02:38:52.132Z",
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": "9207d26d-d65d-45a2-ac76-b9233acea181"
167
+ "id": "185dfd6c-362e-48a4-b717-e900407ced84"
162
168
  },
163
169
  "subIssue1WithRelation": {
164
- "id": "69ce9a01-3bdb-4f4a-a882-e28e23d1b628",
165
- "identifier": "UAT-321",
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,
@@ -184,20 +190,26 @@ export const RECORDED = {
184
190
  "issueEstimationType": "tShirt"
185
191
  },
186
192
  "comments": {
187
- "nodes": []
193
+ "nodes": [
194
+ {
195
+ "body": "This thread is for an agent session with ctclaw.",
196
+ "user": null,
197
+ "createdAt": "2026-02-22T03:40:53.603Z"
198
+ }
199
+ ]
188
200
  },
189
201
  "project": null,
190
202
  "parent": {
191
- "id": "953324f8-878f-40ba-b773-8d8815ee7987",
192
- "identifier": "UAT-320"
203
+ "id": "933fa3c0-981a-4d9e-8b2c-7c5c4d5b4349",
204
+ "identifier": "UAT-438"
193
205
  },
194
206
  "relations": {
195
207
  "nodes": [
196
208
  {
197
209
  "type": "blocks",
198
210
  "relatedIssue": {
199
- "id": "b39cc5eb-ec05-44aa-917b-562a290ecab6",
200
- "identifier": "UAT-322",
211
+ "id": "0519065b-95eb-4752-966b-2099bbc5f3d1",
212
+ "identifier": "UAT-440",
201
213
  "title": "[SMOKE TEST] Sub-Issue 2: Frontend UI"
202
214
  }
203
215
  }
@@ -205,8 +217,8 @@ export const RECORDED = {
205
217
  }
206
218
  },
207
219
  "subIssue2WithRelation": {
208
- "id": "b39cc5eb-ec05-44aa-917b-562a290ecab6",
209
- "identifier": "UAT-322",
220
+ "id": "0519065b-95eb-4752-966b-2099bbc5f3d1",
221
+ "identifier": "UAT-440",
210
222
  "title": "[SMOKE TEST] Sub-Issue 2: Frontend UI",
211
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.",
212
224
  "estimate": 3,
@@ -232,14 +244,14 @@ export const RECORDED = {
232
244
  {
233
245
  "body": "This thread is for an agent session with ctclaw.",
234
246
  "user": null,
235
- "createdAt": "2026-02-22T02:38:53.699Z"
247
+ "createdAt": "2026-02-22T03:40:53.840Z"
236
248
  }
237
249
  ]
238
250
  },
239
251
  "project": null,
240
252
  "parent": {
241
- "id": "953324f8-878f-40ba-b773-8d8815ee7987",
242
- "identifier": "UAT-320"
253
+ "id": "933fa3c0-981a-4d9e-8b2c-7c5c4d5b4349",
254
+ "identifier": "UAT-438"
243
255
  },
244
256
  "relations": {
245
257
  "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
  });
@@ -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 + meta
338
+ // Tool execution start — emit action with tool name + available context
338
339
  if (phase === "start") {
339
340
  lastToolAction = toolName;
340
- emit({ type: "action", action: `Running ${toolName}`, parameter: meta.slice(0, 200) || toolName });
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, 200) || "error" });
359
+ emit({ type: "action", action: `${toolName} failed`, parameter: (meta || "error").slice(0, 300) });
346
360
  }
347
361
  },
348
362