blun-king-cli 9.1.486 → 9.1.489

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 (99) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LIESMICH.txt +28 -7
  3. package/README.md +16 -3
  4. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -0
  5. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -0
  6. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -0
  7. package/agent-spine-plugin/.mcp.json +8 -0
  8. package/agent-spine-plugin/CHANGELOG.md +277 -0
  9. package/agent-spine-plugin/LICENSE +186 -0
  10. package/agent-spine-plugin/README.md +342 -0
  11. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -0
  12. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -0
  13. package/agent-spine-plugin/bin/agentspine.js +7 -0
  14. package/agent-spine-plugin/blun.plugin.json +33 -0
  15. package/agent-spine-plugin/docs/acceptance.md +61 -0
  16. package/agent-spine-plugin/docs/architecture.md +183 -0
  17. package/agent-spine-plugin/docs/attention.md +121 -0
  18. package/agent-spine-plugin/docs/automatic-continuity.md +79 -0
  19. package/agent-spine-plugin/docs/channel-runtime.md +92 -0
  20. package/agent-spine-plugin/docs/coordination.md +138 -0
  21. package/agent-spine-plugin/docs/feed-transport.md +99 -0
  22. package/agent-spine-plugin/docs/gateway-runtime.md +109 -0
  23. package/agent-spine-plugin/docs/harness-reference.md +45 -0
  24. package/agent-spine-plugin/docs/host-integration.md +129 -0
  25. package/agent-spine-plugin/docs/https-transport.md +116 -0
  26. package/agent-spine-plugin/docs/learning.md +98 -0
  27. package/agent-spine-plugin/docs/object-transport.md +93 -0
  28. package/agent-spine-plugin/docs/peer-transport.md +88 -0
  29. package/agent-spine-plugin/docs/preservation-contract.md +53 -0
  30. package/agent-spine-plugin/docs/quality-gates.md +50 -0
  31. package/agent-spine-plugin/docs/relationships.md +67 -0
  32. package/agent-spine-plugin/docs/releasing.md +83 -0
  33. package/agent-spine-plugin/docs/roadmap.md +307 -0
  34. package/agent-spine-plugin/docs/selfstarter.md +88 -0
  35. package/agent-spine-plugin/docs/session-briefing.md +74 -0
  36. package/agent-spine-plugin/docs/shared-memory.md +259 -0
  37. package/agent-spine-plugin/docs/source-roots.md +86 -0
  38. package/agent-spine-plugin/docs/sqlite-transport.md +76 -0
  39. package/agent-spine-plugin/hooks/hooks.json +95 -0
  40. package/agent-spine-plugin/hooks/version.json +5 -0
  41. package/agent-spine-plugin/package.json +69 -0
  42. package/agent-spine-plugin/scripts/check-hosts.js +193 -0
  43. package/agent-spine-plugin/scripts/check-install.js +551 -0
  44. package/agent-spine-plugin/scripts/check-syntax.js +29 -0
  45. package/agent-spine-plugin/scripts/github-actions.js +11 -0
  46. package/agent-spine-plugin/scripts/release-check.js +127 -0
  47. package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
  48. package/agent-spine-plugin/scripts/run-checks.js +46 -0
  49. package/agent-spine-plugin/scripts/run-tests-hermetic.js +73 -0
  50. package/agent-spine-plugin/skill/SKILL.md +76 -0
  51. package/agent-spine-plugin/skills/agent-spine/SKILL.md +84 -0
  52. package/agent-spine-plugin/spine-example/1-identity.md +12 -0
  53. package/agent-spine-plugin/spine-example/2-voice.md +6 -0
  54. package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
  55. package/agent-spine-plugin/spine-example/4-history.md +4 -0
  56. package/agent-spine-plugin/src/cli.js +950 -0
  57. package/agent-spine-plugin/src/hook.js +646 -0
  58. package/agent-spine-plugin/src/index.js +84 -0
  59. package/agent-spine-plugin/src/lib/acceptance.js +294 -0
  60. package/agent-spine-plugin/src/lib/attention.js +755 -0
  61. package/agent-spine-plugin/src/lib/audit.js +308 -0
  62. package/agent-spine-plugin/src/lib/authentication.js +513 -0
  63. package/agent-spine-plugin/src/lib/briefing.js +305 -0
  64. package/agent-spine-plugin/src/lib/catalog.js +142 -0
  65. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -0
  66. package/agent-spine-plugin/src/lib/context.js +154 -0
  67. package/agent-spine-plugin/src/lib/continuity.js +333 -0
  68. package/agent-spine-plugin/src/lib/coordination.js +577 -0
  69. package/agent-spine-plugin/src/lib/documents.js +199 -0
  70. package/agent-spine-plugin/src/lib/feed-transport.js +497 -0
  71. package/agent-spine-plugin/src/lib/filesystem-retry.js +27 -0
  72. package/agent-spine-plugin/src/lib/gateway-runtime.js +760 -0
  73. package/agent-spine-plugin/src/lib/graph.js +284 -0
  74. package/agent-spine-plugin/src/lib/https-transport.js +392 -0
  75. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -0
  76. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -0
  77. package/agent-spine-plugin/src/lib/learning.js +645 -0
  78. package/agent-spine-plugin/src/lib/object-transport.js +206 -0
  79. package/agent-spine-plugin/src/lib/paths.js +60 -0
  80. package/agent-spine-plugin/src/lib/peer-transport.js +283 -0
  81. package/agent-spine-plugin/src/lib/persona-runtime.js +503 -0
  82. package/agent-spine-plugin/src/lib/runtime.js +13 -0
  83. package/agent-spine-plugin/src/lib/selfstarter.js +819 -0
  84. package/agent-spine-plugin/src/lib/sharing.js +969 -0
  85. package/agent-spine-plugin/src/lib/source-roots.js +451 -0
  86. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -0
  87. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -0
  88. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -0
  89. package/agent-spine-plugin/src/mcp.js +526 -0
  90. package/agent-spine-plugin/src/version.js +1 -0
  91. package/agent-spine-plugin/src/worker.js +178 -0
  92. package/bin/identity-context-policy.cjs +51 -1
  93. package/bin/launcher-runtime.js +42 -1
  94. package/bin/plugin-bootstrap.js +68 -27
  95. package/bin/repeated-user-message-projection.cjs +6 -0
  96. package/bin/turn-thinking-policy.cjs +5 -4
  97. package/bin/turn-tool-performance-policy.cjs +16 -2
  98. package/blun.mjs +187 -35
  99. package/package.json +3 -1
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { watch } from "node:fs";
4
+ import { realpath } from "node:fs/promises";
5
+ import { isAbsolute, resolve } from "node:path";
6
+ import { claimGatewayWork, completeGatewayRun, deliverPrepared, failGatewayRun, loadGatewayRuntime, reconcileGateway, updateGatewayHealth } from "./lib/gateway-runtime.js";
7
+ import { createTelegramAdapter } from "./lib/telegram-adapter.js";
8
+ import { acknowledgeChannelDelivery, loadChannelRuntime } from "./lib/channel-runtime.js";
9
+ import { loadPersonaRuntime, syncPersonaRosterFromEnvironment } from "./lib/persona-runtime.js";
10
+ import { isMainModule } from "./lib/runtime.js";
11
+
12
+ const MAX_FRAME = 64 * 1024;
13
+ const WAKE_FILES = new Set(["attention.json", "channel-runtime.json", "gateway-policy.json", "persona-policy.json", "persona-runtime.json"]);
14
+
15
+ export async function waitForGatewayWake(root, delayMs, { watchFactory = watch, realpathFactory = realpath } = {}) {
16
+ const delay = Math.max(250, Math.min(60000, Number(delayMs) || 60000));
17
+ const { directory } = await loadGatewayRuntime(root);
18
+ let watchPath;
19
+ try { watchPath = await realpathFactory(directory); } catch { return "watch-unavailable"; }
20
+ return new Promise((resolvePromise) => {
21
+ let settled = false; let watcher;
22
+ const finish = (reason) => {
23
+ if (settled) return;
24
+ settled = true; clearTimeout(timer);
25
+ watcher?.close();
26
+ resolvePromise(reason);
27
+ };
28
+ const timer = setTimeout(() => finish("timer"), delay);
29
+ try {
30
+ watcher = watchFactory(watchPath, { persistent: true }, (_eventType, filename) => {
31
+ const name = filename === null ? null : String(filename);
32
+ if (name === null || WAKE_FILES.has(name)) finish("event");
33
+ });
34
+ watcher.on?.("error", () => finish("watch-error"));
35
+ } catch { finish("watch-unavailable"); }
36
+ });
37
+ }
38
+
39
+ function exactRunner(env) {
40
+ const runner = env.AGENTSPINE_HOST_RUNNER;
41
+ if (!runner || !isAbsolute(runner)) throw new Error("AGENTSPINE_HOST_RUNNER must be an absolute locally approved executable path");
42
+ return resolve(runner);
43
+ }
44
+
45
+ async function invokeHostRunner(item, { env = process.env, timeoutMs = 300000, spawnProcess = spawn } = {}) {
46
+ const executable = exactRunner(env);
47
+ return new Promise((resolvePromise, reject) => {
48
+ const child = spawnProcess(executable, [], {
49
+ shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"],
50
+ env: Object.fromEntries(["PATH", "Path", "HOME", "USERPROFILE", "SystemRoot", "SYSTEMROOT", "WINDIR",
51
+ "AGENTSPINE_STATE_DIR", "CLAUDE_CONFIG_DIR", "CODEX_HOME"].filter((key) => env[key] !== undefined).map((key) => [key, env[key]]))
52
+ });
53
+ let stdout = Buffer.alloc(0); let stderr = Buffer.alloc(0); let settled = false;
54
+ const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); error ? reject(error) : resolvePromise(value); };
55
+ child.stdout.on("data", (chunk) => { stdout = Buffer.concat([stdout, chunk]); if (stdout.length > MAX_FRAME) child.kill(); });
56
+ child.stderr.on("data", (chunk) => { stderr = Buffer.concat([stderr, chunk]).subarray(0, 2048); });
57
+ child.on("error", (error) => finish(error));
58
+ child.on("close", (code) => {
59
+ if (stdout.length > MAX_FRAME) return finish(new Error("host runner response exceeds 64 KiB"));
60
+ if (code !== 0) return finish(new Error("host runner failed: " + stderr.toString("utf8").slice(0, 2048)));
61
+ try { finish(null, JSON.parse(stdout.toString("utf8"))); } catch { finish(new Error("host runner returned invalid JSON")); }
62
+ });
63
+ const timer = setTimeout(() => { child.kill(); finish(new Error("host runner exceeded its bounded timeout")); }, timeoutMs);
64
+ child.stdin.end(JSON.stringify({ schema: "agentspine.run-request/v1", item, authority: "execution-state-only" }) + "\n");
65
+ });
66
+ }
67
+
68
+ async function hostWorkItem(root, item) {
69
+ const [{ policy }, personas, channel] = await Promise.all([
70
+ loadGatewayRuntime(root), loadPersonaRuntime(root), loadChannelRuntime(root)
71
+ ]);
72
+ const persona = personas.runtime.personas.find((entry) => entry.personaId === item.agentId && entry.status === "active");
73
+ const identity = personas.policy.bindings.find((entry) => entry.id === persona?.bindingId && entry.active);
74
+ if (!persona || !identity) throw new Error("claimed work lost its authenticated agent identity");
75
+ const goal = item.goalId === null ? null : policy.goals.find((entry) => entry.goalId === item.goalId) || null;
76
+ const event = item.channelEventId === null ? null
77
+ : channel.runtime.events.find((entry) => entry.eventId === item.channelEventId) || null;
78
+ if (item.channelEventId && !event) throw new Error("claimed channel work lost its exact event");
79
+ return {
80
+ ...structuredClone(item), host: identity.host, profileId: identity.profileId, projectRoot: root,
81
+ goal: goal ? structuredClone(goal) : null,
82
+ hostEnvironment: {
83
+ AGENTSPINE_GATEWAY_CONTEXT: "agentspine.gateway-start/v1",
84
+ AGENTSPINE_ENTITY_ID: item.agentId,
85
+ AGENTSPINE_PROJECT_ID: item.projectId,
86
+ ...(item.groupId === null ? {} : { AGENTSPINE_GROUP_ID: item.groupId }),
87
+ ...(event ? {
88
+ AGENTSPINE_CHANNEL_EVENT_ID: event.eventId,
89
+ AGENTSPINE_CHANNEL_PROVIDER: event.provider
90
+ } : {})
91
+ },
92
+ channelStart: event ? {
93
+ agent_spine_scope: { entity_id: event.agentId, project_id: event.projectId, group_id: event.groupId },
94
+ agent_spine_channel_event: { event_id: event.eventId, provider: event.provider },
95
+ session_key: event.sessionKey,
96
+ authority: "explicit-local-channel-policy"
97
+ } : null
98
+ };
99
+ }
100
+
101
+ export async function runWorkerTick({ root = process.cwd(), workerId = "gateway-worker:local", now = new Date(),
102
+ hostRunner = invokeHostRunner, adapter = null, env = process.env } = {}) {
103
+ const deliveryAdapter = adapter || createTelegramAdapter({ root, env });
104
+ await syncPersonaRosterFromEnvironment({ root, env, now });
105
+ const initial = await loadGatewayRuntime(root);
106
+ if (!initial.policy.enabled || initial.policy.killSwitch) return { status: "stopped", processed: false };
107
+ if (typeof deliveryAdapter.poll === "function") {
108
+ try { await deliveryAdapter.poll(); }
109
+ catch (error) {
110
+ await updateGatewayHealth({ root, worker: "degraded", adapter: "failed", now });
111
+ throw error;
112
+ }
113
+ }
114
+ await reconcileGateway({ root, now });
115
+ await updateGatewayHealth({ root, worker: "healthy", adapter: "healthy", now });
116
+ const afterReconcile = await loadGatewayRuntime(root);
117
+ const dueOutbox = afterReconcile.runtime.outbox.filter((item) => ["prepared", "failed"].includes(item.status)
118
+ && new Date(item.nextAttemptAt) <= new Date(now)).sort((a, b) => a.nextAttemptAt.localeCompare(b.nextAttemptAt)
119
+ || a.createdAt.localeCompare(b.createdAt) || a.outboxId.localeCompare(b.outboxId))[0] || null;
120
+ if (dueOutbox) {
121
+ const delivery = await deliverPrepared({ root, outboxId: dueOutbox.outboxId, adapter: deliveryAdapter, now });
122
+ if (delivery.outbox.status === "delivered") await acknowledgeChannelDelivery({ root,
123
+ eventId: delivery.outbox.eventId, bindingId: delivery.outbox.bindingId,
124
+ deliveryReceiptId: delivery.receipt.id, now });
125
+ return { status: delivery.outbox.status, processed: true, outboxId: delivery.outbox.outboxId,
126
+ recoveredDelivery: true };
127
+ }
128
+ const claim = await claimGatewayWork({ root, workerId, now });
129
+ if (!claim.item) return { status: claim.reason, processed: false };
130
+ let result;
131
+ try { result = await hostRunner(await hostWorkItem(root, claim.item), { env }); }
132
+ catch (error) {
133
+ const failed = await failGatewayRun({ root, queueId: claim.item.queueId, workerId,
134
+ error: "Host runtime unavailable: " + String(error.message).slice(0, 400), now });
135
+ return { status: failed.item.status, processed: true, queueId: failed.item.queueId, retryAt: failed.item.availableAt };
136
+ }
137
+ const completed = await completeGatewayRun({ root, queueId: claim.item.queueId, workerId, result, now });
138
+ if (!completed.outbox) return { status: completed.item.status, processed: true, queueId: completed.item.queueId };
139
+ const delivery = await deliverPrepared({ root, outboxId: completed.outbox.outboxId,
140
+ adapter: deliveryAdapter, now });
141
+ if (delivery.outbox.status === "delivered") await acknowledgeChannelDelivery({ root,
142
+ eventId: delivery.outbox.eventId, bindingId: delivery.outbox.bindingId,
143
+ deliveryReceiptId: delivery.receipt.id, now });
144
+ return { status: delivery.outbox.status, processed: true, queueId: completed.item.queueId, outboxId: delivery.outbox.outboxId };
145
+ }
146
+
147
+ export async function runWorker({ root = process.cwd(), once = false, env = process.env,
148
+ waitForWake = waitForGatewayWake } = {}) {
149
+ const workerId = "gateway-worker:" + process.pid;
150
+ do {
151
+ const result = await runWorkerTick({ root, workerId, env });
152
+ if (once) return result;
153
+ const { policy, runtime } = await loadGatewayRuntime(root);
154
+ if (!policy.enabled || policy.killSwitch) return { status: "stopped", processed: false };
155
+ const pending = [
156
+ ...runtime.queue.filter((item) => item.status === "pending").map((item) => new Date(item.availableAt).getTime()),
157
+ ...runtime.outbox.filter((item) => ["prepared", "failed"].includes(item.status))
158
+ .map((item) => new Date(item.nextAttemptAt).getTime())
159
+ ];
160
+ const delay = pending.length ? Math.max(250, Math.min(60000, Math.min(...pending) - Date.now())) : 60000;
161
+ await waitForWake(root, delay);
162
+ } while (true);
163
+ }
164
+
165
+ if (isMainModule(import.meta.url)) {
166
+ const args = process.argv.slice(2); const once = args.includes("--once");
167
+ const rootIndex = args.indexOf("--root");
168
+ if (rootIndex >= 0 && (!args[rootIndex + 1] || args[rootIndex + 1].startsWith("--"))) throw new Error("--root requires a path");
169
+ const root = rootIndex >= 0 ? args[rootIndex + 1] : process.cwd();
170
+ const rosterIndex = args.indexOf("--persona-roster");
171
+ if (rosterIndex >= 0 && (!args[rosterIndex + 1] || args[rosterIndex + 1].startsWith("--"))) {
172
+ throw new Error("--persona-roster requires a path");
173
+ }
174
+ const env = { ...process.env };
175
+ if (rosterIndex >= 0) env.AGENTSPINE_PERSONA_ROSTER_FILE = args[rosterIndex + 1];
176
+ runWorker({ root, once, env }).then((result) => process.stdout.write(JSON.stringify(result) + "\n"))
177
+ .catch((error) => { process.stderr.write("AgentSpine worker: " + String(error.message).slice(0, 2048) + "\n"); process.exitCode = 1; });
178
+ }
@@ -21,6 +21,7 @@ const ACTIVITY_FILE_RE = /^\d{4}-\d{2}-\d{2}\.json$/u;
21
21
  const RELATIONSHIP_CANDIDATE_FILE_RE = /^[a-f0-9]{32}\.json$/u;
22
22
  const ACTOR_FILE_RE = /^([A-Za-z0-9][A-Za-z0-9._-]{0,127})\.json$/u;
23
23
  const LONG_GAP_MS = 7 * 24 * 60 * 60 * 1000;
24
+ const ESTABLISHED_INTERACTION_DAYS = 3;
24
25
  const MAX_COLLEAGUES_IN_CONTEXT = 12;
25
26
 
26
27
  function autoGraphEnabled(env) {
@@ -468,6 +469,52 @@ function recordInteractionActivity(root, agentId, actorId, interactedAt) {
468
469
  );
469
470
  }
470
471
 
472
+ function trustedInteractionDayCount(root, agentId, actorId, limit) {
473
+ if (!actorId) return 0;
474
+ const activityRoot = path.resolve(root, 'agents', agentId, 'activity', actorId);
475
+ if (!isInside(root, activityRoot)) return 0;
476
+ try {
477
+ const stat = fs.lstatSync(activityRoot);
478
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return 0;
479
+ if (!isInside(root, fs.realpathSync(activityRoot))) return 0;
480
+ let count = 0;
481
+ for (const name of fs.readdirSync(activityRoot).filter((entry) => ACTIVITY_FILE_RE.test(entry)).sort()) {
482
+ const entry = readJson(root, ['agents', agentId, 'activity', actorId, name]);
483
+ const interactedAt = trustedTimestamp(entry?.interacted_at);
484
+ if (entry?.version !== 1
485
+ || entry?.actor_id !== actorId
486
+ || entry?.source !== 'trusted_channel_metadata'
487
+ || !interactedAt
488
+ || activityDate(interactedAt) !== name.slice(0, 10)) continue;
489
+ count += 1;
490
+ if (count >= limit) break;
491
+ }
492
+ return count;
493
+ } catch {
494
+ return 0;
495
+ }
496
+ }
497
+
498
+ function promoteReturningRelationship(root, agentId, actorId) {
499
+ const relationship = readJson(root, ['agents', agentId, 'relationships', `${actorId}.json`]);
500
+ if (relationship?.version !== 1
501
+ || relationship.actor_id !== actorId
502
+ || relationship.status !== 'new') return false;
503
+ if (trustedInteractionDayCount(root, agentId, actorId, ESTABLISHED_INTERACTION_DAYS)
504
+ < ESTABLISHED_INTERACTION_DAYS) return false;
505
+ return updateJsonAtomically(
506
+ root,
507
+ ['agents', agentId, 'relationships', `${actorId}.json`],
508
+ (relationship) => (
509
+ relationship.version === 1
510
+ && relationship.actor_id === actorId
511
+ && relationship.status === 'new'
512
+ ? { ...relationship, status: 'established' }
513
+ : undefined
514
+ ),
515
+ );
516
+ }
517
+
471
518
  function previousInteractionAfterLongGap(root, agentId, actorId) {
472
519
  if (!actorId) return undefined;
473
520
  const activityRoot = path.resolve(root, 'agents', agentId, 'activity', actorId);
@@ -598,7 +645,10 @@ function recordChannelIdentity(envelope, env = process.env) {
598
645
  messageId: safeId(meta.message_id),
599
646
  receivedAt,
600
647
  });
601
- if (firstSeenAt) recordInteractionActivity(root, agentId, actorId, firstSeenAt);
648
+ if (firstSeenAt) {
649
+ recordInteractionActivity(root, agentId, actorId, firstSeenAt);
650
+ if (promoteReturningRelationship(root, agentId, actorId)) updated.push('relationship_status');
651
+ }
602
652
  let relationshipMemory;
603
653
  let relationshipMemoryUnavailable = false;
604
654
  let relationshipNotes = [];
@@ -18,7 +18,10 @@ const {
18
18
  launcherHelpText,
19
19
  launcherModeFromArgv,
20
20
  } = require('./launcher-mode');
21
- const { installManagedTelegramPlugin } = require('./plugin-bootstrap');
21
+ const {
22
+ installManagedAgentSpinePlugin,
23
+ installManagedTelegramPlugin,
24
+ } = require('./plugin-bootstrap');
22
25
  const {
23
26
  availableStandardToolNames,
24
27
  seedConfiguredMnemoMcp,
@@ -275,6 +278,42 @@ function installTelegramForProfile(packageRoot, blunDir) {
275
278
  }
276
279
  }
277
280
 
281
+ function installAgentSpineForProfile(packageRoot, blunDir) {
282
+ installManagedAgentSpinePlugin({ packageRoot, blunDir });
283
+ }
284
+
285
+ function installAgentSpineForExistingProfiles(packageRoot, sharedHome, currentProfileHome) {
286
+ const profileRoot = path.join(sharedHome, 'profile');
287
+ const profileHomes = new Map();
288
+ const addProfileHome = (profileHome) => {
289
+ const resolved = path.resolve(profileHome);
290
+ const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
291
+ profileHomes.set(key, resolved);
292
+ };
293
+
294
+ addProfileHome(currentProfileHome);
295
+ if (fs.existsSync(profileRoot)) {
296
+ for (const entry of fs.readdirSync(profileRoot, { withFileTypes: true })) {
297
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
298
+ let profileHome;
299
+ try {
300
+ profileHome = resolveProfilePaths(sharedHome, entry.name).home;
301
+ } catch {
302
+ continue;
303
+ }
304
+ const stat = fs.lstatSync(profileHome);
305
+ if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
306
+ addProfileHome(profileHome);
307
+ }
308
+ }
309
+
310
+ for (const profileHome of profileHomes.values()) {
311
+ ensurePrivateDirectory(profileHome);
312
+ installAgentSpineForProfile(packageRoot, profileHome);
313
+ }
314
+ return [...profileHomes.values()];
315
+ }
316
+
278
317
  function createManagedNodeEnvironment(binary, env = process.env, platform = process.platform) {
279
318
  const pathImpl = platform === 'win32' ? path.win32 : path.posix;
280
319
  const separator = platform === 'win32' ? path.win32.delimiter : path.posix.delimiter;
@@ -504,6 +543,7 @@ async function runLauncher(options = {}) {
504
543
  });
505
544
  seedConfiguredMnemoMcp({ blunDir, env: process.env });
506
545
  seedStandardSkills({ packageRoot: PKG, blunDir: privatePaths.sharedHome });
546
+ installAgentSpineForExistingProfiles(PKG, privatePaths.sharedHome, blunDir);
507
547
  if (mode === LAUNCHER_MODES.KING) installTelegramForProfile(PKG, blunDir);
508
548
 
509
549
  const nativeModules = repairConfiguredNativeModules({
@@ -547,6 +587,7 @@ async function runLauncher(options = {}) {
547
587
  module.exports = {
548
588
  clearStartupUpdateViewport,
549
589
  createManagedNodeEnvironment,
590
+ installAgentSpineForExistingProfiles,
550
591
  resolveLauncherPrivatePaths,
551
592
  runLauncher,
552
593
  shouldResumeAfterStartupUpdate,
@@ -75,14 +75,14 @@ function pluginTreeDigest(root) {
75
75
  return hash.digest('hex');
76
76
  }
77
77
 
78
- function hasCurrentTelegramRecord(installed, pluginRoot) {
79
- const records = installed.plugins.filter((plugin) => plugin.id === 'telegram');
78
+ function hasCurrentPluginRecord(installed, pluginRoot, pluginId, mcpServerName) {
79
+ const records = installed.plugins.filter((plugin) => plugin.id === pluginId);
80
80
  if (records.length !== 1) return false;
81
- const [telegram] = records;
82
- return telegram.root === pluginRoot
83
- && telegram.source === 'local-path'
84
- && telegram.enabled === true
85
- && telegram.capabilities?.mcpServers?.telegram?.enabled === true;
81
+ const [plugin] = records;
82
+ return plugin.root === pluginRoot
83
+ && plugin.source === 'local-path'
84
+ && plugin.enabled === true
85
+ && plugin.capabilities?.mcpServers?.[mcpServerName]?.enabled === true;
86
86
  }
87
87
 
88
88
  function readInstalledPlugins(filePath) {
@@ -96,16 +96,16 @@ function readInstalledPlugins(filePath) {
96
96
  return parsed;
97
97
  }
98
98
 
99
- function updatedInstalledPlugins(installed, pluginRoot, now) {
100
- const previous = installed.plugins.find((plugin) => plugin.id === 'telegram');
99
+ function updatedInstalledPlugins(installed, pluginRoot, now, pluginId, mcpServerName) {
100
+ const previous = installed.plugins.find((plugin) => plugin.id === pluginId);
101
101
  const previousCapabilities = isObject(previous?.capabilities) ? previous.capabilities : {};
102
102
  const previousMcpServers = isObject(previousCapabilities.mcpServers)
103
103
  ? previousCapabilities.mcpServers : {};
104
- const previousTelegramMcp = isObject(previousMcpServers.telegram)
105
- ? previousMcpServers.telegram : {};
106
- const telegram = {
104
+ const previousMcp = isObject(previousMcpServers[mcpServerName])
105
+ ? previousMcpServers[mcpServerName] : {};
106
+ const updatedPlugin = {
107
107
  ...previous,
108
- id: 'telegram',
108
+ id: pluginId,
109
109
  root: pluginRoot,
110
110
  source: 'local-path',
111
111
  enabled: true,
@@ -116,7 +116,7 @@ function updatedInstalledPlugins(installed, pluginRoot, now) {
116
116
  ...previousCapabilities,
117
117
  mcpServers: {
118
118
  ...previousMcpServers,
119
- telegram: { ...previousTelegramMcp, enabled: true },
119
+ [mcpServerName]: { ...previousMcp, enabled: true },
120
120
  },
121
121
  },
122
122
  };
@@ -124,14 +124,14 @@ function updatedInstalledPlugins(installed, pluginRoot, now) {
124
124
  const plugins = [];
125
125
  let replaced = false;
126
126
  for (const plugin of installed.plugins) {
127
- if (plugin.id !== 'telegram') {
127
+ if (plugin.id !== pluginId) {
128
128
  plugins.push(plugin);
129
129
  } else if (!replaced) {
130
- plugins.push(telegram);
130
+ plugins.push(updatedPlugin);
131
131
  replaced = true;
132
132
  }
133
133
  }
134
- if (!replaced) plugins.push(telegram);
134
+ if (!replaced) plugins.push(updatedPlugin);
135
135
  return { ...installed, version: 1, plugins };
136
136
  }
137
137
 
@@ -149,11 +149,19 @@ function writeInstalledPlugins(filePath, installed) {
149
149
  }
150
150
  }
151
151
 
152
- function installManagedTelegramPlugin({ packageRoot, blunDir, now = new Date().toISOString() }) {
153
- const sourceRoot = path.join(packageRoot, 'telegram-plugin');
152
+ function installManagedPlugin({
153
+ packageRoot,
154
+ blunDir,
155
+ now = new Date().toISOString(),
156
+ pluginId,
157
+ sourceDirectory,
158
+ mcpServerName,
159
+ displayName,
160
+ }) {
161
+ const sourceRoot = path.join(packageRoot, sourceDirectory);
154
162
  const pluginsDir = path.join(blunDir, 'plugins');
155
163
  const managedDir = path.join(pluginsDir, 'managed');
156
- const legacyPluginRoot = path.join(managedDir, 'telegram');
164
+ const legacyPluginRoot = path.join(managedDir, pluginId);
157
165
  const installedFile = path.join(pluginsDir, 'installed.json');
158
166
 
159
167
  ensurePrivateDirectory(pluginsDir);
@@ -162,26 +170,38 @@ function installManagedTelegramPlugin({ packageRoot, blunDir, now = new Date().t
162
170
  ensurePrivateDirectory(managedDir);
163
171
  if (fs.existsSync(legacyPluginRoot)
164
172
  && pluginTreesAreIdentical(sourceRoot, legacyPluginRoot)) {
165
- if (!hasCurrentTelegramRecord(installed, legacyPluginRoot)) {
166
- const updatedInstalled = updatedInstalledPlugins(installed, legacyPluginRoot, now);
173
+ if (!hasCurrentPluginRecord(installed, legacyPluginRoot, pluginId, mcpServerName)) {
174
+ const updatedInstalled = updatedInstalledPlugins(
175
+ installed,
176
+ legacyPluginRoot,
177
+ now,
178
+ pluginId,
179
+ mcpServerName,
180
+ );
167
181
  writeInstalledPlugins(installedFile, updatedInstalled);
168
182
  }
169
183
  return { installedFile, pluginRoot: legacyPluginRoot, reused: true };
170
184
  }
171
185
  const sourceDigest = pluginTreeDigest(sourceRoot);
172
- const pluginRoot = path.join(managedDir, `telegram-${sourceDigest}`);
173
- const updatedInstalled = updatedInstalledPlugins(installed, pluginRoot, now);
186
+ const pluginRoot = path.join(managedDir, `${pluginId}-${sourceDigest}`);
187
+ const updatedInstalled = updatedInstalledPlugins(
188
+ installed,
189
+ pluginRoot,
190
+ now,
191
+ pluginId,
192
+ mcpServerName,
193
+ );
174
194
  if (fs.existsSync(pluginRoot)) {
175
195
  if (!pluginTreesAreIdentical(sourceRoot, pluginRoot)) {
176
- throw new Error(`Managed Telegram plugin digest mismatch: ${pluginRoot}`);
196
+ throw new Error(`Managed ${displayName} plugin digest mismatch: ${pluginRoot}`);
177
197
  }
178
- if (!hasCurrentTelegramRecord(installed, pluginRoot)) {
198
+ if (!hasCurrentPluginRecord(installed, pluginRoot, pluginId, mcpServerName)) {
179
199
  writeInstalledPlugins(installedFile, updatedInstalled);
180
200
  }
181
201
  return { installedFile, pluginRoot, reused: true };
182
202
  }
183
203
  const nonce = `${process.pid}-${randomUUID()}`;
184
- const stagingRoot = path.join(managedDir, `.telegram-${nonce}.tmp`);
204
+ const stagingRoot = path.join(managedDir, `.${pluginId}-${nonce}.tmp`);
185
205
  let installedNewPlugin = false;
186
206
  try {
187
207
  fs.cpSync(sourceRoot, stagingRoot, { recursive: true, force: true });
@@ -201,7 +221,28 @@ function installManagedTelegramPlugin({ packageRoot, blunDir, now = new Date().t
201
221
  return { installedFile, pluginRoot, reused: false };
202
222
  }
203
223
 
224
+ function installManagedTelegramPlugin(options) {
225
+ return installManagedPlugin({
226
+ ...options,
227
+ pluginId: 'telegram',
228
+ sourceDirectory: 'telegram-plugin',
229
+ mcpServerName: 'telegram',
230
+ displayName: 'Telegram',
231
+ });
232
+ }
233
+
234
+ function installManagedAgentSpinePlugin(options) {
235
+ return installManagedPlugin({
236
+ ...options,
237
+ pluginId: 'agent-spine',
238
+ sourceDirectory: 'agent-spine-plugin',
239
+ mcpServerName: 'agent-spine',
240
+ displayName: 'AgentSpine',
241
+ });
242
+ }
243
+
204
244
  module.exports = {
245
+ installManagedAgentSpinePlugin,
205
246
  installManagedTelegramPlugin,
206
247
  readInstalledPlugins,
207
248
  };
@@ -68,6 +68,12 @@ class TelegramDeliveryLedger {
68
68
  return identity;
69
69
  }
70
70
 
71
+ forget(input, origin) {
72
+ const identity = this.identity(input, origin);
73
+ if (!identity) return false;
74
+ return this.identities.delete(identity);
75
+ }
76
+
71
77
  snapshot() {
72
78
  return [...this.identities];
73
79
  }
@@ -38,10 +38,11 @@ function shouldEnableThinkingForTurn(text, originKind) {
38
38
  return selectThinkingEffortForTurn(text, originKind) !== "off";
39
39
  }
40
40
 
41
- function selectThinkingEffortForWorkStep(stepNumber, previousToolOutcome) {
42
- const normalizedStep = Number(stepNumber);
43
- if (!Number.isInteger(normalizedStep) || normalizedStep <= 1) return undefined;
44
- return previousToolOutcome === "success" ? "low" : undefined;
41
+ function selectThinkingEffortForWorkStep(_stepNumber, _previousToolOutcome) {
42
+ // A successful tool call does not make the next agent decision routine.
43
+ // Preserve the turn-selected effort; short chat and lightweight steers are
44
+ // classified independently before the work loop starts.
45
+ return undefined;
45
46
  }
46
47
 
47
48
  function capCompletionBudgetForAdaptiveEffort(budget, thinkingEffort) {
@@ -17,6 +17,12 @@ const TOOL_SEARCH_INTENT_WORDS = new Set([
17
17
  'find', 'fetch', 'get', 'holen', 'list', 'read', 'search', 'show', 'anzeigen', 'lesen', 'suchen',
18
18
  ]);
19
19
  const TOOL_SEARCH_RELATED_TERM_GROUPS = Object.freeze([
20
+ Object.freeze([
21
+ 'checklist', 'list', 'liste', 'listen',
22
+ ]),
23
+ Object.freeze([
24
+ 'aufgabe', 'aufgaben', 'aufgabenliste', 'todo', 'todos',
25
+ ]),
20
26
  Object.freeze([
21
27
  'current', 'inbox', 'latest', 'message', 'messages', 'nachricht', 'nachrichten', 'queue', 'queued',
22
28
  'recent', 'letzte', 'letzten', 'letzter', 'vergangen', 'update', 'updates',
@@ -40,6 +46,7 @@ const CORE_TOOL_NAMES = Object.freeze([
40
46
  'Grep',
41
47
  'Write',
42
48
  'Glob',
49
+ 'TodoList',
43
50
  'mcp__plugin-telegram_telegram__reply',
44
51
  ]);
45
52
 
@@ -195,7 +202,10 @@ function rankingTokens(value) {
195
202
  function toolRankingDocument(tool) {
196
203
  const name = String(tool?.name || '');
197
204
  const leaf = name.split(/__|:/).at(-1) || name;
198
- const nameTokens = rankingTokens(`${name.replaceAll('_', ' ')} ${leaf.replaceAll('_', ' ')}`);
205
+ const splitToolName = (value) => String(value || '')
206
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
207
+ .replaceAll('_', ' ');
208
+ const nameTokens = rankingTokens(`${splitToolName(name)} ${splitToolName(leaf)}`);
199
209
  const descriptionTokens = rankingTokens(tool?.description);
200
210
  const parameterText = [];
201
211
  const properties = tool?.parameters?.properties;
@@ -247,14 +257,18 @@ function rankedToolNamesForTurnText(tools, value, options = {}) {
247
257
  let score = 0;
248
258
  let matchedTokens = 0;
249
259
  let exactNameMatches = 0;
260
+ let relatedNameMatches = 0;
250
261
  for (const token of queryTokens) {
251
262
  const tokenScore = tokenMatchScore(document, token);
252
263
  if (tokenScore === 0) continue;
253
264
  score += tokenScore;
254
265
  matchedTokens += 1;
255
266
  if (document.nameTokens.has(token)) exactNameMatches += 1;
267
+ else if (relatedSearchTerms(token).some((term) => document.nameTokens.has(term))) relatedNameMatches += 1;
256
268
  }
257
- const highConfidence = matchedTokens >= 2 || (exactNameMatches >= 1 && score >= 10);
269
+ const highConfidence = matchedTokens >= 2
270
+ || (exactNameMatches >= 1 && score >= 10)
271
+ || (relatedNameMatches >= 1 && score >= 5);
258
272
  return highConfidence ? { name: document.name, score, matchedTokens, exactNameMatches } : null;
259
273
  }).filter(Boolean).sort((left, right) => (
260
274
  right.matchedTokens - left.matchedTokens