overlord-cli 0.2606011152.0 → 0.2606021051.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
@@ -141,7 +141,7 @@ Agents can find docs here: https://www.ovld.ai/docs/for-agents
141
141
  - `record-change-rationales` - persist structured change rationales without a normal progress update
142
142
  - `ask` - post a blocking question and move the ticket to review
143
143
  - `permission-request` - notify Overlord that the agent is requesting tool permission
144
- - `hook-event` - record lifecycle hook events such as `UserPromptSubmit` / Cursor `beforeSubmitPrompt` without a session key
144
+ - `hook-event` - record lifecycle hook events such as `UserPromptSubmit` / Cursor `beforeSubmitPrompt` without a session key; optionally persist a native resume/session id onto the active Overlord session
145
145
  - `read-context` - read shared persistent context for this ticket
146
146
  - `write-context` - write shared persistent context for future sessions
147
147
  - `deliver` - send artifacts/rationales and move the ticket to review
@@ -34,6 +34,7 @@ Usage:
34
34
  ${primaryCommand} create "<objective>" Create a ticket with numbered project selection; supports --agent/--model/--delegate
35
35
  ${primaryCommand} prompt "<objective>" Create a ticket, then launch an agent on it
36
36
  ${primaryCommand} add-cwd Register the current directory as a project resource (interactive)
37
+ ${primaryCommand} onboard Create an account, project, cwd link, and onboarding ticket
37
38
  ${primaryCommand} auth <subcommand> Login, logout, repair, or check auth status
38
39
  ${primaryCommand} tickets <subcommand> Create or list tickets
39
40
  ${primaryCommand} ticket <subcommand> Work with a single ticket
@@ -117,6 +118,12 @@ export async function runCli({ primaryCommand }) {
117
118
  return;
118
119
  }
119
120
 
121
+ if (command === 'onboard') {
122
+ const { runOnboardCommand } = await import('./onboard.mjs');
123
+ await runOnboardCommand(rest);
124
+ return;
125
+ }
126
+
120
127
  // Auth group
121
128
  if (command === 'auth') {
122
129
  await runAuthCommand(rest[0], rest.slice(1));
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from 'node:child_process';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import readline from 'node:readline';
7
+ import { stdin as input, stdout as output } from 'node:process';
8
+
9
+ import { authLoginViaDeviceFlow, resolveLoginPlatformUrl } from './auth.mjs';
10
+ import {
11
+ buildAuthHeaders,
12
+ loadCredentials,
13
+ loadRuntime,
14
+ saveCredentials
15
+ } from './credentials.mjs';
16
+ import { upsertLocalOverlordConfig } from './local-config.mjs';
17
+ import { readOrCreateDeviceFingerprint } from './runner.mjs';
18
+
19
+ function parseFlags(args) {
20
+ const flags = {};
21
+ for (let i = 0; i < args.length; i++) {
22
+ const arg = args[i];
23
+ if (!arg.startsWith('--')) continue;
24
+
25
+ const eqIdx = arg.indexOf('=');
26
+ if (eqIdx !== -1) {
27
+ flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
28
+ continue;
29
+ }
30
+
31
+ const key = arg.slice(2);
32
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
33
+ flags[key] = args[i + 1];
34
+ i++;
35
+ } else {
36
+ flags[key] = true;
37
+ }
38
+ }
39
+ return flags;
40
+ }
41
+
42
+ function openBrowser(url) {
43
+ try {
44
+ if (process.platform === 'darwin') execFileSync('open', [url]);
45
+ else if (process.platform === 'win32') execFileSync('cmd', ['/c', 'start', '', url]);
46
+ else execFileSync('xdg-open', [url]);
47
+ } catch {
48
+ // Best effort. The URL is also printed.
49
+ }
50
+ }
51
+
52
+ function trimFlag(flags, name) {
53
+ const value = flags[name];
54
+ return typeof value === 'string' ? value.trim() : '';
55
+ }
56
+
57
+ async function promptForValue(question, defaultValue = '') {
58
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
59
+ return defaultValue;
60
+ }
61
+
62
+ const rl = readline.createInterface({ input, output });
63
+ try {
64
+ return await new Promise((resolve, reject) => {
65
+ const handleError = error => {
66
+ rl.off('error', handleError);
67
+ reject(error);
68
+ };
69
+ rl.once('error', handleError);
70
+ const suffix = defaultValue ? ` (${defaultValue})` : '';
71
+ rl.question(`${question}${suffix}: `, answer => {
72
+ rl.off('error', handleError);
73
+ const trimmed = answer.trim();
74
+ resolve(trimmed || defaultValue);
75
+ });
76
+ });
77
+ } finally {
78
+ rl.close();
79
+ }
80
+ }
81
+
82
+ async function promptForYes(question, defaultYes = true) {
83
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
84
+ return defaultYes;
85
+ }
86
+
87
+ const rl = readline.createInterface({ input, output });
88
+ try {
89
+ return await new Promise((resolve, reject) => {
90
+ const handleError = error => {
91
+ rl.off('error', handleError);
92
+ reject(error);
93
+ };
94
+ rl.once('error', handleError);
95
+ const suffix = defaultYes ? 'Y/n' : 'y/N';
96
+ rl.question(`${question} (${suffix}): `, answer => {
97
+ rl.off('error', handleError);
98
+ const normalized = answer.trim().toLowerCase();
99
+ if (!normalized) resolve(defaultYes);
100
+ else resolve(normalized === 'y' || normalized === 'yes');
101
+ });
102
+ });
103
+ } finally {
104
+ rl.close();
105
+ }
106
+ }
107
+
108
+ function buildSignupUrl(platformUrl, verificationUri, name) {
109
+ const verificationUrl = new URL(verificationUri);
110
+ const signupUrl = new URL('/signup', platformUrl);
111
+ signupUrl.searchParams.set('next', `${verificationUrl.pathname}${verificationUrl.search}`);
112
+ if (name) signupUrl.searchParams.set('name', name);
113
+ return signupUrl.toString();
114
+ }
115
+
116
+ async function completeCliOnboarding(platformUrl, localSecret, credentials, payload) {
117
+ const res = await fetch(`${platformUrl}/api/auth/cli-onboarding`, {
118
+ method: 'POST',
119
+ headers: {
120
+ ...buildAuthHeaders(credentials.access_token, localSecret),
121
+ 'Content-Type': 'application/json'
122
+ },
123
+ body: JSON.stringify(payload)
124
+ });
125
+ const data = await res.json().catch(() => ({}));
126
+ if (!res.ok) {
127
+ throw new Error(
128
+ `Onboarding setup failed (${res.status}): ${data.error ?? JSON.stringify(data)}`
129
+ );
130
+ }
131
+ return data;
132
+ }
133
+
134
+ function buildWebProjectUrl(platformUrl, result) {
135
+ const projectId = result?.project?.id;
136
+ if (!projectId) return `${platformUrl}/u`;
137
+ return `${platformUrl}/projects/${projectId}`;
138
+ }
139
+
140
+ export async function runOnboardCommand(args) {
141
+ const flags = parseFlags(args);
142
+
143
+ if (flags.help === true || flags.h === true || args[0] === 'help') {
144
+ console.log(`Usage: ovld onboard [--name <name>] [--organization-name <name>] [--project-name <name>] [--directory <path>] [--yes] [--no-desktop]
145
+
146
+ Creates an Overlord account setup from the terminal:
147
+ 1. collects your name, organization, project, and repository directory
148
+ 2. opens browser signup/login and authorizes the CLI
149
+ 3. creates the organization/project, links this directory, and creates the first onboarding ticket
150
+ 4. opens the Desktop download page, or the web project when --no-desktop is used
151
+
152
+ Run this from the repository you want Overlord agents to work in.`);
153
+ return;
154
+ }
155
+
156
+ const directoryPath = path.resolve(trimFlag(flags, 'directory') || process.cwd());
157
+ const directoryName = path.basename(directoryPath) || 'My project';
158
+ const inferredName = os.userInfo().username || 'User';
159
+ const name = trimFlag(flags, 'name') || (await promptForValue('Your name', inferredName));
160
+ const organizationName =
161
+ trimFlag(flags, 'organization-name') || (await promptForValue('Organization name', name));
162
+ const projectName =
163
+ trimFlag(flags, 'project-name') || (await promptForValue('Project name', directoryName));
164
+
165
+ if (!name || !organizationName || !projectName) {
166
+ throw new Error('Name, organization name, and project name are required.');
167
+ }
168
+
169
+ const storedCredentials = loadCredentials();
170
+ const platformUrl = resolveLoginPlatformUrl(null, storedCredentials?.platform_url ?? null);
171
+ const runtime = loadRuntime(platformUrl);
172
+ const localSecret = runtime?.local_secret ?? process.env.OVERLORD_LOCAL_SECRET ?? '';
173
+
174
+ console.log('\nStarting Overlord onboarding in your browser.');
175
+ console.log('Create an account or sign in, then approve the CLI authorization request.\n');
176
+
177
+ const credentials = await authLoginViaDeviceFlow(platformUrl, localSecret, {
178
+ browserOpener: verificationUri => {
179
+ const signupUrl = buildSignupUrl(platformUrl, verificationUri, name);
180
+ console.log(` Signup URL: ${signupUrl}`);
181
+ openBrowser(signupUrl);
182
+ }
183
+ });
184
+
185
+ const resolvedPlatformUrl = credentials.platform_url ?? platformUrl;
186
+ const deviceFingerprint = readOrCreateDeviceFingerprint(flags);
187
+ const result = await completeCliOnboarding(resolvedPlatformUrl, localSecret, credentials, {
188
+ name,
189
+ organizationName,
190
+ projectName,
191
+ directoryPath,
192
+ deviceFingerprint,
193
+ deviceHostname: os.hostname(),
194
+ devicePlatform: process.platform
195
+ });
196
+
197
+ saveCredentials({
198
+ access_token: credentials.access_token,
199
+ access_token_expires_at: credentials.access_token_expires_at ?? undefined,
200
+ refresh_token: credentials.refresh_token,
201
+ organization_id: result.organization.organizationId,
202
+ platform_url: resolvedPlatformUrl
203
+ });
204
+
205
+ try {
206
+ const localConfig = await upsertLocalOverlordConfig({
207
+ directoryPath,
208
+ project: { id: result.project.id, name: result.project.name }
209
+ });
210
+ console.log(`\nWrote ${localConfig.filePath} (${localConfig.action}).`);
211
+ } catch (error) {
212
+ console.log(
213
+ `\nWarning: could not update .overlord/project.json: ${error instanceof Error ? error.message : error}`
214
+ );
215
+ }
216
+
217
+ console.log('\nOverlord setup complete.');
218
+ console.log(` Organization: ${result.organization.organizationName}`);
219
+ console.log(` Project: ${result.project.name}`);
220
+ console.log(` Directory: ${directoryPath}`);
221
+ console.log(` Onboarding ticket: ${result.ticket.reference ?? result.ticket.id}`);
222
+
223
+ const skipDesktop = flags['no-desktop'] === true || flags.desktop === 'false';
224
+ const autoYes = flags.yes === true || flags.y === true;
225
+ const shouldOpenDesktop =
226
+ !skipDesktop &&
227
+ (autoYes ||
228
+ (await promptForYes(
229
+ '\nRecommended: download Overlord Desktop for terminal launches and file-change tracking. Open downloads now?',
230
+ true
231
+ )));
232
+
233
+ if (shouldOpenDesktop) {
234
+ const downloadsUrl = `${resolvedPlatformUrl}/downloads`;
235
+ console.log(`Opening ${downloadsUrl}`);
236
+ openBrowser(downloadsUrl);
237
+ } else {
238
+ const webUrl = buildWebProjectUrl(resolvedPlatformUrl, result);
239
+ console.log(`Opening ${webUrl}`);
240
+ openBrowser(webUrl);
241
+ }
242
+
243
+ console.log('\nNext command from this repo:');
244
+ console.log(' ovld prompt --objectives-json \'[{"objective":"Describe the work you want done"}]\'');
245
+ }
@@ -1296,6 +1296,12 @@ async function protocolHookEvent(args) {
1296
1296
  ? { turnIndex: parseInt(String(flags['turn-index']), 10) }
1297
1297
  : {}),
1298
1298
  ...(flags['follow-up-intent'] ? { followUpIntent: String(flags['follow-up-intent']) } : {}),
1299
+ ...(() => {
1300
+ if (flags['external-session-id'] === undefined) return {};
1301
+ const raw = String(flags['external-session-id']).trim();
1302
+ if (raw === '') return {};
1303
+ return { externalSessionId: raw.toLowerCase() === 'null' ? null : raw };
1304
+ })(),
1299
1305
  ...(sessionKey ? { sessionKey } : {})
1300
1306
  };
1301
1307
 
@@ -2200,11 +2206,13 @@ async function protocolConnect(args) {
2200
2206
  const { platformUrl, bearerToken, localSecret, organizationId } =
2201
2207
  await resolveProtocolAuthForFlags(flags, ticketId);
2202
2208
  const timeoutMs = resolveTimeout(flags);
2209
+ const externalSessionId = resolveExternalSessionId(flags);
2203
2210
 
2204
2211
  const body = {
2205
2212
  ticketId,
2206
2213
  agentIdentifier: resolveProtocolAgentIdentifier(flags),
2207
2214
  connectionMethod: String(flags.method ?? 'cli'),
2215
+ ...(externalSessionId !== undefined ? { externalSessionId } : {}),
2208
2216
  metadata: await resolveProtocolMetadata(flags, { cwd: process.cwd() })
2209
2217
  };
2210
2218
 
@@ -2813,9 +2821,12 @@ connect:
2813
2821
  --agent <identifier>
2814
2822
  --model <identifier>
2815
2823
  --method <connectionMethod>
2824
+ --external-session-id <id|null> Store the native agent thread/session id, or clear it with null
2816
2825
  --metadata-json <json> Extra session metadata object
2817
2826
  Returns:
2818
2827
  Session JSON and SESSION_KEY on stderr when available
2828
+ Notes:
2829
+ If --external-session-id is omitted, the CLI may auto-detect Codex or Claude session ids
2819
2830
 
2820
2831
  load-context:
2821
2832
  Purpose:
@@ -2938,6 +2949,7 @@ hook-event:
2938
2949
  --prompt <text>
2939
2950
  --turn-index <n>
2940
2951
  --follow-up-intent <mode> discussion | execution | pending_delivery
2952
+ --external-session-id <id|null> Native agent resume/session id to persist on the attached session
2941
2953
 
2942
2954
  read-context:
2943
2955
  Purpose:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "0.2606011152.0",
3
+ "version": "0.2606021051.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",
@@ -454,6 +454,11 @@ const tools = [
454
454
  type: 'string',
455
455
  enum: ['discussion', 'execution', 'pending_delivery']
456
456
  },
457
+ external_session_id: {
458
+ type: ['string', 'null'],
459
+ description:
460
+ 'Optional native agent resume/session id to persist on the attached Overlord session.'
461
+ },
457
462
  session_key: {
458
463
  type: 'string',
459
464
  description:
@@ -468,6 +473,7 @@ const tools = [
468
473
  prompt: args.prompt,
469
474
  'turn-index': args.turn_index,
470
475
  'follow-up-intent': args.follow_up_intent,
476
+ 'external-session-id': args.external_session_id,
471
477
  'session-key': args.session_key
472
478
  }),
473
479
  subcommand: 'hook-event'
@@ -72,7 +72,29 @@ if turn_index is None:
72
72
  handle.write(str(turn_index))
73
73
 
74
74
  tid = os.environ.get('TICKET_ID', '')
75
- print(json.dumps({'hookType': 'UserPromptSubmit', 'ticketId': tid, 'prompt': text, 'turnIndex': turn_index}))
75
+ session_key = os.environ.get('SESSION_KEY') or ''
76
+ if not session_key:
77
+ encoded = __import__('base64').urlsafe_b64encode(os.getcwd().encode()).decode().rstrip('=')
78
+ session_file = os.path.join(__import__('tempfile').gettempdir(), f'.overlord-session-{encoded}')
79
+ try:
80
+ with open(session_file, encoding='utf-8') as handle:
81
+ persisted = json.load(handle)
82
+ if persisted.get('ticketId') == tid:
83
+ session_key = persisted.get('sessionKey') or ''
84
+ except Exception:
85
+ session_key = ''
86
+
87
+ payload = {
88
+ 'hookType': 'UserPromptSubmit',
89
+ 'ticketId': tid,
90
+ 'prompt': text,
91
+ 'turnIndex': turn_index,
92
+ }
93
+ if sid != 'unknown':
94
+ payload['externalSessionId'] = sid
95
+ if session_key:
96
+ payload['sessionKey'] = session_key
97
+ print(json.dumps(payload))
76
98
  " 2>/dev/null
77
99
  )
78
100
  if [ -z "$PAYLOAD" ]; then
@@ -62,7 +62,29 @@ with open(path, 'w', encoding='utf-8') as handle:
62
62
  handle.write(str(turn_index))
63
63
 
64
64
  tid = os.environ.get('TICKET_ID', '')
65
- print(json.dumps({'hookType': 'UserPromptSubmit', 'ticketId': tid, 'prompt': text, 'turnIndex': turn_index}))
65
+ session_key = os.environ.get('SESSION_KEY') or ''
66
+ if not session_key:
67
+ encoded = __import__('base64').urlsafe_b64encode(os.getcwd().encode()).decode().rstrip('=')
68
+ session_file = os.path.join(__import__('tempfile').gettempdir(), f'.overlord-session-{encoded}')
69
+ try:
70
+ with open(session_file, encoding='utf-8') as handle:
71
+ persisted = json.load(handle)
72
+ if persisted.get('ticketId') == tid:
73
+ session_key = persisted.get('sessionKey') or ''
74
+ except Exception:
75
+ session_key = ''
76
+
77
+ payload = {
78
+ 'hookType': 'UserPromptSubmit',
79
+ 'ticketId': tid,
80
+ 'prompt': text,
81
+ 'turnIndex': turn_index,
82
+ }
83
+ if sid != 'unknown':
84
+ payload['externalSessionId'] = sid
85
+ if session_key:
86
+ payload['sessionKey'] = session_key
87
+ print(json.dumps(payload))
66
88
  " 2>/dev/null
67
89
  )
68
90
  if [ -z "$PAYLOAD" ]; then
@@ -74,7 +74,7 @@ Ordinary deliver artifacts should use `next_steps`, `test_results`, `migration`,
74
74
  ovld protocol revert --objective-id <objective-id>
75
75
  ```
76
76
 
77
- `revert` restores the local working tree to the recorded objective checkpoint and saves a safety ref under `refs/overlord/safety/` first.
77
+ `revert` restores the local working tree to the recorded objective state.
78
78
 
79
79
  ## Record Change Rationales
80
80
 
@@ -1,6 +1,6 @@
1
1
  # Execution Targets And Checkout Paths
2
2
 
3
- Execution targets are canonical rows keyed by a real `device_fingerprint` when known, or by an SSH placeholder until the remote target registers. Organization labels, user access, SSH credential references, and project membership live in separate association rows. Persist a fingerprint per workstation, upsert via `ovld protocol get-device`, then maintain checkout paths via `list-project-resources`, `add-project-resource`, `update-project-resource`, and `update-device` (`ovld protocol help` lists flags).
3
+ Execution targets are canonical rows keyed by a real `device_fingerprint` when known, or by a placeholder until the target registers. Organization labels, user access, project membership, and project checkout paths live in separate association rows. Persist a fingerprint per machine, upsert via `ovld protocol get-device`, then maintain checkout paths via `list-project-resources`, `add-project-resource`, `update-project-resource`, and `update-device` (`ovld protocol help` lists flags).
4
4
 
5
5
  ```bash
6
6
  ovld protocol get-device --device-fingerprint "$OVERLORD_DEVICE_FINGERPRINT"
@@ -46,7 +46,29 @@ with open(path, 'w', encoding='utf-8') as handle:
46
46
  handle.write(str(turn_index))
47
47
 
48
48
  tid = os.environ.get('TICKET_ID', '')
49
- print(json.dumps({'hookType': 'UserPromptSubmit', 'ticketId': tid, 'prompt': text, 'turnIndex': turn_index}))
49
+ session_key = os.environ.get('SESSION_KEY') or ''
50
+ if not session_key:
51
+ encoded = __import__('base64').urlsafe_b64encode(os.getcwd().encode()).decode().rstrip('=')
52
+ session_file = os.path.join(__import__('tempfile').gettempdir(), f'.overlord-session-{encoded}')
53
+ try:
54
+ with open(session_file, encoding='utf-8') as handle:
55
+ persisted = json.load(handle)
56
+ if persisted.get('ticketId') == tid:
57
+ session_key = persisted.get('sessionKey') or ''
58
+ except Exception:
59
+ session_key = ''
60
+
61
+ payload = {
62
+ 'hookType': 'UserPromptSubmit',
63
+ 'ticketId': tid,
64
+ 'prompt': text,
65
+ 'turnIndex': turn_index,
66
+ }
67
+ if cid != 'unknown':
68
+ payload['externalSessionId'] = cid
69
+ if session_key:
70
+ payload['sessionKey'] = session_key
71
+ print(json.dumps(payload))
50
72
  " 2>/dev/null
51
73
  )
52
74
  if [ -n "$PAYLOAD" ]; then
@@ -74,7 +74,7 @@ Ordinary deliver artifacts should use `next_steps`, `test_results`, `migration`,
74
74
  ovld protocol revert --objective-id <objective-id>
75
75
  ```
76
76
 
77
- `revert` restores the local working tree to the recorded objective checkpoint and saves a safety ref under `refs/overlord/safety/` first.
77
+ `revert` restores the local working tree to the recorded objective state.
78
78
 
79
79
  ## Record Change Rationales
80
80
 
@@ -1,6 +1,6 @@
1
1
  # Execution Targets And Checkout Paths
2
2
 
3
- Execution targets are canonical rows keyed by a real `device_fingerprint` when known, or by an SSH placeholder until the remote target registers. Organization labels, user access, SSH credential references, and project membership live in separate association rows. Persist a fingerprint per workstation, upsert via `ovld protocol get-device`, then maintain checkout paths via `list-project-resources`, `add-project-resource`, `update-project-resource`, and `update-device` (`ovld protocol help` lists flags).
3
+ Execution targets are canonical rows keyed by a real `device_fingerprint` when known, or by a placeholder until the target registers. Organization labels, user access, project membership, and project checkout paths live in separate association rows. Persist a fingerprint per machine, upsert via `ovld protocol get-device`, then maintain checkout paths via `list-project-resources`, `add-project-resource`, `update-project-resource`, and `update-device` (`ovld protocol help` lists flags).
4
4
 
5
5
  ```bash
6
6
  ovld protocol get-device --device-fingerprint "$OVERLORD_DEVICE_FINGERPRINT"
@@ -139,6 +139,7 @@ const tools = [
139
139
  },
140
140
  agent: { type: 'string' },
141
141
  method: { type: 'string' },
142
+ external_session_id: { type: ['string', 'null'] },
142
143
  metadata: {
143
144
  type: 'object',
144
145
  description:
@@ -151,6 +152,7 @@ const tools = [
151
152
  'ticket-id': args.ticket_id,
152
153
  agent: args.agent,
153
154
  method: args.method,
155
+ 'external-session-id': args.external_session_id,
154
156
  ...(args.metadata && typeof args.metadata === 'object' && !Array.isArray(args.metadata)
155
157
  ? { 'metadata-json': args.metadata }
156
158
  : {})
@@ -179,7 +181,7 @@ const tools = [
179
181
  {
180
182
  name: 'revert',
181
183
  description:
182
- 'Restore the local working tree to an objective checkpoint (maps to ovld protocol revert).',
184
+ 'Restore the local working tree to the recorded objective state (maps to ovld protocol revert).',
183
185
  inputSchema: {
184
186
  type: 'object',
185
187
  properties: {
@@ -454,6 +456,11 @@ const tools = [
454
456
  type: 'string',
455
457
  enum: ['discussion', 'execution', 'pending_delivery']
456
458
  },
459
+ external_session_id: {
460
+ type: ['string', 'null'],
461
+ description:
462
+ 'Optional native agent resume/session id to persist on the attached Overlord session.'
463
+ },
457
464
  session_key: {
458
465
  type: 'string',
459
466
  description:
@@ -468,6 +475,7 @@ const tools = [
468
475
  prompt: args.prompt,
469
476
  'turn-index': args.turn_index,
470
477
  'follow-up-intent': args.follow_up_intent,
478
+ 'external-session-id': args.external_session_id,
471
479
  'session-key': args.session_key
472
480
  }),
473
481
  subcommand: 'hook-event'
@@ -28,7 +28,40 @@ if [ -z "$OVERLORD_BASE_URL" ] || [ -z "$OVERLORD_TOKEN" ] || [ -z "${TICKET_ID:
28
28
  fi
29
29
 
30
30
  if [ -n "$OVERLORD_BASE_URL" ] && [ -n "$OVERLORD_TOKEN" ] && [ -n "$TICKET_ID" ]; then
31
- PAYLOAD=$(printf '%s' "$BODY" | python3 -c "import json,sys; body=json.load(sys.stdin); print(json.dumps({'hookType': 'UserPromptSubmit', 'ticketId': __import__('os').environ['TICKET_ID'], 'prompt': body.get('prompt', ''), 'turnIndex': body.get('turn_number', 0)}))" 2>/dev/null || echo '')
31
+ PAYLOAD=$(printf '%s' "$BODY" | python3 -c "
32
+ import base64
33
+ import json
34
+ import os
35
+ import sys
36
+ import tempfile
37
+ from pathlib import Path
38
+
39
+ body = json.load(sys.stdin)
40
+ external_session_id = os.environ.get('CODEX_THREAD_ID') or os.environ.get('CODEX_SESSION_ID') or None
41
+ session_key = os.environ.get('SESSION_KEY') or ''
42
+
43
+ if not session_key:
44
+ encoded = base64.urlsafe_b64encode(os.getcwd().encode()).decode().rstrip('=')
45
+ session_file = Path(tempfile.gettempdir()) / f'.overlord-session-{encoded}'
46
+ try:
47
+ persisted = json.loads(session_file.read_text(encoding='utf-8'))
48
+ if persisted.get('ticketId') == os.environ.get('TICKET_ID'):
49
+ session_key = persisted.get('sessionKey') or ''
50
+ except Exception:
51
+ session_key = ''
52
+
53
+ payload = {
54
+ 'hookType': 'UserPromptSubmit',
55
+ 'ticketId': os.environ['TICKET_ID'],
56
+ 'prompt': body.get('prompt', ''),
57
+ 'turnIndex': body.get('turn_number', 0),
58
+ }
59
+ if external_session_id:
60
+ payload['externalSessionId'] = external_session_id
61
+ if session_key:
62
+ payload['sessionKey'] = session_key
63
+ print(json.dumps(payload))
64
+ " 2>/dev/null || echo '')
32
65
  if [ -z "$PAYLOAD" ]; then
33
66
  log_hook "failed to build hook-event payload from submit body"
34
67
  exit 0
@@ -74,7 +74,7 @@ Ordinary deliver artifacts should use `next_steps`, `test_results`, `migration`,
74
74
  ovld protocol revert --objective-id <objective-id>
75
75
  ```
76
76
 
77
- `revert` restores the local working tree to the recorded objective checkpoint and saves a safety ref under `refs/overlord/safety/` first.
77
+ `revert` restores the local working tree to the recorded objective state.
78
78
 
79
79
  ## Record Change Rationales
80
80
 
@@ -1,6 +1,6 @@
1
1
  # Execution Targets And Checkout Paths
2
2
 
3
- Execution targets are canonical rows keyed by a real `device_fingerprint` when known, or by an SSH placeholder until the remote target registers. Organization labels, user access, SSH credential references, and project membership live in separate association rows. Persist a fingerprint per workstation, upsert via `ovld protocol get-device`, then maintain checkout paths via `list-project-resources`, `add-project-resource`, `update-project-resource`, and `update-device` (`ovld protocol help` lists flags).
3
+ Execution targets are canonical rows keyed by a real `device_fingerprint` when known, or by a placeholder until the target registers. Organization labels, user access, project membership, and project checkout paths live in separate association rows. Persist a fingerprint per machine, upsert via `ovld protocol get-device`, then maintain checkout paths via `list-project-resources`, `add-project-resource`, `update-project-resource`, and `update-device` (`ovld protocol help` lists flags).
4
4
 
5
5
  ```bash
6
6
  ovld protocol get-device --device-fingerprint "$OVERLORD_DEVICE_FINGERPRINT"