@prismatic-io/lux 0.0.2-preview.18 → 0.0.2-preview.21

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 (244) hide show
  1. package/lib/answerers/persona/index.d.ts +5 -1
  2. package/lib/answerers/persona/index.d.ts.map +1 -1
  3. package/lib/assertions/rubric/index.d.ts +3 -1
  4. package/lib/assertions/rubric/index.d.ts.map +1 -1
  5. package/lib/authoring.d.ts +3 -1
  6. package/lib/authoring.d.ts.map +1 -1
  7. package/lib/authoring.js +2 -1
  8. package/lib/authoring.js.map +1 -1
  9. package/lib/cli/harness-plan.d.ts +26 -0
  10. package/lib/cli/harness-plan.d.ts.map +1 -0
  11. package/lib/cli/harness-plan.js +75 -0
  12. package/lib/cli/harness-plan.js.map +1 -0
  13. package/lib/cli/init-templates.d.ts +2 -2
  14. package/lib/cli/init-templates.d.ts.map +1 -1
  15. package/lib/cli/init-templates.js +11 -4
  16. package/lib/cli/init-templates.js.map +1 -1
  17. package/lib/cli/init.d.ts +2 -0
  18. package/lib/cli/init.d.ts.map +1 -1
  19. package/lib/cli/init.js +6 -3
  20. package/lib/cli/init.js.map +1 -1
  21. package/lib/cli/output-schemas.d.ts +7 -0
  22. package/lib/cli/output-schemas.d.ts.map +1 -1
  23. package/lib/cli/program.d.ts +5 -10
  24. package/lib/cli/program.d.ts.map +1 -1
  25. package/lib/cli/program.js +44 -77
  26. package/lib/cli/program.js.map +1 -1
  27. package/lib/cli/run-options.d.ts +19 -0
  28. package/lib/cli/run-options.d.ts.map +1 -1
  29. package/lib/cli/run-options.js +92 -0
  30. package/lib/cli/run-options.js.map +1 -1
  31. package/lib/core/case.d.ts +2 -0
  32. package/lib/core/case.d.ts.map +1 -1
  33. package/lib/core/case.js +3 -0
  34. package/lib/core/case.js.map +1 -1
  35. package/lib/core/driver.d.ts +13 -0
  36. package/lib/core/driver.d.ts.map +1 -1
  37. package/lib/core/driver.js +10 -0
  38. package/lib/core/driver.js.map +1 -1
  39. package/lib/core/harness-catalog.d.ts +35 -0
  40. package/lib/core/harness-catalog.d.ts.map +1 -0
  41. package/lib/core/harness-catalog.js +45 -0
  42. package/lib/core/harness-catalog.js.map +1 -0
  43. package/lib/core/harness-catalogs/antigravity.d.ts +11 -0
  44. package/lib/core/harness-catalogs/antigravity.d.ts.map +1 -0
  45. package/lib/core/harness-catalogs/antigravity.js +23 -0
  46. package/lib/core/harness-catalogs/antigravity.js.map +1 -0
  47. package/lib/core/harness-catalogs/claude-code.d.ts +12 -0
  48. package/lib/core/harness-catalogs/claude-code.d.ts.map +1 -0
  49. package/lib/core/harness-catalogs/claude-code.js +24 -0
  50. package/lib/core/harness-catalogs/claude-code.js.map +1 -0
  51. package/lib/core/harness-catalogs/codex.d.ts +11 -0
  52. package/lib/core/harness-catalogs/codex.d.ts.map +1 -0
  53. package/lib/core/harness-catalogs/codex.js +26 -0
  54. package/lib/core/harness-catalogs/codex.js.map +1 -0
  55. package/lib/core/harness-catalogs/copilot.d.ts +29 -0
  56. package/lib/core/harness-catalogs/copilot.d.ts.map +1 -0
  57. package/lib/core/harness-catalogs/copilot.js +45 -0
  58. package/lib/core/harness-catalogs/copilot.js.map +1 -0
  59. package/lib/core/harness-catalogs/cursor.d.ts +2 -0
  60. package/lib/core/harness-catalogs/cursor.d.ts.map +1 -0
  61. package/lib/core/harness-catalogs/cursor.js +13 -0
  62. package/lib/core/harness-catalogs/cursor.js.map +1 -0
  63. package/lib/core/harness-catalogs/index.d.ts +67 -0
  64. package/lib/core/harness-catalogs/index.d.ts.map +1 -0
  65. package/lib/core/harness-catalogs/index.js +15 -0
  66. package/lib/core/harness-catalogs/index.js.map +1 -0
  67. package/lib/core/harness-matrix.d.ts +70 -0
  68. package/lib/core/harness-matrix.d.ts.map +1 -0
  69. package/lib/core/harness-matrix.js +194 -0
  70. package/lib/core/harness-matrix.js.map +1 -0
  71. package/lib/core/index.d.ts +3 -0
  72. package/lib/core/index.d.ts.map +1 -1
  73. package/lib/core/index.js +3 -0
  74. package/lib/core/index.js.map +1 -1
  75. package/lib/core/model-cli.d.ts +4 -11
  76. package/lib/core/model-cli.d.ts.map +1 -1
  77. package/lib/core/model-cli.js +23 -17
  78. package/lib/core/model-cli.js.map +1 -1
  79. package/lib/core/platform-process.d.ts +1 -0
  80. package/lib/core/platform-process.d.ts.map +1 -1
  81. package/lib/core/platform-process.js +1 -1
  82. package/lib/core/platform-process.js.map +1 -1
  83. package/lib/core/run.d.ts +4 -0
  84. package/lib/core/run.d.ts.map +1 -1
  85. package/lib/core/run.js +2 -0
  86. package/lib/core/run.js.map +1 -1
  87. package/lib/drivers/antigravity/config.d.ts +46 -0
  88. package/lib/drivers/antigravity/config.d.ts.map +1 -0
  89. package/lib/drivers/antigravity/config.js +39 -0
  90. package/lib/drivers/antigravity/config.js.map +1 -0
  91. package/lib/drivers/antigravity/events.d.ts +4 -0
  92. package/lib/drivers/antigravity/events.d.ts.map +1 -0
  93. package/lib/drivers/antigravity/events.js +193 -0
  94. package/lib/drivers/antigravity/events.js.map +1 -0
  95. package/lib/drivers/antigravity/index.d.ts +60 -0
  96. package/lib/drivers/antigravity/index.d.ts.map +1 -0
  97. package/lib/drivers/antigravity/index.js +90 -0
  98. package/lib/drivers/antigravity/index.js.map +1 -0
  99. package/lib/drivers/claude-code/index.d.ts +93 -7
  100. package/lib/drivers/claude-code/index.d.ts.map +1 -1
  101. package/lib/drivers/claude-code/index.js +14 -8
  102. package/lib/drivers/claude-code/index.js.map +1 -1
  103. package/lib/drivers/codex/config.d.ts +61 -4
  104. package/lib/drivers/codex/config.d.ts.map +1 -1
  105. package/lib/drivers/codex/config.js +19 -5
  106. package/lib/drivers/codex/config.js.map +1 -1
  107. package/lib/drivers/codex/index.d.ts +63 -5
  108. package/lib/drivers/codex/index.d.ts.map +1 -1
  109. package/lib/drivers/codex/index.js +11 -3
  110. package/lib/drivers/codex/index.js.map +1 -1
  111. package/lib/drivers/copilot/config.d.ts +81 -0
  112. package/lib/drivers/copilot/config.d.ts.map +1 -0
  113. package/lib/drivers/copilot/config.js +54 -0
  114. package/lib/drivers/copilot/config.js.map +1 -0
  115. package/lib/drivers/copilot/index.d.ts +103 -0
  116. package/lib/drivers/copilot/index.d.ts.map +1 -0
  117. package/lib/drivers/copilot/index.js +256 -0
  118. package/lib/drivers/copilot/index.js.map +1 -0
  119. package/lib/drivers/cursor/config.d.ts +6 -1
  120. package/lib/drivers/cursor/config.d.ts.map +1 -1
  121. package/lib/drivers/cursor/config.js +2 -3
  122. package/lib/drivers/cursor/config.js.map +1 -1
  123. package/lib/drivers/cursor/index.d.ts +7 -2
  124. package/lib/drivers/cursor/index.d.ts.map +1 -1
  125. package/lib/drivers/cursor/index.js +9 -6
  126. package/lib/drivers/cursor/index.js.map +1 -1
  127. package/lib/drivers/cursor/interaction.d.ts +1 -2
  128. package/lib/drivers/cursor/interaction.d.ts.map +1 -1
  129. package/lib/drivers/cursor/interaction.js +3 -14
  130. package/lib/drivers/cursor/interaction.js.map +1 -1
  131. package/lib/drivers/{cursor/events.d.ts → shared/acp-events.d.ts} +5 -3
  132. package/lib/drivers/shared/acp-events.d.ts.map +1 -0
  133. package/lib/drivers/{cursor/events.js → shared/acp-events.js} +20 -10
  134. package/lib/drivers/shared/acp-events.js.map +1 -0
  135. package/lib/drivers/shared/acp-interaction.d.ts +7 -0
  136. package/lib/drivers/shared/acp-interaction.d.ts.map +1 -0
  137. package/lib/drivers/shared/acp-interaction.js +20 -0
  138. package/lib/drivers/shared/acp-interaction.js.map +1 -0
  139. package/lib/drivers/{cursor → shared}/acp-transport.d.ts +2 -0
  140. package/lib/drivers/shared/acp-transport.d.ts.map +1 -0
  141. package/lib/drivers/{cursor → shared}/acp-transport.js +10 -10
  142. package/lib/drivers/shared/acp-transport.js.map +1 -0
  143. package/lib/index.d.ts +7 -2
  144. package/lib/index.d.ts.map +1 -1
  145. package/lib/index.js +7 -2
  146. package/lib/index.js.map +1 -1
  147. package/lib/optimization/reflective.d.ts +2 -0
  148. package/lib/optimization/reflective.d.ts.map +1 -1
  149. package/lib/orchestrator/comparison-identity.d.ts.map +1 -1
  150. package/lib/orchestrator/comparison-identity.js +1 -0
  151. package/lib/orchestrator/comparison-identity.js.map +1 -1
  152. package/lib/orchestrator/config.d.ts +7 -0
  153. package/lib/orchestrator/config.d.ts.map +1 -1
  154. package/lib/orchestrator/config.js +8 -1
  155. package/lib/orchestrator/config.js.map +1 -1
  156. package/lib/orchestrator/doctor.d.ts.map +1 -1
  157. package/lib/orchestrator/doctor.js +72 -2
  158. package/lib/orchestrator/doctor.js.map +1 -1
  159. package/lib/orchestrator/experiment-context.d.ts +1 -1
  160. package/lib/orchestrator/harness-catalog-drift.d.ts +22 -0
  161. package/lib/orchestrator/harness-catalog-drift.d.ts.map +1 -0
  162. package/lib/orchestrator/harness-catalog-drift.js +90 -0
  163. package/lib/orchestrator/harness-catalog-drift.js.map +1 -0
  164. package/lib/orchestrator/orchestrator.d.ts +9 -1
  165. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  166. package/lib/orchestrator/orchestrator.js +14 -22
  167. package/lib/orchestrator/orchestrator.js.map +1 -1
  168. package/lib/orchestrator/run-dir.d.ts +2 -0
  169. package/lib/orchestrator/run-dir.d.ts.map +1 -1
  170. package/lib/orchestrator/run-dir.js +1 -1
  171. package/lib/orchestrator/run-dir.js.map +1 -1
  172. package/lib/orchestrator/run-execution.d.ts +1 -0
  173. package/lib/orchestrator/run-execution.d.ts.map +1 -1
  174. package/lib/orchestrator/run-execution.js +2 -0
  175. package/lib/orchestrator/run-execution.js.map +1 -1
  176. package/lib/orchestrator/suite-compare.d.ts +3 -0
  177. package/lib/orchestrator/suite-compare.d.ts.map +1 -1
  178. package/lib/orchestrator/suite-compare.js +27 -17
  179. package/lib/orchestrator/suite-compare.js.map +1 -1
  180. package/lib/orchestrator/suite-runs.d.ts +3 -0
  181. package/lib/orchestrator/suite-runs.d.ts.map +1 -1
  182. package/lib/orchestrator/suite-runs.js +2 -0
  183. package/lib/orchestrator/suite-runs.js.map +1 -1
  184. package/lib/orchestrator/suite-summary.d.ts +9 -0
  185. package/lib/orchestrator/suite-summary.d.ts.map +1 -1
  186. package/lib/orchestrator/suite-summary.js +14 -4
  187. package/lib/orchestrator/suite-summary.js.map +1 -1
  188. package/package.json +1 -1
  189. package/skills/lux-answerer/SKILL.md +1 -1
  190. package/src/authoring.ts +10 -3
  191. package/src/cli/harness-plan.ts +117 -0
  192. package/src/cli/init-templates.ts +12 -5
  193. package/src/cli/init.ts +12 -5
  194. package/src/cli/program.ts +50 -92
  195. package/src/cli/run-options.ts +123 -0
  196. package/src/core/case.ts +3 -0
  197. package/src/core/driver.ts +26 -0
  198. package/src/core/harness-catalog.ts +100 -0
  199. package/src/core/harness-catalogs/antigravity.ts +24 -0
  200. package/src/core/harness-catalogs/claude-code.ts +25 -0
  201. package/src/core/harness-catalogs/codex.ts +27 -0
  202. package/src/core/harness-catalogs/copilot.ts +46 -0
  203. package/src/core/harness-catalogs/cursor.ts +13 -0
  204. package/src/core/harness-catalogs/index.ts +22 -0
  205. package/src/core/harness-matrix.ts +298 -0
  206. package/src/core/index.ts +3 -0
  207. package/src/core/model-cli.ts +38 -21
  208. package/src/core/platform-process.ts +4 -4
  209. package/src/core/run.ts +2 -0
  210. package/src/drivers/antigravity/README.md +46 -0
  211. package/src/drivers/antigravity/config.ts +44 -0
  212. package/src/drivers/antigravity/events.ts +235 -0
  213. package/src/drivers/antigravity/index.ts +109 -0
  214. package/src/drivers/claude-code/index.ts +18 -7
  215. package/src/drivers/codex/config.ts +23 -5
  216. package/src/drivers/codex/index.ts +15 -2
  217. package/src/drivers/copilot/README.md +52 -0
  218. package/src/drivers/copilot/config.ts +58 -0
  219. package/src/drivers/copilot/index.ts +292 -0
  220. package/src/drivers/cursor/README.md +9 -6
  221. package/src/drivers/cursor/config.ts +2 -3
  222. package/src/drivers/cursor/index.ts +10 -6
  223. package/src/drivers/cursor/interaction.ts +3 -19
  224. package/src/drivers/{cursor/events.ts → shared/acp-events.ts} +20 -9
  225. package/src/drivers/shared/acp-interaction.ts +27 -0
  226. package/src/drivers/{cursor → shared}/acp-transport.ts +15 -10
  227. package/src/index.ts +44 -3
  228. package/src/orchestrator/comparison-identity.ts +1 -0
  229. package/src/orchestrator/config.ts +8 -0
  230. package/src/orchestrator/doctor.ts +125 -3
  231. package/src/orchestrator/harness-catalog-drift.ts +132 -0
  232. package/src/orchestrator/orchestrator.ts +31 -26
  233. package/src/orchestrator/run-dir.ts +8 -1
  234. package/src/orchestrator/run-execution.ts +3 -0
  235. package/src/orchestrator/suite-compare.ts +29 -19
  236. package/src/orchestrator/suite-runs.ts +3 -1
  237. package/src/orchestrator/suite-summary.ts +19 -4
  238. package/lib/drivers/cursor/acp-transport.d.ts.map +0 -1
  239. package/lib/drivers/cursor/acp-transport.js.map +0 -1
  240. package/lib/drivers/cursor/events.d.ts.map +0 -1
  241. package/lib/drivers/cursor/events.js.map +0 -1
  242. package/skills/lux/SKILL.md +0 -89
  243. package/skills/lux/references/cli.md +0 -128
  244. package/skills/lux/references/eval-authoring.md +0 -266
@@ -0,0 +1,292 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import {
3
+ type AgentDriver,
4
+ type Answer,
5
+ type Artifact,
6
+ type DriverEventMap,
7
+ defineDriver,
8
+ discoverCliVersion,
9
+ type ReadyState,
10
+ type StartContext,
11
+ selectModelAndReasoningEffort,
12
+ TypedEmitter,
13
+ } from "../../core/index.js";
14
+ import { AcpEvents } from "../shared/acp-events.js";
15
+ import { permissionAnswer, permissionRequestText, records } from "../shared/acp-interaction.js";
16
+ import { AcpTransport, asRecord, type RpcId, type RpcRecord } from "../shared/acp-transport.js";
17
+ import { walkArtifacts } from "../shared/artifacts.js";
18
+ import {
19
+ buildCopilotArgs,
20
+ COPILOT_MODE_IDS,
21
+ type CopilotDriverConfig,
22
+ CopilotDriverConfigSchema,
23
+ } from "./config.js";
24
+
25
+ class CopilotDriver implements AgentDriver {
26
+ private readonly emitter = new TypedEmitter<DriverEventMap>();
27
+ private readonly events: AcpEvents;
28
+ private transport: AcpTransport | undefined;
29
+ private sessionId: string | undefined;
30
+ private cwd: string | undefined;
31
+ private artifactsDir: string | undefined;
32
+ private ready = false;
33
+ private runPromise: Promise<void> | undefined;
34
+ private terminal = false;
35
+ private idleTimer: NodeJS.Timeout | undefined;
36
+ private interruptCount = 0;
37
+ private readonly interrupts = new Map<string, { id: RpcId; params: RpcRecord }>();
38
+
39
+ private readonly config: CopilotDriverConfig;
40
+
41
+ constructor(config: CopilotDriverConfig) {
42
+ this.config = config;
43
+ this.events = new AcpEvents("copilot", config.maxToolCalls, config.maxQueuedBytes);
44
+ }
45
+
46
+ on<K extends keyof DriverEventMap>(
47
+ event: K,
48
+ handler: (e: DriverEventMap[K]) => void | Promise<void>,
49
+ ): void {
50
+ this.emitter.on(event, handler);
51
+ }
52
+ off<K extends keyof DriverEventMap>(
53
+ event: K,
54
+ handler: (e: DriverEventMap[K]) => void | Promise<void>,
55
+ ): void {
56
+ this.emitter.off(event, handler);
57
+ }
58
+
59
+ async start(ctx: StartContext): Promise<ReadyState> {
60
+ if (this.transport || this.terminal)
61
+ throw new Error("Copilot driver cannot be started twice or after close");
62
+ this.cwd = await realpath(this.config.cwd ?? ctx.artifactsDir);
63
+ this.artifactsDir = await realpath(ctx.artifactsDir);
64
+ const cliVersion = await discoverCliVersion(this.config.command);
65
+ const transport = new AcpTransport({
66
+ label: "Copilot ACP",
67
+ command: this.config.command,
68
+ args: buildCopilotArgs(this.config),
69
+ cwd: this.cwd,
70
+ env: { ...process.env, ...this.config.env },
71
+ signal: ctx.abortSignal,
72
+ maxLineBytes: this.config.maxLineBytes,
73
+ maxQueuedBytes: this.config.maxQueuedBytes,
74
+ onMessage: (message) => this.message(message),
75
+ onFailure: (error) => this.fail(error),
76
+ onActivity: () => this.bumpIdleTimer(),
77
+ });
78
+ this.transport = transport;
79
+ try {
80
+ await transport.spawned();
81
+ const initialized = await this.request("initialize", {
82
+ protocolVersion: 1,
83
+ clientInfo: { name: "lux", version: "1" },
84
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
85
+ });
86
+ if (initialized.protocolVersion !== 1)
87
+ throw new Error("Copilot ACP returned an unsupported protocol version");
88
+ if (!records(initialized.authMethods).some((method) => method.id === "copilot-login")) {
89
+ throw new Error(
90
+ "Expected Copilot ACP with copilot-login authentication; check the configured command points to GitHub Copilot CLI",
91
+ );
92
+ }
93
+ const session = await this.request("session/new", { cwd: this.cwd, mcpServers: [] });
94
+ if (typeof session.sessionId !== "string") throw new Error("Copilot ACP omitted sessionId");
95
+ this.sessionId = session.sessionId;
96
+ const modeId = COPILOT_MODE_IDS[this.config.mode];
97
+ const currentModeId = asRecord(session.modes)?.currentModeId;
98
+ if (currentModeId !== modeId) {
99
+ if (!records(asRecord(session.modes)?.availableModes).some((mode) => mode.id === modeId))
100
+ throw new Error(`Copilot ACP does not advertise the '${this.config.mode}' mode`);
101
+ await this.request("session/set_mode", { sessionId: this.sessionId, modeId });
102
+ }
103
+ this.ready = true;
104
+ this.bumpIdleTimer();
105
+ this.runPromise = this.run(ctx.prompt).catch((error: unknown) => this.fail(error));
106
+ return {
107
+ agentId: "copilot",
108
+ model: this.config.model,
109
+ ...(typeof asRecord(initialized.agentInfo)?.version === "string"
110
+ ? { agentVersion: String(asRecord(initialized.agentInfo)?.version) }
111
+ : {}),
112
+ detail: {
113
+ sessionId: this.sessionId,
114
+ transport: "acp",
115
+ protocolVersion: 1,
116
+ ...(cliVersion ? { cliVersion } : {}),
117
+ mode: this.config.mode,
118
+ reasoningEffort: this.config.reasoningEffort,
119
+ modelResolution: "requested",
120
+ nativeConfig: session.configOptions ?? [],
121
+ usageAvailability: "unknown",
122
+ isolation: "none",
123
+ },
124
+ };
125
+ } catch (error) {
126
+ await this.close();
127
+ throw error;
128
+ }
129
+ }
130
+
131
+ private request(method: string, params: RpcRecord): Promise<RpcRecord> {
132
+ if (!this.transport) throw new Error("Copilot ACP has not started");
133
+ return this.transport.request(method, params, this.config.startupTimeoutMs);
134
+ }
135
+
136
+ private async run(prompt: string): Promise<void> {
137
+ const result = await this.transport?.request("session/prompt", {
138
+ sessionId: this.sessionId,
139
+ prompt: [{ type: "text", text: prompt }],
140
+ });
141
+ await this.transport?.drained();
142
+ if (this.terminal) return;
143
+ if (result?.stopReason !== "end_turn")
144
+ throw new Error(`Copilot stopped with ${String(result?.stopReason ?? "unknown reason")}`);
145
+ this.terminal = true;
146
+ clearTimeout(this.idleTimer);
147
+ await this.emitter.emit("done", {
148
+ exitReason: "done",
149
+ summary: {
150
+ sessionId: this.sessionId,
151
+ stopReason: result.stopReason,
152
+ usageAvailability: "unknown",
153
+ },
154
+ });
155
+ }
156
+
157
+ private async message(message: RpcRecord): Promise<void> {
158
+ if (this.terminal) return;
159
+ const method = String(message.method);
160
+ const params = asRecord(message.params) ?? {};
161
+ const id = message.id;
162
+ if (typeof id === "number" || typeof id === "string") {
163
+ if (typeof params.sessionId === "string" && params.sessionId !== this.sessionId) {
164
+ this.transport?.write({ id, error: { code: -32602, message: "Unknown session" } });
165
+ return;
166
+ }
167
+ if (method !== "session/request_permission") {
168
+ this.transport?.write({
169
+ id,
170
+ error: { code: -32601, message: `Unsupported method: ${method}` },
171
+ });
172
+ return;
173
+ }
174
+ if (++this.interruptCount > this.config.maxInterrupts) {
175
+ this.transport?.write({ id, result: { outcome: { outcome: "cancelled" } } });
176
+ throw new Error("Copilot exceeded maxInterrupts");
177
+ }
178
+ const interruptId = `copilot-${id}`;
179
+ if (this.interrupts.has(interruptId)) throw new Error("Copilot reused a pending request ID");
180
+ this.interrupts.set(interruptId, { id, params });
181
+ clearTimeout(this.idleTimer);
182
+ await this.emitter.emit("interrupt", {
183
+ kind: "approve",
184
+ id: interruptId,
185
+ request: permissionRequestText(params),
186
+ context: params,
187
+ });
188
+ return;
189
+ }
190
+ if (method === "session/update") {
191
+ if (params.sessionId !== this.sessionId) return;
192
+ const update = asRecord(params.update);
193
+ if (update)
194
+ for (const event of this.events.update(update)) await this.emitter.emit("progress", event);
195
+ }
196
+ }
197
+
198
+ async respond(answer: Answer): Promise<void> {
199
+ const pending = this.interrupts.get(answer.id);
200
+ if (!pending) throw new Error(`Copilot has no pending interrupt '${answer.id}'`);
201
+ this.transport?.write({ id: pending.id, result: permissionAnswer(pending.params, answer) });
202
+ this.interrupts.delete(answer.id);
203
+ this.bumpIdleTimer();
204
+ }
205
+
206
+ private bumpIdleTimer(): void {
207
+ clearTimeout(this.idleTimer);
208
+ if (!this.ready || this.terminal || this.interrupts.size > 0) return;
209
+ this.idleTimer = setTimeout(
210
+ () =>
211
+ this.fail(
212
+ new Error(`Copilot emitted no event for ${this.config.idleTimeoutMs}ms`),
213
+ "idle-timeout",
214
+ ),
215
+ this.config.idleTimeoutMs,
216
+ );
217
+ }
218
+
219
+ private fail(error: unknown, exitReason: "error" | "idle-timeout" = "error"): void {
220
+ if (this.terminal) return;
221
+ this.terminal = true;
222
+ clearTimeout(this.idleTimer);
223
+ if (this.ready)
224
+ void this.transport?.drained().then(() =>
225
+ this.emitter.emit("error", {
226
+ exitReason,
227
+ reason: error instanceof Error ? error.message : String(error),
228
+ }),
229
+ );
230
+ void this.transport?.close();
231
+ }
232
+
233
+ async quiesce(): Promise<void> {
234
+ clearTimeout(this.idleTimer);
235
+ const cancelling = !this.terminal && this.sessionId !== undefined;
236
+ this.terminal = true;
237
+ if (cancelling) {
238
+ for (const pending of this.interrupts.values())
239
+ this.transport?.write({ id: pending.id, result: { outcome: { outcome: "cancelled" } } });
240
+ this.transport?.write({ method: "session/cancel", params: { sessionId: this.sessionId } });
241
+ let timer: NodeJS.Timeout | undefined;
242
+ try {
243
+ await Promise.race([
244
+ this.runPromise,
245
+ new Promise<void>((resolve) => {
246
+ timer = setTimeout(resolve, 200);
247
+ }),
248
+ ]);
249
+ } finally {
250
+ clearTimeout(timer);
251
+ }
252
+ }
253
+ this.interrupts.clear();
254
+ await this.transport?.close();
255
+ }
256
+
257
+ async collect(): Promise<Artifact[]> {
258
+ await this.quiesce();
259
+ if (!this.cwd) return [];
260
+ const artifacts = await walkArtifacts(this.cwd);
261
+ return this.cwd === this.artifactsDir
262
+ ? artifacts
263
+ : artifacts.map((artifact) => ({ ...artifact, root: this.cwd as string }));
264
+ }
265
+
266
+ async close(): Promise<void> {
267
+ await this.quiesce();
268
+ this.emitter.removeAllListeners();
269
+ }
270
+ }
271
+
272
+ export const copilotDriver = defineDriver({
273
+ name: "copilot",
274
+ configSchema: CopilotDriverConfigSchema,
275
+ runtime: (config) => ({
276
+ commands: [config.command],
277
+ capabilities: {
278
+ interactive: !config.allowAllTools,
279
+ "tool-calls": true,
280
+ "tool-results": true,
281
+ artifacts: true,
282
+ usage: false,
283
+ cost: false,
284
+ isolation: false,
285
+ },
286
+ }),
287
+ selectHarness: selectModelAndReasoningEffort,
288
+ create: (config) => new CopilotDriver(config),
289
+ });
290
+
291
+ export type { CopilotDriverConfig };
292
+ export { buildCopilotArgs, CopilotDriverConfigSchema };
@@ -10,17 +10,20 @@ set `command` to its absolute path if another application owns that name.
10
10
  config: {
11
11
  command: "/absolute/path/to/cursor/agent",
12
12
  model: "YOUR_CURSOR_MODEL_ID",
13
- // Optional: only accepted when ACP advertises this effort value.
13
+ // Optional: one of low, medium, high, xhigh; applied only when ACP advertises it.
14
14
  // reasoningEffort: "low",
15
15
  },
16
16
  }
17
17
  ```
18
18
 
19
- `model` is required. Use an explicit model ID from your installed Cursor CLI;
20
- some IDs encode reasoning effort. Lux passes that ID through `--model` and
21
- rejects a different model when the session reports one. If `reasoningEffort` is
22
- supplied, Lux requires a matching advertised ACP thought-level selector and
23
- checks the response confirms the requested value before sending the prompt.
19
+ `model` is required. Cursor's model list is account-scoped (`agent
20
+ --list-models` after login), so the harness catalog keeps it open: any ID is
21
+ accepted in configuration and in a `matrix`, and some IDs encode reasoning
22
+ effort. Lux passes that ID through `--model` and rejects a different model when
23
+ the session reports one. If `reasoningEffort` is supplied (`low`, `medium`,
24
+ `high`, or `xhigh`), Lux requires a matching advertised ACP thought-level
25
+ selector and checks the response confirms the requested value before sending
26
+ the prompt. No default effort is applied.
24
27
 
25
28
  Supported interactions are Cursor's native multiple-choice questions, plan
26
29
  approval, and ACP tool permissions. Lux grants one-operation permissions only;
@@ -1,12 +1,11 @@
1
1
  import { z } from "zod";
2
+ import { cursorCatalog } from "../../core/index.js";
2
3
 
3
4
  export const CursorDriverConfigSchema = z
4
5
  .object({
5
6
  command: z.string().min(1).default("agent"),
6
- /** Cursor model IDs may include an effort suffix; never inherit Auto. */
7
7
  model: z.string().min(1),
8
- /** Applied only through an advertised ACP thought-level selector. */
9
- reasoningEffort: z.string().min(1).optional(),
8
+ reasoningEffort: z.enum(cursorCatalog.efforts).optional(),
10
9
  mode: z.enum(["agent", "plan", "ask"]).default("agent"),
11
10
  cwd: z.string().optional(),
12
11
  env: z.record(z.string(), z.string()).optional(),
@@ -8,17 +8,19 @@ import {
8
8
  discoverCliVersion,
9
9
  type ReadyState,
10
10
  type StartContext,
11
+ selectModelAndReasoningEffort,
11
12
  TypedEmitter,
12
13
  } from "../../core/index.js";
14
+ import { AcpEvents } from "../shared/acp-events.js";
15
+ import { permissionRequestText, records } from "../shared/acp-interaction.js";
16
+ import { AcpTransport, asRecord, type RpcId, type RpcRecord } from "../shared/acp-transport.js";
13
17
  import { walkArtifacts } from "../shared/artifacts.js";
14
- import { AcpTransport, asRecord, type RpcId, type RpcRecord } from "./acp-transport.js";
15
18
  import { buildCursorArgs, type CursorDriverConfig, CursorDriverConfigSchema } from "./config.js";
16
- import { CursorEvents } from "./events.js";
17
- import { cursorAnswer, cursorQuestionText, effortSelector, records } from "./interaction.js";
19
+ import { cursorAnswer, cursorQuestionText, effortSelector } from "./interaction.js";
18
20
 
19
21
  class CursorDriver implements AgentDriver {
20
22
  private readonly emitter = new TypedEmitter<DriverEventMap>();
21
- private readonly events: CursorEvents;
23
+ private readonly events: AcpEvents;
22
24
  private transport: AcpTransport | undefined;
23
25
  private sessionId: string | undefined;
24
26
  private cwd: string | undefined;
@@ -34,7 +36,7 @@ class CursorDriver implements AgentDriver {
34
36
 
35
37
  constructor(config: CursorDriverConfig) {
36
38
  this.config = config;
37
- this.events = new CursorEvents(config.maxToolCalls, config.maxQueuedBytes);
39
+ this.events = new AcpEvents("cursor", config.maxToolCalls, config.maxQueuedBytes);
38
40
  }
39
41
 
40
42
  on<K extends keyof DriverEventMap>(
@@ -57,6 +59,7 @@ class CursorDriver implements AgentDriver {
57
59
  this.artifactsDir = await realpath(ctx.artifactsDir);
58
60
  const cliVersion = await discoverCliVersion(this.config.command);
59
61
  const transport = new AcpTransport({
62
+ label: "Cursor ACP",
60
63
  command: this.config.command,
61
64
  args: buildCursorArgs(this.config),
62
65
  cwd: this.cwd,
@@ -198,7 +201,7 @@ class CursorDriver implements AgentDriver {
198
201
  request:
199
202
  method === "cursor/create_plan"
200
203
  ? String(params.plan ?? params.overview ?? "Approve this plan?")
201
- : String(asRecord(params.toolCall)?.title ?? "Approve this tool call?"),
204
+ : permissionRequestText(params),
202
205
  context: params,
203
206
  });
204
207
  }
@@ -309,6 +312,7 @@ export const cursorDriver = defineDriver({
309
312
  isolation: false,
310
313
  },
311
314
  }),
315
+ selectHarness: selectModelAndReasoningEffort,
312
316
  create: (config) => new CursorDriver(config),
313
317
  });
314
318
 
@@ -1,13 +1,6 @@
1
1
  import type { Answer } from "../../core/index.js";
2
- import { asRecord, type RpcRecord } from "./acp-transport.js";
3
-
4
- export const records = (value: unknown): RpcRecord[] =>
5
- Array.isArray(value)
6
- ? value.flatMap((entry): RpcRecord[] => {
7
- const record = asRecord(entry);
8
- return record ? [record] : [];
9
- })
10
- : [];
2
+ import { permissionAnswer, records } from "../shared/acp-interaction.js";
3
+ import { asRecord, type RpcRecord } from "../shared/acp-transport.js";
11
4
 
12
5
  export const cursorQuestionText = (params: RpcRecord): string =>
13
6
  `Return a JSON object mapping each question ID to its selected option ID. For questions allowing multiple selections, use an array of option IDs.\n\n${records(
@@ -57,16 +50,7 @@ export const cursorAnswer = (method: string, params: RpcRecord, answer: Answer):
57
50
  : { outcome: "rejected", ...(answer.reason ? { reason: answer.reason } : {}) },
58
51
  };
59
52
  }
60
- const option = records(params.options).find(
61
- (entry) => entry.kind === (answer.approved ? "allow_once" : "reject_once"),
62
- );
63
- // A Lux approval applies to this operation, never a remembered permission.
64
- return {
65
- outcome:
66
- option && typeof option.optionId === "string"
67
- ? { outcome: "selected", optionId: option.optionId }
68
- : { outcome: "cancelled" },
69
- };
53
+ return permissionAnswer(params, answer);
70
54
  };
71
55
 
72
56
  const optionValues = (options: unknown): string[] =>
@@ -1,19 +1,22 @@
1
1
  import { type Progress, toolResultProgress } from "../../core/index.js";
2
+ import { records } from "./acp-interaction.js";
2
3
  import { asRecord, type RpcRecord } from "./acp-transport.js";
3
- import { records } from "./interaction.js";
4
4
 
5
5
  type ToolState = { value: RpcRecord; bytes: number; name: string };
6
6
 
7
- export class CursorEvents {
7
+ /** Normalizes ACP `session/update` notifications into Lux progress events. */
8
+ export class AcpEvents {
8
9
  private seq = 0;
9
10
  private retainedBytes = 0;
10
11
  private readonly tools = new Map<string, ToolState>();
11
12
  private readonly completed = new Set<string>();
12
13
 
14
+ private readonly prefix: string;
13
15
  private readonly maxToolCalls: number;
14
16
  private readonly maxBytes: number;
15
17
 
16
- constructor(maxToolCalls: number, maxBytes: number) {
18
+ constructor(prefix: string, maxToolCalls: number, maxBytes: number) {
19
+ this.prefix = prefix;
17
20
  this.maxToolCalls = maxToolCalls;
18
21
  this.maxBytes = maxBytes;
19
22
  }
@@ -28,7 +31,7 @@ export class CursorEvents {
28
31
  if (content?.type !== "text" || typeof content.text !== "string") return [];
29
32
  return [
30
33
  {
31
- id: `cursor-message-${++this.seq}`,
34
+ id: `${this.prefix}-message-${++this.seq}`,
32
35
  kind: update.sessionUpdate === "agent_message_chunk" ? "agent-message" : "reasoning",
33
36
  payload: { text: content.text },
34
37
  ts,
@@ -36,20 +39,28 @@ export class CursorEvents {
36
39
  ];
37
40
  }
38
41
  if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
39
- return [{ id: `cursor-update-${++this.seq}`, kind: "cursor-update", payload: update, ts }];
42
+ return [
43
+ {
44
+ id: `${this.prefix}-update-${++this.seq}`,
45
+ kind: `${this.prefix}-update`,
46
+ payload: update,
47
+ ts,
48
+ },
49
+ ];
40
50
  }
41
51
  const id = update.toolCallId;
42
- if (typeof id !== "string") throw new Error("Cursor ACP tool update omitted toolCallId");
52
+ if (typeof id !== "string")
53
+ throw new Error(`${this.prefix} ACP tool update omitted toolCallId`);
43
54
  if (this.completed.has(id)) return [];
44
55
  const old = this.tools.get(id);
45
56
  if (!old && this.tools.size + this.completed.size >= this.maxToolCalls)
46
- throw new Error("Cursor ACP exceeded maxToolCalls");
57
+ throw new Error(`${this.prefix} ACP exceeded maxToolCalls`);
47
58
  const value = { ...old?.value, ...update };
48
59
  const bytes = Buffer.byteLength(JSON.stringify(value));
49
60
  this.retainedBytes += bytes - (old?.bytes ?? 0);
50
61
  if (this.retainedBytes > this.maxBytes)
51
- throw new Error("Cursor ACP tool state exceeded maxQueuedBytes");
52
- const name = old?.name ?? String(value.title ?? value.kind ?? "cursor-tool");
62
+ throw new Error(`${this.prefix} ACP tool state exceeded maxQueuedBytes`);
63
+ const name = old?.name ?? String(value.title ?? value.kind ?? `${this.prefix}-tool`);
53
64
  this.tools.set(id, { value, bytes, name });
54
65
  const events: Progress[] = old
55
66
  ? []
@@ -0,0 +1,27 @@
1
+ import type { Answer } from "../../core/index.js";
2
+ import { asRecord, type RpcRecord } from "./acp-transport.js";
3
+
4
+ export const records = (value: unknown): RpcRecord[] =>
5
+ Array.isArray(value)
6
+ ? value.flatMap((entry): RpcRecord[] => {
7
+ const record = asRecord(entry);
8
+ return record ? [record] : [];
9
+ })
10
+ : [];
11
+
12
+ /** A Lux approval applies to this operation only, never a remembered permission. */
13
+ export const permissionAnswer = (params: RpcRecord, answer: Answer): RpcRecord => {
14
+ if (answer.kind !== "approve") throw new Error("ACP permission requires an approval answer");
15
+ const option = records(params.options).find(
16
+ (entry) => entry.kind === (answer.approved ? "allow_once" : "reject_once"),
17
+ );
18
+ return {
19
+ outcome:
20
+ option && typeof option.optionId === "string"
21
+ ? { outcome: "selected", optionId: option.optionId }
22
+ : { outcome: "cancelled" },
23
+ };
24
+ };
25
+
26
+ export const permissionRequestText = (params: RpcRecord): string =>
27
+ String(asRecord(params.toolCall)?.title ?? "Approve this tool call?");
@@ -8,6 +8,8 @@ export const asRecord = (value: unknown): RpcRecord | undefined =>
8
8
  : undefined;
9
9
 
10
10
  type TransportOptions = {
11
+ /** Harness name used in failure messages. */
12
+ label: string;
11
13
  command: string;
12
14
  args: string[];
13
15
  cwd: string;
@@ -55,7 +57,7 @@ export class AcpTransport {
55
57
  if (!this.stopped)
56
58
  this.fail(
57
59
  new Error(
58
- `Cursor ACP exited before completion (code=${exitCode}, signal=${signal})${this.stderrTail ? `: ${this.stderrTail}` : ""}`,
60
+ `${this.options.label} exited before completion (code=${exitCode}, signal=${signal})${this.stderrTail ? `: ${this.stderrTail}` : ""}`,
59
61
  ),
60
62
  );
61
63
  });
@@ -66,14 +68,16 @@ export class AcpTransport {
66
68
  }
67
69
 
68
70
  request(method: string, params: RpcRecord, timeoutMs?: number): Promise<RpcRecord> {
69
- if (this.stopped) return Promise.reject(new Error("Cursor ACP is closed"));
71
+ if (this.stopped) return Promise.reject(new Error(`${this.options.label} is closed`));
70
72
  const id = this.nextId++;
71
73
  return new Promise((resolve, reject) => {
72
74
  const timer =
73
75
  timeoutMs === undefined
74
76
  ? undefined
75
77
  : setTimeout(() => {
76
- this.fail(new Error(`Cursor ACP ${method} timed out after ${timeoutMs}ms`));
78
+ this.fail(
79
+ new Error(`${this.options.label} ${method} timed out after ${timeoutMs}ms`),
80
+ );
77
81
  }, timeoutMs);
78
82
  this.pending.set(id, { resolve, reject, timer });
79
83
  try {
@@ -85,10 +89,10 @@ export class AcpTransport {
85
89
  }
86
90
 
87
91
  write(message: RpcRecord): void {
88
- if (this.stopped) throw new Error("Cursor ACP is closed");
92
+ if (this.stopped) throw new Error(`${this.options.label} is closed`);
89
93
  const line = `${JSON.stringify({ jsonrpc: "2.0", ...message })}\n`;
90
94
  if (Buffer.byteLength(line) > this.options.maxLineBytes)
91
- throw new Error("Cursor ACP outgoing message exceeded maxLineBytes");
95
+ throw new Error(`${this.options.label} outgoing message exceeded maxLineBytes`);
92
96
  this.process.child.stdin.write(line);
93
97
  }
94
98
 
@@ -98,7 +102,7 @@ export class AcpTransport {
98
102
 
99
103
  async close(): Promise<void> {
100
104
  this.stopped = true;
101
- this.rejectPending(new Error("Cursor ACP is closed"));
105
+ this.rejectPending(new Error(`${this.options.label} is closed`));
102
106
  this.buffer = Buffer.alloc(0);
103
107
  await this.process.terminate();
104
108
  }
@@ -126,7 +130,7 @@ export class AcpTransport {
126
130
  const end = newline === -1 ? chunk.length : newline;
127
131
  const segment = chunk.subarray(offset, end);
128
132
  if (this.buffer.length + segment.length > this.options.maxLineBytes) {
129
- this.fail(new Error("Cursor ACP stdout line exceeded maxLineBytes"));
133
+ this.fail(new Error(`${this.options.label} stdout line exceeded maxLineBytes`));
130
134
  return;
131
135
  }
132
136
  this.buffer = Buffer.concat([this.buffer, segment]);
@@ -147,7 +151,7 @@ export class AcpTransport {
147
151
  /* Rejected below. */
148
152
  }
149
153
  if (message?.jsonrpc !== "2.0") {
150
- this.fail(new Error("Cursor ACP returned an invalid JSON-RPC message"));
154
+ this.fail(new Error(`${this.options.label} returned an invalid JSON-RPC message`));
151
155
  return;
152
156
  }
153
157
  this.options.onActivity();
@@ -157,13 +161,14 @@ export class AcpTransport {
157
161
  this.pending.delete(message.id as RpcId);
158
162
  clearTimeout(pending.timer);
159
163
  const error = asRecord(message.error);
160
- if (error) pending.reject(new Error(String(error.message ?? "Cursor ACP request failed")));
164
+ if (error)
165
+ pending.reject(new Error(String(error.message ?? `${this.options.label} request failed`)));
161
166
  else pending.resolve(asRecord(message.result) ?? {});
162
167
  return;
163
168
  }
164
169
  this.queuedBytes += line.length;
165
170
  if (this.queuedBytes > this.options.maxQueuedBytes) {
166
- this.fail(new Error("Cursor ACP pending events exceeded maxQueuedBytes"));
171
+ this.fail(new Error(`${this.options.label} pending events exceeded maxQueuedBytes`));
167
172
  return;
168
173
  }
169
174
  const queued = message;