@calltelemetry/openclaw-linear 0.8.8 → 0.9.0

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.
Files changed (36) hide show
  1. package/README.md +194 -91
  2. package/index.ts +36 -4
  3. package/package.json +1 -1
  4. package/src/__test__/webhook-scenarios.test.ts +1 -1
  5. package/src/gateway/dispatch-methods.test.ts +9 -9
  6. package/src/infra/commands.test.ts +5 -5
  7. package/src/infra/config-paths.test.ts +246 -0
  8. package/src/infra/doctor.ts +45 -36
  9. package/src/infra/notify.test.ts +49 -0
  10. package/src/infra/notify.ts +7 -2
  11. package/src/infra/observability.ts +1 -0
  12. package/src/infra/shared-profiles.test.ts +262 -0
  13. package/src/infra/shared-profiles.ts +116 -0
  14. package/src/infra/template.test.ts +86 -0
  15. package/src/infra/template.ts +18 -0
  16. package/src/infra/validation.test.ts +175 -0
  17. package/src/infra/validation.ts +52 -0
  18. package/src/pipeline/active-session.test.ts +2 -2
  19. package/src/pipeline/agent-end-hook.test.ts +305 -0
  20. package/src/pipeline/artifacts.test.ts +3 -3
  21. package/src/pipeline/dispatch-state.test.ts +111 -8
  22. package/src/pipeline/dispatch-state.ts +48 -13
  23. package/src/pipeline/e2e-dispatch.test.ts +2 -2
  24. package/src/pipeline/intent-classify.test.ts +20 -2
  25. package/src/pipeline/intent-classify.ts +14 -24
  26. package/src/pipeline/pipeline.ts +28 -11
  27. package/src/pipeline/planner.ts +1 -8
  28. package/src/pipeline/planning-state.ts +9 -0
  29. package/src/pipeline/tier-assess.test.ts +39 -39
  30. package/src/pipeline/tier-assess.ts +15 -33
  31. package/src/pipeline/webhook.test.ts +149 -1
  32. package/src/pipeline/webhook.ts +90 -62
  33. package/src/tools/dispatch-history-tool.test.ts +21 -20
  34. package/src/tools/dispatch-history-tool.ts +1 -1
  35. package/src/tools/linear-issues-tool.test.ts +115 -0
  36. package/src/tools/linear-issues-tool.ts +25 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * config-paths.test.ts — Tests for configurable CLI paths, dedup TTLs,
3
+ * and enhanced diagnostic context fields.
4
+ */
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
6
+ import { join } from "node:path";
7
+ import { homedir } from "node:os";
8
+
9
+ // ── Mocks for webhook.ts imports ──────────────────────────────────
10
+
11
+ vi.mock("../pipeline/pipeline.js", () => ({
12
+ runPlannerStage: vi.fn().mockResolvedValue("mock plan"),
13
+ runFullPipeline: vi.fn().mockResolvedValue(undefined),
14
+ resumePipeline: vi.fn().mockResolvedValue(undefined),
15
+ spawnWorker: vi.fn().mockResolvedValue(undefined),
16
+ }));
17
+
18
+ vi.mock("../api/linear-api.js", () => ({
19
+ LinearAgentApi: class MockLinearAgentApi {
20
+ emitActivity = vi.fn().mockResolvedValue(undefined);
21
+ createComment = vi.fn().mockResolvedValue("comment-id");
22
+ getIssueDetails = vi.fn().mockResolvedValue(null);
23
+ getViewerId = vi.fn().mockResolvedValue("viewer-bot-1");
24
+ createSessionOnIssue = vi.fn().mockResolvedValue({ sessionId: null });
25
+ getTeamLabels = vi.fn().mockResolvedValue([]);
26
+ },
27
+ resolveLinearToken: vi.fn().mockReturnValue({
28
+ accessToken: "test-token",
29
+ source: "env",
30
+ }),
31
+ }));
32
+
33
+ vi.mock("../pipeline/active-session.js", () => ({
34
+ setActiveSession: vi.fn(),
35
+ clearActiveSession: vi.fn(),
36
+ }));
37
+
38
+ vi.mock("../infra/observability.js", () => ({
39
+ emitDiagnostic: vi.fn(),
40
+ }));
41
+
42
+ vi.mock("../pipeline/intent-classify.js", () => ({
43
+ classifyIntent: vi.fn().mockResolvedValue({
44
+ intent: "general",
45
+ reasoning: "test",
46
+ fromFallback: true,
47
+ }),
48
+ }));
49
+
50
+ // ── Task 1: CLI paths use HOME env var ───────────────────────────
51
+
52
+ describe("CLI binary path resolution", () => {
53
+ it("constructs default bin paths from HOME env var", () => {
54
+ const origHome = process.env.HOME;
55
+ try {
56
+ process.env.HOME = "/test/custom-home";
57
+ const defaultBinDir = join(process.env.HOME ?? homedir(), ".npm-global", "bin");
58
+ expect(defaultBinDir).toBe("/test/custom-home/.npm-global/bin");
59
+ expect(join(defaultBinDir, "codex")).toBe("/test/custom-home/.npm-global/bin/codex");
60
+ expect(join(defaultBinDir, "claude")).toBe("/test/custom-home/.npm-global/bin/claude");
61
+ expect(join(defaultBinDir, "gemini")).toBe("/test/custom-home/.npm-global/bin/gemini");
62
+ } finally {
63
+ if (origHome !== undefined) process.env.HOME = origHome;
64
+ else delete process.env.HOME;
65
+ }
66
+ });
67
+
68
+ it("falls back to os.homedir() when HOME is unset", () => {
69
+ const origHome = process.env.HOME;
70
+ try {
71
+ delete process.env.HOME;
72
+ const defaultBinDir = join(process.env.HOME ?? homedir(), ".npm-global", "bin");
73
+ // Should use homedir() which returns the system home directory
74
+ expect(defaultBinDir).toContain(".npm-global/bin");
75
+ expect(defaultBinDir).not.toContain("undefined");
76
+ } finally {
77
+ if (origHome !== undefined) process.env.HOME = origHome;
78
+ else delete process.env.HOME;
79
+ }
80
+ });
81
+
82
+ it("pluginConfig overrides take precedence over default paths", () => {
83
+ const pluginConfig: Record<string, unknown> = {
84
+ codexBin: "/custom/path/codex",
85
+ claudeBin: "/custom/path/claude",
86
+ geminiBin: "/custom/path/gemini",
87
+ };
88
+ const defaultBinDir = join(process.env.HOME ?? homedir(), ".npm-global", "bin");
89
+
90
+ const codexBin = pluginConfig?.codexBin as string ?? join(defaultBinDir, "codex");
91
+ const claudeBin = pluginConfig?.claudeBin as string ?? join(defaultBinDir, "claude");
92
+ const geminiBin = pluginConfig?.geminiBin as string ?? join(defaultBinDir, "gemini");
93
+
94
+ expect(codexBin).toBe("/custom/path/codex");
95
+ expect(claudeBin).toBe("/custom/path/claude");
96
+ expect(geminiBin).toBe("/custom/path/gemini");
97
+ });
98
+
99
+ it("uses default when pluginConfig has no overrides", () => {
100
+ const pluginConfig: Record<string, unknown> = {};
101
+ const defaultBinDir = join(process.env.HOME ?? homedir(), ".npm-global", "bin");
102
+
103
+ const codexBin = pluginConfig?.codexBin as string ?? join(defaultBinDir, "codex");
104
+ expect(codexBin).toBe(join(defaultBinDir, "codex"));
105
+ });
106
+ });
107
+
108
+ // ── Task 3: Diagnostic events include extra context fields ───────
109
+
110
+ describe("diagnostic event context fields", () => {
111
+ // Use the real observability module (not mocked for these tests)
112
+ // We import and test directly since the mock only applies to webhook.ts imports
113
+ it("DiagnosticPayload accepts agentId field", async () => {
114
+ // Bypass the mock by importing the actual module implementation
115
+ const info = vi.fn();
116
+ const api = { logger: { info } } as any;
117
+ const PREFIX = "[linear:diagnostic]";
118
+
119
+ // Simulate emitDiagnostic behavior (same as the real function)
120
+ const payload = {
121
+ event: "dispatch_started",
122
+ identifier: "ISS-42",
123
+ agentId: "mason",
124
+ tier: "standard",
125
+ issueId: "abc-123",
126
+ };
127
+ api.logger.info(`${PREFIX} ${JSON.stringify(payload)}`);
128
+
129
+ expect(info).toHaveBeenCalledOnce();
130
+ const json = JSON.parse((info.mock.calls[0][0] as string).replace("[linear:diagnostic] ", ""));
131
+ expect(json.agentId).toBe("mason");
132
+ expect(json.identifier).toBe("ISS-42");
133
+ expect(json.tier).toBe("standard");
134
+ expect(json.issueId).toBe("abc-123");
135
+ });
136
+
137
+ it("DiagnosticPayload accepts durationMs field", () => {
138
+ const info = vi.fn();
139
+ const api = { logger: { info } } as any;
140
+ const PREFIX = "[linear:diagnostic]";
141
+
142
+ const payload = {
143
+ event: "watchdog_kill",
144
+ identifier: "ISS-99",
145
+ durationMs: 45000,
146
+ agentId: "forge",
147
+ attempt: 2,
148
+ };
149
+ api.logger.info(`${PREFIX} ${JSON.stringify(payload)}`);
150
+
151
+ const json = JSON.parse((info.mock.calls[0][0] as string).replace("[linear:diagnostic] ", ""));
152
+ expect(json.durationMs).toBe(45000);
153
+ expect(json.agentId).toBe("forge");
154
+ expect(json.attempt).toBe(2);
155
+ });
156
+
157
+ it("webhook_received diagnostic includes identifier and issueId", () => {
158
+ const info = vi.fn();
159
+ const api = { logger: { info } } as any;
160
+ const PREFIX = "[linear:diagnostic]";
161
+
162
+ const payload = {
163
+ event: "webhook_received",
164
+ webhookType: "Comment",
165
+ webhookAction: "create",
166
+ identifier: "ENG-123",
167
+ issueId: "issue-abc",
168
+ };
169
+ api.logger.info(`${PREFIX} ${JSON.stringify(payload)}`);
170
+
171
+ const json = JSON.parse((info.mock.calls[0][0] as string).replace("[linear:diagnostic] ", ""));
172
+ expect(json.webhookType).toBe("Comment");
173
+ expect(json.webhookAction).toBe("create");
174
+ expect(json.identifier).toBe("ENG-123");
175
+ expect(json.issueId).toBe("issue-abc");
176
+ });
177
+
178
+ it("verdict_processed diagnostic includes tier and agentId", () => {
179
+ const info = vi.fn();
180
+ const api = { logger: { info } } as any;
181
+ const PREFIX = "[linear:diagnostic]";
182
+
183
+ const payload = {
184
+ event: "verdict_processed",
185
+ identifier: "ISS-55",
186
+ issueId: "id-55",
187
+ phase: "done",
188
+ attempt: 1,
189
+ tier: "complex",
190
+ agentId: "eureka",
191
+ };
192
+ api.logger.info(`${PREFIX} ${JSON.stringify(payload)}`);
193
+
194
+ const json = JSON.parse((info.mock.calls[0][0] as string).replace("[linear:diagnostic] ", ""));
195
+ expect(json.tier).toBe("complex");
196
+ expect(json.agentId).toBe("eureka");
197
+ expect(json.phase).toBe("done");
198
+ });
199
+ });
200
+
201
+ // ── Task 4: Configurable dedup TTL ───────────────────────────────
202
+
203
+ describe("configurable dedup TTL", () => {
204
+ beforeEach(async () => {
205
+ const { _resetForTesting } = await import("../pipeline/webhook.js");
206
+ _resetForTesting();
207
+ });
208
+
209
+ afterEach(async () => {
210
+ const { _resetForTesting } = await import("../pipeline/webhook.js");
211
+ _resetForTesting();
212
+ });
213
+
214
+ it("defaults to 60_000ms when no config provided", async () => {
215
+ const { _configureDedupTtls, _getDedupTtlMs } = await import("../pipeline/webhook.js");
216
+ _configureDedupTtls();
217
+ expect(_getDedupTtlMs()).toBe(60_000);
218
+ });
219
+
220
+ it("defaults to 60_000ms when pluginConfig has no dedupTtlMs", async () => {
221
+ const { _configureDedupTtls, _getDedupTtlMs } = await import("../pipeline/webhook.js");
222
+ _configureDedupTtls({});
223
+ expect(_getDedupTtlMs()).toBe(60_000);
224
+ });
225
+
226
+ it("reads dedupTtlMs from pluginConfig", async () => {
227
+ const { _configureDedupTtls, _getDedupTtlMs } = await import("../pipeline/webhook.js");
228
+ _configureDedupTtls({ dedupTtlMs: 120_000 });
229
+ expect(_getDedupTtlMs()).toBe(120_000);
230
+ });
231
+
232
+ it("reads dedupSweepIntervalMs from pluginConfig", async () => {
233
+ const { _configureDedupTtls, _getDedupTtlMs } = await import("../pipeline/webhook.js");
234
+ _configureDedupTtls({ dedupTtlMs: 30_000, dedupSweepIntervalMs: 5_000 });
235
+ expect(_getDedupTtlMs()).toBe(30_000);
236
+ });
237
+
238
+ it("_resetForTesting restores default TTLs", async () => {
239
+ const { _configureDedupTtls, _getDedupTtlMs, _resetForTesting } = await import("../pipeline/webhook.js");
240
+ _configureDedupTtls({ dedupTtlMs: 999 });
241
+ expect(_getDedupTtlMs()).toBe(999);
242
+
243
+ _resetForTesting();
244
+ expect(_getDedupTtlMs()).toBe(60_000);
245
+ });
246
+ });
@@ -52,11 +52,15 @@ export interface DoctorOptions {
52
52
 
53
53
  const AGENT_PROFILES_PATH = join(homedir(), ".openclaw", "agent-profiles.json");
54
54
  const VALID_BACKENDS: readonly string[] = ["claude", "codex", "gemini"];
55
- const CLI_BINS: [string, string][] = [
56
- ["codex", "/home/claw/.npm-global/bin/codex"],
57
- ["claude", "/home/claw/.npm-global/bin/claude"],
58
- ["gemini", "/home/claw/.npm-global/bin/gemini"],
59
- ];
55
+ const DEFAULT_BIN_DIR = join(process.env.HOME ?? homedir(), ".npm-global", "bin");
56
+
57
+ function resolveCliBins(pluginConfig?: Record<string, unknown>): [string, string][] {
58
+ return [
59
+ ["codex", (pluginConfig?.codexBin as string) ?? join(DEFAULT_BIN_DIR, "codex")],
60
+ ["claude", (pluginConfig?.claudeBin as string) ?? join(DEFAULT_BIN_DIR, "claude")],
61
+ ["gemini", (pluginConfig?.geminiBin as string) ?? join(DEFAULT_BIN_DIR, "gemini")],
62
+ ];
63
+ }
60
64
  const STALE_DISPATCH_MS = 2 * 60 * 60_000; // 2 hours
61
65
  const OLD_COMPLETED_MS = 7 * 24 * 60 * 60_000; // 7 days
62
66
  const LOCK_STALE_MS = 30_000; // 30 seconds
@@ -92,7 +96,7 @@ function resolveWorktreeBaseDir(pluginConfig?: Record<string, unknown>): string
92
96
  }
93
97
 
94
98
  function resolveBaseRepo(pluginConfig?: Record<string, unknown>): string {
95
- return (pluginConfig?.codexBaseRepo as string) ?? "/home/claw/ai-workspace";
99
+ return (pluginConfig?.codexBaseRepo as string) ?? join(process.env.HOME ?? homedir(), "ai-workspace");
96
100
  }
97
101
 
98
102
  interface AgentProfile {
@@ -315,7 +319,7 @@ export function checkAgentConfig(pluginConfig?: Record<string, unknown>): CheckR
315
319
  // Section 3: Coding Tools
316
320
  // ---------------------------------------------------------------------------
317
321
 
318
- export function checkCodingTools(): CheckResult[] {
322
+ export function checkCodingTools(pluginConfig?: Record<string, unknown>): CheckResult[] {
319
323
  const checks: CheckResult[] = [];
320
324
 
321
325
  // Load config
@@ -345,7 +349,8 @@ export function checkCodingTools(): CheckResult[] {
345
349
  }
346
350
 
347
351
  // CLI availability
348
- for (const [name, bin] of CLI_BINS) {
352
+ const cliBins = resolveCliBins(pluginConfig);
353
+ for (const [name, bin] of cliBins) {
349
354
  try {
350
355
  const raw = execFileSync(bin, ["--version"], {
351
356
  encoding: "utf8",
@@ -728,7 +733,7 @@ export async function runDoctor(opts: DoctorOptions): Promise<DoctorReport> {
728
733
  sections.push({ name: "Agent Configuration", checks: checkAgentConfig(opts.pluginConfig) });
729
734
 
730
735
  // 3. Coding tools
731
- sections.push({ name: "Coding Tools", checks: checkCodingTools() });
736
+ sections.push({ name: "Coding Tools", checks: checkCodingTools(opts.pluginConfig) });
732
737
 
733
738
  // 4. Files & dirs
734
739
  sections.push({
@@ -788,31 +793,34 @@ interface BackendSpec {
788
793
  unsetEnv?: string[];
789
794
  }
790
795
 
791
- const BACKEND_SPECS: BackendSpec[] = [
792
- {
793
- id: "claude",
794
- label: "Claude Code (Anthropic)",
795
- bin: "/home/claw/.npm-global/bin/claude",
796
- testArgs: ["--print", "-p", "Respond with the single word hello", "--output-format", "stream-json", "--max-turns", "1", "--dangerously-skip-permissions"],
797
- envKeys: ["ANTHROPIC_API_KEY"],
798
- configKey: "claudeApiKey",
799
- unsetEnv: ["CLAUDECODE"],
800
- },
801
- {
802
- id: "codex",
803
- label: "Codex (OpenAI)",
804
- bin: "/home/claw/.npm-global/bin/codex",
805
- testArgs: ["exec", "--json", "--ephemeral", "--full-auto", "echo hello"],
806
- envKeys: ["OPENAI_API_KEY"],
807
- },
808
- {
809
- id: "gemini",
810
- label: "Gemini CLI (Google)",
811
- bin: "/home/claw/.npm-global/bin/gemini",
812
- testArgs: ["-p", "Respond with the single word hello", "-o", "stream-json", "--yolo"],
813
- envKeys: ["GEMINI_API_KEY", "GOOGLE_API_KEY", "GOOGLE_GENAI_API_KEY"],
814
- },
815
- ];
796
+ function resolveBackendSpecs(pluginConfig?: Record<string, unknown>): BackendSpec[] {
797
+ const binDir = join(process.env.HOME ?? homedir(), ".npm-global", "bin");
798
+ return [
799
+ {
800
+ id: "claude",
801
+ label: "Claude Code (Anthropic)",
802
+ bin: (pluginConfig?.claudeBin as string) ?? join(binDir, "claude"),
803
+ testArgs: ["--print", "-p", "Respond with the single word hello", "--output-format", "stream-json", "--max-turns", "1", "--dangerously-skip-permissions"],
804
+ envKeys: ["ANTHROPIC_API_KEY"],
805
+ configKey: "claudeApiKey",
806
+ unsetEnv: ["CLAUDECODE"],
807
+ },
808
+ {
809
+ id: "codex",
810
+ label: "Codex (OpenAI)",
811
+ bin: (pluginConfig?.codexBin as string) ?? join(binDir, "codex"),
812
+ testArgs: ["exec", "--json", "--ephemeral", "--full-auto", "echo hello"],
813
+ envKeys: ["OPENAI_API_KEY"],
814
+ },
815
+ {
816
+ id: "gemini",
817
+ label: "Gemini CLI (Google)",
818
+ bin: (pluginConfig?.geminiBin as string) ?? join(binDir, "gemini"),
819
+ testArgs: ["-p", "Respond with the single word hello", "-o", "stream-json", "--yolo"],
820
+ envKeys: ["GEMINI_API_KEY", "GOOGLE_API_KEY", "GOOGLE_GENAI_API_KEY"],
821
+ },
822
+ ];
823
+ }
816
824
 
817
825
  function checkBackendBinary(spec: BackendSpec): { installed: boolean; checks: CheckResult[] } {
818
826
  const checks: CheckResult[] = [];
@@ -929,8 +937,9 @@ export async function checkCodeRunDeep(
929
937
  const sections: CheckSection[] = [];
930
938
  const config = loadCodingConfig();
931
939
  let callableCount = 0;
940
+ const backendSpecs = resolveBackendSpecs(pluginConfig);
932
941
 
933
- for (const spec of BACKEND_SPECS) {
942
+ for (const spec of backendSpecs) {
934
943
  const checks: CheckResult[] = [];
935
944
 
936
945
  // 1. Binary check
@@ -966,7 +975,7 @@ export async function checkCodeRunDeep(
966
975
  ));
967
976
  }
968
977
 
969
- routingChecks.push(pass(`Callable backends: ${callableCount}/${BACKEND_SPECS.length}`));
978
+ routingChecks.push(pass(`Callable backends: ${callableCount}/${backendSpecs.length}`));
970
979
  sections.push({ name: "Code Run: Routing", checks: routingChecks });
971
980
 
972
981
  return sections;
@@ -371,6 +371,55 @@ describe("createNotifierFromConfig", () => {
371
371
  consoleSpy.mockRestore();
372
372
  });
373
373
 
374
+ it("sanitizes URLs and tokens from error messages", async () => {
375
+ const runtime = mockRuntime();
376
+ runtime.channel.discord.sendMessageDiscord = vi.fn(async () => {
377
+ throw new Error("Failed to POST https://discord.com/api/v10/channels/123/messages with token fake-slack-token-1234567890");
378
+ });
379
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
380
+
381
+ const notify = createNotifierFromConfig({
382
+ notifications: {
383
+ targets: [{ channel: "discord", target: "D-100" }],
384
+ },
385
+ }, runtime);
386
+ await notify("dispatch", basePayload);
387
+
388
+ // Check that the error message was sanitized
389
+ expect(consoleSpy).toHaveBeenCalledOnce();
390
+ const errorMessage = consoleSpy.mock.calls[0][0] as string;
391
+ expect(errorMessage).not.toContain("https://discord.com");
392
+ expect(errorMessage).not.toContain("xoxb-1234567890");
393
+ expect(errorMessage).toContain("[URL]");
394
+ expect(errorMessage).toContain("[TOKEN]");
395
+
396
+ consoleSpy.mockRestore();
397
+ });
398
+
399
+ it("does not leak long token-like strings in console error output", async () => {
400
+ const runtime = mockRuntime();
401
+ const fakeToken = "xoxb-ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
402
+ runtime.channel.discord.sendMessageDiscord = vi.fn(async () => {
403
+ throw new Error(`Auth failed with token ${fakeToken}`);
404
+ });
405
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
406
+
407
+ const notify = createNotifierFromConfig({
408
+ notifications: {
409
+ targets: [{ channel: "discord", target: "D-100" }],
410
+ },
411
+ }, runtime);
412
+ await notify("dispatch", basePayload);
413
+
414
+ expect(consoleSpy).toHaveBeenCalledOnce();
415
+ const errorMessage = consoleSpy.mock.calls[0][0] as string;
416
+ // The long token-like string should be replaced
417
+ expect(errorMessage).not.toContain(fakeToken);
418
+ expect(errorMessage).toContain("[TOKEN]");
419
+
420
+ consoleSpy.mockRestore();
421
+ });
422
+
374
423
  it("skips suppressed events", async () => {
375
424
  const runtime = mockRuntime();
376
425
  const notify = createNotifierFromConfig({
@@ -262,13 +262,18 @@ export function createNotifierFromConfig(
262
262
  try {
263
263
  await sendToTarget(target, message, runtime);
264
264
  } catch (err) {
265
- console.error(`Notify error (${target.channel}:${target.target}):`, err);
265
+ const safeError = err instanceof Error ? err.message : "Unknown error";
266
+ // Strip potential URLs/tokens from error messages to prevent secret leakage
267
+ const sanitizedError = safeError
268
+ .replace(/https?:\/\/[^\s]+/g, "[URL]")
269
+ .replace(/[A-Za-z0-9_-]{20,}/g, "[TOKEN]");
270
+ console.error(`Notify error (${target.channel}:${target.target}): ${sanitizedError}`);
266
271
  if (api) {
267
272
  emitDiagnostic(api, {
268
273
  event: "notify_failed",
269
274
  identifier: payload.identifier,
270
275
  phase: kind,
271
- error: String(err),
276
+ error: sanitizedError,
272
277
  });
273
278
  }
274
279
  }
@@ -23,6 +23,7 @@ export interface DiagnosticPayload {
23
23
  event: DiagnosticEvent;
24
24
  identifier?: string;
25
25
  issueId?: string;
26
+ agentId?: string;
26
27
  phase?: string;
27
28
  from?: string;
28
29
  to?: string;