overlord-cli 0.2605111118.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.
package/README.md CHANGED
@@ -119,7 +119,7 @@ Agents can find docs here: https://www.ovld.ai/docs/for-agents
119
119
  - `permission-request` - notify Overlord that the agent is requesting tool permission
120
120
  - `read-context` - read shared persistent context for this ticket
121
121
  - `write-context` - write shared persistent context for future sessions
122
- - `deliver` - finish work, send artifacts, and move the ticket to review
122
+ - `deliver` - create a local JJ/Git checkpoint when available, send artifacts/rationales, and move the ticket to review
123
123
  - `attachment-prepare-upload` - get a signed upload URL for an objective attachment
124
124
  - `attachment-finalize-upload` - finalize an uploaded objective attachment row after storage upload
125
125
  - `attachment-download-url` - get a signed download URL for an existing objective attachment
@@ -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
@@ -359,6 +358,238 @@ async function resolveSnapshotContext(flags) {
359
358
  return null;
360
359
  }
361
360
 
361
+ /**
362
+ * Shallow-merge snapshot metadata: launch/env context first, then deliver payload
363
+ * (payload wins on duplicate keys) so agents can add provenance details without
364
+ * losing launch context from OVERLORD_SNAPSHOT_JSON.
365
+ * @param {Record<string, unknown> | null | undefined} fromEnvOrFlags
366
+ * @param {Record<string, unknown> | null | undefined} fromPayload
367
+ * @returns {Record<string, unknown> | null}
368
+ */
369
+ function mergeDeliverSnapshot(fromEnvOrFlags, fromPayload) {
370
+ const base =
371
+ fromEnvOrFlags && typeof fromEnvOrFlags === 'object' && !Array.isArray(fromEnvOrFlags)
372
+ ? { ...fromEnvOrFlags }
373
+ : {};
374
+ const overlay =
375
+ fromPayload && typeof fromPayload === 'object' && !Array.isArray(fromPayload)
376
+ ? { ...fromPayload }
377
+ : {};
378
+ const merged = { ...base, ...overlay };
379
+ return Object.keys(merged).length > 0 ? merged : null;
380
+ }
381
+
382
+ function runLocalCommand(command, args, options = {}) {
383
+ return execFileSync(command, args, {
384
+ cwd: options.cwd,
385
+ encoding: 'utf8',
386
+ stdio: ['ignore', 'pipe', 'pipe'],
387
+ timeout: options.timeoutMs ?? 60000,
388
+ maxBuffer: 10 * 1024 * 1024
389
+ });
390
+ }
391
+
392
+ function resolveGitRepoRoot(workspacePath) {
393
+ try {
394
+ return runLocalCommand('git', ['-C', workspacePath, 'rev-parse', '--show-toplevel'], {
395
+ cwd: workspacePath,
396
+ timeoutMs: 15000
397
+ }).trim();
398
+ } catch {
399
+ return null;
400
+ }
401
+ }
402
+
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 }) {
410
+ if (flags['skip-checkpoint']) return null;
411
+
412
+ const workspacePath = path.resolve(
413
+ String(snapshot?.workspacePath ?? process.env.OVERLORD_WORKSPACE_PATH ?? process.cwd())
414
+ );
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.
423
+ try {
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();
430
+ return {
431
+ checkpoint: { diffStat: diffStat || null, kind },
432
+ snapshot: {
433
+ gitCommitId: gitCommitId || null,
434
+ headSha: gitCommitId || null,
435
+ diffStat: diffStat || null
436
+ }
437
+ };
438
+ } catch {
439
+ return null;
440
+ }
441
+ }
442
+
443
+ const ref = `refs/overlord/checkpoints/${resolvedObjectiveId}`;
444
+ const headSha = runLocalCommand('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], {
445
+ cwd: repoRoot
446
+ }).trim();
447
+
448
+ // If the ref already exists, return the existing snapshot.
449
+ let existingSha = '';
450
+ try {
451
+ existingSha = runLocalCommand('git', ['-C', repoRoot, 'rev-parse', '--verify', ref], {
452
+ cwd: repoRoot
453
+ }).trim();
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'
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
+ }
518
+ return {
519
+ checkpoint: { diffStat: diffStat || null, kind },
520
+ snapshot: {
521
+ gitCommitId: commit,
522
+ gitRefName: ref,
523
+ headSha,
524
+ objectiveId: String(resolvedObjectiveId),
525
+ diffStat: diffStat || null
526
+ }
527
+ };
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 */
539
+ }
540
+ }
541
+ }
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
+
362
593
  function normalizeRepoRelativeFilePath(filePath, repoRoot) {
363
594
  if (typeof filePath !== 'string') return null;
364
595
 
@@ -534,7 +765,8 @@ function resolveExternalSessionId(flags) {
534
765
  async function protocolAttach(args) {
535
766
  const flags = parseFlags(args);
536
767
  const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
537
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
768
+ const { platformUrl, bearerToken, localSecret, organizationId } =
769
+ await resolveProtocolAuthForFlags(flags, ticketId);
538
770
  const timeoutMs = resolveTimeout(flags);
539
771
 
540
772
  const externalSessionId = resolveExternalSessionId(flags);
@@ -557,6 +789,20 @@ async function protocolAttach(args) {
557
789
  timeoutMs
558
790
  );
559
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
+
560
806
  const sessionKey = data.session?.sessionKey;
561
807
  console.log(JSON.stringify(data, null, 2));
562
808
 
@@ -580,7 +826,8 @@ async function protocolUpdate(args) {
580
826
  ? readTextFile(String(flags['summary-file']), '--summary-file')
581
827
  : requireFlag(flags, 'summary', undefined);
582
828
 
583
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
829
+ const { platformUrl, bearerToken, localSecret, organizationId } =
830
+ await resolveProtocolAuthForFlags(flags, ticketId);
584
831
  const timeoutMs = resolveTimeout(flags);
585
832
  const changeRationales = await resolveChangeRationales(flags);
586
833
  const externalSessionId = resolveExternalSessionId(flags);
@@ -637,7 +884,8 @@ async function protocolRecordChangeRationales(args) {
637
884
  );
638
885
  }
639
886
 
640
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
887
+ const { platformUrl, bearerToken, localSecret, organizationId } =
888
+ await resolveProtocolAuthForFlags(flags, ticketId);
641
889
  const timeoutMs = resolveTimeout(flags);
642
890
  const snapshot = await resolveSnapshotContext(flags);
643
891
 
@@ -679,7 +927,8 @@ async function protocolAsk(args) {
679
927
  ? readTextFile(String(flags['question-file']), '--question-file')
680
928
  : requireFlag(flags, 'question', undefined);
681
929
 
682
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
930
+ const { platformUrl, bearerToken, localSecret, organizationId } =
931
+ await resolveProtocolAuthForFlags(flags, ticketId);
683
932
  const timeoutMs = resolveTimeout(flags);
684
933
 
685
934
  const body = {
@@ -715,7 +964,8 @@ async function protocolPermissionRequest(args) {
715
964
  ? await readJsonFileOrStdin(String(flags['payload-file']), '--payload-file')
716
965
  : {};
717
966
 
718
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
967
+ const { platformUrl, bearerToken, localSecret, organizationId } =
968
+ await resolveProtocolAuthForFlags(flags, ticketId);
719
969
  const timeoutMs = resolveTimeout(flags);
720
970
 
721
971
  const data = await apiPost(
@@ -740,7 +990,8 @@ async function protocolReadContext(args) {
740
990
  if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
741
991
  if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
742
992
 
743
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
993
+ const { platformUrl, bearerToken, localSecret, organizationId } =
994
+ await resolveProtocolAuthForFlags(flags, ticketId);
744
995
  const timeoutMs = resolveTimeout(flags);
745
996
 
746
997
  const body = {
@@ -784,7 +1035,8 @@ async function protocolWriteContext(args) {
784
1035
  value = String(flags.value);
785
1036
  }
786
1037
 
787
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1038
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1039
+ await resolveProtocolAuthForFlags(flags, ticketId);
788
1040
  const timeoutMs = resolveTimeout(flags);
789
1041
 
790
1042
  const body = {
@@ -831,7 +1083,8 @@ async function protocolDeliver(args) {
831
1083
  ? readTextFile(String(flags['summary-file']), '--summary-file')
832
1084
  : requireFlag(flags, 'summary', undefined));
833
1085
 
834
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1086
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1087
+ await resolveProtocolAuthForFlags(flags, ticketId);
835
1088
  const timeoutMs = resolveTimeout(flags);
836
1089
 
837
1090
  let artifacts = deliverPayload?.artifacts ?? [];
@@ -853,7 +1106,10 @@ async function protocolDeliver(args) {
853
1106
 
854
1107
  const changeRationales =
855
1108
  deliverPayload?.changeRationales ?? (await resolveChangeRationales(flags));
856
- const snapshot = (await resolveSnapshotContext(flags)) ?? deliverPayload?.snapshot ?? null;
1109
+ const snapshot = mergeDeliverSnapshot(
1110
+ await resolveSnapshotContext(flags),
1111
+ deliverPayload?.snapshot ?? null
1112
+ );
857
1113
  validateDeliverFileChanges(flags, changeRationales);
858
1114
 
859
1115
  const body = {
@@ -887,7 +1143,8 @@ async function protocolAttachmentList(args) {
887
1143
  if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
888
1144
  if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
889
1145
 
890
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1146
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1147
+ await resolveProtocolAuthForFlags(flags, ticketId);
891
1148
  const timeoutMs = resolveTimeout(flags);
892
1149
 
893
1150
  const body = {
@@ -916,7 +1173,8 @@ async function protocolAttachmentPrepareUpload(args) {
916
1173
  const objectiveId = requireFlag(flags, 'objective-id', undefined);
917
1174
  const fileName = requireFlag(flags, 'file-name', undefined);
918
1175
 
919
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1176
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1177
+ await resolveProtocolAuthForFlags(flags, ticketId);
920
1178
  const timeoutMs = resolveTimeout(flags);
921
1179
 
922
1180
  const body = {
@@ -953,7 +1211,8 @@ async function protocolAttachmentFinalizeUpload(args) {
953
1211
  const storagePath = requireFlag(flags, 'storage-path', undefined);
954
1212
  const label = requireFlag(flags, 'label', undefined);
955
1213
 
956
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1214
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1215
+ await resolveProtocolAuthForFlags(flags, ticketId);
957
1216
  const timeoutMs = resolveTimeout(flags);
958
1217
 
959
1218
  const body = {
@@ -993,7 +1252,8 @@ async function protocolAttachmentGetDownloadUrl(args) {
993
1252
  throw new Error('--objective-id is required when using --storage-path');
994
1253
  }
995
1254
 
996
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1255
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1256
+ await resolveProtocolAuthForFlags(flags, ticketId);
997
1257
  const timeoutMs = resolveTimeout(flags);
998
1258
 
999
1259
  const body = {
@@ -1034,7 +1294,8 @@ async function protocolAttachmentUploadFile(args) {
1034
1294
  const fileStats = await stat(filePath);
1035
1295
  const fileBytes = await readFile(filePath);
1036
1296
 
1037
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1297
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1298
+ await resolveProtocolAuthForFlags(flags, ticketId);
1038
1299
  const timeoutMs = resolveTimeout(flags);
1039
1300
 
1040
1301
  const metadata = flags['metadata-json']
@@ -1125,7 +1386,8 @@ async function protocolDiscoverProject(args) {
1125
1386
  async function protocolConnect(args) {
1126
1387
  const flags = parseFlags(args);
1127
1388
  const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
1128
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1389
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1390
+ await resolveProtocolAuthForFlags(flags, ticketId);
1129
1391
  const timeoutMs = resolveTimeout(flags);
1130
1392
 
1131
1393
  const body = {
@@ -1160,7 +1422,8 @@ async function protocolConnect(args) {
1160
1422
  async function protocolLoadContext(args) {
1161
1423
  const flags = parseFlags(args);
1162
1424
  const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
1163
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1425
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1426
+ await resolveProtocolAuthForFlags(flags, ticketId);
1164
1427
  const timeoutMs = resolveTimeout(flags);
1165
1428
 
1166
1429
  const body = { ticketId };
@@ -1253,7 +1516,8 @@ async function protocolCreateTicket(args) {
1253
1516
  const flags = parseFlags(args);
1254
1517
  const { sessionKey, ticketId } = resolveSessionFlags(flags);
1255
1518
  const objective = requireFlag(flags, 'objective', undefined);
1256
- const { platformUrl, bearerToken, localSecret, organizationId } = await resolveProtocolAuthForFlags(flags, ticketId);
1519
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1520
+ await resolveProtocolAuthForFlags(flags, ticketId);
1257
1521
  const timeoutMs = resolveTimeout(flags);
1258
1522
  const agentIdentifier = resolveProtocolAgentIdentifier(flags);
1259
1523
  const modelIdentifier = resolveProtocolModelIdentifier(flags);
@@ -1496,10 +1760,12 @@ attach:
1496
1760
  --method <connectionMethod>
1497
1761
  --external-session-id <id|null> Store the native agent thread/session id, or clear it with null
1498
1762
  --metadata-json <json> Extra session metadata object
1763
+ --skip-checkpoint Bypass automatic objective-start git checkpoint creation
1499
1764
  Returns:
1500
1765
  Full JSON including session.sessionKey, ticket, history, artifacts, sharedState, and promptContext
1501
1766
  Notes:
1502
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.
1503
1769
 
1504
1770
  connect:
1505
1771
  Purpose:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "0.2605111118.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",
@@ -65,7 +65,7 @@ ovld protocol deliver --session-key <sessionKey> \
65
65
  --change-rationales-json '[{"label":"Short reviewer title","file_path":"path/to/file.ts","summary":"What changed.","why":"Why it changed.","impact":"Behavioral impact.","hunks":[{"header":"@@ -10,6 +10,14 @@"}]}]'
66
66
  ```
67
67
 
68
- For larger delivery payloads, prefer `--payload-file -` and stream the full JSON on stdin so no scratch file needs to be created or removed. If you use `--payload-file`, `--artifacts-file`, or `--change-rationales-file` with a real path, treat that file as ephemeral scratch data outside the repository and remove it after delivery.
68
+ `ovld protocol deliver` automatically creates a local checkpoint before the API request when the workspace is JJ- or Git-managed; use `--skip-checkpoint` only when intentionally bypassing local provenance. For larger delivery payloads, prefer `--payload-file -` and stream the full JSON on stdin so no scratch file needs to be created or removed. If you use `--payload-file`, `--artifacts-file`, or `--change-rationales-file` with a real path, treat that file as ephemeral scratch data outside the repository and remove it after delivery.
69
69
 
70
70
  ## Mode 2: Asked From Chat To Use Overlord
71
71
 
@@ -73,14 +73,15 @@ Use this mode when the conversation starts normally and the user asks Claude to
73
73
 
74
74
  1. If the user wants to create tickets (and does not ask to start execution), run `ovld protocol create --agent claude-code --objective "..."`.
75
75
  - When `--session-key` and `--ticket-id` are provided, it creates a follow-up draft.
76
- - When session flags are omitted, it resolves the project by matching current working directory (or `--working-directory`) to the caller's configured Overlord `project_user.local_working_directory`, then creates a standalone draft.
77
- 2. Default to `create` for new tickets. Only use `/overlord:spawn` or `ovld protocol spawn --agent claude-code --objective "..."` when the user explicitly asks to create and execute immediately.
78
- `spawn` creates the ticket in `execute` status and attaches immediately.
76
+ - When session flags are omitted, it resolves the project by matching current working directory (or `--working-directory`) to Overlord `local_working_directory`, then creates a standalone draft.
77
+ 2. Default to `create` for new tickets. Only use `/overlord:prompt` or `ovld protocol prompt --agent claude-code --objective "..."` when the user explicitly asks to create and execute immediately.
78
+ `prompt` creates the ticket in `execute` status and attaches immediately.
79
79
  3. If the user already has a ticket ID and only wants to inspect it, use `/overlord:load` or run `ovld protocol load-context --ticket-id <ticket_id>`.
80
80
  4. If the user wants to route the current session onto an existing ticket by ID, use `/overlord:connect` or run `ovld protocol connect --ticket-id <ticket_id>`.
81
- 5. If the user wants to find a ticket but does not know the ID, use `ovld attach` for interactive ticket search and agent launch, or ask the user for the ticket ID if staying strictly inside chat is the better fit.
82
- 6. If you need other lifecycle commands or flags, run `ovld protocol help` and use the real subcommand list instead of guessing.
83
- 7. Once you attach to a ticket, switch back to Mode 1 and follow the full ticket lifecycle.
81
+ 5. If the user wants to establish a persistent session with a ticket by ID, use `/overlord:attach` or run `ovld protocol attach --ticket-id <ticket_id>`.
82
+ 6. If the user wants to find a ticket but does not know the ID, use `ovld attach` for interactive ticket search and agent launch, or run `ovld protocol search-tickets --query "..." --status next-up,execute` and ask the user to confirm.
83
+ 7. If you need other lifecycle commands or flags, run `ovld protocol help` and use the real subcommand list instead of guessing.
84
+ 8. Once you attach to a ticket, switch back to Mode 1 and follow the full ticket lifecycle.
84
85
 
85
86
  ## Change Rationales
86
87
 
@@ -108,7 +109,7 @@ Record only meaningful behavioral changes. Skip formatting-only noise.
108
109
 
109
110
  When creating tickets from within a repository:
110
111
  - Prefer `create` by default for draft ticket creation.
111
- - Use `spawn` only when the user explicitly asks to start execution immediately.
112
+ - Use `prompt` only when the user explicitly asks to start execution immediately.
112
113
  - Both commands can resolve the project from the current working directory; use `--working-directory` to override.
113
114
 
114
115
  ```bash
@@ -116,7 +117,7 @@ ovld protocol create --agent claude-code --objective "Capture follow-up work fro
116
117
  ```
117
118
 
118
119
  ```bash
119
- ovld protocol spawn --agent claude-code --objective "Implement feature X" --priority medium
120
+ ovld protocol prompt --agent claude-code --objective "Implement feature X" --priority medium
120
121
  ```
121
122
 
122
123
  To inspect project resolution explicitly:
@@ -148,6 +149,15 @@ ovld protocol attachment-download-url --session-key <sessionKey> --ticket-id $TI
148
149
 
149
150
  The `attach` and `load-context` responses already include `attachments` and `objectives` arrays — use those for `<attachment-id>` and `<objective-id>` values. Run `attachment-list` mid-session if new files have been uploaded since attach.
150
151
 
152
+ ## Defaults And Notes
153
+
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`).
156
+ - `permission-request` is invoked by the Claude Code permission hook installed by the bundle. Agents do not normally call it directly.
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`.
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.
159
+ - "Artifacts" in `deliver` are the structured records an agent submits at delivery time (next_steps, test_results, migration, decision, note, url) — not user-uploaded files. Files attached by users live on objectives via the attachment tools above.
160
+
151
161
  ## Rules
152
162
 
153
163
  - Always attach first and always deliver last once you are on a ticket.
@@ -155,10 +165,9 @@ The `attach` and `load-context` responses already include `attachments` and `obj
155
165
  - Do not invent protocol subcommands. Use `ovld protocol help` when unsure.
156
166
  - The `summary` in deliver is what the PM reads first, so write it as a narrative, not a command list.
157
167
  - Use `write-context` for facts a future agent session should know.
158
- - If `ovld` reports `OVERLORD_URL` is unreachable, request permission escalation or network access before retrying.
159
168
  - 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.
160
169
  - 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.
161
170
  - Do not add or commit changes unless the user explicitly asks you to commit.
162
171
  - Delivery is the concluding step. After delivering, stop unless the user follows up or the ticket is reopened.
163
172
 
164
- <!-- version: 0.2.6 -->
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,
@@ -17,17 +17,99 @@ Use this skill whenever Cursor needs to work with Overlord, whether the session
17
17
  6. If blocked, call `ovld protocol ask` and stop.
18
18
  7. Deliver last with `ovld protocol deliver`, including `changeRationales` for each meaningful behavioral file change.
19
19
 
20
+ ### Mode 1 Reference
21
+
22
+ Attach:
23
+
24
+ ```bash
25
+ ovld protocol attach --ticket-id $TICKET_ID
26
+ ```
27
+
28
+ Update:
29
+
30
+ ```bash
31
+ ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID --summary "What you did and why." --phase execute
32
+ ```
33
+
34
+ Supported `--phase` values:
35
+ - `draft`
36
+ - `execute`
37
+ - `review`
38
+ - `deliver`
39
+ - `complete`
40
+ - `blocked`
41
+ - `cancelled`
42
+
43
+ Event types:
44
+ - `update` for standard progress updates
45
+ - `user_follow_up` for human follow-up messages after the initial ticket
46
+ - `alert` for warnings or non-blocking issues
47
+
48
+ Ask when blocked:
49
+
50
+ ```bash
51
+ ovld protocol ask --session-key <sessionKey> --ticket-id $TICKET_ID --question "Specific question for the PM."
52
+ ```
53
+
54
+ Deliver:
55
+
56
+ ```bash
57
+ ovld protocol deliver --session-key <sessionKey> \
58
+ --ticket-id $TICKET_ID \
59
+ --summary "Narrative: what you did, next steps." \
60
+ --artifacts-json '[{"type":"next_steps","label":"Next steps","content":"..."}]' \
61
+ --change-rationales-json '[{"label":"Short reviewer title","file_path":"path/to/file.ts","summary":"What changed.","why":"Why it changed.","impact":"Behavioral impact.","hunks":[{"header":"@@ -10,6 +10,14 @@"}]}]'
62
+ ```
63
+
64
+ `ovld protocol deliver` automatically creates a local checkpoint before the API request when the workspace is JJ- or Git-managed; use `--skip-checkpoint` only when intentionally bypassing local provenance. For larger delivery payloads, prefer `--payload-file -` and stream the full JSON on stdin so no scratch file needs to be created or removed.
65
+
20
66
  ## Mode 2: Asked From Chat To Use Overlord
21
67
 
22
68
  1. If the user wants to create tickets (and does not ask to start execution), use `/create` or run `ovld protocol create --agent cursor --objective "..."`.
23
- 2. Default to `create` for new tickets. Only use `/spawn` or `ovld protocol spawn --agent cursor --objective "..."` when the user explicitly asks to create and execute immediately.
69
+ - When `--session-key` and `--ticket-id` are provided, it creates a follow-up draft.
70
+ - When session flags are omitted, it resolves the project by matching current working directory (or `--working-directory`) to Overlord `local_working_directory`, then creates a standalone draft.
71
+ 2. Default to `create` for new tickets. Only use `/prompt` or `ovld protocol prompt --agent cursor --objective "..."` when the user explicitly asks to create and execute immediately.
24
72
  3. If the user already has a ticket ID and only wants to inspect it, use `/load` or run `ovld protocol load-context --ticket-id <ticket_id>`.
25
73
  4. If the user wants to route the current session onto an existing ticket by ID, use `/connect` or run `ovld protocol connect --ticket-id <ticket_id>`.
26
- 5. If the user wants to search for tickets by keyword or status, use the `search_tickets` MCP tool.
27
- 6. If you need other lifecycle commands or flags, run `ovld protocol help` and use the real subcommand list instead of guessing.
28
- 7. Once you attach to a ticket, switch back to Mode 1 and follow the full ticket lifecycle.
74
+ 5. If the user wants to establish a persistent session with a ticket by ID, use `/attach` or run `ovld protocol attach --ticket-id <ticket_id>`.
75
+ 6. If the user wants to find a ticket by keyword/status/project/creator/date, run `ovld protocol search-tickets --query "..." --status next-up,execute`.
76
+ 7. If you need other lifecycle commands or flags, run `ovld protocol help` and use the real subcommand list instead of guessing.
77
+ 8. Once you attach to a ticket, switch back to Mode 1 and follow the full ticket lifecycle.
78
+
79
+ ## Change Rationales
80
+
81
+ Always include `changeRationales` when delivering. Optionally include them on updates during long-running work.
82
+
83
+ Before delivering, make sure every meaningful git-tracked file change is represented in `changeRationales`; do not send `file_changes` as an artifact.
29
84
 
30
- ## Choosing `--execution-target` When Creating Tickets
85
+ ```bash
86
+ ovld protocol record-change-rationales --session-key <sessionKey> --ticket-id $TICKET_ID \
87
+ --summary "Recorded rationale details for the latest code changes." --phase execute \
88
+ --change-rationales-json '[{"label":"Add backoff","file_path":"lib/api.ts","summary":"Added retry.","why":"Transient failures.","impact":"Retries 3x.","hunks":[{"header":"@@ -22,4 +22,18 @@"}]}]'
89
+ ```
90
+
91
+ ```bash
92
+ ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID \
93
+ --summary "Added retry logic." --phase execute \
94
+ --change-rationales-json '[{"label":"Add backoff","file_path":"lib/api.ts","summary":"Added retry.","why":"Transient failures.","impact":"Retries 3x.","hunks":[{"header":"@@ -22,4 +22,18 @@"}]}]'
95
+ ```
96
+
97
+ Record only meaningful behavioral changes. Skip formatting-only noise.
98
+
99
+ ## Project Discovery And Ticket Creation
100
+
101
+ When creating tickets from within a repository:
102
+ - Prefer `create` by default for draft ticket creation.
103
+ - Use `prompt` only when the user explicitly asks to start execution immediately.
104
+ - Both commands resolve the project from the current working directory; use `--working-directory` to override or `--project-id` to be explicit.
105
+
106
+ ```bash
107
+ ovld protocol create --agent cursor --objective "Capture follow-up work from this repository"
108
+ ovld protocol prompt --agent cursor --objective "Implement feature X" --priority medium
109
+ ovld protocol discover-project
110
+ ```
111
+
112
+ ### Choosing `--execution-target`
31
113
 
32
114
  Pass `--execution-target agent` or `--execution-target human` (default: `human`) when creating tickets.
33
115
 
@@ -36,14 +118,38 @@ Pass `--execution-target agent` or `--execution-target human` (default: `human`)
36
118
 
37
119
  When in doubt, ask yourself: *can this be done entirely inside a terminal or browser by an AI without human intervention?* If yes → `agent`. If it requires a human to log in, decide, or act in the real world → `human`.
38
120
 
121
+ ## Context And Artifacts
122
+
123
+ ```bash
124
+ ovld protocol read-context --session-key <sessionKey> --ticket-id $TICKET_ID
125
+ ovld protocol write-context --session-key <sessionKey> --ticket-id $TICKET_ID --key "key" --value '"json-value"'
126
+ ovld protocol attachment-list --session-key <sessionKey> --ticket-id $TICKET_ID
127
+ ovld protocol attachment-upload-file --session-key <sessionKey> --ticket-id $TICKET_ID --objective-id <objective-id> --file ./spec.pdf --content-type application/pdf
128
+ ovld protocol attachment-download-url --session-key <sessionKey> --ticket-id $TICKET_ID --attachment-id <attachment-id>
129
+ ```
130
+
131
+ The `attach` and `load-context` responses already include `attachments` and `objectives` arrays — use those for `<attachment-id>` and `<objective-id>` values. Run `attachment-list` mid-session if new files have been uploaded since attach.
132
+
133
+ Objective attachment uploads also expose two-step variants — `attachment-prepare-upload` and `attachment-finalize-upload` — for callers that need a signed URL directly. Prefer `attachment-upload-file` for one-shot uploads.
134
+
135
+ "Artifacts" in `deliver` are the structured records an agent submits at delivery time (next_steps, test_results, migration, decision, note, url) — not user-uploaded files.
136
+
137
+ ## Defaults And Notes
138
+
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).
141
+ - `permission-request` is invoked by the installed permission hook; agents normally do not call it directly.
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.
143
+
39
144
  ## Rules
40
145
 
41
146
  - Always attach first and always deliver last once you are on a ticket.
42
147
  - Use `ovld protocol` commands, the plugin commands, and the MCP tool instead of ad hoc scripts.
43
148
  - Do not invent protocol subcommands. Use `ovld protocol help` when unsure.
44
- - If `ovld` reports `OVERLORD_URL` is unreachable, request permission escalation or network access before retrying.
45
- - 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.
46
149
  - Include at least one progress update before delivering.
47
- - When creating follow-up tickets, always set `--execution-target` explicitly using the `agent`/`human` rule above.
150
+ - The `summary` in deliver is what the PM reads first, so write it as a narrative, not a command list.
151
+ - 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.
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.
153
+ - Delivery is the concluding step. After delivering, stop unless the user follows up or the ticket is reopened.
48
154
 
49
- <!-- version: 0.2.4 -->
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,56 +44,67 @@ 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',
51
51
  properties: {
52
- ticket_id: { type: 'string', description: 'Target ticket ID' },
52
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
53
53
  agent: { type: 'string' },
54
- model: { type: 'string' },
55
54
  method: { type: 'string' },
56
55
  external_session_id: { type: ['string', 'null'] },
57
- metadata: { type: 'object' }
56
+ metadata: {
57
+ type: 'object',
58
+ description: 'Optional extra metadata merged into the attach request (same as --metadata-json on the CLI).'
59
+ }
58
60
  },
59
61
  required: ['ticket_id']
60
62
  },
61
63
  toCliFlags: args => ({
62
64
  'ticket-id': args.ticket_id,
63
65
  agent: args.agent,
64
- model: args.model,
65
66
  method: args.method,
66
67
  'external-session-id': args.external_session_id,
67
- 'metadata-json': args.metadata
68
+ ...(args.metadata && typeof args.metadata === 'object' && !Array.isArray(args.metadata)
69
+ ? { 'metadata-json': args.metadata }
70
+ : {})
68
71
  }),
69
72
  subcommand: 'attach'
70
73
  },
71
74
  {
72
- name: 'connect_ticket',
75
+ name: 'connect',
73
76
  description: 'Create a lightweight Overlord session without loading the full ticket context.',
74
77
  inputSchema: {
75
78
  type: 'object',
76
79
  properties: {
77
- ticket_id: { type: 'string' },
80
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
78
81
  agent: { type: 'string' },
79
- 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
+ }
80
87
  },
81
88
  required: ['ticket_id']
82
89
  },
83
90
  toCliFlags: args => ({
84
91
  'ticket-id': args.ticket_id,
85
92
  agent: args.agent,
86
- method: args.method
93
+ method: args.method,
94
+ ...(args.metadata && typeof args.metadata === 'object' && !Array.isArray(args.metadata)
95
+ ? { 'metadata-json': args.metadata }
96
+ : {})
87
97
  }),
88
98
  subcommand: 'connect'
89
99
  },
90
100
  {
91
101
  name: 'load_ticket_context',
92
- 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).',
93
104
  inputSchema: {
94
105
  type: 'object',
95
106
  properties: {
96
- ticket_id: { type: 'string' }
107
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' }
97
108
  },
98
109
  required: ['ticket_id']
99
110
  },
@@ -103,8 +114,8 @@ const tools = [
103
114
  subcommand: 'load-context'
104
115
  },
105
116
  {
106
- name: 'spawn_ticket',
107
- description: 'Create a follow-up ticket and attach to it immediately.',
117
+ name: 'prompt',
118
+ description: 'Create a ticket and attach to it immediately (ovld protocol prompt).',
108
119
  inputSchema: {
109
120
  type: 'object',
110
121
  properties: {
@@ -113,6 +124,7 @@ const tools = [
113
124
  priority: { type: 'string', enum: ['low', 'medium', 'high', 'urgent'] },
114
125
  project_id: { type: 'string' },
115
126
  working_directory: { type: 'string' },
127
+ personal: { type: 'boolean', description: 'Create without assigning a project (private ticket).' },
116
128
  acceptance_criteria: { type: 'string' },
117
129
  available_tools: { type: 'string' },
118
130
  execution_target: { type: 'string', enum: ['agent', 'human'] },
@@ -120,8 +132,6 @@ const tools = [
120
132
  parent_session_key: { type: 'string' },
121
133
  parent_ticket_id: { type: 'string' },
122
134
  agent: { type: 'string' },
123
- model: { type: 'string' },
124
- metadata: { type: 'object' },
125
135
  method: { type: 'string' }
126
136
  },
127
137
  required: ['objective']
@@ -132,6 +142,7 @@ const tools = [
132
142
  priority: args.priority,
133
143
  'project-id': args.project_id,
134
144
  'working-directory': args.working_directory,
145
+ personal: args.personal === true ? true : undefined,
135
146
  'acceptance-criteria': args.acceptance_criteria,
136
147
  'available-tools': args.available_tools,
137
148
  'execution-target': args.execution_target,
@@ -139,20 +150,18 @@ const tools = [
139
150
  'parent-session-key': args.parent_session_key,
140
151
  'parent-ticket-id': args.parent_ticket_id,
141
152
  agent: args.agent,
142
- model: args.model,
143
- 'metadata-json': args.metadata,
144
153
  method: args.method
145
154
  }),
146
- subcommand: 'spawn'
155
+ subcommand: 'prompt'
147
156
  },
148
157
  {
149
- name: 'post_update',
158
+ name: 'update',
150
159
  description: 'Post an Overlord progress update or activity event.',
151
160
  inputSchema: {
152
161
  type: 'object',
153
162
  properties: {
154
163
  session_key: { type: 'string' },
155
- ticket_id: { type: 'string' },
164
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
156
165
  summary: { type: 'string' },
157
166
  phase: { type: 'string', enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled'] },
158
167
  event_type: { type: 'string', enum: ['update', 'user_follow_up', 'alert'] },
@@ -183,7 +192,7 @@ const tools = [
183
192
  type: 'object',
184
193
  properties: {
185
194
  session_key: { type: 'string' },
186
- ticket_id: { type: 'string' },
195
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
187
196
  summary: { type: 'string' },
188
197
  phase: { type: 'string', enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled'] },
189
198
  change_rationales: { type: 'array' }
@@ -200,13 +209,13 @@ const tools = [
200
209
  subcommand: 'record-change-rationales'
201
210
  },
202
211
  {
203
- name: 'ask_blocking_question',
212
+ name: 'ask',
204
213
  description: 'Send a blocking question to the human reviewer or PM.',
205
214
  inputSchema: {
206
215
  type: 'object',
207
216
  properties: {
208
217
  session_key: { type: 'string' },
209
- ticket_id: { type: 'string' },
218
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
210
219
  question: { type: 'string' },
211
220
  phase: { type: 'string', enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled'] },
212
221
  payload: { type: 'object' }
@@ -223,13 +232,13 @@ const tools = [
223
232
  subcommand: 'ask'
224
233
  },
225
234
  {
226
- name: 'read_shared_context',
235
+ name: 'read_context',
227
236
  description: 'Read persistent shared context entries for a ticket.',
228
237
  inputSchema: {
229
238
  type: 'object',
230
239
  properties: {
231
240
  session_key: { type: 'string' },
232
- ticket_id: { type: 'string' },
241
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
233
242
  query: { type: 'string' },
234
243
  limit: { type: 'number' }
235
244
  },
@@ -244,13 +253,13 @@ const tools = [
244
253
  subcommand: 'read-context'
245
254
  },
246
255
  {
247
- name: 'write_shared_context',
256
+ name: 'write_context',
248
257
  description: 'Write a persistent shared context entry for future Overlord sessions.',
249
258
  inputSchema: {
250
259
  type: 'object',
251
260
  properties: {
252
261
  session_key: { type: 'string' },
253
- ticket_id: { type: 'string' },
262
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
254
263
  key: { type: 'string' },
255
264
  value: {},
256
265
  tags: { type: 'array', items: { type: 'string' } }
@@ -267,17 +276,28 @@ const tools = [
267
276
  subcommand: 'write-context'
268
277
  },
269
278
  {
270
- name: 'deliver_ticket',
279
+ name: 'deliver',
271
280
  description:
272
- '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.',
273
282
  inputSchema: {
274
283
  type: 'object',
275
284
  properties: {
276
285
  session_key: { type: 'string' },
277
- ticket_id: { type: 'string' },
286
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
278
287
  summary: { type: 'string' },
279
288
  artifacts: { type: 'array' },
280
289
  change_rationales: { type: 'array' },
290
+ snapshot: {
291
+ type: 'object',
292
+ description:
293
+ 'Optional snapshot metadata (jj/git-worktree, workspace, jj ids). Merged with OVERLORD_SNAPSHOT_JSON when both are set.'
294
+ },
295
+ checkpoint: {
296
+ type: 'object',
297
+ description:
298
+ 'Optional checkpoint metadata. The local CLI creates this automatically for JJ/Git workspaces during deliver.'
299
+ },
300
+ skip_checkpoint: { type: 'boolean' },
281
301
  skip_file_change_check: { type: 'boolean' }
282
302
  },
283
303
  required: ['session_key', 'ticket_id', 'summary']
@@ -286,13 +306,17 @@ const tools = [
286
306
  'session-key': args.session_key,
287
307
  'ticket-id': args.ticket_id,
288
308
  'payload-file': '-',
309
+ 'skip-checkpoint': args.skip_checkpoint,
289
310
  'skip-file-change-check': args.skip_file_change_check
290
311
  }),
291
- toCliStdin: args => JSON.stringify({
292
- summary: args.summary,
293
- ...(Array.isArray(args.artifacts) ? { artifacts: args.artifacts } : {}),
294
- ...(Array.isArray(args.change_rationales) ? { changeRationales: args.change_rationales } : {})
295
- }),
312
+ toCliStdin: args =>
313
+ JSON.stringify({
314
+ summary: args.summary,
315
+ ...(Array.isArray(args.artifacts) ? { artifacts: args.artifacts } : {}),
316
+ ...(Array.isArray(args.change_rationales) ? { changeRationales: args.change_rationales } : {}),
317
+ ...(args.snapshot && typeof args.snapshot === 'object' ? { snapshot: args.snapshot } : {}),
318
+ ...(args.checkpoint && typeof args.checkpoint === 'object' ? { checkpoint: args.checkpoint } : {})
319
+ }),
296
320
  subcommand: 'deliver'
297
321
  },
298
322
  {
@@ -303,7 +327,7 @@ const tools = [
303
327
  type: 'object',
304
328
  properties: {
305
329
  session_key: { type: 'string' },
306
- ticket_id: { type: 'string' },
330
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
307
331
  objective_id: { type: 'string' }
308
332
  },
309
333
  required: ['session_key', 'ticket_id']
@@ -322,7 +346,7 @@ const tools = [
322
346
  type: 'object',
323
347
  properties: {
324
348
  session_key: { type: 'string' },
325
- ticket_id: { type: 'string' },
349
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
326
350
  objective_id: { type: 'string' },
327
351
  file_name: { type: 'string' },
328
352
  label: { type: 'string' },
@@ -351,7 +375,7 @@ const tools = [
351
375
  type: 'object',
352
376
  properties: {
353
377
  session_key: { type: 'string' },
354
- ticket_id: { type: 'string' },
378
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
355
379
  objective_id: { type: 'string' },
356
380
  storage_path: { type: 'string' },
357
381
  label: { type: 'string' },
@@ -380,7 +404,7 @@ const tools = [
380
404
  type: 'object',
381
405
  properties: {
382
406
  session_key: { type: 'string' },
383
- ticket_id: { type: 'string' },
407
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
384
408
  objective_id: { type: 'string' },
385
409
  attachment_id: { type: 'string' },
386
410
  storage_path: { type: 'string' },
@@ -405,7 +429,7 @@ const tools = [
405
429
  type: 'object',
406
430
  properties: {
407
431
  session_key: { type: 'string' },
408
- ticket_id: { type: 'string' },
432
+ ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
409
433
  objective_id: { type: 'string' },
410
434
  file: { type: 'string' },
411
435
  file_name: { type: 'string' },
@@ -453,7 +477,7 @@ const searchTicketsTool = {
453
477
  },
454
478
  limit: {
455
479
  type: 'number',
456
- description: 'Maximum number of results to return (1–20, default 8).'
480
+ description: 'Maximum number of results to return (1–50, default 8).'
457
481
  }
458
482
  }
459
483
  },
@@ -466,10 +490,9 @@ const searchTicketsTool = {
466
490
  subcommand: 'search-tickets'
467
491
  };
468
492
 
469
- const toolMap = new Map([
470
- ...tools.map(tool => [tool.name, tool]),
471
- [searchTicketsTool.name, searchTicketsTool]
472
- ]);
493
+ const allListedTools = [...tools, searchTicketsTool];
494
+
495
+ const toolMap = new Map(allListedTools.map(tool => [tool.name, tool]));
473
496
  let buffer = Buffer.alloc(0);
474
497
 
475
498
  function serializeMessage(message) {
@@ -632,10 +655,10 @@ async function handleRequest(message) {
632
655
  },
633
656
  serverInfo: {
634
657
  name: 'overlord',
635
- version: '0.2.1'
658
+ version: '0.1.4'
636
659
  },
637
660
  instructions:
638
- 'Use these tools to drive Overlord ticket workflows through the installed ovld CLI. Most operations expect a session key from attach or connect. If the CLI reports that OVERLORD_URL is unreachable, request permission escalation or network access before retrying.'
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.'
639
662
  });
640
663
  return;
641
664
  }
@@ -647,7 +670,7 @@ async function handleRequest(message) {
647
670
 
648
671
  if (method === 'tools/list') {
649
672
  success(id, {
650
- tools: [...tools, searchTicketsTool].map(tool => ({
673
+ tools: allListedTools.map(tool => ({
651
674
  name: tool.name,
652
675
  description: tool.description,
653
676
  inputSchema: tool.inputSchema
@@ -63,18 +63,45 @@ ovld protocol deliver --session-key <sessionKey> \
63
63
  --change-rationales-json '[{"label":"Short reviewer title","file_path":"path/to/file.ts","summary":"What changed.","why":"Why it changed.","impact":"Behavioral impact.","hunks":[{"header":"@@ -10,6 +10,14 @@"}]}]'
64
64
  ```
65
65
 
66
- For larger delivery payloads, prefer `--payload-file -` and stream the full JSON on stdin so no scratch file needs to be created or removed. If you use `--payload-file`, `--artifacts-file`, or `--change-rationales-file` with a real path, treat that file as ephemeral scratch data outside the repository and remove it after delivery.
66
+ `ovld protocol deliver` automatically creates a local checkpoint before the API request when the workspace is JJ- or Git-managed; use `--skip-checkpoint` only when intentionally bypassing local provenance. For larger delivery payloads, prefer `--payload-file -` and stream the full JSON on stdin so no scratch file needs to be created or removed. If you use `--payload-file`, `--artifacts-file`, or `--change-rationales-file` with a real path, treat that file as ephemeral scratch data outside the repository and remove it after delivery.
67
67
 
68
68
  ## Mode 2: Asked From Chat To Use Overlord
69
69
 
70
70
  1. If the user wants to create tickets (and does not ask to start execution), run `ovld protocol create --agent codex --objective "..."`.
71
- 2. Default to `create` for new tickets. Only use `ovld protocol spawn --agent codex --objective "..."` when the user explicitly asks to create and execute immediately.
71
+ - When `--session-key` and `--ticket-id` are provided, it creates a follow-up draft.
72
+ - When session flags are omitted, it resolves the project by matching current working directory (or `--working-directory`) to Overlord `local_working_directory`, then creates a standalone draft.
73
+ 2. Default to `create` for new tickets. Only use `ovld protocol prompt --agent codex --objective "..."` when the user explicitly asks to create and execute immediately.
74
+ `prompt` creates the ticket in `execute` status and attaches immediately.
72
75
  3. If the user wants to inspect an existing ticket without starting work, use `ovld protocol load-context --ticket-id <ticket_id>`.
73
- 4. If the user wants to work an existing ticket, attach with `ovld protocol attach --ticket-id <ticket_id>` and then switch to Mode 1.
74
- 5. If the user wants to find existing tickets by keyword or workflow state, use the `search_tickets` tool.
75
- 6. If you need to understand project routing before spawning, use `ovld protocol discover-project`.
76
+ 4. If the user wants to work an existing ticket, attach with `ovld protocol attach --ticket-id <ticket_id>` and then switch to Mode 1. Use `ovld protocol connect --ticket-id <ticket_id>` instead when you only need a session key without the full ticket payload.
77
+ 5. If the user wants to find existing tickets by keyword, status, project, creator, or update window, run `ovld protocol search-tickets --query "..." --status next-up,execute --limit 10`. The MCP `search_tickets` tool exposes the same filters.
78
+ 6. If you need to understand project routing before prompting, use `ovld protocol discover-project`.
76
79
  7. If you need other lifecycle commands or flags, run `ovld protocol help` and use the real subcommand list instead of guessing.
77
80
 
81
+ ## Project Discovery And Ticket Creation
82
+
83
+ When creating tickets from within a repository:
84
+ - Prefer `create` by default for draft ticket creation.
85
+ - Use `prompt` only when the user explicitly asks to start execution immediately.
86
+ - Both commands can resolve the project from the current working directory; use `--working-directory` to override.
87
+
88
+ ```bash
89
+ ovld protocol create --agent codex --objective "Capture follow-up work from this repository"
90
+ ```
91
+
92
+ ```bash
93
+ ovld protocol prompt --agent codex --objective "Implement feature X" --priority medium
94
+ ```
95
+
96
+ ### Choosing `--execution-target`
97
+
98
+ Pass `--execution-target agent` or `--execution-target human` (default: `human`) when creating tickets.
99
+
100
+ - **`agent`** — any task an AI agent can complete in a computer environment: coding, internet research, document editing, data analysis, automated testing, etc.
101
+ - **`human`** — any task requiring human presence or judgment: setting credentials or tokens in a third-party UI (e.g. Vercel, AWS), sending physical mail, making a product or business decision, physical-world actions.
102
+
103
+ When in doubt, ask yourself: *can this be done entirely inside a terminal or browser by an AI without human intervention?* If yes → `agent`. If it requires a human to log in, decide, or act in the real world → `human`.
104
+
78
105
  ## Change Rationales
79
106
 
80
107
  Always include `changeRationales` when delivering. Optionally include them on updates during long-running work.
@@ -109,38 +136,23 @@ ovld protocol attachment-download-url --session-key <sessionKey> --ticket-id $TI
109
136
 
110
137
  The `attach` and `load-context` responses already include `attachments` and `objectives` arrays — use those for `<attachment-id>` and `<objective-id>` values. Run `attachment-list` mid-session if new files have been uploaded since attach.
111
138
 
112
- ## Project Discovery And Ticket Creation
113
-
114
- When creating tickets from within a repository:
115
- - Prefer `create` by default for draft ticket creation.
116
- - Use `spawn` only when the user explicitly asks to start execution immediately.
117
- - Both commands can resolve the project from the current working directory; use `--working-directory` to override.
139
+ ## Defaults And Notes
118
140
 
119
- ```bash
120
- ovld protocol create --agent codex --objective "Capture follow-up work from this repository"
121
- ```
122
-
123
- ```bash
124
- ovld protocol spawn --agent codex --objective "Implement feature X" --priority medium
125
- ```
126
-
127
- ### Choosing `--execution-target`
128
-
129
- Pass `--execution-target agent` or `--execution-target human` (default: `human`) when creating tickets.
130
-
131
- - **`agent`** — any task an AI agent can complete in a computer environment: coding, internet research, document editing, data analysis, automated testing, etc.
132
- - **`human`** — any task requiring human presence or judgment: setting credentials or tokens in a third-party UI (e.g. Vercel, AWS), sending physical mail, making a product or business decision, physical-world actions.
133
-
134
- When in doubt, ask yourself: *can this be done entirely inside a terminal or browser by an AI without human intervention?* If yes → `agent`. If it requires a human to log in, decide, or act in the real world → `human`.
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.
143
+ - `permission-request` is invoked by the local Codex plugin's permission rules; agents do not normally call it directly.
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`.
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.
146
+ - "Artifacts" in `deliver` are the structured records an agent submits at delivery time (next_steps, test_results, migration, decision, note, url) — not user-uploaded files.
135
147
 
136
148
  ## Rules
137
149
 
138
150
  - The authoritative lifecycle is the `ovld protocol` CLI once you are on a ticket.
139
151
  - Always attach first and always deliver last once you are working a ticket.
140
- - Prefer the installed `ovld` CLI and the plugin's MCP tools instead of ad hoc repo scripts.
152
+ - Prefer the installed `ovld protocol` CLI and the plugin's MCP tools instead of ad hoc repo scripts.
141
153
  - Do not create or rely on a local Codex `AGENTS.md` bundle for Overlord.
142
- - If `ovld` reports `OVERLORD_URL` is unreachable, request permission escalation or network access before retrying.
143
154
  - When the ticket was launched by Overlord, the ticket prompt remains authoritative for the specific task objective and ticket-level constraints.
144
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.
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.
145
157
 
146
- <!-- version: 0.2.5 -->
158
+ <!-- version: 0.4.7 -->