overlord-cli 0.2605111338.0 → 0.2605120852.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.
@@ -7,7 +7,6 @@ import path from 'node:path';
7
7
 
8
8
  import { buildAuthHeaders, getAuthStatus, resolveAuth } from './credentials.mjs';
9
9
 
10
-
11
10
  /**
12
11
  * Parse simple CLI flags: --key value or --key=value
13
12
  * @param {string[]} args
@@ -361,8 +360,8 @@ async function resolveSnapshotContext(flags) {
361
360
 
362
361
  /**
363
362
  * Shallow-merge snapshot metadata: launch/env context first, then deliver payload
364
- * (payload wins on duplicate keys) so agents can add jj ids without losing workspace
365
- * paths from OVERLORD_SNAPSHOT_JSON.
363
+ * (payload wins on duplicate keys) so agents can add provenance details without
364
+ * losing launch context from OVERLORD_SNAPSHOT_JSON.
366
365
  * @param {Record<string, unknown> | null | undefined} fromEnvOrFlags
367
366
  * @param {Record<string, unknown> | null | undefined} fromPayload
368
367
  * @returns {Record<string, unknown> | null}
@@ -390,133 +389,207 @@ function runLocalCommand(command, args, options = {}) {
390
389
  });
391
390
  }
392
391
 
393
- function commandSucceeds(command, args, cwd) {
392
+ function resolveGitRepoRoot(workspacePath) {
394
393
  try {
395
- runLocalCommand(command, args, { cwd, timeoutMs: 15000 });
396
- return true;
394
+ return runLocalCommand('git', ['-C', workspacePath, 'rev-parse', '--show-toplevel'], {
395
+ cwd: workspacePath,
396
+ timeoutMs: 15000
397
+ }).trim();
397
398
  } catch {
398
- return false;
399
- }
400
- }
401
-
402
- function detectLocalCheckpointBackend(workspacePath, preference) {
403
- if (!workspacePath || !fs.existsSync(workspacePath)) return null;
404
- if (preference === 'jj') return 'jj';
405
- if (preference === 'git') return 'git';
406
- if (
407
- fs.existsSync(path.join(workspacePath, '.jj')) ||
408
- commandSucceeds('jj', ['--repository', workspacePath, 'root'], workspacePath)
409
- ) {
410
- return 'jj';
411
- }
412
- if (commandSucceeds('git', ['-C', workspacePath, 'rev-parse', '--show-toplevel'], workspacePath)) {
413
- return 'git';
399
+ return null;
414
400
  }
415
- return null;
416
401
  }
417
402
 
418
- function createLocalDeliveryCheckpoint(flags, snapshot) {
403
+ /**
404
+ * Snapshot the working tree to a hidden ref refs/overlord/checkpoints/<objectiveId>.
405
+ * Captures tracked + untracked + staged + unstaged in one commit-tree object.
406
+ * Idempotent: if the ref already exists, returns the existing sha.
407
+ * Returns null when not in a git repo or when no objectiveId is available.
408
+ */
409
+ function createLocalCheckpoint({ flags, kind = 'objective', objectiveId, snapshot = null }) {
419
410
  if (flags['skip-checkpoint']) return null;
420
411
 
421
- const preference = String(flags['checkpoint-backend'] ?? 'auto').trim();
422
- if (!['auto', 'jj', 'git'].includes(preference)) {
423
- throw new Error('--checkpoint-backend must be one of: auto, jj, git');
424
- }
425
-
426
412
  const workspacePath = path.resolve(
427
413
  String(snapshot?.workspacePath ?? process.env.OVERLORD_WORKSPACE_PATH ?? process.cwd())
428
414
  );
429
- if (!fs.existsSync(workspacePath)) return null;
430
-
431
- const backend = detectLocalCheckpointBackend(workspacePath, preference);
432
- if (!backend) return null;
433
-
434
- if (backend === 'jj') {
415
+ const repoRoot = fs.existsSync(workspacePath) ? resolveGitRepoRoot(workspacePath) : null;
416
+ if (!repoRoot) return null;
417
+
418
+ const resolvedObjectiveId =
419
+ objectiveId ?? snapshot?.objectiveId ?? process.env.OVERLORD_OBJECTIVE_ID ?? null;
420
+ if (!resolvedObjectiveId || !/^[A-Za-z0-9_-]+$/.test(String(resolvedObjectiveId))) {
421
+ // Without an objective id we cannot name the ref deterministically.
422
+ // Fall back to recording HEAD only so the protocol still has provenance.
435
423
  try {
436
- runLocalCommand('jj', ['--repository', workspacePath, 'util', 'snapshot'], {
437
- cwd: workspacePath
438
- });
439
- const commit = runLocalCommand(
440
- 'jj',
441
- ['--repository', workspacePath, 'log', '-r', '@', '-T', 'change_id ++ " " ++ commit_id'],
442
- { cwd: workspacePath }
443
- );
444
- const operation = runLocalCommand(
445
- 'jj',
446
- [
447
- '--repository',
448
- workspacePath,
449
- 'op',
450
- 'log',
451
- '--at-op=@',
452
- '--ignore-working-copy',
453
- '-n',
454
- '1',
455
- '-T',
456
- 'id'
457
- ],
458
- { cwd: workspacePath }
459
- );
460
- const diffStat = runLocalCommand('jj', ['--repository', workspacePath, 'diff', '--stat'], {
461
- cwd: workspacePath
462
- });
463
- const [jjChangeId = null, jjCommitId = null] = commit.trim().split(/\s+/, 2);
424
+ const gitCommitId = runLocalCommand('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], {
425
+ cwd: repoRoot
426
+ }).trim();
427
+ const diffStat = runLocalCommand('git', ['-C', repoRoot, 'diff', '--stat', 'HEAD'], {
428
+ cwd: repoRoot
429
+ }).trim();
464
430
  return {
465
- checkpoint: {
466
- diffStat: diffStat.trim() || null,
467
- kind: 'delivery'
468
- },
431
+ checkpoint: { diffStat: diffStat || null, kind },
469
432
  snapshot: {
470
- backend: 'jj',
471
- workspacePath,
472
- workspaceName: snapshot?.workspaceName ?? path.basename(workspacePath),
473
- jjChangeId,
474
- jjCommitId,
475
- jjOperationId: operation.trim() || null,
476
- diffStat: diffStat.trim() || null
433
+ gitCommitId: gitCommitId || null,
434
+ headSha: gitCommitId || null,
435
+ diffStat: diffStat || null
477
436
  }
478
437
  };
479
- } catch (error) {
480
- throw new Error(
481
- `Failed to create JJ delivery checkpoint in ${workspacePath}: ${
482
- error instanceof Error ? error.message : String(error)
483
- }\nRe-run with --skip-checkpoint only if you intentionally want to deliver without local JJ provenance.`
484
- );
438
+ } catch {
439
+ return null;
485
440
  }
486
441
  }
487
442
 
488
- if (preference === 'jj') {
489
- throw new Error(`JJ checkpoint requested, but ${workspacePath} is not a JJ repository.`);
490
- }
443
+ const ref = `refs/overlord/checkpoints/${resolvedObjectiveId}`;
444
+ const headSha = runLocalCommand('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], {
445
+ cwd: repoRoot
446
+ }).trim();
491
447
 
448
+ // If the ref already exists, return the existing snapshot.
449
+ let existingSha = '';
492
450
  try {
493
- const gitCommitId = runLocalCommand('git', ['-C', workspacePath, 'rev-parse', 'HEAD'], {
494
- cwd: workspacePath
451
+ existingSha = runLocalCommand('git', ['-C', repoRoot, 'rev-parse', '--verify', ref], {
452
+ cwd: repoRoot
495
453
  }).trim();
496
- const diffStat = runLocalCommand('git', ['-C', workspacePath, 'diff', '--stat', 'HEAD'], {
497
- cwd: workspacePath
454
+ } catch {
455
+ /* not present */
456
+ }
457
+
458
+ if (existingSha) {
459
+ let diffStat = '';
460
+ try {
461
+ diffStat = runLocalCommand('git', ['-C', repoRoot, 'diff', '--stat', `${existingSha}^!`], {
462
+ cwd: repoRoot
463
+ }).trim();
464
+ } catch {
465
+ /* ignore */
466
+ }
467
+ return {
468
+ checkpoint: { diffStat: diffStat || null, kind },
469
+ snapshot: {
470
+ gitCommitId: existingSha,
471
+ gitRefName: ref,
472
+ headSha,
473
+ objectiveId: String(resolvedObjectiveId),
474
+ diffStat: diffStat || null
475
+ }
476
+ };
477
+ }
478
+
479
+ // Build a tree from the current working copy without polluting the user's
480
+ // index by writing to a temporary index file.
481
+ const tempIndex = path.join(repoRoot, '.git', `overlord-snap-index-${Date.now()}`);
482
+ const env = { ...process.env, NO_COLOR: '1', GIT_INDEX_FILE: tempIndex };
483
+ try {
484
+ execFileSync('git', ['-C', repoRoot, 'read-tree', 'HEAD'], { cwd: repoRoot, env });
485
+ execFileSync('git', ['-C', repoRoot, 'add', '-A'], { cwd: repoRoot, env });
486
+ const tree = execFileSync('git', ['-C', repoRoot, 'write-tree'], {
487
+ cwd: repoRoot,
488
+ env,
489
+ encoding: 'utf8'
498
490
  }).trim();
491
+ if (!tree) throw new Error('git write-tree produced no output.');
492
+ const commit = execFileSync(
493
+ 'git',
494
+ [
495
+ '-C',
496
+ repoRoot,
497
+ 'commit-tree',
498
+ tree,
499
+ '-p',
500
+ headSha,
501
+ '-m',
502
+ `overlord checkpoint ${resolvedObjectiveId}`
503
+ ],
504
+ { cwd: repoRoot, env: { ...process.env, NO_COLOR: '1' }, encoding: 'utf8' }
505
+ ).trim();
506
+ if (!commit) throw new Error('git commit-tree produced no output.');
507
+ execFileSync('git', ['-C', repoRoot, 'update-ref', ref, commit], {
508
+ cwd: repoRoot
509
+ });
510
+ let diffStat = '';
511
+ try {
512
+ diffStat = runLocalCommand('git', ['-C', repoRoot, 'diff', '--stat', `${commit}^!`], {
513
+ cwd: repoRoot
514
+ }).trim();
515
+ } catch {
516
+ /* ignore */
517
+ }
499
518
  return {
500
- checkpoint: {
501
- diffStat: diffStat || null,
502
- kind: 'delivery'
503
- },
519
+ checkpoint: { diffStat: diffStat || null, kind },
504
520
  snapshot: {
505
- backend: 'git',
506
- workspacePath,
507
- workspaceName: snapshot?.workspaceName ?? path.basename(workspacePath),
508
- gitCommitId: gitCommitId || null,
521
+ gitCommitId: commit,
522
+ gitRefName: ref,
523
+ headSha,
524
+ objectiveId: String(resolvedObjectiveId),
509
525
  diffStat: diffStat || null
510
526
  }
511
527
  };
512
- } catch {
513
- if (preference === 'git') {
514
- throw new Error(`Git checkpoint requested, but metadata could not be read in ${workspacePath}.`);
528
+ } catch (error) {
529
+ throw new Error(
530
+ `Failed to create git checkpoint in ${repoRoot}: ${
531
+ error instanceof Error ? error.message : String(error)
532
+ }\nRe-run attach with --skip-checkpoint to proceed without local provenance.`
533
+ );
534
+ } finally {
535
+ try {
536
+ fs.unlinkSync(tempIndex);
537
+ } catch {
538
+ /* ignore */
515
539
  }
516
- return null;
517
540
  }
518
541
  }
519
542
 
543
+ async function createAndRecordPendingCheckpoints({
544
+ attachData,
545
+ bearerToken,
546
+ flags,
547
+ localSecret,
548
+ organizationId,
549
+ platformUrl,
550
+ ticketId,
551
+ timeoutMs
552
+ }) {
553
+ const pendingObjectiveIds = Array.isArray(attachData.pendingCheckpointObjectiveIds)
554
+ ? attachData.pendingCheckpointObjectiveIds.filter(id => typeof id === 'string' && id.trim())
555
+ : [];
556
+ const sessionKey = attachData.session?.sessionKey;
557
+ if (!sessionKey || pendingObjectiveIds.length === 0) return [];
558
+
559
+ const recorded = [];
560
+ for (const objectiveId of pendingObjectiveIds) {
561
+ const checkpointResult = createLocalCheckpoint({
562
+ flags,
563
+ kind: 'objective',
564
+ objectiveId: objectiveId.trim()
565
+ });
566
+ if (!checkpointResult?.snapshot) {
567
+ process.stderr.write(
568
+ `[protocol] Skipped checkpoint for objective ${objectiveId}: no git repository found at ${process.cwd()}.\n`
569
+ );
570
+ continue;
571
+ }
572
+
573
+ await apiPost(
574
+ platformUrl,
575
+ bearerToken,
576
+ localSecret,
577
+ organizationId,
578
+ '/api/protocol/update',
579
+ {
580
+ sessionKey,
581
+ ticketId,
582
+ summary: `Created local git checkpoint for objective ${objectiveId}.`,
583
+ phase: 'execute',
584
+ snapshot: checkpointResult.snapshot
585
+ },
586
+ timeoutMs
587
+ );
588
+ recorded.push(objectiveId);
589
+ }
590
+ return recorded;
591
+ }
592
+
520
593
  function normalizeRepoRelativeFilePath(filePath, repoRoot) {
521
594
  if (typeof filePath !== 'string') return null;
522
595
 
@@ -692,7 +765,8 @@ function resolveExternalSessionId(flags) {
692
765
  async function protocolAttach(args) {
693
766
  const flags = parseFlags(args);
694
767
  const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
695
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
768
+ const { platformUrl, bearerToken, localSecret, organizationId } =
769
+ await resolveProtocolAuthForFlags(flags, ticketId);
696
770
  const timeoutMs = resolveTimeout(flags);
697
771
 
698
772
  const externalSessionId = resolveExternalSessionId(flags);
@@ -715,6 +789,20 @@ async function protocolAttach(args) {
715
789
  timeoutMs
716
790
  );
717
791
 
792
+ const recordedCheckpointObjectiveIds = await createAndRecordPendingCheckpoints({
793
+ attachData: data,
794
+ bearerToken,
795
+ flags,
796
+ localSecret,
797
+ organizationId,
798
+ platformUrl,
799
+ ticketId,
800
+ timeoutMs
801
+ });
802
+ if (recordedCheckpointObjectiveIds.length > 0) {
803
+ data.recordedCheckpointObjectiveIds = recordedCheckpointObjectiveIds;
804
+ }
805
+
718
806
  const sessionKey = data.session?.sessionKey;
719
807
  console.log(JSON.stringify(data, null, 2));
720
808
 
@@ -738,7 +826,8 @@ async function protocolUpdate(args) {
738
826
  ? readTextFile(String(flags['summary-file']), '--summary-file')
739
827
  : requireFlag(flags, 'summary', undefined);
740
828
 
741
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
829
+ const { platformUrl, bearerToken, localSecret, organizationId } =
830
+ await resolveProtocolAuthForFlags(flags, ticketId);
742
831
  const timeoutMs = resolveTimeout(flags);
743
832
  const changeRationales = await resolveChangeRationales(flags);
744
833
  const externalSessionId = resolveExternalSessionId(flags);
@@ -795,7 +884,8 @@ async function protocolRecordChangeRationales(args) {
795
884
  );
796
885
  }
797
886
 
798
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
887
+ const { platformUrl, bearerToken, localSecret, organizationId } =
888
+ await resolveProtocolAuthForFlags(flags, ticketId);
799
889
  const timeoutMs = resolveTimeout(flags);
800
890
  const snapshot = await resolveSnapshotContext(flags);
801
891
 
@@ -837,7 +927,8 @@ async function protocolAsk(args) {
837
927
  ? readTextFile(String(flags['question-file']), '--question-file')
838
928
  : requireFlag(flags, 'question', undefined);
839
929
 
840
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
930
+ const { platformUrl, bearerToken, localSecret, organizationId } =
931
+ await resolveProtocolAuthForFlags(flags, ticketId);
841
932
  const timeoutMs = resolveTimeout(flags);
842
933
 
843
934
  const body = {
@@ -873,7 +964,8 @@ async function protocolPermissionRequest(args) {
873
964
  ? await readJsonFileOrStdin(String(flags['payload-file']), '--payload-file')
874
965
  : {};
875
966
 
876
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
967
+ const { platformUrl, bearerToken, localSecret, organizationId } =
968
+ await resolveProtocolAuthForFlags(flags, ticketId);
877
969
  const timeoutMs = resolveTimeout(flags);
878
970
 
879
971
  const data = await apiPost(
@@ -898,7 +990,8 @@ async function protocolReadContext(args) {
898
990
  if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
899
991
  if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
900
992
 
901
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
993
+ const { platformUrl, bearerToken, localSecret, organizationId } =
994
+ await resolveProtocolAuthForFlags(flags, ticketId);
902
995
  const timeoutMs = resolveTimeout(flags);
903
996
 
904
997
  const body = {
@@ -942,7 +1035,8 @@ async function protocolWriteContext(args) {
942
1035
  value = String(flags.value);
943
1036
  }
944
1037
 
945
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1038
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1039
+ await resolveProtocolAuthForFlags(flags, ticketId);
946
1040
  const timeoutMs = resolveTimeout(flags);
947
1041
 
948
1042
  const body = {
@@ -989,7 +1083,8 @@ async function protocolDeliver(args) {
989
1083
  ? readTextFile(String(flags['summary-file']), '--summary-file')
990
1084
  : requireFlag(flags, 'summary', undefined));
991
1085
 
992
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1086
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1087
+ await resolveProtocolAuthForFlags(flags, ticketId);
993
1088
  const timeoutMs = resolveTimeout(flags);
994
1089
 
995
1090
  let artifacts = deliverPayload?.artifacts ?? [];
@@ -1011,14 +1106,10 @@ async function protocolDeliver(args) {
1011
1106
 
1012
1107
  const changeRationales =
1013
1108
  deliverPayload?.changeRationales ?? (await resolveChangeRationales(flags));
1014
- let snapshot = mergeDeliverSnapshot(
1109
+ const snapshot = mergeDeliverSnapshot(
1015
1110
  await resolveSnapshotContext(flags),
1016
1111
  deliverPayload?.snapshot ?? null
1017
1112
  );
1018
- const checkpointResult = createLocalDeliveryCheckpoint(flags, snapshot);
1019
- if (checkpointResult) {
1020
- snapshot = mergeDeliverSnapshot(snapshot, checkpointResult.snapshot);
1021
- }
1022
1113
  validateDeliverFileChanges(flags, changeRationales);
1023
1114
 
1024
1115
  const body = {
@@ -1026,9 +1117,6 @@ async function protocolDeliver(args) {
1026
1117
  ticketId,
1027
1118
  summary,
1028
1119
  artifacts,
1029
- ...(checkpointResult?.checkpoint
1030
- ? { checkpoint: { ...checkpointResult.checkpoint, summary } }
1031
- : {}),
1032
1120
  ...(snapshot ? { snapshot } : {}),
1033
1121
  ...(changeRationales.length > 0 ? { changeRationales } : {})
1034
1122
  };
@@ -1055,7 +1143,8 @@ async function protocolAttachmentList(args) {
1055
1143
  if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
1056
1144
  if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
1057
1145
 
1058
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1146
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1147
+ await resolveProtocolAuthForFlags(flags, ticketId);
1059
1148
  const timeoutMs = resolveTimeout(flags);
1060
1149
 
1061
1150
  const body = {
@@ -1084,7 +1173,8 @@ async function protocolAttachmentPrepareUpload(args) {
1084
1173
  const objectiveId = requireFlag(flags, 'objective-id', undefined);
1085
1174
  const fileName = requireFlag(flags, 'file-name', undefined);
1086
1175
 
1087
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1176
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1177
+ await resolveProtocolAuthForFlags(flags, ticketId);
1088
1178
  const timeoutMs = resolveTimeout(flags);
1089
1179
 
1090
1180
  const body = {
@@ -1121,7 +1211,8 @@ async function protocolAttachmentFinalizeUpload(args) {
1121
1211
  const storagePath = requireFlag(flags, 'storage-path', undefined);
1122
1212
  const label = requireFlag(flags, 'label', undefined);
1123
1213
 
1124
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1214
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1215
+ await resolveProtocolAuthForFlags(flags, ticketId);
1125
1216
  const timeoutMs = resolveTimeout(flags);
1126
1217
 
1127
1218
  const body = {
@@ -1161,7 +1252,8 @@ async function protocolAttachmentGetDownloadUrl(args) {
1161
1252
  throw new Error('--objective-id is required when using --storage-path');
1162
1253
  }
1163
1254
 
1164
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1255
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1256
+ await resolveProtocolAuthForFlags(flags, ticketId);
1165
1257
  const timeoutMs = resolveTimeout(flags);
1166
1258
 
1167
1259
  const body = {
@@ -1202,7 +1294,8 @@ async function protocolAttachmentUploadFile(args) {
1202
1294
  const fileStats = await stat(filePath);
1203
1295
  const fileBytes = await readFile(filePath);
1204
1296
 
1205
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1297
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1298
+ await resolveProtocolAuthForFlags(flags, ticketId);
1206
1299
  const timeoutMs = resolveTimeout(flags);
1207
1300
 
1208
1301
  const metadata = flags['metadata-json']
@@ -1293,7 +1386,8 @@ async function protocolDiscoverProject(args) {
1293
1386
  async function protocolConnect(args) {
1294
1387
  const flags = parseFlags(args);
1295
1388
  const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
1296
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1389
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1390
+ await resolveProtocolAuthForFlags(flags, ticketId);
1297
1391
  const timeoutMs = resolveTimeout(flags);
1298
1392
 
1299
1393
  const body = {
@@ -1328,7 +1422,8 @@ async function protocolConnect(args) {
1328
1422
  async function protocolLoadContext(args) {
1329
1423
  const flags = parseFlags(args);
1330
1424
  const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
1331
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1425
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1426
+ await resolveProtocolAuthForFlags(flags, ticketId);
1332
1427
  const timeoutMs = resolveTimeout(flags);
1333
1428
 
1334
1429
  const body = { ticketId };
@@ -1421,7 +1516,8 @@ async function protocolCreateTicket(args) {
1421
1516
  const flags = parseFlags(args);
1422
1517
  const { sessionKey, ticketId } = resolveSessionFlags(flags);
1423
1518
  const objective = requireFlag(flags, 'objective', undefined);
1424
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1519
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1520
+ await resolveProtocolAuthForFlags(flags, ticketId);
1425
1521
  const timeoutMs = resolveTimeout(flags);
1426
1522
  const agentIdentifier = resolveProtocolAgentIdentifier(flags);
1427
1523
  const modelIdentifier = resolveProtocolModelIdentifier(flags);
@@ -1664,10 +1760,12 @@ attach:
1664
1760
  --method <connectionMethod>
1665
1761
  --external-session-id <id|null> Store the native agent thread/session id, or clear it with null
1666
1762
  --metadata-json <json> Extra session metadata object
1763
+ --skip-checkpoint Bypass automatic objective-start git checkpoint creation
1667
1764
  Returns:
1668
1765
  Full JSON including session.sessionKey, ticket, history, artifacts, sharedState, and promptContext
1669
1766
  Notes:
1670
1767
  If --external-session-id is omitted, the CLI may auto-detect Codex or Claude session ids
1768
+ Attach creates and records missing local git checkpoints for executing objectives before work starts.
1671
1769
 
1672
1770
  connect:
1673
1771
  Purpose:
@@ -1789,13 +1887,10 @@ deliver:
1789
1887
  --artifacts-file <path|->
1790
1888
  --change-rationales-json <json>
1791
1889
  --change-rationales-file <path|->
1792
- --checkpoint-backend <auto|jj|git> Backend for the local delivery checkpoint (default: auto)
1793
- --skip-checkpoint Bypass automatic local checkpoint creation
1794
1890
  --skip-file-change-check Bypass local git vs changeRationales validation
1795
1891
  Notes:
1796
1892
  Use --payload-file - to read the full delivery JSON from stdin without creating a scratch file.
1797
1893
  Do not combine --payload-file with --artifacts-json/--artifacts-file or change-rationale flags.
1798
- Deliver creates a local checkpoint before the API request when the current workspace is JJ- or Git-managed.
1799
1894
  In a git workspace, deliver validates that changed files are represented by changeRationales unless skipped.
1800
1895
 
1801
1896
  prompt:
@@ -1932,7 +2027,6 @@ Examples:
1932
2027
  ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --summary "Done" --artifacts-file ./artifacts.json
1933
2028
  ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --payload-file ./deliver.json
1934
2029
  ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --payload-file -
1935
- ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --summary "Done" --checkpoint-backend jj
1936
2030
  ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --summary "Done" --skip-file-change-check
1937
2031
  ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --summary "Done" --timeout 60000
1938
2032
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "0.2605111338.0",
3
+ "version": "0.2605120852.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "homepage": "https://www.ovld.ai/docs/",
6
6
  "type": "module",
@@ -152,6 +152,7 @@ The `attach` and `load-context` responses already include `attachments` and `obj
152
152
  ## Defaults And Notes
153
153
 
154
154
  - API requires `agentIdentifier` and `connectionMethod` on attach/connect/prompt. The CLI defaults them to `claude-code`/`cli`; the MCP tool defaults to `mcp`. Override with `--agent` / `--method` when calling from a different runtime.
155
+ - Hosted Overlord MCP (`/functions/v1/mcp`) uses the same canonical tool names as any local MCP shim that shells into `ovld protocol` (`attach`, `update`, `deliver`, …). Hosted calls use camelCase JSON keys (`ticketId`, `sessionKey`) matching `POST /api/protocol/*` bodies; the local shim uses snake_case keys mapped to CLI flags (`ticket_id`, `session_key`).
155
156
  - `permission-request` is invoked by the Claude Code permission hook installed by the bundle. Agents do not normally call it directly.
156
157
  - `record_change_rationales` (MCP) and `ovld protocol record-change-rationales` (CLI) both write to the same `file_changes` table. The dedicated CLI route is `POST /api/protocol/record-change-rationales`.
157
158
  - Objective attachment tools follow the `<verb>_<noun>` MCP naming: `list_attachments`, `prepare_attachment_upload`, `finalize_attachment_upload`, `get_attachment_download_url`, `upload_attachment_file`. CLI commands use `attachment-*` and require `--objective-id` for upload/finalize.
@@ -169,4 +170,4 @@ The `attach` and `load-context` responses already include `attachments` and `obj
169
170
  - Do not add or commit changes unless the user explicitly asks you to commit.
170
171
  - Delivery is the concluding step. After delivering, stop unless the user follows up or the ticket is reopened.
171
172
 
172
- <!-- version: 0.4.5 -->
173
+ <!-- version: 0.4.7 -->
@@ -69,7 +69,7 @@ process.stdin.on('data', async chunk => {
69
69
  result: {
70
70
  protocolVersion: PROTOCOL_VERSION,
71
71
  capabilities: { tools: { listChanged: false } },
72
- serverInfo: { name: 'overlord-cursor', version: '0.1.0' }
72
+ serverInfo: { name: 'overlord-cursor', version: '0.1.2' }
73
73
  }
74
74
  });
75
75
  continue;
@@ -81,21 +81,26 @@ process.stdin.on('data', async chunk => {
81
81
  result: {
82
82
  tools: [
83
83
  {
84
- name: 'attach_ticket',
84
+ name: 'attach',
85
85
  description: 'Attach to an Overlord ticket.',
86
86
  inputSchema: { type: 'object', properties: { ticket_id: { type: 'string' } }, required: ['ticket_id'] }
87
87
  },
88
88
  {
89
- name: 'post_update',
89
+ name: 'update',
90
90
  description: 'Post a progress update.',
91
91
  inputSchema: {
92
92
  type: 'object',
93
- properties: { session_key: { type: 'string' }, ticket_id: { type: 'string' }, summary: { type: 'string' } },
93
+ properties: {
94
+ session_key: { type: 'string' },
95
+ ticket_id: { type: 'string' },
96
+ summary: { type: 'string' },
97
+ phase: { type: 'string' }
98
+ },
94
99
  required: ['session_key', 'ticket_id', 'summary']
95
100
  }
96
101
  },
97
102
  {
98
- name: 'deliver_ticket',
103
+ name: 'deliver',
99
104
  description: 'Deliver completed work.',
100
105
  inputSchema: {
101
106
  type: 'object',
@@ -111,9 +116,9 @@ process.stdin.on('data', async chunk => {
111
116
  if (message.method === 'tools/call') {
112
117
  const toolName = message.params?.name;
113
118
  const args = message.params?.arguments ?? {};
114
- if (toolName === 'attach_ticket') {
119
+ if (toolName === 'attach') {
115
120
  send({ jsonrpc: '2.0', id: message.id, result: await runProtocol('attach', { 'ticket-id': args.ticket_id }) });
116
- } else if (toolName === 'post_update') {
121
+ } else if (toolName === 'update') {
117
122
  send({
118
123
  jsonrpc: '2.0',
119
124
  id: message.id,
@@ -121,10 +126,10 @@ process.stdin.on('data', async chunk => {
121
126
  'session-key': args.session_key,
122
127
  'ticket-id': args.ticket_id,
123
128
  summary: args.summary,
124
- phase: 'execute'
129
+ phase: args.phase && String(args.phase).trim() ? String(args.phase).trim() : 'execute'
125
130
  })
126
131
  });
127
- } else if (toolName === 'deliver_ticket') {
132
+ } else if (toolName === 'deliver') {
128
133
  send({
129
134
  jsonrpc: '2.0',
130
135
  id: message.id,
@@ -137,6 +137,7 @@ Objective attachment uploads also expose two-step variants — `attachment-prepa
137
137
  ## Defaults And Notes
138
138
 
139
139
  - The Overlord API requires `agentIdentifier` and `connectionMethod` on attach/connect/prompt, but the CLI defaults them to `cursor`/`cli` (override with `--agent` / `--method`). The MCP tools default to `mcp`.
140
+ - The Cursor local MCP stub exposes `attach`, `update`, and `deliver` only (same names as the hosted Overlord MCP core set).
140
141
  - `permission-request` is invoked by the installed permission hook; agents normally do not call it directly.
141
142
  - The `record_change_rationales` MCP tool and `ovld protocol record-change-rationales` CLI both write to the same `file_changes` table; pick whichever fits your runtime.
142
143
 
@@ -151,4 +152,4 @@ Objective attachment uploads also expose two-step variants — `attachment-prepa
151
152
  - If you must run `ovld auth login`, always include `--organization-id <id>` — use the organization ID from the ticket prompt context to select the organization non-interactively and avoid a blocking TTY prompt.
152
153
  - Delivery is the concluding step. After delivering, stop unless the user follows up or the ticket is reopened.
153
154
 
154
- <!-- version: 0.4.5 -->
155
+ <!-- version: 0.4.7 -->
@@ -22,9 +22,9 @@ personal marketplace entry at `~/.agents/plugins/marketplace.json`.
22
22
 
23
23
  - Project resolution: `discover_project`
24
24
  - Ticket search: `search_tickets` — keyword search + status/filter (direct API call)
25
- - Ticket session flow: `attach_ticket`, `connect_ticket`, `load_ticket_context`, `spawn_ticket`
26
- - Progress and review flow: `post_update`, `record_change_rationales`, `ask_blocking_question`, `deliver_ticket`
27
- - Shared context: `read_shared_context`, `write_shared_context`
25
+ - Ticket session flow: `attach`, `connect`, `load_ticket_context`, `prompt`
26
+ - Progress and review flow: `update`, `record_change_rationales`, `ask`, `deliver`
27
+ - Shared context: `read_context`, `write_context`
28
28
  - Objective attachments: `list_attachments`, `prepare_attachment_upload`, `finalize_attachment_upload`, `get_attachment_download_url`, `upload_attachment_file`
29
29
 
30
30
  The MCP server shells into the installed `ovld` binary so the plugin stays aligned with the shipped CLI behavior instead of depending on this repository checkout.
@@ -44,7 +44,7 @@ const tools = [
44
44
  subcommand: 'discover-project'
45
45
  },
46
46
  {
47
- name: 'attach_ticket',
47
+ name: 'attach',
48
48
  description: 'Attach an agent session to an existing Overlord ticket and return the working context.',
49
49
  inputSchema: {
50
50
  type: 'object',
@@ -52,7 +52,11 @@ const tools = [
52
52
  ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
53
53
  agent: { type: 'string' },
54
54
  method: { type: 'string' },
55
- external_session_id: { type: ['string', 'null'] }
55
+ external_session_id: { type: ['string', 'null'] },
56
+ metadata: {
57
+ type: 'object',
58
+ description: 'Optional extra metadata merged into the attach request (same as --metadata-json on the CLI).'
59
+ }
56
60
  },
57
61
  required: ['ticket_id']
58
62
  },
@@ -60,36 +64,47 @@ const tools = [
60
64
  'ticket-id': args.ticket_id,
61
65
  agent: args.agent,
62
66
  method: args.method,
63
- 'external-session-id': args.external_session_id
67
+ 'external-session-id': args.external_session_id,
68
+ ...(args.metadata && typeof args.metadata === 'object' && !Array.isArray(args.metadata)
69
+ ? { 'metadata-json': args.metadata }
70
+ : {})
64
71
  }),
65
72
  subcommand: 'attach'
66
73
  },
67
74
  {
68
- name: 'connect_ticket',
75
+ name: 'connect',
69
76
  description: 'Create a lightweight Overlord session without loading the full ticket context.',
70
77
  inputSchema: {
71
78
  type: 'object',
72
79
  properties: {
73
80
  ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
74
81
  agent: { type: 'string' },
75
- method: { type: 'string' }
82
+ method: { type: 'string' },
83
+ metadata: {
84
+ type: 'object',
85
+ description: 'Optional extra metadata merged into the connect request (same as --metadata-json on the CLI).'
86
+ }
76
87
  },
77
88
  required: ['ticket_id']
78
89
  },
79
90
  toCliFlags: args => ({
80
91
  'ticket-id': args.ticket_id,
81
92
  agent: args.agent,
82
- method: args.method
93
+ method: args.method,
94
+ ...(args.metadata && typeof args.metadata === 'object' && !Array.isArray(args.metadata)
95
+ ? { 'metadata-json': args.metadata }
96
+ : {})
83
97
  }),
84
98
  subcommand: 'connect'
85
99
  },
86
100
  {
87
101
  name: 'load_ticket_context',
88
- description: 'Fetch Overlord ticket context without creating a session.',
102
+ description:
103
+ 'Fetch Overlord ticket context without creating a session (maps to ovld protocol load-context; not the same as read_context).',
89
104
  inputSchema: {
90
105
  type: 'object',
91
106
  properties: {
92
- ticket_id: { type: 'string' }
107
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' }
93
108
  },
94
109
  required: ['ticket_id']
95
110
  },
@@ -99,8 +114,8 @@ const tools = [
99
114
  subcommand: 'load-context'
100
115
  },
101
116
  {
102
- name: 'prompt_ticket',
103
- description: 'Create a ticket and attach to it immediately.',
117
+ name: 'prompt',
118
+ description: 'Create a ticket and attach to it immediately (ovld protocol prompt).',
104
119
  inputSchema: {
105
120
  type: 'object',
106
121
  properties: {
@@ -109,6 +124,7 @@ const tools = [
109
124
  priority: { type: 'string', enum: ['low', 'medium', 'high', 'urgent'] },
110
125
  project_id: { type: 'string' },
111
126
  working_directory: { type: 'string' },
127
+ personal: { type: 'boolean', description: 'Create without assigning a project (private ticket).' },
112
128
  acceptance_criteria: { type: 'string' },
113
129
  available_tools: { type: 'string' },
114
130
  execution_target: { type: 'string', enum: ['agent', 'human'] },
@@ -126,6 +142,7 @@ const tools = [
126
142
  priority: args.priority,
127
143
  'project-id': args.project_id,
128
144
  'working-directory': args.working_directory,
145
+ personal: args.personal === true ? true : undefined,
129
146
  'acceptance-criteria': args.acceptance_criteria,
130
147
  'available-tools': args.available_tools,
131
148
  'execution-target': args.execution_target,
@@ -138,7 +155,7 @@ const tools = [
138
155
  subcommand: 'prompt'
139
156
  },
140
157
  {
141
- name: 'post_update',
158
+ name: 'update',
142
159
  description: 'Post an Overlord progress update or activity event.',
143
160
  inputSchema: {
144
161
  type: 'object',
@@ -192,7 +209,7 @@ const tools = [
192
209
  subcommand: 'record-change-rationales'
193
210
  },
194
211
  {
195
- name: 'ask_blocking_question',
212
+ name: 'ask',
196
213
  description: 'Send a blocking question to the human reviewer or PM.',
197
214
  inputSchema: {
198
215
  type: 'object',
@@ -215,7 +232,7 @@ const tools = [
215
232
  subcommand: 'ask'
216
233
  },
217
234
  {
218
- name: 'read_shared_context',
235
+ name: 'read_context',
219
236
  description: 'Read persistent shared context entries for a ticket.',
220
237
  inputSchema: {
221
238
  type: 'object',
@@ -236,7 +253,7 @@ const tools = [
236
253
  subcommand: 'read-context'
237
254
  },
238
255
  {
239
- name: 'write_shared_context',
256
+ name: 'write_context',
240
257
  description: 'Write a persistent shared context entry for future Overlord sessions.',
241
258
  inputSchema: {
242
259
  type: 'object',
@@ -259,9 +276,9 @@ const tools = [
259
276
  subcommand: 'write-context'
260
277
  },
261
278
  {
262
- name: 'deliver_ticket',
279
+ name: 'deliver',
263
280
  description:
264
- 'Deliver final work back into Overlord with summary, artifacts, and change rationales. Large payloads are streamed to the CLI through stdin, so this tool does not create delivery scratch files.',
281
+ 'Deliver final work back into Overlord with summary, optional artifacts, and optional change rationales (matches POST /api/protocol/deliver). Large payloads are streamed to the CLI through stdin, so this tool does not create delivery scratch files.',
265
282
  inputSchema: {
266
283
  type: 'object',
267
284
  properties: {
@@ -280,11 +297,6 @@ const tools = [
280
297
  description:
281
298
  'Optional checkpoint metadata. The local CLI creates this automatically for JJ/Git workspaces during deliver.'
282
299
  },
283
- checkpoint_backend: {
284
- type: 'string',
285
- enum: ['auto', 'jj', 'git'],
286
- description: 'Backend for automatic local checkpointing when routed through the CLI.'
287
- },
288
300
  skip_checkpoint: { type: 'boolean' },
289
301
  skip_file_change_check: { type: 'boolean' }
290
302
  },
@@ -294,7 +306,6 @@ const tools = [
294
306
  'session-key': args.session_key,
295
307
  'ticket-id': args.ticket_id,
296
308
  'payload-file': '-',
297
- 'checkpoint-backend': args.checkpoint_backend,
298
309
  'skip-checkpoint': args.skip_checkpoint,
299
310
  'skip-file-change-check': args.skip_file_change_check
300
311
  }),
@@ -466,7 +477,7 @@ const searchTicketsTool = {
466
477
  },
467
478
  limit: {
468
479
  type: 'number',
469
- description: 'Maximum number of results to return (1–20, default 8).'
480
+ description: 'Maximum number of results to return (1–50, default 8).'
470
481
  }
471
482
  }
472
483
  },
@@ -479,10 +490,9 @@ const searchTicketsTool = {
479
490
  subcommand: 'search-tickets'
480
491
  };
481
492
 
482
- const toolMap = new Map([
483
- ...tools.map(tool => [tool.name, tool]),
484
- [searchTicketsTool.name, searchTicketsTool]
485
- ]);
493
+ const allListedTools = [...tools, searchTicketsTool];
494
+
495
+ const toolMap = new Map(allListedTools.map(tool => [tool.name, tool]));
486
496
  let buffer = Buffer.alloc(0);
487
497
 
488
498
  function serializeMessage(message) {
@@ -645,10 +655,10 @@ async function handleRequest(message) {
645
655
  },
646
656
  serverInfo: {
647
657
  name: 'overlord',
648
- version: '0.1.2'
658
+ version: '0.1.4'
649
659
  },
650
660
  instructions:
651
- 'Use these tools to drive Overlord ticket workflows through the installed ovld CLI. Most operations expect a session key from attach or connect.'
661
+ 'Use these tools to drive Overlord ticket workflows through the installed ovld CLI. Tool names match the hosted Overlord MCP server (attach, update, deliver, …). Most operations expect a session key from attach or connect.'
652
662
  });
653
663
  return;
654
664
  }
@@ -660,7 +670,7 @@ async function handleRequest(message) {
660
670
 
661
671
  if (method === 'tools/list') {
662
672
  success(id, {
663
- tools: [...tools, searchTicketsTool].map(tool => ({
673
+ tools: allListedTools.map(tool => ({
664
674
  name: tool.name,
665
675
  description: tool.description,
666
676
  inputSchema: tool.inputSchema
@@ -138,7 +138,8 @@ The `attach` and `load-context` responses already include `attachments` and `obj
138
138
 
139
139
  ## Defaults And Notes
140
140
 
141
- - API requires `agentIdentifier` and `connectionMethod` on attach/connect/prompt. The CLI defaults them to `codex`/`cli`; the MCP tool defaults to `mcp`. Override with `--agent` / `--method` when calling from a different runtime.
141
+ - API requires `agentIdentifier` and `connectionMethod` on attach/connect/prompt. The CLI defaults them to `codex`/`cli`; the hosted MCP defaults to `mcp`. Override with `--agent` / `--method` when calling from a different runtime.
142
+ - Hosted MCP and the local `overlord-mcp.mjs` shim use the same **tool names** (\`attach\`, \`update\`, \`deliver\`, …). The shim uses **snake_case** JSON keys (\`ticket_id\`, \`session_key\`) that map to CLI flags; hosted MCP uses **camelCase** (\`ticketId\`, \`sessionKey\`) matching REST bodies.
142
143
  - `permission-request` is invoked by the local Codex plugin's permission rules; agents do not normally call it directly.
143
144
  - `record_change_rationales` (MCP) and `ovld protocol record-change-rationales` (CLI) both write to the `file_changes` table; the dedicated route is `POST /api/protocol/record-change-rationales`.
144
145
  - Objective attachment MCP tools use `<verb>_<noun>` names — `list_attachments`, `prepare_attachment_upload`, `finalize_attachment_upload`, `get_attachment_download_url`, `upload_attachment_file`. CLI commands use `attachment-*` and require `--objective-id` for upload/finalize.
@@ -154,4 +155,4 @@ The `attach` and `load-context` responses already include `attachments` and `obj
154
155
  - If a protocol or MCP call fails with auth/session errors, run `ovld auth repair` yourself before asking the user to log in again or proceed without Overlord updates.
155
156
  - If you must run `ovld auth login`, always include `--organization-id <id>` — use the organization ID from the ticket prompt context to select the organization non-interactively and avoid a blocking TTY prompt.
156
157
 
157
- <!-- version: 0.4.5 -->
158
+ <!-- version: 0.4.7 -->