orkestrator 2.30.0-beta → 2.31.0-beta

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/dist/client.js CHANGED
@@ -4876,26 +4876,26 @@ import {
4876
4876
  symlink as symlink2,
4877
4877
  utimes
4878
4878
  } from "fs/promises";
4879
- import path15 from "path";
4879
+ import path16 from "path";
4880
4880
  import { pipeline } from "stream/promises";
4881
4881
  import { createGunzip } from "zlib";
4882
4882
  function delay(milliseconds) {
4883
4883
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
4884
4884
  }
4885
4885
  function artifactDirectory(rootDir, artifact) {
4886
- return path15.join(rootDir, artifact.name, artifact.version, `${artifact.platform}-${artifact.architecture}`);
4886
+ return path16.join(rootDir, artifact.name, artifact.version, `${artifact.platform}-${artifact.architecture}`);
4887
4887
  }
4888
4888
  function artifactExecutablePath(rootDir, artifact) {
4889
- return path15.join(artifactDirectory(rootDir, artifact), artifact.executable.fileName);
4889
+ return path16.join(artifactDirectory(rootDir, artifact), artifact.executable.fileName);
4890
4890
  }
4891
4891
  function upstreamExecutablePath(rootDir, artifact) {
4892
- return path15.join(artifactDirectory(rootDir, artifact), `.upstream-${artifact.executable.fileName}`);
4892
+ return path16.join(artifactDirectory(rootDir, artifact), `.upstream-${artifact.executable.fileName}`);
4893
4893
  }
4894
4894
  function artifactCompanions(artifact) {
4895
4895
  return artifact.companions ?? [];
4896
4896
  }
4897
4897
  function companionPath(rootDir, artifact, companion) {
4898
- return path15.join(artifactDirectory(rootDir, artifact), companion.fileName);
4898
+ return path16.join(artifactDirectory(rootDir, artifact), companion.fileName);
4899
4899
  }
4900
4900
  function artifactArchiveBytes(artifact) {
4901
4901
  return artifactCompanions(artifact).reduce((total, companion) => total + companion.archive.size, artifact.archive.size);
@@ -4919,7 +4919,7 @@ function assertValidArtifactMetadata(artifact) {
4919
4919
  }
4920
4920
  const paths = new Set;
4921
4921
  for (const file of artifact.archive.bundleFiles) {
4922
- const normalized = path15.posix.normalize(file.path);
4922
+ const normalized = path16.posix.normalize(file.path);
4923
4923
  if (normalized !== file.path || normalized.startsWith("../") || normalized.startsWith("/") || normalized.includes("\\") || paths.has(normalized) || !Number.isSafeInteger(file.size) || file.size < 1 || !/^[a-f0-9]{64}$/.test(file.sha256)) {
4924
4924
  throw new Error(`${artifact.name} manifest has an unsafe bundle file`);
4925
4925
  }
@@ -4945,7 +4945,7 @@ function assertValidArtifactMetadata(artifact) {
4945
4945
  }
4946
4946
  const activationNames = new Set([artifact.name, artifact.executable.fileName]);
4947
4947
  for (const alias of artifact.activationAliases ?? []) {
4948
- if (!alias || alias.startsWith(".") || alias.includes("/") || alias.includes("\\") || alias !== path15.basename(alias) || activationNames.has(alias)) {
4948
+ if (!alias || alias.startsWith(".") || alias.includes("/") || alias.includes("\\") || alias !== path16.basename(alias) || activationNames.has(alias)) {
4949
4949
  throw new Error(`${artifact.name} manifest has an unsafe or duplicate activation alias`);
4950
4950
  }
4951
4951
  activationNames.add(alias);
@@ -4955,7 +4955,7 @@ function assertValidArtifactMetadata(artifact) {
4955
4955
  if (companion.archive.format !== "zip" && companion.archive.format !== "tar.gz") {
4956
4956
  throw new Error(`${artifact.name} companion has an unsupported archive format`);
4957
4957
  }
4958
- if (!companion.fileName || companion.fileName.startsWith(".") || companion.fileName.includes("/") || companion.fileName.includes("\\") || companion.fileName !== path15.basename(companion.fileName)) {
4958
+ if (!companion.fileName || companion.fileName.startsWith(".") || companion.fileName.includes("/") || companion.fileName.includes("\\") || companion.fileName !== path16.basename(companion.fileName)) {
4959
4959
  throw new Error(`${artifact.name} companion has an unsafe file name`);
4960
4960
  }
4961
4961
  if (companionNames.has(companion.fileName)) {
@@ -5021,7 +5021,7 @@ async function isValidExecutable(rootDir, artifact) {
5021
5021
  if (!await isValidUpstreamExecutable(rootDir, artifact))
5022
5022
  return false;
5023
5023
  for (const bundled of artifact.archive.bundleFiles ?? []) {
5024
- if (!await isValidFile(path15.join(artifactDirectory(rootDir, artifact), ...bundled.path.split("/")), bundled)) {
5024
+ if (!await isValidFile(path16.join(artifactDirectory(rootDir, artifact), ...bundled.path.split("/")), bundled)) {
5025
5025
  return false;
5026
5026
  }
5027
5027
  }
@@ -5038,7 +5038,7 @@ async function isValidExecutable(rootDir, artifact) {
5038
5038
  if ((file.mode & 511) !== 320)
5039
5039
  await chmod(executablePath, 320);
5040
5040
  for (const bundled of artifact.archive.bundleFiles ?? []) {
5041
- if (!await isValidFile(path15.join(artifactDirectory(rootDir, artifact), ...bundled.path.split("/")), bundled)) {
5041
+ if (!await isValidFile(path16.join(artifactDirectory(rootDir, artifact), ...bundled.path.split("/")), bundled)) {
5042
5042
  return false;
5043
5043
  }
5044
5044
  }
@@ -5059,7 +5059,7 @@ async function bundleTreeEntries(directory, artifact) {
5059
5059
  const visit = async (currentDirectory, prefix = "") => {
5060
5060
  for (const entry of await readdir2(currentDirectory, { withFileTypes: true })) {
5061
5061
  const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
5062
- const filePath = path15.join(currentDirectory, entry.name);
5062
+ const filePath = path16.join(currentDirectory, entry.name);
5063
5063
  if (entry.isDirectory()) {
5064
5064
  await visit(filePath, relativePath);
5065
5065
  } else if (entry.isFile() && !entry.isSymbolicLink()) {
@@ -5174,7 +5174,7 @@ async function initializeInstallLock(lockPath, owner, openLockFile) {
5174
5174
  }
5175
5175
  }
5176
5176
  async function acquireInstallLock(rootDir, onProgress, timings, openLockFile, ownerProcessExists) {
5177
- const lockPath = path15.join(rootDir, INSTALL_LOCK);
5177
+ const lockPath = path16.join(rootDir, INSTALL_LOCK);
5178
5178
  const startedAt = Date.now();
5179
5179
  let announcedWait = false;
5180
5180
  while (Date.now() - startedAt < timings.lockWaitTimeoutMs) {
@@ -5355,8 +5355,8 @@ async function extractTarGzipBundle(archivePath, destinationDirectory, label, ar
5355
5355
  stream.resume();
5356
5356
  return;
5357
5357
  }
5358
- const normalized = path15.posix.normalize(relative);
5359
- if (normalized.startsWith("../") || normalized.startsWith("/") || normalized.includes("\\") || path15.isAbsolute(normalized)) {
5358
+ const normalized = path16.posix.normalize(relative);
5359
+ if (normalized.startsWith("../") || normalized.startsWith("/") || normalized.includes("\\") || path16.isAbsolute(normalized)) {
5360
5360
  extract2.destroy(new Error(`Unsafe path in ${label} bundle`));
5361
5361
  stream.resume();
5362
5362
  return;
@@ -5368,7 +5368,7 @@ async function extractTarGzipBundle(archivePath, destinationDirectory, label, ar
5368
5368
  stream.resume();
5369
5369
  return;
5370
5370
  }
5371
- const destination = path15.join(destinationDirectory, ...normalized.split("/"));
5371
+ const destination = path16.join(destinationDirectory, ...normalized.split("/"));
5372
5372
  if (header.type === "directory") {
5373
5373
  stream.once("end", next);
5374
5374
  stream.resume();
@@ -5386,7 +5386,7 @@ async function extractTarGzipBundle(archivePath, destinationDirectory, label, ar
5386
5386
  }
5387
5387
  if (header.name === archive.entryPath)
5388
5388
  foundExecutable = true;
5389
- mkdir3(path15.dirname(destination), { recursive: true, mode: 448 }).then(() => pipeline(stream, createWriteStream(destination, {
5389
+ mkdir3(path16.dirname(destination), { recursive: true, mode: 448 }).then(() => pipeline(stream, createWriteStream(destination, {
5390
5390
  flags: "wx",
5391
5391
  mode: (header.mode ?? 0) & 73 ? 320 : 256
5392
5392
  }))).then(() => next(), (error) => extract2.destroy(error instanceof Error ? error : new Error(String(error))));
@@ -5548,7 +5548,7 @@ async function repairInvalidMacSignature(executablePath, artifact, timeoutMs, sp
5548
5548
  await runCodesign(["--sign", "-", "--force", executablePath], `${artifact.name} could not be ad-hoc signed after source verification`, timeoutMs, spawnProcess);
5549
5549
  }
5550
5550
  async function prepareRepairableExecutable(sourcePath, executablePath, artifact, skipExecutableProbeForTests, processTimeoutMs, spawnProcess) {
5551
- const temporaryPath = path15.join(path15.dirname(executablePath), `.repair-${artifact.executable.fileName}-${randomUUID4()}.tmp`);
5551
+ const temporaryPath = path16.join(path16.dirname(executablePath), `.repair-${artifact.executable.fileName}-${randomUUID4()}.tmp`);
5552
5552
  try {
5553
5553
  await copyFile2(sourcePath, temporaryPath);
5554
5554
  await chmod(temporaryPath, 448);
@@ -5580,8 +5580,8 @@ async function refreshRepairableArtifact(rootDir, artifact, skipExecutableProbeF
5580
5580
  async function stageCompanions(stagingDirectory, artifact, companions, fetchImpl, onBytes, startBytes, allowInsecureDownloadsForTests, skipExecutableProbeForTests, timings, spawnProcess) {
5581
5581
  let downloadedBytes = startBytes;
5582
5582
  for (const companion of companions) {
5583
- const companionArchivePath = path15.join(stagingDirectory, `.archive-${companion.fileName}.${archiveExtension(companion.archive)}`);
5584
- const companionFilePath = path15.join(stagingDirectory, companion.fileName);
5583
+ const companionArchivePath = path16.join(stagingDirectory, `.archive-${companion.fileName}.${archiveExtension(companion.archive)}`);
5584
+ const companionFilePath = path16.join(stagingDirectory, companion.fileName);
5585
5585
  const precedingBytes = downloadedBytes;
5586
5586
  await downloadArchive(companion.fileName, companion.archive, companionArchivePath, fetchImpl, (received) => onBytes(precedingBytes + received), allowInsecureDownloadsForTests, timings.downloadTimeoutMs);
5587
5587
  downloadedBytes += companion.archive.size;
@@ -5606,28 +5606,28 @@ async function stageCompanions(stagingDirectory, artifact, companions, fetchImpl
5606
5606
  }
5607
5607
  }
5608
5608
  async function repairArtifactCompanions(rootDir, artifact, companions, fetchImpl, onBytes, onVerify, allowInsecureDownloadsForTests, skipExecutableProbeForTests, timings, spawnProcess) {
5609
- const stagingDirectory = await mkdtemp2(path15.join(rootDir, `.staging-${artifact.name}-`));
5609
+ const stagingDirectory = await mkdtemp2(path16.join(rootDir, `.staging-${artifact.name}-`));
5610
5610
  try {
5611
5611
  await stageCompanions(stagingDirectory, artifact, companions, fetchImpl, onBytes, 0, allowInsecureDownloadsForTests, skipExecutableProbeForTests, timings, spawnProcess);
5612
5612
  onVerify();
5613
5613
  const destinationDirectory = artifactDirectory(rootDir, artifact);
5614
5614
  for (const companion of companions) {
5615
- await rename2(path15.join(stagingDirectory, companion.fileName), path15.join(destinationDirectory, companion.fileName));
5615
+ await rename2(path16.join(stagingDirectory, companion.fileName), path16.join(destinationDirectory, companion.fileName));
5616
5616
  }
5617
5617
  } finally {
5618
5618
  await rm3(stagingDirectory, { recursive: true, force: true });
5619
5619
  }
5620
5620
  }
5621
5621
  async function installArtifact(rootDir, artifact, fetchImpl, onBytes, onVerify, allowInsecureDownloadsForTests, skipExecutableProbeForTests, timings, spawnProcess) {
5622
- const stagingDirectory = await mkdtemp2(path15.join(rootDir, `.staging-${artifact.name}-`));
5623
- const archivePath = path15.join(stagingDirectory, `archive.${archiveExtension(artifact.archive)}`);
5624
- const executablePath = path15.join(stagingDirectory, artifact.executable.fileName);
5625
- const extractedPath = artifact.executable.repairInvalidMacSignature ? path15.join(stagingDirectory, `.upstream-${artifact.executable.fileName}`) : executablePath;
5622
+ const stagingDirectory = await mkdtemp2(path16.join(rootDir, `.staging-${artifact.name}-`));
5623
+ const archivePath = path16.join(stagingDirectory, `archive.${archiveExtension(artifact.archive)}`);
5624
+ const executablePath = path16.join(stagingDirectory, artifact.executable.fileName);
5625
+ const extractedPath = artifact.executable.repairInvalidMacSignature ? path16.join(stagingDirectory, `.upstream-${artifact.executable.fileName}`) : executablePath;
5626
5626
  try {
5627
5627
  await downloadArchive(artifact.name, artifact.archive, archivePath, fetchImpl, onBytes, allowInsecureDownloadsForTests, timings.downloadTimeoutMs);
5628
5628
  if (artifact.archive.bundleRoot) {
5629
5629
  await extractTarGzipBundle(archivePath, stagingDirectory, artifact.name, artifact.archive);
5630
- const bundledExecutable = path15.join(stagingDirectory, ...artifact.archive.entryPath.slice(artifact.archive.bundleRoot.length).split("/"));
5630
+ const bundledExecutable = path16.join(stagingDirectory, ...artifact.archive.entryPath.slice(artifact.archive.bundleRoot.length).split("/"));
5631
5631
  if (bundledExecutable !== extractedPath) {
5632
5632
  await rename2(bundledExecutable, extractedPath);
5633
5633
  }
@@ -5678,7 +5678,7 @@ async function installArtifact(rootDir, artifact, fetchImpl, onBytes, onVerify,
5678
5678
  await stageCompanions(stagingDirectory, artifact, artifactCompanions(artifact), fetchImpl, onBytes, artifact.archive.size, allowInsecureDownloadsForTests, skipExecutableProbeForTests, timings, spawnProcess);
5679
5679
  onVerify();
5680
5680
  const destinationDirectory = artifactDirectory(rootDir, artifact);
5681
- await mkdir3(path15.dirname(destinationDirectory), { recursive: true, mode: 448 });
5681
+ await mkdir3(path16.dirname(destinationDirectory), { recursive: true, mode: 448 });
5682
5682
  await rm3(destinationDirectory, { recursive: true, force: true });
5683
5683
  await rename2(stagingDirectory, destinationDirectory);
5684
5684
  await chmod(destinationDirectory, 448);
@@ -5690,16 +5690,16 @@ async function installArtifact(rootDir, artifact, fetchImpl, onBytes, onVerify,
5690
5690
  }
5691
5691
  async function cleanStagingDirectories(rootDir) {
5692
5692
  const entries = await readdir2(rootDir, { withFileTypes: true });
5693
- await Promise.all(entries.filter((entry) => entry.isDirectory() && entry.name.startsWith(".staging-")).map((entry) => rm3(path15.join(rootDir, entry.name), { recursive: true, force: true })));
5693
+ await Promise.all(entries.filter((entry) => entry.isDirectory() && entry.name.startsWith(".staging-")).map((entry) => rm3(path16.join(rootDir, entry.name), { recursive: true, force: true })));
5694
5694
  }
5695
5695
  async function touchArtifactDirectory(rootDir, artifact) {
5696
5696
  const now = new Date;
5697
- await utimes(path15.dirname(artifactDirectory(rootDir, artifact)), now, now).catch(() => {
5697
+ await utimes(path16.dirname(artifactDirectory(rootDir, artifact)), now, now).catch(() => {
5698
5698
  return;
5699
5699
  });
5700
5700
  }
5701
5701
  function versionLeaseDirectory(rootDir, name, version) {
5702
- return path15.join(rootDir, LEASE_DIRECTORY, name, version);
5702
+ return path16.join(rootDir, LEASE_DIRECTORY, name, version);
5703
5703
  }
5704
5704
  async function acquireVersionLease(rootDir, artifact, openLeaseFile) {
5705
5705
  const leaseKey = `${rootDir}\x00${artifact.name}\x00${artifact.version}`;
@@ -5714,7 +5714,7 @@ async function acquireVersionLease(rootDir, artifact, openLeaseFile) {
5714
5714
  };
5715
5715
  const directory = versionLeaseDirectory(rootDir, artifact.name, artifact.version);
5716
5716
  await mkdir3(directory, { recursive: true, mode: 448 });
5717
- const leasePath = path15.join(directory, `${owner.pid}-${owner.token}.json`);
5717
+ const leasePath = path16.join(directory, `${owner.pid}-${owner.token}.json`);
5718
5718
  const handle = await openLeaseFile(leasePath, "wx", 384);
5719
5719
  try {
5720
5720
  await handle.writeFile(JSON.stringify(owner), "utf8");
@@ -5735,7 +5735,7 @@ async function versionHasLiveLease(rootDir, name, version, ownerProcessExists) {
5735
5735
  const entries = await readdir2(directory, { withFileTypes: true }).catch(() => []);
5736
5736
  let live = false;
5737
5737
  await Promise.all(entries.filter((entry) => entry.isFile()).map(async (entry) => {
5738
- const leasePath = path15.join(directory, entry.name);
5738
+ const leasePath = path16.join(directory, entry.name);
5739
5739
  try {
5740
5740
  const parsed = JSON.parse(await readFile2(leasePath, "utf8"));
5741
5741
  if (typeof parsed.token === "string" && typeof parsed.pid === "number" && typeof parsed.createdAt === "string" && ownerProcessExists(parsed.pid)) {
@@ -5758,11 +5758,11 @@ async function pruneSupersededVersions(rootDir, artifacts, retainSupersededMs, o
5758
5758
  }
5759
5759
  const staleBefore = Date.now() - retainSupersededMs;
5760
5760
  await Promise.all(Array.from(keepByName, async ([name, keep]) => {
5761
- const toolDir = path15.join(rootDir, name);
5761
+ const toolDir = path16.join(rootDir, name);
5762
5762
  try {
5763
5763
  const entries = await readdir2(toolDir, { withFileTypes: true });
5764
5764
  await Promise.all(entries.filter((entry) => entry.isDirectory() && !keep.has(entry.name)).map(async (entry) => {
5765
- const versionDir = path15.join(toolDir, entry.name);
5765
+ const versionDir = path16.join(toolDir, entry.name);
5766
5766
  try {
5767
5767
  if ((await stat2(versionDir)).mtimeMs > staleBefore)
5768
5768
  return;
@@ -5803,8 +5803,8 @@ function activationSetId(artifacts) {
5803
5803
  return createHash4("sha256").update(JSON.stringify(identity)).digest("hex");
5804
5804
  }
5805
5805
  async function activateExecutables(rootDir, artifacts) {
5806
- const binRoot = path15.join(rootDir, "bin");
5807
- const binDir = path15.join(binRoot, activationSetId(artifacts));
5806
+ const binRoot = path16.join(rootDir, "bin");
5807
+ const binDir = path16.join(binRoot, activationSetId(artifacts));
5808
5808
  await mkdir3(binRoot, { recursive: true, mode: 448 });
5809
5809
  const binRootEntry = await lstat(binRoot);
5810
5810
  if (!binRootEntry.isDirectory() || binRootEntry.isSymbolicLink()) {
@@ -5819,8 +5819,8 @@ async function activateExecutables(rootDir, artifacts) {
5819
5819
  await chmod(binDir, 448);
5820
5820
  const executables = {};
5821
5821
  const activate = async (linkName, target) => {
5822
- const activePath = path15.join(binDir, linkName);
5823
- const temporaryLink = path15.join(binDir, `.${linkName}-${randomUUID4()}.tmp`);
5822
+ const activePath = path16.join(binDir, linkName);
5823
+ const temporaryLink = path16.join(binDir, `.${linkName}-${randomUUID4()}.tmp`);
5824
5824
  const existingTarget = await readlink(activePath).catch(() => null);
5825
5825
  if (existingTarget !== target) {
5826
5826
  await symlink2(target, temporaryLink, "file");
@@ -5868,7 +5868,7 @@ async function ensurePinnedToolchains(options2) {
5868
5868
  const timings = { ...DEFAULT_TIMINGS, ...options2.timingsForTests };
5869
5869
  const spawnProcess = options2.spawnForTests ?? spawn;
5870
5870
  const ownerProcessExists = options2.processExistsForTests ?? ((pid) => processExists(pid, options2.processKillForTests));
5871
- const rootDir = path15.join(options2.dataDir, TOOLCHAIN_DIRECTORY);
5871
+ const rootDir = path16.join(options2.dataDir, TOOLCHAIN_DIRECTORY);
5872
5872
  const totalTools = artifacts.length;
5873
5873
  const toolFractions = new Map(artifacts.map((artifact) => [artifact.name, 0]));
5874
5874
  const progress = (value) => {
@@ -6033,7 +6033,7 @@ var init_agent_platforms = __esm(() => {
6033
6033
 
6034
6034
  // ../toolchain/src/platform-selection.ts
6035
6035
  import { promises as fs7 } from "fs";
6036
- import path16 from "path";
6036
+ import path17 from "path";
6037
6037
  async function readJsonFile(filePath) {
6038
6038
  try {
6039
6039
  return JSON.parse(await fs7.readFile(filePath, "utf8"));
@@ -6042,7 +6042,7 @@ async function readJsonFile(filePath) {
6042
6042
  }
6043
6043
  }
6044
6044
  async function loadAgentPlatformSelection(dataDir) {
6045
- const config = await readJsonFile(path16.join(dataDir, AGENT_CONFIG_FILE));
6045
+ const config = await readJsonFile(path17.join(dataDir, AGENT_CONFIG_FILE));
6046
6046
  if (config && typeof config === "object" && !Array.isArray(config)) {
6047
6047
  const global2 = config.global;
6048
6048
  if (global2 && typeof global2 === "object" && !Array.isArray(global2)) {
@@ -6058,7 +6058,7 @@ async function loadAgentPlatformSelection(dataDir) {
6058
6058
  needsFirstRunChoice: false
6059
6059
  };
6060
6060
  }
6061
- const sidecar = await readJsonFile(path16.join(dataDir, AGENT_PLATFORM_SELECTION_FILE));
6061
+ const sidecar = await readJsonFile(path17.join(dataDir, AGENT_PLATFORM_SELECTION_FILE));
6062
6062
  if (sidecar && typeof sidecar === "object" && !Array.isArray(sidecar)) {
6063
6063
  const enabled = normalizeAgentPlatforms(sidecar.enabled, []);
6064
6064
  if (enabled.length > 0)
@@ -6074,7 +6074,7 @@ var init_platform_selection = __esm(() => {
6074
6074
  // ../toolchain/src/install.ts
6075
6075
  import { randomUUID as randomUUID5 } from "crypto";
6076
6076
  import { lstat as lstat2, mkdir as mkdir4, readdir as readdir3, readlink as readlink2, rename as rename3, rm as rm4, symlink as symlink3 } from "fs/promises";
6077
- import path17 from "path";
6077
+ import path18 from "path";
6078
6078
  async function resolveToolchainSelection(options2) {
6079
6079
  const known = Object.keys(PINNED_TOOL_VERSIONS);
6080
6080
  const withSupportTools = (chosen) => known.filter((name) => chosen.has(name) || isSupportToolName(name));
@@ -6133,16 +6133,16 @@ async function installPinnedToolchains(options2) {
6133
6133
  };
6134
6134
  }
6135
6135
  async function toolsInCurrent(rootDir) {
6136
- const entries = await readdir3(path17.join(rootDir, "bin", CURRENT_TOOLCHAIN_LINK)).catch(() => []);
6136
+ const entries = await readdir3(path18.join(rootDir, "bin", CURRENT_TOOLCHAIN_LINK)).catch(() => []);
6137
6137
  return Object.keys(PINNED_TOOL_VERSIONS).filter((tool) => entries.includes(tool));
6138
6138
  }
6139
6139
  async function pointCurrentAt(rootDir, binDir) {
6140
- const binRoot = path17.join(rootDir, "bin");
6141
- if (path17.dirname(binDir) !== binRoot) {
6140
+ const binRoot = path18.join(rootDir, "bin");
6141
+ if (path18.dirname(binDir) !== binRoot) {
6142
6142
  throw new Error(`Activation directory ${binDir} is not directly inside ${binRoot}`);
6143
6143
  }
6144
- const link = path17.join(binRoot, CURRENT_TOOLCHAIN_LINK);
6145
- const target = path17.basename(binDir);
6144
+ const link = path18.join(binRoot, CURRENT_TOOLCHAIN_LINK);
6145
+ const target = path18.basename(binDir);
6146
6146
  await mkdir4(binRoot, { recursive: true, mode: 448 });
6147
6147
  const existing = await lstat2(link).catch(() => null);
6148
6148
  if (existing && !existing.isSymbolicLink()) {
@@ -6150,7 +6150,7 @@ async function pointCurrentAt(rootDir, binDir) {
6150
6150
  }
6151
6151
  if (existing && await readlink2(link) === target)
6152
6152
  return link;
6153
- const temporary = path17.join(binRoot, `.${CURRENT_TOOLCHAIN_LINK}-${randomUUID5()}.tmp`);
6153
+ const temporary = path18.join(binRoot, `.${CURRENT_TOOLCHAIN_LINK}-${randomUUID5()}.tmp`);
6154
6154
  await symlink3(target, temporary, "dir");
6155
6155
  try {
6156
6156
  await rename3(temporary, link);
@@ -6294,7 +6294,15 @@ var PUBLIC_ACTIONS = {
6294
6294
  effects: ["metadata"],
6295
6295
  observation: "receipt",
6296
6296
  milestone: "B",
6297
- summary: "Remove an empty project registration; never deletes checkouts or remotes."
6297
+ summary: "Remove a project registration (with cascade, after deleting its environments); never deletes checkouts or remotes."
6298
+ },
6299
+ "project.init": {
6300
+ version: 1,
6301
+ mutation: true,
6302
+ effects: ["metadata", "filesystem"],
6303
+ observation: "receipt",
6304
+ milestone: "B",
6305
+ summary: "Initialize a local-only Git repository (initial commit, no remote) at a backend path and register it."
6298
6306
  },
6299
6307
  "project.config.get": {
6300
6308
  version: 1,
@@ -6459,7 +6467,23 @@ var PUBLIC_ACTIONS = {
6459
6467
  effects: ["provider-session"],
6460
6468
  observation: "run",
6461
6469
  milestone: "C",
6462
- summary: "Send a follow-up prompt to one explicit idle conversation."
6470
+ summary: "Send a follow-up prompt to one explicit idle conversation, or queue it behind a busy one."
6471
+ },
6472
+ "session.commands": {
6473
+ version: 1,
6474
+ mutation: false,
6475
+ effects: [],
6476
+ observation: "immediate",
6477
+ milestone: "C",
6478
+ summary: "List the session's slash-command catalogue (IDs, availability, argument policy)."
6479
+ },
6480
+ "session.command": {
6481
+ version: 1,
6482
+ mutation: true,
6483
+ effects: ["provider-session"],
6484
+ observation: "run",
6485
+ milestone: "C",
6486
+ summary: "Run one selected slash command; a stale or unavailable selection fails, never sends text."
6463
6487
  },
6464
6488
  "session.stop": {
6465
6489
  version: 1,
@@ -6533,6 +6557,14 @@ var PUBLIC_ACTIONS = {
6533
6557
  milestone: "C",
6534
6558
  summary: "Answer one exact pending interaction at an expected revision."
6535
6559
  },
6560
+ "session.interaction.hold": {
6561
+ version: 1,
6562
+ mutation: true,
6563
+ effects: ["provider-session"],
6564
+ observation: "receipt",
6565
+ milestone: "C",
6566
+ summary: "Pause or resume one pending interaction's countdown where its deadline is enforced."
6567
+ },
6536
6568
  "session.transcript": {
6537
6569
  version: 1,
6538
6570
  mutation: false,
@@ -6557,6 +6589,70 @@ var PUBLIC_ACTIONS = {
6557
6589
  milestone: "D",
6558
6590
  summary: "Read one page of a session's turns, each with the backend's change projection."
6559
6591
  },
6592
+ "review.launch": {
6593
+ version: 1,
6594
+ mutation: true,
6595
+ effects: ["metadata", "provider-session"],
6596
+ observation: "receipt",
6597
+ milestone: "D",
6598
+ summary: "Start an environment's Multi Review, as the Multi Review button does: reviewers, consolidation, then optional Fix and PR, with saved defaults for what is omitted; an active review is reused."
6599
+ },
6600
+ "review.list": {
6601
+ version: 1,
6602
+ mutation: false,
6603
+ effects: [],
6604
+ observation: "immediate",
6605
+ milestone: "D",
6606
+ summary: "List an environment's Multi Reviews, newest first."
6607
+ },
6608
+ "review.get": {
6609
+ version: 1,
6610
+ mutation: false,
6611
+ effects: [],
6612
+ observation: "immediate",
6613
+ milestone: "D",
6614
+ summary: "Read one Multi Review: phase, reviewers, consolidation, Fix and PR steps."
6615
+ },
6616
+ "review.open": {
6617
+ version: 1,
6618
+ mutation: true,
6619
+ effects: ["metadata"],
6620
+ observation: "receipt",
6621
+ milestone: "D",
6622
+ summary: "Open or focus a Multi Review's root tab or its Fix tab; starts no work."
6623
+ },
6624
+ "review.address": {
6625
+ version: 1,
6626
+ mutation: true,
6627
+ effects: ["provider-session"],
6628
+ observation: "receipt",
6629
+ milestone: "D",
6630
+ summary: "Address a Multi Review's findings: hand them to its Fix session (the Address findings button)."
6631
+ },
6632
+ "review.cancel": {
6633
+ version: 1,
6634
+ mutation: true,
6635
+ effects: ["process", "provider-session"],
6636
+ observation: "receipt",
6637
+ milestone: "D",
6638
+ summary: "Cancel a running Multi Review; its record and tabs are kept."
6639
+ },
6640
+ "pr.create": {
6641
+ version: 1,
6642
+ mutation: true,
6643
+ effects: ["provider-session", "external-repository"],
6644
+ observation: "run",
6645
+ milestone: "D",
6646
+ summary: "Start the Create PR agent session in an environment (the PR tab): commit, push and open a pull request against the target branch. It never merges."
6647
+ },
6648
+ "pr.resolve": {
6649
+ version: 1,
6650
+ mutation: true,
6651
+ effects: ["provider-session", "external-repository"],
6652
+ observation: "run",
6653
+ milestone: "D",
6654
+ summary: "Start the Resolve Conflicts agent session in an environment (the Resolve tab): merge the target branch, resolve and validate conflicts, commit and push."
6655
+ },
6560
6656
  "attention.list": {
6561
6657
  version: 1,
6562
6658
  mutation: false,
@@ -6621,6 +6717,78 @@ var PUBLIC_ACTIONS = {
6621
6717
  milestone: "D",
6622
6718
  summary: "Place a message in one tab's inbox; external messages are never injected into a turn."
6623
6719
  },
6720
+ "node.list": {
6721
+ version: 1,
6722
+ mutation: false,
6723
+ effects: [],
6724
+ observation: "immediate",
6725
+ milestone: "D",
6726
+ summary: "List this machine and the leaf nodes (remote machines) it controls, and whether each can run actions; pass its nodeId to any other action to run it there."
6727
+ },
6728
+ "schedule.list": {
6729
+ version: 1,
6730
+ mutation: false,
6731
+ effects: [],
6732
+ observation: "immediate",
6733
+ milestone: "D",
6734
+ summary: "List scheduled jobs with their next fire time, owner and last outcome."
6735
+ },
6736
+ "schedule.get": {
6737
+ version: 1,
6738
+ mutation: false,
6739
+ effects: [],
6740
+ observation: "immediate",
6741
+ milestone: "D",
6742
+ summary: "Read one scheduled job with its bounded fire history."
6743
+ },
6744
+ "schedule.create": {
6745
+ version: 1,
6746
+ mutation: true,
6747
+ effects: ["metadata"],
6748
+ observation: "receipt",
6749
+ milestone: "D",
6750
+ summary: "Schedule an action, a prompt to an agent tab or an environment launch, once or on a cron or interval."
6751
+ },
6752
+ "schedule.update": {
6753
+ version: 1,
6754
+ mutation: true,
6755
+ effects: ["metadata"],
6756
+ observation: "receipt",
6757
+ milestone: "D",
6758
+ summary: "Change a scheduled job's name, time, time zone, catch-up window or action."
6759
+ },
6760
+ "schedule.pause": {
6761
+ version: 1,
6762
+ mutation: true,
6763
+ effects: ["metadata"],
6764
+ observation: "receipt",
6765
+ milestone: "D",
6766
+ summary: "Pause a scheduled job; nothing fires until it is resumed."
6767
+ },
6768
+ "schedule.resume": {
6769
+ version: 1,
6770
+ mutation: true,
6771
+ effects: ["metadata"],
6772
+ observation: "receipt",
6773
+ milestone: "D",
6774
+ summary: "Resume a paused scheduled job from its next future slot."
6775
+ },
6776
+ "schedule.run": {
6777
+ version: 1,
6778
+ mutation: true,
6779
+ effects: ["metadata"],
6780
+ observation: "receipt",
6781
+ milestone: "D",
6782
+ summary: "Fire a scheduled job now, once, without changing its schedule."
6783
+ },
6784
+ "schedule.delete": {
6785
+ version: 1,
6786
+ mutation: true,
6787
+ effects: ["metadata"],
6788
+ observation: "receipt",
6789
+ milestone: "D",
6790
+ summary: "Delete a scheduled job and its history."
6791
+ },
6624
6792
  "run.get": {
6625
6793
  version: 1,
6626
6794
  mutation: false,
@@ -6760,6 +6928,7 @@ var PUBLIC_ERROR_EXIT = {
6760
6928
  "not-found": PUBLIC_EXIT.notFound,
6761
6929
  "ambiguous-target": PUBLIC_EXIT.notFound,
6762
6930
  "history-expired": PUBLIC_EXIT.notFound,
6931
+ "node-not-found": PUBLIC_EXIT.notFound,
6763
6932
  "connection-failed": PUBLIC_EXIT.connection,
6764
6933
  "connection-not-configured": PUBLIC_EXIT.connection,
6765
6934
  "profile-unavailable": PUBLIC_EXIT.connection,
@@ -6767,11 +6936,13 @@ var PUBLIC_ERROR_EXIT = {
6767
6936
  "identity-mismatch": PUBLIC_EXIT.connection,
6768
6937
  "response-invalid": PUBLIC_EXIT.connection,
6769
6938
  "transport-uncertain": PUBLIC_EXIT.connection,
6939
+ "node-unavailable": PUBLIC_EXIT.connection,
6770
6940
  "deadline-exceeded": PUBLIC_EXIT.deadline,
6771
6941
  "observation-interrupted": PUBLIC_SIGNAL_EXIT.SIGINT,
6772
6942
  "interaction-required": PUBLIC_EXIT.interactionRequired,
6773
6943
  "dispatch-unknown": PUBLIC_EXIT.unknownDispatch,
6774
6944
  "run-unknown": PUBLIC_EXIT.unknownDispatch,
6945
+ "node-outcome-unknown": PUBLIC_EXIT.unknownDispatch,
6775
6946
  conflict: PUBLIC_EXIT.conflict,
6776
6947
  "request-conflict": PUBLIC_EXIT.conflict,
6777
6948
  "revision-conflict": PUBLIC_EXIT.conflict,
@@ -6787,7 +6958,8 @@ var PUBLIC_ERROR_EXIT = {
6787
6958
  "target-mismatch": PUBLIC_EXIT.conflict,
6788
6959
  "session-idle": PUBLIC_EXIT.conflict,
6789
6960
  "interaction-stale": PUBLIC_EXIT.conflict,
6790
- "dispatch-parked": PUBLIC_EXIT.conflict
6961
+ "dispatch-parked": PUBLIC_EXIT.conflict,
6962
+ "node-unsupported": PUBLIC_EXIT.conflict
6791
6963
  };
6792
6964
  function isPublicErrorCode(value) {
6793
6965
  return typeof value === "string" && Object.hasOwn(PUBLIC_ERROR_EXIT, value);
@@ -6820,6 +6992,10 @@ var PUBLIC_API_LIMITS = Object.freeze({
6820
6992
  attentionMaxItems: 50,
6821
6993
  responseMaxBytes: 8 * 1024 * 1024,
6822
6994
  requestMaxBytes: 1024 * 1024,
6995
+ promptAttachmentsMaxItems: 8,
6996
+ promptAttachmentsMaxBytes: 256 * 1024,
6997
+ promptAttachmentNameMaxChars: 128,
6998
+ commandArgumentsMaxBytes: 64 * 1024,
6823
6999
  execArgvMaxItems: 256,
6824
7000
  execArgvMaxBytes: 256 * 1024,
6825
7001
  execEnvMaxEntries: 64,
@@ -6858,6 +7034,17 @@ var TERMINAL_EXECUTION_PHASES = Object.freeze([
6858
7034
  "cancelled",
6859
7035
  "interrupted"
6860
7036
  ]);
7037
+ var PUBLIC_ALL_NODES = "all";
7038
+ var PUBLIC_AGGREGATE_ACTIONS = Object.freeze([
7039
+ "project.list",
7040
+ "environment.list",
7041
+ "attention.list"
7042
+ ]);
7043
+ var PUBLIC_LOCAL_ONLY_ACTIONS = Object.freeze(["node.list"]);
7044
+ var NODE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/;
7045
+ function isPublicNodeTarget(value) {
7046
+ return typeof value === "string" && NODE_ID_PATTERN.test(value);
7047
+ }
6861
7048
  function isPublicActionAvailable(capabilities, action) {
6862
7049
  const entry = capabilities.actions[action];
6863
7050
  return entry?.available === true && entry.version === PUBLIC_ACTIONS[action].version;
@@ -6878,7 +7065,7 @@ var OPERATION_STATES = [
6878
7065
  function isPublicReceipt(value) {
6879
7066
  if (!isRecord(value))
6880
7067
  return false;
6881
- return typeof value.operationId === "string" && value.operationId.length > 0 && typeof value.namespace === "string" && isPublicRequestId(value.requestId) && isPublicActionName(value.action) && OPERATION_STATES.includes(value.state) && typeof value.stage === "string" && typeof value.replayed === "boolean" && isRecord(value.resources) && typeof value.createdAt === "string" && typeof value.updatedAt === "string" && typeof value.retainedUntil === "string" && (value.dispatch === undefined || isRecord(value.dispatch)) && (value.execution === undefined || isRecord(value.execution));
7068
+ return typeof value.operationId === "string" && value.operationId.length > 0 && typeof value.namespace === "string" && isPublicRequestId(value.requestId) && isPublicActionName(value.action) && OPERATION_STATES.includes(value.state) && typeof value.stage === "string" && typeof value.replayed === "boolean" && isRecord(value.resources) && typeof value.createdAt === "string" && typeof value.updatedAt === "string" && typeof value.retainedUntil === "string" && (value.dispatch === undefined || isRecord(value.dispatch)) && (value.execution === undefined || isRecord(value.execution)) && (value.result === undefined || isRecord(value.result));
6882
7069
  }
6883
7070
  function isEnvelopeError(value) {
6884
7071
  return isRecord(value) && isPublicErrorCode(value.code) && typeof value.message === "string" && typeof value.exitCode === "number" && (value.exitCode === exitCodeForError(value.code) || value.code === "observation-interrupted" && value.exitCode === PUBLIC_SIGNAL_EXIT.SIGTERM) && (value.details === undefined || isRecord(value.details));
@@ -6895,6 +7082,9 @@ function isPublicActionResponse(value) {
6895
7082
  if (value.backend !== undefined && !(isRecord(value.backend) && typeof value.backend.installationId === "string" && typeof value.backend.generation === "string")) {
6896
7083
  return false;
6897
7084
  }
7085
+ if (value.node !== undefined && !(isRecord(value.node) && typeof value.node.nodeId === "string" && typeof value.node.name === "string")) {
7086
+ return false;
7087
+ }
6898
7088
  if (value.ok === true) {
6899
7089
  return Object.hasOwn(value, "result") && value.error === undefined && (value.warnings === undefined || Array.isArray(value.warnings) && value.warnings.every((w) => typeof w === "string"));
6900
7090
  }
@@ -6939,6 +7129,7 @@ function parsePublicDuration(value) {
6939
7129
  // src/client/errors.ts
6940
7130
  class CliError extends Error {
6941
7131
  code;
7132
+ node;
6942
7133
  details;
6943
7134
  receipt;
6944
7135
  retryable;
@@ -6947,6 +7138,7 @@ class CliError extends Error {
6947
7138
  super(message);
6948
7139
  this.name = "CliError";
6949
7140
  this.code = code;
7141
+ this.node = options2.node;
6950
7142
  this.details = options2.details;
6951
7143
  this.receipt = options2.receipt;
6952
7144
  this.retryable = options2.retryable;
@@ -6968,6 +7160,12 @@ var GLOBAL_OPTIONS = [
6968
7160
  valueName: "NAME",
6969
7161
  description: "Use a running isolated development profile (`mise run dev:test`)."
6970
7162
  },
7163
+ {
7164
+ name: "node",
7165
+ kind: "string",
7166
+ valueName: "NODE_ID|all",
7167
+ description: "Run on a leaf node the connected command node controls (`node list`); `all` for project list, environment list and attention across nodes."
7168
+ },
6971
7169
  { name: "json", kind: "boolean", description: "Print exactly one JSON envelope on stdout." },
6972
7170
  {
6973
7171
  name: "output",
@@ -7166,6 +7364,12 @@ function parseInvocation(tree, argv) {
7166
7364
  }
7167
7365
  if (typeof globalState.values.profile === "string")
7168
7366
  global2.profile = globalState.values.profile;
7367
+ if (typeof globalState.values.node === "string") {
7368
+ if (!isPublicNodeTarget(globalState.values.node)) {
7369
+ throw new CliError("invalid-input", "--node expects a node ID from `node list`, or all");
7370
+ }
7371
+ global2.node = globalState.values.node;
7372
+ }
7169
7373
  if (global2.connection && global2.profile) {
7170
7374
  throw new CliError("invalid-input", "--connection and --profile select different backends; pass only one");
7171
7375
  }
@@ -7510,7 +7714,7 @@ import path4 from "path";
7510
7714
  // ../../apps/backend/package.json
7511
7715
  var package_default = {
7512
7716
  name: "@orkestrator/backend",
7513
- version: "2.30.0-beta",
7717
+ version: "2.31.0-beta",
7514
7718
  private: true,
7515
7719
  type: "module",
7516
7720
  exports: {
@@ -8232,7 +8436,7 @@ async function submitForWait(context, action, input, options2, timeoutMs) {
8232
8436
  } catch (error) {
8233
8437
  const requestId = error instanceof CliError ? error.details?.requestId : undefined;
8234
8438
  const namespace = error instanceof CliError ? error.details?.namespace : undefined;
8235
- if (!(error instanceof CliError) || error.code !== "transport-uncertain" || typeof requestId !== "string" || typeof namespace !== "string")
8439
+ if (!(error instanceof CliError) || !["transport-uncertain", "node-outcome-unknown"].includes(error.code) || typeof requestId !== "string" || typeof namespace !== "string")
8236
8440
  throw error;
8237
8441
  let receipt;
8238
8442
  try {
@@ -8405,6 +8609,11 @@ async function waitForEnvironment(context, session, environmentId, condition, re
8405
8609
 
8406
8610
  // src/client/commands/attention.ts
8407
8611
  function attentionLine(item) {
8612
+ const tagged = item;
8613
+ const prefix = tagged.nodeId ? `[${tagged.nodeName ?? tagged.nodeId} --node ${tagged.nodeId}] ` : "";
8614
+ return prefix + attentionDetail(item);
8615
+ }
8616
+ function attentionDetail(item) {
8408
8617
  if (item.kind === "interaction") {
8409
8618
  return `${item.blocking ? "waiting" : "pending"} ${item.agent} in ${item.environmentName}: ${item.title} (${item.sessionId} ${item.interactionId} rev ${item.revision})`;
8410
8619
  }
@@ -8415,10 +8624,10 @@ function attentionLine(item) {
8415
8624
  }
8416
8625
  function itemKey(item) {
8417
8626
  if (item.kind === "interaction")
8418
- return `interaction:${item.interactionId}:${item.revision}`;
8627
+ return `${item.nodeId ?? ""}:interaction:${item.interactionId}:${item.revision}`;
8419
8628
  if (item.kind === "setup")
8420
- return `setup:${item.environmentId}:${item.at}`;
8421
- return `run:${item.operationId}:${item.state}`;
8629
+ return `${item.nodeId ?? ""}:setup:${item.environmentId}:${item.at}`;
8630
+ return `${item.nodeId ?? ""}:run:${item.operationId}:${item.state}`;
8422
8631
  }
8423
8632
  var attentionCommands = [
8424
8633
  {
@@ -8450,6 +8659,9 @@ var attentionCommands = [
8450
8659
  input.cursor = parsed.options.cursor;
8451
8660
  if (parsed.options.limit !== undefined)
8452
8661
  input.limit = parsed.options.limit;
8662
+ if (parsed.options.follow === true && context.global.node === "all") {
8663
+ throw new CliError("invalid-input", "--follow follows one node; pass its --node");
8664
+ }
8453
8665
  if (parsed.options.follow !== true) {
8454
8666
  if (context.global.output === "jsonl") {
8455
8667
  throw new CliError("invalid-input", "--jsonl needs --follow");
@@ -8462,7 +8674,7 @@ var attentionCommands = [
8462
8674
  human: [
8463
8675
  ...page.items.length ? page.items.map(attentionLine) : ["Nothing needs attention."],
8464
8676
  ...page.more ? [`and ${page.more} more (raise --limit)`] : [],
8465
- `cursor: ${page.cursor}`
8677
+ ...page.cursor ? [`cursor: ${page.cursor}`] : (page.nodes ?? []).filter((node) => node.cursor).map((node) => `cursor (${node.name}): ${node.cursor}`)
8466
8678
  ]
8467
8679
  };
8468
8680
  }
@@ -8656,8 +8868,14 @@ function projectLines(project) {
8656
8868
  ["revision", project.revision]
8657
8869
  ]);
8658
8870
  }
8871
+ function withNodeColumn(items, headers, rows) {
8872
+ const names = items.map((item) => item.nodeName);
8873
+ if (!names.some((name) => typeof name === "string"))
8874
+ return table(headers, rows);
8875
+ return table(["NODE", ...headers], rows.map((row, index) => [String(names[index] ?? ""), ...row]));
8876
+ }
8659
8877
  function projectRows(projects) {
8660
- return table(["ID", "NAME", "ENVS", "PATH"], projects.map((project) => [
8878
+ return withNodeColumn(projects, ["ID", "NAME", "ENVS", "PATH"], projects.map((project) => [
8661
8879
  project.id,
8662
8880
  project.name,
8663
8881
  String(project.environmentCount),
@@ -8682,7 +8900,7 @@ function environmentLines(environment) {
8682
8900
  ]);
8683
8901
  }
8684
8902
  function environmentRows(environments) {
8685
- return table(["ID", "NAME", "TYPE", "STATUS", "READY"], environments.map((environment) => [
8903
+ return withNodeColumn(environments, ["ID", "NAME", "TYPE", "STATUS", "READY"], environments.map((environment) => [
8686
8904
  environment.id,
8687
8905
  environment.name,
8688
8906
  environment.environmentType,
@@ -8933,6 +9151,201 @@ var changesCommands = [
8933
9151
  }
8934
9152
  ];
8935
9153
 
9154
+ // src/client/commands/completion.ts
9155
+ var SHELLS = ["bash", "zsh", "fish"];
9156
+ function flagsOf(options2) {
9157
+ return options2.flatMap((option) => [
9158
+ `--${option.name}`,
9159
+ ...option.short ? [`-${option.short}`] : [],
9160
+ ...option.negatable ? [`--no-${option.name}`] : []
9161
+ ]);
9162
+ }
9163
+ function completionTable(commands) {
9164
+ const next = new Map;
9165
+ const flags = new Map;
9166
+ const values = new Map;
9167
+ const valueFlags = (options2) => flagsOf(options2.filter((option) => option.kind !== "boolean"));
9168
+ values.set("", valueFlags(GLOBAL_OPTIONS));
9169
+ const add = (prefix, word) => {
9170
+ const words = next.get(prefix) ?? [];
9171
+ if (!words.includes(word))
9172
+ words.push(word);
9173
+ next.set(prefix, words);
9174
+ };
9175
+ for (const command of commands) {
9176
+ command.path.forEach((word, index) => {
9177
+ const prefix = command.path.slice(0, index).join(" ");
9178
+ add(prefix, word);
9179
+ if (!values.has(prefix))
9180
+ values.set(prefix, valueFlags(GLOBAL_OPTIONS));
9181
+ });
9182
+ flags.set(command.path.join(" "), flagsOf(command.options));
9183
+ values.set(command.path.join(" "), [
9184
+ ...valueFlags(GLOBAL_OPTIONS),
9185
+ ...valueFlags(command.options)
9186
+ ]);
9187
+ }
9188
+ add("", "help");
9189
+ add("", "version");
9190
+ return { next, flags, globals: flagsOf(GLOBAL_OPTIONS), values };
9191
+ }
9192
+ function words(list) {
9193
+ return list.join(" ");
9194
+ }
9195
+ function bashScript(table) {
9196
+ const cases = [...table.next].map(([prefix, list]) => ` "${prefix}") echo "${words(list)}" ;;`);
9197
+ const flagCases = [...table.flags].map(([path, list]) => ` "${path}") flags="${words(list)}" ;;`);
9198
+ return [
9199
+ "# orkestrator bash completion. Install: orkestrator completion bash > ~/.local/share/bash-completion/completions/orkestrator",
9200
+ "_orkestrator_next() {",
9201
+ ' case "$1" in',
9202
+ ...cases,
9203
+ " esac",
9204
+ "}",
9205
+ "_orkestrator_takes_value() {",
9206
+ ' case "$1:$2" in',
9207
+ ...[...table.values].filter(([, flags]) => flags.length).map(([path, flags]) => ` ${flags.map((flag) => `"${path}:${flag}"`).join("|")}) return 0 ;;`),
9208
+ " esac",
9209
+ " return 1",
9210
+ "}",
9211
+ "_orkestrator() {",
9212
+ ' local cur="${COMP_WORDS[COMP_CWORD]}" command_path="" word i flags="" skip=0',
9213
+ " COMPREPLY=()",
9214
+ " for ((i = 1; i < COMP_CWORD; i++)); do",
9215
+ ' word="${COMP_WORDS[i]}"',
9216
+ ' if ((skip)); then [[ "$word" == "=" ]] || skip=0; continue; fi',
9217
+ ' [[ "$word" == "--" ]] && return 0',
9218
+ ' [[ "$word" == --*=* ]] && continue',
9219
+ ' if _orkestrator_takes_value "$command_path" "$word"; then skip=1; continue; fi',
9220
+ ' [[ "$word" == -* ]] && continue',
9221
+ ' if [[ " $(_orkestrator_next "$command_path") " == *" $word "* ]]; then command_path="${command_path:+$command_path }$word"; fi',
9222
+ " done",
9223
+ " ((skip)) && return 0",
9224
+ ' case "$command_path" in',
9225
+ ...flagCases,
9226
+ " esac",
9227
+ " local candidates candidate",
9228
+ ' if [[ "$cur" == -* ]]; then',
9229
+ ` candidates="$flags ${words(table.globals)}"`,
9230
+ " else",
9231
+ ' candidates="$(_orkestrator_next "$command_path")"',
9232
+ " fi",
9233
+ " for candidate in $candidates; do",
9234
+ ' [[ "$candidate" == "$cur"* ]] && COMPREPLY+=("$candidate")',
9235
+ " done",
9236
+ " return 0",
9237
+ "}",
9238
+ "complete -o default -F _orkestrator orkestrator",
9239
+ ""
9240
+ ].join(`
9241
+ `);
9242
+ }
9243
+ function zshScript(table) {
9244
+ return [
9245
+ "#compdef orkestrator",
9246
+ '# orkestrator zsh completion (through bashcompinit). Install: orkestrator completion zsh > "${fpath[1]}/_orkestrator"',
9247
+ "autoload -U +X bashcompinit && bashcompinit",
9248
+ bashScript(table).split(`
9249
+ `).slice(1).join(`
9250
+ `).replace(`_orkestrator() {
9251
+ `, `_orkestrator() {
9252
+ emulate -L ksh
9253
+ `)
9254
+ ].join(`
9255
+ `);
9256
+ }
9257
+ function fishScript(table) {
9258
+ const lines = [
9259
+ "# orkestrator fish completion. Install: orkestrator completion fish > ~/.config/fish/completions/orkestrator.fish",
9260
+ "function __orkestrator_next",
9261
+ " switch $argv[1]",
9262
+ ...[...table.next].map(([prefix, list]) => ` case '${prefix}'
9263
+ echo '${words(list)}'`),
9264
+ " end",
9265
+ "end",
9266
+ "function __orkestrator_takes_value",
9267
+ ' switch "$argv[1]:$argv[2]"',
9268
+ ...[...table.values].filter(([, flags]) => flags.length).map(([path, flags]) => ` case ${flags.map((flag) => `'${path}:${flag}'`).join(" ")}
9269
+ return 0`),
9270
+ " end",
9271
+ " return 1",
9272
+ "end",
9273
+ "function __orkestrator_path",
9274
+ " set -l tokens (commandline -opc)",
9275
+ " set -e tokens[1]",
9276
+ " set -l path ''",
9277
+ " set -l skip 0",
9278
+ " for token in $tokens",
9279
+ " if test $skip = 1",
9280
+ " set skip 0",
9281
+ " continue",
9282
+ " end",
9283
+ ' if test "$token" = "--"',
9284
+ " echo __rest",
9285
+ " return",
9286
+ " end",
9287
+ " string match -q -- '--*=*' $token; and continue",
9288
+ ' if __orkestrator_takes_value "$path" "$token"',
9289
+ " set skip 1",
9290
+ " continue",
9291
+ " end",
9292
+ " string match -q -- '-*' $token; and continue",
9293
+ " if contains -- $token (string split ' ' -- (__orkestrator_next $path))",
9294
+ ' set path (string trim -- "$path $token")',
9295
+ " end",
9296
+ " end",
9297
+ " if test $skip = 1",
9298
+ " echo __value",
9299
+ " return",
9300
+ " end",
9301
+ ' echo "$path"',
9302
+ "end",
9303
+ "complete -c orkestrator -f"
9304
+ ];
9305
+ for (const [prefix, list] of table.next) {
9306
+ lines.push(`complete -c orkestrator -n 'test (__orkestrator_path) = "${prefix}"' -a '${words(list)}'`);
9307
+ }
9308
+ for (const [path, list] of table.flags) {
9309
+ for (const flag of list) {
9310
+ lines.push(`complete -c orkestrator -n 'test (__orkestrator_path) = "${path}"' ${flag.startsWith("--") ? `-l '${flag.slice(2)}'` : `-s '${flag.slice(1)}'`}${table.values.get(path)?.includes(flag) ? " -r" : ""}`);
9311
+ }
9312
+ }
9313
+ for (const flag of table.globals) {
9314
+ lines.push(`complete -c orkestrator ${flag.startsWith("--") ? `-l '${flag.slice(2)}'` : `-s '${flag.slice(1)}'`}${table.values.get("")?.includes(flag) ? " -r" : ""}`);
9315
+ }
9316
+ return `${lines.join(`
9317
+ `)}
9318
+ `;
9319
+ }
9320
+ function completionScript(shell, commands) {
9321
+ const table = completionTable(commands);
9322
+ return shell === "bash" ? bashScript(table) : shell === "zsh" ? zshScript(table) : fishScript(table);
9323
+ }
9324
+ function completionCommands(commands) {
9325
+ return [
9326
+ {
9327
+ path: ["completion"],
9328
+ summary: "Print a shell completion script (bash, zsh or fish).",
9329
+ description: "Completes nouns, verbs and flags from this build's command table; it never contacts a backend, so IDs are not completed. Example: `orkestrator completion bash > ~/.local/share/bash-completion/completions/orkestrator`.",
9330
+ positionals: [{ name: "shell", required: true, description: "bash, zsh or fish." }],
9331
+ options: [],
9332
+ local: true,
9333
+ async run(_context, parsed) {
9334
+ const shell = parsed.positionals.shell;
9335
+ if (!SHELLS.includes(shell)) {
9336
+ throw new CliError("invalid-input", "Shell must be one of: bash, zsh, fish");
9337
+ }
9338
+ const script = completionScript(shell, commands());
9339
+ return {
9340
+ action: "completion",
9341
+ result: { shell, script },
9342
+ human: [script.replace(/\n$/, "")]
9343
+ };
9344
+ }
9345
+ }
9346
+ ];
9347
+ }
9348
+
8936
9349
  // src/client/commands/connection.ts
8937
9350
  import path9 from "path";
8938
9351
  import { randomUUID as randomUUID3 } from "crypto";
@@ -9020,8 +9433,11 @@ import { promises as fs2 } from "fs";
9020
9433
  import path6 from "path";
9021
9434
  var MAX_LOCAL_RECEIPTS_PER_INSTALLATION = 1000;
9022
9435
  var MAX_RECEIPT_BYTES = 8 * 1024;
9023
- function receiptKey(action, requestId) {
9024
- return createHash("sha256").update(action).update("\x00").update(requestId).digest("hex");
9436
+ function receiptKey(action, requestId, node) {
9437
+ const hash = createHash("sha256").update(action).update("\x00").update(requestId);
9438
+ if (node)
9439
+ hash.update("\x00node:").update(node);
9440
+ return hash.digest("hex");
9025
9441
  }
9026
9442
  function safeSegment(value) {
9027
9443
  return /^[A-Za-z0-9._-]{1,200}$/.test(value) ? value : createHash("sha256").update(value).digest("hex");
@@ -9035,18 +9451,18 @@ class LocalReceiptStore {
9035
9451
  directory(installationId) {
9036
9452
  return path6.join(this.root, safeSegment(installationId));
9037
9453
  }
9038
- file(installationId, action, requestId) {
9039
- return path6.join(this.directory(installationId), `${receiptKey(action, requestId)}.json`);
9454
+ file(installationId, action, requestId, node) {
9455
+ return path6.join(this.directory(installationId), `${receiptKey(action, requestId, node)}.json`);
9040
9456
  }
9041
- async find(installationId, action, requestId) {
9042
- return this.readFile(this.file(installationId, action, requestId));
9457
+ async find(installationId, action, requestId, node) {
9458
+ return this.readFile(this.file(installationId, action, requestId, node));
9043
9459
  }
9044
9460
  async save(receipt) {
9045
9461
  const text = `${JSON.stringify(receipt)}
9046
9462
  `;
9047
9463
  if (Buffer.byteLength(text) > MAX_RECEIPT_BYTES)
9048
9464
  return;
9049
- await writePrivateFile(this.file(receipt.installationId, receipt.action, receipt.requestId), text);
9465
+ await writePrivateFile(this.file(receipt.installationId, receipt.action, receipt.requestId, receipt.node), text);
9050
9466
  await this.prune(receipt.installationId);
9051
9467
  }
9052
9468
  async update(receipt, remote) {
@@ -9081,7 +9497,7 @@ class LocalReceiptStore {
9081
9497
  if (!stat.isFile() || stat.size > MAX_RECEIPT_BYTES)
9082
9498
  return null;
9083
9499
  const value = JSON.parse(await fs2.readFile(file, "utf8"));
9084
- if (value.version !== 1 || typeof value.installationId !== "string" || typeof value.action !== "string" || !isPublicRequestId(value.requestId) || !isPublicNamespace(value.namespace) || typeof value.intentDigest !== "string" || typeof value.createdAt !== "string" || typeof value.updatedAt !== "string") {
9500
+ if (value.version !== 1 || typeof value.installationId !== "string" || value.node !== undefined && typeof value.node !== "string" || typeof value.action !== "string" || !isPublicRequestId(value.requestId) || !isPublicNamespace(value.namespace) || typeof value.intentDigest !== "string" || typeof value.createdAt !== "string" || typeof value.updatedAt !== "string") {
9085
9501
  return null;
9086
9502
  }
9087
9503
  return value;
@@ -9263,6 +9679,14 @@ class ClientSession {
9263
9679
  options;
9264
9680
  capabilitiesPromise = null;
9265
9681
  transport;
9682
+ readWarnings = new Set;
9683
+ responseNode;
9684
+ get warnings() {
9685
+ return [...this.readWarnings];
9686
+ }
9687
+ get nodeTag() {
9688
+ return this.responseNode ?? (this.options.node && this.options.node !== PUBLIC_ALL_NODES ? { nodeId: this.options.node, name: this.options.node } : undefined);
9689
+ }
9266
9690
  constructor(target, receipts, options2 = {}) {
9267
9691
  this.target = target;
9268
9692
  this.receipts = receipts;
@@ -9272,6 +9696,27 @@ class ClientSession {
9272
9696
  get identity() {
9273
9697
  return this.target.identity;
9274
9698
  }
9699
+ get node() {
9700
+ return this.options.node;
9701
+ }
9702
+ routingChecked = null;
9703
+ ensureNodeRouting() {
9704
+ this.routingChecked ??= (async () => {
9705
+ const response = await this.call("capabilities", {}, undefined, false, undefined, false);
9706
+ if (!response.ok)
9707
+ throw envelopeError(response);
9708
+ if (!isPublicCapabilities(response.result)) {
9709
+ throw new CliError("response-invalid", "The backend returned invalid capabilities");
9710
+ }
9711
+ if (response.result.features.nodeRouting !== true) {
9712
+ throw new CliError("backend-incompatible", "The connected backend cannot run actions on other nodes; upgrade it to use --node");
9713
+ }
9714
+ })().catch((error) => {
9715
+ this.routingChecked = null;
9716
+ throw error;
9717
+ });
9718
+ return this.routingChecked;
9719
+ }
9275
9720
  capabilities() {
9276
9721
  this.capabilitiesPromise ??= this.loadCapabilities().catch((error) => {
9277
9722
  this.capabilitiesPromise = null;
@@ -9305,6 +9750,9 @@ class ClientSession {
9305
9750
  async mutate(action, input, requestId) {
9306
9751
  if (!PUBLIC_ACTIONS[action].mutation)
9307
9752
  throw new Error(`${action} is not a mutation`);
9753
+ if (this.options.node === PUBLIC_ALL_NODES) {
9754
+ throw new CliError("invalid-input", "--node all is only for project list, environment list and attention; name one node");
9755
+ }
9308
9756
  if (requestId !== undefined && !isPublicRequestId(requestId)) {
9309
9757
  throw new CliError("invalid-input", "--request-id must be 1\u2013200 characters of letters, digits and . _ : @ / + = -, starting with a letter or digit");
9310
9758
  }
@@ -9316,7 +9764,7 @@ class ClientSession {
9316
9764
  const installationId = capabilities.backend.installationId;
9317
9765
  const key = requestId ?? `cli-${randomUUID2()}`;
9318
9766
  const intentDigest = createHash2("sha256").update(canonicalJson({ action, input })).digest("hex");
9319
- const existing = await this.receipts.find(installationId, action, key);
9767
+ const existing = await this.receipts.find(installationId, action, key, this.options.node);
9320
9768
  if (existing && existing.intentDigest !== intentDigest) {
9321
9769
  throw new CliError("request-conflict", "This saved request ID was used for a different intent", {
9322
9770
  action,
@@ -9329,6 +9777,7 @@ class ClientSession {
9329
9777
  version: 1,
9330
9778
  installationId,
9331
9779
  connection: this.target.identity.name,
9780
+ ...this.options.node ? { node: this.options.node } : {},
9332
9781
  action,
9333
9782
  requestId: key,
9334
9783
  namespace,
@@ -9345,10 +9794,12 @@ class ClientSession {
9345
9794
  if (error instanceof CliError && error.code === "transport-uncertain") {
9346
9795
  throw new CliError(error.code, error.message, {
9347
9796
  action,
9797
+ node: this.nodeTag,
9348
9798
  details: {
9349
9799
  requestId: key,
9350
9800
  namespace,
9351
- recovery: `orkestrator run get --request-id ${key} --action ${action}`
9801
+ ...this.nodeTag ? { node: this.nodeTag } : {},
9802
+ recovery: `orkestrator run get --request-id ${key} --action ${action}${this.options.node ? ` --node ${quoteArgument(this.options.node)}` : ""}`
9352
9803
  }
9353
9804
  });
9354
9805
  }
@@ -9358,27 +9809,56 @@ class ClientSession {
9358
9809
  await this.receipts.update(local, response.receipt).catch(() => {
9359
9810
  return;
9360
9811
  });
9361
- if (!response.ok)
9812
+ if (!response.ok) {
9813
+ if (response.error.code === "node-outcome-unknown") {
9814
+ throw new CliError(response.error.code, response.error.message, {
9815
+ action,
9816
+ node: this.nodeTag,
9817
+ retryable: response.error.retryable,
9818
+ details: {
9819
+ ...response.error.details,
9820
+ requestId: key,
9821
+ namespace,
9822
+ recovery: `orkestrator run get --request-id ${key} --action ${action}${this.options.node ? ` --node ${quoteArgument(this.options.node)}` : ""}`
9823
+ }
9824
+ });
9825
+ }
9362
9826
  throw envelopeError(response);
9827
+ }
9363
9828
  return {
9364
9829
  result: response.result,
9830
+ ...this.nodeTag ? { node: this.nodeTag } : {},
9365
9831
  ...response.receipt ? { receipt: response.receipt } : {},
9366
9832
  ...response.warnings ? { warnings: response.warnings } : {}
9367
9833
  };
9368
9834
  }
9369
- async call(action, input, request, mutation, timeoutMs) {
9835
+ async call(action, input, request, mutation, timeoutMs, routed = true) {
9836
+ const node = routed && this.options.node && !PUBLIC_LOCAL_ONLY_ACTIONS.includes(action) ? this.options.node : undefined;
9837
+ if (node)
9838
+ await this.ensureNodeRouting();
9370
9839
  const body = {
9371
9840
  schemaVersion: PUBLIC_API_SCHEMA_VERSION,
9372
9841
  action,
9373
9842
  actionVersion: PUBLIC_ACTIONS[action].version,
9374
9843
  input,
9375
- ...request ? { request } : {}
9844
+ ...request ? { request } : {},
9845
+ ...node && !(node === PUBLIC_ALL_NODES && action === "capabilities") ? { node } : {}
9376
9846
  };
9377
9847
  const raw = await this.transport.invoke(PUBLIC_ACTION_COMMAND, body, { mutation, signal: this.options.signal, timeoutMs });
9378
9848
  if (!isPublicActionResponse(raw)) {
9379
9849
  throw new CliError(mutation ? "transport-uncertain" : "response-invalid", "The backend returned a response that does not match the public contract");
9380
9850
  }
9381
9851
  await this.verifyIdentity(raw);
9852
+ if (raw.node)
9853
+ this.responseNode = raw.node;
9854
+ if (!mutation && raw.ok) {
9855
+ for (const warning of raw.warnings ?? [])
9856
+ this.readWarnings.add(warning);
9857
+ const nodes = raw.result?.nodes;
9858
+ for (const failed of nodes?.filter((entry) => !entry.ok) ?? []) {
9859
+ this.readWarnings.add(`${failed.name} (${failed.nodeId}): ${failed.error?.message ?? "not checked"}`);
9860
+ }
9861
+ }
9382
9862
  return raw;
9383
9863
  }
9384
9864
  async verifyIdentity(response) {
@@ -9397,11 +9877,15 @@ class ClientSession {
9397
9877
  function envelopeError(response) {
9398
9878
  return new CliError(response.error.code, response.error.message, {
9399
9879
  action: response.action,
9880
+ ...response.node ? { node: response.node } : {},
9400
9881
  ...response.error.details ? { details: response.error.details } : {},
9401
9882
  ...response.receipt ? { receipt: response.receipt } : {},
9402
9883
  ...response.error.retryable !== undefined ? { retryable: response.error.retryable } : {}
9403
9884
  });
9404
9885
  }
9886
+ function quoteArgument(value) {
9887
+ return /^[A-Za-z0-9_.:@/+=-]+$/.test(value) ? value : "'" + value.replaceAll("'", `'"'"'`) + "'";
9888
+ }
9405
9889
 
9406
9890
  // src/client/targets.ts
9407
9891
  import { promises as fs3 } from "fs";
@@ -11113,63 +11597,532 @@ var hostCommands = [
11113
11597
  })
11114
11598
  ];
11115
11599
 
11116
- // src/client/commands/mail.ts
11117
- var mailCommands = [
11118
- {
11119
- path: ["mail", "mailboxes"],
11120
- action: "mail.mailboxes",
11121
- summary: "List agent mailboxes a message can be sent to.",
11122
- positionals: [],
11123
- options: [
11124
- { name: "q", kind: "string", valueName: "TEXT", description: "Filter by name." },
11125
- { name: "offset", kind: "integer", valueName: "N", description: "Rows to skip." },
11126
- { name: "limit", kind: "integer", valueName: "N", description: "Rows (max 200)." }
11127
- ],
11128
- async run(context, parsed) {
11129
- const session = await context.session();
11130
- const page = await session.read("mail.mailboxes", {
11131
- ...parsed.options.q !== undefined ? { q: parsed.options.q } : {},
11132
- ...parsed.options.offset !== undefined ? { offset: parsed.options.offset } : {},
11133
- ...parsed.options.limit !== undefined ? { limit: parsed.options.limit } : {}
11134
- });
11135
- return {
11136
- action: "mail.mailboxes",
11137
- result: page,
11138
- connection: session.identity,
11139
- human: table(["ENVIRONMENT", "TAB", "NAME", "PRESENCE"], page.mailboxes.map((mailbox) => [
11140
- mailbox.environmentId,
11141
- mailbox.tabId,
11142
- `${mailbox.environmentName} / ${mailbox.displayName}`,
11143
- mailbox.presence
11144
- ]))
11145
- };
11600
+ // ../protocol/src/schedule-time.ts
11601
+ var CRON_SEARCH_DAYS = 366 * 5;
11602
+ var formatters = new Map;
11603
+
11604
+ // ../protocol/src/scheduled-jobs.ts
11605
+ var SCHEDULED_JOB_LIMITS = Object.freeze({
11606
+ maxSchedules: 1000,
11607
+ maxPerOwner: 50,
11608
+ historyEntries: 20,
11609
+ nameMaxChars: 120,
11610
+ promptMaxChars: 1e5,
11611
+ minIntervalSeconds: 60,
11612
+ maxIntervalSeconds: 366 * 24 * 60 * 60,
11613
+ defaultCatchUpMinutes: 15,
11614
+ maxCatchUpMinutes: 7 * 24 * 60,
11615
+ onTimeToleranceMs: 60000,
11616
+ actionInputMaxBytes: 64 * 1024,
11617
+ busyRetryWindowMs: 15 * 60000,
11618
+ busyRetryDelayMs: 30000,
11619
+ fireStaleMs: 90000,
11620
+ fireHeartbeatMs: 20000,
11621
+ maxFireAttempts: 3
11622
+ });
11623
+ function describeScheduleTrigger(trigger, timeZone) {
11624
+ switch (trigger.kind) {
11625
+ case "once":
11626
+ return `once at ${trigger.at}`;
11627
+ case "cron":
11628
+ return `cron "${trigger.expression}" (${timeZone})`;
11629
+ case "interval": {
11630
+ const seconds = trigger.everySeconds;
11631
+ const text = seconds % 86400 === 0 ? `${seconds / 86400}d` : seconds % 3600 === 0 ? `${seconds / 3600}h` : seconds % 60 === 0 ? `${seconds / 60}m` : `${seconds}s`;
11632
+ return `every ${text}`;
11146
11633
  }
11634
+ }
11635
+ }
11636
+
11637
+ // src/client/commands/schedule.ts
11638
+ var SCHEDULE_ID = { name: "schedule", required: true, description: "Schedule ID (sch_...)." };
11639
+ var TIMING_OPTIONS = [
11640
+ {
11641
+ name: "at",
11642
+ kind: "string",
11643
+ valueName: "TIME",
11644
+ description: "Once: HH:MM (next occurrence), YYYY-MM-DDTHH:MM, an ISO time with an offset, or +15m."
11147
11645
  },
11148
11646
  {
11149
- path: ["mail", "send"],
11150
- action: "mail.send",
11151
- summary: "Place a message in one tab's inbox (never injected into a turn).",
11152
- positionals: [
11153
- { name: "environment", required: true },
11154
- { name: "tab", required: true }
11155
- ],
11156
- options: [
11157
- { name: "subject", kind: "string", valueName: "TEXT", description: "Subject line." },
11158
- { name: "body", kind: "string", valueName: "TEXT", description: "Message text." },
11159
- {
11160
- name: "body-file",
11161
- kind: "string",
11162
- valueName: "PATH",
11163
- description: "Read the message from a local file."
11164
- },
11165
- { name: "body-stdin", kind: "boolean", description: "Read the message from stdin." },
11166
- REQUEST_ID_OPTION
11167
- ],
11168
- inputFields: {
11169
- environment: "toEnvironmentId",
11170
- tab: "toTabId",
11171
- "body-file": "body",
11172
- "body-stdin": "body"
11647
+ name: "cron",
11648
+ kind: "string",
11649
+ valueName: "EXPR",
11650
+ description: 'Recurring: five-field cron, e.g. "0 9 * * 1-5", or @daily.'
11651
+ },
11652
+ {
11653
+ name: "every",
11654
+ kind: "duration",
11655
+ valueName: "DURATION",
11656
+ description: "Recurring: a fixed interval (at least 1m)."
11657
+ },
11658
+ {
11659
+ name: "start-at",
11660
+ kind: "string",
11661
+ valueName: "TIME",
11662
+ description: "With --every: the first fire."
11663
+ },
11664
+ {
11665
+ name: "time-zone",
11666
+ kind: "string",
11667
+ valueName: "ZONE",
11668
+ description: "IANA zone for --at and --cron, e.g. Europe/London. Default: the backend host's."
11669
+ },
11670
+ {
11671
+ name: "catch-up-minutes",
11672
+ kind: "integer",
11673
+ valueName: "N",
11674
+ description: "Run a fire missed while the backend was down if at most N minutes late (default 15)."
11675
+ },
11676
+ { name: "name", kind: "string", valueName: "TEXT", description: "Label shown in lists." }
11677
+ ];
11678
+ var ACTION_OPTIONS = [
11679
+ {
11680
+ name: "mail",
11681
+ kind: "string",
11682
+ valueName: "ENV:TAB",
11683
+ description: "Action: send agent mail (mail.send) to this mailbox."
11684
+ },
11685
+ { name: "subject", kind: "string", valueName: "TEXT", description: "With --mail: subject." },
11686
+ { name: "body", kind: "string", valueName: "TEXT", description: "With --mail: message text." },
11687
+ {
11688
+ name: "body-file",
11689
+ kind: "string",
11690
+ valueName: "PATH",
11691
+ description: "With --mail: read the text from a file."
11692
+ },
11693
+ {
11694
+ name: "session",
11695
+ kind: "string",
11696
+ valueName: "SESSION",
11697
+ description: "Action: prompt this idle session (session.prompt); give the prompt with --prompt\u2026"
11698
+ },
11699
+ {
11700
+ name: "launch-project",
11701
+ kind: "string",
11702
+ valueName: "PROJECT_ID",
11703
+ description: "Action: launch an environment in this project (environment.launch)."
11704
+ },
11705
+ {
11706
+ name: "agent",
11707
+ kind: "string",
11708
+ valueName: "AGENT",
11709
+ description: "With --launch-project: agent platform."
11710
+ },
11711
+ {
11712
+ name: "type",
11713
+ kind: "string",
11714
+ valueName: "TYPE",
11715
+ description: "With --launch-project: local (default) or container."
11716
+ },
11717
+ {
11718
+ name: "model",
11719
+ kind: "string",
11720
+ valueName: "MODEL",
11721
+ description: "With --launch-project: model ID."
11722
+ },
11723
+ {
11724
+ name: "base-branch",
11725
+ kind: "string",
11726
+ valueName: "BRANCH",
11727
+ description: "With --launch-project: base branch."
11728
+ },
11729
+ {
11730
+ name: "base-commit",
11731
+ kind: "string",
11732
+ valueName: "SHA",
11733
+ description: "With --launch-project: pin this commit; omitted, the branch's newest commit at fire time."
11734
+ },
11735
+ {
11736
+ name: "mode",
11737
+ kind: "string",
11738
+ valueName: "MODE",
11739
+ description: "With --session or --launch-project: plan or build."
11740
+ },
11741
+ {
11742
+ name: "action",
11743
+ kind: "string",
11744
+ valueName: "ACTION",
11745
+ description: "Action: any public mutation by name (e.g. ticket.comment) with --input-json."
11746
+ },
11747
+ {
11748
+ name: "input-json",
11749
+ kind: "string",
11750
+ valueName: "JSON",
11751
+ description: "With --action: its input object."
11752
+ },
11753
+ ...PROMPT_OPTIONS
11754
+ ];
11755
+ var ACTION_INPUT_FIELDS = Object.fromEntries(ACTION_OPTIONS.map((option) => [option.name, "action"]));
11756
+ function timingInput(options2) {
11757
+ const every = options2.every;
11758
+ if (typeof every === "number" && every % 1000 !== 0) {
11759
+ throw new CliError("invalid-input", "--every must be whole seconds");
11760
+ }
11761
+ const at = stringOption(options2, "at");
11762
+ const cron = stringOption(options2, "cron");
11763
+ const startAt = stringOption(options2, "start-at");
11764
+ const timeZone = stringOption(options2, "time-zone");
11765
+ const name = stringOption(options2, "name");
11766
+ return {
11767
+ ...at !== undefined ? { at } : {},
11768
+ ...cron !== undefined ? { cron } : {},
11769
+ ...typeof every === "number" ? { everySeconds: every / 1000 } : {},
11770
+ ...startAt !== undefined ? { startAt } : {},
11771
+ ...timeZone !== undefined ? { timeZone } : {},
11772
+ ...typeof options2["catch-up-minutes"] === "number" ? { catchUpMinutes: options2["catch-up-minutes"] } : {},
11773
+ ...name !== undefined ? { name } : {}
11774
+ };
11775
+ }
11776
+ async function actionInput(io, options2) {
11777
+ const chosen = ["mail", "session", "launch-project", "action"].filter((name) => options2[name] !== undefined);
11778
+ if (chosen.length === 0)
11779
+ return;
11780
+ if (chosen.length > 1) {
11781
+ throw new CliError("invalid-input", `--${chosen.join(", --")} are mutually exclusive`);
11782
+ }
11783
+ const prompt = () => readTextSource(io, {
11784
+ file: options2["prompt-file"],
11785
+ stdin: options2["prompt-stdin"],
11786
+ inline: options2.prompt,
11787
+ label: "Prompt",
11788
+ maxBytes: 400000,
11789
+ names: { file: "--prompt-file", stdin: "--prompt-stdin", inline: "--prompt" },
11790
+ required: true
11791
+ });
11792
+ const mode = options2.mode !== undefined ? { mode: oneOf(options2.mode, ["plan", "build"], "--mode") } : {};
11793
+ switch (chosen[0]) {
11794
+ case "mail": {
11795
+ const [environmentId, tabId, extra] = String(options2.mail).split(":");
11796
+ if (!environmentId || !tabId || extra !== undefined) {
11797
+ throw new CliError("invalid-input", "--mail expects ENVIRONMENT_ID:TAB_ID");
11798
+ }
11799
+ const body = await readTextSource(io, {
11800
+ file: options2["body-file"],
11801
+ inline: options2.body,
11802
+ label: "Message",
11803
+ maxBytes: 32 * 1024,
11804
+ names: { file: "--body-file", stdin: "--body-stdin", inline: "--body" },
11805
+ required: true
11806
+ });
11807
+ const subject = stringOption(options2, "subject");
11808
+ return {
11809
+ kind: "action",
11810
+ action: "mail.send",
11811
+ input: {
11812
+ toEnvironmentId: environmentId,
11813
+ toTabId: tabId,
11814
+ body,
11815
+ ...subject ? { subject } : {}
11816
+ }
11817
+ };
11818
+ }
11819
+ case "session":
11820
+ return { kind: "prompt", sessionId: options2.session, prompt: await prompt(), ...mode };
11821
+ case "launch-project": {
11822
+ const agent = stringOption(options2, "agent");
11823
+ if (!agent)
11824
+ throw new CliError("invalid-input", "--launch-project needs --agent");
11825
+ const type = options2.type === undefined ? "local" : oneOf(options2.type, ["local", "container"], "--type");
11826
+ const model = stringOption(options2, "model");
11827
+ const baseBranch = stringOption(options2, "base-branch");
11828
+ const baseCommit = stringOption(options2, "base-commit");
11829
+ return {
11830
+ kind: "launch",
11831
+ projectId: options2["launch-project"],
11832
+ agent,
11833
+ type,
11834
+ prompt: await prompt(),
11835
+ ...mode,
11836
+ ...model ? { model } : {},
11837
+ ...baseBranch ? { baseBranch } : {},
11838
+ ...baseCommit ? { baseCommit } : {}
11839
+ };
11840
+ }
11841
+ default: {
11842
+ let input = {};
11843
+ const raw = stringOption(options2, "input-json");
11844
+ if (raw !== undefined) {
11845
+ try {
11846
+ input = JSON.parse(raw);
11847
+ } catch {
11848
+ throw new CliError("invalid-input", "--input-json is not valid JSON");
11849
+ }
11850
+ }
11851
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
11852
+ throw new CliError("invalid-input", "--input-json must be a JSON object");
11853
+ }
11854
+ return { kind: "action", action: options2.action, input };
11855
+ }
11856
+ }
11857
+ }
11858
+ function lastOutcome(job) {
11859
+ const fire = job.activeFire ?? job.lastFire;
11860
+ if (!fire)
11861
+ return "-";
11862
+ return fire.error ? `${fire.state}: ${fire.error.code}` : fire.state;
11863
+ }
11864
+ function scheduleLines(job) {
11865
+ return [
11866
+ ...keyValues([
11867
+ ["id", job.id],
11868
+ ["name", job.name],
11869
+ ["owner", job.ownerLabel],
11870
+ ["state", job.pausedReason ? `${job.state} (${job.pausedReason})` : job.state],
11871
+ ["when", describeScheduleTrigger(job.trigger, job.timeZone)],
11872
+ ["time zone", job.timeZone],
11873
+ ["next fire", job.nextFireAt ?? "-"],
11874
+ ["catch-up", `${job.catchUpMinutes} min`],
11875
+ ["action", job.action.kind === "action" ? job.action.action : job.action.kind]
11876
+ ]),
11877
+ "",
11878
+ ...table(["FIRE", "SLOT", "STATE", "ERROR"], [...job.fires].reverse().map((fire) => [
11879
+ fire.fireId,
11880
+ fire.scheduledFor,
11881
+ fire.state,
11882
+ fire.error ? `${fire.error.code}: ${fire.error.message}` : ""
11883
+ ]))
11884
+ ];
11885
+ }
11886
+ function simpleMutation(verb, summary) {
11887
+ const action = `schedule.${verb}`;
11888
+ return {
11889
+ path: ["schedule", verb],
11890
+ action,
11891
+ summary,
11892
+ positionals: [SCHEDULE_ID],
11893
+ options: [REQUEST_ID_OPTION],
11894
+ inputFields: { schedule: "scheduleId" },
11895
+ idOutput: "schedule ID",
11896
+ async run(context, parsed) {
11897
+ const { session, result, receipt, warnings } = await submit(context, action, { scheduleId: parsed.positionals.schedule }, parsed.options);
11898
+ return {
11899
+ action,
11900
+ result,
11901
+ receipt,
11902
+ warnings,
11903
+ connection: session.identity,
11904
+ ids: [String(parsed.positionals.schedule)],
11905
+ human: keyValues(Object.entries(result))
11906
+ };
11907
+ }
11908
+ };
11909
+ }
11910
+ var scheduleCommands = [
11911
+ {
11912
+ path: ["schedule", "list"],
11913
+ action: "schedule.list",
11914
+ summary: "List scheduled jobs with their next fire, owner and last outcome.",
11915
+ positionals: [],
11916
+ options: [
11917
+ {
11918
+ name: "project",
11919
+ kind: "string",
11920
+ valueName: "PROJECT_ID",
11921
+ description: "Owned in or aimed at this project."
11922
+ },
11923
+ {
11924
+ name: "state",
11925
+ kind: "string",
11926
+ valueName: "STATE",
11927
+ description: "active, paused or completed."
11928
+ },
11929
+ {
11930
+ name: "owner-kind",
11931
+ kind: "string",
11932
+ valueName: "KIND",
11933
+ description: "operator, control-mcp, agent or coordinator."
11934
+ },
11935
+ { name: "offset", kind: "integer", valueName: "N", description: "Rows to skip." },
11936
+ { name: "limit", kind: "integer", valueName: "N", description: "Rows (max 200)." }
11937
+ ],
11938
+ async run(context, parsed) {
11939
+ const session = await context.session();
11940
+ const options2 = parsed.options;
11941
+ const page = await session.read("schedule.list", {
11942
+ ...options2.project !== undefined ? { projectId: options2.project } : {},
11943
+ ...options2.state !== undefined ? { state: options2.state } : {},
11944
+ ...options2["owner-kind"] !== undefined ? { ownerKind: options2["owner-kind"] } : {},
11945
+ ...options2.offset !== undefined ? { offset: options2.offset } : {},
11946
+ ...options2.limit !== undefined ? { limit: options2.limit } : {}
11947
+ });
11948
+ return {
11949
+ action: "schedule.list",
11950
+ result: page,
11951
+ connection: session.identity,
11952
+ human: table(["ID", "NAME", "STATE", "NEXT FIRE", "OWNER", "LAST"], page.schedules.map((job) => [
11953
+ job.id,
11954
+ job.name,
11955
+ job.state,
11956
+ job.nextFireAt ?? "-",
11957
+ job.ownerLabel,
11958
+ lastOutcome(job)
11959
+ ]))
11960
+ };
11961
+ }
11962
+ },
11963
+ {
11964
+ path: ["schedule", "get"],
11965
+ action: "schedule.get",
11966
+ summary: "Show one scheduled job and its fire history.",
11967
+ positionals: [SCHEDULE_ID],
11968
+ options: [],
11969
+ inputFields: { schedule: "scheduleId" },
11970
+ async run(context, parsed) {
11971
+ const session = await context.session();
11972
+ const result = await session.read("schedule.get", {
11973
+ scheduleId: parsed.positionals.schedule
11974
+ });
11975
+ return {
11976
+ action: "schedule.get",
11977
+ result,
11978
+ connection: session.identity,
11979
+ human: scheduleLines(result.schedule)
11980
+ };
11981
+ }
11982
+ },
11983
+ {
11984
+ path: ["schedule", "create"],
11985
+ action: "schedule.create",
11986
+ summary: "Schedule mail, a prompt, an environment launch or another action.",
11987
+ description: "Give one of --at, --cron or --every, and one action: --mail ENV:TAB with --body, --session SES with --prompt, --launch-project P with --agent and --prompt, or --action NAME with --input-json.",
11988
+ positionals: [],
11989
+ options: [
11990
+ ...TIMING_OPTIONS,
11991
+ { name: "paused", kind: "boolean", description: "Create it paused." },
11992
+ ...ACTION_OPTIONS,
11993
+ REQUEST_ID_OPTION
11994
+ ],
11995
+ inputFields: { ...ACTION_INPUT_FIELDS, every: "everySeconds", "start-at": "startAt" },
11996
+ idOutput: "schedule ID",
11997
+ examples: [
11998
+ 'orkestrator schedule create --at 03:12 --mail env-1:startup-agent --body "Resume the stalled workers."',
11999
+ 'orkestrator schedule create --cron "0 9 * * 1-5" --session ses_... --prompt "Summarise CI failures."'
12000
+ ],
12001
+ async run(context, parsed) {
12002
+ const action = await actionInput(context.io, parsed.options);
12003
+ if (!action)
12004
+ throw new CliError("invalid-input", "Choose an action: --mail, --session, --launch-project or --action");
12005
+ const { session, result, receipt, warnings } = await submit(context, "schedule.create", {
12006
+ ...timingInput(parsed.options),
12007
+ ...parsed.options.paused === true ? { paused: true } : {},
12008
+ action
12009
+ }, parsed.options);
12010
+ return {
12011
+ action: "schedule.create",
12012
+ result,
12013
+ receipt,
12014
+ warnings,
12015
+ connection: session.identity,
12016
+ ids: [result.scheduleId],
12017
+ human: [`scheduled ${result.scheduleId}; next fire ${result.nextFireAt ?? "none"}`]
12018
+ };
12019
+ }
12020
+ },
12021
+ {
12022
+ path: ["schedule", "update"],
12023
+ action: "schedule.update",
12024
+ summary: "Change a schedule's time, zone, catch-up window, name or action.",
12025
+ positionals: [SCHEDULE_ID],
12026
+ options: [
12027
+ ...TIMING_OPTIONS,
12028
+ ...ACTION_OPTIONS,
12029
+ {
12030
+ name: "expected-revision",
12031
+ kind: "integer",
12032
+ valueName: "N",
12033
+ description: "Apply only at this revision."
12034
+ },
12035
+ REQUEST_ID_OPTION
12036
+ ],
12037
+ inputFields: {
12038
+ ...ACTION_INPUT_FIELDS,
12039
+ schedule: "scheduleId",
12040
+ every: "everySeconds",
12041
+ "start-at": "startAt"
12042
+ },
12043
+ idOutput: "schedule ID",
12044
+ async run(context, parsed) {
12045
+ const action = await actionInput(context.io, parsed.options);
12046
+ const { session, result, receipt, warnings } = await submit(context, "schedule.update", {
12047
+ scheduleId: parsed.positionals.schedule,
12048
+ ...timingInput(parsed.options),
12049
+ ...action ? { action } : {},
12050
+ ...typeof parsed.options["expected-revision"] === "number" ? { expectedRevision: parsed.options["expected-revision"] } : {}
12051
+ }, parsed.options);
12052
+ return {
12053
+ action: "schedule.update",
12054
+ result,
12055
+ receipt,
12056
+ warnings,
12057
+ connection: session.identity,
12058
+ ids: [String(parsed.positionals.schedule)],
12059
+ human: keyValues(Object.entries(result))
12060
+ };
12061
+ }
12062
+ },
12063
+ simpleMutation("pause", "Pause a schedule; nothing fires until it is resumed."),
12064
+ simpleMutation("resume", "Resume a paused schedule from its next future slot."),
12065
+ simpleMutation("run", "Fire a schedule once now, leaving its regular slots unchanged."),
12066
+ simpleMutation("delete", "Delete a schedule and its history.")
12067
+ ];
12068
+
12069
+ // src/client/commands/mail.ts
12070
+ var mailCommands = [
12071
+ {
12072
+ path: ["mail", "mailboxes"],
12073
+ action: "mail.mailboxes",
12074
+ summary: "List agent mailboxes a message can be sent to.",
12075
+ positionals: [],
12076
+ options: [
12077
+ { name: "q", kind: "string", valueName: "TEXT", description: "Filter by name." },
12078
+ { name: "offset", kind: "integer", valueName: "N", description: "Rows to skip." },
12079
+ { name: "limit", kind: "integer", valueName: "N", description: "Rows (max 200)." }
12080
+ ],
12081
+ async run(context, parsed) {
12082
+ const session = await context.session();
12083
+ const page = await session.read("mail.mailboxes", {
12084
+ ...parsed.options.q !== undefined ? { q: parsed.options.q } : {},
12085
+ ...parsed.options.offset !== undefined ? { offset: parsed.options.offset } : {},
12086
+ ...parsed.options.limit !== undefined ? { limit: parsed.options.limit } : {}
12087
+ });
12088
+ return {
12089
+ action: "mail.mailboxes",
12090
+ result: page,
12091
+ connection: session.identity,
12092
+ human: table(["ENVIRONMENT", "TAB", "NAME", "PRESENCE"], page.mailboxes.map((mailbox) => [
12093
+ mailbox.environmentId,
12094
+ mailbox.tabId,
12095
+ `${mailbox.environmentName} / ${mailbox.displayName}`,
12096
+ mailbox.presence
12097
+ ]))
12098
+ };
12099
+ }
12100
+ },
12101
+ {
12102
+ path: ["mail", "send"],
12103
+ action: "mail.send",
12104
+ summary: "Place a message in one tab's inbox (never injected into a turn).",
12105
+ positionals: [
12106
+ { name: "environment", required: true },
12107
+ { name: "tab", required: true }
12108
+ ],
12109
+ options: [
12110
+ { name: "subject", kind: "string", valueName: "TEXT", description: "Subject line." },
12111
+ { name: "body", kind: "string", valueName: "TEXT", description: "Message text." },
12112
+ {
12113
+ name: "body-file",
12114
+ kind: "string",
12115
+ valueName: "PATH",
12116
+ description: "Read the message from a local file."
12117
+ },
12118
+ { name: "body-stdin", kind: "boolean", description: "Read the message from stdin." },
12119
+ REQUEST_ID_OPTION
12120
+ ],
12121
+ inputFields: {
12122
+ environment: "toEnvironmentId",
12123
+ tab: "toTabId",
12124
+ "body-file": "body",
12125
+ "body-stdin": "body"
11173
12126
  },
11174
12127
  idOutput: "message ID",
11175
12128
  async run(context, parsed) {
@@ -11908,7 +12861,7 @@ var projectCommands = [
11908
12861
  path: ["project", "create"],
11909
12862
  action: "project.create",
11910
12863
  summary: "Create a new repository: git init, a PRIVATE GitHub repository, and a push.",
11911
- description: "This creates an external repository on GitHub using the backend's GitHub CLI credentials, pushes the initial commit, and registers the project. It requires --github-private to confirm that effect. Local-only initialization is not supported.",
12864
+ description: "This creates an external repository on GitHub using the backend's GitHub CLI credentials, pushes the initial commit, and registers the project. It requires --github-private to confirm that effect. For a repository with no remote, use `project init`.",
11912
12865
  positionals: [],
11913
12866
  options: [
11914
12867
  {
@@ -11944,6 +12897,38 @@ var projectCommands = [
11944
12897
  };
11945
12898
  }
11946
12899
  },
12900
+ {
12901
+ path: ["project", "init"],
12902
+ action: "project.init",
12903
+ summary: "Create a local-only repository (git init, initial commit, no remote) and register it.",
12904
+ description: "Runs on the BACKEND host: the path must be new or an empty directory there. Nothing reaches GitHub or the network. Environments start from the repository's own branch; without a remote, container environments cannot clone it and pull requests are unavailable.",
12905
+ positionals: [],
12906
+ options: [
12907
+ {
12908
+ name: "path",
12909
+ kind: "string",
12910
+ valueName: "BACKEND_PATH",
12911
+ description: "New, empty directory on the backend host."
12912
+ },
12913
+ REQUEST_ID_OPTION
12914
+ ],
12915
+ idOutput: "project ID",
12916
+ async run(context, parsed) {
12917
+ const backendPath = stringOption(parsed.options, "path");
12918
+ if (!backendPath)
12919
+ throw new CliError("invalid-input", "--path is required");
12920
+ const { session, result, receipt, warnings } = await submit(context, "project.init", { path: backendPath }, parsed.options);
12921
+ return {
12922
+ action: "project.init",
12923
+ result,
12924
+ receipt,
12925
+ warnings,
12926
+ connection: session.identity,
12927
+ ids: [result.project.id],
12928
+ human: projectLines(result.project)
12929
+ };
12930
+ }
12931
+ },
11947
12932
  {
11948
12933
  path: ["project", "update"],
11949
12934
  action: "project.update",
@@ -12025,13 +13010,46 @@ var projectCommands = [
12025
13010
  {
12026
13011
  path: ["project", "remove"],
12027
13012
  action: "project.remove",
12028
- summary: "Remove an empty project registration (refused while environments exist).",
12029
- description: "Only the registration is removed. The checkout on disk and any remote repository are never deleted.",
13013
+ summary: "Remove a project registration; --cascade deletes its environments first.",
13014
+ description: "Without --cascade a project that still has environments is refused (exit 8). With --cascade the backend deletes each environment through the normal lifecycle, one by one, reports a result for each, and removes the registration only if all were deleted (otherwise the operation is partial and the project is kept). The checkout on disk and any remote repository are never deleted. --wait observes a cascade until it ends.",
12030
13015
  positionals: [{ name: "project", required: true }],
12031
- options: [REQUEST_ID_OPTION],
13016
+ options: [
13017
+ {
13018
+ name: "cascade",
13019
+ kind: "boolean",
13020
+ description: "Delete every environment of the project first."
13021
+ },
13022
+ { name: "wait", kind: "boolean", description: "Observe the removal until it ends." },
13023
+ TIMEOUT_OPTION,
13024
+ REQUEST_ID_OPTION
13025
+ ],
12032
13026
  idOutput: "operation ID",
12033
13027
  async run(context, parsed) {
12034
- const { session, result, receipt, warnings } = await submit(context, "project.remove", { projectId: parsed.positionals.project }, parsed.options);
13028
+ const options2 = parsed.options;
13029
+ if (options2.timeout !== undefined && options2.wait !== true) {
13030
+ throw new CliError("invalid-input", "--timeout needs --wait");
13031
+ }
13032
+ if (typeof options2.timeout === "number" && options2.timeout > PUBLIC_API_LIMITS.waitMaxMs) {
13033
+ throw new CliError("invalid-input", "--timeout may be at most 24h");
13034
+ }
13035
+ const { session, result, receipt, warnings } = await submit(context, "project.remove", {
13036
+ projectId: parsed.positionals.project,
13037
+ ...options2.cascade === true ? { cascade: true } : {}
13038
+ }, options2);
13039
+ if (options2.wait === true && receipt) {
13040
+ const final = await waitForOperation(context, session, receipt, options2.timeout ?? DEFAULT_WAIT_MS);
13041
+ const failure = receiptFailure(final);
13042
+ return {
13043
+ action: "project.remove",
13044
+ result: final.result ?? result,
13045
+ receipt: final,
13046
+ warnings,
13047
+ connection: session.identity,
13048
+ ids: [final.operationId],
13049
+ human: receiptLines(final),
13050
+ ...failure ? { failure } : {}
13051
+ };
13052
+ }
12035
13053
  return {
12036
13054
  action: "project.remove",
12037
13055
  result,
@@ -12089,7 +13107,7 @@ var runCommands = [
12089
13107
  let namespace = stringOption(parsed.options, "namespace");
12090
13108
  if (requestId && !namespace && action) {
12091
13109
  const capabilities = await session.capabilities();
12092
- const local = await new LocalReceiptStore(context.configStore.receiptsDirectory).find(capabilities.backend.installationId, action, requestId);
13110
+ const local = await new LocalReceiptStore(context.configStore.receiptsDirectory).find(capabilities.backend.installationId, action, requestId, session.node);
12093
13111
  namespace = local?.namespace;
12094
13112
  }
12095
13113
  const { receipt } = await session.readWithReceipt("run.get", operationId ? { operationId } : { requestId, ...action ? { action } : {}, ...namespace ? { namespace } : {} });
@@ -12097,7 +13115,7 @@ var runCommands = [
12097
13115
  throw new CliError("response-invalid", "run.get returned no receipt");
12098
13116
  return {
12099
13117
  action: "run.get",
12100
- result: null,
13118
+ result: receipt.result ?? null,
12101
13119
  receipt,
12102
13120
  connection: session.identity,
12103
13121
  ids: [receipt.operationId],
@@ -12127,7 +13145,7 @@ var runCommands = [
12127
13145
  const failure = receiptFailure(final);
12128
13146
  return {
12129
13147
  action: "run.wait",
12130
- result: null,
13148
+ result: final.result ?? null,
12131
13149
  receipt: final,
12132
13150
  connection: session.identity,
12133
13151
  ids: [final.operationId],
@@ -12277,9 +13295,10 @@ var runCommands = [
12277
13295
  return {
12278
13296
  action: "run.receipts",
12279
13297
  result: { items: receipts },
12280
- human: table(["REQUEST", "ACTION", "OPERATION", "STATE", "UPDATED"], receipts.map((receipt) => [
13298
+ human: table(["REQUEST", "ACTION", "NODE", "OPERATION", "STATE", "UPDATED"], receipts.map((receipt) => [
12281
13299
  receipt.requestId,
12282
13300
  receipt.action,
13301
+ receipt.node ?? "(connected node)",
12283
13302
  receipt.operationId ?? "(no response)",
12284
13303
  receipt.state ?? "-",
12285
13304
  receipt.updatedAt
@@ -12290,6 +13309,7 @@ var runCommands = [
12290
13309
  ];
12291
13310
 
12292
13311
  // src/client/commands/session.ts
13312
+ import path15 from "path";
12293
13313
  var WAIT_OPTIONS = [
12294
13314
  { name: "wait", kind: "boolean", description: "Observe this run until its turn ends." },
12295
13315
  TIMEOUT_OPTION,
@@ -12324,7 +13344,45 @@ var CONTROL_OPTIONS = [
12324
13344
  valueName: "plan|build",
12325
13345
  description: "Conversation mode."
12326
13346
  }
12327
- ];
13347
+ ];
13348
+ var LITERAL_OPTION = {
13349
+ name: "literal",
13350
+ kind: "boolean",
13351
+ description: "Send the prompt as plain text: a leading /name never runs a provider command (by default it does)."
13352
+ };
13353
+ var ENQUEUE_OPTION = {
13354
+ name: "enqueue",
13355
+ kind: "boolean",
13356
+ description: "If the session is busy, queue this behind its current work (the backend sends it later) instead of refusing."
13357
+ };
13358
+ var ATTACH_OPTION = {
13359
+ name: "attach",
13360
+ kind: "list",
13361
+ valueName: "FILE",
13362
+ description: `Send a local file with the prompt (repeatable; at most ${PUBLIC_API_LIMITS.promptAttachmentsMaxItems} files, ${PUBLIC_API_LIMITS.promptAttachmentsMaxBytes / 1024} KiB in total).`
13363
+ };
13364
+ async function readAttachments(context, options2) {
13365
+ const files = options2.attach ?? [];
13366
+ if (files.length === 0)
13367
+ return;
13368
+ if (files.length > PUBLIC_API_LIMITS.promptAttachmentsMaxItems) {
13369
+ throw new CliError("input-too-large", `At most ${PUBLIC_API_LIMITS.promptAttachmentsMaxItems} attachments are allowed`);
13370
+ }
13371
+ let remaining = PUBLIC_API_LIMITS.promptAttachmentsMaxBytes;
13372
+ const names = new Set;
13373
+ const attachments = [];
13374
+ for (const file of files) {
13375
+ const name = path15.basename(file);
13376
+ if (names.has(name)) {
13377
+ throw new CliError("invalid-input", `Two attachments are both named ${name}`);
13378
+ }
13379
+ names.add(name);
13380
+ const bytes = await readBinaryFile(context.io, file, `Attachment ${name}`, remaining);
13381
+ remaining -= bytes.byteLength;
13382
+ attachments.push({ name, dataBase64: Buffer.from(bytes).toString("base64") });
13383
+ }
13384
+ return attachments;
13385
+ }
12328
13386
  function waitBudget(options2) {
12329
13387
  return options2.timeout ?? DEFAULT_WAIT_MS;
12330
13388
  }
@@ -12371,6 +13429,17 @@ function controlsInput(options2) {
12371
13429
  input.mode = oneOf(options2.mode, ["plan", "build"], "--mode");
12372
13430
  return input;
12373
13431
  }
13432
+ function holdLabel(hold) {
13433
+ if (!hold)
13434
+ return "";
13435
+ if (hold.heldAt !== undefined) {
13436
+ return ` (paused${hold.reason ? `, ${hold.reason}` : ""}; resume with interactions hold --resume)`;
13437
+ }
13438
+ if (hold.available) {
13439
+ return ` (pausable${hold.reason ? `, ${hold.reason}` : ""} with interactions hold)`;
13440
+ }
13441
+ return ` (not pausable${hold.reason ? `: ${hold.reason}` : ""})`;
13442
+ }
12374
13443
  function parseAnswers(options2) {
12375
13444
  const answers = new Map;
12376
13445
  const entry = (questionId) => {
@@ -12519,9 +13588,12 @@ var sessionCommands = [
12519
13588
  { name: "title", kind: "string", valueName: "TITLE", description: "Tab title." },
12520
13589
  ...CONTROL_OPTIONS,
12521
13590
  ...PROMPT_OPTIONS,
13591
+ LITERAL_OPTION,
13592
+ ATTACH_OPTION,
12522
13593
  ...WAIT_OPTIONS,
12523
13594
  REQUEST_ID_OPTION
12524
13595
  ],
13596
+ inputFields: { attach: "attachments" },
12525
13597
  idOutput: "session ID",
12526
13598
  async run(context, parsed) {
12527
13599
  const options2 = parsed.options;
@@ -12532,12 +13604,15 @@ var sessionCommands = [
12532
13604
  throw new CliError("invalid-input", "--environment and --agent are required");
12533
13605
  }
12534
13606
  const prompt = await readPrompt(context.io, options2);
13607
+ const attachments = await readAttachments(context, options2);
12535
13608
  const { session, result, receipt, warnings, recoveryDeadline } = await submitRun(context, "session.start", {
12536
13609
  environmentId,
12537
13610
  agent,
12538
13611
  prompt,
12539
13612
  ...options2.title !== undefined ? { title: options2.title } : {},
12540
- ...controlsInput(options2)
13613
+ ...controlsInput(options2),
13614
+ ...options2.literal === true ? { literal: true } : {},
13615
+ ...attachments ? { attachments } : {}
12541
13616
  }, options2);
12542
13617
  const fromReceipt = (final) => ({
12543
13618
  result: {
@@ -12563,8 +13638,8 @@ var sessionCommands = [
12563
13638
  {
12564
13639
  path: ["session", "prompt"],
12565
13640
  action: "session.prompt",
12566
- summary: "Send a follow-up prompt to an idle session.",
12567
- description: "Busy sessions are refused (exit 8); use `session steer` to redirect an active turn. The target must be the exact session: a vanished session is never replaced by a new one.",
13641
+ summary: "Send a follow-up prompt to an idle session, or queue it behind a busy one.",
13642
+ description: "Busy sessions are refused (exit 8) unless --enqueue puts the prompt in the backend's durable queue; use `session steer` to redirect an active turn. A leading /name runs that provider command unless --literal is given. The target must be the exact session: a vanished session is never replaced by a new one.",
12568
13643
  positionals: [{ name: "session", required: true }],
12569
13644
  options: [
12570
13645
  {
@@ -12574,18 +13649,26 @@ var sessionCommands = [
12574
13649
  description: "Conversation mode for this turn."
12575
13650
  },
12576
13651
  ...PROMPT_OPTIONS,
13652
+ LITERAL_OPTION,
13653
+ ENQUEUE_OPTION,
13654
+ ATTACH_OPTION,
12577
13655
  ...WAIT_OPTIONS,
12578
13656
  REQUEST_ID_OPTION
12579
13657
  ],
13658
+ inputFields: { attach: "attachments" },
12580
13659
  idOutput: "operation (run) ID",
12581
13660
  async run(context, parsed) {
12582
13661
  const options2 = parsed.options;
12583
13662
  validateWaitOptions(options2);
12584
13663
  const prompt = await readPrompt(context.io, options2);
13664
+ const attachments = await readAttachments(context, options2);
12585
13665
  const { session, result, receipt, warnings, recoveryDeadline } = await submitRun(context, "session.prompt", {
12586
13666
  sessionId: parsed.positionals.session,
12587
13667
  prompt,
12588
- ...options2.mode !== undefined ? { mode: oneOf(options2.mode, ["plan", "build"], "--mode") } : {}
13668
+ ...options2.mode !== undefined ? { mode: oneOf(options2.mode, ["plan", "build"], "--mode") } : {},
13669
+ ...options2.literal === true ? { literal: true } : {},
13670
+ ...options2.enqueue === true ? { enqueue: true } : {},
13671
+ ...attachments ? { attachments } : {}
12589
13672
  }, options2);
12590
13673
  const runId = result?.runId ?? receipt.operationId;
12591
13674
  return finishRun(context, parsed, {
@@ -12599,6 +13682,98 @@ var sessionCommands = [
12599
13682
  }, recoveryDeadline);
12600
13683
  }
12601
13684
  },
13685
+ {
13686
+ path: ["session", "commands"],
13687
+ action: "session.commands",
13688
+ summary: "List the session's slash commands (ID, name, availability, argument policy).",
13689
+ description: "Reads the catalogue the backend already holds; --refresh asks the provider again. Use an ID (or a unique name) with `session command`.",
13690
+ positionals: [{ name: "session", required: true }],
13691
+ options: [
13692
+ { name: "refresh", kind: "boolean", description: "Ask the provider for its list again." }
13693
+ ],
13694
+ idOutput: "command IDs, one per line",
13695
+ async run(context, parsed) {
13696
+ const session = await context.session();
13697
+ const list = await session.read("session.commands", {
13698
+ sessionId: parsed.positionals.session,
13699
+ ...parsed.options.refresh === true ? { refresh: true } : {}
13700
+ });
13701
+ return {
13702
+ action: "session.commands",
13703
+ result: list,
13704
+ connection: session.identity,
13705
+ ids: list.commands.map((command) => command.id),
13706
+ human: [
13707
+ `catalogue ${list.catalogue.status}${list.catalogue.error ? ` (${list.catalogue.error})` : ""}`,
13708
+ ...table(["NAME", "ID", "ARGS", "AVAILABLE", "DESCRIPTION"], list.commands.map((command) => [
13709
+ command.name,
13710
+ command.id,
13711
+ command.arguments,
13712
+ command.available ? "yes" : `no: ${command.unavailableReason ?? ""}`,
13713
+ (command.description ?? "").slice(0, 60)
13714
+ ]))
13715
+ ]
13716
+ };
13717
+ }
13718
+ },
13719
+ {
13720
+ path: ["session", "command"],
13721
+ action: "session.command",
13722
+ summary: "Run one selected slash command in a session.",
13723
+ description: "Pass the command's ID or unique name with --command and its arguments after `--`. The command is sent as a selection: if it is missing, ambiguous, unavailable or changed (--expected-binding-revision), the request fails (exit 3 or 8) and nothing is sent as text. Busy sessions are refused unless --enqueue.",
13724
+ positionals: [{ name: "session", required: true }],
13725
+ options: [
13726
+ {
13727
+ name: "command",
13728
+ kind: "string",
13729
+ valueName: "ID|NAME",
13730
+ description: "Command ID or name from `session commands`."
13731
+ },
13732
+ {
13733
+ name: "expected-binding-revision",
13734
+ kind: "string",
13735
+ valueName: "REV",
13736
+ description: "Refuse if the command changed since `session commands` listed this revision."
13737
+ },
13738
+ ENQUEUE_OPTION,
13739
+ ...WAIT_OPTIONS,
13740
+ REQUEST_ID_OPTION
13741
+ ],
13742
+ acceptsRest: true,
13743
+ idOutput: "operation (run) ID",
13744
+ examples: [
13745
+ "orkestrator session command ses_\u2026 --command /review -- the open diff",
13746
+ "orkestrator session command ses_\u2026 --command /compact --wait"
13747
+ ],
13748
+ async run(context, parsed) {
13749
+ const options2 = parsed.options;
13750
+ validateWaitOptions(options2);
13751
+ const command = stringOption(options2, "command");
13752
+ if (!command)
13753
+ throw new CliError("invalid-input", "--command is required");
13754
+ const args = parsed.rest.join(" ");
13755
+ if (Buffer.byteLength(args) > PUBLIC_API_LIMITS.commandArgumentsMaxBytes) {
13756
+ throw new CliError("input-too-large", "Command arguments are too large");
13757
+ }
13758
+ const { session, result, receipt, warnings, recoveryDeadline } = await submitRun(context, "session.command", {
13759
+ sessionId: parsed.positionals.session,
13760
+ command,
13761
+ ...args.trim() ? { arguments: args } : {},
13762
+ ...options2["expected-binding-revision"] !== undefined ? { expectedBindingRevision: options2["expected-binding-revision"] } : {},
13763
+ ...options2.enqueue === true ? { enqueue: true } : {}
13764
+ }, options2);
13765
+ const runId = result?.runId ?? receipt.operationId;
13766
+ return finishRun(context, parsed, {
13767
+ action: "session.command",
13768
+ result: result ?? { runId },
13769
+ receipt,
13770
+ warnings,
13771
+ connection: session.identity,
13772
+ ids: [runId],
13773
+ human: receipt ? receiptLines(receipt) : [`run ${runId}`]
13774
+ }, recoveryDeadline);
13775
+ }
13776
+ },
12602
13777
  {
12603
13778
  path: ["session", "stop"],
12604
13779
  action: "session.stop",
@@ -12810,6 +13985,7 @@ var sessionCommands = [
12810
13985
  ids: interactions.items.map((item) => item.id),
12811
13986
  human: interactions.items.length ? interactions.items.flatMap((item) => [
12812
13987
  `${item.id} ${item.kind} revision ${item.revision} ${item.title}`,
13988
+ ...item.expiresAt !== undefined ? [` expires ${new Date(item.expiresAt).toISOString()}${holdLabel(item.hold)}`] : [],
12813
13989
  ...item.questions.map((question) => ` ${question.id}: ${question.prompt} [${question.options.map((option) => option.id).join(", ")}]${question.allowFreeText ? " (free text)" : ""}`),
12814
13990
  ` actions: ${item.actions.join(", ")}`
12815
13991
  ]) : ["(no pending interactions)"]
@@ -12889,6 +14065,41 @@ var sessionCommands = [
12889
14065
  };
12890
14066
  }
12891
14067
  },
14068
+ {
14069
+ path: ["session", "interactions", "hold"],
14070
+ action: "session.interaction.hold",
14071
+ inputFields: { resume: "held" },
14072
+ summary: "Pause one pending interaction's countdown; --resume restarts a fresh window.",
14073
+ description: "The pause is enforced by the provider bridge: a paused request still ends at the 24-hour hold ceiling or the provider's own sooner limit (the moved expiresAt). An interaction with no countdown, or whose bridge predates pausing, is refused (exit 8). Nothing is answered.",
14074
+ positionals: [
14075
+ { name: "session", required: true },
14076
+ { name: "interaction", required: true }
14077
+ ],
14078
+ options: [
14079
+ {
14080
+ name: "resume",
14081
+ kind: "boolean",
14082
+ description: "Resume the countdown instead of pausing it."
14083
+ },
14084
+ REQUEST_ID_OPTION
14085
+ ],
14086
+ async run(context, parsed) {
14087
+ const held = parsed.options.resume !== true;
14088
+ const { session, result, receipt, warnings } = await submit(context, "session.interaction.hold", {
14089
+ sessionId: parsed.positionals.session,
14090
+ interactionId: parsed.positionals.interaction,
14091
+ held
14092
+ }, parsed.options);
14093
+ return {
14094
+ action: "session.interaction.hold",
14095
+ result,
14096
+ receipt,
14097
+ warnings,
14098
+ connection: session.identity,
14099
+ human: receipt ? receiptLines(receipt) : [held ? "paused" : "resumed"]
14100
+ };
14101
+ }
14102
+ },
12892
14103
  {
12893
14104
  path: ["session", "transcript"],
12894
14105
  action: "session.transcript",
@@ -13240,9 +14451,9 @@ var ticketCommands = [
13240
14451
  ];
13241
14452
 
13242
14453
  // src/client/commands/toolchain.ts
13243
- import path18 from "path";
14454
+ import path19 from "path";
13244
14455
  function dataDirectory2(context, parsed) {
13245
- return path18.resolve(stringOption(parsed.options, "data-dir") ?? context.io.env.ORKESTRATOR_DATA_DIR ?? defaultDataDir(process.platform, context.io.env));
14456
+ return path19.resolve(stringOption(parsed.options, "data-dir") ?? context.io.env.ORKESTRATOR_DATA_DIR ?? defaultDataDir(process.platform, context.io.env));
13246
14457
  }
13247
14458
  function requestedTools(parsed) {
13248
14459
  const raw = parsed.options.tool;
@@ -13345,7 +14556,7 @@ var toolchainCommands = [
13345
14556
  const rows = installed.tools.map((tool) => ({
13346
14557
  tool,
13347
14558
  version: installed.versions[tool] ?? "",
13348
- executable: path18.join(installed.currentBinDir, path18.basename(installed.executables[tool]))
14559
+ executable: path19.join(installed.currentBinDir, path19.basename(installed.executables[tool]))
13349
14560
  }));
13350
14561
  return {
13351
14562
  action: "toolchain.install",
@@ -13375,6 +14586,337 @@ var toolchainCommands = [
13375
14586
  }
13376
14587
  ];
13377
14588
 
14589
+ // src/client/commands/workflows.ts
14590
+ init_agent_platforms();
14591
+ function parseSelection(raw, flag) {
14592
+ const equals = raw.indexOf("=");
14593
+ const agent = raw.slice(0, equals);
14594
+ if (equals <= 0 || !AGENT_PLATFORMS.includes(agent)) {
14595
+ throw new CliError("invalid-input", `${flag} expects AGENT=MODEL[@EFFORT] with AGENT one of ${AGENT_PLATFORMS.join(", ")}`);
14596
+ }
14597
+ const rest = raw.slice(equals + 1);
14598
+ const at = rest.lastIndexOf("@");
14599
+ const model = at > 0 ? rest.slice(0, at) : rest;
14600
+ const reasoningEffort = at > 0 ? rest.slice(at + 1) : undefined;
14601
+ if (!model)
14602
+ throw new CliError("invalid-input", `${flag} needs a model`);
14603
+ return { agent, model, ...reasoningEffort ? { reasoningEffort } : {} };
14604
+ }
14605
+ var SELECTION_HELP = "AGENT=MODEL[@EFFORT], e.g. codex=gpt-5.5@high.";
14606
+ var AGENT_OPTIONS = [
14607
+ {
14608
+ name: "agent",
14609
+ kind: "string",
14610
+ valueName: "AGENT",
14611
+ description: "Agent platform. Omitted: the action's configured default."
14612
+ },
14613
+ { name: "model", kind: "string", valueName: "MODEL", description: "Requires --agent." },
14614
+ {
14615
+ name: "reasoning",
14616
+ kind: "string",
14617
+ valueName: "LEVEL",
14618
+ description: "Reasoning level; requires --model."
14619
+ },
14620
+ {
14621
+ name: "fast",
14622
+ kind: "boolean",
14623
+ negatable: true,
14624
+ description: "Fast speed where supported."
14625
+ }
14626
+ ];
14627
+ function reviewLines(review) {
14628
+ return keyValues([
14629
+ ["review", review.id],
14630
+ ["environment", review.environmentId],
14631
+ ["phase", review.phase],
14632
+ ...review.targetBranch ? [["target", review.targetBranch]] : [],
14633
+ ...review.error ? [["error", review.error]] : []
14634
+ ]);
14635
+ }
14636
+ function reviewMutation(path, action, summary) {
14637
+ return {
14638
+ path: ["review", path],
14639
+ action,
14640
+ summary,
14641
+ positionals: [{ name: "workflow", required: true, description: "Multi Review ID." }],
14642
+ options: [REQUEST_ID_OPTION],
14643
+ inputFields: { workflow: "workflowId" },
14644
+ idOutput: "workflow ID",
14645
+ async run(context, parsed) {
14646
+ const workflowId = parsed.positionals.workflow;
14647
+ const { session, result, receipt, warnings } = await submit(context, action, { workflowId }, parsed.options);
14648
+ return {
14649
+ action,
14650
+ result,
14651
+ receipt,
14652
+ warnings,
14653
+ connection: session.identity,
14654
+ ids: [workflowId],
14655
+ human: keyValues([
14656
+ ["review", workflowId],
14657
+ ["phase", result.phase ?? receipt?.state ?? ""]
14658
+ ])
14659
+ };
14660
+ }
14661
+ };
14662
+ }
14663
+ function prCommand(verb) {
14664
+ const action = verb === "create" ? "pr.create" : "pr.resolve";
14665
+ return {
14666
+ path: ["pr", verb],
14667
+ action,
14668
+ summary: verb === "create" ? "Start the Create PR agent: commit, push and open a pull request (never merges)." : "Start the Resolve Conflicts agent: merge the target branch, resolve, validate and push.",
14669
+ description: "Opens the same agent tab as the environment's button, without focusing it. The response is a run receipt; add --wait (or `run wait`) to observe it.",
14670
+ positionals: [{ name: "environment", required: true, description: "Ready environment." }],
14671
+ options: [
14672
+ {
14673
+ name: "target-branch",
14674
+ kind: "string",
14675
+ valueName: "BRANCH",
14676
+ description: verb === "create" ? "Pull request base. Omitted: the project's PR base branch." : "Branch to merge in. Omitted: the environment's PR base branch."
14677
+ },
14678
+ ...AGENT_OPTIONS,
14679
+ ...WAIT_OPTIONS,
14680
+ REQUEST_ID_OPTION
14681
+ ],
14682
+ idOutput: "session ID",
14683
+ async run(context, parsed) {
14684
+ const options2 = parsed.options;
14685
+ validateWaitOptions(options2);
14686
+ const fast = optionalBoolean(options2, "fast");
14687
+ const input = {
14688
+ environmentId: parsed.positionals.environment,
14689
+ ...options2["target-branch"] !== undefined ? { targetBranch: options2["target-branch"] } : {},
14690
+ ...options2.agent !== undefined ? { agent: options2.agent } : {},
14691
+ ...options2.model !== undefined ? { model: options2.model } : {},
14692
+ ...options2.reasoning !== undefined ? { reasoning: options2.reasoning } : {},
14693
+ ...fast !== undefined ? { fastMode: fast } : {}
14694
+ };
14695
+ const { session, result, receipt, warnings, recoveryDeadline } = await submitRun(context, action, input, options2);
14696
+ const fromReceipt = (final) => ({
14697
+ result: {
14698
+ sessionId: final.resources.sessionId ?? null,
14699
+ tabId: final.resources.tabId ?? null,
14700
+ runId: final.operationId
14701
+ },
14702
+ ids: final.resources.sessionId ? [final.resources.sessionId] : []
14703
+ });
14704
+ return finishRun(context, parsed, {
14705
+ action,
14706
+ ...result ? { result, ids: [result.sessionId] } : fromReceipt(receipt),
14707
+ receipt,
14708
+ warnings,
14709
+ connection: session.identity,
14710
+ human: result ? keyValues([
14711
+ ["session", result.sessionId],
14712
+ ["run", result.runId]
14713
+ ]) : []
14714
+ }, recoveryDeadline, result ? undefined : fromReceipt);
14715
+ }
14716
+ };
14717
+ }
14718
+ var workflowCommands = [
14719
+ {
14720
+ path: ["node", "list"],
14721
+ action: "node.list",
14722
+ summary: "List this node and the leaf nodes it controls; pass a node's ID with --node.",
14723
+ positionals: [],
14724
+ options: [],
14725
+ idOutput: "node IDs",
14726
+ async run(context) {
14727
+ if (context.global.node) {
14728
+ throw new CliError("invalid-input", "node list always lists the connected node; omit --node");
14729
+ }
14730
+ const session = await context.session();
14731
+ const list = await session.read("node.list", {});
14732
+ return {
14733
+ action: "node.list",
14734
+ result: list,
14735
+ connection: session.identity,
14736
+ ids: list.items.map((node) => node.nodeId),
14737
+ human: table(["NODE", "NAME", "STATE", "VERSION", "ACTIONS"], list.items.map((node) => [
14738
+ node.nodeId,
14739
+ node.self ? `${node.name} (this node)` : node.name,
14740
+ node.state,
14741
+ node.version ?? "",
14742
+ node.publicActions ? "yes" : node.reason ?? "no"
14743
+ ]))
14744
+ };
14745
+ }
14746
+ },
14747
+ {
14748
+ path: ["review", "launch"],
14749
+ action: "review.launch",
14750
+ summary: "Start an environment's Multi Review, as its button does (an active one is reused).",
14751
+ description: "Omitted reviewers, models and auto-actions use the saved Multi Review settings. The review continues in the backend; follow it with `review get`.",
14752
+ positionals: [{ name: "environment", required: true, description: "Environment to review." }],
14753
+ options: [
14754
+ {
14755
+ name: "reviewer",
14756
+ kind: "list",
14757
+ valueName: "SELECTION",
14758
+ description: `Reviewer row (repeatable): ${SELECTION_HELP}`
14759
+ },
14760
+ {
14761
+ name: "review-model",
14762
+ kind: "string",
14763
+ valueName: "SELECTION",
14764
+ description: `Preparation and consolidation model: ${SELECTION_HELP}`
14765
+ },
14766
+ {
14767
+ name: "fix-model",
14768
+ kind: "string",
14769
+ valueName: "SELECTION",
14770
+ description: `Fix model: ${SELECTION_HELP}`
14771
+ },
14772
+ {
14773
+ name: "auto-fix",
14774
+ kind: "boolean",
14775
+ negatable: true,
14776
+ description: "Start the Fix when consolidation reports findings."
14777
+ },
14778
+ {
14779
+ name: "auto-pr",
14780
+ kind: "boolean",
14781
+ negatable: true,
14782
+ description: "Open the pull request after the Fix (never merges)."
14783
+ },
14784
+ {
14785
+ name: "target-branch",
14786
+ kind: "string",
14787
+ valueName: "BRANCH",
14788
+ description: "Branch to review against. Omitted: the project's PR base branch."
14789
+ },
14790
+ {
14791
+ name: "instruction-file",
14792
+ kind: "string",
14793
+ valueName: "PATH",
14794
+ description: "Read the review instruction from a local file (empty: none)."
14795
+ },
14796
+ REQUEST_ID_OPTION
14797
+ ],
14798
+ inputFields: {
14799
+ reviewer: "reviewers",
14800
+ "instruction-file": "reviewInstruction"
14801
+ },
14802
+ idOutput: "workflow ID",
14803
+ async run(context, parsed) {
14804
+ const options2 = parsed.options;
14805
+ const reviewers = (options2.reviewer ?? []).map((raw) => parseSelection(raw, "--reviewer"));
14806
+ const reviewModel = stringOption(options2, "review-model");
14807
+ const fixModel = stringOption(options2, "fix-model");
14808
+ const autoFix = optionalBoolean(options2, "auto-fix");
14809
+ const autoPr = optionalBoolean(options2, "auto-pr");
14810
+ const instructionFile = stringOption(options2, "instruction-file");
14811
+ const reviewInstruction = instructionFile !== undefined ? await readTextSource(context.io, {
14812
+ file: instructionFile,
14813
+ label: "Review instruction",
14814
+ maxBytes: 1e5,
14815
+ names: { file: "--instruction-file", stdin: "--instruction-file -" },
14816
+ required: false
14817
+ }) : undefined;
14818
+ const { session, result, receipt, warnings } = await submit(context, "review.launch", {
14819
+ environmentId: parsed.positionals.environment,
14820
+ ...reviewers.length ? { reviewers } : {},
14821
+ ...reviewModel ? { reviewModel: parseSelection(reviewModel, "--review-model") } : {},
14822
+ ...fixModel ? { fixModel: parseSelection(fixModel, "--fix-model") } : {},
14823
+ ...autoFix !== undefined ? { autoFix } : {},
14824
+ ...autoPr !== undefined ? { autoPr } : {},
14825
+ ...options2["target-branch"] !== undefined ? { targetBranch: options2["target-branch"] } : {},
14826
+ ...reviewInstruction !== undefined ? { reviewInstruction } : {}
14827
+ }, options2);
14828
+ return {
14829
+ action: "review.launch",
14830
+ result,
14831
+ receipt,
14832
+ warnings,
14833
+ connection: session.identity,
14834
+ ids: [result.workflowId],
14835
+ human: keyValues([
14836
+ ["review", result.workflowId],
14837
+ ["phase", result.phase ?? ""],
14838
+ ["reused", result.reused ? "yes" : "no"]
14839
+ ])
14840
+ };
14841
+ }
14842
+ },
14843
+ {
14844
+ path: ["review", "list"],
14845
+ action: "review.list",
14846
+ summary: "List an environment's Multi Reviews, newest first.",
14847
+ positionals: [{ name: "environment", required: true }],
14848
+ options: [],
14849
+ idOutput: "workflow IDs",
14850
+ async run(context, parsed) {
14851
+ const session = await context.session();
14852
+ const page = await session.read("review.list", {
14853
+ environmentId: parsed.positionals.environment
14854
+ });
14855
+ return {
14856
+ action: "review.list",
14857
+ result: page,
14858
+ connection: session.identity,
14859
+ ids: page.items.map((review) => review.id),
14860
+ human: table(["REVIEW", "PHASE", "TARGET", "CREATED"], page.items.map((review) => [
14861
+ review.id,
14862
+ review.phase,
14863
+ review.targetBranch ?? "",
14864
+ review.createdAt ?? ""
14865
+ ]))
14866
+ };
14867
+ }
14868
+ },
14869
+ {
14870
+ path: ["review", "get"],
14871
+ action: "review.get",
14872
+ summary: "Read one Multi Review.",
14873
+ positionals: [{ name: "workflow", required: true, description: "Multi Review ID." }],
14874
+ options: [],
14875
+ inputFields: { workflow: "workflowId" },
14876
+ async run(context, parsed) {
14877
+ const session = await context.session();
14878
+ const review = await session.read("review.get", {
14879
+ workflowId: parsed.positionals.workflow
14880
+ });
14881
+ return {
14882
+ action: "review.get",
14883
+ result: review,
14884
+ connection: session.identity,
14885
+ human: reviewLines(review)
14886
+ };
14887
+ }
14888
+ },
14889
+ {
14890
+ path: ["review", "open"],
14891
+ action: "review.open",
14892
+ summary: "Open or focus a Multi Review's tab (--fix: its Fix tab); starts no work.",
14893
+ positionals: [{ name: "workflow", required: true, description: "Multi Review ID." }],
14894
+ options: [
14895
+ { name: "fix", kind: "boolean", description: "Open the Fix session tab instead." },
14896
+ REQUEST_ID_OPTION
14897
+ ],
14898
+ inputFields: { workflow: "workflowId", fix: "surface" },
14899
+ idOutput: "workflow ID",
14900
+ async run(context, parsed) {
14901
+ const workflowId = parsed.positionals.workflow;
14902
+ const { session, result, receipt, warnings } = await submit(context, "review.open", { workflowId, ...parsed.options.fix === true ? { surface: "fix" } : {} }, parsed.options);
14903
+ return {
14904
+ action: "review.open",
14905
+ result,
14906
+ receipt,
14907
+ warnings,
14908
+ connection: session.identity,
14909
+ ids: [workflowId],
14910
+ human: [`opened review ${workflowId}`]
14911
+ };
14912
+ }
14913
+ },
14914
+ reviewMutation("address", "review.address", "Address a Multi Review's findings: hand them to its Fix session."),
14915
+ reviewMutation("cancel", "review.cancel", "Cancel a running Multi Review; its record is kept."),
14916
+ prCommand("create"),
14917
+ prCommand("resolve")
14918
+ ];
14919
+
13378
14920
  // src/client/help.ts
13379
14921
  var GROUP_SUMMARIES = {
13380
14922
  connection: "Select and verify the backend this client talks to.",
@@ -13509,6 +15051,7 @@ class OutputWriter {
13509
15051
  ok: true,
13510
15052
  ...outcome.connection ? { connection: outcome.connection } : {},
13511
15053
  result: outcome.result ?? null,
15054
+ ...outcome.node ? { node: outcome.node } : {},
13512
15055
  ...outcome.receipt ? { receipt: outcome.receipt } : {},
13513
15056
  ...outcome.warnings && outcome.warnings.length > 0 ? { warnings: outcome.warnings } : {}
13514
15057
  };
@@ -13532,7 +15075,7 @@ class OutputWriter {
13532
15075
  this.io.stderr(`warning: ${warning}
13533
15076
  `);
13534
15077
  if (outcome.receipt)
13535
- this.io.stderr(`${receiptHint(outcome.receipt)}
15078
+ this.io.stderr(`${receiptHint(outcome.receipt, outcome.node)}
13536
15079
  `);
13537
15080
  return outcome.exitCodeOverride ?? 0;
13538
15081
  }
@@ -13540,12 +15083,14 @@ class OutputWriter {
13540
15083
  const cliError = error instanceof CliError ? error : new CliError("internal-error", error instanceof Error ? boundMessage(error.message) : "Unexpected client failure");
13541
15084
  const exitCode = exitCodeForError(cliError.code);
13542
15085
  const receipt = cliError.receipt ?? context.receipt;
15086
+ const node = cliError.node ?? context.node;
13543
15087
  if (this.mode === "json" || this.mode === "jsonl") {
13544
15088
  const envelope = {
13545
15089
  schemaVersion: PUBLIC_API_SCHEMA_VERSION,
13546
15090
  action: cliError.action ?? context.action ?? "cli",
13547
15091
  ok: false,
13548
15092
  ...context.connection ? { connection: context.connection } : {},
15093
+ ...node ? { node } : {},
13549
15094
  error: {
13550
15095
  code: cliError.code,
13551
15096
  message: cliError.message,
@@ -13560,9 +15105,12 @@ class OutputWriter {
13560
15105
  return exitCode;
13561
15106
  }
13562
15107
  this.io.stderr(`error [${cliError.code}]: ${cliError.message}
15108
+ `);
15109
+ if (typeof cliError.details?.recovery === "string")
15110
+ this.io.stderr(`recover with \`${cliError.details.recovery}\`
13563
15111
  `);
13564
15112
  if (receipt)
13565
- this.io.stderr(`${receiptHint(receipt)}
15113
+ this.io.stderr(`${receiptHint(receipt, node)}
13566
15114
  `);
13567
15115
  return exitCode;
13568
15116
  }
@@ -13573,6 +15121,7 @@ class OutputWriter {
13573
15121
  this.io.stdout(`${JSON.stringify({
13574
15122
  schemaVersion: PUBLIC_API_SCHEMA_VERSION,
13575
15123
  action: context.action,
15124
+ ...context.node ? { node: context.node } : {},
13576
15125
  ok: false,
13577
15126
  ...context.connection ? { connection: context.connection } : {},
13578
15127
  error: {
@@ -13589,13 +15138,13 @@ class OutputWriter {
13589
15138
  this.io.stderr(`${message}
13590
15139
  `);
13591
15140
  if (context.receipt)
13592
- this.io.stderr(`${receiptHint(context.receipt)}
15141
+ this.io.stderr(`${receiptHint(context.receipt, context.node)}
13593
15142
  `);
13594
15143
  return code;
13595
15144
  }
13596
15145
  }
13597
- function receiptHint(receipt) {
13598
- return `operation ${receipt.operationId} (${receipt.action}) is ${receipt.state}; inspect with \`orkestrator run get ${receipt.operationId}\``;
15146
+ function receiptHint(receipt, node) {
15147
+ return `operation ${receipt.operationId} (${receipt.action}) is ${receipt.state}; inspect with \`orkestrator run get ${receipt.operationId}${node ? ` --node ${node.nodeId}` : ""}\``;
13599
15148
  }
13600
15149
  function boundMessage(message, max = 500) {
13601
15150
  const single = message.replace(/\s+/g, " ").trim();
@@ -13664,13 +15213,16 @@ var CLIENT_COMMANDS = [
13664
15213
  ...environmentCommands,
13665
15214
  ...sessionCommands,
13666
15215
  ...changesCommands,
15216
+ ...workflowCommands,
13667
15217
  ...attentionCommands,
13668
15218
  ...ticketCommands,
13669
15219
  ...mailCommands,
15220
+ ...scheduleCommands,
13670
15221
  ...runCommands,
13671
15222
  ...toolchainCommands,
13672
15223
  ...hostCommands,
13673
- ...serviceCommands
15224
+ ...serviceCommands,
15225
+ ...completionCommands(() => CLIENT_COMMANDS)
13674
15226
  ];
13675
15227
  var COMMAND_TREE = { commands: CLIENT_COMMANDS };
13676
15228
  function stripHelpWord(argv) {
@@ -13736,11 +15288,15 @@ async function runClient(argv, options2) {
13736
15288
  session = new ClientSession(target, new LocalReceiptStore(configStore.receiptsDirectory), {
13737
15289
  fetchImpl: options2.fetchImpl,
13738
15290
  requestTimeoutMs: parsed.global.requestTimeoutMs,
13739
- signal
15291
+ signal,
15292
+ ...parsed.global.node ? { node: parsed.global.node } : {}
13740
15293
  });
13741
15294
  return session;
13742
15295
  }
13743
15296
  };
15297
+ if (command.spec.local && parsed.global.node) {
15298
+ return output.failure(new CliError("invalid-input", `'${command.spec.path.join(" ")}' does not use --node`), { action });
15299
+ }
13744
15300
  if (command.spec.local && (parsed.global.connection || parsed.global.profile)) {
13745
15301
  if (!(command.spec.path[0] === "connection" && command.spec.path[1] === "check")) {
13746
15302
  return output.failure(new CliError("invalid-input", `'${command.spec.path.join(" ")}' does not use --connection or --profile`), { action });
@@ -13750,12 +15306,14 @@ async function runClient(argv, options2) {
13750
15306
  try {
13751
15307
  outcome = await command.spec.run(context, command);
13752
15308
  } catch (error) {
13753
- const connection = session ? session.identity : undefined;
15309
+ const failedSession = session;
15310
+ const connection = failedSession?.identity;
13754
15311
  if (signal.aborted && !(error instanceof CliError && error.code === "observation-interrupted")) {
13755
15312
  return output.interrupted(signalName(signal), {
13756
15313
  action,
13757
15314
  ...error instanceof CliError && error.receipt ? { receipt: error.receipt } : {},
13758
- ...connection ? { connection } : {}
15315
+ ...connection ? { connection } : {},
15316
+ ...failedSession?.nodeTag ? { node: failedSession.nodeTag } : {}
13759
15317
  });
13760
15318
  }
13761
15319
  if (error instanceof CliError && error.code === "observation-interrupted") {
@@ -13763,12 +15321,22 @@ async function runClient(argv, options2) {
13763
15321
  action: error.action ?? action,
13764
15322
  details: error.details,
13765
15323
  ...error.receipt ? { receipt: error.receipt } : {},
13766
- ...connection ? { connection } : {}
15324
+ ...connection ? { connection } : {},
15325
+ ...failedSession?.nodeTag ? { node: failedSession.nodeTag } : {}
13767
15326
  });
13768
15327
  }
13769
- return output.failure(error, { action, ...connection ? { connection } : {} });
15328
+ return output.failure(error, {
15329
+ action,
15330
+ ...connection ? { connection } : {},
15331
+ ...failedSession?.nodeTag ? { node: failedSession.nodeTag } : {}
15332
+ });
13770
15333
  }
13771
- return output.success(outcome);
15334
+ const activeSession = session;
15335
+ return output.success({
15336
+ ...outcome,
15337
+ ...activeSession?.nodeTag ? { node: activeSession.nodeTag } : {},
15338
+ warnings: [...new Set([...outcome.warnings ?? [], ...activeSession?.warnings ?? []])]
15339
+ });
13772
15340
  }
13773
15341
  async function runClientProcess(argv, version) {
13774
15342
  const controller = new AbortController;