overlord-cli 0.2606010745.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
@@ -158,6 +158,8 @@ Devices are keyed by **(organization, user, fingerprint)** so the same physical
158
158
 
159
159
  `ovld runner start` keeps a foreground runner alive for CLI-only or remote hosts. It claims compatible execution requests for the current device and launches the assigned agent/model with `ovld launch`; `ovld runner once` claims at most one request and exits. `ovld runner status` now prints the local device record plus the active queue visible to that runner. `ovld runner clear <objective_id>` clears one queued objective, and `ovld runner clear-all` clears every active queue row visible to the caller. By default a single runner drains queued requests from **every organization you belong to**, so you do not need one runner per org. Pin it to a single org with `--organization-id <id>` or `OVERLORD_ORGANIZATION_ID`.
160
160
 
161
+ If you are troubleshooting "why is `ovld runner` randomly executing", the runner is usually claiming a request that was already queued by manual Run or auto-advance. Inspect the queue with `ovld runner status`, then clear the specific objective with `ovld runner clear 8974e557-bec4-4984-b12c-be46bd63207c` or `ovld protocol clear-execution-requests --objective-id 8974e557-bec4-4984-b12c-be46bd63207c`.
162
+
161
163
  Use `create` for future work you want to track, `prompt` for work that should start immediately, and `record-work` for work that was already completed in chat and now needs a review ticket plus feed post. Use multiple tickets when prompts represent different features or goals; use `add-objectives` or `--objectives-json '[{"objective":"Step one"},{"objective":"Step two"}]'` when prompts are sequential steps toward the same feature or goal.
162
164
 
163
165
  `ovld protocol deliver` accepts either discrete flags like `--summary` / `--artifacts-json`, an inline full payload with `--payload-json '{"summary":"...","artifacts":[...],"changeRationales":[...]}'`, or a file/stdin payload with `--payload-file <path|->`.
@@ -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
+ }
@@ -85,6 +85,18 @@ async function resolveProtocolMetadata(flags = {}, base = {}) {
85
85
  metadata.model = modelIdentifier;
86
86
  }
87
87
 
88
+ // Thread the originating execution request id (set by the runner via
89
+ // OVERLORD_EXECUTION_REQUEST_ID, or an explicit flag) into attach metadata so
90
+ // the server marks that exact request `launched` instead of falling back to
91
+ // matching by objective.
92
+ const executionRequestId =
93
+ (typeof flags['execution-request-id'] === 'string'
94
+ ? flags['execution-request-id'].trim()
95
+ : '') || process.env.OVERLORD_EXECUTION_REQUEST_ID?.trim();
96
+ if (executionRequestId) {
97
+ metadata.executionRequestId = executionRequestId;
98
+ }
99
+
88
100
  const snapshot = await resolveSnapshotContext(flags);
89
101
  if (snapshot) {
90
102
  metadata.snapshot = snapshot;
@@ -1284,6 +1296,12 @@ async function protocolHookEvent(args) {
1284
1296
  ? { turnIndex: parseInt(String(flags['turn-index']), 10) }
1285
1297
  : {}),
1286
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
+ })(),
1287
1305
  ...(sessionKey ? { sessionKey } : {})
1288
1306
  };
1289
1307
 
@@ -2188,11 +2206,13 @@ async function protocolConnect(args) {
2188
2206
  const { platformUrl, bearerToken, localSecret, organizationId } =
2189
2207
  await resolveProtocolAuthForFlags(flags, ticketId);
2190
2208
  const timeoutMs = resolveTimeout(flags);
2209
+ const externalSessionId = resolveExternalSessionId(flags);
2191
2210
 
2192
2211
  const body = {
2193
2212
  ticketId,
2194
2213
  agentIdentifier: resolveProtocolAgentIdentifier(flags),
2195
2214
  connectionMethod: String(flags.method ?? 'cli'),
2215
+ ...(externalSessionId !== undefined ? { externalSessionId } : {}),
2196
2216
  metadata: await resolveProtocolMetadata(flags, { cwd: process.cwd() })
2197
2217
  };
2198
2218
 
@@ -2801,9 +2821,12 @@ connect:
2801
2821
  --agent <identifier>
2802
2822
  --model <identifier>
2803
2823
  --method <connectionMethod>
2824
+ --external-session-id <id|null> Store the native agent thread/session id, or clear it with null
2804
2825
  --metadata-json <json> Extra session metadata object
2805
2826
  Returns:
2806
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
2807
2830
 
2808
2831
  load-context:
2809
2832
  Purpose:
@@ -2926,6 +2949,7 @@ hook-event:
2926
2949
  --prompt <text>
2927
2950
  --turn-index <n>
2928
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
2929
2953
 
2930
2954
  read-context:
2931
2955
  Purpose:
@@ -171,13 +171,17 @@ function resolvePinnedOrganizationId(flags) {
171
171
  const ORGANIZATION_REFRESH_MS = 60_000;
172
172
 
173
173
  /**
174
- * Resolves which organizations a poll pass should claim from.
174
+ * Resolves which organizations a fan-out pass (list / clear-all) should act on.
175
175
  *
176
176
  * - When the user pins an org (`--organization-id` flag or
177
- * `OVERLORD_ORGANIZATION_ID`), only that org is polled.
178
- * - Otherwise the runner discovers every org the authenticated user belongs to
179
- * and polls each one. If discovery fails or returns nothing, it falls back to
180
- * the credential default (an empty scope id) so behavior never regresses.
177
+ * `OVERLORD_ORGANIZATION_ID`), only that org is used.
178
+ * - Otherwise the runner discovers every org the authenticated user belongs to.
179
+ * If discovery fails or returns nothing, it falls back to the credential
180
+ * default (an empty scope id) so behavior never regresses.
181
+ *
182
+ * NOTE: this is for the per-org fan-out commands. The claim poll uses
183
+ * {@link createClaimOrganizationScope}, which is org-agnostic — the server's
184
+ * `claim-execution` is authoritative and spans the user's target-sharing orgs.
181
185
  */
182
186
  export function createOrganizationScope(flags, deviceFingerprint) {
183
187
  const pinned = resolvePinnedOrganizationId(flags);
@@ -211,6 +215,17 @@ export function createOrganizationScope(flags, deviceFingerprint) {
211
215
  };
212
216
  }
213
217
 
218
+ /**
219
+ * Resolves the org scope for the claim poll (G3). A pinned org still scopes the
220
+ * poll; otherwise the runner does a single org-agnostic poll (empty scope) and
221
+ * lets the server claim across every org the user belongs to that shares the
222
+ * claiming target — no org discovery or per-org looping needed.
223
+ */
224
+ export function createClaimOrganizationScope(flags) {
225
+ const pinned = resolvePinnedOrganizationId(flags);
226
+ return { pinned: Boolean(pinned), resolve: () => [pinned || ''] };
227
+ }
228
+
214
229
  function completeLaunch(requestId, deviceFingerprint) {
215
230
  protocolJson(
216
231
  'complete-execution-launch',
@@ -276,11 +291,19 @@ function normalizeRunnerTerminalProfile(value) {
276
291
  };
277
292
  }
278
293
 
279
- function buildRunnerLaunchShellCommand(args, deviceFingerprint) {
280
- return [
281
- `export OVERLORD_DEVICE_FINGERPRINT=${shellQuote(deviceFingerprint)}`,
294
+ function buildRunnerLaunchShellCommand(args, deviceFingerprint, executionRequestId) {
295
+ const lines = [`export OVERLORD_DEVICE_FINGERPRINT=${shellQuote(deviceFingerprint)}`];
296
+ // Terminal-profile launches run this generated script in a fresh shell, so the
297
+ // request id must be exported here too (the direct-spawn path inherits it via
298
+ // the child env). Without it, attach in the new terminal falls back to
299
+ // objective matching.
300
+ if (executionRequestId) {
301
+ lines.push(`export OVERLORD_EXECUTION_REQUEST_ID=${shellQuote(executionRequestId)}`);
302
+ }
303
+ lines.push(
282
304
  `exec ${shellQuote(process.execPath)} ${[OVLD_ENTRY, ...args].map(shellQuote).join(' ')}`
283
- ].join('\n');
305
+ );
306
+ return lines.join('\n');
284
307
  }
285
308
 
286
309
  function writeRunnerLaunchScript(launchCommand) {
@@ -375,7 +398,7 @@ export function buildRunnerTerminalOpenCommand(
375
398
 
376
399
  function spawnLaunchProcess(args, claim, deviceFingerprint) {
377
400
  const spawnImpl = runnerTestHooks.spawn ?? spawn;
378
- const launchCommand = buildRunnerLaunchShellCommand(args, deviceFingerprint);
401
+ const launchCommand = buildRunnerLaunchShellCommand(args, deviceFingerprint, claim.request?.id);
379
402
  const terminalCommand = buildRunnerTerminalOpenCommand(
380
403
  claim.launch?.runnerTerminalProfile,
381
404
  launchCommand,
@@ -466,7 +489,7 @@ export async function launchClaimedRequest(claim, deviceFingerprint) {
466
489
  }
467
490
 
468
491
  export async function runOnce(flags, deviceFingerprint, organizationScope) {
469
- const scope = organizationScope ?? createOrganizationScope(flags, deviceFingerprint);
492
+ const scope = organizationScope ?? createClaimOrganizationScope(flags);
470
493
  const organizationIds = scope.resolve();
471
494
 
472
495
  let launchedAny = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "0.2606010745.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"