@hegemonart/get-design-done 1.25.0 → 1.27.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 (58) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +96 -0
  4. package/README.md +12 -6
  5. package/SKILL.md +3 -0
  6. package/agents/README.md +89 -0
  7. package/agents/design-reflector.md +43 -0
  8. package/agents/gdd-intel-updater.md +34 -1
  9. package/hooks/budget-enforcer.ts +143 -4
  10. package/package.json +1 -1
  11. package/reference/model-prices.md +40 -19
  12. package/reference/peer-cli-capabilities.md +151 -0
  13. package/reference/peer-protocols.md +266 -0
  14. package/reference/prices/antigravity.md +21 -0
  15. package/reference/prices/augment.md +21 -0
  16. package/reference/prices/claude.md +42 -0
  17. package/reference/prices/cline.md +23 -0
  18. package/reference/prices/codebuddy.md +21 -0
  19. package/reference/prices/codex.md +25 -0
  20. package/reference/prices/copilot.md +21 -0
  21. package/reference/prices/cursor.md +21 -0
  22. package/reference/prices/gemini.md +25 -0
  23. package/reference/prices/kilo.md +21 -0
  24. package/reference/prices/opencode.md +23 -0
  25. package/reference/prices/qwen.md +25 -0
  26. package/reference/prices/trae.md +23 -0
  27. package/reference/prices/windsurf.md +21 -0
  28. package/reference/registry.json +121 -1
  29. package/reference/runtime-models.md +446 -0
  30. package/reference/schemas/runtime-models.schema.json +123 -0
  31. package/scripts/install.cjs +8 -0
  32. package/scripts/lib/bandit-router.cjs +214 -7
  33. package/scripts/lib/budget-enforcer.cjs +514 -0
  34. package/scripts/lib/cost-arbitrage.cjs +294 -0
  35. package/scripts/lib/event-stream/index.ts +14 -1
  36. package/scripts/lib/event-stream/types.ts +125 -1
  37. package/scripts/lib/install/installer.cjs +188 -11
  38. package/scripts/lib/install/parse-runtime-models.cjs +267 -0
  39. package/scripts/lib/install/runtimes.cjs +101 -0
  40. package/scripts/lib/peer-cli/acp-client.cjs +375 -0
  41. package/scripts/lib/peer-cli/adapters/codex.cjs +101 -0
  42. package/scripts/lib/peer-cli/adapters/copilot.cjs +79 -0
  43. package/scripts/lib/peer-cli/adapters/cursor.cjs +78 -0
  44. package/scripts/lib/peer-cli/adapters/gemini.cjs +81 -0
  45. package/scripts/lib/peer-cli/adapters/qwen.cjs +72 -0
  46. package/scripts/lib/peer-cli/asp-client.cjs +587 -0
  47. package/scripts/lib/peer-cli/broker-lifecycle.cjs +406 -0
  48. package/scripts/lib/peer-cli/registry.cjs +434 -0
  49. package/scripts/lib/peer-cli/spawn-cmd.cjs +149 -0
  50. package/scripts/lib/runtime-detect.cjs +96 -0
  51. package/scripts/lib/session-runner/index.ts +215 -0
  52. package/scripts/lib/session-runner/types.ts +60 -0
  53. package/scripts/lib/tier-resolver.cjs +311 -0
  54. package/scripts/validate-frontmatter.ts +297 -2
  55. package/skills/peer-cli-add/SKILL.md +170 -0
  56. package/skills/peer-cli-customize/SKILL.md +110 -0
  57. package/skills/peers/SKILL.md +101 -0
  58. package/skills/router/SKILL.md +51 -2
@@ -0,0 +1,375 @@
1
+ // scripts/lib/peer-cli/acp-client.cjs
2
+ //
3
+ // Plan 27-01 — Agent Client Protocol (ACP) client.
4
+ //
5
+ // Protocol shape adapted from greenpolo/cc-multi-cli `acp-client.mjs`
6
+ // (Apache 2.0). See NOTICE for full attribution (added by Plan 27-12).
7
+ //
8
+ // ACP is the line-delimited JSON-RPC 2.0 transport spoken by the Gemini,
9
+ // Cursor, Copilot, and Qwen CLIs when launched in their `acp` mode. We
10
+ // drive a child process over stdio, send framed JSON requests on stdin,
11
+ // and read framed JSON responses + notifications off stdout. Every
12
+ // outbound request gets a numeric `id`; the server's reply is correlated
13
+ // back via that id. Server-pushed notifications (`agent_message_chunk`,
14
+ // `tool_call`, `file_change`, etc.) lack an id and are surfaced through
15
+ // the `onNotification` callback supplied to `prompt()`.
16
+ //
17
+ // Wire framing:
18
+ // send: `<json>\n` over stdin.
19
+ // recv: `<json>\n` over stdout, line-delimited.
20
+ //
21
+ // The framing layer must handle three real-world conditions:
22
+ // (a) multiple complete JSON messages arriving in one stdout chunk,
23
+ // (b) a single JSON message split across multiple stdout chunks, and
24
+ // (c) misbehaved peer never emitting a newline — we cap the buffered
25
+ // line length at 16 MiB and reject the active prompt rather than
26
+ // grow memory unbounded.
27
+ //
28
+ // Module exports:
29
+ // createAcpClient({command, args, cwd, env}) -> AcpClient
30
+ //
31
+ // AcpClient:
32
+ // initialize(params) -> Promise<result>
33
+ // First call after spawn. Sends the JSON-RPC `initialize` request
34
+ // with the negotiated `protocolVersion` + `clientCapabilities` and
35
+ // resolves with the server's capability reply.
36
+ //
37
+ // prompt(text, opts) -> Promise<result>
38
+ // Sends a `prompt` request. Notifications received between request
39
+ // send and response receive are forwarded to opts.onNotification(n).
40
+ // Resolves with the final `result` payload tied to the request id.
41
+ //
42
+ // close() -> Promise<void>
43
+ // Sends SIGTERM to the child, waits for exit, drains any in-flight
44
+ // promises with a "client closed" rejection.
45
+ //
46
+ // This module has no external dependencies — only Node built-ins
47
+ // (`child_process`, `events`).
48
+
49
+ 'use strict';
50
+
51
+ const { spawn } = require('child_process');
52
+ const { EventEmitter } = require('events');
53
+
54
+ /**
55
+ * Hard cap on the size of a single un-terminated line read from the
56
+ * peer's stdout. If the peer streams more than this without a `\n`, we
57
+ * treat it as a protocol violation and reject all pending requests.
58
+ * 16 MiB is well above any legitimate ACP payload (largest observed in
59
+ * cc-multi-cli traces is ~2 MiB for tool-call results with embedded
60
+ * file diffs) but small enough that a runaway peer can't OOM the host.
61
+ */
62
+ const MAX_LINE_BYTES = 16 * 1024 * 1024;
63
+
64
+ /**
65
+ * Default ACP protocol version we negotiate with. Callers can override
66
+ * via `initialize({ protocolVersion: '...' })`. The current Gemini /
67
+ * Cursor / Copilot / Qwen CLIs all advertise `2025-06-18` as of writing.
68
+ */
69
+ const DEFAULT_PROTOCOL_VERSION = '2025-06-18';
70
+
71
+ /**
72
+ * Create an ACP client wrapping a freshly spawned peer-CLI process.
73
+ *
74
+ * @param {object} opts
75
+ * @param {string} opts.command Absolute path (or PATH-resolvable name)
76
+ * to the peer-CLI binary. The caller is responsible for resolving
77
+ * peer-binary location (Plan 27-11 ships `peerBinary` on runtimes.cjs
78
+ * for that). On Windows, `.cmd` shims need the spawn-cmd.cjs
79
+ * workaround (Plan 27-03) — this module does NOT apply that shim
80
+ * itself, callers must wrap.
81
+ * @param {string[]} [opts.args=[]] Extra args to the peer binary;
82
+ * typical value is `['acp']` to launch the peer in ACP mode.
83
+ * @param {string} [opts.cwd=process.cwd()] Working directory for the
84
+ * child process.
85
+ * @param {Record<string,string>} [opts.env] Environment overrides.
86
+ * Defaults to inheriting from process.env.
87
+ * @returns {{
88
+ * initialize: (params: object) => Promise<unknown>,
89
+ * prompt: (text: string, opts?: object) => Promise<unknown>,
90
+ * close: () => Promise<void>,
91
+ * on: (event: string, listener: Function) => void,
92
+ * pid: number | undefined,
93
+ * }}
94
+ */
95
+ function createAcpClient(opts) {
96
+ if (!opts || typeof opts.command !== 'string' || opts.command.length === 0) {
97
+ throw new TypeError('createAcpClient: opts.command (string) is required');
98
+ }
99
+ const command = opts.command;
100
+ const args = Array.isArray(opts.args) ? opts.args : [];
101
+ const cwd = typeof opts.cwd === 'string' ? opts.cwd : process.cwd();
102
+ const env = opts.env && typeof opts.env === 'object' ? opts.env : process.env;
103
+
104
+ const events = new EventEmitter();
105
+
106
+ // Spawn the child. We use plain `spawn` (no shell) — Windows `.cmd`
107
+ // dispatch is the caller's responsibility (Plan 27-03 spawn-cmd.cjs).
108
+ const child = spawn(command, args, {
109
+ cwd,
110
+ env,
111
+ stdio: ['pipe', 'pipe', 'pipe'],
112
+ windowsHide: true,
113
+ });
114
+
115
+ // Per-request correlation. Server replies carry the id we sent; we
116
+ // resolve/reject the matching pending entry. Notifications (no id)
117
+ // bypass this map and route to the active prompt's onNotification.
118
+ let nextRequestId = 1;
119
+ /** @type {Map<number, {resolve: Function, reject: Function, onNotification?: Function}>} */
120
+ const pending = new Map();
121
+
122
+ // The id of the request whose notifications we currently surface.
123
+ // ACP is half-duplex per stream — only one prompt is in flight at
124
+ // once from the host's perspective — so we track it as a single ref.
125
+ let activeNotificationTargetId = null;
126
+
127
+ // Closure / error state.
128
+ let closed = false;
129
+ /** @type {Error | null} */
130
+ let fatalError = null;
131
+
132
+ // Line-buffer state. Stdout chunks accumulate here; each `\n` flushes
133
+ // a complete JSON message into handleMessage().
134
+ let lineBuffer = '';
135
+
136
+ child.stdout.setEncoding('utf8');
137
+ child.stdout.on('data', onStdoutData);
138
+ child.stderr.setEncoding('utf8');
139
+ child.stderr.on('data', (chunk) => {
140
+ // Surface stderr verbatim so callers can wire it into Phase 22
141
+ // event chain or simple debug logging without us imposing a
142
+ // structure on it.
143
+ events.emit('stderr', chunk);
144
+ });
145
+ child.on('error', (err) => {
146
+ fail(err);
147
+ });
148
+ child.on('exit', (code, signal) => {
149
+ events.emit('exit', { code, signal });
150
+ if (!closed) {
151
+ // Unexpected exit → fail every in-flight request.
152
+ fail(new Error(`ACP peer exited unexpectedly (code=${code}, signal=${signal})`));
153
+ }
154
+ });
155
+
156
+ /**
157
+ * Append a stdout chunk to the line buffer and flush every complete
158
+ * line. A "line" is everything up to (but not including) a `\n`.
159
+ * Lines longer than MAX_LINE_BYTES indicate a malformed peer stream
160
+ * — we tear the client down with an error so callers don't OOM.
161
+ */
162
+ function onStdoutData(chunk) {
163
+ if (fatalError) return;
164
+ lineBuffer += chunk;
165
+
166
+ if (lineBuffer.length > MAX_LINE_BYTES) {
167
+ // We measured length in code units (UTF-16) but the cap is in
168
+ // bytes; this is an over-eager check (UTF-16 length <= UTF-8
169
+ // byte count is not always true, but for our payloads of mostly
170
+ // ASCII JSON the two are within a small constant). The intent
171
+ // is "no peer should ever emit this much without a newline" —
172
+ // exact byte accounting isn't worth the Buffer churn here.
173
+ fail(new Error(
174
+ `ACP peer emitted ${lineBuffer.length} bytes without a newline ` +
175
+ `(cap: ${MAX_LINE_BYTES} bytes) — protocol violation`,
176
+ ));
177
+ return;
178
+ }
179
+
180
+ let newlineIdx;
181
+ while ((newlineIdx = lineBuffer.indexOf('\n')) !== -1) {
182
+ const line = lineBuffer.slice(0, newlineIdx);
183
+ lineBuffer = lineBuffer.slice(newlineIdx + 1);
184
+ if (line.length === 0) continue; // tolerate keep-alive blank lines
185
+ handleLine(line);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Parse one JSON-RPC frame and dispatch it to either the response
191
+ * correlation table (if it has an `id` matching a pending request)
192
+ * or the active notification sink.
193
+ */
194
+ function handleLine(line) {
195
+ let msg;
196
+ try {
197
+ msg = JSON.parse(line);
198
+ } catch (err) {
199
+ // Malformed JSON from the peer is a protocol violation but not
200
+ // necessarily fatal — the peer may recover on the next line.
201
+ // Surface it so callers can log, but keep going.
202
+ events.emit('parse_error', { line, error: err });
203
+ return;
204
+ }
205
+
206
+ // Standard JSON-RPC 2.0 dispatch:
207
+ // - response : has `id` AND (`result` OR `error`)
208
+ // - notification : has `method` AND no `id`
209
+ // - request from server (e.g. tool_call back-channel) : has `id`
210
+ // AND `method`. ACP peers can issue these; for v1 we treat
211
+ // them as notifications (callers handle via onNotification).
212
+ //
213
+ // We dispatch in priority: response first (id present + no method),
214
+ // then notifications (method present).
215
+ const hasId = Object.prototype.hasOwnProperty.call(msg, 'id') && msg.id !== null;
216
+ const hasMethod = typeof msg.method === 'string';
217
+
218
+ if (hasId && !hasMethod) {
219
+ const entry = pending.get(msg.id);
220
+ if (!entry) {
221
+ // Unknown id — peer replied to a request we didn't send (or
222
+ // we already timed it out). Surface for diagnostic but ignore.
223
+ events.emit('orphan_response', msg);
224
+ return;
225
+ }
226
+ pending.delete(msg.id);
227
+ if (msg.id === activeNotificationTargetId) {
228
+ activeNotificationTargetId = null;
229
+ }
230
+ if (msg.error) {
231
+ const err = new Error(
232
+ (msg.error && msg.error.message) || 'ACP peer returned error',
233
+ );
234
+ // Preserve the JSON-RPC error envelope so callers can inspect
235
+ // .code and .data without losing typing.
236
+ err.code = msg.error.code;
237
+ err.data = msg.error.data;
238
+ entry.reject(err);
239
+ } else {
240
+ entry.resolve(msg.result);
241
+ }
242
+ return;
243
+ }
244
+
245
+ if (hasMethod) {
246
+ // Notification (or server-issued request — same handling for v1).
247
+ if (activeNotificationTargetId !== null) {
248
+ const entry = pending.get(activeNotificationTargetId);
249
+ if (entry && typeof entry.onNotification === 'function') {
250
+ try {
251
+ entry.onNotification(msg);
252
+ } catch (err) {
253
+ // Caller's notification handler threw — surface but don't
254
+ // tear down the protocol stream.
255
+ events.emit('notification_handler_error', { error: err, notification: msg });
256
+ }
257
+ }
258
+ }
259
+ // Always emit on the EventEmitter too, so callers without an
260
+ // active prompt (e.g. health monitor) can still observe.
261
+ events.emit('notification', msg);
262
+ return;
263
+ }
264
+
265
+ // Neither id+result nor method — malformed. Surface for diagnostic.
266
+ events.emit('protocol_violation', msg);
267
+ }
268
+
269
+ /**
270
+ * Send a JSON-RPC request and return a Promise resolving to its
271
+ * `result`. Caller-supplied `onNotification` is fired for every
272
+ * notification received between send and reply.
273
+ */
274
+ function sendRequest(method, params, onNotification) {
275
+ if (closed) {
276
+ return Promise.reject(new Error('ACP client is closed'));
277
+ }
278
+ if (fatalError) {
279
+ return Promise.reject(fatalError);
280
+ }
281
+ const id = nextRequestId++;
282
+ const frame = JSON.stringify({ jsonrpc: '2.0', id, method, params });
283
+ return new Promise((resolve, reject) => {
284
+ pending.set(id, { resolve, reject, onNotification });
285
+ activeNotificationTargetId = id;
286
+ const ok = child.stdin.write(frame + '\n', 'utf8', (err) => {
287
+ if (err) {
288
+ pending.delete(id);
289
+ if (activeNotificationTargetId === id) activeNotificationTargetId = null;
290
+ reject(err);
291
+ }
292
+ });
293
+ // Backpressure: `write` returning false signals the kernel buffer
294
+ // is full. For a JSON-RPC line of typical size (<10 KiB) this is
295
+ // exceedingly rare; we don't block on `drain` but trust Node's
296
+ // internal queue. If a pathological peer stalls reads, the
297
+ // kernel buffer fills and write throws via the callback above.
298
+ void ok;
299
+ });
300
+ }
301
+
302
+ /**
303
+ * Tear down all in-flight promises with the given error. Safe to
304
+ * call multiple times — subsequent calls are no-ops.
305
+ */
306
+ function fail(err) {
307
+ if (fatalError) return;
308
+ fatalError = err;
309
+ for (const [, entry] of pending) {
310
+ try { entry.reject(err); } catch { /* ignore */ }
311
+ }
312
+ pending.clear();
313
+ activeNotificationTargetId = null;
314
+ }
315
+
316
+ /** ACP `initialize` — first call after spawn. */
317
+ function initialize(params) {
318
+ const merged = Object.assign(
319
+ { protocolVersion: DEFAULT_PROTOCOL_VERSION, clientCapabilities: {} },
320
+ params || {},
321
+ );
322
+ return sendRequest('initialize', merged);
323
+ }
324
+
325
+ /** ACP `prompt` — primary turn-driver. */
326
+ function prompt(text, promptOpts) {
327
+ const params = Object.assign({ text }, (promptOpts && promptOpts.params) || {});
328
+ const onNotification = promptOpts && typeof promptOpts.onNotification === 'function'
329
+ ? promptOpts.onNotification
330
+ : undefined;
331
+ return sendRequest('prompt', params, onNotification);
332
+ }
333
+
334
+ /** Gracefully terminate the peer; resolves once the child exits. */
335
+ function close() {
336
+ if (closed) return Promise.resolve();
337
+ closed = true;
338
+ return new Promise((resolve) => {
339
+ const onExit = () => {
340
+ fail(new Error('ACP client is closed'));
341
+ resolve();
342
+ };
343
+ if (child.exitCode !== null || child.signalCode !== null) {
344
+ // Already exited.
345
+ onExit();
346
+ return;
347
+ }
348
+ child.once('exit', onExit);
349
+ try {
350
+ child.stdin.end();
351
+ } catch { /* ignore */ }
352
+ // Give the peer a moment to exit on its own after stdin EOF.
353
+ // 500ms is well above ACP cleanup time observed in cc-multi-cli.
354
+ setTimeout(() => {
355
+ if (child.exitCode === null && child.signalCode === null) {
356
+ try { child.kill('SIGTERM'); } catch { /* ignore */ }
357
+ }
358
+ }, 500);
359
+ });
360
+ }
361
+
362
+ return {
363
+ initialize,
364
+ prompt,
365
+ close,
366
+ on: events.on.bind(events),
367
+ get pid() { return child.pid; },
368
+ };
369
+ }
370
+
371
+ module.exports = {
372
+ createAcpClient,
373
+ MAX_LINE_BYTES,
374
+ DEFAULT_PROTOCOL_VERSION,
375
+ };
@@ -0,0 +1,101 @@
1
+ // scripts/lib/peer-cli/adapters/codex.cjs
2
+ //
3
+ // Plan 27-04 — Per-peer adapter for the Codex CLI.
4
+ //
5
+ // Codex is the only peer in our matrix that speaks ASP (App Server
6
+ // Protocol) rather than ACP. The ASP transport is thread-oriented:
7
+ // every turn lives inside a `threadId` that we obtain via `threadStart`
8
+ // before driving the conversation with `turn(threadId, text)`. See
9
+ // scripts/lib/peer-cli/asp-client.cjs for the protocol details.
10
+ //
11
+ // Capability matrix (CONTEXT.md D-05):
12
+ // * Codex claims the `execute` role only.
13
+ //
14
+ // The slash-command translation layer is wafer-thin: when the registry
15
+ // dispatches role=`execute`, we prepend `/execute ` to the user's text
16
+ // so Codex routes the prompt through its execute pipeline. Roles the
17
+ // peer doesn't claim are rejected at `dispatch()` time — the registry
18
+ // (Plan 27-05) uses `claims(role)` to filter before reaching us, so
19
+ // hitting this branch indicates a registry bug or a misconfigured
20
+ // `delegate_to:` frontmatter.
21
+
22
+ 'use strict';
23
+
24
+ const { createAspClient } = require('../asp-client.cjs');
25
+
26
+ /** Roles this peer claims. Roles outside this set are refused. */
27
+ const ROLES_CLAIMED = Object.freeze(['execute']);
28
+
29
+ /**
30
+ * Per-role prompt prefix. Codex understands `/execute` natively, so the
31
+ * prefix doubles as both a role marker and a Codex slash command.
32
+ */
33
+ const ROLE_PREFIX = Object.freeze({
34
+ execute: '/execute ',
35
+ });
36
+
37
+ /**
38
+ * Cheap predicate the registry consults to decide whether to dispatch
39
+ * a role to this peer. Pure / synchronous on purpose.
40
+ */
41
+ function claims(role) {
42
+ return ROLES_CLAIMED.includes(role);
43
+ }
44
+
45
+ /**
46
+ * Drive a single Codex turn for the supplied role + text.
47
+ *
48
+ * @param {{command: string, args?: string[], cwd?: string, env?: object}} peer
49
+ * Spawn descriptor for the Codex binary. The registry resolves these
50
+ * from runtimes.cjs (Plan 27-11 ships `peerBinary`).
51
+ * @param {string} role Must satisfy `claims(role)`.
52
+ * @param {string} text User-supplied prompt; we prepend ROLE_PREFIX.
53
+ * @param {{onNotification?: (n: object) => void, threadId?: string}} [opts]
54
+ * `onNotification` is forwarded to the ASP turn for streaming
55
+ * visibility. `threadId` lets the broker (Plan 27-03) resume a thread
56
+ * instead of starting a fresh one — v1.27.0 always passes undefined.
57
+ * @returns {Promise<object>} The ASP `turn()` result envelope —
58
+ * `{status, content, usage, threadId, turnId, notifications}` — passed
59
+ * through unchanged so the caller can branch on `status === 'error'`.
60
+ */
61
+ async function dispatch(peer, role, text, opts) {
62
+ if (!claims(role)) {
63
+ throw new Error(`codex adapter does not claim role: ${role}`);
64
+ }
65
+ if (typeof text !== 'string') {
66
+ throw new TypeError('codex adapter: text must be a string');
67
+ }
68
+ const onNotification = opts && typeof opts.onNotification === 'function'
69
+ ? opts.onNotification
70
+ : undefined;
71
+ const explicitThreadId = opts && typeof opts.threadId === 'string' && opts.threadId.length > 0
72
+ ? opts.threadId
73
+ : null;
74
+
75
+ const client = createAspClient({
76
+ command: peer.command,
77
+ args: peer.args,
78
+ cwd: peer.cwd,
79
+ env: peer.env,
80
+ });
81
+ try {
82
+ let threadId = explicitThreadId;
83
+ if (threadId === null) {
84
+ const started = await client.threadStart({ service_name: 'gdd_peer_delegation' });
85
+ threadId = started.threadId;
86
+ }
87
+ const prompt = ROLE_PREFIX[role] + text;
88
+ return await client.turn(threadId, prompt, { onNotification });
89
+ } finally {
90
+ await client.close();
91
+ }
92
+ }
93
+
94
+ module.exports = {
95
+ name: 'codex',
96
+ protocol: 'asp',
97
+ ROLES_CLAIMED,
98
+ ROLE_PREFIX,
99
+ claims,
100
+ dispatch,
101
+ };
@@ -0,0 +1,79 @@
1
+ // scripts/lib/peer-cli/adapters/copilot.cjs
2
+ //
3
+ // Plan 27-04 — Per-peer adapter for the GitHub Copilot CLI.
4
+ //
5
+ // Copilot speaks ACP (line-delimited JSON-RPC over stdio); see
6
+ // scripts/lib/peer-cli/acp-client.cjs.
7
+ //
8
+ // Capability matrix (CONTEXT.md D-05):
9
+ // * Copilot claims the `review` and `research` roles.
10
+ //
11
+ // Note: `research` overlaps with Gemini. The registry (Plan 27-05)
12
+ // arbitrates by per-peer health + posterior win-rate; this adapter
13
+ // just declares membership in both role pools.
14
+ //
15
+ // Copilot exposes `/review` and `/research` as slash commands.
16
+
17
+ 'use strict';
18
+
19
+ const { createAcpClient } = require('../acp-client.cjs');
20
+
21
+ /** Roles this peer claims. */
22
+ const ROLES_CLAIMED = Object.freeze(['review', 'research']);
23
+
24
+ /**
25
+ * Per-role slash-command prefix. Copilot recognizes `/review` and
26
+ * `/research` natively at the start of a prompt.
27
+ */
28
+ const ROLE_PREFIX = Object.freeze({
29
+ review: '/review ',
30
+ research: '/research ',
31
+ });
32
+
33
+ function claims(role) {
34
+ return ROLES_CLAIMED.includes(role);
35
+ }
36
+
37
+ /**
38
+ * Drive one Copilot ACP `prompt` for the supplied role + text.
39
+ *
40
+ * @param {{command: string, args?: string[], cwd?: string, env?: object}} peer
41
+ * @param {string} role
42
+ * @param {string} text
43
+ * @param {{onNotification?: (n: object) => void}} [opts]
44
+ * @returns {Promise<object>}
45
+ */
46
+ async function dispatch(peer, role, text, opts) {
47
+ if (!claims(role)) {
48
+ throw new Error(`copilot adapter does not claim role: ${role}`);
49
+ }
50
+ if (typeof text !== 'string') {
51
+ throw new TypeError('copilot adapter: text must be a string');
52
+ }
53
+ const onNotification = opts && typeof opts.onNotification === 'function'
54
+ ? opts.onNotification
55
+ : undefined;
56
+
57
+ const client = createAcpClient({
58
+ command: peer.command,
59
+ args: peer.args,
60
+ cwd: peer.cwd,
61
+ env: peer.env,
62
+ });
63
+ try {
64
+ await client.initialize({ protocolVersion: '2025-06-18' });
65
+ const prompt = ROLE_PREFIX[role] + text;
66
+ return await client.prompt(prompt, { onNotification });
67
+ } finally {
68
+ await client.close();
69
+ }
70
+ }
71
+
72
+ module.exports = {
73
+ name: 'copilot',
74
+ protocol: 'acp',
75
+ ROLES_CLAIMED,
76
+ ROLE_PREFIX,
77
+ claims,
78
+ dispatch,
79
+ };
@@ -0,0 +1,78 @@
1
+ // scripts/lib/peer-cli/adapters/cursor.cjs
2
+ //
3
+ // Plan 27-04 — Per-peer adapter for the Cursor CLI.
4
+ //
5
+ // Cursor speaks ACP (line-delimited JSON-RPC over stdio); see
6
+ // scripts/lib/peer-cli/acp-client.cjs.
7
+ //
8
+ // Capability matrix (CONTEXT.md D-05):
9
+ // * Cursor claims the `debug` and `plan` roles.
10
+ //
11
+ // Cursor exposes first-class slash commands for both roles — `/debug`
12
+ // invokes its debugger-aware planner; `/plan` invokes its multi-step
13
+ // planning flow. Translation is therefore literal: prepend the slash
14
+ // command + space to the user's text.
15
+
16
+ 'use strict';
17
+
18
+ const { createAcpClient } = require('../acp-client.cjs');
19
+
20
+ /** Roles this peer claims. */
21
+ const ROLES_CLAIMED = Object.freeze(['debug', 'plan']);
22
+
23
+ /**
24
+ * Per-role slash-command prefix. Cursor parses `/debug` / `/plan` as
25
+ * slash commands at the start of a prompt and routes accordingly.
26
+ */
27
+ const ROLE_PREFIX = Object.freeze({
28
+ debug: '/debug ',
29
+ plan: '/plan ',
30
+ });
31
+
32
+ function claims(role) {
33
+ return ROLES_CLAIMED.includes(role);
34
+ }
35
+
36
+ /**
37
+ * Drive one Cursor ACP `prompt` for the supplied role + text.
38
+ *
39
+ * @param {{command: string, args?: string[], cwd?: string, env?: object}} peer
40
+ * @param {string} role
41
+ * @param {string} text
42
+ * @param {{onNotification?: (n: object) => void}} [opts]
43
+ * @returns {Promise<object>}
44
+ */
45
+ async function dispatch(peer, role, text, opts) {
46
+ if (!claims(role)) {
47
+ throw new Error(`cursor adapter does not claim role: ${role}`);
48
+ }
49
+ if (typeof text !== 'string') {
50
+ throw new TypeError('cursor adapter: text must be a string');
51
+ }
52
+ const onNotification = opts && typeof opts.onNotification === 'function'
53
+ ? opts.onNotification
54
+ : undefined;
55
+
56
+ const client = createAcpClient({
57
+ command: peer.command,
58
+ args: peer.args,
59
+ cwd: peer.cwd,
60
+ env: peer.env,
61
+ });
62
+ try {
63
+ await client.initialize({ protocolVersion: '2025-06-18' });
64
+ const prompt = ROLE_PREFIX[role] + text;
65
+ return await client.prompt(prompt, { onNotification });
66
+ } finally {
67
+ await client.close();
68
+ }
69
+ }
70
+
71
+ module.exports = {
72
+ name: 'cursor',
73
+ protocol: 'acp',
74
+ ROLES_CLAIMED,
75
+ ROLE_PREFIX,
76
+ claims,
77
+ dispatch,
78
+ };