@calltelemetry/openclaw-linear 0.9.12 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calltelemetry/openclaw-linear",
3
- "version": "0.9.12",
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",
@@ -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:20:08.396Z
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": "2becfd65-c313-4d4b-9362-b3ac431872c6",
48
- "identifier": "UAT-354"
47
+ "id": "933fa3c0-981a-4d9e-8b2c-7c5c4d5b4349",
48
+ "identifier": "UAT-438"
49
49
  },
50
50
  "createSubIssue1": {
51
- "id": "b7433275-83a2-4e6e-a98c-5fe2b3aa1147",
52
- "identifier": "UAT-355"
51
+ "id": "4caa7593-5a51-4795-b98c-29e532589dfe",
52
+ "identifier": "UAT-439"
53
53
  },
54
54
  "createSubIssue2": {
55
- "id": "3526cea7-2261-4eee-bd8a-5206e12c6124",
56
- "identifier": "UAT-356"
55
+ "id": "0519065b-95eb-4752-966b-2099bbc5f3d1",
56
+ "identifier": "UAT-440"
57
57
  },
58
58
  "subIssue1Details": {
59
- "id": "b7433275-83a2-4e6e-a98c-5fe2b3aa1147",
60
- "identifier": "UAT-355",
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": "2becfd65-c313-4d4b-9362-b3ac431872c6",
87
- "identifier": "UAT-354"
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": "3526cea7-2261-4eee-bd8a-5206e12c6124",
95
- "identifier": "UAT-356",
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": "2becfd65-c313-4d4b-9362-b3ac431872c6",
122
- "identifier": "UAT-354"
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": "2becfd65-c313-4d4b-9362-b3ac431872c6",
130
- "identifier": "UAT-354",
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:20:04.703Z",
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": "a9cac881-3d13-45ab-b000-855777698d71"
167
+ "id": "185dfd6c-362e-48a4-b717-e900407ced84"
162
168
  },
163
169
  "subIssue1WithRelation": {
164
- "id": "b7433275-83a2-4e6e-a98c-5fe2b3aa1147",
165
- "identifier": "UAT-355",
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:20:06.930Z"
197
+ "createdAt": "2026-02-22T03:40:53.603Z"
192
198
  }
193
199
  ]
194
200
  },
195
201
  "project": null,
196
202
  "parent": {
197
- "id": "2becfd65-c313-4d4b-9362-b3ac431872c6",
198
- "identifier": "UAT-354"
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": "3526cea7-2261-4eee-bd8a-5206e12c6124",
206
- "identifier": "UAT-356",
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": "3526cea7-2261-4eee-bd8a-5206e12c6124",
215
- "identifier": "UAT-356",
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:20:06.960Z"
247
+ "createdAt": "2026-02-22T03:40:53.840Z"
242
248
  }
243
249
  ]
244
250
  },
245
251
  "project": null,
246
252
  "parent": {
247
- "id": "2becfd65-c313-4d4b-9362-b3ac431872c6",
248
- "identifier": "UAT-354"
253
+ "id": "933fa3c0-981a-4d9e-8b2c-7c5c4d5b4349",
254
+ "identifier": "UAT-438"
249
255
  },
250
256
  "relations": {
251
257
  "nodes": []
@@ -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
 
@@ -1630,621 +1630,120 @@ describe("checkFilesAndDirs — dispatch state non-Error exception", () => {
1630
1630
  });
1631
1631
 
1632
1632
  // ---------------------------------------------------------------------------
1633
- // checkFilesAndDirs — lock file branches
1633
+ // checkFilesAndDirs — CLAUDE.md and AGENTS.md checks
1634
1634
  // ---------------------------------------------------------------------------
1635
1635
 
1636
- describe("checkFilesAndDirs — lock file branches", () => {
1636
+ describe("checkFilesAndDirs — CLAUDE.md and AGENTS.md", () => {
1637
1637
  let tmpDir: string;
1638
1638
 
1639
1639
  beforeEach(() => {
1640
- tmpDir = mkdtempSync(join(tmpdir(), "doctor-lock-"));
1640
+ tmpDir = mkdtempSync(join(tmpdir(), "doctor-md-"));
1641
+ const { execFileSync } = require("node:child_process");
1642
+ execFileSync("git", ["init"], { cwd: tmpDir, stdio: "ignore" });
1641
1643
  });
1642
1644
 
1643
- it("warns about stale lock file without --fix", async () => {
1644
- const statePath = join(tmpDir, "state.json");
1645
- const lockPath = statePath + ".lock";
1646
- writeFileSync(statePath, '{}');
1647
- writeFileSync(lockPath, "locked");
1648
- // Make the lock file appear stale by backdating its mtime
1649
- const staleTime = Date.now() - 60_000; // 60 seconds old (> 30s LOCK_STALE_MS)
1650
- const { utimesSync } = await import("node:fs");
1651
- utimesSync(lockPath, staleTime / 1000, staleTime / 1000);
1652
-
1653
- vi.mocked(readDispatchState).mockResolvedValueOnce({
1654
- dispatches: { active: {}, completed: {} },
1655
- sessionMap: {},
1656
- processedEvents: [],
1657
- });
1658
-
1659
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath }, false);
1660
- const lockCheck = checks.find((c) => c.label.includes("Stale lock file"));
1661
- expect(lockCheck?.severity).toBe("warn");
1662
- expect(lockCheck?.fixable).toBe(true);
1645
+ it("passes when CLAUDE.md exists in base repo", async () => {
1646
+ writeFileSync(join(tmpDir, "CLAUDE.md"), "# My Project\n## Tech Stack\n- TypeScript");
1647
+ const checks = await checkFilesAndDirs({ codexBaseRepo: tmpDir });
1648
+ const claudeCheck = checks.find((c) => c.label.includes("CLAUDE.md found"));
1649
+ expect(claudeCheck).toBeDefined();
1650
+ expect(claudeCheck?.severity).toBe("pass");
1663
1651
  });
1664
1652
 
1665
- it("removes stale lock file with --fix", async () => {
1666
- const statePath = join(tmpDir, "state.json");
1667
- const lockPath = statePath + ".lock";
1668
- writeFileSync(statePath, '{}');
1669
- writeFileSync(lockPath, "locked");
1670
- const staleTime = Date.now() - 60_000;
1671
- const { utimesSync } = await import("node:fs");
1672
- utimesSync(lockPath, staleTime / 1000, staleTime / 1000);
1673
-
1674
- vi.mocked(readDispatchState).mockResolvedValueOnce({
1675
- dispatches: { active: {}, completed: {} },
1676
- sessionMap: {},
1677
- processedEvents: [],
1678
- });
1679
-
1680
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath }, true);
1681
- const lockCheck = checks.find((c) => c.label.includes("Stale lock file removed"));
1682
- expect(lockCheck?.severity).toBe("pass");
1683
- expect(existsSync(lockPath)).toBe(false);
1653
+ it("warns when CLAUDE.md is missing from base repo", async () => {
1654
+ const checks = await checkFilesAndDirs({ codexBaseRepo: tmpDir });
1655
+ const claudeCheck = checks.find((c) => c.label.includes("No CLAUDE.md"));
1656
+ expect(claudeCheck).toBeDefined();
1657
+ expect(claudeCheck?.severity).toBe("warn");
1658
+ expect(claudeCheck?.fix).toContain("CLAUDE.md");
1684
1659
  });
1685
1660
 
1686
- it("warns about active (non-stale) lock file", async () => {
1687
- const statePath = join(tmpDir, "state.json");
1688
- const lockPath = statePath + ".lock";
1689
- writeFileSync(statePath, '{}');
1690
- writeFileSync(lockPath, "locked");
1691
- // Lock file is fresh (just created), so lockAge < LOCK_STALE_MS
1692
-
1693
- vi.mocked(readDispatchState).mockResolvedValueOnce({
1694
- dispatches: { active: {}, completed: {} },
1695
- sessionMap: {},
1696
- processedEvents: [],
1697
- });
1698
-
1699
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath }, false);
1700
- const lockCheck = checks.find((c) => c.label.includes("Lock file active"));
1701
- expect(lockCheck?.severity).toBe("warn");
1702
- expect(lockCheck?.label).toContain("may be in use");
1661
+ it("passes when AGENTS.md exists in base repo", async () => {
1662
+ writeFileSync(join(tmpDir, "AGENTS.md"), "# Agent Guidelines\n## Code Style");
1663
+ const checks = await checkFilesAndDirs({ codexBaseRepo: tmpDir });
1664
+ const agentsCheck = checks.find((c) => c.label.includes("AGENTS.md found"));
1665
+ expect(agentsCheck).toBeDefined();
1666
+ expect(agentsCheck?.severity).toBe("pass");
1703
1667
  });
1704
- });
1705
-
1706
- // ---------------------------------------------------------------------------
1707
- // checkFilesAndDirs — prompt variable edge cases
1708
- // ---------------------------------------------------------------------------
1709
1668
 
1710
- describe("checkFilesAndDirs prompt variable edge cases", () => {
1711
- it("reports when variable missing from worker.task but present in audit.task", async () => {
1712
- vi.mocked(loadPrompts).mockReturnValueOnce({
1713
- worker: {
1714
- system: "ok",
1715
- task: "Do something", // missing all vars
1716
- },
1717
- audit: {
1718
- system: "ok",
1719
- task: "Audit {{identifier}} {{title}} {{description}} in {{worktreePath}}", // has all vars
1720
- },
1721
- rework: { addendum: "Fix these gaps: {{gaps}}" },
1722
- } as any);
1723
-
1724
- const checks = await checkFilesAndDirs();
1725
- const promptCheck = checks.find((c) => c.label.includes("Prompt issues"));
1726
- expect(promptCheck?.severity).toBe("fail");
1727
- expect(promptCheck?.label).toContain("worker.task missing");
1728
- // Crucially, audit.task should NOT be missing
1729
- expect(promptCheck?.label).not.toContain("audit.task missing");
1669
+ it("warns when AGENTS.md is missing from base repo", async () => {
1670
+ const checks = await checkFilesAndDirs({ codexBaseRepo: tmpDir });
1671
+ const agentsCheck = checks.find((c) => c.label.includes("No AGENTS.md"));
1672
+ expect(agentsCheck).toBeDefined();
1673
+ expect(agentsCheck?.severity).toBe("warn");
1674
+ expect(agentsCheck?.fix).toContain("AGENTS.md");
1730
1675
  });
1731
1676
 
1732
- it("reports loadPrompts throwing non-Error value", async () => {
1733
- vi.mocked(loadPrompts).mockImplementationOnce(() => { throw "raw string error"; });
1677
+ it("passes both when both files exist", async () => {
1678
+ writeFileSync(join(tmpDir, "CLAUDE.md"), "# Project");
1679
+ writeFileSync(join(tmpDir, "AGENTS.md"), "# Guidelines");
1680
+ const checks = await checkFilesAndDirs({ codexBaseRepo: tmpDir });
1681
+ const claudeCheck = checks.find((c) => c.label.includes("CLAUDE.md found"));
1682
+ const agentsCheck = checks.find((c) => c.label.includes("AGENTS.md found"));
1683
+ expect(claudeCheck?.severity).toBe("pass");
1684
+ expect(agentsCheck?.severity).toBe("pass");
1685
+ });
1734
1686
 
1735
- const checks = await checkFilesAndDirs();
1736
- const promptCheck = checks.find((c) => c.label.includes("Failed to load prompts"));
1737
- expect(promptCheck?.severity).toBe("fail");
1738
- expect(promptCheck?.detail).toContain("raw string error");
1687
+ it("shows file size in KB when CLAUDE.md exists", async () => {
1688
+ writeFileSync(join(tmpDir, "CLAUDE.md"), "x".repeat(2048));
1689
+ const checks = await checkFilesAndDirs({ codexBaseRepo: tmpDir });
1690
+ const claudeCheck = checks.find((c) => c.label.includes("CLAUDE.md found"));
1691
+ expect(claudeCheck?.label).toContain("2KB");
1739
1692
  });
1740
1693
  });
1741
1694
 
1742
1695
  // ---------------------------------------------------------------------------
1743
- // checkFilesAndDirs — worktree & base repo edge cases
1696
+ // checkFilesAndDirs — multi-repo path validation
1744
1697
  // ---------------------------------------------------------------------------
1745
1698
 
1746
- describe("checkFilesAndDirs — worktree & base repo edge cases", () => {
1747
- it("reports worktree base dir does not exist", async () => {
1748
- const checks = await checkFilesAndDirs({
1749
- worktreeBaseDir: "/tmp/nonexistent-worktree-dir-" + Date.now(),
1750
- });
1751
- const wtCheck = checks.find((c) => c.label.includes("Worktree base dir does not exist"));
1752
- expect(wtCheck?.severity).toBe("warn");
1753
- expect(wtCheck?.detail).toContain("Will be created on first dispatch");
1699
+ describe("checkFilesAndDirs — multi-repo validation", () => {
1700
+ let tmpDir: string;
1701
+
1702
+ beforeEach(() => {
1703
+ tmpDir = mkdtempSync(join(tmpdir(), "doctor-repos-"));
1704
+ const { execFileSync } = require("node:child_process");
1705
+ execFileSync("git", ["init"], { cwd: tmpDir, stdio: "ignore" });
1754
1706
  });
1755
1707
 
1756
- it("reports base repo does not exist", async () => {
1708
+ it("passes for valid git repo paths", async () => {
1709
+ const repoDir = join(tmpDir, "myrepo");
1710
+ mkdirSync(repoDir);
1711
+ const { execFileSync } = require("node:child_process");
1712
+ execFileSync("git", ["init"], { cwd: repoDir, stdio: "ignore" });
1713
+
1757
1714
  const checks = await checkFilesAndDirs({
1758
- codexBaseRepo: "/tmp/nonexistent-repo-" + Date.now(),
1715
+ codexBaseRepo: tmpDir,
1716
+ repos: { frontend: repoDir },
1759
1717
  });
1760
- const repoCheck = checks.find((c) => c.label.includes("Base repo does not exist"));
1761
- expect(repoCheck?.severity).toBe("fail");
1762
- expect(repoCheck?.fix).toContain("codexBaseRepo");
1718
+ const repoCheck = checks.find((c) => c.label.includes('Repo "frontend"'));
1719
+ expect(repoCheck).toBeDefined();
1720
+ expect(repoCheck?.severity).toBe("pass");
1721
+ expect(repoCheck?.label).toContain("valid git repo");
1763
1722
  });
1764
1723
 
1765
- it("reports base repo exists but is not a git repo", async () => {
1766
- const tmpDir = mkdtempSync(join(tmpdir(), "doctor-nongit-"));
1724
+ it("fails for non-existent repo paths", async () => {
1767
1725
  const checks = await checkFilesAndDirs({
1768
1726
  codexBaseRepo: tmpDir,
1727
+ repos: { backend: "/tmp/nonexistent-repo-path-12345" },
1769
1728
  });
1770
- const repoCheck = checks.find((c) => c.label.includes("Base repo is not a git repo"));
1729
+ const repoCheck = checks.find((c) => c.label.includes('Repo "backend"'));
1730
+ expect(repoCheck).toBeDefined();
1771
1731
  expect(repoCheck?.severity).toBe("fail");
1772
- expect(repoCheck?.fix).toContain("git init");
1732
+ expect(repoCheck?.label).toContain("does not exist");
1773
1733
  });
1774
- });
1775
-
1776
- // ---------------------------------------------------------------------------
1777
- // checkFilesAndDirs — tilde path resolution branches
1778
- // ---------------------------------------------------------------------------
1779
-
1780
- describe("checkFilesAndDirs — tilde path resolution", () => {
1781
- it("resolves ~/... dispatch state path", async () => {
1782
- vi.mocked(readDispatchState).mockRejectedValueOnce(new Error("ENOENT"));
1783
1734
 
1784
- // Providing a path with ~/ triggers the tilde resolution branch
1785
- const checks = await checkFilesAndDirs({
1786
- dispatchStatePath: "~/nonexistent-state-file.json",
1787
- });
1788
- // The file won't exist (tilde-resolved), so we get the "no file yet" message
1789
- const stateCheck = checks.find((c) => c.label.includes("Dispatch state"));
1790
- expect(stateCheck).toBeDefined();
1791
- });
1735
+ it("fails for paths that exist but are not git repos", async () => {
1736
+ // Create a dir outside the git repo so git rev-parse fails
1737
+ const nonGitDir = mkdtempSync(join(tmpdir(), "doctor-nongit-"));
1792
1738
 
1793
- it("resolves ~/... worktree base dir path", async () => {
1794
1739
  const checks = await checkFilesAndDirs({
1795
- worktreeBaseDir: "~/nonexistent-worktree-base",
1796
- });
1797
- const wtCheck = checks.find((c) => c.label.includes("Worktree base dir"));
1798
- expect(wtCheck).toBeDefined();
1799
- });
1800
- });
1801
-
1802
- // ---------------------------------------------------------------------------
1803
- // checkDispatchHealth — orphaned worktree singular
1804
- // ---------------------------------------------------------------------------
1805
-
1806
- describe("checkDispatchHealth — edge cases", () => {
1807
- it("reports single orphaned worktree (singular)", async () => {
1808
- vi.mocked(listWorktrees).mockReturnValueOnce([
1809
- { issueIdentifier: "ORPHAN-1", path: "/tmp/wt1" } as any,
1810
- ]);
1811
-
1812
- const checks = await checkDispatchHealth();
1813
- const orphanCheck = checks.find((c) => c.label.includes("orphaned worktree"));
1814
- expect(orphanCheck?.severity).toBe("warn");
1815
- expect(orphanCheck?.label).toContain("1 orphaned worktree");
1816
- expect(orphanCheck?.label).not.toContain("worktrees"); // singular, not plural
1817
- });
1818
-
1819
- it("prunes multiple old completed dispatches (plural)", async () => {
1820
- vi.mocked(readDispatchState).mockResolvedValueOnce({
1821
- dispatches: {
1822
- active: {},
1823
- completed: {
1824
- "A-1": { completedAt: new Date(Date.now() - 10 * 24 * 3_600_000).toISOString() } as any,
1825
- "A-2": { completedAt: new Date(Date.now() - 10 * 24 * 3_600_000).toISOString() } as any,
1826
- },
1827
- },
1828
- sessionMap: {},
1829
- processedEvents: [],
1740
+ codexBaseRepo: tmpDir,
1741
+ repos: { api: nonGitDir },
1830
1742
  });
1831
- vi.mocked(pruneCompleted).mockResolvedValueOnce(2);
1832
-
1833
- const checks = await checkDispatchHealth(undefined, true);
1834
- const pruneCheck = checks.find((c) => c.label.includes("Pruned"));
1835
- expect(pruneCheck?.severity).toBe("pass");
1836
- expect(pruneCheck?.label).toContain("2 old completed dispatches");
1837
- });
1838
-
1839
- it("reports single stale dispatch (singular)", async () => {
1840
- vi.mocked(listStaleDispatches).mockReturnValueOnce([
1841
- { issueIdentifier: "API-1", status: "working" } as any,
1842
- ]);
1843
-
1844
- const checks = await checkDispatchHealth();
1845
- const staleCheck = checks.find((c) => c.label.includes("stale dispatch"));
1846
- expect(staleCheck?.severity).toBe("warn");
1847
- // Singular: "1 stale dispatch" not "1 stale dispatches"
1848
- expect(staleCheck?.label).toMatch(/1 stale dispatch(?!es)/);
1849
- });
1850
- });
1851
-
1852
- // ---------------------------------------------------------------------------
1853
- // checkConnectivity — webhook self-test with ok but body !== "ok"
1854
- // ---------------------------------------------------------------------------
1855
-
1856
- describe("checkConnectivity — webhook non-ok body", () => {
1857
- it("warns when webhook returns ok status but body is not 'ok'", async () => {
1858
- vi.stubGlobal("fetch", vi.fn(async (url: string) => {
1859
- if (url.includes("localhost")) {
1860
- return { ok: true, text: async () => "pong" };
1861
- }
1862
- throw new Error("unexpected");
1863
- }));
1864
-
1865
- const checks = await checkConnectivity({}, { viewer: { name: "T" } });
1866
- const webhookCheck = checks.find((c) => c.label.includes("Webhook self-test:"));
1867
- // ok is true but body is "pong" not "ok" — the condition is `res.ok && body === "ok"`
1868
- // Since body !== "ok", it falls into the warn branch
1869
- expect(webhookCheck?.severity).toBe("warn");
1870
- });
1871
- });
1872
-
1873
- // ---------------------------------------------------------------------------
1874
- // formatReport — icon function TTY branches
1875
- // ---------------------------------------------------------------------------
1876
-
1877
- describe("formatReport — TTY icon rendering", () => {
1878
- it("renders colored icons when stdout.isTTY is true", () => {
1879
- const origIsTTY = process.stdout.isTTY;
1880
- try {
1881
- Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true, configurable: true });
1882
-
1883
- const report = {
1884
- sections: [{
1885
- name: "Test",
1886
- checks: [
1887
- { label: "pass check", severity: "pass" as const },
1888
- { label: "warn check", severity: "warn" as const },
1889
- { label: "fail check", severity: "fail" as const },
1890
- ],
1891
- }],
1892
- summary: { passed: 1, warnings: 1, errors: 1 },
1893
- };
1894
-
1895
- const output = formatReport(report);
1896
- // TTY output includes ANSI escape codes
1897
- expect(output).toContain("\x1b[32m"); // green for pass
1898
- expect(output).toContain("\x1b[33m"); // yellow for warn
1899
- expect(output).toContain("\x1b[31m"); // red for fail
1900
- } finally {
1901
- Object.defineProperty(process.stdout, "isTTY", { value: origIsTTY, writable: true, configurable: true });
1902
- }
1743
+ const repoCheck = checks.find((c) => c.label.includes('Repo "api"'));
1744
+ expect(repoCheck).toBeDefined();
1745
+ expect(repoCheck?.severity).toBe("fail");
1746
+ expect(repoCheck?.label).toContain("not a git repo");
1903
1747
  });
1904
1748
  });
1905
1749
 
1906
- // ---------------------------------------------------------------------------
1907
- // checkCodingTools — codingTool fallback to "codex" in label
1908
- // ---------------------------------------------------------------------------
1909
-
1910
- describe("checkCodingTools — codingTool null fallback", () => {
1911
- it("shows 'codex' as default when codingTool is undefined but backends exist", () => {
1912
- vi.mocked(loadCodingConfig).mockReturnValueOnce({
1913
- codingTool: undefined,
1914
- backends: { codex: { aliases: ["codex"] } },
1915
- } as any);
1916
-
1917
- const checks = checkCodingTools();
1918
- const configCheck = checks.find((c) => c.label.includes("coding-tools.json loaded"));
1919
- expect(configCheck?.severity).toBe("pass");
1920
- expect(configCheck?.label).toContain("codex"); // falls back to "codex" via ??
1921
- });
1922
- });
1923
-
1924
- // ---------------------------------------------------------------------------
1925
- // checkFilesAndDirs — dispatch state non-Error catch branch
1926
- // ---------------------------------------------------------------------------
1927
-
1928
- describe("checkFilesAndDirs — dispatch state non-Error exception", () => {
1929
- it("handles non-Error thrown during dispatch state read", async () => {
1930
- const tmpDir = mkdtempSync(join(tmpdir(), "doctor-nonError-"));
1931
- const statePath = join(tmpDir, "state.json");
1932
- writeFileSync(statePath, '{}');
1933
- vi.mocked(readDispatchState).mockRejectedValueOnce("raw string dispatch error");
1934
-
1935
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath });
1936
- const stateCheck = checks.find((c) => c.label.includes("Dispatch state corrupt"));
1937
- expect(stateCheck?.severity).toBe("fail");
1938
- expect(stateCheck?.detail).toContain("raw string dispatch error");
1939
- });
1940
- });
1941
-
1942
- // ---------------------------------------------------------------------------
1943
- // checkFilesAndDirs — lock file branches
1944
- // ---------------------------------------------------------------------------
1945
-
1946
- describe("checkFilesAndDirs — lock file branches", () => {
1947
- let tmpDir: string;
1948
-
1949
- beforeEach(() => {
1950
- tmpDir = mkdtempSync(join(tmpdir(), "doctor-lock-"));
1951
- });
1952
-
1953
- it("warns about stale lock file without --fix", async () => {
1954
- const statePath = join(tmpDir, "state.json");
1955
- const lockPath = statePath + ".lock";
1956
- writeFileSync(statePath, '{}');
1957
- writeFileSync(lockPath, "locked");
1958
- // Make the lock file appear stale by backdating its mtime
1959
- const staleTime = Date.now() - 60_000; // 60 seconds old (> 30s LOCK_STALE_MS)
1960
- const { utimesSync } = await import("node:fs");
1961
- utimesSync(lockPath, staleTime / 1000, staleTime / 1000);
1962
-
1963
- vi.mocked(readDispatchState).mockResolvedValueOnce({
1964
- dispatches: { active: {}, completed: {} },
1965
- sessionMap: {},
1966
- processedEvents: [],
1967
- });
1968
-
1969
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath }, false);
1970
- const lockCheck = checks.find((c) => c.label.includes("Stale lock file"));
1971
- expect(lockCheck?.severity).toBe("warn");
1972
- expect(lockCheck?.fixable).toBe(true);
1973
- });
1974
-
1975
- it("removes stale lock file with --fix", async () => {
1976
- const statePath = join(tmpDir, "state.json");
1977
- const lockPath = statePath + ".lock";
1978
- writeFileSync(statePath, '{}');
1979
- writeFileSync(lockPath, "locked");
1980
- const staleTime = Date.now() - 60_000;
1981
- const { utimesSync } = await import("node:fs");
1982
- utimesSync(lockPath, staleTime / 1000, staleTime / 1000);
1983
-
1984
- vi.mocked(readDispatchState).mockResolvedValueOnce({
1985
- dispatches: { active: {}, completed: {} },
1986
- sessionMap: {},
1987
- processedEvents: [],
1988
- });
1989
-
1990
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath }, true);
1991
- const lockCheck = checks.find((c) => c.label.includes("Stale lock file removed"));
1992
- expect(lockCheck?.severity).toBe("pass");
1993
- expect(existsSync(lockPath)).toBe(false);
1994
- });
1995
-
1996
- it("warns about active (non-stale) lock file", async () => {
1997
- const statePath = join(tmpDir, "state.json");
1998
- const lockPath = statePath + ".lock";
1999
- writeFileSync(statePath, '{}');
2000
- writeFileSync(lockPath, "locked");
2001
- // Lock file is fresh (just created), so lockAge < LOCK_STALE_MS
2002
-
2003
- vi.mocked(readDispatchState).mockResolvedValueOnce({
2004
- dispatches: { active: {}, completed: {} },
2005
- sessionMap: {},
2006
- processedEvents: [],
2007
- });
2008
-
2009
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath }, false);
2010
- const lockCheck = checks.find((c) => c.label.includes("Lock file active"));
2011
- expect(lockCheck?.severity).toBe("warn");
2012
- expect(lockCheck?.label).toContain("may be in use");
2013
- });
2014
- });
2015
-
2016
- // ---------------------------------------------------------------------------
2017
- // checkFilesAndDirs — prompt variable edge cases
2018
- // ---------------------------------------------------------------------------
2019
-
2020
- describe("checkFilesAndDirs — prompt variable edge cases", () => {
2021
- it("reports when variable missing from worker.task but present in audit.task", async () => {
2022
- vi.mocked(loadPrompts).mockReturnValueOnce({
2023
- worker: {
2024
- system: "ok",
2025
- task: "Do something", // missing all vars
2026
- },
2027
- audit: {
2028
- system: "ok",
2029
- task: "Audit {{identifier}} {{title}} {{description}} in {{worktreePath}}", // has all vars
2030
- },
2031
- rework: { addendum: "Fix these gaps: {{gaps}}" },
2032
- } as any);
2033
-
2034
- const checks = await checkFilesAndDirs();
2035
- const promptCheck = checks.find((c) => c.label.includes("Prompt issues"));
2036
- expect(promptCheck?.severity).toBe("fail");
2037
- expect(promptCheck?.label).toContain("worker.task missing");
2038
- // Crucially, audit.task should NOT be missing
2039
- expect(promptCheck?.label).not.toContain("audit.task missing");
2040
- });
2041
-
2042
- it("reports loadPrompts throwing non-Error value", async () => {
2043
- vi.mocked(loadPrompts).mockImplementationOnce(() => { throw "raw string error"; });
2044
-
2045
- const checks = await checkFilesAndDirs();
2046
- const promptCheck = checks.find((c) => c.label.includes("Failed to load prompts"));
2047
- expect(promptCheck?.severity).toBe("fail");
2048
- expect(promptCheck?.detail).toContain("raw string error");
2049
- });
2050
- });
2051
-
2052
- // ---------------------------------------------------------------------------
2053
- // checkFilesAndDirs — worktree & base repo edge cases
2054
- // ---------------------------------------------------------------------------
2055
-
2056
- describe("checkFilesAndDirs — worktree & base repo edge cases", () => {
2057
- it("reports worktree base dir does not exist", async () => {
2058
- const checks = await checkFilesAndDirs({
2059
- worktreeBaseDir: "/tmp/nonexistent-worktree-dir-" + Date.now(),
2060
- });
2061
- const wtCheck = checks.find((c) => c.label.includes("Worktree base dir does not exist"));
2062
- expect(wtCheck?.severity).toBe("warn");
2063
- expect(wtCheck?.detail).toContain("Will be created on first dispatch");
2064
- });
2065
-
2066
- it("reports base repo does not exist", async () => {
2067
- const checks = await checkFilesAndDirs({
2068
- codexBaseRepo: "/tmp/nonexistent-repo-" + Date.now(),
2069
- });
2070
- const repoCheck = checks.find((c) => c.label.includes("Base repo does not exist"));
2071
- expect(repoCheck?.severity).toBe("fail");
2072
- expect(repoCheck?.fix).toContain("codexBaseRepo");
2073
- });
2074
-
2075
- it("reports base repo exists but is not a git repo", async () => {
2076
- const tmpDir = mkdtempSync(join(tmpdir(), "doctor-nongit-"));
2077
- const checks = await checkFilesAndDirs({
2078
- codexBaseRepo: tmpDir,
2079
- });
2080
- const repoCheck = checks.find((c) => c.label.includes("Base repo is not a git repo"));
2081
- expect(repoCheck?.severity).toBe("fail");
2082
- expect(repoCheck?.fix).toContain("git init");
2083
- });
2084
- });
2085
-
2086
- // ---------------------------------------------------------------------------
2087
- // checkFilesAndDirs — tilde path resolution branches
2088
- // ---------------------------------------------------------------------------
2089
-
2090
- describe("checkFilesAndDirs — tilde path resolution", () => {
2091
- it("resolves ~/... dispatch state path", async () => {
2092
- vi.mocked(readDispatchState).mockRejectedValueOnce(new Error("ENOENT"));
2093
-
2094
- // Providing a path with ~/ triggers the tilde resolution branch
2095
- const checks = await checkFilesAndDirs({
2096
- dispatchStatePath: "~/nonexistent-state-file.json",
2097
- });
2098
- // The file won't exist (tilde-resolved), so we get the "no file yet" message
2099
- const stateCheck = checks.find((c) => c.label.includes("Dispatch state"));
2100
- expect(stateCheck).toBeDefined();
2101
- });
2102
-
2103
- it("resolves ~/... worktree base dir path", async () => {
2104
- const checks = await checkFilesAndDirs({
2105
- worktreeBaseDir: "~/nonexistent-worktree-base",
2106
- });
2107
- const wtCheck = checks.find((c) => c.label.includes("Worktree base dir"));
2108
- expect(wtCheck).toBeDefined();
2109
- });
2110
- });
2111
-
2112
- // ---------------------------------------------------------------------------
2113
- // checkDispatchHealth — orphaned worktree singular
2114
- // ---------------------------------------------------------------------------
2115
-
2116
- describe("checkDispatchHealth — edge cases", () => {
2117
- it("reports single orphaned worktree (singular)", async () => {
2118
- vi.mocked(listWorktrees).mockReturnValueOnce([
2119
- { issueIdentifier: "ORPHAN-1", path: "/tmp/wt1" } as any,
2120
- ]);
2121
-
2122
- const checks = await checkDispatchHealth();
2123
- const orphanCheck = checks.find((c) => c.label.includes("orphaned worktree"));
2124
- expect(orphanCheck?.severity).toBe("warn");
2125
- expect(orphanCheck?.label).toContain("1 orphaned worktree");
2126
- expect(orphanCheck?.label).not.toContain("worktrees"); // singular, not plural
2127
- });
2128
-
2129
- it("prunes multiple old completed dispatches (plural)", async () => {
2130
- vi.mocked(readDispatchState).mockResolvedValueOnce({
2131
- dispatches: {
2132
- active: {},
2133
- completed: {
2134
- "A-1": { completedAt: new Date(Date.now() - 10 * 24 * 3_600_000).toISOString() } as any,
2135
- "A-2": { completedAt: new Date(Date.now() - 10 * 24 * 3_600_000).toISOString() } as any,
2136
- },
2137
- },
2138
- sessionMap: {},
2139
- processedEvents: [],
2140
- });
2141
- vi.mocked(pruneCompleted).mockResolvedValueOnce(2);
2142
-
2143
- const checks = await checkDispatchHealth(undefined, true);
2144
- const pruneCheck = checks.find((c) => c.label.includes("Pruned"));
2145
- expect(pruneCheck?.severity).toBe("pass");
2146
- expect(pruneCheck?.label).toContain("2 old completed dispatches");
2147
- });
2148
-
2149
- it("reports single stale dispatch (singular)", async () => {
2150
- vi.mocked(listStaleDispatches).mockReturnValueOnce([
2151
- { issueIdentifier: "API-1", status: "working" } as any,
2152
- ]);
2153
-
2154
- const checks = await checkDispatchHealth();
2155
- const staleCheck = checks.find((c) => c.label.includes("stale dispatch"));
2156
- expect(staleCheck?.severity).toBe("warn");
2157
- // Singular: "1 stale dispatch" not "1 stale dispatches"
2158
- expect(staleCheck?.label).toMatch(/1 stale dispatch(?!es)/);
2159
- });
2160
- });
2161
-
2162
- // ---------------------------------------------------------------------------
2163
- // checkConnectivity — webhook self-test with ok but body !== "ok"
2164
- // ---------------------------------------------------------------------------
2165
-
2166
- describe("checkConnectivity — webhook non-ok body", () => {
2167
- it("warns when webhook returns ok status but body is not 'ok'", async () => {
2168
- vi.stubGlobal("fetch", vi.fn(async (url: string) => {
2169
- if (url.includes("localhost")) {
2170
- return { ok: true, text: async () => "pong" };
2171
- }
2172
- throw new Error("unexpected");
2173
- }));
2174
-
2175
- const checks = await checkConnectivity({}, { viewer: { name: "T" } });
2176
- const webhookCheck = checks.find((c) => c.label.includes("Webhook self-test:"));
2177
- // ok is true but body is "pong" not "ok" — the condition is `res.ok && body === "ok"`
2178
- // Since body !== "ok", it falls into the warn branch
2179
- expect(webhookCheck?.severity).toBe("warn");
2180
- });
2181
- });
2182
-
2183
- // ---------------------------------------------------------------------------
2184
- // formatReport — icon function TTY branches
2185
- // ---------------------------------------------------------------------------
2186
-
2187
- describe("formatReport — TTY icon rendering", () => {
2188
- it("renders colored icons when stdout.isTTY is true", () => {
2189
- const origIsTTY = process.stdout.isTTY;
2190
- try {
2191
- Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true, configurable: true });
2192
-
2193
- const report = {
2194
- sections: [{
2195
- name: "Test",
2196
- checks: [
2197
- { label: "pass check", severity: "pass" as const },
2198
- { label: "warn check", severity: "warn" as const },
2199
- { label: "fail check", severity: "fail" as const },
2200
- ],
2201
- }],
2202
- summary: { passed: 1, warnings: 1, errors: 1 },
2203
- };
2204
-
2205
- const output = formatReport(report);
2206
- // TTY output includes ANSI escape codes
2207
- expect(output).toContain("\x1b[32m"); // green for pass
2208
- expect(output).toContain("\x1b[33m"); // yellow for warn
2209
- expect(output).toContain("\x1b[31m"); // red for fail
2210
- } finally {
2211
- Object.defineProperty(process.stdout, "isTTY", { value: origIsTTY, writable: true, configurable: true });
2212
- }
2213
- });
2214
- });
2215
-
2216
- // ---------------------------------------------------------------------------
2217
- // checkCodingTools — codingTool fallback to "codex" in label
2218
- // ---------------------------------------------------------------------------
2219
-
2220
- describe("checkCodingTools — codingTool null fallback", () => {
2221
- it("shows 'codex' as default when codingTool is undefined but backends exist", () => {
2222
- vi.mocked(loadCodingConfig).mockReturnValueOnce({
2223
- codingTool: undefined,
2224
- backends: { codex: { aliases: ["codex"] } },
2225
- } as any);
2226
-
2227
- const checks = checkCodingTools();
2228
- const configCheck = checks.find((c) => c.label.includes("coding-tools.json loaded"));
2229
- expect(configCheck?.severity).toBe("pass");
2230
- expect(configCheck?.label).toContain("codex"); // falls back to "codex" via ??
2231
- });
2232
- });
2233
-
2234
- // ---------------------------------------------------------------------------
2235
- // checkFilesAndDirs — dispatch state non-Error catch branch
2236
- // ---------------------------------------------------------------------------
2237
-
2238
- describe("checkFilesAndDirs — dispatch state non-Error exception", () => {
2239
- it("handles non-Error thrown during dispatch state read", async () => {
2240
- const tmpDir = mkdtempSync(join(tmpdir(), "doctor-nonError-"));
2241
- const statePath = join(tmpDir, "state.json");
2242
- writeFileSync(statePath, '{}');
2243
- vi.mocked(readDispatchState).mockRejectedValueOnce("raw string dispatch error");
2244
-
2245
- const checks = await checkFilesAndDirs({ dispatchStatePath: statePath });
2246
- const stateCheck = checks.find((c) => c.label.includes("Dispatch state corrupt"));
2247
- expect(stateCheck?.severity).toBe("fail");
2248
- expect(stateCheck?.detail).toContain("raw string dispatch error");
2249
- });
2250
- });
@@ -477,7 +477,14 @@ export async function handleLinearWebhook(
477
477
  commentContext ? `\n**Conversation:**\n${commentContext}` : "",
478
478
  userMessage ? `\n**Latest message:**\n> ${userMessage}` : "",
479
479
  ``,
480
- `Respond to the user's request. For work requests, dispatch via \`code_run\` and summarize the result. Be concise and action-oriented.`,
480
+ `## Scope Rules`,
481
+ `1. **Read the issue first.** The issue title + description define your scope. Everything you do must serve the issue as written.`,
482
+ `2. **\`code_run\` is ONLY for issue-body work.** Only dispatch \`code_run\` when the issue description contains implementation requirements. A greeting, question, or conversational issue gets a conversational response — NOT code_run.`,
483
+ `3. **Comments explore, issue body builds.** User comments may explore scope or ask questions but NEVER trigger \`code_run\` alone. If a comment requests new implementation, update the issue description first, then build from the issue text.`,
484
+ `4. **Plan before building.** For non-trivial work, respond with a plan first. Only dispatch \`code_run\` after the plan is clear and grounded in the issue body.`,
485
+ `5. **Match response to request.** Greeting → greet. Question → answer. No implementation requirements → no code_run.`,
486
+ ``,
487
+ `Respond within the scope defined above. Be concise and action-oriented.`,
481
488
  ].filter(Boolean).join("\n");
482
489
 
483
490
  // Run agent directly (non-blocking)
@@ -720,7 +727,12 @@ export async function handleLinearWebhook(
720
727
  commentContext ? `\n**Recent conversation:**\n${commentContext}` : "",
721
728
  `\n**User's follow-up message:**\n> ${userMessage}`,
722
729
  ``,
723
- `Respond to the user's follow-up. For work requests, dispatch via \`code_run\`. Be concise and action-oriented.`,
730
+ `## Scope Rules`,
731
+ `1. **The issue body is your scope.** Re-read the description above before acting.`,
732
+ `2. **Comments explore, issue body builds.** The follow-up may refine understanding or ask questions — NEVER dispatch \`code_run\` from a comment alone. If the user requests implementation, suggest updating the issue description first.`,
733
+ `3. **Match response to request.** Answer questions with answers. Do NOT escalate conversational messages into builds.`,
734
+ ``,
735
+ `Respond to the follow-up within the scope defined above. Be concise and action-oriented.`,
724
736
  ].filter(Boolean).join("\n");
725
737
 
726
738
  setActiveSession({
@@ -178,6 +178,11 @@ export function createCodeTool(
178
178
  required: ["prompt"],
179
179
  },
180
180
  execute: async (toolCallId: string, params: CliToolParams & { backend?: string }, ...rest: unknown[]) => {
181
+ // Extract onUpdate callback for progress reporting to Linear
182
+ const onUpdate = typeof rest[1] === "function"
183
+ ? rest[1] as (update: Record<string, unknown>) => void
184
+ : undefined;
185
+
181
186
  // Resolve backend: explicit alias → per-agent config → global default
182
187
  const currentSession = getCurrentSession();
183
188
  const agentId = currentSession?.agentId;
@@ -189,6 +194,13 @@ export function createCodeTool(
189
194
 
190
195
  api.logger.info(`code_run: backend=${backend} agent=${agentId ?? "unknown"}`);
191
196
 
197
+ // Emit prompt summary so Linear users see what's being built
198
+ const promptSummary = (params.prompt ?? "").slice(0, 200);
199
+ api.logger.info(`code_run prompt: [${backend}] ${promptSummary}`);
200
+ if (onUpdate) {
201
+ try { onUpdate({ status: "running", summary: `[${backend}] ${promptSummary}` }); } catch {}
202
+ }
203
+
192
204
  const result = await runner(api, params, pluginConfig);
193
205
 
194
206
  return jsonResult({