@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,435 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import type { ConversationMessage, ConversationOptions } from "../../adapters/AgentAdapter.js";
4
+ import { AgentStatus } from "../../adapters/AgentAdapter.js";
5
+ import {
6
+ IncrementalJsonlSummary,
7
+ reduceJsonlContent,
8
+ type JsonlSummaryReducer,
9
+ } from "../../utils/IncrementalJsonlSummary.js";
10
+ import { JsonlTailReader, normalizeTail } from "../../utils/jsonlTail.js";
11
+
12
+ /**
13
+ * A single unwrapped inner record from a Muse session frame.
14
+ *
15
+ * Each session.jsonl line is one of: a direct inner record, a
16
+ * `{ record_json: string }` envelope, or a `retained_frame` wrapper whose
17
+ * `children[]` carry `record_json` strings. Inner records dispatch on
18
+ * `payload_type`; timestamps (`recorded_at`) are microseconds.
19
+ */
20
+ export interface MuseInnerRecord {
21
+ stream?: { kind?: string; id?: string };
22
+ sequence?: number;
23
+ recorded_at?: number;
24
+ record_type?: string;
25
+ payload_type?: string;
26
+ payload?: Record<string, unknown>;
27
+ }
28
+
29
+ /** Parsed session state extracted from a Muse transcript. */
30
+ export interface MuseSession {
31
+ sessionId: string;
32
+ projectPath: string;
33
+ /** Last display name from `session.name.changed`, when present. */
34
+ displayName?: string;
35
+ modelId?: string;
36
+ sessionStart: Date;
37
+ lastActive: Date;
38
+ /** Last conversation signal: user-intent, assistant reply, approval, tool batch… */
39
+ lastSignal?: string;
40
+ lastUserMessage?: string;
41
+ /** First meaningful user prompt in the session. */
42
+ firstUserMessage?: string;
43
+ }
44
+
45
+ interface MuseSummaryState {
46
+ seenFirstLine: boolean;
47
+ pastHead?: boolean;
48
+ sessionStartMs?: number;
49
+ lastActiveMs?: number;
50
+ workspaceRoot?: string;
51
+ modelId?: string;
52
+ displayName?: string;
53
+ lastSignal?: string;
54
+ lastUserMessage?: string;
55
+ firstUserMessage?: string;
56
+ }
57
+
58
+ /**
59
+ * Conversation-relevant shape of one inner record. Both the summary fold and
60
+ * the message reader switch on this instead of re-matching payload shapes.
61
+ */
62
+ type MuseRecordKind =
63
+ | "metadata"
64
+ | "name-changed"
65
+ | "user-intent"
66
+ | "assistant"
67
+ | "tool-calls"
68
+ | "approval-started"
69
+ | "approval-settled"
70
+ | "input-requested"
71
+ | "input-settled"
72
+ | "run-terminal"
73
+ | "other";
74
+
75
+ function classifyRecord(record: MuseInnerRecord): MuseRecordKind {
76
+ const payload = record.payload;
77
+ if (!isRecord(payload)) return "other";
78
+ switch (record.payload_type) {
79
+ case "runtime.session.metadata":
80
+ return "metadata";
81
+ case "session.name.changed":
82
+ return "name-changed";
83
+ case "runtime.user_intent.accepted":
84
+ case "runtime.user_intent.materialized":
85
+ return "user-intent";
86
+ case "runtime.session":
87
+ break;
88
+ default:
89
+ return "other";
90
+ }
91
+ if (!isRecord(payload.event) || typeof payload.event.kind !== "string") return "other";
92
+ if (payload.kind === "run") {
93
+ switch (payload.event.kind) {
94
+ case "assistant_message_committed":
95
+ return "assistant";
96
+ case "assistant_tool_calls_committed":
97
+ return "tool-calls";
98
+ case "terminal":
99
+ return "run-terminal";
100
+ }
101
+ }
102
+ switch (payload.event.kind) {
103
+ case "approval_wait.effect.started":
104
+ return "approval-started";
105
+ case "approval_wait.effect.terminal":
106
+ return "approval-settled";
107
+ case "user_input_prompt_requested":
108
+ return "input-requested";
109
+ case "user_input_prompt_settled":
110
+ return "input-settled";
111
+ default:
112
+ return "other";
113
+ }
114
+ }
115
+
116
+ const MICROSECONDS_THRESHOLD = 1_000_000_000_000_000;
117
+
118
+ /** `recorded_at` microseconds → epoch ms; null when invalid. */
119
+ export function museTimestampToMs(value: unknown): number | null {
120
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
121
+ const ms = value >= MICROSECONDS_THRESHOLD ? Math.floor(value / 1000) : value;
122
+ return ms >= 0 ? ms : null;
123
+ }
124
+
125
+ /** Unwrap one raw frame line into zero or more inner records. */
126
+ export function unwrapFrameRecords(line: string): MuseInnerRecord[] {
127
+ let outer: unknown;
128
+ try {
129
+ outer = JSON.parse(line);
130
+ } catch {
131
+ return [];
132
+ }
133
+ return unwrapOuterRecords(outer);
134
+ }
135
+
136
+ /** Unwrap an already-parsed frame value into zero or more inner records. */
137
+ export function unwrapOuterRecords(outer: unknown): MuseInnerRecord[] {
138
+ if (!outer || typeof outer !== "object") return [];
139
+ const obj = outer as { record_json?: unknown; children?: unknown };
140
+ if (typeof obj.record_json === "string") {
141
+ try {
142
+ const inner = JSON.parse(obj.record_json) as MuseInnerRecord;
143
+ return [inner];
144
+ } catch {
145
+ return [];
146
+ }
147
+ }
148
+ if (Array.isArray(obj.children)) {
149
+ const out: MuseInnerRecord[] = [];
150
+ for (const child of obj.children) {
151
+ if (
152
+ child &&
153
+ typeof child === "object" &&
154
+ typeof (child as { record_json?: unknown }).record_json === "string"
155
+ ) {
156
+ try {
157
+ out.push(JSON.parse((child as { record_json: string }).record_json));
158
+ } catch {
159
+ /* skip malformed child */
160
+ }
161
+ }
162
+ }
163
+ return out;
164
+ }
165
+ return [obj as MuseInnerRecord];
166
+ }
167
+
168
+ function isRecord(value: unknown): value is Record<string, unknown> {
169
+ return value !== null && typeof value === "object" && !Array.isArray(value);
170
+ }
171
+
172
+ function nonEmptyText(value: unknown): string | null {
173
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
174
+ }
175
+
176
+ /** Collect `{ kind: "text", text }` strings from intent/model message blocks. */
177
+ function collectTextBlocks(value: unknown): string[] {
178
+ const out: string[] = [];
179
+ if (Array.isArray(value)) {
180
+ for (const block of value) {
181
+ if (isRecord(block) && block.kind === "text") {
182
+ const text = nonEmptyText(block.text);
183
+ if (text) out.push(text);
184
+ }
185
+ }
186
+ }
187
+ return out;
188
+ }
189
+
190
+ /** User turn text from an intent payload (`refill_blocks` then `model_messages`). */
191
+ function intentText(payload: Record<string, unknown>): string | null {
192
+ const refill = collectTextBlocks(payload.refill_blocks);
193
+ if (refill.length > 0) return refill.join("\n");
194
+ const modelMessages = payload.model_messages;
195
+ if (Array.isArray(modelMessages)) {
196
+ const parts: string[] = [];
197
+ for (const message of modelMessages) {
198
+ if (isRecord(message)) parts.push(...collectTextBlocks(message.content));
199
+ }
200
+ if (parts.length > 0) return parts.join("\n");
201
+ }
202
+ return null;
203
+ }
204
+
205
+ /** Assistant reply text from a `run.assistant_message_committed` event. */
206
+ function assistantEventText(event: Record<string, unknown>): string | null {
207
+ return nonEmptyText(event.text);
208
+ }
209
+
210
+ export class MuseSessionParser {
211
+ private readonly tailReader = new JsonlTailReader();
212
+ private readonly sessionCache: IncrementalJsonlSummary<MuseSummaryState>;
213
+
214
+ constructor() {
215
+ this.sessionCache = new IncrementalJsonlSummary(this.summaryReducer, {});
216
+ }
217
+
218
+ readSession(filePath: string, projectPath: string): MuseSession | null {
219
+ let content: string;
220
+ try {
221
+ content = fs.readFileSync(filePath, "utf-8");
222
+ } catch {
223
+ return null;
224
+ }
225
+ if (!content.trim()) return null;
226
+ return this.toSession(filePath, projectPath, reduceJsonlContent(this.summaryReducer, content));
227
+ }
228
+
229
+ readSessionIncremental(filePath: string, projectPath: string): MuseSession | null {
230
+ const result = this.sessionCache.read(filePath);
231
+ return result ? this.toSession(filePath, projectPath, result.state) : null;
232
+ }
233
+
234
+ pruneSessionCache(): void {
235
+ this.sessionCache.prune();
236
+ }
237
+
238
+ private readonly summaryReducer: JsonlSummaryReducer<MuseSummaryState> = {
239
+ initial: () => ({ seenFirstLine: false }),
240
+ reduce: (state, value) => {
241
+ const next: MuseSummaryState = { ...state, seenFirstLine: true };
242
+ // Session start comes from the first line only; the first user message
243
+ // may come from anywhere before a bounded-skip (mirrors Claude semantics).
244
+ const flags = { isHead: !state.seenFirstLine, canBeFirst: !state.pastHead };
245
+ for (const record of unwrapOuterRecords(value)) {
246
+ this.foldRecord(next, record, flags);
247
+ }
248
+ return next;
249
+ },
250
+ skip: (state) => ({
251
+ seenFirstLine: true,
252
+ pastHead: true,
253
+ sessionStartMs: state.sessionStartMs,
254
+ firstUserMessage: state.firstUserMessage,
255
+ }),
256
+ };
257
+
258
+ /**
259
+ * Fold one inner record into the summary. `isHead` marks the first line
260
+ * (session start only); `canBeFirst` allows the first user message from any
261
+ * line read before a bounded-skip.
262
+ */
263
+ private foldRecord(
264
+ next: MuseSummaryState,
265
+ record: MuseInnerRecord,
266
+ flags: { isHead: boolean; canBeFirst: boolean },
267
+ ): void {
268
+ const { isHead, canBeFirst } = flags;
269
+ const atMs = museTimestampToMs(record.recorded_at);
270
+ if (atMs !== null) {
271
+ if (isHead && next.sessionStartMs === undefined) next.sessionStartMs = atMs;
272
+ next.lastActiveMs = atMs;
273
+ }
274
+ const payload = record.payload;
275
+ if (!isRecord(payload)) return;
276
+ switch (classifyRecord(record)) {
277
+ case "metadata": {
278
+ if (!isRecord(payload.record)) return;
279
+ const root = nonEmptyText(payload.record.workspace_root);
280
+ if (root) next.workspaceRoot = root;
281
+ const model = nonEmptyText(payload.record.model_id);
282
+ if (model) next.modelId = model;
283
+ return;
284
+ }
285
+ case "name-changed": {
286
+ const name = nonEmptyText(payload.new_name);
287
+ if (name) next.displayName = name;
288
+ return;
289
+ }
290
+ case "user-intent": {
291
+ next.lastSignal = "user-intent";
292
+ const text = intentText(payload);
293
+ if (text) {
294
+ next.lastUserMessage = text;
295
+ if (canBeFirst && !next.firstUserMessage) next.firstUserMessage = text;
296
+ }
297
+ return;
298
+ }
299
+ case "assistant":
300
+ next.lastSignal = "assistant";
301
+ return;
302
+ case "approval-started":
303
+ next.lastSignal = "approval-wait";
304
+ return;
305
+ case "approval-settled":
306
+ if (next.lastSignal === "approval-wait") next.lastSignal = "assistant";
307
+ return;
308
+ case "input-requested":
309
+ next.lastSignal = "input-requested";
310
+ return;
311
+ case "input-settled":
312
+ if (next.lastSignal === "input-requested") next.lastSignal = "assistant";
313
+ return;
314
+ case "run-terminal":
315
+ next.lastSignal = "run-terminal";
316
+ return;
317
+ default:
318
+ return;
319
+ }
320
+ }
321
+
322
+ private toSession(filePath: string, projectPath: string, state: MuseSummaryState): MuseSession {
323
+ const sessionStartMs = state.sessionStartMs ?? state.lastActiveMs;
324
+ return {
325
+ sessionId: path.basename(path.dirname(filePath)),
326
+ projectPath: projectPath || state.workspaceRoot || "",
327
+ displayName: state.displayName,
328
+ modelId: state.modelId,
329
+ sessionStart: sessionStartMs !== undefined ? new Date(sessionStartMs) : new Date(),
330
+ lastActive: state.lastActiveMs !== undefined ? new Date(state.lastActiveMs) : new Date(),
331
+ lastSignal: state.lastSignal,
332
+ lastUserMessage: state.lastUserMessage,
333
+ firstUserMessage: state.firstUserMessage,
334
+ };
335
+ }
336
+
337
+ /**
338
+ * Status mapping over the last conversation signal:
339
+ * - user-intent → RUNNING (agent is processing the new turn)
340
+ * - assistant → WAITING (reply committed, awaiting user)
341
+ * - approval-wait / input-requested → WAITING (blocked on the user)
342
+ * - run-terminal → IDLE; no signal → UNKNOWN.
343
+ */
344
+ determineStatus(session: MuseSession): AgentStatus {
345
+ switch (session.lastSignal) {
346
+ case "user-intent":
347
+ return AgentStatus.RUNNING;
348
+ case "assistant":
349
+ case "approval-wait":
350
+ case "input-requested":
351
+ return AgentStatus.WAITING;
352
+ case "run-terminal":
353
+ return AgentStatus.IDLE;
354
+ default:
355
+ return AgentStatus.UNKNOWN;
356
+ }
357
+ }
358
+
359
+ getConversation(sessionFilePath: string, options?: ConversationOptions): ConversationMessage[] {
360
+ const verbose = options?.verbose ?? false;
361
+ const tail = normalizeTail(options?.tail);
362
+ if (tail !== undefined) {
363
+ return this.tailReader.read(
364
+ sessionFilePath,
365
+ tail,
366
+ { parseLine: (line) => this.lineToMessage(line, verbose) },
367
+ String(verbose),
368
+ );
369
+ }
370
+ let content: string;
371
+ try {
372
+ content = fs.readFileSync(sessionFilePath, "utf-8");
373
+ } catch {
374
+ return [];
375
+ }
376
+ const messages: ConversationMessage[] = [];
377
+ for (const line of content.trim().split("\n")) {
378
+ messages.push(...this.linesToMessages(line, verbose));
379
+ }
380
+ return messages;
381
+ }
382
+
383
+ /** One raw frame line → conversation messages (usually zero or one). */
384
+ private linesToMessages(line: string, verbose: boolean): ConversationMessage[] {
385
+ const out: ConversationMessage[] = [];
386
+ for (const record of unwrapFrameRecords(line)) {
387
+ const message = this.recordToMessage(record, verbose);
388
+ if (message) out.push(message);
389
+ }
390
+ return out;
391
+ }
392
+
393
+ /**
394
+ * One raw frame line → its first conversation message, for tail reads.
395
+ * A retained frame can theoretically hold several messages; the tail
396
+ * window counts lines, so a boundary line keeps only its first message.
397
+ */
398
+ private lineToMessage(line: string, verbose: boolean): ConversationMessage | null {
399
+ return this.linesToMessages(line, verbose)[0] ?? null;
400
+ }
401
+
402
+ private recordToMessage(record: MuseInnerRecord, verbose: boolean): ConversationMessage | null {
403
+ const payload = record.payload;
404
+ if (!isRecord(payload)) return null;
405
+ const atMs = museTimestampToMs(record.recorded_at);
406
+ const timestamp = atMs !== null ? new Date(atMs).toISOString() : undefined;
407
+
408
+ switch (classifyRecord(record)) {
409
+ case "user-intent": {
410
+ const text = intentText(payload);
411
+ if (!text) return null;
412
+ return { role: "user", content: text, timestamp };
413
+ }
414
+ case "assistant": {
415
+ if (!isRecord(payload.event)) return null;
416
+ const text = assistantEventText(payload.event);
417
+ if (!text) return null;
418
+ return { role: "assistant", content: text, timestamp };
419
+ }
420
+ case "tool-calls": {
421
+ if (!verbose || !isRecord(payload.event) || !Array.isArray(payload.event.tool_calls)) {
422
+ return null;
423
+ }
424
+ const names = (payload.event.tool_calls as unknown[])
425
+ .filter(isRecord)
426
+ .map((call) => nonEmptyText(call.tool_name))
427
+ .filter((name): name is string => name !== null);
428
+ if (names.length === 0) return null;
429
+ return { role: "assistant", content: `tool calls: ${names.join(", ")}`, timestamp };
430
+ }
431
+ default:
432
+ return null;
433
+ }
434
+ }
435
+ }
@@ -0,0 +1,38 @@
1
+ import { join } from "node:path";
2
+ import { displayHome, record } from "../readiness/checks.js";
3
+ import type {
4
+ AuthReadinessCheck,
5
+ HarnessReadinessProfile,
6
+ ReadinessRuntime,
7
+ } from "../readiness/types.js";
8
+
9
+ async function authCheck(runtime: ReadinessRuntime): Promise<AuthReadinessCheck> {
10
+ const sourcePath = join(runtime.homeDir, ".config", "muse", "auth.json");
11
+ try {
12
+ const parsed = record(JSON.parse(await runtime.readFile(sourcePath)));
13
+ if (!parsed) throw new Error("invalid");
14
+ const authenticated = Object.keys(parsed).length > 0;
15
+ return {
16
+ state: authenticated ? "authenticated" : "unauthenticated",
17
+ source: displayHome(sourcePath, runtime.homeDir),
18
+ provider: null,
19
+ availableProviders: [],
20
+ status: authenticated ? "pass" : "fail",
21
+ errors: authenticated ? [] : ["Muse credential file has no entries"],
22
+ };
23
+ } catch {
24
+ return {
25
+ state: "unauthenticated",
26
+ source: displayHome(sourcePath, runtime.homeDir),
27
+ provider: null,
28
+ availableProviders: [],
29
+ status: "fail",
30
+ errors: ["Muse credential file is missing or invalid"],
31
+ };
32
+ }
33
+ }
34
+
35
+ export const museReadiness: HarnessReadinessProfile = {
36
+ configDir: ".config/muse",
37
+ auth: authCheck,
38
+ };
@@ -4,6 +4,7 @@ import { copilotReadiness } from "../copilot/readiness.js";
4
4
  import { openCodeReadiness } from "../opencode/readiness.js";
5
5
  import { piReadiness } from "../pi/readiness.js";
6
6
  import { devinReadiness } from "../devin/readiness.js";
7
+ import { museReadiness } from "../muse/readiness.js";
7
8
  import {
8
9
  builtInSkillsCheck,
9
10
  createReadinessRuntime,
@@ -31,6 +32,7 @@ const READINESS_PROFILES: Record<ReadinessAgentType, HarnessReadinessProfile> =
31
32
  copilot: copilotReadiness,
32
33
  pi: piReadiness,
33
34
  devin: devinReadiness,
35
+ muse: museReadiness,
34
36
  };
35
37
 
36
38
  export async function getAgentReadinessReport(
@@ -22,6 +22,7 @@ export const HARNESS_RUNTIME_PROFILES: Record<StartableAgentType, HarnessRuntime
22
22
  pi: profile("pi", matchAnyBasename(["pi"])),
23
23
  kiro: profile("kiro", matchAnyBasename(["kiro-cli", "kiro"]), ["kiro_cli", "kiro-cli"]),
24
24
  devin: profile("devin", matchArgv0("devin")),
25
+ muse: profile("muse", matchArgv0Name("muse")),
25
26
  };
26
27
 
27
28
  export function runtimeAgentMatchesHarness(runtimeAgent: string, type: AgentType): boolean {
@@ -16,6 +16,7 @@ import {
16
16
  captureProcessSnapshot,
17
17
  executableBasename,
18
18
  filterByProcessNames,
19
+ snapshotOptions,
19
20
  } from "../utils/process.js";
20
21
 
21
22
  /** A session untouched for longer than this is IDLE. */
@@ -37,15 +38,19 @@ export interface HarnessProcesses {
37
38
  * (e.g. a harness's child runtime) that detection may need to walk through.
38
39
  */
39
40
  export async function findHarnessProcesses(
40
- adapter: Pick<AgentAdapter, "canHandle"> & { readonly processNames: readonly string[] },
41
+ adapter: Pick<AgentAdapter, "canHandle" | "processNamePrefixes"> & {
42
+ readonly processNames: readonly string[];
43
+ },
41
44
  context?: AgentDetectionContext,
42
45
  ): Promise<HarnessProcesses> {
46
+ const prefixes = adapter.processNamePrefixes ? [...adapter.processNamePrefixes] : [];
43
47
  const snapshot =
44
48
  context?.processes ??
45
- (await captureProcessSnapshot(adapter.processNames, {
46
- isCandidate: (process) => adapter.canHandle(process),
47
- }));
48
- const relevant = filterByProcessNames(snapshot, adapter.processNames);
49
+ (await captureProcessSnapshot(
50
+ adapter.processNames,
51
+ snapshotOptions(prefixes, (process) => adapter.canHandle(process)),
52
+ ));
53
+ const relevant = filterByProcessNames(snapshot, adapter.processNames, prefixes);
49
54
 
50
55
  const byPid = new Map<number, ProcessInfo>();
51
56
  for (const process of relevant) {
@@ -88,13 +88,42 @@ function matchesExecutableName(command: string, names: ReadonlySet<string>): boo
88
88
  return mayContinuePath && names.has(normalizeExecutableName(executableBasename(command)));
89
89
  }
90
90
 
91
+ /**
92
+ * Whether argv[0]'s basename is exactly `prefix` or starts with `prefix-`.
93
+ *
94
+ * Covers versioned launchers such as `muse-bin-1.4.4-R5419.1` for prefix
95
+ * `muse`. This is a recall-oriented prefilter: adapters still apply
96
+ * `canHandle` for precise shaping, so a coincidental `prefix-*` binary
97
+ * cannot leak into another harness.
98
+ */
99
+ export function matchesExecutablePrefix(command: string, prefixes: ReadonlySet<string>): boolean {
100
+ if (prefixes.size === 0) return false;
101
+ const base = normalizeExecutableName(pathBasename(command.trim().split(/\s+/)[0] ?? ""));
102
+ if (prefixes.has(base)) return true;
103
+ for (const prefix of prefixes) {
104
+ if (base.startsWith(`${prefix}-`)) return true;
105
+ }
106
+ return false;
107
+ }
108
+
109
+ /** Whether argv[0] matches one of `names` exactly or one of `prefixes`. */
110
+ export function matchesProcessPatterns(
111
+ command: string,
112
+ names: ReadonlySet<string>,
113
+ prefixes: ReadonlySet<string> = new Set(),
114
+ ): boolean {
115
+ return matchesExecutableName(command, names) || matchesExecutablePrefix(command, prefixes);
116
+ }
117
+
91
118
  export function filterByProcessNames(
92
119
  processes: readonly ProcessInfo[],
93
120
  namePatterns: readonly string[],
121
+ namePrefixes: readonly string[] = [],
94
122
  ): ProcessInfo[] {
95
123
  const names = normalizedProcessNames(namePatterns);
96
- if (names.size === 0) return [];
97
- return processes.filter((process) => matchesExecutableName(process.command, names));
124
+ const prefixes = normalizedProcessNames(namePrefixes);
125
+ if (names.size === 0 && prefixes.size === 0) return [];
126
+ return processes.filter((process) => matchesProcessPatterns(process.command, names, prefixes));
98
127
  }
99
128
 
100
129
  /**
@@ -160,6 +189,11 @@ export interface ProcessSnapshotOptions {
160
189
  * only) so adapters can walk parent chains. Omitted = enrich everything.
161
190
  */
162
191
  isCandidate?: (process: ProcessInfo) => boolean;
192
+ /**
193
+ * Basename prefixes matched like `processNamePrefixes`: exact or
194
+ * `<prefix>-*`. Same validation as namePatterns.
195
+ */
196
+ namePrefixes?: readonly string[];
163
197
  }
164
198
 
165
199
  export type ProcessSnapshotCapture = (
@@ -167,6 +201,20 @@ export type ProcessSnapshotCapture = (
167
201
  options?: ProcessSnapshotOptions,
168
202
  ) => Promise<ProcessInfo[]>;
169
203
 
204
+ /**
205
+ * One construction site for snapshot options: `namePrefixes` is sent only
206
+ * when non-empty, so adapters without prefixes keep the exact legacy call
207
+ * shape that existing mock assertions pin.
208
+ */
209
+ export function snapshotOptions(
210
+ namePrefixes: readonly string[],
211
+ isCandidate: (process: ProcessInfo) => boolean,
212
+ ): ProcessSnapshotOptions {
213
+ return namePrefixes.length > 0
214
+ ? { namePrefixes: [...namePrefixes], isCandidate }
215
+ : { isCandidate };
216
+ }
217
+
170
218
  const execFileText: ProcessExec = (file, args) =>
171
219
  new Promise((resolve, reject) => {
172
220
  execFile(
@@ -190,7 +238,11 @@ interface ParsedProcessList {
190
238
  matched: ProcessInfo[];
191
239
  }
192
240
 
193
- function parseProcessList(output: string, namePatterns: ReadonlySet<string>): ParsedProcessList {
241
+ function parseProcessList(
242
+ output: string,
243
+ namePatterns: ReadonlySet<string>,
244
+ namePrefixes: ReadonlySet<string> = new Set(),
245
+ ): ParsedProcessList {
194
246
  const allPids = new Set<number>();
195
247
  const matched: ProcessInfo[] = [];
196
248
 
@@ -207,7 +259,7 @@ function parseProcessList(output: string, namePatterns: ReadonlySet<string>): Pa
207
259
 
208
260
  const tty = match[3];
209
261
  const command = match[4];
210
- if (!matchesExecutableName(command, namePatterns)) continue;
262
+ if (!matchesProcessPatterns(command, namePatterns, namePrefixes)) continue;
211
263
 
212
264
  matched.push({
213
265
  pid,
@@ -317,14 +369,14 @@ export function createProcessSnapshotCapture(
317
369
  };
318
370
 
319
371
  return async (namePatterns, options = {}) => {
320
- const names = normalizedProcessNames(
321
- namePatterns.filter((name) => Boolean(name) && VALID_EXECUTABLE_NAME.test(name)),
322
- );
323
- if (names.size === 0) return [];
372
+ const valid = (name: string) => Boolean(name) && VALID_EXECUTABLE_NAME.test(name);
373
+ const names = normalizedProcessNames(namePatterns.filter(valid));
374
+ const prefixes = normalizedProcessNames((options.namePrefixes ?? []).filter(valid));
375
+ if (names.size === 0 && prefixes.size === 0) return [];
324
376
 
325
377
  try {
326
378
  const output = await exec("ps", ["-axo", "pid=,ppid=,tty=,command="]);
327
- const { allPids, matched } = parseProcessList(output, names);
379
+ const { allPids, matched } = parseProcessList(output, names, prefixes);
328
380
 
329
381
  for (const pid of startTimeCache.keys()) {
330
382
  if (!allPids.has(pid)) startTimeCache.delete(pid);