overlord-cli 3.5.3 → 3.8.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.
@@ -0,0 +1,626 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+
6
+ const execFileAsync = promisify(execFile);
7
+ const OVLD_BIN = process.env.OVLD_BIN?.trim() || 'ovld';
8
+ const PROTOCOL_VERSION = '2025-06-18';
9
+
10
+ const tools = [
11
+ {
12
+ name: 'discover_project',
13
+ description: 'Resolve the Overlord project that matches a working directory.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ working_directory: { type: 'string', description: 'Directory to match. Defaults to the current workspace.' }
18
+ }
19
+ },
20
+ toCliFlags: args => ({
21
+ 'working-directory': args.working_directory
22
+ }),
23
+ subcommand: 'discover-project'
24
+ },
25
+ {
26
+ name: 'attach_ticket',
27
+ description: 'Attach an agent session to an existing Overlord ticket and return the working context.',
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: {
31
+ ticket_id: { type: 'string', description: 'Target ticket ID' },
32
+ agent: { type: 'string' },
33
+ method: { type: 'string' },
34
+ external_session_id: { type: ['string', 'null'] }
35
+ },
36
+ required: ['ticket_id']
37
+ },
38
+ toCliFlags: args => ({
39
+ 'ticket-id': args.ticket_id,
40
+ agent: args.agent,
41
+ method: args.method,
42
+ 'external-session-id': args.external_session_id
43
+ }),
44
+ subcommand: 'attach'
45
+ },
46
+ {
47
+ name: 'connect_ticket',
48
+ description: 'Create a lightweight Overlord session without loading the full ticket context.',
49
+ inputSchema: {
50
+ type: 'object',
51
+ properties: {
52
+ ticket_id: { type: 'string' },
53
+ agent: { type: 'string' },
54
+ method: { type: 'string' }
55
+ },
56
+ required: ['ticket_id']
57
+ },
58
+ toCliFlags: args => ({
59
+ 'ticket-id': args.ticket_id,
60
+ agent: args.agent,
61
+ method: args.method
62
+ }),
63
+ subcommand: 'connect'
64
+ },
65
+ {
66
+ name: 'load_ticket_context',
67
+ description: 'Fetch Overlord ticket context without creating a session.',
68
+ inputSchema: {
69
+ type: 'object',
70
+ properties: {
71
+ ticket_id: { type: 'string' }
72
+ },
73
+ required: ['ticket_id']
74
+ },
75
+ toCliFlags: args => ({
76
+ 'ticket-id': args.ticket_id
77
+ }),
78
+ subcommand: 'load-context'
79
+ },
80
+ {
81
+ name: 'spawn_ticket',
82
+ description: 'Create a follow-up ticket and attach to it immediately.',
83
+ inputSchema: {
84
+ type: 'object',
85
+ properties: {
86
+ objective: { type: 'string' },
87
+ title: { type: 'string' },
88
+ priority: { type: 'string', enum: ['low', 'medium', 'high', 'urgent'] },
89
+ project_id: { type: 'string' },
90
+ working_directory: { type: 'string' },
91
+ acceptance_criteria: { type: 'string' },
92
+ available_tools: { type: 'string' },
93
+ execution_target: { type: 'string', enum: ['agent', 'human'] },
94
+ delegate: { type: 'string' },
95
+ parent_session_key: { type: 'string' },
96
+ parent_ticket_id: { type: 'string' },
97
+ agent: { type: 'string' },
98
+ method: { type: 'string' }
99
+ },
100
+ required: ['objective']
101
+ },
102
+ toCliFlags: args => ({
103
+ objective: args.objective,
104
+ title: args.title,
105
+ priority: args.priority,
106
+ 'project-id': args.project_id,
107
+ 'working-directory': args.working_directory,
108
+ 'acceptance-criteria': args.acceptance_criteria,
109
+ 'available-tools': args.available_tools,
110
+ 'execution-target': args.execution_target,
111
+ delegate: args.delegate,
112
+ 'parent-session-key': args.parent_session_key,
113
+ 'parent-ticket-id': args.parent_ticket_id,
114
+ agent: args.agent,
115
+ method: args.method
116
+ }),
117
+ subcommand: 'spawn'
118
+ },
119
+ {
120
+ name: 'post_update',
121
+ description: 'Post an Overlord progress update or activity event.',
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ session_key: { type: 'string' },
126
+ ticket_id: { type: 'string' },
127
+ summary: { type: 'string' },
128
+ phase: { type: 'string', enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled'] },
129
+ event_type: { type: 'string', enum: ['update', 'user_follow_up', 'alert'] },
130
+ external_url: { type: ['string', 'null'] },
131
+ external_session_id: { type: ['string', 'null'] },
132
+ payload: { type: 'object' },
133
+ change_rationales: { type: 'array' }
134
+ },
135
+ required: ['session_key', 'ticket_id', 'summary']
136
+ },
137
+ toCliFlags: args => ({
138
+ 'session-key': args.session_key,
139
+ 'ticket-id': args.ticket_id,
140
+ summary: args.summary,
141
+ phase: args.phase,
142
+ 'event-type': args.event_type,
143
+ 'external-url': args.external_url,
144
+ 'external-session-id': args.external_session_id,
145
+ 'payload-json': args.payload,
146
+ 'change-rationales-json': args.change_rationales
147
+ }),
148
+ subcommand: 'update'
149
+ },
150
+ {
151
+ name: 'record_change_rationales',
152
+ description: 'Persist structured change rationale rows without posting a separate update.',
153
+ inputSchema: {
154
+ type: 'object',
155
+ properties: {
156
+ session_key: { type: 'string' },
157
+ ticket_id: { type: 'string' },
158
+ summary: { type: 'string' },
159
+ phase: { type: 'string', enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled'] },
160
+ change_rationales: { type: 'array' }
161
+ },
162
+ required: ['session_key', 'ticket_id', 'change_rationales']
163
+ },
164
+ toCliFlags: args => ({
165
+ 'session-key': args.session_key,
166
+ 'ticket-id': args.ticket_id,
167
+ summary: args.summary,
168
+ phase: args.phase,
169
+ 'change-rationales-json': args.change_rationales
170
+ }),
171
+ subcommand: 'record-change-rationales'
172
+ },
173
+ {
174
+ name: 'ask_blocking_question',
175
+ description: 'Send a blocking question to the human reviewer or PM.',
176
+ inputSchema: {
177
+ type: 'object',
178
+ properties: {
179
+ session_key: { type: 'string' },
180
+ ticket_id: { type: 'string' },
181
+ question: { type: 'string' },
182
+ phase: { type: 'string', enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled'] },
183
+ payload: { type: 'object' }
184
+ },
185
+ required: ['session_key', 'ticket_id', 'question']
186
+ },
187
+ toCliFlags: args => ({
188
+ 'session-key': args.session_key,
189
+ 'ticket-id': args.ticket_id,
190
+ question: args.question,
191
+ phase: args.phase,
192
+ 'payload-json': args.payload
193
+ }),
194
+ subcommand: 'ask'
195
+ },
196
+ {
197
+ name: 'read_shared_context',
198
+ description: 'Read persistent shared context entries for a ticket.',
199
+ inputSchema: {
200
+ type: 'object',
201
+ properties: {
202
+ session_key: { type: 'string' },
203
+ ticket_id: { type: 'string' },
204
+ query: { type: 'string' },
205
+ limit: { type: 'number' }
206
+ },
207
+ required: ['session_key', 'ticket_id']
208
+ },
209
+ toCliFlags: args => ({
210
+ 'session-key': args.session_key,
211
+ 'ticket-id': args.ticket_id,
212
+ query: args.query,
213
+ limit: args.limit
214
+ }),
215
+ subcommand: 'read-context'
216
+ },
217
+ {
218
+ name: 'write_shared_context',
219
+ description: 'Write a persistent shared context entry for future Overlord sessions.',
220
+ inputSchema: {
221
+ type: 'object',
222
+ properties: {
223
+ session_key: { type: 'string' },
224
+ ticket_id: { type: 'string' },
225
+ key: { type: 'string' },
226
+ value: {},
227
+ tags: { type: 'array', items: { type: 'string' } }
228
+ },
229
+ required: ['session_key', 'ticket_id', 'key', 'value']
230
+ },
231
+ toCliFlags: args => ({
232
+ 'session-key': args.session_key,
233
+ 'ticket-id': args.ticket_id,
234
+ key: args.key,
235
+ value: typeof args.value === 'string' ? args.value : JSON.stringify(args.value),
236
+ tags: Array.isArray(args.tags) ? args.tags.join(',') : args.tags
237
+ }),
238
+ subcommand: 'write-context'
239
+ },
240
+ {
241
+ name: 'deliver_ticket',
242
+ description: 'Deliver final work back into Overlord with summary, artifacts, and change rationales.',
243
+ inputSchema: {
244
+ type: 'object',
245
+ properties: {
246
+ session_key: { type: 'string' },
247
+ ticket_id: { type: 'string' },
248
+ summary: { type: 'string' },
249
+ artifacts: { type: 'array' },
250
+ change_rationales: { type: 'array' },
251
+ skip_file_change_check: { type: 'boolean' }
252
+ },
253
+ required: ['session_key', 'ticket_id', 'summary']
254
+ },
255
+ toCliFlags: args => ({
256
+ 'session-key': args.session_key,
257
+ 'ticket-id': args.ticket_id,
258
+ summary: args.summary,
259
+ 'artifacts-json': args.artifacts,
260
+ 'change-rationales-json': args.change_rationales,
261
+ 'skip-file-change-check': args.skip_file_change_check
262
+ }),
263
+ subcommand: 'deliver'
264
+ },
265
+ {
266
+ name: 'artifact_prepare_upload',
267
+ description: 'Prepare an Overlord artifact upload and return a signed upload URL.',
268
+ inputSchema: {
269
+ type: 'object',
270
+ properties: {
271
+ session_key: { type: 'string' },
272
+ ticket_id: { type: 'string' },
273
+ file_name: { type: 'string' },
274
+ label: { type: 'string' },
275
+ artifact_type: { type: 'string' },
276
+ content_type: { type: 'string' },
277
+ file_size: { type: 'number' },
278
+ metadata: { type: 'object' }
279
+ },
280
+ required: ['session_key', 'ticket_id', 'file_name']
281
+ },
282
+ toCliFlags: args => ({
283
+ 'session-key': args.session_key,
284
+ 'ticket-id': args.ticket_id,
285
+ 'file-name': args.file_name,
286
+ label: args.label,
287
+ 'artifact-type': args.artifact_type,
288
+ 'content-type': args.content_type,
289
+ 'file-size': args.file_size,
290
+ 'metadata-json': args.metadata
291
+ }),
292
+ subcommand: 'artifact-prepare-upload'
293
+ },
294
+ {
295
+ name: 'artifact_finalize_upload',
296
+ description: 'Finalize an artifact after uploading bytes to the signed storage URL.',
297
+ inputSchema: {
298
+ type: 'object',
299
+ properties: {
300
+ session_key: { type: 'string' },
301
+ ticket_id: { type: 'string' },
302
+ storage_path: { type: 'string' },
303
+ label: { type: 'string' },
304
+ artifact_type: { type: 'string' },
305
+ content_type: { type: 'string' },
306
+ file_size: { type: 'number' },
307
+ metadata: { type: 'object' }
308
+ },
309
+ required: ['session_key', 'ticket_id', 'storage_path', 'label']
310
+ },
311
+ toCliFlags: args => ({
312
+ 'session-key': args.session_key,
313
+ 'ticket-id': args.ticket_id,
314
+ 'storage-path': args.storage_path,
315
+ label: args.label,
316
+ 'artifact-type': args.artifact_type,
317
+ 'content-type': args.content_type,
318
+ 'file-size': args.file_size,
319
+ 'metadata-json': args.metadata
320
+ }),
321
+ subcommand: 'artifact-finalize-upload'
322
+ },
323
+ {
324
+ name: 'artifact_download_url',
325
+ description: 'Create a signed download URL for an uploaded Overlord artifact.',
326
+ inputSchema: {
327
+ type: 'object',
328
+ properties: {
329
+ session_key: { type: 'string' },
330
+ ticket_id: { type: 'string' },
331
+ artifact_id: { type: 'string' },
332
+ storage_path: { type: 'string' },
333
+ expires_in: { type: 'number' }
334
+ },
335
+ required: ['session_key', 'ticket_id']
336
+ },
337
+ toCliFlags: args => ({
338
+ 'session-key': args.session_key,
339
+ 'ticket-id': args.ticket_id,
340
+ 'artifact-id': args.artifact_id,
341
+ 'storage-path': args.storage_path,
342
+ 'expires-in': args.expires_in
343
+ }),
344
+ subcommand: 'artifact-download-url'
345
+ },
346
+ {
347
+ name: 'artifact_upload_file',
348
+ description: 'Prepare, upload, and finalize a local file as an Overlord artifact in one step.',
349
+ inputSchema: {
350
+ type: 'object',
351
+ properties: {
352
+ session_key: { type: 'string' },
353
+ ticket_id: { type: 'string' },
354
+ file: { type: 'string' },
355
+ file_name: { type: 'string' },
356
+ label: { type: 'string' },
357
+ artifact_type: { type: 'string' },
358
+ content_type: { type: 'string' },
359
+ metadata: { type: 'object' }
360
+ },
361
+ required: ['session_key', 'ticket_id', 'file']
362
+ },
363
+ toCliFlags: args => ({
364
+ 'session-key': args.session_key,
365
+ 'ticket-id': args.ticket_id,
366
+ file: args.file,
367
+ 'file-name': args.file_name,
368
+ label: args.label,
369
+ 'artifact-type': args.artifact_type,
370
+ 'content-type': args.content_type,
371
+ 'metadata-json': args.metadata
372
+ }),
373
+ subcommand: 'artifact-upload-file'
374
+ }
375
+ ];
376
+
377
+ const searchTicketsTool = {
378
+ name: 'search_tickets',
379
+ description:
380
+ 'Search Overlord tickets by keyword and/or filter by status. Leave query empty to list all tickets matching the status filter. Useful when the user asks to find tickets related to a subject or in a specific workflow state.',
381
+ inputSchema: {
382
+ type: 'object',
383
+ properties: {
384
+ query: {
385
+ type: 'string',
386
+ description:
387
+ 'Keyword or phrase to search for in ticket titles and objectives. Leave empty to list without text filtering.'
388
+ },
389
+ statuses: {
390
+ type: 'array',
391
+ items: { type: 'string' },
392
+ description:
393
+ 'Filter by one or more ticket statuses (e.g. ["next-up", "execute", "review"]). Omit to include all non-completed statuses.'
394
+ },
395
+ include_completed: {
396
+ type: 'boolean',
397
+ description: 'Whether to include completed tickets in results. Defaults to false.'
398
+ },
399
+ limit: {
400
+ type: 'number',
401
+ description: 'Maximum number of results to return (1–20, default 8).'
402
+ }
403
+ }
404
+ },
405
+ toCliFlags: args => ({
406
+ query: args.query,
407
+ statuses: Array.isArray(args.statuses) ? args.statuses.join(',') : args.statuses,
408
+ 'include-completed': args.include_completed,
409
+ limit: args.limit
410
+ }),
411
+ subcommand: 'search-tickets'
412
+ };
413
+
414
+ const toolMap = new Map([
415
+ ...tools.map(tool => [tool.name, tool]),
416
+ [searchTicketsTool.name, searchTicketsTool]
417
+ ]);
418
+ let buffer = Buffer.alloc(0);
419
+
420
+ function serializeMessage(message) {
421
+ const json = JSON.stringify(message);
422
+ const body = Buffer.from(json, 'utf8');
423
+ const header = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'utf8');
424
+ return Buffer.concat([header, body]);
425
+ }
426
+
427
+ function send(message) {
428
+ process.stdout.write(serializeMessage(message));
429
+ }
430
+
431
+ function parseMessages(chunk) {
432
+ buffer = Buffer.concat([buffer, chunk]);
433
+ const messages = [];
434
+
435
+ while (true) {
436
+ const headerEnd = buffer.indexOf('\r\n\r\n');
437
+ if (headerEnd === -1) break;
438
+
439
+ const headerText = buffer.subarray(0, headerEnd).toString('utf8');
440
+ const headers = Object.fromEntries(
441
+ headerText
442
+ .split('\r\n')
443
+ .map(line => {
444
+ const separatorIndex = line.indexOf(':');
445
+ return [line.slice(0, separatorIndex).trim().toLowerCase(), line.slice(separatorIndex + 1).trim()];
446
+ })
447
+ );
448
+
449
+ const contentLength = Number(headers['content-length']);
450
+ if (!Number.isFinite(contentLength)) {
451
+ throw new Error('Missing Content-Length header');
452
+ }
453
+
454
+ const totalLength = headerEnd + 4 + contentLength;
455
+ if (buffer.length < totalLength) break;
456
+
457
+ const body = buffer.subarray(headerEnd + 4, totalLength).toString('utf8');
458
+ buffer = buffer.subarray(totalLength);
459
+ messages.push(JSON.parse(body));
460
+ }
461
+
462
+ return messages;
463
+ }
464
+
465
+ function success(id, result) {
466
+ send({ jsonrpc: '2.0', id, result });
467
+ }
468
+
469
+ function failure(id, code, message) {
470
+ send({ jsonrpc: '2.0', id, error: { code, message } });
471
+ }
472
+
473
+ function cliArgsFromFlags(flags) {
474
+ const args = [];
475
+
476
+ for (const [key, value] of Object.entries(flags)) {
477
+ if (value === undefined || value === null) continue;
478
+
479
+ if (typeof value === 'boolean') {
480
+ if (value) args.push(`--${key}`);
481
+ continue;
482
+ }
483
+
484
+ const serialized =
485
+ typeof value === 'string' || typeof value === 'number' ? String(value) : JSON.stringify(value);
486
+ args.push(`--${key}`, serialized);
487
+ }
488
+
489
+ return args;
490
+ }
491
+
492
+ async function runProtocol(tool, args) {
493
+ const cliArgs = ['protocol', tool.subcommand, ...cliArgsFromFlags(tool.toCliFlags(args ?? {}))];
494
+
495
+ try {
496
+ const { stdout, stderr } = await execFileAsync(OVLD_BIN, cliArgs, {
497
+ cwd: process.cwd(),
498
+ env: {
499
+ ...process.env,
500
+ AGENT_IDENTIFIER: process.env.AGENT_IDENTIFIER ?? 'codex-overlord-plugin'
501
+ },
502
+ maxBuffer: 20 * 1024 * 1024
503
+ });
504
+
505
+ const trimmed = stdout.trim();
506
+ const data = trimmed ? JSON.parse(trimmed) : {};
507
+
508
+ return {
509
+ content: [
510
+ {
511
+ type: 'text',
512
+ text: JSON.stringify(
513
+ {
514
+ subcommand: tool.subcommand,
515
+ data,
516
+ stderr: stderr.trim() || undefined
517
+ },
518
+ null,
519
+ 2
520
+ )
521
+ }
522
+ ],
523
+ structuredContent: data
524
+ };
525
+ } catch (error) {
526
+ const stdout = typeof error?.stdout === 'string' ? error.stdout.trim() : '';
527
+ const stderr = typeof error?.stderr === 'string' ? error.stderr.trim() : '';
528
+ const message = error instanceof Error ? error.message : String(error);
529
+
530
+ let parsedStdout = stdout;
531
+ if (stdout) {
532
+ try {
533
+ parsedStdout = JSON.parse(stdout);
534
+ } catch {
535
+ // keep raw stdout
536
+ }
537
+ }
538
+
539
+ return {
540
+ content: [
541
+ {
542
+ type: 'text',
543
+ text: JSON.stringify(
544
+ {
545
+ subcommand: tool.subcommand,
546
+ error: message,
547
+ stdout: parsedStdout || undefined,
548
+ stderr: stderr || undefined
549
+ },
550
+ null,
551
+ 2
552
+ )
553
+ }
554
+ ],
555
+ isError: true
556
+ };
557
+ }
558
+ }
559
+
560
+ async function handleRequest(message) {
561
+ const { id, method, params } = message;
562
+
563
+ if (method === 'initialize') {
564
+ success(id, {
565
+ protocolVersion: PROTOCOL_VERSION,
566
+ capabilities: {
567
+ tools: {
568
+ listChanged: false
569
+ }
570
+ },
571
+ serverInfo: {
572
+ name: 'overlord',
573
+ version: '0.1.0'
574
+ },
575
+ instructions:
576
+ 'Use these tools to drive Overlord ticket workflows through the installed ovld CLI. Most operations expect a session key from attach or connect.'
577
+ });
578
+ return;
579
+ }
580
+
581
+ if (method === 'ping') {
582
+ success(id, {});
583
+ return;
584
+ }
585
+
586
+ if (method === 'tools/list') {
587
+ success(id, {
588
+ tools: [...tools, searchTicketsTool].map(tool => ({
589
+ name: tool.name,
590
+ description: tool.description,
591
+ inputSchema: tool.inputSchema
592
+ }))
593
+ });
594
+ return;
595
+ }
596
+
597
+ if (method === 'tools/call') {
598
+ const tool = toolMap.get(params?.name);
599
+ if (!tool) {
600
+ failure(id, -32602, `Unknown tool: ${params?.name ?? 'undefined'}`);
601
+ return;
602
+ }
603
+
604
+ success(id, await runProtocol(tool, params?.arguments ?? {}));
605
+ return;
606
+ }
607
+
608
+ failure(id, -32601, `Method not found: ${method}`);
609
+ }
610
+
611
+ process.stdin.on('data', async chunk => {
612
+ try {
613
+ for (const message of parseMessages(chunk)) {
614
+ if (!message || typeof message !== 'object') continue;
615
+ if ('method' in message && 'id' in message) {
616
+ await handleRequest(message);
617
+ }
618
+ }
619
+ } catch (error) {
620
+ const message = error instanceof Error ? error.message : String(error);
621
+ process.stderr.write(`Overlord MCP server failed: ${message}\n`);
622
+ process.exit(1);
623
+ }
624
+ });
625
+
626
+ process.stdin.resume();
@@ -0,0 +1,27 @@
1
+ # Overlord Ticket Workflow
2
+
3
+ Use this skill when the user wants to work on an Overlord ticket from Codex through the local
4
+ Overlord plugin.
5
+
6
+ ## Workflow
7
+
8
+ 1. Attach first with `ovld protocol attach --ticket-id <ticket-id>`.
9
+ 2. Store the returned `SESSION_KEY` or `session.sessionKey`.
10
+ 3. While working, publish meaningful progress with:
11
+ `ovld protocol update --session-key <sessionKey> --ticket-id <ticket-id> --phase execute --summary "..."`
12
+ 4. If a later user message arrives during the ticket session, publish it immediately with
13
+ `--event-type user_follow_up` before doing anything else.
14
+ 5. If blocked on a human-only action, ask a precise blocking question with `ovld protocol ask`
15
+ and stop.
16
+ 6. Deliver last with `ovld protocol deliver`, including meaningful `changeRationales` for every
17
+ behavioral git-tracked change.
18
+ If you need `--payload-file`, `--artifacts-file`, or `--change-rationales-file`, treat that JSON as ephemeral scratch data, not as a repository file. Remove it after delivery and never commit it.
19
+
20
+ ## Rules
21
+
22
+ - The authoritative lifecycle is the `ovld protocol` CLI.
23
+ - Always attach first and always deliver last.
24
+ - Do not create or rely on a local Codex `AGENTS.md` bundle for Overlord.
25
+ - Prefer the installed `ovld` CLI and the plugin's MCP tools instead of ad hoc repo scripts.
26
+ - When the ticket was launched by Overlord, the ticket prompt remains authoritative for the
27
+ specific task objective and any ticket-level constraints.