@ai-devkit/agent-manager 0.34.0 → 0.36.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 (211) hide show
  1. package/dist/AgentManager.d.ts +2 -0
  2. package/dist/AgentManager.d.ts.map +1 -1
  3. package/dist/AgentManager.js +25 -0
  4. package/dist/AgentManager.js.map +1 -1
  5. package/dist/__tests__/AgentManager.test.js +74 -0
  6. package/dist/__tests__/AgentManager.test.js.map +1 -1
  7. package/dist/__tests__/adapters/AntigravityCliAdapter.test.js +381 -0
  8. package/dist/__tests__/adapters/AntigravityCliAdapter.test.js.map +1 -0
  9. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +28 -0
  10. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
  11. package/dist/__tests__/adapters/CodexAdapter.test.js +7 -0
  12. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
  13. package/dist/__tests__/adapters/KiroAdapter.test.js +607 -0
  14. package/dist/__tests__/adapters/KiroAdapter.test.js.map +1 -0
  15. package/dist/__tests__/capacity/claude.test.js +485 -0
  16. package/dist/__tests__/capacity/claude.test.js.map +1 -0
  17. package/dist/__tests__/capacity/openai.test.js +672 -0
  18. package/dist/__tests__/capacity/openai.test.js.map +1 -0
  19. package/dist/__tests__/capacity/wham.test.js +144 -0
  20. package/dist/__tests__/capacity/wham.test.js.map +1 -0
  21. package/dist/__tests__/providers/copilot/CopilotAdapter.test.js +18 -0
  22. package/dist/__tests__/providers/copilot/CopilotAdapter.test.js.map +1 -1
  23. package/dist/__tests__/providers/gemini/GeminiCliAdapter.test.js +36 -0
  24. package/dist/__tests__/providers/gemini/GeminiCliAdapter.test.js.map +1 -1
  25. package/dist/__tests__/providers/grok/GrokAgentMapper.test.js +56 -0
  26. package/dist/__tests__/providers/grok/GrokAgentMapper.test.js.map +1 -0
  27. package/dist/__tests__/{adapters → providers/grok}/GrokCliAdapter.test.js +19 -6
  28. package/dist/__tests__/providers/grok/GrokCliAdapter.test.js.map +1 -0
  29. package/dist/__tests__/providers/grok/GrokSessionLocator.test.js +133 -0
  30. package/dist/__tests__/providers/grok/GrokSessionLocator.test.js.map +1 -0
  31. package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js +24 -0
  32. package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js.map +1 -1
  33. package/dist/__tests__/providers/pi/PiAdapter.test.js +30 -0
  34. package/dist/__tests__/providers/pi/PiAdapter.test.js.map +1 -1
  35. package/dist/__tests__/utils/agents.test.js +6 -0
  36. package/dist/__tests__/utils/agents.test.js.map +1 -1
  37. package/dist/__tests__/utils/session.test.js +18 -1
  38. package/dist/__tests__/utils/session.test.js.map +1 -1
  39. package/dist/adapters/AgentAdapter.d.ts +9 -1
  40. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  41. package/dist/adapters/AgentAdapter.js.map +1 -1
  42. package/dist/adapters/AntigravityCliAdapter.d.ts +57 -0
  43. package/dist/adapters/AntigravityCliAdapter.d.ts.map +1 -0
  44. package/dist/adapters/AntigravityCliAdapter.js +273 -0
  45. package/dist/adapters/AntigravityCliAdapter.js.map +1 -0
  46. package/dist/adapters/KiroAdapter.d.ts +46 -0
  47. package/dist/adapters/KiroAdapter.d.ts.map +1 -0
  48. package/dist/adapters/KiroAdapter.js +344 -0
  49. package/dist/adapters/KiroAdapter.js.map +1 -0
  50. package/dist/adapters/index.d.ts +3 -1
  51. package/dist/adapters/index.d.ts.map +1 -1
  52. package/dist/adapters/index.js +3 -1
  53. package/dist/adapters/index.js.map +1 -1
  54. package/dist/capacity/claude.d.ts +17 -0
  55. package/dist/capacity/claude.d.ts.map +1 -0
  56. package/dist/capacity/claude.js +271 -0
  57. package/dist/capacity/claude.js.map +1 -0
  58. package/dist/capacity/codex.d.ts +1 -2
  59. package/dist/capacity/codex.d.ts.map +1 -1
  60. package/dist/capacity/codex.js +5 -47
  61. package/dist/capacity/codex.js.map +1 -1
  62. package/dist/capacity/index.d.ts +11 -0
  63. package/dist/capacity/index.d.ts.map +1 -1
  64. package/dist/capacity/index.js +17 -0
  65. package/dist/capacity/index.js.map +1 -1
  66. package/dist/capacity/openai.d.ts +48 -0
  67. package/dist/capacity/openai.d.ts.map +1 -0
  68. package/dist/capacity/openai.js +312 -0
  69. package/dist/capacity/openai.js.map +1 -0
  70. package/dist/capacity/wham.d.ts +16 -0
  71. package/dist/capacity/wham.d.ts.map +1 -0
  72. package/dist/capacity/wham.js +66 -0
  73. package/dist/capacity/wham.js.map +1 -0
  74. package/dist/index.d.ts +5 -3
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +4 -2
  77. package/dist/index.js.map +1 -1
  78. package/dist/providers/claude/ClaudeCodeAdapter.d.ts +2 -0
  79. package/dist/providers/claude/ClaudeCodeAdapter.d.ts.map +1 -1
  80. package/dist/providers/claude/ClaudeCodeAdapter.js +22 -19
  81. package/dist/providers/claude/ClaudeCodeAdapter.js.map +1 -1
  82. package/dist/providers/claude/ClaudeSessionLocator.d.ts +4 -0
  83. package/dist/providers/claude/ClaudeSessionLocator.d.ts.map +1 -1
  84. package/dist/providers/claude/ClaudeSessionLocator.js +18 -1
  85. package/dist/providers/claude/ClaudeSessionLocator.js.map +1 -1
  86. package/dist/providers/codex/CodexAdapter.d.ts +1 -0
  87. package/dist/providers/codex/CodexAdapter.d.ts.map +1 -1
  88. package/dist/providers/codex/CodexAdapter.js +8 -0
  89. package/dist/providers/codex/CodexAdapter.js.map +1 -1
  90. package/dist/providers/copilot/CopilotAdapter.d.ts +2 -0
  91. package/dist/providers/copilot/CopilotAdapter.d.ts.map +1 -1
  92. package/dist/providers/copilot/CopilotAdapter.js +24 -9
  93. package/dist/providers/copilot/CopilotAdapter.js.map +1 -1
  94. package/dist/providers/copilot/CopilotSessionLocator.d.ts +1 -0
  95. package/dist/providers/copilot/CopilotSessionLocator.d.ts.map +1 -1
  96. package/dist/providers/copilot/CopilotSessionLocator.js +9 -1
  97. package/dist/providers/copilot/CopilotSessionLocator.js.map +1 -1
  98. package/dist/providers/gemini/GeminiCliAdapter.d.ts +1 -0
  99. package/dist/providers/gemini/GeminiCliAdapter.d.ts.map +1 -1
  100. package/dist/providers/gemini/GeminiCliAdapter.js +9 -0
  101. package/dist/providers/gemini/GeminiCliAdapter.js.map +1 -1
  102. package/dist/providers/grok/GrokAgentMapper.d.ts +14 -0
  103. package/dist/providers/grok/GrokAgentMapper.d.ts.map +1 -0
  104. package/dist/providers/grok/GrokAgentMapper.js +36 -0
  105. package/dist/providers/grok/GrokAgentMapper.js.map +1 -0
  106. package/dist/providers/grok/GrokCliAdapter.d.ts +36 -0
  107. package/dist/providers/grok/GrokCliAdapter.d.ts.map +1 -0
  108. package/dist/providers/grok/GrokCliAdapter.js +98 -0
  109. package/dist/providers/grok/GrokCliAdapter.js.map +1 -0
  110. package/dist/providers/grok/GrokSessionLocator.d.ts +62 -0
  111. package/dist/providers/grok/GrokSessionLocator.d.ts.map +1 -0
  112. package/dist/providers/grok/GrokSessionLocator.js +140 -0
  113. package/dist/providers/grok/GrokSessionLocator.js.map +1 -0
  114. package/dist/providers/grok/GrokSessionParser.d.ts +54 -0
  115. package/dist/providers/grok/GrokSessionParser.d.ts.map +1 -0
  116. package/dist/providers/grok/GrokSessionParser.js +122 -0
  117. package/dist/providers/grok/GrokSessionParser.js.map +1 -0
  118. package/dist/providers/opencode/OpenCodeAdapter.d.ts +1 -0
  119. package/dist/providers/opencode/OpenCodeAdapter.d.ts.map +1 -1
  120. package/dist/providers/opencode/OpenCodeAdapter.js +3 -0
  121. package/dist/providers/opencode/OpenCodeAdapter.js.map +1 -1
  122. package/dist/providers/opencode/OpenCodeSessionLocator.d.ts +2 -0
  123. package/dist/providers/opencode/OpenCodeSessionLocator.d.ts.map +1 -1
  124. package/dist/providers/opencode/OpenCodeSessionLocator.js +31 -12
  125. package/dist/providers/opencode/OpenCodeSessionLocator.js.map +1 -1
  126. package/dist/providers/pi/PiAdapter.d.ts +1 -0
  127. package/dist/providers/pi/PiAdapter.d.ts.map +1 -1
  128. package/dist/providers/pi/PiAdapter.js +10 -0
  129. package/dist/providers/pi/PiAdapter.js.map +1 -1
  130. package/dist/providers/pi/PiSessionLocator.d.ts +1 -0
  131. package/dist/providers/pi/PiSessionLocator.d.ts.map +1 -1
  132. package/dist/providers/pi/PiSessionLocator.js +8 -1
  133. package/dist/providers/pi/PiSessionLocator.js.map +1 -1
  134. package/dist/readiness/AgentReadiness.d.ts +1 -1
  135. package/dist/readiness/AgentReadiness.d.ts.map +1 -1
  136. package/dist/readiness/AgentReadiness.js.map +1 -1
  137. package/dist/runtime/ManagedAgentRuntime.js +3 -1
  138. package/dist/runtime/ManagedAgentRuntime.js.map +1 -1
  139. package/dist/runtime/herdr/HerdrAgentDiscovery.js +10 -0
  140. package/dist/runtime/herdr/HerdrAgentDiscovery.js.map +1 -1
  141. package/dist/utils/agents.d.ts +1 -1
  142. package/dist/utils/agents.d.ts.map +1 -1
  143. package/dist/utils/agents.js +11 -0
  144. package/dist/utils/agents.js.map +1 -1
  145. package/dist/utils/session.d.ts +2 -0
  146. package/dist/utils/session.d.ts.map +1 -1
  147. package/dist/utils/session.js +3 -0
  148. package/dist/utils/session.js.map +1 -1
  149. package/package.json +1 -1
  150. package/src/AgentManager.ts +34 -0
  151. package/src/__tests__/AgentManager.test.ts +60 -0
  152. package/src/__tests__/adapters/AntigravityCliAdapter.test.ts +324 -0
  153. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +23 -0
  154. package/src/__tests__/adapters/CodexAdapter.test.ts +7 -0
  155. package/src/__tests__/adapters/KiroAdapter.test.ts +534 -0
  156. package/src/__tests__/capacity/claude.test.ts +467 -0
  157. package/src/__tests__/capacity/fixtures/claude-credentials.json +7 -0
  158. package/src/__tests__/capacity/fixtures/claude-usage-malformed.json +1 -0
  159. package/src/__tests__/capacity/fixtures/claude-usage-partial.json +15 -0
  160. package/src/__tests__/capacity/fixtures/claude-usage.json +63 -0
  161. package/src/__tests__/capacity/fixtures/openai-auth.json +6 -0
  162. package/src/__tests__/capacity/fixtures/openai-codex-auth.json +9 -0
  163. package/src/__tests__/capacity/fixtures/openai-usage-completions.json +67 -0
  164. package/src/__tests__/capacity/fixtures/openai-usage-responses.json +20 -0
  165. package/src/__tests__/capacity/fixtures/wham-usage.json +32 -0
  166. package/src/__tests__/capacity/openai.test.ts +570 -0
  167. package/src/__tests__/capacity/wham.test.ts +104 -0
  168. package/src/__tests__/providers/copilot/CopilotAdapter.test.ts +12 -0
  169. package/src/__tests__/providers/gemini/GeminiCliAdapter.test.ts +21 -0
  170. package/src/__tests__/providers/grok/GrokAgentMapper.test.ts +57 -0
  171. package/src/__tests__/{adapters → providers/grok}/GrokCliAdapter.test.ts +18 -9
  172. package/src/__tests__/providers/grok/GrokSessionLocator.test.ts +106 -0
  173. package/src/__tests__/providers/opencode/OpenCodeAdapter.test.ts +18 -0
  174. package/src/__tests__/providers/pi/PiAdapter.test.ts +15 -0
  175. package/src/__tests__/utils/agents.test.ts +7 -0
  176. package/src/__tests__/utils/session.test.ts +11 -1
  177. package/src/adapters/AgentAdapter.ts +11 -0
  178. package/src/adapters/AntigravityCliAdapter.ts +356 -0
  179. package/src/adapters/KiroAdapter.ts +456 -0
  180. package/src/adapters/index.ts +3 -1
  181. package/src/capacity/claude.ts +313 -0
  182. package/src/capacity/codex.ts +25 -134
  183. package/src/capacity/index.ts +33 -4
  184. package/src/capacity/openai.ts +361 -0
  185. package/src/capacity/wham.ts +84 -0
  186. package/src/index.ts +11 -2
  187. package/src/providers/claude/ClaudeCodeAdapter.ts +28 -23
  188. package/src/providers/claude/ClaudeSessionLocator.ts +20 -0
  189. package/src/providers/codex/CodexAdapter.ts +8 -0
  190. package/src/providers/copilot/CopilotAdapter.ts +26 -10
  191. package/src/providers/copilot/CopilotSessionLocator.ts +7 -1
  192. package/src/providers/gemini/GeminiCliAdapter.ts +8 -0
  193. package/src/providers/grok/GrokAgentMapper.ts +41 -0
  194. package/src/providers/grok/GrokCliAdapter.ts +125 -0
  195. package/src/providers/grok/GrokSessionLocator.ts +179 -0
  196. package/src/providers/grok/GrokSessionParser.ts +166 -0
  197. package/src/providers/opencode/OpenCodeAdapter.ts +4 -0
  198. package/src/providers/opencode/OpenCodeSessionLocator.ts +43 -15
  199. package/src/providers/pi/PiAdapter.ts +12 -0
  200. package/src/providers/pi/PiSessionLocator.ts +16 -1
  201. package/src/readiness/AgentReadiness.ts +4 -1
  202. package/src/runtime/ManagedAgentRuntime.ts +2 -0
  203. package/src/runtime/herdr/HerdrAgentDiscovery.ts +5 -1
  204. package/src/utils/agents.ts +11 -1
  205. package/src/utils/session.ts +11 -0
  206. package/dist/__tests__/adapters/GrokCliAdapter.test.js.map +0 -1
  207. package/dist/adapters/GrokCliAdapter.d.ts +0 -80
  208. package/dist/adapters/GrokCliAdapter.d.ts.map +0 -1
  209. package/dist/adapters/GrokCliAdapter.js +0 -310
  210. package/dist/adapters/GrokCliAdapter.js.map +0 -1
  211. package/src/adapters/GrokCliAdapter.ts +0 -408
@@ -1185,6 +1185,27 @@ describe("GeminiCliAdapter", () => {
1185
1185
  });
1186
1186
  });
1187
1187
 
1188
+ it("finds an exact session ID from Gemini metadata", async () => {
1189
+ const wantedPath = writeSession(tmpHome, "aaa", "session-one", {
1190
+ sessionId: "wanted",
1191
+ projectHash: hashProjectRoot("/repo"),
1192
+ startTime: "2025-01-01T00:00:00Z",
1193
+ directories: ["/repo"],
1194
+ messages: [{ type: "user", content: "wanted prompt" }],
1195
+ });
1196
+ writeSession(tmpHome, "bbb", "session-two", {
1197
+ sessionId: "other",
1198
+ projectHash: hashProjectRoot("/other"),
1199
+ startTime: "2025-01-01T00:00:00Z",
1200
+ directories: ["/other"],
1201
+ messages: [{ type: "user", content: "other prompt" }],
1202
+ });
1203
+
1204
+ await expect(adapter.findSessionsById("wanted")).resolves.toEqual([
1205
+ expect.objectContaining({ sessionId: "wanted", sessionFilePath: wantedPath }),
1206
+ ]);
1207
+ });
1208
+
1188
1209
  it("applies strict-equality cwd filter against directories[0]", async () => {
1189
1210
  writeSession(tmpHome, "aaa", "session-keep", {
1190
1211
  sessionId: "keep",
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { AgentStatus, type ProcessInfo } from "../../../adapters/AgentAdapter.js";
4
+ import { GrokAgentMapper } from "../../../providers/grok/GrokAgentMapper.js";
5
+ import type { GrokSession } from "../../../providers/grok/GrokSessionParser.js";
6
+
7
+ function makeProcess(overrides: Partial<ProcessInfo> = {}): ProcessInfo {
8
+ return { pid: 123, command: "grok", cwd: "/repo/my-app", tty: "ttys001", ...overrides };
9
+ }
10
+
11
+ function makeSession(overrides: Partial<GrokSession> = {}): GrokSession {
12
+ return {
13
+ sessionId: "session-1",
14
+ projectPath: "/repo/my-app",
15
+ sessionFilePath: "/home/.grok/sessions/%2Frepo%2Fmy-app/session-1/chat_history.jsonl",
16
+ sessionStart: new Date(),
17
+ lastActive: new Date(),
18
+ lastUserMessage: "Review this change",
19
+ lastRole: "assistant",
20
+ ...overrides,
21
+ };
22
+ }
23
+
24
+ describe("GrokAgentMapper", () => {
25
+ const mapper = new GrokAgentMapper();
26
+
27
+ it("maps a session to a grok_cli agent pointing at chat_history.jsonl", () => {
28
+ const agent = mapper.mapSessionToAgent({
29
+ session: makeSession(),
30
+ processInfo: makeProcess(),
31
+ });
32
+
33
+ expect(agent).toMatchObject({
34
+ name: "my-app-123",
35
+ type: "grok_cli",
36
+ status: AgentStatus.WAITING,
37
+ summary: "Review this change",
38
+ pid: 123,
39
+ projectPath: "/repo/my-app",
40
+ sessionId: "session-1",
41
+ sessionFilePath: "/home/.grok/sessions/%2Frepo%2Fmy-app/session-1/chat_history.jsonl",
42
+ });
43
+ });
44
+
45
+ it("maps a process-only agent as RUNNING with a pid session id", () => {
46
+ const agent = mapper.mapProcessOnlyAgent(makeProcess(), "/repo/resolved");
47
+
48
+ expect(agent).toMatchObject({
49
+ type: "grok_cli",
50
+ status: AgentStatus.RUNNING,
51
+ summary: "Grok CLI process running",
52
+ projectPath: "/repo/resolved",
53
+ sessionId: "pid-123",
54
+ });
55
+ expect(agent.sessionFilePath).toBeUndefined();
56
+ });
57
+ });
@@ -10,18 +10,18 @@ import * as fs from "fs";
10
10
  import * as os from "os";
11
11
  import * as path from "path";
12
12
 
13
- import { GrokCliAdapter } from "../../adapters/GrokCliAdapter.js";
14
- import type { ProcessInfo } from "../../adapters/AgentAdapter.js";
15
- import { AgentStatus } from "../../adapters/AgentAdapter.js";
13
+ import { GrokCliAdapter } from "../../../providers/grok/GrokCliAdapter.js";
14
+ import type { ProcessInfo } from "../../../adapters/AgentAdapter.js";
15
+ import { AgentStatus } from "../../../adapters/AgentAdapter.js";
16
16
  import {
17
17
  listAgentProcesses,
18
18
  enrichProcesses,
19
19
  captureProcessSnapshot,
20
- } from "../../utils/process.js";
21
- import { generateAgentName } from "../../utils/matching.js";
20
+ } from "../../../utils/process.js";
21
+ import { generateAgentName } from "../../../utils/matching.js";
22
22
 
23
- vi.mock("../../utils/process.js", async (importOriginal) => {
24
- const actual = (await importOriginal()) as typeof import("../../utils/process.js");
23
+ vi.mock("../../../utils/process.js", async (importOriginal) => {
24
+ const actual = (await importOriginal()) as typeof import("../../../utils/process.js");
25
25
  return {
26
26
  ...actual,
27
27
  listAgentProcesses: vi.fn(),
@@ -30,8 +30,8 @@ vi.mock("../../utils/process.js", async (importOriginal) => {
30
30
  };
31
31
  });
32
32
 
33
- vi.mock("../../utils/matching.js", async (importOriginal) => {
34
- const actual = (await importOriginal()) as typeof import("../../utils/matching.js");
33
+ vi.mock("../../../utils/matching.js", async (importOriginal) => {
34
+ const actual = (await importOriginal()) as typeof import("../../../utils/matching.js");
35
35
  return {
36
36
  ...actual,
37
37
  generateAgentName: vi.fn(),
@@ -335,6 +335,15 @@ describe("GrokCliAdapter", () => {
335
335
  );
336
336
  });
337
337
 
338
+ it("finds an exact session ID across project groups", async () => {
339
+ writeSession({});
340
+ writeSession({ sessionCwd: "/Users/dev/other", id: "other-session" });
341
+
342
+ await expect(adapter.findSessionsById(SESSION_ID)).resolves.toEqual([
343
+ expect.objectContaining({ sessionId: SESSION_ID, cwd }),
344
+ ]);
345
+ });
346
+
338
347
  it("applies the cwd filter against the decoded cwd", async () => {
339
348
  writeSession({
340
349
  sessionCwd: "/Users/dev/project-a",
@@ -0,0 +1,106 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+
6
+ import type { ProcessInfo } from "../../../adapters/AgentAdapter.js";
7
+ import { GrokSessionLocator } from "../../../providers/grok/GrokSessionLocator.js";
8
+
9
+ describe("GrokSessionLocator", () => {
10
+ let baseDir: string;
11
+ let locator: GrokSessionLocator;
12
+
13
+ beforeEach(() => {
14
+ baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "grok-locator-test-"));
15
+ locator = new GrokSessionLocator({ baseDir });
16
+ });
17
+
18
+ afterEach(() => {
19
+ fs.rmSync(baseDir, { recursive: true, force: true });
20
+ });
21
+
22
+ function writeSession(cwd: string, id: string, mtime?: Date): string {
23
+ const sessionDir = path.join(baseDir, "sessions", encodeURIComponent(cwd), id);
24
+ fs.mkdirSync(sessionDir, { recursive: true });
25
+ const chatPath = path.join(sessionDir, "chat_history.jsonl");
26
+ fs.writeFileSync(chatPath, "");
27
+ if (mtime) fs.utimesSync(chatPath, mtime, mtime);
28
+ return sessionDir;
29
+ }
30
+
31
+ function proc(overrides: Partial<ProcessInfo> = {}): ProcessInfo {
32
+ return { pid: 4242, command: "grok", cwd: "/repo/app", tty: "ttys001", ...overrides };
33
+ }
34
+
35
+ describe("matchRunningProcesses", () => {
36
+ it("prefers the active_sessions.json cwd over the process cwd", () => {
37
+ const sessionDir = writeSession("/repo/real", "s1");
38
+ fs.writeFileSync(
39
+ path.join(baseDir, "active_sessions.json"),
40
+ JSON.stringify([{ pid: 4242, cwd: "/repo/real" }]),
41
+ );
42
+
43
+ expect(locator.matchRunningProcesses([proc({ cwd: "/wrong" })])).toEqual([
44
+ { process: expect.objectContaining({ pid: 4242 }), cwd: "/repo/real", sessionDir },
45
+ ]);
46
+ });
47
+
48
+ it("ignores a malformed active_sessions.json and falls back to the process cwd", () => {
49
+ const sessionDir = writeSession("/repo/app", "s1");
50
+ fs.writeFileSync(path.join(baseDir, "active_sessions.json"), "{not json");
51
+
52
+ expect(locator.matchRunningProcesses([proc()])[0]).toMatchObject({
53
+ cwd: "/repo/app",
54
+ sessionDir,
55
+ });
56
+ });
57
+
58
+ it("picks the session with the newest chat_history.jsonl", () => {
59
+ writeSession("/repo/app", "old", new Date(Date.now() - 60_000));
60
+ const newest = writeSession("/repo/app", "new");
61
+
62
+ expect(locator.matchRunningProcesses([proc()])[0].sessionDir).toBe(newest);
63
+ });
64
+
65
+ it("returns a null sessionDir when no cwd is known", () => {
66
+ expect(locator.matchRunningProcesses([proc({ cwd: "" })])[0]).toMatchObject({
67
+ cwd: "",
68
+ sessionDir: null,
69
+ });
70
+ });
71
+ });
72
+
73
+ describe("historical sessions", () => {
74
+ it("discovers session dirs with the decoded group cwd", () => {
75
+ const a = writeSession("/repo/a", "s1");
76
+ const b = writeSession("/repo/b", "s2");
77
+
78
+ expect(locator.discoverHistoricalSessionDirs()).toEqual(
79
+ expect.arrayContaining([
80
+ { sessionDir: a, defaultCwd: "/repo/a" },
81
+ { sessionDir: b, defaultCwd: "/repo/b" },
82
+ ]),
83
+ );
84
+ });
85
+
86
+ it("prefers the .cwd file over the encoded group name", () => {
87
+ const groupDir = path.join(baseDir, "sessions", "slug-abc123");
88
+ fs.mkdirSync(path.join(groupDir, "s1"), { recursive: true });
89
+ fs.writeFileSync(path.join(groupDir, ".cwd"), "/very/long/path\n");
90
+
91
+ expect(locator.discoverHistoricalSessionDirs()).toEqual([
92
+ { sessionDir: path.join(groupDir, "s1"), defaultCwd: "/very/long/path" },
93
+ ]);
94
+ });
95
+
96
+ it("finds session dirs by id and rejects unsafe ids", () => {
97
+ const dir = writeSession("/repo/a", "s1");
98
+ writeSession("/repo/b", "s2");
99
+
100
+ expect(locator.findHistoricalSessionDirsById("s1")).toEqual([
101
+ { sessionDir: dir, defaultCwd: "/repo/a" },
102
+ ]);
103
+ expect(locator.findHistoricalSessionDirsById("../s1")).toEqual([]);
104
+ });
105
+ });
106
+ });
@@ -121,6 +121,24 @@ describe("OpenCodeAdapter", () => {
121
121
  ]);
122
122
  expect(adapter.getConversation("/not-an-opencode-ref")).toEqual([]);
123
123
  });
124
+
125
+ it("finds an exact session ID with a targeted database query", async () => {
126
+ const now = Date.now();
127
+ writeDatabase(dbPath, {
128
+ sessions: [
129
+ { id: "wanted", directory: "/repo", timeCreated: now },
130
+ { id: "other", directory: "/other", timeCreated: now - 1_000 },
131
+ ],
132
+ });
133
+
134
+ await expect(adapter.findSessionsById("wanted")).resolves.toEqual([
135
+ expect.objectContaining({
136
+ type: "opencode",
137
+ sessionId: "wanted",
138
+ sessionFilePath: `${dbPath}::wanted`,
139
+ }),
140
+ ]);
141
+ });
124
142
  });
125
143
 
126
144
  function writeDatabase(
@@ -260,6 +260,21 @@ describe("PiAdapter", () => {
260
260
  ]);
261
261
  });
262
262
 
263
+ it("finds an exact session ID from the filename suffix", async () => {
264
+ const wanted = writePiSession("/repo/wanted", [
265
+ { timestamp: "2026-06-10T08:58:20.754Z", sessionId: "sess-wanted", cwd: "/repo/wanted" },
266
+ { role: "user", content: "wanted" },
267
+ ]);
268
+ writePiSession("/repo/other", [
269
+ { timestamp: "2026-06-10T08:58:20.754Z", sessionId: "sess-other", cwd: "/repo/other" },
270
+ { role: "user", content: "other" },
271
+ ]);
272
+
273
+ await expect(adapter.findSessionsById("sess-wanted")).resolves.toEqual([
274
+ expect.objectContaining({ sessionId: "sess-wanted", sessionFilePath: wanted }),
275
+ ]);
276
+ });
277
+
263
278
  function makeProcess(overrides: Partial<ProcessInfo>): ProcessInfo {
264
279
  return {
265
280
  pid: 1,
@@ -21,4 +21,11 @@ describe("AGENTS", () => {
21
21
  expect(AGENTS.grok_cli.matches("/Users/dev/.grok/bin/grok --always-approve")).toBe(true);
22
22
  expect(AGENTS.grok_cli.matches("node /repo/feature-grok-cli/script.js")).toBe(false);
23
23
  });
24
+
25
+ it("includes Kiro as a startable agent", () => {
26
+ expect(AGENTS.kiro.command).toBe("kiro-cli");
27
+ expect(AGENTS.kiro.matches("kiro-cli")).toBe(true);
28
+ expect(AGENTS.kiro.matches("/usr/local/bin/kiro --model x")).toBe(true);
29
+ expect(AGENTS.kiro.matches("node /repo/feature-kiro-adapter/script.js")).toBe(false);
30
+ });
24
31
  });
@@ -5,7 +5,7 @@
5
5
  import type { MockedFunction } from "vitest";
6
6
 
7
7
  import * as fs from "fs";
8
- import { batchGetSessionFileBirthtimes } from "../../utils/session.js";
8
+ import { batchGetSessionFileBirthtimes, isSafePathSegment } from "../../utils/session.js";
9
9
 
10
10
  vi.mock("fs", () => ({
11
11
  readdirSync: vi.fn(),
@@ -15,6 +15,16 @@ vi.mock("fs", () => ({
15
15
  const mockedReaddirSync = fs.readdirSync as MockedFunction<typeof fs.readdirSync>;
16
16
  const mockedStatSync = fs.statSync as MockedFunction<typeof fs.statSync>;
17
17
 
18
+ describe("isSafePathSegment", () => {
19
+ it.each(["", ".", "..", "nested/session", "bad\0id"])("rejects unsafe segment %j", (value) => {
20
+ expect(isSafePathSegment(value)).toBe(false);
21
+ });
22
+
23
+ it.each(["session-id", ".hidden-session"])("accepts safe segment %j", (value) => {
24
+ expect(isSafePathSegment(value)).toBe(true);
25
+ });
26
+ });
27
+
18
28
  describe("batchGetSessionFileBirthtimes", () => {
19
29
  beforeEach(() => {
20
30
  mockedReaddirSync.mockReset();
@@ -12,6 +12,8 @@ export type AgentType =
12
12
  | "claude"
13
13
  | "gemini_cli"
14
14
  | "grok_cli"
15
+ | "kiro"
16
+ | "antigravity_cli"
15
17
  | "codex"
16
18
  | "opencode"
17
19
  | "copilot"
@@ -208,4 +210,13 @@ export interface AgentAdapter {
208
210
  * @returns Array of sessions discovered on disk
209
211
  */
210
212
  listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]>;
213
+
214
+ /**
215
+ * Resolve exact historical session matches without enumerating every summary.
216
+ *
217
+ * Built-in adapters implement this using their provider-native storage shape.
218
+ * The method remains optional so external adapters can fall back to
219
+ * {@link listSessions} until they adopt direct lookup.
220
+ */
221
+ findSessionsById?(sessionId: string): Promise<SessionSummary[]>;
211
222
  }