heyio 0.30.0 → 0.31.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.
@@ -105,7 +105,7 @@ export async function startApiServer() {
105
105
  api.get("/feed/count", (req, res) => {
106
106
  try {
107
107
  const rawType = req.query.type;
108
- const type = rawType === "deliverable" || rawType === "notification"
108
+ const type = rawType === "inbox" || rawType === "notification"
109
109
  ? rawType
110
110
  : undefined;
111
111
  const count = countUnreadFeedEntries(type);
@@ -119,7 +119,7 @@ export async function startApiServer() {
119
119
  api.get("/feed", (req, res) => {
120
120
  try {
121
121
  const rawType = req.query.type;
122
- const type = rawType === "deliverable" || rawType === "notification"
122
+ const type = rawType === "inbox" || rawType === "notification"
123
123
  ? rawType
124
124
  : undefined;
125
125
  const unreadOnly = req.query.unread === "true";
@@ -253,7 +253,7 @@ export async function startApiServer() {
253
253
  api.post("/feed/read-all", (req, res) => {
254
254
  try {
255
255
  const rawType = req.query.type;
256
- const type = rawType === "deliverable" || rawType === "notification"
256
+ const type = rawType === "inbox" || rawType === "notification"
257
257
  ? rawType
258
258
  : undefined;
259
259
  const marked = markAllFeedEntriesRead(type);
@@ -316,8 +316,8 @@ export async function startApiServer() {
316
316
  });
317
317
  api.post("/feed", (req, res) => {
318
318
  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'" });
319
+ if (type !== "inbox" && type !== "notification") {
320
+ res.status(400).json({ error: "type must be 'inbox' or 'notification'" });
321
321
  return;
322
322
  }
323
323
  if (!title || typeof title !== "string" || title.trim() === "") {
@@ -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
@@ -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
  }
@@ -407,7 +407,7 @@ export function createTools(deps) {
407
407
  const taskId = await deps.delegateToAgent(slug, task, (id, result) => {
408
408
  console.error(`[io] Agent task ${id} completed for squad ${slug}`);
409
409
  if (shouldRouteToInbox(task)) {
410
- createFeedEntry({ type: "deliverable", title: `[${slug}] Task result`, body: result });
410
+ createFeedEntry({ type: "inbox", title: `[${slug}] Task result`, body: result, squad_slug: slug });
411
411
  console.error(`[io] Task ${id} result routed to inbox`);
412
412
  }
413
413
  }, agent, deps.activeInstanceId);
@@ -1811,6 +1811,10 @@ export function createTools(deps) {
1811
1811
  const projectPath = squad?.projectPath ?? instance.worktree_path.replace(/\/\.io-worktrees\/.*$/, "");
1812
1812
  deps.removeWorktree(projectPath, instance.worktree_path);
1813
1813
  deps.updateInstanceStatus(instance_id, "done");
1814
+ // Auto-deactivate if this was the active instance
1815
+ if (deps.activeInstanceId === instance_id) {
1816
+ deps.activeInstanceId = undefined;
1817
+ }
1814
1818
  return `Instance "${instance_id}" completed.\n- ${merged} decision(s) merged to master squad "${instance.master_squad_slug}"\n- Worktree cleaned up`;
1815
1819
  }
1816
1820
  catch (err) {
@@ -1832,6 +1836,10 @@ export function createTools(deps) {
1832
1836
  return `Instance already in terminal state: ${instance.status}`;
1833
1837
  }
1834
1838
  deps.updateInstanceStatus(instance_id, "failed");
1839
+ // Auto-deactivate if this was the active instance
1840
+ if (deps.activeInstanceId === instance_id) {
1841
+ deps.activeInstanceId = undefined;
1842
+ }
1835
1843
  return `Instance "${instance_id}" aborted. Worktree preserved at: ${instance.worktree_path}\nUse squad_instance_cleanup to remove it.`;
1836
1844
  },
1837
1845
  });
@@ -1885,7 +1893,48 @@ export function createTools(deps) {
1885
1893
  return prev ? `Instance context deactivated (was: ${prev})` : `No instance context was active.`;
1886
1894
  },
1887
1895
  });
1888
- return [wikiRead, wikiWrite, wikiSearch, wikiDelete, wikiList, squadCreate, squadRecall, squadStatus, squadLogDecision, squadDelegate, squadTaskStatus, squadDelete, squadAnalyze, squadAddAgent, squadAgents, squadRemoveAgent, squadResetAgent, squadSetLead, squadSetQA, squadTaskReviews, squadScheduleCreate, squadScheduleList, squadScheduleDelete, squadSchedulePause, squadScheduleResume, squadScheduleRunNow, scheduleCreate, scheduleList, scheduleDelete, schedulePause, scheduleResume, scheduleRunNow, skillList, skillInstall, skillRemove, skillSearch, configUpdate, checkUpdate, shell, fileOps, bash, readFile, viewTool, grepTool, strReplaceEditor, github, squadInstanceCreate, squadInstanceList, squadInstanceStatus, squadInstanceComplete, squadInstanceAbort, squadInstanceCleanup, squadInstanceActivate, squadInstanceDeactivate];
1896
+ const sendToInbox = defineTool("send_to_inbox", {
1897
+ description: "Send a message directly to Michael's IO inbox. Use this to deliver results, reports, summaries, or any content that should appear in the inbox feed.",
1898
+ skipPermission: true,
1899
+ parameters: z.object({
1900
+ title: z.string().describe("Short title for the inbox item"),
1901
+ body: z.string().describe("Full content/body of the message (supports markdown)"),
1902
+ squad_slug: z.string().optional().describe("Squad slug to prefix the title with (e.g. 'io-assistant')"),
1903
+ instance_id: z.string().optional().describe("Instance ID if this message is from a squad instance"),
1904
+ task_id: z.string().optional().describe("Task ID associated with this message"),
1905
+ }),
1906
+ handler: async ({ title, body, squad_slug, instance_id, task_id }) => {
1907
+ const prefix = squad_slug ? `[${squad_slug}] ` : "";
1908
+ createFeedEntry({
1909
+ type: "inbox",
1910
+ title: `${prefix}${title}`,
1911
+ body,
1912
+ squad_slug: squad_slug ?? null,
1913
+ instance_id: instance_id ?? null,
1914
+ task_id: task_id ?? null,
1915
+ });
1916
+ return "Message sent to inbox successfully.";
1917
+ },
1918
+ });
1919
+ const sendNotification = defineTool("send_notification", {
1920
+ description: "Send a short status notification to the IO feed. Use for brief updates, alerts, and FYIs (one sentence). For longer content, use send_to_inbox instead.",
1921
+ skipPermission: true,
1922
+ parameters: z.object({
1923
+ message: z.string().describe("Short notification message (one sentence)"),
1924
+ squad_slug: z.string().optional().describe("Squad slug for context"),
1925
+ }),
1926
+ handler: async ({ message, squad_slug }) => {
1927
+ const prefix = squad_slug ? `[${squad_slug}] ` : "";
1928
+ createFeedEntry({
1929
+ type: "notification",
1930
+ title: `${prefix}${message}`,
1931
+ body: message,
1932
+ squad_slug: squad_slug ?? null,
1933
+ });
1934
+ return "Notification sent.";
1935
+ },
1936
+ });
1937
+ return [wikiRead, wikiWrite, wikiSearch, wikiDelete, wikiList, squadCreate, squadRecall, squadStatus, squadLogDecision, squadDelegate, squadTaskStatus, squadDelete, squadAnalyze, squadAddAgent, squadAgents, squadRemoveAgent, squadResetAgent, squadSetLead, squadSetQA, squadTaskReviews, squadScheduleCreate, squadScheduleList, squadScheduleDelete, squadSchedulePause, squadScheduleResume, squadScheduleRunNow, scheduleCreate, scheduleList, scheduleDelete, schedulePause, scheduleResume, scheduleRunNow, skillList, skillInstall, skillRemove, skillSearch, configUpdate, checkUpdate, shell, fileOps, bash, readFile, viewTool, grepTool, strReplaceEditor, github, squadInstanceCreate, squadInstanceList, squadInstanceStatus, squadInstanceComplete, squadInstanceAbort, squadInstanceCleanup, squadInstanceActivate, squadInstanceDeactivate, sendToInbox, sendNotification];
1889
1938
  }
1890
1939
  function walkDirectory(dir, maxDepth = 3, depth = 0) {
1891
1940
  if (depth >= maxDepth)
package/dist/store/db.js CHANGED
@@ -162,7 +162,7 @@ GROUP BY agent_slug`,
162
162
  )`,
163
163
  `CREATE TABLE IF NOT EXISTS unified_feed (
164
164
  id INTEGER PRIMARY KEY AUTOINCREMENT,
165
- type TEXT NOT NULL CHECK(type IN ('deliverable', 'notification')),
165
+ type TEXT NOT NULL CHECK(type IN ('inbox', 'notification')),
166
166
  title TEXT NOT NULL,
167
167
  body TEXT NOT NULL,
168
168
  source_type TEXT,
@@ -193,6 +193,26 @@ GROUP BY agent_slug`,
193
193
  )`,
194
194
  `ALTER TABLE agent_tasks ADD COLUMN instance_id TEXT`,
195
195
  `CREATE INDEX IF NOT EXISTS idx_instance_decisions_instance ON instance_decisions(instance_id, merged_to_master)`,
196
+ `ALTER TABLE unified_feed RENAME TO unified_feed_old`,
197
+ `CREATE TABLE unified_feed (
198
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
199
+ type TEXT NOT NULL CHECK(type IN ('inbox', 'notification')),
200
+ title TEXT NOT NULL,
201
+ body TEXT NOT NULL,
202
+ source_type TEXT,
203
+ source_ref TEXT,
204
+ squad_slug TEXT,
205
+ instance_id TEXT,
206
+ task_id TEXT,
207
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
208
+ read_at DATETIME
209
+ )`,
210
+ `INSERT INTO unified_feed (id, type, title, body, source_type, source_ref, created_at, read_at)
211
+ SELECT id, CASE WHEN type='deliverable' THEN 'inbox' ELSE type END, title, body, source_type, source_ref, created_at, read_at
212
+ FROM unified_feed_old`,
213
+ `DROP TABLE unified_feed_old`,
214
+ `CREATE INDEX IF NOT EXISTS idx_unified_feed_type ON unified_feed(type, created_at)`,
215
+ `CREATE INDEX IF NOT EXISTS idx_unified_feed_unread ON unified_feed(read_at, created_at)`,
196
216
  ];
197
217
  for (const migration of migrations) {
198
218
  try {
@@ -2,9 +2,9 @@ import { getDb } from "./db.js";
2
2
  export function createFeedEntry(input) {
3
3
  const db = getDb();
4
4
  const info = db
5
- .prepare(`INSERT INTO unified_feed (type, title, body, source_type, source_ref)
6
- VALUES (?, ?, ?, ?, ?)`)
7
- .run(input.type, input.title, input.body, input.source_type ?? null, input.source_ref ?? null);
5
+ .prepare(`INSERT INTO unified_feed (type, title, body, source_type, source_ref, squad_slug, instance_id, task_id)
6
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
7
+ .run(input.type, input.title, input.body, input.source_type ?? null, input.source_ref ?? null, input.squad_slug ?? null, input.instance_id ?? null, input.task_id ?? null);
8
8
  return db
9
9
  .prepare("SELECT * FROM unified_feed WHERE id = ?")
10
10
  .get(info.lastInsertRowid);
@@ -27,8 +27,8 @@ beforeEach(() => {
27
27
  // ── createFeedEntry ───────────────────────────────────────────────────────────
28
28
  describe("createFeedEntry", () => {
29
29
  it("creates a deliverable entry with correct fields", () => {
30
- const entry = createFeedEntry({ type: "deliverable", title: "Task done", body: "Here are the results." });
31
- assert.equal(entry.type, "deliverable");
30
+ const entry = createFeedEntry({ type: "inbox", title: "Task done", body: "Here are the results." });
31
+ assert.equal(entry.type, "inbox");
32
32
  assert.equal(entry.title, "Task done");
33
33
  assert.equal(entry.body, "Here are the results.");
34
34
  assert.equal(entry.read_at, null);
@@ -54,7 +54,7 @@ describe("createFeedEntry", () => {
54
54
  assert.equal(entry.source_ref, JSON.stringify({ id: 42 }));
55
55
  });
56
56
  it("autoincrements ids", () => {
57
- const a = createFeedEntry({ type: "deliverable", title: "A", body: "a" });
57
+ const a = createFeedEntry({ type: "inbox", title: "A", body: "a" });
58
58
  const b = createFeedEntry({ type: "notification", title: "B", body: "b" });
59
59
  assert.ok(b.id > a.id);
60
60
  });
@@ -62,7 +62,7 @@ describe("createFeedEntry", () => {
62
62
  // ── listFeedEntries ───────────────────────────────────────────────────────────
63
63
  describe("listFeedEntries", () => {
64
64
  it("returns all entries newest first", () => {
65
- createFeedEntry({ type: "deliverable", title: "First", body: "x" });
65
+ createFeedEntry({ type: "inbox", title: "First", body: "x" });
66
66
  createFeedEntry({ type: "notification", title: "Second", body: "y" });
67
67
  const entries = listFeedEntries();
68
68
  assert.equal(entries.length, 2);
@@ -70,14 +70,14 @@ describe("listFeedEntries", () => {
70
70
  assert.equal(entries[1].title, "First");
71
71
  });
72
72
  it("filters by type=deliverable", () => {
73
- createFeedEntry({ type: "deliverable", title: "D", body: "d" });
73
+ createFeedEntry({ type: "inbox", title: "D", body: "d" });
74
74
  createFeedEntry({ type: "notification", title: "N", body: "n" });
75
- const entries = listFeedEntries({ type: "deliverable" });
75
+ const entries = listFeedEntries({ type: "inbox" });
76
76
  assert.equal(entries.length, 1);
77
- assert.equal(entries[0].type, "deliverable");
77
+ assert.equal(entries[0].type, "inbox");
78
78
  });
79
79
  it("filters by type=notification", () => {
80
- createFeedEntry({ type: "deliverable", title: "D", body: "d" });
80
+ createFeedEntry({ type: "inbox", title: "D", body: "d" });
81
81
  createFeedEntry({ type: "notification", title: "N", body: "n" });
82
82
  const entries = listFeedEntries({ type: "notification" });
83
83
  assert.equal(entries.length, 1);
@@ -108,20 +108,20 @@ describe("countUnreadFeedEntries", () => {
108
108
  assert.equal(countUnreadFeedEntries(), 0);
109
109
  });
110
110
  it("increments on insert", () => {
111
- createFeedEntry({ type: "deliverable", title: "T", body: "b" });
111
+ createFeedEntry({ type: "inbox", title: "T", body: "b" });
112
112
  assert.equal(countUnreadFeedEntries(), 1);
113
113
  createFeedEntry({ type: "notification", title: "N", body: "b" });
114
114
  assert.equal(countUnreadFeedEntries(), 2);
115
115
  });
116
116
  it("decreases when marked read", () => {
117
- const e = createFeedEntry({ type: "deliverable", title: "T", body: "b" });
117
+ const e = createFeedEntry({ type: "inbox", title: "T", body: "b" });
118
118
  markFeedEntryRead(e.id);
119
119
  assert.equal(countUnreadFeedEntries(), 0);
120
120
  });
121
121
  it("filters by type", () => {
122
- createFeedEntry({ type: "deliverable", title: "D", body: "d" });
122
+ createFeedEntry({ type: "inbox", title: "D", body: "d" });
123
123
  createFeedEntry({ type: "notification", title: "N", body: "n" });
124
- assert.equal(countUnreadFeedEntries("deliverable"), 1);
124
+ assert.equal(countUnreadFeedEntries("inbox"), 1);
125
125
  assert.equal(countUnreadFeedEntries("notification"), 1);
126
126
  });
127
127
  });
@@ -158,11 +158,11 @@ describe("markAllFeedEntriesRead", () => {
158
158
  assert.equal(markAllFeedEntriesRead(), 0);
159
159
  });
160
160
  it("respects type filter — only marks matching type", () => {
161
- createFeedEntry({ type: "deliverable", title: "D", body: "d" });
161
+ createFeedEntry({ type: "inbox", title: "D", body: "d" });
162
162
  createFeedEntry({ type: "notification", title: "N", body: "n" });
163
163
  const count = markAllFeedEntriesRead("notification");
164
164
  assert.equal(count, 1);
165
- assert.equal(countUnreadFeedEntries("deliverable"), 1);
165
+ assert.equal(countUnreadFeedEntries("inbox"), 1);
166
166
  assert.equal(countUnreadFeedEntries("notification"), 0);
167
167
  });
168
168
  });
@@ -170,7 +170,7 @@ describe("markAllFeedEntriesRead", () => {
170
170
  describe("markFeedEntriesRead", () => {
171
171
  it("marks multiple entries read and returns change count", () => {
172
172
  const a = createFeedEntry({ type: "notification", title: "A", body: "a" });
173
- const b = createFeedEntry({ type: "deliverable", title: "B", body: "b" });
173
+ const b = createFeedEntry({ type: "inbox", title: "B", body: "b" });
174
174
  const c = createFeedEntry({ type: "notification", title: "C", body: "c" });
175
175
  const count = markFeedEntriesRead([a.id, b.id, c.id]);
176
176
  assert.equal(count, 3);
@@ -182,7 +182,7 @@ describe("markFeedEntriesRead", () => {
182
182
  assert.equal(countUnreadFeedEntries(), 1);
183
183
  });
184
184
  it("works correctly for a single id", () => {
185
- const e = createFeedEntry({ type: "deliverable", title: "Solo", body: "b" });
185
+ const e = createFeedEntry({ type: "inbox", title: "Solo", body: "b" });
186
186
  assert.equal(markFeedEntriesRead([e.id]), 1);
187
187
  const entries = listFeedEntries();
188
188
  assert.ok(entries[0].read_at !== null);
@@ -211,13 +211,13 @@ describe("deleteFeedEntry", () => {
211
211
  assert.equal(deleteFeedEntry(9999), false);
212
212
  });
213
213
  it("returns true and removes the entry", () => {
214
- const e = createFeedEntry({ type: "deliverable", title: "T", body: "b" });
214
+ const e = createFeedEntry({ type: "inbox", title: "T", body: "b" });
215
215
  assert.equal(deleteFeedEntry(e.id), true);
216
216
  const entries = listFeedEntries();
217
217
  assert.equal(entries.find((x) => x.id === e.id), undefined);
218
218
  });
219
219
  it("second delete returns false (not idempotent)", () => {
220
- const e = createFeedEntry({ type: "deliverable", title: "T", body: "b" });
220
+ const e = createFeedEntry({ type: "inbox", title: "T", body: "b" });
221
221
  deleteFeedEntry(e.id);
222
222
  assert.equal(deleteFeedEntry(e.id), false);
223
223
  });
@@ -226,7 +226,7 @@ describe("deleteFeedEntry", () => {
226
226
  describe("deleteFeedEntries", () => {
227
227
  it("deletes multiple entries and returns change count", () => {
228
228
  const a = createFeedEntry({ type: "notification", title: "A", body: "a" });
229
- const b = createFeedEntry({ type: "deliverable", title: "B", body: "b" });
229
+ const b = createFeedEntry({ type: "inbox", title: "B", body: "b" });
230
230
  const c = createFeedEntry({ type: "notification", title: "C", body: "c" });
231
231
  const count = deleteFeedEntries([a.id, b.id, c.id]);
232
232
  assert.equal(count, 3);
@@ -249,7 +249,7 @@ describe("deleteFeedEntries", () => {
249
249
  assert.equal(deleteFeedEntries([9991, 9992, 9993]), 0);
250
250
  });
251
251
  it("mix of existing and non-existent ids — only deletes what exists", () => {
252
- const e = createFeedEntry({ type: "deliverable", title: "Real", body: "b" });
252
+ const e = createFeedEntry({ type: "inbox", title: "Real", body: "b" });
253
253
  const count = deleteFeedEntries([e.id, 9999]);
254
254
  assert.equal(count, 1);
255
255
  assert.deepEqual(listFeedEntries(), []);
@@ -133,7 +133,7 @@ export function logDecision(squadSlug, decision, context) {
133
133
  }
134
134
  export function getDecisions(squadSlug, limit = 20) {
135
135
  return getDb()
136
- .prepare("SELECT * FROM squad_decisions WHERE squad_slug = ? ORDER BY created_at DESC LIMIT ?")
136
+ .prepare("SELECT * FROM squad_decisions WHERE squad_slug = ? ORDER BY created_at DESC, id DESC LIMIT ?")
137
137
  .all(squadSlug, limit);
138
138
  }
139
139
  export function getDecisionsSummary(squadSlug) {