@ai-devkit/agent-manager 0.41.0 → 0.42.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.
Files changed (101) hide show
  1. package/dist/AgentManager.d.ts.map +1 -1
  2. package/dist/AgentManager.js +25 -9
  3. package/dist/AgentManager.js.map +1 -1
  4. package/dist/__tests__/fixtures.test.js.map +1 -1
  5. package/dist/__tests__/harnesses/devin/DevinAdapter.test.js.map +1 -1
  6. package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js.map +1 -1
  7. package/dist/__tests__/harnesses/muse/MuseAdapter.test.js +247 -0
  8. package/dist/__tests__/harnesses/muse/MuseAdapter.test.js.map +1 -0
  9. package/dist/__tests__/harnesses/muse/MuseAgentMapper.test.js +81 -0
  10. package/dist/__tests__/harnesses/muse/MuseAgentMapper.test.js.map +1 -0
  11. package/dist/__tests__/harnesses/muse/MuseSessionLocator.test.js +185 -0
  12. package/dist/__tests__/harnesses/muse/MuseSessionLocator.test.js.map +1 -0
  13. package/dist/__tests__/harnesses/muse/MuseSessionParser.test.js +328 -0
  14. package/dist/__tests__/harnesses/muse/MuseSessionParser.test.js.map +1 -0
  15. package/dist/__tests__/harnesses/opencode/OpenCodeSessionParser.test.js.map +1 -1
  16. package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js +42 -1
  17. package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js.map +1 -1
  18. package/dist/__tests__/harnesses/runtimeProfiles.test.js +12 -0
  19. package/dist/__tests__/harnesses/runtimeProfiles.test.js.map +1 -1
  20. package/dist/__tests__/utils/process.test.js +71 -1
  21. package/dist/__tests__/utils/process.test.js.map +1 -1
  22. package/dist/adapters/AgentAdapter.d.ts +8 -1
  23. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  24. package/dist/adapters/AgentAdapter.js +2 -1
  25. package/dist/adapters/AgentAdapter.js.map +1 -1
  26. package/dist/fixtures/capture.d.ts.map +1 -1
  27. package/dist/fixtures/capture.js +5 -4
  28. package/dist/fixtures/capture.js.map +1 -1
  29. package/dist/harnesses/devin/DevinAdapter.d.ts.map +1 -1
  30. package/dist/harnesses/devin/DevinAdapter.js.map +1 -1
  31. package/dist/harnesses/devin/DevinAgentMapper.d.ts.map +1 -1
  32. package/dist/harnesses/devin/DevinAgentMapper.js.map +1 -1
  33. package/dist/harnesses/devin/DevinSessionParser.d.ts.map +1 -1
  34. package/dist/harnesses/devin/DevinSessionParser.js.map +1 -1
  35. package/dist/harnesses/index.d.ts.map +1 -1
  36. package/dist/harnesses/index.js +3 -1
  37. package/dist/harnesses/index.js.map +1 -1
  38. package/dist/harnesses/muse/MuseAdapter.d.ts +33 -0
  39. package/dist/harnesses/muse/MuseAdapter.d.ts.map +1 -0
  40. package/dist/harnesses/muse/MuseAdapter.js +122 -0
  41. package/dist/harnesses/muse/MuseAdapter.js.map +1 -0
  42. package/dist/harnesses/muse/MuseAgentMapper.d.ts +15 -0
  43. package/dist/harnesses/muse/MuseAgentMapper.d.ts.map +1 -0
  44. package/dist/harnesses/muse/MuseAgentMapper.js +29 -0
  45. package/dist/harnesses/muse/MuseAgentMapper.js.map +1 -0
  46. package/dist/harnesses/muse/MuseSessionLocator.d.ts +61 -0
  47. package/dist/harnesses/muse/MuseSessionLocator.d.ts.map +1 -0
  48. package/dist/harnesses/muse/MuseSessionLocator.js +233 -0
  49. package/dist/harnesses/muse/MuseSessionLocator.js.map +1 -0
  50. package/dist/harnesses/muse/MuseSessionParser.d.ts +77 -0
  51. package/dist/harnesses/muse/MuseSessionParser.d.ts.map +1 -0
  52. package/dist/harnesses/muse/MuseSessionParser.js +350 -0
  53. package/dist/harnesses/muse/MuseSessionParser.js.map +1 -0
  54. package/dist/harnesses/muse/readiness.d.ts +3 -0
  55. package/dist/harnesses/muse/readiness.d.ts.map +1 -0
  56. package/dist/harnesses/muse/readiness.js +37 -0
  57. package/dist/harnesses/muse/readiness.js.map +1 -0
  58. package/dist/harnesses/readiness/AgentReadiness.d.ts.map +1 -1
  59. package/dist/harnesses/readiness/AgentReadiness.js +3 -1
  60. package/dist/harnesses/readiness/AgentReadiness.js.map +1 -1
  61. package/dist/harnesses/readiness/types.d.ts +1 -1
  62. package/dist/harnesses/readiness/types.d.ts.map +1 -1
  63. package/dist/harnesses/runtimeProfiles.d.ts.map +1 -1
  64. package/dist/harnesses/runtimeProfiles.js +2 -1
  65. package/dist/harnesses/runtimeProfiles.js.map +1 -1
  66. package/dist/harnesses/shared.d.ts +1 -1
  67. package/dist/harnesses/shared.d.ts.map +1 -1
  68. package/dist/harnesses/shared.js +6 -5
  69. package/dist/harnesses/shared.js.map +1 -1
  70. package/dist/utils/process.d.ts +23 -1
  71. package/dist/utils/process.d.ts.map +1 -1
  72. package/dist/utils/process.js +44 -8
  73. package/dist/utils/process.js.map +1 -1
  74. package/package.json +1 -1
  75. package/src/AgentManager.ts +34 -8
  76. package/src/__tests__/fixtures.test.ts +15 -17
  77. package/src/__tests__/harnesses/devin/DevinAdapter.test.ts +1 -3
  78. package/src/__tests__/harnesses/devin/DevinSessionParser.test.ts +3 -9
  79. package/src/__tests__/harnesses/muse/MuseAdapter.test.ts +197 -0
  80. package/src/__tests__/harnesses/muse/MuseAgentMapper.test.ts +83 -0
  81. package/src/__tests__/harnesses/muse/MuseSessionLocator.test.ts +177 -0
  82. package/src/__tests__/harnesses/muse/MuseSessionParser.test.ts +279 -0
  83. package/src/__tests__/harnesses/opencode/OpenCodeSessionParser.test.ts +3 -2
  84. package/src/__tests__/harnesses/readiness/AgentReadiness.test.ts +36 -0
  85. package/src/__tests__/harnesses/runtimeProfiles.test.ts +13 -0
  86. package/src/__tests__/utils/process.test.ts +30 -0
  87. package/src/adapters/AgentAdapter.ts +9 -0
  88. package/src/fixtures/capture.ts +32 -17
  89. package/src/harnesses/devin/DevinAdapter.ts +3 -15
  90. package/src/harnesses/devin/DevinAgentMapper.ts +1 -5
  91. package/src/harnesses/devin/DevinSessionParser.ts +3 -12
  92. package/src/harnesses/index.ts +2 -0
  93. package/src/harnesses/muse/MuseAdapter.ts +157 -0
  94. package/src/harnesses/muse/MuseAgentMapper.ts +33 -0
  95. package/src/harnesses/muse/MuseSessionLocator.ts +286 -0
  96. package/src/harnesses/muse/MuseSessionParser.ts +435 -0
  97. package/src/harnesses/muse/readiness.ts +38 -0
  98. package/src/harnesses/readiness/AgentReadiness.ts +2 -0
  99. package/src/harnesses/runtimeProfiles.ts +1 -0
  100. package/src/harnesses/shared.ts +10 -5
  101. package/src/utils/process.ts +61 -9
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Tests for harnesses/muse/MuseSessionParser.ts — framed transcript parsing.
3
+ *
4
+ * Muse session.jsonl lines are frames: either a direct inner record, a
5
+ * `record_json` string, or a `retained_frame` wrapper whose children carry
6
+ * `record_json` strings. Inner records dispatch on `payload_type`.
7
+ */
8
+
9
+ import * as fs from "fs";
10
+ import * as os from "os";
11
+ import * as path from "path";
12
+ import { AgentStatus } from "../../../adapters/AgentAdapter.js";
13
+ import { MuseSessionParser } from "../../../harnesses/muse/MuseSessionParser.js";
14
+
15
+ const SID = "01a12618-d918-7b63-91c8-4e896326eb1a";
16
+
17
+ function frame(inner: unknown): string {
18
+ return JSON.stringify(inner);
19
+ }
20
+
21
+ function wrapped(inner: unknown): string {
22
+ return JSON.stringify({ record_json: JSON.stringify(inner) });
23
+ }
24
+
25
+ function retained(inner: unknown): string {
26
+ return JSON.stringify({
27
+ retained_frame: "session_permission_transaction",
28
+ frame_schema_version: 1,
29
+ children: [{ child_index: 0, record_json: JSON.stringify(inner) }],
30
+ });
31
+ }
32
+
33
+ function intent(text: string, sequence: number, recordedAtUs: number): unknown {
34
+ return {
35
+ schema_version: 1,
36
+ stream: { kind: "session", id: SID },
37
+ sequence,
38
+ recorded_at: recordedAtUs,
39
+ record_type: "event",
40
+ payload_type: "runtime.user_intent.accepted",
41
+ payload: {
42
+ intent_id: `intent-${sequence}`,
43
+ refill_blocks: [{ kind: "text", text }],
44
+ },
45
+ };
46
+ }
47
+
48
+ function assistantMessage(text: string, sequence: number, recordedAtUs: number): unknown {
49
+ return {
50
+ schema_version: 1,
51
+ stream: { kind: "session", id: SID },
52
+ sequence,
53
+ recorded_at: recordedAtUs,
54
+ record_type: "event",
55
+ payload_type: "runtime.session",
56
+ payload: {
57
+ kind: "run",
58
+ run_id: "run-1",
59
+ event: { kind: "assistant_message_committed", text },
60
+ },
61
+ };
62
+ }
63
+
64
+ function metadata(recordedAtUs: number): unknown {
65
+ return {
66
+ schema_version: 1,
67
+ stream: { kind: "session", id: SID },
68
+ sequence: 1,
69
+ recorded_at: recordedAtUs,
70
+ record_type: "event",
71
+ payload_type: "runtime.session.metadata",
72
+ payload: {
73
+ kind: "metadata",
74
+ record: { workspace_root: "/Users/tester/proj", model_id: "muse-spark-1.3" },
75
+ },
76
+ };
77
+ }
78
+
79
+ function nameChanged(name: string, sequence: number, recordedAtUs: number): unknown {
80
+ return {
81
+ schema_version: 1,
82
+ stream: { kind: "session", id: SID },
83
+ sequence,
84
+ recorded_at: recordedAtUs,
85
+ record_type: "event",
86
+ payload_type: "session.name.changed",
87
+ payload: { session_id: SID, previous_name: null, new_name: name },
88
+ };
89
+ }
90
+
91
+ describe("MuseSessionParser", () => {
92
+ let parser: MuseSessionParser;
93
+ const tempDirs: string[] = [];
94
+
95
+ beforeEach(() => {
96
+ parser = new MuseSessionParser();
97
+ });
98
+
99
+ afterEach(() => {
100
+ for (const dir of tempDirs) {
101
+ try {
102
+ fs.rmSync(dir, { recursive: true, force: true });
103
+ } catch {
104
+ /* best effort */
105
+ }
106
+ }
107
+ tempDirs.length = 0;
108
+ });
109
+
110
+ function writeTranscript(lines: string[]): string {
111
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "muse-parser-test-"));
112
+ tempDirs.push(dir);
113
+ const filePath = path.join(dir, "session.jsonl");
114
+ fs.writeFileSync(filePath, lines.join("\n"));
115
+ return filePath;
116
+ }
117
+
118
+ it("returns null for missing or empty files", () => {
119
+ expect(parser.readSession("/nonexistent/session.jsonl", "/proj")).toBeNull();
120
+ expect(parser.readSession(writeTranscript([]), "/proj")).toBeNull();
121
+ });
122
+
123
+ it("skips unparseable lines and wrapper frames without records", () => {
124
+ const filePath = writeTranscript([
125
+ "not json at all",
126
+ JSON.stringify({ retained_frame: "x", children: [] }),
127
+ frame(intent("hello", 2, 1791640525332173)),
128
+ ]);
129
+ const session = parser.readSession(filePath, "/proj");
130
+ expect(session?.lastUserMessage).toBe("hello");
131
+ });
132
+
133
+ it("unwraps record_json strings and retained children", () => {
134
+ const filePath = writeTranscript([
135
+ wrapped(intent("first", 2, 1791640525332173)),
136
+ retained(intent("second", 3, 1791640525332174)),
137
+ ]);
138
+ const session = parser.readSession(filePath, "/proj");
139
+ expect(session?.firstUserMessage).toBe("first");
140
+ expect(session?.lastUserMessage).toBe("second");
141
+ });
142
+
143
+ it("reads workspace root, model, and display name", () => {
144
+ const filePath = writeTranscript([
145
+ frame(metadata(1791640525332173)),
146
+ frame(intent("hi", 2, 1791640525332174)),
147
+ frame(nameChanged("flowing-monoceros", 3, 1791640525332175)),
148
+ ]);
149
+ // Empty project path lets the recorded workspace root win (archive sessions).
150
+ const session = parser.readSession(filePath, "");
151
+ expect(session?.projectPath).toBe("/Users/tester/proj");
152
+ expect(session?.modelId).toBe("muse-spark-1.3");
153
+ expect(session?.displayName).toBe("flowing-monoceros");
154
+ // First user message is found even when it is not on the first line.
155
+ expect(session?.firstUserMessage).toBe("hi");
156
+ });
157
+
158
+ it("normalizes microsecond timestamps to millisecond dates", () => {
159
+ const filePath = writeTranscript([frame(intent("hi", 2, 1791640525332173))]);
160
+ const session = parser.readSession(filePath, "/proj");
161
+ expect(session?.lastActive).toEqual(new Date(1791640525332));
162
+ });
163
+
164
+ it("maps last user intent to RUNNING and committed reply to WAITING", () => {
165
+ const running = parser.readSession(
166
+ writeTranscript([
167
+ frame(intent("q", 2, 1791640525332173)),
168
+ frame(assistantMessage("a", 3, 1791640525332174)),
169
+ frame(intent("q2", 4, 1791640525332175)),
170
+ ]),
171
+ "/proj",
172
+ );
173
+ expect(parser.determineStatus(running!)).toBe(AgentStatus.RUNNING);
174
+
175
+ const waiting = parser.readSession(
176
+ writeTranscript([
177
+ frame(intent("q", 2, 1791640525332173)),
178
+ frame(assistantMessage("a", 3, 1791640525332174)),
179
+ ]),
180
+ "/proj",
181
+ );
182
+ expect(parser.determineStatus(waiting!)).toBe(AgentStatus.WAITING);
183
+ });
184
+
185
+ it("maps approval waits and terminal runs to WAITING and IDLE", () => {
186
+ const waiting = parser.readSession(
187
+ writeTranscript([
188
+ frame({
189
+ schema_version: 1,
190
+ stream: { kind: "session", id: SID },
191
+ sequence: 2,
192
+ recorded_at: 1791640525332173,
193
+ record_type: "event",
194
+ payload_type: "runtime.session",
195
+ payload: {
196
+ kind: "run",
197
+ run_id: "run-1",
198
+ event: { kind: "approval_wait.effect.started", tool_name: "edit" },
199
+ },
200
+ }),
201
+ ]),
202
+ "/proj",
203
+ );
204
+ expect(parser.determineStatus(waiting!)).toBe(AgentStatus.WAITING);
205
+
206
+ const idle = parser.readSession(
207
+ writeTranscript([
208
+ frame({
209
+ schema_version: 1,
210
+ stream: { kind: "session", id: SID },
211
+ sequence: 2,
212
+ recorded_at: 1791640525332173,
213
+ record_type: "event",
214
+ payload_type: "runtime.session",
215
+ payload: { kind: "run", run_id: "run-1", event: { kind: "terminal" } },
216
+ }),
217
+ ]),
218
+ "/proj",
219
+ );
220
+ expect(parser.determineStatus(idle!)).toBe(AgentStatus.IDLE);
221
+ });
222
+
223
+ it("returns UNKNOWN when no conversation signal exists", () => {
224
+ const session = parser.readSession(
225
+ writeTranscript([frame(metadata(1791640525332173))]),
226
+ "/proj",
227
+ );
228
+ expect(parser.determineStatus(session!)).toBe(AgentStatus.UNKNOWN);
229
+ });
230
+
231
+ it("getConversation returns ordered messages and honors tail", () => {
232
+ const filePath = writeTranscript([
233
+ frame(metadata(1791640525332173)),
234
+ frame(intent("first question", 2, 1791640525332174)),
235
+ frame(assistantMessage("first answer", 3, 1791640525332175)),
236
+ frame(intent("second question", 4, 1791640525332176)),
237
+ ]);
238
+ const full = parser.getConversation(filePath);
239
+ expect(full.map((m) => [m.role, m.content])).toEqual([
240
+ ["user", "first question"],
241
+ ["assistant", "first answer"],
242
+ ["user", "second question"],
243
+ ]);
244
+ const tail = parser.getConversation(filePath, { tail: 1 });
245
+ expect(tail).toEqual([
246
+ { role: "user", content: "second question", timestamp: expect.anything() },
247
+ ]);
248
+ });
249
+
250
+ it("returns [] for a missing transcript and tool calls in verbose mode", () => {
251
+ expect(parser.getConversation("/nonexistent/session.jsonl")).toEqual([]);
252
+ const filePath = writeTranscript([
253
+ frame({
254
+ schema_version: 1,
255
+ stream: { kind: "session", id: SID },
256
+ sequence: 2,
257
+ recorded_at: 1791640525332173,
258
+ record_type: "event",
259
+ payload_type: "runtime.session",
260
+ payload: {
261
+ kind: "run",
262
+ run_id: "run-1",
263
+ event: {
264
+ kind: "assistant_tool_calls_committed",
265
+ tool_calls: [{ tool_name: "read_file" }, { tool_name: "search" }],
266
+ },
267
+ },
268
+ }),
269
+ ]);
270
+ expect(parser.getConversation(filePath)).toEqual([]);
271
+ expect(parser.getConversation(filePath, { verbose: true })).toEqual([
272
+ {
273
+ role: "assistant",
274
+ content: "tool calls: read_file, search",
275
+ timestamp: expect.anything(),
276
+ },
277
+ ]);
278
+ });
279
+ });
@@ -143,6 +143,7 @@ function insertPart(
143
143
  db: Database.Database,
144
144
  input: { messageId: string; sessionId: string; data: unknown; timeCreated: number },
145
145
  ): void {
146
- db.prepare("INSERT INTO part (message_id, session_id, data, time_created) VALUES (?, ?, ?, ?)")
147
- .run(input.messageId, input.sessionId, JSON.stringify(input.data), input.timeCreated);
146
+ db.prepare(
147
+ "INSERT INTO part (message_id, session_id, data, time_created) VALUES (?, ?, ?, ?)",
148
+ ).run(input.messageId, input.sessionId, JSON.stringify(input.data), input.timeCreated);
148
149
  }
@@ -68,6 +68,7 @@ function fixture(overrides: Partial<AgentReadinessOptions> = {}) {
68
68
  ".gemini/antigravity-cli",
69
69
  ".config/opencode",
70
70
  ".config/devin",
71
+ ".config/muse",
71
72
  ".pi",
72
73
  ]) {
73
74
  files[path.join(homeDir, directory)] = "<dir>";
@@ -128,6 +129,7 @@ describe("agent readiness", () => {
128
129
  "copilot",
129
130
  "pi",
130
131
  "devin",
132
+ "muse",
131
133
  ]);
132
134
  for (const [agent, report] of Object.entries(reports) as Array<
133
135
  [ReadinessAgentType, (typeof reports)[ReadinessAgentType]]
@@ -166,6 +168,7 @@ describe("agent readiness", () => {
166
168
  expect(reports.kiro.integration).toBeUndefined();
167
169
  expect(reports.antigravity_cli.integration).toBeUndefined();
168
170
  expect(reports.opencode.integration).toBeUndefined();
171
+ expect(reports.muse.integration).toBeUndefined();
169
172
  });
170
173
 
171
174
  it("resolves agent executables from PATH directories containing spaces", async () => {
@@ -309,6 +312,39 @@ describe("agent readiness", () => {
309
312
  });
310
313
  });
311
314
 
315
+ it("checks Muse auth from the muse auth.json file", async () => {
316
+ const authPath = path.join("/home/test", ".config", "muse", "auth.json");
317
+ const { options, files } = fixture();
318
+ files[authPath] = JSON.stringify({ meta: { token: "secret" } });
319
+
320
+ const report = await getAgentReadinessReport("muse", options);
321
+
322
+ expect(report.auth).toMatchObject({ state: "authenticated", status: "pass" });
323
+ expect(report.globalConfig).toMatchObject({ status: "pass" });
324
+ });
325
+
326
+ it("reports Muse as unauthenticated with an empty auth file", async () => {
327
+ const authPath = path.join("/home/test", ".config", "muse", "auth.json");
328
+ const { options, files } = fixture();
329
+ files[authPath] = JSON.stringify({});
330
+
331
+ const report = await getAgentReadinessReport("muse", options);
332
+
333
+ expect(report.auth).toMatchObject({
334
+ state: "unauthenticated",
335
+ status: "fail",
336
+ errors: ["Muse credential file has no entries"],
337
+ });
338
+ });
339
+
340
+ it("reports Muse as unauthenticated without an auth file", async () => {
341
+ const { options } = fixture();
342
+
343
+ const report = await getAgentReadinessReport("muse", options);
344
+
345
+ expect(report.auth).toMatchObject({ state: "unauthenticated", status: "fail" });
346
+ });
347
+
312
348
  it("reports Devin as unauthenticated without a login line", async () => {
313
349
  const { options } = fixture({
314
350
  runCommand: vi.fn(async (command) => {
@@ -36,6 +36,19 @@ describe("harness runtime profiles", () => {
36
36
  expect(runtimeAgentMatchesHarness("gemini", "gemini_cli")).toBe(true);
37
37
  expect(runtimeAgentMatchesHarness("codex", "claude")).toBe(false);
38
38
  });
39
+
40
+ it("profiles the versioned muse launcher", () => {
41
+ expect(HARNESS_RUNTIME_PROFILES.muse).toMatchObject({
42
+ command: "muse",
43
+ runtimeKind: "muse",
44
+ discoveryAliases: ["muse"],
45
+ });
46
+ expect(
47
+ HARNESS_RUNTIME_PROFILES.muse.matches("/Users/dev/.local/bin/muse-bin-1.4.4-R5419.1"),
48
+ ).toBe(true);
49
+ expect(HARNESS_RUNTIME_PROFILES.muse.matches("/usr/local/bin/muse")).toBe(true);
50
+ expect(runtimeAgentMatchesHarness("muse", "muse")).toBe(true);
51
+ });
39
52
  });
40
53
 
41
54
  describe("harness matchers with executables under paths containing spaces", () => {
@@ -18,6 +18,8 @@ import {
18
18
  captureProcessSnapshot,
19
19
  createProcessSnapshotCapture,
20
20
  filterByProcessNames,
21
+ matchesExecutablePrefix,
22
+ snapshotOptions,
21
23
  executableBasename,
22
24
  executablePath,
23
25
  type ProcessExec,
@@ -259,6 +261,34 @@ describe("filterByProcessNames", () => {
259
261
  expect(filterByProcessNames(processes, ["node"])).toEqual([processes[0], processes[1]]);
260
262
  expect(filterByProcessNames(processes, ["node.exe"])).toEqual([processes[0], processes[1]]);
261
263
  });
264
+
265
+ it("matches versioned launchers by basename prefix without leaking to siblings", () => {
266
+ const processes: ProcessInfo[] = [
267
+ { pid: 1, command: "/Users/dev/.local/bin/muse-bin-1.4.4-R5419.1", cwd: "", tty: "" },
268
+ { pid: 2, command: "/usr/local/bin/muse", cwd: "", tty: "" },
269
+ { pid: 3, command: "/usr/bin/museums", cwd: "", tty: "" },
270
+ { pid: 4, command: "/usr/local/bin/claude", cwd: "", tty: "" },
271
+ ];
272
+
273
+ expect(filterByProcessNames(processes, ["muse"], ["muse"])).toEqual([
274
+ processes[0],
275
+ processes[1],
276
+ ]);
277
+ // Exact names alone never match the versioned launcher.
278
+ expect(filterByProcessNames(processes, ["muse"])).toEqual([processes[1]]);
279
+ // No prefixes: previous behavior is unchanged.
280
+ expect(filterByProcessNames(processes, ["claude"])).toEqual([processes[3]]);
281
+ expect(matchesExecutablePrefix(processes[0].command, new Set(["muse"]))).toBe(true);
282
+ expect(matchesExecutablePrefix(processes[2].command, new Set(["muse"]))).toBe(false);
283
+ expect(matchesExecutablePrefix(processes[0].command, new Set())).toBe(false);
284
+ });
285
+
286
+ it("builds snapshot options with prefixes only when present", () => {
287
+ const isCandidate = () => true;
288
+ expect(snapshotOptions([], isCandidate)).toEqual({ isCandidate });
289
+ const withPrefixes = snapshotOptions(["muse"], isCandidate);
290
+ expect(withPrefixes).toMatchObject({ namePrefixes: ["muse"], isCandidate });
291
+ });
262
292
  });
263
293
 
264
294
  describe("executables installed under paths containing spaces", () => {
@@ -17,6 +17,7 @@ export const AGENT_TYPES = [
17
17
  "copilot",
18
18
  "pi",
19
19
  "devin",
20
+ "muse",
20
21
  ] as const;
21
22
 
22
23
  /**
@@ -201,6 +202,14 @@ export interface AgentAdapter {
201
202
  /** Executable basenames required for shared process discovery. */
202
203
  readonly processNames?: readonly string[];
203
204
 
205
+ /**
206
+ * Basename prefixes for shared process discovery: an argv[0] basename
207
+ * matches when it equals the prefix or starts with `<prefix>-`.
208
+ * For versioned launchers such as `muse-bin-1.4.4-R5419.1`.
209
+ * Recall-oriented like `processNames`; `canHandle` stays the precision gate.
210
+ */
211
+ readonly processNamePrefixes?: readonly string[];
212
+
204
213
  /**
205
214
  * Detect running agents of this type
206
215
  * @returns List of detected agents
@@ -13,7 +13,7 @@ import { fileURLToPath } from "node:url";
13
13
  import Database from "better-sqlite3";
14
14
  import type { AgentAdapter, AgentInfo } from "../adapters/AgentAdapter.js";
15
15
  import { AgentRegistry } from "../utils/AgentRegistry.js";
16
- import { captureProcessSnapshot } from "../utils/process.js";
16
+ import { captureProcessSnapshot, snapshotOptions } from "../utils/process.js";
17
17
  import { FixtureBundle, normalizeAgents, sanitize, withFrozenClock } from "./bundle.js";
18
18
 
19
19
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../..");
@@ -133,12 +133,22 @@ function collectSqliteDumps(
133
133
  (dump) => {
134
134
  const dirs = uniqueCwds(processes);
135
135
  if (dirs.length === 0) return;
136
- const sessions = dump.rows("session", `WHERE directory IN ${dump.inList(dirs)} ORDER BY rowid`, dirs);
136
+ const sessions = dump.rows(
137
+ "session",
138
+ `WHERE directory IN ${dump.inList(dirs)} ORDER BY rowid`,
139
+ dirs,
140
+ );
137
141
  const sids = sessions.map((s) => s.id);
138
142
  dump.insertRows("session", sessions);
139
143
  if (sids.length === 0) return;
140
- dump.insertRows("message", dump.rows("message", `WHERE session_id IN ${dump.inList(sids)} ORDER BY rowid`, sids));
141
- dump.insertRows("part", dump.rows("part", `WHERE session_id IN ${dump.inList(sids)} ORDER BY rowid`, sids));
144
+ dump.insertRows(
145
+ "message",
146
+ dump.rows("message", `WHERE session_id IN ${dump.inList(sids)} ORDER BY rowid`, sids),
147
+ );
148
+ dump.insertRows(
149
+ "part",
150
+ dump.rows("part", `WHERE session_id IN ${dump.inList(sids)} ORDER BY rowid`, sids),
151
+ );
142
152
  },
143
153
  ["session", "message", "part"],
144
154
  );
@@ -170,7 +180,9 @@ function collectSqliteDumps(
170
180
  clauses.push(`id IN ${dump.inList(lockSids)}`);
171
181
  args.push(...lockSids);
172
182
  }
173
- const sessions = clauses.length ? dump.rows("sessions", `WHERE ${clauses.join(" OR ")} ORDER BY rowid`, args) : [];
183
+ const sessions = clauses.length
184
+ ? dump.rows("sessions", `WHERE ${clauses.join(" OR ")} ORDER BY rowid`, args)
185
+ : [];
174
186
  const sids = sessions.map((s) => s.id);
175
187
  dump.insertRows("sessions", sessions);
176
188
  if (sids.length === 0) return;
@@ -180,11 +192,17 @@ function collectSqliteDumps(
180
192
  // replay without hauling full transcripts into the bundle.
181
193
  for (const sid of sids) {
182
194
  const reachable = [
183
- ...dump.rows("message_nodes", "WHERE session_id = ? ORDER BY node_id DESC LIMIT 1", [sid]),
195
+ ...dump.rows("message_nodes", "WHERE session_id = ? ORDER BY node_id DESC LIMIT 1", [
196
+ sid,
197
+ ]),
184
198
  // The newest-created_at row — `MAX(created_at)` reads only the
185
199
  // value, and `= MAX(...)` unbounded explodes when batch-written
186
200
  // nodes share a timestamp.
187
- ...dump.rows("message_nodes", "WHERE session_id = ? ORDER BY created_at DESC, node_id DESC LIMIT 1", [sid]),
201
+ ...dump.rows(
202
+ "message_nodes",
203
+ "WHERE session_id = ? ORDER BY created_at DESC, node_id DESC LIMIT 1",
204
+ [sid],
205
+ ),
188
206
  ...dump.rows(
189
207
  "message_nodes",
190
208
  "WHERE session_id = ? AND json_extract(chat_message, '$.role') = 'user' ORDER BY node_id DESC LIMIT 8",
@@ -245,15 +263,10 @@ function dumpSqliteDb(
245
263
  const dump = {
246
264
  inList: (xs: unknown[]) => `(${xs.map(() => "?").join(",")})`,
247
265
  rows: (table: string, clause: string, args: unknown[]) =>
248
- db.prepare(`SELECT * FROM "${table}" ${clause}`).all(...args) as Record<
249
- string,
250
- unknown
251
- >[],
266
+ db.prepare(`SELECT * FROM "${table}" ${clause}`).all(...args) as Record<string, unknown>[],
252
267
  insertRows: (table: string, rs: Record<string, unknown>[]) => {
253
268
  for (const r of rs) {
254
- stmts.push(
255
- `INSERT INTO "${table}" VALUES (${Object.values(r).map(sqlLit).join(",")});`,
256
- );
269
+ stmts.push(`INSERT INTO "${table}" VALUES (${Object.values(r).map(sqlLit).join(",")});`);
257
270
  }
258
271
  },
259
272
  };
@@ -317,9 +330,11 @@ export async function captureLive(adapter: AgentAdapter, caseName = "live"): Pro
317
330
  }
318
331
 
319
332
  const { processes, agents, registry } = await withFrozenClock(frozenNow, async () => {
320
- const snapshot = await captureProcessSnapshot([...names], {
321
- isCandidate: (p) => adapter.canHandle(p),
322
- });
333
+ const prefixes = adapter.processNamePrefixes ? [...adapter.processNamePrefixes] : [];
334
+ const snapshot = await captureProcessSnapshot(
335
+ [...names],
336
+ snapshotOptions(prefixes, (p) => adapter.canHandle(p)),
337
+ );
323
338
  // Registry read must precede detection — detectAgents persists its own
324
339
  // results, which would poison the captured view.
325
340
  const registryBefore = collectRegistryEntries(realHome, snapshot);
@@ -64,12 +64,7 @@ const UTILITY_SUBCOMMANDS = new Set([
64
64
  * Flags that always consume the next argv token as their value.
65
65
  * (`devin --help`, main-command options.)
66
66
  */
67
- const REQUIRED_VALUE_FLAGS = new Set([
68
- "--prompt-file",
69
- "--config",
70
- "--permission-mode",
71
- "--model",
72
- ]);
67
+ const REQUIRED_VALUE_FLAGS = new Set(["--prompt-file", "--config", "--permission-mode", "--model"]);
73
68
 
74
69
  /**
75
70
  * Flags whose value is optional: the next token is consumed only when it
@@ -91,11 +86,7 @@ const OPTIONAL_VALUE_FLAGS = new Set([
91
86
  */
92
87
  function firstPositionalToken(command: string): string | null {
93
88
  const argv0 = executablePath(command);
94
- const tokens = command
95
- .trim()
96
- .slice(argv0.length)
97
- .trim()
98
- .split(/\s+/);
89
+ const tokens = command.trim().slice(argv0.length).trim().split(/\s+/);
99
90
  for (let i = 0; i < tokens.length; i++) {
100
91
  const token = tokens[i];
101
92
  if (!token) continue;
@@ -213,10 +204,7 @@ export class DevinAdapter implements AgentAdapter {
213
204
  * backend, not a separate agent. Standalone `acp` processes (external ACP
214
205
  * clients) keep their own agent entry.
215
206
  */
216
- private isAcpBackendChild(
217
- proc: ProcessInfo,
218
- processByPid: Map<number, ProcessInfo>,
219
- ): boolean {
207
+ private isAcpBackendChild(proc: ProcessInfo, processByPid: Map<number, ProcessInfo>): boolean {
220
208
  if (firstPositionalToken(proc.command)?.toLowerCase() !== "acp") return false;
221
209
  if (proc.ppid === undefined) return false;
222
210
  return processByPid.has(proc.ppid);
@@ -9,11 +9,7 @@ import { isIdle, processOnlyAgent } from "../shared.js";
9
9
  export class DevinAgentMapper {
10
10
  constructor(private readonly dbPath: string) {}
11
11
 
12
- mapSessionToAgent(
13
- session: DevinSession,
14
- stats: DevinSessionStats,
15
- proc: ProcessInfo,
16
- ): AgentInfo {
12
+ mapSessionToAgent(session: DevinSession, stats: DevinSessionStats, proc: ProcessInfo): AgentInfo {
17
13
  const lastActive =
18
14
  stats.lastTimeUpdated > 0
19
15
  ? new Date(stats.lastTimeUpdated)
@@ -78,10 +78,7 @@ export class DevinSessionParser {
78
78
  }
79
79
 
80
80
  for (const { nodeId, message } of parsed) {
81
- if (
82
- message.message_id &&
83
- latestNodeByMessageId.get(message.message_id) !== nodeId
84
- ) {
81
+ if (message.message_id && latestNodeByMessageId.get(message.message_id) !== nodeId) {
85
82
  continue;
86
83
  }
87
84
 
@@ -98,9 +95,7 @@ export class DevinSessionParser {
98
95
 
99
96
  if (role === "assistant") {
100
97
  const thinking =
101
- typeof message.thinking === "string"
102
- ? message.thinking
103
- : message.thinking?.thinking;
98
+ typeof message.thinking === "string" ? message.thinking : message.thinking?.thinking;
104
99
  if (verbose && thinking) {
105
100
  messages.push({ role: "assistant", content: `[thinking] ${thinking}` });
106
101
  }
@@ -179,11 +174,7 @@ export class DevinSessionParser {
179
174
  * shell `!` commands and `/` slash commands are skipped. Falls back to a
180
175
  * bounded `is_user_input` node scan in the same direction.
181
176
  */
182
- private userPrompt(
183
- db: Database.Database,
184
- sessionId: string,
185
- direction: "ASC" | "DESC",
186
- ): string {
177
+ private userPrompt(db: Database.Database, sessionId: string, direction: "ASC" | "DESC"): string {
187
178
  try {
188
179
  const prompt = db
189
180
  .prepare<[string], { content: string }>(`
@@ -9,6 +9,7 @@ import { KiroAdapter } from "./kiro/KiroAdapter.js";
9
9
  import { OpenCodeAdapter } from "./opencode/OpenCodeAdapter.js";
10
10
  import { PiAdapter } from "./pi/PiAdapter.js";
11
11
  import { DevinAdapter } from "./devin/DevinAdapter.js";
12
+ import { MuseAdapter } from "./muse/MuseAdapter.js";
12
13
 
13
14
  /**
14
15
  * One instance of every built-in harness adapter, in registration order
@@ -26,5 +27,6 @@ export function createBuiltinAdapters(): AgentAdapter[] {
26
27
  new OpenCodeAdapter(),
27
28
  new PiAdapter(),
28
29
  new DevinAdapter(),
30
+ new MuseAdapter(),
29
31
  ];
30
32
  }