overlord-cli 0.2606060856.0 → 0.2606081020.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.
- package/bin/_cli/protocol.mjs +433 -51
- package/package.json +1 -1
- package/plugins/claude/commands/create.md +1 -1
- package/plugins/claude/commands/prompt.md +1 -1
- package/plugins/claude/commands/spawn.md +1 -1
- package/plugins/claude/skills/overlord-ticket/reference/cli.md +12 -0
- package/plugins/cursor/skills/overlord-ticket/reference/cli.md +12 -0
- package/plugins/overlord/.codex-plugin/plugin.json +1 -1
- package/plugins/overlord/scripts/overlord-mcp.mjs +18 -0
- package/plugins/overlord/scripts/user-prompt-submit-hook.sh +62 -1
- package/plugins/overlord/skills/overlord-ticket/reference/cli.md +12 -0
package/bin/_cli/protocol.mjs
CHANGED
|
@@ -234,7 +234,10 @@ async function uploadToSignedUrl(uploadUrl, bytes, contentType, timeoutMs = DEFA
|
|
|
234
234
|
// ---------------------------------------------------------------------------
|
|
235
235
|
|
|
236
236
|
function getSessionFilePath() {
|
|
237
|
-
return path.join(
|
|
237
|
+
return path.join(
|
|
238
|
+
os.tmpdir(),
|
|
239
|
+
`.overlord-session-${Buffer.from(process.cwd()).toString('base64url')}`
|
|
240
|
+
);
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
function persistSession(sessionKey, ticketId) {
|
|
@@ -244,7 +247,9 @@ function persistSession(sessionKey, ticketId) {
|
|
|
244
247
|
JSON.stringify({ sessionKey, ticketId, ts: Date.now() }),
|
|
245
248
|
'utf8'
|
|
246
249
|
);
|
|
247
|
-
} catch {
|
|
250
|
+
} catch {
|
|
251
|
+
/* best-effort */
|
|
252
|
+
}
|
|
248
253
|
}
|
|
249
254
|
|
|
250
255
|
function readPersistedSession() {
|
|
@@ -252,7 +257,9 @@ function readPersistedSession() {
|
|
|
252
257
|
const raw = fs.readFileSync(getSessionFilePath(), 'utf8');
|
|
253
258
|
const data = JSON.parse(raw);
|
|
254
259
|
if (data.sessionKey && data.ticketId) return data;
|
|
255
|
-
} catch {
|
|
260
|
+
} catch {
|
|
261
|
+
/* missing or corrupt — ignore */
|
|
262
|
+
}
|
|
256
263
|
return null;
|
|
257
264
|
}
|
|
258
265
|
|
|
@@ -274,6 +281,161 @@ function resolveSessionFlags(flags) {
|
|
|
274
281
|
return { sessionKey, ticketId };
|
|
275
282
|
}
|
|
276
283
|
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Pending ticket drafts — local durability before DB confirmation
|
|
286
|
+
//
|
|
287
|
+
// New tickets/objectives are written to the server over the network. On a
|
|
288
|
+
// flaky connection a `create`/`prompt`/`add-objectives`/`record-work` call can
|
|
289
|
+
// fail after the user has already composed real text, and that text is lost.
|
|
290
|
+
// To prevent that, we persist the request body to a durable local file BEFORE
|
|
291
|
+
// the network call and only delete it once the server confirms the write
|
|
292
|
+
// succeeded. Anything left behind can be inspected, retried, or cleared with
|
|
293
|
+
// `ovld protocol pending-tickets`.
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
function getPendingTicketsDir() {
|
|
297
|
+
const override = process.env.OVERLORD_PENDING_TICKETS_DIR?.trim();
|
|
298
|
+
if (override) return override;
|
|
299
|
+
return path.join(os.homedir(), '.overlord', 'pending-tickets');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getPendingTicketFilePath(id) {
|
|
303
|
+
return path.join(getPendingTicketsDir(), `${id}.json`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function generatePendingTicketId() {
|
|
307
|
+
// Timestamp prefix keeps drafts naturally sortable; random suffix avoids
|
|
308
|
+
// collisions when several drafts are created within the same millisecond.
|
|
309
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Pull human-readable objective text out of a request body for display. */
|
|
313
|
+
function describePendingObjectives(body) {
|
|
314
|
+
const objectives = Array.isArray(body?.objectives)
|
|
315
|
+
? body.objectives
|
|
316
|
+
: typeof body?.objective === 'string'
|
|
317
|
+
? [body.objective]
|
|
318
|
+
: [];
|
|
319
|
+
return objectives
|
|
320
|
+
.map(objective => (typeof objective === 'string' ? objective : objective?.objective))
|
|
321
|
+
.filter(value => typeof value === 'string' && value.trim().length > 0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function truncateForDisplay(value, max = 200) {
|
|
325
|
+
const text = String(value ?? '');
|
|
326
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Persist the request body for a new ticket/objective before sending it.
|
|
331
|
+
* Best-effort: if the local filesystem is not writable we return null and let
|
|
332
|
+
* the network call proceed unguarded rather than blocking the user's write.
|
|
333
|
+
* @returns {{ id: string, filePath: string } | null}
|
|
334
|
+
*/
|
|
335
|
+
function savePendingTicket({ kind, endpoint, body }) {
|
|
336
|
+
let dir;
|
|
337
|
+
try {
|
|
338
|
+
dir = getPendingTicketsDir();
|
|
339
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const id = generatePendingTicketId();
|
|
345
|
+
const filePath = getPendingTicketFilePath(id);
|
|
346
|
+
const record = {
|
|
347
|
+
id,
|
|
348
|
+
kind,
|
|
349
|
+
endpoint,
|
|
350
|
+
createdAt: new Date().toISOString(),
|
|
351
|
+
cwd: process.cwd(),
|
|
352
|
+
title: typeof body?.title === 'string' ? body.title : null,
|
|
353
|
+
ticketId: body?.ticketId ?? null,
|
|
354
|
+
projectId: body?.projectId ?? null,
|
|
355
|
+
objectives: describePendingObjectives(body),
|
|
356
|
+
body
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
fs.writeFileSync(filePath, JSON.stringify(record, null, 2), 'utf8');
|
|
361
|
+
return { id, filePath };
|
|
362
|
+
} catch {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Remove a pending draft once the server has confirmed the write. */
|
|
368
|
+
function resolvePendingTicket(id) {
|
|
369
|
+
if (!id) return;
|
|
370
|
+
try {
|
|
371
|
+
fs.unlinkSync(getPendingTicketFilePath(id));
|
|
372
|
+
} catch {
|
|
373
|
+
/* already gone — best effort */
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Read every saved draft, skipping any that are missing or corrupt. */
|
|
378
|
+
function listPendingTickets() {
|
|
379
|
+
let entries;
|
|
380
|
+
try {
|
|
381
|
+
entries = fs.readdirSync(getPendingTicketsDir());
|
|
382
|
+
} catch {
|
|
383
|
+
return [];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const records = [];
|
|
387
|
+
for (const name of entries) {
|
|
388
|
+
if (!name.endsWith('.json')) continue;
|
|
389
|
+
try {
|
|
390
|
+
const raw = fs.readFileSync(path.join(getPendingTicketsDir(), name), 'utf8');
|
|
391
|
+
records.push(JSON.parse(raw));
|
|
392
|
+
} catch {
|
|
393
|
+
/* skip corrupt draft */
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
records.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
|
|
397
|
+
return records;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Post a new-ticket/objective request with local durability: save the body
|
|
402
|
+
* first, delete it only on a confirmed (2xx) response, and on any failure keep
|
|
403
|
+
* it and point the user at the recovery command. Mirrors apiPost's contract.
|
|
404
|
+
*/
|
|
405
|
+
async function apiPostCreatingTicket(auth, endpoint, body, kind) {
|
|
406
|
+
const { platformUrl, bearerToken, localSecret, organizationId, timeoutMs } = auth;
|
|
407
|
+
const pending = savePendingTicket({ kind, endpoint, body });
|
|
408
|
+
if (pending) {
|
|
409
|
+
process.stderr.write(
|
|
410
|
+
`[protocol] saved local draft ${pending.id} before ${endpoint} (kept until the server confirms)\n`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let data;
|
|
415
|
+
try {
|
|
416
|
+
data = await apiPost(
|
|
417
|
+
platformUrl,
|
|
418
|
+
bearerToken,
|
|
419
|
+
localSecret,
|
|
420
|
+
organizationId,
|
|
421
|
+
endpoint,
|
|
422
|
+
body,
|
|
423
|
+
timeoutMs
|
|
424
|
+
);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (pending && error instanceof Error) {
|
|
427
|
+
error.message +=
|
|
428
|
+
`\n\nYour ticket text was NOT lost — it was saved locally at:\n ${pending.filePath}\n` +
|
|
429
|
+
`List it with: ovld protocol pending-tickets\n` +
|
|
430
|
+
`Retry it with: ovld protocol pending-tickets --retry ${pending.id}`;
|
|
431
|
+
}
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (pending) resolvePendingTicket(pending.id);
|
|
436
|
+
return data;
|
|
437
|
+
}
|
|
438
|
+
|
|
277
439
|
/** Resolve request timeout from --timeout flag or OVERLORD_TIMEOUT env var. */
|
|
278
440
|
function resolveTimeout(flags) {
|
|
279
441
|
const raw = flags['timeout'] ?? process.env.OVERLORD_TIMEOUT;
|
|
@@ -420,7 +582,10 @@ async function readJsonFileOrStdin(filePath, label) {
|
|
|
420
582
|
}
|
|
421
583
|
}
|
|
422
584
|
|
|
423
|
-
async function resolveObjectivesInput(
|
|
585
|
+
async function resolveObjectivesInput(
|
|
586
|
+
flags,
|
|
587
|
+
{ payload = null, requireSingleObjective = true } = {}
|
|
588
|
+
) {
|
|
424
589
|
if (flags['objectives-json'] && flags['objectives-file']) {
|
|
425
590
|
throw new Error('Use either --objectives-json or --objectives-file, not both');
|
|
426
591
|
}
|
|
@@ -436,7 +601,10 @@ async function resolveObjectivesInput(flags, { payload = null, requireSingleObje
|
|
|
436
601
|
}
|
|
437
602
|
|
|
438
603
|
if (flags['objectives-file']) {
|
|
439
|
-
const objectives = await readJsonFileOrStdin(
|
|
604
|
+
const objectives = await readJsonFileOrStdin(
|
|
605
|
+
String(flags['objectives-file']),
|
|
606
|
+
'--objectives-file'
|
|
607
|
+
);
|
|
440
608
|
if (!Array.isArray(objectives) || objectives.length === 0) {
|
|
441
609
|
throw new Error('--objectives-file must contain a non-empty JSON array');
|
|
442
610
|
}
|
|
@@ -934,13 +1102,122 @@ function detectClaudeSessionId() {
|
|
|
934
1102
|
}
|
|
935
1103
|
}
|
|
936
1104
|
|
|
1105
|
+
const CODEX_SESSION_UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Read the first line of a (potentially large) rollout file without slurping
|
|
1109
|
+
* the whole thing into memory. Codex writes the session meta as the first
|
|
1110
|
+
* JSONL record.
|
|
1111
|
+
*/
|
|
1112
|
+
function readFirstLine(filePath, maxBytes = 16384) {
|
|
1113
|
+
let fd;
|
|
1114
|
+
try {
|
|
1115
|
+
fd = fs.openSync(filePath, 'r');
|
|
1116
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
1117
|
+
const bytesRead = fs.readSync(fd, buffer, 0, maxBytes, 0);
|
|
1118
|
+
const text = buffer.toString('utf8', 0, bytesRead);
|
|
1119
|
+
const newlineIdx = text.indexOf('\n');
|
|
1120
|
+
return newlineIdx === -1 ? text : text.slice(0, newlineIdx);
|
|
1121
|
+
} catch {
|
|
1122
|
+
return null;
|
|
1123
|
+
} finally {
|
|
1124
|
+
if (fd !== undefined) {
|
|
1125
|
+
try {
|
|
1126
|
+
fs.closeSync(fd);
|
|
1127
|
+
} catch {
|
|
1128
|
+
/* ignore */
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Extract the session id and recorded cwd from a Codex rollout meta line.
|
|
1136
|
+
* Newer Codex builds wrap the meta in a `{ type, payload }` envelope; older
|
|
1137
|
+
* builds emit the meta object directly. Returns null when no usable id exists.
|
|
1138
|
+
*/
|
|
1139
|
+
export function extractCodexRolloutMeta(line) {
|
|
1140
|
+
if (!line) return null;
|
|
1141
|
+
let parsed;
|
|
1142
|
+
try {
|
|
1143
|
+
parsed = JSON.parse(line);
|
|
1144
|
+
} catch {
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
1148
|
+
const meta = parsed.payload && typeof parsed.payload === 'object' ? parsed.payload : parsed;
|
|
1149
|
+
const idMatch = typeof meta.id === 'string' ? meta.id.match(CODEX_SESSION_UUID_RE) : null;
|
|
1150
|
+
const cwd = typeof meta.cwd === 'string' ? meta.cwd : null;
|
|
1151
|
+
return idMatch ? { id: idMatch[0], cwd } : null;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Detect the active Codex session id from disk when the runtime env vars are
|
|
1156
|
+
* absent. Codex persists rollout files under
|
|
1157
|
+
* ~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl. We pick the
|
|
1158
|
+
* most recently modified rollout whose recorded cwd matches the current
|
|
1159
|
+
* working directory, falling back to the most recent rollout overall.
|
|
1160
|
+
*/
|
|
1161
|
+
export function detectCodexSessionIdFromDisk() {
|
|
1162
|
+
try {
|
|
1163
|
+
const sessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
1164
|
+
if (!fs.existsSync(sessionsDir)) return null;
|
|
1165
|
+
|
|
1166
|
+
const cwd = process.cwd();
|
|
1167
|
+
const candidates = [];
|
|
1168
|
+
const walk = (dir, depth) => {
|
|
1169
|
+
if (depth > 5 || candidates.length > 1000) return;
|
|
1170
|
+
let entries;
|
|
1171
|
+
try {
|
|
1172
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1173
|
+
} catch {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
for (const entry of entries) {
|
|
1177
|
+
const full = path.join(dir, entry.name);
|
|
1178
|
+
if (entry.isDirectory()) {
|
|
1179
|
+
walk(full, depth + 1);
|
|
1180
|
+
} else if (
|
|
1181
|
+
entry.isFile() &&
|
|
1182
|
+
entry.name.startsWith('rollout-') &&
|
|
1183
|
+
entry.name.endsWith('.jsonl')
|
|
1184
|
+
) {
|
|
1185
|
+
try {
|
|
1186
|
+
candidates.push({ full, name: entry.name, mtime: fs.statSync(full).mtimeMs });
|
|
1187
|
+
} catch {
|
|
1188
|
+
/* ignore unreadable file */
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
walk(sessionsDir, 0);
|
|
1194
|
+
|
|
1195
|
+
if (candidates.length === 0) return null;
|
|
1196
|
+
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
1197
|
+
|
|
1198
|
+
let fallbackId = null;
|
|
1199
|
+
for (const candidate of candidates.slice(0, 25)) {
|
|
1200
|
+
const meta = extractCodexRolloutMeta(readFirstLine(candidate.full));
|
|
1201
|
+
const id = meta?.id ?? candidate.name.match(CODEX_SESSION_UUID_RE)?.[0] ?? null;
|
|
1202
|
+
if (!id) continue;
|
|
1203
|
+
if (fallbackId === null) fallbackId = id;
|
|
1204
|
+
if (meta?.cwd && meta.cwd === cwd) return id;
|
|
1205
|
+
}
|
|
1206
|
+
return fallbackId;
|
|
1207
|
+
} catch {
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
937
1212
|
/**
|
|
938
|
-
* Codex exposes the active resumable thread id
|
|
939
|
-
*
|
|
1213
|
+
* Codex exposes the active resumable thread id in the runtime environment, but
|
|
1214
|
+
* does not set those vars reliably across launch paths. Prefer the env var when
|
|
1215
|
+
* present, otherwise fall back to reading the active rollout file from disk.
|
|
940
1216
|
*/
|
|
941
|
-
function detectCodexSessionId() {
|
|
942
|
-
const
|
|
943
|
-
|
|
1217
|
+
export function detectCodexSessionId() {
|
|
1218
|
+
const fromEnv = process.env.CODEX_THREAD_ID?.trim() || process.env.CODEX_SESSION_ID?.trim();
|
|
1219
|
+
if (fromEnv) return fromEnv;
|
|
1220
|
+
return detectCodexSessionIdFromDisk();
|
|
944
1221
|
}
|
|
945
1222
|
|
|
946
1223
|
/**
|
|
@@ -1758,7 +2035,9 @@ async function protocolGetDevice(args) {
|
|
|
1758
2035
|
await resolveProtocolAuthForFlags(flags);
|
|
1759
2036
|
const timeoutMs = resolveTimeout(flags);
|
|
1760
2037
|
|
|
1761
|
-
const deviceFingerprint = String(
|
|
2038
|
+
const deviceFingerprint = String(
|
|
2039
|
+
flags['device-fingerprint'] ?? process.env.OVERLORD_DEVICE_FINGERPRINT ?? ''
|
|
2040
|
+
);
|
|
1762
2041
|
if (!deviceFingerprint) {
|
|
1763
2042
|
console.error('Error: --device-fingerprint is required (or set OVERLORD_DEVICE_FINGERPRINT)');
|
|
1764
2043
|
process.exit(1);
|
|
@@ -1800,7 +2079,9 @@ async function protocolUpdateDevice(args) {
|
|
|
1800
2079
|
await resolveProtocolAuthForFlags(flags);
|
|
1801
2080
|
const timeoutMs = resolveTimeout(flags);
|
|
1802
2081
|
|
|
1803
|
-
const deviceFingerprint = String(
|
|
2082
|
+
const deviceFingerprint = String(
|
|
2083
|
+
flags['device-fingerprint'] ?? process.env.OVERLORD_DEVICE_FINGERPRINT ?? ''
|
|
2084
|
+
);
|
|
1804
2085
|
if (!deviceFingerprint) {
|
|
1805
2086
|
console.error('Error: --device-fingerprint is required (or set OVERLORD_DEVICE_FINGERPRINT)');
|
|
1806
2087
|
process.exit(1);
|
|
@@ -1839,9 +2120,10 @@ async function protocolListProjectResources(args) {
|
|
|
1839
2120
|
process.exit(1);
|
|
1840
2121
|
}
|
|
1841
2122
|
|
|
1842
|
-
const deviceFingerprint =
|
|
1843
|
-
|
|
1844
|
-
|
|
2123
|
+
const deviceFingerprint =
|
|
2124
|
+
typeof flags['device-fingerprint'] === 'string'
|
|
2125
|
+
? flags['device-fingerprint'].trim()
|
|
2126
|
+
: (process.env.OVERLORD_DEVICE_FINGERPRINT?.trim() ?? '');
|
|
1845
2127
|
|
|
1846
2128
|
const body = {
|
|
1847
2129
|
projectId,
|
|
@@ -1871,9 +2153,8 @@ async function protocolAddProjectResource(args) {
|
|
|
1871
2153
|
const timeoutMs = resolveTimeout(flags);
|
|
1872
2154
|
|
|
1873
2155
|
const projectId = typeof flags['project-id'] === 'string' ? flags['project-id'].trim() : '';
|
|
1874
|
-
const directoryPath =
|
|
1875
|
-
? flags['directory'].trim()
|
|
1876
|
-
: process.cwd();
|
|
2156
|
+
const directoryPath =
|
|
2157
|
+
typeof flags['directory'] === 'string' ? flags['directory'].trim() : process.cwd();
|
|
1877
2158
|
const deviceFingerprint = String(
|
|
1878
2159
|
flags['device-fingerprint'] ?? process.env.OVERLORD_DEVICE_FINGERPRINT ?? ''
|
|
1879
2160
|
);
|
|
@@ -2489,20 +2770,18 @@ async function protocolPrompt(args) {
|
|
|
2489
2770
|
...(flags['available-tools'] ? { availableTools: String(flags['available-tools']) } : {}),
|
|
2490
2771
|
...(forHuman !== undefined ? { forHuman } : {}),
|
|
2491
2772
|
delegate: resolveProtocolTicketDelegate(flags, modelIdentifier, agentIdentifier),
|
|
2773
|
+
...(flags['assigned-to'] ? { assignedTo: String(flags['assigned-to']) } : {}),
|
|
2492
2774
|
...(flags['parent-session-key']
|
|
2493
2775
|
? { parentSessionKey: String(flags['parent-session-key']) }
|
|
2494
2776
|
: {}),
|
|
2495
2777
|
...(parentTicketId ? { parentTicketId } : {})
|
|
2496
2778
|
};
|
|
2497
2779
|
|
|
2498
|
-
const data = await
|
|
2499
|
-
platformUrl,
|
|
2500
|
-
bearerToken,
|
|
2501
|
-
localSecret,
|
|
2502
|
-
organizationId,
|
|
2780
|
+
const data = await apiPostCreatingTicket(
|
|
2781
|
+
{ platformUrl, bearerToken, localSecret, organizationId, timeoutMs },
|
|
2503
2782
|
'/api/protocol/prompt',
|
|
2504
2783
|
body,
|
|
2505
|
-
|
|
2784
|
+
'prompt'
|
|
2506
2785
|
);
|
|
2507
2786
|
|
|
2508
2787
|
const sessionKey = data.session?.sessionKey;
|
|
@@ -2577,17 +2856,15 @@ async function protocolCreateTicket(args) {
|
|
|
2577
2856
|
: {}),
|
|
2578
2857
|
...(flags['available-tools'] ? { availableTools: String(flags['available-tools']) } : {}),
|
|
2579
2858
|
...(forHuman !== undefined ? { forHuman } : {}),
|
|
2580
|
-
delegate: resolveProtocolTicketDelegate(flags, modelIdentifier, agentIdentifier)
|
|
2859
|
+
delegate: resolveProtocolTicketDelegate(flags, modelIdentifier, agentIdentifier),
|
|
2860
|
+
...(flags['assigned-to'] ? { assignedTo: String(flags['assigned-to']) } : {})
|
|
2581
2861
|
};
|
|
2582
2862
|
|
|
2583
|
-
const data = await
|
|
2584
|
-
platformUrl,
|
|
2585
|
-
bearerToken,
|
|
2586
|
-
localSecret,
|
|
2587
|
-
organizationId,
|
|
2863
|
+
const data = await apiPostCreatingTicket(
|
|
2864
|
+
{ platformUrl, bearerToken, localSecret, organizationId, timeoutMs },
|
|
2588
2865
|
'/api/protocol/create-ticket',
|
|
2589
2866
|
body,
|
|
2590
|
-
|
|
2867
|
+
'create'
|
|
2591
2868
|
);
|
|
2592
2869
|
console.log(JSON.stringify(data, null, 2));
|
|
2593
2870
|
return;
|
|
@@ -2617,17 +2894,15 @@ async function protocolCreateTicket(args) {
|
|
|
2617
2894
|
: {}),
|
|
2618
2895
|
...(flags['available-tools'] ? { availableTools: String(flags['available-tools']) } : {}),
|
|
2619
2896
|
...(forHuman !== undefined ? { forHuman } : {}),
|
|
2620
|
-
delegate: resolveProtocolTicketDelegate(flags, modelIdentifier, agentIdentifier)
|
|
2897
|
+
delegate: resolveProtocolTicketDelegate(flags, modelIdentifier, agentIdentifier),
|
|
2898
|
+
...(flags['assigned-to'] ? { assignedTo: String(flags['assigned-to']) } : {})
|
|
2621
2899
|
};
|
|
2622
2900
|
|
|
2623
|
-
const data = await
|
|
2624
|
-
platformUrl,
|
|
2625
|
-
bearerToken,
|
|
2626
|
-
localSecret,
|
|
2627
|
-
organizationId,
|
|
2901
|
+
const data = await apiPostCreatingTicket(
|
|
2902
|
+
{ platformUrl, bearerToken, localSecret, organizationId, timeoutMs },
|
|
2628
2903
|
'/api/protocol/tickets',
|
|
2629
2904
|
standaloneBody,
|
|
2630
|
-
|
|
2905
|
+
'create'
|
|
2631
2906
|
);
|
|
2632
2907
|
console.log(JSON.stringify(data, null, 2));
|
|
2633
2908
|
}
|
|
@@ -2712,21 +2987,19 @@ async function protocolRecordWork(args) {
|
|
|
2712
2987
|
? { acceptanceCriteria: String(flags['acceptance-criteria']) }
|
|
2713
2988
|
: {}),
|
|
2714
2989
|
...(flags['available-tools'] ? { availableTools: String(flags['available-tools']) } : {}),
|
|
2715
|
-
delegate: resolveProtocolTicketDelegate(flags, modelIdentifier, agentIdentifier)
|
|
2990
|
+
delegate: resolveProtocolTicketDelegate(flags, modelIdentifier, agentIdentifier),
|
|
2991
|
+
...(flags['assigned-to'] ? { assignedTo: String(flags['assigned-to']) } : {})
|
|
2716
2992
|
};
|
|
2717
2993
|
|
|
2718
2994
|
const { platformUrl, bearerToken, localSecret, organizationId } =
|
|
2719
2995
|
await resolveProtocolAuthForFlags(flags);
|
|
2720
2996
|
const timeoutMs = resolveTimeout(flags);
|
|
2721
2997
|
|
|
2722
|
-
const data = await
|
|
2723
|
-
platformUrl,
|
|
2724
|
-
bearerToken,
|
|
2725
|
-
localSecret,
|
|
2726
|
-
organizationId,
|
|
2998
|
+
const data = await apiPostCreatingTicket(
|
|
2999
|
+
{ platformUrl, bearerToken, localSecret, organizationId, timeoutMs },
|
|
2727
3000
|
'/api/protocol/record-work',
|
|
2728
3001
|
body,
|
|
2729
|
-
|
|
3002
|
+
'record-work'
|
|
2730
3003
|
);
|
|
2731
3004
|
console.log(JSON.stringify(data, null, 2));
|
|
2732
3005
|
|
|
@@ -2751,21 +3024,103 @@ async function protocolAddObjectives(args) {
|
|
|
2751
3024
|
await resolveProtocolAuthForFlags(flags, ticketId);
|
|
2752
3025
|
const timeoutMs = resolveTimeout(flags);
|
|
2753
3026
|
|
|
2754
|
-
const data = await
|
|
2755
|
-
platformUrl,
|
|
2756
|
-
bearerToken,
|
|
2757
|
-
localSecret,
|
|
2758
|
-
organizationId,
|
|
3027
|
+
const data = await apiPostCreatingTicket(
|
|
3028
|
+
{ platformUrl, bearerToken, localSecret, organizationId, timeoutMs },
|
|
2759
3029
|
'/api/protocol/add-objectives',
|
|
2760
3030
|
{
|
|
2761
3031
|
ticketId,
|
|
2762
3032
|
objectives: objectiveInput.objectives
|
|
2763
3033
|
},
|
|
2764
|
-
|
|
3034
|
+
'add-objectives'
|
|
2765
3035
|
);
|
|
2766
3036
|
console.log(JSON.stringify(data, null, 2));
|
|
2767
3037
|
}
|
|
2768
3038
|
|
|
3039
|
+
// ---------------------------------------------------------------------------
|
|
3040
|
+
// pending-tickets (inspect / retry / clear locally-saved unconfirmed drafts)
|
|
3041
|
+
// ---------------------------------------------------------------------------
|
|
3042
|
+
|
|
3043
|
+
async function protocolPendingTickets(args) {
|
|
3044
|
+
const flags = parseFlags(args);
|
|
3045
|
+
|
|
3046
|
+
if (flags['clear-all']) {
|
|
3047
|
+
const records = listPendingTickets();
|
|
3048
|
+
for (const record of records) resolvePendingTicket(record.id);
|
|
3049
|
+
console.log(JSON.stringify({ cleared: records.length }, null, 2));
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
if (flags.clear) {
|
|
3054
|
+
const id = String(flags.clear);
|
|
3055
|
+
resolvePendingTicket(id);
|
|
3056
|
+
console.log(JSON.stringify({ cleared: id }, null, 2));
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
if (flags.retry) {
|
|
3061
|
+
const id = String(flags.retry);
|
|
3062
|
+
const record = listPendingTickets().find(entry => entry.id === id);
|
|
3063
|
+
if (!record) {
|
|
3064
|
+
throw new Error(
|
|
3065
|
+
`No pending ticket draft with id "${id}". Run \`ovld protocol pending-tickets\` to list drafts.`
|
|
3066
|
+
);
|
|
3067
|
+
}
|
|
3068
|
+
if (!record.endpoint || !record.body) {
|
|
3069
|
+
throw new Error(
|
|
3070
|
+
`Pending draft "${id}" is missing the original request and cannot be retried.`
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
const ticketIdHint = record.ticketId ?? record.body?.ticketId ?? '';
|
|
3075
|
+
const { platformUrl, bearerToken, localSecret, organizationId } =
|
|
3076
|
+
await resolveProtocolAuthForFlags(flags, ticketIdHint);
|
|
3077
|
+
const timeoutMs = resolveTimeout(flags);
|
|
3078
|
+
|
|
3079
|
+
const data = await apiPost(
|
|
3080
|
+
platformUrl,
|
|
3081
|
+
bearerToken,
|
|
3082
|
+
localSecret,
|
|
3083
|
+
organizationId,
|
|
3084
|
+
record.endpoint,
|
|
3085
|
+
record.body,
|
|
3086
|
+
timeoutMs
|
|
3087
|
+
);
|
|
3088
|
+
resolvePendingTicket(id);
|
|
3089
|
+
console.log(JSON.stringify(data, null, 2));
|
|
3090
|
+
return;
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
const records = listPendingTickets();
|
|
3094
|
+
|
|
3095
|
+
if (flags.json) {
|
|
3096
|
+
console.log(JSON.stringify(records, null, 2));
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
if (records.length === 0) {
|
|
3101
|
+
console.log('No pending ticket drafts. Everything you created was confirmed by the server.');
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
const lines = [`${records.length} pending ticket draft(s) not yet confirmed by the server:`, ''];
|
|
3106
|
+
for (const record of records) {
|
|
3107
|
+
const objectiveText = record.objectives?.length
|
|
3108
|
+
? record.objectives.join(' | ')
|
|
3109
|
+
: (record.title ?? '(no objective text captured)');
|
|
3110
|
+
lines.push(`• ${record.id} [${record.kind}] ${record.createdAt}`);
|
|
3111
|
+
if (record.title) lines.push(` title: ${record.title}`);
|
|
3112
|
+
lines.push(` text: ${truncateForDisplay(objectiveText)}`);
|
|
3113
|
+
if (record.ticketId) lines.push(` ticket: ${record.ticketId}`);
|
|
3114
|
+
lines.push(` file: ${getPendingTicketFilePath(record.id)}`);
|
|
3115
|
+
lines.push('');
|
|
3116
|
+
}
|
|
3117
|
+
lines.push('These drafts were saved before the server confirmed them.');
|
|
3118
|
+
lines.push(' Retry a draft: ovld protocol pending-tickets --retry <id>');
|
|
3119
|
+
lines.push(' Clear a draft: ovld protocol pending-tickets --clear <id>');
|
|
3120
|
+
lines.push(' Clear all: ovld protocol pending-tickets --clear-all');
|
|
3121
|
+
console.log(lines.join('\n'));
|
|
3122
|
+
}
|
|
3123
|
+
|
|
2769
3124
|
// ---------------------------------------------------------------------------
|
|
2770
3125
|
// search-tickets (find tickets by query/status/project/created_by/dates)
|
|
2771
3126
|
// ---------------------------------------------------------------------------
|
|
@@ -2873,6 +3228,7 @@ Subcommands:
|
|
|
2873
3228
|
search-tickets Find tickets by keyword, status, project, creator, or update date
|
|
2874
3229
|
discuss-objective Mark a draft objective as submitted (ready for review/execution)
|
|
2875
3230
|
add-objectives Append ordered objectives to an existing ticket
|
|
3231
|
+
pending-tickets List, retry, or clear locally-saved drafts the server never confirmed
|
|
2876
3232
|
create Create a draft ticket without attaching (follow-up or standalone)
|
|
2877
3233
|
prompt Create a ticket and attach to it immediately
|
|
2878
3234
|
record-work Record completed-from-chat work as a ticket in review + feed post (no attach)
|
|
@@ -3164,6 +3520,7 @@ prompt:
|
|
|
3164
3520
|
--for-human Mark the new ticket as requiring a human
|
|
3165
3521
|
--execution-target <t> Deprecated alias: agent | human
|
|
3166
3522
|
--delegate <model> Model or delegate identifier that created the ticket
|
|
3523
|
+
--assigned-to <member> Assign the ticket to a member: username, email, user-id, or orgid:username (defaults to creator)
|
|
3167
3524
|
--parent-session-key <key>
|
|
3168
3525
|
--parent-ticket-id <ticket_id>
|
|
3169
3526
|
--agent <identifier>
|
|
@@ -3204,6 +3561,7 @@ create:
|
|
|
3204
3561
|
--for-human Mark the new ticket as requiring a human
|
|
3205
3562
|
--execution-target <t> Deprecated alias: agent | human
|
|
3206
3563
|
--delegate <model> Model or delegate identifier that created the ticket
|
|
3564
|
+
--assigned-to <member> Assign the ticket to a member: username, email, user-id, or orgid:username (defaults to creator)
|
|
3207
3565
|
--agent <identifier>
|
|
3208
3566
|
--model <identifier>
|
|
3209
3567
|
Returns:
|
|
@@ -3226,6 +3584,22 @@ add-objectives:
|
|
|
3226
3584
|
goal. Create multiple tickets instead when prompts describe different
|
|
3227
3585
|
features or goals.
|
|
3228
3586
|
|
|
3587
|
+
pending-tickets:
|
|
3588
|
+
Purpose:
|
|
3589
|
+
Inspect, retry, or clear ticket/objective drafts that were saved locally
|
|
3590
|
+
before a create/prompt/add-objectives/record-work call but never confirmed
|
|
3591
|
+
by the server (e.g. the network dropped). Drafts are written before the
|
|
3592
|
+
request and deleted automatically once the server confirms the write, so a
|
|
3593
|
+
flaky connection never loses your text.
|
|
3594
|
+
Usage:
|
|
3595
|
+
--json Print the raw saved drafts as JSON instead of a summary
|
|
3596
|
+
--retry <id> Re-send a saved draft to the server; clears it on success
|
|
3597
|
+
--clear <id> Delete a single saved draft after confirming it landed
|
|
3598
|
+
--clear-all Delete every saved draft
|
|
3599
|
+
Notes:
|
|
3600
|
+
Drafts live under ~/.overlord/pending-tickets/. With no flags the command
|
|
3601
|
+
lists outstanding drafts with their captured objective text.
|
|
3602
|
+
|
|
3229
3603
|
record-work:
|
|
3230
3604
|
Purpose:
|
|
3231
3605
|
Record work the agent already completed in a chat as a ticket in \`review\` status
|
|
@@ -3252,6 +3626,7 @@ record-work:
|
|
|
3252
3626
|
--acceptance-criteria <text>
|
|
3253
3627
|
--available-tools <text>
|
|
3254
3628
|
--delegate <model>
|
|
3629
|
+
--assigned-to <member> Assign the ticket to a member: username, email, user-id, or orgid:username (defaults to creator)
|
|
3255
3630
|
--agent <identifier>
|
|
3256
3631
|
--model <identifier>
|
|
3257
3632
|
Notes:
|
|
@@ -3467,6 +3842,9 @@ Examples:
|
|
|
3467
3842
|
ovld protocol create --agent codex --objectives-json '[{"objective":"Step one"},{"objective":"Step two"}]'
|
|
3468
3843
|
ovld protocol create --agent codex --session-key <key> --ticket-id <ticket_id> --objective "Capture follow-up work"
|
|
3469
3844
|
ovld protocol prompt --agent codex --objective "Implement user auth" --priority high
|
|
3845
|
+
ovld protocol pending-tickets
|
|
3846
|
+
ovld protocol pending-tickets --retry <draft-id>
|
|
3847
|
+
ovld protocol pending-tickets --clear-all
|
|
3470
3848
|
ovld protocol update --session-key <key> --ticket-id <ticket_id> --summary "Did X" --phase execute
|
|
3471
3849
|
ovld protocol update --session-key <key> --ticket-id <ticket_id> --summary-file ./update.txt --event-type user_follow_up
|
|
3472
3850
|
ovld protocol record-change-rationales --session-key <key> --ticket-id <ticket_id> --change-rationales-json '[{"label":"...","file_path":"...","summary":"...","why":"...","impact":"...","hunks":[{"header":"@@ ... @@"}]}]'
|
|
@@ -3546,6 +3924,10 @@ EOF
|
|
|
3546
3924
|
await protocolAddObjectives(args);
|
|
3547
3925
|
return;
|
|
3548
3926
|
}
|
|
3927
|
+
if (subcommand === 'pending-tickets') {
|
|
3928
|
+
await protocolPendingTickets(args);
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
3549
3931
|
if (subcommand === 'create' || subcommand === 'create-ticket') {
|
|
3550
3932
|
await protocolCreateTicket(args);
|
|
3551
3933
|
return;
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ Create a draft Overlord ticket from the user's request.
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
Use `$ARGUMENTS` as the input.
|
|
11
|
-
If it already contains flags such as `--title`, `--priority`, `--project-id`, or `--for-human`, pass those flags through after `ovld protocol create --agent claude-code`.
|
|
11
|
+
If it already contains flags such as `--title`, `--priority`, `--project-id`, `--assigned-to`, or `--for-human`, pass those flags through after `ovld protocol create --agent claude-code`.
|
|
12
12
|
Otherwise, treat `$ARGUMENTS` as the objective text and run:
|
|
13
13
|
`ovld protocol create --agent claude-code --objective "<objective>"`
|
|
14
14
|
|
|
@@ -8,7 +8,7 @@ Create a new Overlord ticket from the user's request.
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
Use `$ARGUMENTS` as the input.
|
|
11
|
-
If it already contains flags such as `--title`, `--priority`, `--project-id`, or `--for-human`, pass those flags through after `ovld protocol prompt --agent claude-code`.
|
|
11
|
+
If it already contains flags such as `--title`, `--priority`, `--project-id`, `--assigned-to`, or `--for-human`, pass those flags through after `ovld protocol prompt --agent claude-code`.
|
|
12
12
|
Otherwise, treat `$ARGUMENTS` as the objective text and run:
|
|
13
13
|
`ovld protocol prompt --agent claude-code --objective "<objective>"`
|
|
14
14
|
|
|
@@ -9,7 +9,7 @@ This command is deprecated. Use `/overlord:prompt` instead.
|
|
|
9
9
|
Create a new Overlord ticket from the user's request.
|
|
10
10
|
|
|
11
11
|
Use `$ARGUMENTS` as the input.
|
|
12
|
-
If it already contains flags such as `--title`, `--priority`, `--project-id`, or `--for-human`, pass those flags through after `ovld protocol prompt --agent claude-code`.
|
|
12
|
+
If it already contains flags such as `--title`, `--priority`, `--project-id`, `--assigned-to`, or `--for-human`, pass those flags through after `ovld protocol prompt --agent claude-code`.
|
|
13
13
|
Otherwise, treat `$ARGUMENTS` as the objective text and run:
|
|
14
14
|
`ovld protocol prompt --agent claude-code --objective "<objective>"`
|
|
15
15
|
|
|
@@ -118,6 +118,7 @@ When creating tickets from within a repository:
|
|
|
118
118
|
- Create multiple tickets when each prompt represents a different feature or goal.
|
|
119
119
|
- Add objectives to the same ticket when each prompt is a sequential step toward the same feature or goal; use `ovld protocol add-objectives --ticket-id <ticket_id> --objectives-json '[{"objective":"..."}]'`.
|
|
120
120
|
- `create`, `prompt`, and `record-work` require `--objectives-json` or `--objectives-file` with an ordered array of `{ "objective": "...", "title": "...", "autoAdvance": true }` objects. A single objective is just an array with one item.
|
|
121
|
+
- `create`, `prompt`, `create-ticket`, and `record-work` accept `--assigned-to <member>` to set the ticket's human owner. Accepts a username, an email, a user-id UUID, or the `orgid:username` member ID. When omitted, the assignee defaults to the ticket creator.
|
|
121
122
|
|
|
122
123
|
```bash
|
|
123
124
|
ovld protocol create --agent claude-code --objectives-json '[{"objective":"Capture follow-up work from this repository"}]'
|
|
@@ -131,6 +132,17 @@ ovld protocol prompt --agent claude-code --objectives-json '[{"objective":"Imple
|
|
|
131
132
|
ovld protocol add-objectives --ticket-id 1:899 --objectives-json '[{"objective":"Implement the API"},{"objective":"Add CLI docs"}]'
|
|
132
133
|
```
|
|
133
134
|
|
|
135
|
+
### Local Durability For New Tickets
|
|
136
|
+
|
|
137
|
+
`create`, `prompt`, `add-objectives`, and `record-work` save the objective/ticket text to a local draft (`~/.overlord/pending-tickets/`) **before** sending it, and delete that draft only once the server confirms the write. If the network drops mid-call, your text is never lost — the failure message points at the saved file. Manage outstanding drafts with `pending-tickets`:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
ovld protocol pending-tickets # list drafts the server never confirmed
|
|
141
|
+
ovld protocol pending-tickets --retry <id> # re-send a saved draft; clears it on success
|
|
142
|
+
ovld protocol pending-tickets --clear <id> # delete one draft after confirming it landed
|
|
143
|
+
ovld protocol pending-tickets --clear-all # delete every draft
|
|
144
|
+
```
|
|
145
|
+
|
|
134
146
|
To inspect project resolution explicitly:
|
|
135
147
|
|
|
136
148
|
```bash
|
|
@@ -118,6 +118,7 @@ When creating tickets from within a repository:
|
|
|
118
118
|
- Create multiple tickets when each prompt represents a different feature or goal.
|
|
119
119
|
- Add objectives to the same ticket when each prompt is a sequential step toward the same feature or goal; use `ovld protocol add-objectives --ticket-id <ticket_id> --objectives-json '[{"objective":"..."}]'`.
|
|
120
120
|
- `create`, `prompt`, and `record-work` require `--objectives-json` or `--objectives-file` with an ordered array of `{ "objective": "...", "title": "...", "autoAdvance": true }` objects. A single objective is just an array with one item.
|
|
121
|
+
- `create`, `prompt`, `create-ticket`, and `record-work` accept `--assigned-to <member>` to set the ticket's human owner. Accepts a username, an email, a user-id UUID, or the `orgid:username` member ID. When omitted, the assignee defaults to the ticket creator.
|
|
121
122
|
|
|
122
123
|
```bash
|
|
123
124
|
ovld protocol create --agent cursor --objectives-json '[{"objective":"Capture follow-up work from this repository"}]'
|
|
@@ -131,6 +132,17 @@ ovld protocol prompt --agent cursor --objectives-json '[{"objective":"Implement
|
|
|
131
132
|
ovld protocol add-objectives --ticket-id 1:899 --objectives-json '[{"objective":"Implement the API"},{"objective":"Add CLI docs"}]'
|
|
132
133
|
```
|
|
133
134
|
|
|
135
|
+
### Local Durability For New Tickets
|
|
136
|
+
|
|
137
|
+
`create`, `prompt`, `add-objectives`, and `record-work` save the objective/ticket text to a local draft (`~/.overlord/pending-tickets/`) **before** sending it, and delete that draft only once the server confirms the write. If the network drops mid-call, your text is never lost — the failure message points at the saved file. Manage outstanding drafts with `pending-tickets`:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
ovld protocol pending-tickets # list drafts the server never confirmed
|
|
141
|
+
ovld protocol pending-tickets --retry <id> # re-send a saved draft; clears it on success
|
|
142
|
+
ovld protocol pending-tickets --clear <id> # delete one draft after confirming it landed
|
|
143
|
+
ovld protocol pending-tickets --clear-all # delete every draft
|
|
144
|
+
```
|
|
145
|
+
|
|
134
146
|
To inspect project resolution explicitly:
|
|
135
147
|
|
|
136
148
|
```bash
|
|
@@ -244,6 +244,11 @@ const tools = [
|
|
|
244
244
|
available_tools: { type: 'string' },
|
|
245
245
|
for_human: { type: 'boolean' },
|
|
246
246
|
delegate: { type: 'string' },
|
|
247
|
+
assigned_to: {
|
|
248
|
+
type: 'string',
|
|
249
|
+
description:
|
|
250
|
+
'Assign the ticket to a member: username, email, user-id, or orgid:username (defaults to creator).'
|
|
251
|
+
},
|
|
247
252
|
parent_session_key: { type: 'string' },
|
|
248
253
|
parent_ticket_id: { type: 'string' },
|
|
249
254
|
agent: { type: 'string' },
|
|
@@ -262,6 +267,7 @@ const tools = [
|
|
|
262
267
|
'available-tools': args.available_tools,
|
|
263
268
|
'for-human': args.for_human === true ? true : undefined,
|
|
264
269
|
delegate: args.delegate,
|
|
270
|
+
'assigned-to': args.assigned_to,
|
|
265
271
|
'parent-session-key': args.parent_session_key,
|
|
266
272
|
'parent-ticket-id': args.parent_ticket_id,
|
|
267
273
|
agent: args.agent,
|
|
@@ -286,6 +292,11 @@ const tools = [
|
|
|
286
292
|
available_tools: { type: 'string' },
|
|
287
293
|
for_human: { type: 'boolean' },
|
|
288
294
|
delegate: { type: 'string' },
|
|
295
|
+
assigned_to: {
|
|
296
|
+
type: 'string',
|
|
297
|
+
description:
|
|
298
|
+
'Assign the ticket to a member: username, email, user-id, or orgid:username (defaults to creator).'
|
|
299
|
+
},
|
|
289
300
|
session_key: { type: 'string' },
|
|
290
301
|
ticket_id: { type: 'string' },
|
|
291
302
|
agent: { type: 'string' }
|
|
@@ -303,6 +314,7 @@ const tools = [
|
|
|
303
314
|
'available-tools': args.available_tools,
|
|
304
315
|
'for-human': args.for_human === true ? true : undefined,
|
|
305
316
|
delegate: args.delegate,
|
|
317
|
+
'assigned-to': args.assigned_to,
|
|
306
318
|
'session-key': args.session_key,
|
|
307
319
|
'ticket-id': args.ticket_id,
|
|
308
320
|
agent: args.agent
|
|
@@ -628,6 +640,11 @@ const tools = [
|
|
|
628
640
|
acceptance_criteria: { type: 'string' },
|
|
629
641
|
available_tools: { type: 'string' },
|
|
630
642
|
delegate: { type: 'string' },
|
|
643
|
+
assigned_to: {
|
|
644
|
+
type: 'string',
|
|
645
|
+
description:
|
|
646
|
+
'Assign the ticket to a member: username, email, user-id, or orgid:username (defaults to creator).'
|
|
647
|
+
},
|
|
631
648
|
agent: { type: 'string' },
|
|
632
649
|
skip_file_change_check: { type: 'boolean' }
|
|
633
650
|
},
|
|
@@ -643,6 +660,7 @@ const tools = [
|
|
|
643
660
|
'acceptance-criteria': args.acceptance_criteria,
|
|
644
661
|
'available-tools': args.available_tools,
|
|
645
662
|
delegate: args.delegate,
|
|
663
|
+
'assigned-to': args.assigned_to,
|
|
646
664
|
agent: args.agent,
|
|
647
665
|
'skip-file-change-check': args.skip_file_change_check
|
|
648
666
|
}),
|
|
@@ -32,12 +32,73 @@ if [ -n "$OVERLORD_BASE_URL" ] && [ -n "$OVERLORD_TOKEN" ] && [ -n "$TICKET_ID"
|
|
|
32
32
|
import base64
|
|
33
33
|
import json
|
|
34
34
|
import os
|
|
35
|
+
import re
|
|
35
36
|
import sys
|
|
36
37
|
import tempfile
|
|
37
38
|
from pathlib import Path
|
|
38
39
|
|
|
40
|
+
UUID_RE = re.compile(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', re.I)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def detect_codex_session_id_from_disk():
|
|
44
|
+
# Codex does not reliably export CODEX_THREAD_ID/CODEX_SESSION_ID, so fall
|
|
45
|
+
# back to the active rollout file under ~/.codex/sessions. Prefer the most
|
|
46
|
+
# recent rollout whose recorded cwd matches ours.
|
|
47
|
+
try:
|
|
48
|
+
sessions_dir = Path.home() / '.codex' / 'sessions'
|
|
49
|
+
if not sessions_dir.is_dir():
|
|
50
|
+
return None
|
|
51
|
+
cwd = os.getcwd()
|
|
52
|
+
candidates = []
|
|
53
|
+
for entry in sessions_dir.rglob('rollout-*.jsonl'):
|
|
54
|
+
try:
|
|
55
|
+
candidates.append((entry.stat().st_mtime, entry))
|
|
56
|
+
except OSError:
|
|
57
|
+
continue
|
|
58
|
+
if len(candidates) > 1000:
|
|
59
|
+
break
|
|
60
|
+
if not candidates:
|
|
61
|
+
return None
|
|
62
|
+
candidates.sort(key=lambda item: item[0], reverse=True)
|
|
63
|
+
fallback_id = None
|
|
64
|
+
for _, entry in candidates[:25]:
|
|
65
|
+
meta_id = None
|
|
66
|
+
meta_cwd = None
|
|
67
|
+
try:
|
|
68
|
+
with entry.open('r', encoding='utf-8', errors='replace') as handle:
|
|
69
|
+
first_line = handle.readline()
|
|
70
|
+
obj = json.loads(first_line)
|
|
71
|
+
meta = obj.get('payload') if isinstance(obj.get('payload'), dict) else obj
|
|
72
|
+
if isinstance(meta, dict):
|
|
73
|
+
raw_id = meta.get('id')
|
|
74
|
+
if isinstance(raw_id, str):
|
|
75
|
+
match = UUID_RE.search(raw_id)
|
|
76
|
+
meta_id = match.group(0) if match else None
|
|
77
|
+
if isinstance(meta.get('cwd'), str):
|
|
78
|
+
meta_cwd = meta.get('cwd')
|
|
79
|
+
except Exception:
|
|
80
|
+
meta_id = None
|
|
81
|
+
if not meta_id:
|
|
82
|
+
match = UUID_RE.search(entry.name)
|
|
83
|
+
meta_id = match.group(0) if match else None
|
|
84
|
+
if not meta_id:
|
|
85
|
+
continue
|
|
86
|
+
if fallback_id is None:
|
|
87
|
+
fallback_id = meta_id
|
|
88
|
+
if meta_cwd and meta_cwd == cwd:
|
|
89
|
+
return meta_id
|
|
90
|
+
return fallback_id
|
|
91
|
+
except Exception:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
39
95
|
body = json.load(sys.stdin)
|
|
40
|
-
external_session_id =
|
|
96
|
+
external_session_id = (
|
|
97
|
+
os.environ.get('CODEX_THREAD_ID')
|
|
98
|
+
or os.environ.get('CODEX_SESSION_ID')
|
|
99
|
+
or detect_codex_session_id_from_disk()
|
|
100
|
+
or None
|
|
101
|
+
)
|
|
41
102
|
session_key = os.environ.get('SESSION_KEY') or ''
|
|
42
103
|
|
|
43
104
|
if not session_key:
|
|
@@ -118,6 +118,7 @@ When creating tickets from within a repository:
|
|
|
118
118
|
- Create multiple tickets when each prompt represents a different feature or goal.
|
|
119
119
|
- Add objectives to the same ticket when each prompt is a sequential step toward the same feature or goal; use `ovld protocol add-objectives --ticket-id <ticket_id> --objectives-json '[{"objective":"..."}]'`.
|
|
120
120
|
- `create`, `prompt`, and `record-work` require `--objectives-json` or `--objectives-file` with an ordered array of `{ "objective": "...", "title": "...", "autoAdvance": true }` objects. A single objective is just an array with one item.
|
|
121
|
+
- `create`, `prompt`, `create-ticket`, and `record-work` accept `--assigned-to <member>` to set the ticket's human owner. Accepts a username, an email, a user-id UUID, or the `orgid:username` member ID. When omitted, the assignee defaults to the ticket creator.
|
|
121
122
|
|
|
122
123
|
```bash
|
|
123
124
|
ovld protocol create --agent codex --objectives-json '[{"objective":"Capture follow-up work from this repository"}]'
|
|
@@ -131,6 +132,17 @@ ovld protocol prompt --agent codex --objectives-json '[{"objective":"Implement f
|
|
|
131
132
|
ovld protocol add-objectives --ticket-id 1:899 --objectives-json '[{"objective":"Implement the API"},{"objective":"Add CLI docs"}]'
|
|
132
133
|
```
|
|
133
134
|
|
|
135
|
+
### Local Durability For New Tickets
|
|
136
|
+
|
|
137
|
+
`create`, `prompt`, `add-objectives`, and `record-work` save the objective/ticket text to a local draft (`~/.overlord/pending-tickets/`) **before** sending it, and delete that draft only once the server confirms the write. If the network drops mid-call, your text is never lost — the failure message points at the saved file. Manage outstanding drafts with `pending-tickets`:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
ovld protocol pending-tickets # list drafts the server never confirmed
|
|
141
|
+
ovld protocol pending-tickets --retry <id> # re-send a saved draft; clears it on success
|
|
142
|
+
ovld protocol pending-tickets --clear <id> # delete one draft after confirming it landed
|
|
143
|
+
ovld protocol pending-tickets --clear-all # delete every draft
|
|
144
|
+
```
|
|
145
|
+
|
|
134
146
|
To inspect project resolution explicitly:
|
|
135
147
|
|
|
136
148
|
```bash
|