@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.
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +25 -9
- package/dist/AgentManager.js.map +1 -1
- package/dist/__tests__/fixtures.test.js.map +1 -1
- package/dist/__tests__/harnesses/devin/DevinAdapter.test.js.map +1 -1
- package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js.map +1 -1
- package/dist/__tests__/harnesses/muse/MuseAdapter.test.js +247 -0
- package/dist/__tests__/harnesses/muse/MuseAdapter.test.js.map +1 -0
- package/dist/__tests__/harnesses/muse/MuseAgentMapper.test.js +81 -0
- package/dist/__tests__/harnesses/muse/MuseAgentMapper.test.js.map +1 -0
- package/dist/__tests__/harnesses/muse/MuseSessionLocator.test.js +185 -0
- package/dist/__tests__/harnesses/muse/MuseSessionLocator.test.js.map +1 -0
- package/dist/__tests__/harnesses/muse/MuseSessionParser.test.js +328 -0
- package/dist/__tests__/harnesses/muse/MuseSessionParser.test.js.map +1 -0
- package/dist/__tests__/harnesses/opencode/OpenCodeSessionParser.test.js.map +1 -1
- package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js +42 -1
- package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js.map +1 -1
- package/dist/__tests__/harnesses/runtimeProfiles.test.js +12 -0
- package/dist/__tests__/harnesses/runtimeProfiles.test.js.map +1 -1
- package/dist/__tests__/utils/process.test.js +71 -1
- package/dist/__tests__/utils/process.test.js.map +1 -1
- package/dist/adapters/AgentAdapter.d.ts +8 -1
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js +2 -1
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/fixtures/capture.d.ts.map +1 -1
- package/dist/fixtures/capture.js +5 -4
- package/dist/fixtures/capture.js.map +1 -1
- package/dist/harnesses/devin/DevinAdapter.d.ts.map +1 -1
- package/dist/harnesses/devin/DevinAdapter.js.map +1 -1
- package/dist/harnesses/devin/DevinAgentMapper.d.ts.map +1 -1
- package/dist/harnesses/devin/DevinAgentMapper.js.map +1 -1
- package/dist/harnesses/devin/DevinSessionParser.d.ts.map +1 -1
- package/dist/harnesses/devin/DevinSessionParser.js.map +1 -1
- package/dist/harnesses/index.d.ts.map +1 -1
- package/dist/harnesses/index.js +3 -1
- package/dist/harnesses/index.js.map +1 -1
- package/dist/harnesses/muse/MuseAdapter.d.ts +33 -0
- package/dist/harnesses/muse/MuseAdapter.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseAdapter.js +122 -0
- package/dist/harnesses/muse/MuseAdapter.js.map +1 -0
- package/dist/harnesses/muse/MuseAgentMapper.d.ts +15 -0
- package/dist/harnesses/muse/MuseAgentMapper.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseAgentMapper.js +29 -0
- package/dist/harnesses/muse/MuseAgentMapper.js.map +1 -0
- package/dist/harnesses/muse/MuseSessionLocator.d.ts +61 -0
- package/dist/harnesses/muse/MuseSessionLocator.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseSessionLocator.js +233 -0
- package/dist/harnesses/muse/MuseSessionLocator.js.map +1 -0
- package/dist/harnesses/muse/MuseSessionParser.d.ts +77 -0
- package/dist/harnesses/muse/MuseSessionParser.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseSessionParser.js +350 -0
- package/dist/harnesses/muse/MuseSessionParser.js.map +1 -0
- package/dist/harnesses/muse/readiness.d.ts +3 -0
- package/dist/harnesses/muse/readiness.d.ts.map +1 -0
- package/dist/harnesses/muse/readiness.js +37 -0
- package/dist/harnesses/muse/readiness.js.map +1 -0
- package/dist/harnesses/readiness/AgentReadiness.d.ts.map +1 -1
- package/dist/harnesses/readiness/AgentReadiness.js +3 -1
- package/dist/harnesses/readiness/AgentReadiness.js.map +1 -1
- package/dist/harnesses/readiness/types.d.ts +1 -1
- package/dist/harnesses/readiness/types.d.ts.map +1 -1
- package/dist/harnesses/runtimeProfiles.d.ts.map +1 -1
- package/dist/harnesses/runtimeProfiles.js +2 -1
- package/dist/harnesses/runtimeProfiles.js.map +1 -1
- package/dist/harnesses/shared.d.ts +1 -1
- package/dist/harnesses/shared.d.ts.map +1 -1
- package/dist/harnesses/shared.js +6 -5
- package/dist/harnesses/shared.js.map +1 -1
- package/dist/utils/process.d.ts +23 -1
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +44 -8
- package/dist/utils/process.js.map +1 -1
- package/package.json +1 -1
- package/src/AgentManager.ts +34 -8
- package/src/__tests__/fixtures.test.ts +15 -17
- package/src/__tests__/harnesses/devin/DevinAdapter.test.ts +1 -3
- package/src/__tests__/harnesses/devin/DevinSessionParser.test.ts +3 -9
- package/src/__tests__/harnesses/muse/MuseAdapter.test.ts +197 -0
- package/src/__tests__/harnesses/muse/MuseAgentMapper.test.ts +83 -0
- package/src/__tests__/harnesses/muse/MuseSessionLocator.test.ts +177 -0
- package/src/__tests__/harnesses/muse/MuseSessionParser.test.ts +279 -0
- package/src/__tests__/harnesses/opencode/OpenCodeSessionParser.test.ts +3 -2
- package/src/__tests__/harnesses/readiness/AgentReadiness.test.ts +36 -0
- package/src/__tests__/harnesses/runtimeProfiles.test.ts +13 -0
- package/src/__tests__/utils/process.test.ts +30 -0
- package/src/adapters/AgentAdapter.ts +9 -0
- package/src/fixtures/capture.ts +32 -17
- package/src/harnesses/devin/DevinAdapter.ts +3 -15
- package/src/harnesses/devin/DevinAgentMapper.ts +1 -5
- package/src/harnesses/devin/DevinSessionParser.ts +3 -12
- package/src/harnesses/index.ts +2 -0
- package/src/harnesses/muse/MuseAdapter.ts +157 -0
- package/src/harnesses/muse/MuseAgentMapper.ts +33 -0
- package/src/harnesses/muse/MuseSessionLocator.ts +286 -0
- package/src/harnesses/muse/MuseSessionParser.ts +435 -0
- package/src/harnesses/muse/readiness.ts +38 -0
- package/src/harnesses/readiness/AgentReadiness.ts +2 -0
- package/src/harnesses/runtimeProfiles.ts +1 -0
- package/src/harnesses/shared.ts +10 -5
- 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(
|
|
147
|
-
|
|
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
|
package/src/fixtures/capture.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
141
|
-
|
|
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
|
|
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", [
|
|
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(
|
|
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
|
|
321
|
-
|
|
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 }>(`
|
package/src/harnesses/index.ts
CHANGED
|
@@ -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
|
}
|