overlord-cli 3.5.2 → 3.5.4
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.
- package/README.md +5 -2
- package/bin/_cli/auth.mjs +6 -2
- package/bin/_cli/index.mjs +1 -1
- package/bin/_cli/setup.mjs +547 -52
- package/package.json +3 -2
- package/plugins/overlord/.codex-plugin/plugin.json +35 -0
- package/plugins/overlord/.mcp.json +11 -0
- package/plugins/overlord/README.md +41 -0
- package/plugins/overlord/assets/icon.png +0 -0
- package/plugins/overlord/assets/logo.png +0 -0
- package/plugins/overlord/assets/screenshot.svg +28 -0
- package/plugins/overlord/scripts/overlord-mcp.mjs +626 -0
- package/plugins/overlord/skills/overlord-ticket-workflow/SKILL.md +26 -0
|
@@ -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,26 @@
|
|
|
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
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
- The authoritative lifecycle is the `ovld protocol` CLI.
|
|
22
|
+
- Always attach first and always deliver last.
|
|
23
|
+
- Do not create or rely on a local Codex `AGENTS.md` bundle for Overlord.
|
|
24
|
+
- Prefer the installed `ovld` CLI and the plugin's MCP tools instead of ad hoc repo scripts.
|
|
25
|
+
- When the ticket was launched by Overlord, the ticket prompt remains authoritative for the
|
|
26
|
+
specific task objective and any ticket-level constraints.
|