overlord-cli 0.2605310708.0 → 0.2605312007.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
@@ -149,10 +149,11 @@ Agents can find docs here: https://www.ovld.ai/docs/for-agents
149
149
  - `get-device` / `update-device` - register the caller machine (per organization + user + fingerprint) and rename its label
150
150
  - `list-project-resources` / `add-project-resource` / `update-project-resource` - manage filesystem directories tied to a project device
151
151
  - `request-execution` / `claim-execution` / `complete-execution-launch` / `fail-execution-launch` - durable runner queue operations used by auto-advance and manual Run
152
+ - `list-organizations` - list every organization the authenticated user (or agent token) belongs to; the runner uses this to poll all of your organizations, not just the one stored at login
152
153
 
153
154
  Devices are keyed by **(organization, user, fingerprint)** so the same physical workstation can appear once per org session.
154
155
 
155
- `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.
156
+ `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. 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`.
156
157
 
157
158
  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.
158
159
 
@@ -2049,6 +2049,24 @@ async function protocolClaimExecution(args) {
2049
2049
  console.log(JSON.stringify(data, null, 2));
2050
2050
  }
2051
2051
 
2052
+ async function protocolListOrganizations(args) {
2053
+ const flags = parseFlags(args);
2054
+ const { platformUrl, bearerToken, localSecret, organizationId } =
2055
+ await resolveProtocolAuthForFlags(flags);
2056
+ const timeoutMs = resolveTimeout(flags);
2057
+
2058
+ const data = await apiPost(
2059
+ platformUrl,
2060
+ bearerToken,
2061
+ localSecret,
2062
+ organizationId,
2063
+ '/api/protocol/organizations',
2064
+ {},
2065
+ timeoutMs
2066
+ );
2067
+ console.log(JSON.stringify(data, null, 2));
2068
+ }
2069
+
2052
2070
  async function protocolCompleteExecutionLaunch(args) {
2053
2071
  const flags = parseFlags(args);
2054
2072
  const { platformUrl, bearerToken, localSecret, organizationId } =
@@ -2644,6 +2662,7 @@ Subcommands:
2644
2662
  request-approval-gate Flip auto_advance=false on the next queued future objective
2645
2663
  request-execution Queue an objective for local/remote runner execution
2646
2664
  claim-execution Claim one queued execution request for this device
2665
+ list-organizations List organizations the authenticated user belongs to
2647
2666
  complete-execution-launch Mark a claimed execution request launched
2648
2667
  fail-execution-launch Mark a claimed execution request failed
2649
2668
  permission-request Notify Overlord that the agent is requesting tool permission
@@ -3139,6 +3158,12 @@ claim-execution:
3139
3158
  --lease-seconds <n>
3140
3159
  --project-id <uuid>
3141
3160
 
3161
+ list-organizations:
3162
+ Purpose:
3163
+ Print every organization the authenticated user (or agent token) belongs to.
3164
+ The runner uses this to poll all of your organizations, not just the one
3165
+ stored at login. Takes no required flags.
3166
+
3142
3167
  complete-execution-launch:
3143
3168
  Required:
3144
3169
  --request-id <uuid> (or EXECUTION_REQUEST_ID env var)
@@ -3184,6 +3209,7 @@ Examples:
3184
3209
  ovld protocol attachment-download-url --session-key <key> --attachment-id <attachment-id>
3185
3210
  ovld protocol request-execution --ticket-id 1:899 --agent codex --requested-from manual_run
3186
3211
  ovld protocol claim-execution --device-fingerprint $OVERLORD_DEVICE_FINGERPRINT
3212
+ ovld protocol list-organizations
3187
3213
  ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --summary "Done"
3188
3214
  ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --summary "Done" --artifacts-file ./artifacts.json
3189
3215
  ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --payload-json '{"summary":"Done","artifacts":[{"type":"note","label":"Delivery","content":"..."}]}'
@@ -3303,6 +3329,10 @@ EOF
3303
3329
  await protocolClaimExecution(args);
3304
3330
  return;
3305
3331
  }
3332
+ if (subcommand === 'list-organizations') {
3333
+ await protocolListOrganizations(args);
3334
+ return;
3335
+ }
3306
3336
  if (subcommand === 'complete-execution-launch') {
3307
3337
  await protocolCompleteExecutionLaunch(args);
3308
3338
  return;
@@ -27,8 +27,12 @@ Options:
27
27
  --device-fingerprint <fp> Override runner device identity
28
28
  --poll-interval-ms <ms> Poll interval for start mode (default 3000)
29
29
  --project-id <uuid> Only claim requests for one project
30
+ --organization-id <id> Only poll one organization (also OVERLORD_ORGANIZATION_ID).
31
+ Default: poll every organization you belong to.
30
32
 
31
33
  The runner claims durable execution requests and launches them with \`${primaryCommand} launch\`.
34
+ By default it drains queued requests from all of your organizations, so a single
35
+ runner serves every org without needing one instance per org.
32
36
  `);
33
37
  }
34
38
 
@@ -89,7 +93,7 @@ function protocolJson(subcommand, args, env) {
89
93
  return stdout.trim() ? JSON.parse(stdout) : {};
90
94
  }
91
95
 
92
- function claimExecution(flags, deviceFingerprint) {
96
+ function claimExecution(flags, deviceFingerprint, organizationId) {
93
97
  const args = [
94
98
  '--device-fingerprint',
95
99
  deviceFingerprint,
@@ -98,12 +102,82 @@ function claimExecution(flags, deviceFingerprint) {
98
102
  '--device-platform',
99
103
  process.platform
100
104
  ];
105
+ // An explicit organization id scopes this claim to one org. The CLI turns it
106
+ // into the `x-organization-id` header, which OAuth-authenticated protocol
107
+ // requests require and the server validates against the user's membership.
108
+ if (organizationId) {
109
+ args.push('--organization-id', String(organizationId));
110
+ }
101
111
  if (typeof flags['project-id'] === 'string') {
102
112
  args.push('--project-id', flags['project-id']);
103
113
  }
104
114
  return protocolJson('claim-execution', args, { OVERLORD_DEVICE_FINGERPRINT: deviceFingerprint });
105
115
  }
106
116
 
117
+ function listOrganizationIds(deviceFingerprint) {
118
+ const data = protocolJson('list-organizations', [], {
119
+ OVERLORD_DEVICE_FINGERPRINT: deviceFingerprint
120
+ });
121
+ const organizations = Array.isArray(data.organizations) ? data.organizations : [];
122
+ return organizations
123
+ .map(organization => organization?.id)
124
+ .filter(id => Number.isInteger(id) && id > 0)
125
+ .map(id => String(id));
126
+ }
127
+
128
+ function resolvePinnedOrganizationId(flags) {
129
+ const fromFlag =
130
+ typeof flags['organization-id'] === 'string' ? flags['organization-id'].trim() : '';
131
+ if (fromFlag) return fromFlag;
132
+ const fromEnv = process.env.OVERLORD_ORGANIZATION_ID?.trim();
133
+ return fromEnv || '';
134
+ }
135
+
136
+ // Re-discover the user's organizations periodically so a runner picks up newly
137
+ // joined orgs without a restart, without hitting the API on every poll.
138
+ const ORGANIZATION_REFRESH_MS = 60_000;
139
+
140
+ /**
141
+ * Resolves which organizations a poll pass should claim from.
142
+ *
143
+ * - When the user pins an org (`--organization-id` flag or
144
+ * `OVERLORD_ORGANIZATION_ID`), only that org is polled.
145
+ * - Otherwise the runner discovers every org the authenticated user belongs to
146
+ * and polls each one. If discovery fails or returns nothing, it falls back to
147
+ * the credential default (an empty scope id) so behavior never regresses.
148
+ */
149
+ export function createOrganizationScope(flags, deviceFingerprint) {
150
+ const pinned = resolvePinnedOrganizationId(flags);
151
+ if (pinned) {
152
+ return { pinned: true, resolve: () => [pinned] };
153
+ }
154
+
155
+ let cache = [];
156
+ let fetchedAt = 0;
157
+ return {
158
+ pinned: false,
159
+ resolve() {
160
+ const now = Date.now();
161
+ if (cache.length === 0 || now - fetchedAt >= ORGANIZATION_REFRESH_MS) {
162
+ try {
163
+ const ids = listOrganizationIds(deviceFingerprint);
164
+ fetchedAt = now;
165
+ // Keep the last known good list on a transient empty result.
166
+ if (ids.length > 0) cache = ids;
167
+ } catch (error) {
168
+ process.stderr.write(
169
+ `[runner] could not list organizations (${
170
+ error instanceof Error ? error.message : error
171
+ }); using the default organization\n`
172
+ );
173
+ }
174
+ }
175
+ // Empty string = "no override", i.e. the org stored in credentials.
176
+ return cache.length > 0 ? cache : [''];
177
+ }
178
+ };
179
+ }
180
+
107
181
  function completeLaunch(requestId, deviceFingerprint) {
108
182
  protocolJson(
109
183
  'complete-execution-launch',
@@ -275,11 +349,21 @@ function spawnLaunchProcess(args, claim, deviceFingerprint) {
275
349
  runnerTestHooks.platform ?? process.platform
276
350
  );
277
351
 
352
+ // Phase 4: thread the execution request id to the launched process so the
353
+ // agent's `ovld protocol attach` can mark this exact request `launched`.
354
+ // Attach falls back to matching by objective when this is absent, so this is
355
+ // a precision aid, not a correctness requirement.
356
+ const launchEnv = {
357
+ ...process.env,
358
+ OVERLORD_DEVICE_FINGERPRINT: deviceFingerprint,
359
+ ...(claim.request?.id ? { OVERLORD_EXECUTION_REQUEST_ID: claim.request.id } : {})
360
+ };
361
+
278
362
  if (!terminalCommand) {
279
363
  return {
280
364
  child: spawnImpl(process.execPath, [OVLD_ENTRY, ...args], {
281
365
  stdio: 'inherit',
282
- env: { ...process.env, OVERLORD_DEVICE_FINGERPRINT: deviceFingerprint }
366
+ env: launchEnv
283
367
  }),
284
368
  completeOnClose: false
285
369
  };
@@ -288,7 +372,7 @@ function spawnLaunchProcess(args, claim, deviceFingerprint) {
288
372
  return {
289
373
  child: spawnImpl('sh', ['-lc', terminalCommand], {
290
374
  stdio: 'inherit',
291
- env: { ...process.env, OVERLORD_DEVICE_FINGERPRINT: deviceFingerprint }
375
+ env: launchEnv
292
376
  }),
293
377
  completeOnClose: true
294
378
  };
@@ -348,14 +432,34 @@ export async function launchClaimedRequest(claim, deviceFingerprint) {
348
432
  return true;
349
433
  }
350
434
 
351
- export async function runOnce(flags, deviceFingerprint) {
352
- const claim = claimExecution(flags, deviceFingerprint);
353
- if (!claim.request) {
435
+ export async function runOnce(flags, deviceFingerprint, organizationScope) {
436
+ const scope = organizationScope ?? createOrganizationScope(flags, deviceFingerprint);
437
+ const organizationIds = scope.resolve();
438
+
439
+ let launchedAny = false;
440
+ for (const organizationId of organizationIds) {
441
+ let claim;
442
+ try {
443
+ claim = claimExecution(flags, deviceFingerprint, organizationId);
444
+ } catch (error) {
445
+ // A single organization failing (transient API/auth error) must not stop
446
+ // the runner from servicing the others.
447
+ process.stderr.write(
448
+ `[runner] claim failed for organization ${organizationId || 'default'}: ${
449
+ error instanceof Error ? error.message : error
450
+ }\n`
451
+ );
452
+ continue;
453
+ }
454
+ if (!claim.request) continue;
455
+ await launchClaimedRequest(claim, deviceFingerprint);
456
+ launchedAny = true;
457
+ }
458
+
459
+ if (!launchedAny) {
354
460
  process.stderr.write('[runner] no queued execution requests\n');
355
- return false;
356
461
  }
357
- await launchClaimedRequest(claim, deviceFingerprint);
358
- return true;
462
+ return launchedAny;
359
463
  }
360
464
 
361
465
  function sleep(ms) {
@@ -398,10 +502,18 @@ export async function runRunnerCommand(subcommand, args, primaryCommand = 'ovld'
398
502
  typeof flags['poll-interval-ms'] === 'string'
399
503
  ? Math.max(1000, Number.parseInt(flags['poll-interval-ms'], 10) || 3000)
400
504
  : 3000;
505
+ // Build the organization scope once so its discovery cache persists across
506
+ // polls instead of refetching the org list every iteration.
507
+ const organizationScope = createOrganizationScope(flags, deviceFingerprint);
401
508
  process.stderr.write(`[runner] started with device ${deviceFingerprint}\n`);
509
+ process.stderr.write(
510
+ organizationScope.pinned
511
+ ? `[runner] polling a single organization\n`
512
+ : `[runner] polling every organization you belong to\n`
513
+ );
402
514
  for (;;) {
403
515
  try {
404
- const launched = await runOnce(flags, deviceFingerprint);
516
+ const launched = await runOnce(flags, deviceFingerprint, organizationScope);
405
517
  if (!launched) await sleep(pollIntervalMs);
406
518
  } catch (error) {
407
519
  process.stderr.write(`[runner] ${error instanceof Error ? error.message : error}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "0.2605310708.0",
3
+ "version": "0.2605312007.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",