overlord-cli 0.2605210944.0 → 0.2605211800.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
@@ -68,6 +68,7 @@ ovld protocol discover-project
68
68
  ovld protocol attach --ticket-id <ticket_id>
69
69
  ovld protocol search-tickets --query "auth refactor" --status next-up,execute
70
70
  ovld protocol update --session-key <session-key> --ticket-id <ticket_id> --summary "Working on it" --phase execute
71
+ ovld runner start
71
72
  ovld setup
72
73
  ovld setup codex
73
74
  ovld setup claude
@@ -100,6 +101,7 @@ Top-level commands (see `ovld help`):
100
101
  - `tickets` - `create` or `list` (optional `--status`)
101
102
  - `ticket` - `context <ticketId>` to print context for one ticket
102
103
  - `launch`, `restart` - launch or resume an agent session
104
+ - `runner` - claim queued execution requests and launch assigned agents with `ovld launch`
103
105
  - `connect`, `run`, `resume` - legacy aliases for `launch` and `restart`
104
106
  - `setup` - install the Overlord connector for an agent; `ovld setup [agent|all]` (interactive with no args). `ovld setup claude` also performs the one-time v3.25.0 to v4 Claude plugin migration
105
107
  - `update` - install the latest CLI release from npm
@@ -137,9 +139,12 @@ Agents can find docs here: https://www.ovld.ai/docs/for-agents
137
139
  - `attachment-upload-file` - prepare, upload, and finalize a local objective attachment in one command
138
140
  - `get-device` / `update-device` - register the caller machine (per organization + user + fingerprint) and rename its label
139
141
  - `list-project-resources` / `add-project-resource` / `update-project-resource` - manage filesystem directories tied to a project device
142
+ - `request-execution` / `claim-execution` / `complete-execution-launch` / `fail-execution-launch` - durable runner queue operations used by auto-advance and manual Run
140
143
 
141
144
  Devices are keyed by **(organization, user, fingerprint)** so the same physical workstation can appear once per org session.
142
145
 
146
+ `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.
147
+
143
148
  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.
144
149
 
145
150
  `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|->`.
@@ -5,6 +5,7 @@ import { runAuthCommand } from './auth.mjs';
5
5
  import { checkForCliUpdate, printCliUpdateNotice, runCliUpdateCommand } from './cli-update.mjs';
6
6
  import { runLauncherCommand } from './launcher.mjs';
7
7
  import { runProtocolCommand } from './protocol.mjs';
8
+ import { runRunnerCommand } from './runner.mjs';
8
9
  import { runDoctorCommand, runSetupCommand } from './setup.mjs';
9
10
  import { runTicketCommand } from './ticket.mjs';
10
11
  import { runTicketsCommand } from './tickets.mjs';
@@ -35,6 +36,7 @@ Usage:
35
36
  ${primaryCommand} tickets <subcommand> Create or list tickets
36
37
  ${primaryCommand} ticket <subcommand> Work with a single ticket
37
38
  ${primaryCommand} protocol <subcommand> Agent workflow commands
39
+ ${primaryCommand} runner <subcommand> Claim and launch queued execution requests
38
40
  ${primaryCommand} launch <agent> Launch an agent on a ticket
39
41
  ${primaryCommand} connect <agent> Launch an agent on a ticket (legacy alias)
40
42
  ${primaryCommand} restart <agent> Resume an agent session
@@ -127,6 +129,11 @@ export async function runCli({ primaryCommand }) {
127
129
  return;
128
130
  }
129
131
 
132
+ if (command === 'runner') {
133
+ await runRunnerCommand(rest[0], rest.slice(1), primaryCommand);
134
+ return;
135
+ }
136
+
130
137
  if (command === 'setup') {
131
138
  await runSetupCommand(rest);
132
139
  return;
@@ -230,6 +230,23 @@ function resolveTimeout(flags) {
230
230
  return DEFAULT_TIMEOUT_MS;
231
231
  }
232
232
 
233
+ function parseRepeatedCliFlags(args, name) {
234
+ const result = [];
235
+ const prefix = `--${name}=`;
236
+ for (let i = 0; i < args.length; i++) {
237
+ const arg = args[i];
238
+ if (arg === `--${name}` && i + 1 < args.length) {
239
+ result.push(args[i + 1]);
240
+ i++;
241
+ continue;
242
+ }
243
+ if (arg.startsWith(prefix)) {
244
+ result.push(arg.slice(prefix.length));
245
+ }
246
+ }
247
+ return result;
248
+ }
249
+
233
250
  function requireFlag(flags, name, envAlias) {
234
251
  const value = flags[name] ?? (envAlias ? process.env[envAlias] : undefined);
235
252
  if (!value) {
@@ -1821,6 +1838,155 @@ async function protocolUpdateProjectResource(args) {
1821
1838
  console.log(JSON.stringify(data, null, 2));
1822
1839
  }
1823
1840
 
1841
+ // ---------------------------------------------------------------------------
1842
+ // execution requests (durable runner queue)
1843
+ // ---------------------------------------------------------------------------
1844
+
1845
+ async function protocolRequestExecution(args) {
1846
+ const flags = parseFlags(args);
1847
+ const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
1848
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1849
+ await resolveProtocolAuthForFlags(flags, ticketId);
1850
+ const timeoutMs = resolveTimeout(flags);
1851
+
1852
+ const body = {
1853
+ ticketId,
1854
+ ...(typeof flags['objective-id'] === 'string' ? { objectiveId: flags['objective-id'] } : {}),
1855
+ requestedFrom: String(flags['requested-from'] ?? 'api'),
1856
+ ...(typeof flags['idempotency-key'] === 'string'
1857
+ ? { idempotencyKey: flags['idempotency-key'] }
1858
+ : {}),
1859
+ ...(typeof flags.agent === 'string' ? { agentIdentifier: flags.agent } : {}),
1860
+ ...(typeof flags.model === 'string' ? { modelIdentifier: flags.model } : {}),
1861
+ ...(typeof flags.thinking === 'string' ? { thinkingLevel: flags.thinking } : {}),
1862
+ launchMode: flags['launch-mode'] === 'ask' ? 'ask' : 'run',
1863
+ flags: parseRepeatedCliFlags(args, 'flag'),
1864
+ ...(typeof flags['working-directory'] === 'string'
1865
+ ? { workingDirectory: flags['working-directory'] }
1866
+ : {}),
1867
+ ...(typeof flags['ssh-command'] === 'string' ? { sshCommand: flags['ssh-command'] } : {}),
1868
+ ...(typeof flags['remote-working-directory'] === 'string'
1869
+ ? { remoteWorkingDirectory: flags['remote-working-directory'] }
1870
+ : {}),
1871
+ ...(flags['server-multiplexer'] === 'tmux' ? { serverMultiplexer: 'tmux' } : {}),
1872
+ ...(typeof flags['tmux-command'] === 'string' ? { tmuxCommand: flags['tmux-command'] } : {}),
1873
+ ...(typeof flags['target-kind'] === 'string' ? { targetKind: flags['target-kind'] } : {}),
1874
+ ...(typeof flags['target-device-id'] === 'string'
1875
+ ? { targetDeviceId: flags['target-device-id'] }
1876
+ : {}),
1877
+ ...(typeof flags['target-resource-id'] === 'string'
1878
+ ? { targetResourceId: flags['target-resource-id'] }
1879
+ : {})
1880
+ };
1881
+
1882
+ const data = await apiPost(
1883
+ platformUrl,
1884
+ bearerToken,
1885
+ localSecret,
1886
+ organizationId,
1887
+ '/api/protocol/request-execution',
1888
+ body,
1889
+ timeoutMs
1890
+ );
1891
+ console.log(JSON.stringify(data, null, 2));
1892
+
1893
+ if (data.request?.id) {
1894
+ process.stderr.write(`\nEXECUTION_REQUEST_ID=${data.request.id}\n`);
1895
+ }
1896
+ }
1897
+
1898
+ async function protocolClaimExecution(args) {
1899
+ const flags = parseFlags(args);
1900
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1901
+ await resolveProtocolAuthForFlags(flags);
1902
+ const timeoutMs = resolveTimeout(flags);
1903
+ const deviceFingerprint = String(
1904
+ flags['device-fingerprint'] ?? process.env.OVERLORD_DEVICE_FINGERPRINT ?? ''
1905
+ );
1906
+ if (!deviceFingerprint) {
1907
+ console.error('Error: --device-fingerprint is required (or set OVERLORD_DEVICE_FINGERPRINT)');
1908
+ process.exit(1);
1909
+ }
1910
+
1911
+ const data = await apiPost(
1912
+ platformUrl,
1913
+ bearerToken,
1914
+ localSecret,
1915
+ organizationId,
1916
+ '/api/protocol/claim-execution',
1917
+ {
1918
+ deviceFingerprint,
1919
+ ...(flags['device-hostname'] ? { deviceHostname: String(flags['device-hostname']) } : {}),
1920
+ ...(flags['device-platform'] ? { devicePlatform: String(flags['device-platform']) } : {}),
1921
+ ...(typeof flags['lease-seconds'] === 'string'
1922
+ ? { leaseSeconds: Number.parseInt(flags['lease-seconds'], 10) }
1923
+ : {}),
1924
+ ...(typeof flags['project-id'] === 'string' ? { projectId: flags['project-id'] } : {})
1925
+ },
1926
+ timeoutMs
1927
+ );
1928
+ console.log(JSON.stringify(data, null, 2));
1929
+ }
1930
+
1931
+ async function protocolCompleteExecutionLaunch(args) {
1932
+ const flags = parseFlags(args);
1933
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1934
+ await resolveProtocolAuthForFlags(flags);
1935
+ const timeoutMs = resolveTimeout(flags);
1936
+ const requestId = requireFlag(flags, 'request-id', 'EXECUTION_REQUEST_ID');
1937
+ const deviceFingerprint = String(
1938
+ flags['device-fingerprint'] ?? process.env.OVERLORD_DEVICE_FINGERPRINT ?? ''
1939
+ );
1940
+ if (!deviceFingerprint) {
1941
+ console.error('Error: --device-fingerprint is required (or set OVERLORD_DEVICE_FINGERPRINT)');
1942
+ process.exit(1);
1943
+ }
1944
+
1945
+ const data = await apiPost(
1946
+ platformUrl,
1947
+ bearerToken,
1948
+ localSecret,
1949
+ organizationId,
1950
+ '/api/protocol/complete-execution-launch',
1951
+ {
1952
+ requestId,
1953
+ deviceFingerprint,
1954
+ ...(typeof flags['launched-session-id'] === 'string'
1955
+ ? { launchedSessionId: flags['launched-session-id'] }
1956
+ : {})
1957
+ },
1958
+ timeoutMs
1959
+ );
1960
+ console.log(JSON.stringify(data, null, 2));
1961
+ }
1962
+
1963
+ async function protocolFailExecutionLaunch(args) {
1964
+ const flags = parseFlags(args);
1965
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1966
+ await resolveProtocolAuthForFlags(flags);
1967
+ const timeoutMs = resolveTimeout(flags);
1968
+ const requestId = requireFlag(flags, 'request-id', 'EXECUTION_REQUEST_ID');
1969
+ const deviceFingerprint = String(
1970
+ flags['device-fingerprint'] ?? process.env.OVERLORD_DEVICE_FINGERPRINT ?? ''
1971
+ );
1972
+ if (!deviceFingerprint) {
1973
+ console.error('Error: --device-fingerprint is required (or set OVERLORD_DEVICE_FINGERPRINT)');
1974
+ process.exit(1);
1975
+ }
1976
+ const error = requireFlag(flags, 'error');
1977
+
1978
+ const data = await apiPost(
1979
+ platformUrl,
1980
+ bearerToken,
1981
+ localSecret,
1982
+ organizationId,
1983
+ '/api/protocol/fail-execution-launch',
1984
+ { requestId, deviceFingerprint, error },
1985
+ timeoutMs
1986
+ );
1987
+ console.log(JSON.stringify(data, null, 2));
1988
+ }
1989
+
1824
1990
  // ---------------------------------------------------------------------------
1825
1991
  // connect (lightweight session, no context returned)
1826
1992
  // ---------------------------------------------------------------------------
@@ -2349,6 +2515,10 @@ Subcommands:
2349
2515
  record-change-rationales Persist structured change rationales without a progress update
2350
2516
  ask Post a blocking question and move the ticket to review
2351
2517
  request-approval-gate Flip auto_advance=false on the next queued future objective
2518
+ request-execution Queue an objective for local/remote runner execution
2519
+ claim-execution Claim one queued execution request for this device
2520
+ complete-execution-launch Mark a claimed execution request launched
2521
+ fail-execution-launch Mark a claimed execution request failed
2352
2522
  permission-request Notify Overlord that the agent is requesting tool permission
2353
2523
  hook-event Record a lifecycle hook event without a session key
2354
2524
  read-context Read shared persistent context for this ticket
@@ -2779,6 +2949,47 @@ update-project-resource:
2779
2949
  --label <text> New label (pass 'null' to clear)
2780
2950
  --is-primary <true|false>
2781
2951
 
2952
+ request-execution:
2953
+ Required:
2954
+ --ticket-id <ticket_id>
2955
+ Optional:
2956
+ --objective-id <uuid>
2957
+ --requested-from <source> manual_run | auto_advance | api | ssh
2958
+ --idempotency-key <key>
2959
+ --agent <agent>
2960
+ --model <model>
2961
+ --thinking <level>
2962
+ --flag <value> Extra agent flag (repeatable)
2963
+ --working-directory <path>
2964
+ --ssh-command <command>
2965
+ --remote-working-directory <path>
2966
+ --server-multiplexer <none|tmux>
2967
+ --target-kind <any|local|ssh>
2968
+ --target-device-id <uuid>
2969
+ --target-resource-id <uuid>
2970
+
2971
+ claim-execution:
2972
+ Required:
2973
+ --device-fingerprint <fp> (or OVERLORD_DEVICE_FINGERPRINT env var)
2974
+ Optional:
2975
+ --device-hostname <name>
2976
+ --device-platform <platform>
2977
+ --lease-seconds <n>
2978
+ --project-id <uuid>
2979
+
2980
+ complete-execution-launch:
2981
+ Required:
2982
+ --request-id <uuid> (or EXECUTION_REQUEST_ID env var)
2983
+ --device-fingerprint <fp> (or OVERLORD_DEVICE_FINGERPRINT env var)
2984
+ Optional:
2985
+ --launched-session-id <uuid>
2986
+
2987
+ fail-execution-launch:
2988
+ Required:
2989
+ --request-id <uuid> (or EXECUTION_REQUEST_ID env var)
2990
+ --device-fingerprint <fp> (or OVERLORD_DEVICE_FINGERPRINT env var)
2991
+ --error <message>
2992
+
2782
2993
  Examples:
2783
2994
  ovld protocol auth-status
2784
2995
  ovld protocol discover-project
@@ -2808,6 +3019,8 @@ Examples:
2808
3019
  ovld protocol attachment-prepare-upload --session-key <key> --ticket-id <ticket_id> --objective-id <objective-id> --file-name spec.pdf --content-type application/pdf
2809
3020
  ovld protocol attachment-upload-file --session-key <key> --ticket-id <ticket_id> --objective-id <objective-id> --file ./spec.pdf
2810
3021
  ovld protocol attachment-download-url --session-key <key> --ticket-id <ticket_id> --attachment-id <attachment-id>
3022
+ ovld protocol request-execution --ticket-id 1:899 --agent codex --requested-from manual_run
3023
+ ovld protocol claim-execution --device-fingerprint $OVERLORD_DEVICE_FINGERPRINT
2811
3024
  ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --summary "Done"
2812
3025
  ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --summary "Done" --artifacts-file ./artifacts.json
2813
3026
  ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --payload-json '{"summary":"Done","artifacts":[{"type":"note","label":"Delivery","content":"..."}]}'
@@ -2915,6 +3128,22 @@ EOF
2915
3128
  await protocolRequestApprovalGate(args);
2916
3129
  return;
2917
3130
  }
3131
+ if (subcommand === 'request-execution') {
3132
+ await protocolRequestExecution(args);
3133
+ return;
3134
+ }
3135
+ if (subcommand === 'claim-execution') {
3136
+ await protocolClaimExecution(args);
3137
+ return;
3138
+ }
3139
+ if (subcommand === 'complete-execution-launch') {
3140
+ await protocolCompleteExecutionLaunch(args);
3141
+ return;
3142
+ }
3143
+ if (subcommand === 'fail-execution-launch') {
3144
+ await protocolFailExecutionLaunch(args);
3145
+ return;
3146
+ }
2918
3147
  if (subcommand === 'permission-request') {
2919
3148
  await protocolPermissionRequest(args);
2920
3149
  return;
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync, spawn } from 'node:child_process';
4
+ import crypto from 'node:crypto';
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+
9
+ const OVLD_ENTRY = process.argv[1];
10
+ const DEVICE_FILE = path.join(os.homedir(), '.ovld', 'device.json');
11
+
12
+ function printRunnerHelp(primaryCommand = 'ovld') {
13
+ console.log(`Usage:
14
+ ${primaryCommand} runner once [options]
15
+ ${primaryCommand} runner start [options]
16
+ ${primaryCommand} runner status [options]
17
+
18
+ Options:
19
+ --device-fingerprint <fp> Override runner device identity
20
+ --poll-interval-ms <ms> Poll interval for start mode (default 3000)
21
+ --project-id <uuid> Only claim requests for one project
22
+
23
+ The runner claims durable execution requests and launches them with \`${primaryCommand} launch\`.
24
+ `);
25
+ }
26
+
27
+ function parseFlags(args) {
28
+ const flags = {};
29
+ for (let i = 0; i < args.length; i++) {
30
+ const arg = args[i];
31
+ if (!arg.startsWith('--')) continue;
32
+ const eqIdx = arg.indexOf('=');
33
+ if (eqIdx !== -1) {
34
+ flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
35
+ continue;
36
+ }
37
+ const key = arg.slice(2);
38
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
39
+ flags[key] = args[i + 1];
40
+ i++;
41
+ } else {
42
+ flags[key] = true;
43
+ }
44
+ }
45
+ return flags;
46
+ }
47
+
48
+ function readOrCreateDeviceFingerprint(flags) {
49
+ const explicit =
50
+ typeof flags['device-fingerprint'] === 'string'
51
+ ? flags['device-fingerprint'].trim()
52
+ : process.env.OVERLORD_DEVICE_FINGERPRINT?.trim();
53
+ if (explicit) return explicit;
54
+
55
+ try {
56
+ const raw = fs.readFileSync(DEVICE_FILE, 'utf8');
57
+ const parsed = JSON.parse(raw);
58
+ if (typeof parsed.deviceFingerprint === 'string' && parsed.deviceFingerprint.trim()) {
59
+ return parsed.deviceFingerprint.trim();
60
+ }
61
+ } catch {
62
+ // Create below.
63
+ }
64
+
65
+ const deviceFingerprint = crypto.randomUUID();
66
+ fs.mkdirSync(path.dirname(DEVICE_FILE), { recursive: true });
67
+ fs.writeFileSync(DEVICE_FILE, `${JSON.stringify({ deviceFingerprint }, null, 2)}\n`, 'utf8');
68
+ return deviceFingerprint;
69
+ }
70
+
71
+ function runOvld(args, env = {}) {
72
+ return execFileSync(process.execPath, [OVLD_ENTRY, ...args], {
73
+ encoding: 'utf8',
74
+ env: { ...process.env, ...env }
75
+ });
76
+ }
77
+
78
+ function protocolJson(subcommand, args, env) {
79
+ const stdout = runOvld(['protocol', subcommand, ...args], env);
80
+ return stdout.trim() ? JSON.parse(stdout) : {};
81
+ }
82
+
83
+ function claimExecution(flags, deviceFingerprint) {
84
+ const args = [
85
+ '--device-fingerprint',
86
+ deviceFingerprint,
87
+ '--device-hostname',
88
+ os.hostname(),
89
+ '--device-platform',
90
+ process.platform
91
+ ];
92
+ if (typeof flags['project-id'] === 'string') {
93
+ args.push('--project-id', flags['project-id']);
94
+ }
95
+ return protocolJson('claim-execution', args, { OVERLORD_DEVICE_FINGERPRINT: deviceFingerprint });
96
+ }
97
+
98
+ function completeLaunch(requestId, deviceFingerprint) {
99
+ protocolJson(
100
+ 'complete-execution-launch',
101
+ ['--request-id', requestId, '--device-fingerprint', deviceFingerprint],
102
+ { OVERLORD_DEVICE_FINGERPRINT: deviceFingerprint }
103
+ );
104
+ }
105
+
106
+ function failLaunch(requestId, deviceFingerprint, error) {
107
+ protocolJson(
108
+ 'fail-execution-launch',
109
+ ['--request-id', requestId, '--device-fingerprint', deviceFingerprint, '--error', error],
110
+ { OVERLORD_DEVICE_FINGERPRINT: deviceFingerprint }
111
+ );
112
+ }
113
+
114
+ function buildLaunchArgs(launch) {
115
+ const args = ['launch', launch.agent, '--ticket-id', launch.ticketId];
116
+ if (launch.workingDirectory) args.push('--working-directory', launch.workingDirectory);
117
+ if (launch.launchMode === 'ask') args.push('--launch-mode', 'ask');
118
+ if (launch.model) args.push('--model', launch.model);
119
+ if (launch.thinking) args.push('--thinking', launch.thinking);
120
+ for (const flag of launch.flags ?? []) args.push('--flag', flag);
121
+ if (launch.sshCommand) args.push('--ssh-command', launch.sshCommand);
122
+ if (launch.remoteWorkingDirectory) {
123
+ args.push('--remote-working-directory', launch.remoteWorkingDirectory);
124
+ }
125
+ if (launch.serverMultiplexer === 'tmux') args.push('--server-multiplexer', 'tmux');
126
+ if (launch.tmuxCommand) args.push('--tmux-command', launch.tmuxCommand);
127
+ return args;
128
+ }
129
+
130
+ async function launchClaimedRequest(claim, deviceFingerprint) {
131
+ const requestId = claim.request?.id;
132
+ if (!requestId || !claim.launch?.agent || !claim.launch?.ticketId) return false;
133
+
134
+ const args = buildLaunchArgs(claim.launch);
135
+ process.stderr.write(
136
+ `[runner] launching ${claim.launch.agent} for ${claim.launch.ticketId} (${requestId})\n`
137
+ );
138
+
139
+ await new Promise((resolve, reject) => {
140
+ const child = spawn(process.execPath, [OVLD_ENTRY, ...args], {
141
+ stdio: 'inherit',
142
+ env: { ...process.env, OVERLORD_DEVICE_FINGERPRINT: deviceFingerprint }
143
+ });
144
+
145
+ child.once('spawn', () => {
146
+ try {
147
+ completeLaunch(requestId, deviceFingerprint);
148
+ } catch (error) {
149
+ process.stderr.write(`[runner] failed to mark request launched: ${error}\n`);
150
+ }
151
+ });
152
+
153
+ child.once('error', error => {
154
+ try {
155
+ failLaunch(requestId, deviceFingerprint, error.message);
156
+ } catch {
157
+ // Preserve the original launch error.
158
+ }
159
+ reject(error);
160
+ });
161
+
162
+ child.once('close', code => {
163
+ if (code && code !== 0) {
164
+ process.stderr.write(`[runner] launch process exited with code ${code}\n`);
165
+ }
166
+ resolve();
167
+ });
168
+ });
169
+
170
+ return true;
171
+ }
172
+
173
+ async function runOnce(flags, deviceFingerprint) {
174
+ const claim = claimExecution(flags, deviceFingerprint);
175
+ if (!claim.request) {
176
+ process.stderr.write('[runner] no queued execution requests\n');
177
+ return false;
178
+ }
179
+ await launchClaimedRequest(claim, deviceFingerprint);
180
+ return true;
181
+ }
182
+
183
+ function sleep(ms) {
184
+ return new Promise(resolve => setTimeout(resolve, ms));
185
+ }
186
+
187
+ export async function runRunnerCommand(subcommand, args, primaryCommand = 'ovld') {
188
+ if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
189
+ printRunnerHelp(primaryCommand);
190
+ return;
191
+ }
192
+
193
+ const flags = parseFlags(args);
194
+ const deviceFingerprint = readOrCreateDeviceFingerprint(flags);
195
+
196
+ if (subcommand === 'status') {
197
+ const data = protocolJson(
198
+ 'get-device',
199
+ [
200
+ '--device-fingerprint',
201
+ deviceFingerprint,
202
+ '--device-hostname',
203
+ os.hostname(),
204
+ '--device-platform',
205
+ process.platform
206
+ ],
207
+ { OVERLORD_DEVICE_FINGERPRINT: deviceFingerprint }
208
+ );
209
+ console.log(JSON.stringify({ ok: true, device: data.device }, null, 2));
210
+ return;
211
+ }
212
+
213
+ if (subcommand === 'once') {
214
+ await runOnce(flags, deviceFingerprint);
215
+ return;
216
+ }
217
+
218
+ if (subcommand === 'start') {
219
+ const pollIntervalMs =
220
+ typeof flags['poll-interval-ms'] === 'string'
221
+ ? Math.max(1000, Number.parseInt(flags['poll-interval-ms'], 10) || 3000)
222
+ : 3000;
223
+ process.stderr.write(`[runner] started with device ${deviceFingerprint}\n`);
224
+ for (;;) {
225
+ try {
226
+ const launched = await runOnce(flags, deviceFingerprint);
227
+ if (!launched) await sleep(pollIntervalMs);
228
+ } catch (error) {
229
+ process.stderr.write(`[runner] ${error instanceof Error ? error.message : error}\n`);
230
+ await sleep(pollIntervalMs);
231
+ }
232
+ }
233
+ }
234
+
235
+ console.error(`Unknown runner command: ${subcommand}\n`);
236
+ printRunnerHelp(primaryCommand);
237
+ process.exit(1);
238
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "0.2605210944.0",
3
+ "version": "0.2605211800.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",
@@ -7,6 +7,8 @@ ovld protocol get-device --device-fingerprint "$OVERLORD_DEVICE_FINGERPRINT"
7
7
  ovld protocol list-project-resources --project-id <project_uuid> --device-fingerprint "$OVERLORD_DEVICE_FINGERPRINT"
8
8
  ```
9
9
 
10
+ `ovld runner start` uses the same device identity and project resource directories to claim queued execution requests from manual Run and auto-advance. `ovld runner once` claims at most one request and exits.
11
+
10
12
  ## Choosing `--execution-target`
11
13
 
12
14
  Pass `--execution-target agent` or `--execution-target human` (default: `human`) when creating tickets.
@@ -3,6 +3,7 @@
3
3
  - 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.
4
4
  - Hosted Overlord MCP (`/functions/v1/mcp`) uses the same canonical tool names as the local shim that shells into `ovld protocol` (`attach`, `update`, `deliver`, `get_device`, `list_project_resources`, …). Hosted calls use camelCase JSON keys (`ticketId`, `sessionKey`, `deviceFingerprint`) matching `POST /api/protocol/*` bodies; the local shim uses snake_case keys mapped to CLI flags (`ticket_id`, `session_key`, `device_fingerprint`).
5
5
  - `permission-request` is invoked by the Claude Code permission hook installed by the bundle. Agents do not normally call it directly.
6
+ - Execution request tools (`request_execution`, `claim_execution`, `complete_execution_launch`, `fail_execution_launch`) back the local runner queue. Agents normally work tickets with `attach`/`update`/`deliver`; runner processes use these queue tools.
6
7
  - `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`.
7
8
  - 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.
8
9
  - "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.
@@ -7,6 +7,8 @@ ovld protocol get-device --device-fingerprint "$OVERLORD_DEVICE_FINGERPRINT"
7
7
  ovld protocol list-project-resources --project-id <project_uuid> --device-fingerprint "$OVERLORD_DEVICE_FINGERPRINT"
8
8
  ```
9
9
 
10
+ `ovld runner start` uses the same device identity and project resource directories to claim queued execution requests from manual Run and auto-advance. `ovld runner once` claims at most one request and exits.
11
+
10
12
  ## Choosing `--execution-target`
11
13
 
12
14
  Pass `--execution-target agent` or `--execution-target human` (default: `human`) when creating tickets.
@@ -3,6 +3,7 @@
3
3
  - API requires `agentIdentifier` and `connectionMethod` on attach/connect/prompt. The CLI defaults them to `cursor`/`cli`; the MCP tool defaults to `mcp`. Override with `--agent` / `--method` when calling from a different runtime.
4
4
  - Hosted Overlord MCP (`/functions/v1/mcp`) uses the same canonical tool names as the local shim that shells into `ovld protocol` (`attach`, `update`, `deliver`, `get_device`, `list_project_resources`, …). Hosted calls use camelCase JSON keys (`ticketId`, `sessionKey`, `deviceFingerprint`) matching `POST /api/protocol/*` bodies; the local shim uses snake_case keys mapped to CLI flags (`ticket_id`, `session_key`, `device_fingerprint`).
5
5
  - `permission-request` is invoked by the Cursor permission hook installed by the bundle. Agents do not normally call it directly.
6
+ - Execution request tools (`request_execution`, `claim_execution`, `complete_execution_launch`, `fail_execution_launch`) back the local runner queue. Agents normally work tickets with `attach`/`update`/`deliver`; runner processes use these queue tools.
6
7
  - `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`.
7
8
  - 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.
8
9
  - "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.
@@ -7,6 +7,8 @@ ovld protocol get-device --device-fingerprint "$OVERLORD_DEVICE_FINGERPRINT"
7
7
  ovld protocol list-project-resources --project-id <project_uuid> --device-fingerprint "$OVERLORD_DEVICE_FINGERPRINT"
8
8
  ```
9
9
 
10
+ `ovld runner start` uses the same device identity and project resource directories to claim queued execution requests from manual Run and auto-advance. `ovld runner once` claims at most one request and exits.
11
+
10
12
  ## Choosing `--execution-target`
11
13
 
12
14
  Pass `--execution-target agent` or `--execution-target human` (default: `human`) when creating tickets.
@@ -3,6 +3,7 @@
3
3
  - API requires `agentIdentifier` and `connectionMethod` on attach/connect/prompt. The CLI defaults them to `codex`/`cli`; the MCP tool defaults to `mcp`. Override with `--agent` / `--method` when calling from a different runtime.
4
4
  - Hosted Overlord MCP (`/functions/v1/mcp`) uses the same canonical tool names as the local shim that shells into `ovld protocol` (`attach`, `update`, `deliver`, `get_device`, `list_project_resources`, …). Hosted calls use camelCase JSON keys (`ticketId`, `sessionKey`, `deviceFingerprint`) matching `POST /api/protocol/*` bodies; the local shim uses snake_case keys mapped to CLI flags (`ticket_id`, `session_key`, `device_fingerprint`).
5
5
  - `permission-request` is invoked by the Codex permission hook installed by the bundle. Agents do not normally call it directly.
6
+ - Execution request tools (`request_execution`, `claim_execution`, `complete_execution_launch`, `fail_execution_launch`) back the local runner queue. Agents normally work tickets with `attach`/`update`/`deliver`; runner processes use these queue tools.
6
7
  - `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`.
7
8
  - 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.
8
9
  - "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.