overlord-cli 0.2606070914.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 +423 -48
- package/package.json +1 -1
- package/plugins/claude/skills/overlord-ticket/reference/cli.md +11 -0
- package/plugins/cursor/skills/overlord-ticket/reference/cli.md +11 -0
- package/plugins/overlord/.codex-plugin/plugin.json +1 -1
- package/plugins/overlord/scripts/user-prompt-submit-hook.sh +62 -1
- package/plugins/overlord/skills/overlord-ticket/reference/cli.md +11 -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
|
);
|
|
@@ -2496,14 +2777,11 @@ async function protocolPrompt(args) {
|
|
|
2496
2777
|
...(parentTicketId ? { parentTicketId } : {})
|
|
2497
2778
|
};
|
|
2498
2779
|
|
|
2499
|
-
const data = await
|
|
2500
|
-
platformUrl,
|
|
2501
|
-
bearerToken,
|
|
2502
|
-
localSecret,
|
|
2503
|
-
organizationId,
|
|
2780
|
+
const data = await apiPostCreatingTicket(
|
|
2781
|
+
{ platformUrl, bearerToken, localSecret, organizationId, timeoutMs },
|
|
2504
2782
|
'/api/protocol/prompt',
|
|
2505
2783
|
body,
|
|
2506
|
-
|
|
2784
|
+
'prompt'
|
|
2507
2785
|
);
|
|
2508
2786
|
|
|
2509
2787
|
const sessionKey = data.session?.sessionKey;
|
|
@@ -2582,14 +2860,11 @@ async function protocolCreateTicket(args) {
|
|
|
2582
2860
|
...(flags['assigned-to'] ? { assignedTo: String(flags['assigned-to']) } : {})
|
|
2583
2861
|
};
|
|
2584
2862
|
|
|
2585
|
-
const data = await
|
|
2586
|
-
platformUrl,
|
|
2587
|
-
bearerToken,
|
|
2588
|
-
localSecret,
|
|
2589
|
-
organizationId,
|
|
2863
|
+
const data = await apiPostCreatingTicket(
|
|
2864
|
+
{ platformUrl, bearerToken, localSecret, organizationId, timeoutMs },
|
|
2590
2865
|
'/api/protocol/create-ticket',
|
|
2591
2866
|
body,
|
|
2592
|
-
|
|
2867
|
+
'create'
|
|
2593
2868
|
);
|
|
2594
2869
|
console.log(JSON.stringify(data, null, 2));
|
|
2595
2870
|
return;
|
|
@@ -2623,14 +2898,11 @@ async function protocolCreateTicket(args) {
|
|
|
2623
2898
|
...(flags['assigned-to'] ? { assignedTo: String(flags['assigned-to']) } : {})
|
|
2624
2899
|
};
|
|
2625
2900
|
|
|
2626
|
-
const data = await
|
|
2627
|
-
platformUrl,
|
|
2628
|
-
bearerToken,
|
|
2629
|
-
localSecret,
|
|
2630
|
-
organizationId,
|
|
2901
|
+
const data = await apiPostCreatingTicket(
|
|
2902
|
+
{ platformUrl, bearerToken, localSecret, organizationId, timeoutMs },
|
|
2631
2903
|
'/api/protocol/tickets',
|
|
2632
2904
|
standaloneBody,
|
|
2633
|
-
|
|
2905
|
+
'create'
|
|
2634
2906
|
);
|
|
2635
2907
|
console.log(JSON.stringify(data, null, 2));
|
|
2636
2908
|
}
|
|
@@ -2723,14 +2995,11 @@ async function protocolRecordWork(args) {
|
|
|
2723
2995
|
await resolveProtocolAuthForFlags(flags);
|
|
2724
2996
|
const timeoutMs = resolveTimeout(flags);
|
|
2725
2997
|
|
|
2726
|
-
const data = await
|
|
2727
|
-
platformUrl,
|
|
2728
|
-
bearerToken,
|
|
2729
|
-
localSecret,
|
|
2730
|
-
organizationId,
|
|
2998
|
+
const data = await apiPostCreatingTicket(
|
|
2999
|
+
{ platformUrl, bearerToken, localSecret, organizationId, timeoutMs },
|
|
2731
3000
|
'/api/protocol/record-work',
|
|
2732
3001
|
body,
|
|
2733
|
-
|
|
3002
|
+
'record-work'
|
|
2734
3003
|
);
|
|
2735
3004
|
console.log(JSON.stringify(data, null, 2));
|
|
2736
3005
|
|
|
@@ -2755,21 +3024,103 @@ async function protocolAddObjectives(args) {
|
|
|
2755
3024
|
await resolveProtocolAuthForFlags(flags, ticketId);
|
|
2756
3025
|
const timeoutMs = resolveTimeout(flags);
|
|
2757
3026
|
|
|
2758
|
-
const data = await
|
|
2759
|
-
platformUrl,
|
|
2760
|
-
bearerToken,
|
|
2761
|
-
localSecret,
|
|
2762
|
-
organizationId,
|
|
3027
|
+
const data = await apiPostCreatingTicket(
|
|
3028
|
+
{ platformUrl, bearerToken, localSecret, organizationId, timeoutMs },
|
|
2763
3029
|
'/api/protocol/add-objectives',
|
|
2764
3030
|
{
|
|
2765
3031
|
ticketId,
|
|
2766
3032
|
objectives: objectiveInput.objectives
|
|
2767
3033
|
},
|
|
2768
|
-
|
|
3034
|
+
'add-objectives'
|
|
2769
3035
|
);
|
|
2770
3036
|
console.log(JSON.stringify(data, null, 2));
|
|
2771
3037
|
}
|
|
2772
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
|
+
|
|
2773
3124
|
// ---------------------------------------------------------------------------
|
|
2774
3125
|
// search-tickets (find tickets by query/status/project/created_by/dates)
|
|
2775
3126
|
// ---------------------------------------------------------------------------
|
|
@@ -2877,6 +3228,7 @@ Subcommands:
|
|
|
2877
3228
|
search-tickets Find tickets by keyword, status, project, creator, or update date
|
|
2878
3229
|
discuss-objective Mark a draft objective as submitted (ready for review/execution)
|
|
2879
3230
|
add-objectives Append ordered objectives to an existing ticket
|
|
3231
|
+
pending-tickets List, retry, or clear locally-saved drafts the server never confirmed
|
|
2880
3232
|
create Create a draft ticket without attaching (follow-up or standalone)
|
|
2881
3233
|
prompt Create a ticket and attach to it immediately
|
|
2882
3234
|
record-work Record completed-from-chat work as a ticket in review + feed post (no attach)
|
|
@@ -3232,6 +3584,22 @@ add-objectives:
|
|
|
3232
3584
|
goal. Create multiple tickets instead when prompts describe different
|
|
3233
3585
|
features or goals.
|
|
3234
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
|
+
|
|
3235
3603
|
record-work:
|
|
3236
3604
|
Purpose:
|
|
3237
3605
|
Record work the agent already completed in a chat as a ticket in \`review\` status
|
|
@@ -3474,6 +3842,9 @@ Examples:
|
|
|
3474
3842
|
ovld protocol create --agent codex --objectives-json '[{"objective":"Step one"},{"objective":"Step two"}]'
|
|
3475
3843
|
ovld protocol create --agent codex --session-key <key> --ticket-id <ticket_id> --objective "Capture follow-up work"
|
|
3476
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
|
|
3477
3848
|
ovld protocol update --session-key <key> --ticket-id <ticket_id> --summary "Did X" --phase execute
|
|
3478
3849
|
ovld protocol update --session-key <key> --ticket-id <ticket_id> --summary-file ./update.txt --event-type user_follow_up
|
|
3479
3850
|
ovld protocol record-change-rationales --session-key <key> --ticket-id <ticket_id> --change-rationales-json '[{"label":"...","file_path":"...","summary":"...","why":"...","impact":"...","hunks":[{"header":"@@ ... @@"}]}]'
|
|
@@ -3553,6 +3924,10 @@ EOF
|
|
|
3553
3924
|
await protocolAddObjectives(args);
|
|
3554
3925
|
return;
|
|
3555
3926
|
}
|
|
3927
|
+
if (subcommand === 'pending-tickets') {
|
|
3928
|
+
await protocolPendingTickets(args);
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
3556
3931
|
if (subcommand === 'create' || subcommand === 'create-ticket') {
|
|
3557
3932
|
await protocolCreateTicket(args);
|
|
3558
3933
|
return;
|
package/package.json
CHANGED
|
@@ -132,6 +132,17 @@ ovld protocol prompt --agent claude-code --objectives-json '[{"objective":"Imple
|
|
|
132
132
|
ovld protocol add-objectives --ticket-id 1:899 --objectives-json '[{"objective":"Implement the API"},{"objective":"Add CLI docs"}]'
|
|
133
133
|
```
|
|
134
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
|
+
|
|
135
146
|
To inspect project resolution explicitly:
|
|
136
147
|
|
|
137
148
|
```bash
|
|
@@ -132,6 +132,17 @@ ovld protocol prompt --agent cursor --objectives-json '[{"objective":"Implement
|
|
|
132
132
|
ovld protocol add-objectives --ticket-id 1:899 --objectives-json '[{"objective":"Implement the API"},{"objective":"Add CLI docs"}]'
|
|
133
133
|
```
|
|
134
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
|
+
|
|
135
146
|
To inspect project resolution explicitly:
|
|
136
147
|
|
|
137
148
|
```bash
|
|
@@ -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:
|
|
@@ -132,6 +132,17 @@ ovld protocol prompt --agent codex --objectives-json '[{"objective":"Implement f
|
|
|
132
132
|
ovld protocol add-objectives --ticket-id 1:899 --objectives-json '[{"objective":"Implement the API"},{"objective":"Add CLI docs"}]'
|
|
133
133
|
```
|
|
134
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
|
+
|
|
135
146
|
To inspect project resolution explicitly:
|
|
136
147
|
|
|
137
148
|
```bash
|