@integrity-labs/agt-cli 0.12.9 → 0.14.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/dist/bin/agt.js +3 -3
- package/dist/{chunk-ZFTZDO5E.js → chunk-Y2ZGJIXI.js} +26 -3
- package/dist/chunk-Y2ZGJIXI.js.map +1 -0
- package/dist/lib/manager-worker.js +339 -37
- package/dist/lib/manager-worker.js.map +1 -1
- package/mcp/direct-chat-channel.js +1 -0
- package/mcp/slack-channel.js +47 -5
- package/mcp/telegram-channel.js +1 -0
- package/package.json +1 -1
- package/dist/chunk-ZFTZDO5E.js.map +0 -1
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getIntegration,
|
|
10
10
|
isParseError,
|
|
11
11
|
isResolveError,
|
|
12
|
+
isSuppressOutput,
|
|
12
13
|
parseDeliveryTarget,
|
|
13
14
|
provision,
|
|
14
15
|
provisionIsolationHook,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
resolveChannels,
|
|
18
19
|
resolveDmTarget,
|
|
19
20
|
wrapScheduledTaskPrompt
|
|
20
|
-
} from "../chunk-
|
|
21
|
+
} from "../chunk-Y2ZGJIXI.js";
|
|
21
22
|
import {
|
|
22
23
|
findTaskByTemplate,
|
|
23
24
|
getProjectDir,
|
|
@@ -451,6 +452,237 @@ function parseExpiresAt(raw) {
|
|
|
451
452
|
return null;
|
|
452
453
|
}
|
|
453
454
|
|
|
455
|
+
// src/lib/channel-sweep.ts
|
|
456
|
+
import { execFileSync } from "child_process";
|
|
457
|
+
var CHANNEL_BASENAMES = [
|
|
458
|
+
"slack-channel",
|
|
459
|
+
"direct-chat-channel",
|
|
460
|
+
"telegram-channel"
|
|
461
|
+
];
|
|
462
|
+
function parseEtime(s) {
|
|
463
|
+
const trimmed = s.trim();
|
|
464
|
+
if (!trimmed) return 0;
|
|
465
|
+
let days = 0;
|
|
466
|
+
let rest = trimmed;
|
|
467
|
+
const dashIdx = rest.indexOf("-");
|
|
468
|
+
if (dashIdx >= 0) {
|
|
469
|
+
days = parseInt(rest.slice(0, dashIdx), 10) || 0;
|
|
470
|
+
rest = rest.slice(dashIdx + 1);
|
|
471
|
+
}
|
|
472
|
+
const parts = rest.split(":").map((p) => parseInt(p, 10) || 0);
|
|
473
|
+
let h = 0;
|
|
474
|
+
let m = 0;
|
|
475
|
+
let sec = 0;
|
|
476
|
+
if (parts.length === 3) {
|
|
477
|
+
[h, m, sec] = parts;
|
|
478
|
+
} else if (parts.length === 2) {
|
|
479
|
+
[m, sec] = parts;
|
|
480
|
+
} else if (parts.length === 1) {
|
|
481
|
+
[sec] = parts;
|
|
482
|
+
}
|
|
483
|
+
return days * 86400 + h * 3600 + m * 60 + sec;
|
|
484
|
+
}
|
|
485
|
+
function parsePsOutput(psOutput) {
|
|
486
|
+
const results = [];
|
|
487
|
+
const lines = psOutput.split("\n");
|
|
488
|
+
for (const rawLine of lines) {
|
|
489
|
+
const line = rawLine.trimStart();
|
|
490
|
+
if (!line) continue;
|
|
491
|
+
const firstSpace = line.search(/\s+/);
|
|
492
|
+
if (firstSpace < 0) continue;
|
|
493
|
+
const pid = parseInt(line.slice(0, firstSpace), 10);
|
|
494
|
+
if (!Number.isFinite(pid)) continue;
|
|
495
|
+
const afterPid = line.slice(firstSpace).trimStart();
|
|
496
|
+
const secondSpace = afterPid.search(/\s+/);
|
|
497
|
+
if (secondSpace < 0) continue;
|
|
498
|
+
const ppid = parseInt(afterPid.slice(0, secondSpace), 10);
|
|
499
|
+
if (!Number.isFinite(ppid)) continue;
|
|
500
|
+
const afterPpid = afterPid.slice(secondSpace).trimStart();
|
|
501
|
+
const thirdSpace = afterPpid.search(/\s+/);
|
|
502
|
+
if (thirdSpace < 0) continue;
|
|
503
|
+
const etime = afterPpid.slice(0, thirdSpace);
|
|
504
|
+
const command = afterPpid.slice(thirdSpace).trimStart();
|
|
505
|
+
const channelMatch = command.match(
|
|
506
|
+
new RegExp(`(?:^|\\s)(?:[^\\s=]*/)?(${CHANNEL_BASENAMES.join("|")})\\.js(?:\\s|$)`)
|
|
507
|
+
);
|
|
508
|
+
const channelType = channelMatch?.[1];
|
|
509
|
+
if (!channelType) continue;
|
|
510
|
+
const match = command.match(/(?:^|\s)AGT_AGENT_CODE_NAME=([^\s]+)/);
|
|
511
|
+
if (!match) continue;
|
|
512
|
+
const codeName = match[1];
|
|
513
|
+
results.push({
|
|
514
|
+
pid,
|
|
515
|
+
ppid,
|
|
516
|
+
channelType,
|
|
517
|
+
codeName,
|
|
518
|
+
etimeSeconds: parseEtime(etime),
|
|
519
|
+
command: command.slice(0, 500)
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
return results;
|
|
523
|
+
}
|
|
524
|
+
function pickKillTargets(processes, agentCodeNames) {
|
|
525
|
+
const kills = [];
|
|
526
|
+
const ambiguousGroups = [];
|
|
527
|
+
const groups = /* @__PURE__ */ new Map();
|
|
528
|
+
for (const proc of processes) {
|
|
529
|
+
if (!agentCodeNames.has(proc.codeName)) continue;
|
|
530
|
+
const key = `${proc.codeName}:${proc.channelType}`;
|
|
531
|
+
const bucket = groups.get(key);
|
|
532
|
+
if (bucket) bucket.push(proc);
|
|
533
|
+
else groups.set(key, [proc]);
|
|
534
|
+
}
|
|
535
|
+
for (const [key, bucket] of groups) {
|
|
536
|
+
const orphans = bucket.filter((p) => p.ppid === 1);
|
|
537
|
+
const nonOrphans = bucket.filter((p) => p.ppid !== 1);
|
|
538
|
+
if (orphans.length === 0 && nonOrphans.length <= 1) continue;
|
|
539
|
+
if (nonOrphans.length > 1) {
|
|
540
|
+
ambiguousGroups.push(key);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
for (const proc of orphans) {
|
|
544
|
+
kills.push({
|
|
545
|
+
pid: proc.pid,
|
|
546
|
+
codeName: proc.codeName,
|
|
547
|
+
channelType: proc.channelType,
|
|
548
|
+
etimeSeconds: proc.etimeSeconds,
|
|
549
|
+
reason: "orphan"
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return { kills, ambiguousGroups };
|
|
554
|
+
}
|
|
555
|
+
function defaultKill(pid) {
|
|
556
|
+
try {
|
|
557
|
+
process.kill(pid, "SIGTERM");
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async function sweepChannelProcesses(opts) {
|
|
562
|
+
const { agentCodeNames, dryRun, log: log2 } = opts;
|
|
563
|
+
const kill = opts.killFn ?? defaultKill;
|
|
564
|
+
let psOutput = "";
|
|
565
|
+
try {
|
|
566
|
+
psOutput = execFileSync("ps", ["eww", "-o", "pid=,ppid=,etime=,command="], {
|
|
567
|
+
encoding: "utf-8",
|
|
568
|
+
timeout: 5e3,
|
|
569
|
+
maxBuffer: 10 * 1024 * 1024
|
|
570
|
+
});
|
|
571
|
+
} catch (err) {
|
|
572
|
+
log2(`[channel-sweep] ps failed: ${err.message}`);
|
|
573
|
+
return { kills: [], inspected: 0, scannedAgents: [...agentCodeNames], dryRun };
|
|
574
|
+
}
|
|
575
|
+
const processes = parsePsOutput(psOutput);
|
|
576
|
+
const { kills, ambiguousGroups } = pickKillTargets(processes, agentCodeNames);
|
|
577
|
+
for (const target of kills) {
|
|
578
|
+
const ageMin = Math.round(target.etimeSeconds / 60);
|
|
579
|
+
log2(
|
|
580
|
+
`[channel-sweep]${dryRun ? "[dry-run]" : ""} surplus ${target.channelType} for ${target.codeName}: killing pid=${target.pid} (${target.reason}, age=${ageMin}m)`
|
|
581
|
+
);
|
|
582
|
+
if (!dryRun) kill(target.pid);
|
|
583
|
+
}
|
|
584
|
+
for (const group of ambiguousGroups) {
|
|
585
|
+
log2(
|
|
586
|
+
`[channel-sweep] ambiguous group ${group} \u2014 >1 non-orphan process, cannot determine which is live. Manual check recommended.`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
kills,
|
|
591
|
+
inspected: processes.length,
|
|
592
|
+
scannedAgents: [...agentCodeNames],
|
|
593
|
+
dryRun
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/lib/delivery-hint.ts
|
|
598
|
+
var DEFAULT_PROBABILITY = 0.1;
|
|
599
|
+
function envSuffixFor(codeName) {
|
|
600
|
+
return codeName.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
601
|
+
}
|
|
602
|
+
function hintProbability(codeName, env = process.env) {
|
|
603
|
+
const suffix = codeName ? `__${envSuffixFor(codeName)}` : "";
|
|
604
|
+
if (suffix && env[`AGT_DELIVERY_HINT_DISABLED${suffix}`] === "1") return 0;
|
|
605
|
+
const perAgent = suffix ? env[`AGT_DELIVERY_HINT_PROBABILITY${suffix}`] : void 0;
|
|
606
|
+
if (perAgent != null) return clampProbability(perAgent);
|
|
607
|
+
if (env["AGT_DELIVERY_HINT_DISABLED"] === "1") return 0;
|
|
608
|
+
const host = env["AGT_DELIVERY_HINT_PROBABILITY"];
|
|
609
|
+
if (host != null) return clampProbability(host);
|
|
610
|
+
return DEFAULT_PROBABILITY;
|
|
611
|
+
}
|
|
612
|
+
function clampProbability(raw) {
|
|
613
|
+
const parsed = parseFloat(raw);
|
|
614
|
+
if (!Number.isFinite(parsed)) return DEFAULT_PROBABILITY;
|
|
615
|
+
if (parsed < 0) return 0;
|
|
616
|
+
if (parsed > 1) return 1;
|
|
617
|
+
return parsed;
|
|
618
|
+
}
|
|
619
|
+
function shouldIncludeHint(probability, rng = Math.random) {
|
|
620
|
+
if (probability <= 0) return false;
|
|
621
|
+
if (probability >= 1) return true;
|
|
622
|
+
return rng() < probability;
|
|
623
|
+
}
|
|
624
|
+
var HINT_VARIANTS = Object.freeze([
|
|
625
|
+
'Quick note: you can change this scheduled task just by asking me \u2014 e.g. "Change the schedule for this task" or "Make this report less verbose in future".',
|
|
626
|
+
`By the way, this is on a schedule \u2014 if you'd like to tweak it, just say something like "run this weekly instead of daily" or "make this report less verbose in future" and I'll handle it.`,
|
|
627
|
+
'Heads up: I deliver this on a schedule you can edit conversationally. Try "Change the schedule for this task" or "Make this report shorter" whenever you want to adjust it.',
|
|
628
|
+
`PS \u2014 no UI needed to tune this. Just tell me "Send this at 9am instead" or "Skip weekends for this report" and I'll update the schedule.`,
|
|
629
|
+
'FYI this is a scheduled delivery. You can reshape it in plain English \u2014 e.g. "Make this fortnightly" or "Only include items from the last 24 hours".',
|
|
630
|
+
`You're the boss of this schedule \u2014 ask me things like "Pause this for two weeks" or "Include a summary at the top next time" and I'll apply it.`,
|
|
631
|
+
`Side note: you can say "Change this to Mondays only" or "Drop the preamble in future reports" and I'll update the task. No config screen required.`,
|
|
632
|
+
'Reminder: I run this on a schedule you can edit by talking. Good openers: "Change when this fires" or "Make future reports more concise".',
|
|
633
|
+
`Small tip \u2014 this delivery is editable on the fly. Try "Move this to afternoons" or "Cut the detail down in future runs" and I'll retune it.`,
|
|
634
|
+
`If this cadence or format isn't quite right, just ask \u2014 "Only run this on weekdays" or "Shorter summaries from now on" both work, no form to fill out.`
|
|
635
|
+
]);
|
|
636
|
+
function pickHintVariant(rng = Math.random) {
|
|
637
|
+
const idx = Math.floor(rng() * HINT_VARIANTS.length) % HINT_VARIANTS.length;
|
|
638
|
+
return HINT_VARIANTS[idx];
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/lib/delivery-schedule-link.ts
|
|
642
|
+
function envSuffixFor2(codeName) {
|
|
643
|
+
return codeName.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
644
|
+
}
|
|
645
|
+
function scheduleLinkFooterEnabled(codeName, env = process.env) {
|
|
646
|
+
if (codeName) {
|
|
647
|
+
const perAgent = env[`AGT_SCHEDULE_LINK_FOOTER_DISABLED__${envSuffixFor2(codeName)}`];
|
|
648
|
+
if (perAgent === "1") return false;
|
|
649
|
+
}
|
|
650
|
+
if (env["AGT_SCHEDULE_LINK_FOOTER_DISABLED"] === "1") return false;
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
function getConsoleUrl(env = process.env) {
|
|
654
|
+
const canonical = env["AGT_CONSOLE_URL"]?.trim();
|
|
655
|
+
if (canonical) return canonical.replace(/\/+$/, "");
|
|
656
|
+
const fallback = env["NEXT_PUBLIC_APP_URL"]?.trim();
|
|
657
|
+
if (fallback) return fallback.replace(/\/+$/, "");
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
function buildScheduleEditLink(consoleUrl, agentId, taskId) {
|
|
661
|
+
const base = consoleUrl.replace(/\/+$/, "");
|
|
662
|
+
return `${base}/agents/${encodeURIComponent(agentId)}/schedules/${encodeURIComponent(taskId)}`;
|
|
663
|
+
}
|
|
664
|
+
function formatScheduleLinkFooter(url, medium) {
|
|
665
|
+
if (medium === "slack") return `<${url}|Edit schedule>`;
|
|
666
|
+
if (medium === "telegram") return `Edit schedule: ${url}`;
|
|
667
|
+
return url;
|
|
668
|
+
}
|
|
669
|
+
function appendScheduleLinkFooter(body, url, medium) {
|
|
670
|
+
const footer = formatScheduleLinkFooter(url, medium);
|
|
671
|
+
const trimmed = body.replace(/\s+$/, "");
|
|
672
|
+
if (trimmed.endsWith(footer)) return trimmed;
|
|
673
|
+
return `${trimmed}
|
|
674
|
+
|
|
675
|
+
${footer}`;
|
|
676
|
+
}
|
|
677
|
+
function withScheduleLinkFooter(opts) {
|
|
678
|
+
if (!opts.taskId) return opts.body;
|
|
679
|
+
if (!scheduleLinkFooterEnabled(opts.codeName, opts.env)) return opts.body;
|
|
680
|
+
const consoleUrl = getConsoleUrl(opts.env);
|
|
681
|
+
if (!consoleUrl) return opts.body;
|
|
682
|
+
const url = buildScheduleEditLink(consoleUrl, opts.agentId, opts.taskId);
|
|
683
|
+
return appendScheduleLinkFooter(opts.body, url, opts.medium);
|
|
684
|
+
}
|
|
685
|
+
|
|
454
686
|
// src/lib/realtime-chat.ts
|
|
455
687
|
import { createClient } from "@supabase/supabase-js";
|
|
456
688
|
var client = null;
|
|
@@ -736,6 +968,13 @@ var GATEWAY_PORT_STEP = 10;
|
|
|
736
968
|
var GATEWAY_PORT_MAX = 18899;
|
|
737
969
|
var AUGMENTED_DIR = join2(process.env["HOME"] ?? "/tmp", ".augmented");
|
|
738
970
|
var GATEWAY_PORTS_FILE = join2(AUGMENTED_DIR, "gateway-ports.json");
|
|
971
|
+
var CHANNEL_SWEEP_INTERVAL_MS = (() => {
|
|
972
|
+
const raw = parseInt(process.env["AGT_CHANNEL_SWEEP_INTERVAL_MS"] ?? "", 10);
|
|
973
|
+
if (!Number.isFinite(raw)) return 5 * 60 * 1e3;
|
|
974
|
+
return Math.max(raw, 3e4);
|
|
975
|
+
})();
|
|
976
|
+
var CHANNEL_SWEEP_DRY_RUN = process.env["AGT_CHANNEL_SWEEP_DRY_RUN"] === "1";
|
|
977
|
+
var lastChannelSweepAt = 0;
|
|
739
978
|
var config = null;
|
|
740
979
|
var running = false;
|
|
741
980
|
var pollTimer = null;
|
|
@@ -811,9 +1050,9 @@ function clearAgentCaches(agentId, codeName) {
|
|
|
811
1050
|
var cachedFrameworkVersion = null;
|
|
812
1051
|
var lastVersionCheckAt = 0;
|
|
813
1052
|
var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
814
|
-
function resolveBrewPath(
|
|
1053
|
+
function resolveBrewPath(execFileSync2) {
|
|
815
1054
|
try {
|
|
816
|
-
const out =
|
|
1055
|
+
const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
|
|
817
1056
|
if (out) return out;
|
|
818
1057
|
} catch {
|
|
819
1058
|
}
|
|
@@ -835,9 +1074,9 @@ async function ensureToolkitCli(toolkitSlug) {
|
|
|
835
1074
|
}
|
|
836
1075
|
const { binary, installer, package: pkg, script } = integration.cli_tool;
|
|
837
1076
|
const resolvedInstaller = installer ?? "manual";
|
|
838
|
-
const { execFileSync, execSync } = await import("child_process");
|
|
1077
|
+
const { execFileSync: execFileSync2, execSync } = await import("child_process");
|
|
839
1078
|
try {
|
|
840
|
-
|
|
1079
|
+
execFileSync2("which", [binary], { timeout: 5e3, stdio: "pipe" });
|
|
841
1080
|
toolkitCliEnsured.add(toolkitSlug);
|
|
842
1081
|
toolkitCliRetryAfter.delete(toolkitSlug);
|
|
843
1082
|
return;
|
|
@@ -857,14 +1096,14 @@ async function ensureToolkitCli(toolkitSlug) {
|
|
|
857
1096
|
return;
|
|
858
1097
|
}
|
|
859
1098
|
log(`[toolkit-install] ${toolkitSlug}: installing via npm (${pkg})\u2026`);
|
|
860
|
-
|
|
1099
|
+
execFileSync2("npm", ["install", "-g", pkg], { timeout: 18e4, stdio: "pipe" });
|
|
861
1100
|
} else if (resolvedInstaller === "brew") {
|
|
862
1101
|
if (!pkg) {
|
|
863
1102
|
log(`[toolkit-install] ${toolkitSlug}: installer=brew but no package declared`);
|
|
864
1103
|
toolkitCliEnsured.add(toolkitSlug);
|
|
865
1104
|
return;
|
|
866
1105
|
}
|
|
867
|
-
const brewPath = resolveBrewPath(
|
|
1106
|
+
const brewPath = resolveBrewPath(execFileSync2);
|
|
868
1107
|
if (!brewPath) {
|
|
869
1108
|
log(`[toolkit-install] ${toolkitSlug}: installer=brew but Homebrew not available \u2014 install manually: brew install ${pkg}`);
|
|
870
1109
|
toolkitCliEnsured.add(toolkitSlug);
|
|
@@ -874,9 +1113,9 @@ async function ensureToolkitCli(toolkitSlug) {
|
|
|
874
1113
|
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
875
1114
|
log(`[toolkit-install] ${toolkitSlug}: installing via brew (${pkg})\u2026`);
|
|
876
1115
|
if (isRoot) {
|
|
877
|
-
|
|
1116
|
+
execFileSync2("sudo", ["-u", "ec2-user", "-H", brewPath, "install", pkg], { timeout: 18e4, stdio: "pipe" });
|
|
878
1117
|
} else {
|
|
879
|
-
|
|
1118
|
+
execFileSync2(brewPath, ["install", pkg], { timeout: 18e4, stdio: "pipe" });
|
|
880
1119
|
}
|
|
881
1120
|
} else if (resolvedInstaller === "script") {
|
|
882
1121
|
if (!script) {
|
|
@@ -897,7 +1136,7 @@ async function ensureToolkitCli(toolkitSlug) {
|
|
|
897
1136
|
process.env.PATH = `${brewBinDir}:${process.env.PATH ?? ""}`;
|
|
898
1137
|
}
|
|
899
1138
|
try {
|
|
900
|
-
|
|
1139
|
+
execFileSync2("which", [binary], { timeout: 5e3, stdio: "pipe" });
|
|
901
1140
|
log(`[toolkit-install] ${toolkitSlug}: installed \u2014 ${binary} now on PATH`);
|
|
902
1141
|
toolkitCliEnsured.add(toolkitSlug);
|
|
903
1142
|
toolkitCliRetryAfter.delete(toolkitSlug);
|
|
@@ -910,8 +1149,8 @@ async function ensureFrameworkBinary(frameworkId) {
|
|
|
910
1149
|
if (frameworkId !== "claude-code") return;
|
|
911
1150
|
if (frameworkBinaryChecked.has(frameworkId)) return;
|
|
912
1151
|
frameworkBinaryChecked.add(frameworkId);
|
|
913
|
-
const { execFileSync } = await import("child_process");
|
|
914
|
-
const brewPath = resolveBrewPath(
|
|
1152
|
+
const { execFileSync: execFileSync2 } = await import("child_process");
|
|
1153
|
+
const brewPath = resolveBrewPath(execFileSync2);
|
|
915
1154
|
if (!brewPath) {
|
|
916
1155
|
log("Homebrew not found (no `brew` on PATH, no /home/linuxbrew/.linuxbrew/bin/brew). Cannot auto-install Claude Code. Install manually: https://claude.ai/download");
|
|
917
1156
|
return;
|
|
@@ -921,14 +1160,14 @@ async function ensureFrameworkBinary(frameworkId) {
|
|
|
921
1160
|
const runBrew = (args, opts) => {
|
|
922
1161
|
if (isRoot) {
|
|
923
1162
|
const sudoArgs = ["-u", "ec2-user", "-H", brewPath, ...args];
|
|
924
|
-
return
|
|
1163
|
+
return execFileSync2("sudo", sudoArgs, { ...opts, stdio: "pipe" }).toString();
|
|
925
1164
|
}
|
|
926
|
-
return
|
|
1165
|
+
return execFileSync2(brewPath, args, { ...opts, stdio: "pipe" }).toString();
|
|
927
1166
|
};
|
|
928
1167
|
let claudeExists = existsSync("/home/linuxbrew/.linuxbrew/bin/claude");
|
|
929
1168
|
if (!claudeExists) {
|
|
930
1169
|
try {
|
|
931
|
-
|
|
1170
|
+
execFileSync2("which", ["claude"], { timeout: 5e3 });
|
|
932
1171
|
claudeExists = true;
|
|
933
1172
|
} catch {
|
|
934
1173
|
}
|
|
@@ -981,20 +1220,20 @@ async function checkAndUpdateCli() {
|
|
|
981
1220
|
if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL_MS) return;
|
|
982
1221
|
} catch {
|
|
983
1222
|
}
|
|
984
|
-
const { execFileSync } = await import("child_process");
|
|
1223
|
+
const { execFileSync: execFileSync2 } = await import("child_process");
|
|
985
1224
|
let brewPath;
|
|
986
1225
|
try {
|
|
987
|
-
brewPath =
|
|
1226
|
+
brewPath = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
|
|
988
1227
|
} catch {
|
|
989
1228
|
return;
|
|
990
1229
|
}
|
|
991
1230
|
try {
|
|
992
|
-
|
|
1231
|
+
execFileSync2(brewPath, ["update", "--quiet"], { timeout: 6e4, stdio: "pipe" });
|
|
993
1232
|
} catch (err) {
|
|
994
1233
|
log(`[self-update] brew update failed (continuing with stale cache): ${err.message}`);
|
|
995
1234
|
}
|
|
996
1235
|
try {
|
|
997
|
-
const outdated =
|
|
1236
|
+
const outdated = execFileSync2(brewPath, ["outdated", "--json=v2"], {
|
|
998
1237
|
timeout: 3e4,
|
|
999
1238
|
encoding: "utf-8"
|
|
1000
1239
|
});
|
|
@@ -1005,7 +1244,7 @@ async function checkAndUpdateCli() {
|
|
|
1005
1244
|
const latest = agtOutdated.current_version ?? "unknown";
|
|
1006
1245
|
log(`[self-update] agt CLI update available: ${installed} \u2192 ${latest}. Upgrading...`);
|
|
1007
1246
|
try {
|
|
1008
|
-
|
|
1247
|
+
execFileSync2(brewPath, ["upgrade", "integrity-labs/tap/agt"], {
|
|
1009
1248
|
timeout: 12e4,
|
|
1010
1249
|
stdio: "pipe"
|
|
1011
1250
|
});
|
|
@@ -1591,6 +1830,17 @@ async function pollCycle() {
|
|
|
1591
1830
|
}
|
|
1592
1831
|
}
|
|
1593
1832
|
await healthCheckGateways(agentStates);
|
|
1833
|
+
if (Date.now() - lastChannelSweepAt >= CHANNEL_SWEEP_INTERVAL_MS) {
|
|
1834
|
+
lastChannelSweepAt = Date.now();
|
|
1835
|
+
const agentCodeNames = new Set(agentStates.map((a) => a.codeName));
|
|
1836
|
+
sweepChannelProcesses({
|
|
1837
|
+
agentCodeNames,
|
|
1838
|
+
dryRun: CHANNEL_SWEEP_DRY_RUN,
|
|
1839
|
+
log
|
|
1840
|
+
}).catch((err) => {
|
|
1841
|
+
log(`[channel-sweep] sweep error: ${err.message}`);
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1594
1844
|
const lastHealthCheck = lastHarvestAt.get("__cron_health__") ?? 0;
|
|
1595
1845
|
if (Date.now() - lastHealthCheck >= HARVEST_INTERVAL_MS) {
|
|
1596
1846
|
lastHarvestAt.set("__cron_health__", Date.now());
|
|
@@ -2958,6 +3208,16 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
|
|
|
2958
3208
|
}
|
|
2959
3209
|
async function processClaudeTaskResult(codeName, agentId, templateId, output, delivery) {
|
|
2960
3210
|
try {
|
|
3211
|
+
if (isSuppressOutput(output)) {
|
|
3212
|
+
log(`[claude-scheduler] Suppressing delivery for '${codeName}' \u2014 output matched no-delivery sentinel or was empty`);
|
|
3213
|
+
if (delivery?.mode === "announce" && delivery.to) {
|
|
3214
|
+
await reportDeliveryStatus(agentId, delivery.taskId, {
|
|
3215
|
+
status: "skipped",
|
|
3216
|
+
error_code: "NO_CONTENT"
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
2961
3221
|
if (STANDUP_TEMPLATES.has(templateId)) {
|
|
2962
3222
|
const standup = parseStandupSummary(output);
|
|
2963
3223
|
await api.post("/host/agent-status", {
|
|
@@ -2991,7 +3251,7 @@ async function processClaudeTaskResult(codeName, agentId, templateId, output, de
|
|
|
2991
3251
|
log(`[claude-scheduler] Kanban updates posted for '${codeName}' (${kanbanUpdates.length} updates)`);
|
|
2992
3252
|
}
|
|
2993
3253
|
}
|
|
2994
|
-
if (delivery?.mode === "announce" && delivery.to
|
|
3254
|
+
if (delivery?.mode === "announce" && delivery.to) {
|
|
2995
3255
|
await deliverScheduledTaskOutput(
|
|
2996
3256
|
codeName,
|
|
2997
3257
|
agentId,
|
|
@@ -4086,37 +4346,68 @@ async function sendSlackWebhookMessage(text) {
|
|
|
4086
4346
|
}
|
|
4087
4347
|
}
|
|
4088
4348
|
async function sendSlackChannelMessage(agentCodeName, channelId, text) {
|
|
4349
|
+
const result = await postSlackChannelMessage(agentCodeName, channelId, text);
|
|
4350
|
+
return result.ok;
|
|
4351
|
+
}
|
|
4352
|
+
async function postSlackChannelMessage(agentCodeName, channelId, text, threadTs) {
|
|
4089
4353
|
const botToken = agentChannelTokens.get(agentCodeName)?.slack;
|
|
4090
4354
|
if (!botToken) {
|
|
4091
4355
|
log(`No Slack bot token cached for '${agentCodeName}' \u2014 cannot post to ${channelId}`);
|
|
4092
|
-
return false;
|
|
4356
|
+
return { ok: false };
|
|
4093
4357
|
}
|
|
4094
4358
|
try {
|
|
4095
4359
|
const controller = new AbortController();
|
|
4096
4360
|
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
4097
4361
|
try {
|
|
4362
|
+
const body = { channel: channelId, text };
|
|
4363
|
+
if (threadTs) body.thread_ts = threadTs;
|
|
4098
4364
|
const response = await fetch("https://slack.com/api/chat.postMessage", {
|
|
4099
4365
|
method: "POST",
|
|
4100
4366
|
headers: {
|
|
4101
4367
|
"Authorization": `Bearer ${botToken}`,
|
|
4102
4368
|
"Content-Type": "application/json"
|
|
4103
4369
|
},
|
|
4104
|
-
body: JSON.stringify(
|
|
4370
|
+
body: JSON.stringify(body),
|
|
4105
4371
|
signal: controller.signal
|
|
4106
4372
|
});
|
|
4107
4373
|
clearTimeout(timeout);
|
|
4108
4374
|
const data = await response.json();
|
|
4109
4375
|
if (!data.ok) {
|
|
4110
4376
|
log(`Slack chat.postMessage failed for '${agentCodeName}' to ${channelId}: ${data.error}`);
|
|
4111
|
-
return false;
|
|
4377
|
+
return { ok: false };
|
|
4112
4378
|
}
|
|
4113
|
-
return true;
|
|
4379
|
+
return { ok: true, ts: data.ts };
|
|
4114
4380
|
} finally {
|
|
4115
4381
|
clearTimeout(timeout);
|
|
4116
4382
|
}
|
|
4117
4383
|
} catch (err) {
|
|
4118
4384
|
log(`Slack channel message error for '${agentCodeName}': ${err.message}`);
|
|
4119
|
-
return false;
|
|
4385
|
+
return { ok: false };
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
async function maybePostSlackThreadHint(agentCodeName, channelId, primaryTs) {
|
|
4389
|
+
if (!shouldIncludeHint(hintProbability(agentCodeName))) return;
|
|
4390
|
+
if (!primaryTs) {
|
|
4391
|
+
return;
|
|
4392
|
+
}
|
|
4393
|
+
const hint = pickHintVariant();
|
|
4394
|
+
const result = await postSlackChannelMessage(agentCodeName, channelId, hint, primaryTs);
|
|
4395
|
+
if (result.ok) {
|
|
4396
|
+
log(`[delivery-hint] Slack thread hint posted for '${agentCodeName}'`);
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
async function maybeSendTelegramFollowUpHint(agentCodeName, botToken, chatId) {
|
|
4400
|
+
if (!shouldIncludeHint(hintProbability(agentCodeName))) return;
|
|
4401
|
+
const hint = pickHintVariant();
|
|
4402
|
+
try {
|
|
4403
|
+
const result = await telegramApiCall(botToken, "sendMessage", { chat_id: chatId, text: hint });
|
|
4404
|
+
if (!result.ok) {
|
|
4405
|
+
log(`[delivery-hint] Telegram follow-up failed for '${agentCodeName}': ${result.description ?? "unknown"}`);
|
|
4406
|
+
return;
|
|
4407
|
+
}
|
|
4408
|
+
log(`[delivery-hint] Telegram follow-up hint posted for '${agentCodeName}'`);
|
|
4409
|
+
} catch (err) {
|
|
4410
|
+
log(`[delivery-hint] Telegram follow-up failed for '${agentCodeName}': ${err.message}`);
|
|
4120
4411
|
}
|
|
4121
4412
|
}
|
|
4122
4413
|
async function sendSlackAlert(alerts) {
|
|
@@ -4132,9 +4423,10 @@ ${a.detail}`;
|
|
|
4132
4423
|
${blocks.join("\n\n")}`);
|
|
4133
4424
|
}
|
|
4134
4425
|
async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, body, taskId) {
|
|
4426
|
+
const withLink = (b, medium) => withScheduleLinkFooter({ body: b, medium, codeName: agentCodeName, agentId, taskId });
|
|
4135
4427
|
if (typeof rawTarget === "string") {
|
|
4136
4428
|
if (rawTarget.startsWith("channel:")) {
|
|
4137
|
-
const result = await sendTaskNotification(agentCodeName, "slack", rawTarget, body);
|
|
4429
|
+
const result = await sendTaskNotification(agentCodeName, "slack", rawTarget, withLink(body, "slack"));
|
|
4138
4430
|
await reportDeliveryStatus(agentId, taskId, {
|
|
4139
4431
|
status: result.ok ? "ok" : "failed",
|
|
4140
4432
|
medium: "slack",
|
|
@@ -4143,7 +4435,7 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
|
|
|
4143
4435
|
return;
|
|
4144
4436
|
}
|
|
4145
4437
|
if (rawTarget.startsWith("chat:")) {
|
|
4146
|
-
const result = await sendTaskNotification(agentCodeName, "telegram", rawTarget, body);
|
|
4438
|
+
const result = await sendTaskNotification(agentCodeName, "telegram", rawTarget, withLink(body, "telegram"));
|
|
4147
4439
|
await reportDeliveryStatus(agentId, taskId, {
|
|
4148
4440
|
status: result.ok ? "ok" : "failed",
|
|
4149
4441
|
medium: "telegram",
|
|
@@ -4163,21 +4455,28 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
|
|
|
4163
4455
|
}
|
|
4164
4456
|
if (parsed.kind === "channel") {
|
|
4165
4457
|
if (parsed.provider === "slack") {
|
|
4166
|
-
const
|
|
4458
|
+
const channelId = parsed.channel_id ?? "";
|
|
4459
|
+
const sent = await postSlackChannelMessage(agentCodeName, channelId, withLink(body, "slack"));
|
|
4167
4460
|
await reportDeliveryStatus(agentId, taskId, {
|
|
4168
|
-
status: sent ? "ok" : "failed",
|
|
4461
|
+
status: sent.ok ? "ok" : "failed",
|
|
4169
4462
|
medium: "slack",
|
|
4170
|
-
error_code: sent ? null : "SLACK_SEND_FAILED"
|
|
4463
|
+
error_code: sent.ok ? null : "SLACK_SEND_FAILED"
|
|
4171
4464
|
});
|
|
4465
|
+
if (sent.ok) await maybePostSlackThreadHint(agentCodeName, channelId, sent.ts);
|
|
4172
4466
|
return;
|
|
4173
4467
|
}
|
|
4174
|
-
const
|
|
4175
|
-
const
|
|
4468
|
+
const chatId = parsed.chat_id ?? "";
|
|
4469
|
+
const toStr = `chat:${chatId}`;
|
|
4470
|
+
const result = await sendTaskNotification(agentCodeName, "telegram", toStr, withLink(body, "telegram"));
|
|
4176
4471
|
await reportDeliveryStatus(agentId, taskId, {
|
|
4177
4472
|
status: result.ok ? "ok" : "failed",
|
|
4178
4473
|
medium: "telegram",
|
|
4179
4474
|
error_code: result.ok ? null : result.error_code ?? "TELEGRAM_SEND_FAILED"
|
|
4180
4475
|
});
|
|
4476
|
+
if (result.ok) {
|
|
4477
|
+
const botToken = agentChannelTokens.get(agentCodeName)?.telegram;
|
|
4478
|
+
if (botToken) await maybeSendTelegramFollowUpHint(agentCodeName, botToken, chatId);
|
|
4479
|
+
}
|
|
4181
4480
|
return;
|
|
4182
4481
|
}
|
|
4183
4482
|
const agentRow = agentInfoForDelivery.get(agentCodeName);
|
|
@@ -4192,11 +4491,12 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
|
|
|
4192
4491
|
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: resolved.code });
|
|
4193
4492
|
return;
|
|
4194
4493
|
}
|
|
4195
|
-
const
|
|
4494
|
+
const attributionBody = appendDmFooter(
|
|
4196
4495
|
body,
|
|
4197
4496
|
agentRow.ownerTeamName,
|
|
4198
4497
|
agentRow.agentDisplayName
|
|
4199
4498
|
);
|
|
4499
|
+
const footeredBody = resolved.kind === "dm" ? withLink(attributionBody, resolved.medium === "slack" ? "slack" : "telegram") : attributionBody;
|
|
4200
4500
|
if (resolved.kind === "dm" && resolved.medium === "slack") {
|
|
4201
4501
|
const tokens = agentChannelTokens.get(agentCodeName);
|
|
4202
4502
|
const botToken = tokens?.slack;
|
|
@@ -4229,12 +4529,13 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
|
|
|
4229
4529
|
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: errCode, medium: "slack" });
|
|
4230
4530
|
return;
|
|
4231
4531
|
}
|
|
4232
|
-
const sent = await
|
|
4532
|
+
const sent = await postSlackChannelMessage(agentCodeName, openJson.channel.id, footeredBody);
|
|
4233
4533
|
await reportDeliveryStatus(agentId, taskId, {
|
|
4234
|
-
status: sent ? "ok" : "failed",
|
|
4534
|
+
status: sent.ok ? "ok" : "failed",
|
|
4235
4535
|
medium: "slack",
|
|
4236
|
-
error_code: sent ? null : "SLACK_SEND_FAILED"
|
|
4536
|
+
error_code: sent.ok ? null : "SLACK_SEND_FAILED"
|
|
4237
4537
|
});
|
|
4538
|
+
if (sent.ok) await maybePostSlackThreadHint(agentCodeName, openJson.channel.id, sent.ts);
|
|
4238
4539
|
} catch (err) {
|
|
4239
4540
|
const isAbort = err.name === "AbortError";
|
|
4240
4541
|
const errCode = isAbort ? "SLACK_OPEN_TIMEOUT" : "SLACK_EXCEPTION";
|
|
@@ -4262,6 +4563,7 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
|
|
|
4262
4563
|
return;
|
|
4263
4564
|
}
|
|
4264
4565
|
await reportDeliveryStatus(agentId, taskId, { status: "ok", medium: "telegram" });
|
|
4566
|
+
await maybeSendTelegramFollowUpHint(agentCodeName, botToken, resolved.telegram_chat_id);
|
|
4265
4567
|
} catch (err) {
|
|
4266
4568
|
log(`[delivery] Telegram DM exception for '${agentCodeName}': ${err.message}`);
|
|
4267
4569
|
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: "TELEGRAM_EXCEPTION", medium: "telegram" });
|