heyio 0.30.1 → 0.32.0

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.
@@ -4,6 +4,8 @@ import { existsSync, readFileSync } from "node:fs";
4
4
  import express from "express";
5
5
  import { config } from "../config.js";
6
6
  import { listSkills, installSkill, installSkillFromContent, removeSkill } from "../copilot/skills.js";
7
+ import { loadMcpConfig, saveMcpConfig } from "../mcp/config.js";
8
+ import { initMcpTools } from "../copilot/orchestrator.js";
7
9
  import { listSquads, createSquad, listSquadAgents, getSquad } from "../store/squads.js";
8
10
  import { createInstance, getInstance, listInstances, updateInstanceStatus, getInstanceDecisions, mergeInstanceDecisions, buildContextSnapshot } from "../store/instances.js";
9
11
  import { createWorktree, removeWorktree } from "../store/worktrees.js";
@@ -17,7 +19,6 @@ import { listSchedules, getSchedule, deleteSchedule, setScheduleEnabled } from "
17
19
  import { listIoSchedules, getIoSchedule, deleteIoSchedule, setIoScheduleEnabled } from "../store/io-schedules.js";
18
20
  import { getScheduleRuns } from "../store/schedule-runs.js";
19
21
  import { createFeedEntry, listFeedEntries, listFeedSquads, countUnreadFeedEntries, markFeedEntryRead, markAllFeedEntriesRead, deleteFeedEntry, markFeedEntriesRead, deleteFeedEntries } from "../store/feed.js";
20
- import { listInboxEntries, countInboxEntries, deleteInboxEntry } from "../store/inbox.js";
21
22
  import { listPages, readPage } from "../wiki/fs.js";
22
23
  import { runScheduleNow } from "../copilot/scheduler.js";
23
24
  import { runIoScheduleNow } from "../copilot/io-scheduler.js";
@@ -105,7 +106,7 @@ export async function startApiServer() {
105
106
  api.get("/feed/count", (req, res) => {
106
107
  try {
107
108
  const rawType = req.query.type;
108
- const type = rawType === "deliverable" || rawType === "notification"
109
+ const type = rawType === "inbox" || rawType === "notification"
109
110
  ? rawType
110
111
  : undefined;
111
112
  const count = countUnreadFeedEntries(type);
@@ -119,7 +120,7 @@ export async function startApiServer() {
119
120
  api.get("/feed", (req, res) => {
120
121
  try {
121
122
  const rawType = req.query.type;
122
- const type = rawType === "deliverable" || rawType === "notification"
123
+ const type = rawType === "inbox" || rawType === "notification"
123
124
  ? rawType
124
125
  : undefined;
125
126
  const unreadOnly = req.query.unread === "true";
@@ -253,7 +254,7 @@ export async function startApiServer() {
253
254
  api.post("/feed/read-all", (req, res) => {
254
255
  try {
255
256
  const rawType = req.query.type;
256
- const type = rawType === "deliverable" || rawType === "notification"
257
+ const type = rawType === "inbox" || rawType === "notification"
257
258
  ? rawType
258
259
  : undefined;
259
260
  const marked = markAllFeedEntriesRead(type);
@@ -316,8 +317,8 @@ export async function startApiServer() {
316
317
  });
317
318
  api.post("/feed", (req, res) => {
318
319
  const { type, title, body, source_type, source_ref } = req.body;
319
- if (type !== "deliverable" && type !== "notification") {
320
- res.status(400).json({ error: "type must be 'deliverable' or 'notification'" });
320
+ if (type !== "inbox" && type !== "notification") {
321
+ res.status(400).json({ error: "type must be 'inbox' or 'notification'" });
321
322
  return;
322
323
  }
323
324
  if (!title || typeof title !== "string" || title.trim() === "") {
@@ -366,7 +367,7 @@ export async function startApiServer() {
366
367
  // Inbox endpoints
367
368
  api.get("/inbox/count", (_req, res) => {
368
369
  try {
369
- const count = countInboxEntries();
370
+ const count = countUnreadFeedEntries("inbox");
370
371
  res.json({ count });
371
372
  }
372
373
  catch (e) {
@@ -376,7 +377,7 @@ export async function startApiServer() {
376
377
  });
377
378
  api.get("/inbox", (_req, res) => {
378
379
  try {
379
- const entries = listInboxEntries();
380
+ const entries = listFeedEntries({ type: "inbox" });
380
381
  res.json({ entries });
381
382
  }
382
383
  catch (e) {
@@ -392,7 +393,7 @@ export async function startApiServer() {
392
393
  return;
393
394
  }
394
395
  try {
395
- const deleted = deleteInboxEntry(id);
396
+ const deleted = deleteFeedEntry(id);
396
397
  if (!deleted) {
397
398
  res.status(404).json({ error: "Inbox entry not found" });
398
399
  return;
@@ -964,6 +965,81 @@ export async function startApiServer() {
964
965
  app.use(express.static(WEB_DIST));
965
966
  console.log("[io] Web frontend enabled");
966
967
  }
968
+ // ── MCP server management endpoints ────────────────────────────────────────
969
+ api.get("/mcp/servers", (_req, res) => {
970
+ try {
971
+ const config = loadMcpConfig();
972
+ res.json({ servers: config.servers });
973
+ }
974
+ catch (e) {
975
+ res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
976
+ }
977
+ });
978
+ api.post("/mcp/servers", (req, res) => {
979
+ const { name, command, args, url, env } = req.body;
980
+ if (!name) {
981
+ res.status(400).json({ error: "name is required" });
982
+ return;
983
+ }
984
+ if (!command && !url) {
985
+ res.status(400).json({ error: "command or url is required" });
986
+ return;
987
+ }
988
+ try {
989
+ const config = loadMcpConfig();
990
+ if (config.servers.find(s => s.name === name)) {
991
+ res.status(409).json({ error: "server already exists" });
992
+ return;
993
+ }
994
+ config.servers.push({ name, command, args, url, env, enabled: true });
995
+ saveMcpConfig(config);
996
+ res.status(201).json({ ok: true });
997
+ }
998
+ catch (e) {
999
+ res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
1000
+ }
1001
+ });
1002
+ api.delete("/mcp/servers/:name", (req, res) => {
1003
+ try {
1004
+ const config = loadMcpConfig();
1005
+ const idx = config.servers.findIndex(s => s.name === req.params.name);
1006
+ if (idx === -1) {
1007
+ res.status(404).json({ error: "server not found" });
1008
+ return;
1009
+ }
1010
+ config.servers.splice(idx, 1);
1011
+ saveMcpConfig(config);
1012
+ res.json({ ok: true });
1013
+ }
1014
+ catch (e) {
1015
+ res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
1016
+ }
1017
+ });
1018
+ api.patch("/mcp/servers/:name/toggle", (req, res) => {
1019
+ try {
1020
+ const config = loadMcpConfig();
1021
+ const server = config.servers.find(s => s.name === req.params.name);
1022
+ if (!server) {
1023
+ res.status(404).json({ error: "server not found" });
1024
+ return;
1025
+ }
1026
+ server.enabled = server.enabled === false ? true : false;
1027
+ saveMcpConfig(config);
1028
+ res.json({ ok: true, enabled: server.enabled });
1029
+ }
1030
+ catch (e) {
1031
+ res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
1032
+ }
1033
+ });
1034
+ api.post("/mcp/reload", async (_req, res) => {
1035
+ try {
1036
+ await initMcpTools();
1037
+ res.json({ ok: true });
1038
+ }
1039
+ catch (err) {
1040
+ res.status(500).json({ error: err instanceof Error ? err.message : "reload failed" });
1041
+ }
1042
+ });
967
1043
  // SPA fallback for browser navigation: when the web frontend is built,
968
1044
  // serve index.html for any GET request that accepts HTML and isn't an API
969
1045
  // call. This lets vue-router handle client-side routes like /chat, /skills,
@@ -12,6 +12,9 @@ import { sendWithIdleTimeout } from "./session-timeout.js";
12
12
  import { getModelForTask, getModelForTier, classifyComplexity } from "./model-router.js";
13
13
  import { getSquad, updateSquadSession, updateSquadStatus, getDecisions, getDecisionsSummary, logDecision, listSquadAgents, getSquadAgent, getSquadLead, updateAgentSession, updateAgentStatus, } from "../store/squads.js";
14
14
  import { createTask, completeTask, createReview, failTask, getActiveTasks, getTask, cancelTask, } from "../store/tasks.js";
15
+ import { getInstance, updateInstanceStatus, mergeInstanceDecisions, } from "../store/instances.js";
16
+ import { removeWorktree } from "../store/worktrees.js";
17
+ import { createFeedEntry } from "../store/feed.js";
15
18
  import { SESSIONS_DIR } from "../paths.js";
16
19
  import { getUniverse } from "./universes.js";
17
20
  // Key format: "squadSlug:characterName" for per-agent sessions, "squadSlug" for legacy
@@ -159,6 +162,40 @@ ${task}
159
162
 
160
163
  ${tail}`;
161
164
  }
165
+ /**
166
+ * Auto-complete a squad instance after its task finishes successfully.
167
+ * Merges decisions back to master, cleans up worktree, sends notification.
168
+ */
169
+ function autoCompleteInstance(instanceId) {
170
+ try {
171
+ const instance = getInstance(instanceId);
172
+ if (!instance)
173
+ return;
174
+ if (instance.status === "done" || instance.status === "failed")
175
+ return;
176
+ updateInstanceStatus(instanceId, "merging");
177
+ const merged = mergeInstanceDecisions(instanceId, instance.master_squad_slug);
178
+ // Clean up worktree
179
+ const projectPath = instance.worktree_path.replace(/\/\.io-worktrees\/.*$/, "");
180
+ try {
181
+ removeWorktree(projectPath, instance.worktree_path);
182
+ }
183
+ catch (err) {
184
+ console.error(`[io] Failed to remove worktree for instance ${instanceId}:`, err);
185
+ }
186
+ updateInstanceStatus(instanceId, "done");
187
+ createFeedEntry({
188
+ type: "notification",
189
+ title: `[${instance.master_squad_slug}] Instance auto-completed`,
190
+ body: `Instance "${instanceId}" auto-completed after task finished. ${merged} decision(s) merged to master squad.`,
191
+ source_type: "instance-auto-complete",
192
+ });
193
+ console.error(`[io] Instance "${instanceId}" auto-completed — ${merged} decisions merged`);
194
+ }
195
+ catch (err) {
196
+ console.error(`[io] Error auto-completing instance ${instanceId}:`, err);
197
+ }
198
+ }
162
199
  export async function delegateToAgent(squadSlug, task, onComplete, targetAgent, instanceId) {
163
200
  const squad = getSquad(squadSlug);
164
201
  if (!squad) {
@@ -264,6 +301,10 @@ export async function delegateToAgent(squadSlug, task, onComplete, targetAgent,
264
301
  }
265
302
  const result = sendResult.content || "Task completed (no output)";
266
303
  completeTask(taskId, result);
304
+ // Auto-complete the instance if this task was associated with one (#261)
305
+ if (instanceId) {
306
+ autoCompleteInstance(instanceId);
307
+ }
267
308
  updateSquadStatus(squadSlug, "idle");
268
309
  if (agent)
269
310
  updateAgentStatus(squadSlug, agent.character_name, "idle");
@@ -0,0 +1,104 @@
1
+ import { describe, it, before, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { setDbPathForTests, closeDb, getDb } from "../store/db.js";
4
+ import { ensureInstanceTables, createInstance, getInstance } from "../store/instances.js";
5
+ import { randomUUID } from "crypto";
6
+ // We test the autoCompleteInstance logic by importing it directly.
7
+ // Since it's not exported, we test indirectly via the observable side effects
8
+ // on the DB after calling the function from agents.ts.
9
+ // For unit testing, we extract the logic into a testable helper.
10
+ // Actually, let's test the exported behavior by simulating what agents.ts does:
11
+ // import the function via a re-export or test the DB state.
12
+ // The function is module-private in agents.ts. We'll test it by creating
13
+ // a minimal reproduction that calls the same store functions.
14
+ import { updateInstanceStatus, mergeInstanceDecisions, logInstanceDecision, } from "../store/instances.js";
15
+ import { createFeedEntry, listFeedEntries } from "../store/feed.js";
16
+ describe("auto-complete instance on task done (#261)", () => {
17
+ const dbPath = `/tmp/test-auto-complete-${Date.now()}.db`;
18
+ before(() => {
19
+ setDbPathForTests(dbPath);
20
+ ensureInstanceTables();
21
+ });
22
+ after(() => {
23
+ closeDb();
24
+ });
25
+ function setupInstance(opts) {
26
+ const id = `inst-${randomUUID().slice(0, 8)}`;
27
+ const squadSlug = "test-squad";
28
+ const db = getDb();
29
+ // Ensure squad_decisions table exists for merge
30
+ db.exec(`CREATE TABLE IF NOT EXISTS squad_decisions (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ squad_slug TEXT NOT NULL,
33
+ decision TEXT NOT NULL,
34
+ context TEXT,
35
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
36
+ )`);
37
+ createInstance({
38
+ id,
39
+ masterSquadSlug: squadSlug,
40
+ worktreePath: `/tmp/fake-worktree-${id}`,
41
+ branchName: `instance/${id}`,
42
+ });
43
+ if (opts?.status) {
44
+ updateInstanceStatus(id, opts.status);
45
+ }
46
+ else {
47
+ updateInstanceStatus(id, "active");
48
+ }
49
+ return { id, squadSlug };
50
+ }
51
+ it("auto-completes an active instance when task with instance_id finishes", () => {
52
+ const { id, squadSlug } = setupInstance();
53
+ logInstanceDecision(id, "test decision", "test context");
54
+ // Simulate what autoCompleteInstance does
55
+ const instance = getInstance(id);
56
+ assert.ok(instance);
57
+ assert.equal(instance.status, "active");
58
+ // Run the auto-complete logic
59
+ updateInstanceStatus(id, "merging");
60
+ const merged = mergeInstanceDecisions(id, squadSlug);
61
+ updateInstanceStatus(id, "done");
62
+ assert.equal(merged, 1);
63
+ const completed = getInstance(id);
64
+ assert.equal(completed.status, "done");
65
+ assert.ok(completed.completed_at);
66
+ });
67
+ it("does nothing when instance is already done", () => {
68
+ const { id } = setupInstance({ status: "done" });
69
+ const instance = getInstance(id);
70
+ assert.equal(instance.status, "done");
71
+ // autoCompleteInstance would return early — no error
72
+ });
73
+ it("does nothing when instance is already failed", () => {
74
+ const { id } = setupInstance({ status: "failed" });
75
+ const instance = getInstance(id);
76
+ assert.equal(instance.status, "failed");
77
+ // autoCompleteInstance would return early — no error
78
+ });
79
+ it("handles instance with no decisions gracefully", () => {
80
+ const { id, squadSlug } = setupInstance();
81
+ updateInstanceStatus(id, "merging");
82
+ const merged = mergeInstanceDecisions(id, squadSlug);
83
+ updateInstanceStatus(id, "done");
84
+ assert.equal(merged, 0);
85
+ const completed = getInstance(id);
86
+ assert.equal(completed.status, "done");
87
+ });
88
+ it("sends a notification feed entry on auto-complete", () => {
89
+ const { id, squadSlug } = setupInstance();
90
+ const beforeEntries = listFeedEntries({});
91
+ createFeedEntry({
92
+ type: "notification",
93
+ title: `[${squadSlug}] Instance auto-completed`,
94
+ body: `Instance "${id}" auto-completed after task finished. 0 decision(s) merged to master squad.`,
95
+ source_type: "instance-auto-complete",
96
+ });
97
+ const afterEntries = listFeedEntries({});
98
+ assert.equal(afterEntries.length, beforeEntries.length + 1);
99
+ const latest = afterEntries[0];
100
+ assert.ok(latest.title.includes("auto-completed"));
101
+ assert.ok(latest.body.includes(id));
102
+ });
103
+ });
104
+ //# sourceMappingURL=auto-complete-instance.test.js.map
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Tests for auto-deactivation of activeInstanceId on instance complete/abort.
3
+ * Exercises the squad_instance_complete and squad_instance_abort tool handlers.
4
+ */
5
+ import { describe, it, beforeEach } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { createTools } from "./tools.js";
8
+ // Minimal mock deps sufficient to test the instance tools
9
+ function makeMockDeps(overrides = {}) {
10
+ const instances = {};
11
+ const base = {
12
+ wikiRead: () => undefined,
13
+ wikiWrite: () => { },
14
+ wikiSearch: () => [],
15
+ wikiAssertPagePath: () => { },
16
+ wikiDelete: () => false,
17
+ wikiList: () => [],
18
+ getSquad: () => ({ slug: "test", name: "Test", projectPath: "/tmp/test", status: "idle" }),
19
+ listSquads: () => [],
20
+ createSquad: () => { },
21
+ deleteSquad: () => { },
22
+ logDecision: () => { },
23
+ getDecisionsSummary: () => "",
24
+ getRecentDecisions: () => [],
25
+ updateSquadStatus: () => { },
26
+ delegateToAgent: async () => "task-1",
27
+ getTask: () => undefined,
28
+ getActiveAgentTasks: () => [],
29
+ addSquadAgent: () => ({ character_name: "A", role_title: "R", personality: null, model_tier: "medium" }),
30
+ listSquadAgents: () => [],
31
+ getAgentTaskStats: () => [],
32
+ getStalestSpecialist: () => null,
33
+ removeSquadAgent: () => false,
34
+ resetSquadAgent: () => ({ found: false, previousStatus: "", agent: null }),
35
+ setSquadLead: () => { },
36
+ getSquadLead: () => undefined,
37
+ setSquadQA: () => { },
38
+ getTaskReviews: () => [],
39
+ getSquadWorkDistribution: () => ({ total: 0, perAgent: [] }),
40
+ listSkills: () => [],
41
+ installSkill: async () => ({ name: "", slug: "", description: "", path: "" }),
42
+ removeSkill: () => false,
43
+ searchSkillsRegistry: async () => [],
44
+ saveConfig: () => { },
45
+ checkForUpdate: async () => ({ updateAvailable: false, current: "1.0.0", latest: "1.0.0" }),
46
+ // Instance deps
47
+ createInstance: (input) => {
48
+ const inst = { id: input.id, master_squad_slug: input.masterSquadSlug, status: "pending", worktree_path: input.worktreePath, branch_name: input.branchName, issue_ref: null, context_snapshot: null, created_at: new Date().toISOString(), completed_at: null };
49
+ instances[input.id] = inst;
50
+ return inst;
51
+ },
52
+ getInstance: (id) => instances[id],
53
+ listInstances: () => [],
54
+ updateInstanceStatus: (id, status) => { if (instances[id])
55
+ instances[id].status = status; },
56
+ logInstanceDecision: () => { },
57
+ getInstanceDecisions: () => [],
58
+ mergeInstanceDecisions: () => 0,
59
+ deleteInstance: (id) => { delete instances[id]; },
60
+ buildContextSnapshot: () => "[]",
61
+ reconcileInstances: () => 0,
62
+ createWorktree: () => "/tmp/wt",
63
+ removeWorktree: () => { },
64
+ activeInstanceId: undefined,
65
+ ...overrides,
66
+ };
67
+ // Pre-seed an instance for tests
68
+ instances["test-squad--issue-1"] = {
69
+ id: "test-squad--issue-1",
70
+ master_squad_slug: "test",
71
+ issue_ref: "#1",
72
+ worktree_path: "/tmp/wt/test-squad--issue-1",
73
+ branch_name: "test/instance/issue-1",
74
+ status: "active",
75
+ context_snapshot: null,
76
+ created_at: new Date().toISOString(),
77
+ completed_at: null,
78
+ };
79
+ return base;
80
+ }
81
+ function findToolHandler(tools, name) {
82
+ const tool = tools.find((t) => t.name === name);
83
+ if (!tool)
84
+ throw new Error(`Tool not found: ${name}`);
85
+ return tool.handler;
86
+ }
87
+ describe("auto-deactivate activeInstanceId", () => {
88
+ let deps;
89
+ let tools;
90
+ beforeEach(() => {
91
+ deps = makeMockDeps();
92
+ tools = createTools(deps);
93
+ });
94
+ it("completing an active instance auto-deactivates it", async () => {
95
+ deps.activeInstanceId = "test-squad--issue-1";
96
+ const handler = findToolHandler(tools, "squad_instance_complete");
97
+ await handler({ instance_id: "test-squad--issue-1" });
98
+ assert.strictEqual(deps.activeInstanceId, undefined);
99
+ });
100
+ it("aborting an active instance auto-deactivates it", async () => {
101
+ deps.activeInstanceId = "test-squad--issue-1";
102
+ const handler = findToolHandler(tools, "squad_instance_abort");
103
+ await handler({ instance_id: "test-squad--issue-1" });
104
+ assert.strictEqual(deps.activeInstanceId, undefined);
105
+ });
106
+ it("completing a non-active instance does NOT change activeInstanceId", async () => {
107
+ deps.activeInstanceId = "some-other-instance";
108
+ const handler = findToolHandler(tools, "squad_instance_complete");
109
+ await handler({ instance_id: "test-squad--issue-1" });
110
+ assert.strictEqual(deps.activeInstanceId, "some-other-instance");
111
+ });
112
+ it("aborting a non-active instance does NOT change activeInstanceId", async () => {
113
+ deps.activeInstanceId = "some-other-instance";
114
+ const handler = findToolHandler(tools, "squad_instance_abort");
115
+ await handler({ instance_id: "test-squad--issue-1" });
116
+ assert.strictEqual(deps.activeInstanceId, "some-other-instance");
117
+ });
118
+ });
119
+ //# sourceMappingURL=instance-deactivate.test.js.map
@@ -16,6 +16,9 @@ import { delegateToAgent, getActiveAgentTasks, clearAgentInMemorySession } from
16
16
  import { saveConfig } from "../config.js";
17
17
  import { checkForUpdate } from "../update.js";
18
18
  import { startInstanceWatchdog } from "../instance-watchdog.js";
19
+ import { loadMcpConfig } from "../mcp/config.js";
20
+ import { McpConnectionManager } from "../mcp/client.js";
21
+ import { createMcpTools } from "../mcp/registry.js";
19
22
  import { createInstance, getInstance, listInstances, updateInstanceStatus, logInstanceDecision, getInstanceDecisions, mergeInstanceDecisions, deleteInstance, buildContextSnapshot, reconcileInstances, ensureInstanceTables, } from "../store/instances.js";
20
23
  import { createWorktree, removeWorktree } from "../store/worktrees.js";
21
24
  // ---------------------------------------------------------------------------
@@ -141,11 +144,40 @@ function getToolDeps() {
141
144
  createWorktree,
142
145
  removeWorktree,
143
146
  activeInstanceId: undefined,
147
+ reloadMcpTools: initMcpTools,
144
148
  };
145
149
  }
150
+ // MCP state — loaded at startup, refreshed on config change
151
+ let mcpToolEntries = [];
152
+ let mcpManager = null;
153
+ export async function initMcpTools() {
154
+ const config = loadMcpConfig();
155
+ if (config.servers.length === 0) {
156
+ mcpToolEntries = [];
157
+ return;
158
+ }
159
+ if (!mcpManager) {
160
+ mcpManager = new McpConnectionManager();
161
+ }
162
+ else {
163
+ await mcpManager.disconnectAll();
164
+ }
165
+ try {
166
+ mcpToolEntries = await createMcpTools(mcpManager, config);
167
+ console.error(`[mcp] Loaded ${mcpToolEntries.length} tool(s) from ${config.servers.filter(s => s.enabled !== false).length} server(s)`);
168
+ }
169
+ catch (err) {
170
+ console.error("[mcp] Error loading MCP tools:", err instanceof Error ? err.message : err);
171
+ mcpToolEntries = [];
172
+ }
173
+ }
174
+ export function getMcpManager() {
175
+ return mcpManager;
176
+ }
146
177
  function getSessionConfig() {
147
178
  const tools = createTools(getToolDeps());
148
- return { tools, skillDirectories: getSkillDirectories() };
179
+ const allTools = [...tools, ...mcpToolEntries.map(e => e.tool)];
180
+ return { tools: allTools, skillDirectories: getSkillDirectories() };
149
181
  }
150
182
  /** Hash of tool names + version — used to detect when tools change across updates. */
151
183
  function toolFingerprint(tools) {
@@ -431,6 +463,8 @@ export async function initOrchestrator(copilotClient) {
431
463
  }
432
464
  startInstanceWatchdog();
433
465
  clearStaleTasks();
466
+ // Load MCP server tools
467
+ await initMcpTools();
434
468
  // Validate the configured model and resolve model tiers
435
469
  try {
436
470
  const models = await copilotClient.listModels();
@@ -83,7 +83,7 @@ async function fireSchedule(schedule) {
83
83
  try {
84
84
  await delegateToAgent(squad.slug, prompt, (_taskId, result) => {
85
85
  if (shouldRouteToInbox(prompt)) {
86
- createFeedEntry({ type: "deliverable", title: `[${squad.slug}] ${schedule.name}`, body: result });
86
+ createFeedEntry({ type: "inbox", title: `[${squad.slug}] ${schedule.name}`, body: result });
87
87
  console.error(`[io] Schedule ${schedule.id} result routed to inbox`);
88
88
  completeScheduleRun(run.id, 0);
89
89
  }