@integrity-labs/agt-cli 0.10.7 → 0.10.9

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.
@@ -10,7 +10,7 @@ import {
10
10
  provisionStopHook,
11
11
  requireHost,
12
12
  resolveChannels
13
- } from "../chunk-VDUA5FEW.js";
13
+ } from "../chunk-PZG4XPJV.js";
14
14
  import {
15
15
  findTaskByTemplate,
16
16
  getProjectDir,
@@ -34,8 +34,8 @@ import {
34
34
  import { createHash } from "crypto";
35
35
  import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync, readdirSync, statSync, unlinkSync, copyFileSync } from "fs";
36
36
  import https from "https";
37
- import { join, dirname } from "path";
38
- import { homedir } from "os";
37
+ import { join as join2, dirname } from "path";
38
+ import { homedir as homedir2 } from "os";
39
39
  import { fileURLToPath } from "url";
40
40
 
41
41
  // src/lib/plugin-context-render.ts
@@ -339,6 +339,88 @@ var GatewayClientPool = class extends EventEmitter {
339
339
  }
340
340
  };
341
341
 
342
+ // src/lib/claude-auth-detect.ts
343
+ import { readFile } from "fs/promises";
344
+ import { homedir, platform } from "os";
345
+ import { join } from "path";
346
+ import { execFile } from "child_process";
347
+ import { promisify } from "util";
348
+ var execFileAsync = promisify(execFile);
349
+ var EXPIRING_SOON_MS = 48 * 60 * 60 * 1e3;
350
+ async function detectClaudeAuth() {
351
+ if (process.env["ANTHROPIC_API_KEY"] || process.env["ANTHROPIC_AUTH_TOKEN"]) {
352
+ return { mode: "api_key", status: "valid", expires_at: null };
353
+ }
354
+ const creds = await readOauthCredentials();
355
+ if (!creds) return null;
356
+ return computeSubscriptionStatus(creds);
357
+ }
358
+ async function readOauthCredentials() {
359
+ if (platform() === "darwin") {
360
+ const fromKeychain = await readMacKeychain();
361
+ if (fromKeychain) return fromKeychain;
362
+ }
363
+ const candidates = [
364
+ join(homedir(), ".claude", ".credentials.json"),
365
+ join(homedir(), ".claude", "credentials.json")
366
+ ];
367
+ for (const path of candidates) {
368
+ const parsed = await readJsonSilently(path);
369
+ if (parsed) {
370
+ return parsed.claudeAiOauth ?? parsed.oauth ?? parsed;
371
+ }
372
+ }
373
+ return null;
374
+ }
375
+ async function readMacKeychain() {
376
+ try {
377
+ const { stdout } = await execFileAsync(
378
+ "security",
379
+ ["find-generic-password", "-s", "Claude Code-credentials", "-w"],
380
+ { timeout: 3e3 }
381
+ );
382
+ const parsed = JSON.parse(stdout.trim());
383
+ return parsed.claudeAiOauth ?? parsed;
384
+ } catch {
385
+ return null;
386
+ }
387
+ }
388
+ async function readJsonSilently(path) {
389
+ try {
390
+ const raw = await readFile(path, "utf-8");
391
+ return JSON.parse(raw);
392
+ } catch {
393
+ return null;
394
+ }
395
+ }
396
+ function computeSubscriptionStatus(creds) {
397
+ const expiresAtMs = parseExpiresAt(creds.expiresAt);
398
+ if (expiresAtMs === null) {
399
+ return {
400
+ mode: "subscription",
401
+ status: "unknown",
402
+ expires_at: null
403
+ };
404
+ }
405
+ const msUntilExpiry = expiresAtMs - Date.now();
406
+ const status = msUntilExpiry <= 0 ? "expired" : msUntilExpiry <= EXPIRING_SOON_MS ? "expiring_soon" : "valid";
407
+ return {
408
+ mode: "subscription",
409
+ status,
410
+ expires_at: new Date(expiresAtMs).toISOString()
411
+ };
412
+ }
413
+ function parseExpiresAt(raw) {
414
+ if (typeof raw === "number" && Number.isFinite(raw)) {
415
+ return raw < 1e12 ? raw * 1e3 : raw;
416
+ }
417
+ if (typeof raw === "string") {
418
+ const parsed = Date.parse(raw);
419
+ return Number.isNaN(parsed) ? null : parsed;
420
+ }
421
+ return null;
422
+ }
423
+
342
424
  // src/lib/realtime-chat.ts
343
425
  import { createClient } from "@supabase/supabase-js";
344
426
  var client = null;
@@ -622,8 +704,8 @@ function stopRealtimeChat() {
622
704
  var GATEWAY_PORT_BASE = 18800;
623
705
  var GATEWAY_PORT_STEP = 10;
624
706
  var GATEWAY_PORT_MAX = 18899;
625
- var AUGMENTED_DIR = join(process.env["HOME"] ?? "/tmp", ".augmented");
626
- var GATEWAY_PORTS_FILE = join(AUGMENTED_DIR, "gateway-ports.json");
707
+ var AUGMENTED_DIR = join2(process.env["HOME"] ?? "/tmp", ".augmented");
708
+ var GATEWAY_PORTS_FILE = join2(AUGMENTED_DIR, "gateway-ports.json");
627
709
  var config = null;
628
710
  var running = false;
629
711
  var pollTimer = null;
@@ -764,9 +846,9 @@ async function checkAndUpdateCli() {
764
846
  const isDevMode = cliPath.includes("/src/") || cliPath.includes("tsx");
765
847
  if (isDevMode) return;
766
848
  if (!isHomebrew && !cliPath.includes("node_modules")) return;
767
- const { homedir: homedir2 } = await import("os");
849
+ const { homedir: homedir3 } = await import("os");
768
850
  const { readFileSync: readF, writeFileSync: writeF } = await import("fs");
769
- const markerPath = join(homedir2(), ".augmented", ".last-update-check");
851
+ const markerPath = join2(homedir3(), ".augmented", ".last-update-check");
770
852
  try {
771
853
  const lastCheck = parseInt(readF(markerPath, "utf-8").trim(), 10);
772
854
  if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL_MS) return;
@@ -863,7 +945,7 @@ function freePort(codeName) {
863
945
  saveGatewayPorts(ports);
864
946
  }
865
947
  }
866
- var STATE_FILE = join(process.env["HOME"] ?? "/tmp", ".augmented", "manager-state.json");
948
+ var STATE_FILE = join2(process.env["HOME"] ?? "/tmp", ".augmented", "manager-state.json");
867
949
  function send(msg) {
868
950
  if (msg.type === "state-update") {
869
951
  try {
@@ -914,12 +996,12 @@ function parseSkillFrontmatter(content) {
914
996
  }
915
997
  async function refreshSkillsIndexInClaudeMd(configDir, codeName, log2) {
916
998
  const { readdirSync: readdirSync2, readFileSync: rfs, existsSync: ex, writeFileSync: writeFileSync2 } = await import("fs");
917
- const skillsDir = join(configDir, codeName, "project", ".claude", "skills");
918
- const claudeMdPath = join(configDir, codeName, "project", "CLAUDE.md");
999
+ const skillsDir = join2(configDir, codeName, "project", ".claude", "skills");
1000
+ const claudeMdPath = join2(configDir, codeName, "project", "CLAUDE.md");
919
1001
  if (!ex(skillsDir) || !ex(claudeMdPath)) return;
920
1002
  const entries = [];
921
1003
  for (const dir of readdirSync2(skillsDir).sort()) {
922
- const skillFile = join(skillsDir, dir, "SKILL.md");
1004
+ const skillFile = join2(skillsDir, dir, "SKILL.md");
923
1005
  if (!ex(skillFile)) continue;
924
1006
  try {
925
1007
  const { name, description } = parseSkillFrontmatter(rfs(skillFile, "utf-8"));
@@ -967,7 +1049,7 @@ ${SKILLS_INDEX_END}`;
967
1049
  }
968
1050
  async function migrateToProfiles() {
969
1051
  const homeDir = process.env["HOME"] ?? "/tmp";
970
- const sharedConfigPath = join(homeDir, ".openclaw", "openclaw.json");
1052
+ const sharedConfigPath = join2(homeDir, ".openclaw", "openclaw.json");
971
1053
  let sharedConfig;
972
1054
  try {
973
1055
  sharedConfig = JSON.parse(readFileSync(sharedConfigPath, "utf-8"));
@@ -983,19 +1065,19 @@ async function migrateToProfiles() {
983
1065
  const codeName = agentEntry["id"];
984
1066
  if (!codeName) continue;
985
1067
  if (codeName === "main") continue;
986
- const profileDir = join(homeDir, `.openclaw-${codeName}`);
987
- if (existsSync(join(profileDir, "openclaw.json"))) continue;
1068
+ const profileDir = join2(homeDir, `.openclaw-${codeName}`);
1069
+ if (existsSync(join2(profileDir, "openclaw.json"))) continue;
988
1070
  log(`Migrating agent '${codeName}' to per-agent profile`);
989
1071
  if (adapter.seedProfileConfig) {
990
1072
  adapter.seedProfileConfig(codeName);
991
1073
  }
992
- const sharedAuthDir = join(homeDir, ".openclaw", "agents", codeName, "agent");
993
- const profileAuthDir = join(profileDir, "agents", codeName, "agent");
994
- const authFile = join(sharedAuthDir, "auth-profiles.json");
1074
+ const sharedAuthDir = join2(homeDir, ".openclaw", "agents", codeName, "agent");
1075
+ const profileAuthDir = join2(profileDir, "agents", codeName, "agent");
1076
+ const authFile = join2(sharedAuthDir, "auth-profiles.json");
995
1077
  if (existsSync(authFile)) {
996
1078
  mkdirSync(profileAuthDir, { recursive: true });
997
1079
  const authContent = readFileSync(authFile, "utf-8");
998
- writeFileSync(join(profileAuthDir, "auth-profiles.json"), authContent);
1080
+ writeFileSync(join2(profileAuthDir, "auth-profiles.json"), authContent);
999
1081
  }
1000
1082
  allocatePort(codeName);
1001
1083
  migrated++;
@@ -1007,7 +1089,7 @@ async function migrateToProfiles() {
1007
1089
  function resolveModelChain(refreshData) {
1008
1090
  const agent = refreshData.agent;
1009
1091
  const modelDefaults = refreshData.model_defaults;
1010
- const platform = modelDefaults?.platform ?? {};
1092
+ const platform2 = modelDefaults?.platform ?? {};
1011
1093
  const org = modelDefaults?.org ?? {};
1012
1094
  function resolve(tier) {
1013
1095
  const agentField = `${tier}_model`;
@@ -1016,7 +1098,7 @@ function resolveModelChain(refreshData) {
1016
1098
  if (agentVal) return agentVal;
1017
1099
  const orgVal = org?.[platformField];
1018
1100
  if (orgVal) return orgVal;
1019
- const platformVal = platform?.[platformField];
1101
+ const platformVal = platform2?.[platformField];
1020
1102
  if (platformVal) return platformVal;
1021
1103
  return void 0;
1022
1104
  }
@@ -1033,7 +1115,7 @@ function readGatewayToken(codeName) {
1033
1115
  }
1034
1116
  const homeDir = process.env["HOME"] ?? "/tmp";
1035
1117
  try {
1036
- const cfg = JSON.parse(readFileSync(join(homeDir, `.openclaw-${codeName}`, "openclaw.json"), "utf-8"));
1118
+ const cfg = JSON.parse(readFileSync(join2(homeDir, `.openclaw-${codeName}`, "openclaw.json"), "utf-8"));
1037
1119
  return cfg?.gateway?.auth?.token;
1038
1120
  } catch {
1039
1121
  return void 0;
@@ -1042,7 +1124,7 @@ function readGatewayToken(codeName) {
1042
1124
  var GATEWAY_HUNG_TIMEOUT_MS = 5 * 6e4;
1043
1125
  function isGatewayHung(codeName) {
1044
1126
  const homeDir = process.env["HOME"] ?? "/tmp";
1045
- const jobsPath = join(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
1127
+ const jobsPath = join2(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
1046
1128
  if (!existsSync(jobsPath)) return false;
1047
1129
  try {
1048
1130
  const data = JSON.parse(readFileSync(jobsPath, "utf-8"));
@@ -1078,13 +1160,13 @@ async function ensureGatewayRunning(codeName, adapter) {
1078
1160
  }
1079
1161
  await new Promise((r) => setTimeout(r, 2e3));
1080
1162
  const homeDir = process.env["HOME"] ?? "/tmp";
1081
- const cronJobsPath = join(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
1163
+ const cronJobsPath = join2(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
1082
1164
  clearStaleCronRunState(cronJobsPath);
1083
1165
  } else {
1084
1166
  if (status.port) {
1085
1167
  try {
1086
1168
  const homeDir = process.env["HOME"] ?? "/tmp";
1087
- const configPath = join(homeDir, `.openclaw-${codeName}`, "openclaw.json");
1169
+ const configPath = join2(homeDir, `.openclaw-${codeName}`, "openclaw.json");
1088
1170
  if (existsSync(configPath)) {
1089
1171
  const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
1090
1172
  if (cfg.gateway?.port !== status.port) {
@@ -1110,7 +1192,7 @@ async function ensureGatewayRunning(codeName, adapter) {
1110
1192
  gatewaysStartedThisCycle.add(codeName);
1111
1193
  try {
1112
1194
  const homeDir = process.env["HOME"] ?? "/tmp";
1113
- const configPath = join(homeDir, `.openclaw-${codeName}`, "openclaw.json");
1195
+ const configPath = join2(homeDir, `.openclaw-${codeName}`, "openclaw.json");
1114
1196
  if (existsSync(configPath)) {
1115
1197
  const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
1116
1198
  if (!cfg.gateway) cfg.gateway = {};
@@ -1244,6 +1326,14 @@ async function pollCycle() {
1244
1326
  osUsername = userInfo().username;
1245
1327
  } catch {
1246
1328
  }
1329
+ let claudeAuth = null;
1330
+ try {
1331
+ claudeAuth = await detectClaudeAuth();
1332
+ } catch (err) {
1333
+ const errText = err instanceof Error ? err.message : String(err);
1334
+ const errId = createHash("sha256").update(errText).digest("hex").slice(0, 12);
1335
+ log(`Claude auth detection failed (error_id=${errId})`);
1336
+ }
1247
1337
  await api.post("/host/heartbeat", {
1248
1338
  host_id: hostId,
1249
1339
  framework_version: cachedFrameworkVersion ?? void 0,
@@ -1251,7 +1341,8 @@ async function pollCycle() {
1251
1341
  agent_runtime_authenticated: agentRuntimeAuthenticated,
1252
1342
  agent_diagnostics: agentDiagnostics,
1253
1343
  hostname: tailscaleHostname,
1254
- os_username: osUsername
1344
+ os_username: osUsername,
1345
+ claude_auth: claudeAuth ?? void 0
1255
1346
  });
1256
1347
  } catch (err) {
1257
1348
  log(`Heartbeat failed: ${err.message}`);
@@ -1310,7 +1401,7 @@ async function pollCycle() {
1310
1401
  const adapter = resolveAgentFramework(prev.codeName);
1311
1402
  await stopGatewayIfRunning(prev.codeName, adapter);
1312
1403
  freePort(prev.codeName);
1313
- const agentDir = join(config.configDir, prev.codeName, "provision");
1404
+ const agentDir = join2(config.configDir, prev.codeName, "provision");
1314
1405
  await cleanupAgentFiles(prev.codeName, agentDir);
1315
1406
  clearAgentCaches(prev.agentId, prev.codeName);
1316
1407
  }
@@ -1378,7 +1469,7 @@ async function processAgent(agent, agentStates) {
1378
1469
  agentFrameworkCache.set(agent.code_name, agent.framework);
1379
1470
  }
1380
1471
  const now = (/* @__PURE__ */ new Date()).toISOString();
1381
- const agentDir = join(config.configDir, agent.code_name, "provision");
1472
+ const agentDir = join2(config.configDir, agent.code_name, "provision");
1382
1473
  const adapter = resolveAgentFramework(agent.code_name);
1383
1474
  if (agent.status === "draft" || agent.status === "paused") {
1384
1475
  log(`Agent '${agent.code_name}' is ${agent.status}, skipping provisioning`);
@@ -1531,12 +1622,12 @@ async function processAgent(agent, agentStates) {
1531
1622
  const changedFiles = [];
1532
1623
  mkdirSync(agentDir, { recursive: true });
1533
1624
  for (const artifact of artifacts) {
1534
- const filePath = join(agentDir, artifact.relativePath);
1625
+ const filePath = join2(agentDir, artifact.relativePath);
1535
1626
  const newHash = sha256(artifact.content);
1536
1627
  let existingHash;
1537
1628
  if (artifact.relativePath === "CLAUDE.md") {
1538
1629
  try {
1539
- const projectClaudeMd = join(config.configDir, agent.code_name, "project", "CLAUDE.md");
1630
+ const projectClaudeMd = join2(config.configDir, agent.code_name, "project", "CLAUDE.md");
1540
1631
  const existing = readFileSync(projectClaudeMd, "utf-8");
1541
1632
  const stripped = existing.replace(new RegExp(`${SKILLS_INDEX_START}[\\s\\S]*?${SKILLS_INDEX_END}`), "").trimEnd();
1542
1633
  existingHash = sha256(stripped);
@@ -1551,22 +1642,22 @@ async function processAgent(agent, agentStates) {
1551
1642
  }
1552
1643
  }
1553
1644
  if (changedFiles.length > 0) {
1554
- const isFirst = !existsSync(join(agentDir, "CHARTER.md"));
1645
+ const isFirst = !existsSync(join2(agentDir, "CHARTER.md"));
1555
1646
  const verb = isFirst ? "Provisioning" : "Updating";
1556
1647
  const fileNames = changedFiles.map((f) => f.relativePath).join(", ");
1557
1648
  log(`${verb} '${agent.code_name}': ${fileNames}`);
1558
1649
  for (const file of changedFiles) {
1559
- const filePath = join(agentDir, file.relativePath);
1650
+ const filePath = join2(agentDir, file.relativePath);
1560
1651
  mkdirSync(dirname(filePath), { recursive: true });
1561
1652
  writeFileSync(filePath, file.content);
1562
1653
  }
1563
1654
  try {
1564
- const provSkillsDir = join(agentDir, ".claude", "skills");
1655
+ const provSkillsDir = join2(agentDir, ".claude", "skills");
1565
1656
  if (existsSync(provSkillsDir)) {
1566
1657
  for (const folder of readdirSync(provSkillsDir)) {
1567
1658
  if (folder.startsWith("knowledge-")) {
1568
1659
  try {
1569
- rmSync(join(provSkillsDir, folder), { recursive: true });
1660
+ rmSync(join2(provSkillsDir, folder), { recursive: true });
1570
1661
  } catch {
1571
1662
  }
1572
1663
  }
@@ -1579,7 +1670,7 @@ async function processAgent(agent, agentStates) {
1579
1670
  const trackedFiles = frameworkAdapter.driftTrackedFiles();
1580
1671
  const hashes = /* @__PURE__ */ new Map();
1581
1672
  for (const file of trackedFiles) {
1582
- const h = hashFile(join(agentDir, file));
1673
+ const h = hashFile(join2(agentDir, file));
1583
1674
  if (h) hashes.set(file, h);
1584
1675
  }
1585
1676
  writtenHashes.set(agent.agent_id, hashes);
@@ -1626,7 +1717,7 @@ async function processAgent(agent, agentStates) {
1626
1717
  if (written && existsSync(agentDir)) {
1627
1718
  const driftedFiles = [];
1628
1719
  for (const [file, expectedHash] of written) {
1629
- const localHash = hashFile(join(agentDir, file));
1720
+ const localHash = hashFile(join2(agentDir, file));
1630
1721
  if (localHash && localHash !== expectedHash) {
1631
1722
  driftedFiles.push(file);
1632
1723
  }
@@ -1637,7 +1728,7 @@ async function processAgent(agent, agentStates) {
1637
1728
  try {
1638
1729
  const localHashes = {};
1639
1730
  for (const file of driftedFiles) {
1640
- localHashes[file] = hashFile(join(agentDir, file));
1731
+ localHashes[file] = hashFile(join2(agentDir, file));
1641
1732
  }
1642
1733
  await api.post("/host/drift", {
1643
1734
  agent_id: agent.agent_id,
@@ -1707,7 +1798,7 @@ async function processAgent(agent, agentStates) {
1707
1798
  const entry = refreshData.channel_configs[channelId];
1708
1799
  if (!entry || entry.status !== "active" && entry.status !== "pending") continue;
1709
1800
  try {
1710
- const installedPath = join(homedir(), ".claude", "plugins", "installed_plugins.json");
1801
+ const installedPath = join2(homedir2(), ".claude", "plugins", "installed_plugins.json");
1711
1802
  if (existsSync(installedPath)) {
1712
1803
  const installed = JSON.parse(readFileSync(installedPath, "utf-8"));
1713
1804
  const pluginKeys = Object.keys(installed.plugins ?? {});
@@ -1741,16 +1832,16 @@ async function processAgent(agent, agentStates) {
1741
1832
  const agentSessionMode = refreshData.agent.session_mode;
1742
1833
  if (agentSessionMode === "persistent" && (agentFrameworkCache.get(agent.code_name) ?? "openclaw") === "claude-code") {
1743
1834
  try {
1744
- const projectDir = join(homedir(), ".augmented", agent.code_name, "project");
1835
+ const projectDir = join2(homedir2(), ".augmented", agent.code_name, "project");
1745
1836
  mkdirSync(projectDir, { recursive: true });
1746
- const channelsPath = join(projectDir, ".mcp-channels.json");
1837
+ const channelsPath = join2(projectDir, ".mcp-channels.json");
1747
1838
  let channelsMcp = { mcpServers: {} };
1748
1839
  try {
1749
1840
  channelsMcp = JSON.parse(readFileSync(channelsPath, "utf-8"));
1750
1841
  if (!channelsMcp.mcpServers) channelsMcp.mcpServers = {};
1751
1842
  } catch {
1752
1843
  }
1753
- const localDirectChatChannel = join(homedir(), ".augmented", "_mcp", "direct-chat-channel.js");
1844
+ const localDirectChatChannel = join2(homedir2(), ".augmented", "_mcp", "direct-chat-channel.js");
1754
1845
  if (existsSync(localDirectChatChannel) && !channelsMcp.mcpServers["direct-chat"]) {
1755
1846
  channelsMcp.mcpServers["direct-chat"] = {
1756
1847
  command: "node",
@@ -1857,9 +1948,9 @@ async function processAgent(agent, agentStates) {
1857
1948
  if (frameworkAdapter.removeMcpServer) {
1858
1949
  try {
1859
1950
  const { readFileSync: readFileSync2 } = await import("fs");
1860
- const { join: join2 } = await import("path");
1861
- const { homedir: homedir2 } = await import("os");
1862
- const mcpPath = join2(homedir2(), ".augmented", "agents", agent.code_name, "provision", ".mcp.json");
1951
+ const { join: join3 } = await import("path");
1952
+ const { homedir: homedir3 } = await import("os");
1953
+ const mcpPath = join3(homedir3(), ".augmented", "agents", agent.code_name, "provision", ".mcp.json");
1863
1954
  const mcpConfig = JSON.parse(readFileSync2(mcpPath, "utf-8"));
1864
1955
  if (mcpConfig.mcpServers) {
1865
1956
  const managedPrefixes = [
@@ -1952,7 +2043,7 @@ async function processAgent(agent, agentStates) {
1952
2043
  if (agent.status === "active") {
1953
2044
  if (frameworkAdapter.installPlugin) {
1954
2045
  try {
1955
- const pluginPath = join(process.cwd(), "packages", "openclaw-plugin-augmented", "src", "index.ts");
2046
+ const pluginPath = join2(process.cwd(), "packages", "openclaw-plugin-augmented", "src", "index.ts");
1956
2047
  if (existsSync(pluginPath)) {
1957
2048
  frameworkAdapter.installPlugin(agent.code_name, "augmented", pluginPath, {
1958
2049
  agtHost: requireHost(),
@@ -2007,15 +2098,15 @@ async function processAgent(agent, agentStates) {
2007
2098
  }
2008
2099
  }
2009
2100
  try {
2010
- const agentSkillsDir = join(config.configDir, agent.code_name, "project", ".claude", "skills");
2101
+ const agentSkillsDir = join2(config.configDir, agent.code_name, "project", ".claude", "skills");
2011
2102
  if (existsSync(agentSkillsDir)) {
2012
2103
  const { readdirSync: readdirSync2, rmSync: rmSync2 } = await import("fs");
2013
2104
  for (const entry of readdirSync2(agentSkillsDir)) {
2014
2105
  if (entry.startsWith("plugin-") && !currentPluginSkillIds.has(entry)) {
2015
- const orphanPath = join(agentSkillsDir, entry);
2106
+ const orphanPath = join2(agentSkillsDir, entry);
2016
2107
  rmSync2(orphanPath, { recursive: true, force: true });
2017
2108
  log(`Removed orphaned plugin skill '${entry}' for '${agent.code_name}'`);
2018
- const provisionSkillPath = join(config.configDir, agent.code_name, "skills", entry);
2109
+ const provisionSkillPath = join2(config.configDir, agent.code_name, "skills", entry);
2019
2110
  if (existsSync(provisionSkillPath)) {
2020
2111
  rmSync2(provisionSkillPath, { recursive: true, force: true });
2021
2112
  }
@@ -2169,7 +2260,7 @@ async function processAgent(agent, agentStates) {
2169
2260
  lastWorkTriggerAt.set(agent.code_name, triggerTs);
2170
2261
  if (agentFw === "openclaw" && gatewayRunning && gatewayPort) {
2171
2262
  const homeDir = process.env["HOME"] ?? "/tmp";
2172
- const jobsPath = join(homeDir, `.openclaw-${agent.code_name}`, "cron", "jobs.json");
2263
+ const jobsPath = join2(homeDir, `.openclaw-${agent.code_name}`, "cron", "jobs.json");
2173
2264
  if (existsSync(jobsPath)) {
2174
2265
  try {
2175
2266
  const jobsData = JSON.parse(readFileSync(jobsPath, "utf-8"));
@@ -2324,16 +2415,16 @@ function cleanupStaleSessions(codeName) {
2324
2415
  lastCleanupAt.set(codeName, Date.now());
2325
2416
  const homeDir = process.env["HOME"] ?? "/tmp";
2326
2417
  for (const agentDir of ["main", codeName]) {
2327
- const sessionsDir = join(homeDir, `.openclaw-${codeName}`, "agents", agentDir, "sessions");
2418
+ const sessionsDir = join2(homeDir, `.openclaw-${codeName}`, "agents", agentDir, "sessions");
2328
2419
  cleanupCronSessions(sessionsDir, CRON_SESSION_KEEP_COUNT);
2329
2420
  }
2330
- const cronRunsDir = join(homeDir, `.openclaw-${codeName}`, "cron", "runs");
2421
+ const cronRunsDir = join2(homeDir, `.openclaw-${codeName}`, "cron", "runs");
2331
2422
  cleanupOldFiles(cronRunsDir, CRON_RUN_RETENTION_DAYS, ".jsonl");
2332
- const cronJobsPath = join(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
2423
+ const cronJobsPath = join2(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
2333
2424
  clearStaleCronRunState(cronJobsPath);
2334
2425
  }
2335
2426
  function cleanupCronSessions(sessionsDir, keepCount) {
2336
- const indexPath = join(sessionsDir, "sessions.json");
2427
+ const indexPath = join2(sessionsDir, "sessions.json");
2337
2428
  if (!existsSync(indexPath)) return;
2338
2429
  try {
2339
2430
  const raw = readFileSync(indexPath, "utf-8");
@@ -2349,7 +2440,7 @@ function cleanupCronSessions(sessionsDir, keepCount) {
2349
2440
  for (const entry of toDelete) {
2350
2441
  delete index[entry.key];
2351
2442
  if (entry.sessionId) {
2352
- const sessionFile = join(sessionsDir, `${entry.sessionId}.jsonl`);
2443
+ const sessionFile = join2(sessionsDir, `${entry.sessionId}.jsonl`);
2353
2444
  try {
2354
2445
  if (existsSync(sessionFile)) {
2355
2446
  unlinkSync(sessionFile);
@@ -2371,7 +2462,7 @@ function cleanupCronSessions(sessionsDir, keepCount) {
2371
2462
  delete index[parentKey];
2372
2463
  if (parentSessionId) {
2373
2464
  try {
2374
- const f = join(sessionsDir, `${parentSessionId}.jsonl`);
2465
+ const f = join2(sessionsDir, `${parentSessionId}.jsonl`);
2375
2466
  if (existsSync(f)) {
2376
2467
  unlinkSync(f);
2377
2468
  deletedFiles++;
@@ -2429,7 +2520,7 @@ function cleanupOldFiles(dir, maxAgeDays, ext) {
2429
2520
  try {
2430
2521
  for (const f of readdirSync(dir)) {
2431
2522
  if (!f.endsWith(ext)) continue;
2432
- const fullPath = join(dir, f);
2523
+ const fullPath = join2(dir, f);
2433
2524
  try {
2434
2525
  const st = statSync(fullPath);
2435
2526
  if (st.mtimeMs < cutoff) {
@@ -2489,6 +2580,12 @@ async function syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData
2489
2580
  for (const task of ready) {
2490
2581
  if (inFlightClaudeTasks.has(task.taskId)) continue;
2491
2582
  if ((claudeTaskConcurrency.get(codeName) ?? 0) >= MAX_CLAUDE_CONCURRENCY) break;
2583
+ if (KANBAN_WORK_TEMPLATES.has(task.templateId) && !hasActionableItems(boardItems)) {
2584
+ log(`[claude-scheduler] Skipping '${task.name}' for '${codeName}' \u2014 board is empty`);
2585
+ const updated = markTaskFired(codeName, task.taskId, "ok");
2586
+ claudeSchedulerStates.set(codeName, updated);
2587
+ continue;
2588
+ }
2492
2589
  let prompt = task.prompt;
2493
2590
  if (BOARD_INJECT_TEMPLATES.has(task.templateId) && boardItems.length > 0) {
2494
2591
  const template = PLAN_TEMPLATES.has(task.templateId) ? "morning-plan" : "follow-up";
@@ -2520,12 +2617,12 @@ async function syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData
2520
2617
  }
2521
2618
  async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
2522
2619
  const projectDir = getProjectDir(codeName);
2523
- const mcpConfigPath = join(projectDir, ".mcp.json");
2620
+ const mcpConfigPath = join2(projectDir, ".mcp.json");
2524
2621
  sanitizeMcpJson(mcpConfigPath, requireHost());
2525
2622
  try {
2526
- const claudeMdPath = join(projectDir, "CLAUDE.md");
2623
+ const claudeMdPath = join2(projectDir, "CLAUDE.md");
2527
2624
  const serverNames = [];
2528
- const channelsConfigPath = join(projectDir, ".mcp-channels.json");
2625
+ const channelsConfigPath = join2(projectDir, ".mcp-channels.json");
2529
2626
  for (const p of [mcpConfigPath, channelsConfigPath]) {
2530
2627
  if (existsSync(p)) {
2531
2628
  try {
@@ -2554,7 +2651,7 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
2554
2651
  claudeArgs.push("--system-prompt-file", claudeMdPath);
2555
2652
  }
2556
2653
  const childEnv = { ...process.env };
2557
- const envIntPath = join(projectDir, ".env.integrations");
2654
+ const envIntPath = join2(projectDir, ".env.integrations");
2558
2655
  if (existsSync(envIntPath)) {
2559
2656
  try {
2560
2657
  for (const line of readFileSync(envIntPath, "utf-8").split("\n")) {
@@ -2675,8 +2772,8 @@ var persistentSessionAgents = /* @__PURE__ */ new Set();
2675
2772
  async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
2676
2773
  const codeName = agent.code_name;
2677
2774
  const projectDir = getProjectDir2(codeName);
2678
- const mcpConfigPath = join(projectDir, ".mcp.json");
2679
- const claudeMdPath = join(projectDir, "CLAUDE.md");
2775
+ const mcpConfigPath = join2(projectDir, ".mcp.json");
2776
+ const claudeMdPath = join2(projectDir, "CLAUDE.md");
2680
2777
  const channelConfigs = refreshData.channel_configs;
2681
2778
  const channels = [];
2682
2779
  const devChannels = [];
@@ -2763,6 +2860,12 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
2763
2860
  log(`[persistent-session] ${ready.length} ready task(s) for '${codeName}': ${ready.map((t) => `${t.name}(next=${t.nextFireAt ? new Date(t.nextFireAt).toISOString() : "null"})`).join(", ")}`);
2764
2861
  }
2765
2862
  for (const task of ready) {
2863
+ if (KANBAN_WORK_TEMPLATES.has(task.templateId) && !hasActionableItems(boardItems)) {
2864
+ log(`[persistent-session] Skipping '${task.name}' for '${codeName}' \u2014 board is empty`);
2865
+ const updated = markTaskFired(codeName, task.taskId, "ok");
2866
+ claudeSchedulerStates.set(codeName, updated);
2867
+ continue;
2868
+ }
2766
2869
  let prompt = task.prompt;
2767
2870
  if (BOARD_INJECT_TEMPLATES.has(task.templateId) && boardItems.length > 0) {
2768
2871
  const template = PLAN_TEMPLATES.has(task.templateId) ? "morning-plan" : "follow-up";
@@ -3062,8 +3165,8 @@ async function processDirectChatMessage(agent, msg) {
3062
3165
  if (fw === "claude-code") {
3063
3166
  const { getProjectDir: ccProjectDir } = await import("../claude-scheduler-7PVWQHWU.js");
3064
3167
  const projDir = ccProjectDir(agent.codeName);
3065
- const mcpConfigPath = join(projDir, ".mcp.json");
3066
- const channelsConfigPath = join(projDir, ".mcp-channels.json");
3168
+ const mcpConfigPath = join2(projDir, ".mcp.json");
3169
+ const channelsConfigPath = join2(projDir, ".mcp-channels.json");
3067
3170
  const serverNames = [];
3068
3171
  for (const p of [mcpConfigPath, channelsConfigPath]) {
3069
3172
  if (existsSync(p)) {
@@ -3088,11 +3191,11 @@ async function processDirectChatMessage(agent, msg) {
3088
3191
  allowedTools
3089
3192
  ];
3090
3193
  if (existsSync(channelsConfigPath)) chatArgs.push("--mcp-config", channelsConfigPath);
3091
- const chatClaudeMd = join(projDir, "CLAUDE.md");
3194
+ const chatClaudeMd = join2(projDir, "CLAUDE.md");
3092
3195
  if (existsSync(chatClaudeMd)) {
3093
3196
  chatArgs.push("--system-prompt-file", chatClaudeMd);
3094
3197
  }
3095
- const envIntPath = join(projDir, ".env.integrations");
3198
+ const envIntPath = join2(projDir, ".env.integrations");
3096
3199
  const childEnv = { ...process.env };
3097
3200
  if (existsSync(envIntPath)) {
3098
3201
  try {
@@ -3154,6 +3257,10 @@ var TASK_UPDATE_TEMPLATES = /* @__PURE__ */ new Set(["hourly-status", "task-upda
3154
3257
  var PLAN_TEMPLATES = /* @__PURE__ */ new Set(["morning-plan"]);
3155
3258
  var KANBAN_WORK_TEMPLATES = /* @__PURE__ */ new Set(["kanban-work"]);
3156
3259
  var BOARD_INJECT_TEMPLATES = /* @__PURE__ */ new Set(["morning-plan", "task-update", "hourly-status", "end-of-day-summary", "kanban-work"]);
3260
+ var ACTIONABLE_STATUSES = /* @__PURE__ */ new Set(["backlog", "today", "in_progress"]);
3261
+ function hasActionableItems(items) {
3262
+ return items.some((item) => ACTIONABLE_STATUSES.has(item.status));
3263
+ }
3157
3264
  var lastHarvestAt = /* @__PURE__ */ new Map();
3158
3265
  var HARVEST_INTERVAL_MS = 3 * 60 * 1e3;
3159
3266
  var kanbanBoardCache = /* @__PURE__ */ new Map();
@@ -3369,8 +3476,8 @@ function getBuiltInSkillContent(skillId) {
3369
3476
  if (builtInSkillCache.has(skillId)) return builtInSkillCache.get(skillId);
3370
3477
  try {
3371
3478
  const candidates = [
3372
- join(process.cwd(), "skills", skillId, "SKILL.md"),
3373
- join(new URL(".", import.meta.url).pathname, "..", "..", "..", "..", "skills", skillId, "SKILL.md")
3479
+ join2(process.cwd(), "skills", skillId, "SKILL.md"),
3480
+ join2(new URL(".", import.meta.url).pathname, "..", "..", "..", "..", "skills", skillId, "SKILL.md")
3374
3481
  ];
3375
3482
  for (const candidate of candidates) {
3376
3483
  if (existsSync(candidate)) {
@@ -3864,8 +3971,8 @@ function parseMemoryFile(raw, fallbackName) {
3864
3971
  };
3865
3972
  }
3866
3973
  async function syncMemories(agent, configDir, log2) {
3867
- const projectDir = join(configDir, agent.code_name, "project");
3868
- const memoryDir = join(projectDir, "memory");
3974
+ const projectDir = join2(configDir, agent.code_name, "project");
3975
+ const memoryDir = join2(projectDir, "memory");
3869
3976
  if (existsSync(memoryDir)) {
3870
3977
  const prevHashes = memoryFileHashes.get(agent.agent_id) ?? /* @__PURE__ */ new Map();
3871
3978
  const currentHashes = /* @__PURE__ */ new Map();
@@ -3873,7 +3980,7 @@ async function syncMemories(agent, configDir, log2) {
3873
3980
  for (const file of readdirSync(memoryDir)) {
3874
3981
  if (!file.endsWith(".md")) continue;
3875
3982
  try {
3876
- const raw = readFileSync(join(memoryDir, file), "utf-8");
3983
+ const raw = readFileSync(join2(memoryDir, file), "utf-8");
3877
3984
  const fileHash = createHash("sha256").update(raw).digest("hex").slice(0, 16);
3878
3985
  currentHashes.set(file, fileHash);
3879
3986
  if (prevHashes.get(file) === fileHash) continue;
@@ -3897,8 +4004,8 @@ async function syncMemories(agent, configDir, log2) {
3897
4004
  log2(`Synced ${changedMemories.length} changed memories to DB for '${agent.code_name}'`);
3898
4005
  } catch (err) {
3899
4006
  for (const mem of changedMemories) {
3900
- for (const [file, hash] of currentHashes) {
3901
- const parsed = parseMemoryFile(readFileSync(join(memoryDir, file), "utf-8"), "");
4007
+ for (const [file] of currentHashes) {
4008
+ const parsed = parseMemoryFile(readFileSync(join2(memoryDir, file), "utf-8"), file.replace(/\.md$/, ""));
3902
4009
  if (parsed?.name === mem.name) currentHashes.delete(file);
3903
4010
  }
3904
4011
  }
@@ -3910,12 +4017,14 @@ async function syncMemories(agent, configDir, log2) {
3910
4017
  const localListHash = createHash("sha256").update(localFiles.join(",")).digest("hex").slice(0, 16);
3911
4018
  const prevLocalHash = lastLocalFileHash.get(agent.agent_id);
3912
4019
  const prevDownload = lastDownloadHash.get(agent.agent_id);
3913
- if (prevDownload && prevLocalHash === localListHash) return;
3914
4020
  try {
3915
4021
  const dbMemories = await api.post("/host/memories", {
3916
4022
  agent_id: agent.agent_id
3917
4023
  });
3918
4024
  const responseHash = createHash("sha256").update(JSON.stringify(dbMemories.memories ?? [])).digest("hex").slice(0, 16);
4025
+ if (prevDownload && prevLocalHash === localListHash && lastDownloadHash.get(agent.agent_id) === responseHash) {
4026
+ return;
4027
+ }
3919
4028
  lastDownloadHash.set(agent.agent_id, responseHash);
3920
4029
  lastLocalFileHash.set(agent.agent_id, localListHash);
3921
4030
  if (dbMemories.memories?.length) {
@@ -3934,7 +4043,7 @@ description: ${JSON.stringify(mem.content.slice(0, 200))}
3934
4043
 
3935
4044
  ${mem.content}
3936
4045
  `;
3937
- writeFileSync(join(memoryDir, `${slug}.md`), fileContent);
4046
+ writeFileSync(join2(memoryDir, `${slug}.md`), fileContent);
3938
4047
  written++;
3939
4048
  }
3940
4049
  if (written > 0) {
@@ -4105,14 +4214,14 @@ function startManager(opts) {
4105
4214
  startPolling();
4106
4215
  }
4107
4216
  function deployMcpAssets() {
4108
- const targetDir = join(homedir(), ".augmented", "_mcp");
4217
+ const targetDir = join2(homedir2(), ".augmented", "_mcp");
4109
4218
  mkdirSync(targetDir, { recursive: true });
4110
4219
  const moduleDir = dirname(fileURLToPath(import.meta.url));
4111
4220
  let mcpSourceDir = "";
4112
4221
  let dir = moduleDir;
4113
4222
  for (let i = 0; i < 6; i++) {
4114
- const candidate = join(dir, "mcp");
4115
- if (existsSync(join(candidate, "index.js"))) {
4223
+ const candidate = join2(dir, "mcp");
4224
+ if (existsSync(join2(candidate, "index.js"))) {
4116
4225
  mcpSourceDir = candidate;
4117
4226
  break;
4118
4227
  }
@@ -4125,8 +4234,8 @@ function deployMcpAssets() {
4125
4234
  return;
4126
4235
  }
4127
4236
  for (const file of ["index.js", "slack-channel.js", "direct-chat-channel.js"]) {
4128
- const src = join(mcpSourceDir, file);
4129
- const dst = join(targetDir, file);
4237
+ const src = join2(mcpSourceDir, file);
4238
+ const dst = join2(targetDir, file);
4130
4239
  if (!existsSync(src)) continue;
4131
4240
  try {
4132
4241
  copyFileSync(src, dst);
@@ -4135,14 +4244,14 @@ function deployMcpAssets() {
4135
4244
  }
4136
4245
  }
4137
4246
  log(`[manager] MCP assets deployed to ${targetDir}`);
4138
- const localMcpPath = join(targetDir, "index.js");
4247
+ const localMcpPath = join2(targetDir, "index.js");
4139
4248
  try {
4140
- const agentsDir = join(homedir(), ".augmented", "agents");
4249
+ const agentsDir = join2(homedir2(), ".augmented", "agents");
4141
4250
  if (existsSync(agentsDir)) {
4142
4251
  for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
4143
4252
  if (!entry.isDirectory()) continue;
4144
4253
  for (const subdir of ["provision", "project"]) {
4145
- const mcpJsonPath = join(agentsDir, entry.name, subdir, ".mcp.json");
4254
+ const mcpJsonPath = join2(agentsDir, entry.name, subdir, ".mcp.json");
4146
4255
  try {
4147
4256
  const raw = readFileSync(mcpJsonPath, "utf-8");
4148
4257
  if (!raw.includes("@integrity-labs/augmented-mcp")) continue;