@markus-global/cli 0.4.21 → 0.4.23

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.
Files changed (77) hide show
  1. package/dist/commands/doctor.d.ts.map +1 -1
  2. package/dist/commands/doctor.js +8 -0
  3. package/dist/commands/doctor.js.map +1 -1
  4. package/dist/commands/start.d.ts.map +1 -1
  5. package/dist/commands/start.js +16 -0
  6. package/dist/commands/start.js.map +1 -1
  7. package/dist/markus.mjs +1157 -299
  8. package/dist/web-ui/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  9. package/dist/web-ui/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  10. package/dist/web-ui/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  11. package/dist/web-ui/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  12. package/dist/web-ui/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  13. package/dist/web-ui/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  14. package/dist/web-ui/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  15. package/dist/web-ui/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  16. package/dist/web-ui/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  17. package/dist/web-ui/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  18. package/dist/web-ui/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  19. package/dist/web-ui/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  20. package/dist/web-ui/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  21. package/dist/web-ui/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  22. package/dist/web-ui/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  23. package/dist/web-ui/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  24. package/dist/web-ui/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  25. package/dist/web-ui/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  26. package/dist/web-ui/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  27. package/dist/web-ui/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  28. package/dist/web-ui/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  29. package/dist/web-ui/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  30. package/dist/web-ui/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  31. package/dist/web-ui/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  32. package/dist/web-ui/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  33. package/dist/web-ui/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  34. package/dist/web-ui/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  35. package/dist/web-ui/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  36. package/dist/web-ui/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  37. package/dist/web-ui/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  38. package/dist/web-ui/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  39. package/dist/web-ui/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  40. package/dist/web-ui/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  41. package/dist/web-ui/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  42. package/dist/web-ui/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  43. package/dist/web-ui/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  44. package/dist/web-ui/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  45. package/dist/web-ui/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  46. package/dist/web-ui/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  47. package/dist/web-ui/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  48. package/dist/web-ui/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  49. package/dist/web-ui/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  50. package/dist/web-ui/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  51. package/dist/web-ui/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  52. package/dist/web-ui/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  53. package/dist/web-ui/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  54. package/dist/web-ui/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  55. package/dist/web-ui/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  56. package/dist/web-ui/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  57. package/dist/web-ui/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  58. package/dist/web-ui/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  59. package/dist/web-ui/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  60. package/dist/web-ui/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  61. package/dist/web-ui/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  62. package/dist/web-ui/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  63. package/dist/web-ui/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  64. package/dist/web-ui/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  65. package/dist/web-ui/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  66. package/dist/web-ui/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  67. package/dist/web-ui/assets/index-FxtMolbG.js +343 -0
  68. package/dist/web-ui/assets/index-jBDtl4o2.css +1 -0
  69. package/dist/web-ui/index.html +2 -2
  70. package/package.json +1 -1
  71. package/templates/roles/org-manager/HEARTBEAT.md +8 -0
  72. package/templates/roles/secretary/HEARTBEAT.md +8 -0
  73. package/templates/skills/agent-building/SKILL.md +19 -10
  74. package/templates/skills/self-evolution/SKILL.md +43 -7
  75. package/templates/skills/team-building/SKILL.md +26 -10
  76. package/dist/web-ui/assets/index-BWjiUsem.js +0 -66
  77. package/dist/web-ui/assets/index-BgcwiZ1h.css +0 -1
package/dist/markus.mjs CHANGED
@@ -3566,7 +3566,7 @@ function buildManifest(type, raw) {
3566
3566
  if (type === "agent") {
3567
3567
  const agentRaw = raw.agent ?? raw;
3568
3568
  base.agent = {
3569
- roleName: agentRaw.roleName ?? raw.roleName ?? "developer",
3569
+ roleName: agentRaw.roleName || raw.roleName || void 0,
3570
3570
  agentRole: agentRaw.agentRole ?? raw.agentRole ?? "worker",
3571
3571
  llmProvider: agentRaw.llmProvider || raw.llmProvider || void 0,
3572
3572
  llmModel: agentRaw.llmModel || raw.llmModel || void 0,
@@ -3585,7 +3585,7 @@ function buildManifest(type, raw) {
3585
3585
  members: rawMembers.map((m) => ({
3586
3586
  name: m.name ?? "Agent",
3587
3587
  role: m.role ?? "worker",
3588
- roleName: m.roleName ?? "developer",
3588
+ roleName: m.roleName || void 0,
3589
3589
  count: m.count ?? 1,
3590
3590
  skills: toArr(m.skills).length > 0 ? toArr(m.skills) : void 0
3591
3591
  }))
@@ -3652,8 +3652,8 @@ function validateManifest(m) {
3652
3652
  errors.push("version must be semver (e.g. 1.0.0)");
3653
3653
  if (o.type === "agent" && o.agent) {
3654
3654
  const a = o.agent;
3655
- if (!a.roleName || typeof a.roleName !== "string")
3656
- errors.push("agent.roleName is required");
3655
+ if (a.roleName !== void 0 && typeof a.roleName !== "string")
3656
+ errors.push("agent.roleName must be a string if provided");
3657
3657
  }
3658
3658
  if (o.type === "team" && o.team) {
3659
3659
  const t = o.team;
@@ -3662,10 +3662,12 @@ function validateManifest(m) {
3662
3662
  }
3663
3663
  return errors;
3664
3664
  }
3665
- function kebab(s) {
3665
+ function kebab(s, fallback) {
3666
3666
  const result = s.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "");
3667
3667
  if (result)
3668
3668
  return result;
3669
+ if (fallback)
3670
+ return fallback;
3669
3671
  let hash = 0;
3670
3672
  for (let i = 0; i < s.length; i++)
3671
3673
  hash = (hash << 5) - hash + s.charCodeAt(i) | 0;
@@ -4409,6 +4411,7 @@ __export(dist_exports, {
4409
4411
  getTextContent: () => getTextContent,
4410
4412
  isPlaceholder: () => isPlaceholder,
4411
4413
  isValidTaskTransition: () => isValidTaskTransition,
4414
+ kebab: () => kebab,
4412
4415
  loadConfig: () => loadConfig,
4413
4416
  manifestFilename: () => manifestFilename,
4414
4417
  msgId: () => msgId,
@@ -41152,13 +41155,41 @@ var init_web_fetch = __esm({
41152
41155
  });
41153
41156
 
41154
41157
  // ../core/dist/tools/web-search.js
41158
+ async function resolveProxyDispatcher() {
41159
+ if (_dispatcher !== false)
41160
+ return _dispatcher || void 0;
41161
+ const proxyUrl = process.env["HTTPS_PROXY"] || process.env["HTTP_PROXY"] || process.env["https_proxy"] || process.env["http_proxy"];
41162
+ if (!proxyUrl) {
41163
+ _dispatcher = void 0;
41164
+ return void 0;
41165
+ }
41166
+ try {
41167
+ const load = new Function("id", "return import(id)");
41168
+ const { ProxyAgent } = await load("undici");
41169
+ _dispatcher = new ProxyAgent(proxyUrl);
41170
+ } catch {
41171
+ _dispatcher = void 0;
41172
+ }
41173
+ return _dispatcher || void 0;
41174
+ }
41175
+ async function proxyFetch(url, init) {
41176
+ const dispatcher = await resolveProxyDispatcher();
41177
+ const signal = init?.signal ?? AbortSignal.timeout(SEARCH_TIMEOUT_MS);
41178
+ const opts = { ...init, signal };
41179
+ if (dispatcher)
41180
+ opts["dispatcher"] = dispatcher;
41181
+ return fetch(url, opts);
41182
+ }
41183
+ function hasProxy() {
41184
+ return !!(process.env["HTTPS_PROXY"] || process.env["HTTP_PROXY"] || process.env["https_proxy"] || process.env["http_proxy"]);
41185
+ }
41155
41186
  async function searchSerper(query2, maxResults) {
41156
41187
  const apiKey = process.env["SERPER_API_KEY"];
41157
41188
  if (!apiKey)
41158
41189
  throw new Error("SERPER_API_KEY not configured");
41159
41190
  let res;
41160
41191
  try {
41161
- res = await fetch("https://google.serper.dev/search", {
41192
+ res = await proxyFetch("https://google.serper.dev/search", {
41162
41193
  method: "POST",
41163
41194
  headers: {
41164
41195
  "X-API-KEY": apiKey,
@@ -41186,7 +41217,7 @@ async function searchBrave(query2, maxResults) {
41186
41217
  const params = new URLSearchParams({ q: query2, count: String(maxResults) });
41187
41218
  let res;
41188
41219
  try {
41189
- res = await fetch(`https://api.search.brave.com/res/v1/web/search?${params}`, {
41220
+ res = await proxyFetch(`https://api.search.brave.com/res/v1/web/search?${params}`, {
41190
41221
  headers: {
41191
41222
  "Accept": "application/json",
41192
41223
  "Accept-Encoding": "gzip",
@@ -41206,23 +41237,28 @@ async function searchBrave(query2, maxResults) {
41206
41237
  date: r.page_age
41207
41238
  }));
41208
41239
  }
41209
- async function searchDuckDuckGoLite(query2, maxResults) {
41240
+ async function searchDuckDuckGo(query2, maxResults) {
41210
41241
  const encoded = encodeURIComponent(query2);
41211
- let res;
41212
- try {
41213
- res = await fetch(`https://lite.duckduckgo.com/lite/?q=${encoded}`, {
41214
- headers: {
41215
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
41242
+ let lastError;
41243
+ for (const base of DDG_ENDPOINTS) {
41244
+ try {
41245
+ const res = await proxyFetch(`${base}?q=${encoded}`, {
41246
+ headers: { "User-Agent": DDG_UA }
41247
+ });
41248
+ if (!res.ok) {
41249
+ lastError = new Error(`HTTP ${res.status} ${res.statusText}`);
41250
+ continue;
41216
41251
  }
41217
- });
41218
- } catch (err) {
41219
- throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
41220
- }
41221
- if (!res.ok) {
41222
- return searchDuckDuckGoHtml(query2, maxResults);
41252
+ const html = await res.text();
41253
+ const results = base.includes("lite") ? parseDDGLite(html, maxResults) : parseDDGHtml(html, maxResults);
41254
+ if (results.length > 0)
41255
+ return results;
41256
+ lastError = new Error(`Parsed 0 results from ${base}`);
41257
+ } catch (err) {
41258
+ lastError = err instanceof Error ? err : new Error(String(err));
41259
+ }
41223
41260
  }
41224
- const html = await res.text();
41225
- return parseDDGLite(html, maxResults);
41261
+ throw new Error(`Network error: ${lastError?.message ?? "all DuckDuckGo endpoints failed"}`);
41226
41262
  }
41227
41263
  function parseDDGLite(html, max) {
41228
41264
  const results = [];
@@ -41261,21 +41297,7 @@ function parseDDGLite(html, max) {
41261
41297
  }
41262
41298
  return results;
41263
41299
  }
41264
- async function searchDuckDuckGoHtml(query2, maxResults) {
41265
- const encoded = encodeURIComponent(query2);
41266
- let res;
41267
- try {
41268
- res = await fetch(`https://html.duckduckgo.com/html/?q=${encoded}`, {
41269
- headers: {
41270
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
41271
- }
41272
- });
41273
- } catch (err) {
41274
- throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
41275
- }
41276
- if (!res.ok)
41277
- throw new Error(`HTTP ${res.status} ${res.statusText}`);
41278
- const html = await res.text();
41300
+ function parseDDGHtml(html, maxResults) {
41279
41301
  const results = [];
41280
41302
  const linkRegex = /<a[^>]+class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/g;
41281
41303
  const snippetRegex = /<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
@@ -41303,10 +41325,12 @@ async function searchDuckDuckGoHtml(query2, maxResults) {
41303
41325
  function stripHtml(html) {
41304
41326
  return html.replace(/<[^>]*>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;/g, "'").replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
41305
41327
  }
41306
- var WebSearchTool;
41328
+ var SEARCH_TIMEOUT_MS, _dispatcher, WebSearchTool, DDG_UA, DDG_ENDPOINTS;
41307
41329
  var init_web_search = __esm({
41308
41330
  "../core/dist/tools/web-search.js"() {
41309
41331
  "use strict";
41332
+ SEARCH_TIMEOUT_MS = 15e3;
41333
+ _dispatcher = false;
41310
41334
  WebSearchTool = {
41311
41335
  name: "web_search",
41312
41336
  description: "Search the web for real-time information. Returns search result titles, snippets, and URLs. Use this when you need up-to-date information that might not be in your training data.",
@@ -41330,7 +41354,7 @@ var init_web_search = __esm({
41330
41354
  const backends = [
41331
41355
  { name: "Serper", fn: searchSerper },
41332
41356
  { name: "Brave", fn: searchBrave },
41333
- { name: "DuckDuckGo", fn: searchDuckDuckGoLite }
41357
+ { name: "DuckDuckGo", fn: searchDuckDuckGo }
41334
41358
  ];
41335
41359
  const errors = [];
41336
41360
  for (const { name, fn } of backends) {
@@ -41345,13 +41369,27 @@ var init_web_search = __esm({
41345
41369
  errors.push({ backend: name, error: message });
41346
41370
  }
41347
41371
  }
41372
+ const hasNetworkErr = errors.some((e) => e.error.includes("Network error") || e.error.includes("timed out"));
41373
+ const hints = [];
41374
+ if (hasNetworkErr && !hasProxy()) {
41375
+ hints.push("All network requests failed and no HTTP proxy is configured. If you are behind a firewall or in a restricted network, set HTTPS_PROXY (e.g. export HTTPS_PROXY=http://127.0.0.1:7890).");
41376
+ }
41377
+ if (hasNetworkErr && hasProxy()) {
41378
+ hints.push(`HTTP proxy is configured (${process.env["HTTPS_PROXY"] || process.env["HTTP_PROXY"] || process.env["https_proxy"] || process.env["http_proxy"]}), but network requests still failed. Verify the proxy is reachable and allows outbound HTTPS.`);
41379
+ }
41348
41380
  return JSON.stringify({
41349
41381
  status: "error",
41350
41382
  error: "All search backends failed.",
41351
- details: errors
41383
+ details: errors,
41384
+ ...hints.length > 0 ? { hints } : {}
41352
41385
  });
41353
41386
  }
41354
41387
  };
41388
+ DDG_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
41389
+ DDG_ENDPOINTS = [
41390
+ "https://lite.duckduckgo.com/lite/",
41391
+ "https://html.duckduckgo.com/html/"
41392
+ ];
41355
41393
  }
41356
41394
  });
41357
41395
 
@@ -42914,6 +42952,18 @@ ${typeLabel}${item.payload.content}`;
42914
42952
  this.persistence?.updateStatus(item.id, "deferred", { deferredUntil: until });
42915
42953
  return item;
42916
42954
  }
42955
+ /**
42956
+ * Defer an item that has already been dequeued (not in the queue).
42957
+ * Used when processing is interrupted (preempted) and the item should
42958
+ * be resumed later. Only updates persistence — does NOT re-insert
42959
+ * into the in-memory queue (resurfaceDue handles that on the next idle cycle).
42960
+ */
42961
+ deferDequeued(item, until) {
42962
+ item.status = "deferred";
42963
+ item.deferredUntil = until;
42964
+ item.startedAt = void 0;
42965
+ this.persistence?.updateStatus(item.id, "deferred", { deferredUntil: until });
42966
+ }
42917
42967
  /**
42918
42968
  * Mark an item as merged into another item.
42919
42969
  */
@@ -43155,7 +43205,7 @@ function detectAbnormalCompletion(reply, item) {
43155
43205
  const registry = MAILBOX_TYPE_REGISTRY[item.sourceType];
43156
43206
  if (!registry?.invokesLLM)
43157
43207
  return void 0;
43158
- if (reply === "[preempted]")
43208
+ if (reply === "[preempted]" || reply === "[cancelled]")
43159
43209
  return void 0;
43160
43210
  const isUserFacing = item.sourceType === "human_chat" || item.sourceType === "a2a_message";
43161
43211
  if (isUserFacing)
@@ -43192,6 +43242,8 @@ var init_attention = __esm({
43192
43242
  triageToolHandlers;
43193
43243
  triageChatFn;
43194
43244
  lastTriageResult;
43245
+ /** Tracks whether the last yield-point decision was 'cancel' vs 'preempt'. */
43246
+ lastYieldDecision;
43195
43247
  unsubscribeNewItem;
43196
43248
  decisions = [];
43197
43249
  watchdogTimer;
@@ -43388,6 +43440,7 @@ var init_attention = __esm({
43388
43440
  this.interruptSignal = false;
43389
43441
  this.pendingInterruptItem = void 0;
43390
43442
  this.criticalInterruptResolve = void 0;
43443
+ this.lastYieldDecision = void 0;
43391
43444
  this.processingStartedAt = Date.now();
43392
43445
  this.delegate?.onFocusChanged(item);
43393
43446
  let reply = void 0;
@@ -43422,6 +43475,27 @@ var init_attention = __esm({
43422
43475
  this.processingStartedAt = void 0;
43423
43476
  if (timedOut) {
43424
43477
  this.mailbox.requeue(item);
43478
+ } else if (reply === "[cancelled]" || this.lastYieldDecision === "cancel") {
43479
+ log14.info("Item cancelled \u2014 dropping permanently", {
43480
+ agentId: this.agentId,
43481
+ itemId: item.id,
43482
+ type: item.sourceType
43483
+ });
43484
+ this.mailbox.complete(item.id);
43485
+ } else if (reply === "[preempted]" || this.lastYieldDecision === "preempt") {
43486
+ log14.info("Item preempted (paused) \u2014 deferring for later resumption", {
43487
+ agentId: this.agentId,
43488
+ itemId: item.id,
43489
+ type: item.sourceType
43490
+ });
43491
+ this.mailbox.deferDequeued(item);
43492
+ } else if (!this.running) {
43493
+ log14.info("Agent stopped during processing \u2014 requeueing item", {
43494
+ agentId: this.agentId,
43495
+ itemId: item.id,
43496
+ type: item.sourceType
43497
+ });
43498
+ this.mailbox.requeue(item);
43425
43499
  } else {
43426
43500
  const abnormalReason = detectAbnormalCompletion(reply, item);
43427
43501
  const retries = item.retryCount ?? 0;
@@ -43497,7 +43571,8 @@ var init_attention = __esm({
43497
43571
  *
43498
43572
  * Returns the decision type so the caller knows what to do:
43499
43573
  * - 'continue' → keep working on current task
43500
- * - 'preempt' → caller should save state and return; controller handles the switch
43574
+ * - 'preempt' → caller should save state and return '[preempted]'; item is deferred for later resumption
43575
+ * - 'cancel' → caller should abort and return '[cancelled]'; item is permanently dropped
43501
43576
  * - 'merge' → item was absorbed into current work (caller may inject into session)
43502
43577
  */
43503
43578
  async checkYieldPoint() {
@@ -43530,8 +43605,13 @@ var init_attention = __esm({
43530
43605
  return { decision: "defer", item: newItem, reasoning };
43531
43606
  }
43532
43607
  if (decisionType === "preempt") {
43608
+ this.lastYieldDecision = "preempt";
43533
43609
  return { decision: "preempt", item: newItem, reasoning };
43534
43610
  }
43611
+ if (decisionType === "cancel") {
43612
+ this.lastYieldDecision = "cancel";
43613
+ return { decision: "cancel", item: newItem, reasoning };
43614
+ }
43535
43615
  return { decision: "continue", reasoning };
43536
43616
  }
43537
43617
  /**
@@ -43614,7 +43694,7 @@ var init_attention = __esm({
43614
43694
  try {
43615
43695
  const prompt = this.buildJudgePrompt(currentItem, newItem);
43616
43696
  const llmDecision = await this.llmJudge(prompt);
43617
- const validDecisions = ["continue", "preempt", "merge", "defer"];
43697
+ const validDecisions = ["continue", "preempt", "cancel", "merge", "defer"];
43618
43698
  if (validDecisions.includes(llmDecision))
43619
43699
  return llmDecision;
43620
43700
  } catch (err) {
@@ -43629,17 +43709,29 @@ var init_attention = __esm({
43629
43709
  const recentDecs = this.decisions.slice(-5).map((d) => `- ${d.decisionType}: ${d.reasoning}`).join("\n");
43630
43710
  const queuedItems = this.mailbox.getQueuedItems();
43631
43711
  const queueSection = queuedItems.length > 0 ? queuedItems.map((i) => ` - [${i.sourceType}] p${i.priority}: "${i.payload.summary}"`).join("\n") : " (empty)";
43712
+ const currentContent = currentItem.payload.content?.slice(0, 500) ?? "";
43713
+ const newContent = newItem.payload.content?.slice(0, 800) ?? "";
43632
43714
  return [
43633
- "You are deciding how to handle a new incoming item in your mailbox.",
43634
- "CRITICAL RULE: Human messages (human_chat) and human comments ALWAYS take priority over all other work.",
43715
+ "You are deciding whether to INTERRUPT your current work to handle a new incoming message.",
43716
+ "",
43717
+ "RULES (in priority order):",
43718
+ "1. Human messages (human_chat) ALWAYS preempt non-human work \u2014 users are top priority.",
43719
+ '2. If the new message explicitly CANCELS or REVOKES the current work (e.g. "cancel that task",',
43720
+ ` "don't publish this anymore", "delete the draft"), choose cancel \u2014 current work is permanently abandoned.`,
43721
+ '3. If the new message asks to PAUSE or DELAY current work (e.g. "hold off for now",',
43722
+ ' "wait before deploying", "pause that"), choose preempt \u2014 current work is saved and can be resumed later.',
43723
+ "4. If the new message is about the SAME task/topic as current work and adds info, consider merge.",
43724
+ "5. If the new message is lower priority and unrelated, continue current work.",
43635
43725
  "",
43636
43726
  `CURRENT FOCUS: [${currentItem.sourceType}] priority=${currentItem.priority}`,
43637
- ` "${currentItem.payload.summary}"`,
43727
+ ` Summary: "${currentItem.payload.summary}"`,
43638
43728
  ` Task: ${currentItem.payload.taskId ?? "none"}`,
43729
+ currentContent ? ` Content preview: ${currentContent}` : "",
43639
43730
  "",
43640
43731
  `NEW ITEM: [${newItem.sourceType}] priority=${newItem.priority}`,
43641
- ` "${newItem.payload.summary}"`,
43732
+ ` Summary: "${newItem.payload.summary}"`,
43642
43733
  ` Task: ${newItem.payload.taskId ?? "none"}`,
43734
+ newContent ? ` Content: ${newContent}` : "",
43643
43735
  "",
43644
43736
  `FULL QUEUE (${this.mailbox.depth} items):`,
43645
43737
  queueSection,
@@ -43647,7 +43739,14 @@ var init_attention = __esm({
43647
43739
  "RECENT DECISIONS:",
43648
43740
  recentDecs || "(none)",
43649
43741
  "",
43650
- "Choose exactly one: continue | preempt | merge | defer",
43742
+ "DECISIONS:",
43743
+ "- preempt: PAUSE current work (session saved for later resumption) and handle the new item",
43744
+ "- cancel: PERMANENTLY STOP current work (session dropped, will NOT be resumed) and handle the new item",
43745
+ "- merge: Inject new item into current session as additional context (no interruption)",
43746
+ "- defer: Put the new item aside for later (no interruption)",
43747
+ "- continue: Ignore the new item for now, keep working",
43748
+ "",
43749
+ "Choose exactly one: continue | preempt | cancel | merge | defer",
43651
43750
  "Reply with ONLY the decision word, nothing else."
43652
43751
  ].join("\n");
43653
43752
  }
@@ -45193,6 +45292,7 @@ ${notification.stdoutTail}`);
45193
45292
  });
45194
45293
  });
45195
45294
  const roleFilePath = join10(this.dataDir, "role", "ROLE.md");
45295
+ const heartbeatFilePath = join10(this.dataDir, "role", "HEARTBEAT.md");
45196
45296
  this.toolHooks.register({
45197
45297
  name: "role-auto-reload",
45198
45298
  after: async (ctx) => {
@@ -45202,6 +45302,10 @@ ${notification.stdoutTail}`);
45202
45302
  log17.info("Agent modified its own ROLE.md \u2014 reloading role definition");
45203
45303
  this.reloadRole();
45204
45304
  }
45305
+ if (targetPath === heartbeatFilePath || targetPath.endsWith("/role/HEARTBEAT.md")) {
45306
+ log17.info("Agent modified its own HEARTBEAT.md \u2014 reloading heartbeat checklist");
45307
+ this.reloadHeartbeat();
45308
+ }
45205
45309
  }
45206
45310
  }
45207
45311
  });
@@ -45264,6 +45368,7 @@ ${notification.stdoutTail}`);
45264
45368
  log17.info(`Agent started: ${this.config.name}`);
45265
45369
  }
45266
45370
  async stop() {
45371
+ this.cancelActiveStream();
45267
45372
  this.attentionController.stop();
45268
45373
  this.heartbeat.stop();
45269
45374
  this._bgCompletionUnsub?.();
@@ -45465,7 +45570,7 @@ ${notification.stdoutTail}`);
45465
45570
  }
45466
45571
  },
45467
45572
  evaluateInterrupt: async (currentItem, newItem) => {
45468
- return this.attentionController.heuristicDecision(currentItem, newItem);
45573
+ return this.attentionController.evaluateWithLLMFallback(currentItem, newItem);
45469
45574
  },
45470
45575
  getTriageContext: async () => {
45471
45576
  const messages = this.currentSessionId ? this.memory.getRecentMessages(this.currentSessionId, TRIAGE_CONTEXT_MESSAGES_MAX * 2).filter((m) => m.role === "user" || m.role === "assistant").slice(-TRIAGE_CONTEXT_MESSAGES_MAX).map((m) => ({ role: m.role, content: typeof m.content === "string" ? m.content.slice(0, TRIAGE_CONTEXT_MSG_CHARS) : String(m.content).slice(0, TRIAGE_CONTEXT_MSG_CHARS) })) : [];
@@ -45561,7 +45666,7 @@ ${notification.stdoutTail}`);
45561
45666
  case "requirement_update": {
45562
45667
  if (extra.actionRequired) {
45563
45668
  const reqId = item.payload.requirementId ?? "unknown";
45564
- const reply = await this.handleMessage(item.payload.content + markerSuffix, item.metadata?.senderId, senderInfo, buildHandleOpts({ sessionId: `comment_${reqId}_${ts}`, scenario: "comment_response" }));
45669
+ const reply = await this.handleMessage(item.payload.content + markerSuffix, item.metadata?.senderId, senderInfo, buildHandleOpts({ sessionId: `requirement_${reqId}_${ts}`, scenario: "requirement_action" }));
45565
45670
  resolveResponse(reply);
45566
45671
  return reply;
45567
45672
  }
@@ -45710,6 +45815,25 @@ ${notification.stdoutTail}`);
45710
45815
  log17.warn(`Failed to reload role for agent ${this.config.name}`, { error: String(err) });
45711
45816
  }
45712
45817
  }
45818
+ /**
45819
+ * Reload the agent's heartbeat checklist from its HEARTBEAT.md file on disk.
45820
+ * Called when the agent modifies its own HEARTBEAT.md via file_edit/file_write.
45821
+ */
45822
+ reloadHeartbeat() {
45823
+ const heartbeatFile = join10(this.dataDir, "role", "HEARTBEAT.md");
45824
+ if (!existsSync15(heartbeatFile))
45825
+ return;
45826
+ try {
45827
+ const content = readFileSync10(heartbeatFile, "utf-8");
45828
+ this.role = {
45829
+ ...this.role,
45830
+ heartbeatChecklist: content
45831
+ };
45832
+ log17.info(`Heartbeat checklist reloaded from disk for agent ${this.config.name}`);
45833
+ } catch (err) {
45834
+ log17.warn(`Failed to reload heartbeat for agent ${this.config.name}`, { error: String(err) });
45835
+ }
45836
+ }
45713
45837
  /**
45714
45838
  * Start a fresh conversation session, discarding the current in-memory session context.
45715
45839
  * Called when the user explicitly starts a "New Chat".
@@ -45776,6 +45900,7 @@ ${notification.stdoutTail}`);
45776
45900
  if (this.state.status === "offline")
45777
45901
  return;
45778
45902
  this.pauseReason = reason;
45903
+ this.cancelActiveStream();
45779
45904
  this.heartbeat.stop();
45780
45905
  this.attentionController.stop();
45781
45906
  if (this.memoryConsolidationTimer) {
@@ -46606,10 +46731,11 @@ ${report}`);
46606
46731
  if (!this.state.currentActivity) {
46607
46732
  let actType = "chat";
46608
46733
  let actLabel;
46734
+ const peerName = senderInfo?.name || senderId || void 0;
46609
46735
  switch (scenario) {
46610
46736
  case "a2a":
46611
46737
  actType = "a2a";
46612
- actLabel = `Chat from ${senderInfo?.name ?? senderId}`;
46738
+ actLabel = peerName ? `Chat from ${peerName}` : "Agent Message";
46613
46739
  break;
46614
46740
  case "heartbeat":
46615
46741
  actType = "internal";
@@ -46617,14 +46743,14 @@ ${report}`);
46617
46743
  break;
46618
46744
  case "comment_response":
46619
46745
  actType = "chat";
46620
- actLabel = `Comment reply to ${senderInfo?.name ?? senderId ?? "user"}`;
46746
+ actLabel = peerName ? `Comment reply to ${peerName}` : "Comment Reply";
46621
46747
  break;
46622
46748
  case "review":
46623
46749
  actType = "internal";
46624
- actLabel = `Reviewing task from ${senderInfo?.name ?? senderId ?? "worker"}`;
46750
+ actLabel = peerName ? `Reviewing task from ${peerName}` : "Task Review";
46625
46751
  break;
46626
46752
  default:
46627
- actLabel = `Chat with ${senderInfo?.name ?? senderId ?? "user"}`;
46753
+ actLabel = peerName ? `Chat with ${peerName}` : "Human Chat";
46628
46754
  break;
46629
46755
  }
46630
46756
  chatActivityId = this.startActivity(actType, actLabel);
@@ -46867,13 +46993,14 @@ ${block}
46867
46993
  }
46868
46994
  }
46869
46995
  const chatYield = await this.checkAttentionYieldPoint();
46870
- if (chatYield.decision === "preempt" && isPreemptable) {
46871
- log17.info("handleMessage preempted by higher-priority item", {
46996
+ if ((chatYield.decision === "preempt" || chatYield.decision === "cancel") && isPreemptable) {
46997
+ const marker = chatYield.decision === "cancel" ? "[cancelled]" : "[preempted]";
46998
+ log17.info(`handleMessage ${chatYield.decision} by higher-priority item`, {
46872
46999
  agentId: this.id,
46873
47000
  scenario,
46874
47001
  preemptedBy: chatYield.item?.sourceType
46875
47002
  });
46876
- return "[preempted]";
47003
+ return marker;
46877
47004
  }
46878
47005
  if (chatYield.decision === "merge" && chatYield.item) {
46879
47006
  const mergeMsg = `[LIVE UPDATE] ${chatYield.item.payload.summary}
@@ -47054,6 +47181,7 @@ ${chatYield.item.payload.content}`;
47054
47181
  this.checkDailyTokenBudget();
47055
47182
  const llmStart = Date.now();
47056
47183
  let response = await this.withNetworkRetry(() => this.llmRouter.chatStream({ messages, tools: llmTools.length > 0 ? llmTools : void 0, metadata: this.getLLMMetadata(this.currentSessionId), compaction: useCompaction }, wrappedOnEvent, this.getEffectiveProvider(), abortController.signal), "Stream LLM call");
47184
+ streamMarkerDelta.flush();
47057
47185
  const tokensThisCall = response.usage.inputTokens + response.usage.outputTokens;
47058
47186
  this.updateTokensUsed(tokensThisCall);
47059
47187
  this.calibrateTokenCounter(response.usage.inputTokens);
@@ -47204,6 +47332,7 @@ ${chatYield.item.payload.content}`;
47204
47332
  }
47205
47333
  const llmStart2 = Date.now();
47206
47334
  response = await this.withNetworkRetry(() => this.llmRouter.chatStream({ messages: updatedMessages, tools: llmTools.length > 0 ? llmTools : void 0, metadata: this.getLLMMetadata(this.currentSessionId), compaction: useCompaction }, wrappedOnEvent, this.getEffectiveProvider(), abortController.signal), "Stream LLM continuation");
47335
+ streamMarkerDelta.flush();
47207
47336
  const tokens2 = response.usage.inputTokens + response.usage.outputTokens;
47208
47337
  this.updateTokensUsed(tokens2);
47209
47338
  this.calibrateTokenCounter(response.usage.inputTokens);
@@ -47610,14 +47739,16 @@ ${chatYield.item.payload.content}`;
47610
47739
  }
47611
47740
  this.flushPendingInjections(sessionId);
47612
47741
  const yieldResult = await this.checkAttentionYieldPoint();
47613
- if (yieldResult.decision === "preempt") {
47614
- emit("status", "preempted", {
47742
+ if (yieldResult.decision === "preempt" || yieldResult.decision === "cancel") {
47743
+ const statusLabel = yieldResult.decision === "cancel" ? "cancelled" : "preempted";
47744
+ emit("status", statusLabel, {
47615
47745
  reason: yieldResult.reasoning,
47616
47746
  preemptedBy: yieldResult.item?.sourceType
47617
47747
  });
47618
- log17.info("Task preempted by higher-priority mailbox item", {
47748
+ log17.info(`Task ${statusLabel} by higher-priority mailbox item`, {
47619
47749
  taskId: taskId2,
47620
47750
  agentId: this.id,
47751
+ decision: yieldResult.decision,
47621
47752
  preemptedBy: yieldResult.item?.sourceType,
47622
47753
  reasoning: yieldResult.reasoning?.slice(0, 120)
47623
47754
  });
@@ -48627,6 +48758,10 @@ ${todayLog.slice(0, HEARTBEAT_DAILY_LOG_CHARS)}
48627
48758
  '- Check existing skills first: `discover_tools({ mode: "list_skills" })` and `builder_list`.',
48628
48759
  "- To update an existing skill: edit files in `~/.markus/builder-artifacts/skills/{name}/`, bump version, re-install with `builder_install`.",
48629
48760
  "",
48761
+ "**Direct self-evolution** (simplest and most impactful):",
48762
+ "- **Update ROLE.md** \u2014 When you discover a behavioral rule, working style, or guiding principle that should always apply, append it to your ROLE.md via `file_edit`. ROLE.md is loaded into every conversation, so changes take effect immediately. Read first, then append. No need to accumulate 3 insights \u2014 even a single validated lesson can warrant a role update if it is fundamental.",
48763
+ "- **Update HEARTBEAT.md** \u2014 When you realize your patrol routine should include a new recurring check (or remove an obsolete one), modify your HEARTBEAT.md via `file_edit`. This is your personal checklist \u2014 customize it to match your actual responsibilities. Changes take effect at the next heartbeat.",
48764
+ "",
48630
48765
  "**Decision guide \u2014 where does this insight go?**",
48631
48766
  "| Observation type | Action |",
48632
48767
  "|---|---|",
@@ -48634,7 +48769,8 @@ ${todayLog.slice(0, HEARTBEAT_DAILY_LOG_CHARS)}
48634
48769
  '| Tool tip or preference | `memory_save` with tags: `["insight", "tool:<name>"]` |',
48635
48770
  '| Multi-step repeatable workflow | `memory_update_longterm({ section: "procedures", mode: "patch" })` |',
48636
48771
  "| Practice worth sharing with the team | Create skill via **skill-building**, then install with `builder_install` |",
48637
- "| 3+ related insights \u2192 behavioral rule | Update ROLE.md (read first, append, log change) |",
48772
+ "| Behavioral rule or guiding principle | Update ROLE.md (`file_read` \u2192 `file_edit` to append) |",
48773
+ "| New recurring check for your patrol | Update HEARTBEAT.md (`file_read` \u2192 `file_edit`) |",
48638
48774
  "",
48639
48775
  "Quality bar: Only record insights that are **specific**, **actionable**, and **non-obvious**.",
48640
48776
  "Skip if nothing meaningful happened since last heartbeat."
@@ -48662,7 +48798,8 @@ ${todayLog.slice(0, HEARTBEAT_DAILY_LOG_CHARS)}
48662
48798
  "- Tasks with `executionRound > 1` required revision \u2014 your initial approach had issues.",
48663
48799
  "- A high revision rate (>30%) suggests your knowledge is not being applied effectively.",
48664
48800
  "- Check: does your MEMORY.md knowledge actually cover the failure patterns you see?",
48665
- "- If you keep making the same type of mistake, escalate: save as insight \u2192 add to MEMORY.md \u2192 update ROLE.md."
48801
+ "- If you keep making the same type of mistake, escalate: save as insight \u2192 add to MEMORY.md \u2192 update ROLE.md or HEARTBEAT.md.",
48802
+ "- Consider: would a ROLE.md rule or a HEARTBEAT.md check have prevented any recent failures?"
48666
48803
  ].join("\n");
48667
48804
  const prompt = [
48668
48805
  "[HEARTBEAT CHECK-IN]",
@@ -50326,6 +50463,67 @@ function createA2ATools(ctx) {
50326
50463
  return JSON.stringify({ status: "error", error: String(err) });
50327
50464
  }
50328
50465
  }
50466
+ }] : [],
50467
+ ...ctx.getChannelMessages ? [{
50468
+ name: "recall_context",
50469
+ description: [
50470
+ "Recall historical context you may need to respond effectively.",
50471
+ "Use this BEFORE responding when the provided context is insufficient.",
50472
+ "",
50473
+ "Supported scopes:",
50474
+ '\u2022 "channel" \u2014 Read chat messages from a group chat or DM channel. Requires channel_key.',
50475
+ " Use when you joined a discussion late, were @mentioned, or need to understand prior conversation.",
50476
+ "",
50477
+ "For other context types, use these existing tools instead:",
50478
+ "\u2022 task_get \u2014 Full task details including all comments (scope=task context)",
50479
+ "\u2022 requirement_get \u2014 Full requirement details including all comments (scope=requirement context)",
50480
+ "\u2022 recall_activity \u2014 Your own execution history, activity logs, and past work (scope=your activities)",
50481
+ "\u2022 memory_search \u2014 Your saved memories and notes (scope=personal knowledge)"
50482
+ ].join("\n"),
50483
+ inputSchema: {
50484
+ type: "object",
50485
+ properties: {
50486
+ scope: {
50487
+ type: "string",
50488
+ enum: ["channel"],
50489
+ description: 'Type of context to recall. Currently: "channel" for chat/group messages.'
50490
+ },
50491
+ channel_key: {
50492
+ type: "string",
50493
+ description: 'Required when scope="channel". The channel key (e.g., "group:<teamId>" for team chats, "dm:<id1>_<id2>" for DMs).'
50494
+ },
50495
+ limit: { type: "number", description: "Number of items to fetch (default 30, max 50)." },
50496
+ before: { type: "string", description: "ISO timestamp \u2014 fetch items older than this for pagination. Omit for most recent." }
50497
+ },
50498
+ required: ["scope"]
50499
+ },
50500
+ async execute(args) {
50501
+ const scope = args["scope"];
50502
+ if (scope === "channel") {
50503
+ const channelKey = args["channel_key"];
50504
+ if (!channelKey) {
50505
+ return JSON.stringify({ status: "error", error: 'channel_key is required when scope="channel"' });
50506
+ }
50507
+ const limit = Math.min(args["limit"] ?? 30, 50);
50508
+ const before2 = args["before"];
50509
+ try {
50510
+ const result = await ctx.getChannelMessages(channelKey, limit, before2);
50511
+ const formatted = result.messages.map((m) => `[${m.createdAt}] ${m.senderType === "agent" ? `[agent] ${m.senderName}` : `[human] ${m.senderName}`}: ${m.text.slice(0, 500)}`);
50512
+ return JSON.stringify({
50513
+ messages: formatted,
50514
+ count: result.messages.length,
50515
+ hasMore: result.hasMore,
50516
+ oldestTimestamp: result.messages[0]?.createdAt
50517
+ });
50518
+ } catch (err) {
50519
+ return JSON.stringify({ status: "error", error: String(err) });
50520
+ }
50521
+ }
50522
+ return JSON.stringify({
50523
+ status: "error",
50524
+ error: `Unknown scope: "${scope}". Use "channel" for chat messages, or use task_get / requirement_get / recall_activity for other context.`
50525
+ });
50526
+ }
50329
50527
  }] : []
50330
50528
  ];
50331
50529
  }
@@ -52842,7 +53040,7 @@ var init_dist3 = __esm({
52842
53040
 
52843
53041
  // ../core/dist/agent-manager.js
52844
53042
  import { join as join13 } from "node:path";
52845
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync13, existsSync as existsSync18, copyFileSync, rmSync, readdirSync as readdirSync4 } from "node:fs";
53043
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync13, existsSync as existsSync18, copyFileSync, rmSync, readdirSync as readdirSync4, writeFileSync as writeFileSync11 } from "node:fs";
52846
53044
  import { homedir as homedir6 } from "node:os";
52847
53045
  function resolveCurrentTaskId(agentObj, ts, agentId2) {
52848
53046
  const activeTasks = agentObj?.getActiveTasks?.() ?? [];
@@ -53209,24 +53407,50 @@ Priority: ${delegation.priority}`, envelope.from, { name: envelope.from, role: "
53209
53407
  if (!request.name?.trim())
53210
53408
  throw new Error("Agent name is required");
53211
53409
  const id = agentId();
53212
- const role = this.roleLoader.loadRole(request.roleName);
53410
+ const roleName = request.roleName || "custom";
53411
+ const isCustomRole = roleName === "custom";
53412
+ const role = isCustomRole ? {
53413
+ id: generateId("role"),
53414
+ name: request.name,
53415
+ description: "",
53416
+ category: "custom",
53417
+ systemPrompt: `# ${request.name}
53418
+
53419
+ You are ${request.name}.`,
53420
+ defaultSkills: [],
53421
+ heartbeatChecklist: "",
53422
+ defaultPolicies: [],
53423
+ builtIn: false
53424
+ } : this.roleLoader.loadRole(roleName);
53213
53425
  const agentDataDir = join13(this.dataDir, id);
53214
53426
  mkdirSync12(agentDataDir, { recursive: true });
53215
53427
  const agentRoleDir = join13(agentDataDir, "role");
53216
53428
  mkdirSync12(agentRoleDir, { recursive: true });
53217
- const templateDir = this.roleLoader.resolveTemplateDir(request.roleName);
53218
- if (templateDir) {
53219
- for (const file of ["ROLE.md", "HEARTBEAT.md", "POLICIES.md", "CONTEXT.md"]) {
53220
- const src = join13(templateDir, file);
53221
- if (existsSync18(src))
53222
- copyFileSync(src, join13(agentRoleDir, file));
53429
+ if (!isCustomRole && !request.skipTemplateCopy) {
53430
+ const templateDir = this.roleLoader.resolveTemplateDir(roleName);
53431
+ if (templateDir) {
53432
+ for (const file of ["ROLE.md", "HEARTBEAT.md", "POLICIES.md", "CONTEXT.md"]) {
53433
+ const src = join13(templateDir, file);
53434
+ if (existsSync18(src))
53435
+ copyFileSync(src, join13(agentRoleDir, file));
53436
+ }
53223
53437
  }
53224
53438
  }
53439
+ const heartbeatPath = join13(agentRoleDir, "HEARTBEAT.md");
53440
+ if (!existsSync18(heartbeatPath)) {
53441
+ writeFileSync11(heartbeatPath, [
53442
+ "# Heartbeat Checklist",
53443
+ "",
53444
+ "- [ ] Check mailbox for new messages and respond to urgent items",
53445
+ "- [ ] Review assigned tasks \u2014 update progress, unblock if possible",
53446
+ "- [ ] Check team announcements for new information",
53447
+ "- [ ] Scan recent channel messages for anything requiring attention"
53448
+ ].join("\n"), "utf-8");
53449
+ }
53225
53450
  const config = {
53226
53451
  id,
53227
53452
  name: request.name,
53228
- // Store the template folder name so agents can be restored on restart
53229
- roleId: request.roleName,
53453
+ roleId: roleName,
53230
53454
  orgId: request.orgId ?? "default",
53231
53455
  teamId: request.teamId,
53232
53456
  agentRole: request.agentRole ?? "worker",
@@ -53388,7 +53612,8 @@ Priority: ${delegation.priority}`, envelope.from, { name: envelope.from, role: "
53388
53612
  ...this.groupChatHandlers ? {
53389
53613
  sendGroupMessage: this.groupChatHandlers.sendGroupMessage,
53390
53614
  createGroupChat: (name, memberIds) => this.groupChatHandlers.createGroupChat(name, id, config.name, memberIds),
53391
- listGroupChats: this.groupChatHandlers.listGroupChats
53615
+ listGroupChats: this.groupChatHandlers.listGroupChats,
53616
+ getChannelMessages: this.groupChatHandlers.getChannelMessages
53392
53617
  } : {}
53393
53618
  };
53394
53619
  for (const tool of createA2ATools(a2aContext))
@@ -53742,6 +53967,20 @@ Known issues: ${knownIssues}` : ""}`
53742
53967
  let role;
53743
53968
  if (existsSync18(join13(agentRoleDir, "ROLE.md"))) {
53744
53969
  role = this.roleLoader.loadRole(agentRoleDir);
53970
+ } else if (row.roleId === "custom") {
53971
+ role = {
53972
+ id: generateId("role"),
53973
+ name: row.name,
53974
+ description: "",
53975
+ category: "custom",
53976
+ systemPrompt: `# ${row.name}
53977
+
53978
+ You are ${row.name}.`,
53979
+ defaultSkills: [],
53980
+ heartbeatChecklist: "",
53981
+ defaultPolicies: [],
53982
+ builtIn: false
53983
+ };
53745
53984
  } else {
53746
53985
  role = (() => {
53747
53986
  try {
@@ -53947,7 +54186,8 @@ Known issues: ${knownIssues}` : ""}`
53947
54186
  ...this.groupChatHandlers ? {
53948
54187
  sendGroupMessage: this.groupChatHandlers.sendGroupMessage,
53949
54188
  createGroupChat: (name, memberIds) => this.groupChatHandlers.createGroupChat(name, id, config.name, memberIds),
53950
- listGroupChats: this.groupChatHandlers.listGroupChats
54189
+ listGroupChats: this.groupChatHandlers.listGroupChats,
54190
+ getChannelMessages: this.groupChatHandlers.getChannelMessages
53951
54191
  } : {}
53952
54192
  };
53953
54193
  for (const tool of createA2ATools(a2aCtx))
@@ -55884,7 +56124,7 @@ var init_ollama = __esm({
55884
56124
  });
55885
56125
 
55886
56126
  // ../core/dist/llm/auth-profiles.js
55887
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync11, mkdirSync as mkdirSync13, existsSync as existsSync19, unlinkSync as unlinkSync2 } from "node:fs";
56127
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync12, mkdirSync as mkdirSync13, existsSync as existsSync19, unlinkSync as unlinkSync2 } from "node:fs";
55888
56128
  import { join as join14 } from "node:path";
55889
56129
  import { homedir as homedir7 } from "node:os";
55890
56130
  var log34, AuthProfileStore;
@@ -55915,7 +56155,7 @@ var init_auth_profiles = __esm({
55915
56155
  }
55916
56156
  }
55917
56157
  write(data) {
55918
- writeFileSync11(this.filePath, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
56158
+ writeFileSync12(this.filePath, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
55919
56159
  }
55920
56160
  /**
55921
56161
  * Acquire a simple filesystem lock for atomic profile updates (token refresh).
@@ -55924,7 +56164,7 @@ var init_auth_profiles = __esm({
55924
56164
  acquireLock() {
55925
56165
  const lockContent = `${process.pid}-${Date.now()}`;
55926
56166
  try {
55927
- writeFileSync11(this.lockPath, lockContent, { flag: "wx" });
56167
+ writeFileSync12(this.lockPath, lockContent, { flag: "wx" });
55928
56168
  return () => {
55929
56169
  try {
55930
56170
  unlinkSync2(this.lockPath);
@@ -55936,7 +56176,7 @@ var init_auth_profiles = __esm({
55936
56176
  const existing = readFileSync14(this.lockPath, "utf-8");
55937
56177
  const ts = Number(existing.split("-").pop());
55938
56178
  if (Date.now() - ts > 3e4) {
55939
- writeFileSync11(this.lockPath, lockContent, { mode: 384 });
56179
+ writeFileSync12(this.lockPath, lockContent, { mode: 384 });
55940
56180
  return () => {
55941
56181
  try {
55942
56182
  unlinkSync2(this.lockPath);
@@ -58049,7 +58289,7 @@ var init_registry = __esm({
58049
58289
  skills = /* @__PURE__ */ new Map();
58050
58290
  aliases = /* @__PURE__ */ new Map();
58051
58291
  static normalize(s) {
58052
- return s.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
58292
+ return kebab(s);
58053
58293
  }
58054
58294
  register(skill) {
58055
58295
  const name = skill.manifest.name;
@@ -59286,7 +59526,7 @@ var init_dist4 = __esm({
59286
59526
  });
59287
59527
 
59288
59528
  // ../org-manager/dist/org-service.js
59289
- import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync12, existsSync as existsSync23, rmSync as rmSync2 } from "node:fs";
59529
+ import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync13, existsSync as existsSync23, rmSync as rmSync2 } from "node:fs";
59290
59530
  import { join as join19 } from "node:path";
59291
59531
  import { homedir as homedir10 } from "node:os";
59292
59532
  var log50, OrganizationService;
@@ -59420,15 +59660,15 @@ var init_org_service = __esm({
59420
59660
  mkdirSync15(dir, { recursive: true });
59421
59661
  const annPath = join19(dir, "ANNOUNCEMENT.md");
59422
59662
  if (announcements) {
59423
- writeFileSync12(annPath, announcements, "utf-8");
59663
+ writeFileSync13(annPath, announcements, "utf-8");
59424
59664
  } else if (!existsSync23(annPath)) {
59425
- writeFileSync12(annPath, "", "utf-8");
59665
+ writeFileSync13(annPath, "", "utf-8");
59426
59666
  }
59427
59667
  const normsPath = join19(dir, "NORMS.md");
59428
59668
  if (norms) {
59429
- writeFileSync12(normsPath, norms, "utf-8");
59669
+ writeFileSync13(normsPath, norms, "utf-8");
59430
59670
  } else if (!existsSync23(normsPath)) {
59431
- writeFileSync12(normsPath, "", "utf-8");
59671
+ writeFileSync13(normsPath, "", "utf-8");
59432
59672
  }
59433
59673
  }
59434
59674
  async createTeam(orgId2, name, description) {
@@ -59794,15 +60034,17 @@ var init_org_service = __esm({
59794
60034
  }
59795
60035
  }
59796
60036
  this.refreshIdentityContextsForOrg(request.orgId);
59797
- try {
59798
- await this.agentManager.startAgent(agent.id);
59799
- log50.info(`Agent onboarded: ${request.name} (auto-started)`, {
59800
- orgId: request.orgId,
59801
- agentId: agent.id,
59802
- agentRole: request.agentRole ?? "worker"
59803
- });
59804
- } catch (error) {
59805
- log50.warn(`Agent created but auto-start failed: ${request.name}`, { error: String(error) });
60037
+ if (!request.skipAutoStart) {
60038
+ try {
60039
+ await this.agentManager.startAgent(agent.id);
60040
+ log50.info(`Agent onboarded: ${request.name} (auto-started)`, {
60041
+ orgId: request.orgId,
60042
+ agentId: agent.id,
60043
+ agentRole: request.agentRole ?? "worker"
60044
+ });
60045
+ } catch (error) {
60046
+ log50.warn(`Agent created but auto-start failed: ${request.name}`, { error: String(error) });
60047
+ }
59806
60048
  }
59807
60049
  return agent;
59808
60050
  }
@@ -60125,9 +60367,9 @@ var init_org_service = __esm({
60125
60367
  const roleNames = this.listAvailableRoles();
60126
60368
  if (roleNames.length > 0) {
60127
60369
  parts.push("");
60128
- parts.push("## Available Role Templates (live from system)");
60370
+ parts.push("## Built-in Role Templates (for reference only)");
60129
60371
  parts.push("");
60130
- parts.push("The `roleName` field must be one of:");
60372
+ parts.push("These templates are available for reading as **reference** when writing custom ROLE.md files. You do NOT need to set `roleName` \u2014 the agent's identity is fully defined by its ROLE.md.");
60131
60373
  parts.push("");
60132
60374
  for (const r of roleNames) {
60133
60375
  parts.push(`- \`${r}\``);
@@ -60135,9 +60377,9 @@ var init_org_service = __esm({
60135
60377
  const templateDirs = this.roleLoader.getTemplateDirs();
60136
60378
  if (templateDirs.length > 0) {
60137
60379
  parts.push("");
60138
- parts.push("**Tip**: You can read existing role templates for reference when writing custom ROLE.md files.");
60380
+ parts.push("**Tip**: Read existing templates for inspiration when writing custom ROLE.md files.");
60139
60381
  parts.push(`Use \`file_read\` to inspect any template, e.g.: \`file_read("${templateDirs[0]}/developer/ROLE.md")\``);
60140
- parts.push("This is especially useful for understanding the level of detail and workflow guidance expected in a good ROLE.md.");
60382
+ parts.push("This is useful for understanding the level of detail and workflow guidance expected in a good ROLE.md.");
60141
60383
  }
60142
60384
  }
60143
60385
  parts.push("");
@@ -60162,7 +60404,7 @@ var init_org_service = __esm({
60162
60404
  });
60163
60405
 
60164
60406
  // ../org-manager/dist/task-service.js
60165
- import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync13, readFileSync as readFileSync18, existsSync as existsSync24, cpSync } from "node:fs";
60407
+ import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync14, readFileSync as readFileSync18, existsSync as existsSync24, cpSync } from "node:fs";
60166
60408
  import { join as join20, resolve as resolve12 } from "node:path";
60167
60409
  import { homedir as homedir11 } from "node:os";
60168
60410
  function formatLocalTimestamp(d = /* @__PURE__ */ new Date()) {
@@ -60515,7 +60757,7 @@ var init_task_service = __esm({
60515
60757
  mkdirSync16(join20(dir, "knowledge"), { recursive: true });
60516
60758
  const userMdPath = join20(dir, "USER.md");
60517
60759
  if (!existsSync24(userMdPath)) {
60518
- writeFileSync13(userMdPath, [
60760
+ writeFileSync14(userMdPath, [
60519
60761
  "# About the Owner",
60520
60762
  "",
60521
60763
  "- Name:",
@@ -62581,7 +62823,7 @@ Action: ${guidance}` : ""
62581
62823
  testResults: d.testResults
62582
62824
  }))
62583
62825
  };
62584
- writeFileSync13(join20(taskSharedDir, "manifest.json"), JSON.stringify(manifest, null, 2));
62826
+ writeFileSync14(join20(taskSharedDir, "manifest.json"), JSON.stringify(manifest, null, 2));
62585
62827
  for (const d of deliverables) {
62586
62828
  if (d.type === "file" && d.reference) {
62587
62829
  const src = resolve12(d.reference);
@@ -62596,7 +62838,7 @@ Action: ${guidance}` : ""
62596
62838
  }
62597
62839
  if (d.type === "file" && d.summary && !existsSync24(d.reference)) {
62598
62840
  const safeName = d.reference.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
62599
- writeFileSync13(join20(taskSharedDir, `${safeName}.md`), d.summary);
62841
+ writeFileSync14(join20(taskSharedDir, `${safeName}.md`), d.summary);
62600
62842
  }
62601
62843
  }
62602
62844
  log51.info("Deliverables published to shared workspace", { taskId: task.id, dir: taskSharedDir });
@@ -62714,7 +62956,11 @@ Action: ${guidance}` : ""
62714
62956
  "",
62715
62957
  'Save each lesson using `memory_save` with tags `["lesson", ...]`.',
62716
62958
  'If it is a repeatable multi-step procedure, promote to SOP via `memory_update_longterm({ section: "sops", mode: "patch" })`.',
62717
- "If the best practice would benefit other agents on the team, create a shareable skill via **skill-building** and install it with `builder_install`."
62959
+ "If the best practice would benefit other agents on the team, create a shareable skill via **skill-building** and install it with `builder_install`.",
62960
+ "",
62961
+ "**Direct self-evolution** \u2014 consider the simplest, most impactful options:",
62962
+ "- If this lesson reveals a behavioral rule that should always guide your work, append it to your ROLE.md via `file_edit`.",
62963
+ "- If you should be checking for this class of issue regularly, add a check to your HEARTBEAT.md via `file_edit`."
62718
62964
  ].join("\n") : [
62719
62965
  "[SELF-EVOLUTION \u2014 Post-Task Reflection (Success)]",
62720
62966
  "",
@@ -62731,6 +62977,11 @@ Action: ${guidance}` : ""
62731
62977
  'If you identify a meaningful insight, save it using `memory_save` with tags `["lesson", "best-practice", ...]`.',
62732
62978
  'If it is a multi-step workflow, promote to SOP via `memory_update_longterm({ section: "sops", mode: "patch" })`.',
62733
62979
  "If worth sharing with the team, create a skill via **skill-building** and install with `builder_install`.",
62980
+ "",
62981
+ "**Direct self-evolution** \u2014 consider the simplest, most impactful options:",
62982
+ "- If this success reveals a guiding principle or working style worth keeping, append it to your ROLE.md via `file_edit`.",
62983
+ "- If there is a periodic check that would help maintain this quality, add it to your HEARTBEAT.md via `file_edit`.",
62984
+ "",
62734
62985
  "If nothing noteworthy stands out, it is fine to skip saving."
62735
62986
  ].join("\n");
62736
62987
  void agent.sendMessage(prompt, void 0, void 0, {
@@ -63413,7 +63664,7 @@ ${task.description}`;
63413
63664
 
63414
63665
  // ../org-manager/dist/builder-service.js
63415
63666
  import { join as join21 } from "node:path";
63416
- import { readdirSync as readdirSync8, readFileSync as readFileSync19, existsSync as existsSync25, writeFileSync as writeFileSync14, mkdirSync as mkdirSync17, copyFileSync as copyFileSync2, statSync as statSync4 } from "node:fs";
63667
+ import { readdirSync as readdirSync8, readFileSync as readFileSync19, existsSync as existsSync25, writeFileSync as writeFileSync15, mkdirSync as mkdirSync17, copyFileSync as copyFileSync2, statSync as statSync4, cpSync as cpSync2 } from "node:fs";
63417
63668
  import { homedir as homedir12 } from "node:os";
63418
63669
  var log52, FS_HELPER, BuilderService;
63419
63670
  var init_builder_service = __esm({
@@ -63497,17 +63748,17 @@ var init_builder_service = __esm({
63497
63748
  async installAgent(artDir, manifest, mfName, artifactName) {
63498
63749
  const agentManager = this.orgService.getAgentManager();
63499
63750
  const agentName = manifest.displayName ?? manifest.name ?? artifactName;
63500
- const knownRoles = this.orgService.listAvailableRoles();
63501
- const requestedRole = manifest.agent?.roleName ?? "developer";
63502
- const roleName = knownRoles.includes(requestedRole) ? requestedRole : "developer";
63751
+ const hasCustomRole = existsSync25(join21(artDir, "ROLE.md"));
63503
63752
  const skills = manifest.dependencies?.skills ?? [];
63504
63753
  const agentRole = manifest.agent?.agentRole ?? "worker";
63505
63754
  const agent = await this.orgService.hireAgent({
63506
63755
  name: agentName,
63507
- roleName,
63756
+ roleName: manifest.agent?.roleName || (hasCustomRole ? "custom" : "developer"),
63508
63757
  orgId: "default",
63509
63758
  agentRole,
63510
- skills
63759
+ skills,
63760
+ skipAutoStart: true,
63761
+ skipTemplateCopy: hasCustomRole
63511
63762
  });
63512
63763
  const agentRoleDir = join21(agentManager.getDataDir(), agent.id, "role");
63513
63764
  mkdirSync17(agentRoleDir, { recursive: true });
@@ -63519,7 +63770,7 @@ var init_builder_service = __esm({
63519
63770
  copyFileSync2(srcFile, join21(agentRoleDir, fname));
63520
63771
  }
63521
63772
  }
63522
- writeFileSync14(join21(agentRoleDir, ".role-origin.json"), JSON.stringify({ customRole: true, source: "builder-artifact", artifact: artifactName, artifactType: "agent" }));
63773
+ writeFileSync15(join21(agentRoleDir, ".role-origin.json"), JSON.stringify({ customRole: true, source: "builder-artifact", artifact: artifactName, artifactType: "agent" }));
63523
63774
  agent.reloadRole();
63524
63775
  await agentManager.startAgent(agent.id);
63525
63776
  return {
@@ -63547,38 +63798,42 @@ var init_builder_service = __esm({
63547
63798
  const norms = existsSync25(normsPath) ? readFileSync19(normsPath, "utf-8") : "";
63548
63799
  this.orgService.ensureTeamDataDir(team.id, announcements, norms);
63549
63800
  const members = manifest.team?.members ?? [];
63550
- const knownRoles = this.orgService.listAvailableRoles();
63551
63801
  const createdAgents = [];
63802
+ const usedMemberDirs = /* @__PURE__ */ new Set();
63552
63803
  for (const member of members) {
63553
63804
  const count = member.count ?? 1;
63554
63805
  const memberRole = member.role ?? "worker";
63555
63806
  const memberName = member.name ?? "Agent";
63556
- const roleName = knownRoles.includes(member.roleName) ? member.roleName : "developer";
63557
63807
  const memberSkills = member.skills ?? [];
63558
- const memberSlug = memberName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
63559
- const memberFilesDir = join21(artDir, "members", memberSlug);
63808
+ const memberFilesDir = this.findMemberDir(artDir, memberName, usedMemberDirs);
63809
+ const hasCustomRole = !!memberFilesDir && existsSync25(join21(memberFilesDir, "ROLE.md"));
63810
+ if (memberFilesDir)
63811
+ usedMemberDirs.add(memberFilesDir);
63812
+ log52.info("installTeam: member lookup", { memberName, memberFilesDir, hasCustomRole });
63560
63813
  for (let i = 0; i < count; i++) {
63561
63814
  const displayName = count > 1 ? `${memberName} ${i + 1}` : memberName;
63562
63815
  const agent = await this.orgService.hireAgent({
63563
63816
  name: displayName,
63564
- roleName,
63817
+ roleName: member.roleName || (hasCustomRole ? "custom" : "developer"),
63565
63818
  orgId: "default",
63566
63819
  teamId: team.id,
63567
63820
  agentRole: memberRole,
63568
- skills: memberSkills.length > 0 ? memberSkills : void 0
63821
+ skills: memberSkills.length > 0 ? memberSkills : void 0,
63822
+ skipAutoStart: true,
63823
+ skipTemplateCopy: hasCustomRole
63569
63824
  });
63570
63825
  const agentRoleDir = join21(agentManager.getDataDir(), agent.id, "role");
63571
63826
  mkdirSync17(agentRoleDir, { recursive: true });
63572
- if (existsSync25(memberFilesDir)) {
63827
+ if (memberFilesDir && existsSync25(memberFilesDir)) {
63573
63828
  for (const fname of readdirSync8(memberFilesDir)) {
63574
63829
  const srcFile = join21(memberFilesDir, fname);
63575
63830
  if (statSync4(srcFile).isFile()) {
63576
63831
  copyFileSync2(srcFile, join21(agentRoleDir, fname));
63577
63832
  }
63578
63833
  }
63579
- agent.reloadRole();
63580
63834
  }
63581
- writeFileSync14(join21(agentRoleDir, ".role-origin.json"), JSON.stringify({ customRole: true, source: "builder-artifact", artifact: artifactName, artifactType: "team" }));
63835
+ writeFileSync15(join21(agentRoleDir, ".role-origin.json"), JSON.stringify({ customRole: true, source: "builder-artifact", artifact: artifactName, artifactType: "team" }));
63836
+ agent.reloadRole();
63582
63837
  if (memberRole === "manager") {
63583
63838
  await this.orgService.updateTeam(team.id, { managerId: agent.id, managerType: "agent" });
63584
63839
  }
@@ -63591,13 +63846,56 @@ var init_builder_service = __esm({
63591
63846
  installed: { team: { id: team.id, name: teamName }, agents: createdAgents }
63592
63847
  };
63593
63848
  }
63849
+ /**
63850
+ * Find the member directory under artDir/members/ by trying multiple slug strategies.
63851
+ * Returns the absolute path to the member directory, or null if not found.
63852
+ */
63853
+ findMemberDir(artDir, memberName, usedDirs) {
63854
+ const membersBase = join21(artDir, "members");
63855
+ if (!existsSync25(membersBase))
63856
+ return null;
63857
+ const slug = kebab(memberName, "agent");
63858
+ const exact = join21(membersBase, slug);
63859
+ if (existsSync25(exact) && !usedDirs.has(exact))
63860
+ return exact;
63861
+ try {
63862
+ for (const entry of readdirSync8(membersBase, { withFileTypes: true })) {
63863
+ if (!entry.isDirectory())
63864
+ continue;
63865
+ const candidateDir = join21(membersBase, entry.name);
63866
+ if (usedDirs.has(candidateDir))
63867
+ continue;
63868
+ const rolePath = join21(candidateDir, "ROLE.md");
63869
+ if (!existsSync25(rolePath))
63870
+ continue;
63871
+ try {
63872
+ const content = readFileSync19(rolePath, "utf-8");
63873
+ const title = content.match(/^#\s+(.+)$/m)?.[1]?.trim();
63874
+ if (title && title.toLowerCase() === memberName.toLowerCase())
63875
+ return candidateDir;
63876
+ } catch {
63877
+ }
63878
+ }
63879
+ } catch {
63880
+ }
63881
+ try {
63882
+ const remaining = readdirSync8(membersBase, { withFileTypes: true }).filter((e) => e.isDirectory() && !usedDirs.has(join21(membersBase, e.name)));
63883
+ if (remaining.length === 1)
63884
+ return join21(membersBase, remaining[0].name);
63885
+ } catch {
63886
+ }
63887
+ return null;
63888
+ }
63594
63889
  async installSkill(artDir, manifest, artifactName) {
63595
63890
  const skillDir = join21(homedir12(), ".markus", "skills", artifactName);
63596
63891
  mkdirSync17(skillDir, { recursive: true });
63597
63892
  for (const fname of readdirSync8(artDir)) {
63598
63893
  const srcFile = join21(artDir, fname);
63894
+ const destFile = join21(skillDir, fname);
63599
63895
  if (statSync4(srcFile).isFile()) {
63600
- copyFileSync2(srcFile, join21(skillDir, fname));
63896
+ copyFileSync2(srcFile, destFile);
63897
+ } else if (statSync4(srcFile).isDirectory()) {
63898
+ cpSync2(srcFile, destFile, { recursive: true });
63601
63899
  }
63602
63900
  }
63603
63901
  if (this.skillRegistry) {
@@ -64249,23 +64547,6 @@ var init_sse_handler = __esm({
64249
64547
  this.sseBuffer.sendProgress(this.processedTokens, this.totalTokens, `\u5DE5\u5177\u6267\u884C\u5B8C\u6210: ${event.tool}`);
64250
64548
  }
64251
64549
  } else if (event.type === "message_end") {
64252
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
64253
- let turnThinking;
64254
- if (this.thinkingBuf) {
64255
- this.sseBuffer.send({ type: "thinking_commit", thinking: this.thinkingBuf, createdAt: now2 });
64256
- turnThinking = this.thinkingBuf;
64257
- this.thinkingBuf = "";
64258
- }
64259
- if (this.textBuf) {
64260
- this.sseBuffer.send({ type: "text_commit", text: this.textBuf, createdAt: now2 });
64261
- const seg2 = { type: "text", content: this.textBuf, createdAt: now2 };
64262
- if (turnThinking)
64263
- seg2.thinking = turnThinking;
64264
- this.msgSegments.push(seg2);
64265
- this.textBuf = "";
64266
- } else if (turnThinking) {
64267
- this.msgSegments.push({ type: "text", content: "", thinking: turnThinking, createdAt: now2 });
64268
- }
64269
64550
  if (event.usage?.outputTokens) {
64270
64551
  this.totalTokens = Math.max(this.totalTokens, event.usage.outputTokens);
64271
64552
  this.processedTokens = event.usage.outputTokens;
@@ -64339,7 +64620,7 @@ var init_sse_handler = __esm({
64339
64620
 
64340
64621
  // ../org-manager/dist/skill-service.js
64341
64622
  import { join as join22, resolve as resolve13 } from "node:path";
64342
- import { existsSync as existsSync26, writeFileSync as writeFileSync15, mkdirSync as mkdirSync18, readFileSync as readFileSync20, readdirSync as readdirSync9, copyFileSync as copyFileSync3 } from "node:fs";
64623
+ import { existsSync as existsSync26, writeFileSync as writeFileSync16, mkdirSync as mkdirSync18, readFileSync as readFileSync20, readdirSync as readdirSync9, copyFileSync as copyFileSync3 } from "node:fs";
64343
64624
  import { homedir as homedir13 } from "node:os";
64344
64625
  import { execSync as execSync2 } from "node:child_process";
64345
64626
  async function searchSkillHub(query2) {
@@ -64490,7 +64771,7 @@ async function installSkill(request, skillRegistry) {
64490
64771
  if (zipResp.ok) {
64491
64772
  const tmpZip = join22(skillsDir, `_tmp_${safeName}.zip`);
64492
64773
  const buffer = Buffer.from(await zipResp.arrayBuffer());
64493
- writeFileSync15(tmpZip, buffer);
64774
+ writeFileSync16(tmpZip, buffer);
64494
64775
  mkdirSync18(targetDir, { recursive: true });
64495
64776
  try {
64496
64777
  execSync2(`unzip -o "${tmpZip}" -d "${targetDir}"`, { timeout: 3e4 });
@@ -64516,7 +64797,7 @@ async function installSkill(request, skillRegistry) {
64516
64797
  const mdResp = await fetch(skillMdUrl, { signal: AbortSignal.timeout(15e3) });
64517
64798
  if (mdResp.ok) {
64518
64799
  mkdirSync18(targetDir, { recursive: true });
64519
- writeFileSync15(join22(targetDir, "SKILL.md"), await mdResp.text(), "utf-8");
64800
+ writeFileSync16(join22(targetDir, "SKILL.md"), await mdResp.text(), "utf-8");
64520
64801
  installed = true;
64521
64802
  installMethod = "github-skillmd";
64522
64803
  }
@@ -64530,7 +64811,7 @@ async function installSkill(request, skillRegistry) {
64530
64811
  const mdResp = await fetch(rootMd, { signal: AbortSignal.timeout(15e3) });
64531
64812
  if (mdResp.ok) {
64532
64813
  mkdirSync18(targetDir, { recursive: true });
64533
- writeFileSync15(join22(targetDir, "SKILL.md"), await mdResp.text(), "utf-8");
64814
+ writeFileSync16(join22(targetDir, "SKILL.md"), await mdResp.text(), "utf-8");
64534
64815
  installed = true;
64535
64816
  installMethod = "github-root-skillmd";
64536
64817
  }
@@ -64546,7 +64827,7 @@ async function installSkill(request, skillRegistry) {
64546
64827
  if (zipResp.ok) {
64547
64828
  const tmpZip = join22(skillsDir, `_tmp_${safeName}.zip`);
64548
64829
  const buffer = Buffer.from(await zipResp.arrayBuffer());
64549
- writeFileSync15(tmpZip, buffer);
64830
+ writeFileSync16(tmpZip, buffer);
64550
64831
  mkdirSync18(targetDir, { recursive: true });
64551
64832
  try {
64552
64833
  execSync2(`unzip -o "${tmpZip}" -d "${targetDir}"`, { timeout: 3e4 });
@@ -64578,13 +64859,13 @@ async function installSkill(request, skillRegistry) {
64578
64859
  };
64579
64860
  const manifest = buildManifest("skill", raw);
64580
64861
  manifest.source = skillSource;
64581
- writeFileSync15(skillMfPath, JSON.stringify(manifest, null, 2), "utf-8");
64862
+ writeFileSync16(skillMfPath, JSON.stringify(manifest, null, 2), "utf-8");
64582
64863
  } else {
64583
64864
  try {
64584
64865
  const mf = JSON.parse(readFileSync20(skillMfPath, "utf-8"));
64585
64866
  if (!mf.source) {
64586
64867
  mf.source = skillSource;
64587
- writeFileSync15(skillMfPath, JSON.stringify(mf, null, 2), "utf-8");
64868
+ writeFileSync16(skillMfPath, JSON.stringify(mf, null, 2), "utf-8");
64588
64869
  }
64589
64870
  } catch {
64590
64871
  }
@@ -64618,7 +64899,7 @@ var init_skill_service = __esm({
64618
64899
  // ../org-manager/dist/api-server.js
64619
64900
  import { createServer as createServer2 } from "node:http";
64620
64901
  import { join as join23, resolve as resolve14, dirname as dirname6 } from "node:path";
64621
- import { readdirSync as readdirSync10, readFileSync as readFileSync21, existsSync as existsSync27, writeFileSync as writeFileSync16, mkdirSync as mkdirSync19, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
64902
+ import { readdirSync as readdirSync10, readFileSync as readFileSync21, existsSync as existsSync27, writeFileSync as writeFileSync17, mkdirSync as mkdirSync19, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
64622
64903
  import { homedir as homedir14 } from "node:os";
64623
64904
  import { execSync as execSync3 } from "node:child_process";
64624
64905
  async function signToken(payload, secret) {
@@ -64708,7 +64989,7 @@ var init_api_server = __esm({
64708
64989
  init_skill_service();
64709
64990
  log57 = createLogger("api-server");
64710
64991
  PBKDF2_ITERATIONS = 1e4;
64711
- APIServer = class {
64992
+ APIServer = class _APIServer {
64712
64993
  orgService;
64713
64994
  taskService;
64714
64995
  port;
@@ -64733,7 +65014,7 @@ var init_api_server = __esm({
64733
65014
  builderService;
64734
65015
  workflowEngine;
64735
65016
  teamTemplateRegistry;
64736
- customGroupChats = [];
65017
+ // Custom group chats are now persisted in SQLite via storage.groupChatRepo
64737
65018
  constructor(orgService, taskService, port = 8056) {
64738
65019
  this.orgService = orgService;
64739
65020
  this.taskService = taskService;
@@ -64748,15 +65029,18 @@ var init_api_server = __esm({
64748
65029
  am.setGroupChatHandlers({
64749
65030
  sendGroupMessage: async (channelKey, message, senderId, senderName) => {
64750
65031
  const cleanText = stripInternalBlocks(message);
65032
+ const orgId2 = "default";
65033
+ let persistedMsgId;
64751
65034
  if (this.storage) {
64752
- await this.storage.channelMessageRepo.append({
64753
- orgId: "default",
65035
+ const saved = await this.storage.channelMessageRepo.append({
65036
+ orgId: orgId2,
64754
65037
  channel: channelKey,
64755
65038
  senderId,
64756
65039
  senderType: "agent",
64757
65040
  senderName,
64758
65041
  text: cleanText
64759
65042
  });
65043
+ persistedMsgId = saved.id;
64760
65044
  }
64761
65045
  this.ws.broadcast({
64762
65046
  type: "chat:message",
@@ -64769,41 +65053,81 @@ var init_api_server = __esm({
64769
65053
  },
64770
65054
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
64771
65055
  });
64772
- const chat = this.customGroupChats.find((c) => c.id === channelKey);
64773
- if (chat) {
64774
- for (const memberId of chat.memberIds) {
64775
- if (memberId === senderId)
64776
- continue;
64777
- try {
64778
- const memberAgent = am.getAgent(memberId);
64779
- memberAgent.enqueueToMailbox("a2a_message", {
64780
- summary: `Group chat message from ${senderName}`,
64781
- content: `[Group chat: ${chat.name}] ${senderName}: ${cleanText}`,
64782
- extra: { senderId, senderName, channelKey }
64783
- });
64784
- } catch {
65056
+ let allAgentIds = [];
65057
+ if (channelKey.startsWith("group:custom:") && this.storage?.groupChatRepo) {
65058
+ allAgentIds = this.storage.groupChatRepo.getAgentMemberIds(channelKey);
65059
+ } else if (channelKey.startsWith("group:")) {
65060
+ const teamId = channelKey.replace(/^group:/, "");
65061
+ const team = this.orgService.getTeam(teamId);
65062
+ allAgentIds = team?.memberAgentIds ?? [];
65063
+ }
65064
+ const peerAgentIds = allAgentIds.filter((id) => id !== senderId);
65065
+ if (peerAgentIds.length > 0) {
65066
+ const agentManager = this.orgService.getAgentManager();
65067
+ const nameMap = this.buildAgentNameMap(allAgentIds, agentManager);
65068
+ const mentionedNames = this.parseAgentMentions(cleanText, [...nameMap.keys()]);
65069
+ const filteredMentions = mentionedNames.filter((n) => {
65070
+ const id = nameMap.get(n);
65071
+ return id && id !== senderId;
65072
+ });
65073
+ if (filteredMentions.length > 0) {
65074
+ let channelContext = [];
65075
+ if (this.storage) {
65076
+ try {
65077
+ const recent = await this.storage.channelMessageRepo.getMessages(channelKey, 20);
65078
+ channelContext = (recent.messages ?? []).map((m) => ({
65079
+ role: m.senderType === "agent" ? "assistant" : "user",
65080
+ content: m.senderType === "agent" ? stripInternalBlocks(m.text) : `[${m.senderName}]: ${m.text}`
65081
+ }));
65082
+ } catch {
65083
+ }
65084
+ }
65085
+ const roundId = `round_${Date.now()}`;
65086
+ const respondedAgents = /* @__PURE__ */ new Set([senderId]);
65087
+ void this.triggerAgentToAgentChain(filteredMentions, nameMap, senderName, senderId, cleanText, persistedMsgId, channelKey, orgId2, agentManager, allAgentIds.length, { roundId, depth: 1, respondedAgents, allAgentIds });
65088
+ } else {
65089
+ for (const peerId of peerAgentIds) {
65090
+ try {
65091
+ agentManager.getAgent(peerId).enqueueToMailbox("a2a_message", {
65092
+ summary: `Group chat message from ${senderName}`,
65093
+ content: `[Group chat message from ${senderName}]:
65094
+ ${cleanText}`,
65095
+ extra: { senderId, senderName, channelKey }
65096
+ }, {
65097
+ metadata: { senderId, senderName, senderRole: "agent" }
65098
+ });
65099
+ } catch {
65100
+ }
64785
65101
  }
64786
65102
  }
64787
65103
  }
64788
65104
  return "Message sent to group chat";
64789
65105
  },
64790
65106
  createGroupChat: async (name, creatorId, creatorName, memberIds) => {
64791
- const chatId = `group:custom:${Date.now().toString(36)}`;
64792
- this.customGroupChats.push({
64793
- id: chatId,
64794
- name,
64795
- orgId: "default",
64796
- creatorId,
64797
- creatorName,
64798
- memberIds,
64799
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
64800
- });
64801
- this.ws.broadcast({
64802
- type: "chat:group_created",
64803
- payload: { chatId, name, creatorId, creatorName },
64804
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
64805
- });
64806
- return { id: chatId, name };
65107
+ if (this.storage?.groupChatRepo) {
65108
+ const members = memberIds.map((id) => {
65109
+ try {
65110
+ const agentInfo = this.orgService.getAgentManager().getAgent(id);
65111
+ return { id, type: "agent", name: agentInfo?.config?.name ?? id };
65112
+ } catch {
65113
+ return { id, type: "agent", name: id };
65114
+ }
65115
+ });
65116
+ const gc = this.storage.groupChatRepo.create({
65117
+ orgId: "default",
65118
+ name,
65119
+ creatorId,
65120
+ creatorName,
65121
+ members
65122
+ });
65123
+ this.ws.broadcast({
65124
+ type: "chat:group_created",
65125
+ payload: { chatId: gc.channelKey, name, creatorId, creatorName },
65126
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
65127
+ });
65128
+ return { id: gc.channelKey, name };
65129
+ }
65130
+ return { id: `group:custom:${Date.now().toString(36)}`, name };
64807
65131
  },
64808
65132
  listGroupChats: async () => {
64809
65133
  const teams = this.orgService.listTeamsWithMembers("default");
@@ -64813,13 +65137,27 @@ var init_api_server = __esm({
64813
65137
  type: "team",
64814
65138
  channelKey: `group:${t.id}`
64815
65139
  }));
64816
- const customChats = this.customGroupChats.map((c) => ({
64817
- id: c.id,
65140
+ const customChats = this.storage?.groupChatRepo ? this.storage.groupChatRepo.list("default").map((c) => ({
65141
+ id: c.channelKey,
64818
65142
  name: c.name,
64819
65143
  type: "custom",
64820
- channelKey: c.id
64821
- }));
65144
+ channelKey: c.channelKey
65145
+ })) : [];
64822
65146
  return [...teamChats, ...customChats];
65147
+ },
65148
+ getChannelMessages: async (channelKey, limit, before2) => {
65149
+ if (!this.storage)
65150
+ return { messages: [], hasMore: false };
65151
+ const result = this.storage.channelMessageRepo.getMessages(channelKey, limit, before2);
65152
+ return {
65153
+ messages: result.messages.map((m) => ({
65154
+ senderName: m.senderName,
65155
+ senderType: m.senderType,
65156
+ text: stripInternalBlocks(m.text),
65157
+ createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : String(m.createdAt)
65158
+ })),
65159
+ hasMore: result.hasMore
65160
+ };
64823
65161
  }
64824
65162
  });
64825
65163
  }
@@ -64876,7 +65214,7 @@ var init_api_server = __esm({
64876
65214
  throw new Error(`Hub download failed: ${res.status}`);
64877
65215
  const data = await res.json();
64878
65216
  const name = data.name;
64879
- const slug = name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
65217
+ const slug = kebab(name, "hub-pkg");
64880
65218
  const mode = data.itemType === "team" ? "team" : data.itemType === "skill" ? "skill" : "agent";
64881
65219
  const typeDir = mode === "agent" ? "agents" : mode === "team" ? "teams" : "skills";
64882
65220
  const artDir = join23(homedir14(), ".markus", "builder-artifacts", typeDir, slug);
@@ -64885,10 +65223,10 @@ var init_api_server = __esm({
64885
65223
  for (const [fname, content] of Object.entries(data.files)) {
64886
65224
  const filePath = join23(artDir, fname);
64887
65225
  mkdirSync19(dirname6(filePath), { recursive: true });
64888
- writeFileSync16(filePath, content, "utf-8");
65226
+ writeFileSync17(filePath, content, "utf-8");
64889
65227
  }
64890
65228
  } else if (data.config) {
64891
- writeFileSync16(join23(artDir, manifestFilename(mode)), JSON.stringify(data.config, null, 2), "utf-8");
65229
+ writeFileSync17(join23(artDir, manifestFilename(mode)), JSON.stringify(data.config, null, 2), "utf-8");
64892
65230
  }
64893
65231
  return self.builderService.installArtifact(mode, slug);
64894
65232
  }
@@ -65119,48 +65457,167 @@ var init_api_server = __esm({
65119
65457
  log57.warn("Failed to persist chat turn", { error: String(err) });
65120
65458
  }
65121
65459
  }
65460
+ // ── Agent-to-agent reply storm prevention ───────────────────────────────────
65461
+ static A2A_MAX_DEPTH = 3;
65462
+ static A2A_COOLDOWN_MS = 3e4;
65463
+ /** Per-channel cooldown tracker: channel → agentId → last reply timestamp */
65464
+ a2aCooldowns = /* @__PURE__ */ new Map();
65465
+ /** Extract @mentions from agent reply text. Returns agent names found.
65466
+ * Handles multi-word names (e.g. "Ryan 莱恩") by checking known names after each @ sign.
65467
+ * Also matches partial names: "@Sofia" matches "Sofia 索菲亚" if "Sofia" is a token in the name. */
65468
+ parseAgentMentions(text, knownAgentNames) {
65469
+ const mentioned = /* @__PURE__ */ new Set();
65470
+ const sorted = [...knownAgentNames].sort((a, b) => b.length - a.length);
65471
+ const tokenToName = /* @__PURE__ */ new Map();
65472
+ for (const name of sorted) {
65473
+ for (const token of name.split(/\s+/)) {
65474
+ if (token.length >= 2) {
65475
+ const key2 = token.toLowerCase();
65476
+ if (!tokenToName.has(key2))
65477
+ tokenToName.set(key2, name);
65478
+ }
65479
+ }
65480
+ }
65481
+ let idx = 0;
65482
+ while (idx < text.length) {
65483
+ const atPos = text.indexOf("@", idx);
65484
+ if (atPos < 0)
65485
+ break;
65486
+ if (text[atPos + 1] === "[") {
65487
+ const close = text.indexOf("]", atPos + 2);
65488
+ if (close > atPos + 2) {
65489
+ const bracketed = text.slice(atPos + 2, close);
65490
+ const match = sorted.find((n) => n.toLowerCase() === bracketed.toLowerCase());
65491
+ if (match)
65492
+ mentioned.add(match);
65493
+ idx = close + 1;
65494
+ continue;
65495
+ }
65496
+ }
65497
+ const after2 = text.slice(atPos + 1);
65498
+ const afterLower = after2.toLowerCase();
65499
+ const fullMatch = sorted.find((n) => afterLower.startsWith(n.toLowerCase()));
65500
+ if (fullMatch) {
65501
+ mentioned.add(fullMatch);
65502
+ idx = atPos + 1 + fullMatch.length;
65503
+ continue;
65504
+ }
65505
+ const tokenMatch = after2.match(/^(\S+)/);
65506
+ if (tokenMatch) {
65507
+ const token = tokenMatch[1].toLowerCase().replace(/[,,。!?!?;;::、()()[\]【】]+$/, "");
65508
+ const resolved = tokenToName.get(token);
65509
+ if (resolved) {
65510
+ mentioned.add(resolved);
65511
+ idx = atPos + 1 + tokenMatch[1].length;
65512
+ continue;
65513
+ }
65514
+ }
65515
+ idx = atPos + 1;
65516
+ }
65517
+ return [...mentioned];
65518
+ }
65519
+ /** Build name→id lookup for agents in a channel */
65520
+ buildAgentNameMap(agentIds, agentManager) {
65521
+ const map = /* @__PURE__ */ new Map();
65522
+ for (const id of agentIds) {
65523
+ try {
65524
+ map.set(agentManager.getAgent(id).config.name, id);
65525
+ } catch {
65526
+ }
65527
+ }
65528
+ return map;
65529
+ }
65122
65530
  /**
65123
65531
  * Process a single agent's reply in a group chat broadcast.
65124
65532
  * Each agent decides independently whether to respond.
65125
65533
  * Replies are persisted and broadcast via WebSocket.
65534
+ * If the agent @mentions other agents, a chain reply is triggered (with storm prevention).
65126
65535
  */
65127
- async processGroupChatReply(agentId2, userMessage, senderId, senderInfo, channel, orgId2, channelContext, agentManager, teamSize) {
65536
+ async processGroupChatReply(agentId2, userMessage, senderId, senderInfo, channel, orgId2, channelContext, agentManager, teamSize, opts, chainCtx) {
65128
65537
  try {
65129
65538
  const agent = agentManager.getAgent(agentId2);
65130
65539
  const agentName = agent.config.name;
65131
- const groupChatPrefix = [
65132
- `[GROUP CHAT \u2014 ${teamSize} team members receiving this independently]`,
65133
- "",
65134
- "You are ONE of several agents in a group chat. Rules:",
65135
- "1. **Check channel history first** \u2014 if another agent already answered the question or addressed the topic, do NOT repeat their answer. Add only if you have a *different* perspective or expertise.",
65136
- '2. **Only respond if your role/expertise is specifically relevant.** Generic offers like "Let me know if you need help" add no value.',
65137
- "3. **Be concise** \u2014 this is a multi-party conversation. Short, actionable responses only.",
65138
- "4. **Do NOT create tasks** unless the user explicitly asks you to and you are the manager/coordinator.",
65139
- "5. **Prefer [NO_RESPONSE]** \u2014 when in doubt, say nothing. It is better to stay silent than to add noise.",
65140
- "",
65141
- "If you have nothing uniquely valuable to contribute, respond with exactly: [NO_RESPONSE]",
65142
- "---",
65540
+ const isA2A = !!chainCtx && chainCtx.depth > 0;
65541
+ const hasReplyTarget = !!opts?.replyToAgentName;
65542
+ const hasMentions = !!opts?.mentionedNames?.length;
65543
+ const isTargeted = hasReplyTarget || hasMentions;
65544
+ const targetNames = /* @__PURE__ */ new Set();
65545
+ if (opts?.replyToAgentName)
65546
+ targetNames.add(opts.replyToAgentName);
65547
+ if (opts?.mentionedNames)
65548
+ opts.mentionedNames.forEach((n) => targetNames.add(n));
65549
+ const thisAgentIsTarget = targetNames.has(agentName);
65550
+ const prefixLines = [
65551
+ `[GROUP CHAT \u2014 ${teamSize} team members | You are: ${agentName}]`,
65143
65552
  ""
65144
- ].join("\n");
65553
+ ];
65554
+ if (isA2A) {
65555
+ prefixLines.push(`[AGENT COLLABORATION] This message is from a fellow agent (${opts?.replyToAgentName ?? "teammate"}), not from the user.`);
65556
+ prefixLines.push("You were @mentioned because your expertise is needed. Respond concisely to the specific request.");
65557
+ prefixLines.push("You may @mention another agent if (and ONLY if) you genuinely need their specific expertise to answer.");
65558
+ prefixLines.push("Do NOT @mention agents just to be polite or to pass the conversation along.");
65559
+ prefixLines.push("");
65560
+ }
65561
+ if (isTargeted && !isA2A) {
65562
+ if (hasReplyTarget) {
65563
+ prefixLines.push(`[REPLY] The user is replying to ${opts.replyToAgentName}'s message: "${(opts.replyToText ?? "").slice(0, 200)}"`);
65564
+ }
65565
+ if (hasMentions) {
65566
+ prefixLines.push(`[MENTIONED] The user @mentioned: ${opts.mentionedNames.join(", ")}`);
65567
+ }
65568
+ prefixLines.push("");
65569
+ if (thisAgentIsTarget) {
65570
+ prefixLines.push(">>> You are the target of this message. You SHOULD respond. <<<");
65571
+ } else {
65572
+ prefixLines.push(">>> STOP. This message is NOT for you. The user is talking to " + [...targetNames].join(", ") + ", not you.");
65573
+ prefixLines.push("You MUST respond with exactly: [NO_RESPONSE]");
65574
+ prefixLines.push('The ONLY exception: you are directly contradicted by a factual error. Offering opinions, agreement, "me too", or generic help does NOT count. <<<');
65575
+ }
65576
+ } else if (!isA2A) {
65577
+ prefixLines.push("This is an open group message (no specific @mention or reply target).");
65578
+ }
65579
+ prefixLines.push("");
65580
+ prefixLines.push("Rules for ALL group chat messages:");
65581
+ prefixLines.push("1. Check channel history \u2014 if another agent already answered, do NOT repeat.");
65582
+ prefixLines.push("2. Only respond if your specific role/expertise is directly relevant.");
65583
+ prefixLines.push("3. Be concise \u2014 short, actionable responses only.");
65584
+ if (agent.config.agentRole === "manager") {
65585
+ prefixLines.push("4. When assigning work to agents, you MUST use `task_create` to formalize each assignment as a tracked task. Verbal delegation without a task is NOT allowed \u2014 oral promises are meaningless without task tracking. Every commitment must have a corresponding task.");
65586
+ } else {
65587
+ prefixLines.push("4. When you accept a work assignment, verify a task has been created for it (`task_list`). If the coordinator did not create one, remind them or create it yourself with the correct `assigned_agent_id`. Do NOT make promises without task tracking.");
65588
+ }
65589
+ prefixLines.push("5. DEFAULT IS SILENCE. When in doubt, respond with exactly: [NO_RESPONSE]");
65590
+ prefixLines.push('6. If the provided context is insufficient, use `recall_context` (scope="channel") to fetch earlier messages before responding. For task/requirement details use `task_get`/`requirement_get`. Do NOT guess about prior discussion.');
65591
+ if (isTargeted && !thisAgentIsTarget && !isA2A) {
65592
+ prefixLines.push("7. REMINDER: This message is directed at " + [...targetNames].join(", ") + ". You are " + agentName + ". Respond ONLY with [NO_RESPONSE].");
65593
+ }
65594
+ prefixLines.push("---", "");
65595
+ const groupChatPrefix = prefixLines.join("\n");
65145
65596
  const toolEvents = [];
65146
65597
  const reply = await agent.sendMessage(groupChatPrefix + userMessage, senderId, senderInfo, {
65147
- sourceType: "human_chat",
65598
+ sourceType: isA2A ? "a2a_message" : "human_chat",
65599
+ scenario: isA2A ? "a2a" : void 0,
65148
65600
  channelContext,
65149
65601
  toolEventCollector: toolEvents
65150
65602
  });
65151
- if (!reply || reply.trim() === "[NO_RESPONSE]" || !reply.trim()) {
65603
+ if (!reply || !reply.trim() || reply.includes("[NO_RESPONSE]")) {
65152
65604
  return;
65153
65605
  }
65154
- const { thinking, clean: cleanReply } = extractThinkBlocks(reply);
65155
- if (!cleanReply.trim() || cleanReply.trim() === "[NO_RESPONSE]")
65606
+ const { thinking, clean: rawClean } = extractThinkBlocks(reply);
65607
+ const cleanReply = rawClean.replace(/\[NO_RESPONSE\]/gi, "").trim();
65608
+ if (!cleanReply)
65156
65609
  return;
65157
65610
  const metadata = {};
65158
65611
  if (thinking.length > 0)
65159
65612
  metadata["thinking"] = thinking;
65160
65613
  if (toolEvents.length > 0)
65161
65614
  metadata["toolCalls"] = toolEvents;
65615
+ if (isA2A && opts?.replyToAgentName) {
65616
+ metadata["replyToAgent"] = opts.replyToAgentName;
65617
+ }
65618
+ let persistedMsgId;
65162
65619
  if (this.storage) {
65163
- await this.storage.channelMessageRepo.append({
65620
+ const saved = await this.storage.channelMessageRepo.append({
65164
65621
  orgId: orgId2,
65165
65622
  channel,
65166
65623
  senderId: agentId2,
@@ -65168,8 +65625,10 @@ var init_api_server = __esm({
65168
65625
  senderName: agentName,
65169
65626
  text: cleanReply,
65170
65627
  mentions: [],
65171
- metadata: Object.keys(metadata).length > 0 ? metadata : void 0
65628
+ metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
65629
+ replyToId: isA2A ? opts?.replyToMsgId : void 0
65172
65630
  });
65631
+ persistedMsgId = saved.id;
65173
65632
  }
65174
65633
  this.ws.broadcast({
65175
65634
  type: "chat:message",
@@ -65179,10 +65638,29 @@ var init_api_server = __esm({
65179
65638
  senderType: "agent",
65180
65639
  senderName: agentName,
65181
65640
  text: cleanReply,
65182
- metadata: Object.keys(metadata).length > 0 ? metadata : void 0
65641
+ metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
65642
+ ...isA2A ? {
65643
+ replyToId: opts?.replyToMsgId,
65644
+ replyToSender: opts?.replyToAgentName,
65645
+ replyToText: (opts?.replyToText ?? "").slice(0, 120)
65646
+ } : {}
65183
65647
  },
65184
65648
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
65185
65649
  });
65650
+ const allAgentIds = chainCtx?.allAgentIds ?? [];
65651
+ if (allAgentIds.length > 0) {
65652
+ const nameMap = this.buildAgentNameMap(allAgentIds, agentManager);
65653
+ const mentionedNames = this.parseAgentMentions(cleanReply, [...nameMap.keys()]);
65654
+ const filteredMentions = mentionedNames.filter((n) => n !== agentName);
65655
+ log57.info("A2A mention scan", { agentName, mentionedNames, filteredMentions, depth: chainCtx?.depth ?? 0 });
65656
+ if (filteredMentions.length > 0) {
65657
+ const depth = (chainCtx?.depth ?? 0) + 1;
65658
+ const roundId = chainCtx?.roundId ?? `round_${Date.now()}`;
65659
+ const responded = chainCtx?.respondedAgents ?? /* @__PURE__ */ new Set();
65660
+ responded.add(agentId2);
65661
+ void this.triggerAgentToAgentChain(filteredMentions, nameMap, agentName, agentId2, cleanReply, persistedMsgId, channel, orgId2, agentManager, teamSize, { roundId, depth, respondedAgents: responded, allAgentIds });
65662
+ }
65663
+ }
65186
65664
  } catch (err) {
65187
65665
  log57.warn("Group chat agent reply failed", { agentId: agentId2, error: String(err) });
65188
65666
  if (this.storage) {
@@ -65202,6 +65680,56 @@ var init_api_server = __esm({
65202
65680
  }
65203
65681
  }
65204
65682
  }
65683
+ /**
65684
+ * Trigger agent-to-agent chain replies with 3-layer storm prevention:
65685
+ * 1. Depth limit (max 3 hops)
65686
+ * 2. Per-round dedup (each agent responds at most once per conversation round)
65687
+ * 3. Cooldown window (same agent won't be triggered twice within 30s on same channel)
65688
+ */
65689
+ async triggerAgentToAgentChain(mentionedNames, nameMap, fromAgentName, fromAgentId, fromText, fromMsgId, channel, orgId2, agentManager, teamSize, chainCtx) {
65690
+ if (chainCtx.depth > _APIServer.A2A_MAX_DEPTH) {
65691
+ log57.info("A2A chain depth limit reached", { channel, depth: chainCtx.depth, roundId: chainCtx.roundId });
65692
+ return;
65693
+ }
65694
+ const now2 = Date.now();
65695
+ if (!this.a2aCooldowns.has(channel)) {
65696
+ this.a2aCooldowns.set(channel, /* @__PURE__ */ new Map());
65697
+ }
65698
+ const channelCooldowns = this.a2aCooldowns.get(channel);
65699
+ let channelContext = [];
65700
+ if (this.storage) {
65701
+ try {
65702
+ const recent = await this.storage.channelMessageRepo.getMessages(channel, 20);
65703
+ channelContext = (recent.messages ?? []).map((m) => ({
65704
+ role: m.senderType === "agent" ? "assistant" : "user",
65705
+ content: m.senderType === "agent" ? stripInternalBlocks(m.text) : `[${m.senderName}]: ${m.text}`
65706
+ }));
65707
+ } catch {
65708
+ }
65709
+ }
65710
+ for (const targetName of mentionedNames) {
65711
+ const targetId = nameMap.get(targetName);
65712
+ if (!targetId)
65713
+ continue;
65714
+ if (chainCtx.respondedAgents.has(targetId)) {
65715
+ log57.info("A2A skipping (already responded this round)", { targetName, roundId: chainCtx.roundId });
65716
+ continue;
65717
+ }
65718
+ const lastReply = channelCooldowns.get(targetId) ?? 0;
65719
+ if (now2 - lastReply < _APIServer.A2A_COOLDOWN_MS) {
65720
+ log57.info("A2A skipping (cooldown)", { targetName, channel, cooldownRemaining: _APIServer.A2A_COOLDOWN_MS - (now2 - lastReply) });
65721
+ continue;
65722
+ }
65723
+ channelCooldowns.set(targetId, now2);
65724
+ const a2aMessage = `[From @${fromAgentName}]: ${fromText}`;
65725
+ void this.processGroupChatReply(targetId, a2aMessage, fromAgentId, { name: fromAgentName, role: "agent" }, channel, orgId2, channelContext, agentManager, teamSize, {
65726
+ mentionedNames: [targetName],
65727
+ replyToAgentName: fromAgentName,
65728
+ replyToText: fromText.slice(0, 200),
65729
+ replyToMsgId: fromMsgId
65730
+ }, chainCtx);
65731
+ }
65732
+ }
65205
65733
  /** Persist the user message first (before LLM), returns session id for subsequent assistant persistence.
65206
65734
  * When sessionId is provided, appends to that session; when null/undefined, creates a new session. */
65207
65735
  async persistUserMessage(agentId2, userMessage, senderId, images, sessionId) {
@@ -65457,7 +65985,7 @@ var init_api_server = __esm({
65457
65985
  const avatarDir = join23(homedir14(), ".markus", "avatars");
65458
65986
  mkdirSync19(avatarDir, { recursive: true });
65459
65987
  const filename = `${targetType}_${targetId}.${ext}`;
65460
- writeFileSync16(join23(avatarDir, filename), buf);
65988
+ writeFileSync17(join23(avatarDir, filename), buf);
65461
65989
  const avatarUrl = `/api/avatars/${filename}`;
65462
65990
  if (targetType === "user" && this.storage) {
65463
65991
  this.storage.userRepo.updateAvatarUrl(targetId, avatarUrl);
@@ -65534,6 +66062,7 @@ var init_api_server = __esm({
65534
66062
  const senderName = body["senderName"] ?? "You";
65535
66063
  const mentions = body["mentions"] ?? [];
65536
66064
  const targetAgentId = body["targetAgentId"];
66065
+ const replyToId = body["replyToId"];
65537
66066
  const orgId2 = body["orgId"] ?? "default";
65538
66067
  let userMsg;
65539
66068
  if (this.storage) {
@@ -65544,7 +66073,8 @@ var init_api_server = __esm({
65544
66073
  senderType: "human",
65545
66074
  senderName,
65546
66075
  text,
65547
- mentions
66076
+ mentions,
66077
+ replyToId
65548
66078
  });
65549
66079
  }
65550
66080
  const humanOnly = body["humanOnly"] === true;
@@ -65562,21 +66092,50 @@ var init_api_server = __esm({
65562
66092
  return [];
65563
66093
  }
65564
66094
  };
65565
- if (!isHumanChannel && channel.startsWith("group:") && !targetAgentId) {
65566
- const teamId = channel.replace(/^group:/, "");
65567
- const team = this.orgService.getTeam(teamId);
65568
- const allAgentIds = team?.memberAgentIds ?? [];
66095
+ let replyToAgentName;
66096
+ let replyToText;
66097
+ if (replyToId && this.storage) {
66098
+ try {
66099
+ const original = this.storage.channelMessageRepo.getMessageById(replyToId);
66100
+ if (original) {
66101
+ replyToAgentName = original.senderName;
66102
+ replyToText = original.text.slice(0, 200);
66103
+ }
66104
+ } catch {
66105
+ }
66106
+ }
66107
+ const enrichedUserMsg = userMsg ? {
66108
+ ...userMsg,
66109
+ ...replyToAgentName ? { replyToSender: replyToAgentName, replyToText } : {}
66110
+ } : null;
66111
+ if (!isHumanChannel && channel.startsWith("group:")) {
66112
+ let allAgentIds;
66113
+ if (channel.startsWith("group:custom:")) {
66114
+ allAgentIds = this.storage?.groupChatRepo?.getAgentMemberIds(channel) ?? [];
66115
+ } else {
66116
+ const teamId = channel.replace(/^group:/, "");
66117
+ const team = this.orgService.getTeam(teamId);
66118
+ allAgentIds = team?.memberAgentIds ?? [];
66119
+ }
65569
66120
  if (allAgentIds.length === 0) {
65570
- this.json(res, 200, { userMessage: userMsg ?? null, agentMessage: null });
66121
+ this.json(res, 200, { userMessage: enrichedUserMsg, agentMessage: null });
65571
66122
  return;
65572
66123
  }
65573
66124
  const channelContext2 = await buildChannelContext();
65574
66125
  const senderInfo2 = this.orgService.resolveHumanIdentity(senderId);
65575
66126
  const agentManager = this.orgService.getAgentManager();
66127
+ const broadcastOpts = {
66128
+ mentionedNames: mentions.length > 0 ? mentions : void 0,
66129
+ replyToAgentName,
66130
+ replyToText
66131
+ };
66132
+ const roundId = `round_${Date.now()}`;
66133
+ const respondedAgents = /* @__PURE__ */ new Set();
66134
+ const chainCtx = { roundId, depth: 0, respondedAgents, allAgentIds };
65576
66135
  for (const agentId2 of allAgentIds) {
65577
- void this.processGroupChatReply(agentId2, text, senderId, senderInfo2, channel, orgId2, channelContext2, agentManager, allAgentIds.length);
66136
+ void this.processGroupChatReply(agentId2, text, senderId, senderInfo2, channel, orgId2, channelContext2, agentManager, allAgentIds.length, broadcastOpts, chainCtx);
65578
66137
  }
65579
- this.json(res, 200, { userMessage: userMsg ?? null, agentMessage: null });
66138
+ this.json(res, 200, { userMessage: enrichedUserMsg, agentMessage: null });
65580
66139
  return;
65581
66140
  }
65582
66141
  let routedAgentId = null;
@@ -65590,7 +66149,7 @@ var init_api_server = __esm({
65590
66149
  }
65591
66150
  }
65592
66151
  if (!routedAgentId) {
65593
- this.json(res, 200, { userMessage: userMsg ?? null, agentMessage: null });
66152
+ this.json(res, 200, { userMessage: enrichedUserMsg, agentMessage: null });
65594
66153
  return;
65595
66154
  }
65596
66155
  const agent = this.orgService.getAgentManager().getAgent(routedAgentId);
@@ -65633,7 +66192,7 @@ var init_api_server = __esm({
65633
66192
  }
65634
66193
  const statusCode = raw.includes("402") ? 402 : raw.includes("401") ? 401 : raw.includes("429") ? 429 : 502;
65635
66194
  this.json(res, statusCode, {
65636
- userMessage: userMsg ?? null,
66195
+ userMessage: enrichedUserMsg,
65637
66196
  agentMessage: errorMsg ?? null,
65638
66197
  error: detail2
65639
66198
  });
@@ -65660,7 +66219,7 @@ var init_api_server = __esm({
65660
66219
  void this.persistChatTurn(routedAgentId, text, reply, senderId);
65661
66220
  }
65662
66221
  this.json(res, 200, {
65663
- userMessage: userMsg ?? null,
66222
+ userMessage: enrichedUserMsg,
65664
66223
  agentMessage: agentMsg ?? {
65665
66224
  id: `tmp_${Date.now()}`,
65666
66225
  channel,
@@ -65705,13 +66264,9 @@ var init_api_server = __esm({
65705
66264
  this.json(res, 400, { error: "name is required" });
65706
66265
  return;
65707
66266
  }
65708
- if (!roleName?.trim()) {
65709
- this.json(res, 400, { error: "roleName is required" });
65710
- return;
65711
- }
65712
66267
  const agent = await this.orgService.hireAgent({
65713
66268
  name: agentName,
65714
- roleName,
66269
+ roleName: roleName?.trim() || void 0,
65715
66270
  orgId: body["orgId"] ?? "default",
65716
66271
  teamId: body["teamId"],
65717
66272
  skills: body["skills"],
@@ -65864,7 +66419,7 @@ var init_api_server = __esm({
65864
66419
  if (path === "/api/group-chats" && req.method === "GET") {
65865
66420
  const orgId2 = url.searchParams.get("orgId") ?? "default";
65866
66421
  const teams = this.orgService.listTeamsWithMembers(orgId2);
65867
- const groupChats = teams.map((t) => ({
66422
+ const teamChats = teams.map((t) => ({
65868
66423
  id: `group:${t.id}`,
65869
66424
  name: t.name,
65870
66425
  type: "team",
@@ -65872,21 +66427,16 @@ var init_api_server = __esm({
65872
66427
  memberCount: t.members.length,
65873
66428
  channelKey: `group:${t.id}`
65874
66429
  }));
65875
- const customChats = this.customGroupChats.filter((c) => c.orgId === orgId2);
65876
- this.json(res, 200, {
65877
- chats: [
65878
- ...groupChats,
65879
- ...customChats.map((c) => ({
65880
- id: c.id,
65881
- name: c.name,
65882
- type: "custom",
65883
- creatorId: c.creatorId,
65884
- creatorName: c.creatorName,
65885
- memberCount: c.memberIds?.length ?? 0,
65886
- channelKey: c.id
65887
- }))
65888
- ]
65889
- });
66430
+ const customChats = this.storage?.groupChatRepo ? this.storage.groupChatRepo.list(orgId2).map((c) => ({
66431
+ id: c.id,
66432
+ name: c.name,
66433
+ type: "custom",
66434
+ creatorId: c.creatorId,
66435
+ creatorName: c.creatorName,
66436
+ memberCount: c.memberCount,
66437
+ channelKey: c.channelKey
66438
+ })) : [];
66439
+ this.json(res, 200, { chats: [...teamChats, ...customChats] });
65890
66440
  return;
65891
66441
  }
65892
66442
  if (path === "/api/group-chats" && req.method === "POST") {
@@ -65896,31 +66446,129 @@ var init_api_server = __esm({
65896
66446
  const creatorId = body["creatorId"];
65897
66447
  const creatorName = body["creatorName"];
65898
66448
  const memberIds = body["memberIds"];
66449
+ const memberTypes = body["memberTypes"];
66450
+ const memberNames = body["memberNames"];
65899
66451
  if (!name) {
65900
66452
  this.json(res, 400, { error: "name is required" });
65901
66453
  return;
65902
66454
  }
65903
- const chatId = `group:custom:${Date.now().toString(36)}`;
65904
- const chat = {
65905
- id: chatId,
65906
- name,
65907
- orgId: orgId2,
65908
- creatorId,
65909
- creatorName,
65910
- memberIds: memberIds ?? [],
65911
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
65912
- };
65913
- this.customGroupChats.push(chat);
66455
+ if (!this.storage?.groupChatRepo) {
66456
+ this.json(res, 500, { error: "Storage not initialized" });
66457
+ return;
66458
+ }
66459
+ const members = (memberIds ?? []).map((id) => {
66460
+ const mType = memberTypes?.[id] ?? "agent";
66461
+ let mName = memberNames?.[id] ?? id;
66462
+ if (mType === "agent") {
66463
+ try {
66464
+ mName = this.orgService.getAgentManager().getAgent(id)?.config?.name ?? mName;
66465
+ } catch {
66466
+ }
66467
+ }
66468
+ return { id, type: mType, name: mName };
66469
+ });
66470
+ const gc = this.storage.groupChatRepo.create({ orgId: orgId2, name, creatorId, creatorName, members });
65914
66471
  this.ws?.broadcast({
65915
66472
  type: "chat:group_created",
65916
- payload: { chatId, name, creatorId, creatorName },
66473
+ payload: { chatId: gc.channelKey, name, creatorId, creatorName },
65917
66474
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
65918
66475
  });
65919
66476
  this.json(res, 201, {
65920
- chat: { id: chatId, name, type: "custom", creatorId, creatorName, channelKey: chatId }
66477
+ chat: {
66478
+ id: gc.id,
66479
+ name: gc.name,
66480
+ type: "custom",
66481
+ creatorId: gc.creatorId,
66482
+ creatorName: gc.creatorName,
66483
+ channelKey: gc.channelKey,
66484
+ memberCount: gc.members.length,
66485
+ members: gc.members.map((m) => ({ id: m.memberId, name: m.memberName, type: m.memberType }))
66486
+ }
66487
+ });
66488
+ return;
66489
+ }
66490
+ if (path.match(/^\/api\/group-chats\/[^/]+$/) && req.method === "GET") {
66491
+ const gcId = path.split("/").pop();
66492
+ if (!this.storage?.groupChatRepo) {
66493
+ this.json(res, 500, { error: "Storage not initialized" });
66494
+ return;
66495
+ }
66496
+ const gc = this.storage.groupChatRepo.getById(gcId);
66497
+ if (!gc) {
66498
+ this.json(res, 404, { error: "Group chat not found" });
66499
+ return;
66500
+ }
66501
+ this.json(res, 200, {
66502
+ chat: {
66503
+ id: gc.id,
66504
+ name: gc.name,
66505
+ type: "custom",
66506
+ channelKey: gc.channelKey,
66507
+ creatorId: gc.creatorId,
66508
+ creatorName: gc.creatorName,
66509
+ members: gc.members.map((m) => ({ id: m.memberId, name: m.memberName, type: m.memberType }))
66510
+ }
65921
66511
  });
65922
66512
  return;
65923
66513
  }
66514
+ if (path.match(/^\/api\/group-chats\/[^/]+$/) && req.method === "PATCH") {
66515
+ const gcId = path.split("/").pop();
66516
+ if (!this.storage?.groupChatRepo) {
66517
+ this.json(res, 500, { error: "Storage not initialized" });
66518
+ return;
66519
+ }
66520
+ const body = await this.readBody(req);
66521
+ const newName = body["name"];
66522
+ if (newName)
66523
+ this.storage.groupChatRepo.updateName(gcId, newName);
66524
+ this.json(res, 200, { ok: true });
66525
+ return;
66526
+ }
66527
+ if (path.match(/^\/api\/group-chats\/[^/]+$/) && req.method === "DELETE") {
66528
+ const gcId = path.split("/").pop();
66529
+ if (!this.storage?.groupChatRepo) {
66530
+ this.json(res, 500, { error: "Storage not initialized" });
66531
+ return;
66532
+ }
66533
+ this.storage.groupChatRepo.delete(gcId);
66534
+ this.ws?.broadcast({ type: "chat:group_deleted", payload: { chatId: gcId }, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
66535
+ this.json(res, 200, { ok: true });
66536
+ return;
66537
+ }
66538
+ if (path.match(/^\/api\/group-chats\/[^/]+\/members$/) && req.method === "POST") {
66539
+ const gcId = path.split("/")[3];
66540
+ if (!this.storage?.groupChatRepo) {
66541
+ this.json(res, 500, { error: "Storage not initialized" });
66542
+ return;
66543
+ }
66544
+ const body = await this.readBody(req);
66545
+ const memberId = body["memberId"];
66546
+ const memberType = body["memberType"] ?? "agent";
66547
+ let memberName = body["memberName"] ?? memberId;
66548
+ if (memberType === "agent") {
66549
+ try {
66550
+ memberName = this.orgService.getAgentManager().getAgent(memberId)?.config?.name ?? memberName;
66551
+ } catch {
66552
+ }
66553
+ }
66554
+ this.storage.groupChatRepo.addMember(gcId, memberId, memberType, memberName);
66555
+ this.ws?.broadcast({ type: "chat:group_updated", payload: { chatId: gcId }, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
66556
+ this.json(res, 200, { ok: true });
66557
+ return;
66558
+ }
66559
+ if (path.match(/^\/api\/group-chats\/[^/]+\/members\/[^/]+$/) && req.method === "DELETE") {
66560
+ const parts = path.split("/");
66561
+ const gcId = parts[3];
66562
+ const memberId = parts[5];
66563
+ if (!this.storage?.groupChatRepo) {
66564
+ this.json(res, 500, { error: "Storage not initialized" });
66565
+ return;
66566
+ }
66567
+ this.storage.groupChatRepo.removeMember(gcId, memberId);
66568
+ this.ws?.broadcast({ type: "chat:group_updated", payload: { chatId: gcId }, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
66569
+ this.json(res, 200, { ok: true });
66570
+ return;
66571
+ }
65924
66572
  if (path === "/api/teams" && req.method === "GET") {
65925
66573
  const orgId2 = url.searchParams.get("orgId") ?? "default";
65926
66574
  const teams = this.orgService.listTeamsWithMembers(orgId2);
@@ -66155,7 +66803,7 @@ var init_api_server = __esm({
66155
66803
  const content = body["content"];
66156
66804
  const dir = this.orgService.getTeamDataDir(teamId);
66157
66805
  mkdirSync19(dir, { recursive: true });
66158
- writeFileSync16(join23(dir, filename), content ?? "", "utf-8");
66806
+ writeFileSync17(join23(dir, filename), content ?? "", "utf-8");
66159
66807
  this.json(res, 200, { ok: true });
66160
66808
  return;
66161
66809
  }
@@ -66685,7 +67333,7 @@ var init_api_server = __esm({
66685
67333
  const roleDir = this.resolveAgentRoleDir(agent);
66686
67334
  if (!roleDir)
66687
67335
  continue;
66688
- const slug = agent.config.name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "") || agentId2;
67336
+ const slug = kebab(agent.config.name, agentId2);
66689
67337
  for (const fname of roleFileNames) {
66690
67338
  const fpath = join23(roleDir, fname);
66691
67339
  if (existsSync27(fpath)) {
@@ -67000,7 +67648,7 @@ var init_api_server = __esm({
67000
67648
  }
67001
67649
  const body = await this.readBody(req);
67002
67650
  const content = body["content"] ?? "";
67003
- writeFileSync16(join23(roleDir, filename), content, "utf-8");
67651
+ writeFileSync17(join23(roleDir, filename), content, "utf-8");
67004
67652
  this.json(res, 200, { ok: true });
67005
67653
  } catch {
67006
67654
  this.json(res, 404, { error: `Agent not found: ${agentId2}` });
@@ -68339,7 +68987,7 @@ EXPLANATION_END`;
68339
68987
  const mfName = manifestFilename(pkgType);
68340
68988
  const artDir = join23(homedir14(), ".markus", "builder-artifacts", typeDir, manifest.name);
68341
68989
  mkdirSync19(artDir, { recursive: true });
68342
- writeFileSync16(join23(artDir, mfName), JSON.stringify(manifest, null, 2), "utf-8");
68990
+ writeFileSync17(join23(artDir, mfName), JSON.stringify(manifest, null, 2), "utf-8");
68343
68991
  const artFiles = artifact.files;
68344
68992
  if (artFiles) {
68345
68993
  for (const [fn, c] of Object.entries(artFiles)) {
@@ -68347,38 +68995,38 @@ EXPLANATION_END`;
68347
68995
  continue;
68348
68996
  const filePath = join23(artDir, fn);
68349
68997
  mkdirSync19(dirname6(filePath), { recursive: true });
68350
- writeFileSync16(filePath, c, "utf-8");
68998
+ writeFileSync17(filePath, c, "utf-8");
68351
68999
  }
68352
69000
  }
68353
69001
  if (mode === "team") {
68354
69002
  const fileSet = new Set(artFiles ? Object.keys(artFiles) : []);
68355
69003
  const announcement = artifact.announcement;
68356
69004
  if (announcement && !fileSet.has("ANNOUNCEMENT.md")) {
68357
- writeFileSync16(join23(artDir, "ANNOUNCEMENT.md"), announcement, "utf-8");
69005
+ writeFileSync17(join23(artDir, "ANNOUNCEMENT.md"), announcement, "utf-8");
68358
69006
  }
68359
69007
  const norms = artifact.norms;
68360
69008
  if (norms && !fileSet.has("NORMS.md")) {
68361
- writeFileSync16(join23(artDir, "NORMS.md"), norms, "utf-8");
69009
+ writeFileSync17(join23(artDir, "NORMS.md"), norms, "utf-8");
68362
69010
  }
68363
69011
  const rawMembers = Array.isArray(artifact.team?.members) ? artifact.team.members : Array.isArray(artifact.members) ? artifact.members : [];
68364
69012
  for (const m of rawMembers) {
68365
69013
  const mName = m.name ?? "Agent";
68366
- const slug = mName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "") || "agent";
69014
+ const slug = kebab(mName, "agent");
68367
69015
  const memberDir = join23(artDir, "members", slug);
68368
69016
  const roleContent = m.roleContent || m.role_md;
68369
69017
  const policiesContent = m.policiesContent || m.policies_md;
68370
69018
  const contextContent = m.contextContent || m.context_md;
68371
69019
  if (roleContent && !fileSet.has(`members/${slug}/ROLE.md`)) {
68372
69020
  mkdirSync19(memberDir, { recursive: true });
68373
- writeFileSync16(join23(memberDir, "ROLE.md"), roleContent, "utf-8");
69021
+ writeFileSync17(join23(memberDir, "ROLE.md"), roleContent, "utf-8");
68374
69022
  }
68375
69023
  if (policiesContent && !fileSet.has(`members/${slug}/POLICIES.md`)) {
68376
69024
  mkdirSync19(memberDir, { recursive: true });
68377
- writeFileSync16(join23(memberDir, "POLICIES.md"), policiesContent, "utf-8");
69025
+ writeFileSync17(join23(memberDir, "POLICIES.md"), policiesContent, "utf-8");
68378
69026
  }
68379
69027
  if (contextContent && !fileSet.has(`members/${slug}/CONTEXT.md`)) {
68380
69028
  mkdirSync19(memberDir, { recursive: true });
68381
- writeFileSync16(join23(memberDir, "CONTEXT.md"), contextContent, "utf-8");
69029
+ writeFileSync17(join23(memberDir, "CONTEXT.md"), contextContent, "utf-8");
68382
69030
  }
68383
69031
  }
68384
69032
  const memberFiles = artifact.memberFiles;
@@ -68387,7 +69035,7 @@ EXPLANATION_END`;
68387
69035
  const memberDir = join23(artDir, "members", slug);
68388
69036
  mkdirSync19(memberDir, { recursive: true });
68389
69037
  for (const [fn, c] of Object.entries(files))
68390
- writeFileSync16(join23(memberDir, fn), c, "utf-8");
69038
+ writeFileSync17(join23(memberDir, fn), c, "utf-8");
68391
69039
  }
68392
69040
  }
68393
69041
  }
@@ -68425,7 +69073,7 @@ EXPLANATION_END`;
68425
69073
  for (const [fn, content] of Object.entries(files)) {
68426
69074
  const filePath = join23(artDir, fn);
68427
69075
  mkdirSync19(dirname6(filePath), { recursive: true });
68428
- writeFileSync16(filePath, content, "utf-8");
69076
+ writeFileSync17(filePath, content, "utf-8");
68429
69077
  }
68430
69078
  if (source) {
68431
69079
  const mfName = manifestFilename(type);
@@ -68434,7 +69082,7 @@ EXPLANATION_END`;
68434
69082
  try {
68435
69083
  const mf = JSON.parse(readFileSync21(mfPath, "utf-8"));
68436
69084
  mf.source = source;
68437
- writeFileSync16(mfPath, JSON.stringify(mf, null, 2), "utf-8");
69085
+ writeFileSync17(mfPath, JSON.stringify(mf, null, 2), "utf-8");
68438
69086
  } catch {
68439
69087
  }
68440
69088
  }
@@ -69168,11 +69816,18 @@ EXPLANATION_END`;
69168
69816
  "Content-Type": "application/json",
69169
69817
  "Authorization": `Bearer ${hubToken}`
69170
69818
  };
69171
- const hubRes = await fetch(`${hubUrl}/api/items`, {
69819
+ let hubRes = await fetch(`${hubUrl}/api/items`, {
69172
69820
  method: "POST",
69173
69821
  headers,
69174
- body: JSON.stringify(body["payload"])
69822
+ body: JSON.stringify(body["payload"]),
69823
+ redirect: "manual"
69175
69824
  });
69825
+ if (hubRes.status >= 300 && hubRes.status < 400) {
69826
+ const location = hubRes.headers.get("location");
69827
+ if (location) {
69828
+ hubRes = await fetch(location, { method: "POST", headers, body: JSON.stringify(body["payload"]), redirect: "manual" });
69829
+ }
69830
+ }
69176
69831
  const hubData = await hubRes.json();
69177
69832
  this.json(res, hubRes.status, hubData);
69178
69833
  } catch (err) {
@@ -69180,6 +69835,43 @@ EXPLANATION_END`;
69180
69835
  }
69181
69836
  return;
69182
69837
  }
69838
+ if (path.startsWith("/api/hub/")) {
69839
+ const hubPath = path.slice("/api/hub".length);
69840
+ const reqUrl = new URL(req.url, `http://${req.headers.host}`);
69841
+ const hubTargetUrl = `${this.hubUrl}/api${hubPath}${reqUrl.search}`;
69842
+ const proxyHeaders = { "Content-Type": "application/json" };
69843
+ const authHeader = req.headers["authorization"];
69844
+ if (authHeader)
69845
+ proxyHeaders["Authorization"] = authHeader;
69846
+ try {
69847
+ let body;
69848
+ if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
69849
+ body = JSON.stringify(await this.readBody(req));
69850
+ }
69851
+ let hubRes = await fetch(hubTargetUrl, {
69852
+ method: req.method,
69853
+ headers: proxyHeaders,
69854
+ body,
69855
+ redirect: "manual"
69856
+ });
69857
+ if (hubRes.status >= 300 && hubRes.status < 400) {
69858
+ const location = hubRes.headers.get("location");
69859
+ if (location) {
69860
+ hubRes = await fetch(location, {
69861
+ method: req.method,
69862
+ headers: proxyHeaders,
69863
+ body,
69864
+ redirect: "manual"
69865
+ });
69866
+ }
69867
+ }
69868
+ const data = await hubRes.json();
69869
+ this.json(res, hubRes.status, data);
69870
+ } catch (err) {
69871
+ this.json(res, 502, { error: `Hub request failed: ${String(err)}` });
69872
+ }
69873
+ return;
69874
+ }
69183
69875
  if (path === "/api/settings/hub" && req.method === "GET") {
69184
69876
  this.json(res, 200, { hubUrl: this.hubUrl });
69185
69877
  return;
@@ -69191,7 +69883,7 @@ EXPLANATION_END`;
69191
69883
  try {
69192
69884
  if (token) {
69193
69885
  mkdirSync19(join23(homedir14(), ".markus"), { recursive: true });
69194
- writeFileSync16(tokenPath, token, "utf-8");
69886
+ writeFileSync17(tokenPath, token, "utf-8");
69195
69887
  } else if (existsSync27(tokenPath)) {
69196
69888
  rmSync3(tokenPath);
69197
69889
  }
@@ -72483,11 +73175,13 @@ var init_requirement_service = __esm({
72483
73175
  req.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
72484
73176
  }
72485
73177
  /**
72486
- * Check if all tasks for a requirement are done. If so, mark it as completed.
73178
+ * Check if all tasks for a requirement are done. Instead of auto-completing,
73179
+ * notify the creator agent so they can review results and decide whether to
73180
+ * complete the requirement or create additional tasks.
72487
73181
  */
72488
73182
  checkCompletion(requirementId2, taskStatuses) {
72489
73183
  const req = this.requirements.get(requirementId2);
72490
- if (!req || req.status === "completed")
73184
+ if (!req || req.status === "completed" || req.status === "cancelled")
72491
73185
  return false;
72492
73186
  if (req.taskIds.length === 0)
72493
73187
  return false;
@@ -72495,14 +73189,28 @@ var init_requirement_service = __esm({
72495
73189
  const status = taskStatuses.get(tid);
72496
73190
  return status === "completed" || status === "cancelled" || status === "archived";
72497
73191
  });
72498
- if (allDone) {
72499
- req.status = "completed";
72500
- req.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
72501
- if (this.requirementRepo) {
72502
- this.requirementRepo.updateStatus(requirementId2, "completed").catch((e) => log62.error("Failed to persist requirement completion", { id: requirementId2, error: String(e) }));
73192
+ if (allDone && req.createdBy && this.agentManager) {
73193
+ try {
73194
+ const agent = this.agentManager.getAgent(req.createdBy);
73195
+ if (agent) {
73196
+ agent.enqueueToMailbox("requirement_update", {
73197
+ summary: `All tasks for requirement "${req.title}" are done \u2014 review needed`,
73198
+ content: [
73199
+ `[REQUIREMENT REVIEW NEEDED] All linked tasks for "${req.title}" (ID: ${requirementId2}) have reached terminal state.`,
73200
+ "",
73201
+ "Please review the results and decide:",
73202
+ `1. If the requirement is fully satisfied, complete it: requirement_update_status(requirement_id="${requirementId2}", status="completed")`,
73203
+ `2. If more work is needed, create additional tasks with task_create(requirement_id="${requirementId2}", ...)`
73204
+ ].join("\n"),
73205
+ requirementId: requirementId2,
73206
+ extra: { actionRequired: true }
73207
+ }, {
73208
+ priority: 2,
73209
+ metadata: { senderName: "System", senderRole: "manager" }
73210
+ });
73211
+ }
73212
+ } catch {
72503
73213
  }
72504
- this.broadcast("requirement:completed", req);
72505
- log62.info("Requirement completed", { id: requirementId2 });
72506
73214
  return true;
72507
73215
  }
72508
73216
  return false;
@@ -72646,14 +73354,13 @@ var init_requirement_service = __esm({
72646
73354
  parts.push("2. Abandon this requirement if the feedback indicates it is not needed");
72647
73355
  }
72648
73356
  const agent = this.agentManager.getAgent(creatorId);
72649
- const isRejection = decision === "rejected";
72650
73357
  agent.enqueueToMailbox("requirement_update", {
72651
73358
  summary: `Requirement "${req.title}" ${decision}`,
72652
73359
  content: parts.join("\n"),
72653
73360
  requirementId: req.id,
72654
- extra: isRejection ? { actionRequired: true } : void 0
73361
+ extra: { actionRequired: true }
72655
73362
  }, {
72656
- priority: isRejection ? 1 : 3,
73363
+ priority: 1,
72657
73364
  metadata: { senderName: "System", senderRole: "manager" }
72658
73365
  });
72659
73366
  log62.info("Notified creator agent about requirement decision", {
@@ -72871,7 +73578,7 @@ var init_knowledge_service = __esm({
72871
73578
  });
72872
73579
 
72873
73580
  // ../org-manager/dist/file-knowledge-store.js
72874
- import { readFileSync as readFileSync22, writeFileSync as writeFileSync17, existsSync as existsSync28, mkdirSync as mkdirSync20, readdirSync as readdirSync11, unlinkSync as unlinkSync3 } from "node:fs";
73581
+ import { readFileSync as readFileSync22, writeFileSync as writeFileSync18, existsSync as existsSync28, mkdirSync as mkdirSync20, readdirSync as readdirSync11, unlinkSync as unlinkSync3 } from "node:fs";
72875
73582
  import { join as join24 } from "node:path";
72876
73583
  function readdirSafe(dir) {
72877
73584
  try {
@@ -72927,12 +73634,12 @@ var init_file_knowledge_store = __esm({
72927
73634
  saveEntry(entry) {
72928
73635
  const dir = this.scopeDir(entry.scope, entry.scopeId);
72929
73636
  mkdirSync20(dir, { recursive: true });
72930
- writeFileSync17(join24(dir, `${entry.id}.json`), JSON.stringify(entry, null, 2));
73637
+ writeFileSync18(join24(dir, `${entry.id}.json`), JSON.stringify(entry, null, 2));
72931
73638
  }
72932
73639
  saveIndex(scope, scopeId, entries2) {
72933
73640
  const dir = this.scopeDir(scope, scopeId);
72934
73641
  mkdirSync20(dir, { recursive: true });
72935
- writeFileSync17(join24(dir, "_index.json"), JSON.stringify(entries2, null, 2));
73642
+ writeFileSync18(join24(dir, "_index.json"), JSON.stringify(entries2, null, 2));
72936
73643
  }
72937
73644
  removeEntryFile(entry) {
72938
73645
  const p = join24(this.scopeDir(entry.scope, entry.scopeId), `${entry.id}.json`);
@@ -73970,7 +74677,8 @@ function openSqlite(dbPath) {
73970
74677
  { table: "chat_sessions", column: "is_main", sql: "ALTER TABLE chat_sessions ADD COLUMN is_main INTEGER NOT NULL DEFAULT 0" },
73971
74678
  { table: "mailbox_items", column: "retry_count", sql: "ALTER TABLE mailbox_items ADD COLUMN retry_count INTEGER NOT NULL DEFAULT 0" },
73972
74679
  { table: "users", column: "avatar_url", sql: "ALTER TABLE users ADD COLUMN avatar_url TEXT" },
73973
- { table: "agents", column: "avatar_url", sql: "ALTER TABLE agents ADD COLUMN avatar_url TEXT" }
74680
+ { table: "agents", column: "avatar_url", sql: "ALTER TABLE agents ADD COLUMN avatar_url TEXT" },
74681
+ { table: "channel_messages", column: "reply_to_id", sql: "ALTER TABLE channel_messages ADD COLUMN reply_to_id TEXT" }
73974
74682
  ];
73975
74683
  for (const m of migrations) {
73976
74684
  const cols = _db.prepare(`PRAGMA table_info(${m.table})`).all();
@@ -74027,7 +74735,7 @@ function migrateToExecutionStreamLogs(db) {
74027
74735
  log71.info(`Migration: copied ${actLogCount} agent_activity_logs to execution_stream_logs`);
74028
74736
  }
74029
74737
  }
74030
- var log71, SCHEMA_SQL, _db, SqliteOrgRepo, SqliteAgentRepo, SqliteTaskRepo, SqliteRequirementRepo, SqliteProjectRepo, SqliteTaskLogRepo, SqliteTaskCommentRepo, SqliteRequirementCommentRepo, SqliteMessageRepo, SqliteChatSessionRepo, SqliteChannelMessageRepo, SqliteUserRepo, SqliteTeamRepo, SqliteMarketplaceTemplateRepo, SqliteMarketplaceSkillRepo, SqliteMarketplaceRatingRepo, SqliteAgentKnowledgeRepo, SqliteExternalAgentRepo, SqliteDeliverableRepo, SqliteActivityRepo, SqliteExecutionStreamRepo, SqliteMailboxRepo, SqliteDecisionRepo, SqliteNotificationRepo, SqliteApprovalRepo;
74738
+ var log71, SCHEMA_SQL, _db, SqliteOrgRepo, SqliteAgentRepo, SqliteTaskRepo, SqliteRequirementRepo, SqliteProjectRepo, SqliteTaskLogRepo, SqliteTaskCommentRepo, SqliteRequirementCommentRepo, SqliteMessageRepo, SqliteChatSessionRepo, SqliteChannelMessageRepo, SqliteUserRepo, SqliteTeamRepo, SqliteMarketplaceTemplateRepo, SqliteMarketplaceSkillRepo, SqliteMarketplaceRatingRepo, SqliteAgentKnowledgeRepo, SqliteExternalAgentRepo, SqliteDeliverableRepo, SqliteActivityRepo, SqliteExecutionStreamRepo, SqliteMailboxRepo, SqliteDecisionRepo, SqliteNotificationRepo, SqliteApprovalRepo, SqliteGroupChatRepo;
74031
74739
  var init_sqlite_storage = __esm({
74032
74740
  "../storage/dist/sqlite-storage.js"() {
74033
74741
  "use strict";
@@ -74165,6 +74873,7 @@ CREATE TABLE IF NOT EXISTS channel_messages (
74165
74873
  text TEXT NOT NULL,
74166
74874
  mentions TEXT NOT NULL DEFAULT '[]',
74167
74875
  metadata TEXT DEFAULT '{}',
74876
+ reply_to_id TEXT,
74168
74877
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
74169
74878
  );
74170
74879
  CREATE INDEX IF NOT EXISTS idx_channel_messages_channel ON channel_messages(channel, created_at);
@@ -74525,6 +75234,26 @@ CREATE TABLE IF NOT EXISTS approvals (
74525
75234
  selected_option TEXT
74526
75235
  );
74527
75236
  CREATE INDEX IF NOT EXISTS idx_approvals_status ON approvals(status, requested_at DESC);
75237
+
75238
+ CREATE TABLE IF NOT EXISTS group_chats (
75239
+ id TEXT PRIMARY KEY,
75240
+ org_id TEXT NOT NULL DEFAULT 'default',
75241
+ name TEXT NOT NULL,
75242
+ channel_key TEXT NOT NULL UNIQUE,
75243
+ creator_id TEXT NOT NULL,
75244
+ creator_name TEXT NOT NULL,
75245
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
75246
+ );
75247
+
75248
+ CREATE TABLE IF NOT EXISTS group_chat_members (
75249
+ group_chat_id TEXT NOT NULL REFERENCES group_chats(id) ON DELETE CASCADE,
75250
+ member_id TEXT NOT NULL,
75251
+ member_type TEXT NOT NULL CHECK(member_type IN ('human', 'agent')),
75252
+ member_name TEXT NOT NULL,
75253
+ added_at TEXT NOT NULL DEFAULT (datetime('now')),
75254
+ PRIMARY KEY (group_chat_id, member_id)
75255
+ );
75256
+ CREATE INDEX IF NOT EXISTS idx_group_chat_members_group ON group_chat_members(group_chat_id);
74528
75257
  `;
74529
75258
  _db = null;
74530
75259
  SqliteOrgRepo = class {
@@ -75393,7 +76122,7 @@ CREATE INDEX IF NOT EXISTS idx_approvals_status ON approvals(status, requested_a
75393
76122
  async append(data) {
75394
76123
  const id = generateId2("chm");
75395
76124
  const ts = now();
75396
- this.db.prepare("INSERT INTO channel_messages (id, org_id, channel, sender_id, sender_type, sender_name, text, mentions, metadata, created_at) VALUES (?,?,?,?,?,?,?,?,?,?)").run(id, data.orgId, data.channel, data.senderId, data.senderType, data.senderName, data.text, toJson(data.mentions ?? []), toJson(data.metadata ?? {}), ts);
76125
+ this.db.prepare("INSERT INTO channel_messages (id, org_id, channel, sender_id, sender_type, sender_name, text, mentions, metadata, reply_to_id, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)").run(id, data.orgId, data.channel, data.senderId, data.senderType, data.senderName, data.text, toJson(data.mentions ?? []), toJson(data.metadata ?? {}), data.replyToId ?? null, ts);
75397
76126
  return {
75398
76127
  id,
75399
76128
  orgId: data.orgId,
@@ -75404,17 +76133,20 @@ CREATE INDEX IF NOT EXISTS idx_approvals_status ON approvals(status, requested_a
75404
76133
  text: data.text,
75405
76134
  mentions: data.mentions ?? [],
75406
76135
  metadata: data.metadata ?? null,
76136
+ replyToId: data.replyToId,
75407
76137
  createdAt: new Date(ts)
75408
76138
  };
75409
76139
  }
75410
76140
  getMessages(channel, limit = 50, before2) {
75411
- let q = "SELECT * FROM channel_messages WHERE channel = ?";
76141
+ let q = `SELECT m.*, r.sender_name AS reply_to_sender, SUBSTR(r.text, 1, 120) AS reply_to_text
76142
+ FROM channel_messages m LEFT JOIN channel_messages r ON m.reply_to_id = r.id
76143
+ WHERE m.channel = ?`;
75412
76144
  const vals = [channel];
75413
76145
  if (before2) {
75414
- q += " AND created_at < ?";
76146
+ q += " AND m.created_at < ?";
75415
76147
  vals.push(before2);
75416
76148
  }
75417
- q += " ORDER BY created_at DESC LIMIT ?";
76149
+ q += " ORDER BY m.created_at DESC LIMIT ?";
75418
76150
  vals.push(limit + 1);
75419
76151
  const rows = this.db.prepare(q).all(...vals).map((r) => ({
75420
76152
  id: r["id"],
@@ -75426,11 +76158,26 @@ CREATE INDEX IF NOT EXISTS idx_approvals_status ON approvals(status, requested_a
75426
76158
  text: r["text"],
75427
76159
  mentions: fromJson(r["mentions"]),
75428
76160
  metadata: fromJson(r["metadata"]),
76161
+ replyToId: r["reply_to_id"] ?? void 0,
76162
+ replyToSender: r["reply_to_sender"] ?? void 0,
76163
+ replyToText: r["reply_to_text"] ?? void 0,
75429
76164
  createdAt: toDate(r["created_at"])
75430
76165
  }));
75431
76166
  const hasMore = rows.length > limit;
75432
76167
  return { messages: rows.slice(0, limit).reverse(), hasMore };
75433
76168
  }
76169
+ getMessageById(id) {
76170
+ const r = this.db.prepare("SELECT * FROM channel_messages WHERE id = ?").get(id);
76171
+ if (!r)
76172
+ return void 0;
76173
+ return {
76174
+ id: r["id"],
76175
+ senderName: r["sender_name"],
76176
+ senderType: r["sender_type"],
76177
+ senderId: r["sender_id"],
76178
+ text: r["text"]
76179
+ };
76180
+ }
75434
76181
  };
75435
76182
  SqliteUserRepo = class {
75436
76183
  db;
@@ -76548,6 +77295,96 @@ CREATE INDEX IF NOT EXISTS idx_approvals_status ON approvals(status, requested_a
76548
77295
  };
76549
77296
  }
76550
77297
  };
77298
+ SqliteGroupChatRepo = class {
77299
+ db;
77300
+ constructor(db) {
77301
+ this.db = db;
77302
+ }
77303
+ create(data) {
77304
+ const id = generateId2("gc");
77305
+ const channelKey = `group:custom:${id}`;
77306
+ const ts = now();
77307
+ this.db.prepare("INSERT INTO group_chats (id, org_id, name, channel_key, creator_id, creator_name, created_at) VALUES (?,?,?,?,?,?,?)").run(id, data.orgId, data.name, channelKey, data.creatorId, data.creatorName, ts);
77308
+ const insertMember = this.db.prepare("INSERT OR IGNORE INTO group_chat_members (group_chat_id, member_id, member_type, member_name, added_at) VALUES (?,?,?,?,?)");
77309
+ const members = [];
77310
+ for (const m of data.members) {
77311
+ insertMember.run(id, m.id, m.type, m.name, ts);
77312
+ members.push({ groupChatId: id, memberId: m.id, memberType: m.type, memberName: m.name, addedAt: new Date(ts) });
77313
+ }
77314
+ return {
77315
+ id,
77316
+ orgId: data.orgId,
77317
+ name: data.name,
77318
+ channelKey,
77319
+ creatorId: data.creatorId,
77320
+ creatorName: data.creatorName,
77321
+ createdAt: new Date(ts),
77322
+ members
77323
+ };
77324
+ }
77325
+ list(orgId2) {
77326
+ const rows = this.db.prepare(`SELECT g.*, (SELECT COUNT(*) FROM group_chat_members WHERE group_chat_id = g.id) AS member_count
77327
+ FROM group_chats g WHERE g.org_id = ? ORDER BY g.created_at DESC`).all(orgId2);
77328
+ return rows.map((r) => ({
77329
+ ...this.mapRow(r),
77330
+ memberCount: r["member_count"]
77331
+ }));
77332
+ }
77333
+ getById(id) {
77334
+ const r = this.db.prepare("SELECT * FROM group_chats WHERE id = ?").get(id);
77335
+ if (!r)
77336
+ return void 0;
77337
+ const members = this.getMembers(id);
77338
+ return { ...this.mapRow(r), members };
77339
+ }
77340
+ getByChannelKey(channelKey) {
77341
+ const r = this.db.prepare("SELECT * FROM group_chats WHERE channel_key = ?").get(channelKey);
77342
+ if (!r)
77343
+ return void 0;
77344
+ const gc = this.mapRow(r);
77345
+ return { ...gc, members: this.getMembers(gc.id) };
77346
+ }
77347
+ getMembers(groupChatId) {
77348
+ const rows = this.db.prepare("SELECT * FROM group_chat_members WHERE group_chat_id = ?").all(groupChatId);
77349
+ return rows.map((r) => ({
77350
+ groupChatId: r["group_chat_id"],
77351
+ memberId: r["member_id"],
77352
+ memberType: r["member_type"],
77353
+ memberName: r["member_name"],
77354
+ addedAt: toDate(r["added_at"])
77355
+ }));
77356
+ }
77357
+ getAgentMemberIds(channelKey) {
77358
+ const gc = this.db.prepare("SELECT id FROM group_chats WHERE channel_key = ?").get(channelKey);
77359
+ if (!gc)
77360
+ return [];
77361
+ const rows = this.db.prepare("SELECT member_id FROM group_chat_members WHERE group_chat_id = ? AND member_type = 'agent'").all(gc.id);
77362
+ return rows.map((r) => r.member_id);
77363
+ }
77364
+ addMember(groupChatId, memberId, memberType, memberName) {
77365
+ this.db.prepare("INSERT OR IGNORE INTO group_chat_members (group_chat_id, member_id, member_type, member_name, added_at) VALUES (?,?,?,?,?)").run(groupChatId, memberId, memberType, memberName, now());
77366
+ }
77367
+ removeMember(groupChatId, memberId) {
77368
+ this.db.prepare("DELETE FROM group_chat_members WHERE group_chat_id = ? AND member_id = ?").run(groupChatId, memberId);
77369
+ }
77370
+ updateName(id, name) {
77371
+ this.db.prepare("UPDATE group_chats SET name = ? WHERE id = ?").run(name, id);
77372
+ }
77373
+ delete(id) {
77374
+ this.db.prepare("DELETE FROM group_chats WHERE id = ?").run(id);
77375
+ }
77376
+ mapRow(r) {
77377
+ return {
77378
+ id: r["id"],
77379
+ orgId: r["org_id"],
77380
+ name: r["name"],
77381
+ channelKey: r["channel_key"],
77382
+ creatorId: r["creator_id"],
77383
+ creatorName: r["creator_name"],
77384
+ createdAt: toDate(r["created_at"])
77385
+ };
77386
+ }
77387
+ };
76551
77388
  }
76552
77389
  });
76553
77390
 
@@ -76564,6 +77401,7 @@ __export(dist_exports3, {
76564
77401
  SqliteDeliverableRepo: () => SqliteDeliverableRepo,
76565
77402
  SqliteExecutionStreamRepo: () => SqliteExecutionStreamRepo,
76566
77403
  SqliteExternalAgentRepo: () => SqliteExternalAgentRepo,
77404
+ SqliteGroupChatRepo: () => SqliteGroupChatRepo,
76567
77405
  SqliteMailboxRepo: () => SqliteMailboxRepo,
76568
77406
  SqliteMarketplaceRatingRepo: () => SqliteMarketplaceRatingRepo,
76569
77407
  SqliteMarketplaceSkillRepo: () => SqliteMarketplaceSkillRepo,
@@ -76633,7 +77471,8 @@ async function initSqliteStorage(url) {
76633
77471
  mailboxRepo: new storage.SqliteMailboxRepo(db),
76634
77472
  decisionRepo: new storage.SqliteDecisionRepo(db),
76635
77473
  notificationRepo: new storage.SqliteNotificationRepo(db),
76636
- approvalRepo: new storage.SqliteApprovalRepo(db)
77474
+ approvalRepo: new storage.SqliteApprovalRepo(db),
77475
+ groupChatRepo: new storage.SqliteGroupChatRepo(db)
76637
77476
  };
76638
77477
  log72.info("SQLite storage initialized", { path: dbPath });
76639
77478
  return bridge;
@@ -77725,7 +78564,7 @@ var init_startupProgress = __esm({
77725
78564
 
77726
78565
  // src/connector-service.ts
77727
78566
  import { resolve as resolve15, join as join28, dirname as dirname8 } from "node:path";
77728
- import { existsSync as existsSync31, readFileSync as readFileSync23, writeFileSync as writeFileSync18, mkdirSync as mkdirSync24, readdirSync as readdirSync12, cpSync as cpSync2 } from "node:fs";
78567
+ import { existsSync as existsSync31, readFileSync as readFileSync23, writeFileSync as writeFileSync19, mkdirSync as mkdirSync24, readdirSync as readdirSync12, cpSync as cpSync3 } from "node:fs";
77729
78568
  import { homedir as homedir18 } from "node:os";
77730
78569
  import { execSync as execSync4 } from "node:child_process";
77731
78570
  import { fileURLToPath as fileURLToPath4 } from "node:url";
@@ -77826,7 +78665,7 @@ function writePlatformConfig(connector, markusUrl, token) {
77826
78665
  setNestedField(config, connector.integration.tokenField, token);
77827
78666
  try {
77828
78667
  const content = JSON.stringify(config, null, 2);
77829
- writeFileSync18(configPath, content, "utf-8");
78668
+ writeFileSync19(configPath, content, "utf-8");
77830
78669
  return true;
77831
78670
  } catch {
77832
78671
  return false;
@@ -77856,7 +78695,7 @@ function installSkillTemplate(connector) {
77856
78695
  mkdirSync24(targetDir, { recursive: true });
77857
78696
  }
77858
78697
  try {
77859
- cpSync2(sourceDir, targetDir, { recursive: true });
78698
+ cpSync3(sourceDir, targetDir, { recursive: true });
77860
78699
  return true;
77861
78700
  } catch {
77862
78701
  return false;
@@ -77906,7 +78745,7 @@ __export(init_exports, {
77906
78745
  registerInitCommand: () => registerInitCommand
77907
78746
  });
77908
78747
  import { resolve as resolve16 } from "node:path";
77909
- import { readFileSync as readFileSync24, existsSync as existsSync32, cpSync as cpSync3 } from "node:fs";
78748
+ import { readFileSync as readFileSync24, existsSync as existsSync32, cpSync as cpSync4 } from "node:fs";
77910
78749
  import { homedir as homedir19 } from "node:os";
77911
78750
  function registerInitCommand(program2) {
77912
78751
  program2.command("init").description("Setup wizard: configure LLM provider, API keys, and server settings").option("--force", "Overwrite existing configuration").option("--non-interactive", "Run without prompts (use env vars or --import-from)").option("--provider <name>", "LLM provider (anthropic/openai/google/minimax/siliconflow/zai/ollama)").option("--api-key <key>", "LLM API key").option("--port <port>", "API server port", "8056").option("--import-from <platform>", "Import LLM config from an installed agent platform (e.g. openclaw, hermes)").option("--auto-connect", "Auto-connect detected agent platforms after init").action(async (opts) => {
@@ -77922,7 +78761,7 @@ function registerInitCommand(program2) {
77922
78761
  });
77923
78762
  }
77924
78763
  async function quickInit(options) {
77925
- const { writeFileSync: writeFileSync20, mkdirSync: mkdirSync26 } = await import("node:fs");
78764
+ const { writeFileSync: writeFileSync21, mkdirSync: mkdirSync26 } = await import("node:fs");
77926
78765
  const { join: pathJoin } = await import("node:path");
77927
78766
  const readline4 = await import("node:readline");
77928
78767
  const nonInteractive = options?.nonInteractive ?? false;
@@ -78195,13 +79034,13 @@ async function quickInit(options) {
78195
79034
  if (builtinTemplatesDir && existsSync32(builtinTemplatesDir) && !existsSync32(userTemplatesDir)) {
78196
79035
  const builtinRoot = resolve16(builtinTemplatesDir, "..");
78197
79036
  mkdirSync26(userTemplatesDir, { recursive: true });
78198
- cpSync3(builtinRoot, userTemplatesDir, { recursive: true });
79037
+ cpSync4(builtinRoot, userTemplatesDir, { recursive: true });
78199
79038
  console.log(` Copied templates to ${userTemplatesDir}`);
78200
79039
  }
78201
79040
  const devRoleDir = pathJoin(userTemplatesDir || pathJoin(process.cwd(), "templates"), "roles", "developer");
78202
79041
  if (!existsSync32(devRoleDir)) {
78203
79042
  mkdirSync26(devRoleDir, { recursive: true });
78204
- writeFileSync20(
79043
+ writeFileSync21(
78205
79044
  pathJoin(devRoleDir, "ROLE.md"),
78206
79045
  [
78207
79046
  "---",
@@ -79109,6 +79948,19 @@ ${reason}`;
79109
79948
  }, triageProvider);
79110
79949
  return response.content;
79111
79950
  });
79951
+ agent.getAttentionController().setLLMJudge(async (prompt) => {
79952
+ const response = await llmRouter.chat({
79953
+ messages: [
79954
+ { role: "system", content: 'You are an attention interrupt judge. Decide whether to interrupt current work for a new incoming message. Reply with ONLY one word: continue, preempt, cancel, merge, or defer. Use "preempt" to pause (resume later) and "cancel" to permanently stop current work.' },
79955
+ { role: "user", content: prompt }
79956
+ ],
79957
+ temperature: 0.1,
79958
+ maxTokens: 32
79959
+ }, triageProvider);
79960
+ const raw = response.content.trim().toLowerCase();
79961
+ const valid = ["continue", "preempt", "cancel", "merge", "defer"];
79962
+ return valid.includes(raw) ? raw : "continue";
79963
+ });
79112
79964
  agent.getAttentionController().setTriageChatFn(async (messages, tools) => {
79113
79965
  const llmTools = tools?.map((t) => ({
79114
79966
  name: t.name,
@@ -80135,6 +80987,12 @@ ${C3.BOLD}${C3.CYAN}\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
80135
80987
  checkWarn("Skills directory not found", `${skillsDir} \u2014 run: markus skill install <name>`);
80136
80988
  }
80137
80989
  section("Network");
80990
+ const proxyUrl = process.env["HTTPS_PROXY"] || process.env["HTTP_PROXY"] || process.env["https_proxy"] || process.env["http_proxy"];
80991
+ if (proxyUrl) {
80992
+ checkOk("HTTP proxy configured", proxyUrl);
80993
+ } else {
80994
+ checkWarn("HTTP proxy", "not configured \u2014 if behind firewall, set HTTPS_PROXY env var");
80995
+ }
80138
80996
  try {
80139
80997
  const res = await fetch("https://api.anthropic.com/.well-known/ready", {
80140
80998
  method: "HEAD",
@@ -82816,7 +83674,7 @@ var skill_exports = {};
82816
83674
  __export(skill_exports, {
82817
83675
  registerSkillCommands: () => registerSkillCommands
82818
83676
  });
82819
- import { writeFileSync as writeFileSync19, mkdirSync as mkdirSync25 } from "node:fs";
83677
+ import { writeFileSync as writeFileSync20, mkdirSync as mkdirSync25 } from "node:fs";
82820
83678
  import { resolve as resolve18 } from "node:path";
82821
83679
  function registerSkillCommands(program2) {
82822
83680
  const root = program2.command("skill").description("Skills registry and local scaffold");
@@ -82924,8 +83782,8 @@ Describe what this skill does for agents.
82924
83782
  ## Tools / behavior
82925
83783
 
82926
83784
  `;
82927
- writeFileSync19(resolve18(dir, "skill.json"), JSON.stringify(skillJson, null, 2) + "\n");
82928
- writeFileSync19(resolve18(dir, "SKILL.md"), skillMd);
83785
+ writeFileSync20(resolve18(dir, "skill.json"), JSON.stringify(skillJson, null, 2) + "\n");
83786
+ writeFileSync20(resolve18(dir, "SKILL.md"), skillMd);
82929
83787
  if (out.json) {
82930
83788
  console.log(JSON.stringify({ dir, files: ["skill.json", "SKILL.md"] }, null, 2));
82931
83789
  } else {