codexteer 0.18.1 → 0.19.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/src/cli.mjs CHANGED
@@ -15,7 +15,7 @@ import { acquireResource, resourceStatus, renewResource, releaseResource, listRe
15
15
  import { playSendSound } from "./sound.mjs";
16
16
  import { prepareSupervisorPrompt, superviseAgent } from "./supervise.mjs";
17
17
  import { prepareOrchestrationPrompt, orchestrateAgent } from "./orchestrate.mjs";
18
- import { supervisorId, registerSupervisor, getSupervisor, assertSupervisor, controlSupervisor, supervisorStatus, listSupervisors } from "./supervision.mjs";
18
+ import { supervisorId, registerSupervisor, getSupervisor, assertSupervisor, assertSupervisorRegistration, controlSupervisor, supervisorStatus, listSupervisors } from "./supervision.mjs";
19
19
  import { createFinding, findingStatus, listFindings, resolveFinding, dismissFinding, reopenFinding, evaluateFinding, findingStats } from "./findings.mjs";
20
20
  import { connectionMode, observationConnection, desktopReadOnly } from "./connection.mjs";
21
21
  import { repairTurns } from "./turn-repair.mjs";
@@ -39,6 +39,7 @@ export function fail(error, json) {
39
39
  if (json) console.log(JSON.stringify({ ok: false, ...(error.watch ? { command: "watch", data: error.watch } : {}), error: {
40
40
  message, code: error.code, ...hostFields(), delivery_status: error.delivery_status,
41
41
  operation: error.operation, capability: error.capability,
42
+ expected_connection: error.expected_connection, connection: error.connection,
42
43
  ...((error.execution_environment === "cloud" || (typeof error.code === "string" && error.code.startsWith("CLOUD_"))) ? {
43
44
  execution_environment: error.execution_environment, task_id: error.task_id, url: error.url,
44
45
  submission_status: error.submission_status, uncertain: error.uncertain,
@@ -65,7 +66,7 @@ export function fail(error, json) {
65
66
  if (error.execution_environment === "cloud" || (typeof error.code === "string" && error.code.startsWith("DESKTOP_CLOUD_"))) {
66
67
  // Recovery metadata must survive an uncertain submission or a stale
67
68
  // on-disk receipt. Never print the child error, prompt, or diagnostics.
68
- for (const key of ["creation_status", "submission_status", "delivery_status", "attempt_id", "receipt_path", "existing_attempt_id", "existing_receipt_path", "task_id", "thread_id", "url"]) {
69
+ for (const key of ["creation_status", "submission_status", "delivery_status", "attempt_id", "receipt_path", "existing_message_id", "existing_attempt_id", "existing_receipt_path", "task_id", "thread_id", "url"]) {
69
70
  if (typeof error[key] === "string") console.error(`${key}: ${JSON.stringify(error[key])}`);
70
71
  }
71
72
  if (error.receipt_update_required) console.error("receipt_update_required: true");
@@ -76,11 +77,13 @@ export function fail(error, json) {
76
77
  process.exitCode = 1;
77
78
  }
78
79
 
80
+ const usage = message => Object.assign(new Error(message), { code: "OPTIONS_INVALID" });
81
+
79
82
  function takeOption(args, name, defaultValue) {
80
83
  const index = args.indexOf(name);
81
84
  if (index === -1 || (args.includes("--") && index > args.indexOf("--"))) return defaultValue;
82
85
  const value = args[index + 1];
83
- if (value == null || value.startsWith("--")) throw new Error(`${name} requires a value.`);
86
+ if (value == null || value.startsWith("--")) throw usage(`${name} requires a value.`);
84
87
  args.splice(index, 2);
85
88
  return value;
86
89
  }
@@ -123,7 +126,7 @@ function readMessage(parts) {
123
126
 
124
127
  function backendOption(args, fallback) {
125
128
  const backend = takeOption(args, "--backend", fallback);
126
- if (backend != null && !["app-server", "ui"].includes(backend)) throw new Error("--backend must be app-server or ui.");
129
+ if (backend != null && !["app-server", "ui"].includes(backend)) throw usage("--backend must be app-server or ui.");
127
130
  return backend;
128
131
  }
129
132
 
@@ -141,7 +144,7 @@ function printThreads(threads) {
141
144
  function printSupervisor(data) {
142
145
  if (Array.isArray(data)) {
143
146
  if (!data.length) console.log("No supervisor sessions registered.");
144
- for (const entry of data) console.log(`${entry.thread_id} ${entry.state} ${entry.owner} ${entry.connection} observed:${entry.observation?.last_observed_at ?? "none"}`);
147
+ for (const entry of data) console.log(`${entry.thread_id} ${entry.state} ${entry.owner} ${entry.connection} observed:${entry.observation?.last_observed_at ?? "none"}${entry.session_conflict ? ` session-conflict:${entry.session_conflict.thread_ids.join(",")}` : ""}${entry.observation_error ? ` observation-error:${entry.observation_error.code}` : ""}`);
145
148
  return;
146
149
  }
147
150
  console.log(`${data.thread_id} ${data.state}${data.owner ? ` ${data.owner} ${data.connection}` : ""}`);
@@ -161,7 +164,7 @@ export async function main(argv) {
161
164
  try {
162
165
  const transport = validateRemoteTransport(takeOption(args, "--remote-transport", "native"));
163
166
  const explicitHost = takeOption(args, "--host", undefined);
164
- if (args.slice(0, args.includes("--") ? args.indexOf("--") : args.length).includes("--host")) throw new Error("Specify --host only once.");
167
+ if (args.slice(0, args.includes("--") ? args.indexOf("--") : args.length).includes("--host")) throw usage("Specify --host only once.");
165
168
  if (explicitHost !== undefined) validateHostId(explicitHost);
166
169
  const probe = [...args];
167
170
  takeFlag(probe, "--json");
@@ -196,7 +199,7 @@ async function mainOnTarget(argv) {
196
199
  const connectionDependencies = observationConnection(connection);
197
200
  const requiredNode = takeOption(args, "--require-node-version", undefined);
198
201
  if (requiredNode !== undefined) {
199
- if (!/^v\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(requiredNode)) throw new Error("--require-node-version requires a Node version such as v25.1.0.");
202
+ if (!/^v\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(requiredNode)) throw usage("--require-node-version requires a Node version such as v25.1.0.");
200
203
  if (requiredNode !== process.version) throw Object.assign(new Error(`This supervision command requires Node ${requiredNode}, but is running ${process.version}. Regenerate the supervisor prompt with the intended Node runtime.`), { code: "NODE_VERSION_MISMATCH" });
201
204
  }
202
205
  if (takeFlag(args, "--version")) {
@@ -216,6 +219,7 @@ async function mainOnTarget(argv) {
216
219
  }
217
220
 
218
221
  let command = args.shift();
222
+ if (command === "diff") throw usage("diff reads a Web Cloud task's recorded patch and is not available for local or remote Desktop tasks. Use read --include-output to inspect file changes.");
219
223
  const control = isControlTransport();
220
224
  if (targetHost() !== "local" && ["desktop", "debug-ui"].includes(command)) throw Object.assign(new Error("Desktop process/update and Accessibility diagnostics cannot target the remote host. No local operation was run."), { code: "REMOTE_HOST_OPERATION_UNSUPPORTED" });
221
225
  if (targetHost() !== "local" && !["remote", "projects", "read", "status", "watch", "doctor", "supervise", "findings", "history", "instructions", "thread", "open", ...(control ? ["send"] : []), ...(remoteTransport() === "paired" ? ["threads", "resource", "checkpoint", "orchestrate"] : [])].includes(command)
@@ -229,14 +233,14 @@ async function mainOnTarget(argv) {
229
233
  if (command === "remote") {
230
234
  if (supervisor) throw Object.assign(new Error("A bound supervisor cannot enroll, pair or manage account credentials."), { code: "SUPERVISOR_MISMATCH" });
231
235
  const action = args.shift(), codeStdin = takeFlag(args, "--code-stdin"), provider = takeOption(args, "--key-provider", undefined);
232
- if (provider !== undefined && action !== "prepare") throw new Error("--key-provider is only available for remote prepare.");
233
- if (!["prepare", "recover-key", "enroll", "status", "identity", "pair", "verify"].includes(action) || args.length || (codeStdin && action !== "pair") || (action === "pair" && !codeStdin)) throw new Error("Expected: remote prepare|recover-key|enroll|status|identity|verify, or --host HOST remote pair --code-stdin.");
234
- if (["prepare", "enroll"].includes(action) && targetHost() !== "local") throw new Error("remote prepare/enroll registers this local controller; omit --host.");
236
+ if (provider !== undefined && action !== "prepare") throw usage("--key-provider is only available for remote prepare.");
237
+ if (!["prepare", "recover-key", "enroll", "status", "identity", "pair", "verify"].includes(action) || args.length || (codeStdin && action !== "pair") || (action === "pair" && !codeStdin)) throw usage("Expected: remote prepare|recover-key|enroll|status|identity|verify, or --host HOST remote pair --code-stdin.");
238
+ if (["prepare", "enroll"].includes(action) && targetHost() !== "local") throw usage("remote prepare/enroll registers this local controller; omit --host.");
235
239
  // Code input is never an argv option or a chat message.
236
- if (action === "pair" && !/^remote-control:env_[A-Za-z0-9_-]+$/.test(targetHost())) throw new Error("remote pair requires --host remote-control:env_...");
240
+ if (action === "pair" && !/^remote-control:env_[A-Za-z0-9_-]+$/.test(targetHost())) throw Object.assign(new Error("remote pair requires --host remote-control:env_..."), { code: "REMOTE_PAIR_INPUT_INVALID" });
237
241
  if (action === "pair" && process.stdin.isTTY) console.error("codexteer: Enter the selected host's fresh pairing code (hidden), then press Enter.");
238
242
  const code = action === "pair" ? await readPairingCode() : undefined;
239
- if (code && code.length > 128) throw new Error("Invalid pairing code input.");
243
+ if (code && code.length > 128) throw Object.assign(new Error("Invalid pairing code input."), { code: "REMOTE_PAIR_INPUT_INVALID" });
240
244
  const data = await withDeviceAccount(context => action === "prepare" ? prepareDevice(context, { ...(provider ? { provider } : {}) }) : action === "recover-key" ? recoverDeviceKey(context) : action === "enroll" ? enrollDevice(context, { progress: text => console.error(`codexteer: ${text}`) })
241
245
  : action === "pair" ? pairDevice(context, targetHost(), code)
242
246
  : action === "verify" ? verifyDevice(context) : action === "identity" ? deviceIdentityInfo(context) : readDevice(context.home, context.identity).then(deviceStatus));
@@ -244,12 +248,13 @@ async function mainOnTarget(argv) {
244
248
  }
245
249
  if (command === "supervise" && ["register", "status", "list", "pause", "resume", "stop"].includes(args[0])) {
246
250
  const action = args.shift(), owner = action === "register" ? takeOption(args, "--owner", "external") : undefined;
247
- if (args.length !== (action === "list" ? 0 : 1)) throw new Error(`Expected: supervise ${action}${action === "list" ? "" : " <THREAD>"}.`);
251
+ if (args.length !== (action === "list" ? 0 : 1)) throw usage(`Expected: supervise ${action}${action === "list" ? "" : " <THREAD>"}.`);
248
252
  let data;
249
253
  if (action === "register") {
250
- if (!supervisor) throw new Error("supervise register requires the --supervisor ID from a generated prompt.");
254
+ if (!supervisor) throw usage("supervise register requires the --supervisor ID from a generated prompt.");
251
255
  data = await registerSupervisor(args[0], supervisor, { owner, connection });
252
256
  } else {
257
+ if (supervisor && action === "list") throw Object.assign(new Error("A bound supervisor cannot list other supervisor sessions. Use supervise status <THREAD> for its own task, or run supervise list from the user's terminal without --supervisor."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
253
258
  if (supervisor && action !== "status") throw Object.assign(new Error("Supervisors cannot change their own control state. Run this control command from the user's terminal without --supervisor."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
254
259
  if (supervisor) {
255
260
  const record = await getSupervisor(args[0]);
@@ -280,7 +285,7 @@ async function mainOnTarget(argv) {
280
285
  else if (action === "list") options = { all: takeFlag(args, "--all"), limit: Number(takeOption(args, "--limit", "50")) };
281
286
  const handlers = { create: createFinding, show: findingStatus, list: listFindings, resolve: resolveFinding, dismiss: dismissFinding, reopen: reopenFinding, evaluate: evaluateFinding, stats: findingStats };
282
287
  const single = ["create", "list", "stats"].includes(action);
283
- if (!Object.hasOwn(handlers, action) || args.length !== (single ? 1 : 2) || args.some(arg => arg.startsWith("--"))) throw new Error("See codexteer help findings.");
288
+ if (!Object.hasOwn(handlers, action) || args.length !== (single ? 1 : 2) || args.some(arg => arg.startsWith("--"))) throw usage("See codexteer help findings.");
284
289
  if (supervisor) await assertSupervisor(args[0], supervisor, { connection });
285
290
  data = single ? await handlers[action](args[0], options) : await handlers[action](args[0], args[1], options);
286
291
  success(`findings.${action}`, data, json);
@@ -291,7 +296,7 @@ async function mainOnTarget(argv) {
291
296
  if (supervisor) throw Object.assign(new Error("Managed supervision is bound to its existing task and cannot orchestrate a project."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
292
297
  const promptOnly = args[0] === "prompt";
293
298
  if (promptOnly) args.shift();
294
- else if (json) throw new Error("orchestrate inherits agent output and does not support --json. Use help orchestrate --json or orchestrate prompt --project ID|NAME <GOAL> --json.");
299
+ else if (json) throw usage("orchestrate inherits agent output and does not support --json. Use help orchestrate --json or orchestrate prompt --project ID|NAME <GOAL> --json.");
295
300
  const separator = promptOnly ? -1 : args.indexOf("--");
296
301
  const ownArgs = separator < 0 ? args : args.slice(0, separator);
297
302
  const agentArgs = separator < 0 ? [] : args.slice(separator + 1);
@@ -299,8 +304,8 @@ async function mainOnTarget(argv) {
299
304
  const agent = takeOption(ownArgs, "--agent", promptOnly ? undefined : "claude");
300
305
  const usePathCli = takeFlag(ownArgs, "--use-path-cli");
301
306
  if (promptOnly && ownArgs[0] === "--") ownArgs.shift();
302
- else if (ownArgs.some(arg => arg.startsWith("--"))) throw new Error("Unknown orchestrate option. See help orchestrate; agent arguments belong after --.");
303
- if (ownArgs.length !== 1) throw new Error("Expected: orchestrate [prompt] --project <ID-or-exact-name> <GOAL>. Quote the goal as one argument, or use - for stdin.");
307
+ else if (ownArgs.some(arg => arg.startsWith("--"))) throw usage("Unknown orchestrate option. See help orchestrate; agent arguments belong after --.");
308
+ if (ownArgs.length !== 1) throw usage("Expected: orchestrate [prompt] --project <ID-or-exact-name> <GOAL>. Quote the goal as one argument, or use - for stdin.");
304
309
  const goal = ownArgs[0] === "-" ? readFileSync(0, "utf8") : ownArgs[0];
305
310
  if (promptOnly) {
306
311
  const data = await prepareOrchestrationPrompt(project, goal, { agent, connection, usePathCli });
@@ -313,47 +318,49 @@ async function mainOnTarget(argv) {
313
318
  args.shift();
314
319
  const agent = takeOption(args, "--agent", "claude");
315
320
  const usePathCli = takeFlag(args, "--use-path-cli");
316
- if (args.length < 1 || args.length > 2 || args.some(arg => arg.startsWith("--"))) throw new Error("Expected: supervise prompt <THREAD> [MESSAGE]. See codexteer help supervise prompt.");
317
- if (supervisor) throw new Error("A supervisor cannot generate a replacement session.");
321
+ if (args.length < 1 || args.length > 2 || args.some(arg => arg.startsWith("--"))) throw usage("Expected: supervise prompt <THREAD> [MESSAGE]. See codexteer help supervise prompt.");
322
+ if (supervisor) throw Object.assign(new Error("A supervisor cannot generate a replacement session."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
318
323
  const data = await prepareSupervisorPrompt(args[0], args[1], { agent, connection, usePathCli });
319
324
  success("supervise.prompt", data, json);
320
325
  if (!json) console.log(data.prompt);
321
326
  return;
322
327
  }
323
328
  if (command === "supervise") {
324
- if (supervisor) throw new Error("A supervisor cannot start a replacement session.");
325
- if (json) throw new Error("supervise inherits agent output and does not support --json. Use help supervise --json or supervise prompt <THREAD> --json.");
329
+ if (supervisor) throw Object.assign(new Error("A supervisor cannot start a replacement session."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
330
+ if (json) throw usage("supervise inherits agent output and does not support --json. Use help supervise --json or supervise prompt <THREAD> --json.");
326
331
  const separator = args.indexOf("--");
327
332
  const ownArgs = separator < 0 ? args : args.slice(0, separator);
328
333
  const agentArgs = separator < 0 ? [] : args.slice(separator + 1);
329
334
  const agent = takeOption(ownArgs, "--agent", "claude");
330
335
  const usePathCli = takeFlag(ownArgs, "--use-path-cli");
331
- if (ownArgs.length < 1 || ownArgs.length > 2 || ownArgs.some(arg => arg.startsWith("--"))) throw new Error("Expected: supervise <THREAD> [MESSAGE] [--agent claude|codex] [--use-path-cli] [-- <AGENT-ARGS...>].");
336
+ if (ownArgs.length < 1 || ownArgs.length > 2 || ownArgs.some(arg => arg.startsWith("--"))) throw usage("Expected: supervise <THREAD> [MESSAGE] [--agent claude|codex] [--use-path-cli] [-- <AGENT-ARGS...>].");
332
337
  process.exitCode = await superviseAgent(ownArgs[0], { agent, agentArgs, policy: ownArgs[1], connection, usePathCli });
333
338
  return;
334
339
  }
335
340
  if (command === "resource") {
336
- const action = args.shift(); let data;
341
+ const threadId = takeOption(args, "--thread", undefined), action = args.shift(); let data;
342
+ if (supervisor && threadId === undefined) throw Object.assign(new Error("A supervisor resource command requires its --thread target."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
343
+ // Like read/watch, inspecting leases and giving one up stay available while intervention is paused.
344
+ if (supervisor) await assertSupervisor(threadId, supervisor, { connection, sending: ["acquire", "renew", "run"].includes(action) });
337
345
  if (["acquire", "run"].includes(action)) {
338
- const options = { owner: takeOption(args, "--owner", undefined), ttlMs: Number(takeOption(args, "--ttl-ms", "600000")), reason: takeOption(args, "--reason", ""), condition: takeOption(args, "--condition", ""), threadId: takeOption(args, "--thread", undefined), cwd: takeOption(args, "--cwd", undefined) };
346
+ const options = { owner: takeOption(args, "--owner", undefined), ttlMs: Number(takeOption(args, "--ttl-ms", "600000")), reason: takeOption(args, "--reason", ""), condition: takeOption(args, "--condition", ""), threadId, cwd: takeOption(args, "--cwd", undefined) };
339
347
  if (action === "run") {
340
348
  const timeoutMs = Number(takeOption(args, "--timeout-ms", "0")), includeOutput = takeFlag(args, "--include-output"), execBackend = takeOption(args, "--exec-backend", "app-server"), remoteShell = takeOption(args, "--shell", undefined);
341
- if (supervisor) await assertSupervisor(options.threadId, supervisor, { connection, sending: execBackend === "thread-shell" });
342
- if (args[1] !== "--" || args.length < 3) throw new Error("Expected: resource run <NAME> --owner NAME -- <COMMAND> [ARGS...]");
349
+ if (args[1] !== "--" || args.length < 3) throw usage("Expected: resource run <NAME> --owner NAME -- <COMMAND> [ARGS...]");
343
350
  data = await runWithResource(args[0], args.slice(2), { ...options, timeoutMs, execBackend, remoteShell, supervisor, connection });
344
351
  if (!includeOutput) delete data.output_tail;
345
352
  if (!data.valid) process.exitCode = 1;
346
353
  } else {
347
- if (args.length !== 1) throw new Error("Expected: resource acquire <NAME> --owner NAME.");
354
+ if (args.length !== 1) throw usage("Expected: resource acquire <NAME> --owner NAME.");
348
355
  data = await acquireResource(args[0], options);
349
356
  }
350
357
  } else if (["renew", "release"].includes(action)) {
351
358
  const token = takeOption(args, "--token", undefined), ttlMs = action === "renew" ? Number(takeOption(args, "--ttl-ms", "600000")) : undefined;
352
- if (args.length !== 1) throw new Error(`Expected: resource ${action} <NAME> --token TOKEN.`);
359
+ if (args.length !== 1) throw usage(`Expected: resource ${action} <NAME> --token TOKEN.`);
353
360
  data = action === "renew" ? await renewResource(args[0], token, { ttlMs }) : await releaseResource(args[0], token);
354
361
  } else if (action === "status" && args.length === 1) data = await resourceStatus(args[0]);
355
362
  else if (action === "list" && args.length === 0) data = await listResources();
356
- else throw new Error("See codexteer help resource.");
363
+ else throw usage("See codexteer help resource.");
357
364
  success(`resource.${action}`, data, json);
358
365
  if (!json) console.log(JSON.stringify(data, null, 2));
359
366
  return;
@@ -364,27 +371,27 @@ async function mainOnTarget(argv) {
364
371
  let data;
365
372
  if (action === "capture") {
366
373
  const paths = takeOptions(args, "--path"), excludes = takeOptions(args, "--exclude");
367
- if (args.length !== 2) throw new Error("Expected: checkpoint capture <THREAD> <NAME> --path PATH ...");
374
+ if (args.length !== 2) throw usage("Expected: checkpoint capture <THREAD> <NAME> --path PATH ...");
368
375
  data = await createCheckpoint(args[0], args[1], { paths, excludes });
369
376
  } else if (action === "run") {
370
377
  const timeoutMs = Number(takeOption(args, "--timeout-ms", "0")), execBackend = takeOption(args, "--exec-backend", "app-server"), remoteShell = takeOption(args, "--shell", undefined);
371
378
  if (supervisor && execBackend === "thread-shell") await assertSupervisor(args[0], supervisor, { connection, sending: true });
372
- if (args[2] !== "--" || args.length < 4) throw new Error("Expected: checkpoint run <THREAD> <ID> -- <COMMAND> [ARGS...]");
379
+ if (args[2] !== "--" || args.length < 4) throw usage("Expected: checkpoint run <THREAD> <ID> -- <COMMAND> [ARGS...]");
373
380
  data = await runCheckpoint(args[0], args[1], args.slice(3), { timeoutMs, execBackend, remoteShell, supervisor, connection });
374
381
  if (!includeOutput) delete data.output_tail;
375
382
  if (!data.valid) process.exitCode = 1;
376
383
  } else if (action === "attach") {
377
384
  const refs = takeOptions(args, "--artifact");
378
- if (args.length !== 2) throw new Error("Expected: checkpoint attach <THREAD> <ID> --artifact FILE ...");
385
+ if (args.length !== 2) throw usage("Expected: checkpoint attach <THREAD> <ID> --artifact FILE ...");
379
386
  data = await attachArtifacts(args[0], args[1], refs);
380
387
  } else if (action === "list") {
381
- if (args.length !== 1) throw new Error("Expected: checkpoint list <THREAD>.");
388
+ if (args.length !== 1) throw usage("Expected: checkpoint list <THREAD>.");
382
389
  data = await listCheckpoints(args[0]);
383
390
  } else if (["show", "verify"].includes(action)) {
384
- if (args.length !== 2) throw new Error(`Expected: checkpoint ${action} <THREAD> <ID>.`);
391
+ if (args.length !== 2) throw usage(`Expected: checkpoint ${action} <THREAD> <ID>.`);
385
392
  data = action === "verify" ? await verifyCheckpoint(args[0], args[1]) : checkpointSummary(await getCheckpoint(args[0], args[1]), includeOutput);
386
393
  if (action === "verify" && !data.valid) process.exitCode = 1;
387
- } else throw new Error("See codexteer help checkpoint.");
394
+ } else throw usage("See codexteer help checkpoint.");
388
395
  success(`checkpoint.${action}`, data, json);
389
396
  if (!json) console.log(JSON.stringify(data, null, 2));
390
397
  return;
@@ -393,18 +400,18 @@ async function mainOnTarget(argv) {
393
400
  const action = args.shift();
394
401
  if (action === "list") {
395
402
  const all = takeFlag(args, "--all");
396
- if (args.length !== 1) throw new Error("Expected: instructions list <THREAD> [--all].");
403
+ if (args.length !== 1) throw usage("Expected: instructions list <THREAD> [--all].");
397
404
  if (supervisor) await assertSupervisor(args[0], supervisor, { connection });
398
405
  const data = await listInstructions(args[0], { all });
399
406
  success("instructions.list", data, json);
400
407
  if (!json) { for (const entry of data.instructions) console.log(`${entry.id} ${entry.instruction_status} ${entry.metadata?.source ?? "unspecified"}\n${entry.body}`); if (data.pending_changes.length) console.log(`配送未確認: ${data.pending_changes.map(e => e.id).join(", ")}`); }
401
408
  return;
402
409
  }
403
- if (action !== "retract") throw new Error("See codexteer help instructions.");
410
+ if (action !== "retract") throw usage("See codexteer help instructions.");
404
411
  if (connection === "desktop") throw desktopReadOnly();
405
412
  const reason = takeOption(args, "--reason", undefined), source = takeOption(args, "--source", undefined), basedOn = takeOption(args, "--based-on", undefined);
406
413
  const dryRun = takeFlag(args, "--dry-run"), newTurn = takeFlag(args, "--new-turn");
407
- if (args.length !== 2 || !reason?.trim()) throw new Error("Expected: instructions retract <THREAD> <MESSAGE-ID> --reason TEXT.");
414
+ if (args.length !== 2 || !reason?.trim()) throw usage("Expected: instructions retract <THREAD> <MESSAGE-ID> --reason TEXT.");
408
415
  const data = await sendTrackedMessage(args[0], reason, { retracts: args[1], source, basedOn, dryRun, newTurn, supervisor, connection });
409
416
  success("instructions.retract", data, json);
410
417
  if (!json) console.log(dryRun ? "撤回の送信プレビューです。" : `撤回を受け付けました。message_id: ${data.message_id}`);
@@ -416,7 +423,7 @@ async function mainOnTarget(argv) {
416
423
  const pending = takeFlag(args, "--pending");
417
424
  const report = action === "mark" ? { status: takeOption(args, "--status", undefined), note: takeOption(args, "--note", undefined), evidence: takeOptions(args, "--evidence"), by: takeOption(args, "--by", "local") } : {};
418
425
  const [threadId, id] = args;
419
- if (!threadId || args.length > 2 || !["list", "show", "check", "mark"].includes(action) || (["show", "mark"].includes(action) && !id) || (action === "list" && id)) throw new Error("See codexteer help history.");
426
+ if (!threadId || args.length > 2 || !["list", "show", "check", "mark"].includes(action) || (["show", "mark"].includes(action) && !id) || (action === "list" && id)) throw usage("See codexteer help history.");
420
427
  if (supervisor) await assertSupervisor(threadId, supervisor, { connection });
421
428
  const data = action === "list" ? await listMessages(threadId, { includeText, pending }) : action === "show" ? messageSummary(await getMessage(threadId, id), includeText) : action === "check" ? await reconcileMessages(threadId, id, { includeText }, connectionDependencies) : await markMessage(threadId, id, report);
422
429
  success(`history.${action}`, data, json);
@@ -437,14 +444,14 @@ async function mainOnTarget(argv) {
437
444
  if (stream) json = true;
438
445
  if (command === "watch") {
439
446
  const until = takeOption(args, "--until", undefined), timeout = takeOption(args, "--timeout-ms", undefined);
440
- if (stream && (until !== undefined || timeout !== undefined)) throw new Error("--stream runs until cancelled; do not combine it with --until or --timeout-ms. See codexteer help monitor.");
447
+ if (stream && (until !== undefined || timeout !== undefined)) throw usage("--stream runs until cancelled; do not combine it with --until or --timeout-ms. See codexteer help monitor.");
441
448
  const notify = takeOption(args, "--notify", stream ? "digest" : "all"), settle = takeOption(args, "--settle-ms", undefined), maxHold = takeOption(args, "--max-hold-ms", undefined);
442
- if (!stream && (notify === "digest" || settle !== undefined || maxHold !== undefined)) throw new Error("--notify digest, --settle-ms and --max-hold-ms require --stream.");
443
- if (notify !== "digest" && (settle !== undefined || maxHold !== undefined)) throw new Error("--settle-ms and --max-hold-ms require --notify digest.");
449
+ if (!stream && (notify === "digest" || settle !== undefined || maxHold !== undefined)) throw usage("--notify digest, --settle-ms and --max-hold-ms require --stream.");
450
+ if (notify !== "digest" && (settle !== undefined || maxHold !== undefined)) throw usage("--settle-ms and --max-hold-ms require --notify digest.");
444
451
  Object.assign(options, notificationOptions({ notify, ...(settle === undefined ? {} : { settleMs: Number(settle) }), ...(maxHold === undefined ? {} : { maxHoldMs: Number(maxHold) }) }));
445
452
  Object.assign(options, { watch: true, until: until ?? "change", timeoutMs: Number(timeout ?? "30000"), pollMs: Number(takeOption(args, "--poll-ms", "1000")) });
446
453
  }
447
- if (args.length !== 1) throw new Error(`Expected: ${command} <THREAD>. See codexteer help ${command}.`);
454
+ if (args.length !== 1) throw usage(`Expected: ${command} <THREAD>. See codexteer help ${command}.`);
448
455
  if (stream) { await monitorCommand(args[0], options, connectionDependencies); return; }
449
456
  const data = await observeThread(args[0], options, connectionDependencies);
450
457
  data.connection_mode = connection;
@@ -457,14 +464,10 @@ async function mainOnTarget(argv) {
457
464
  const backend = backendOption(args, "app-server");
458
465
  if (connection === "desktop" && backend === "ui") throw desktopReadOnly();
459
466
  const target = takeOption(args, "--thread", undefined);
460
- if (target !== undefined && backend !== "app-server") throw new Error("doctor --thread requires --backend app-server.");
467
+ if (target !== undefined && backend !== "app-server") throw usage("doctor --thread requires --backend app-server.");
461
468
  const threadId = target === undefined ? undefined : normalizeThreadId(target);
462
- if (supervisor && threadId) {
463
- const record = await getSupervisor(threadId);
464
- if (record && record.session_id !== supervisor && record.state !== "stopped") throw Object.assign(new Error("Another supervisor is registered for this task."), { code: "SUPERVISOR_CONFLICT" });
465
- if (record?.session_id === supervisor) await assertSupervisor(threadId, supervisor, { connection });
466
- }
467
- if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
469
+ if (supervisor && threadId) await assertSupervisorRegistration(threadId, supervisor, { connection });
470
+ if (args.length) throw usage(`Unexpected argument: ${args[0]}`);
468
471
  if (targetHost() !== "local" && backend === "ui") throw remoteReadOnly();
469
472
  const report = backend === "ui" ? desktopDoctor() : await appServerDoctor({ threadId, ...connectionDependencies });
470
473
  if (report.connection) Object.assign(report.connection, { mode: connection, observation_only: connection === "desktop" || (targetHost() !== "local" && !control), ...hostFields() });
@@ -496,7 +499,7 @@ async function mainOnTarget(argv) {
496
499
  if (command === "desktop") {
497
500
  const action = args.shift();
498
501
  if (action === "update-status") {
499
- if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
502
+ if (args.length) throw usage(`Unexpected argument: ${args[0]}`);
500
503
  const data = await desktopUpdateStatus();
501
504
  success("desktop.update-status", data, json);
502
505
  if (!json) console.log(JSON.stringify(data, null, 2));
@@ -506,15 +509,15 @@ async function mainOnTarget(argv) {
506
509
  if (supervisor) throw Object.assign(new Error("A supervisor cannot update or restart Desktop. Run this command from the user's terminal without --supervisor."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
507
510
  const dryRun = takeFlag(args, "--dry-run");
508
511
  const timeoutMs = Number(takeOption(args, "--timeout-ms", "600000"));
509
- if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
512
+ if (args.length) throw usage(`Unexpected argument: ${args[0]}`);
510
513
  const data = await launchDesktopUpdate({ dryRun, timeoutMs });
511
514
  success("desktop.update", data, json);
512
515
  if (!json) console.log(dryRun ? "Dry run: official update, relaunch, normal quit, wrapper startup, and verification." : `Update worker started (${data.worker_pid}). Check: ${data.status_command}`);
513
516
  return;
514
517
  }
515
- if (action !== "start") throw new Error("Expected: codexteer desktop start|update|update-status");
518
+ if (action !== "start") throw usage("Expected: codexteer desktop start|update|update-status");
516
519
  const dryRun = takeFlag(args, "--dry-run");
517
- if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
520
+ if (args.length) throw usage(`Unexpected argument: ${args[0]}`);
518
521
  const data = await startDesktop({ dryRun });
519
522
  success("desktop.start", data, json);
520
523
  if (!json) console.log(dryRun ? "Dry run: would launch Desktop with the shared App Server wrapper." : "Desktop shared App Server is ready.");
@@ -523,9 +526,9 @@ async function mainOnTarget(argv) {
523
526
 
524
527
  if (command === "projects") {
525
528
  if (connection === "desktop") throw desktopReadOnly();
526
- if (args.shift() !== "list") throw new Error("Expected: codexteer projects list [--remote | --all] [--host HOST-ID]");
529
+ if (args.shift() !== "list") throw usage("Expected: codexteer projects list [--remote | --all] [--host HOST-ID]");
527
530
  const remote = takeFlag(args, "--remote"), all = takeFlag(args, "--all");
528
- if (args.length || (remote && all) || (all && targetHost() !== "local")) throw new Error("Use projects list [--remote | --all] or projects list --host HOST-ID.");
531
+ if (args.length || (remote && all) || (all && targetHost() !== "local")) throw usage("Use projects list [--remote | --all] or projects list --host HOST-ID.");
529
532
  const saved = targetHost() !== "local" && remoteTransport() === "paired" ? await listRemoteProjects()
530
533
  : remote || all || targetHost() !== "local" ? await registeredRemoteProjects() : [];
531
534
  const projects = [...(!remote && (all || targetHost() === "local") ? await listProjects() : []),
@@ -536,15 +539,15 @@ async function mainOnTarget(argv) {
536
539
  }
537
540
 
538
541
  if (command === "threads") {
539
- if (args.shift() !== "list") throw new Error("Expected: codexteer threads list");
542
+ if (args.shift() !== "list") throw usage("Expected: codexteer threads list");
540
543
  const limit = Number.parseInt(takeOption(args, "--limit", "20"), 10);
541
544
  const desktopOnly = takeFlag(args, "--desktop-only");
542
545
  const cursor = takeOption(args, "--cursor", undefined), projectId = takeOption(args, "--project", undefined), search = takeOption(args, "--search", undefined);
543
- if (args.length > 0) throw new Error(`Unexpected argument: ${args[0]}`);
546
+ if (args.length > 0) throw usage(`Unexpected argument: ${args[0]}`);
544
547
  if (!Number.isInteger(limit) || limit < 1 || limit > 500) {
545
- throw new Error("--limit must be an integer between 1 and 500.");
548
+ throw usage("--limit must be an integer between 1 and 500.");
546
549
  }
547
- if (targetHost() === "local" && [cursor, projectId, search].some(value => value !== undefined)) throw new Error("--cursor/--project/--search currently require a paired remote host.");
550
+ if (targetHost() === "local" && [cursor, projectId, search].some(value => value !== undefined)) throw usage("--cursor/--project/--search currently require a paired remote host.");
548
551
  const data = targetHost() === "local" ? { threads: await listLocalThreads({ limit, desktopOnly }) } : await listRemoteThreads({ limit, desktopOnly, cursor, projectId, search });
549
552
  const threads = data.threads;
550
553
  success("threads.list", { ...data, count: threads.length }, json);
@@ -558,7 +561,7 @@ async function mainOnTarget(argv) {
558
561
  if (connection === "desktop") throw desktopReadOnly();
559
562
  if (supervisor) throw Object.assign(new Error("Managed supervision is bound to its existing task. Verify a project creation receipt without --supervisor."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
560
563
  const timeoutMs = Number(takeOption(args, "--timeout-ms", "120000"));
561
- if (args.length !== 1 || args[0].startsWith("--")) throw new Error("Expected: thread verify <ATTEMPT-ID> [--timeout-ms N].");
564
+ if (args.length !== 1 || args[0].startsWith("--")) throw usage("Expected: thread verify <ATTEMPT-ID> [--timeout-ms N].");
562
565
  const data = await (targetHost() === "local" ? verifyProjectThread : verifyRemoteProjectThread)(args[0], { timeoutMs });
563
566
  success("thread.verify", data, json);
564
567
  if (!json) console.log(`Verified existing ${data.deep_link}; creation and initial instruction were not repeated.`);
@@ -576,7 +579,7 @@ async function mainOnTarget(argv) {
576
579
  options.timeoutMs = Number(options.timeoutMs);
577
580
  }
578
581
  const literal = args.indexOf("--");
579
- if (args.slice(0, literal < 0 ? args.length : literal).some(arg => arg.startsWith("--"))) throw new Error("Unknown thread create option. Use -- before an instruction starting with --.");
582
+ if (args.slice(0, literal < 0 ? args.length : literal).some(arg => arg.startsWith("--"))) throw usage("Unknown thread create option. Use -- before an instruction starting with --.");
580
583
  const message = readMessage(args);
581
584
  const data = await (targetHost() === "local" ? createProjectThread : createRemoteProjectThread)(selector, message, options);
582
585
  success("thread.create", data, json);
@@ -585,7 +588,7 @@ async function mainOnTarget(argv) {
585
588
  }
586
589
  if (action === "repair") {
587
590
  const apply = takeFlag(args, "--apply"), dryRun = takeFlag(args, "--dry-run");
588
- if (args.length !== 1 || args[0].startsWith("--") || (apply && dryRun)) throw new Error("Expected: thread repair <THREAD> [--dry-run | --apply].");
591
+ if (args.length !== 1 || args[0].startsWith("--") || (apply && dryRun)) throw usage("Expected: thread repair <THREAD> [--dry-run | --apply].");
589
592
  if (connection === "desktop") throw desktopReadOnly();
590
593
  if (supervisor) throw Object.assign(new Error("A supervisor cannot repair task history. Run this command from the user's terminal without --supervisor."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
591
594
  const data = await repairTurns(args[0], { apply });
@@ -593,8 +596,8 @@ async function mainOnTarget(argv) {
593
596
  if (!json) console.log(JSON.stringify(data, null, 2));
594
597
  return;
595
598
  }
596
- if (action !== "resolve") throw new Error("Expected: codexteer thread create|verify|resolve|repair. See help thread.");
597
- if (args.length !== 1) throw new Error("thread resolve requires exactly one thread ID or URL.");
599
+ if (action !== "resolve") throw usage("Expected: codexteer thread create|verify|resolve|repair. See help thread.");
600
+ if (args.length !== 1) throw usage("thread resolve requires exactly one thread ID or URL.");
598
601
  const threadId = normalizeThreadId(args[0]);
599
602
  const data = { thread_id: threadId, deep_link: threadDeepLink(threadId) };
600
603
  success("thread.resolve", data, json);
@@ -603,7 +606,7 @@ async function mainOnTarget(argv) {
603
606
  }
604
607
 
605
608
  if (command === "open") {
606
- if (args.length !== 1) throw new Error("open requires exactly one thread ID or URL.");
609
+ if (args.length !== 1) throw usage("open requires exactly one thread ID or URL.");
607
610
  const threadId = normalizeThreadId(args[0]);
608
611
  const data = targetHost() === "local" ? openDesktopThread(threadId) : await ensureRemoteDesktopSubscription(threadId);
609
612
  success("open", data, json);
@@ -613,7 +616,7 @@ async function mainOnTarget(argv) {
613
616
 
614
617
  if (command === "debug-ui") {
615
618
  const waitMs = Number.parseInt(takeOption(args, "--wait-ms", "1500"), 10);
616
- if (args.length !== 1) throw new Error("debug-ui requires exactly one thread ID or URL.");
619
+ if (args.length !== 1) throw usage("debug-ui requires exactly one thread ID or URL.");
617
620
  const threadId = normalizeThreadId(args[0]);
618
621
  const data = inspectDesktopUi(threadId, { waitMs });
619
622
  success("debug-ui", data, json);
@@ -632,22 +635,23 @@ async function mainOnTarget(argv) {
632
635
  if (remoteTransport() !== "paired") throw remoteReadOnly();
633
636
  throw Object.assign(new Error("UI sending on the remote host is unsupported. No UI or other delivery route was used."), { code: "REMOTE_UI_UNSUPPORTED", sent: false, delivery_status: "not_sent" });
634
637
  }
635
- if (supervisor && backend !== "app-server") throw new Error("Managed supervision requires the app-server backend.");
638
+ if (supervisor && backend !== "app-server") throw usage("Managed supervision requires the app-server backend.");
636
639
  const explicitSound = takeFlag(args, "--sound");
637
640
  const noSound = takeFlag(args, "--no-sound");
638
- if (explicitSound && noSound) throw new Error("--sound and --no-sound cannot be combined.");
639
- if (explicitSound && backend !== "app-server") throw new Error("--sound requires --backend app-server so receipt can be confirmed.");
641
+ if (explicitSound && noSound) throw usage("--sound and --no-sound cannot be combined.");
642
+ if (explicitSound && backend !== "app-server") throw usage("--sound requires --backend app-server so receipt can be confirmed.");
640
643
  const newTurn = takeFlag(args, "--new-turn");
641
644
  const directive = { source: takeOption(args, "--source", undefined), kind: takeOption(args, "--kind", undefined), evidence: takeOptions(args, "--evidence"), basedOn: takeOption(args, "--based-on", undefined), supersedes: takeOption(args, "--supersedes", undefined), expiresAt: takeOption(args, "--expires-at", undefined), checkpoint: takeOption(args, "--checkpoint", undefined), finding: takeOption(args, "--finding", undefined) };
642
- if (backend === "ui" && (directive.source || directive.kind || directive.evidence.length || directive.basedOn || directive.supersedes || directive.expiresAt || directive.checkpoint || directive.finding)) throw new Error("Directive metadata requires --backend app-server.");
645
+ if (backend === "ui" && (directive.source || directive.kind || directive.evidence.length || directive.basedOn || directive.supersedes || directive.expiresAt || directive.checkpoint || directive.finding)) throw usage("Directive metadata requires --backend app-server.");
643
646
  const keepFocus = takeFlag(args, "--keep-focus");
644
647
  const waitInput = takeOption(args, "--wait-ms", undefined);
645
- if (backend !== "ui" && (keepFocus || waitInput != null)) throw new Error("--keep-focus and --wait-ms require --backend ui.");
648
+ if (backend !== "ui" && (keepFocus || waitInput != null)) throw usage("--keep-focus and --wait-ms require --backend ui.");
646
649
  const waitMs = Number(waitInput ?? "1500");
650
+ if (args.slice(0, args.includes("--") ? args.indexOf("--") : args.length).some(arg => arg.startsWith("--"))) throw usage("Unknown send option. Use -- before instruction text starting with --.");
647
651
  const threadInput = args.shift();
648
- if (!threadInput) throw new Error("send requires a thread ID or codex://threads URL.");
652
+ if (!threadInput) throw usage("send requires a thread ID or codex://threads URL.");
649
653
  const message = readMessage(args);
650
- if (message.trim() === "") throw new Error("send requires a non-empty message.");
654
+ if (message.trim() === "") throw usage("send requires a non-empty message.");
651
655
  const threadId = normalizeThreadId(threadInput);
652
656
  const data = backend === "app-server"
653
657
  ? await sendTrackedMessage(threadId, message, { dryRun, newTurn, ...directive, supervisor, connection })
package/src/cloud-cli.mjs CHANGED
@@ -1,18 +1,20 @@
1
- import { desktopCloudProjectDoctor, assignDesktopCloudProject, desktopCloudProjectReceipt } from "./desktop-cloud-project.mjs";
1
+ import { desktopCloudProjectDoctor, assignDesktopCloudProject, desktopCloudProjectReceipt, resolveDesktopCloudProjectAttempt } from "./desktop-cloud-project.mjs";
2
2
  import { listProjects as listDesktopProjects } from "./projects.mjs";
3
3
  import { isDesktopCloudTask, desktopCloudId, desktopCloudLink } from "./desktop-cloud-id.mjs";
4
4
  import { desktopCloudConnection, desktopCloudEnvironments } from "./desktop-cloud-api.mjs";
5
5
  import { createDesktopCloudTask, listDesktopCloudTasks, desktopCloudReceipt } from "./desktop-cloud.mjs";
6
6
  import { isCloudTask } from "./task-id.mjs";
7
7
  import { cloudTaskId } from "./cloud-id.mjs";
8
+ import { normalizeThreadId } from "./thread-id.mjs";
9
+ import { connectionMismatch } from "./connection-error.mjs";
8
10
  import { cloudConnection } from "./cloud-connection.mjs";
9
11
  import { observeThread, printObservation } from "./observe.mjs";
10
12
  import { monitorCommand } from "./monitor.mjs";
11
13
  import { notificationOptions } from "./notify.mjs";
12
14
  import { prepareSupervisorPrompt, superviseAgent } from "./supervise.mjs";
13
- import { supervisorId, registerSupervisor, getSupervisor, assertSupervisor, controlSupervisor, supervisorStatus, listSupervisors } from "./supervision.mjs";
15
+ import { supervisorId, registerSupervisor, getSupervisor, assertSupervisor, assertSupervisorRegistration, controlSupervisor, supervisorStatus, listSupervisors } from "./supervision.mjs";
14
16
  import { createFinding, findingStatus, listFindings, resolveFinding, dismissFinding, reopenFinding, evaluateFinding, findingStats } from "./findings.mjs";
15
- import { sendTrackedMessage, listMessages, getMessage, messageSummary, reconcileMessages, markMessage, listInstructions } from "./journal.mjs";
17
+ import { sendTrackedMessage, listMessages, getMessage, messageSummary, reconcileMessages, markMessage, resolveUnknownDelivery, listInstructions } from "./journal.mjs";
16
18
  import { createCloudTask, cloudTaskDiff, listCloudTasks, cloudCreationReceipt } from "./cloud.mjs";
17
19
  import { cloudSendReceipt, reconcileCloudSend } from "./cloud-observe.mjs";
18
20
  import { cloudEnvironments } from "./cloud-api.mjs";
@@ -67,8 +69,18 @@ export const cloudInvocation = argv => !!selectedCloudInvocation(argv);
67
69
  export async function mainCloud(argv) {
68
70
  const args = [...argv], json = flag(args, "--json"), connection = option(args, "--connection", selectedCloudInvocation(argv) || "cloud");
69
71
  const desktopCloud = connection === "desktop-cloud";
70
- const targetId = desktopCloud ? desktopCloudId : cloudTaskId;
71
- if (!["cloud", "desktop-cloud"].includes(connection)) throw failure("A cloud task requires the cloud connection.", "CLOUD_TARGET_CONFLICT");
72
+ const targetId = desktopCloud ? desktopCloudId : value => {
73
+ try { return cloudTaskId(value); }
74
+ catch (error) {
75
+ // Preserve the Web Cloud ID validator's existing error code.
76
+ if (isDesktopCloudTask(value)) throw connectionMismatch("desktop-cloud", connection, error.code);
77
+ let local = false;
78
+ try { normalizeThreadId(value); local = true; } catch {}
79
+ if (local) throw connectionMismatch("shared", connection, error.code);
80
+ throw error;
81
+ }
82
+ };
83
+ if (!["cloud", "desktop-cloud"].includes(connection)) throw Object.assign(connectionMismatch(selectedCloudInvocation(argv) || "cloud", connection), { execution_environment: "cloud" });
72
84
  const own = args.slice(0, args.includes("--") ? args.indexOf("--") : args.length);
73
85
  if (own.some(value => ["--host", "--remote-transport", "--context-thread"].includes(value))) throw failure("Desktop host routes cannot select a cloud task.", "CLOUD_TARGET_CONFLICT");
74
86
  const requiredNode = option(args, "--require-node-version");
@@ -84,9 +96,8 @@ export async function mainCloud(argv) {
84
96
  const bound = async (target, { allowUnregistered = false } = {}) => {
85
97
  const id = targetId(target);
86
98
  if (supervisor) {
87
- const record = allowUnregistered ? await getSupervisor(id) : null;
88
- if (!allowUnregistered || record?.session_id === supervisor) await assertSupervisor(id, supervisor, { connection });
89
- else if (record && record.state !== "stopped") throw failure("Another supervisor owns this cloud task.", "SUPERVISOR_CONFLICT");
99
+ if (allowUnregistered) await assertSupervisorRegistration(id, supervisor, { connection });
100
+ else await assertSupervisor(id, supervisor, { connection });
90
101
  }
91
102
  return id;
92
103
  };
@@ -160,11 +171,12 @@ export async function mainCloud(argv) {
160
171
  const settings = { taskId: id };
161
172
  output(`history.${action}`, action === "show" ? await cloudSendReceipt(attempt, settings) : await reconcileCloudSend(attempt, settings)); return;
162
173
  }
163
- const includeText = flag(args, "--include-text"), pending = flag(args, "--pending");
164
- const report = action === "mark" ? { status: option(args, "--status"), note: option(args, "--note"), evidence: repeated(args, "--evidence"), by: option(args, "--by", "local") } : {};
174
+ const includeText = flag(args, "--include-text"), pending = flag(args, "--pending"), resolveUnknown = desktopCloud && action === "mark" && flag(args, "--resolve-unknown");
175
+ const report = resolveUnknown ? { note: option(args, "--note") } : action === "mark" ? { status: option(args, "--status"), note: option(args, "--note"), evidence: repeated(args, "--evidence"), by: option(args, "--by", "local") } : {};
165
176
  if (!args[0] || args.length > 2 || !["list", "show", "check", "mark"].includes(action) || (["show", "mark"].includes(action) && !args[1]) || (action === "list" && args[1])) throw failure("Invalid history arguments.");
177
+ if (resolveUnknown && supervisor) throw failure("A supervisor cannot release its own unknown delivery. The user must run this without --supervisor.", "SUPERVISOR_CONTROL_FORBIDDEN");
166
178
  const id = await bound(args[0]); const data = action === "list" ? await listMessages(id, { includeText, pending }) : action === "show" ? messageSummary(await getMessage(id, args[1]), includeText)
167
- : action === "check" ? await reconcileMessages(id, args[1], { includeText }, dependencies) : await markMessage(id, args[1], report);
179
+ : action === "check" ? await reconcileMessages(id, args[1], { includeText }, dependencies) : resolveUnknown ? await resolveUnknownDelivery(id, args[1], report) : await markMessage(id, args[1], report);
168
180
  output(`history.${action}`, data); return;
169
181
  }
170
182
  if (command === "instructions") {
@@ -301,7 +313,8 @@ async function desktopCloudCommands(command, args, { supervisor, output, bound,
301
313
  output("thread.create", await createDesktopCloudTask(environment, message(args), { project, title, dryRun })); return true;
302
314
  }
303
315
  if (action === "assign") { const project = option(args, "--project"); exact(args, 1); const data = await assignDesktopCloudProject(await bound(args[0]), project); output("thread.assign", data); if (data.verification_status !== "verified") process.exitCode = 1; return true; }
304
- exact(args, 1);
316
+ const resolveUnknown = action === "receipt" && flag(args, "--resolve-unknown"), note = resolveUnknown ? option(args, "--note") : undefined; exact(args, 1);
317
+ if (resolveUnknown) { output("thread.receipt", await resolveDesktopCloudProjectAttempt(args[0], { note })); return true; }
305
318
  if (action === "receipt") { let data; try { data = await desktopCloudReceipt(args[0]); } catch (error) { if (error.code !== "DESKTOP_CLOUD_CREATION_RECEIPT_NOT_FOUND") throw error; data = await desktopCloudProjectReceipt(args[0]); } output("thread.receipt", data); return true; }
306
319
  if (action === "resolve") { const id = await bound(args[0]); output("thread.resolve", { thread_id: id, deep_link: desktopCloudLink(id) }); return true; }
307
320
  throw failure("Expected thread create|assign|receipt|resolve.");
@@ -0,0 +1,7 @@
1
+ // Diagnostics only: callers retain their existing routing and rejection rules.
2
+ export function connectionMismatch(expected, selected, code = "CLOUD_TARGET_CONFLICT") {
3
+ const message = expected === "shared"
4
+ ? `For a local task, use --connection shared (the default local connection; got ${selected}).`
5
+ : `This task requires --connection ${expected} (got ${selected}).`;
6
+ return Object.assign(new Error(message), { code, expected_connection: expected, connection: selected });
7
+ }
@@ -1,11 +1,19 @@
1
1
  import { normalizeThreadId } from "./thread-id.mjs";
2
+ import { isCloudTask } from "./cloud-id.mjs";
3
+ import { connectionMismatch } from "./connection-error.mjs";
2
4
  export function isDesktopCloudTask(value) {
3
5
  if (typeof value !== "string") return false;
4
6
  if (value.startsWith("durable_")) return true;
5
7
  try { const u = new URL(value); return u.protocol === "codex:" && u.hostname === "threads" && u.searchParams.getAll("hostId").includes("durable"); } catch { return false; }
6
8
  }
7
9
  export function desktopCloudId(value) {
8
- if (!isDesktopCloudTask(value)) throw Object.assign(new Error("Desktop Cloud requires a durable_ ID or a link with hostId=durable."), { code: "CLOUD_TARGET_CONFLICT" });
10
+ if (!isDesktopCloudTask(value)) {
11
+ if (isCloudTask(value)) throw connectionMismatch("cloud", "desktop-cloud");
12
+ let local = false;
13
+ try { normalizeThreadId(value); local = true; } catch {}
14
+ if (local) throw connectionMismatch("shared", "desktop-cloud");
15
+ throw Object.assign(new Error("Desktop Cloud requires a durable_ ID or a link with hostId=durable."), { code: "CLOUD_TARGET_CONFLICT" });
16
+ }
9
17
  let uuid = value.slice(8);
10
18
  if (!value.startsWith("durable_")) {
11
19
  const url = new URL(value);