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 +2 -1
- package/bin/_cli/protocol.mjs +30 -0
- package/bin/_cli/runner.mjs +122 -10
- package/package.json +1 -1
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
|
|
package/bin/_cli/protocol.mjs
CHANGED
|
@@ -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;
|
package/bin/_cli/runner.mjs
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
353
|
-
|
|
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
|
-
|
|
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`);
|