@calltelemetry/openclaw-linear 0.8.4 → 0.8.5
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 +43 -2
- package/package.json +1 -1
- package/src/__test__/fixtures/linear-responses.ts +2 -1
- package/src/api/linear-api.ts +2 -0
- package/src/infra/cli.ts +27 -0
- package/src/infra/doctor.test.ts +130 -3
- package/src/infra/doctor.ts +216 -11
- package/src/pipeline/webhook.ts +95 -20
package/README.md
CHANGED
|
@@ -904,9 +904,45 @@ Example output:
|
|
|
904
904
|
|
|
905
905
|
Every warning and error includes a `→` line telling you what to do. Run `doctor --fix` to auto-repair what it can.
|
|
906
906
|
|
|
907
|
+
### Code-run health check
|
|
908
|
+
|
|
909
|
+
For deeper diagnostics on coding tool backends (Claude Code, Codex, Gemini CLI), run the dedicated code-run doctor. It checks binary installation, API key configuration, and actually invokes each backend to verify it can authenticate and respond:
|
|
910
|
+
|
|
911
|
+
```bash
|
|
912
|
+
openclaw openclaw-linear code-run doctor
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
Example output:
|
|
916
|
+
|
|
917
|
+
```
|
|
918
|
+
Code Run: Claude Code (Anthropic)
|
|
919
|
+
✓ Binary: 2.1.50 (/home/claw/.npm-global/bin/claude)
|
|
920
|
+
✓ API key: configured (ANTHROPIC_API_KEY)
|
|
921
|
+
✓ Live test: responded in 3.2s
|
|
922
|
+
|
|
923
|
+
Code Run: Codex (OpenAI)
|
|
924
|
+
✓ Binary: 0.101.0 (/home/claw/.npm-global/bin/codex)
|
|
925
|
+
✓ API key: configured (OPENAI_API_KEY)
|
|
926
|
+
✓ Live test: responded in 2.8s
|
|
927
|
+
|
|
928
|
+
Code Run: Gemini CLI (Google)
|
|
929
|
+
✓ Binary: 0.28.2 (/home/claw/.npm-global/bin/gemini)
|
|
930
|
+
✓ API key: configured (GEMINI_API_KEY)
|
|
931
|
+
✓ Live test: responded in 4.1s
|
|
932
|
+
|
|
933
|
+
Code Run: Routing
|
|
934
|
+
✓ Default backend: codex
|
|
935
|
+
✓ Mal → codex (default)
|
|
936
|
+
✓ Kaylee → codex (default)
|
|
937
|
+
✓ Inara → claude (override)
|
|
938
|
+
✓ Callable backends: 3/3
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
This is separate from the main `doctor` because each live test spawns a real CLI subprocess (~5-10s per backend). Use `--json` for machine-readable output.
|
|
942
|
+
|
|
907
943
|
### Unit tests
|
|
908
944
|
|
|
909
|
-
|
|
945
|
+
532 tests covering the full pipeline — triage, dispatch, audit, planning, intent classification, cross-model review, notifications, and infrastructure:
|
|
910
946
|
|
|
911
947
|
```bash
|
|
912
948
|
cd ~/claw-extensions/linear
|
|
@@ -1069,6 +1105,10 @@ openclaw openclaw-linear webhooks delete <id> # Delete a webhook by ID
|
|
|
1069
1105
|
openclaw openclaw-linear doctor # Run health checks
|
|
1070
1106
|
openclaw openclaw-linear doctor --fix # Auto-fix issues
|
|
1071
1107
|
openclaw openclaw-linear doctor --json # JSON output
|
|
1108
|
+
|
|
1109
|
+
# Code-run backends
|
|
1110
|
+
openclaw openclaw-linear code-run doctor # Deep check all backends (binary, API key, live test)
|
|
1111
|
+
openclaw openclaw-linear code-run doctor --json # JSON output
|
|
1072
1112
|
```
|
|
1073
1113
|
|
|
1074
1114
|
---
|
|
@@ -1089,7 +1129,8 @@ journalctl --user -u openclaw-gateway -f # Watch live logs
|
|
|
1089
1129
|
|---|---|
|
|
1090
1130
|
| Agent goes silent | Watchdog auto-kills after `inactivitySec` and retries. Check logs for `Watchdog KILL`. |
|
|
1091
1131
|
| Dispatch stuck after watchdog | Both retries failed. Check `.claw/log.jsonl`. Re-assign issue to restart. |
|
|
1092
|
-
| `code_run` uses wrong backend | Check `coding-tools.json` — explicit backend > per-agent > global default. |
|
|
1132
|
+
| `code_run` uses wrong backend | Check `coding-tools.json` — explicit backend > per-agent > global default. Run `code-run doctor` to see routing. |
|
|
1133
|
+
| `code_run` fails at runtime | Run `openclaw openclaw-linear code-run doctor` — checks binary, API key, and live callability for each backend. |
|
|
1093
1134
|
| Webhook events not arriving | Run `openclaw openclaw-linear webhooks setup` to auto-provision. Both webhooks must point to `/linear/webhook`. Check tunnel is running. |
|
|
1094
1135
|
| OAuth token expired | Auto-refreshes. If stuck, re-run `openclaw openclaw-linear auth` and restart. |
|
|
1095
1136
|
| Audit always fails | Run `openclaw openclaw-linear prompts validate` to check prompt syntax. |
|
package/package.json
CHANGED
|
@@ -11,7 +11,8 @@ export function makeIssueDetails(overrides?: Record<string, unknown>) {
|
|
|
11
11
|
title: "Fix webhook routing",
|
|
12
12
|
description: "The webhook handler needs fixing.",
|
|
13
13
|
estimate: 3,
|
|
14
|
-
state: { name: "In Progress" },
|
|
14
|
+
state: { name: "In Progress", type: "started" },
|
|
15
|
+
creator: { name: "Test User", email: "test@example.com" },
|
|
15
16
|
assignee: { name: "Agent" },
|
|
16
17
|
labels: { nodes: [] as Array<{ id: string; name: string }> },
|
|
17
18
|
team: { id: "team-1", name: "Engineering", issueEstimationType: "notUsed" },
|
package/src/api/linear-api.ts
CHANGED
|
@@ -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")
|
package/src/infra/doctor.test.ts
CHANGED
|
@@ -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: "
|
|
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("
|
|
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 — these are fast (no live invocation), use separate calls
|
|
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
|
+
});
|
|
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
|
+
});
|
|
560
|
+
});
|
package/src/infra/doctor.ts
CHANGED
|
@@ -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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/pipeline/webhook.ts
CHANGED
|
@@ -325,16 +325,35 @@ export async function handleLinearWebhook(
|
|
|
325
325
|
.join("\n\n");
|
|
326
326
|
|
|
327
327
|
const issueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
|
|
328
|
+
const stateType = enrichedIssue?.state?.type ?? "";
|
|
329
|
+
const isTriaged = stateType === "started" || stateType === "completed" || stateType === "canceled";
|
|
330
|
+
|
|
331
|
+
const toolAccessLines = isTriaged
|
|
332
|
+
? [
|
|
333
|
+
`**Tool access:**`,
|
|
334
|
+
`- \`linearis\` CLI: Full access. You can read, update, close, and comment on issues. Use \`linearis issues update ${issueRef} --state <state>\` to change status, \`linearis issues update ${issueRef} --priority <1-4>\` to set priority, etc.`,
|
|
335
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
336
|
+
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
337
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
338
|
+
]
|
|
339
|
+
: [
|
|
340
|
+
`**Tool access:**`,
|
|
341
|
+
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${issueRef}\`), list issues (\`linearis issues list\`), and search (\`linearis issues search "..."\`). Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
342
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
343
|
+
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
344
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
const roleLines = isTriaged
|
|
348
|
+
? [`**Your role:** Orchestrator with full Linear access. You can update issue fields, change status, and dispatch work via \`code_run\`. Do NOT post comments yourself — the handler posts your text output.`]
|
|
349
|
+
: [`**Your role:** You are the dispatcher. For any coding or implementation work, use \`code_run\` to dispatch it. Workers return text output. You summarize results. You do NOT update issue status or post linearis comments — the audit system handles lifecycle transitions.`];
|
|
350
|
+
|
|
328
351
|
const message = [
|
|
329
352
|
`You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
|
|
330
353
|
``,
|
|
331
|
-
|
|
332
|
-
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${issueRef}\`), list issues (\`linearis issues list\`), and search (\`linearis issues search "..."\`). Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
333
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
334
|
-
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
335
|
-
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
354
|
+
...toolAccessLines,
|
|
336
355
|
``,
|
|
337
|
-
|
|
356
|
+
...roleLines,
|
|
338
357
|
``,
|
|
339
358
|
`## Issue: ${issueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
340
359
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
@@ -367,7 +386,7 @@ export async function handleLinearWebhook(
|
|
|
367
386
|
// Emit initial thought
|
|
368
387
|
await linearApi.emitActivity(session.id, {
|
|
369
388
|
type: "thought",
|
|
370
|
-
body:
|
|
389
|
+
body: `${label} is processing request for ${enrichedIssue?.identifier ?? issue.id}...`,
|
|
371
390
|
}).catch(() => {});
|
|
372
391
|
|
|
373
392
|
// Run agent with streaming to Linear
|
|
@@ -503,16 +522,35 @@ export async function handleLinearWebhook(
|
|
|
503
522
|
.join("\n\n");
|
|
504
523
|
|
|
505
524
|
const followUpIssueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
|
|
525
|
+
const followUpStateType = enrichedIssue?.state?.type ?? "";
|
|
526
|
+
const followUpIsTriaged = followUpStateType === "started" || followUpStateType === "completed" || followUpStateType === "canceled";
|
|
527
|
+
|
|
528
|
+
const followUpToolAccessLines = followUpIsTriaged
|
|
529
|
+
? [
|
|
530
|
+
`**Tool access:**`,
|
|
531
|
+
`- \`linearis\` CLI: Full access. You can read, update, close, and comment on issues. Use \`linearis issues update ${followUpIssueRef} --state <state>\` to change status, \`linearis issues update ${followUpIssueRef} --priority <1-4>\` to set priority, etc.`,
|
|
532
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
533
|
+
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
534
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
535
|
+
]
|
|
536
|
+
: [
|
|
537
|
+
`**Tool access:**`,
|
|
538
|
+
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${followUpIssueRef}\`), list, and search. Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
539
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
540
|
+
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
541
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
542
|
+
];
|
|
543
|
+
|
|
544
|
+
const followUpRoleLines = followUpIsTriaged
|
|
545
|
+
? [`**Your role:** Orchestrator with full Linear access. You can update issue fields, change status, and dispatch work via \`code_run\`. Do NOT post comments yourself — the handler posts your text output.`]
|
|
546
|
+
: [`**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`];
|
|
547
|
+
|
|
506
548
|
const message = [
|
|
507
549
|
`You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
|
|
508
550
|
``,
|
|
509
|
-
|
|
510
|
-
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${followUpIssueRef}\`), list, and search. Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
511
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
512
|
-
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
513
|
-
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
551
|
+
...followUpToolAccessLines,
|
|
514
552
|
``,
|
|
515
|
-
|
|
553
|
+
...followUpRoleLines,
|
|
516
554
|
``,
|
|
517
555
|
`## Issue: ${followUpIssueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
518
556
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
@@ -536,7 +574,7 @@ export async function handleLinearWebhook(
|
|
|
536
574
|
try {
|
|
537
575
|
await linearApi.emitActivity(session.id, {
|
|
538
576
|
type: "thought",
|
|
539
|
-
body:
|
|
577
|
+
body: `${label} is processing follow-up for ${enrichedIssue?.identifier ?? issue.id}...`,
|
|
540
578
|
}).catch(() => {});
|
|
541
579
|
|
|
542
580
|
const sessionId = `linear-session-${session.id}`;
|
|
@@ -1006,11 +1044,18 @@ export async function handleLinearWebhook(
|
|
|
1006
1044
|
}).catch(() => {});
|
|
1007
1045
|
}
|
|
1008
1046
|
|
|
1047
|
+
const creatorName = enrichedIssue?.creator?.name ?? "Unknown";
|
|
1048
|
+
const creatorEmail = enrichedIssue?.creator?.email ?? null;
|
|
1049
|
+
const creatorLine = creatorEmail
|
|
1050
|
+
? `**Created by:** ${creatorName} (${creatorEmail})`
|
|
1051
|
+
: `**Created by:** ${creatorName}`;
|
|
1052
|
+
|
|
1009
1053
|
const message = [
|
|
1010
1054
|
`IMPORTANT: You are triaging a new Linear issue. You MUST respond with a JSON block containing your triage decisions, followed by your assessment as plain text.`,
|
|
1011
1055
|
``,
|
|
1012
1056
|
`## Issue: ${enrichedIssue?.identifier ?? issue.identifier ?? issue.id} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
1013
1057
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Current Estimate:** ${enrichedIssue?.estimate ?? "None"} | **Current Labels:** ${currentLabelNames}`,
|
|
1058
|
+
creatorLine,
|
|
1014
1059
|
``,
|
|
1015
1060
|
`**Description:**`,
|
|
1016
1061
|
description,
|
|
@@ -1038,6 +1083,8 @@ export async function handleLinearWebhook(
|
|
|
1038
1083
|
`}`,
|
|
1039
1084
|
'```',
|
|
1040
1085
|
``,
|
|
1086
|
+
`IMPORTANT: Only reference real users from the issue data above. Do NOT fabricate or guess user names, emails, or identities. The issue creator is shown in the "Created by" field.`,
|
|
1087
|
+
``,
|
|
1041
1088
|
`Then write your full assessment as markdown below the JSON block.`,
|
|
1042
1089
|
].filter(Boolean).join("\n");
|
|
1043
1090
|
|
|
@@ -1191,24 +1238,45 @@ async function dispatchCommentToAgent(
|
|
|
1191
1238
|
.join("\n");
|
|
1192
1239
|
|
|
1193
1240
|
const issueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
|
|
1241
|
+
const stateType = enrichedIssue?.state?.type ?? "";
|
|
1242
|
+
const isTriaged = stateType === "started" || stateType === "completed" || stateType === "canceled";
|
|
1243
|
+
|
|
1244
|
+
const toolAccessLines = isTriaged
|
|
1245
|
+
? [
|
|
1246
|
+
`**Tool access:**`,
|
|
1247
|
+
`- \`linearis\` CLI: Full access. You can read, update, close, and comment on issues. Use \`linearis issues update ${issueRef} --state <state>\` to change status, \`linearis issues update ${issueRef} --priority <1-4>\` to set priority, etc.`,
|
|
1248
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
1249
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
1250
|
+
]
|
|
1251
|
+
: [
|
|
1252
|
+
`**Tool access:**`,
|
|
1253
|
+
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${issueRef}\`), list, and search. Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
1254
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
1255
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
1256
|
+
];
|
|
1257
|
+
|
|
1258
|
+
const roleLines = isTriaged
|
|
1259
|
+
? [`**Your role:** Orchestrator with full Linear access. You can update issue fields, change status, and dispatch work via \`code_run\`. Do NOT post comments yourself — the handler posts your text output.`]
|
|
1260
|
+
: [`**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`];
|
|
1261
|
+
|
|
1194
1262
|
const message = [
|
|
1195
1263
|
`You are an orchestrator responding to a Linear comment. Your text output will be automatically posted as a comment on the issue (do NOT post a comment yourself — the handler does it).`,
|
|
1196
1264
|
``,
|
|
1197
|
-
|
|
1198
|
-
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${issueRef}\`), list, and search. Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
1199
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
1200
|
-
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
1265
|
+
...toolAccessLines,
|
|
1201
1266
|
``,
|
|
1202
|
-
|
|
1267
|
+
...roleLines,
|
|
1203
1268
|
``,
|
|
1204
1269
|
`## Issue: ${issueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
1205
1270
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
1271
|
+
enrichedIssue?.creator ? `**Created by:** ${enrichedIssue.creator.name}${enrichedIssue.creator.email ? ` (${enrichedIssue.creator.email})` : ""}` : "",
|
|
1206
1272
|
``,
|
|
1207
1273
|
`**Description:**`,
|
|
1208
1274
|
description,
|
|
1209
1275
|
commentSummary ? `\n**Recent comments:**\n${commentSummary}` : "",
|
|
1210
1276
|
`\n**${commentor} says:**\n> ${commentBody}`,
|
|
1211
1277
|
``,
|
|
1278
|
+
`IMPORTANT: Only reference real users from the issue data above. Do NOT fabricate or guess user names, emails, or identities.`,
|
|
1279
|
+
``,
|
|
1212
1280
|
`Respond concisely. For work requests, dispatch via \`code_run\` and summarize the result.`,
|
|
1213
1281
|
].filter(Boolean).join("\n");
|
|
1214
1282
|
|
|
@@ -1354,12 +1422,15 @@ async function handleCloseIssue(
|
|
|
1354
1422
|
``,
|
|
1355
1423
|
`## Issue: ${issueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
1356
1424
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
1425
|
+
enrichedIssue?.creator ? `**Created by:** ${enrichedIssue.creator.name}${enrichedIssue.creator.email ? ` (${enrichedIssue.creator.email})` : ""}` : "",
|
|
1357
1426
|
``,
|
|
1358
1427
|
`**Description:**`,
|
|
1359
1428
|
description,
|
|
1360
1429
|
commentSummary ? `\n**Comment history:**\n${commentSummary}` : "",
|
|
1361
1430
|
`\n**${commentor} says (closure request):**\n> ${commentBody}`,
|
|
1362
1431
|
``,
|
|
1432
|
+
`IMPORTANT: Only reference real users from the issue data above. Do NOT fabricate or guess user names, emails, or identities.`,
|
|
1433
|
+
``,
|
|
1363
1434
|
`Write a concise closure report with:`,
|
|
1364
1435
|
`- **Summary**: What was done (1-2 sentences)`,
|
|
1365
1436
|
`- **Resolution**: How it was resolved`,
|
|
@@ -1404,9 +1475,13 @@ async function handleCloseIssue(
|
|
|
1404
1475
|
readOnly: true,
|
|
1405
1476
|
});
|
|
1406
1477
|
|
|
1478
|
+
if (!result.success) {
|
|
1479
|
+
api.logger.error(`Closure report agent failed for ${issueRef}: ${(result.output ?? "no output").slice(0, 500)}`);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1407
1482
|
const closureReport = result.success
|
|
1408
1483
|
? result.output
|
|
1409
|
-
:
|
|
1484
|
+
: `Issue closed by ${commentor}.\n\n> ${commentBody}\n\n*Closure report generation failed — agent returned: ${(result.output ?? "no output").slice(0, 200)}*`;
|
|
1410
1485
|
|
|
1411
1486
|
const fullReport = `## Closure Report\n\n${closureReport}`;
|
|
1412
1487
|
|