@calltelemetry/openclaw-linear 0.8.4 → 0.8.6

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.
@@ -628,4 +628,154 @@ describe("webhook scenario tests — full handler flows", () => {
628
628
  expect(mockGetIssueDetails).not.toHaveBeenCalled();
629
629
  });
630
630
  });
631
+
632
+ describe("Guidance integration", () => {
633
+ it("created: appends guidance to agent prompt", async () => {
634
+ const api = createApi();
635
+ const payload = makeAgentSessionEventCreated({
636
+ guidance: "Always use the main branch. Run make test before closing.",
637
+ });
638
+ await postWebhook(api, payload);
639
+
640
+ await waitForMock(mockClearActiveSession);
641
+
642
+ expect(mockRunAgent).toHaveBeenCalledOnce();
643
+ const runArgs = mockRunAgent.mock.calls[0][0];
644
+ expect(runArgs.message).toContain("Additional Guidance");
645
+ expect(runArgs.message).toContain("Always use the main branch");
646
+ });
647
+
648
+ it("created: guidance is NOT used as user message", async () => {
649
+ const api = createApi();
650
+ const payload = makeAgentSessionEventCreated({
651
+ guidance: "Always use the main branch. Run make test before closing.",
652
+ previousComments: [
653
+ { body: "Please fix the routing bug", userId: "user-1", createdAt: new Date().toISOString() },
654
+ ],
655
+ });
656
+ await postWebhook(api, payload);
657
+
658
+ await waitForMock(mockClearActiveSession);
659
+
660
+ expect(mockRunAgent).toHaveBeenCalledOnce();
661
+ const msg = mockRunAgent.mock.calls[0][0].message;
662
+
663
+ // Guidance text should appear in the appendix section, not as the user's comment
664
+ const userMsgSection = msg.split("Additional Guidance")[0];
665
+ expect(userMsgSection).toContain("Please fix the routing bug");
666
+ // The guidance string itself should not appear before the appendix
667
+ expect(userMsgSection).not.toContain("Always use the main branch");
668
+ });
669
+
670
+ it("prompted: includes guidance from promptContext", async () => {
671
+ const api = createApi();
672
+ const payload = makeAgentSessionEventPrompted({
673
+ agentActivity: { content: { body: "Can you also fix the tests?" } },
674
+ promptContext: "## Issue\nENG-123\n\n## Guidance\nUse TypeScript strict mode.\n\n## Comments\nThread.",
675
+ });
676
+ await postWebhook(api, payload);
677
+
678
+ await waitForMock(mockClearActiveSession);
679
+
680
+ expect(mockRunAgent).toHaveBeenCalledOnce();
681
+ const msg = mockRunAgent.mock.calls[0][0].message;
682
+ expect(msg).toContain("Can you also fix the tests?");
683
+ expect(msg).toContain("Additional Guidance");
684
+ expect(msg).toContain("Use TypeScript strict mode");
685
+ });
686
+
687
+ it("guidance disabled via config: no guidance section in prompt", async () => {
688
+ const api = createApi();
689
+ (api as any).pluginConfig = { defaultAgentId: "mal", enableGuidance: false };
690
+ const payload = makeAgentSessionEventCreated({
691
+ guidance: "Should not appear in prompt",
692
+ });
693
+ await postWebhook(api, payload);
694
+
695
+ await waitForMock(mockClearActiveSession);
696
+
697
+ expect(mockRunAgent).toHaveBeenCalledOnce();
698
+ const msg = mockRunAgent.mock.calls[0][0].message;
699
+ expect(msg).not.toContain("Additional Guidance");
700
+ expect(msg).not.toContain("Should not appear in prompt");
701
+ });
702
+
703
+ it("team override disables guidance for specific team", async () => {
704
+ const api = createApi();
705
+ (api as any).pluginConfig = {
706
+ defaultAgentId: "mal",
707
+ enableGuidance: true,
708
+ teamGuidanceOverrides: { "team-1": false },
709
+ };
710
+ const payload = makeAgentSessionEventCreated({
711
+ guidance: "Should be suppressed for team-1",
712
+ });
713
+ await postWebhook(api, payload);
714
+
715
+ await waitForMock(mockClearActiveSession);
716
+
717
+ expect(mockRunAgent).toHaveBeenCalledOnce();
718
+ const msg = mockRunAgent.mock.calls[0][0].message;
719
+ expect(msg).not.toContain("Additional Guidance");
720
+ expect(msg).not.toContain("Should be suppressed");
721
+ });
722
+
723
+ it("comment handler uses cached guidance from prior session event", async () => {
724
+ // Step 1: Trigger a created event to cache guidance
725
+ const api = createApi();
726
+ const sessionPayload = makeAgentSessionEventCreated({
727
+ guidance: "Cached guidance from session event",
728
+ });
729
+ await postWebhook(api, sessionPayload);
730
+ await waitForMock(mockClearActiveSession);
731
+
732
+ // Reset mocks for the next webhook
733
+ vi.clearAllMocks();
734
+ mockGetViewerId.mockResolvedValue("viewer-bot-1");
735
+ mockGetIssueDetails.mockResolvedValue(makeIssueDetails());
736
+ mockCreateComment.mockResolvedValue("comment-new-id");
737
+ mockEmitActivity.mockResolvedValue(undefined);
738
+ mockUpdateSession.mockResolvedValue(undefined);
739
+ mockUpdateIssue.mockResolvedValue(true);
740
+ mockCreateSessionOnIssue.mockResolvedValue({ sessionId: "session-mock-2" });
741
+ mockGetTeamLabels.mockResolvedValue([]);
742
+ mockGetTeamStates.mockResolvedValue([
743
+ { id: "st-backlog", name: "Backlog", type: "backlog" },
744
+ { id: "st-started", name: "In Progress", type: "started" },
745
+ { id: "st-done", name: "Done", type: "completed" },
746
+ ]);
747
+ mockRunAgent.mockResolvedValue({ success: true, output: "Agent response text" });
748
+ mockClassifyIntent.mockResolvedValue({
749
+ intent: "ask_agent",
750
+ agentId: "mal",
751
+ reasoning: "user requesting help",
752
+ fromFallback: false,
753
+ });
754
+
755
+ // Step 2: Now send a comment — it should pick up cached guidance
756
+ const commentPayload = makeCommentCreate({
757
+ data: {
758
+ id: "comment-guidance-1",
759
+ body: "Can you investigate further?",
760
+ user: { id: "user-human", name: "Human" },
761
+ issue: {
762
+ id: "issue-1",
763
+ identifier: "ENG-123",
764
+ title: "Fix webhook routing",
765
+ team: { id: "team-1" },
766
+ project: null,
767
+ },
768
+ createdAt: new Date().toISOString(),
769
+ },
770
+ });
771
+ await postWebhook(api, commentPayload);
772
+
773
+ await waitForMock(mockClearActiveSession);
774
+
775
+ expect(mockRunAgent).toHaveBeenCalledOnce();
776
+ const msg = mockRunAgent.mock.calls[0][0].message;
777
+ expect(msg).toContain("Additional Guidance");
778
+ expect(msg).toContain("Cached guidance from session event");
779
+ });
780
+ });
631
781
  });
@@ -307,6 +307,7 @@ export class LinearAgentApi {
307
307
  description: string | null;
308
308
  estimate: number | null;
309
309
  state: { name: string; type: string };
310
+ creator: { name: string; email: string | null } | null;
310
311
  assignee: { name: string } | null;
311
312
  labels: { nodes: Array<{ id: string; name: string }> };
312
313
  team: { id: string; name: string; issueEstimationType: string };
@@ -324,6 +325,7 @@ export class LinearAgentApi {
324
325
  description
325
326
  estimate
326
327
  state { name type }
328
+ creator { name email }
327
329
  assignee { name }
328
330
  labels { nodes { id name } }
329
331
  team { id name issueEstimationType }
package/src/infra/cli.ts CHANGED
@@ -645,6 +645,33 @@ export function registerCli(program: Command, api: OpenClawPluginApi): void {
645
645
  }
646
646
  });
647
647
 
648
+ // --- openclaw openclaw-linear code-run ---
649
+ const codeRunCmd = linear
650
+ .command("code-run")
651
+ .description("Manage and diagnose coding tool backends");
652
+
653
+ codeRunCmd
654
+ .command("doctor")
655
+ .description("Deep health check: verify each coding backend (Claude, Codex, Gemini) is callable")
656
+ .option("--json", "Output results as JSON")
657
+ .action(async (opts: { json?: boolean }) => {
658
+ const { checkCodeRunDeep, buildSummary, formatReport, formatReportJson } = await import("./doctor.js");
659
+ const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
660
+
661
+ const sections = await checkCodeRunDeep(pluginConfig);
662
+ const report = { sections, summary: buildSummary(sections) };
663
+
664
+ if (opts.json) {
665
+ console.log(formatReportJson(report));
666
+ } else {
667
+ console.log(formatReport(report));
668
+ }
669
+
670
+ if (report.summary.errors > 0) {
671
+ process.exitCode = 1;
672
+ }
673
+ });
674
+
648
675
  // --- openclaw openclaw-linear webhooks ---
649
676
  const webhooksCmd = linear
650
677
  .command("webhooks")
@@ -50,14 +50,18 @@ vi.mock("./codex-worktree.js", () => ({
50
50
 
51
51
  vi.mock("../tools/code-tool.js", () => ({
52
52
  loadCodingConfig: vi.fn(() => ({
53
- codingTool: "claude",
54
- agentCodingTools: {},
53
+ codingTool: "codex",
54
+ agentCodingTools: { inara: "claude" },
55
55
  backends: {
56
56
  claude: { aliases: ["claude", "anthropic"] },
57
57
  codex: { aliases: ["codex", "openai"] },
58
58
  gemini: { aliases: ["gemini", "google"] },
59
59
  },
60
60
  })),
61
+ resolveCodingBackend: vi.fn((config: any, agentId?: string) => {
62
+ if (agentId && config?.agentCodingTools?.[agentId]) return config.agentCodingTools[agentId];
63
+ return config?.codingTool ?? "codex";
64
+ }),
61
65
  }));
62
66
 
63
67
  vi.mock("./webhook-provision.js", () => ({
@@ -78,6 +82,8 @@ import {
78
82
  checkConnectivity,
79
83
  checkDispatchHealth,
80
84
  checkWebhooks,
85
+ checkCodeRunDeep,
86
+ buildSummary,
81
87
  runDoctor,
82
88
  formatReport,
83
89
  formatReportJson,
@@ -199,7 +205,7 @@ describe("checkCodingTools", () => {
199
205
  const checks = checkCodingTools();
200
206
  const configCheck = checks.find((c) => c.label.includes("coding-tools.json"));
201
207
  expect(configCheck?.severity).toBe("pass");
202
- expect(configCheck?.label).toContain("claude");
208
+ expect(configCheck?.label).toContain("codex");
203
209
  });
204
210
 
205
211
  it("reports warn for missing CLIs", () => {
@@ -431,3 +437,124 @@ describe("formatReportJson", () => {
431
437
  expect(parsed.summary.passed).toBe(1);
432
438
  });
433
439
  });
440
+
441
+ // ---------------------------------------------------------------------------
442
+ // buildSummary
443
+ // ---------------------------------------------------------------------------
444
+
445
+ describe("buildSummary", () => {
446
+ it("counts pass/warn/fail across sections", () => {
447
+ const sections = [
448
+ { name: "A", checks: [{ label: "ok", severity: "pass" as const }, { label: "meh", severity: "warn" as const }] },
449
+ { name: "B", checks: [{ label: "bad", severity: "fail" as const }] },
450
+ ];
451
+ const summary = buildSummary(sections);
452
+ expect(summary).toEqual({ passed: 1, warnings: 1, errors: 1 });
453
+ });
454
+ });
455
+
456
+ // ---------------------------------------------------------------------------
457
+ // checkCodeRunDeep
458
+ // ---------------------------------------------------------------------------
459
+
460
+ describe("checkCodeRunDeep", () => {
461
+ // Run a single invocation and share results across assertions to avoid
462
+ // repeated 30s live CLI calls (the live test spawns all 3 backends).
463
+ let sections: Awaited<ReturnType<typeof checkCodeRunDeep>>;
464
+
465
+ beforeEach(async () => {
466
+ if (!sections) {
467
+ sections = await checkCodeRunDeep();
468
+ }
469
+ }, 120_000);
470
+
471
+ it("returns 4 sections (3 backends + routing)", () => {
472
+ expect(sections).toHaveLength(4);
473
+ expect(sections.map((s) => s.name)).toEqual([
474
+ "Code Run: Claude Code (Anthropic)",
475
+ "Code Run: Codex (OpenAI)",
476
+ "Code Run: Gemini CLI (Google)",
477
+ "Code Run: Routing",
478
+ ]);
479
+ });
480
+
481
+ it("each backend section has binary, API key, and live test checks", () => {
482
+ for (const section of sections.slice(0, 3)) {
483
+ const labels = section.checks.map((c) => c.label);
484
+ expect(labels.some((l) => l.includes("Binary:"))).toBe(true);
485
+ expect(labels.some((l) => l.includes("API key:"))).toBe(true);
486
+ expect(labels.some((l) => l.includes("Live test:"))).toBe(true);
487
+ }
488
+ });
489
+
490
+ it("all check results have valid severity", () => {
491
+ for (const section of sections) {
492
+ for (const check of section.checks) {
493
+ expect(check).toHaveProperty("label");
494
+ expect(check).toHaveProperty("severity");
495
+ expect(["pass", "warn", "fail"]).toContain(check.severity);
496
+ }
497
+ }
498
+ });
499
+
500
+ it("shows routing with default backend and callable count", () => {
501
+ const routing = sections.find((s) => s.name === "Code Run: Routing")!;
502
+ expect(routing).toBeDefined();
503
+
504
+ const defaultCheck = routing.checks.find((c) => c.label.includes("Default backend:"));
505
+ expect(defaultCheck?.severity).toBe("pass");
506
+ expect(defaultCheck?.label).toContain("codex");
507
+
508
+ const callableCheck = routing.checks.find((c) => c.label.includes("Callable backends:"));
509
+ expect(callableCheck?.severity).toBe("pass");
510
+ expect(callableCheck?.label).toMatch(/Callable backends: \d+\/3/);
511
+ });
512
+
513
+ it("shows agent override routing for inara", () => {
514
+ const routing = sections.find((s) => s.name === "Code Run: Routing")!;
515
+ const inaraCheck = routing.checks.find((c) => c.label.toLowerCase().includes("inara"));
516
+ if (inaraCheck) {
517
+ expect(inaraCheck.label).toContain("claude");
518
+ expect(inaraCheck.label).toContain("override");
519
+ }
520
+ });
521
+
522
+ // API key tests — still call checkCodeRunDeep which runs live CLI checks
523
+ it("detects API key from plugin config", async () => {
524
+ const origKey = process.env.ANTHROPIC_API_KEY;
525
+ delete process.env.ANTHROPIC_API_KEY;
526
+
527
+ try {
528
+ const result = await checkCodeRunDeep({ claudeApiKey: "sk-from-config" });
529
+ const claudeKey = result[0].checks.find((c) => c.label.includes("API key:"));
530
+ expect(claudeKey?.severity).toBe("pass");
531
+ expect(claudeKey?.label).toContain("claudeApiKey");
532
+ } finally {
533
+ if (origKey) process.env.ANTHROPIC_API_KEY = origKey;
534
+ else delete process.env.ANTHROPIC_API_KEY;
535
+ }
536
+ }, 120_000);
537
+
538
+ it("warns when API key missing", async () => {
539
+ const origKeys = {
540
+ GEMINI_API_KEY: process.env.GEMINI_API_KEY,
541
+ GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
542
+ GOOGLE_GENAI_API_KEY: process.env.GOOGLE_GENAI_API_KEY,
543
+ };
544
+ delete process.env.GEMINI_API_KEY;
545
+ delete process.env.GOOGLE_API_KEY;
546
+ delete process.env.GOOGLE_GENAI_API_KEY;
547
+
548
+ try {
549
+ const result = await checkCodeRunDeep();
550
+ const geminiKey = result[2].checks.find((c) => c.label.includes("API key:"));
551
+ expect(geminiKey?.severity).toBe("warn");
552
+ expect(geminiKey?.label).toContain("not found");
553
+ } finally {
554
+ for (const [k, v] of Object.entries(origKeys)) {
555
+ if (v) process.env[k] = v;
556
+ else delete process.env[k];
557
+ }
558
+ }
559
+ }, 120_000);
560
+ });
@@ -6,13 +6,13 @@
6
6
  import { existsSync, readFileSync, statSync, accessSync, unlinkSync, chmodSync, constants } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { homedir } from "node:os";
9
- import { execFileSync } from "node:child_process";
9
+ import { execFileSync, spawnSync } from "node:child_process";
10
10
 
11
11
  import { resolveLinearToken, LinearAgentApi, AUTH_PROFILES_PATH, LINEAR_GRAPHQL_URL } from "../api/linear-api.js";
12
12
  import { readDispatchState, listActiveDispatches, listStaleDispatches, pruneCompleted, type DispatchState } from "../pipeline/dispatch-state.js";
13
13
  import { loadPrompts, clearPromptCache } from "../pipeline/pipeline.js";
14
14
  import { listWorktrees } from "./codex-worktree.js";
15
- import { loadCodingConfig, type CodingBackend } from "../tools/code-tool.js";
15
+ import { loadCodingConfig, resolveCodingBackend, type CodingBackend } from "../tools/code-tool.js";
16
16
  import { getWebhookStatus, provisionWebhook, REQUIRED_RESOURCE_TYPES } from "./webhook-provision.js";
17
17
 
18
18
  // ---------------------------------------------------------------------------
@@ -767,19 +767,209 @@ export async function runDoctor(opts: DoctorOptions): Promise<DoctorReport> {
767
767
  }
768
768
  }
769
769
 
770
- // Build summary
771
- let passed = 0, warnings = 0, errors = 0;
772
- for (const section of sections) {
773
- for (const check of section.checks) {
774
- switch (check.severity) {
775
- case "pass": passed++; break;
776
- case "warn": warnings++; break;
777
- case "fail": errors++; break;
770
+ return { sections, summary: buildSummary(sections) };
771
+ }
772
+
773
+ // ---------------------------------------------------------------------------
774
+ // Code Run Deep Checks
775
+ // ---------------------------------------------------------------------------
776
+
777
+ interface BackendSpec {
778
+ id: CodingBackend;
779
+ label: string;
780
+ bin: string;
781
+ /** CLI args for a minimal live invocation test */
782
+ testArgs: string[];
783
+ /** Environment variable names that provide an API key */
784
+ envKeys: string[];
785
+ /** Plugin config key for API key (if any) */
786
+ configKey?: string;
787
+ /** Env vars to unset before spawning (e.g. CLAUDECODE) */
788
+ unsetEnv?: string[];
789
+ }
790
+
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
+ ];
816
+
817
+ function checkBackendBinary(spec: BackendSpec): { installed: boolean; checks: CheckResult[] } {
818
+ const checks: CheckResult[] = [];
819
+
820
+ // Binary existence
821
+ try {
822
+ accessSync(spec.bin, constants.X_OK);
823
+ } catch {
824
+ checks.push(fail(
825
+ `Binary: not found at ${spec.bin}`,
826
+ undefined,
827
+ `Install ${spec.id}: npm install -g <package>`,
828
+ ));
829
+ return { installed: false, checks };
830
+ }
831
+
832
+ // Version check
833
+ try {
834
+ const env = { ...process.env } as Record<string, string | undefined>;
835
+ for (const key of spec.unsetEnv ?? []) delete env[key];
836
+ const raw = execFileSync(spec.bin, ["--version"], {
837
+ encoding: "utf8",
838
+ timeout: 15_000,
839
+ env: env as NodeJS.ProcessEnv,
840
+ }).trim();
841
+ checks.push(pass(`Binary: ${raw || "installed"} (${spec.bin})`));
842
+ } catch {
843
+ checks.push(pass(`Binary: installed (${spec.bin})`));
844
+ }
845
+
846
+ return { installed: true, checks };
847
+ }
848
+
849
+ function checkBackendApiKey(spec: BackendSpec, pluginConfig?: Record<string, unknown>): CheckResult {
850
+ // Check plugin config first
851
+ if (spec.configKey) {
852
+ const configVal = pluginConfig?.[spec.configKey];
853
+ if (typeof configVal === "string" && configVal) {
854
+ return pass(`API key: configured (${spec.configKey} in plugin config)`);
855
+ }
856
+ }
857
+
858
+ // Check env vars
859
+ for (const envKey of spec.envKeys) {
860
+ if (process.env[envKey]) {
861
+ return pass(`API key: configured (${envKey})`);
862
+ }
863
+ }
864
+
865
+ return warn(
866
+ `API key: not found`,
867
+ `Checked: ${spec.envKeys.join(", ")}${spec.configKey ? `, pluginConfig.${spec.configKey}` : ""}`,
868
+ { fix: `Set ${spec.envKeys[0]} environment variable or configure in plugin config` },
869
+ );
870
+ }
871
+
872
+ function checkBackendLive(spec: BackendSpec, pluginConfig?: Record<string, unknown>): CheckResult {
873
+ const env = { ...process.env } as Record<string, string | undefined>;
874
+ for (const key of spec.unsetEnv ?? []) delete env[key];
875
+
876
+ // Pass API key from plugin config if available (Claude-specific)
877
+ if (spec.configKey) {
878
+ const configVal = pluginConfig?.[spec.configKey] as string | undefined;
879
+ if (configVal && spec.envKeys[0]) {
880
+ env[spec.envKeys[0]] = configVal;
881
+ }
882
+ }
883
+
884
+ const start = Date.now();
885
+ try {
886
+ const result = spawnSync(spec.bin, spec.testArgs, {
887
+ encoding: "utf8",
888
+ timeout: 30_000,
889
+ env: env as NodeJS.ProcessEnv,
890
+ stdio: ["ignore", "pipe", "pipe"],
891
+ });
892
+
893
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
894
+
895
+ if (result.error) {
896
+ const msg = result.error.message ?? String(result.error);
897
+ if (msg.includes("ETIMEDOUT") || msg.includes("timed out")) {
898
+ return warn(`Live test: timed out after 30s`);
778
899
  }
900
+ return warn(`Live test: spawn error — ${msg.slice(0, 200)}`);
779
901
  }
902
+
903
+ if (result.status === 0) {
904
+ return pass(`Live test: responded in ${elapsed}s`);
905
+ }
906
+
907
+ // Non-zero exit
908
+ const stderr = (result.stderr ?? "").trim().slice(0, 200);
909
+ const stdout = (result.stdout ?? "").trim().slice(0, 200);
910
+ const detail = stderr || stdout || "(no output)";
911
+ return warn(`Live test: exit code ${result.status} (${elapsed}s) — ${detail}`);
912
+ } catch (err) {
913
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
914
+ return warn(`Live test: error (${elapsed}s) — ${err instanceof Error ? err.message.slice(0, 200) : String(err)}`);
780
915
  }
916
+ }
917
+
918
+ /**
919
+ * Deep health checks for coding tool backends.
920
+ *
921
+ * Verifies binary installation, API key configuration, and live callability
922
+ * for each backend (Claude, Codex, Gemini). Also shows agent routing.
923
+ *
924
+ * Usage: openclaw openclaw-linear code-run doctor [--json]
925
+ */
926
+ export async function checkCodeRunDeep(
927
+ pluginConfig?: Record<string, unknown>,
928
+ ): Promise<CheckSection[]> {
929
+ const sections: CheckSection[] = [];
930
+ const config = loadCodingConfig();
931
+ let callableCount = 0;
932
+
933
+ for (const spec of BACKEND_SPECS) {
934
+ const checks: CheckResult[] = [];
935
+
936
+ // 1. Binary check
937
+ const { installed, checks: binChecks } = checkBackendBinary(spec);
938
+ checks.push(...binChecks);
939
+
940
+ if (installed) {
941
+ // 2. API key check
942
+ checks.push(checkBackendApiKey(spec, pluginConfig));
943
+
944
+ // 3. Live invocation test
945
+ const liveResult = checkBackendLive(spec, pluginConfig);
946
+ checks.push(liveResult);
781
947
 
782
- return { sections, summary: { passed, warnings, errors } };
948
+ if (liveResult.severity === "pass") callableCount++;
949
+ }
950
+
951
+ sections.push({ name: `Code Run: ${spec.label}`, checks });
952
+ }
953
+
954
+ // Routing summary section
955
+ const routingChecks: CheckResult[] = [];
956
+ const defaultBackend = resolveCodingBackend(config);
957
+ routingChecks.push(pass(`Default backend: ${defaultBackend}`));
958
+
959
+ const profiles = loadAgentProfiles();
960
+ for (const [agentId, profile] of Object.entries(profiles)) {
961
+ const resolved = resolveCodingBackend(config, agentId);
962
+ const isOverride = config.agentCodingTools?.[agentId] != null;
963
+ const label = profile.label ?? agentId;
964
+ routingChecks.push(pass(
965
+ `${label} → ${resolved}${isOverride ? " (override)" : " (default)"}`,
966
+ ));
967
+ }
968
+
969
+ routingChecks.push(pass(`Callable backends: ${callableCount}/${BACKEND_SPECS.length}`));
970
+ sections.push({ name: "Code Run: Routing", checks: routingChecks });
971
+
972
+ return sections;
783
973
  }
784
974
 
785
975
  // ---------------------------------------------------------------------------
@@ -831,3 +1021,18 @@ export function formatReport(report: DoctorReport): string {
831
1021
  export function formatReportJson(report: DoctorReport): string {
832
1022
  return JSON.stringify(report, null, 2);
833
1023
  }
1024
+
1025
+ /** Build a summary by counting pass/warn/fail across sections. */
1026
+ export function buildSummary(sections: CheckSection[]): { passed: number; warnings: number; errors: number } {
1027
+ let passed = 0, warnings = 0, errors = 0;
1028
+ for (const section of sections) {
1029
+ for (const check of section.checks) {
1030
+ switch (check.severity) {
1031
+ case "pass": passed++; break;
1032
+ case "warn": warnings++; break;
1033
+ case "fail": errors++; break;
1034
+ }
1035
+ }
1036
+ }
1037
+ return { passed, warnings, errors };
1038
+ }