@calltelemetry/openclaw-linear 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +359 -195
- package/index.ts +10 -10
- package/openclaw.plugin.json +4 -1
- package/package.json +9 -2
- package/src/agent/agent.test.ts +127 -0
- package/src/{agent.ts → agent/agent.ts} +84 -7
- package/src/agent/watchdog.test.ts +266 -0
- package/src/agent/watchdog.ts +176 -0
- package/src/{cli.ts → infra/cli.ts} +5 -5
- package/src/{codex-worktree.ts → infra/codex-worktree.ts} +4 -0
- package/src/infra/notify.test.ts +169 -0
- package/src/{notify.ts → infra/notify.ts} +6 -1
- package/src/pipeline/active-session.test.ts +154 -0
- package/src/pipeline/artifacts.test.ts +383 -0
- package/src/pipeline/artifacts.ts +273 -0
- package/src/{dispatch-service.ts → pipeline/dispatch-service.ts} +1 -1
- package/src/pipeline/dispatch-state.test.ts +382 -0
- package/src/{dispatch-state.ts → pipeline/dispatch-state.ts} +1 -0
- package/src/pipeline/pipeline.test.ts +226 -0
- package/src/{pipeline.ts → pipeline/pipeline.ts} +134 -10
- package/src/{tier-assess.ts → pipeline/tier-assess.ts} +1 -1
- package/src/{webhook.test.ts → pipeline/webhook.test.ts} +1 -1
- package/src/{webhook.ts → pipeline/webhook.ts} +30 -8
- package/src/{claude-tool.ts → tools/claude-tool.ts} +31 -5
- package/src/{cli-shared.ts → tools/cli-shared.ts} +5 -4
- package/src/{code-tool.ts → tools/code-tool.ts} +2 -2
- package/src/{codex-tool.ts → tools/codex-tool.ts} +31 -5
- package/src/{gemini-tool.ts → tools/gemini-tool.ts} +31 -5
- package/src/{orchestration-tools.ts → tools/orchestration-tools.ts} +1 -1
- package/src/client.ts +0 -94
- /package/src/{auth.ts → api/auth.ts} +0 -0
- /package/src/{linear-api.ts → api/linear-api.ts} +0 -0
- /package/src/{oauth-callback.ts → api/oauth-callback.ts} +0 -0
- /package/src/{active-session.ts → pipeline/active-session.ts} +0 -0
- /package/src/{tools.ts → tools/tools.ts} +0 -0
|
@@ -16,8 +16,8 @@ import { join, dirname } from "node:path";
|
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { parse as parseYaml } from "yaml";
|
|
18
18
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
19
|
-
import type { LinearAgentApi, ActivityContent } from "
|
|
20
|
-
import { runAgent } from "
|
|
19
|
+
import type { LinearAgentApi, ActivityContent } from "../api/linear-api.js";
|
|
20
|
+
import { runAgent } from "../agent/agent.js";
|
|
21
21
|
import { setActiveSession, clearActiveSession } from "./active-session.js";
|
|
22
22
|
import {
|
|
23
23
|
type Tier,
|
|
@@ -33,7 +33,18 @@ import {
|
|
|
33
33
|
readDispatchState,
|
|
34
34
|
getActiveDispatch,
|
|
35
35
|
} from "./dispatch-state.js";
|
|
36
|
-
import { type NotifyFn } from "
|
|
36
|
+
import { type NotifyFn } from "../infra/notify.js";
|
|
37
|
+
import {
|
|
38
|
+
saveWorkerOutput,
|
|
39
|
+
saveAuditVerdict,
|
|
40
|
+
appendLog,
|
|
41
|
+
updateManifest,
|
|
42
|
+
writeSummary,
|
|
43
|
+
buildSummaryFromArtifacts,
|
|
44
|
+
writeDispatchMemory,
|
|
45
|
+
resolveOrchestratorWorkspace,
|
|
46
|
+
} from "./artifacts.js";
|
|
47
|
+
import { resolveWatchdogConfig } from "../agent/watchdog.js";
|
|
37
48
|
|
|
38
49
|
// ---------------------------------------------------------------------------
|
|
39
50
|
// Prompt loading
|
|
@@ -76,7 +87,7 @@ export function loadPrompts(pluginConfig?: Record<string, unknown>): PromptTempl
|
|
|
76
87
|
raw = readFileSync(resolved, "utf-8");
|
|
77
88
|
} else {
|
|
78
89
|
// Load from plugin directory (sidecar file)
|
|
79
|
-
const pluginRoot = join(dirname(fileURLToPath(import.meta.url)), "
|
|
90
|
+
const pluginRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
80
91
|
raw = readFileSync(join(pluginRoot, "prompts.yaml"), "utf-8");
|
|
81
92
|
}
|
|
82
93
|
|
|
@@ -264,6 +275,9 @@ export async function triggerAudit(
|
|
|
264
275
|
|
|
265
276
|
api.logger.info(`${TAG} worker completed, triggering audit (attempt ${dispatch.attempt})`);
|
|
266
277
|
|
|
278
|
+
// Update .claw/ manifest
|
|
279
|
+
try { updateManifest(dispatch.worktreePath, { status: "auditing", attempts: dispatch.attempt }); } catch {}
|
|
280
|
+
|
|
267
281
|
// Fetch fresh issue details for audit context
|
|
268
282
|
const issueDetails = await linearApi.getIssueDetails(dispatch.issueId).catch(() => null);
|
|
269
283
|
const issue: IssueContext = {
|
|
@@ -313,7 +327,9 @@ export async function triggerAudit(
|
|
|
313
327
|
agentId: (pluginConfig?.defaultAgentId as string) ?? "default",
|
|
314
328
|
sessionId: auditSessionId,
|
|
315
329
|
message: `${auditPrompt.system}\n\n${auditPrompt.task}`,
|
|
316
|
-
|
|
330
|
+
streaming: dispatch.agentSessionId
|
|
331
|
+
? { linearApi, agentSessionId: dispatch.agentSessionId }
|
|
332
|
+
: undefined,
|
|
317
333
|
});
|
|
318
334
|
|
|
319
335
|
// runAgent returns inline (embedded runner) — process verdict directly.
|
|
@@ -371,6 +387,15 @@ export async function processVerdict(
|
|
|
371
387
|
}
|
|
372
388
|
}
|
|
373
389
|
|
|
390
|
+
// Log audit interaction to .claw/
|
|
391
|
+
try {
|
|
392
|
+
appendLog(dispatch.worktreePath, {
|
|
393
|
+
ts: new Date().toISOString(), phase: "audit", attempt: dispatch.attempt,
|
|
394
|
+
agent: "auditor", prompt: "(audit task)",
|
|
395
|
+
outputPreview: auditOutput.slice(0, 500), success: event.success,
|
|
396
|
+
});
|
|
397
|
+
} catch {}
|
|
398
|
+
|
|
374
399
|
// Parse verdict
|
|
375
400
|
const verdict = parseVerdict(auditOutput);
|
|
376
401
|
if (!verdict) {
|
|
@@ -406,9 +431,13 @@ async function handleAuditPass(
|
|
|
406
431
|
dispatch: ActiveDispatch,
|
|
407
432
|
verdict: AuditVerdict,
|
|
408
433
|
): Promise<void> {
|
|
409
|
-
const { api, linearApi, notify, configPath } = hookCtx;
|
|
434
|
+
const { api, linearApi, notify, pluginConfig, configPath } = hookCtx;
|
|
410
435
|
const TAG = `[${dispatch.issueIdentifier}]`;
|
|
411
436
|
|
|
437
|
+
// Save audit verdict to .claw/
|
|
438
|
+
try { saveAuditVerdict(dispatch.worktreePath, dispatch.attempt, verdict); } catch {}
|
|
439
|
+
try { updateManifest(dispatch.worktreePath, { status: "done", attempts: dispatch.attempt + 1 }); } catch {}
|
|
440
|
+
|
|
412
441
|
// CAS transition: auditing → done
|
|
413
442
|
try {
|
|
414
443
|
await transitionDispatch(dispatch.issueIdentifier, "auditing", "done", undefined, configPath);
|
|
@@ -428,11 +457,26 @@ async function handleAuditPass(
|
|
|
428
457
|
project: dispatch.project,
|
|
429
458
|
}, configPath);
|
|
430
459
|
|
|
431
|
-
//
|
|
460
|
+
// Build summary from .claw/ artifacts and write to memory
|
|
461
|
+
let summary: string | null = null;
|
|
462
|
+
try {
|
|
463
|
+
summary = buildSummaryFromArtifacts(dispatch.worktreePath);
|
|
464
|
+
if (summary) {
|
|
465
|
+
writeSummary(dispatch.worktreePath, summary);
|
|
466
|
+
const wsDir = resolveOrchestratorWorkspace(api, pluginConfig);
|
|
467
|
+
writeDispatchMemory(dispatch.issueIdentifier, summary, wsDir);
|
|
468
|
+
api.logger.info(`${TAG} .claw/ summary and memory written`);
|
|
469
|
+
}
|
|
470
|
+
} catch (err) {
|
|
471
|
+
api.logger.warn(`${TAG} failed to write summary/memory: ${err}`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Post approval comment (with summary excerpt if available)
|
|
432
475
|
const criteriaList = verdict.criteria.map((c) => `- ${c}`).join("\n");
|
|
476
|
+
const summaryExcerpt = summary ? `\n\n**Summary:**\n${summary.slice(0, 2000)}` : "";
|
|
433
477
|
await linearApi.createComment(
|
|
434
478
|
dispatch.issueId,
|
|
435
|
-
`## Audit Passed\n\n**Criteria verified:**\n${criteriaList}\n\n**Tests:** ${verdict.testResults || "N/A"}\n\n---\n*Attempt ${dispatch.attempt + 1} — audit passed
|
|
479
|
+
`## Audit Passed\n\n**Criteria verified:**\n${criteriaList}\n\n**Tests:** ${verdict.testResults || "N/A"}${summaryExcerpt}\n\n---\n*Attempt ${dispatch.attempt + 1} — audit passed. Artifacts: \`${dispatch.worktreePath}/.claw/\`*`,
|
|
436
480
|
).catch((err) => api.logger.error(`${TAG} failed to post audit pass comment: ${err}`));
|
|
437
481
|
|
|
438
482
|
api.logger.info(`${TAG} audit PASSED — dispatch completed (attempt ${dispatch.attempt})`);
|
|
@@ -458,8 +502,13 @@ async function handleAuditFail(
|
|
|
458
502
|
const maxAttempts = (pluginConfig?.maxReworkAttempts as number) ?? 2;
|
|
459
503
|
const nextAttempt = dispatch.attempt + 1;
|
|
460
504
|
|
|
505
|
+
// Save audit verdict to .claw/ (both escalation and rework paths)
|
|
506
|
+
try { saveAuditVerdict(dispatch.worktreePath, dispatch.attempt, verdict); } catch {}
|
|
507
|
+
|
|
461
508
|
if (nextAttempt > maxAttempts) {
|
|
462
509
|
// Escalate — too many failures
|
|
510
|
+
try { updateManifest(dispatch.worktreePath, { status: "stuck", attempts: nextAttempt }); } catch {}
|
|
511
|
+
|
|
463
512
|
try {
|
|
464
513
|
await transitionDispatch(
|
|
465
514
|
dispatch.issueIdentifier,
|
|
@@ -476,10 +525,20 @@ async function handleAuditFail(
|
|
|
476
525
|
throw err;
|
|
477
526
|
}
|
|
478
527
|
|
|
528
|
+
// Write summary + memory for stuck dispatches too
|
|
529
|
+
try {
|
|
530
|
+
const summary = buildSummaryFromArtifacts(dispatch.worktreePath);
|
|
531
|
+
if (summary) {
|
|
532
|
+
writeSummary(dispatch.worktreePath, summary);
|
|
533
|
+
const wsDir = resolveOrchestratorWorkspace(api, pluginConfig);
|
|
534
|
+
writeDispatchMemory(dispatch.issueIdentifier, summary, wsDir);
|
|
535
|
+
}
|
|
536
|
+
} catch {}
|
|
537
|
+
|
|
479
538
|
const gapsList = verdict.gaps.map((g) => `- ${g}`).join("\n");
|
|
480
539
|
await linearApi.createComment(
|
|
481
540
|
dispatch.issueId,
|
|
482
|
-
`## Audit Failed — Escalating\n\n**Attempt ${nextAttempt} of ${maxAttempts + 1}**\n\n**Gaps:**\n${gapsList}\n\n**Tests:** ${verdict.testResults || "N/A"}\n\n---\n*Max rework attempts reached. Needs human review
|
|
541
|
+
`## Audit Failed — Escalating\n\n**Attempt ${nextAttempt} of ${maxAttempts + 1}**\n\n**Gaps:**\n${gapsList}\n\n**Tests:** ${verdict.testResults || "N/A"}\n\n---\n*Max rework attempts reached. Needs human review. Artifacts: \`${dispatch.worktreePath}/.claw/\`*`,
|
|
483
542
|
).catch((err) => api.logger.error(`${TAG} failed to post escalation comment: ${err}`));
|
|
484
543
|
|
|
485
544
|
api.logger.warn(`${TAG} audit FAILED ${nextAttempt}x — escalating to human`);
|
|
@@ -608,14 +667,79 @@ export async function spawnWorker(
|
|
|
608
667
|
|
|
609
668
|
api.logger.info(`${TAG} spawning worker agent session=${workerSessionId} (attempt ${dispatch.attempt})`);
|
|
610
669
|
|
|
670
|
+
const workerStartTime = Date.now();
|
|
611
671
|
const result = await runAgent({
|
|
612
672
|
api,
|
|
613
673
|
agentId: (pluginConfig?.defaultAgentId as string) ?? "default",
|
|
614
674
|
sessionId: workerSessionId,
|
|
615
675
|
message: `${workerPrompt.system}\n\n${workerPrompt.task}`,
|
|
616
|
-
|
|
676
|
+
streaming: dispatch.agentSessionId
|
|
677
|
+
? { linearApi, agentSessionId: dispatch.agentSessionId }
|
|
678
|
+
: undefined,
|
|
617
679
|
});
|
|
618
680
|
|
|
681
|
+
// Save worker output to .claw/
|
|
682
|
+
const workerElapsed = Date.now() - workerStartTime;
|
|
683
|
+
const agentId = (pluginConfig?.defaultAgentId as string) ?? "default";
|
|
684
|
+
try { saveWorkerOutput(dispatch.worktreePath, dispatch.attempt, result.output); } catch {}
|
|
685
|
+
try {
|
|
686
|
+
appendLog(dispatch.worktreePath, {
|
|
687
|
+
ts: new Date().toISOString(), phase: "worker", attempt: dispatch.attempt,
|
|
688
|
+
agent: agentId,
|
|
689
|
+
prompt: workerPrompt.task.slice(0, 200),
|
|
690
|
+
outputPreview: result.output.slice(0, 500),
|
|
691
|
+
success: result.success, durationMs: workerElapsed,
|
|
692
|
+
});
|
|
693
|
+
} catch {}
|
|
694
|
+
|
|
695
|
+
// Handle watchdog kill (runAgent already retried once — both attempts failed)
|
|
696
|
+
if (result.watchdogKilled) {
|
|
697
|
+
const wdConfig = resolveWatchdogConfig(agentId, pluginConfig ?? undefined);
|
|
698
|
+
const thresholdSec = Math.round(wdConfig.inactivityMs / 1000);
|
|
699
|
+
|
|
700
|
+
api.logger.warn(`${TAG} worker killed by inactivity watchdog 2x — escalating to stuck`);
|
|
701
|
+
|
|
702
|
+
try {
|
|
703
|
+
appendLog(dispatch.worktreePath, {
|
|
704
|
+
ts: new Date().toISOString(), phase: "watchdog", attempt: dispatch.attempt,
|
|
705
|
+
agent: agentId, prompt: "(watchdog kill)",
|
|
706
|
+
outputPreview: result.output.slice(0, 500), success: false,
|
|
707
|
+
durationMs: workerElapsed,
|
|
708
|
+
watchdog: { reason: "inactivity", silenceSec: thresholdSec, thresholdSec, retried: true },
|
|
709
|
+
});
|
|
710
|
+
} catch {}
|
|
711
|
+
|
|
712
|
+
try { updateManifest(dispatch.worktreePath, { status: "stuck", attempts: dispatch.attempt + 1 }); } catch {}
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
await transitionDispatch(
|
|
716
|
+
dispatch.issueIdentifier, "working", "stuck",
|
|
717
|
+
{ stuckReason: "watchdog_kill_2x" }, configPath,
|
|
718
|
+
);
|
|
719
|
+
} catch (err) {
|
|
720
|
+
if (err instanceof TransitionError) {
|
|
721
|
+
api.logger.warn(`${TAG} CAS failed for watchdog stuck transition: ${err.message}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
await linearApi.createComment(
|
|
726
|
+
dispatch.issueId,
|
|
727
|
+
`## Watchdog Kill\n\nAgent killed by inactivity watchdog (no I/O for ${thresholdSec}s). ` +
|
|
728
|
+
`Automatic retry also failed.\n\n---\n*Needs human review. Artifacts: \`${dispatch.worktreePath}/.claw/\`*`,
|
|
729
|
+
).catch(() => {});
|
|
730
|
+
|
|
731
|
+
await hookCtx.notify("watchdog_kill", {
|
|
732
|
+
identifier: dispatch.issueIdentifier,
|
|
733
|
+
title: issue.title,
|
|
734
|
+
status: "stuck",
|
|
735
|
+
attempt: dispatch.attempt,
|
|
736
|
+
reason: `no I/O for ${thresholdSec}s`,
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
clearActiveSession(dispatch.issueId);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
619
743
|
// runAgent returns inline — trigger audit directly.
|
|
620
744
|
// Re-read dispatch state since it may have changed during worker run.
|
|
621
745
|
const freshState = await readDispatchState(configPath);
|
|
@@ -75,7 +75,7 @@ export async function assessTier(
|
|
|
75
75
|
const message = `${ASSESS_PROMPT}\n\n${issueText}`;
|
|
76
76
|
|
|
77
77
|
try {
|
|
78
|
-
const { runAgent } = await import("
|
|
78
|
+
const { runAgent } = await import("../agent/agent.js");
|
|
79
79
|
const result = await runAgent({
|
|
80
80
|
api,
|
|
81
81
|
agentId: agentId ?? resolveDefaultAgent(api),
|
|
@@ -17,7 +17,7 @@ vi.mock("./pipeline.js", () => ({
|
|
|
17
17
|
}));
|
|
18
18
|
|
|
19
19
|
// Mock the linear-api module
|
|
20
|
-
vi.mock("
|
|
20
|
+
vi.mock("../api/linear-api.js", () => ({
|
|
21
21
|
LinearAgentApi: class MockLinearAgentApi {
|
|
22
22
|
emitActivity = vi.fn().mockResolvedValue(undefined);
|
|
23
23
|
createComment = vi.fn().mockResolvedValue("comment-id");
|
|
@@ -2,13 +2,14 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
5
|
-
import { LinearAgentApi, resolveLinearToken } from "
|
|
5
|
+
import { LinearAgentApi, resolveLinearToken } from "../api/linear-api.js";
|
|
6
6
|
import { spawnWorker, type HookContext } from "./pipeline.js";
|
|
7
7
|
import { setActiveSession, clearActiveSession } from "./active-session.js";
|
|
8
8
|
import { readDispatchState, getActiveDispatch, registerDispatch, updateDispatchStatus, completeDispatch, removeActiveDispatch } from "./dispatch-state.js";
|
|
9
|
-
import { createDiscordNotifier, createNoopNotifier, type NotifyFn } from "
|
|
9
|
+
import { createDiscordNotifier, createNoopNotifier, type NotifyFn } from "../infra/notify.js";
|
|
10
10
|
import { assessTier } from "./tier-assess.js";
|
|
11
|
-
import { createWorktree, prepareWorkspace } from "
|
|
11
|
+
import { createWorktree, prepareWorkspace } from "../infra/codex-worktree.js";
|
|
12
|
+
import { ensureClawDir, writeManifest } from "./artifacts.js";
|
|
12
13
|
|
|
13
14
|
// ── Agent profiles (loaded from config, no hardcoded names) ───────
|
|
14
15
|
interface AgentProfile {
|
|
@@ -244,7 +245,7 @@ export async function handleLinearWebhook(
|
|
|
244
245
|
|
|
245
246
|
// 3. Run agent with streaming
|
|
246
247
|
const sessionId = `linear-notif-${notification?.type ?? "unknown"}-${Date.now()}`;
|
|
247
|
-
const { runAgent } = await import("
|
|
248
|
+
const { runAgent } = await import("../agent/agent.js");
|
|
248
249
|
const result = await runAgent({
|
|
249
250
|
api,
|
|
250
251
|
agentId,
|
|
@@ -417,7 +418,7 @@ export async function handleLinearWebhook(
|
|
|
417
418
|
|
|
418
419
|
// Run agent with streaming to Linear
|
|
419
420
|
const sessionId = `linear-session-${session.id}`;
|
|
420
|
-
const { runAgent } = await import("
|
|
421
|
+
const { runAgent } = await import("../agent/agent.js");
|
|
421
422
|
const result = await runAgent({
|
|
422
423
|
api,
|
|
423
424
|
agentId,
|
|
@@ -595,7 +596,7 @@ export async function handleLinearWebhook(
|
|
|
595
596
|
}).catch(() => {});
|
|
596
597
|
|
|
597
598
|
const sessionId = `linear-session-${session.id}`;
|
|
598
|
-
const { runAgent } = await import("
|
|
599
|
+
const { runAgent } = await import("../agent/agent.js");
|
|
599
600
|
const result = await runAgent({
|
|
600
601
|
api,
|
|
601
602
|
agentId,
|
|
@@ -797,7 +798,7 @@ export async function handleLinearWebhook(
|
|
|
797
798
|
|
|
798
799
|
// 3. Run agent subprocess with streaming
|
|
799
800
|
const sessionId = `linear-comment-${comment.id ?? Date.now()}`;
|
|
800
|
-
const { runAgent } = await import("
|
|
801
|
+
const { runAgent } = await import("../agent/agent.js");
|
|
801
802
|
const result = await runAgent({
|
|
802
803
|
api,
|
|
803
804
|
agentId: mentionedAgent,
|
|
@@ -1035,7 +1036,7 @@ export async function handleLinearWebhook(
|
|
|
1035
1036
|
].filter(Boolean).join("\n");
|
|
1036
1037
|
|
|
1037
1038
|
const sessionId = `linear-triage-${issue.id}-${Date.now()}`;
|
|
1038
|
-
const { runAgent } = await import("
|
|
1039
|
+
const { runAgent } = await import("../agent/agent.js");
|
|
1039
1040
|
const result = await runAgent({
|
|
1040
1041
|
api,
|
|
1041
1042
|
agentId,
|
|
@@ -1250,11 +1251,32 @@ async function handleDispatch(
|
|
|
1250
1251
|
api.logger.warn(`@dispatch: could not create agent session: ${err}`);
|
|
1251
1252
|
}
|
|
1252
1253
|
|
|
1254
|
+
// 6b. Initialize .claw/ artifact directory
|
|
1255
|
+
try {
|
|
1256
|
+
ensureClawDir(worktree.path);
|
|
1257
|
+
writeManifest(worktree.path, {
|
|
1258
|
+
issueIdentifier: identifier,
|
|
1259
|
+
issueTitle: enrichedIssue.title ?? "(untitled)",
|
|
1260
|
+
issueId: issue.id,
|
|
1261
|
+
tier: assessment.tier,
|
|
1262
|
+
model: assessment.model,
|
|
1263
|
+
dispatchedAt: new Date().toISOString(),
|
|
1264
|
+
worktreePath: worktree.path,
|
|
1265
|
+
branch: worktree.branch,
|
|
1266
|
+
attempts: 0,
|
|
1267
|
+
status: "dispatched",
|
|
1268
|
+
plugin: "openclaw-linear",
|
|
1269
|
+
});
|
|
1270
|
+
} catch (err) {
|
|
1271
|
+
api.logger.warn(`@dispatch: .claw/ init failed: ${err}`);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1253
1274
|
// 7. Register dispatch in persistent state
|
|
1254
1275
|
const now = new Date().toISOString();
|
|
1255
1276
|
await registerDispatch(identifier, {
|
|
1256
1277
|
issueId: issue.id,
|
|
1257
1278
|
issueIdentifier: identifier,
|
|
1279
|
+
issueTitle: enrichedIssue.title ?? "(untitled)",
|
|
1258
1280
|
worktreePath: worktree.path,
|
|
1259
1281
|
branch: worktree.branch,
|
|
1260
1282
|
tier: assessment.tier,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
4
|
-
import type { ActivityContent } from "
|
|
4
|
+
import type { ActivityContent } from "../api/linear-api.js";
|
|
5
5
|
import {
|
|
6
6
|
buildLinearApi,
|
|
7
7
|
resolveSession,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type CliToolParams,
|
|
12
12
|
type CliResult,
|
|
13
13
|
} from "./cli-shared.js";
|
|
14
|
+
import { InactivityWatchdog, resolveWatchdogConfig } from "../agent/watchdog.js";
|
|
14
15
|
|
|
15
16
|
const CLAUDE_BIN = "/home/claw/.npm-global/bin/claude";
|
|
16
17
|
|
|
@@ -116,7 +117,9 @@ export async function runClaude(
|
|
|
116
117
|
|
|
117
118
|
api.logger.info(`claude_run: session=${agentSessionId ?? "none"}, issue=${issueIdentifier ?? "none"}`);
|
|
118
119
|
|
|
119
|
-
const
|
|
120
|
+
const agentId = (params as any).agentId ?? (pluginConfig?.defaultAgentId as string) ?? "default";
|
|
121
|
+
const wdConfig = resolveWatchdogConfig(agentId, pluginConfig ?? undefined);
|
|
122
|
+
const timeout = timeoutMs ?? (pluginConfig?.claudeTimeoutMs as number) ?? wdConfig.toolTimeoutMs;
|
|
120
123
|
const workingDir = params.workingDir ?? (pluginConfig?.claudeBaseRepo as string) ?? DEFAULT_BASE_REPO;
|
|
121
124
|
|
|
122
125
|
const linearApi = buildLinearApi(api, agentSessionId);
|
|
@@ -155,12 +158,27 @@ export async function runClaude(
|
|
|
155
158
|
});
|
|
156
159
|
|
|
157
160
|
let killed = false;
|
|
161
|
+
let killedByWatchdog = false;
|
|
158
162
|
const timer = setTimeout(() => {
|
|
159
163
|
killed = true;
|
|
160
164
|
child.kill("SIGTERM");
|
|
161
165
|
setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 5_000);
|
|
162
166
|
}, timeout);
|
|
163
167
|
|
|
168
|
+
const watchdog = new InactivityWatchdog({
|
|
169
|
+
inactivityMs: wdConfig.inactivityMs,
|
|
170
|
+
label: `claude:${agentSessionId ?? "unknown"}`,
|
|
171
|
+
logger: api.logger,
|
|
172
|
+
onKill: () => {
|
|
173
|
+
killedByWatchdog = true;
|
|
174
|
+
killed = true;
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
child.kill("SIGTERM");
|
|
177
|
+
setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 5_000);
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
watchdog.start();
|
|
181
|
+
|
|
164
182
|
const collectedMessages: string[] = [];
|
|
165
183
|
const collectedCommands: string[] = [];
|
|
166
184
|
let stderrOutput = "";
|
|
@@ -169,6 +187,7 @@ export async function runClaude(
|
|
|
169
187
|
const rl = createInterface({ input: child.stdout! });
|
|
170
188
|
rl.on("line", (line) => {
|
|
171
189
|
if (!line.trim()) return;
|
|
190
|
+
watchdog.tick();
|
|
172
191
|
|
|
173
192
|
let event: any;
|
|
174
193
|
try {
|
|
@@ -229,11 +248,13 @@ export async function runClaude(
|
|
|
229
248
|
});
|
|
230
249
|
|
|
231
250
|
child.stderr?.on("data", (chunk) => {
|
|
251
|
+
watchdog.tick();
|
|
232
252
|
stderrOutput += chunk.toString();
|
|
233
253
|
});
|
|
234
254
|
|
|
235
255
|
child.on("close", (code) => {
|
|
236
256
|
clearTimeout(timer);
|
|
257
|
+
watchdog.stop();
|
|
237
258
|
rl.close();
|
|
238
259
|
|
|
239
260
|
const parts: string[] = [];
|
|
@@ -242,11 +263,15 @@ export async function runClaude(
|
|
|
242
263
|
const output = parts.join("\n\n") || stderrOutput || "(no output)";
|
|
243
264
|
|
|
244
265
|
if (killed) {
|
|
245
|
-
|
|
266
|
+
const errorType = killedByWatchdog ? "inactivity_timeout" : "timeout";
|
|
267
|
+
const reason = killedByWatchdog
|
|
268
|
+
? `Claude killed by inactivity watchdog (no I/O for ${Math.round(wdConfig.inactivityMs / 1000)}s)`
|
|
269
|
+
: `Claude timed out after ${Math.round(timeout / 1000)}s`;
|
|
270
|
+
api.logger.warn(reason);
|
|
246
271
|
resolve({
|
|
247
272
|
success: false,
|
|
248
|
-
output:
|
|
249
|
-
error:
|
|
273
|
+
output: `${reason}. Partial output:\n${output}`,
|
|
274
|
+
error: errorType,
|
|
250
275
|
});
|
|
251
276
|
return;
|
|
252
277
|
}
|
|
@@ -267,6 +292,7 @@ export async function runClaude(
|
|
|
267
292
|
|
|
268
293
|
child.on("error", (err) => {
|
|
269
294
|
clearTimeout(timer);
|
|
295
|
+
watchdog.stop();
|
|
270
296
|
rl.close();
|
|
271
297
|
api.logger.error(`Claude spawn error: ${err}`);
|
|
272
298
|
resolve({
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import type { LinearAgentApi } from "
|
|
3
|
-
import { resolveLinearToken, LinearAgentApi as LinearAgentApiClass } from "
|
|
4
|
-
import { getCurrentSession, getActiveSessionByIdentifier } from "
|
|
2
|
+
import type { LinearAgentApi } from "../api/linear-api.js";
|
|
3
|
+
import { resolveLinearToken, LinearAgentApi as LinearAgentApiClass } from "../api/linear-api.js";
|
|
4
|
+
import { getCurrentSession, getActiveSessionByIdentifier } from "../pipeline/active-session.js";
|
|
5
5
|
|
|
6
|
-
export const DEFAULT_TIMEOUT_MS = 10 * 60_000; // 10 minutes
|
|
6
|
+
export const DEFAULT_TIMEOUT_MS = 10 * 60_000; // 10 minutes (legacy — prefer watchdog config)
|
|
7
|
+
export { DEFAULT_INACTIVITY_SEC, DEFAULT_MAX_TOTAL_SEC, DEFAULT_TOOL_TIMEOUT_SEC } from "../agent/watchdog.js";
|
|
7
8
|
export const DEFAULT_BASE_REPO = "/home/claw/ai-workspace";
|
|
8
9
|
|
|
9
10
|
export interface CliToolParams {
|
|
@@ -3,7 +3,7 @@ import { join, dirname } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
5
5
|
import { jsonResult } from "openclaw/plugin-sdk";
|
|
6
|
-
import { getCurrentSession } from "
|
|
6
|
+
import { getCurrentSession } from "../pipeline/active-session.js";
|
|
7
7
|
import { runCodex } from "./codex-tool.js";
|
|
8
8
|
import { runClaude } from "./claude-tool.js";
|
|
9
9
|
import { runGemini } from "./gemini-tool.js";
|
|
@@ -43,7 +43,7 @@ export interface CodingToolsConfig {
|
|
|
43
43
|
export function loadCodingConfig(): CodingToolsConfig {
|
|
44
44
|
try {
|
|
45
45
|
// Resolve relative to the plugin root (one level up from src/)
|
|
46
|
-
const pluginRoot = join(dirname(fileURLToPath(import.meta.url)), "
|
|
46
|
+
const pluginRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
47
47
|
const raw = readFileSync(join(pluginRoot, "coding-tools.json"), "utf8");
|
|
48
48
|
return JSON.parse(raw) as CodingToolsConfig;
|
|
49
49
|
} catch {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
4
|
-
import type { ActivityContent } from "
|
|
4
|
+
import type { ActivityContent } from "../api/linear-api.js";
|
|
5
5
|
import {
|
|
6
6
|
buildLinearApi,
|
|
7
7
|
resolveSession,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type CliToolParams,
|
|
12
12
|
type CliResult,
|
|
13
13
|
} from "./cli-shared.js";
|
|
14
|
+
import { InactivityWatchdog, resolveWatchdogConfig } from "../agent/watchdog.js";
|
|
14
15
|
|
|
15
16
|
const CODEX_BIN = "/home/claw/.npm-global/bin/codex";
|
|
16
17
|
|
|
@@ -103,7 +104,9 @@ export async function runCodex(
|
|
|
103
104
|
|
|
104
105
|
api.logger.info(`codex_run: session=${agentSessionId ?? "none"}, issue=${issueIdentifier ?? "none"}`);
|
|
105
106
|
|
|
106
|
-
const
|
|
107
|
+
const agentId = (params as any).agentId ?? (pluginConfig?.defaultAgentId as string) ?? "default";
|
|
108
|
+
const wdConfig = resolveWatchdogConfig(agentId, pluginConfig ?? undefined);
|
|
109
|
+
const timeout = timeoutMs ?? (pluginConfig?.codexTimeoutMs as number) ?? wdConfig.toolTimeoutMs;
|
|
107
110
|
const workingDir = params.workingDir ?? (pluginConfig?.codexBaseRepo as string) ?? DEFAULT_BASE_REPO;
|
|
108
111
|
|
|
109
112
|
// Build Linear API for activity streaming
|
|
@@ -134,12 +137,27 @@ export async function runCodex(
|
|
|
134
137
|
});
|
|
135
138
|
|
|
136
139
|
let killed = false;
|
|
140
|
+
let killedByWatchdog = false;
|
|
137
141
|
const timer = setTimeout(() => {
|
|
138
142
|
killed = true;
|
|
139
143
|
child.kill("SIGTERM");
|
|
140
144
|
setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 5_000);
|
|
141
145
|
}, timeout);
|
|
142
146
|
|
|
147
|
+
const watchdog = new InactivityWatchdog({
|
|
148
|
+
inactivityMs: wdConfig.inactivityMs,
|
|
149
|
+
label: `codex:${agentSessionId ?? "unknown"}`,
|
|
150
|
+
logger: api.logger,
|
|
151
|
+
onKill: () => {
|
|
152
|
+
killedByWatchdog = true;
|
|
153
|
+
killed = true;
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
child.kill("SIGTERM");
|
|
156
|
+
setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 5_000);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
watchdog.start();
|
|
160
|
+
|
|
143
161
|
const collectedMessages: string[] = [];
|
|
144
162
|
const collectedCommands: string[] = [];
|
|
145
163
|
let stderrOutput = "";
|
|
@@ -147,6 +165,7 @@ export async function runCodex(
|
|
|
147
165
|
const rl = createInterface({ input: child.stdout! });
|
|
148
166
|
rl.on("line", (line) => {
|
|
149
167
|
if (!line.trim()) return;
|
|
168
|
+
watchdog.tick();
|
|
150
169
|
|
|
151
170
|
let event: any;
|
|
152
171
|
try {
|
|
@@ -189,11 +208,13 @@ export async function runCodex(
|
|
|
189
208
|
});
|
|
190
209
|
|
|
191
210
|
child.stderr?.on("data", (chunk) => {
|
|
211
|
+
watchdog.tick();
|
|
192
212
|
stderrOutput += chunk.toString();
|
|
193
213
|
});
|
|
194
214
|
|
|
195
215
|
child.on("close", (code) => {
|
|
196
216
|
clearTimeout(timer);
|
|
217
|
+
watchdog.stop();
|
|
197
218
|
rl.close();
|
|
198
219
|
|
|
199
220
|
const parts: string[] = [];
|
|
@@ -202,11 +223,15 @@ export async function runCodex(
|
|
|
202
223
|
const output = parts.join("\n\n") || stderrOutput || "(no output)";
|
|
203
224
|
|
|
204
225
|
if (killed) {
|
|
205
|
-
|
|
226
|
+
const errorType = killedByWatchdog ? "inactivity_timeout" : "timeout";
|
|
227
|
+
const reason = killedByWatchdog
|
|
228
|
+
? `Codex killed by inactivity watchdog (no I/O for ${Math.round(wdConfig.inactivityMs / 1000)}s)`
|
|
229
|
+
: `Codex timed out after ${Math.round(timeout / 1000)}s`;
|
|
230
|
+
api.logger.warn(reason);
|
|
206
231
|
resolve({
|
|
207
232
|
success: false,
|
|
208
|
-
output:
|
|
209
|
-
error:
|
|
233
|
+
output: `${reason}. Partial output:\n${output}`,
|
|
234
|
+
error: errorType,
|
|
210
235
|
});
|
|
211
236
|
return;
|
|
212
237
|
}
|
|
@@ -227,6 +252,7 @@ export async function runCodex(
|
|
|
227
252
|
|
|
228
253
|
child.on("error", (err) => {
|
|
229
254
|
clearTimeout(timer);
|
|
255
|
+
watchdog.stop();
|
|
230
256
|
rl.close();
|
|
231
257
|
api.logger.error(`Codex spawn error: ${err}`);
|
|
232
258
|
resolve({
|