@integrity-labs/agt-cli 0.10.1 → 0.10.2

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.
@@ -6,10 +6,11 @@ import {
6
6
  getFramework,
7
7
  getHostId,
8
8
  provision,
9
+ provisionIsolationHook,
9
10
  provisionStopHook,
10
11
  requireHost,
11
12
  resolveChannels
12
- } from "../chunk-N7TRKQMT.js";
13
+ } from "../chunk-VCKY6MN2.js";
13
14
  import {
14
15
  findTaskByTemplate,
15
16
  getProjectDir,
@@ -37,6 +38,38 @@ import { join, dirname } from "path";
37
38
  import { homedir } from "os";
38
39
  import { fileURLToPath } from "url";
39
40
 
41
+ // src/lib/plugin-context-render.ts
42
+ var PLUGIN_CONTEXT_PLACEHOLDER_RE = /\{\{\s*context\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
43
+ var TEAM_OVERRIDES_HEADER = "## Team Overrides\n\n> **The following overrides anything you've read above.** If any rule here conflicts with earlier instructions in this skill, follow what is written here. These are user-supplied directives that take precedence over the plugin's default guidance.\n";
44
+ function formatContextValue(value) {
45
+ if (value === null || value === void 0) return "";
46
+ if (typeof value === "string") return value;
47
+ if (typeof value === "boolean" || typeof value === "number") return String(value);
48
+ try {
49
+ return JSON.stringify(value);
50
+ } catch {
51
+ return "";
52
+ }
53
+ }
54
+ function renderPluginSkillContent(raw, values, overrides, warn = () => {
55
+ }) {
56
+ const substituted = raw.replace(PLUGIN_CONTEXT_PLACEHOLDER_RE, (_match, fieldName) => {
57
+ if (!(fieldName in values)) {
58
+ warn(`unresolved placeholder {{context.${fieldName}}} \u2014 substituting empty string`);
59
+ return "";
60
+ }
61
+ return formatContextValue(values[fieldName]);
62
+ });
63
+ const trimmedOverrides = overrides.trim();
64
+ if (!trimmedOverrides) return substituted;
65
+ const separator = substituted.endsWith("\n") ? "\n" : "\n\n";
66
+ return `${substituted}${separator}---
67
+
68
+ ${TEAM_OVERRIDES_HEADER}
69
+ ${trimmedOverrides}
70
+ `;
71
+ }
72
+
40
73
  // src/lib/gateway-client.ts
41
74
  import { EventEmitter } from "events";
42
75
  import WebSocket from "ws";
@@ -314,6 +347,7 @@ var driftChannel = null;
314
347
  var assignChannel = null;
315
348
  var configChannel = null;
316
349
  var kanbanChannel = null;
350
+ var pluginContextChannel = null;
317
351
  var connected = false;
318
352
  var tearingDown = false;
319
353
  function ensureClient(config2) {
@@ -358,6 +392,7 @@ function startRealtimeChat(config2) {
358
392
  assignChannel = null;
359
393
  configChannel = null;
360
394
  kanbanChannel = null;
395
+ pluginContextChannel = null;
361
396
  if (client) {
362
397
  try {
363
398
  client.removeAllChannels();
@@ -489,6 +524,38 @@ function startRealtimeKanban(config2) {
489
524
  });
490
525
  log2(`[realtime] Subscribing to agent_kanban_items for ${agentIds.length} agent(s)`);
491
526
  }
527
+ function startRealtimePluginContext(config2) {
528
+ const { agentIds, onContextChange, log: log2 } = config2;
529
+ if (agentIds.length === 0) return;
530
+ const sb = ensureClient(config2);
531
+ const filterStr = agentIds.length === 1 ? `agent_id=eq.${agentIds[0]}` : `agent_id=in.(${agentIds.join(",")})`;
532
+ pluginContextChannel = sb.channel("plugin-context-realtime").on("postgres_changes", {
533
+ event: "INSERT",
534
+ schema: "public",
535
+ table: "plugin_context",
536
+ filter: filterStr
537
+ }, (payload) => {
538
+ const row = payload.new;
539
+ log2(`[realtime] plugin_context INSERT for agent ${row.agent_id} (plugin ${row.plugin_id})`);
540
+ onContextChange(row);
541
+ }).on("postgres_changes", {
542
+ event: "UPDATE",
543
+ schema: "public",
544
+ table: "plugin_context",
545
+ filter: filterStr
546
+ }, (payload) => {
547
+ const row = payload.new;
548
+ log2(`[realtime] plugin_context UPDATE for agent ${row.agent_id} (plugin ${row.plugin_id})`);
549
+ onContextChange(row);
550
+ }).subscribe((status) => {
551
+ if (status === "SUBSCRIBED") {
552
+ log2("[realtime] Plugin context channel connected");
553
+ } else if (status === "CLOSED" || status === "CHANNEL_ERROR") {
554
+ log2(`[realtime] Plugin context channel: ${status}`);
555
+ }
556
+ });
557
+ log2(`[realtime] Subscribing to plugin_context for ${agentIds.length} agent(s)`);
558
+ }
492
559
  function isRealtimeConnected() {
493
560
  return connected;
494
561
  }
@@ -531,6 +598,13 @@ function stopRealtimeChat() {
531
598
  }
532
599
  kanbanChannel = null;
533
600
  }
601
+ if (pluginContextChannel) {
602
+ try {
603
+ pluginContextChannel.unsubscribe();
604
+ } catch {
605
+ }
606
+ pluginContextChannel = null;
607
+ }
534
608
  if (client) {
535
609
  try {
536
610
  client.removeAllChannels();
@@ -818,6 +892,76 @@ function hashFile(filePath) {
818
892
  return null;
819
893
  }
820
894
  }
895
+ var SKILLS_INDEX_START = "<!-- AGT:SKILLS_INDEX_START -->";
896
+ var SKILLS_INDEX_END = "<!-- AGT:SKILLS_INDEX_END -->";
897
+ function sanitizeSkillsIndexText(value) {
898
+ return value.replaceAll(SKILLS_INDEX_START, "").replaceAll(SKILLS_INDEX_END, "").replace(/<!--[\s\S]*?-->/g, "").replace(/\r?\n+/g, " ").trim();
899
+ }
900
+ function parseSkillFrontmatter(content) {
901
+ if (!content.startsWith("---")) return {};
902
+ const end = content.indexOf("\n---", 3);
903
+ if (end === -1) return {};
904
+ const block = content.slice(3, end);
905
+ const out = {};
906
+ for (const line of block.split("\n")) {
907
+ const m = line.match(/^(name|description)\s*:\s*(.*)$/);
908
+ if (m && m[1] && m[2] !== void 0) out[m[1]] = m[2].trim().replace(/^["']|["']$/g, "");
909
+ }
910
+ return out;
911
+ }
912
+ async function refreshSkillsIndexInClaudeMd(configDir, codeName, log2) {
913
+ const { readdirSync: readdirSync2, readFileSync: rfs, existsSync: ex, writeFileSync: writeFileSync2 } = await import("fs");
914
+ const skillsDir = join(configDir, codeName, "project", ".claude", "skills");
915
+ const claudeMdPath = join(configDir, codeName, "project", "CLAUDE.md");
916
+ if (!ex(skillsDir) || !ex(claudeMdPath)) return;
917
+ const entries = [];
918
+ for (const dir of readdirSync2(skillsDir).sort()) {
919
+ const skillFile = join(skillsDir, dir, "SKILL.md");
920
+ if (!ex(skillFile)) continue;
921
+ try {
922
+ const { name, description } = parseSkillFrontmatter(rfs(skillFile, "utf-8"));
923
+ entries.push({
924
+ id: dir,
925
+ name: sanitizeSkillsIndexText(name ?? dir),
926
+ description: sanitizeSkillsIndexText(description ?? "(no description)")
927
+ });
928
+ } catch {
929
+ }
930
+ }
931
+ const body = entries.length ? entries.map((e) => `- **${e.name}** \u2014 ${e.description}`).join("\n") : "_(no skills installed)_";
932
+ const section = `${SKILLS_INDEX_START}
933
+ ## Available Skills
934
+
935
+ The following skills are installed in \`.claude/skills/\`. Claude Code auto-activates them when relevant \u2014 you don't need to read them manually, but you should know they exist.
936
+
937
+ ${body}
938
+
939
+ ## Updating Plugins (ENG-4341)
940
+
941
+ Plugin skills under \`.claude/skills/plugin-*/SKILL.md\` are **read-only** and managed by the platform. They are derived from the plugin's database row plus any per-agent context overrides, and are re-rendered every time the manager polls or a context change is broadcast over Supabase Realtime.
942
+
943
+ **Never edit \`.claude/skills/plugin-*/SKILL.md\` files directly.** If you do, your edit will be silently overwritten on the next manager refresh, AND it won't propagate to other agents using the same plugin.
944
+
945
+ To change a plugin's behavior (add a rule, update a default, tell the plugin not to do something), call the **\`plugin.improve\`** MCP tool with the user's request. The tool calls the platform API, which uses an LLM to translate the request into a structured update of the plugin's typed context fields and/or freeform overrides text. You'll get a diff back to show the user; on confirmation, call the tool again with \`auto_apply: true\` to apply.
946
+
947
+ Examples of when to use \`plugin.improve\`:
948
+ - *"Update the Coding plugin so we always use trunk instead of main"*
949
+ - *"Add a rule to the Coding plugin: never merge directly to main, always open a PR"*
950
+ - *"Tell the Knowledge Base plugin not to return results below 70% relevance"*
951
+ - *"The Slack plugin should post incidents to #oncall, not #general"*
952
+ ${SKILLS_INDEX_END}`;
953
+ const current = rfs(claudeMdPath, "utf-8");
954
+ let next;
955
+ if (current.includes(SKILLS_INDEX_START) && current.includes(SKILLS_INDEX_END)) {
956
+ next = current.replace(new RegExp(`${SKILLS_INDEX_START}[\\s\\S]*?${SKILLS_INDEX_END}`), section);
957
+ } else {
958
+ next = current.trimEnd() + "\n\n" + section + "\n";
959
+ }
960
+ if (next !== current) {
961
+ writeFileSync2(claudeMdPath, next, "utf-8");
962
+ log2(`Refreshed skills index in CLAUDE.md for '${codeName}' (${entries.length} skills)`);
963
+ }
964
+ }
821
965
  async function migrateToProfiles() {
822
966
  const homeDir = process.env["HOME"] ?? "/tmp";
823
967
  const sharedConfigPath = join(homeDir, ".openclaw", "openclaw.json");
@@ -1186,6 +1330,7 @@ async function pollCycle() {
1186
1330
  ensureRealtimeAssignStarted(agentStates);
1187
1331
  ensureRealtimeConfigStarted(agentStates);
1188
1332
  ensureRealtimeKanbanStarted(agentStates);
1333
+ ensureRealtimePluginContextStarted(agentStates);
1189
1334
  try {
1190
1335
  const spawnData = await api.post("/host/kanban/recurring/spawn");
1191
1336
  if (spawnData.spawned > 0) {
@@ -1527,6 +1672,36 @@ async function processAgent(agent, agentStates) {
1527
1672
  }
1528
1673
  }
1529
1674
  }
1675
+ const agentSessionMode = refreshData.agent.session_mode;
1676
+ if (agentSessionMode === "persistent" && (agentFrameworkCache.get(agent.code_name) ?? "openclaw") === "claude-code") {
1677
+ try {
1678
+ const projectDir = join(homedir(), ".augmented", agent.code_name, "project");
1679
+ mkdirSync(projectDir, { recursive: true });
1680
+ const channelsPath = join(projectDir, ".mcp-channels.json");
1681
+ let channelsMcp = { mcpServers: {} };
1682
+ try {
1683
+ channelsMcp = JSON.parse(readFileSync(channelsPath, "utf-8"));
1684
+ if (!channelsMcp.mcpServers) channelsMcp.mcpServers = {};
1685
+ } catch {
1686
+ }
1687
+ const localDirectChatChannel = join(homedir(), ".augmented", "_mcp", "direct-chat-channel.js");
1688
+ if (existsSync(localDirectChatChannel) && !channelsMcp.mcpServers["direct-chat"]) {
1689
+ channelsMcp.mcpServers["direct-chat"] = {
1690
+ command: "node",
1691
+ args: [localDirectChatChannel],
1692
+ env: {
1693
+ AGT_HOST: requireHost(),
1694
+ AGT_API_KEY: getApiKey() ?? "",
1695
+ AGT_AGENT_ID: agent.agent_id
1696
+ }
1697
+ };
1698
+ writeFileSync(channelsPath, JSON.stringify(channelsMcp, null, 2));
1699
+ log(`Channel credentials written for '${agent.code_name}/direct-chat'`);
1700
+ }
1701
+ } catch (err) {
1702
+ log(`Failed to provision direct-chat channel for '${agent.code_name}': ${err.message}`);
1703
+ }
1704
+ }
1530
1705
  let lastSecretsProvisionAt = state.agents.find((a) => a.agentId === agent.agent_id)?.lastSecretsProvisionAt ?? null;
1531
1706
  let secretsHash = knownSecretsHashes.get(agent.agent_id) ?? null;
1532
1707
  try {
@@ -1604,7 +1779,7 @@ async function processAgent(agent, agentStates) {
1604
1779
  const expectedServerIds = /* @__PURE__ */ new Set();
1605
1780
  for (const tk of toolkitData.toolkits) {
1606
1781
  if (tk.agent_id !== agent.agent_id) continue;
1607
- const serverId = tk.toolkit_id.replace(/\//g, "-");
1782
+ const serverId = tk.toolkit_id.replace(/[^a-z0-9]/gi, "_").toLowerCase();
1608
1783
  expectedServerIds.add(serverId);
1609
1784
  const mcpUrl = tk.mcp_url;
1610
1785
  const mcpHeaders = tk.mcp_headers;
@@ -1621,7 +1796,19 @@ async function processAgent(agent, agentStates) {
1621
1796
  const mcpPath = join2(homedir2(), ".augmented", "agents", agent.code_name, "provision", ".mcp.json");
1622
1797
  const mcpConfig = JSON.parse(readFileSync2(mcpPath, "utf-8"));
1623
1798
  if (mcpConfig.mcpServers) {
1624
- const managedPrefixes = ["composio-", "one-", "nango-", "paragon-"];
1799
+ const managedPrefixes = [
1800
+ "composio_",
1801
+ "one_",
1802
+ "pipedream_",
1803
+ "nango_",
1804
+ "paragon_",
1805
+ // Legacy hyphenated format
1806
+ "composio-",
1807
+ "one-",
1808
+ "pipedream-",
1809
+ "nango-",
1810
+ "paragon-"
1811
+ ];
1625
1812
  for (const key of Object.keys(mcpConfig.mcpServers)) {
1626
1813
  if (managedPrefixes.some((p) => key.startsWith(p)) && !expectedServerIds.has(key)) {
1627
1814
  frameworkAdapter.removeMcpServer(agent.code_name, key);
@@ -1657,93 +1844,6 @@ async function processAgent(agent, agentStates) {
1657
1844
  }, delay);
1658
1845
  }
1659
1846
  }
1660
- const resolvedDefIds = new Set(integrations.map((i) => i.definition_id));
1661
- for (const capId of resolvedDefIds) {
1662
- try {
1663
- const capData = await api.post("/host/capability-skill", { definition_id: capId });
1664
- const allSatisfied = capData.requiredIntegrations.every((reqId) => resolvedDefIds.has(reqId));
1665
- if (!allSatisfied) {
1666
- const missing = capData.requiredIntegrations.filter((reqId) => !resolvedDefIds.has(reqId));
1667
- log(`Capability '${capId}' skipped \u2014 missing integration(s): ${missing.join(", ")}`);
1668
- continue;
1669
- }
1670
- const skillContent = JSON.stringify(capData.skills.map((s) => s.files.map((f) => `${f.relativePath}:${f.content}`)));
1671
- const skillHash = createHash("sha256").update(skillContent).digest("hex").slice(0, 16);
1672
- const skillKey = `${agent.agent_id}:${capId}`;
1673
- const prevSkillHash = knownSkillHashes.get(skillKey);
1674
- if (skillHash !== prevSkillHash) {
1675
- const installedSkillNames = [];
1676
- if (frameworkAdapter.installSkillFiles && capData.skills) {
1677
- for (const skill of capData.skills) {
1678
- if (skill.files.length > 0) {
1679
- frameworkAdapter.installSkillFiles(agent.code_name, skill.id, skill.files);
1680
- installedSkillNames.push(skill.name || skill.id);
1681
- log(`Installed skill '${skill.id}' for '${agent.code_name}' (${skill.files.length} file(s))`);
1682
- }
1683
- }
1684
- }
1685
- knownSkillHashes.set(skillKey, skillHash);
1686
- const agentFw2 = agentFrameworkCache.get(agent.code_name) ?? "openclaw";
1687
- if (agentFw2 === "claude-code" && installedSkillNames.length > 0 && isSessionHealthy(agent.code_name)) {
1688
- const names = installedSkillNames.join(", ");
1689
- injectMessage(
1690
- agent.code_name,
1691
- "system",
1692
- `New skills installed: ${names}. These are available immediately \u2014 Claude Code loads skills on demand from .claude/skills/.`,
1693
- { task_name: "skill-update" },
1694
- log
1695
- ).catch(() => {
1696
- });
1697
- log(`[hot-reload] Notified '${agent.code_name}' about new skills: ${names}`);
1698
- }
1699
- }
1700
- const ALLOWED_CLI_PACKAGES = /* @__PURE__ */ new Set([
1701
- "xero-cli",
1702
- "@openapitools/openapi-generator-cli",
1703
- "gh",
1704
- "@schpet/linear-cli",
1705
- "@googleworkspace/cli",
1706
- "@tobilu/qmd"
1707
- ]);
1708
- if (intHash !== prevIntHash) {
1709
- const { execFileSync } = await import("child_process");
1710
- for (const tool of capData.cliTools) {
1711
- if (!ALLOWED_CLI_PACKAGES.has(tool.package)) {
1712
- log(`Skipping CLI tool '${tool.package}' for '${agent.code_name}' \u2014 not on the allowed packages list`);
1713
- continue;
1714
- }
1715
- try {
1716
- execFileSync("which", [tool.binary], { stdio: "ignore" });
1717
- } catch {
1718
- log(`Installing CLI tool '${tool.package}' for '${agent.code_name}'...`);
1719
- try {
1720
- execFileSync("npm", ["install", "-g", tool.package], { stdio: "ignore", timeout: 6e4 });
1721
- log(`CLI tool '${tool.binary}' installed successfully`);
1722
- } catch (installErr) {
1723
- log(`Failed to install CLI tool '${tool.package}': ${installErr.message}`);
1724
- }
1725
- }
1726
- if (tool.binary === "qmd") {
1727
- try {
1728
- const agentDir2 = join(process.env["HOME"] ?? "/tmp", ".augmented", agent.code_name);
1729
- execFileSync("qmd", ["collection", "add", agent.code_name, "project"], {
1730
- stdio: "ignore",
1731
- timeout: 3e4,
1732
- cwd: agentDir2
1733
- });
1734
- log(`QMD collection '${agent.code_name}' configured`);
1735
- } catch {
1736
- }
1737
- }
1738
- }
1739
- }
1740
- } catch (skillErr) {
1741
- const msg = skillErr.message ?? "";
1742
- if (!msg.includes("404") && !msg.includes("Unknown capability")) {
1743
- log(`Capability fetch failed for '${capId}': ${msg}`);
1744
- }
1745
- }
1746
- }
1747
1847
  }
1748
1848
  } catch (err) {
1749
1849
  log(`Integration provisioning failed for '${agent.code_name}': ${err.message}`);
@@ -1808,6 +1908,107 @@ async function processAgent(agent, agentStates) {
1808
1908
  log(`Kanban skill install failed for '${agent.code_name}': ${err.message}`);
1809
1909
  }
1810
1910
  }
1911
+ if (frameworkAdapter.installSkillFiles) {
1912
+ const currentPluginSkillIds = /* @__PURE__ */ new Set();
1913
+ const installedPluginSkills = [];
1914
+ const { createHash: createHash2 } = await import("crypto");
1915
+ const contextBySlug = /* @__PURE__ */ new Map();
1916
+ for (const ctx of refreshData.plugin_contexts ?? []) {
1917
+ contextBySlug.set(ctx.plugin_slug, { values: ctx.values ?? {}, overrides: (ctx.overrides ?? "").trim() });
1918
+ }
1919
+ for (const ps of refreshData.plugin_skills ?? []) {
1920
+ try {
1921
+ const skillId = `plugin-${ps.plugin_slug}-${ps.skill_id}`.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1922
+ currentPluginSkillIds.add(skillId);
1923
+ const ctx = contextBySlug.get(ps.plugin_slug);
1924
+ const renderedContent = renderPluginSkillContent(
1925
+ ps.content,
1926
+ ctx?.values ?? {},
1927
+ ctx?.overrides ?? "",
1928
+ (warning) => log(`[plugin-context] ${ps.plugin_slug}/${ps.skill_id}: ${warning}`)
1929
+ );
1930
+ const contentHash = createHash2("sha256").update(renderedContent).digest("hex").slice(0, 12);
1931
+ const hashKey = `plugin-skill:${agent.code_name}:${skillId}`;
1932
+ if (knownSkillHashes.get(hashKey) === contentHash) continue;
1933
+ frameworkAdapter.installSkillFiles(agent.code_name, skillId, [
1934
+ { relativePath: "SKILL.md", content: renderedContent }
1935
+ ]);
1936
+ knownSkillHashes.set(hashKey, contentHash);
1937
+ installedPluginSkills.push(ps.skill_name);
1938
+ log(`Installed plugin skill '${skillId}' for '${agent.code_name}'`);
1939
+ } catch (err) {
1940
+ log(`Plugin skill install failed for '${agent.code_name}' / '${ps.skill_id}': ${err.message}`);
1941
+ }
1942
+ }
1943
+ try {
1944
+ const agentSkillsDir = join(config.configDir, agent.code_name, "project", ".claude", "skills");
1945
+ if (existsSync(agentSkillsDir)) {
1946
+ const { readdirSync: readdirSync2, rmSync: rmSync2 } = await import("fs");
1947
+ for (const entry of readdirSync2(agentSkillsDir)) {
1948
+ if (entry.startsWith("plugin-") && !currentPluginSkillIds.has(entry)) {
1949
+ const orphanPath = join(agentSkillsDir, entry);
1950
+ rmSync2(orphanPath, { recursive: true, force: true });
1951
+ log(`Removed orphaned plugin skill '${entry}' for '${agent.code_name}'`);
1952
+ const provisionSkillPath = join(config.configDir, agent.code_name, "skills", entry);
1953
+ if (existsSync(provisionSkillPath)) {
1954
+ rmSync2(provisionSkillPath, { recursive: true, force: true });
1955
+ }
1956
+ }
1957
+ }
1958
+ }
1959
+ } catch (err) {
1960
+ log(`Plugin skill cleanup failed for '${agent.code_name}': ${err.message}`);
1961
+ }
1962
+ try {
1963
+ const agentFwForIndex = agentFrameworkCache.get(agent.code_name) ?? "openclaw";
1964
+ if (agentFwForIndex === "claude-code") {
1965
+ await refreshSkillsIndexInClaudeMd(config.configDir, agent.code_name, log);
1966
+ }
1967
+ } catch (err) {
1968
+ log(`Skills index refresh failed for '${agent.code_name}': ${err.message}`);
1969
+ }
1970
+ if (frameworkAdapter.executePluginHook && refreshData.plugin_install_hooks?.length) {
1971
+ for (const hook of refreshData.plugin_install_hooks) {
1972
+ try {
1973
+ const scriptHash = createHash2("sha256").update(hook.script).digest("hex").slice(0, 12);
1974
+ const hookKey = `${agent.agent_id}:${frameworkAdapter.id}:plugin-hook:${hook.plugin_slug}:on_install`;
1975
+ if (knownSkillHashes.get(hookKey) === scriptHash) continue;
1976
+ const result = await frameworkAdapter.executePluginHook({
1977
+ codeName: agent.code_name,
1978
+ pluginSlug: hook.plugin_slug,
1979
+ hookName: "on_install",
1980
+ script: hook.script
1981
+ });
1982
+ if (result.exitCode === 0) {
1983
+ knownSkillHashes.set(hookKey, scriptHash);
1984
+ log(`Plugin hook on_install '${hook.plugin_slug}' succeeded for '${agent.code_name}' (${result.durationMs}ms)`);
1985
+ } else if (result.timedOut) {
1986
+ log(`Plugin hook on_install '${hook.plugin_slug}' TIMED OUT for '${agent.code_name}' after ${result.durationMs}ms`);
1987
+ } else {
1988
+ const stderrHash = createHash2("sha256").update(result.stderr).digest("hex").slice(0, 12);
1989
+ log(
1990
+ `Plugin hook on_install '${hook.plugin_slug}' exited ${result.exitCode} for '${agent.code_name}' [stderr_hash=${stderrHash} stderr_len=${result.stderr.length}]`
1991
+ );
1992
+ }
1993
+ } catch (err) {
1994
+ log(`Plugin hook on_install failed for '${agent.code_name}' / '${hook.plugin_slug}': ${err.message}`);
1995
+ }
1996
+ }
1997
+ }
1998
+ const agentFw2 = agentFrameworkCache.get(agent.code_name) ?? "openclaw";
1999
+ if (agentFw2 === "claude-code" && installedPluginSkills.length > 0 && isSessionHealthy(agent.code_name)) {
2000
+ const names = installedPluginSkills.join(", ");
2001
+ injectMessage(
2002
+ agent.code_name,
2003
+ "system",
2004
+ `New plugin skills installed: ${names}. These are available immediately \u2014 Claude Code loads skills on demand from .claude/skills/.`,
2005
+ { task_name: "plugin-skill-update" },
2006
+ log
2007
+ ).catch(() => {
2008
+ });
2009
+ log(`[hot-reload] Notified '${agent.code_name}' about new plugin skills: ${names}`);
2010
+ }
2011
+ }
1811
2012
  }
1812
2013
  let boardItems = [];
1813
2014
  const hasBoardTemplates = tasks.some((t) => BOARD_INJECT_TEMPLATES.has(t.template_id));
@@ -2408,6 +2609,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
2408
2609
  devChannels.push("server:slack");
2409
2610
  }
2410
2611
  }
2612
+ devChannels.push("server:direct-chat");
2411
2613
  if (!agentRuntimeAuthenticated) {
2412
2614
  const { execFileSync } = await import("child_process");
2413
2615
  agentRuntimeAuthenticated = checkClaudeAuth(execFileSync);
@@ -2425,6 +2627,11 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
2425
2627
  } catch (err) {
2426
2628
  log(`[persistent-session] Failed to provision Stop hook for '${codeName}': ${err.message}`);
2427
2629
  }
2630
+ try {
2631
+ provisionIsolationHook(codeName);
2632
+ } catch (err) {
2633
+ log(`[persistent-session] Failed to provision isolation hook for '${codeName}': ${err.message}`);
2634
+ }
2428
2635
  startPersistentSession({
2429
2636
  codeName,
2430
2637
  agentId: agent.agent_id,
@@ -2514,6 +2721,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
2514
2721
  var realtimeStarted = false;
2515
2722
  var realtimeDriftStarted = false;
2516
2723
  var realtimeKanbanStarted = false;
2724
+ var realtimePluginContextStarted = false;
2517
2725
  var realtimeAssignStarted = false;
2518
2726
  var realtimeConfigStarted = false;
2519
2727
  var realtimeSubscribedAgentIds = /* @__PURE__ */ new Set();
@@ -2532,6 +2740,7 @@ function ensureRealtimeStarted(agentStates) {
2532
2740
  realtimeAssignStarted = false;
2533
2741
  realtimeConfigStarted = false;
2534
2742
  realtimeKanbanStarted = false;
2743
+ realtimePluginContextStarted = false;
2535
2744
  }
2536
2745
  const activeAgentIds = agentStates.filter((a) => a.status === "active").map((a) => a.agentId);
2537
2746
  if (activeAgentIds.length === 0) return;
@@ -2698,6 +2907,51 @@ function ensureRealtimeKanbanStarted(agentStates) {
2698
2907
  log(`[realtime] Kanban subscription failed: ${err.message}`);
2699
2908
  });
2700
2909
  }
2910
+ function ensureRealtimePluginContextStarted(agentStates) {
2911
+ if (realtimePluginContextStarted) return;
2912
+ const activeAgentIds = agentStates.filter((a) => a.status === "active").map((a) => a.agentId);
2913
+ if (activeAgentIds.length === 0) return;
2914
+ const apiKey = process.env["AGT_API_KEY"];
2915
+ if (!apiKey) return;
2916
+ void exchangeApiKey(apiKey).then((exchange) => {
2917
+ if (!exchange.supabaseUrl || !exchange.supabaseAnonKey) return;
2918
+ startRealtimePluginContext({
2919
+ supabaseUrl: exchange.supabaseUrl,
2920
+ supabaseAnonKey: exchange.supabaseAnonKey,
2921
+ token: exchange.token,
2922
+ agentIds: activeAgentIds,
2923
+ onContextChange: (payload) => {
2924
+ const agent = agentStates.find((a) => a.agentId === payload.agent_id);
2925
+ if (agent) {
2926
+ for (const key of knownSkillHashes.keys()) {
2927
+ if (key.startsWith(`plugin-skill:${agent.codeName}:`)) {
2928
+ knownSkillHashes.delete(key);
2929
+ }
2930
+ }
2931
+ }
2932
+ triggerEarlyPoll(`plugin context changed for agent ${payload.agent_id}`);
2933
+ },
2934
+ log
2935
+ });
2936
+ realtimePluginContextStarted = true;
2937
+ log(`[realtime] Plugin context subscription started for ${activeAgentIds.length} agent(s)`);
2938
+ }).catch((err) => {
2939
+ log(`[realtime] Plugin context subscription failed: ${err.message}`);
2940
+ });
2941
+ }
2942
+ function triggerEarlyPoll(reason) {
2943
+ if (!running) return;
2944
+ if (pollTimer) {
2945
+ clearTimeout(pollTimer);
2946
+ pollTimer = null;
2947
+ }
2948
+ log(`[realtime] Triggering early poll: ${reason}`);
2949
+ pollTimer = setTimeout(() => {
2950
+ void pollCycle().then(() => {
2951
+ scheduleNext();
2952
+ });
2953
+ }, 0);
2954
+ }
2701
2955
  var directChatInFlight = /* @__PURE__ */ new Set();
2702
2956
  async function pollDirectChatMessages(agentStates) {
2703
2957
  for (const agent of agentStates) {
@@ -2801,7 +3055,7 @@ async function processDirectChatMessage(agent, msg) {
2801
3055
  } catch (err) {
2802
3056
  const errMsg = err instanceof Error ? err.message : String(err);
2803
3057
  const errorId = createHash("sha256").update(errMsg).digest("hex").slice(0, 12);
2804
- log(`[direct-chat] Failed to process message for '${agent.codeName}': error_id=${errorId}`);
3058
+ log(`[direct-chat] Failed to process message for '${agent.codeName}': error_id=${errorId} error=${errMsg.slice(0, 500)}`);
2805
3059
  try {
2806
3060
  await api.post("/host/direct-chat/reply", {
2807
3061
  agent_id: agent.agentId,
@@ -3445,8 +3699,9 @@ function generateArtifacts(agent, refreshData, adapter) {
3445
3699
  const teamTimezoneRaw = refreshData.team?.timezone;
3446
3700
  const teamTimezone = typeof teamTimezoneRaw === "string" && teamTimezoneRaw.trim().length > 0 ? teamTimezoneRaw.trim() : void 0;
3447
3701
  const agentTimezone = (taskTimezones.length === 1 ? taskTimezones[0] : void 0) ?? teamTimezone ?? "UTC";
3702
+ const agentPersonalitySeed = refreshData.agent.personality_seed;
3448
3703
  const orgDefaults = refreshData.model_defaults;
3449
- const personalitySeed = orgDefaults?.org?.settings?.personality_seed;
3704
+ const personalitySeed = agentPersonalitySeed || orgDefaults?.org?.settings?.personality_seed;
3450
3705
  let reportsTo;
3451
3706
  const agentData = refreshData.agent;
3452
3707
  const reportsToId = agentData.reports_to;
@@ -3573,7 +3828,8 @@ function startPolling() {
3573
3828
  void startCaffeinate();
3574
3829
  log(`Starting poll loop (interval=${config.intervalMs}ms, configDir=${config.configDir})`);
3575
3830
  checkAndUpdateCli().catch((err) => log(`[self-update] Check failed: ${err.message}`));
3576
- void migrateToProfiles().then(() => {
3831
+ void killAllAgtTmuxSessions().catch(() => {
3832
+ }).then(() => migrateToProfiles()).then(() => {
3577
3833
  startGatewayPool();
3578
3834
  return pollCycle();
3579
3835
  }).then(() => {
@@ -3617,18 +3873,19 @@ async function stopPolling() {
3617
3873
  pollTimer = null;
3618
3874
  }
3619
3875
  const shutdownTimer = setTimeout(() => {
3620
- log("Shutdown timeout exceeded (5s), forcing exit");
3876
+ log("Shutdown timeout exceeded (15s), forcing exit");
3621
3877
  process.exit(1);
3622
- }, 5e3);
3878
+ }, 15e3);
3623
3879
  shutdownTimer.unref();
3624
3880
  stopCaffeinate();
3625
3881
  stopRealtimeChat();
3626
3882
  stopGatewayPool();
3883
+ log("Killing tmux sessions...");
3884
+ await killAllAgtTmuxSessions();
3627
3885
  log("Stopping persistent sessions...");
3628
3886
  await stopAllSessionsAndWait(log, { timeoutMs: 4e3 });
3629
3887
  log("Stopping gateway processes...");
3630
3888
  await stopAllGateways();
3631
- await killAllAgtTmuxSessions();
3632
3889
  clearTimeout(shutdownTimer);
3633
3890
  }
3634
3891
  function startManager(opts) {
@@ -3656,7 +3913,7 @@ function deployMcpAssets() {
3656
3913
  log("[manager] MCP assets not found in CLI package \u2014 skipping deployment");
3657
3914
  return;
3658
3915
  }
3659
- for (const file of ["index.js", "slack-channel.js"]) {
3916
+ for (const file of ["index.js", "slack-channel.js", "direct-chat-channel.js"]) {
3660
3917
  const src = join(mcpSourceDir, file);
3661
3918
  const dst = join(targetDir, file);
3662
3919
  if (!existsSync(src)) continue;
@@ -3697,8 +3954,14 @@ function deployMcpAssets() {
3697
3954
  async function stopManager() {
3698
3955
  await stopPolling();
3699
3956
  }
3957
+ var shuttingDown = false;
3700
3958
  for (const sig of ["SIGTERM", "SIGINT"]) {
3701
3959
  process.on(sig, () => {
3960
+ if (shuttingDown) {
3961
+ log(`Received ${sig} again during shutdown \u2014 ignoring (cleanup in progress)`);
3962
+ return;
3963
+ }
3964
+ shuttingDown = true;
3702
3965
  log(`Received ${sig}, shutting down`);
3703
3966
  void stopPolling().then(() => {
3704
3967
  process.exit(0);