blun-king-cli 9.1.486 → 9.1.488

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 (97) hide show
  1. package/LIESMICH.txt +26 -7
  2. package/README.md +14 -3
  3. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -0
  4. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -0
  5. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -0
  6. package/agent-spine-plugin/.mcp.json +8 -0
  7. package/agent-spine-plugin/CHANGELOG.md +277 -0
  8. package/agent-spine-plugin/LICENSE +186 -0
  9. package/agent-spine-plugin/README.md +342 -0
  10. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -0
  11. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -0
  12. package/agent-spine-plugin/bin/agentspine.js +7 -0
  13. package/agent-spine-plugin/blun.plugin.json +33 -0
  14. package/agent-spine-plugin/docs/acceptance.md +61 -0
  15. package/agent-spine-plugin/docs/architecture.md +183 -0
  16. package/agent-spine-plugin/docs/attention.md +121 -0
  17. package/agent-spine-plugin/docs/automatic-continuity.md +79 -0
  18. package/agent-spine-plugin/docs/channel-runtime.md +92 -0
  19. package/agent-spine-plugin/docs/coordination.md +138 -0
  20. package/agent-spine-plugin/docs/feed-transport.md +99 -0
  21. package/agent-spine-plugin/docs/gateway-runtime.md +109 -0
  22. package/agent-spine-plugin/docs/harness-reference.md +45 -0
  23. package/agent-spine-plugin/docs/host-integration.md +129 -0
  24. package/agent-spine-plugin/docs/https-transport.md +116 -0
  25. package/agent-spine-plugin/docs/learning.md +98 -0
  26. package/agent-spine-plugin/docs/object-transport.md +93 -0
  27. package/agent-spine-plugin/docs/peer-transport.md +88 -0
  28. package/agent-spine-plugin/docs/preservation-contract.md +53 -0
  29. package/agent-spine-plugin/docs/quality-gates.md +50 -0
  30. package/agent-spine-plugin/docs/relationships.md +67 -0
  31. package/agent-spine-plugin/docs/releasing.md +83 -0
  32. package/agent-spine-plugin/docs/roadmap.md +307 -0
  33. package/agent-spine-plugin/docs/selfstarter.md +88 -0
  34. package/agent-spine-plugin/docs/session-briefing.md +74 -0
  35. package/agent-spine-plugin/docs/shared-memory.md +259 -0
  36. package/agent-spine-plugin/docs/source-roots.md +86 -0
  37. package/agent-spine-plugin/docs/sqlite-transport.md +76 -0
  38. package/agent-spine-plugin/hooks/hooks.json +95 -0
  39. package/agent-spine-plugin/hooks/version.json +5 -0
  40. package/agent-spine-plugin/package.json +69 -0
  41. package/agent-spine-plugin/scripts/check-hosts.js +193 -0
  42. package/agent-spine-plugin/scripts/check-install.js +551 -0
  43. package/agent-spine-plugin/scripts/check-syntax.js +29 -0
  44. package/agent-spine-plugin/scripts/github-actions.js +11 -0
  45. package/agent-spine-plugin/scripts/release-check.js +127 -0
  46. package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
  47. package/agent-spine-plugin/scripts/run-checks.js +46 -0
  48. package/agent-spine-plugin/scripts/run-tests-hermetic.js +73 -0
  49. package/agent-spine-plugin/skill/SKILL.md +76 -0
  50. package/agent-spine-plugin/skills/agent-spine/SKILL.md +84 -0
  51. package/agent-spine-plugin/spine-example/1-identity.md +12 -0
  52. package/agent-spine-plugin/spine-example/2-voice.md +6 -0
  53. package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
  54. package/agent-spine-plugin/spine-example/4-history.md +4 -0
  55. package/agent-spine-plugin/src/cli.js +950 -0
  56. package/agent-spine-plugin/src/hook.js +646 -0
  57. package/agent-spine-plugin/src/index.js +84 -0
  58. package/agent-spine-plugin/src/lib/acceptance.js +294 -0
  59. package/agent-spine-plugin/src/lib/attention.js +755 -0
  60. package/agent-spine-plugin/src/lib/audit.js +308 -0
  61. package/agent-spine-plugin/src/lib/authentication.js +513 -0
  62. package/agent-spine-plugin/src/lib/briefing.js +305 -0
  63. package/agent-spine-plugin/src/lib/catalog.js +142 -0
  64. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -0
  65. package/agent-spine-plugin/src/lib/context.js +154 -0
  66. package/agent-spine-plugin/src/lib/continuity.js +333 -0
  67. package/agent-spine-plugin/src/lib/coordination.js +577 -0
  68. package/agent-spine-plugin/src/lib/documents.js +199 -0
  69. package/agent-spine-plugin/src/lib/feed-transport.js +497 -0
  70. package/agent-spine-plugin/src/lib/filesystem-retry.js +27 -0
  71. package/agent-spine-plugin/src/lib/gateway-runtime.js +760 -0
  72. package/agent-spine-plugin/src/lib/graph.js +284 -0
  73. package/agent-spine-plugin/src/lib/https-transport.js +392 -0
  74. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -0
  75. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -0
  76. package/agent-spine-plugin/src/lib/learning.js +645 -0
  77. package/agent-spine-plugin/src/lib/object-transport.js +206 -0
  78. package/agent-spine-plugin/src/lib/paths.js +60 -0
  79. package/agent-spine-plugin/src/lib/peer-transport.js +283 -0
  80. package/agent-spine-plugin/src/lib/persona-runtime.js +503 -0
  81. package/agent-spine-plugin/src/lib/runtime.js +13 -0
  82. package/agent-spine-plugin/src/lib/selfstarter.js +819 -0
  83. package/agent-spine-plugin/src/lib/sharing.js +969 -0
  84. package/agent-spine-plugin/src/lib/source-roots.js +451 -0
  85. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -0
  86. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -0
  87. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -0
  88. package/agent-spine-plugin/src/mcp.js +526 -0
  89. package/agent-spine-plugin/src/version.js +1 -0
  90. package/agent-spine-plugin/src/worker.js +178 -0
  91. package/bin/identity-context-policy.cjs +51 -1
  92. package/bin/launcher-runtime.js +9 -1
  93. package/bin/plugin-bootstrap.js +68 -27
  94. package/bin/turn-thinking-policy.cjs +5 -4
  95. package/bin/turn-tool-performance-policy.cjs +15 -2
  96. package/blun.mjs +96 -31
  97. package/package.json +2 -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,10 @@ function installTelegramForProfile(packageRoot, blunDir) {
275
278
  }
276
279
  }
277
280
 
281
+ function installAgentSpineForProfile(packageRoot, blunDir) {
282
+ installManagedAgentSpinePlugin({ packageRoot, blunDir });
283
+ }
284
+
278
285
  function createManagedNodeEnvironment(binary, env = process.env, platform = process.platform) {
279
286
  const pathImpl = platform === 'win32' ? path.win32 : path.posix;
280
287
  const separator = platform === 'win32' ? path.win32.delimiter : path.posix.delimiter;
@@ -504,6 +511,7 @@ async function runLauncher(options = {}) {
504
511
  });
505
512
  seedConfiguredMnemoMcp({ blunDir, env: process.env });
506
513
  seedStandardSkills({ packageRoot: PKG, blunDir: privatePaths.sharedHome });
514
+ installAgentSpineForProfile(PKG, blunDir);
507
515
  if (mode === LAUNCHER_MODES.KING) installTelegramForProfile(PKG, blunDir);
508
516
 
509
517
  const nativeModules = repairConfiguredNativeModules({
@@ -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
  };
@@ -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',
@@ -195,7 +201,10 @@ function rankingTokens(value) {
195
201
  function toolRankingDocument(tool) {
196
202
  const name = String(tool?.name || '');
197
203
  const leaf = name.split(/__|:/).at(-1) || name;
198
- const nameTokens = rankingTokens(`${name.replaceAll('_', ' ')} ${leaf.replaceAll('_', ' ')}`);
204
+ const splitToolName = (value) => String(value || '')
205
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
206
+ .replaceAll('_', ' ');
207
+ const nameTokens = rankingTokens(`${splitToolName(name)} ${splitToolName(leaf)}`);
199
208
  const descriptionTokens = rankingTokens(tool?.description);
200
209
  const parameterText = [];
201
210
  const properties = tool?.parameters?.properties;
@@ -247,14 +256,18 @@ function rankedToolNamesForTurnText(tools, value, options = {}) {
247
256
  let score = 0;
248
257
  let matchedTokens = 0;
249
258
  let exactNameMatches = 0;
259
+ let relatedNameMatches = 0;
250
260
  for (const token of queryTokens) {
251
261
  const tokenScore = tokenMatchScore(document, token);
252
262
  if (tokenScore === 0) continue;
253
263
  score += tokenScore;
254
264
  matchedTokens += 1;
255
265
  if (document.nameTokens.has(token)) exactNameMatches += 1;
266
+ else if (relatedSearchTerms(token).some((term) => document.nameTokens.has(term))) relatedNameMatches += 1;
256
267
  }
257
- const highConfidence = matchedTokens >= 2 || (exactNameMatches >= 1 && score >= 10);
268
+ const highConfidence = matchedTokens >= 2
269
+ || (exactNameMatches >= 1 && score >= 10)
270
+ || (relatedNameMatches >= 1 && score >= 5);
258
271
  return highConfidence ? { name: document.name, score, matchedTokens, exactNameMatches } : null;
259
272
  }).filter(Boolean).sort((left, right) => (
260
273
  right.matchedTokens - left.matchedTokens