@rosthq/cli 0.6.7 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -472,13 +472,13 @@ function __disposeResources(env) {
472
472
  }
473
473
  return next();
474
474
  }
475
- function __rewriteRelativeImportExtension(path4, preserveJsx) {
476
- if (typeof path4 === "string" && /^\.\.?\//.test(path4)) {
477
- return path4.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
475
+ function __rewriteRelativeImportExtension(path5, preserveJsx) {
476
+ if (typeof path5 === "string" && /^\.\.?\//.test(path5)) {
477
+ return path5.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
478
478
  return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
479
479
  });
480
480
  }
481
- return path4;
481
+ return path5;
482
482
  }
483
483
  var extendStatics, __assign, __createBinding, __setModuleDefault, ownKeys, _SuppressedError, tslib_es6_default;
484
484
  var init_tslib_es6 = __esm({
@@ -13257,7 +13257,7 @@ var require_main3 = __commonJS({
13257
13257
  });
13258
13258
 
13259
13259
  // src/index.ts
13260
- import { realpathSync } from "node:fs";
13260
+ import { realpathSync as realpathSync2 } from "node:fs";
13261
13261
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
13262
13262
 
13263
13263
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
@@ -14026,10 +14026,10 @@ function mergeDefs(...defs) {
14026
14026
  function cloneDef(schema) {
14027
14027
  return mergeDefs(schema._zod.def);
14028
14028
  }
14029
- function getElementAtPath(obj, path4) {
14030
- if (!path4)
14029
+ function getElementAtPath(obj, path5) {
14030
+ if (!path5)
14031
14031
  return obj;
14032
- return path4.reduce((acc, key) => acc?.[key], obj);
14032
+ return path5.reduce((acc, key) => acc?.[key], obj);
14033
14033
  }
14034
14034
  function promiseAllObject(promisesObj) {
14035
14035
  const keys = Object.keys(promisesObj);
@@ -14438,11 +14438,11 @@ function explicitlyAborted(x, startIndex = 0) {
14438
14438
  }
14439
14439
  return false;
14440
14440
  }
14441
- function prefixIssues(path4, issues) {
14441
+ function prefixIssues(path5, issues) {
14442
14442
  return issues.map((iss) => {
14443
14443
  var _a3;
14444
14444
  (_a3 = iss).path ?? (_a3.path = []);
14445
- iss.path.unshift(path4);
14445
+ iss.path.unshift(path5);
14446
14446
  return iss;
14447
14447
  });
14448
14448
  }
@@ -14589,16 +14589,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
14589
14589
  }
14590
14590
  function formatError(error51, mapper = (issue2) => issue2.message) {
14591
14591
  const fieldErrors = { _errors: [] };
14592
- const processError = (error52, path4 = []) => {
14592
+ const processError = (error52, path5 = []) => {
14593
14593
  for (const issue2 of error52.issues) {
14594
14594
  if (issue2.code === "invalid_union" && issue2.errors.length) {
14595
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
14595
+ issue2.errors.map((issues) => processError({ issues }, [...path5, ...issue2.path]));
14596
14596
  } else if (issue2.code === "invalid_key") {
14597
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
14597
+ processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
14598
14598
  } else if (issue2.code === "invalid_element") {
14599
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
14599
+ processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
14600
14600
  } else {
14601
- const fullpath = [...path4, ...issue2.path];
14601
+ const fullpath = [...path5, ...issue2.path];
14602
14602
  if (fullpath.length === 0) {
14603
14603
  fieldErrors._errors.push(mapper(issue2));
14604
14604
  } else {
@@ -14625,17 +14625,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
14625
14625
  }
14626
14626
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
14627
14627
  const result = { errors: [] };
14628
- const processError = (error52, path4 = []) => {
14628
+ const processError = (error52, path5 = []) => {
14629
14629
  var _a3, _b;
14630
14630
  for (const issue2 of error52.issues) {
14631
14631
  if (issue2.code === "invalid_union" && issue2.errors.length) {
14632
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
14632
+ issue2.errors.map((issues) => processError({ issues }, [...path5, ...issue2.path]));
14633
14633
  } else if (issue2.code === "invalid_key") {
14634
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
14634
+ processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
14635
14635
  } else if (issue2.code === "invalid_element") {
14636
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
14636
+ processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
14637
14637
  } else {
14638
- const fullpath = [...path4, ...issue2.path];
14638
+ const fullpath = [...path5, ...issue2.path];
14639
14639
  if (fullpath.length === 0) {
14640
14640
  result.errors.push(mapper(issue2));
14641
14641
  continue;
@@ -14667,8 +14667,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
14667
14667
  }
14668
14668
  function toDotPath(_path) {
14669
14669
  const segs = [];
14670
- const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
14671
- for (const seg of path4) {
14670
+ const path5 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
14671
+ for (const seg of path5) {
14672
14672
  if (typeof seg === "number")
14673
14673
  segs.push(`[${seg}]`);
14674
14674
  else if (typeof seg === "symbol")
@@ -27360,13 +27360,13 @@ function resolveRef(ref, ctx) {
27360
27360
  if (!ref.startsWith("#")) {
27361
27361
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
27362
27362
  }
27363
- const path4 = ref.slice(1).split("/").filter(Boolean);
27364
- if (path4.length === 0) {
27363
+ const path5 = ref.slice(1).split("/").filter(Boolean);
27364
+ if (path5.length === 0) {
27365
27365
  return ctx.rootSchema;
27366
27366
  }
27367
27367
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
27368
- if (path4[0] === defsKey) {
27369
- const key = path4[1];
27368
+ if (path5[0] === defsKey) {
27369
+ const key = path5[1];
27370
27370
  if (!key || !ctx.defs[key]) {
27371
27371
  throw new Error(`Reference not found: ${ref}`);
27372
27372
  }
@@ -27828,7 +27828,7 @@ var CommandClient = class {
27828
27828
  this.fetchImpl = options.fetchImpl ?? fetch;
27829
27829
  }
27830
27830
  async execute(commandId, body = {}, options = {}) {
27831
- const path4 = `/api/commands/${encodeURIComponent(commandId)}`;
27831
+ const path5 = `/api/commands/${encodeURIComponent(commandId)}`;
27832
27832
  const headers = {
27833
27833
  authorization: `Bearer ${this.token}`,
27834
27834
  "content-type": "application/json"
@@ -27836,7 +27836,7 @@ var CommandClient = class {
27836
27836
  if (options.targetSeatId !== void 0 && options.targetSeatId.length > 0) {
27837
27837
  headers["x-rost-seat"] = options.targetSeatId;
27838
27838
  }
27839
- const response = await this.fetchImpl(`${this.appUrl}${path4}`, {
27839
+ const response = await this.fetchImpl(`${this.appUrl}${path5}`, {
27840
27840
  method: "POST",
27841
27841
  headers,
27842
27842
  body: JSON.stringify(body)
@@ -27849,7 +27849,7 @@ var CommandClient = class {
27849
27849
  const snippet = redactSecrets(text.replace(/\s+/g, " ").trim()).slice(0, 200);
27850
27850
  throw new CommandClientError(
27851
27851
  response.status,
27852
- `server returned HTTP ${response.status} (non-JSON) from ${path4}` + (snippet ? ` \u2014 ${snippet}` : "") + " \u2014 the deployment may be unhealthy"
27852
+ `server returned HTTP ${response.status} (non-JSON) from ${path5}` + (snippet ? ` \u2014 ${snippet}` : "") + " \u2014 the deployment may be unhealthy"
27853
27853
  );
27854
27854
  }
27855
27855
  const parsed = commandResponseSchema.parse(raw);
@@ -33056,8 +33056,8 @@ var IcebergError = class extends Error {
33056
33056
  return this.status === 419;
33057
33057
  }
33058
33058
  };
33059
- function buildUrl(baseUrl, path4, query) {
33060
- const url2 = new URL(path4, baseUrl);
33059
+ function buildUrl(baseUrl, path5, query) {
33060
+ const url2 = new URL(path5, baseUrl);
33061
33061
  if (query) {
33062
33062
  for (const [key, value] of Object.entries(query)) {
33063
33063
  if (value !== void 0) {
@@ -33087,12 +33087,12 @@ function createFetchClient(options) {
33087
33087
  return {
33088
33088
  async request({
33089
33089
  method,
33090
- path: path4,
33090
+ path: path5,
33091
33091
  query,
33092
33092
  body,
33093
33093
  headers
33094
33094
  }) {
33095
- const url2 = buildUrl(options.baseUrl, path4, query);
33095
+ const url2 = buildUrl(options.baseUrl, path5, query);
33096
33096
  const authHeaders = await buildAuthHeaders(options.auth);
33097
33097
  const res = await fetchFn(url2, {
33098
33098
  method,
@@ -33954,7 +33954,7 @@ var StorageFileApi = class extends BaseApiClient {
33954
33954
  * @param path The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
33955
33955
  * @param fileBody The body of the file to be stored in the bucket.
33956
33956
  */
33957
- async uploadOrUpdate(method, path4, fileBody, fileOptions) {
33957
+ async uploadOrUpdate(method, path5, fileBody, fileOptions) {
33958
33958
  var _this = this;
33959
33959
  return _this.handleOperation(async () => {
33960
33960
  let body;
@@ -33978,7 +33978,7 @@ var StorageFileApi = class extends BaseApiClient {
33978
33978
  if ((typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body && typeof body === "object" && "pipe" in body && typeof body.pipe === "function") && !options.duplex) options.duplex = "half";
33979
33979
  }
33980
33980
  if (fileOptions === null || fileOptions === void 0 ? void 0 : fileOptions.headers) for (const [key, value] of Object.entries(fileOptions.headers)) headers = setHeader(headers, key, value);
33981
- const cleanPath = _this._removeEmptyFolders(path4);
33981
+ const cleanPath = _this._removeEmptyFolders(path5);
33982
33982
  const _path = _this._getFinalPath(cleanPath);
33983
33983
  const data = await (method == "PUT" ? put : post)(_this.fetch, `${_this.url}/object/${_path}`, body, _objectSpread22({ headers }, (options === null || options === void 0 ? void 0 : options.duplex) ? { duplex: options.duplex } : {}));
33984
33984
  return {
@@ -34055,8 +34055,8 @@ var StorageFileApi = class extends BaseApiClient {
34055
34055
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
34056
34056
  * - For React Native, using either `Blob`, `File` or `FormData` does not work as intended. Upload file using `ArrayBuffer` from base64 file data instead, see example below.
34057
34057
  */
34058
- async upload(path4, fileBody, fileOptions) {
34059
- return this.uploadOrUpdate("POST", path4, fileBody, fileOptions);
34058
+ async upload(path5, fileBody, fileOptions) {
34059
+ return this.uploadOrUpdate("POST", path5, fileBody, fileOptions);
34060
34060
  }
34061
34061
  /**
34062
34062
  * Upload a file with a token generated from `createSignedUploadUrl`.
@@ -34096,9 +34096,9 @@ var StorageFileApi = class extends BaseApiClient {
34096
34096
  * - `objects` table permissions: none
34097
34097
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
34098
34098
  */
34099
- async uploadToSignedUrl(path4, token, fileBody, fileOptions) {
34099
+ async uploadToSignedUrl(path5, token, fileBody, fileOptions) {
34100
34100
  var _this3 = this;
34101
- const cleanPath = _this3._removeEmptyFolders(path4);
34101
+ const cleanPath = _this3._removeEmptyFolders(path5);
34102
34102
  const _path = _this3._getFinalPath(cleanPath);
34103
34103
  const url2 = new URL(_this3.url + `/object/upload/sign/${_path}`);
34104
34104
  url2.searchParams.set("token", token);
@@ -34167,10 +34167,10 @@ var StorageFileApi = class extends BaseApiClient {
34167
34167
  * - `objects` table permissions: `insert`
34168
34168
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
34169
34169
  */
34170
- async createSignedUploadUrl(path4, options) {
34170
+ async createSignedUploadUrl(path5, options) {
34171
34171
  var _this4 = this;
34172
34172
  return _this4.handleOperation(async () => {
34173
- let _path = _this4._getFinalPath(path4);
34173
+ let _path = _this4._getFinalPath(path5);
34174
34174
  const headers = _objectSpread22({}, _this4.headers);
34175
34175
  if (options === null || options === void 0 ? void 0 : options.upsert) headers["x-upsert"] = "true";
34176
34176
  const data = await post(_this4.fetch, `${_this4.url}/object/upload/sign/${_path}`, {}, { headers });
@@ -34179,7 +34179,7 @@ var StorageFileApi = class extends BaseApiClient {
34179
34179
  if (!token) throw new StorageError("No token returned by API");
34180
34180
  return {
34181
34181
  signedUrl: url2.toString(),
34182
- path: path4,
34182
+ path: path5,
34183
34183
  token
34184
34184
  };
34185
34185
  });
@@ -34239,8 +34239,8 @@ var StorageFileApi = class extends BaseApiClient {
34239
34239
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
34240
34240
  * - For React Native, using either `Blob`, `File` or `FormData` does not work as intended. Update file using `ArrayBuffer` from base64 file data instead, see example below.
34241
34241
  */
34242
- async update(path4, fileBody, fileOptions) {
34243
- return this.uploadOrUpdate("PUT", path4, fileBody, fileOptions);
34242
+ async update(path5, fileBody, fileOptions) {
34243
+ return this.uploadOrUpdate("PUT", path5, fileBody, fileOptions);
34244
34244
  }
34245
34245
  /**
34246
34246
  * Moves an existing file to a new path in the same bucket.
@@ -34391,10 +34391,10 @@ var StorageFileApi = class extends BaseApiClient {
34391
34391
  * - `objects` table permissions: `select`
34392
34392
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
34393
34393
  */
34394
- async createSignedUrl(path4, expiresIn, options) {
34394
+ async createSignedUrl(path5, expiresIn, options) {
34395
34395
  var _this8 = this;
34396
34396
  return _this8.handleOperation(async () => {
34397
- let _path = _this8._getFinalPath(path4);
34397
+ let _path = _this8._getFinalPath(path5);
34398
34398
  const hasTransform = typeof (options === null || options === void 0 ? void 0 : options.transform) === "object" && options.transform !== null && Object.keys(options.transform).length > 0;
34399
34399
  let data = await post(_this8.fetch, `${_this8.url}/object/sign/${_path}`, _objectSpread22({ expiresIn }, hasTransform ? { transform: options.transform } : {}), { headers: _this8.headers });
34400
34400
  const query = new URLSearchParams();
@@ -34530,13 +34530,13 @@ var StorageFileApi = class extends BaseApiClient {
34530
34530
  * - `objects` table permissions: `select`
34531
34531
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
34532
34532
  */
34533
- download(path4, options, parameters) {
34533
+ download(path5, options, parameters) {
34534
34534
  const renderPath = typeof (options === null || options === void 0 ? void 0 : options.transform) === "object" && options.transform !== null && Object.keys(options.transform).length > 0 ? "render/image/authenticated" : "object";
34535
34535
  const query = new URLSearchParams();
34536
34536
  if (options === null || options === void 0 ? void 0 : options.transform) this.applyTransformOptsToQuery(query, options.transform);
34537
34537
  if ((options === null || options === void 0 ? void 0 : options.cacheNonce) != null) query.set("cacheNonce", String(options.cacheNonce));
34538
34538
  const queryString = query.toString();
34539
- const _path = this._getFinalPath(path4);
34539
+ const _path = this._getFinalPath(path5);
34540
34540
  const downloadFn = () => get(this.fetch, `${this.url}/${renderPath}/${_path}${queryString ? `?${queryString}` : ""}`, {
34541
34541
  headers: this.headers,
34542
34542
  noResolveJson: true
@@ -34567,9 +34567,9 @@ var StorageFileApi = class extends BaseApiClient {
34567
34567
  * }
34568
34568
  * ```
34569
34569
  */
34570
- async info(path4) {
34570
+ async info(path5) {
34571
34571
  var _this10 = this;
34572
- const _path = _this10._getFinalPath(path4);
34572
+ const _path = _this10._getFinalPath(path5);
34573
34573
  return _this10.handleOperation(async () => {
34574
34574
  return recursiveToCamel(await get(_this10.fetch, `${_this10.url}/object/info/${_path}`, { headers: _this10.headers }));
34575
34575
  });
@@ -34590,9 +34590,9 @@ var StorageFileApi = class extends BaseApiClient {
34590
34590
  * .exists('folder/avatar1.png')
34591
34591
  * ```
34592
34592
  */
34593
- async exists(path4) {
34593
+ async exists(path5) {
34594
34594
  var _this11 = this;
34595
- const _path = _this11._getFinalPath(path4);
34595
+ const _path = _this11._getFinalPath(path5);
34596
34596
  try {
34597
34597
  await head(_this11.fetch, `${_this11.url}/object/${_path}`, { headers: _this11.headers });
34598
34598
  return {
@@ -34671,8 +34671,8 @@ var StorageFileApi = class extends BaseApiClient {
34671
34671
  * - `objects` table permissions: none
34672
34672
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
34673
34673
  */
34674
- getPublicUrl(path4, options) {
34675
- const _path = this._getFinalPath(path4);
34674
+ getPublicUrl(path5, options) {
34675
+ const _path = this._getFinalPath(path5);
34676
34676
  const query = new URLSearchParams();
34677
34677
  if (options === null || options === void 0 ? void 0 : options.download) query.set("download", options.download === true ? "" : options.download);
34678
34678
  if (options === null || options === void 0 ? void 0 : options.transform) this.applyTransformOptsToQuery(query, options.transform);
@@ -34811,10 +34811,10 @@ var StorageFileApi = class extends BaseApiClient {
34811
34811
  * - `objects` table permissions: `select`
34812
34812
  * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
34813
34813
  */
34814
- async list(path4, options, parameters) {
34814
+ async list(path5, options, parameters) {
34815
34815
  var _this13 = this;
34816
34816
  return _this13.handleOperation(async () => {
34817
- const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix: path4 || "" });
34817
+ const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix: path5 || "" });
34818
34818
  return await post(_this13.fetch, `${_this13.url}/object/list/${_this13.bucketId}`, body, { headers: _this13.headers }, parameters);
34819
34819
  });
34820
34820
  }
@@ -34879,11 +34879,11 @@ var StorageFileApi = class extends BaseApiClient {
34879
34879
  if (typeof Buffer !== "undefined") return Buffer.from(data).toString("base64");
34880
34880
  return btoa(data);
34881
34881
  }
34882
- _getFinalPath(path4) {
34883
- return `${this.bucketId}/${path4.replace(/^\/+/, "")}`;
34882
+ _getFinalPath(path5) {
34883
+ return `${this.bucketId}/${path5.replace(/^\/+/, "")}`;
34884
34884
  }
34885
- _removeEmptyFolders(path4) {
34886
- return path4.replace(/^\/|\/$/g, "").replace(/\/+/g, "/");
34885
+ _removeEmptyFolders(path5) {
34886
+ return path5.replace(/^\/|\/$/g, "").replace(/\/+/g, "/");
34887
34887
  }
34888
34888
  /** Modifies the `query`, appending values the from `transform` */
34889
34889
  applyTransformOptsToQuery(query, transform2) {
@@ -37273,17 +37273,30 @@ import { constants as fsConstants } from "node:fs";
37273
37273
  import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
37274
37274
  import { homedir } from "node:os";
37275
37275
  import path from "node:path";
37276
- import { promisify as promisify2 } from "node:util";
37277
- var execFile2 = promisify2(execFileCallback);
37278
37276
  var sessionSchema = external_exports.object({
37279
37277
  accessToken: external_exports.string().min(1),
37280
37278
  refreshToken: external_exports.string().min(1).optional(),
37281
37279
  expiresAt: external_exports.string().datetime({ offset: true }).optional()
37282
37280
  }).strict();
37281
+ var runExecFile = (file2, args, options) => new Promise((resolve2, reject) => {
37282
+ const child = execFileCallback(file2, [...args], (error51, stdout, stderr) => {
37283
+ if (error51) {
37284
+ reject(error51);
37285
+ return;
37286
+ }
37287
+ resolve2({ stdout, stderr });
37288
+ });
37289
+ const { stdin } = child;
37290
+ if (stdin) {
37291
+ stdin.on("error", () => {
37292
+ });
37293
+ stdin.end(options?.input ?? "");
37294
+ }
37295
+ });
37283
37296
  var serviceName = `${cliBrand.binName}-cli`;
37284
37297
  var accountName = "session";
37285
37298
  var KeychainTokenStore = class {
37286
- constructor(run2 = execFile2) {
37299
+ constructor(run2 = runExecFile) {
37287
37300
  this.run = run2;
37288
37301
  }
37289
37302
  run;
@@ -37304,16 +37317,14 @@ var KeychainTokenStore = class {
37304
37317
  }
37305
37318
  async write(session) {
37306
37319
  sessionSchema.parse(session);
37307
- await this.run("security", [
37308
- "add-generic-password",
37309
- "-U",
37310
- "-s",
37311
- serviceName,
37312
- "-a",
37313
- accountName,
37314
- "-w",
37315
- JSON.stringify(session)
37316
- ]);
37320
+ const secret = JSON.stringify(session);
37321
+ await this.run(
37322
+ "security",
37323
+ ["add-generic-password", "-U", "-s", serviceName, "-a", accountName, "-w"],
37324
+ { input: `${secret}
37325
+ ${secret}
37326
+ ` }
37327
+ );
37317
37328
  }
37318
37329
  async clear() {
37319
37330
  try {
@@ -37326,6 +37337,122 @@ var KeychainTokenStore = class {
37326
37337
  return "macOS Keychain";
37327
37338
  }
37328
37339
  };
37340
+ var DarwinTokenStore = class extends KeychainTokenStore {
37341
+ constructor(run2 = runExecFile, filePath) {
37342
+ super(run2);
37343
+ this.filePath = filePath;
37344
+ }
37345
+ filePath;
37346
+ backend = null;
37347
+ fileStore = null;
37348
+ async read() {
37349
+ return await this.selectBackend() === "file" ? this.file().read() : super.read();
37350
+ }
37351
+ async write(session) {
37352
+ if (await this.selectBackend() === "file") {
37353
+ await this.file().write(session);
37354
+ return;
37355
+ }
37356
+ await super.write(session);
37357
+ }
37358
+ async clear() {
37359
+ if (await this.selectBackend() === "file") {
37360
+ await this.file().clear();
37361
+ return;
37362
+ }
37363
+ await super.clear();
37364
+ }
37365
+ describe() {
37366
+ return this.backend === "file" ? "owner-only file store (no usable macOS keychain)" : super.describe();
37367
+ }
37368
+ file() {
37369
+ return this.fileStore ??= new FileTokenStore(this.filePath);
37370
+ }
37371
+ async selectBackend() {
37372
+ if (this.backend) {
37373
+ return this.backend;
37374
+ }
37375
+ try {
37376
+ await this.run("security", ["find-generic-password", "-s", serviceName, "-a", accountName, "-w"]);
37377
+ this.backend = "keychain";
37378
+ } catch (error51) {
37379
+ this.backend = isKeychainUnusableError(error51) ? "file" : "keychain";
37380
+ }
37381
+ return this.backend;
37382
+ }
37383
+ };
37384
+ var WindowsCredentialTokenStore = class {
37385
+ constructor(run2 = runExecFile) {
37386
+ this.run = run2;
37387
+ }
37388
+ run;
37389
+ async read() {
37390
+ try {
37391
+ const { stdout } = await this.run("powershell", powershellArgs(windowsReadScript(windowsCredentialTarget)));
37392
+ const trimmed = stdout.trim();
37393
+ if (trimmed === "") {
37394
+ return null;
37395
+ }
37396
+ return sessionSchema.parse(JSON.parse(trimmed));
37397
+ } catch {
37398
+ return null;
37399
+ }
37400
+ }
37401
+ async write(session) {
37402
+ sessionSchema.parse(session);
37403
+ await this.run(
37404
+ "powershell",
37405
+ powershellArgs(windowsWriteScript(windowsCredentialTarget)),
37406
+ { input: JSON.stringify(session) }
37407
+ );
37408
+ }
37409
+ async clear() {
37410
+ try {
37411
+ await this.run("powershell", powershellArgs(windowsClearScript(windowsCredentialTarget)));
37412
+ } catch {
37413
+ return;
37414
+ }
37415
+ }
37416
+ describe() {
37417
+ return "Windows Credential Manager";
37418
+ }
37419
+ };
37420
+ var SecretServiceTokenStore = class {
37421
+ constructor(run2 = runExecFile) {
37422
+ this.run = run2;
37423
+ }
37424
+ run;
37425
+ async read() {
37426
+ try {
37427
+ const { stdout } = await this.run("secret-tool", ["lookup", "service", serviceName, "account", accountName]);
37428
+ const trimmed = stdout.trim();
37429
+ if (trimmed === "") {
37430
+ return null;
37431
+ }
37432
+ return sessionSchema.parse(JSON.parse(trimmed));
37433
+ } catch {
37434
+ return null;
37435
+ }
37436
+ }
37437
+ async write(session) {
37438
+ sessionSchema.parse(session);
37439
+ await this.run(
37440
+ "secret-tool",
37441
+ ["store", `--label=${cliBrand.name} CLI session`, "service", serviceName, "account", accountName],
37442
+ { input: JSON.stringify(session) }
37443
+ );
37444
+ }
37445
+ async clear() {
37446
+ try {
37447
+ await this.run("secret-tool", ["clear", "service", serviceName, "account", accountName]);
37448
+ } catch {
37449
+ return;
37450
+ }
37451
+ }
37452
+ describe() {
37453
+ return "Secret Service";
37454
+ }
37455
+ };
37329
37456
  var FileTokenStore = class {
37330
37457
  constructor(filePath = defaultTokenPath()) {
37331
37458
  this.filePath = filePath;
@@ -37366,6 +37493,7 @@ var FileTokenStore = class {
37366
37493
  function createTokenStore(options = {}) {
37367
37494
  const env = options.env ?? process.env;
37368
37495
  const allowFileStoreKey = brandEnvKey2("CLI_ALLOW_FILE_TOKEN_STORE");
37496
+ const tokenPath = env[brandEnvKey2("CLI_TOKEN_PATH")];
37369
37497
  if (env[allowFileStoreKey] === "1") {
37370
37498
  const warning = [
37371
37499
  "WARNING: using the development-only plain-file CLI token store.",
@@ -37373,17 +37501,23 @@ function createTokenStore(options = {}) {
37373
37501
  ].join(" ");
37374
37502
  (options.stderr ?? process.stderr).write(`${warning}
37375
37503
  `);
37376
- return new FileTokenStore(env[brandEnvKey2("CLI_TOKEN_PATH")]);
37504
+ return new FileTokenStore(tokenPath);
37377
37505
  }
37378
37506
  const platform = options.platform ?? process.platform;
37379
37507
  if (platform === "darwin") {
37380
- return new KeychainTokenStore(options.execFile);
37508
+ return new DarwinTokenStore(options.execFile, tokenPath);
37509
+ }
37510
+ if (platform === "win32") {
37511
+ return new WindowsCredentialTokenStore(options.execFile);
37512
+ }
37513
+ if (platform === "linux" && env[brandEnvKey2("CLI_USE_SECRET_SERVICE")] === "1") {
37514
+ return new SecretServiceTokenStore(options.execFile);
37381
37515
  }
37382
37516
  throw new Error(
37383
37517
  [
37384
- "No OS credential store is available (the CLI uses the macOS Keychain by default; there is no Windows or Linux keychain integration).",
37385
- `For headless Linux, CI runners, or any no-keychain host, export ${allowFileStoreKey}=1 (set it for every invocation) to opt into the development-only plain-file token store.`,
37386
- "It is not encrypted: it writes an owner-only file (directory 0700, file 0600) holding only CLI session tokens \u2014 never tenant secrets, API keys, or vault refs."
37518
+ "No OS credential store is available (the CLI uses the macOS Keychain by default, Windows Credential Manager on Windows, and the Linux Secret Service when enabled).",
37519
+ `For headless Linux, export ${brandEnvKey2("CLI_USE_SECRET_SERVICE")}=1 to use the Secret Service, or ${allowFileStoreKey}=1 (set it for every invocation) to opt into the development-only plain-file token store.`,
37520
+ "The file store is not encrypted: it writes an owner-only file (directory 0700, file 0600) holding only CLI session tokens \u2014 never tenant secrets, API keys, or vault refs."
37387
37521
  ].join(" ")
37388
37522
  );
37389
37523
  }
@@ -37393,6 +37527,105 @@ function defaultTokenPath() {
37393
37527
  function brandEnvKey2(suffix) {
37394
37528
  return `${cliBrand.envPrefix}_${suffix}`;
37395
37529
  }
37530
+ function isKeychainUnusableError(error51) {
37531
+ if (isKeychainItemMissing(error51)) {
37532
+ return false;
37533
+ }
37534
+ return true;
37535
+ }
37536
+ function isKeychainItemMissing(error51) {
37537
+ if (!error51 || typeof error51 !== "object") {
37538
+ return false;
37539
+ }
37540
+ if (error51.code === 44) {
37541
+ return true;
37542
+ }
37543
+ return /could not be found|SecKeychainSearchCopyNext/i.test(errorText(error51));
37544
+ }
37545
+ function errorText(error51) {
37546
+ if (!error51 || typeof error51 !== "object") {
37547
+ return String(error51 ?? "");
37548
+ }
37549
+ const record2 = error51;
37550
+ const parts = [];
37551
+ for (const key of ["message", "stderr", "stdout"]) {
37552
+ const value = record2[key];
37553
+ if (typeof value === "string") {
37554
+ parts.push(value);
37555
+ }
37556
+ }
37557
+ return parts.join("\n");
37558
+ }
37559
+ var windowsCredentialTarget = `${serviceName}/${accountName}`;
37560
+ function powershellArgs(script) {
37561
+ return ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script];
37562
+ }
37563
+ function psQuote(value) {
37564
+ return `'${value.replace(/'/g, "''")}'`;
37565
+ }
37566
+ var WINDOWS_CRED_MODULE = [
37567
+ "$ErrorActionPreference = 'Stop'",
37568
+ "Add-Type -TypeDefinition @'",
37569
+ "using System;",
37570
+ "using System.Runtime.InteropServices;",
37571
+ "public static class RostCred {",
37572
+ " [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]",
37573
+ " public struct CREDENTIAL {",
37574
+ " public uint Flags; public uint Type;",
37575
+ " [MarshalAs(UnmanagedType.LPWStr)] public string TargetName;",
37576
+ " [MarshalAs(UnmanagedType.LPWStr)] public string Comment;",
37577
+ " public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;",
37578
+ " public uint CredentialBlobSize; public IntPtr CredentialBlob;",
37579
+ " public uint Persist; public uint AttributeCount; public IntPtr Attributes;",
37580
+ " [MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;",
37581
+ " [MarshalAs(UnmanagedType.LPWStr)] public string UserName;",
37582
+ " }",
37583
+ ' [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]',
37584
+ " public static extern bool CredWriteW(ref CREDENTIAL cred, uint flags);",
37585
+ ' [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]',
37586
+ " public static extern bool CredReadW(string target, uint type, uint flags, out IntPtr cred);",
37587
+ ' [DllImport("advapi32.dll")] public static extern void CredFree(IntPtr cred);',
37588
+ ' [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]',
37589
+ " public static extern bool CredDeleteW(string target, uint type, uint flags);",
37590
+ "}",
37591
+ "'@"
37592
+ ].join("\n");
37593
+ function windowsWriteScript(target) {
37594
+ return [
37595
+ WINDOWS_CRED_MODULE,
37596
+ "$secret = [Console]::In.ReadToEnd()",
37597
+ "$bytes = [System.Text.Encoding]::Unicode.GetBytes($secret)",
37598
+ "$blob = [Runtime.InteropServices.Marshal]::AllocHGlobal($bytes.Length)",
37599
+ "[Runtime.InteropServices.Marshal]::Copy($bytes, 0, $blob, $bytes.Length)",
37600
+ "$cred = New-Object RostCred+CREDENTIAL",
37601
+ "$cred.Type = 1",
37602
+ `$cred.TargetName = ${psQuote(target)}`,
37603
+ "$cred.UserName = 'session'",
37604
+ "$cred.CredentialBlobSize = $bytes.Length",
37605
+ "$cred.CredentialBlob = $blob",
37606
+ "$cred.Persist = 2",
37607
+ "$ok = [RostCred]::CredWriteW([ref]$cred, 0)",
37608
+ "[Runtime.InteropServices.Marshal]::FreeHGlobal($blob)",
37609
+ "if (-not $ok) { exit 1 }"
37610
+ ].join("\n");
37611
+ }
37612
+ function windowsReadScript(target) {
37613
+ return [
37614
+ WINDOWS_CRED_MODULE,
37615
+ "$ptr = [IntPtr]::Zero",
37616
+ `if (-not [RostCred]::CredReadW(${psQuote(target)}, 1, 0, [ref]$ptr)) { exit 0 }`,
37617
+ "$cred = [Runtime.InteropServices.Marshal]::PtrToStructure($ptr, [Type][RostCred+CREDENTIAL])",
37618
+ "$secret = [Runtime.InteropServices.Marshal]::PtrToStringUni($cred.CredentialBlob, [int]($cred.CredentialBlobSize / 2))",
37619
+ "[RostCred]::CredFree($ptr)",
37620
+ "[Console]::Out.Write($secret)"
37621
+ ].join("\n");
37622
+ }
37623
+ function windowsClearScript(target) {
37624
+ return [
37625
+ WINDOWS_CRED_MODULE,
37626
+ `[void][RostCred]::CredDeleteW(${psQuote(target)}, 1, 0)`
37627
+ ].join("\n");
37628
+ }
37396
37629
 
37397
37630
  // ../../packages/prompts/src/index.ts
37398
37631
  import { readFileSync } from "node:fs";
@@ -37569,10 +37802,10 @@ var util;
37569
37802
  function assertIs2(_arg) {
37570
37803
  }
37571
37804
  util2.assertIs = assertIs2;
37572
- function assertNever2(_x) {
37805
+ function assertNever3(_x) {
37573
37806
  throw new Error();
37574
37807
  }
37575
- util2.assertNever = assertNever2;
37808
+ util2.assertNever = assertNever3;
37576
37809
  util2.arrayToEnum = (items) => {
37577
37810
  const obj = {};
37578
37811
  for (const item of items) {
@@ -37927,8 +38160,8 @@ function getErrorMap2() {
37927
38160
 
37928
38161
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
37929
38162
  var makeIssue = (params) => {
37930
- const { data, path: path4, errorMaps, issueData } = params;
37931
- const fullPath = [...path4, ...issueData.path || []];
38163
+ const { data, path: path5, errorMaps, issueData } = params;
38164
+ const fullPath = [...path5, ...issueData.path || []];
37932
38165
  const fullIssue = {
37933
38166
  ...issueData,
37934
38167
  path: fullPath
@@ -38044,11 +38277,11 @@ var errorUtil;
38044
38277
 
38045
38278
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
38046
38279
  var ParseInputLazyPath = class {
38047
- constructor(parent, value, path4, key) {
38280
+ constructor(parent, value, path5, key) {
38048
38281
  this._cachedPath = [];
38049
38282
  this.parent = parent;
38050
38283
  this.data = value;
38051
- this._path = path4;
38284
+ this._path = path5;
38052
38285
  this._key = key;
38053
38286
  }
38054
38287
  get path() {
@@ -41638,6 +41871,78 @@ The Compass is drafted, then activated by a human through supersession.
41638
41871
  ## When to stop for confirmation
41639
41872
 
41640
41873
  \`onboarding.finish\`, \`compass.approve_version\`, \`compass.reject_draft\`, and \`compass.set\` are \`human_required\`. \`onboarding.advance_step\`, \`onboarding.create_invite\`, \`onboarding.attach_reference\`, \`compass.answer_gap\`, and drafting a Compass are \`none\` \u2014 none of them returns a pending confirmation. \`none\` is not the same as "agent-callable", though: \`onboarding.create_invite\` and \`compass.answer_gap\` record the human who ran them, so they run as a person, not from an agent session, while advancing a step, attaching a reference, and drafting a Compass are safe for an agent. Approving a version (a supersession) is a human act. The agent drafts and surfaces the approve link. See the confirmations guide.`
41874
+ },
41875
+ {
41876
+ slug: "aicos-chat-guide",
41877
+ order: 15,
41878
+ title: "AICOS chat guide",
41879
+ summary: "How the AI Chief of Staff chat works in the authenticated app shell and what it can safely do today.",
41880
+ version: "2026-07-02.3",
41881
+ public: true,
41882
+ audiences: ["human", "in_app_agent"],
41883
+ stages: ["company_setup", "operating_rhythm"],
41884
+ relatedCommandIds: ["confirmation.approve", "onboarding.status", "graph.get", "seat.get"],
41885
+ legal: {
41886
+ publicRisk: "low",
41887
+ notes: [
41888
+ "Describes the authenticated AICOS chat surface without promising autonomous action.",
41889
+ "Keeps authority, lane, and confirmation language aligned with the platform governance model."
41890
+ ]
41891
+ },
41892
+ sources: [
41893
+ {
41894
+ label: "AICOS Company Chief of Staff plan",
41895
+ internalPath: "docs/plans/2026-07-01-003-feat-aicos-company-chief-of-staff-plan.md",
41896
+ note: "Defines the AICOS product contract, lane portability, and governance boundaries."
41897
+ },
41898
+ {
41899
+ label: "MVP implementation plan",
41900
+ internalPath: "docs/specs/MVP-implementation-plan.md",
41901
+ note: "FR-10.6 records the AICOS extension and chat harness rollout."
41902
+ }
41903
+ ],
41904
+ body: `# AICOS chat guide
41905
+
41906
+ AICOS is the AI Chief of Staff for an authenticated {{brand}} tenant. It is a governed agent seat in the Responsibility Graph, not an anonymous public chatbot and not a hidden superuser.
41907
+
41908
+ ## Where AICOS appears
41909
+
41910
+ In the authenticated app shell, AICOS opens from the floating assistant control. The panel is available across normal app pages and keeps the same thread while you move through the product. Full-bleed public, login, onboarding, docs, invite, and demo routes do not receive the authenticated shell.
41911
+
41912
+ Each thread has a purpose:
41913
+
41914
+ - **Onboarding** \u2014 setup help and page-aware setup context.
41915
+ - **Seat** \u2014 seat and agent setup context.
41916
+ - **General** \u2014 company operating help.
41917
+
41918
+ You can reopen prior threads or start a new one. Session history is stored in tenant-scoped conversation tables so chat history is not reconstructed from analytics, events, or run logs.
41919
+
41920
+ ## What context AICOS receives
41921
+
41922
+ Each turn carries server-normalized page context: route template, current path, safe search state, selected seat or object ids when authorized, and onboarding state when relevant. The server derives tenant and user authority from the authenticated session. Client-supplied tenant context is never accepted as authority.
41923
+
41924
+ The current chat harness shows the effective lane and model. The initial lane is cloud, with a deterministic harness response while live model execution, action execution, and runner execution waves land. The contract is intentionally backend-neutral: future cloud and runner turns use the same sessions, messages, lane labels, sources, and linked artifacts.
41925
+
41926
+ Model behavior is now explicit in the AICOS session contract. A thread can run in **Auto** or use a pinned compatible model from the guided catalog. Auto chooses a fast tier for short low-risk turns, the balanced default for normal operating help, and a higher-reasoning tier for onboarding synthesis or large context. The panel shows the effective model, disables lower-context choices once the active thread outgrows them, and tells the user to start a new session for those models. Session context is estimated and compacted into a durable summary when it crosses the configured threshold; raw messages remain in history, but future model calls can use the summary rather than resending the whole thread. Platform allowance and BYOK state are checked before routing; an exhausted included allowance keeps AICOS in guided mode and points the user toward BYOK/manual continuation. Because the DER-1068 implementation still uses the deterministic harness, it does not create fake llm_calls rows or settle fake allowance usage; reservation/settlement belongs to the real provider call path.
41927
+
41928
+ AICOS grounds answers from two source classes before producing user-visible setup or operating guidance:
41929
+
41930
+ - **Reference documents** \u2014 public {{brand}} guides such as this chat guide, onboarding guidance, the Responsibility Graph playbook, tool-access guidance, and confirmation guidance. AICOS retrieves the narrowest relevant guide before setup, Charter, Signal, Friction, Cascade, Sync, tool-access, or onboarding advice.
41931
+ - **Tenant read models** \u2014 bounded summaries of tenant operating state, including onboarding status, graph/setup readiness, Compass/Charter readiness, operating health, agent/system health, approvals/inbox, and selected Seat context. These read models expose counts and short observations rather than raw database dumps.
41932
+
41933
+ Assistant messages can carry source counts and persisted source metadata so a thread can later show which reference docs or read models informed the answer.
41934
+
41935
+ ## Governance boundary
41936
+
41937
+ AICOS can preserve context and explain what it is ready to do, but durable operating changes still use the same {{brand}} controls:
41938
+
41939
+ - Tenant isolation is enforced by RLS and server-side tenant context.
41940
+ - Raw secrets, vault refs, hidden tool arguments, storage refs, provider payloads, agent custom prompts, and secret-shaped text are not stored in chat history or read-model context.
41941
+ - Durable decisions remain human decisions.
41942
+ - Action execution must route through command guards and confirmation when required.
41943
+ - Expanding authority, approving Charters or Compass versions, going live, credentials, external sends, money, destructive operations, and policy changes stay human-gated.
41944
+
41945
+ Treat AICOS as a coordinator over the operating system. It can become more useful as model grounding, read tools, draft actions, and runner execution are enabled, but it does not bypass the Responsibility Graph, Seat Steward accountability, command guards, or confirmations.`
41641
41946
  },
41642
41947
  {
41643
41948
  slug: "responsibility-graph-playbook",
@@ -42157,7 +42462,7 @@ Decisions should be recorded as human decisions. Handoffs should attach to seats
42157
42462
  order: 45,
42158
42463
  title: "How agents work",
42159
42464
  summary: "How {{brand}} agents operate inside seats, use Charters, report work, and escalate beyond authority.",
42160
- version: "2026-06-30.1",
42465
+ version: "2026-07-02.1",
42161
42466
  public: true,
42162
42467
  audiences: ["human", "cli", "mcp", "in_app_agent"],
42163
42468
  stages: ["staffing", "operating_rhythm"],
@@ -42255,7 +42560,7 @@ Beyond each seat's own manifest, the company can set one ceiling on how much an
42255
42560
 
42256
42561
  ## What humans should review
42257
42562
 
42258
- The Agents page shows fleet health first: live state, work evidence, failed runs, open held actions, cost, and the next operator action. Work evidence distinguishes fresh real runs, stale runs, no completed run yet, queued or running work orders, and recent failures. A failure link opens the failed run itself; queued or stale rows route the operator to the seat's agent operations instead of pretending there is a hidden run to inspect. The page also shows configuration completeness for each staffed agent seat: Steward chain, lane or runtime substrate, vaulted credentials, granted tools, model config, sandbox dry run, and go-live state, each with a remediation link when incomplete. A seat's Trust Card drills into the same facts: run history, one-run detail, tool-call outcomes, product-visible errors, transcript references, loaded Skill versions, and held confirmations or escalations. Open confirmations stay visible until they are decided or expire, even when they are older than the default activity window. Approving or rejecting a held confirmation from the seat page is scoped to that seat; rejection can include a short reason. For a live agent, the seat page's agent operations panel can queue one immediate governed run through the same \`agent.run_now\` command path used by CLI and MCP; it does not expand authority or change the saved schedule.
42563
+ The Agents page shows fleet health first: live state, work evidence, failed runs, open held actions, cost, and the next operator action. Work evidence distinguishes fresh real runs, stale runs, no completed run yet, queued or running work orders, and recent failures. A failure link opens the failed run itself; queued or stale rows route the operator to the seat's agent operations instead of pretending there is a hidden run to inspect. The page also shows configuration completeness for each staffed agent seat: Steward chain, lane or runtime substrate, vaulted credentials, granted tools, model config, sandbox dry run, and go-live state, each with a remediation link when incomplete. A seat's Trust Card drills into the same facts: run history, one-run detail, tool-call outcomes, product-visible errors, transcript references, loaded Skill versions, and held confirmations or escalations. Open confirmations stay visible until they are decided or expire, even when they are older than the default activity window. Approving or rejecting a held confirmation from the seat page is scoped to that seat; rejection can include a short reason. For a live agent, the seat page's agent operations panel can queue one immediate governed run through the same \`agent.run_now\` command path used by CLI and MCP; it does not expand authority or change the saved schedule. Seat pages render legacy Charter manifest scope text with customer-safe labels when old setup data used internal placeholder wording.
42259
42564
 
42260
42565
  Review the first dry runs, fleet overview, tool-call audit rows, escalations, deliverables, and Signal impact. A seat's completed-work area combines explicit deliverables, work-log evidence, and successful run summaries into one operator-facing trail with safe links labelled as internal, Linear, GitHub, or external. The fleet view at \`/agents\` shows every staffed agent seat at a glance; the agent-native equivalents are \`{{cli}} command agent.list_fleet --json '{}'\` / \`rost_list_agent_fleet\` for the compact overview, and \`{{cli}} agent fleet-digest --json\` / \`agent.fleet_digest\` / \`rost_get_dogfood_fleet_health_digest\` for the daily dogfood evidence bundle. The digest returns live/idle state, 24h/7d turns, 7d/30d spend, recent failed runs, unresolved product errors, failed notifications, and a next action per seat.
42261
42566
 
@@ -42310,7 +42615,7 @@ Connecting a tool is a human-controlled step. The agent can recommend a tool, ex
42310
42615
 
42311
42616
  ## Generic REST connector
42312
42617
 
42313
- For an API with no dedicated connector, the generic REST tool lets a seat read an HTTPS endpoint with a credential you paste through the vault \u2014 no {{brand}}-owned app. The agent-facing tool is GET-only. It is escalate-by-default: the agent may only call a host and explicit path prefix a steward has signed onto the allowlist; any other host or path is refused and escalated, with no request made. Mutating methods and request bodies fail before credential binding. A path prefix of \`/\` means the steward intentionally approved every path. The connector sets the Authorization header from the vaulted credential itself \u2014 the agent never sees the token, and the secret is redacted from the response before it reaches the agent, the audit row, or the logs. The token is only ever sent over HTTPS, only to the signed host and scoped path, and a redirect is never followed \u2014 so an allowlisted endpoint cannot bounce the call (and the token) to another host. A sandbox dry run of a REST tool makes no real request.
42618
+ For an API with no dedicated connector, the generic REST tool lets a seat read an HTTPS endpoint with a credential you paste through the vault \u2014 no {{brand}}-owned app. The agent-facing tool is GET-only. It is escalate-by-default: the agent may only call a host and explicit path prefix a steward has signed onto the allowlist; any other host or path is refused and escalated, with no request made. Mutating methods and request bodies fail before credential binding. A path prefix of \`/\` means the steward intentionally approved every path. The connector sets the Authorization header from the vaulted credential itself \u2014 the agent never sees the token, and the secret is redacted from the response before it reaches the agent, the audit row, or the logs. The token is only ever sent over HTTPS, only to the signed host and scoped path, and a redirect is never followed \u2014 so an allowlisted endpoint cannot bounce the call (and the token) to another host. Signal-source pulls also resolve the host at execution time, reject private or reserved addresses, and pin the outbound socket to the validated address so DNS rebinding cannot move the request after approval. A sandbox dry run of a REST tool makes no real request.
42314
42619
 
42315
42620
  ## Slack post connector
42316
42621
 
@@ -42359,7 +42664,7 @@ There is exactly one way to give a connected tool its credential, and it is the
42359
42664
  order: 47,
42360
42665
  title: "Available tools guide",
42361
42666
  summary: "How to think about tool categories available to seats and what each category should be used for.",
42362
- version: "2026-07-02.1",
42667
+ version: "2026-06-30.1",
42363
42668
  public: true,
42364
42669
  audiences: ["human", "cli", "mcp", "in_app_agent"],
42365
42670
  stages: ["staffing"],
@@ -42412,7 +42717,7 @@ External connectors are being rolled out provider by provider, conservatively (r
42412
42717
  order: 48,
42413
42718
  title: "CLI and MCP installation guide",
42414
42719
  summary: "Install the public CLI, register remote token-backed MCP clients, and find the full command and tool catalog.",
42415
- version: "2026-07-02.3",
42720
+ version: "2026-07-02.6",
42416
42721
  public: true,
42417
42722
  audiences: ["human", "cli", "mcp", "in_app_agent"],
42418
42723
  stages: ["company_setup", "staffing"],
@@ -42869,12 +43174,13 @@ The signed-in app exposes the same Skill command surface under **Skills** in the
42869
43174
  | \`{{cli}} error list|resolve|supersede\` | \`error_log.list\`, \`error_log.resolve\`, \`error_log.supersede\` | List active, acknowledged, resolved, or superseded product error logs and let a human acknowledge, resolve, or supersede stale errors with evidence links. | Tenant | \`{{cli}} error list --resolved active --json\`; \`{{cli}} error resolve --error-log-id <id> --reason "Fixed in PR #123"\` |
42870
43175
  | \`{{cli}} sync brief|compile|complete\` | \`sync.brief.get\`, \`sync.brief.compile\`, \`sync.run.complete\` | Compile, read, and complete a weekly Sync. | Tenant | \`{{cli}} sync brief --json\` |
42871
43176
  | \`{{cli}} runner list|status|diagnose|repair|work-orders|revoke|serve\` | \`runner.list\`, \`runner.status\`, \`runner.diagnose\`, \`runner.repair\`, \`work_order.list\`, \`runner.revoke\`, runner endpoint APIs | Inspect heartbeat and execute-readiness evidence, inspect work orders, diagnose offline runners, get repair guidance, revoke a runner, or run the headless local runner loop. | Tenant | \`{{cli}} runner diagnose --runner-id <id> --json\`; \`{{cli}} runner serve --once\` |
43177
+ | \`{{cli}} runner install-service|start|stop|restart|status|logs|uninstall --name <name>\` | launchd/service manager (macOS today) + \`{{cli}} runner serve\` | Install and manage an always-on local runner service that keeps the runner online across reboots; the service shells out to \`{{cli}} runner serve --execute\` and stores no secret in the service definition, its arguments, or logs. Non-macOS emits an explicit unsupported message. | Tenant (local machine) | \`{{cli}} runner install-service --name mac-mini --execute\`; \`{{cli}} runner logs --name mac-mini\` |
42872
43178
  | \`{{cli}} notification settings|test|errors\` | \`notification.settings.get\`, \`notification.test\`, \`notification.list_errors\` | Read notification settings, send a test, and list failed deliveries with linked product error source, seat id, and run id when available. | Tenant | \`{{cli}} notification errors --limit 10 --json\` |
42873
43179
  | \`{{cli}} integration list|readiness|status|test\` / credential flow for Baserow REST connect | \`integration.connect_rest\`, \`integration.list\`, \`integration.readiness\`, \`integration.status\`, \`integration.test\` | Create a vault-backed Baserow REST integration through the credential flow, list connector metadata, read setup-readiness, read one connector's health, and run provider-specific tests without exposing credentials. | Tenant-admin for connect; tenant for reads/tests | \`{{cli}} integration readiness --provider google --json\` |
42874
43180
  | \`{{cli}} system health\` | \`system.health\` | Read the scoped system-health snapshot across agent runs, unresolved errors, Signals, Cascade setup, work loop, integrations, runners, notifications, and Sync Brief readiness. | Tenant, member, or seat token | \`{{cli}} system health --json\`; \`{{cli}} system health --seat <id> --json\` |
42875
43181
  | \`{{cli}} command usage.report|usage.snapshot\` | \`usage.report\`, \`usage.snapshot\` | Read the AI-workforce ROI report (per-period agent turns, token/run cost, human-accepted value, exceptions, approvals) from immutable usage snapshots, and freeze a period's totals into a new snapshot (insert-only, idempotent). | Tenant | \`{{cli}} command usage.report --json\`; \`{{cli}} command usage.snapshot --json\` |
42876
43182
  | \`{{cli}} settings get|update|product-learning|agent-policy|rename\` | \`settings.get\`, \`settings.update\`, \`settings.product_learning.get\`, \`settings.product_learning.update\`, \`settings.agent_policy.get\`, \`settings.agent_policy.update\`, \`tenant.rename\` | Read tenant settings, update budget caps, read or update product-learning participation, read or set the company autonomy ceiling, and rename the company. Budget, product-learning, and policy updates stop at human confirmation in non-interactive CLI/MCP sessions; company rename is owner-only and human-gated. | Tenant / Tenant-admin for updates | \`{{cli}} settings product-learning get --json\`; \`{{cli}} settings agent-policy get --json\`; \`{{cli}} settings rename --company "Acme Operations"\` |
42877
- | \`{{cli}} member invite|update|remove\` | \`member.invite\`, \`member.update\`, \`member.remove\` | Manage tenant members. | Tenant | \`{{cli}} member invite --email ops@example.com --role member\` |
43183
+ | \`{{cli}} member invite|update|remove\` | \`member.invite\`, \`member.update\`, \`member.remove\` | Manage tenant members; execution requires an owner or admin membership. | Tenant | \`{{cli}} member invite --email ops@example.com --role member\` |
42878
43184
  | \`{{cli}} agent templates|create|setup|tools|dry-run|go-live|status|run-now|fleet-digest|get-run|show\` | \`agent_template.list\`, \`agent.create_from_template\`, \`agent.create_custom\`, \`agent_setup.get\`, \`agent_setup.update\`, \`agent.configure_tools\`, \`agent.run_dry_run\`, \`agent.go_live\`, \`agent.status\`, \`agent.run_now\`, \`agent.fleet_digest\`, \`agent.get_run\`, \`agent.show_markdown\` | Run the full agent setup and operation flow: list templates, create a draft from a template or guided custom answers (with \`--model\` and \`--effort\`), read or answer setup state, connect or decline tools, dry-run, go live, run on demand, capture the fleet-health digest, read one run's transcript/error diagnostics, and show a markdown readout. Human CLI dry-runs print the rehearsal transcript and, when present, the per-tool preview labels (\`Would run\`, \`Blocked\`, \`Escalated\`) before go-live. Create and go-live stop at human gates; the dry-run is ungated by human approval but requires a signed manifest first. | Tenant and seat | \`{{cli}} agent fleet-digest --json\` |
42879
43185
  | \`{{cli}} tools list\` | \`tool.catalog\` | List the discoverable tool catalog the builder reads (id, scope tiers, credential requirement, access policy, and execution-boundary guidance). | Tenant | \`{{cli}} tools list --json\` |
42880
43186
  | \`{{cli}} skills list|get|file|assigned|check-dependencies\` | \`skill.list\`, \`skill.get\`, \`skill.file.get\`, \`skill.assigned.list\`, \`skill.check_dependencies\` | Discover reusable Skills, read descriptors and stored package files, list approved Seat assignments, and compare required/optional tool dependencies with a Seat's signed Charter manifest. Skills are instructions, not authority. | Tenant-admin; seat-scoped reads for assigned Skills | \`{{cli}} skills list --json\`; \`{{cli}} skills file --slug invoice-review --path SKILL.md\` |
@@ -43046,9 +43352,9 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
43046
43352
  | \`rost_check_integration_readiness\` | \`integration.readiness\` | Return the connector setup checklist: OAuth env/callback, tenant connection, scopes, latest test state, external verification/CASA, handler availability, and DER-coded next actions. | Tenant | Call with \`{"provider":"google"}\`; metadata only, no secrets or vault refs. |
43047
43353
  | \`rost_get_integration_status\` | \`integration.status\` | Read one integration's metadata by provider or integration id. | Tenant | Call with \`{"provider":"google"}\` or \`{"integration_id":"<id>"}\`. |
43048
43354
  | \`rost_test_integration_connection\` | \`integration.test\` | Run the provider-specific connection test and update integration health. | Tenant | Call with \`{"provider":"google"}\`; result is bounded metadata only. |
43049
- | \`rost_invite_member\` | \`member.invite\` | Create a pending tenant invite for a human teammate. | Tenant | Call with \`email\` and \`role\`. |
43050
- | \`rost_update_member_role\` | \`member.update\` | Change a tenant member's role. | Tenant | Call with \`member_id\` and \`role\`; expect human confirmation. |
43051
- | \`rost_remove_member\` | \`member.remove\` | Remove a tenant member. | Tenant | Call with \`member_id\`; blocked if it would orphan an agent steward chain. |
43355
+ | \`rost_invite_member\` | \`member.invite\` | Create a pending tenant invite for a human teammate. | Tenant | Owner/admin only; call with \`email\` and \`role\`. |
43356
+ | \`rost_update_member_role\` | \`member.update\` | Change a tenant member's role. | Tenant | Owner/admin only; call with \`member_id\` and \`role\`; expect human confirmation. |
43357
+ | \`rost_remove_member\` | \`member.remove\` | Remove a tenant member. | Tenant | Owner/admin only; call with \`member_id\`; blocked if it would orphan an agent steward chain. |
43052
43358
  | \`rost_get_tenant_settings\` | \`settings.get\` | Read tenant operating settings and integration status. | Tenant | Call with \`{}\`; metadata only. |
43053
43359
  | \`rost_update_tenant_settings\` | \`settings.update\` | Update tenant AI budget caps. | Tenant | Call with \`soft_cap_usd\` or \`hard_cap_usd\`; expect human confirmation. |
43054
43360
  | \`rost_rename_company\` | \`tenant.rename\` | Rename the current company. | Tenant-admin | Call with \`company_name\`; expect human confirmation. |
@@ -43461,7 +43767,7 @@ Set \`ROST_SKILL_INSTALL_ROOT\` only for a sandbox or CI override. Local copies
43461
43767
  order: 55,
43462
43768
  title: "Agent reference map",
43463
43769
  summary: "Where CLI sessions, MCP clients, and in-app agents should retrieve {{brand}} guidance before recommending setup changes.",
43464
- version: "2026-07-02.2",
43770
+ version: "2026-07-02.3",
43465
43771
  public: true,
43466
43772
  audiences: ["cli", "mcp", "in_app_agent"],
43467
43773
  stages: ["company_setup", "graph_design", "charter_design", "staffing", "operating_rhythm"],
@@ -43525,28 +43831,29 @@ Agents that run on {{brand}}-managed inference draw against a tenant inference b
43525
43831
 
43526
43832
  1. rost-implementation-method
43527
43833
  2. compass-authoring-guide
43528
- 3. responsibility-graph-playbook
43529
- 4. charter-design-playbook
43530
- 5. charter-authoring-deep-dive
43531
- 6. agent-staffing-playbook
43532
- 7. add-agents-guide
43533
- 8. custom-agents-guide
43534
- 9. agent-builder-guide
43535
- 10. how-agents-work
43536
- 11. tool-access-and-vault
43537
- 12. available-tools-guide
43538
- 13. mcp-and-cli-guide
43539
- 14. skill-builder-guide
43540
- 15. agent-skill-authoring-guide
43541
- 16. agent-skill-setup-guide
43542
- 17. ai-model-data-handling-guide
43543
- 18. cascade-guide
43544
- 19. signal-guide
43545
- 20. friction-guide
43546
- 21. confirmations-guide
43547
- 22. steward-queue-guide
43548
- 23. sync-rhythm-playbook
43549
- 24. billing-and-pricing-guide
43834
+ 3. aicos-chat-guide
43835
+ 4. responsibility-graph-playbook
43836
+ 5. charter-design-playbook
43837
+ 6. charter-authoring-deep-dive
43838
+ 7. agent-staffing-playbook
43839
+ 8. add-agents-guide
43840
+ 9. custom-agents-guide
43841
+ 10. agent-builder-guide
43842
+ 11. how-agents-work
43843
+ 12. tool-access-and-vault
43844
+ 13. available-tools-guide
43845
+ 14. mcp-and-cli-guide
43846
+ 15. skill-builder-guide
43847
+ 16. agent-skill-authoring-guide
43848
+ 17. agent-skill-setup-guide
43849
+ 18. ai-model-data-handling-guide
43850
+ 19. cascade-guide
43851
+ 20. signal-guide
43852
+ 21. friction-guide
43853
+ 22. confirmations-guide
43854
+ 23. steward-queue-guide
43855
+ 24. sync-rhythm-playbook
43856
+ 25. billing-and-pricing-guide
43550
43857
 
43551
43858
  ## Workflow to guide map
43552
43859
 
@@ -43554,6 +43861,7 @@ Read the listed guide before recommending or running each workflow. Every workfl
43554
43861
 
43555
43862
  - Connect a local agent (CLI login, MCP token, client registration): mcp-and-cli-guide, then agent-reference-map.
43556
43863
  - Company onboarding (context, Compass draft, finish): rost-implementation-method, then settings-members-and-invites-guide for invites.
43864
+ - Use the in-app AI Chief of Staff chat or explain its current authority boundary: aicos-chat-guide, then confirmations-guide for any durable change.
43557
43865
  - Author a strong Compass directly (attach references, draft, answer gaps, approve): compass-authoring-guide \u2014 it shows the full JSON contract, what good vs weak looks like per field, and worked examples. Attaching reference docs (onboarding.attach_reference) is reference-only; you author the Compass, the server does not build it for you.
43558
43866
  - Design the graph (create, rename, reparent, set type): responsibility-graph-playbook.
43559
43867
  - Write Charters (draft, edit, approve, sign manifest): charter-design-playbook for the narrative, then charter-authoring-deep-dive for the full document contract field by field (decision_authority, escalation_rules, measurables, budget, permission_manifest, seat-type), then tool-access-and-vault for permissions. Submit the finished Charter directly with charter.update_draft or charter.set.
@@ -43577,11 +43885,11 @@ Read the listed guide before recommending or running each workflow. Every workfl
43577
43885
 
43578
43886
  ## Stage map
43579
43887
 
43580
- - Company setup: rost-implementation-method, compass-authoring-guide, settings-guide, settings-members-and-invites-guide
43888
+ - Company setup: rost-implementation-method, compass-authoring-guide, aicos-chat-guide, settings-guide, settings-members-and-invites-guide
43581
43889
  - Graph design: responsibility-graph-playbook
43582
43890
  - Charter design: charter-design-playbook, charter-authoring-deep-dive
43583
43891
  - Staffing: agent-staffing-playbook, add-agents-guide, custom-agents-guide, agent-builder-guide, how-agents-work, tool-access-and-vault, available-tools-guide, skill-builder-guide, agent-skill-authoring-guide, agent-skill-setup-guide, mcp-and-cli-guide, stock-agents-guide
43584
- - Operating rhythm: cascade-guide, signal-guide, friction-guide, confirmations-guide, steward-queue-guide, sync-rhythm-playbook, notifications-guide
43892
+ - Operating rhythm: aicos-chat-guide, cascade-guide, signal-guide, friction-guide, confirmations-guide, steward-queue-guide, sync-rhythm-playbook, notifications-guide
43585
43893
  - Local agents and runners: mcp-and-cli-guide, runner-guide, agent-reference-map
43586
43894
  - Security and troubleshooting: ai-model-data-handling-guide, security-model-guide, troubleshooting-guide
43587
43895
 
@@ -43700,7 +44008,7 @@ Agents can suggest commitments and report progress. They should not create a new
43700
44008
  order: 61,
43701
44009
  title: "Signal guide",
43702
44010
  summary: "How to define and read measurables so the company runs on evidence instead of status theater.",
43703
- version: "2026-07-02.3",
44011
+ version: "2026-07-02.4",
43704
44012
  public: true,
43705
44013
  audiences: ["human", "cli", "mcp", "in_app_agent"],
43706
44014
  stages: ["operating_rhythm"],
@@ -43784,12 +44092,14 @@ Once a measurable's number lives in a connected system, you can teach {{brand}}
43784
44092
  - \`signal.bind\` persists the validated recipe as an inert \`draft\` binding. It requires the \`preview_token\` from a preview of the same recipe, so nothing is bound that was not first seen working.
43785
44093
  - \`signal.bind_confirm\` is human-only. It activates the binding (\`draft -> active\`); an agent that calls it produces a pending confirmation instead. Only after a human confirms does the scheduled pull begin writing confirmed readings.
43786
44094
 
43787
- Discovery always returns a \`recipe_presets\` array; it is empty when {{brand}} does not recognize a safe source type. Today those presets cover Baserow row fields and the generic REST JSON fallback. Google Sheets uses OAuth JSON credentials and is withheld from REST presets until the pull executor extracts and refreshes the Google access token instead of treating the stored credential blob as a bearer token. A preset includes a label, cadence, required metadata, a draft \`rest_for_signals\` recipe, and the host allowlist to preview with. The human still fills in the real Baserow table/row/field or exact REST URL and value path before running \`signal.preview\`; presets are starter recipes, not secret stores.
44095
+ Discovery always returns a \`recipe_presets\` array; it is empty when {{brand}} does not recognize a safe source type. Today the proven self-filling path is Baserow REST through \`integration.connect_rest\`: a tenant admin creates or rotates the Baserow connection through the command/vault path, then Signal runs \`integration.discover -> signal.preview -> signal.bind -> signal.bind_confirm\` against that connected source. Baserow row-field recipes and custom filtered-count recipes are supported when the human supplies the real table, field, filter, host, and value path. A first-class filtered-count preset is still a follow-up; do not imply every filtered count appears as a one-click preset. Google Sheets Signal pulls are not live yet: Google uses OAuth JSON credentials and remains withheld from REST presets until the pull executor extracts and refreshes the Google access token instead of treating the stored credential blob as a bearer token. A preset includes a label, cadence, required metadata, a draft \`rest_for_signals\` recipe, and the host allowlist to preview with. The human still fills in the real Baserow table/row/field or exact REST URL and value path before running \`signal.preview\`; presets are starter recipes, not secret stores.
43788
44096
 
43789
44097
  Recipes are validated data, never code: a read-only GET, a closed set of non-secret headers, and a restricted scalar path to the value. Every fetch passes one SSRF guard (HTTPS only, a steward-confirmed host allowlist set at bind, and a runtime check that the host does not resolve to a private address). The preset output never contains \`nango://\` handles, vault refs, tokens, or static credential headers. Deterministic pulls write \`integration\`-sourced readings on the measurable's cadence.
43790
44098
 
43791
44099
  In the web app, the same workflow lives on \`/signal\` in the "Connect a self-filling source" panel. It shows each measurable's setup state \u2014 manual, agent draft, source draft, or auto-filled \u2014 plus the latest safe pull outcome when one exists. Choose a connected source, discover its non-secret metadata, preview a GET URL and value path, bind the working preview as a draft, then confirm the draft before scheduled pulls can write readings. The UI never displays credential material, vault refs, raw response bodies, or recipe secrets; if no connected source exists, connect one from Settings first.
43792
44100
 
44101
+ A source binding can inform a measurable without automatically changing a Cascade goal. The proven Leiluna Baserow binding uses \`informs\`. Use \`drives_status\` only after a second business anchor confirms that this exact measurable should compute a childless goal's status; otherwise the Signal should stay visible as evidence and exception context, not an automatic goal-status driver.
44102
+
43793
44103
  ## Collect a Signal by judgment (the Signal agent)
43794
44104
 
43795
44105
  Some measurables cannot be a deterministic pull \u2014 the number lives in a PDF, in messy records that need reconciling, or is a judgment estimate of progress. For those, mark the measurable \`agent\`-sourced and let a per-seat Signal agent read it. The agent proposes each reading with \`signal.report\` (scope: seat): one draft reading for one of its OWN seat's agent-sourced measurables, with a \`value\` and a short \`note\` citing the source. The reading lands unconfirmed, exactly like a deterministic draft \u2014 a human clears it with \`signal.confirm_reading\`. \`signal.report\` refuses any measurable not owned by the acting seat and any measurable whose source is not \`agent\`, and it never confirms its own reading. Deliver it as the \`stock/signal-collector\` template (a read-only agent whose only durable grant is \`signal.report\`) or run the \`collect-seat-signals\` how-to from an occupant agent. When the agent cannot read a number confidently \u2014 ambiguous or conflicting sources \u2014 it escalates instead of guessing.
@@ -44115,7 +44425,7 @@ Do not claim that a live charge happened unless Stripe test/live evidence proves
44115
44425
  order: 72,
44116
44426
  title: "Settings guide",
44117
44427
  summary: "How to use Settings as the control plane for company access, channels, providers, tokens, and operating defaults.",
44118
- version: "2026-07-02.2",
44428
+ version: "2026-07-02.3",
44119
44429
  public: true,
44120
44430
  audiences: ["human", "cli", "mcp", "in_app_agent"],
44121
44431
  stages: ["company_setup", "staffing"],
@@ -44201,6 +44511,7 @@ The company autonomy ceiling is a single tenant-wide dial that caps how much ANY
44201
44511
 
44202
44512
  - Read it with \`settings.agent_policy.get\` (CLI: \`{{cli}} settings agent-policy get\`).
44203
44513
  - Set it with \`settings.agent_policy.update\` (CLI: \`{{cli}} settings agent-policy set --profile locked_down\`). Owner-only and human-gated.
44514
+ - In the signed-in app, tenant owners manage the same policy from **Settings \u2192 Company guardrails**. Non-owners can see the current posture but cannot change it.
44204
44515
  - Profiles are named ceilings: \`locked_down\` (agents autonomously read/draft only; every write/send escalates), \`balanced\` (the default; read/draft autonomous, writes escalate), \`high_autonomy\` (approval-marked write tools may run autonomously; only full-access escalates), and \`custom\` + \`max_autonomous_risk\` (\`low\`, \`medium\`, \`high\`, \`critical\`).
44205
44516
  - An explicitly set ceiling is ENFORCED by default. Enforcement has two modes: \`enforce\` blocks an over-ceiling autonomous call and routes it to the steward (it is never a dead-end deny \u2014 a human can still approve), and \`observe\` allows the call but logs it. The platform default for a company that never set a policy is balanced + observe, so existing live agents are never retro-bricked.
44206
44517
  - When enforcement is on, signing a manifest or taking an agent live is REFUSED if the manifest grants always_allow to a tool whose risk exceeds the ceiling; the error names the exact ceiling, tool, and risk in its \`details\`. Lower that tool to always_ask, or raise the ceiling, then retry.
@@ -44214,7 +44525,7 @@ Agents may explain which setting is needed and why. They should not ask users to
44214
44525
  order: 73,
44215
44526
  title: "Members and invites guide",
44216
44527
  summary: "How to invite humans into the company without confusing user access with seat accountability.",
44217
- version: "2026-06-13.1",
44528
+ version: "2026-07-02.1",
44218
44529
  public: true,
44219
44530
  audiences: ["human", "cli", "mcp", "in_app_agent"],
44220
44531
  stages: ["company_setup", "staffing"],
@@ -44239,6 +44550,8 @@ Invite the person as a tenant member. Then assign them to one or more seats only
44239
44550
 
44240
44551
  Tenant roles control app access. Seats control operating accountability. Do not attach goals, tasks, or measurables directly to a user when the work belongs to a seat.
44241
44552
 
44553
+ Owners and admins can invite people and manage member roles. Use \`external_implementer\` plus setup-limited access for a temporary implementation partner helping configure onboarding, Responsibility Graph, Compass, Charters, and agent setup. Do not grant external implementers sensitive run history, hidden tool arguments, credentials, Memory, or company-operating data unless an owner explicitly expands their profile.
44554
+
44242
44555
  ## Agent guidance
44243
44556
 
44244
44557
  When a user asks to add a person, clarify whether they mean app access, seat occupancy, or both. Use invites for access and staffing commands for occupancies.`
@@ -44287,7 +44600,7 @@ Every notification should include the seat, cause, evidence, and requested decis
44287
44600
  order: 75,
44288
44601
  title: "Local runner guide",
44289
44602
  summary: "How local agent sessions and runner surfaces should operate through {{brand}} without bypassing Charters or audit.",
44290
- version: "2026-06-30.1",
44603
+ version: "2026-07-02.1",
44291
44604
  public: true,
44292
44605
  audiences: ["human", "cli", "mcp", "in_app_agent"],
44293
44606
  stages: ["staffing", "operating_rhythm"],
@@ -44313,6 +44626,26 @@ The local runner is for human-controlled local agent work. It should retrieve {{
44313
44626
  5. Record work, Friction, handoffs, and escalations.
44314
44627
  6. Revoke access when the session ends.
44315
44628
 
44629
+ ## Install a local Runner
44630
+
44631
+ Use Local Runner when a Seat should run on a machine you control. Install it from the public CLI \u2014 no repository checkout and no desktop app are required. A signed desktop app is planned; until verified installers ship, Settings > Runners shows no desktop download link.
44632
+
44633
+ Recommended paths:
44634
+
44635
+ - **Always-on host:** \`{{cli}} runner install-service --name <runner-name> --execute\` installs a background service (macOS launchd user agent) that keeps the runner online across reboots. Manage it with \`{{cli}} runner start|stop|restart|status|logs|uninstall --name <runner-name>\`.
44636
+ - **Manual smoke:** \`{{cli}} runner serve --once\` heartbeats once and exits; \`{{cli}} runner serve --execute\` claims and executes runner work orders while it runs.
44637
+
44638
+ Two ways to authenticate and pair \u2014 both first-class, an agent can drive either entirely from the terminal:
44639
+
44640
+ - **CLI-native (agent):** \`{{cli}} login --device\` prints a code for one human OAuth approval; after that the session is stored locally and the agent runs unattended ("approve once, then autonomous"). Then \`{{cli}} runner serve\`/\`install-service\` pairs against your active company \u2014 an authenticated CLI can also self-issue a pairing code with \`runner.pairing.start\` and claim it.
44641
+ - **Web-guided (human):** issue a pairing code from Settings > Runners > Add a Runner, then on the host run \`{{cli}} runner serve --user-code <code>\`. The host stores only the runner bearer secret in an owner-only file and never logs in.
44642
+
44643
+ Headless and service accounts are supported first-class: when no usable OS keychain exists (for example a service or SSH account), the CLI stores the session in an owner-only file (directory 0700, file 0600) automatically \u2014 no development flag. That file holds only the CLI session token, never tenant secrets, API keys, or vault refs.
44644
+
44645
+ Choose the local runtime with \`--runtime auto|claude|codex\` (default \`auto\`). The same short-lived seat-scoped MCP token and server-side guard apply regardless of runtime; results are tagged \`claude-cli\` or \`codex-cli\`.
44646
+
44647
+ The runner bearer secret is stored locally and is never displayed. Each claimed turn receives a short-lived seat-scoped MCP token from {{brand}}, and server-side guards still decide what tools the Seat may use.
44648
+
44316
44649
  ## Inspect and control runners from CLI or MCP
44317
44650
 
44318
44651
  - Pair a new runner: \`runner.pairing.start\` / \`rost_start_runner_pairing\` with \`name\` and \`platform\` returns a human pairing code. This is owner/admin-only because it mints a short-lived runner pairing session that leads to machine credentials.
@@ -46168,9 +46501,9 @@ var COMMAND_MANIFEST = [
46168
46501
  { "id": "measurable_template.adopt", "namespace": "measurable_template", "action": "adopt", "title": "Adopt a measurable template", "description": "Create a seat measurable from a catalog template (unit/direction/cadence from the template; target optionally overridden). The seat owns it; readings attach afterward.", "requiredScope": "seat", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "template_id", "flag": "template-id", "type": "string", "required": true }, { "name": "seat_id", "flag": "seat-id", "type": "string", "required": true }, { "name": "target", "flag": "target", "type": "number", "required": false }], "hasComplexInput": false, "help": "Create a seat measurable from a catalog template (unit/direction/cadence from the template; target optionally overridden). Adoption is a human act \u2014 source is human." },
46169
46502
  { "id": "measurable_template.list", "namespace": "measurable_template", "action": "list", "title": "List measurable templates", "description": "List the measurable template catalog (Sales/Finance/Ops/Marketing/HR) with unit, direction, cadence, and a suggested target. Adopt one with measurable_template.adopt.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "function", "flag": "function", "type": "enum", "required": false, "enumValues": ["sales", "finance", "operations", "marketing", "hr"] }], "hasComplexInput": false, "help": "List the measurable template catalog by function (Sales/Finance/Ops/Marketing/HR); adopt one with measurable_template.adopt to create a seat measurable." },
46170
46503
  { "id": "measurable.create", "namespace": "measurable", "action": "create", "title": "Add a measurable", "description": "Create a measurable (a Signal a seat owns) with its target, unit, direction, and cadence. The seat owns it; readings attach to the measurable. Source is 'human' \u2014 a human-defined measurable a human logs.", "requiredScope": "seat", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "seat_id", "flag": "seat-id", "type": "string", "required": true }, { "name": "name", "flag": "name", "type": "string", "required": true }, { "name": "unit", "flag": "unit", "type": "string", "required": true }, { "name": "direction", "flag": "direction", "type": "enum", "required": true, "enumValues": ["up_good", "down_good"] }, { "name": "target", "flag": "target", "type": "number", "required": true }, { "name": "cadence", "flag": "cadence", "type": "enum", "required": true, "enumValues": ["daily", "weekly", "monthly", "quarterly", "annual"] }], "hasComplexInput": false, "help": "Add a measurable a seat owns (name, unit, direction, target, cadence) before recording readings against it." },
46171
- { "id": "member.invite", "namespace": "member", "action": "invite", "title": "Invite member", "description": "Create a pending tenant invite for a human teammate. The acceptance link is delivered out of band, not in the command result.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "email", "flag": "email", "type": "string", "required": true }, { "name": "role", "flag": "role", "type": "enum", "required": true, "enumValues": ["owner", "steward", "member"] }, { "name": "byo_key", "flag": "byo-key", "type": "boolean", "required": false }], "hasComplexInput": false, "help": "Invite humans to the company by seat-aware role; the invite token is never returned over CLI or MCP." },
46504
+ { "id": "member.invite", "namespace": "member", "action": "invite", "title": "Invite member", "description": "Create a pending tenant invite for a human teammate. The acceptance link is delivered out of band, not in the command result.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "email", "flag": "email", "type": "string", "required": true }, { "name": "role", "flag": "role", "type": "enum", "required": true, "enumValues": ["owner", "admin", "steward", "member"] }, { "name": "member_type", "flag": "member-type", "type": "enum", "required": false, "enumValues": ["internal", "external_implementer"] }, { "name": "access_profile", "flag": "access-profile", "type": "enum", "required": false, "enumValues": ["full_setup", "setup_limited", "seat_scoped", "custom"] }, { "name": "aicos_access_scope", "flag": "aicos-access-scope", "type": "enum", "required": false, "enumValues": ["none", "own_scope", "setup", "company_read", "company_operate"] }, { "name": "byo_key", "flag": "byo-key", "type": "boolean", "required": false }], "hasComplexInput": false, "help": "Invite humans to the company by seat-aware role; the invite token is never returned over CLI or MCP." },
46172
46505
  { "id": "member.remove", "namespace": "member", "action": "remove", "title": "Remove member", "description": "Remove a tenant member. Blocked when it would orphan an agent steward chain.", "requiredScope": "tenant", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "member_id", "flag": "member-id", "type": "string", "required": true }], "hasComplexInput": false, "help": "Remove a member; the no-orphan-agent guard blocks removing the only steward in an agent chain." },
46173
- { "id": "member.update", "namespace": "member", "action": "update", "title": "Update member role", "description": "Change a tenant member's role.", "requiredScope": "tenant", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "member_id", "flag": "member-id", "type": "string", "required": true }, { "name": "role", "flag": "role", "type": "enum", "required": true, "enumValues": ["owner", "steward", "member"] }], "hasComplexInput": false, "help": "Change a member's role; role changes are human-gated." },
46506
+ { "id": "member.update", "namespace": "member", "action": "update", "title": "Update member role", "description": "Change a tenant member's role.", "requiredScope": "tenant", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "member_id", "flag": "member-id", "type": "string", "required": true }, { "name": "role", "flag": "role", "type": "enum", "required": true, "enumValues": ["owner", "admin", "steward", "member"] }], "hasComplexInput": false, "help": "Change a member's role; role changes are human-gated." },
46174
46507
  { "id": "model.catalog", "namespace": "model", "action": "catalog", "title": "List model catalog", "description": "List the guided model tiers the agent builder reads \u2014 recommendation, token prices, cost band, effort, model id, and when to choose or avoid each tier.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "List guided model tiers with recommendations, token prices, cost bands, best-fit work, and model ids before choosing --model for an agent." },
46175
46508
  { "id": "notification.list_errors", "namespace": "notification", "action": "list_errors", "title": "List notification errors", "description": "List recent failed notification deliveries for the tenant.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "limit", "flag": "limit", "type": "integer", "required": false }], "hasComplexInput": false, "help": "List recent failed notification deliveries when diagnosing missed escalations or briefs." },
46176
46509
  { "id": "notification.settings.get", "namespace": "notification", "action": "settings.get", "title": "Get notification settings", "description": "Read tenant notification preferences (escalations, Sync brief, default channel).", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Read notification preferences before changing escalation or Sync brief delivery." },
@@ -46178,7 +46511,7 @@ var COMMAND_MANIFEST = [
46178
46511
  { "id": "notification.test", "namespace": "notification", "action": "test", "title": "Send test notification", "description": "Emit an in-app test notification to the acting human to verify delivery is wired.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Send an in-app test notification to confirm the delivery path is wired." },
46179
46512
  { "id": "onboarding.advance_step", "namespace": "onboarding", "action": "advance_step", "title": "Advance onboarding step", "description": "Persist the current onboarding step after server-side validation.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "step", "flag": "step", "type": "string", "required": true }, { "name": "completed", "flag": "completed", "type": "boolean", "required": false }], "hasComplexInput": true, "help": "Advance onboarding only after the current setup step is represented in the tenant state." },
46180
46513
  { "id": "onboarding.attach_reference", "namespace": "onboarding", "action": "attach_reference", "title": "Attach reference document", "description": "Attach a company reference document (text/markdown) so Compass and Charters can cite it. Reference-only \u2014 it does not build a Compass; convert each meaningful doc to markdown and attach it with a title, kind, and source.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "text", "flag": "text", "type": "string", "required": true }, { "name": "filename", "flag": "filename", "type": "string", "required": false }, { "name": "mime_type", "flag": "mime-type", "type": "enum", "required": false, "enumValues": ["text/plain", "text/markdown", "text/csv"] }, { "name": "title", "flag": "title", "type": "string", "required": false }, { "name": "kind", "flag": "kind", "type": "enum", "required": false, "enumValues": ["plan", "financial", "org_chart", "process"] }, { "name": "source", "flag": "source", "type": "string", "required": false }], "hasComplexInput": false, "help": "Convert each meaningful company doc to markdown and attach it as a reference (title, kind, source) so Compass and Charters can cite it; this attaches a reference, it does not build a Compass \u2014 author Compass directly.", "example": { "text": "# Company handbook\n\nWe build precision components for the aerospace supply chain.", "filename": "handbook.md", "mime_type": "text/markdown", "title": "Company handbook", "kind": "plan", "source": "handbook/overview.md" } },
46181
- { "id": "onboarding.create_invite", "namespace": "onboarding", "action": "create_invite", "title": "Create onboarding invite", "description": "Create a pending invite for an onboarding team member.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "email", "flag": "email", "type": "string", "required": true }, { "name": "role", "flag": "role", "type": "enum", "required": false, "enumValues": ["owner", "steward", "member"] }], "hasComplexInput": true, "help": "Invites should give humans access to the company without assigning work directly to people instead of seats." },
46514
+ { "id": "onboarding.create_invite", "namespace": "onboarding", "action": "create_invite", "title": "Create onboarding invite", "description": "Create a pending invite for an onboarding team member.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "email", "flag": "email", "type": "string", "required": true }, { "name": "role", "flag": "role", "type": "enum", "required": false, "enumValues": ["owner", "admin", "steward", "member"] }], "hasComplexInput": true, "help": "Invites should give humans access to the company without assigning work directly to people instead of seats." },
46182
46515
  { "id": "onboarding.finish", "namespace": "onboarding", "action": "finish", "title": "Finish onboarding", "description": "Mark onboarding complete after the existing onboarding requirements pass.", "requiredScope": "tenant", "confirmation": "human_required", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Finish onboarding when the graph, at least one Charter, and the first operating rhythm are clear." },
46183
46516
  { "id": "onboarding.status", "namespace": "onboarding", "action": "status", "title": "Onboard status", "description": "Return current onboarding progress, graph summary, and the next recommended actions.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Start by checking onboarding status, then use the implementation method guide to choose the next setup step." },
46184
46517
  { "id": "onboarding.upload_context", "namespace": "onboarding", "action": "upload_context", "title": "Upload org context", "description": "Compatibility alias for onboarding.attach_reference: attach a company reference document (text/markdown) that Compass and Charters can cite. Reference-only \u2014 it does not build a Compass.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "text", "flag": "text", "type": "string", "required": true }, { "name": "filename", "flag": "filename", "type": "string", "required": false }, { "name": "mime_type", "flag": "mime-type", "type": "enum", "required": false, "enumValues": ["text/plain", "text/markdown", "text/csv"] }, { "name": "title", "flag": "title", "type": "string", "required": false }, { "name": "kind", "flag": "kind", "type": "enum", "required": false, "enumValues": ["plan", "financial", "org_chart", "process"] }, { "name": "source", "flag": "source", "type": "string", "required": false }], "hasComplexInput": false, "help": "Compatibility alias for onboarding.attach_reference: attach a company reference document so Compass and Charters can cite it. It does not build a Compass.", "example": { "text": "# Company handbook\n\nWe build precision components for the aerospace supply chain.", "filename": "handbook.md", "mime_type": "text/markdown", "title": "Company handbook", "kind": "plan", "source": "handbook/overview.md" } },
@@ -47950,6 +48283,9 @@ function operationUsageLines(bin) {
47950
48283
  `${bin} error list|resolve|supersede`,
47951
48284
  `${bin} sync brief|compile|complete`,
47952
48285
  `${bin} runner list|status|diagnose|repair|work-orders|revoke|serve`,
48286
+ // Local runner service lifecycle (DER-1142). The orchestrator mirrors this
48287
+ // exact line into the mcp-and-cli-guide so the help/guide parity gate passes.
48288
+ `${bin} runner install-service|start|stop|restart|status|logs|uninstall --name <name>`,
47953
48289
  `${bin} notification settings|test|errors`,
47954
48290
  `${bin} settings get|update|product-learning|agent-policy|rename`,
47955
48291
  `${bin} member invite|update|remove`,
@@ -48524,8 +48860,8 @@ function pendingConfirmationFromError(error51) {
48524
48860
  import { execFile as execFileCallback2 } from "node:child_process";
48525
48861
  import { access, readFile as readFile3 } from "node:fs/promises";
48526
48862
  import { delimiter } from "node:path";
48527
- import { promisify as promisify3 } from "node:util";
48528
- var execFile3 = promisify3(execFileCallback2);
48863
+ import { promisify as promisify2 } from "node:util";
48864
+ var execFile2 = promisify2(execFileCallback2);
48529
48865
  async function runDoctor(options) {
48530
48866
  const env = options.env ?? process.env;
48531
48867
  const store = options.store ?? createTokenStore();
@@ -48573,7 +48909,7 @@ async function checkPath(env) {
48573
48909
  }
48574
48910
  async function probeCli(command) {
48575
48911
  try {
48576
- const { stdout, stderr } = await execFile3(command, ["--version"], { timeout: 1500 });
48912
+ const { stdout, stderr } = await execFile2(command, ["--version"], { timeout: 1500 });
48577
48913
  const version4 = (stdout || stderr).trim().split("\n")[0]?.trim();
48578
48914
  return version4 ? `installed (${version4})` : "installed";
48579
48915
  } catch {
@@ -48648,8 +48984,8 @@ import { chmod, mkdir as mkdir2, readFile as readFile4, rm as rm2, writeFile as
48648
48984
  import { hostname as hostname3, tmpdir } from "node:os";
48649
48985
  import path3 from "node:path";
48650
48986
  import { setTimeout as sleep2 } from "node:timers/promises";
48651
- import { promisify as promisify4 } from "node:util";
48652
- var execFile4 = promisify4(execFileCallback3);
48987
+ import { promisify as promisify3 } from "node:util";
48988
+ var execFile3 = promisify3(execFileCallback3);
48653
48989
  async function runRunnerServe(options) {
48654
48990
  const env = options.env ?? process.env;
48655
48991
  const fetchImpl = options.fetchImpl ?? fetch;
@@ -48688,8 +49024,11 @@ ${usage()}
48688
49024
  options.io.stdout.write(`heartbeat ok #${beat} runner_id=${state.runner_id} claude=${capabilities.claude.installed} codex=${capabilities.codex.installed}
48689
49025
  `);
48690
49026
  }
48691
- if (config2.execute && capabilities.claude.installed) {
48692
- await claimAndExecute(fetchImpl, options.appUrl, state, options.io);
49027
+ if (config2.execute) {
49028
+ const localRuntime = effectiveExecutionRuntime(config2.runtime, capabilities);
49029
+ if (localRuntime) {
49030
+ await claimAndExecute(fetchImpl, options.appUrl, state, options.io, localRuntime);
49031
+ }
48693
49032
  }
48694
49033
  } catch (error51) {
48695
49034
  failed = true;
@@ -48709,6 +49048,7 @@ function parseServeArgs(args, appUrl2, env) {
48709
49048
  let heartbeatMs = Number(env.RUNNER_HEARTBEAT_MS ?? 3e4);
48710
49049
  let execute2 = env.RUNNER_EXECUTE === "1";
48711
49050
  let once = false;
49051
+ let runtime = parseRuntime(env.RUNNER_RUNTIME ?? "auto");
48712
49052
  let userCode = env.RUNNER_USER_CODE;
48713
49053
  for (let index = 0; index < args.length; index += 1) {
48714
49054
  const token = args[index];
@@ -48724,6 +49064,9 @@ function parseServeArgs(args, appUrl2, env) {
48724
49064
  } else if (token === "--user-code") {
48725
49065
  userCode = requiredValue(args, index);
48726
49066
  index += 1;
49067
+ } else if (token === "--runtime") {
49068
+ runtime = parseRuntime(requiredValue(args, index));
49069
+ index += 1;
48727
49070
  } else if (token === "--execute") {
48728
49071
  execute2 = true;
48729
49072
  } else if (token === "--once") {
@@ -48735,7 +49078,19 @@ function parseServeArgs(args, appUrl2, env) {
48735
49078
  if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3) {
48736
49079
  throw new Error("--heartbeat-ms must be an integer >= 1000.");
48737
49080
  }
48738
- return { name, stateFile, heartbeatMs, execute: execute2, once, ...userCode ? { userCode } : {} };
49081
+ return { name, stateFile, heartbeatMs, execute: execute2, once, runtime, ...userCode ? { userCode } : {} };
49082
+ }
49083
+ function parseRuntime(value) {
49084
+ if (value === "auto" || value === "claude" || value === "codex") {
49085
+ return value;
49086
+ }
49087
+ throw new Error(`--runtime must be one of auto, claude, codex (got ${value}).`);
49088
+ }
49089
+ function effectiveExecutionRuntime(runtime, capabilities) {
49090
+ if (runtime === "codex") {
49091
+ return capabilities.codex.installed ? "codex" : null;
49092
+ }
49093
+ return capabilities.claude.installed ? "claude" : null;
48739
49094
  }
48740
49095
  function requiredValue(args, index) {
48741
49096
  const value = args[index + 1];
@@ -48825,7 +49180,7 @@ async function detectCapabilities() {
48825
49180
  }
48826
49181
  async function probe(command) {
48827
49182
  try {
48828
- const { stdout, stderr } = await execFile4(command, ["--version"], { timeout: 1500 });
49183
+ const { stdout, stderr } = await execFile3(command, ["--version"], { timeout: 1500 });
48829
49184
  return { installed: true, version: (stdout || stderr).trim() || null, auth_state: "ready" };
48830
49185
  } catch {
48831
49186
  return { installed: false, version: null, auth_state: "unknown" };
@@ -48850,7 +49205,7 @@ async function post2(fetchImpl, appUrl2, pathName, body, token) {
48850
49205
  }
48851
49206
  return { status: response.status, json: json2 };
48852
49207
  }
48853
- async function claimAndExecute(fetchImpl, appUrl2, state, io) {
49208
+ async function claimAndExecute(fetchImpl, appUrl2, state, io, runtime) {
48854
49209
  const claimed = await post2(fetchImpl, appUrl2, "/api/runner/work-orders/claim", {}, state.runner_secret);
48855
49210
  const workOrder = claimed.json.work_order;
48856
49211
  if (claimed.status !== 200 || !workOrder || typeof workOrder !== "object") {
@@ -48864,12 +49219,12 @@ async function claimAndExecute(fetchImpl, appUrl2, state, io) {
48864
49219
  if (started.status !== 200) {
48865
49220
  throw new Error(`work order start failed ${started.status}: ${redactForLog(JSON.stringify(started.json))}`);
48866
49221
  }
48867
- const result = await runClaudeTurn(appUrl2, workOrder);
49222
+ const result = await runLocalTurn(appUrl2, workOrder, runtime);
48868
49223
  const reported = await post2(fetchImpl, appUrl2, `/api/runner/work-orders/${encodeURIComponent(workOrderId)}/result`, {
48869
49224
  status: result.ok ? "succeeded" : "failed",
48870
49225
  summary: result.summary,
48871
49226
  log_ref: `runner-local:${workOrderId}`,
48872
- model: "claude-cli"
49227
+ model: runtime === "codex" ? "codex-cli" : "claude-cli"
48873
49228
  }, state.runner_secret);
48874
49229
  if (reported.status !== 200) {
48875
49230
  throw new Error(`work order result failed ${reported.status}: ${redactForLog(JSON.stringify(reported.json))}`);
@@ -48877,7 +49232,28 @@ async function claimAndExecute(fetchImpl, appUrl2, state, io) {
48877
49232
  io.stdout.write(`work_order_id=${workOrderId} status=${result.ok ? "succeeded" : "failed"}
48878
49233
  `);
48879
49234
  }
48880
- async function runClaudeTurn(appUrl2, workOrder) {
49235
+ function runLocalTurn(appUrl2, workOrder, runtime) {
49236
+ return runtime === "codex" ? runCodexTurn(appUrl2, workOrder) : runClaudeTurn(appUrl2, workOrder);
49237
+ }
49238
+ function runClaudeTurn(appUrl2, workOrder) {
49239
+ return spawnRunnerTurn(appUrl2, workOrder, "claude");
49240
+ }
49241
+ function runCodexTurn(appUrl2, workOrder) {
49242
+ return spawnRunnerTurn(appUrl2, workOrder, "codex");
49243
+ }
49244
+ function buildTurnCommand(runtime, prompt, configPath) {
49245
+ if (runtime === "codex") {
49246
+ return {
49247
+ command: "codex",
49248
+ args: ["exec", prompt, "--mcp-config", configPath, "--sandbox", "read-only", "--skip-git-repo-check"]
49249
+ };
49250
+ }
49251
+ return {
49252
+ command: "claude",
49253
+ args: ["-p", prompt, "--mcp-config", configPath, "--strict-mcp-config", "--output-format", "text"]
49254
+ };
49255
+ }
49256
+ async function spawnRunnerTurn(appUrl2, workOrder, runtime) {
48881
49257
  const mcp = typeof workOrder.mcp === "object" && workOrder.mcp !== null ? workOrder.mcp : {};
48882
49258
  const token = typeof mcp.token === "string" ? mcp.token : "";
48883
49259
  const url2 = typeof mcp.url === "string" ? `${appUrl2.replace(/\/+$/, "")}${mcp.url}` : `${appUrl2.replace(/\/+$/, "")}/mcp`;
@@ -48898,6 +49274,7 @@ async function runClaudeTurn(appUrl2, workOrder) {
48898
49274
  await writeFile2(configPath, `${mcpConfig}
48899
49275
  `, { mode: 384 });
48900
49276
  await chmod(configPath, 384);
49277
+ const { command, args } = buildTurnCommand(runtime, prompt, configPath);
48901
49278
  return new Promise((resolve2) => {
48902
49279
  let settled = false;
48903
49280
  const finish = (result) => {
@@ -48908,7 +49285,7 @@ async function runClaudeTurn(appUrl2, workOrder) {
48908
49285
  void rm2(configPath, { force: true });
48909
49286
  resolve2(result);
48910
49287
  };
48911
- const child = spawn("claude", ["-p", prompt, "--mcp-config", configPath, "--strict-mcp-config", "--output-format", "text"], {
49288
+ const child = spawn(command, args, {
48912
49289
  cwd: tmpdir(),
48913
49290
  stdio: ["ignore", "pipe", "pipe"]
48914
49291
  });
@@ -48935,7 +49312,431 @@ async function runClaudeTurn(appUrl2, workOrder) {
48935
49312
  });
48936
49313
  }
48937
49314
  function usage() {
48938
- return "Usage: rost runner serve [--name <text>] [--state-file <path>] [--heartbeat-ms <n>] [--user-code <code>] [--once] [--execute]";
49315
+ return "Usage: rost runner serve [--name <text>] [--state-file <path>] [--heartbeat-ms <n>] [--user-code <code>] [--runtime auto|claude|codex] [--once] [--execute]";
49316
+ }
49317
+
49318
+ // src/runner-service.ts
49319
+ import { execFile as execFileCallback4 } from "node:child_process";
49320
+ import { realpathSync } from "node:fs";
49321
+ import { access as access2, mkdir as mkdir3, readFile as readFile5, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
49322
+ import { homedir as homedir2 } from "node:os";
49323
+ import path4 from "node:path";
49324
+ import { promisify as promisify4 } from "node:util";
49325
+ var execFile4 = promisify4(execFileCallback4);
49326
+ var SERVICE_VERBS = ["install-service", "start", "stop", "restart", "status", "logs", "uninstall"];
49327
+ function isServiceVerb(value) {
49328
+ return value !== void 0 && SERVICE_VERBS.includes(value);
49329
+ }
49330
+ function isRunnerServiceInvocation(args) {
49331
+ const verb = args[0];
49332
+ if (verb === "status") {
49333
+ return args.includes("--name");
49334
+ }
49335
+ return isServiceVerb(verb);
49336
+ }
49337
+ function runnerServicePaths(homeDir, runnerName) {
49338
+ const safeName = safeRunnerName(runnerName);
49339
+ return {
49340
+ launchAgentPath: path4.join(homeDir, "Library", "LaunchAgents", `${serviceLabel(safeName)}.plist`),
49341
+ stateFile: path4.join(homeDir, "Library", "Application Support", cliBrand.name, `runner-${safeName}.json`),
49342
+ logDir: path4.join(homeDir, "Library", "Logs", cliBrand.name, "runner", safeName)
49343
+ };
49344
+ }
49345
+ function renderLaunchdPlist(config2) {
49346
+ const programArgs = [
49347
+ config2.binPath,
49348
+ "runner",
49349
+ "serve",
49350
+ ...config2.execute ? ["--execute"] : [],
49351
+ "--name",
49352
+ config2.runnerName,
49353
+ "--state-file",
49354
+ config2.stateFile
49355
+ ];
49356
+ const programArgsXml = programArgs.map((arg) => ` <string>${escapePlist(arg)}</string>`).join("\n");
49357
+ return `<?xml version="1.0" encoding="UTF-8"?>
49358
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
49359
+ <plist version="1.0">
49360
+ <dict>
49361
+ <key>Label</key>
49362
+ <string>${escapePlist(serviceLabel(config2.runnerName))}</string>
49363
+ <key>ProgramArguments</key>
49364
+ <array>
49365
+ ${programArgsXml}
49366
+ </array>
49367
+ <key>EnvironmentVariables</key>
49368
+ <dict>
49369
+ <key>${cliBrand.envPrefix}_APP_URL</key>
49370
+ <string>${escapePlist(config2.appUrl)}</string>
49371
+ </dict>
49372
+ <key>KeepAlive</key>
49373
+ <true/>
49374
+ <key>RunAtLoad</key>
49375
+ <true/>
49376
+ <key>StandardOutPath</key>
49377
+ <string>${escapePlist(path4.join(config2.logDir, "stdout.log"))}</string>
49378
+ <key>StandardErrorPath</key>
49379
+ <string>${escapePlist(path4.join(config2.logDir, "stderr.log"))}</string>
49380
+ </dict>
49381
+ </plist>
49382
+ `;
49383
+ }
49384
+ async function runRunnerService(options) {
49385
+ const verb = options.args[0];
49386
+ if (!isServiceVerb(verb)) {
49387
+ options.io.stderr.write(`Unknown runner service command: ${verb ?? ""}
49388
+ ${serviceUsage()}
49389
+ `);
49390
+ return 1;
49391
+ }
49392
+ if (options.args.includes("--help") || options.args.includes("-h")) {
49393
+ options.io.stdout.write(`${serviceUsage()}
49394
+ `);
49395
+ return 0;
49396
+ }
49397
+ let parsed;
49398
+ try {
49399
+ parsed = parseServiceArgs(options.args.slice(1));
49400
+ } catch (error51) {
49401
+ options.io.stderr.write(`${error51 instanceof Error ? error51.message : String(error51)}
49402
+ ${serviceUsage()}
49403
+ `);
49404
+ return 1;
49405
+ }
49406
+ const cmd = {
49407
+ io: options.io,
49408
+ runnerName: parsed.runnerName,
49409
+ appUrl: options.appUrl,
49410
+ execute: parsed.execute,
49411
+ binPath: options.binPath ?? resolveBinPath(),
49412
+ homeDir: options.homeDir ?? homedir2(),
49413
+ platform: options.platform ?? process.platform,
49414
+ exec: options.exec ?? defaultExec,
49415
+ uid: options.uid ?? (process.getuid ? process.getuid() : 0)
49416
+ };
49417
+ switch (verb) {
49418
+ case "install-service":
49419
+ return installService(cmd);
49420
+ case "start":
49421
+ return startService(cmd);
49422
+ case "stop":
49423
+ return stopService(cmd);
49424
+ case "restart":
49425
+ return restartService(cmd);
49426
+ case "status":
49427
+ return statusService(cmd);
49428
+ case "logs":
49429
+ return logsService(cmd);
49430
+ case "uninstall":
49431
+ return uninstallService(cmd);
49432
+ default:
49433
+ return assertNever2(verb);
49434
+ }
49435
+ }
49436
+ async function installService(options) {
49437
+ if (options.platform !== "darwin") {
49438
+ return unsupportedPlatform(options.io, options.platform);
49439
+ }
49440
+ const safeName = safeRunnerName(options.runnerName);
49441
+ const paths = runnerServicePaths(options.homeDir, safeName);
49442
+ const label = serviceLabel(safeName);
49443
+ await mkdir3(paths.logDir, { recursive: true });
49444
+ await mkdir3(path4.dirname(paths.stateFile), { recursive: true });
49445
+ await mkdir3(path4.dirname(paths.launchAgentPath), { recursive: true });
49446
+ const plist = renderLaunchdPlist({
49447
+ binPath: options.binPath,
49448
+ runnerName: safeName,
49449
+ stateFile: paths.stateFile,
49450
+ logDir: paths.logDir,
49451
+ appUrl: options.appUrl,
49452
+ execute: options.execute
49453
+ });
49454
+ await writeFile3(paths.launchAgentPath, plist, { mode: 420 });
49455
+ await bootoutQuietly(options.exec, options.uid, paths.launchAgentPath);
49456
+ try {
49457
+ await options.exec("launchctl", ["bootstrap", domainTarget(options.uid), paths.launchAgentPath]);
49458
+ } catch (error51) {
49459
+ options.io.stderr.write(
49460
+ `Failed to load the runner service: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
49461
+ `
49462
+ );
49463
+ return 1;
49464
+ }
49465
+ options.io.stdout.write(
49466
+ `${[
49467
+ `Installed runner service ${label}.`,
49468
+ `plist: ${paths.launchAgentPath}`,
49469
+ `state file: ${paths.stateFile}`,
49470
+ `logs: ${paths.logDir}`,
49471
+ `execute: ${options.execute ? "on (claims and runs work orders)" : "off (heartbeat only)"}`,
49472
+ `The runner pairs on first run; its bearer secret is stored locally and never printed.`
49473
+ ].join("\n")}
49474
+ `
49475
+ );
49476
+ return 0;
49477
+ }
49478
+ async function startService(options) {
49479
+ if (options.platform !== "darwin") {
49480
+ return unsupportedPlatform(options.io, options.platform);
49481
+ }
49482
+ const safeName = safeRunnerName(options.runnerName);
49483
+ const paths = runnerServicePaths(options.homeDir, safeName);
49484
+ const label = serviceLabel(safeName);
49485
+ if (!await fileExists(paths.launchAgentPath)) {
49486
+ options.io.stderr.write(
49487
+ `No runner service found for "${safeName}". Run \`${cliBrand.binName} runner install-service --name ${safeName}\` first.
49488
+ `
49489
+ );
49490
+ return 1;
49491
+ }
49492
+ try {
49493
+ await options.exec("launchctl", ["bootstrap", domainTarget(options.uid), paths.launchAgentPath]);
49494
+ options.io.stdout.write(`Started runner service ${label}.
49495
+ `);
49496
+ return 0;
49497
+ } catch (bootstrapError) {
49498
+ try {
49499
+ await options.exec("launchctl", ["kickstart", "-k", serviceTarget(options.uid, label)]);
49500
+ options.io.stdout.write(`Runner service ${label} was already loaded; restarted it.
49501
+ `);
49502
+ return 0;
49503
+ } catch (kickstartError) {
49504
+ options.io.stderr.write(
49505
+ `Failed to start runner service ${label}: ${redactForLog(kickstartError instanceof Error ? kickstartError.message : String(kickstartError))}
49506
+ `
49507
+ );
49508
+ return 1;
49509
+ }
49510
+ }
49511
+ }
49512
+ async function stopService(options) {
49513
+ if (options.platform !== "darwin") {
49514
+ return unsupportedPlatform(options.io, options.platform);
49515
+ }
49516
+ const safeName = safeRunnerName(options.runnerName);
49517
+ const paths = runnerServicePaths(options.homeDir, safeName);
49518
+ const label = serviceLabel(safeName);
49519
+ try {
49520
+ await options.exec("launchctl", ["bootout", domainTarget(options.uid), paths.launchAgentPath]);
49521
+ options.io.stdout.write(`Stopped runner service ${label}.
49522
+ `);
49523
+ } catch {
49524
+ options.io.stdout.write(`Runner service ${label} was not running.
49525
+ `);
49526
+ }
49527
+ return 0;
49528
+ }
49529
+ async function restartService(options) {
49530
+ if (options.platform !== "darwin") {
49531
+ return unsupportedPlatform(options.io, options.platform);
49532
+ }
49533
+ const safeName = safeRunnerName(options.runnerName);
49534
+ const label = serviceLabel(safeName);
49535
+ try {
49536
+ await options.exec("launchctl", ["kickstart", "-k", serviceTarget(options.uid, label)]);
49537
+ options.io.stdout.write(`Restarted runner service ${label}.
49538
+ `);
49539
+ return 0;
49540
+ } catch (error51) {
49541
+ options.io.stderr.write(
49542
+ `Failed to restart runner service ${label}: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}. Is it installed? Run \`${cliBrand.binName} runner install-service --name ${safeName}\`.
49543
+ `
49544
+ );
49545
+ return 1;
49546
+ }
49547
+ }
49548
+ async function statusService(options) {
49549
+ if (options.platform !== "darwin") {
49550
+ return unsupportedPlatform(options.io, options.platform);
49551
+ }
49552
+ const safeName = safeRunnerName(options.runnerName);
49553
+ const paths = runnerServicePaths(options.homeDir, safeName);
49554
+ const label = serviceLabel(safeName);
49555
+ const installed = await fileExists(paths.launchAgentPath);
49556
+ try {
49557
+ const { stdout } = await options.exec("launchctl", ["print", serviceTarget(options.uid, label)]);
49558
+ const state = extractLaunchdState(stdout);
49559
+ options.io.stdout.write(
49560
+ `${[
49561
+ `runner service: ${label}`,
49562
+ `installed: ${installed ? "yes" : "no"}`,
49563
+ `loaded: yes`,
49564
+ `state: ${state ?? "unknown"}`,
49565
+ `plist: ${paths.launchAgentPath}`,
49566
+ `logs: ${paths.logDir}`
49567
+ ].join("\n")}
49568
+ `
49569
+ );
49570
+ return 0;
49571
+ } catch {
49572
+ options.io.stdout.write(
49573
+ `${[
49574
+ `runner service: ${label}`,
49575
+ `installed: ${installed ? "yes" : "no"}`,
49576
+ `loaded: no`,
49577
+ `plist: ${paths.launchAgentPath}`,
49578
+ `logs: ${paths.logDir}`
49579
+ ].join("\n")}
49580
+ `
49581
+ );
49582
+ return 1;
49583
+ }
49584
+ }
49585
+ async function logsService(options) {
49586
+ if (options.platform !== "darwin") {
49587
+ return unsupportedPlatform(options.io, options.platform);
49588
+ }
49589
+ const safeName = safeRunnerName(options.runnerName);
49590
+ const paths = runnerServicePaths(options.homeDir, safeName);
49591
+ const label = serviceLabel(safeName);
49592
+ const stdoutLog = path4.join(paths.logDir, "stdout.log");
49593
+ const stderrLog = path4.join(paths.logDir, "stderr.log");
49594
+ options.io.stdout.write(
49595
+ `${[
49596
+ `runner service logs for ${label}:`,
49597
+ ` stdout: ${stdoutLog}`,
49598
+ ` stderr: ${stderrLog}`,
49599
+ `Follow live with: tail -f ${JSON.stringify(stdoutLog)} ${JSON.stringify(stderrLog)}`
49600
+ ].join("\n")}
49601
+ `
49602
+ );
49603
+ for (const [name, file2] of [["stdout", stdoutLog], ["stderr", stderrLog]]) {
49604
+ const tail = await readTail(file2, 4e3);
49605
+ if (tail !== null && tail.trim().length > 0) {
49606
+ options.io.stdout.write(`
49607
+ ----- ${name} (tail) -----
49608
+ ${redactForLog(tail)}
49609
+ `);
49610
+ }
49611
+ }
49612
+ return 0;
49613
+ }
49614
+ async function uninstallService(options) {
49615
+ if (options.platform !== "darwin") {
49616
+ return unsupportedPlatform(options.io, options.platform);
49617
+ }
49618
+ const safeName = safeRunnerName(options.runnerName);
49619
+ const paths = runnerServicePaths(options.homeDir, safeName);
49620
+ const label = serviceLabel(safeName);
49621
+ await bootoutQuietly(options.exec, options.uid, paths.launchAgentPath);
49622
+ let removed = false;
49623
+ try {
49624
+ await rm3(paths.launchAgentPath, { force: true });
49625
+ removed = true;
49626
+ } catch (error51) {
49627
+ options.io.stderr.write(
49628
+ `Failed to remove ${paths.launchAgentPath}: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
49629
+ `
49630
+ );
49631
+ }
49632
+ options.io.stdout.write(
49633
+ `${[
49634
+ `Uninstalled runner service ${label}.`,
49635
+ removed ? `Removed ${paths.launchAgentPath}.` : `Could not remove ${paths.launchAgentPath}; remove it manually.`,
49636
+ `Left the runner state file at ${paths.stateFile} (it holds the local bearer secret).`,
49637
+ `Delete that file to forget this runner locally, and run \`${cliBrand.binName} runner revoke <runner-id>\` to revoke it in ${cliBrand.name}.`
49638
+ ].join("\n")}
49639
+ `
49640
+ );
49641
+ return removed ? 0 : 1;
49642
+ }
49643
+ function parseServiceArgs(args) {
49644
+ let runnerName;
49645
+ let execute2 = false;
49646
+ for (let index = 0; index < args.length; index += 1) {
49647
+ const token = args[index];
49648
+ if (token === "--name") {
49649
+ const value = args[index + 1];
49650
+ if (value === void 0 || value.startsWith("--")) {
49651
+ throw new Error("--name requires a value.");
49652
+ }
49653
+ runnerName = value;
49654
+ index += 1;
49655
+ } else if (token === "--execute") {
49656
+ execute2 = true;
49657
+ } else {
49658
+ throw new Error(`Unexpected runner service argument: ${token ?? ""}`);
49659
+ }
49660
+ }
49661
+ if (runnerName === void 0 || runnerName.trim().length === 0) {
49662
+ throw new Error("--name <runner-name> is required.");
49663
+ }
49664
+ return { runnerName, execute: execute2 };
49665
+ }
49666
+ function serviceUsage() {
49667
+ const bin = cliBrand.binName;
49668
+ return [
49669
+ `Usage:`,
49670
+ ` ${bin} runner install-service --name <name> [--execute]`,
49671
+ ` ${bin} runner start|stop|restart|status|logs|uninstall --name <name>`
49672
+ ].join("\n");
49673
+ }
49674
+ function safeRunnerName(runnerName) {
49675
+ const safe = runnerName.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
49676
+ return safe.length > 0 ? safe : "runner";
49677
+ }
49678
+ function serviceLabel(safeName) {
49679
+ return `com.${cliBrand.binName}.runner.${safeName}`;
49680
+ }
49681
+ function domainTarget(uid) {
49682
+ return `gui/${uid}`;
49683
+ }
49684
+ function serviceTarget(uid, label) {
49685
+ return `gui/${uid}/${label}`;
49686
+ }
49687
+ async function bootoutQuietly(exec, uid, plistPath) {
49688
+ try {
49689
+ await exec("launchctl", ["bootout", domainTarget(uid), plistPath]);
49690
+ } catch {
49691
+ }
49692
+ }
49693
+ function unsupportedPlatform(io, platform) {
49694
+ io.stderr.write(
49695
+ `Service install is not supported on ${platform} yet. Run \`${cliBrand.binName} runner serve --execute\` under a process manager (for example systemd, pm2, or nssm) to keep a runner online.
49696
+ `
49697
+ );
49698
+ return 1;
49699
+ }
49700
+ function resolveBinPath() {
49701
+ const entry = process.argv[1];
49702
+ if (entry === void 0 || entry.length === 0) {
49703
+ return cliBrand.binName;
49704
+ }
49705
+ try {
49706
+ return realpathSync(entry);
49707
+ } catch {
49708
+ return entry;
49709
+ }
49710
+ }
49711
+ function extractLaunchdState(printOutput) {
49712
+ const match = printOutput.match(/state\s*=\s*(\S+)/);
49713
+ return match ? match[1] ?? null : null;
49714
+ }
49715
+ async function fileExists(file2) {
49716
+ try {
49717
+ await access2(file2);
49718
+ return true;
49719
+ } catch {
49720
+ return false;
49721
+ }
49722
+ }
49723
+ async function readTail(file2, maxBytes) {
49724
+ try {
49725
+ const content = await readFile5(file2, "utf8");
49726
+ return content.length > maxBytes ? content.slice(content.length - maxBytes) : content;
49727
+ } catch {
49728
+ return null;
49729
+ }
49730
+ }
49731
+ function escapePlist(value) {
49732
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
49733
+ }
49734
+ var defaultExec = async (file2, args) => {
49735
+ const result = await execFile4(file2, args);
49736
+ return { stdout: String(result.stdout), stderr: String(result.stderr) };
49737
+ };
49738
+ function assertNever2(value) {
49739
+ throw new Error(`Unhandled runner service verb: ${String(value)}`);
48939
49740
  }
48940
49741
 
48941
49742
  // src/index.ts
@@ -49085,6 +49886,9 @@ async function main(argv = process.argv.slice(2), options = {}) {
49085
49886
  env: process.env
49086
49887
  });
49087
49888
  }
49889
+ if (command === "runner" && isRunnerServiceInvocation(args)) {
49890
+ return runRunnerService({ io, args, appUrl: config2.appUrl });
49891
+ }
49088
49892
  if (!session) {
49089
49893
  io.stderr.write(`Not logged in. Run ${cliBrand.binName} login first.
49090
49894
  `);
@@ -49562,7 +50366,7 @@ function isCliEntrypoint(moduleUrl, argvPath) {
49562
50366
  return false;
49563
50367
  }
49564
50368
  try {
49565
- return realpathSync(fileURLToPath3(moduleUrl)) === realpathSync(argvPath);
50369
+ return realpathSync2(fileURLToPath3(moduleUrl)) === realpathSync2(argvPath);
49566
50370
  } catch {
49567
50371
  return moduleUrl === pathToFileURL(argvPath).href;
49568
50372
  }